Skip to content

Commit f3c7a00

Browse files
kevinccbsgclaude
andcommitted
fix(plugin): respect Vite base path in script injection and SW URL
The plugin was emitting a hardcoded "/@id/virtual:twd/init" script tag in transformIndexHtml(), which 404s on dev servers configured with a non-root base (e.g. base: "/platform-admin/") because Vite serves all dev resources under that prefix. Capture the resolved base via configResolved() and prefix: - script src in transformIndexHtml — always - default serviceWorkerUrl — only when the user did NOT explicitly set it (so user-supplied paths like "/platform-admin/mock-sw.js" are not double-prefixed) Default base "/" preserves existing behavior. Tests cover all four combinations (base x serviceWorkerUrl override). Reported by a real-project consumer with: base: "/platform-admin/" → GET http://localhost:5173/@id/virtual:twd/init 404 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9ad9e40 commit f3c7a00

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

src/plugin/twd.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Plugin } from 'vite';
1+
import type { Plugin, ResolvedConfig } from 'vite';
22
import type { TWDTheme } from '../ui/utils/theme';
33

44
/**
@@ -99,11 +99,28 @@ const DEFAULT_INIT_OPTIONS = {
9999
*/
100100
export function twd(options: TwdPluginOptions = {}): Plugin {
101101
const { testFilePattern = DEFAULT_PATTERN, ...userInitOptions } = options;
102+
// Track whether the user explicitly set serviceWorkerUrl so we don't
103+
// double-prefix when applying the resolved Vite base path.
104+
const userSetServiceWorkerUrl = userInitOptions.serviceWorkerUrl !== undefined;
102105
const initOptions = { ...DEFAULT_INIT_OPTIONS, ...userInitOptions };
103106

107+
// Captured from configResolved. Vite normalizes this to start and end
108+
// with "/" (default "/"). Used to prefix dev-server-relative URLs so
109+
// the plugin works under non-root `base` configs (e.g. "/platform-admin/").
110+
let resolvedBase = '/';
111+
104112
return {
105113
name: 'twd',
106114
apply: 'serve',
115+
configResolved(config: ResolvedConfig) {
116+
resolvedBase = config.base;
117+
// If the user didn't override serviceWorkerUrl, prefix the default
118+
// with the resolved base so the SW resolves under non-root deployments.
119+
if (!userSetServiceWorkerUrl && resolvedBase !== '/') {
120+
const trimmed = DEFAULT_INIT_OPTIONS.serviceWorkerUrl.replace(/^\//, '');
121+
initOptions.serviceWorkerUrl = `${resolvedBase}${trimmed}`;
122+
}
123+
},
107124
resolveId(id) {
108125
if (id === VIRTUAL_ID) {
109126
return RESOLVED_VIRTUAL_ID;
@@ -124,7 +141,7 @@ export function twd(options: TwdPluginOptions = {}): Plugin {
124141
return [
125142
{
126143
tag: 'script',
127-
attrs: { type: 'module', src: `/@id/${VIRTUAL_ID}` },
144+
attrs: { type: 'module', src: `${resolvedBase}@id/${VIRTUAL_ID}` },
128145
injectTo: 'head' as const,
129146
},
130147
];

src/tests/plugin/twd.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,51 @@ describe('twd vite plugin', () => {
8888
},
8989
]);
9090
});
91+
92+
it('prefixes the script src with a non-root resolved base', () => {
93+
const plugin = twd();
94+
const configResolved = plugin.configResolved as (config: { base: string }) => void;
95+
configResolved.call({}, { base: '/platform-admin/' });
96+
const transform = plugin.transformIndexHtml as () => unknown;
97+
const result = transform.call({});
98+
expect(result).toEqual([
99+
{
100+
tag: 'script',
101+
attrs: { type: 'module', src: '/platform-admin/@id/virtual:twd/init' },
102+
injectTo: 'head',
103+
},
104+
]);
105+
});
106+
});
107+
108+
describe('base path handling for serviceWorkerUrl', () => {
109+
it('prefixes the default serviceWorkerUrl with a non-root base', () => {
110+
const plugin = twd();
111+
const configResolved = plugin.configResolved as (config: { base: string }) => void;
112+
configResolved.call({}, { base: '/platform-admin/' });
113+
const load = plugin.load as (id: string) => string | null;
114+
const code = load.call({}, '\0virtual:twd/init');
115+
expect(code).toContain(`"serviceWorkerUrl":"/platform-admin/mock-sw.js"`);
116+
});
117+
118+
it('leaves the default serviceWorkerUrl unchanged when base is "/"', () => {
119+
const plugin = twd();
120+
const configResolved = plugin.configResolved as (config: { base: string }) => void;
121+
configResolved.call({}, { base: '/' });
122+
const load = plugin.load as (id: string) => string | null;
123+
const code = load.call({}, '\0virtual:twd/init');
124+
expect(code).toContain(`"serviceWorkerUrl":"/mock-sw.js"`);
125+
});
126+
127+
it('preserves a user-supplied serviceWorkerUrl regardless of base', () => {
128+
const plugin = twd({ serviceWorkerUrl: '/custom/path/mock-sw.js' });
129+
const configResolved = plugin.configResolved as (config: { base: string }) => void;
130+
configResolved.call({}, { base: '/platform-admin/' });
131+
const load = plugin.load as (id: string) => string | null;
132+
const code = load.call({}, '\0virtual:twd/init');
133+
// Not double-prefixed — user-supplied value wins.
134+
expect(code).toContain(`"serviceWorkerUrl":"/custom/path/mock-sw.js"`);
135+
expect(code).not.toContain(`/platform-admin/custom`);
136+
});
91137
});
92138
});

0 commit comments

Comments
 (0)