diff --git a/packages/kit/src/internal/cjs.ts b/packages/kit/src/internal/cjs.ts index fe57f953084b..d15f4cf57439 100644 --- a/packages/kit/src/internal/cjs.ts +++ b/packages/kit/src/internal/cjs.ts @@ -60,15 +60,19 @@ function getRequireCacheItem (id: string) { } } +export function getModulePaths (paths?: string[] | string) { + return ([] as Array).concat( + global.__NUXT_PREPATHS__, + paths || [], + process.cwd(), + global.__NUXT_PATHS__ + ).filter(Boolean) as string[] +} + /** @deprecated Do not use CJS utils */ export function resolveModule (id: string, opts: ResolveModuleOptions = {}) { return normalize(_require.resolve(id, { - paths: ([] as Array).concat( - global.__NUXT_PREPATHS__, - opts.paths || [], - process.cwd(), - global.__NUXT_PATHS__ - ).filter(Boolean) as string[] + paths: getModulePaths(opts.paths) })) } diff --git a/packages/kit/src/template.ts b/packages/kit/src/template.ts index bce72cfd791c..9de92087c984 100644 --- a/packages/kit/src/template.ts +++ b/packages/kit/src/template.ts @@ -1,8 +1,14 @@ -import { existsSync } from 'node:fs' -import { basename, parse, resolve } from 'pathe' +import { existsSync, promises as fsp } from 'node:fs' +import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe' import hash from 'hash-sum' -import type { NuxtTemplate, ResolvedNuxtTemplate } from '@nuxt/schema' +import type { Nuxt, NuxtTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema' +import { withTrailingSlash } from 'ufo' +import { defu } from 'defu' +import type { TSConfig } from 'pkg-types' +import { readPackageJSON } from 'pkg-types' + import { tryUseNuxt, useNuxt } from './context' +import { getModulePaths } from './internal/cjs' /** * Renders given template using lodash template during build into the project buildDir @@ -101,3 +107,142 @@ export function normalizeTemplate (template: NuxtTemplate | string): Resolv export async function updateTemplates (options?: { filter?: (template: ResolvedNuxtTemplate) => boolean }) { return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options) } +export async function writeTypes (nuxt: Nuxt) { + const modulePaths = getModulePaths(nuxt.options.modulesDir) + + const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir) + + const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, { + compilerOptions: { + forceConsistentCasingInFileNames: true, + jsx: 'preserve', + target: 'ESNext', + module: 'ESNext', + moduleResolution: nuxt.options.experimental?.typescriptBundlerResolution ? 'Bundler' : 'Node', + skipLibCheck: true, + strict: nuxt.options.typescript?.strict ?? true, + allowJs: true, + // TODO: remove by default in 3.7 + baseUrl: nuxt.options.srcDir, + noEmit: true, + resolveJsonModule: true, + allowSyntheticDefaultImports: true, + types: ['node'], + paths: {} + }, + include: [ + './nuxt.d.ts', + join(relative(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'), + ...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [], + ...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd) + .filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules')) + .map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')), + ...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : [] + ], + exclude: [ + ...nuxt.options.modulesDir.map(m => relative(nuxt.options.buildDir, m)), + // nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186 + relative(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist')) + ] + } satisfies TSConfig) + + const aliases: Record = { + ...nuxt.options.alias, + '#build': nuxt.options.buildDir + } + + // Exclude bridge alias types to support Volar + const excludedAlias = [/^@vue\/.*$/] + + const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir + + tsConfig.compilerOptions = tsConfig.compilerOptions || {} + tsConfig.include = tsConfig.include || [] + + for (const alias in aliases) { + if (excludedAlias.some(re => re.test(alias))) { + continue + } + const absolutePath = resolve(basePath, aliases[alias]) + const relativePath = relative(nuxt.options.buildDir, absolutePath) + + const stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */) + if (stats?.isDirectory()) { + tsConfig.compilerOptions.paths[alias] = [absolutePath] + tsConfig.compilerOptions.paths[`${alias}/*`] = [`${absolutePath}/*`] + + if (!absolutePath.startsWith(rootDirWithSlash)) { + tsConfig.include.push(relativePath) + } + } else { + const path = stats?.isFile() + ? absolutePath.replace(/(?<=\w)\.\w+$/g, '') /* remove extension */ + : absolutePath + + tsConfig.compilerOptions.paths[alias] = [path] + + if (!absolutePath.startsWith(rootDirWithSlash)) { + tsConfig.include.push(path) + } + } + } + + const references: TSReference[] = await Promise.all([ + ...nuxt.options.modules, + ...nuxt.options._modules + ] + .filter(f => typeof f === 'string') + .map(async id => ({ types: (await readPackageJSON(id, { url: modulePaths }).catch(() => null))?.name || id }))) + + if (nuxt.options.experimental?.reactivityTransform) { + references.push({ types: 'vue/macros-global' }) + } + + const declarations: string[] = [] + + tsConfig.include = [...new Set(tsConfig.include)] + tsConfig.exclude = [...new Set(tsConfig.exclude)] + + await nuxt.callHook('prepare:types', { references, declarations, tsConfig }) + + const declaration = [ + ...references.map((ref) => { + if ('path' in ref && isAbsolute(ref.path)) { + ref.path = relative(nuxt.options.buildDir, ref.path) + } + return `/// ` + }), + ...declarations, + '', + 'export {}', + '' + ].join('\n') + + async function writeFile () { + const GeneratedBy = '// Generated by nuxi' + + const tsConfigPath = resolve(nuxt.options.buildDir, 'tsconfig.json') + await fsp.mkdir(nuxt.options.buildDir, { recursive: true }) + await fsp.writeFile(tsConfigPath, GeneratedBy + '\n' + JSON.stringify(tsConfig, null, 2)) + + const declarationPath = resolve(nuxt.options.buildDir, 'nuxt.d.ts') + await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration) + } + + // This is needed for Nuxt 2 which clears the build directory again before building + // https://github.com/nuxt/nuxt/blob/2.x/packages/builder/src/builder.js#L144 + // @ts-expect-error TODO: Nuxt 2 hook + const unsub = nuxt.hook('builder:prepared', writeFile) + + await writeFile() + + unsub() +} + +function renderAttrs (obj: Record) { + return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ') +} + +function renderAttr (key: string, value: string) { + return value ? `${key}="${value}"` : '' +} diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index f4a9793b19ca..e87a22f54635 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -1,6 +1,8 @@ import { relative, resolve } from 'pathe' import { consola } from 'consola' -import { writeTypes } from '../utils/prepare' + +// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7` +import { writeTypes as writeTypesLegacy } from '../../../kit/src/template' import { loadKit } from '../utils/kit' import { clearBuildDir } from '../utils/fs' import { overrideEnv } from '../utils/env' @@ -19,7 +21,7 @@ export default defineNuxtCommand({ const rootDir = resolve(args._[0] || '.') showVersions(rootDir) - const { loadNuxt, buildNuxt, useNitro } = await loadKit(rootDir) + const { loadNuxt, buildNuxt, useNitro, writeTypes = writeTypesLegacy } = await loadKit(rootDir) const nuxt = await loadNuxt({ rootDir, diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index 9da83dc68541..d979872066fb 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -7,8 +7,10 @@ import type { Nuxt } from '@nuxt/schema' import { consola } from 'consola' import { withTrailingSlash } from 'ufo' import { setupDotenv } from 'c12' + +// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7` +import { writeTypes as writeTypesLegacy } from '../../../kit/src/template' import { showBanner, showVersions } from '../utils/banner' -import { writeTypes } from '../utils/prepare' import { loadKit } from '../utils/kit' import { importModule } from '../utils/esm' import { overrideEnv } from '../utils/env' @@ -30,7 +32,7 @@ export default defineNuxtCommand({ await setupDotenv({ cwd: rootDir, fileName: args.dotenv }) - const { loadNuxt, loadNuxtConfig, buildNuxt } = await loadKit(rootDir) + const { loadNuxt, loadNuxtConfig, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir) const config = await loadNuxtConfig({ cwd: rootDir, diff --git a/packages/nuxi/src/commands/prepare.ts b/packages/nuxi/src/commands/prepare.ts index 026df3899332..dfec71e16e76 100644 --- a/packages/nuxi/src/commands/prepare.ts +++ b/packages/nuxi/src/commands/prepare.ts @@ -1,8 +1,10 @@ import { relative, resolve } from 'pathe' import { consola } from 'consola' + +// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7` +import { writeTypes as writeTypesLegacy } from '../../../kit/src/template' import { clearBuildDir } from '../utils/fs' import { loadKit } from '../utils/kit' -import { writeTypes } from '../utils/prepare' import { defineNuxtCommand } from './index' export default defineNuxtCommand({ @@ -15,7 +17,7 @@ export default defineNuxtCommand({ process.env.NODE_ENV = process.env.NODE_ENV || 'production' const rootDir = resolve(args._[0] || '.') - const { loadNuxt, buildNuxt } = await loadKit(rootDir) + const { loadNuxt, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir) const nuxt = await loadNuxt({ rootDir, overrides: { diff --git a/packages/nuxi/src/commands/typecheck.ts b/packages/nuxi/src/commands/typecheck.ts index aa21e7d1e175..ee5a28ab56e5 100644 --- a/packages/nuxi/src/commands/typecheck.ts +++ b/packages/nuxi/src/commands/typecheck.ts @@ -1,9 +1,10 @@ import { execa } from 'execa' import { resolve } from 'pathe' -import { tryResolveModule } from '../utils/esm' +// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7` +import { writeTypes as writeTypesLegacy } from '../../../kit/src/template' +import { tryResolveModule } from '../utils/esm' import { loadKit } from '../utils/kit' -import { writeTypes } from '../utils/prepare' import { defineNuxtCommand } from './index' export default defineNuxtCommand({ @@ -16,7 +17,7 @@ export default defineNuxtCommand({ process.env.NODE_ENV = process.env.NODE_ENV || 'production' const rootDir = resolve(args._[0] || '.') - const { loadNuxt, buildNuxt } = await loadKit(rootDir) + const { loadNuxt, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir) const nuxt = await loadNuxt({ rootDir, overrides: { diff --git a/packages/nuxi/src/utils/cjs.ts b/packages/nuxi/src/utils/cjs.ts index a8fefc11df22..1f9c1125a073 100644 --- a/packages/nuxi/src/utils/cjs.ts +++ b/packages/nuxi/src/utils/cjs.ts @@ -1,7 +1,7 @@ import { createRequire } from 'node:module' -import { dirname, normalize } from 'pathe' +import { normalize } from 'pathe' -export function getModulePaths (paths?: string | string[]): string[] { +function getModulePaths (paths?: string | string[]): string[] { return ([] as Array) .concat( global.__NUXT_PREPATHS__, @@ -25,11 +25,3 @@ function requireModule (id: string, paths?: string | string[]) { export function tryRequireModule (id: string, paths?: string | string[]) { try { return requireModule(id, paths) } catch { return null } } - -export function getNearestPackage (id: string, paths?: string | string[]) { - while (dirname(id) !== id) { - try { return requireModule(id + '/package.json', paths) } catch {} - id = dirname(id) - } - return null -} diff --git a/packages/nuxi/src/utils/prepare.ts b/packages/nuxi/src/utils/prepare.ts deleted file mode 100644 index bdcb64fda687..000000000000 --- a/packages/nuxi/src/utils/prepare.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { promises as fsp } from 'node:fs' -import { isAbsolute, join, relative, resolve } from 'pathe' -import type { Nuxt, TSReference } from '@nuxt/schema' -import { defu } from 'defu' -import type { TSConfig } from 'pkg-types' -import { withTrailingSlash } from 'ufo' -import { getModulePaths, getNearestPackage } from './cjs' - -export const writeTypes = async (nuxt: Nuxt) => { - const modulePaths = getModulePaths(nuxt.options.modulesDir) - - const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir) - - const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, { - compilerOptions: { - forceConsistentCasingInFileNames: true, - jsx: 'preserve', - target: 'ESNext', - module: 'ESNext', - moduleResolution: nuxt.options.experimental?.typescriptBundlerResolution ? 'Bundler' : 'Node', - skipLibCheck: true, - strict: nuxt.options.typescript?.strict ?? true, - allowJs: true, - // TODO: remove by default in 3.7 - baseUrl: nuxt.options.srcDir, - noEmit: true, - resolveJsonModule: true, - allowSyntheticDefaultImports: true, - types: ['node'], - paths: {} - }, - include: [ - './nuxt.d.ts', - join(relative(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'), - ...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [], - ...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd) - .filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules')) - .map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')), - ...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : [] - ], - exclude: [ - // nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186 - relative(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist')) - ] - } satisfies TSConfig) - - const aliases: Record = { - ...nuxt.options.alias, - '#build': nuxt.options.buildDir - } - - // Exclude bridge alias types to support Volar - const excludedAlias = [/^@vue\/.*$/] - - const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir - - tsConfig.compilerOptions = tsConfig.compilerOptions || {} - tsConfig.include = tsConfig.include || [] - - for (const alias in aliases) { - if (excludedAlias.some(re => re.test(alias))) { - continue - } - const absolutePath = resolve(basePath, aliases[alias]) - - const stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */) - if (stats?.isDirectory()) { - tsConfig.compilerOptions.paths[alias] = [absolutePath] - tsConfig.compilerOptions.paths[`${alias}/*`] = [`${absolutePath}/*`] - - if (!absolutePath.startsWith(rootDirWithSlash)) { - tsConfig.include.push(absolutePath) - tsConfig.include.push(`${absolutePath}/*`) - } - } else { - const path = stats?.isFile() - ? absolutePath.replace(/(?<=\w)\.\w+$/g, '') /* remove extension */ - : absolutePath - - tsConfig.compilerOptions.paths[alias] = [path] - - if (!absolutePath.startsWith(rootDirWithSlash)) { - tsConfig.include.push(path) - } - } - } - - const references: TSReference[] = [ - ...nuxt.options.modules, - ...nuxt.options._modules - ] - .filter(f => typeof f === 'string') - .map(id => ({ types: getNearestPackage(id, modulePaths)?.name || id })) - - if (nuxt.options.experimental?.reactivityTransform) { - references.push({ types: 'vue/macros-global' }) - } - - const declarations: string[] = [] - - await nuxt.callHook('prepare:types', { references, declarations, tsConfig }) - - const declaration = [ - ...references.map((ref) => { - if ('path' in ref && isAbsolute(ref.path)) { - ref.path = relative(nuxt.options.buildDir, ref.path) - } - return `/// ` - }), - ...declarations, - '', - 'export {}', - '' - ].join('\n') - - async function writeFile () { - const GeneratedBy = '// Generated by nuxi' - - const tsConfigPath = resolve(nuxt.options.buildDir, 'tsconfig.json') - await fsp.mkdir(nuxt.options.buildDir, { recursive: true }) - await fsp.writeFile(tsConfigPath, GeneratedBy + '\n' + JSON.stringify(tsConfig, null, 2)) - - const declarationPath = resolve(nuxt.options.buildDir, 'nuxt.d.ts') - await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration) - } - - // This is needed for Nuxt 2 which clears the build directory again before building - // https://github.com/nuxt/nuxt/blob/2.x/packages/builder/src/builder.js#L144 - // @ts-expect-error TODO: Nuxt 2 hook - nuxt.hook('builder:prepared', writeFile) - - await writeFile() -} - -function renderAttrs (obj: Record) { - return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ') -} - -function renderAttr (key: string, value: string) { - return value ? `${key}="${value}"` : '' -}