2023-07-31 08:42:42 +00:00
|
|
|
import { existsSync, promises as fsp } from 'node:fs'
|
|
|
|
import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe'
|
2021-11-21 16:14:46 +00:00
|
|
|
import hash from 'hash-sum'
|
2023-09-04 11:23:03 +00:00
|
|
|
import type { Nuxt, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema'
|
2023-07-31 08:42:42 +00:00
|
|
|
import { withTrailingSlash } from 'ufo'
|
|
|
|
import { defu } from 'defu'
|
|
|
|
import type { TSConfig } from 'pkg-types'
|
|
|
|
import { readPackageJSON } from 'pkg-types'
|
|
|
|
|
2023-08-01 09:17:02 +00:00
|
|
|
import { tryResolveModule } from './internal/esm'
|
2023-04-07 16:02:47 +00:00
|
|
|
import { tryUseNuxt, useNuxt } from './context'
|
2023-07-31 08:42:42 +00:00
|
|
|
import { getModulePaths } from './internal/cjs'
|
2021-11-21 16:14:46 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders given template using lodash template during build into the project buildDir
|
|
|
|
*/
|
2022-08-22 10:12:02 +00:00
|
|
|
export function addTemplate (_template: NuxtTemplate<any> | string) {
|
2021-11-21 16:14:46 +00:00
|
|
|
const nuxt = useNuxt()
|
|
|
|
|
2022-08-22 10:12:02 +00:00
|
|
|
// Normalize template
|
2021-11-21 16:14:46 +00:00
|
|
|
const template = normalizeTemplate(_template)
|
|
|
|
|
|
|
|
// Remove any existing template with the same filename
|
|
|
|
nuxt.options.build.templates = nuxt.options.build.templates
|
|
|
|
.filter(p => normalizeTemplate(p).filename !== template.filename)
|
|
|
|
|
|
|
|
// Add to templates array
|
|
|
|
nuxt.options.build.templates.push(template)
|
|
|
|
|
|
|
|
return template
|
|
|
|
}
|
|
|
|
|
2023-06-09 21:24:03 +00:00
|
|
|
/**
|
|
|
|
* Renders given types using lodash template during build into the project buildDir
|
|
|
|
* and register them as types.
|
|
|
|
*/
|
2023-09-04 11:23:03 +00:00
|
|
|
export function addTypeTemplate (_template: NuxtTypeTemplate<any>) {
|
2023-06-09 21:24:03 +00:00
|
|
|
const nuxt = useNuxt()
|
|
|
|
|
|
|
|
const template = addTemplate(_template)
|
|
|
|
|
|
|
|
if (!template.filename.endsWith('.d.ts')) {
|
|
|
|
throw new Error(`Invalid type template. Filename must end with .d.ts : "${template.filename}"`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add template to types reference
|
|
|
|
nuxt.hook('prepare:types', ({ references }) => {
|
|
|
|
references.push({ path: template.dst })
|
|
|
|
})
|
|
|
|
|
|
|
|
return template
|
|
|
|
}
|
|
|
|
|
2021-11-21 16:14:46 +00:00
|
|
|
/**
|
|
|
|
* Normalize a nuxt template object
|
|
|
|
*/
|
2022-08-22 10:12:02 +00:00
|
|
|
export function normalizeTemplate (template: NuxtTemplate<any> | string): ResolvedNuxtTemplate<any> {
|
2021-11-21 16:14:46 +00:00
|
|
|
if (!template) {
|
|
|
|
throw new Error('Invalid template: ' + JSON.stringify(template))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Normalize
|
|
|
|
if (typeof template === 'string') {
|
|
|
|
template = { src: template }
|
|
|
|
} else {
|
|
|
|
template = { ...template }
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use src if provided
|
|
|
|
if (template.src) {
|
|
|
|
if (!existsSync(template.src)) {
|
|
|
|
throw new Error('Template not found: ' + template.src)
|
|
|
|
}
|
|
|
|
if (!template.filename) {
|
|
|
|
const srcPath = parse(template.src)
|
2022-11-16 02:26:35 +00:00
|
|
|
template.filename = (template as any).fileName ||
|
2021-11-21 16:14:46 +00:00
|
|
|
`${basename(srcPath.dir)}.${srcPath.name}.${hash(template.src)}${srcPath.ext}`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!template.src && !template.getContents) {
|
|
|
|
throw new Error('Invalid template. Either getContents or src options should be provided: ' + JSON.stringify(template))
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!template.filename) {
|
|
|
|
throw new Error('Invalid template. Either filename should be provided: ' + JSON.stringify(template))
|
|
|
|
}
|
|
|
|
|
2022-02-07 10:20:01 +00:00
|
|
|
// Always write declaration files
|
|
|
|
if (template.filename.endsWith('.d.ts')) {
|
|
|
|
template.write = true
|
|
|
|
}
|
|
|
|
|
2021-11-21 16:14:46 +00:00
|
|
|
// Resolve dst
|
|
|
|
if (!template.dst) {
|
|
|
|
const nuxt = useNuxt()
|
|
|
|
template.dst = resolve(nuxt.options.buildDir, template.filename)
|
|
|
|
}
|
|
|
|
|
2022-08-22 10:12:02 +00:00
|
|
|
return template as ResolvedNuxtTemplate<any>
|
2021-11-21 16:14:46 +00:00
|
|
|
}
|
2022-10-24 08:53:02 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Trigger rebuilding Nuxt templates
|
|
|
|
*
|
|
|
|
* You can pass a filter within the options to selectively regenerate a subset of templates.
|
|
|
|
*/
|
2023-03-23 00:24:18 +00:00
|
|
|
export async function updateTemplates (options?: { filter?: (template: ResolvedNuxtTemplate<any>) => boolean }) {
|
|
|
|
return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options)
|
2022-10-24 08:53:02 +00:00
|
|
|
}
|
2023-07-31 08:42:42 +00:00
|
|
|
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',
|
2023-09-11 13:40:36 +00:00
|
|
|
jsxImportSource: 'vue',
|
2023-07-31 08:42:42 +00:00
|
|
|
target: 'ESNext',
|
|
|
|
module: 'ESNext',
|
|
|
|
moduleResolution: nuxt.options.experimental?.typescriptBundlerResolution ? 'Bundler' : 'Node',
|
|
|
|
skipLibCheck: true,
|
2023-08-03 15:38:31 +00:00
|
|
|
isolatedModules: true,
|
|
|
|
useDefineForClassFields: true,
|
2023-07-31 08:42:42 +00:00
|
|
|
strict: nuxt.options.typescript?.strict ?? true,
|
2023-09-11 13:40:36 +00:00
|
|
|
noImplicitThis: true,
|
|
|
|
esModuleInterop: true,
|
|
|
|
// TODO: enable by default in v3.8
|
|
|
|
// types: [],
|
|
|
|
// verbatimModuleSyntax: true,
|
2023-07-31 08:42:42 +00:00
|
|
|
allowJs: true,
|
|
|
|
noEmit: true,
|
|
|
|
resolveJsonModule: true,
|
|
|
|
allowSyntheticDefaultImports: true,
|
|
|
|
paths: {}
|
|
|
|
},
|
|
|
|
include: [
|
|
|
|
'./nuxt.d.ts',
|
2023-08-01 09:17:02 +00:00
|
|
|
join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'),
|
2023-07-31 08:42:42 +00:00
|
|
|
...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: [
|
2023-08-01 09:17:02 +00:00
|
|
|
...nuxt.options.modulesDir.map(m => relativeWithDot(nuxt.options.buildDir, m)),
|
2023-07-31 08:42:42 +00:00
|
|
|
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
|
2023-08-01 09:17:02 +00:00
|
|
|
relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist'))
|
2023-07-31 08:42:42 +00:00
|
|
|
]
|
|
|
|
} 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
|
|
|
|
}
|
2023-08-01 09:17:02 +00:00
|
|
|
let absolutePath = resolve(basePath, aliases[alias])
|
|
|
|
let stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */)
|
|
|
|
if (!stats) {
|
|
|
|
const resolvedModule = await tryResolveModule(aliases[alias], nuxt.options.modulesDir)
|
|
|
|
if (resolvedModule) {
|
|
|
|
absolutePath = resolvedModule
|
|
|
|
stats = await fsp.stat(resolvedModule).catch(() => null)
|
|
|
|
}
|
|
|
|
}
|
2023-07-31 08:42:42 +00:00
|
|
|
|
2023-08-01 09:17:02 +00:00
|
|
|
const relativePath = relativeWithDot(nuxt.options.buildDir, absolutePath)
|
2023-07-31 08:42:42 +00:00
|
|
|
if (stats?.isDirectory()) {
|
2023-08-01 09:17:02 +00:00
|
|
|
tsConfig.compilerOptions.paths[alias] = [relativePath]
|
|
|
|
tsConfig.compilerOptions.paths[`${alias}/*`] = [`${relativePath}/*`]
|
2023-07-31 08:42:42 +00:00
|
|
|
|
|
|
|
if (!absolutePath.startsWith(rootDirWithSlash)) {
|
|
|
|
tsConfig.include.push(relativePath)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const path = stats?.isFile()
|
2023-08-01 09:17:02 +00:00
|
|
|
// remove extension
|
|
|
|
? relativePath.replace(/(?<=\w)\.\w+$/g, '')
|
|
|
|
// non-existent file probably shouldn't be resolved
|
|
|
|
: aliases[alias]
|
2023-07-31 08:42:42 +00:00
|
|
|
|
|
|
|
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[] = []
|
|
|
|
|
|
|
|
await nuxt.callHook('prepare:types', { references, declarations, tsConfig })
|
|
|
|
|
2023-08-01 09:17:02 +00:00
|
|
|
for (const alias in tsConfig.compilerOptions!.paths) {
|
|
|
|
const paths = tsConfig.compilerOptions!.paths[alias]
|
|
|
|
tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => {
|
|
|
|
if (!isAbsolute(path)) { return path }
|
|
|
|
const stats = await fsp.stat(path).catch(() => null /* file does not exist */)
|
|
|
|
return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/(?<=\w)\.\w+$/g, '') /* remove extension */ : path)
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
tsConfig.include = [...new Set(tsConfig.include.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
|
|
|
|
tsConfig.exclude = [...new Set(tsConfig.exclude!.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
|
|
|
|
|
2023-07-31 08:42:42 +00:00
|
|
|
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
|
2023-07-31 14:44:25 +00:00
|
|
|
nuxt.hook('builder:prepared', writeFile)
|
2023-07-31 08:42:42 +00:00
|
|
|
|
|
|
|
await writeFile()
|
|
|
|
}
|
|
|
|
|
|
|
|
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}"` : ''
|
|
|
|
}
|
2023-08-01 09:17:02 +00:00
|
|
|
|
|
|
|
function relativeWithDot (from: string, to: string) {
|
|
|
|
return relative(from, to).replace(/^([^.])/, './$1') || '.'
|
|
|
|
}
|