rails-vite-plugin 0.2.1 → 0.2.3

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.
@@ -56,6 +56,9 @@ export default function jsbundling(options = {}) {
56
56
  outDir: userConfig.build?.outDir ?? ssrConfig.outDir,
57
57
  [bundlerOptionsKey]: {
58
58
  input: userBundlerInput ?? ssrConfig.entry,
59
+ output: {
60
+ assetFileNames: '[name][extname]',
61
+ },
59
62
  },
60
63
  },
61
64
  } : {}),
@@ -80,7 +83,7 @@ export default function jsbundling(options = {}) {
80
83
  if (chunkInfo.facadeModuleId && cssExtensions.test(chunkInfo.facadeModuleId)) {
81
84
  return `${CSS_FACADE_PREFIX}[name].js`;
82
85
  }
83
- return '[name].js';
86
+ return '_[name]-[hash].js';
84
87
  },
85
88
  chunkFileNames: '[name]-[hash].js',
86
89
  assetFileNames: '[name][extname]',
@@ -120,19 +123,30 @@ export default function jsbundling(options = {}) {
120
123
  // SSR bundles are Node.js server code — not served to browsers.
121
124
  if (resolvedConfig.build.ssr)
122
125
  return;
123
- // Copy entry files (JS + CSS) to the asset pipeline directory
124
- // so Propshaft/Sprockets can serve them via Rails helpers.
125
- // Chunks stay in outputDir and are served directly by the web server.
126
126
  fs.mkdirSync(assetPipelineDir, { recursive: true });
127
127
  const outDir = resolvedConfig.build.outDir;
128
- // Only copy CSS files that correspond to entries (entry CSS or CSS extracted
129
- // from JS entries). Shared chunk CSS stays in outputDir.
130
128
  const entryNames = new Set(entries.map(e => e.name));
131
129
  for (const [fileName, chunk] of Object.entries(bundle)) {
132
130
  const isEntryJs = chunk.type === 'chunk' && chunk.isEntry;
133
131
  const isEntryCss = chunk.type === 'asset' && cssExtensions.test(fileName)
134
132
  && entryNames.has(fileName.replace(/\.[^.]+$/, ''));
135
- if (isEntryJs || isEntryCss) {
133
+ if (isEntryJs) {
134
+ // JS entries are built as _[name]-[hash].js (content-hashed).
135
+ // Write a thin shim as [name].js for the asset pipeline so that
136
+ // Propshaft/Sprockets can serve it via javascript_include_tag.
137
+ //
138
+ // Why: Propshaft digests entry filenames (inertia.js → inertia-abc123.js)
139
+ // but cannot rewrite import paths inside Vite's chunks. Without the shim,
140
+ // the <script> tag and chunk imports resolve to different ES module
141
+ // instances — duplicating React, Inertia, and other stateful singletons.
142
+ // The shim ensures both paths chain to the same _[name]-[hash].js module.
143
+ const shimName = chunk.name + '.js';
144
+ const shim = `import "./${fileName}";export * from "./${fileName}";\n`;
145
+ fs.writeFileSync(path.join(outDir, shimName), shim);
146
+ fs.writeFileSync(path.join(assetPipelineDir, shimName), shim);
147
+ }
148
+ else if (isEntryCss) {
149
+ // Copy entry CSS to the asset pipeline. Shared chunk CSS stays in outputDir.
136
150
  const src = path.join(outDir, fileName);
137
151
  const dest = path.join(assetPipelineDir, fileName);
138
152
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import picomatch from 'picomatch';
3
4
  import { cssExtensionList } from './css.js';
4
5
  const jsExtensions = ['mjs', 'js', 'mts', 'ts', 'jsx', 'tsx'];
5
6
  const entrypointExtensions = new RegExp(`\\.(${[...jsExtensions, ...cssExtensionList].join('|')})$`);
@@ -13,7 +14,8 @@ export function resolveEntries(input, sourceDir) {
13
14
  sourcePath: prefixWithSourceDir(value, sourceDir),
14
15
  }));
15
16
  }
16
- const inputs = Array.isArray(input) ? input : [input];
17
+ const rawInputs = Array.isArray(input) ? input : [input];
18
+ const inputs = rawInputs.flatMap(entry => expandGlob(entry, sourceDir));
17
19
  const sourcePaths = inputs.map(entry => prefixWithSourceDir(entry, sourceDir));
18
20
  const commonPrefix = detectCommonEntryPrefix(sourcePaths, sourceDir);
19
21
  return sourcePaths.map(sourcePath => ({
@@ -65,6 +67,52 @@ export function detectEntrypoint(sourceDir) {
65
67
  }
66
68
  return 'application.js';
67
69
  }
70
+ function isGlob(pattern) {
71
+ return /[*?{]/.test(pattern);
72
+ }
73
+ function expandGlob(entry, sourceDir) {
74
+ if (!isGlob(entry))
75
+ return [entry];
76
+ const matcher = picomatch(entry);
77
+ if (entry.includes('**')) {
78
+ // Recursive: find the static prefix directory, then walk it
79
+ const parts = entry.split('/');
80
+ const staticParts = [];
81
+ for (const part of parts) {
82
+ if (isGlob(part))
83
+ break;
84
+ staticParts.push(part);
85
+ }
86
+ const baseDir = staticParts.length > 0
87
+ ? path.join(sourceDir, ...staticParts)
88
+ : sourceDir;
89
+ if (!fs.existsSync(baseDir))
90
+ return [];
91
+ return walkDir(baseDir, sourceDir).filter(f => matcher(f));
92
+ }
93
+ // Non-recursive: single directory match
94
+ const dir = path.join(sourceDir, path.dirname(entry));
95
+ const pattern = path.basename(entry);
96
+ const fileMatcher = picomatch(pattern);
97
+ if (!fs.existsSync(dir))
98
+ return [];
99
+ return fs.readdirSync(dir, { withFileTypes: true })
100
+ .filter(f => f.isFile() && fileMatcher(f.name))
101
+ .map(f => path.relative(sourceDir, path.join(dir, f.name)));
102
+ }
103
+ function walkDir(dir, baseDir) {
104
+ const results = [];
105
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
106
+ const full = path.join(dir, entry.name);
107
+ if (entry.isDirectory()) {
108
+ results.push(...walkDir(full, baseDir));
109
+ }
110
+ else if (entry.isFile()) {
111
+ results.push(path.relative(baseDir, full));
112
+ }
113
+ }
114
+ return results;
115
+ }
68
116
  function discoverEntrypoints(dir, base = dir) {
69
117
  const entries = [];
70
118
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rails-vite-plugin",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Vite plugin for Rails integration",
5
5
  "author": "Svyatoslav Kryukov <me@skryukov.dev>",
6
6
  "license": "MIT",