shakapacker 8.4.0 → 9.0.0.beta.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/STATUS.md +1 -0
  3. data/.github/workflows/dummy.yml +1 -1
  4. data/.github/workflows/generator.yml +4 -14
  5. data/.github/workflows/node.yml +1 -1
  6. data/CHANGELOG.md +4 -2
  7. data/Gemfile.lock +3 -3
  8. data/README.md +2 -2
  9. data/docs/css-modules-export-mode.md +288 -0
  10. data/docs/peer-dependencies.md +40 -0
  11. data/docs/rspack.md +190 -0
  12. data/docs/troubleshooting.md +5 -0
  13. data/lib/install/bin/shakapacker +14 -2
  14. data/lib/install/bin/shakapacker-rspack +13 -0
  15. data/lib/install/config/rspack/rspack.config.js +6 -0
  16. data/lib/install/config/shakapacker.yml +3 -0
  17. data/lib/install/package.json +30 -0
  18. data/lib/install/template.rb +12 -2
  19. data/lib/shakapacker/configuration.rb +12 -0
  20. data/lib/shakapacker/dev_server_runner.rb +17 -7
  21. data/lib/shakapacker/manifest.rb +3 -2
  22. data/lib/shakapacker/rspack_runner.rb +57 -0
  23. data/lib/shakapacker/runner.rb +48 -2
  24. data/lib/shakapacker/version.rb +1 -1
  25. data/package/environments/base.js +10 -65
  26. data/package/environments/development.js +18 -3
  27. data/package/environments/production.js +24 -51
  28. data/package/environments/test.js +15 -1
  29. data/package/index.d.ts +14 -0
  30. data/package/index.js +4 -2
  31. data/package/optimization/rspack.js +25 -0
  32. data/package/optimization/webpack.js +49 -0
  33. data/package/plugins/rspack.js +104 -0
  34. data/package/plugins/webpack.js +62 -0
  35. data/package/rules/css.js +1 -1
  36. data/package/rules/file.js +11 -5
  37. data/package/rules/less.js +1 -1
  38. data/package/rules/raw.js +11 -1
  39. data/package/rules/rspack.js +96 -0
  40. data/package/rules/sass.js +6 -2
  41. data/package/rules/stylus.js +1 -1
  42. data/package/utils/getStyleRule.js +16 -3
  43. data/package/utils/requireOrError.js +15 -0
  44. data/package.json +19 -31
  45. data/test/package/environments/base.test.js +1 -1
  46. data/test/package/rules/{index.test.js → webpack.test.js} +1 -1
  47. data/yarn.lock +2136 -726
  48. metadata +21 -11
  49. /data/package/rules/{index.js → webpack.js} +0 -0
@@ -0,0 +1,49 @@
1
+ const { requireOrError } = require("../utils/requireOrError")
2
+
3
+ const TerserPlugin = requireOrError("terser-webpack-plugin")
4
+ const { moduleExists } = require("../utils/helpers")
5
+
6
+ const tryCssMinimizer = () => {
7
+ if (
8
+ moduleExists("css-loader") &&
9
+ moduleExists("css-minimizer-webpack-plugin")
10
+ ) {
11
+ const CssMinimizerPlugin = requireOrError("css-minimizer-webpack-plugin")
12
+ return new CssMinimizerPlugin()
13
+ }
14
+
15
+ return null
16
+ }
17
+
18
+ const getOptimization = () => {
19
+ return {
20
+ minimizer: [
21
+ tryCssMinimizer(),
22
+ new TerserPlugin({
23
+ parallel: Number.parseInt(process.env.SHAKAPACKER_PARALLEL, 10) || true,
24
+ terserOptions: {
25
+ parse: {
26
+ // Let terser parse ecma 8 code but always output
27
+ // ES5 compliant code for older browsers
28
+ ecma: 8
29
+ },
30
+ compress: {
31
+ ecma: 5,
32
+ warnings: false,
33
+ comparisons: false
34
+ },
35
+ mangle: { safari10: true },
36
+ output: {
37
+ ecma: 5,
38
+ comments: false,
39
+ ascii_only: true
40
+ }
41
+ }
42
+ })
43
+ ].filter(Boolean)
44
+ }
45
+ }
46
+
47
+ module.exports = {
48
+ getOptimization
49
+ }
@@ -0,0 +1,104 @@
1
+ const { existsSync, readFileSync } = require("fs")
2
+ const { requireOrError } = require("../utils/requireOrError")
3
+
4
+ const { RspackManifestPlugin } = requireOrError("rspack-manifest-plugin")
5
+ const rspack = requireOrError("@rspack/core")
6
+ const config = require("../config")
7
+ const { isProduction } = require("../env")
8
+ const { moduleExists } = require("../utils/helpers")
9
+
10
+ const getPlugins = () => {
11
+ const plugins = [
12
+ new rspack.EnvironmentPlugin(process.env),
13
+ new RspackManifestPlugin({
14
+ fileName: config.manifestPath.split("/").pop(), // Get just the filename
15
+ publicPath: config.publicPathWithoutCDN,
16
+ writeToFileEmit: true,
17
+ // rspack-manifest-plugin uses different option names than webpack-assets-manifest
18
+ generate: (seed, files, entrypoints) => {
19
+ let manifest = seed || {}
20
+
21
+ // Load existing manifest if it exists to handle concurrent builds
22
+ try {
23
+ if (existsSync(config.manifestPath)) {
24
+ const existingContent = readFileSync(config.manifestPath, "utf8")
25
+ const parsed = JSON.parse(existingContent)
26
+ if (parsed && typeof parsed === "object") {
27
+ manifest = {
28
+ ...manifest,
29
+ ...parsed
30
+ }
31
+ }
32
+ }
33
+ } catch (error) {
34
+ // eslint-disable-next-line no-console
35
+ console.warn(
36
+ "[SHAKAPACKER]: Warning: Could not read existing manifest.json:",
37
+ String(error)
38
+ )
39
+ }
40
+
41
+ // Add files mapping first
42
+ files.forEach((file) => {
43
+ manifest[file.name] = file.path
44
+ })
45
+
46
+ // Add entrypoints information compatible with Shakapacker expectations
47
+ const entrypointsManifest = {}
48
+ Object.entries(entrypoints).forEach(
49
+ ([entrypointName, entrypointFiles]) => {
50
+ const jsFiles = entrypointFiles.filter((file) =>
51
+ file.endsWith(".js")
52
+ )
53
+ const cssFiles = entrypointFiles.filter((file) =>
54
+ file.endsWith(".css")
55
+ )
56
+
57
+ entrypointsManifest[entrypointName] = {
58
+ assets: {
59
+ js: jsFiles,
60
+ css: cssFiles
61
+ }
62
+ }
63
+ }
64
+ )
65
+ manifest.entrypoints = entrypointsManifest
66
+
67
+ return manifest
68
+ }
69
+ })
70
+ ]
71
+
72
+ if (moduleExists("css-loader")) {
73
+ const hash = isProduction || config.useContentHash ? "-[contenthash:8]" : ""
74
+ // Use Rspack's built-in CSS extraction
75
+ const { CssExtractRspackPlugin } = rspack
76
+ plugins.push(
77
+ new CssExtractRspackPlugin({
78
+ filename: `css/[name]${hash}.css`,
79
+ chunkFilename: `css/[id]${hash}.css`,
80
+ // For projects where css ordering has been mitigated through consistent use of scoping or naming conventions,
81
+ // the css order warnings can be disabled by setting the ignoreOrder flag.
82
+ ignoreOrder: config.css_extract_ignore_order_warnings,
83
+ // Force writing CSS files to disk in development for Rails compatibility
84
+ emit: true
85
+ })
86
+ )
87
+ }
88
+
89
+ // Use Rspack's built-in SubresourceIntegrityPlugin
90
+ if (config.integrity.enabled) {
91
+ plugins.push(
92
+ new rspack.SubresourceIntegrityPlugin({
93
+ hashFuncNames: config.integrity.hash_functions,
94
+ enabled: isProduction
95
+ })
96
+ )
97
+ }
98
+
99
+ return plugins
100
+ }
101
+
102
+ module.exports = {
103
+ getPlugins
104
+ }
@@ -0,0 +1,62 @@
1
+ const { requireOrError } = require("../utils/requireOrError")
2
+ // TODO: Change to `const { WebpackAssetsManifest }` when dropping 'webpack-assets-manifest < 6.0.0' (Node >=20.10.0) support
3
+ const WebpackAssetsManifest = requireOrError("webpack-assets-manifest")
4
+ const webpack = requireOrError("webpack")
5
+ const config = require("../config")
6
+ const { isProduction } = require("../env")
7
+ const { moduleExists } = require("../utils/helpers")
8
+
9
+ const getPlugins = () => {
10
+ // TODO: Remove WebpackAssetsManifestConstructor workaround when dropping 'webpack-assets-manifest < 6.0.0' (Node >=20.10.0) support
11
+ const WebpackAssetsManifestConstructor =
12
+ "WebpackAssetsManifest" in WebpackAssetsManifest
13
+ ? WebpackAssetsManifest.WebpackAssetsManifest
14
+ : WebpackAssetsManifest
15
+ const plugins = [
16
+ new webpack.EnvironmentPlugin(process.env),
17
+ new WebpackAssetsManifestConstructor({
18
+ entrypoints: true,
19
+ writeToDisk: true,
20
+ output: config.manifestPath,
21
+ entrypointsUseAssets: true,
22
+ publicPath: config.publicPathWithoutCDN,
23
+ integrity: config.integrity.enabled,
24
+ integrityHashes: config.integrity.hash_functions
25
+ })
26
+ ]
27
+
28
+ if (moduleExists("css-loader") && moduleExists("mini-css-extract-plugin")) {
29
+ const hash = isProduction || config.useContentHash ? "-[contenthash:8]" : ""
30
+ const MiniCssExtractPlugin = requireOrError("mini-css-extract-plugin")
31
+ plugins.push(
32
+ new MiniCssExtractPlugin({
33
+ filename: `css/[name]${hash}.css`,
34
+ chunkFilename: `css/[id]${hash}.css`,
35
+ // For projects where css ordering has been mitigated through consistent use of scoping or naming conventions,
36
+ // the css order warnings can be disabled by setting the ignoreOrder flag.
37
+ ignoreOrder: config.css_extract_ignore_order_warnings
38
+ })
39
+ )
40
+ }
41
+
42
+ if (
43
+ config.integrity.enabled &&
44
+ moduleExists("webpack-subresource-integrity")
45
+ ) {
46
+ const SubresourceIntegrityPlugin = requireOrError(
47
+ "webpack-subresource-integrity"
48
+ )
49
+ plugins.push(
50
+ new SubresourceIntegrityPlugin({
51
+ hashFuncNames: config.integrity.hash_functions,
52
+ enabled: isProduction
53
+ })
54
+ )
55
+ }
56
+
57
+ return plugins
58
+ }
59
+
60
+ module.exports = {
61
+ getPlugins
62
+ }
data/package/rules/css.js CHANGED
@@ -1,3 +1,3 @@
1
- const getStyleRule = require("../utils/getStyleRule")
1
+ const { getStyleRule } = require("../utils/getStyleRule")
2
2
 
3
3
  module.exports = getStyleRule(/\.(css)$/i)
@@ -1,4 +1,4 @@
1
- const { dirname } = require("path")
1
+ const { dirname, sep, normalize } = require("path")
2
2
  const {
3
3
  additional_paths: additionalPaths,
4
4
  source_path: sourcePath
@@ -10,16 +10,22 @@ module.exports = {
10
10
  type: "asset/resource",
11
11
  generator: {
12
12
  filename: (pathData) => {
13
- const path = dirname(pathData.filename)
14
- const stripPaths = [...additionalPaths, sourcePath]
13
+ const path = normalize(dirname(pathData.filename))
14
+ const stripPaths = [...additionalPaths, sourcePath].map((p) =>
15
+ normalize(p)
16
+ )
15
17
 
16
18
  const selectedStripPath = stripPaths.find((includePath) =>
17
19
  path.startsWith(includePath)
18
20
  )
19
21
 
22
+ if (!selectedStripPath) {
23
+ return `static/[name]-[hash][ext][query]`
24
+ }
25
+
20
26
  const folders = path
21
- .replace(`${selectedStripPath}`, "")
22
- .split("/")
27
+ .replace(selectedStripPath, "")
28
+ .split(sep)
23
29
  .filter(Boolean)
24
30
 
25
31
  const foldersWithStatic = ["static", ...folders].join("/")
@@ -1,6 +1,6 @@
1
1
  const path = require("path")
2
2
  const { canProcess } = require("../utils/helpers")
3
- const getStyleRule = require("../utils/getStyleRule")
3
+ const { getStyleRule } = require("../utils/getStyleRule")
4
4
 
5
5
  const {
6
6
  additional_paths: paths,
data/package/rules/raw.js CHANGED
@@ -1,5 +1,15 @@
1
- module.exports = {
1
+ const config = require("../config")
2
+
3
+ const rspackRawConfig = {
4
+ resourceQuery: /raw/,
5
+ type: "asset/source"
6
+ }
7
+
8
+ const webpackRawConfig = {
2
9
  test: /\.html$/,
3
10
  exclude: /\.(js|mjs|jsx|ts|tsx)$/,
4
11
  type: "asset/source"
5
12
  }
13
+
14
+ module.exports =
15
+ config.bundler === "rspack" ? rspackRawConfig : webpackRawConfig
@@ -0,0 +1,96 @@
1
+ /* eslint global-require: 0 */
2
+
3
+ const { moduleExists } = require("../utils/helpers")
4
+
5
+ const rules = []
6
+
7
+ // Use Rspack's built-in SWC loader for JavaScript files
8
+ rules.push({
9
+ test: /\.(js|jsx|mjs)$/,
10
+ exclude: /node_modules/,
11
+ type: "javascript/auto",
12
+ use: [
13
+ {
14
+ loader: "builtin:swc-loader",
15
+ options: {
16
+ jsc: {
17
+ parser: {
18
+ syntax: "ecmascript",
19
+ jsx: true
20
+ },
21
+ transform: {
22
+ react: {
23
+ runtime: "automatic"
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ]
30
+ })
31
+
32
+ // Use Rspack's built-in SWC loader for TypeScript files
33
+ rules.push({
34
+ test: /\.(ts|tsx)$/,
35
+ exclude: /node_modules/,
36
+ type: "javascript/auto",
37
+ use: [
38
+ {
39
+ loader: "builtin:swc-loader",
40
+ options: {
41
+ jsc: {
42
+ parser: {
43
+ syntax: "typescript",
44
+ tsx: true
45
+ },
46
+ transform: {
47
+ react: {
48
+ runtime: "automatic"
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ]
55
+ })
56
+
57
+ // CSS rules using Rspack's built-in CSS handling
58
+ if (moduleExists("css-loader")) {
59
+ const css = require("./css")
60
+ rules.push(css)
61
+ }
62
+
63
+ // Sass rules
64
+ if (moduleExists("sass") && moduleExists("sass-loader")) {
65
+ const sass = require("./sass")
66
+ rules.push(sass)
67
+ }
68
+
69
+ // Less rules
70
+ if (moduleExists("less") && moduleExists("less-loader")) {
71
+ const less = require("./less")
72
+ rules.push(less)
73
+ }
74
+
75
+ // Stylus rules
76
+ if (moduleExists("stylus") && moduleExists("stylus-loader")) {
77
+ const stylus = require("./stylus")
78
+ rules.push(stylus)
79
+ }
80
+
81
+ // ERB template support
82
+ const erb = require("./erb")
83
+
84
+ rules.push(erb)
85
+
86
+ // File/asset handling using Rspack's built-in asset modules
87
+ const file = require("./file")
88
+
89
+ rules.push(file)
90
+
91
+ // Raw file loading
92
+ const raw = require("./raw")
93
+
94
+ rules.push(raw)
95
+
96
+ module.exports = rules
@@ -1,6 +1,6 @@
1
1
  /* eslint global-require: 0 */
2
2
 
3
- const getStyleRule = require("../utils/getStyleRule")
3
+ const { getStyleRule } = require("../utils/getStyleRule")
4
4
  const { canProcess, packageMajorVersion } = require("../utils/helpers")
5
5
  const { additional_paths: extraPaths } = require("../config")
6
6
 
@@ -11,7 +11,11 @@ module.exports = canProcess("sass-loader", (resolvedPath) => {
11
11
  {
12
12
  loader: resolvedPath,
13
13
  options: {
14
- sassOptions: { [optionKey]: extraPaths }
14
+ sourceMap: true,
15
+ sassOptions: {
16
+ [optionKey]: extraPaths,
17
+ quietDeps: true
18
+ }
15
19
  }
16
20
  }
17
21
  ])
@@ -1,6 +1,6 @@
1
1
  const path = require("path")
2
2
  const { canProcess } = require("../utils/helpers")
3
- const getStyleRule = require("../utils/getStyleRule")
3
+ const { getStyleRule } = require("../utils/getStyleRule")
4
4
 
5
5
  const {
6
6
  additional_paths: paths,
@@ -1,5 +1,7 @@
1
1
  /* eslint global-require: 0 */
2
2
  const { canProcess, moduleExists } = require("./helpers")
3
+ const { requireOrError } = require("./requireOrError")
4
+ const config = require("../config")
3
5
  const inliningCss = require("./inliningCss")
4
6
 
5
7
  const getStyleRule = (test, preprocessors = []) => {
@@ -12,8 +14,13 @@ const getStyleRule = (test, preprocessors = []) => {
12
14
 
13
15
  // style-loader is required when using css modules with HMR on the webpack-dev-server
14
16
 
17
+ const extractionPlugin =
18
+ config.bundle === "rspack"
19
+ ? requireOrError("@rspack/core").CssExtractRspackPlugin.loader
20
+ : requireOrError("mini-css-extract-plugin").loader
21
+
15
22
  const use = [
16
- inliningCss ? "style-loader" : require("mini-css-extract-plugin").loader,
23
+ inliningCss ? "style-loader" : extractionPlugin,
17
24
  {
18
25
  loader: require.resolve("css-loader"),
19
26
  options: {
@@ -28,13 +35,19 @@ const getStyleRule = (test, preprocessors = []) => {
28
35
  ...preprocessors
29
36
  ].filter(Boolean)
30
37
 
31
- return {
38
+ const result = {
32
39
  test,
33
40
  use
34
41
  }
42
+
43
+ if (config.bundle === "rspack") {
44
+ result.type = "javascript/auto" // Required for rspack CSS extraction
45
+ }
46
+
47
+ return result
35
48
  }
36
49
 
37
50
  return null
38
51
  }
39
52
 
40
- module.exports = getStyleRule
53
+ module.exports = { getStyleRule }
@@ -0,0 +1,15 @@
1
+ /* eslint global-require: 0 */
2
+ /* eslint import/no-dynamic-require: 0 */
3
+ const config = require("../config")
4
+
5
+ const requireOrError = (moduleName) => {
6
+ try {
7
+ return require(moduleName)
8
+ } catch (error) {
9
+ throw new Error(
10
+ `[SHAKAPACKER]: ${moduleName} is required for ${config.bundler} but is not installed. View Shakapacker's documented dependencies at https://github.com/shakacode/shakapacker/tree/main/docs/peer-dependencies.md`
11
+ )
12
+ }
13
+ }
14
+
15
+ module.exports = { requireOrError }
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shakapacker",
3
- "version": "8.4.0",
3
+ "version": "9.0.0-beta.0",
4
4
  "description": "Use webpack to manage app-like JavaScript modules in Rails",
5
5
  "homepage": "https://github.com/shakacode/shakapacker",
6
6
  "bugs": {
@@ -14,6 +14,16 @@
14
14
  "author": "David Heinemeier Hansson <david@basecamp.com>, Justin Gordon <justin@shakacode.com>",
15
15
  "main": "package/index.js",
16
16
  "types": "package/index.d.ts",
17
+ "exports": {
18
+ ".": "./package/index.js",
19
+ "./webpack": "./package/webpack/index.js",
20
+ "./rspack": "./package/rspack/index.js",
21
+ "./swc": "./package/swc/index.js",
22
+ "./esbuild": "./package/esbuild/index.js",
23
+ "./package.json": "./package.json",
24
+ "./package/babel/preset.js": "./package/babel/preset.js",
25
+ "./package/*": "./package/*"
26
+ },
17
27
  "files": [
18
28
  "package",
19
29
  "lib/install/config/shakapacker.yml"
@@ -27,8 +37,11 @@
27
37
  "path-complete-extname": "^1.0.0"
28
38
  },
29
39
  "devDependencies": {
40
+ "@rspack/cli": "^1.4.11",
41
+ "@rspack/core": "^1.4.11",
30
42
  "babel-loader": "^8.2.4",
31
43
  "compression-webpack-plugin": "^9.0.0",
44
+ "css-loader": "^7.1.2",
32
45
  "esbuild-loader": "^2.18.0",
33
46
  "eslint": "^8.0.0",
34
47
  "eslint-config-airbnb": "^19.0.0",
@@ -41,41 +54,16 @@
41
54
  "eslint-plugin-react-hooks": "^4.6.0",
42
55
  "jest": "^29.7.0",
43
56
  "memory-fs": "^0.5.0",
57
+ "mini-css-extract-plugin": "^2.9.4",
44
58
  "prettier": "^3.2.5",
59
+ "rspack-manifest-plugin": "^5.0.3",
60
+ "sass-loader": "^16.0.5",
45
61
  "swc-loader": "^0.1.15",
46
62
  "thenify": "^3.3.1",
47
63
  "webpack": "5.93.0",
48
64
  "webpack-assets-manifest": "^5.0.6",
49
- "webpack-subresource-integrity": "^5.1.0",
50
- "webpack-merge": "^5.8.0"
51
- },
52
- "peerDependencies": {
53
- "@babel/core": "^7.17.9",
54
- "@babel/plugin-transform-runtime": "^7.17.0",
55
- "@babel/preset-env": "^7.16.11",
56
- "@babel/runtime": "^7.17.9",
57
- "@types/babel__core": "^7.0.0",
58
- "@types/webpack": "^5.0.0",
59
- "babel-loader": "^8.2.4 || ^9.0.0 || ^10.0.0",
60
- "compression-webpack-plugin": "^9.0.0 || ^10.0.0|| ^11.0.0",
61
- "terser-webpack-plugin": "^5.3.1",
62
- "webpack": "^5.76.0",
63
- "webpack-assets-manifest": "^5.0.6 || ^6.0.0",
64
- "webpack-subresource-integrity": "^5.1.0",
65
- "webpack-cli": "^4.9.2 || ^5.0.0 || ^6.0.0",
66
- "webpack-dev-server": "^4.15.2 || ^5.2.2",
67
- "webpack-merge": "^5.8.0 || ^6.0.0"
68
- },
69
- "peerDependenciesMeta": {
70
- "@types/babel__core": {
71
- "optional": true
72
- },
73
- "@types/webpack": {
74
- "optional": true
75
- },
76
- "webpack-subresource-integrity": {
77
- "optional": true
78
- }
65
+ "webpack-merge": "^5.8.0",
66
+ "webpack-subresource-integrity": "^5.1.0"
79
67
  },
80
68
  "packageManager": "yarn@1.22.22",
81
69
  "engines": {
@@ -79,7 +79,7 @@ describe("Base config", () => {
79
79
  })
80
80
 
81
81
  test("should return default loader rules for each file in config/loaders", () => {
82
- const rules = require("../../../package/rules")
82
+ const rules = require("../../../package/rules/webpack")
83
83
 
84
84
  const defaultRules = Object.keys(rules)
85
85
  const configRules = baseConfig.module.rules
@@ -1,4 +1,4 @@
1
- const rules = require("../../../package/rules/index")
1
+ const rules = require("../../../package/rules/webpack")
2
2
 
3
3
  jest.mock("../../../package/utils/helpers", () => {
4
4
  const original = jest.requireActual("../../../package/utils/helpers")