diff --git a/.changeset/fix-windows-nitro-module-paths.md b/.changeset/fix-windows-nitro-module-paths.md new file mode 100644 index 00000000..3b242259 --- /dev/null +++ b/.changeset/fix-windows-nitro-module-paths.md @@ -0,0 +1,7 @@ +--- +'evlog': patch +--- + +Fix `evlog/nitro` and `evlog/nitro/v3` on Windows. The module passed native `path.resolve()` paths to `nitro.options.plugins` and `nitro.options.errorHandler`. Nitro raw-interpolates those into the `#nitro/virtual/plugins` and `#nitro/virtual/error-handler` JS string literals, so Windows backslashes were parsed as escape sequences (`\n`, `\v`, …) and broke module resolution — surfacing as `Cannot find module … imported from '#nitro/virtual/error-handler'`. Paths are now normalized to forward slashes before being handed to Nitro. + +Closes [#345](https://github.com/HugoRCD/evlog/issues/345). diff --git a/packages/evlog/src/nitro-v3/module.ts b/packages/evlog/src/nitro-v3/module.ts index 5f311b3e..1d867f4f 100644 --- a/packages/evlog/src/nitro-v3/module.ts +++ b/packages/evlog/src/nitro-v3/module.ts @@ -7,13 +7,21 @@ export type { NitroModuleOptions } const _dir = dirname(fileURLToPath(import.meta.url)) +// Nitro raw-interpolates these paths into JS string literals when generating +// the #nitro/virtual/plugins and #nitro/virtual/error-handler modules, so +// Windows backslashes would be parsed as escape sequences (\n, \v, …) and +// break module resolution. Normalize to POSIX separators. +function resolveModulePath(name: string): string { + return resolve(_dir, name).replace(/\\/g, '/') +} + export default function evlog(options?: NitroModuleOptions) { return { name: 'evlog', setup(nitro: Nitro) { // Push the plugin (no extension — Nitro's bundler resolves it) nitro.options.plugins = nitro.options.plugins || [] - nitro.options.plugins.push(resolve(_dir, 'plugin')) + nitro.options.plugins.push(resolveModulePath('plugin')) // explicitly tell nitro to bundle evlog's files to correctly resolve nitro dependencies if (!nitro.options.noExternals) { @@ -21,15 +29,15 @@ export default function evlog(options?: NitroModuleOptions) { } else if (Array.isArray(nitro.options.noExternals)) { nitro.options.noExternals.push('evlog') } - + // Set error handler only if not already configured by user if (!nitro.options.errorHandler) { - nitro.options.errorHandler = [resolve(_dir, 'errorHandler')] + nitro.options.errorHandler = [resolveModulePath('errorHandler')] } else if (Array.isArray(nitro.options.errorHandler)) { - nitro.options.errorHandler.unshift(resolve(_dir, 'errorHandler')) + nitro.options.errorHandler.unshift(resolveModulePath('errorHandler')) } else if (typeof nitro.options.errorHandler === 'string') { - nitro.options.errorHandler = [resolve(_dir, 'errorHandler'), nitro.options.errorHandler] + nitro.options.errorHandler = [resolveModulePath('errorHandler'), nitro.options.errorHandler] } // Inject config into runtimeConfig — works in production where the diff --git a/packages/evlog/src/nitro/module.ts b/packages/evlog/src/nitro/module.ts index fb99b224..524bb699 100644 --- a/packages/evlog/src/nitro/module.ts +++ b/packages/evlog/src/nitro/module.ts @@ -7,17 +7,25 @@ export type { NitroModuleOptions } const _dir = dirname(fileURLToPath(import.meta.url)) +// Nitro raw-interpolates these paths into JS string literals when generating +// the #nitro/virtual/plugins and #nitro/virtual/error-handler modules, so +// Windows backslashes would be parsed as escape sequences (\n, \v, …) and +// break module resolution. Normalize to POSIX separators. +function resolveModulePath(name: string): string { + return resolve(_dir, name).replace(/\\/g, '/') +} + export default function evlog(options?: NitroModuleOptions) { return { name: 'evlog', setup(nitro: Nitro) { // Push the plugin (no extension — Nitro's bundler resolves it) nitro.options.plugins = nitro.options.plugins || [] - nitro.options.plugins.push(resolve(_dir, 'plugin')) + nitro.options.plugins.push(resolveModulePath('plugin')) // Set error handler only if not already configured by user if (!nitro.options.errorHandler) { - nitro.options.errorHandler = resolve(_dir, 'errorHandler') + nitro.options.errorHandler = resolveModulePath('errorHandler') } // explicitly tell nitro to bundle evlog's files to correctly resolve nitro dependencies diff --git a/packages/evlog/test/nitro/module-paths.test.ts b/packages/evlog/test/nitro/module-paths.test.ts new file mode 100644 index 00000000..507c3320 --- /dev/null +++ b/packages/evlog/test/nitro/module-paths.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest' +import nitroV2Module from '../../src/nitro/module' +import nitroV3Module from '../../src/nitro-v3/module' + +// Nitro builds its #nitro/virtual/plugins and #nitro/virtual/error-handler +// virtual modules by raw-interpolating these paths into JS string literals: +// import _abc from "${plugin}" +// import errorHandler$0 from "${h}" +// On Windows, path.resolve() returns paths with backslashes (e.g. C:\…\plugin). +// Those backslashes would then be parsed as JS escape sequences (\n, \v, …), +// silently corrupting the specifier. Regression test for evlog#345. +describe('nitro modules avoid backslash paths', () => { + function makeNitroStub() { + return { + options: { + plugins: [] as string[], + errorHandler: undefined as string | string[] | undefined, + noExternals: undefined as undefined | true | string[], + runtimeConfig: {} as Record, + }, + } + } + + it('nitro v2 module pushes POSIX-style plugin and errorHandler paths', () => { + const nitro = makeNitroStub() + nitroV2Module({ env: { service: 'test' } }).setup(nitro as never) + + expect(nitro.options.plugins).toHaveLength(1) + expect(nitro.options.plugins[0]).not.toMatch(/\\/) + expect(nitro.options.plugins[0]).toMatch(/\/nitro\/plugin$/) + + expect(nitro.options.errorHandler).toBeTypeOf('string') + expect(nitro.options.errorHandler).not.toMatch(/\\/) + expect(nitro.options.errorHandler).toMatch(/\/nitro\/errorHandler$/) + }) + + it('nitro v3 module pushes POSIX-style plugin and errorHandler paths', () => { + const nitro = makeNitroStub() + nitroV3Module({ env: { service: 'test' } }).setup(nitro as never) + + expect(nitro.options.plugins).toHaveLength(1) + expect(nitro.options.plugins[0]).not.toMatch(/\\/) + expect(nitro.options.plugins[0]).toMatch(/\/nitro-v3\/plugin$/) + + expect(Array.isArray(nitro.options.errorHandler)).toBe(true) + const handlers = nitro.options.errorHandler as string[] + expect(handlers).toHaveLength(1) + expect(handlers[0]).not.toMatch(/\\/) + expect(handlers[0]).toMatch(/\/nitro-v3\/errorHandler$/) + }) +})