Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(kit,nuxi): add writeTypes utility #22385

Merged
merged 7 commits into from Jul 31, 2023
16 changes: 10 additions & 6 deletions packages/kit/src/internal/cjs.ts
Expand Up @@ -60,15 +60,19 @@ function getRequireCacheItem (id: string) {
}
}

export function getModulePaths (paths?: string[] | string) {
return ([] as Array<string | undefined>).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<string | undefined>).concat(
global.__NUXT_PREPATHS__,
opts.paths || [],
process.cwd(),
global.__NUXT_PATHS__
).filter(Boolean) as string[]
paths: getModulePaths(opts.paths)
}))
}

Expand Down
151 changes: 148 additions & 3 deletions 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
Expand Down Expand Up @@ -101,3 +107,142 @@ export function normalizeTemplate (template: NuxtTemplate<any> | string): Resolv
export async function updateTemplates (options?: { filter?: (template: ResolvedNuxtTemplate<any>) => 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<string, string> = {
...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 `/// <reference ${renderAttrs(ref)} />`
}),
...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<string, string>) {
return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ')
}

function renderAttr (key: string, value: string) {
return value ? `${key}="${value}"` : ''
}
6 changes: 4 additions & 2 deletions 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'
Expand All @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions packages/nuxi/src/commands/dev.ts
Expand Up @@ -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'
Expand All @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions 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({
Expand All @@ -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: {
Expand Down
7 changes: 4 additions & 3 deletions 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({
Expand All @@ -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: {
Expand Down
12 changes: 2 additions & 10 deletions 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<string | undefined>)
.concat(
global.__NUXT_PREPATHS__,
Expand All @@ -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
}