rails-vite-plugin 0.2.0 → 0.2.1

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.
package/dist/index.js CHANGED
@@ -2,9 +2,11 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import picomatch from 'picomatch';
4
4
  import { loadEnv, defaultAllowedOrigins, } from 'vite';
5
+ import { ORIGIN_PLACEHOLDER } from './shared/types.js';
5
6
  import { resolveInput, detectEntrypointsDir, discoverEntrypointInputs, detectEntrypoint } from './shared/entries.js';
6
7
  import { resolveAlias } from './shared/alias.js';
7
- import { resolveDevServerUrl, isAddressInfo } from './shared/dev-server.js';
8
+ import { resolveDevServerUrl, isAddressInfo, replaceOriginPlaceholder } from './shared/dev-server.js';
9
+ import { resolveBundlerOptionsKey, getUserBundlerInput } from './shared/bundler-compat.js';
8
10
  import { ensureCommandShouldRunInEnvironment } from './shared/env-guard.js';
9
11
  import { refreshPaths, resolveRefreshPaths } from './shared/refresh.js';
10
12
  import { readDevServerIndexHtml } from './shared/dev-server-page.js';
@@ -16,32 +18,39 @@ export default function rails(options = {}) {
16
18
  const entrypointsDir = options.input === undefined ? detectEntrypointsDir(sourceDir) : null;
17
19
  const input = options.input ?? (entrypointsDir ? discoverEntrypointInputs(sourceDir, entrypointsDir) : detectEntrypoint(sourceDir));
18
20
  const publicDir = options.publicDir ?? 'public';
19
- const buildDir = options.buildDir ?? 'vite';
21
+ const userBuildDir = options.buildDir;
20
22
  const devMetaPath = options.devMetaFile ?? path.join('tmp', 'rails-vite.json');
21
23
  const ssrOutDir = options.ssrOutDir ?? 'ssr';
22
24
  const resolvedInput = resolveInput(input, sourceDir);
23
25
  const resolvedSsr = options.ssr !== undefined ? resolveInput(options.ssr, sourceDir) : undefined;
24
26
  let resolvedConfig;
25
27
  let reactRefresh = false;
28
+ let devServerUrl = null;
29
+ let effectiveBuildDir;
26
30
  return {
27
31
  name: 'rails-vite',
28
32
  enforce: 'post',
29
33
  config(userConfig, { command, mode, isSsrBuild }) {
30
34
  const env = loadEnv(mode, userConfig.envDir || process.cwd(), '');
31
35
  ensureCommandShouldRunInEnvironment(command, env, 'rails-vite-plugin');
36
+ // @ts-expect-error -- `this.meta.rolldownVersion` exists in Vite 8+
37
+ const bundlerOptionsKey = resolveBundlerOptionsKey(this.meta);
38
+ const userBundlerInput = getUserBundlerInput(userConfig);
39
+ effectiveBuildDir = userBuildDir ?? (mode === 'test' ? 'vite-test' : 'vite');
32
40
  return {
33
- base: userConfig.base ?? (command === 'build' ? `/${buildDir}/` : ''),
41
+ base: userConfig.base ?? (command === 'build' ? `/${effectiveBuildDir}/` : ''),
34
42
  publicDir: userConfig.publicDir ?? false,
35
43
  build: {
36
44
  manifest: userConfig.build?.manifest ?? (isSsrBuild ? false : 'manifest.json'),
37
45
  ssrManifest: userConfig.build?.ssrManifest ?? (isSsrBuild ? 'ssr-manifest.json' : false),
38
- outDir: userConfig.build?.outDir ?? (isSsrBuild ? ssrOutDir : path.join(publicDir, buildDir)),
39
- rollupOptions: {
40
- input: userConfig.build?.rollupOptions?.input ?? (isSsrBuild ? resolvedSsr : resolvedInput),
46
+ outDir: userConfig.build?.outDir ?? (isSsrBuild ? ssrOutDir : path.join(publicDir, effectiveBuildDir)),
47
+ [bundlerOptionsKey]: {
48
+ input: userBundlerInput ?? (isSsrBuild ? resolvedSsr : resolvedInput),
41
49
  },
42
50
  assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? 0,
43
51
  },
44
52
  server: {
53
+ origin: command === 'serve' ? (userConfig.server?.origin ?? ORIGIN_PLACEHOLDER) : undefined,
45
54
  cors: userConfig.server?.cors ?? {
46
55
  origin: userConfig.server?.origin ?? defaultAllowedOrigins,
47
56
  },
@@ -62,15 +71,20 @@ export default function rails(options = {}) {
62
71
  if (resolvedConfig.build.ssr)
63
72
  return;
64
73
  const outDir = resolvedConfig.build.outDir;
65
- fs.writeFileSync(path.join(outDir, 'rails-vite.json'), JSON.stringify(entrypointsDir ? { sourceDir, entrypointsDir } : { sourceDir }));
74
+ const meta = { sourceDir, buildDir: effectiveBuildDir };
75
+ if (entrypointsDir)
76
+ meta.entrypointsDir = entrypointsDir;
77
+ fs.writeFileSync(path.join(outDir, 'rails-vite.json'), JSON.stringify(meta));
78
+ },
79
+ transform(code) {
80
+ return replaceOriginPlaceholder(code, devServerUrl);
66
81
  },
67
82
  configureServer(server) {
68
83
  server.httpServer?.once('listening', () => {
69
84
  const address = server.httpServer?.address();
70
85
  if (isAddressInfo(address)) {
71
- const devServerUrl = resolveDevServerUrl(address, resolvedConfig);
72
- resolvedConfig.server.origin = devServerUrl;
73
- const meta = { url: devServerUrl, sourceDir };
86
+ devServerUrl = resolveDevServerUrl(address, resolvedConfig);
87
+ const meta = { url: devServerUrl, sourceDir, buildDir: effectiveBuildDir };
74
88
  if (entrypointsDir)
75
89
  meta.entrypointsDir = entrypointsDir;
76
90
  if (reactRefresh)
@@ -2,9 +2,11 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import picomatch from 'picomatch';
4
4
  import { loadEnv, defaultAllowedOrigins, } from 'vite';
5
+ import { ORIGIN_PLACEHOLDER } from './shared/types.js';
5
6
  import { resolveEntries, entriesToRollupInput, prefixWithSourceDir, detectEntrypointsDir, discoverEntrypointInputs, detectEntrypoint, isEntrypointFile } from './shared/entries.js';
6
7
  import { resolveAlias } from './shared/alias.js';
7
- import { resolveDevServerUrl, isAddressInfo } from './shared/dev-server.js';
8
+ import { resolveDevServerUrl, isAddressInfo, replaceOriginPlaceholder } from './shared/dev-server.js';
9
+ import { resolveBundlerOptionsKey, getUserBundlerInput } from './shared/bundler-compat.js';
8
10
  import { ensureCommandShouldRunInEnvironment } from './shared/env-guard.js';
9
11
  import { refreshPaths, resolveRefreshPaths } from './shared/refresh.js';
10
12
  import { cssExtensions } from './shared/css.js';
@@ -30,6 +32,7 @@ export default function jsbundling(options = {}) {
30
32
  const ssrConfig = resolveSsrConfig(options.ssr, sourceDir, outputDir);
31
33
  let resolvedConfig;
32
34
  let reactRefresh = false;
35
+ let devServerUrl = null;
33
36
  // Track stubs written in dev so we can clean them up
34
37
  const writtenStubs = [];
35
38
  return {
@@ -38,6 +41,9 @@ export default function jsbundling(options = {}) {
38
41
  config(userConfig, { command, mode, isSsrBuild }) {
39
42
  const env = loadEnv(mode, userConfig.envDir || process.cwd(), '');
40
43
  ensureCommandShouldRunInEnvironment(command, env, 'rails-vite-plugin/jsbundling');
44
+ // @ts-expect-error -- `this.meta.rolldownVersion` exists in Vite 8+
45
+ const bundlerOptionsKey = resolveBundlerOptionsKey(this.meta);
46
+ const userBundlerInput = getUserBundlerInput(userConfig);
41
47
  // SSR builds get minimal config — just entry, outDir, base, and alias.
42
48
  // No asset pipeline stubs or client-specific settings.
43
49
  if (isSsrBuild) {
@@ -48,8 +54,8 @@ export default function jsbundling(options = {}) {
48
54
  build: {
49
55
  ssrManifest: userConfig.build?.ssrManifest ?? 'ssr-manifest.json',
50
56
  outDir: userConfig.build?.outDir ?? ssrConfig.outDir,
51
- rollupOptions: {
52
- input: userConfig.build?.rollupOptions?.input ?? ssrConfig.entry,
57
+ [bundlerOptionsKey]: {
58
+ input: userBundlerInput ?? ssrConfig.entry,
53
59
  },
54
60
  },
55
61
  } : {}),
@@ -61,19 +67,14 @@ export default function jsbundling(options = {}) {
61
67
  },
62
68
  };
63
69
  }
64
- // In dev mode, write placeholder stubs immediately so Propshaft/Sprockets
65
- // can discover them when Rails boots (both may start concurrently via bin/dev).
66
- if (command === 'serve') {
67
- writePlaceholderStubs(entries, assetPipelineDir, writtenStubs);
68
- }
69
70
  return {
70
71
  base: userConfig.base ?? (command === 'build' ? buildBase : ''),
71
72
  publicDir: userConfig.publicDir ?? false,
72
73
  build: {
73
74
  manifest: userConfig.build?.manifest ?? false,
74
75
  outDir: userConfig.build?.outDir ?? outputDir,
75
- rollupOptions: {
76
- input: userConfig.build?.rollupOptions?.input ?? rollupInput,
76
+ [bundlerOptionsKey]: {
77
+ input: userBundlerInput ?? rollupInput,
77
78
  output: {
78
79
  entryFileNames: (chunkInfo) => {
79
80
  if (chunkInfo.facadeModuleId && cssExtensions.test(chunkInfo.facadeModuleId)) {
@@ -89,6 +90,7 @@ export default function jsbundling(options = {}) {
89
90
  cssCodeSplit: true,
90
91
  },
91
92
  server: {
93
+ origin: command === 'serve' ? (userConfig.server?.origin ?? ORIGIN_PLACEHOLDER) : undefined,
92
94
  cors: userConfig.server?.cors ?? {
93
95
  origin: userConfig.server?.origin ?? defaultAllowedOrigins,
94
96
  },
@@ -138,8 +140,10 @@ export default function jsbundling(options = {}) {
138
140
  }
139
141
  }
140
142
  },
143
+ transform(code) {
144
+ return replaceOriginPlaceholder(code, devServerUrl);
145
+ },
141
146
  configureServer(server) {
142
- let devServerUrl = null;
143
147
  let syncTimer = null;
144
148
  // Re-discover entries from the entrypoints dir and regenerate all stubs.
145
149
  // Called on initial listen and whenever entrypoint files are added/removed.
@@ -162,6 +166,13 @@ export default function jsbundling(options = {}) {
162
166
  clearTimeout(syncTimer);
163
167
  syncTimer = setTimeout(syncStubs, 100);
164
168
  };
169
+ // Write placeholder stubs so Propshaft/Sprockets can discover asset files
170
+ // at boot time, before the Vite dev server is ready with its URL.
171
+ // Only write when httpServer exists — skip for embedded Vite instances
172
+ // (e.g., Storybook) that would otherwise overwrite real dev stubs.
173
+ if (server.httpServer && !server.httpServer.listening) {
174
+ writePlaceholderStubs(entries, assetPipelineDir, writtenStubs);
175
+ }
165
176
  server.httpServer?.once('listening', () => {
166
177
  const address = server.httpServer?.address();
167
178
  if (isAddressInfo(address)) {
@@ -0,0 +1,5 @@
1
+ import type { UserConfig } from 'vite';
2
+ export declare function resolveBundlerOptionsKey(meta: {
3
+ rolldownVersion?: string;
4
+ }): 'rolldownOptions' | 'rollupOptions';
5
+ export declare function getUserBundlerInput(userConfig: UserConfig): unknown;
@@ -0,0 +1,9 @@
1
+ // Vite 8 (Rolldown) uses `rolldownOptions` instead of `rollupOptions`.
2
+ // Detect at runtime to support both Vite 7 and 8.
3
+ export function resolveBundlerOptionsKey(meta) {
4
+ return meta?.rolldownVersion ? 'rolldownOptions' : 'rollupOptions';
5
+ }
6
+ export function getUserBundlerInput(userConfig) {
7
+ // @ts-expect-error -- `rolldownOptions` exists in Vite 8+ only
8
+ return userConfig.build?.rolldownOptions?.input ?? userConfig.build?.rollupOptions?.input;
9
+ }
@@ -3,3 +3,4 @@ import type { ResolvedConfig } from 'vite';
3
3
  import type { DevServerUrl } from './types.js';
4
4
  export declare function resolveDevServerUrl(address: AddressInfo, config: ResolvedConfig): DevServerUrl;
5
5
  export declare function isAddressInfo(x: string | AddressInfo | null | undefined): x is AddressInfo;
6
+ export declare function replaceOriginPlaceholder(code: string, devServerUrl: string | null): string | undefined;
@@ -1,3 +1,4 @@
1
+ import { ORIGIN_PLACEHOLDER } from './types.js';
1
2
  export function resolveDevServerUrl(address, config) {
2
3
  const hmr = typeof config.server.hmr === 'object' ? config.server.hmr : null;
3
4
  const clientProtocol = hmr?.protocol
@@ -18,3 +19,8 @@ export function resolveDevServerUrl(address, config) {
18
19
  export function isAddressInfo(x) {
19
20
  return typeof x === 'object' && x !== null;
20
21
  }
22
+ export function replaceOriginPlaceholder(code, devServerUrl) {
23
+ if (devServerUrl && code.includes(ORIGIN_PLACEHOLDER)) {
24
+ return code.replaceAll(ORIGIN_PLACEHOLDER, devServerUrl);
25
+ }
26
+ }
@@ -1,5 +1,6 @@
1
1
  export type InputOption = string | string[] | Record<string, string>;
2
2
  export type DevServerUrl = `${'http' | 'https'}://${string}:${number}`;
3
+ export declare const ORIGIN_PLACEHOLDER = "http://__rails_vite_placeholder__.test";
3
4
  export interface ResolvedEntry {
4
5
  /** Output name (e.g., 'application', 'admin/index') */
5
6
  name: string;
@@ -1 +1,3 @@
1
- export {};
1
+ // Placeholder URL used as `server.origin` during config resolution.
2
+ // Replaced with the real dev server URL once the server starts listening.
3
+ export const ORIGIN_PLACEHOLDER = 'http://__rails_vite_placeholder__.test';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rails-vite-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Vite plugin for Rails integration",
5
5
  "author": "Svyatoslav Kryukov <me@skryukov.dev>",
6
6
  "license": "MIT",