Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/fix-lazy-module-url-windows-backslash.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 23 additions & 0 deletions scripts/test-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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}`;
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -535,7 +535,7 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin {
const cleanId = resolved.id.split('?')[0];
resolutions.push({
placeholder: match[0],
resolved: '"' + path.relative(projectRoot, cleanId) + '"',
resolved: '"' + normalizeLazyModulePath(path.relative(projectRoot, cleanId)) + '"',
});
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/lazy-module-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions test/lazy-module-url.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});