UNPKG

nuxt

Version:

Nuxt is a free and open-source framework with an intuitive and extendable way to create type-safe, performant and production-grade full-stack web applications and websites with Vue.js.

1,303 lines (1,290 loc) 334 kB
import process from 'node:process'; import fs, { statSync, promises, existsSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs'; import { mkdir, readFile, readdir, writeFile, stat, unlink, open, rm } from 'node:fs/promises'; import { randomUUID } from 'node:crypto'; import { AsyncLocalStorage } from 'node:async_hooks'; import { dirname, resolve, normalize, basename, extname, relative, join, isAbsolute, parse as parse$1 } from 'pathe'; import { createHooks, createDebugger } from 'hookable'; import ignore from 'ignore'; import { useLogger, tryUseNuxt, useNuxt, directoryToURL, getLayerDirectories, resolveFiles, resolvePath, defineNuxtModule, findPath, addPlugin, addTemplate, addTypeTemplate, addComponent, isIgnored, useNitro, addBuildPlugin, resolveAlias as resolveAlias$1, addPluginTemplate, addImportsSources, addVitePlugin, createIsIgnored, updateTemplates, tryResolveModule, normalizeModuleTranspilePath, importModule, createResolver, runWithNuxtContext, nuxtCtx, loadNuxtConfig, installModules, resolveIgnorePatterns, addRouteMiddleware, resolveModuleWithOptions, normalizeTemplate, normalizePlugin } from '@nuxt/kit'; import { resolvePackageJSON, readPackage, readPackageJSON } from 'pkg-types'; import { hash, isEqual, serialize } from 'ohash'; import { consola } from 'consola'; import onChange from 'on-change'; import { colors } from 'consola/utils'; import { resolveCompatibilityDatesFromEnv, formatDate } from 'compatx'; import escapeRE from 'escape-string-regexp'; import { withTrailingSlash as withTrailingSlash$1, joinURL, withoutLeadingSlash } from 'ufo'; import { ImpoundPlugin } from 'impound'; import { defu } from 'defu'; import { satisfies, coerce } from 'semver'; import { isCI, provider, hasTTY } from 'std-env'; import { genArrayFromRaw, genSafeVariableName, genImport, genDynamicImport, genObjectFromRawEntries, genString, genObjectKey, genInlineTypeImport, genDynamicTypeImport, genExport } from 'knitwork'; import { resolveModulePath } from 'exsolve'; import { addDependency } from 'nypm'; import { reverseResolveAlias, filename, resolveAlias } from 'pathe/utils'; import { createRoutesContext, resolveOptions } from 'vue-router/unplugin'; import { createRouter, addRoute, findAllRoutes } from 'rou3'; import { fileURLToPath } from 'node:url'; import picomatch from 'picomatch'; import { runInNewContext } from 'node:vm'; import { klona } from 'klona'; import { parseAndWalk, ScopeTracker, walk, isBindingIdentifier, getUndeclaredIdentifiersInFunction } from 'oxc-walker'; import { parseSync } from 'oxc-parser'; import { transformSync } from 'oxc-transform'; import { compileParsePath, buildTree, removeFile, addFile, toVueRouter4 } from 'unrouting'; import { splitByCase, kebabCase, pascalCase, camelCase } from 'scule'; import { createUnplugin } from 'unplugin'; import { findStaticImports, findExports, parseStaticImport, parseNodeModulePath, lookupNodeModuleSubpath } from 'mlly'; import MagicString from 'magic-string'; import { unheadVueComposablesImports } from '@unhead/vue'; import { defineUnimportPreset, createUnimport, toExports, toTypeDeclarationFile, scanDirExports } from 'unimport'; import { glob } from 'tinyglobby'; import { parse, walk as walk$1, ELEMENT_NODE } from 'ultrahtml'; import { isObject } from '@vue/shared'; import { resolve as resolve$1 } from 'node:path'; import { parseTar, createTar } from 'nanotar'; import { createTransformer } from 'unctx/transform'; import { performance } from 'node:perf_hooks'; import { watch as watch$1 } from 'chokidar'; import { debounce } from 'perfect-debounce'; import { resolveSchema, generateTypes } from 'untyped'; import untypedPlugin from 'untyped/babel-plugin'; import { createJiti } from 'jiti'; import { minifySync } from 'oxc-minify'; function toArray(value) { return Array.isArray(value) ? value : [value]; } async function isDirectory(path) { return (await promises.lstat(path)).isDirectory(); } function isDirectorySync(path) { try { return statSync(path).isDirectory(); } catch { return false; } } const LEADING_DOT_RE = /^\.+/g; function normalizeExtension(input) { return input.replace(LEADING_DOT_RE, ""); } function stripExtension(path) { return path.replace(/\.[^./\\]+$/, ""); } function isWhitespace(char) { const c = typeof char === "string" ? char.charCodeAt(0) : char; return c === 32 || c === 9 || c === 10 || c === 13 || c === 12; } const JS_EXT_RE = /^[^?]*\.(?:[jt]sx?|[cm][jt]s)(?:$|\?)/; const NUXT_LIB_RE = /^[^?]*node_modules\/(?:nuxt|nuxt3|nuxt-nightly|@nuxt)\//; const STYLE_QUERY_RE$1 = /[?&]type=style/; const MACRO_QUERY_RE$2 = /[?&]macro(?:=|&|$)/; const DECLARATION_EXTENSIONS = ["d.ts", "d.mts", "d.cts", "d.vue.ts", "d.vue.mts", "d.vue.cts"]; const logger = useLogger("nuxt"); function resolveToAlias(path, nuxt = tryUseNuxt()) { return reverseResolveAlias(path, { ...nuxt?.options.alias || {}, ...strippedAtAliases$1 }).pop() || path; } const strippedAtAliases$1 = { "@": "", "@@": "" }; const isStackblitz = provider === "stackblitz"; async function promptToInstall(name, installCommand, options) { for (const parent of options.searchPaths || []) { if (await resolvePackageJSON(name, { parent }).catch(() => null)) { return true; } } logger.info(`Package ${name} is missing`); if (isCI) { return false; } if (options.prompt === true || options.prompt !== false && !isStackblitz) { const confirm = await logger.prompt(`Do you want to install ${name} package?`, { type: "confirm", name: "confirm", initial: true }); if (!confirm) { return false; } } logger.info(`Installing ${name}...`); try { await installCommand(); logger.success(`Installed ${name}`); return true; } catch (err) { logger.error(err); return false; } } const installPrompts = /* @__PURE__ */ new Set(); function installNuxtModule(name, options) { if (installPrompts.has(name)) { return; } installPrompts.add(name); const nuxt = useNuxt(); return promptToInstall(name, async () => { const { runCommand } = await import('@nuxt/cli'); await runCommand("module", ["add", name, "--cwd", nuxt.options.rootDir]); }, { rootDir: nuxt.options.rootDir, searchPaths: nuxt.options.modulesDir, ...options }); } function ensurePackageInstalled(name, options) { return promptToInstall(name, () => addDependency(name, { cwd: options.rootDir, dev: true }), options); } const features = { __proto__: null, ensurePackageInstalled: ensurePackageInstalled, installNuxtModule: installNuxtModule }; let _distDir = dirname(fileURLToPath(import.meta.url)); if (/(?:chunks|shared)$/.test(_distDir)) { _distDir = dirname(_distDir); } const distDir = _distDir; const pkgDir = resolve(distDir, ".."); async function resolveTypePath(path, subpath, searchPaths = tryUseNuxt()?.options.modulesDir) { try { const r = resolveModulePath(path, { from: searchPaths?.map((d) => directoryToURL(d)), conditions: ["types", "import", "require"], extensions: [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"] }); if (subpath) { return r.replace(/(?:\.d)?\.[mc]?[jt]s$/, ""); } const rootPath = await resolvePackageJSON(r); return dirname(rootPath); } catch { return null; } } function getNameFromPath(path, relativeTo) { const relativePath = relativeTo ? normalize(path).replace(withTrailingSlash$1(normalize(relativeTo)), "") : basename(path); const prefixParts = splitByCase(dirname(relativePath)); const fileName = basename(relativePath, extname(relativePath)); const segments = resolveComponentNameSegments(fileName.toLowerCase() === "index" ? "" : fileName, prefixParts).filter(Boolean); return kebabCase(segments).replace(QUOTE_RE, ""); } function hasSuffix(path, suffix) { return basename(path, extname(path)).endsWith(suffix); } function resolveComponentNameSegments(fileName, prefixParts) { const fileNameParts = splitByCase(fileName); const fileNamePartsContent = fileNameParts.join("/").toLowerCase(); const componentNameParts = prefixParts.flatMap((p) => splitByCase(p)); let index = prefixParts.length - 1; const matchedSuffix = []; while (index >= 0) { const prefixPart = prefixParts[index]; matchedSuffix.unshift(...splitByCase(prefixPart).map((p) => p.toLowerCase())); const matchedSuffixContent = matchedSuffix.join("/"); if (fileNamePartsContent === matchedSuffixContent || fileNamePartsContent.startsWith(matchedSuffixContent + "/") || // e.g. Item/Item/Item.vue -> Item prefixPart.toLowerCase() === fileNamePartsContent && prefixParts[index + 1] && prefixParts[index] === prefixParts[index + 1]) { componentNameParts.length = index; } index--; } return [...componentNameParts, ...fileNameParts]; } function parseModuleId(id) { const qIndex = id.indexOf("?"); if (qIndex === -1) { return { pathname: id, search: "" }; } return { pathname: id.slice(0, qIndex), search: id.slice(qIndex) }; } const NUXT_COMPONENT_RE = /[?&]nuxt_component=/; const MACRO_RE$1 = /[?&]macro=/; const VUE_QUERY_RE = /[?&]vue(?:&|$)/; const SETUP_QUERY_RE = /[?&]setup(?:=|&|$)/; const TYPE_QUERY_RE = /[?&]type=([^&]*)/; function isVue(id, opts = {}) { const { search } = parseModuleId(id); if (id.endsWith(".vue") && !search) { return true; } if (!search) { return false; } if (NUXT_COMPONENT_RE.test(search)) { return false; } if (MACRO_RE$1.test(search) && (search === "?macro=true" || !opts.type || opts.type.includes("script"))) { return true; } if (!VUE_QUERY_RE.test(search)) { return false; } if (opts.type) { const type = SETUP_QUERY_RE.test(search) ? "script" : TYPE_QUERY_RE.exec(search)?.[1]; if (!type || !opts.type.includes(type)) { return false; } } return true; } const JS_RE$1 = /\.(?:[cm]?j|t)sx?$/; const VUE_ID_RE = /\.vue(?:\?|$)/; function isJS(id) { const { pathname } = parseModuleId(id); return JS_RE$1.test(pathname); } function getLoader(id) { const { pathname } = parseModuleId(id); const ext = extname(pathname); if (ext === ".vue") { return "vue"; } if (!JS_RE$1.test(ext)) { return null; } return ext.endsWith("x") ? "tsx" : "ts"; } function uniqueBy(arr, key) { if (arr.length < 2) { return arr; } const res = []; const seen = /* @__PURE__ */ new Set(); for (const item of arr) { if (seen.has(item[key])) { continue; } seen.add(item[key]); res.push(item); } return res; } const QUOTE_RE = /["']/g; const EXTENSION_RE = /\b\.\w+$/g; const SX_RE = /\.[tj]sx$/; function createPagesContext(options = {}) { const modes = options.shouldUseServerComponents ? ["server", "client"] : ["client"]; const treeOptions = { roots: options.roots, modes, warn: (msg) => logger.warn(msg) }; const emitOptions = { onDuplicateRouteName: (_name, file, existingFile) => { logger.warn(`Route name generated for \`${file}\` is the same as \`${existingFile}\`. You may wish to set a custom name using \`definePageMeta\` within the page file.`); }, attrs: { mode: modes } }; const compiledParse = compileParsePath(treeOptions); let tree = buildTree([], treeOptions); const trackedFiles = /* @__PURE__ */ new Set(); return { emit() { return toVueRouter4(tree, emitOptions); }, addFile(filePath, priority = 0) { addFile(tree, { path: filePath, priority }, compiledParse); trackedFiles.add(filePath); }, removeFile(filePath) { const removed = removeFile(tree, filePath); if (removed) { trackedFiles.delete(filePath); } return removed; }, rebuild(files) { tree = buildTree(files, treeOptions); trackedFiles.clear(); for (const f of files) { trackedFiles.add(f.path); } }, trackedFiles }; } async function resolvePagesRoutes(pattern, nuxt = useNuxt(), ctx) { const pagesDirs = getLayerDirectories(nuxt).map((d) => d.appPages); const inputFiles = []; for (let priority = 0; priority < pagesDirs.length; priority++) { const dir = pagesDirs[priority]; const files = await resolveFiles(dir, pattern); for (const file of files) { inputFiles.push({ path: file, priority }); } } let pages; if (ctx) { ctx.rebuild(inputFiles); pages = ctx.emit(); } else { const oneShot = createPagesContext({ roots: pagesDirs, shouldUseServerComponents: !!nuxt.options.experimental.componentIslands }); oneShot.rebuild(inputFiles); pages = oneShot.emit(); } return augmentAndResolve(pages, ctx?.trackedFiles ?? new Set(inputFiles.map((f) => f.path)), nuxt); } async function augmentAndResolve(pages, trackedFiles, nuxt = useNuxt()) { const shouldAugment = nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages; if (shouldAugment === false) { await nuxt.callHook("pages:extend", pages); return pages; } const extraPageMetaExtractionKeys = nuxt.options?.experimental?.extraPageMetaExtractionKeys || []; const augmentCtx = { extraExtractionKeys: /* @__PURE__ */ new Set([ "middleware", ...extraPageMetaExtractionKeys ]), fullyResolvedPaths: trackedFiles }; if (shouldAugment === "after-resolve") { await nuxt.callHook("pages:extend", pages); await augmentPages(pages, nuxt.vfs, augmentCtx); } else { const augmentedPages = await augmentPages(pages, nuxt.vfs, augmentCtx); await nuxt.callHook("pages:extend", pages); await augmentPages(pages, nuxt.vfs, { pagesToSkip: augmentedPages, ...augmentCtx }); augmentedPages?.clear(); } await nuxt.callHook("pages:resolved", pages); return pages; } async function augmentPages(routes, vfs, ctx = {}) { ctx.augmentedPages ??= /* @__PURE__ */ new Set(); for (const route of routes) { if (route.file && !ctx.pagesToSkip?.has(route.file)) { const fileContent = vfs[route.file] ?? fs.readFileSync(ctx.fullyResolvedPaths?.has(route.file) ? route.file : await resolvePath(route.file), "utf-8"); const routeMeta = getRouteMeta(fileContent, route.file, ctx.extraExtractionKeys); if (route.meta) { routeMeta.meta = defu({}, routeMeta.meta, route.meta); } if (route.rules) { routeMeta.rules = defu({}, routeMeta.rules, route.rules); } Object.assign(route, routeMeta); ctx.augmentedPages.add(route.file); } if (route.children && route.children.length > 0) { await augmentPages(route.children, vfs, ctx); } } return ctx.augmentedPages; } const SFC_SCRIPT_RE = /<script(?=\s|>)(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script\s*>/gi; function extractScriptContent(sfc) { const contents = []; for (const match of sfc.matchAll(SFC_SCRIPT_RE)) { if (match?.groups?.content) { contents.push({ loader: match.groups.attrs && /[tj]sx/.test(match.groups.attrs) ? "tsx" : "ts", code: match.groups.content.trim() }); } } return contents; } const PAGE_EXTRACT_RE = /(definePageMeta|defineRouteRules)\([\s\S]*?\)/g; const defaultExtractionKeys = ["name", "path", "props", "alias", "redirect", "middleware"]; const DYNAMIC_META_KEY = "__nuxt_dynamic_meta_key"; const pageContentsCache = {}; const extractCache = {}; function getRouteMeta(contents, absolutePath, extraExtractionKeys = /* @__PURE__ */ new Set()) { if (!(absolutePath in pageContentsCache) || pageContentsCache[absolutePath] !== contents) { pageContentsCache[absolutePath] = contents; delete extractCache[absolutePath]; } if (absolutePath in extractCache && extractCache[absolutePath]) { return klona(extractCache[absolutePath]); } const loader = getLoader(absolutePath); const scriptBlocks = !loader ? null : loader === "vue" ? extractScriptContent(contents) : [{ code: contents, loader }]; if (!scriptBlocks) { extractCache[absolutePath] = {}; return {}; } const extractedData = {}; const extractionKeys = /* @__PURE__ */ new Set([...defaultExtractionKeys, ...extraExtractionKeys]); for (const script of scriptBlocks) { const found = {}; for (const macro of script.code.matchAll(PAGE_EXTRACT_RE)) { found[macro[1]] = false; } if (Object.keys(found).length === 0) { continue; } const dynamicProperties = /* @__PURE__ */ new Set(); parseAndWalk(script.code, absolutePath.replace(/\.\w+$/, "." + script.loader), (node) => { if (node.type !== "ExpressionStatement" || node.expression.type !== "CallExpression" || node.expression.callee.type !== "Identifier") { return; } const fnName = node.expression.callee.name; if (fnName in found === false || found[fnName] !== false) { return; } found[fnName] = true; let code = script.code; let pageExtractArgument = node.expression.arguments[0]; if (/tsx?/.test(script.loader)) { const transformed = transformSync(absolutePath, script.code.slice(node.start, node.end), { lang: script.loader }); if (transformed.errors.length) { for (const error of transformed.errors) { logger.warn(`Error while transforming \`${fnName}()\`` + error.codeframe); } return; } pageExtractArgument = parseSync("", transformed.code, { lang: "js" }).program.body[0].expression.arguments[0]; code = transformed.code; } if (pageExtractArgument?.type !== "ObjectExpression") { logger.warn(`\`${fnName}\` must be called with an object literal (reading \`${absolutePath}\`), found ${pageExtractArgument?.type} instead.`); return; } if (fnName === "defineRouteRules") { const { value, serializable } = isSerializable(code, pageExtractArgument); if (!serializable) { logger.warn(`\`${fnName}\` must be called with a serializable object literal (reading \`${absolutePath}\`).`); return; } extractedData.rules = value; return; } if (fnName === "definePageMeta") { for (const key of extractionKeys) { const property = pageExtractArgument.properties.find((property2) => property2.type === "Property" && property2.key.type === "Identifier" && property2.key.name === key); if (!property) { continue; } const { value, serializable } = isSerializable(code, property.value); if (!serializable) { logger.debug(`Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`); dynamicProperties.add(extraExtractionKeys.has(key) ? "meta" : key); continue; } if (extraExtractionKeys.has(key)) { extractedData.meta ??= {}; extractedData.meta[key] = value; } else { extractedData[key] = value; } } for (const property of pageExtractArgument.properties) { if (property.type !== "Property") { continue; } const isIdentifierOrLiteral = property.key.type === "Literal" || property.key.type === "Identifier"; if (!isIdentifierOrLiteral) { continue; } const name = property.key.type === "Identifier" ? property.key.name : String(property.value); if (!extractionKeys.has(name)) { dynamicProperties.add("meta"); break; } } if (dynamicProperties.size) { extractedData.meta ??= {}; extractedData.meta[DYNAMIC_META_KEY] = dynamicProperties; } } }); } extractCache[absolutePath] = extractedData; return klona(extractedData); } function serializeRouteValue(value, skipSerialisation = false) { if (skipSerialisation || value === void 0) { return void 0; } return JSON.stringify(value); } function normalizeComponent(page, pageImport, routeName) { if (page.mode === "server") { return `() => createIslandPage(${routeName})`; } if (page.mode === "client") { return `() => createClientPage(${pageImport})`; } return pageImport; } function normalizeComponentWithName(page, isSyncImport, pageImportName, pageImport, routeName, metaRouteName) { if (isSyncImport) { return `Object.assign(${pageImportName}, { __name: ${metaRouteName} })`; } if (page.mode === "server") { return `() => createIslandPage(${routeName})`; } if (page.mode === "client") { return `() => createClientPage(${pageImport}).then((c) => Object.assign(c, { __name: ${metaRouteName} }))`; } return `${pageImport}.then((m) => Object.assign(m.default, { __name: ${metaRouteName} }))`; } function normalizeRoutes(routes, metaImports = /* @__PURE__ */ new Set(), options) { const nuxt = useNuxt(); return { imports: metaImports, routes: genArrayFromRaw(routes.map((page) => { const markedDynamic = page.meta?.[DYNAMIC_META_KEY] ?? /* @__PURE__ */ new Set(); const metaFiltered = {}; let skipMeta = true; for (const key in page.meta || {}) { if (key !== DYNAMIC_META_KEY && page.meta[key] !== void 0) { skipMeta = false; metaFiltered[key] = page.meta[key]; } } const skipAlias = toArray(page.alias).every((val) => !val); const route = { path: serializeRouteValue(page.path), props: serializeRouteValue(page.props), name: serializeRouteValue(page.name), meta: serializeRouteValue(metaFiltered, skipMeta), alias: serializeRouteValue(toArray(page.alias), skipAlias), redirect: serializeRouteValue(page.redirect) }; for (const key of [...defaultExtractionKeys, "meta"]) { if (route[key] === void 0) { delete route[key]; } } if (page.children?.length) { route.children = normalizeRoutes(page.children, metaImports, options).routes; } if (!page.file) { return route; } const file = normalize(page.file); const pageImportName = genSafeVariableName(filename(file) + hash(file).replace(/-/g, "_")); const metaImportName = pageImportName + "Meta"; metaImports.add(genImport(`${file}?macro=true`, [{ name: "default", as: metaImportName }])); if (page._sync) { metaImports.add(genImport(file, [{ name: "default", as: pageImportName }])); } const isSyncImport = page._sync && page.mode !== "client"; const pageImport = isSyncImport ? pageImportName : genDynamicImport(file); const metaRouteName = `${metaImportName}?.name ?? ${route.name}`; const component = nuxt.options.experimental.normalizePageNames ? normalizeComponentWithName(page, isSyncImport, pageImportName, pageImport, route.name, metaRouteName) : normalizeComponent(page, pageImport, route.name); const metaRoute = { name: metaRouteName, path: `${metaImportName}?.path ?? ${route.path}`, props: `${metaImportName}?.props ?? ${route.props ?? false}`, meta: `${metaImportName} || {}`, alias: `${metaImportName}?.alias || []`, redirect: `${metaImportName}?.redirect`, component }; if (page.mode === "server") { metaImports.add(` let _createIslandPage async function createIslandPage (name) { _createIslandPage ||= await import(${JSON.stringify(options?.serverComponentRuntime)}).then(r => r.createIslandPage) return _createIslandPage(name) };`); } else if (page.mode === "client") { metaImports.add(` let _createClientPage async function createClientPage(loader) { _createClientPage ||= await import(${JSON.stringify(options?.clientComponentRuntime)}).then(r => r.createClientPage) return _createClientPage(loader); }`); } if (route.children) { metaRoute.children = route.children; } if (route.meta) { metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`; } if (options?.overrideMeta) { for (const key of ["name", "path"]) { if (markedDynamic.has(key)) { continue; } metaRoute[key] = route[key] ?? `${metaImportName}?.${key}`; } for (const key of ["meta", "alias", "redirect", "props"]) { if (markedDynamic.has(key)) { continue; } if (route[key] == null) { delete metaRoute[key]; continue; } metaRoute[key] = route[key]; } } else { if (route.alias != null) { metaRoute.alias = `${route.alias}.concat(${metaImportName}?.alias || [])`; } if (route.redirect != null) { metaRoute.redirect = route.redirect; } } return metaRoute; })) }; } const PATH_TO_NITRO_GLOB_RE = /\/[^:/]*:\w.*$/; function pathToNitroGlob(path) { if (!path) { return null; } if (path.indexOf(":") !== path.lastIndexOf(":")) { return null; } return path.replace(PATH_TO_NITRO_GLOB_RE, "/**"); } function resolveRoutePaths(page, parent = "/") { return [ joinURL(parent, page.path), ...page.children?.flatMap((child) => resolveRoutePaths(child, joinURL(parent, page.path))) || [] ]; } function isSerializable(code, node) { if (node.type === "ObjectExpression") { const valueString = code.slice(node.start, node.end); try { return { value: JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {})), serializable: true }; } catch { return { serializable: false }; } } if (node.type === "ArrayExpression") { const values = []; for (const element of node.elements) { if (!element) { continue; } const { serializable, value } = isSerializable(code, element); if (!serializable) { return { serializable: false }; } values.push(value); } return { value: values, serializable: true }; } if (node.type === "Literal" && (typeof node.value === "string" || typeof node.value === "boolean" || typeof node.value === "number" || node.value === null)) { return { value: node.value, serializable: true }; } return { serializable: false }; } function toRou3Patterns(pages, prefix = "/") { const routes = []; for (const page of pages) { const path = page.path.replace(/\([^)]*\)/g, "").replace(/:(\w+)\*.*/g, (_, name) => `**:${name}`).replace(/:([^/*]*)/g, (_, name) => `:${name.replace(/\W/g, (r) => r === "?" ? "" : "_")}`); routes.push(joinURL(prefix, path)); if (page.children) { routes.push(...toRou3Patterns(page.children, joinURL(prefix, path))); } } return routes; } function globRouteRulesFromPages(pages, paths = {}, prefix = "") { for (const page of pages) { if (page.rules) { if (Object.keys(page.rules).length) { const glob = pathToNitroGlob(prefix + page.path); if (glob) { paths[glob] = page.rules; } } delete page.rules; } if (page.children?.length) { globRouteRulesFromPages(page.children, paths, prefix + page.path + "/"); } } return paths; } function removePagesRules(routes) { for (const route of routes) { delete route.rules; if (route.children?.length) { removePagesRules(route.children); } } } const HAS_MACRO_RE = /\bdefinePageMeta\s*\(\s*/; const CODE_EMPTY = ` const __nuxt_page_meta = null export default __nuxt_page_meta `; const CODE_DEV_EMPTY = ` const __nuxt_page_meta = {} export default __nuxt_page_meta `; const CODE_HMR = ` // Vite if (import.meta.hot) { import.meta.hot.accept(mod => { Object.assign(__nuxt_page_meta, mod) }) } // webpack if (import.meta.webpackHot) { import.meta.webpackHot.accept((err) => { if (err) { window.location = window.location.href } }) }`; const PageMetaPlugin = (options = {}) => createUnplugin(() => { return { name: "nuxt:pages-macros-transform", enforce: "post", transform: { filter: { id: { include: /[?&]macro=true\b/, exclude: [/(?:\?|%3F).*type=(?:style|template)/] }, code: { include: [ HAS_MACRO_RE, /\bfrom\s+["'][^"'?]*\?[^"']*type=script[^"']*["']/, /export\s+\{\s*default\s*\}\s+from\s+["'][^"'?]*\?[^"']*type=script[^"']*["']/, /^(?!.*__nuxt_page_meta)(?!.*export\s+\{\s*default\s*\})(?!.*\bdefinePageMeta\s*\()[\s\S]*$/ ] } }, handler(code, id) { const query = parseMacroQuery(id); if (query.type && query.type !== "script") { return; } const s = new MagicString(code); function result() { if (s.hasChanged()) { return { code: s.toString(), map: options.sourcemap ? s.generateMap({ hires: true }) : void 0 }; } } const hasMacro = HAS_MACRO_RE.test(code); const imports = findStaticImports(code); const scriptImport = imports.find((i) => parseMacroQuery(i.specifier).type === "script"); if (scriptImport) { const reorderedQuery = rewriteQuery(scriptImport.specifier); const quotedSpecifier = getQuotedSpecifier(scriptImport.code)?.replace(scriptImport.specifier, reorderedQuery) ?? JSON.stringify(reorderedQuery); s.overwrite(0, code.length, `export { default } from ${quotedSpecifier}`); return result(); } const currentExports = findExports(code); for (const match of currentExports) { if (match.type !== "default" || !match.specifier) { continue; } const reorderedQuery = rewriteQuery(match.specifier); const quotedSpecifier = getQuotedSpecifier(match.code)?.replace(match.specifier, reorderedQuery) ?? JSON.stringify(reorderedQuery); s.overwrite(0, code.length, `export { default } from ${quotedSpecifier}`); return result(); } if (!hasMacro && !code.includes("export { default }") && !code.includes("__nuxt_page_meta")) { if (!code) { s.append(options.dev ? CODE_DEV_EMPTY + CODE_HMR : CODE_EMPTY); const { pathname } = parseModuleId(id); logger.error(`The file \`${pathname}\` is not a valid page as it has no content.`); } else { s.overwrite(0, code.length, options.dev ? CODE_DEV_EMPTY + CODE_HMR : CODE_EMPTY); } return result(); } const importMap = /* @__PURE__ */ new Map(); const addedImports = /* @__PURE__ */ new Set(); for (const i of imports) { const parsed = parseStaticImport(i); for (const name of [ parsed.defaultImport, ...Object.values(parsed.namedImports || {}), parsed.namespacedImport ].filter(Boolean)) { importMap.set(name, i); } } function isStaticIdentifier(name) { return !!(name && importMap.has(name)); } function addImport(name) { if (!isStaticIdentifier(name)) { return; } const importValue = importMap.get(name).code.trim(); if (!addedImports.has(importValue)) { addedImports.add(importValue); } } const declarationNodes = []; const addedDeclarations = /* @__PURE__ */ new Set(); function addDeclaration(node) { const codeSectionKey = `${resolveStart(node)}-${resolveEnd(node)}`; if (addedDeclarations.has(codeSectionKey)) { return; } addedDeclarations.add(codeSectionKey); declarationNodes.push(node); } function addImportOrDeclaration(name, node) { if (isStaticIdentifier(name)) { addImport(name); } else { const declaration = scopeTracker.getDeclaration(name); if (declaration && declaration !== node) { processDeclaration(declaration); } } } const scopeTracker = new ScopeTracker({ preserveExitedScopes: true }); function processDeclaration(scopeTrackerNode) { if (scopeTrackerNode?.type === "Variable") { addDeclaration(scopeTrackerNode); for (const decl of scopeTrackerNode.variableNode.declarations) { if (!decl.init) { continue; } walk(decl.init, { enter: (node, parent) => { if (node.type === "AwaitExpression") { logger.error(`Await expressions are not supported in definePageMeta. File: '${id}'`); throw new Error("await in definePageMeta"); } if (isBindingIdentifier(node, parent) || node.type !== "Identifier") { return; } addImportOrDeclaration(node.name, scopeTrackerNode); } }); } } else if (scopeTrackerNode?.type === "Function") { if (scopeTrackerNode.node.type === "ArrowFunctionExpression") { return; } const name = scopeTrackerNode.node.id?.name; if (!name) { return; } addDeclaration(scopeTrackerNode); const undeclaredIdentifiers = getUndeclaredIdentifiersInFunction(scopeTrackerNode.node); for (const name2 of undeclaredIdentifiers) { addImportOrDeclaration(name2); } } } const { program: ast } = parseAndWalk(code, id, { scopeTracker, parseOptions: { lang: query.lang ?? "ts" } }); scopeTracker.freeze(); let instances = 0; walk(ast, { scopeTracker, enter: (node) => { if (node.type !== "CallExpression" || node.callee.type !== "Identifier") { return; } if (!("name" in node.callee) || node.callee.name !== "definePageMeta") { return; } instances++; const meta = node.arguments[0]; if (!meta) { return; } const metaCode = code.slice(meta.start, meta.end); const m = new MagicString(metaCode); if (meta.type === "ObjectExpression") { const omitProp = (prop, i) => { const nextProperty = meta.properties[i + 1]; if (nextProperty) { m.overwrite(prop.start - meta.start, nextProperty.start - meta.start, ""); } else if (code[prop.end] === ",") { m.overwrite(prop.start - meta.start, prop.end - meta.start + 1, ""); } else { m.overwrite(prop.start - meta.start, prop.end - meta.start, ""); } }; for (let i = 0; i < meta.properties.length; i++) { const prop = meta.properties[i]; if (prop.type !== "Property" || prop.key.type !== "Identifier") { continue; } if (options.extractedKeys?.includes(prop.key.name)) { const { serializable } = isSerializable(metaCode, prop.value); if (serializable) { omitProp(prop, i); } } else if (prop.key.name === "layout" && prop.value.type === "ObjectExpression") { for (const layoutProp of prop.value.properties) { if (layoutProp.type !== "Property" || layoutProp.key.type !== "Identifier") { continue; } if (layoutProp.key.name === "name") { m.appendLeft( prop.start - meta.start, `layout: ${code.slice(layoutProp.value.start, layoutProp.value.end)}, ` ); } else if (layoutProp.key.name === "props") { m.appendLeft( prop.start - meta.start, `layoutProps: ${code.slice(layoutProp.value.start, layoutProp.value.end)}, ` ); } } omitProp(prop, i); } } } const definePageMetaScope = scopeTracker.getCurrentScope(); walk(meta, { scopeTracker, enter(node2, parent) { if (isBindingIdentifier(node2, parent) || node2.type !== "Identifier") { return; } const declaration = scopeTracker.getDeclaration(node2.name); if (declaration) { if (declaration.isUnderScope(definePageMetaScope) && (scopeTracker.isCurrentScopeUnder(declaration.scope) || resolveStart(declaration) < node2.start)) { return; } } if (isStaticIdentifier(node2.name)) { addImport(node2.name); } else if (declaration) { processDeclaration(declaration); } } }); const importStatements = Array.from(addedImports).join("\n"); const declarations = declarationNodes.sort((a, b) => resolveStart(a) - resolveStart(b)).map((node2) => code.slice(resolveStart(node2), resolveEnd(node2))).join("\n"); const extracted = [ importStatements, declarations, `const __nuxt_page_meta = ${m.toString() || "null"} export default __nuxt_page_meta` + (options.dev ? CODE_HMR : "") ].join("\n"); s.overwrite(0, code.length, extracted.trim()); } }); if (instances > 1) { throw new Error("Multiple `definePageMeta` calls are not supported. File: " + id.replace(/\?.+$/, "")); } if (!s.hasChanged() && !code.includes("__nuxt_page_meta")) { s.overwrite(0, code.length, options.dev ? CODE_DEV_EMPTY + CODE_HMR : CODE_EMPTY); } return result(); } }, vite: { handleHotUpdate: { order: "post", handler: ({ file, modules, server }) => { if (options.routesPath && options.isPage?.(file)) { const macroModule = server.moduleGraph.getModuleById(file + "?macro=true"); const routesModule = server.moduleGraph.getModuleById("virtual:nuxt:" + encodeURIComponent(options.routesPath)); return [ ...modules, ...macroModule ? [macroModule] : [], ...routesModule ? [routesModule] : [] ]; } } } } }; }); const QUERY_START_RE = /^\?/; const MACRO_RE = /&macro=true/; function rewriteQuery(id) { return id.replace(/\?.+$/, (r) => "?macro=true&" + r.replace(QUERY_START_RE, "").replace(MACRO_RE, "")); } const MACRO_QUERY_RE$1 = /[?&]macro=true(?:&|$)/; const TYPE_PARAM_RE = /[?&]type=([^?&]+)/; const LANG_PARAM_RE = /[?&]lang=([^?&]+)/; function parseMacroQuery(id) { const { search } = parseModuleId(id); const query = { type: TYPE_PARAM_RE.exec(search)?.[1], lang: LANG_PARAM_RE.exec(search)?.[1] ?? void 0 }; if (MACRO_QUERY_RE$1.test(search)) { query.macro = "true"; } return query; } const QUOTED_SPECIFIER_RE = /(["']).*\1/; function getQuotedSpecifier(id) { return id.match(QUOTED_SPECIFIER_RE)?.[0]; } function resolveStart(node) { return "fnNode" in node ? node.fnNode.start : node.start; } function resolveEnd(node) { return "fnNode" in node ? node.fnNode.end : node.end; } const INJECTION_SINGLE_RE = /\bthis\.\$route\b|\b_ctx\.\$route\b/; const RouteInjectionPlugin = (nuxt) => createUnplugin(() => { return { name: "nuxt:route-injection-plugin", enforce: "post", transformInclude(id) { return isVue(id, { type: ["template", "script"] }); }, transform: { filter: { code: { include: INJECTION_SINGLE_RE, exclude: [ `_ctx._.provides[__nuxt_route_symbol`, "this._.provides[__nuxt_route_symbol" ] } }, handler(code, id) { const s = new MagicString(code); parseAndWalk(code, id, (node) => { if (node.type !== "MemberExpression") { return; } if (node.object.type === "ThisExpression" && node.property.type === "Identifier" && node.property.name === "$route") { s.overwrite(node.start, node.end, "(this._.provides[__nuxt_route_symbol] || this.$route)"); return; } if (node.object.type === "Identifier" && node.object.name === "_ctx" && node.property.type === "Identifier" && node.property.name === "$route") { s.overwrite(node.start, node.end, "(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)"); } }); if (s.hasChanged()) { s.prepend("import { PageRouteSymbol as __nuxt_route_symbol } from '#app/components/injections';\n"); return { code: s.toString(), map: nuxt.options.sourcemap.client || nuxt.options.sourcemap.server ? s.generateMap({ hires: true }) : void 0 }; } } } }; }); const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/; const pagesImportPresets = [ { imports: ["definePageMeta"], from: "#app/composables/pages" }, { imports: ["PageMeta"], from: "#app/composables/pages", type: true }, { imports: ["useLink"], from: "vue-router" } ]; const routeRulesPresets = [ { imports: ["defineRouteRules"], from: "#app/composables/pages" } ]; async function resolveRouterOptions(nuxt, builtInRouterOptions) { const context = { files: [] }; for (const layer of nuxt.options._layers) { const path = await findPath(resolve(layer.config.srcDir, layer.config.dir?.app || "app", "router.options")); if (path) { context.files.unshift({ path }); } } context.files.unshift({ path: builtInRouterOptions, optional: true }); await nuxt.callHook("pages:routerOptions", context); return context.files; } const pagesModule = defineNuxtModule({ meta: { name: "nuxt:pages", configKey: "pages" }, defaults: (nuxt) => ({ enabled: typeof nuxt.options.pages === "boolean" ? nuxt.options.pages : void 0, pattern: `**/*{${nuxt.options.extensions.join(",")}}` }), async setup(_options, nuxt) { const runtimeDir = resolve(distDir, "pages/runtime"); const options = typeof _options === "boolean" ? { enabled: _options ?? nuxt.options.pages, pattern: `**/*{${nuxt.options.extensions.join(",")}}` } : { ..._options }; options.pattern = Array.isArray(options.pattern) ? [...new Set(options.pattern)] : options.pattern; let inlineRulesCache = {}; let updateRouteConfig; if (nuxt.options.experimental.inlineRouteRules) { nuxt.hook("nitro:init", (nitro) => { updateRouteConfig = async (inlineRules) => { if (!isEqual(inlineRulesCache, inlineRules)) { await nitro.updateConfig({ routeRules: defu(inlineRules, nitro.options._config.routeRules) }); inlineRulesCache = inlineRules; } }; }); } const useExperimentalTypedPages = nuxt.options.experimental.typedPages; const builtInRouterOptions = await findPath(resolve(runtimeDir, "router.options")) || resolve(runtimeDir, "router.options"); const pagesDirs = getLayerDirectories(nuxt).map((dirs) => dirs.appPages); const pagesCtx = nuxt.options.dev ? createPagesContext({ roots: pagesDirs, shouldUseServerComponents: !!nuxt.options.experimental.componentIslands }) : void 0; const isPagePattern = picomatch( Array.isArray(options.pattern) ? options.pattern : [options.pattern] ); const handleRouteRules = async (pages) => { if (nuxt.options.experimental.inlineRouteRules) { const routeRules = globRouteRulesFromPages(pages); await updateRouteConfig?.(routeRules); } else { removePagesRules(pages); } }; const resolvePagesRoutes$1 = async (pattern, nuxt2) => { const pages = await resolvePagesRoutes(pattern, nuxt2, pagesCtx); await handleRouteRules(pages); return pages; }; const augmentAndResolvePages = async (pages, trackedFiles, nuxt2) => { const resolved = await augmentAndResolve(pages, trackedFiles, nuxt2); await handleRouteRules(resolved); return resolved; }; nuxt.options.alias["#vue-router"] = "vue-router"; const routerPath = await resolveTypePath("vue-router", "", nuxt.options.modulesDir) || "vue-router"; nuxt.hook("prepare:types", ({ tsConfig }) => { tsConfig.compilerOptions ||= {}; tsConfig.compilerOptions.paths ||= {}; tsConfig.compilerOptions.paths["#vue-router"] = [routerPath]; delete tsConfig.compilerOptions.paths["#vue-router/*"]; }); const isNonEmptyDir = (dir) => existsSync(dir) && readdirSync(dir).length; const userPreference = options.enabled; const isPagesEnabled = async () => { if (typeof userPreference === "boolean") { return userPreference; } const routerOptionsFiles = await resolveRouterOptions(nuxt, builtInRouterOptions); if (routerOptionsFiles.filter((p) => !p.optional).length > 0) { return true; } if (pagesDirs.some((dir) => isNonEmptyDir(dir))) { return true; } const pages = await resolvePagesRoutes$1(options.pattern, nuxt); if (pages.length) { if (nuxt.apps.default) { nuxt.apps.default.pages = pages; } return true; } return false; }; options.enabled = await isPagesEnabled(); nuxt.options.pages = options; Object.defineProperty(nuxt.options.pages, "toString", { enumerable: false, get: () => () => options.enabled }); if (nuxt.options.dev && options.enabled) { addPlugin(resolve(runtimeDir, "plugins/check-if-page-unused")); } nuxt.hook("app:templates", (app) => { if (!nuxt.options.ssr && app.pages?.some((p) => p.mode === "server")) { logger.warn("Using server pages with `ssr: false` is not supported with auto-detected component islands. Set `experimental.componentIslands` to `true`."); } }); const restartPaths = nuxt.options._layers.flatMap((layer) => { const pagesDir = (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : layer.config.dir)?.pages || "pages"; return [ resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.app || "app", "router.options.ts"), resolve(layer.config.srcDir || layer.cwd, pagesDir) ]; }); nuxt.hooks.hook("builder:watch", async (event, relativePath) => { const path = resolve(nuxt.options.srcDir, relativePath); if (restartPaths.some((p) => p === path || path.startsWith(p + "/"))) { const newSetting = await isPagesEnabled(); if (options.enabled !== newSetting) { logger.info("Pages", newSetting ? "enabled" : "disabled"); return nuxt.callHook("restart"); } } }); if (!options.enabled) { addPlugin(resolve(distDir, "app/plugins/router")); addTemplate({ filename: "pages.mjs", getContents: () => [ "export { useRoute } from '#app/composables/router'", "export const START_LOCATION = Symbol('router:start-location')" ].join("\n") }); addTemplate({ filename: "router.options.mjs", getContents: () => { return [ "export const hashMode = false", "export default {}" ].join("\n"); } }); addTypeTemplate({ filename: "types/middleware.d.ts", getContents: () => [ "declare module 'nitropack/types' {", " interface NitroRouteConfig {", " appMiddleware?: string | string[] | Record<string, boolean>", " }", "}", "declare module 'nitropack' {", " interface NitroRouteConfig {", " appMiddleware?: string | string[] | Record<string, boolean>", " }", "}", "export {}" ].join("\n") }, { nuxt: true, nitro: true, node: true }); addComponent({ name: "NuxtPage", priority: 10, // built-in that we do not expect the user to override filePath: resolve(distDir, "pages/runtime/page-placeholder") }); nuxt.hook("nitro:init", (nitro) => { if (nuxt.options.dev || !nuxt.options.ssr || !nitro.options.static || !nitro.options.prerender.crawlLinks) { return; } nitro.options.prerender.routes.push("/"); }); return; } if (useExperimentalTypedPages) { const declarationFile = resolve(nuxt.options.buildDir, "types/typed-router.d.ts"); const typedRouterOptions = { routesFolder: [], dts: declarationFile, logs: nuxt.options.debug && nuxt.options.debug.router, async beforeWriteFiles(rootPage) { for (const child of rootPage.children) { child.delete(); } const pages = nuxt.apps.default?.pages || await resolvePagesRoutes$1(options.pattern, nuxt); if (nuxt.apps.default) { nuxt.apps.default.pages = pages; } const addedPagePaths = /* @__PURE__ */ new Set(); function addPage(parent, page, basePath = "") { const absolutePagePath = joinURL(basePath, page.path); const route = addedPagePaths.has(absolutePagePath) ? parent : page.path[0] === "/" ? rootPage.insert(page.path, page.file) : parent.insert(page.path, page.f