Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/fix-windows-nitro-module-paths.md
Original file line number Diff line number Diff line change
@@ -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).
18 changes: 13 additions & 5 deletions packages/evlog/src/nitro-v3/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,37 @@ 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) {
nitro.options.noExternals = ['evlog']
} 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
Expand Down
12 changes: 10 additions & 2 deletions packages/evlog/src/nitro/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions packages/evlog/test/nitro/module-paths.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>,
},
}
}
Comment on lines +13 to +22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Move makeNitroStub() into test/helpers/ and import it here.

This test re-implements a helper inline, which violates the test helper policy and makes reuse harder across Nitro tests.

As per coding guidelines, "Always import real source helpers in tests, never re-implement them in tests." and "Use the helpers in test/helpers/ (drain spies, fake timers, fetch mock, framework matrix) in tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/evlog/test/nitro/module-paths.test.ts` around lines 13 - 22, The
test currently reimplements the helper makeNitroStub; move that helper into the
shared test/helpers directory as a named export (e.g., export function
makeNitroStub()) and update this test to import { makeNitroStub } from the
test/helpers module instead of defining it inline; ensure the exported helper
returns the same shape (options.plugins, options.errorHandler,
options.noExternals, options.runtimeConfig) so existing tests using
makeNitroStub() continue to work and update any other tests to import the shared
helper rather than reimplementing it.


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$/)
})
})
Loading