mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-15 18:34:50 +00:00
457 lines
17 KiB
TypeScript
457 lines
17 KiB
TypeScript
import { join, normalize, relative, resolve } from 'pathe'
|
|
import { createDebugger, createHooks } from 'hookable'
|
|
import type { LoadNuxtOptions } from '@nuxt/kit'
|
|
import { addBuildPlugin, addComponent, addPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
|
|
import type { Nuxt, NuxtHooks, NuxtOptions } from 'nuxt/schema'
|
|
|
|
import escapeRE from 'escape-string-regexp'
|
|
import fse from 'fs-extra'
|
|
import { withoutLeadingSlash } from 'ufo'
|
|
/* eslint-disable import/no-restricted-paths */
|
|
import pagesModule from '../pages/module'
|
|
import metaModule from '../head/module'
|
|
import componentsModule from '../components/module'
|
|
import importsModule from '../imports/module'
|
|
/* eslint-enable */
|
|
import { distDir, pkgDir } from '../dirs'
|
|
import { version } from '../../package.json'
|
|
import { ImportProtectionPlugin, vueAppPatterns } from './plugins/import-protection'
|
|
import { UnctxTransformPlugin } from './plugins/unctx'
|
|
import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake'
|
|
import { TreeShakeComposablesPlugin } from './plugins/tree-shake'
|
|
import { DevOnlyPlugin } from './plugins/dev-only'
|
|
import { LayerAliasingPlugin } from './plugins/layer-aliasing'
|
|
import { addModuleTranspiles } from './modules'
|
|
import { initNitro } from './nitro'
|
|
import schemaModule from './schema'
|
|
import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata'
|
|
|
|
export function createNuxt (options: NuxtOptions): Nuxt {
|
|
const hooks = createHooks<NuxtHooks>()
|
|
|
|
const nuxt: Nuxt = {
|
|
_version: version,
|
|
options,
|
|
hooks,
|
|
callHook: hooks.callHook,
|
|
addHooks: hooks.addHooks,
|
|
hook: hooks.hook,
|
|
ready: () => initNuxt(nuxt),
|
|
close: () => Promise.resolve(hooks.callHook('close', nuxt)),
|
|
vfs: {},
|
|
apps: {}
|
|
}
|
|
|
|
return nuxt
|
|
}
|
|
|
|
async function initNuxt (nuxt: Nuxt) {
|
|
// Register user hooks
|
|
nuxt.hooks.addHooks(nuxt.options.hooks)
|
|
|
|
// Set nuxt instance for useNuxt
|
|
nuxtCtx.set(nuxt)
|
|
nuxt.hook('close', () => nuxtCtx.unset())
|
|
|
|
// Add nuxt types
|
|
nuxt.hook('prepare:types', (opts) => {
|
|
opts.references.push({ types: 'nuxt' })
|
|
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/plugins.d.ts') })
|
|
// Add vue shim
|
|
if (nuxt.options.typescript.shim) {
|
|
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/vue-shim.d.ts') })
|
|
}
|
|
// Add module augmentations directly to NuxtConfig
|
|
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/schema.d.ts') })
|
|
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/app.config.d.ts') })
|
|
|
|
for (const layer of nuxt.options._layers) {
|
|
const declaration = join(layer.cwd, 'index.d.ts')
|
|
if (fse.existsSync(declaration)) {
|
|
opts.references.push({ path: declaration })
|
|
}
|
|
}
|
|
})
|
|
|
|
// Add plugin normalisation plugin
|
|
addBuildPlugin(RemovePluginMetadataPlugin(nuxt))
|
|
|
|
// Add import protection
|
|
const config = {
|
|
rootDir: nuxt.options.rootDir,
|
|
// Exclude top-level resolutions by plugins
|
|
exclude: [join(nuxt.options.rootDir, 'index.html')],
|
|
patterns: vueAppPatterns(nuxt)
|
|
}
|
|
addVitePlugin(() => ImportProtectionPlugin.vite(config))
|
|
addWebpackPlugin(() => ImportProtectionPlugin.webpack(config))
|
|
|
|
if (nuxt.options.experimental.localLayerAliases) {
|
|
// Add layer aliasing support for ~, ~~, @ and @@ aliases
|
|
addVitePlugin(() => LayerAliasingPlugin.vite({
|
|
sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client,
|
|
dev: nuxt.options.dev,
|
|
root: nuxt.options.srcDir,
|
|
// skip top-level layer (user's project) as the aliases will already be correctly resolved
|
|
layers: nuxt.options._layers.slice(1)
|
|
}))
|
|
addWebpackPlugin(() => LayerAliasingPlugin.webpack({
|
|
sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client,
|
|
dev: nuxt.options.dev,
|
|
root: nuxt.options.srcDir,
|
|
// skip top-level layer (user's project) as the aliases will already be correctly resolved
|
|
layers: nuxt.options._layers.slice(1),
|
|
transform: true
|
|
}))
|
|
}
|
|
|
|
nuxt.hook('modules:done', () => {
|
|
// Add unctx transform
|
|
const options = {
|
|
sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client,
|
|
transformerOptions: nuxt.options.optimization.asyncTransforms
|
|
}
|
|
addVitePlugin(() => UnctxTransformPlugin.vite(options))
|
|
addWebpackPlugin(() => UnctxTransformPlugin.webpack(options))
|
|
|
|
// Add composable tree-shaking optimisations
|
|
const serverTreeShakeOptions: TreeShakeComposablesPluginOptions = {
|
|
sourcemap: nuxt.options.sourcemap.server,
|
|
composables: nuxt.options.optimization.treeShake.composables.server
|
|
}
|
|
if (Object.keys(serverTreeShakeOptions.composables).length) {
|
|
addVitePlugin(() => TreeShakeComposablesPlugin.vite(serverTreeShakeOptions), { client: false })
|
|
addWebpackPlugin(() => TreeShakeComposablesPlugin.webpack(serverTreeShakeOptions), { client: false })
|
|
}
|
|
const clientTreeShakeOptions: TreeShakeComposablesPluginOptions = {
|
|
sourcemap: nuxt.options.sourcemap.client,
|
|
composables: nuxt.options.optimization.treeShake.composables.client
|
|
}
|
|
if (Object.keys(clientTreeShakeOptions.composables).length) {
|
|
addVitePlugin(() => TreeShakeComposablesPlugin.vite(clientTreeShakeOptions), { server: false })
|
|
addWebpackPlugin(() => TreeShakeComposablesPlugin.webpack(clientTreeShakeOptions), { server: false })
|
|
}
|
|
})
|
|
|
|
if (!nuxt.options.dev) {
|
|
// DevOnly component tree-shaking - build time only
|
|
addVitePlugin(() => DevOnlyPlugin.vite({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client }))
|
|
addWebpackPlugin(() => DevOnlyPlugin.webpack({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client }))
|
|
}
|
|
|
|
// TODO: [Experimental] Avoid emitting assets when flag is enabled
|
|
if (nuxt.options.experimental.noScripts && !nuxt.options.dev) {
|
|
nuxt.hook('build:manifest', async (manifest) => {
|
|
for (const file in manifest) {
|
|
if (manifest[file].resourceType === 'script') {
|
|
await fse.rm(resolve(nuxt.options.buildDir, 'dist/client', withoutLeadingSlash(nuxt.options.app.buildAssetsDir), manifest[file].file), { force: true })
|
|
manifest[file].file = ''
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Transpile #app if it is imported directly from subpath export
|
|
nuxt.options.build.transpile.push('nuxt/app')
|
|
|
|
// Transpile layers within node_modules
|
|
nuxt.options.build.transpile.push(
|
|
...nuxt.options._layers.filter(i => i.cwd.includes('node_modules')).map(i => i.cwd as string)
|
|
)
|
|
|
|
// Init user modules
|
|
await nuxt.callHook('modules:before')
|
|
const modulesToInstall = []
|
|
|
|
const watchedPaths = new Set<string>()
|
|
const specifiedModules = new Set<string>()
|
|
|
|
for (const _mod of nuxt.options.modules) {
|
|
const mod = Array.isArray(_mod) ? _mod[0] : _mod
|
|
if (typeof mod !== 'string') { continue }
|
|
const modPath = await resolvePath(resolveAlias(mod))
|
|
specifiedModules.add(modPath)
|
|
}
|
|
|
|
// Automatically register user modules
|
|
for (const config of nuxt.options._layers.map(layer => layer.config).reverse()) {
|
|
const layerModules = await resolveFiles(config.srcDir, [
|
|
`${config.dir?.modules || 'modules'}/*{${nuxt.options.extensions.join(',')}}`,
|
|
`${config.dir?.modules || 'modules'}/*/index{${nuxt.options.extensions.join(',')}}`
|
|
])
|
|
for (const mod of layerModules) {
|
|
watchedPaths.add(mod)
|
|
if (specifiedModules.has(mod)) { continue }
|
|
specifiedModules.add(mod)
|
|
modulesToInstall.push(mod)
|
|
}
|
|
}
|
|
|
|
// Register user and then ad-hoc modules
|
|
modulesToInstall.push(...nuxt.options.modules, ...nuxt.options._modules)
|
|
|
|
// Add <NuxtWelcome>
|
|
addComponent({
|
|
name: 'NuxtWelcome',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: (await tryResolveModule('@nuxt/ui-templates/templates/welcome.vue', nuxt.options.modulesDir))!
|
|
})
|
|
|
|
addComponent({
|
|
name: 'NuxtLayout',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: resolve(nuxt.options.appDir, 'components/nuxt-layout')
|
|
})
|
|
|
|
// Add <NuxtErrorBoundary>
|
|
addComponent({
|
|
name: 'NuxtErrorBoundary',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: resolve(nuxt.options.appDir, 'components/nuxt-error-boundary')
|
|
})
|
|
|
|
// Add <ClientOnly>
|
|
addComponent({
|
|
name: 'ClientOnly',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: resolve(nuxt.options.appDir, 'components/client-only')
|
|
})
|
|
|
|
// Add <DevOnly>
|
|
addComponent({
|
|
name: 'DevOnly',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: resolve(nuxt.options.appDir, 'components/dev-only')
|
|
})
|
|
|
|
// Add <ServerPlaceholder>
|
|
addComponent({
|
|
name: 'ServerPlaceholder',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: resolve(nuxt.options.appDir, 'components/server-placeholder')
|
|
})
|
|
|
|
// Add <NuxtLink>
|
|
addComponent({
|
|
name: 'NuxtLink',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: resolve(nuxt.options.appDir, 'components/nuxt-link')
|
|
})
|
|
|
|
// Add <NuxtLoadingIndicator>
|
|
addComponent({
|
|
name: 'NuxtLoadingIndicator',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: resolve(nuxt.options.appDir, 'components/nuxt-loading-indicator')
|
|
})
|
|
|
|
// Add <NuxtClientFallback>
|
|
if (nuxt.options.experimental.clientFallback) {
|
|
addComponent({
|
|
name: 'NuxtClientFallback',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: resolve(nuxt.options.appDir, 'components/client-fallback.client'),
|
|
mode: 'client'
|
|
})
|
|
|
|
addComponent({
|
|
name: 'NuxtClientFallback',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: resolve(nuxt.options.appDir, 'components/client-fallback.server'),
|
|
mode: 'server'
|
|
})
|
|
}
|
|
|
|
// Add <NuxtIsland>
|
|
if (nuxt.options.experimental.componentIslands) {
|
|
addComponent({
|
|
name: 'NuxtIsland',
|
|
priority: 10, // built-in that we do not expect the user to override
|
|
filePath: resolve(nuxt.options.appDir, 'components/nuxt-island')
|
|
})
|
|
}
|
|
|
|
// Add experimental cross-origin prefetch support using Speculation Rules API
|
|
if (nuxt.options.experimental.crossOriginPrefetch) {
|
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/cross-origin-prefetch.client'))
|
|
}
|
|
|
|
// Add experimental page reload support
|
|
if (nuxt.options.experimental.emitRouteChunkError === 'automatic') {
|
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/chunk-reload.client'))
|
|
}
|
|
// Add experimental session restoration support
|
|
if (nuxt.options.experimental.restoreState) {
|
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/restore-state.client'))
|
|
}
|
|
|
|
// Add experimental automatic view transition api support
|
|
if (nuxt.options.experimental.viewTransition) {
|
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/view-transitions.client'))
|
|
}
|
|
|
|
// Add experimental support for custom types in JSON payload
|
|
if (nuxt.options.experimental.renderJsonPayloads) {
|
|
nuxt.hooks.hook('modules:done', () => {
|
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/revive-payload.client'))
|
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/revive-payload.server'))
|
|
})
|
|
}
|
|
|
|
// Track components used to render for webpack
|
|
if (nuxt.options.builder === '@nuxt/webpack-builder') {
|
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/preload.server'))
|
|
}
|
|
|
|
const envMap = {
|
|
// defaults from `builder` based on package name
|
|
'@nuxt/vite-builder': 'vite/client',
|
|
'@nuxt/webpack-builder': 'webpack/module',
|
|
// simpler overrides from `typescript.builder` for better DX
|
|
vite: 'vite/client',
|
|
webpack: 'webpack/module',
|
|
// default 'merged' builder environment for module authors
|
|
shared: '@nuxt/schema/builder-env'
|
|
}
|
|
|
|
nuxt.hook('prepare:types', ({ references }) => {
|
|
// Disable entirely if `typescript.builder` is false
|
|
if (nuxt.options.typescript.builder === false) { return }
|
|
|
|
const overrideEnv = nuxt.options.typescript.builder && envMap[nuxt.options.typescript.builder]
|
|
// If there's no override, infer based on builder. If a custom builder is provided, we disable shared types
|
|
const defaultEnv = typeof nuxt.options.builder === 'string' ? envMap[nuxt.options.builder] : false
|
|
const types = overrideEnv || defaultEnv
|
|
|
|
if (types) { references.push({ types }) }
|
|
})
|
|
|
|
// Add nuxt app debugger
|
|
if (nuxt.options.debug) {
|
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/debug'))
|
|
}
|
|
|
|
for (const m of modulesToInstall) {
|
|
if (Array.isArray(m)) {
|
|
await installModule(m[0], m[1])
|
|
} else {
|
|
await installModule(m, {})
|
|
}
|
|
}
|
|
|
|
await nuxt.callHook('modules:done')
|
|
|
|
nuxt.hooks.hook('builder:watch', (event, relativePath) => {
|
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
|
// Local module patterns
|
|
if (watchedPaths.has(path)) {
|
|
return nuxt.callHook('restart', { hard: true })
|
|
}
|
|
|
|
// User provided patterns
|
|
const layerRelativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
|
|
for (const pattern of nuxt.options.watch) {
|
|
if (typeof pattern === 'string') {
|
|
// Test (normalised) strings against absolute path and relative path to any layer `srcDir`
|
|
if (pattern === path || layerRelativePaths.includes(pattern)) { return nuxt.callHook('restart') }
|
|
continue
|
|
}
|
|
// Test regular expressions against path to _any_ layer `srcDir`
|
|
if (layerRelativePaths.some(p => pattern.test(p))) {
|
|
return nuxt.callHook('restart')
|
|
}
|
|
}
|
|
|
|
// Core Nuxt files: app.vue, error.vue and app.config.ts
|
|
const isFileChange = ['add', 'unlink'].includes(event)
|
|
if (isFileChange && RESTART_RE.test(path)) {
|
|
console.info(`\`${path}\` ${event === 'add' ? 'created' : 'removed'}`)
|
|
return nuxt.callHook('restart')
|
|
}
|
|
})
|
|
|
|
// Normalize windows transpile paths added by modules
|
|
nuxt.options.build.transpile = nuxt.options.build.transpile.map(t => typeof t === 'string' ? normalize(t) : t)
|
|
|
|
addModuleTranspiles()
|
|
|
|
// Init nitro
|
|
await initNitro(nuxt)
|
|
|
|
// TODO: remove when app manifest support is landed in https://github.com/nuxt/nuxt/pull/21641
|
|
// Add prerender payload support
|
|
const nitro = useNitro()
|
|
if (nitro.options.static && nuxt.options.experimental.payloadExtraction === undefined) {
|
|
console.warn('Using experimental payload extraction for full-static output. You can opt-out by setting `experimental.payloadExtraction` to `false`.')
|
|
nuxt.options.experimental.payloadExtraction = true
|
|
}
|
|
nitro.options.replace['process.env.NUXT_PAYLOAD_EXTRACTION'] = String(!!nuxt.options.experimental.payloadExtraction)
|
|
nitro.options._config.replace!['process.env.NUXT_PAYLOAD_EXTRACTION'] = String(!!nuxt.options.experimental.payloadExtraction)
|
|
|
|
if (!nuxt.options.dev && nuxt.options.experimental.payloadExtraction) {
|
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/payload.client'))
|
|
}
|
|
|
|
await nuxt.callHook('ready', nuxt)
|
|
}
|
|
|
|
export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
|
|
const options = await loadNuxtConfig(opts)
|
|
|
|
// Temporary until finding better placement for each
|
|
options.appDir = options.alias['#app'] = resolve(distDir, 'app')
|
|
options._majorVersion = 3
|
|
|
|
// Nuxt DevTools is currently opt-in
|
|
if (options.devtools === true || (options.devtools && options.devtools.enabled !== false)) {
|
|
if (await import('./features').then(r => r.ensurePackageInstalled(options.rootDir, '@nuxt/devtools', options.modulesDir))) {
|
|
options._modules.push('@nuxt/devtools')
|
|
} else {
|
|
logger.warn('Failed to install `@nuxt/devtools`, please install it manually, or disable `devtools` in `nuxt.config`')
|
|
}
|
|
}
|
|
|
|
// Nuxt Webpack Builder is currently opt-in
|
|
if (options.builder === '@nuxt/webpack-builder') {
|
|
if (!await import('./features').then(r => r.ensurePackageInstalled(options.rootDir, '@nuxt/webpack-builder', options.modulesDir))) {
|
|
logger.warn('Failed to install `@nuxt/webpack-builder`, please install it manually, or change the `builder` option to vite in `nuxt.config`')
|
|
}
|
|
}
|
|
|
|
// Add core modules
|
|
options._modules.push(pagesModule, metaModule, componentsModule)
|
|
options._modules.push([importsModule, {
|
|
transform: {
|
|
include: options._layers
|
|
.filter(i => i.cwd && i.cwd.includes('node_modules'))
|
|
.map(i => new RegExp(`(^|\\/)${escapeRE(i.cwd!.split('node_modules/').pop()!)}(\\/|$)(?!node_modules\\/)`))
|
|
}
|
|
}])
|
|
options._modules.push(schemaModule)
|
|
options.modulesDir.push(resolve(options.workspaceDir, 'node_modules'))
|
|
options.modulesDir.push(resolve(pkgDir, 'node_modules'))
|
|
options.build.transpile.push(
|
|
'@nuxt/ui-templates', // this exposes vue SFCs
|
|
'std-env' // we need to statically replace process.env when used in runtime code
|
|
)
|
|
options.alias['vue-demi'] = resolve(options.appDir, 'compat/vue-demi')
|
|
options.alias['@vue/composition-api'] = resolve(options.appDir, 'compat/capi')
|
|
if (options.telemetry !== false && !process.env.NUXT_TELEMETRY_DISABLED) {
|
|
options._modules.push('@nuxt/telemetry')
|
|
}
|
|
|
|
const nuxt = createNuxt(options)
|
|
|
|
if (nuxt.options.debug) {
|
|
createDebugger(nuxt.hooks, { tag: 'nuxt' })
|
|
}
|
|
|
|
if (opts.ready !== false) {
|
|
await nuxt.ready()
|
|
}
|
|
|
|
return nuxt
|
|
}
|
|
|
|
const RESTART_RE = /^(app|error|app\.config)\.(js|ts|mjs|jsx|tsx|vue)$/i
|