diff --git a/.changeset/fix-lazy-module-url-windows-backslash.md b/.changeset/fix-lazy-module-url-windows-backslash.md new file mode 100644 index 0000000..6d08174 --- /dev/null +++ b/.changeset/fix-lazy-module-url-windows-backslash.md @@ -0,0 +1,10 @@ +--- +'vite-plugin-solid': patch +--- + +fix: normalize lazy() module-url paths to forward slashes on Windows + +The `solid-lazy-module-url` transform appended the resolved module path as the +2nd argument to `lazy(() => import(...))` using `path.relative`, which yields +backslashes on Windows. The injected `"src\components\App.tsx"` is an invalid +escape sequence and broke dev/build for any `lazy()` route on Windows. diff --git a/scripts/test-examples.ts b/scripts/test-examples.ts index 58ead2d..1121e43 100644 --- a/scripts/test-examples.ts +++ b/scripts/test-examples.ts @@ -21,6 +21,21 @@ function cleanup() { process.on('SIGTERM', cleanup); process.on('SIGINT', cleanup); +// Run the node:test unit suites (test/**/*.test.ts) in a child process so their +// TAP output streams through and a failure aborts the whole run. +async function runUnitTests() { + console.log('Running unit tests...'); + await new Promise((resolve, reject) => { + const proc = spawn('node', ['--test', 'test/**/*.test.ts'], { stdio: 'inherit' }); + activeProcesses.add(proc); + proc.on('error', reject); + proc.on('exit', (code: number | null) => { + activeProcesses.delete(proc); + code === 0 ? resolve() : reject(new Error(`Unit tests failed (exit code ${code})`)); + }); + }); +} + async function runExample(example) { console.log(`Testing ${example}...`); const examplePath = `examples/${example}`; @@ -59,6 +74,14 @@ async function runExample(example) { } async function runAll() { + try { + await runUnitTests(); + } catch (error) { + console.error(error); + cleanup(); + process.exit(1); + } + for (const example of examples) { try { await runExample(example); diff --git a/src/index.ts b/src/index.ts index 72d6ce9..2d208f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import { createRequire } from 'module'; import solidRefresh from 'solid-refresh/babel'; // TODO use proper path import type { Options as RefreshOptions } from 'solid-refresh/babel'; -import lazyModuleUrl, { LAZY_PLACEHOLDER_PREFIX } from './lazy-module-url.js'; +import lazyModuleUrl, { LAZY_PLACEHOLDER_PREFIX, normalizeLazyModulePath } from './lazy-module-url.js'; import path from 'path'; import type { Alias, AliasOptions, FilterPattern, Plugin } from 'vite'; import { createFilter, version } from 'vite'; @@ -535,7 +535,7 @@ export default function solidPlugin(options: Partial = {}): Plugin { const cleanId = resolved.id.split('?')[0]; resolutions.push({ placeholder: match[0], - resolved: '"' + path.relative(projectRoot, cleanId) + '"', + resolved: '"' + normalizeLazyModulePath(path.relative(projectRoot, cleanId)) + '"', }); } } diff --git a/src/lazy-module-url.ts b/src/lazy-module-url.ts index 34a9fd7..40d95b7 100644 --- a/src/lazy-module-url.ts +++ b/src/lazy-module-url.ts @@ -2,6 +2,18 @@ import type { PluginObj, types as t } from '@babel/core'; export const LAZY_PLACEHOLDER_PREFIX = '__SOLID_LAZY_MODULE__:'; +/** + * Normalize a resolved lazy() module path to a POSIX-style string. + * + * The path is built with `path.relative`, which yields OS-native separators. + * It is then embedded back into JS source as the 2nd argument to `lazy()`, so + * on Windows the backslashes (`"src\components\App.tsx"`) become invalid escape + * sequences that break the parse. Always use forward slashes. + */ +export function normalizeLazyModulePath(relativePath: string): string { + return relativePath.replace(/\\/g, '/'); +} + /** * Detects whether a CallExpression argument is `() => import("specifier")` * and returns the specifier string if so. diff --git a/test/lazy-module-url.test.ts b/test/lazy-module-url.test.ts new file mode 100644 index 0000000..c357c86 --- /dev/null +++ b/test/lazy-module-url.test.ts @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { normalizeLazyModulePath } from '../src/lazy-module-url.ts'; + +// Regression test for the Windows-only bug where `path.relative` returns +// backslash separators that, once embedded as the 2nd arg to `lazy()`, are +// invalid JS escape sequences (`"src\components\App.tsx"`) and break the parse. +test('normalizeLazyModulePath converts Windows backslashes to forward slashes', () => { + assert.equal(normalizeLazyModulePath('src\\components\\App.tsx'), 'src/components/App.tsx'); +}); + +test('normalizeLazyModulePath leaves POSIX paths untouched', () => { + assert.equal(normalizeLazyModulePath('src/components/App.tsx'), 'src/components/App.tsx'); +}); + +test('normalizeLazyModulePath handles mixed separators', () => { + assert.equal(normalizeLazyModulePath('src\\ui/buttons\\Base.tsx'), 'src/ui/buttons/Base.tsx'); +}); + +test('normalizeLazyModulePath leaves a bare filename untouched', () => { + assert.equal(normalizeLazyModulePath('App.tsx'), 'App.tsx'); +});