diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c76f0d..bb34bfe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,8 +20,6 @@ jobs: ruby: ["3.0"] gemfile: ["gemfiles/rails7.gemfile"] include: - - ruby: "2.6" - gemfile: "gemfiles/rails6.gemfile" - ruby: "3.1" gemfile: "gemfiles/railsmaster.gemfile" - ruby: "2.7" diff --git a/.rbnextrc b/.rbnextrc index d4cb1e1..4bb3ee7 100644 --- a/.rbnextrc +++ b/.rbnextrc @@ -1,5 +1,5 @@ nextify: | ./lib - --min-version=2.6 + --min-version=2.7 --edge --proposed diff --git a/CHANGELOG.md b/CHANGELOG.md index e2b3a70..7eca1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## master +- **Require Ruby 2.7+**. ([@palkan][]) + +- Add system tests to generator. ([@palkan][]) + +- Drop Webpack-related stuff from the generator. ([@palkan][]) + ## 0.1.6 (2023-11-07) - Support preview classes named `_preview.rb`. ([@palkan][]) diff --git a/README.md b/README.md index 854009c..6a9ab7a 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ The command above: - Configure `view_component` paths. - Adds `ApplicationViewComponent` and `ApplicationViewComponentPreview` classes. - Configures testing framework (RSpec or Minitest). -- Adds required JS/CSS configuration. - **Adds a custom generator to create components**. The custom generator would allow you to create all the required component files in a single command: @@ -94,6 +93,8 @@ ActiveSupport.on_load(:view_component) do end ``` +You can still continue using preview clases with the `_preview.rb` suffix, they would work as before. + #### Reducing previews boilerplate In most cases, previews contain only the `default` example and a very simple template (`= render Component.new(**options)`). @@ -182,7 +183,7 @@ If you need more control over your template, you can add a custom `preview.html. ## Organizing assets (JS, CSS) -**NOTE*: This section assumes the usage of Webpack, Vite or other _frontend_ builder (e.g., not Sprockets). +**NOTE**: This section assumes the usage of Vite or Webpack. See [this discussion](https://github.com/palkan/view_component-contrib/discussions/14) for other options. We store JS and CSS files in the same sidecar folder: @@ -203,6 +204,18 @@ import "./index.css" In the root of the `components` folder we have the `index.js` file, which loads all the components: +- With Vite: + +```js +// With Vite +import.meta.glob("./**/index.js").forEach((path) => { + const mod = await import(path); + mod.default(); +}); +``` + +- With Webpack: + ```js // components/index.js const context = require.context(".", true, /index.js$/) @@ -211,12 +224,11 @@ context.keys().forEach(context); ### Using with StimulusJS -You can define Stimulus controllers right in the `index.js` file using the following approach: +You can define Stimulus controllers right in the component folder in the `controller.js` file: ```js -import "./index.css" // We reserve Controller for the export name -import { Controller as BaseController } from "stimulus"; +import { Controller as BaseController } from "@hotwired/stimulus"; export class Controller extends BaseController { connect() { @@ -225,20 +237,60 @@ export class Controller extends BaseController { } ``` -Then, we need to update the `components/index.js` to automatically register controllers: +Then, in your Stimulus entrypoint, you can load and register your component controllers as follows: + +- With Vite: ```js -// We recommend putting Stimulus application instance into its own -// module, so you can use it for non-component controllers +import { Application } from "@hotwired/stimulus"; + +const application = Application.start(); + +// Configure Stimulus development experience +application.debug = false; +window.Stimulus = application; + +// Generic controllers +const genericControllers = import.meta.globEager( + "../controllers/**/*_controller.js" +); + +for (let path in genericControllers) { + let module = genericControllers[path]; + let name = path + .match(/controllers\/(.+)_controller\.js$/)[1] + .replaceAll("/", "-") + .replaceAll("_", "-"); -// init/stimulus.js + application.register(name, module.default); +} + +// Controllers from components +const controllers = import.meta.globEager( + "./../../app/frontend/components/**/controller.js" +); + +for (let path in controllers) { + let module = controllers[path]; + let name = path + .match(/app\/frontend\/components\/(.+)\/controller\.js$/)[1] + .replaceAll("/", "-") + .replaceAll("_", "-"); + application.register(name, module.default); +} + +export default application; +``` + +- With Webpack: + +```js import { Application } from "stimulus"; export const application = Application.start(); -// components/index.js -import { application } from "../init/stimulus"; +// ... other controllers -const context = require.context(".", true, /index.js$/) +const context = require.context("./../../app/frontend/components/", true, /controllers.js$/) context.keys().forEach((path) => { const mod = context(path); @@ -248,9 +300,10 @@ context.keys().forEach((path) => { // Convert path into a controller identifier: // example/index.js -> example // nav/user_info/index.js -> nav--user-info - const identifier = path.replace(/^\.\//, '') - .replace(/\/index\.js$/, '') - .replace(/\//g, '--'); + const identifier = path + .match(/app\/frontend\/components\/(.+)\/controller\.js$/)[1] + .replaceAll("/", "-") + .replaceAll("_", "-"); application.register(identifier, mod.Controller); }); @@ -265,6 +318,8 @@ class ApplicationViewComponent def identifier @identifier ||= self.class.name.sub("::Component", "").underscore.split("/").join("--") end + + alias_method :controller_name, :identifier end ``` @@ -272,7 +327,7 @@ And now in your template: ```erb -
+
``` @@ -495,11 +550,6 @@ And the template looks like this now: You can use the `#wrapped` method on any component inherited from `ApplicationViewComponent` to wrap it automatically: -## ToDo list - -- Better preview tools (w/o JS deps 😉). -- Hotwire-related extensions. - ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). diff --git a/templates/install/class_for.rb b/templates/install/class_for.rb deleted file mode 100644 index ccdb395..0000000 --- a/templates/install/class_for.rb +++ /dev/null @@ -1,3 +0,0 @@ - def class_for(name, from: identifier) - "c-\#{from}-\#{name}" - end \ No newline at end of file diff --git a/templates/install/generator.rb b/templates/install/generator.rb index 0ef7ae6..b745baf 100644 --- a/templates/install/generator.rb +++ b/templates/install/generator.rb @@ -14,6 +14,7 @@ class ViewComponentGenerator < Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) class_option :skip_test, type: :boolean, default: false + class_option :skip_system_test, type: :boolean, default: false class_option :skip_preview, type: :boolean, default: false argument :attributes, type: :array, default: [], banner: "attribute" @@ -32,6 +33,12 @@ def create_test_file template "component_#{TEST_SUFFIX}.rb", File.join("#{TEST_ROOT_PATH}", class_path, "\#{file_name}_#{TEST_SUFFIX}.rb") end + def create_system_test_file + return if options[:skip_system_test] + + template "component_system_#{TEST_SUFFIX}.rb", File.join("#{TEST_SYSTEM_ROOT_PATH}", class_path, "\#{file_name}_#{TEST_SUFFIX}.rb") + end + def create_preview_file return if options[:skip_preview] @@ -50,31 +57,6 @@ def preview_parent_class end CODE - if USE_WEBPACK - inject_into_file "lib/generators/view_component/view_component_generator.rb", after: "class_option :skip_preview, type: :boolean, default: false\n" do - <<-CODE - class_option :skip_js, type: :boolean, default: false - class_option :skip_css, type: :boolean, default: false - CODE - end - - inject_into_file "lib/generators/view_component/view_component_generator.rb", before: "\n private" do - <<-CODE - def create_css_file - return if options[:skip_css] || options[:skip_js] - - template "index.css", File.join("#{ROOT_PATH}", class_path, file_name, "index.css") - end - - def create_js_file - return if options[:skip_js] - - template "index.js", File.join("#{ROOT_PATH}", class_path, file_name, "index.js") - end - CODE - end - end - if USE_DRY inject_into_file "lib/generators/view_component/view_component_generator.rb", before: "\nend" do <<-CODE @@ -167,43 +149,6 @@ def default end CODE - if USE_WEBPACK - if USE_STIMULUS - file "lib/generators/view_component/templates/index.js.tt", - <<-CODE -import "./index.css" - -// Add a Stimulus controller for this component. -// It will automatically registered and its name will be available -// via #component_name in the component class. -// -// import { Controller as BaseController } from "stimulus"; -// -// export class Controller extends BaseController { -// connect() { -// } -// -// disconnect() { -// } -// } - CODE - else - file "lib/generators/view_component/templates/index.js.tt", <<~CODE - import "./index.css" - - CODE - end - - if USE_POSTCSS_MODULES - file "lib/generators/view_component/templates/index.css.tt", <<~CODE - /* Use component-local class names and add them to HTML via #class_for(name) helper */ - - CODE - else - file "lib/generators/view_component/templates/index.css.tt", "" - end - end - if USE_RSPEC file "lib/generators/view_component/templates/component_spec.rb.tt", <<~CODE # frozen_string_literal: true @@ -223,15 +168,29 @@ def default end end CODE + + file "lib/generators/view_component/templates/component_system_spec.rb.tt", <<~CODE + # frozen_string_literal: true + + require "rails_helper" + + describe "<%%= file_name %> component" do + it "default preview" do + visit("/rails/view_components<%%= File.join(class_path, file_name) %>/default") + + # is_expected.to have_text "Hello!" + # click_on "Click me" + # is_expected.to have_text "Good-bye!" + end + end + CODE else file "lib/generators/view_component/templates/component_test.rb.tt", <<~CODE # frozen_string_literal: true require "test_helper" - class <%%= class_name %>::ComponentTest < ActiveSupport::TestCase - include ViewComponent::TestHelpers - + class <%%= class_name %>::ComponentTest < ViewComponent::TestCase def test_renders component = build_component @@ -245,6 +204,22 @@ def test_renders def build_component(**options) <%%= class_name %>::Component.new(**options) end + end + CODE + + file "lib/generators/view_component/templates/component_system_test.rb.tt", <<~CODE + # frozen_string_literal: true + + require "application_system_test_case" + + class <%%= class_name %>::ComponentSystemTest < ApplicationSystemTestCase + def test_default_preview + visit("/rails/view_components<%%= File.join(class_path, file_name) %>/default") + + # assert_text "Hello!" + # click_on("Click me!") + # assert_text "Good-bye!" + end end CODE end @@ -263,18 +238,10 @@ def build_component(**options) Component: #{ROOT_PATH}/profile/component.rb Template: #{ROOT_PATH}/profile/component.html#{TEMPLATE_EXT} Test: #{TEST_ROOT_PATH}/profile_component_#{TEST_SUFFIX}.rb + System Test: #{TEST_SYSTEM_ROOT_PATH}/profile_component_#{TEST_SUFFIX}.rb Preview: #{ROOT_PATH}/profile/component_preview.rb CODE - if USE_WEBPACK - inject_into_file "lib/generators/view_component/USAGE" do - <<-CODE - JS: #{ROOT_PATH}/profile/component.js - CSS: #{ROOT_PATH}/profile/component.css - CODE - end - end - # Check if autoload_lib is configured if File.file?("config/application.rb") && File.read("config/application.rb").include?("config.autoload_lib") say_status :info, "⚠️ Make sure you configured autoload_lib to ignore the lib/generators folder" diff --git a/templates/install/index.js b/templates/install/index.js deleted file mode 100644 index 9519e78..0000000 --- a/templates/install/index.js +++ /dev/null @@ -1,2 +0,0 @@ -const context = require.context(".", true, /index.js$/) -context.keys().forEach(context); \ No newline at end of file diff --git a/templates/install/index.stimulus.js b/templates/install/index.stimulus.js deleted file mode 100644 index a66245b..0000000 --- a/templates/install/index.stimulus.js +++ /dev/null @@ -1,20 +0,0 @@ -// IMPORTANT: Update this import to reflect the location of your Stimulus application -// See https://github.com/palkan/view_component-contrib#using-with-stimulusjs -import { application } from "../init/stimulus"; - -const context = require.context(".", true, /index.js$/) -context.keys().forEach((path) => { - const mod = context(path); - - // Check whether a module has the Controller export defined - if (!mod.Controller) return; - - // Convert path into a controller identifier: - // example/index.js -> example - // nav/user_info/index.js -> nav--user-info - const identifier = path.replace(/^\\.\\//, '') - .replace(/\\/index\\.js$/, '') - .replace(/\\//g, '--'); - - application.register(identifier, mod.Controller); -}); \ No newline at end of file diff --git a/templates/install/postcss-modules.js b/templates/install/postcss-modules.js deleted file mode 100644 index 25c811a..0000000 --- a/templates/install/postcss-modules.js +++ /dev/null @@ -1,13 +0,0 @@ - generateScopedName: (name, filename, _css) => { - const matches = filename.match(/#{ROOT_PATH.gsub('/', '\/')}\\/?(.*)\\/index.css$/); - // Do not transform CSS files from outside of the components folder - if (!matches) return name; - - // identifier here is the same identifier we used for Stimulus controller (see above) - const identifier = matches[1].replace(/\\//g, "--"); - - // We also add the `c-` prefix to all components classes - return `c-${identifier}-${name}`; - }, - // Do not generate *.css.json files (we don't use them) - getJSON: () => {} \ No newline at end of file diff --git a/templates/install/template.rb b/templates/install/template.rb index 48b3cb3..f04af14 100644 --- a/templates/install/template.rb +++ b/templates/install/template.rb @@ -27,6 +27,7 @@ USE_RSPEC = File.directory?("spec") TEST_ROOT_PATH = USE_RSPEC ? File.join("spec", ROOT_PATH.sub("app/", "")) : File.join("test", ROOT_PATH.sub("app/", "")) +TEST_SYSTEM_ROOT_PATH = USE_RSPEC ? File.join("spec", "system", ROOT_PATH.sub("app/", "")) : File.join("test", "system", ROOT_PATH.sub("app/", "")) USE_DRY = yes? "Would you like to use dry-initializer in your component classes? (y/n)" @@ -63,67 +64,18 @@ say_status :info, "✅ RSpec configured" -USE_WEBPACK = File.directory?("config/webpack") || File.file?("webpack.config.js") +USE_STIMULUS = yes? "Do you use Stimulus? (y/n)" -if USE_WEBPACK - USE_STIMULUS = yes? "Do you use StimulusJS? (y/n)" - - if USE_STIMULUS - file "#{ROOT_PATH}/index.js", - <%= code("./index.stimulus.js") %> - - inject_into_file "#{ROOT_PATH}/application_view_component.rb", before: "\nend" do - <%= code("./identifier.rb") %> - end - else - file "#{ROOT_PATH}/index.js", - <%= code("./index.js") %> - end - - say_status :info, "✅ Added index.js to load components JS/CSS" - say "⚠️ Don't forget to import component JS/CSS (#{ROOT_PATH}/index.js) from your application.js entrypoint" - - say "⚠️ Don't forget to add #{ROOT_PATH} to `additional_paths` in your `webpacker.yml` (unless your `source_path` already includes it)" - - USE_POSTCSS_MODULES = yes? "Would you like to use postcss-modules to isolate component styles? (y/n)" - - if USE_POSTCSS_MODULES - run "yarn add postcss-modules" - - if File.read("postcss.config.js").match(/plugins:\s*\[/) - inject_into_file "postcss.config.js", after: "plugins: [" do - <<-CODE - - require('postcss-modules')({ - <%= include("./postcss-modules.js") %> - }), - CODE - end - else - inject_into_file "postcss.config.js", after: "plugins: {" do - <<-CODE - - 'postcss-modules': { - <%= include("./postcss-modules.js") %> - }, - CODE - end - end - - if !USE_STIMULUS - inject_into_file "#{ROOT_PATH}/application_view_component.rb", before: "\nend" do - <%= code("./identifier.rb") %> - end - end +if USE_STIMULUS + say "⚠️ See the discussion on how to configure your JS bundler to auto-load controllers: https://github.com/palkan/view_component-contrib/discussions/14" +end - inject_into_file "#{ROOT_PATH}/application_view_component.rb", before: "\nend" do - <%= code("./class_for.rb") %> - end +USE_TAILWIND = yes? "Do you use TailwindCSS? (y/n)" - say_status :info, "✅ postcss-modules configured" - end +if USE_TAILWIND + # TODO: Use styled variants else - say "⚠️ See the discussion on how to configure non-Wepback JS/CSS installations: https://github.com/palkan/view_component-contrib/discussions/14" + say "⚠️ Check out PostCSS modules to keep your CSS isolated and closer to your components: https://github.com/palkan/view_component-contrib#isolating-css-with-postcss-modules end <%= include("./generator.rb") %> diff --git a/view_component-contrib.gemspec b/view_component-contrib.gemspec index 026f1a5..cccd93c 100644 --- a/view_component-contrib.gemspec +++ b/view_component-contrib.gemspec @@ -23,16 +23,16 @@ Gem::Specification.new do |s| s.files = Dir.glob("app/**/*") + Dir.glob("lib/**/*") + %w[README.md LICENSE.txt CHANGELOG.md] s.require_paths = ["lib"] - s.required_ruby_version = ">= 2.6" + s.required_ruby_version = ">= 2.7" s.add_dependency "view_component" # When gem is installed from source, we add `ruby-next` as a dependency # to auto-transpile source files during the first load if ENV["RELEASING_GEM"].nil? && File.directory?(File.join(__dir__, ".git")) - s.add_runtime_dependency "ruby-next", ">= 0.12.0" + s.add_runtime_dependency "ruby-next", ">= 0.15.0" else - s.add_dependency "ruby-next-core", ">= 0.12.0" + s.add_dependency "ruby-next-core", ">= 0.15.0" end s.add_development_dependency "bundler", ">= 1.15"