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
5 changes: 5 additions & 0 deletions .changeset/fix-nuxt-typecheck-server-globals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"evlog": patch
---

Fix `nuxt typecheck` failing with `TS2304: Cannot find name 'useLogger'` (and `createEvlogError`) on server routes. `$fetch`'s return-type inference pulls server routes — and their auto-imported globals — into the app tsconfig project's typecheck too, but the Nuxt module only declared these globals for the server project. `useLogger` and `createEvlogError` are now declared for both projects; the server-only `log` export stays scoped to the server project since it shares its global name with the (differently-typed) client `log`.
29 changes: 28 additions & 1 deletion packages/evlog/src/nuxt/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,11 +486,38 @@ export {}
filename: 'types/evlog-server.d.ts',
getContents: () => `declare global {
const useLogger: typeof import('evlog').useLogger
const log: typeof import('evlog').log
const createEvlogError: typeof import('evlog').createEvlogError
}
export {}
`,
// `nitro: true` puts the reference on the server tsconfig project.
// `nuxt: true` also puts it on the app tsconfig project — required
// because `$fetch` typings resolve server route return types by
// importing the route modules directly, which pulls server routes
// (and therefore these globals) into the app project's typecheck too.
// `createEvlogError` is safe to expose in both contexts: it resolves
// to the same `evlog` export as the one declared in evlog-client.d.ts.
// `log` is intentionally NOT declared here — the server `log` (a
// module-level logger for background/non-request code) and the
// client `log` (declared in evlog-client.d.ts) are different values
// with different types but share the same global name, so it can
// only be safely typed within the project that actually needs it.
}, { nitro: true, nuxt: true })
Comment thread
coderabbitai[bot] marked this conversation as resolved.

addTypeTemplate({
filename: 'types/evlog-server-log.d.ts',
getContents: () => `declare global {
const log: typeof import('evlog').log
}
export {}
`,
// Nitro-only: the app tsconfig project already declares a (different)
// global `log` via evlog-client.d.ts. Referencing this template there
// too would make the two ambient declarations fight over the same
// name — since \`skipLibCheck\` is on by default in Nuxt's generated
// tsconfigs, TypeScript silently keeps whichever declaration it sees
// first instead of erroring, so the losing side's shape goes unused
// without warning.
}, { nitro: true })

const stripLevels = options.strip ?? ['debug']
Expand Down
114 changes: 114 additions & 0 deletions packages/evlog/test/nuxt/module-type-templates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

// Regression test for https://github.com/HugoRCD/evlog/issues/435
//
// `nuxt typecheck` (vue-tsc -b) type-checks the app tsconfig project *and*
// the server one. `$fetch`'s return-type inference does
// `typeof import('../../server/api/...')`, which pulls every server route
// (and therefore the auto-imported server globals it uses) into the app
// project's typecheck too. `useLogger` and `createEvlogError` must
// therefore be declared on both the `nitro` context (server tsconfig) and
// the `nuxt` context (app tsconfig) — declaring them on `nitro` alone
// leaves the app project unable to resolve them, causing a
// `TS2304: Cannot find name` failure.
//
// `log` is the one exception: it is declared globally by *both* the
// client type template (`typeof import('evlog/client').log`) and the
// server one (`typeof import('evlog').log`) — two different types sharing
// one global name. The app tsconfig project already declares the client
// `log` (evlog-client.d.ts, nuxt-only). If the server `log` were also
// exposed there, the two ambient declarations would collide. Because
// Nuxt's generated tsconfigs set `skipLibCheck: true`, TypeScript doesn't
// error on that collision — it silently keeps whichever declaration it
// resolves first, so the server `log` template must stay nitro-only.

const addTypeTemplate = vi.fn()
const addImports = vi.fn()
const addServerImports = vi.fn()
const addServerHandler = vi.fn()
const addServerPlugin = vi.fn()
const addPlugin = vi.fn()
const addVitePlugin = vi.fn()
const createResolver = vi.fn(() => ({ resolve: (path: string) => path }))

vi.mock('@nuxt/kit', () => ({
addImports,
addPlugin,
addServerHandler,
addServerImports,
addServerPlugin,
addTypeTemplate,
addVitePlugin,
createResolver,
defineNuxtModule: (definition: { setup: (options: unknown, nuxt: unknown) => unknown }) => definition,
}))

function makeFakeNuxt() {
return {
hook: vi.fn(),
options: {
dev: false,
runtimeConfig: {
public: {},
},
},
}
}

function findTemplateCall(filename: string) {
return addTypeTemplate.mock.calls.find(([template]) => template?.filename === filename)
}

describe('nuxt module server type templates', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('registers useLogger/createEvlogError for both the nitro and the app (nuxt) tsconfig projects', async () => {
const moduleDefinition = (await import('../../src/nuxt/module')).default as unknown as {
setup: (options: Record<string, unknown>, nuxt: ReturnType<typeof makeFakeNuxt>) => void
}

moduleDefinition.setup({}, makeFakeNuxt())

const serverTemplateCall = findTemplateCall('types/evlog-server.d.ts')
expect(serverTemplateCall).toBeDefined()

const [template, context] = serverTemplateCall!
// `nitro: true` alone only reaches `.nuxt/tsconfig.server.json` — the app
// project (`.nuxt/tsconfig.app.json`) never sees the declaration and
// `useLogger`/`createEvlogError` resolve to TS2304 there.
expect(context).toEqual({ nitro: true, nuxt: true })

const contents = template.getContents()
expect(contents).toContain('const useLogger: typeof import(\'evlog\').useLogger')
expect(contents).toContain('const createEvlogError: typeof import(\'evlog\').createEvlogError')
// `log` must NOT be declared here — see module comment above.
expect(contents).not.toContain('const log:')
})

it('keeps the server-scoped `log` global nitro-only, separate from the client `log`', async () => {
const moduleDefinition = (await import('../../src/nuxt/module')).default as unknown as {
setup: (options: Record<string, unknown>, nuxt: ReturnType<typeof makeFakeNuxt>) => void
}

moduleDefinition.setup({}, makeFakeNuxt())

const serverLogTemplateCall = findTemplateCall('types/evlog-server-log.d.ts')
expect(serverLogTemplateCall).toBeDefined()

const [template, context] = serverLogTemplateCall!
// Must stay nitro-only: adding `nuxt: true` would make this collide
// with the client `log` declared in evlog-client.d.ts (already
// registered nuxt-only, by default) in the app tsconfig project.
expect(context).toEqual({ nitro: true })
expect(template.getContents()).toContain('const log: typeof import(\'evlog\').log')

const clientTemplateCall = findTemplateCall('types/evlog-client.d.ts')
expect(clientTemplateCall).toBeDefined()
const [clientTemplate, clientContext] = clientTemplateCall!
// No explicit context passed → nuxt-only by @nuxt/kit's default.
expect(clientContext).toBeUndefined()
expect(clientTemplate.getContents()).toContain('const log: typeof import(\'evlog/client\').log')
})
})
Loading