rspack_rails 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3eb3563c7877029531bd93b2ed9036ce92b8a89c4008600ea251dfad3b641ff
4
- data.tar.gz: 5d210e018044764c5e2f3f94cd06e3cb296d16e1da97d010b9e18907c1416644
3
+ metadata.gz: d38b564f0260e86ab95775b11d6435853d520922028c59c3acce6c7ad1af639b
4
+ data.tar.gz: 062aff2e71411f542808ec77ed5054935040ec6018a1a3839ba374b2cfd10dc2
5
5
  SHA512:
6
- metadata.gz: 5dc66b20aadec8d3695d98448c80eb39e31a0d2c55d693002c6d318ac25c95a96ff81b8ac4829d0e60a6e09af7959ceca287f1f5c440a09af333069b6ffa0458
7
- data.tar.gz: fc965e7fd0ddd80f60d120b77cf135ce9e4da264b87e66e298767c5e14e367cf9bbfda64130abc6d03d2f0368528196826e5425f8dcae8ddbe36714e9c534da8
6
+ metadata.gz: 0d1ab868d62a2ce92bb0cf1f330f4113e311658fea8e0174f50ef73c001fa7f67ccb580094bdb7f805606026536b7a3580ece740dd4f88d3b946baa32e84883a
7
+ data.tar.gz: c919c924784fb9885e4b8c9b8ae5db4255475adb9fd915a71e1a7a882a20bd02d6cf6c0eb2120928d7c89a0b214676ec3ddf92b1e785f48a89059e5e9ed02bd5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-11-13
4
+
5
+ - Multiple entry points: every `app/javascript/entrypoints/*.{js,ts}` becomes a bundle named after the file; `application.ts` works as the main entry
6
+ - `rspack:doctor` lists entry points and reports any that are not built
7
+ - Fix: the dev server no longer writes `*.hot-update.js` files into `app/assets/builds`
8
+ - Existing apps: re-run `bin/rails rspack:install` and accept the new `rspack.config.mjs`
9
+
10
+ ## [0.2.0] - 2026-11-13
11
+
12
+ - CSS imported from JavaScript is extracted to `app/assets/builds/<entry>.bundle.css` and hot-reloaded under `rspack serve`; `*.module.css` are CSS modules
13
+ - **Changed:** `rspack_stylesheet_tag "application"` now links `application.bundle.css` (was `application.css`)
14
+ - TypeScript: `.ts` files and `*_controller.ts` Stimulus controllers compile with the built-in SWC loader
15
+ - `rspack:install:typescript` adds `tsconfig.json`, `typescript` and a `typecheck` script
16
+ - Dev server writes bundles to disk so `app/assets/builds` stays current
17
+ - `rspack:doctor` warns when `application.bundle.css` is built but not linked
18
+ - Existing apps: re-run `bin/rails rspack:install` (and `rspack:install:stimulus`) and accept the new `rspack.config.mjs` / `autoload.js`
19
+
3
20
  ## [0.1.0] - 2026-11-13
4
21
 
5
22
  - `rspack:doctor`: checks Node.js against rspack's `engines`, packages, config, entry, build output, layout, Procfile.dev, Stimulus and the dev server
data/README.md CHANGED
@@ -57,17 +57,61 @@ end
57
57
  bin/rails rspack:install:stimulus
58
58
  ```
59
59
 
60
- This installs `@hotwired/stimulus` and adds `app/javascript/controllers/autoload.js`, imported from `application.js`. Every `*_controller.js` under `app/javascript/controllers` is registered automatically, with the usual naming (`admin/user_card_controller.js` becomes `admin--user-card`). There is no manifest to keep up to date.
60
+ This installs `@hotwired/stimulus` and adds `app/javascript/controllers/autoload.js`, imported from `application.js`. Every `*_controller.js` (or `.ts`) under `app/javascript/controllers` is registered automatically, with the usual naming (`admin/user_card_controller.js` becomes `admin--user-card`). There is no manifest to keep up to date.
61
61
 
62
62
  Under `rspack serve`, editing a controller or adding a new one hot-swaps it in the open page without a reload, so form input and other page state survive.
63
63
 
64
64
  If `application.js` imported `./controllers` (the stimulus-rails index), the installer switches it to `./controllers/autoload`. stimulus-rails may still rewrite `controllers/index.js` when you run `bin/rails generate stimulus`, but nothing imports that file any more.
65
65
 
66
+ ### Multiple entry points
67
+
68
+ Besides `app/javascript/application.js` (or `.ts`), every file in `app/javascript/entrypoints/` becomes its own bundle, named after the file:
69
+
70
+ ```
71
+ app/javascript/application.js -> application.js
72
+ app/javascript/entrypoints/admin.ts -> admin.js
73
+ app/javascript/entrypoints/checkout.js -> checkout.js (+ checkout.bundle.css if it imports CSS)
74
+ ```
75
+
76
+ ```erb
77
+ <%# app/views/layouts/admin.html.erb %>
78
+ <%= rspack_javascript_tag "admin" %>
79
+ ```
80
+
81
+ Each bundle is self-contained, so load one entry per page (for example one per layout). Restart the dev server after adding a new entry point.
82
+
83
+ ### CSS
84
+
85
+ Import CSS from your JavaScript and rspack extracts it to `app/assets/builds/<entry>.bundle.css`:
86
+
87
+ ```js
88
+ // app/javascript/application.js
89
+ import "./styles/application.css"
90
+ ```
91
+
92
+ ```erb
93
+ <%= rspack_stylesheet_tag "application" %>
94
+ ```
95
+
96
+ `*.module.css` files are compiled as CSS modules. Under `rspack serve`, CSS changes are applied without a page reload.
97
+
98
+ The `.bundle` suffix keeps the output from clashing with `app/assets/stylesheets/application.css`. Rails 8's default `stylesheet_link_tag :app` links every CSS file under `app/assets`, so it also links the bundle. That is harmless: the dev server keeps `app/assets/builds` up to date and its own link comes later in the cascade. A rule you delete keeps applying until the next reload.
99
+
100
+ ### TypeScript
101
+
102
+ `.ts` files compile out of the box with Rspack's built-in SWC loader, including Stimulus controllers (`*_controller.ts`). SWC strips types without checking them. For type checking:
103
+
104
+ ```bash
105
+ bin/rails rspack:install:typescript # tsconfig.json, typescript, "typecheck" script
106
+ npm run typecheck
107
+ ```
108
+
66
109
  ### Tasks
67
110
 
68
111
  | Task | What it does |
69
112
  |---|---|
70
113
  | `bin/rails rspack:install:stimulus` | Sets up Stimulus with controller autoloading |
114
+ | `bin/rails rspack:install:typescript` | Adds tsconfig.json, typescript and a `typecheck` script |
71
115
  | `bin/rails rspack:doctor` | Checks Node.js, packages, config, layout, Procfile and dev server |
72
116
  | `bin/rails rspack:build` | Installs JS dependencies, then runs `build` |
73
117
  | `bin/rails rspack:clobber` | Removes rspack output from `app/assets/builds` |
@@ -97,6 +141,8 @@ The required Node.js version comes from the `engines` field of the installed `@r
97
141
  ```bash
98
142
  bin/setup
99
143
  bundle exec rake # specs + rubocop
144
+ bin/integration # new Rails app + every installer, build, doctor and precompile
145
+ bin/integration "~> 7.2.0" # against a specific Rails version
100
146
  ```
101
147
 
102
148
  ## License
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { defineConfig } from "@rspack/cli";
@@ -12,24 +13,66 @@ const isProduction =
12
13
  const host = process.env.RSPACK_DEV_SERVER_HOST || "localhost";
13
14
  const port = Number(process.env.RSPACK_DEV_SERVER_PORT || 3036);
14
15
 
16
+ // Entries: app/javascript/application.{js,ts} plus every file in
17
+ // app/javascript/entrypoints/, named after the file (admin.js -> "admin").
18
+ // Restart the dev server after adding an entrypoint.
19
+ const entry = {};
20
+ for (const ext of ["js", "ts"]) {
21
+ const file = `./app/javascript/application.${ext}`;
22
+ if (!entry.application && fs.existsSync(path.resolve(root, file))) entry.application = file;
23
+ }
24
+ const entrypointsDir = path.resolve(root, "app/javascript/entrypoints");
25
+ if (fs.existsSync(entrypointsDir)) {
26
+ for (const file of fs.readdirSync(entrypointsDir).sort()) {
27
+ const match = file.match(/^(.+)\.(js|ts)$/);
28
+ if (match) entry[match[1]] = `./app/javascript/entrypoints/${file}`;
29
+ }
30
+ }
31
+
15
32
  export default defineConfig({
16
33
  mode: isProduction ? "production" : "development",
17
34
  devtool: "source-map",
18
- entry: {
19
- application: "./app/javascript/application.js",
20
- },
35
+ entry,
21
36
  output: {
22
37
  path: path.resolve(root, "app/assets/builds"),
23
38
  filename: "[name].js",
24
39
  // ".digested" tells Propshaft the file is already fingerprinted.
25
40
  chunkFilename: "[name]-[contenthash].digested.js",
41
+ // CSS imported from JavaScript. ".bundle" avoids clashing with
42
+ // app/assets/stylesheets/application.css in Propshaft.
43
+ cssFilename: "[name].bundle.css",
44
+ cssChunkFilename: "[name]-[contenthash].digested.css",
26
45
  // Under `rspack serve`, chunks and hot updates load from the dev server.
27
46
  publicPath: isDevServer ? `http://${host}:${port}/` : "/assets/",
28
47
  },
48
+ resolve: {
49
+ extensions: ["...", ".ts"],
50
+ },
51
+ module: {
52
+ rules: [
53
+ {
54
+ // Transpile TypeScript with Rspack's built-in SWC (no type checking).
55
+ test: /\.ts$/,
56
+ exclude: /node_modules/,
57
+ loader: "builtin:swc-loader",
58
+ options: { jsc: { parser: { syntax: "typescript" } } },
59
+ type: "javascript/auto",
60
+ },
61
+ {
62
+ // Native CSS; *.module.css files become CSS modules.
63
+ test: /\.css$/,
64
+ type: "css/auto",
65
+ },
66
+ ],
67
+ },
29
68
  devServer: {
30
69
  host,
31
70
  port,
32
71
  hot: true,
72
+ // Keep app/assets/builds current while the dev server runs, so Propshaft
73
+ // (and `stylesheet_link_tag :app`) never serve a stale bundle. Hot update
74
+ // files stay in memory so they never reach public/assets.
75
+ devMiddleware: { writeToDisk: (file) => !/\.hot-update\./.test(file) },
33
76
  allowedHosts: "all",
34
77
  headers: { "Access-Control-Allow-Origin": "*" },
35
78
  client: {
@@ -1,16 +1,16 @@
1
- // Registers every app/javascript/controllers/**/*_controller.js with Stimulus.
1
+ // Registers every app/javascript/controllers/**/*_controller.{js,ts} with Stimulus.
2
2
  // Managed by rspack_rails: new controllers need no manual import, and edited
3
3
  // controllers are hot-swapped under `rspack serve` without a page reload.
4
4
  import { application } from "./application"
5
5
 
6
6
  const controllers = () =>
7
- import.meta.webpackContext("./", { recursive: true, regExp: /_controller\.js$/ })
7
+ import.meta.webpackContext("./", { recursive: true, regExp: /_controller\.[jt]s$/ })
8
8
 
9
9
  // "./users/profile_controller.js" -> "users--profile"
10
10
  const identifierFor = (key) =>
11
11
  key
12
12
  .replace(/^\.\//, "")
13
- .replace(/_controller\.js$/, "")
13
+ .replace(/_controller\.[jt]s$/, "")
14
14
  .replace(/\//g, "--")
15
15
  .replace(/_/g, "-")
16
16
 
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "types": ["@rspack/core/module"],
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "isolatedModules": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["app/javascript/**/*.ts"]
14
+ }
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rails application template run by `bin/rails rspack:install:typescript`.
4
+ require "json"
5
+
6
+ pm = RspackRails::PackageManager.detect(Rails.root)
7
+
8
+ say "Adding tsconfig.json"
9
+ copy_file "#{__dir__}/typescript/tsconfig.json", "tsconfig.json"
10
+
11
+ say "Adding typecheck script"
12
+ package = JSON.parse(File.read("package.json"))
13
+ (package["scripts"] ||= {})["typecheck"] = "tsc --noEmit"
14
+ File.write("package.json", "#{JSON.pretty_generate(package)}\n")
15
+
16
+ say "Installing typescript"
17
+ run pm.add_dev_command("typescript")
18
+
19
+ say "TypeScript ready. Rspack compiles .ts files; run `#{pm.run_command("typecheck")}` to type-check.", :green
@@ -4,11 +4,13 @@ require "json"
4
4
  require "open3"
5
5
  require "pathname"
6
6
  require_relative "node_version"
7
+ require_relative "entrypoints"
7
8
 
8
9
  module RspackRails
9
10
  # Inspects a Rails app's rspack setup and reports what is missing.
10
11
  class Doctor
11
12
  Check = Struct.new(:status, :name, :detail, :hint)
13
+ LAYOUT = "app/views/layouts/application.html.erb"
12
14
  SYMBOLS = { ok: "✓", warn: "!", error: "✗" }.freeze
13
15
 
14
16
  def initialize(root, config: RspackRails.config, dev_server: RspackRails.dev_server, node_version: nil)
@@ -21,7 +23,7 @@ module RspackRails
21
23
  def checks
22
24
  @checks ||= [
23
25
  node_check, package_check(:cli), package_check(:core), package_check(:"dev-server"),
24
- config_check, scripts_check, entry_check, build_check, layout_check, procfile_check,
26
+ config_check, scripts_check, entry_check, build_check, layout_check, stylesheet_check, procfile_check,
25
27
  stimulus_check, dev_server_check
26
28
  ].compact
27
29
  end
@@ -90,22 +92,22 @@ module RspackRails
90
92
  end
91
93
 
92
94
  def entry_check
93
- return Check.new(:ok, "Entry point", "app/javascript/application.js") if exist?("app/javascript/application.js")
95
+ return Check.new(:ok, "Entry points", entrypoints.keys.join(", ")) if entrypoints.any?
94
96
 
95
- Check.new(:error, "Entry point", "app/javascript/application.js is missing",
96
- "Create it, or point `entry` in rspack.config.mjs at your entry file")
97
+ Check.new(:error, "Entry points", "none found",
98
+ "Create app/javascript/application.js or a file in app/javascript/entrypoints/")
97
99
  end
98
100
 
99
101
  def build_check
100
- built = @root.join("app/assets/builds/application.js")
101
- return Check.new(:ok, "Build output", "app/assets/builds/application.js (#{built.size} bytes)") if built.exist?
102
+ missing = entrypoints.keys.reject { |name| exist?("app/assets/builds/#{name}.js") }
103
+ return Check.new(:ok, "Build output", "app/assets/builds has #{entrypoints.keys.join(", ")}") if missing.empty?
102
104
 
103
- Check.new(:warn, "Build output", "app/assets/builds/application.js not built yet",
105
+ Check.new(:warn, "Build output", "not built yet: #{missing.join(", ")}",
104
106
  "Run `bin/rails rspack:build` (only needed without the dev server)")
105
107
  end
106
108
 
107
109
  def layout_check
108
- layout = read("app/views/layouts/application.html.erb")
110
+ layout = read(LAYOUT)
109
111
  return Check.new(:warn, "Layout", "app/views/layouts/application.html.erb not found") unless layout
110
112
 
111
113
  if layout.include?("javascript_importmap_tags")
@@ -118,6 +120,16 @@ module RspackRails
118
120
  %(Add <%= rspack_javascript_tag "application" %> to get the dev server and HMR))
119
121
  end
120
122
 
123
+ def stylesheet_check
124
+ return unless exist?("app/assets/builds/application.bundle.css")
125
+
126
+ linked = read(LAYOUT)&.include?("rspack_stylesheet_tag")
127
+ return Check.new(:ok, "Stylesheet", "uses rspack_stylesheet_tag") if linked
128
+
129
+ Check.new(:warn, "Stylesheet", "application.bundle.css is built but not linked",
130
+ %(Add <%= rspack_stylesheet_tag "application" %> to your layout))
131
+ end
132
+
121
133
  def procfile_check
122
134
  procfile = read("Procfile.dev")
123
135
  if procfile&.match?(/^js: .*\bdev$/)
@@ -175,6 +187,10 @@ module RspackRails
175
187
  nil
176
188
  end
177
189
 
190
+ def entrypoints
191
+ @entrypoints ||= Entrypoints.discover(@root)
192
+ end
193
+
178
194
  def package_manager
179
195
  @package_manager ||= PackageManager.detect(@root)
180
196
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module RspackRails
6
+ # Mirrors the entry discovery in the generated rspack.config.mjs:
7
+ # app/javascript/application.{js,ts} plus every
8
+ # app/javascript/entrypoints/*.{js,ts}, named after the file.
9
+ module Entrypoints
10
+ EXTENSIONS = %w[js ts].freeze
11
+
12
+ module_function
13
+
14
+ # => { "application" => "app/javascript/application.js", "admin" => "app/javascript/entrypoints/admin.js" }
15
+ def discover(root)
16
+ root = Pathname(root)
17
+ entries = {}
18
+ main = EXTENSIONS.map { |ext| "app/javascript/application.#{ext}" }.find { |path| root.join(path).file? }
19
+ entries["application"] = main if main
20
+
21
+ Dir.glob("app/javascript/entrypoints/*.{#{EXTENSIONS.join(",")}}", base: root).sort.each do |path|
22
+ entries[File.basename(path, ".*")] = path
23
+ end
24
+ entries
25
+ end
26
+ end
27
+ end
@@ -12,11 +12,16 @@ module RspackRails
12
12
  "\n")
13
13
  end
14
14
 
15
+ # Links the CSS that rspack extracts from an entry's imports
16
+ # (app/assets/builds/<entry>.bundle.css).
15
17
  def rspack_stylesheet_tag(*entries, **options)
16
- return stylesheet_link_tag(*entries, **options) unless RspackRails.dev_server.running?
18
+ unless RspackRails.dev_server.running?
19
+ return stylesheet_link_tag(*entries.map { |entry| "#{entry}.bundle" }, **options)
20
+ end
17
21
 
18
- safe_join(entries.map { |entry| stylesheet_link_tag(RspackRails.dev_server.url("#{entry}.css"), **options) },
19
- "\n")
22
+ safe_join(entries.map do |entry|
23
+ stylesheet_link_tag(RspackRails.dev_server.url("#{entry}.bundle.css"), **options)
24
+ end, "\n")
20
25
  end
21
26
  end
22
27
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RspackRails
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/rspack_rails.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "rspack_rails/version"
4
4
  require_relative "rspack_rails/package_manager"
5
5
  require_relative "rspack_rails/configuration"
6
6
  require_relative "rspack_rails/dev_server"
7
+ require_relative "rspack_rails/entrypoints"
7
8
 
8
9
  # Rspack integration for Rails: rake tasks, installer and view helpers.
9
10
  module RspackRails
@@ -15,6 +15,13 @@ namespace :rspack do
15
15
  system(RbConfig.ruby, "./bin/rails", "app:template", "LOCATION=#{template}") ||
16
16
  raise(RspackRails::Error, "rspack:install:stimulus failed")
17
17
  end
18
+
19
+ desc "Set up TypeScript type checking (tsconfig.json, typescript, typecheck script)"
20
+ task :typescript do
21
+ template = File.expand_path("../install/typescript.rb", __dir__)
22
+ system(RbConfig.ruby, "./bin/rails", "app:template", "LOCATION=#{template}") ||
23
+ raise(RspackRails::Error, "rspack:install:typescript failed")
24
+ end
18
25
  end
19
26
 
20
27
  desc "Check the rspack setup of this app"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspack_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tungnt
@@ -51,10 +51,13 @@ files:
51
51
  - lib/install/stimulus/application.js
52
52
  - lib/install/stimulus/autoload.js
53
53
  - lib/install/stimulus/hello_controller.js
54
+ - lib/install/typescript.rb
55
+ - lib/install/typescript/tsconfig.json
54
56
  - lib/rspack_rails.rb
55
57
  - lib/rspack_rails/configuration.rb
56
58
  - lib/rspack_rails/dev_server.rb
57
59
  - lib/rspack_rails/doctor.rb
60
+ - lib/rspack_rails/entrypoints.rb
58
61
  - lib/rspack_rails/helper.rb
59
62
  - lib/rspack_rails/node_version.rb
60
63
  - lib/rspack_rails/package_manager.rb