Skip to content

Commit 5e48c55

Browse files
committed
fix: rewrite #app to nuxt/app in build output
1 parent f844496 commit 5e48c55

2 files changed

Lines changed: 45 additions & 5 deletions

File tree

src/commands/build.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ export default defineCommand({
185185

186186
// Generate types
187187
await writeTypes(ctx.options.outDir, ctx.options.stub)
188+
189+
if (!ctx.options.stub) {
190+
await rewriteRuntimeTypeAliases(resolve(ctx.options.outDir, 'runtime'))
191+
}
188192
},
189193
async 'build:done'(ctx) {
190194
const logs = [...ctx.warnings].filter(l => l.startsWith('Potential missing package.json files:'))
@@ -277,6 +281,34 @@ ${moduleReExports.filter(e => e.type === 'star').map(e => `\nexport * from '${e.
277281
await fsp.writeFile(dtsFile, dtsContents, 'utf8')
278282
}
279283

284+
const DECLARATION_RE = /\.d\.[^/]*ts$/
285+
286+
// `#app`/`#app/*` are build-time-only aliases with no package resolution, so leaking
287+
// them into emitted declarations breaks `tsc`/`attw` for consumers. `nuxt/app` is the
288+
// resolvable public entry and re-exports the `#app/*` symbols.
289+
async function rewriteRuntimeTypeAliases(runtimeDir: string) {
290+
if (!existsSync(runtimeDir)) {
291+
return
292+
}
293+
294+
const entries = await fsp.readdir(runtimeDir, { recursive: true })
295+
await Promise.all(entries.map(async (entry) => {
296+
if (!DECLARATION_RE.test(entry)) {
297+
return
298+
}
299+
300+
const file = join(runtimeDir, entry)
301+
const contents = await fsp.readFile(file, 'utf8')
302+
const rewritten = contents
303+
.replace(/import\("#app(?:\/[^"]*)?"\)/g, 'import("nuxt/app")')
304+
.replace(/(\bfrom\s*)(['"])#app(?:\/[^'"]*)?\2/g, '$1$2nuxt/app$2')
305+
306+
if (rewritten !== contents) {
307+
await fsp.writeFile(file, rewritten, 'utf8')
308+
}
309+
}))
310+
}
311+
280312
async function loadTSCompilerOptions(path: string): Promise<NonNullable<TSConfig['compilerOptions']>> {
281313
const config = await parse(path)
282314
const resolvedCompilerOptions = config?.tsconfig.compilerOptions || {}

test/build.spec.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,7 @@ describe('module builder', () => {
120120

121121
it('should generate typed plugin', async () => {
122122
const pluginDts = await readFile(join(distDir, 'runtime/plugins/plugin.d.ts'), 'utf-8')
123-
// aliases flip between `nuxt/app` and `#app` in repo vs ecosystem-ci
124-
const appAlias = `import\\("(?:nuxt/app|#app)"\\)`
123+
const appAlias = `import\\("nuxt/app"\\)`
125124
expect(pluginDts).toMatch(/^export type SharedTypeFromRuntime = 'shared-type';$/m)
126125
expect(pluginDts).toMatch(
127126
new RegExp(
@@ -131,6 +130,16 @@ describe('module builder', () => {
131130
expect(pluginDts).toMatch(/^export default _default;$/m)
132131
})
133132

133+
it('should not leak build-only virtual aliases into emitted runtime declarations', async () => {
134+
const files = await readdir(runtimeDir, { recursive: true })
135+
const declarations = files.filter(file => /\.d\.[^/]*ts$/.test(file))
136+
expect(declarations.length).toBeGreaterThan(0)
137+
for (const file of declarations) {
138+
const contents = await readFile(join(runtimeDir, file), 'utf-8')
139+
expect(contents, file).not.toMatch(/(["'])#app(?:\/[^"']*)?\1/)
140+
}
141+
})
142+
134143
it('should correctly add extensions to imports from runtime/ directory', async () => {
135144
const moduleDts = await readFile(join(distDir, 'module.d.mts'), 'utf-8')
136145
const runtimeImport = findStaticImports(moduleDts).find(i => i.specifier.includes('runtime'))
@@ -158,16 +167,15 @@ describe('module builder', () => {
158167

159168
it('should generate wrapped composables', async () => {
160169
const componentFile = await readFile(join(distDir, 'runtime/composables/useWrappedFetch.d.ts'), 'utf-8')
161-
// aliases flip between `nuxt/app` and `#app` in repo vs ecosystem-ci
162170
// `null` vs `undefined` error-type difference between Nuxt 3 and 4+ is intentional.
163171
// Nuxt 4+ moved the default error from `FetchError` to `NuxtError<unknown>` (nuxt/nuxt#35346); accept both until it lands in a release.
164172
const expectedErrorType = satisfies(nuxtVersion, '^3') ? 'null' : 'undefined'
165173
const errorType = satisfies(nuxtVersion, '^3')
166174
? 'import\\("ofetch"\\)\\.FetchError<any>'
167-
: '(?:import\\("ofetch"\\)\\.FetchError<any>|import\\("(?:nuxt/app|#app)"\\)\\.NuxtError<unknown>)'
175+
: '(?:import\\("ofetch"\\)\\.FetchError<any>|import\\("nuxt/app"\\)\\.NuxtError<unknown>)'
168176
expect(componentFile).toMatch(
169177
new RegExp(
170-
`^export declare const useWrappedFetch: \\(\\) => import\\("(?:nuxt/app|#app)"\\)\\.AsyncData<unknown, ${errorType} \\| ${expectedErrorType}>;\\s*$`,
178+
`^export declare const useWrappedFetch: \\(\\) => import\\("nuxt/app"\\)\\.AsyncData<unknown, ${errorType} \\| ${expectedErrorType}>;\\s*$`,
171179
),
172180
)
173181
})

0 commit comments

Comments
 (0)