Skip to content

Commit c685f43

Browse files
committed
fix: broken type augmenting
Fixes #570
1 parent 3b4e676 commit c685f43

File tree

7 files changed

+155
-44
lines changed

7 files changed

+155
-44
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"lint": "eslint .",
5555
"lint:fix": "eslint . --fix",
5656
"test": "pnpm dev:prepare && vitest --run",
57-
"test:types": "echo 'broken due to type regeneration, use pnpm typecheck' && npx nuxi typecheck"
57+
"test:types": "vitest run --project typecheck"
5858
},
5959
"build": {
6060
"externals": [

src/module.ts

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import {
22
addBuildPlugin,
33
addComponentsDir,
44
addImports,
5-
addPluginTemplate, addTemplate, addTypeTemplate,
5+
addPluginTemplate,
6+
addTemplate,
67
createResolver,
78
defineNuxtModule,
89
hasNuxtModule,
@@ -24,8 +25,7 @@ import type {
2425
RegistryScripts,
2526
} from './runtime/types'
2627
import { NuxtScriptsCheckScripts } from './plugins/check-scripts'
27-
import { templatePlugin, templateTriggerResolver } from './templates'
28-
import { relative, resolve } from 'pathe'
28+
import { registerTypeTemplates, templatePlugin, templateTriggerResolver } from './templates'
2929

3030
export interface ModuleOptions {
3131
/**
@@ -201,42 +201,7 @@ export default defineNuxtModule<ModuleOptions>({
201201
const registryScriptsWithImport = registryScripts.filter(i => !!i.import?.name) as Required<RegistryScript>[]
202202
const newScripts = registryScriptsWithImport.filter(i => !scripts.some(r => r.import?.name === i.import.name))
203203

204-
addTypeTemplate({
205-
filename: 'module/nuxt-scripts.d.ts',
206-
getContents: (data) => {
207-
const typesPath = relative(resolve(data.nuxt!.options.rootDir, data.nuxt!.options.buildDir, 'module'), resolve('runtime/types'))
208-
let types = `
209-
declare module '#app' {
210-
interface NuxtApp {
211-
$scripts: Record<${[...Object.keys(config.globals || {}), ...Object.keys(config.registry || {})].map(k => `'${k}'`).concat(['string']).join(' | ')}, (import('#nuxt-scripts/types').UseScriptContext<any>)>
212-
_scripts: Record<string, (import('#nuxt-scripts/types').UseScriptContext<any>)>
213-
}
214-
interface RuntimeNuxtHooks {
215-
'scripts:updated': (ctx: { scripts: Record<string, (import('#nuxt-scripts/types').UseScriptContext<any>)> }) => void | Promise<void>
216-
}
217-
}
218-
`
219-
if (newScripts.length) {
220-
types = `${types}
221-
declare module '#nuxt-scripts/types' {
222-
type NuxtUseScriptOptions = Omit<import('${typesPath}').NuxtUseScriptOptions, 'use' | 'beforeInit'>
223-
interface ScriptRegistry {
224-
${newScripts.map((i) => {
225-
const key = i.import?.name.replace('useScript', '')
226-
const keyLcFirst = key.substring(0, 1).toLowerCase() + key.substring(1)
227-
return ` ${keyLcFirst}?: import('${i.import?.from}').${key}Input | [import('${i.import?.from}').${key}Input, NuxtUseScriptOptions]`
228-
}).join('\n')}
229-
}
230-
}`
231-
return types
232-
}
233-
return `${types}
234-
export {}`
235-
},
236-
}, {
237-
nuxt: true,
238-
node: true,
239-
})
204+
registerTypeTemplates({ nuxt, config, newScripts })
240205

241206
if (Object.keys(config.globals || {}).length || Object.keys(config.registry || {}).length) {
242207
// create a virtual plugin

src/templates.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,60 @@
11
import { hash } from 'ohash'
2+
import { relative, resolve } from 'pathe'
3+
import { addTypeTemplate } from '@nuxt/kit'
4+
import type { Nuxt } from '@nuxt/schema'
25
import type { ModuleOptions } from './module'
36
import { logger } from './logger'
47
import type { RegistryScript } from '#nuxt-scripts/types'
58

9+
interface TypeTemplateContext {
10+
nuxt: Nuxt
11+
config: ModuleOptions
12+
newScripts: Required<RegistryScript>[]
13+
}
14+
15+
export function registerTypeTemplates({ nuxt, config, newScripts }: TypeTemplateContext) {
16+
// Type augmentations for existing modules (#app, #nuxt-scripts/types)
17+
addTypeTemplate({
18+
filename: 'types/nuxt-scripts-augments.d.ts',
19+
getContents: () => {
20+
const typesPath = relative(
21+
resolve(nuxt.options.rootDir, nuxt.options.buildDir, 'types'),
22+
resolve('runtime/types'),
23+
)
24+
25+
let augments = `// Generated by @nuxt/scripts
26+
declare module '#app' {
27+
interface NuxtApp {
28+
$scripts: Record<${[...Object.keys(config.globals || {}), ...Object.keys(config.registry || {})].map(k => `'${k}'`).concat(['string']).join(' | ')}, import('#nuxt-scripts/types').UseScriptContext<any>>
29+
_scripts: Record<string, import('#nuxt-scripts/types').UseScriptContext<any>>
30+
}
31+
interface RuntimeNuxtHooks {
32+
'scripts:updated': (ctx: { scripts: Record<string, import('#nuxt-scripts/types').UseScriptContext<any>> }) => void | Promise<void>
33+
}
34+
}
35+
`
36+
37+
if (newScripts.length) {
38+
augments += `
39+
declare module '#nuxt-scripts/types' {
40+
type _NuxtScriptOptions = Omit<import('${typesPath}').NuxtUseScriptOptions, 'use' | 'beforeInit'>
41+
interface ScriptRegistry {
42+
${newScripts.map((i) => {
43+
const key = i.import.name.replace('useScript', '')
44+
const keyLcFirst = key.substring(0, 1).toLowerCase() + key.substring(1)
45+
return ` ${keyLcFirst}?: import('${i.import.from}').${key}Input | [import('${i.import.from}').${key}Input, _NuxtScriptOptions]`
46+
}).join('\n')}
47+
}
48+
}
49+
`
50+
}
51+
52+
return `${augments}
53+
export {}`
54+
},
55+
}, { nuxt: true })
56+
}
57+
658
export function templateTriggerResolver(defaultScriptOptions?: ModuleOptions['defaultScriptOptions']) {
759
const needsIdleTimeout = defaultScriptOptions?.trigger && typeof defaultScriptOptions.trigger === 'object' && 'idleTimeout' in defaultScriptOptions.trigger
860
const needsInteraction = defaultScriptOptions?.trigger && typeof defaultScriptOptions.trigger === 'object' && 'interaction' in defaultScriptOptions.trigger

test/fixtures/extend-registry/scripts/my-custom-script.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { useRegistryScript } from '#nuxt-scripts/utils'
22

3+
declare global {
4+
interface Window {
5+
myScript: () => void
6+
}
7+
}
8+
39
export interface CustomApi {
410
myScript: () => void
511
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expectTypeOf, it } from 'vitest'
2+
3+
// Import from the generated augments file to test it's a valid module
4+
// This tests the fix for issue #570 - the file must have `export {}`
5+
// to be treated as a module, otherwise the augmentations break base exports
6+
import type {} from '../../test/fixtures/extend-registry/.nuxt/types/nuxt-scripts-augments'
7+
8+
// Import base types to verify they're still accessible after augmentation
9+
import type {
10+
NuxtUseScriptOptions,
11+
ScriptRegistry,
12+
UseScriptContext,
13+
} from '../../src/runtime/types'
14+
15+
// Issue #570: When adding a new script to registry via hooks, the generated
16+
// nuxt-scripts-augments.d.ts was missing `export {}`, causing TypeScript to
17+
// treat it as a script instead of a module. This broke imports from
18+
// #nuxt-scripts/types because the module augmentation interfered with
19+
// the original module's exports.
20+
describe('extend-registry fixture types (issue #570)', () => {
21+
it('base types remain accessible after augmentation', () => {
22+
// The core issue in #570 was that after augmentation, the base exports
23+
// from #nuxt-scripts/types became inaccessible
24+
expectTypeOf<UseScriptContext<{ foo: string }>>().not.toBeAny()
25+
expectTypeOf<NuxtUseScriptOptions>().not.toBeAny()
26+
expectTypeOf<ScriptRegistry>().not.toBeAny()
27+
})
28+
29+
it('ScriptRegistry maintains known properties', () => {
30+
// Ensure base registry entries still work
31+
expectTypeOf<ScriptRegistry>().toHaveProperty('googleAnalytics')
32+
expectTypeOf<ScriptRegistry>().toHaveProperty('clarity')
33+
})
34+
})

test/types/types.test-d.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,50 @@
11
import { describe, expectTypeOf, it } from 'vitest'
22
import type { ModuleOptions } from '../../src/module'
3-
import type { ScriptRegistry } from '../../src/runtime/types'
3+
import type {
4+
NuxtUseScriptOptions,
5+
RegistryScriptInput,
6+
ScriptRegistry,
7+
UseScriptContext,
8+
} from '../../src/runtime/types'
49

5-
describe('module options registry', async () => {
6-
it('expect no any', async () => {
7-
expectTypeOf<NonNullable<NonNullable<ModuleOptions>['registry']>[keyof ScriptRegistry]>().not.toBeAny()
10+
describe('module options registry', () => {
11+
it('registry entries are typed', () => {
12+
// Check specific registry keys have proper types (not any)
13+
// Using specific keys because the index signature `[key: \`${string}-npm\`]`
14+
// causes `keyof ScriptRegistry` to include template literals which resolve to any
15+
type Registry = NonNullable<ModuleOptions['registry']>
16+
expectTypeOf<Registry['googleAnalytics']>().not.toBeAny()
17+
expectTypeOf<Registry['clarity']>().not.toBeAny()
18+
expectTypeOf<Registry['stripe']>().not.toBeAny()
19+
})
20+
})
21+
22+
// Issue #570: Verify #nuxt-scripts/types exports are available
23+
// When adding new scripts to registry, the type definitions were broken
24+
// because `export {}` was missing from the generated .d.ts file
25+
describe('#nuxt-scripts/types exports', () => {
26+
it('exports UseScriptContext type', () => {
27+
expectTypeOf<UseScriptContext<{ foo: string }>>().not.toBeAny()
28+
expectTypeOf<UseScriptContext<{ foo: string }>>().toBeObject()
29+
})
30+
31+
it('exports NuxtUseScriptOptions type', () => {
32+
expectTypeOf<NuxtUseScriptOptions>().not.toBeAny()
33+
expectTypeOf<NuxtUseScriptOptions>().toBeObject()
34+
})
35+
36+
it('exports RegistryScriptInput type', () => {
37+
expectTypeOf<RegistryScriptInput>().not.toBeAny()
38+
})
39+
40+
it('exports ScriptRegistry interface', () => {
41+
expectTypeOf<ScriptRegistry>().not.toBeAny()
42+
expectTypeOf<ScriptRegistry>().toBeObject()
43+
})
44+
45+
it('ScriptRegistry has known keys', () => {
46+
expectTypeOf<ScriptRegistry>().toHaveProperty('googleAnalytics')
47+
expectTypeOf<ScriptRegistry>().toHaveProperty('googleTagManager')
48+
expectTypeOf<ScriptRegistry>().toHaveProperty('clarity')
849
})
950
})

vitest.config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ import { defineConfig, defineProject } from 'vitest/config'
44
export default defineConfig({
55
test: {
66
projects: [
7+
// type tests using vitest typecheck
8+
defineProject({
9+
test: {
10+
name: 'typecheck',
11+
include: [
12+
'./test/types/**/*.test-d.ts',
13+
],
14+
typecheck: {
15+
enabled: true,
16+
include: ['./test/types/**/*.test-d.ts'],
17+
},
18+
},
19+
}),
720
// utils folders as *.test.ts in either test/unit or in src/**/*.test.ts
821
defineProject({
922
test: {

0 commit comments

Comments
 (0)