import process from "node:process"; import fs, { existsSync, mkdirSync, promises, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { mkdir, open, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises"; import { createHash, randomUUID } from "node:crypto"; import { AsyncLocalStorage } from "node:async_hooks"; import { basename, dirname, extname, isAbsolute, join, normalize, parse, relative, resolve } from "pathe"; import { createDebugger, createHooks } from "hookable"; import ignore from "ignore"; import { addBuildPlugin, addComponent, addImportsSources, addPlugin, addPluginTemplate, addRouteMiddleware, addRspackPlugin, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, createIsIgnored, createResolver, defineNuxtModule, directoryToURL, ensureDependencyInstalled, findPath, getAddDependencyCommand, getLayerDirectories, importModule, isIgnored, loadNuxtConfig, normalizeModuleTranspilePath, normalizePlugin, normalizeTemplate, nuxtCtx, packageName, recoverThrottledChanges, resolveAlias, resolveDeclarationPath, resolveFiles, resolveIgnorePatterns, resolveModuleWithOptions, resolvePath, resolveTypePaths, runWithNuxtContext, setBuildOutput, tryUseNitro, tryUseNuxt, updateTemplates, useLogger, useNuxt, useTerminal, writeTypes } from "@nuxt/kit"; import { buildDiagnostics, bundlerDiagnostics, componentDiagnostics, configDiagnostics, createServerBuild, hasOwnSubpathImport, headDiagnostics, installModules, loadJiti, pageDiagnostics, pluginDiagnostics, trackPendingTemplate, useServerBuild } from "@nuxt/kit/internal"; import { readPackageJSON, resolvePackageJSON } from "pkg-types"; import { hash, isEqual, serialize } from "ohash"; import onChange from "on-change"; import { formatDate, resolveCompatibilityDatesFromEnv } from "compatx"; import escapeStringRegexp from "escape-string-regexp"; import { joinURL, withLeadingSlash, withTrailingSlash, withoutLeadingSlash, withoutTrailingSlash } from "ufo"; import { ImpoundPlugin } from "impound"; import { defu } from "defu"; import { coerce, satisfies } from "verkit"; import { hasTTY, isCI, provider } from "std-env"; import { genArrayFromRaw, genDynamicImport, genDynamicTypeImport, genExport, genImport, genInlineTypeImport, genObjectFromRawEntries, genObjectKey, genSafeVariableName, genString, genTypeImport } from "knitwork"; import { resolveModulePath } from "exsolve"; import { link } from "clickable-path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { filename, resolveAlias as resolveAlias$1, reverseResolveAlias } from "pathe/utils"; import picomatch from "picomatch"; import { addFile, buildTree, compileParsePath, removeFile, rou3PatternToURLPattern, toVueRouter4, vueRouterToRou3 } from "unrouting"; import { klona } from "klona"; import { ScopeTracker, getUndeclaredIdentifiersInFunction, isReferenceIdentifier, parseAndWalk, walk } from "oxc-walker"; import { camelCase, kebabCase, pascalCase, splitByCase } from "scule"; import { addRoute, createRouter, findAllRoutes } from "rou3"; import { compileRouterToString } from "rou3/compiler"; import { createUnplugin } from "unplugin"; import MagicString from "magic-string"; import { generateTransform, rolldownString } from "rolldown-string"; import { minifySync, parseSync, transformSync } from "rolldown/utils"; import { streamingIifeCode } from "unhead/stream/iife"; import { unheadVueComposablesImports } from "@unhead/vue"; import { createUnimport, defineUnimportPreset, scanDirExports, toExports, toTypeDeclarationFile, toTypeReExports } from "unimport"; import { glob } from "tinyglobby"; import { performance } from "node:perf_hooks"; import { Diagnostic } from "nostics"; import { generateTypes, resolveSchema } from "untyped"; import { ELEMENT_NODE, parse as parse$1, walk as walk$1, walkSync } from "ultrahtml"; import { consola } from "consola"; import { createTar, parseTar } from "nanotar"; import { compileRoutes } from "fetchdts/compiler"; import { createTransformer, getTransformFilter } from "unctx/transform"; import { colors } from "consola/utils"; import { watch } from "chokidar"; import { debounce } from "perfect-debounce"; //#region \0rolldown/runtime.js var __defProp = Object.defineProperty; var __exportAll = (all, no_symbols) => { let target = {}; for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" }); return target; }; //#endregion //#region src/core/features.ts const installPrompts = /* @__PURE__ */ new Set(); async function installNuxtModule(name, options) { if (installPrompts.has(name)) return; installPrompts.add(name); const nuxt = useNuxt(); const rootDir = options?.rootDir || nuxt.options.rootDir; const searchPaths = options?.searchPaths || nuxt.options.modulesDir; for (const parent of searchPaths) if (await resolvePackageJSON(name, { parent }).catch(() => null)) return true; configDiagnostics.NUXT_B5011({ name }); if (isCI) return false; const terminal = useTerminal(); if (options?.prompt === true || options?.prompt !== false && provider !== "stackblitz") { if (await terminal.prompt(`Do you want to install ${name} package?`, { type: "confirm", name: "confirm", initial: true }) !== true) return false; } const task = terminal.startTask(`Installing ${name}...`); try { const { runCommand } = await import("@nuxt/cli"); await runCommand("module", [ "add", name, "--cwd", rootDir ]); task.stop(`Installed ${name}`); return true; } catch (err) { task.stop(void 0, "failure"); buildDiagnostics.NUXT_B1004({ installCommand: await getAddDependencyCommand(name, rootDir), cause: err }); return false; } } //#endregion //#region src/dirs.ts let _distDir = dirname(fileURLToPath(import.meta.url)); if (/(?:chunks|shared)$/.test(_distDir)) _distDir = dirname(_distDir); const distDir = _distDir; const pkgDir = resolve(distDir, ".."); //#endregion //#region src/utils.ts /** @since 3.9.0 */ function toArray(value) { return Array.isArray(value) ? value : [value]; } async function isDirectory(path) { try { return (await promises.lstat(path)).isDirectory(); } catch (err) { const code = err.code; if (code === "ENOENT" || code === "ENOTDIR") return false; throw err; } } function isDirectorySync(path) { try { return statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false; } catch (err) { if (err.code === "ENOTDIR") return false; throw err; } } const LEADING_DOT_RE = /^\.+/g; /** * Normalizes a file extension from a string to just the extension part (without the dot). * In case the string does not contain a dot, it returns the string as is. * * @example * normalizeExtension('.ts') // 'ts' * normalizeExtension('.d.ts') // 'd.ts' * normalizeExtension('ts') // 'ts' * normalizeExtension('d.ts') // 'ts' */ 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 SUPPORTED_EXT_RE = /^[^?]*\.(?:m?[jt]sx?|vue)(?:$|\?)/; const NUXT_LIB_RE = /^[^?]*node_modules\/(?:nuxt|nuxt3|nuxt-nightly|@nuxt)\//; const STYLE_QUERY_RE = /[?&]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 QUERY_RE$1 = /\?.*$/; /** Convert a 0-based offset within `code` into a 1-based line and column. */ function offsetToPosition(code, offset) { let line = 1; let lineStart = 0; const end = Math.min(offset, code.length); for (let i = 0; i < end; i++) if (code.charCodeAt(i) === 10) { line++; lineStart = i + 1; } return { line, column: end - lineStart + 1 }; } /** * Format `path` as its aliased form (`~/pages/index.vue`), wrapped in an OSC 8 * hyperlink so that supporting terminals can open the file at `line`/`column`. * * Aliased paths cannot be opened by clicking, and the hyperlink target is an * absolute `file://` URL that isn't rendered, so the printed text is unchanged. * * Any module query (`?vue&type=template`) is dropped, as it is not part of the * path on disk. */ function linkToAlias(path, nuxt = tryUseNuxt(), position) { return link(path.replace(QUERY_RE$1, ""), { ...position, cwd: nuxt?.options.rootDir, formatter: nuxt ? (absolute, line, column) => resolveToAlias(absolute, nuxt) + (line === void 0 ? "" : `:${line}${column === void 0 ? "" : `:${column}`}`) : void 0 }); } //#endregion //#region src/core/utils/names.ts const GROUPING_FOLDER_RE = /^\(.*\)$/; function isGroupingFolder(segment) { return GROUPING_FOLDER_RE.test(segment.trim()); } function getNameFromPath(path, relativeTo) { const relativePath = relativeTo ? normalize(path).replace(withTrailingSlash(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) { /** * Array of fileName parts split by case, / or - * @example third-component -> ['third', 'component'] * @example AwesomeComponent -> ['Awesome', 'Component'] */ const fileNameParts = splitByCase(fileName); const fileNamePartsContent = fileNameParts.join("/").toLowerCase(); const componentPrefixParts = prefixParts.filter((part) => !isGroupingFolder(part)); const componentNameParts = componentPrefixParts.flatMap((p) => splitByCase(p)); let index = componentPrefixParts.length - 1; const matchedSuffix = []; while (index >= 0) { const prefixPart = componentPrefixParts[index]; matchedSuffix.unshift(...splitByCase(prefixPart).map((p) => p.toLowerCase())); const matchedSuffixContent = matchedSuffix.join("/"); if (fileNamePartsContent === matchedSuffixContent || fileNamePartsContent.startsWith(matchedSuffixContent + "/") || prefixPart.toLowerCase() === fileNamePartsContent && componentPrefixParts[index + 1] === prefixPart) componentNameParts.length = index; index--; } return [...componentNameParts, ...fileNameParts]; } //#endregion //#region src/core/utils/plugins.ts /** * Split a bundler module ID into its pathname and search (query) parts. * * Module IDs from Vite/webpack are already-normalized filesystem paths * that may carry query strings (e.g. `?vue&type=style&lang=css`). */ function parseModuleId(id) { const qIndex = id.indexOf("?"); if (qIndex === -1) return { pathname: id, search: "" }; return { pathname: id.slice(0, qIndex), search: id.slice(qIndex) }; } /** * Combine multiple patterns into a single regular expression, for hook filters * that accept only one `RegExp` (such as `resolveId.filter.id`). */ function combineRE(patterns) { return new RegExp(patterns.map((re) => `(?:${re.source})`).join("|")); } const VUE_FILE_RE = /\.vue$/; const MACRO_QUERY_RE$1 = /[?&]macro=/; const EXACT_MACRO_QUERY_RE = /\?macro=true$/; const VUE_QUERY_RE = /[?&]vue(?:&|$)/; const VUE_SCRIPT_BLOCK_RE = /[?&]vue&type=script\b/; const VUE_TEMPLATE_BLOCK_RE = /[?&]vue&type=template\b/; /** * Module id filters for Vue SFC requests, usable directly as `transform.filter.id` * includes. Each array matches whole `.vue` files, page-meta macro requests and the * relevant SFC block requests (`?vue&type=...` from `@vitejs/plugin-vue` and `vue-loader`). */ const VUE_ID_FILTER = [ VUE_FILE_RE, MACRO_QUERY_RE$1, VUE_QUERY_RE ]; const VUE_SCRIPT_ID_FILTER = [ VUE_FILE_RE, MACRO_QUERY_RE$1, VUE_SCRIPT_BLOCK_RE ]; const VUE_TEMPLATE_ID_FILTER = [ VUE_FILE_RE, EXACT_MACRO_QUERY_RE, VUE_TEMPLATE_BLOCK_RE ]; const VUE_SCRIPT_TEMPLATE_ID_FILTER = [ VUE_FILE_RE, MACRO_QUERY_RE$1, VUE_SCRIPT_BLOCK_RE, VUE_TEMPLATE_BLOCK_RE ]; /** * SFC block requests that never contain user script code but may carry a JS-like * extension in their query (e.g. `?vue&type=template&lang.js`). Use as an `exclude` * when combining `JS_ID_RE` with a filter that should not match template blocks. */ const VUE_NON_SCRIPT_BLOCK_RE = /[?&]vue&type=(?:template|style|custom)\b/; function isVue(id, opts = {}) { return (opts.type ? opts.type.includes("script") ? opts.type.includes("template") ? VUE_SCRIPT_TEMPLATE_ID_FILTER : VUE_SCRIPT_ID_FILTER : VUE_TEMPLATE_ID_FILTER : VUE_ID_FILTER).some((re) => re.test(id)); } const JS_RE = /\.(?:[cm]?[jt]s|[jt]sx)$/; /** Like {@link isJS} but usable as an id filter (allows a query string after the extension). */ const JS_ID_RE = /\.(?:[cm]?[jt]s|[jt]sx)(?:\?|$)/; /** Matches module IDs for Vue files (ignoring query strings). */ const VUE_ID_RE = /\.vue(?:\?|$)/; function isJS(id) { const { pathname } = parseModuleId(id); return JS_RE.test(pathname); } function getLoader(id) { const { pathname } = parseModuleId(id); const ext = extname(pathname); if (ext === ".vue") return "vue"; if (!JS_RE.test(ext)) return null; return ext.endsWith("x") ? "tsx" : "ts"; } //#endregion //#region src/core/utils/index.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$1 = /\b\.\w+$/g; const SX_RE = /\.[tj]sx$/; /** * Decode a route path for route-rule matching, returning it unchanged if it is not * percent-encoded or cannot be decoded. Reserved characters (`%2F`, `%3F`, …) are * left encoded, so decoding cannot introduce additional path segments. Only the path * portion is decoded; a query string, if present, is left untouched to avoid * double-decoding it. */ function decodeRoutePath(path) { if (!path.includes("%")) return path; const queryIndex = path.indexOf("?"); const pathname = queryIndex === -1 ? path : path.slice(0, queryIndex); try { return queryIndex === -1 ? decodeURI(pathname) : decodeURI(pathname) + path.slice(queryIndex); } catch { return path; } } //#endregion //#region src/pages/utils.ts function createPagesContext(options = {}) { const modes = options.shouldUseServerComponents ? ["server", "client"] : ["client"]; const treeOptions = { roots: options.roots, modes, warn: (message) => pageDiagnostics.NUXT_B4011({ message }) }; const emitOptions = { onDuplicateRouteName: (_name, file, existingFile) => { pageDiagnostics.NUXT_B4004({ file: linkToAlias(file), existingFile: linkToAlias(existingFile) }); }, 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, originalPagePaths) { 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, originalPagePaths); } /** * Whether serializable page meta is written into the route record. * * The macro transform and route augmentation both have to agree on this, and neither can * extract when page meta is not scanned at all: the route record does not override the macro * module in that case, so extracting would duplicate the values rather than replace them. */ function shouldExtractSerializablePageMeta(nuxt = useNuxt()) { return !!nuxt.options.experimental.scanPageMeta && !!nuxt.options.experimental.extractSerializablePageMeta; } async function augmentAndResolve(pages, trackedFiles, nuxt = useNuxt(), originalPagePaths) { 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]), extractSerializable: shouldExtractSerializablePageMeta(nuxt), fullyResolvedPaths: trackedFiles, originalPagePaths, nuxt }; 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(); const dynamicPageMeta = dynamicPageMetaCache(ctx.nuxt ?? tryUseNuxt()); for (const route of routes) { if (route.file && !ctx.pagesToSkip?.has(route.file)) { const routeMeta = getRouteMeta(vfs[route.file] ?? fs.readFileSync(ctx.fullyResolvedPaths?.has(route.file) ? route.file : await resolvePath(route.file), "utf-8"), route.file, ctx.extraExtractionKeys, { extractSerializable: ctx.extractSerializable }); const dynamic = getDynamicMetaKeys(route.file, ctx.extraExtractionKeys, { extractSerializable: ctx.extractSerializable }); if (dynamic.size) dynamicPageMeta.set(normalize(route.file), dynamic); else dynamicPageMeta.delete(normalize(route.file)); if (route.meta) routeMeta.meta = defu({}, routeMeta.meta, route.meta); if (route.rules) routeMeta.rules = defu({}, routeMeta.rules, route.rules); if (ctx.augmentedPages.has(route.file)) { delete routeMeta.name; delete routeMeta.path; } if (routeMeta.path !== void 0 && routeMeta.path !== route.path) ctx.originalPagePaths?.set(route, route.path); 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 = /)(?[^>]*)>(?[\s\S]*?)<\/script\s*>/dgi; function extractScriptContent(sfc) { const contents = []; for (const match of sfc.matchAll(SFC_SCRIPT_RE)) { const content = match?.groups?.content; if (content) { const [contentStart] = match.indices.groups.content; contents.push({ loader: match.groups.attrs && /[tj]sx/.test(match.groups.attrs) ? "tsx" : "ts", code: content.trim(), offset: contentStart + content.length - content.trimStart().length }); } } return contents; } const PAGE_META_MACRO_NAMES = /* @__PURE__ */ new Set(["definePageMeta", "defineRouteRules"]); const HAS_PAGE_MACRO_RE = new RegExp(`\\b(?:${[...PAGE_META_MACRO_NAMES].join("|")})\\b`); const defaultExtractionKeys = [ "name", "path", "props", "alias", "redirect", "middleware" ]; const EMPTY_DYNAMIC_META = /* @__PURE__ */ new Set(); /** * Which `definePageMeta` keys could not be statically extracted and must come from the runtime * macro module, recorded per nuxt instance and keyed by page file. * * A page's dynamic keys are a pure function of its file, because extraction parses that file's * contents and nothing else, and `page.file` is the one field that survives the page being * rebuilt: modules in `pages:extend`/`pages:resolved` clone, merge and delete `page.meta` and * replace the page objects wholesale (route localization emits a shallow copy per locale), while a * page that loses its file loses its macro module too, which fails loudly instead of silently. * * Keyed by path with the latest extraction winning rather than by content, because * `normalizeRoutes` has no contents to hash and augmentation always re-runs for a changed file * before routes are generated, so a stale set cannot be observed. Scoped per instance because * `extraPageMetaExtractionKeys` can differ between two instances sharing an absolute path. */ const dynamicPageMetaCaches = /* @__PURE__ */ new WeakMap(); const detachedDynamicPageMeta = /* @__PURE__ */ new Map(); function dynamicPageMetaCache(nuxt = tryUseNuxt()) { if (!nuxt) return detachedDynamicPageMeta; let cache = dynamicPageMetaCaches.get(nuxt); if (!cache) { cache = /* @__PURE__ */ new Map(); dynamicPageMetaCaches.set(nuxt, cache); } return cache; } /** * Keys of `page` that `definePageMeta` sets at runtime rather than at build time. A page without a * file has no macro module to defer to, so it never needs these and no fallback is required. */ function getDynamicPageMeta(page, cache = dynamicPageMetaCache()) { return (page.file ? cache.get(normalize(page.file)) : void 0) ?? EMPTY_DYNAMIC_META; } const STATIC_EXPRESSION_WRAPPERS = /* @__PURE__ */ new Set([ "ParenthesizedExpression", "TSAsExpression", "TSNonNullExpression", "TSSatisfiesExpression", "TSTypeAssertion" ]); function unwrapStaticExpression(node) { let current = node; while (current && STATIC_EXPRESSION_WRAPPERS.has(current.type)) current = current.expression; return current; } function collectStatementCalls(body, into) { for (const statement of body) { if (statement.type !== "ExpressionStatement") continue; const expression = unwrapStaticExpression(statement.expression); if (expression?.type === "CallExpression") into.add(expression); } } /** * The body of the `setup()` option of an Options API default export, if the module has one. */ function findOptionsApiSetupBody(program) { for (const statement of program.body) { if (statement.type !== "ExportDefaultDeclaration") continue; let declaration = unwrapStaticExpression(statement.declaration); if (declaration?.type === "CallExpression") declaration = unwrapStaticExpression(declaration.arguments[0]); if (declaration?.type !== "ObjectExpression") return; for (const property of declaration.properties) { if (property.type !== "Property" || property.computed || property.kind !== "init") continue; if ((property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? property.key.value : void 0) !== "setup") continue; const value = property.value; if ((value.type === "FunctionExpression" || value.type === "ArrowFunctionExpression") && value.body?.type === "BlockStatement") return value.body.body; } return; } } /** * The keys of `definePageMeta` whose values are read at build time, given the user's * `experimental.extraPageMetaExtractionKeys`. * * Both halves of the macro pipeline have to agree on this set: `getRouteMeta` reads these keys * into the route record, and `PageMetaPlugin` removes them from the macro module. */ function resolvePageMetaExtractionKeys(extraKeys = []) { return /* @__PURE__ */ new Set([...defaultExtractionKeys, ...extraKeys]); } /** * Classify a property of a `definePageMeta` object literal. * * A property may only be removed from the macro module when it is also extracted into the route * record, and vice versa, or its value is lost. Both sides call this so that the two cannot * drift apart. */ function classifyPageMetaProperty(property, extractionKeys, options = {}) { if (property.type !== "Property" || property.computed || property.kind !== "init" || property.method) return { kind: "runtime" }; let key; if (property.key.type === "Identifier") key = property.key.name; else if (property.key.type === "Literal" && (typeof property.key.value === "string" || typeof property.key.value === "number")) key = String(property.key.value); else return { kind: "runtime" }; const value = unwrapStaticExpression(property.value); if (key === "layout" && value?.type === "ObjectExpression") { if (!value.properties.every((subProperty) => subProperty.type === "Property" && !subProperty.computed && subProperty.kind === "init" && !subProperty.method && subProperty.key.type === "Identifier" && (subProperty.key.name === "name" || subProperty.key.name === "props"))) return { kind: "runtime" }; if (options.extractSerializable) { const serialized = isSerializable(value); if (serialized.serializable && serialized.value && "name" in serialized.value) return { kind: "reshape", key, node: value, value: serialized.value }; } return { kind: "reshape", key, node: value }; } const isExtractionKey = extractionKeys.has(key); if (!isExtractionKey && !options.extractSerializable) return { kind: "runtime" }; const serialized = isSerializable(property.value); if (serialized.serializable) return { kind: "extract", key, value: serialized.value }; return isExtractionKey ? { kind: "dynamic", key } : { kind: "runtime" }; } const pageContentsCache = {}; const extractCache = /* @__PURE__ */ new Map(); const dynamicMetaCache = /* @__PURE__ */ new Map(); function getExtractVariant(extraExtractionKeys, options) { return `${options.extractSerializable ? "serializable" : "raw"}\0${[...extraExtractionKeys].sort().join(",")}`; } function cacheVariant(cache, absolutePath) { let variants = cache.get(absolutePath); if (!variants) { variants = /* @__PURE__ */ new Map(); cache.set(absolutePath, variants); } return variants; } /** * Keys a page file's `definePageMeta` call sets at runtime rather than at build time. Derived * purely from file contents, so it is cached alongside the extracted metadata for that file. */ function getDynamicMetaKeys(absolutePath, extraExtractionKeys = /* @__PURE__ */ new Set(), options = {}) { return dynamicMetaCache.get(absolutePath)?.get(getExtractVariant(extraExtractionKeys, options)) ?? EMPTY_DYNAMIC_META; } /** * Whether build-time scanning can see a page file's `definePageMeta` call. A file in a format the * scanner cannot parse (e.g. `.md` compiled to a component by a build plugin) may still call the * macro once transformed, so all of its metadata has to be left to the runtime macro module. * Extensionless entries have no module to transform and so nothing to fall back to. */ function isScannablePageFile(path) { const { pathname } = parseModuleId(path); return !!getLoader(pathname) || !extname(pathname); } function getRouteMeta(contents, absolutePath, extraExtractionKeys = /* @__PURE__ */ new Set(), options = {}) { const variant = getExtractVariant(extraExtractionKeys, options); if (!(absolutePath in pageContentsCache) || pageContentsCache[absolutePath] !== contents) { pageContentsCache[absolutePath] = contents; extractCache.delete(absolutePath); dynamicMetaCache.delete(absolutePath); } const cached = extractCache.get(absolutePath)?.get(variant); if (cached) return klona(cached); const extractionKeys = resolvePageMetaExtractionKeys(extraExtractionKeys); const loader = getLoader(absolutePath); if (!loader) { if (!isScannablePageFile(absolutePath)) cacheVariant(dynamicMetaCache, absolutePath).set(variant, /* @__PURE__ */ new Set([...extractionKeys, "meta"])); cacheVariant(extractCache, absolutePath).set(variant, {}); return {}; } const scriptBlocks = loader === "vue" ? extractScriptContent(contents) : [{ code: contents, loader, offset: 0 }]; const extractedData = {}; const fileAt = (offset) => linkToAlias(absolutePath, void 0, offsetToPosition(contents, offset)); const routeFieldKeys = extractionKeys; const dynamicProperties = /* @__PURE__ */ new Set(); for (const script of scriptBlocks) { if (!HAS_PAGE_MACRO_RE.test(script.code)) continue; const macroCalls = []; const { program } = parseAndWalk(script.code, absolutePath.replace(/\.\w+$/, "." + script.loader), (node) => { if (node.type === "CallExpression" && node.callee.type === "Identifier" && PAGE_META_MACRO_NAMES.has(node.callee.name)) macroCalls.push({ fnName: node.callee.name, node }); }); const topLevelCalls = /* @__PURE__ */ new Set(); collectStatementCalls(program.body, topLevelCalls); const setupBody = findOptionsApiSetupBody(program); if (setupBody) collectStatementCalls(setupBody, topLevelCalls); const extractedMacros = /* @__PURE__ */ new Set(); for (const { fnName, node } of macroCalls) { if (!topLevelCalls.has(node)) pageDiagnostics.NUXT_B4007({ fnName, file: fileAt(script.offset + node.start) }); if (extractedMacros.has(fnName)) continue; extractedMacros.add(fnName); const argument = unwrapStaticExpression(node.arguments[0]); if (argument?.type !== "ObjectExpression") { pageDiagnostics.NUXT_B4005({ fnName, file: fileAt(script.offset + node.start), receivedType: String(argument?.type) }); if (fnName === "definePageMeta") dynamicProperties.add("meta"); continue; } if (fnName === "defineRouteRules") { const { value, serializable } = isSerializable(argument); if (!serializable) { pageDiagnostics.NUXT_B4006({ fnName, file: fileAt(script.offset + node.start) }); continue; } extractedData.rules = value; continue; } const classified = /* @__PURE__ */ new Map(); let hasRuntimeProperty = false; for (const property of argument.properties) { const classification = classifyPageMetaProperty(property, extractionKeys, options); if (classification.kind === "extract" || classification.kind === "dynamic" || classification.kind === "reshape" && classification.value) { classified.set(classification.key, classification); continue; } hasRuntimeProperty = true; } for (const key of extractionKeys) { const classification = classified.get(key); if (!classification || classification.kind === "reshape") continue; if (classification.kind === "dynamic") { logger.debug(`Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${fileAt(script.offset + node.start)}\`).`); dynamicProperties.add(extraExtractionKeys.has(key) ? "meta" : key); continue; } if (extraExtractionKeys.has(key)) { extractedData.meta ??= {}; extractedData.meta[key] = classification.value; } else extractedData[key] = classification.value; } for (const [key, classification] of classified) { if (classification.kind === "reshape") { extractedData.meta ??= {}; extractedData.meta.layout = classification.value.name; if (classification.value.props !== void 0) extractedData.meta.layoutProps = classification.value.props; continue; } if (routeFieldKeys.has(key) || classification.kind !== "extract") continue; extractedData.meta ??= {}; extractedData.meta[key] = classification.value; } if (hasRuntimeProperty) dynamicProperties.add("meta"); } } if (dynamicProperties.size) cacheVariant(dynamicMetaCache, absolutePath).set(variant, dynamicProperties); cacheVariant(extractCache, absolutePath).set(variant, extractedData); return klona(extractedData); } function serializeRouteValue(value, skipSerialisation = false) { if (skipSerialisation || value === void 0) return; return JSON.stringify(value); } const PAGE_STUBS = { noScripts: { env: "server", name: "_noScriptsPageStub", declaration: "\nconst _noScriptsPageStub = { mounted: () => window.location.reload(), render: () => null };" }, spaOnly: { env: "client", name: "_spaOnlyPageStub", declaration: "\nconst _spaOnlyPageStub = { render: () => null };" } }; function normalizeComponent(page, pageImport, routeName, islandKey) { if (page.mode === "server") return `() => createIslandPage(${routeName}, import.meta.server ? ${islandKey} : undefined)`; if (page.mode === "client") return `() => createClientPage(${onlyOnClient(pageImport)})`; return pageImport; } function onlyOnClient(pageImport) { return `import.meta.client ? ${pageImport} : undefined`; } function normalizeComponentWithName(page, isSyncImport, pageImportName, pageImport, routeName, metaRouteName, islandKey) { if (isSyncImport) return `Object.assign(${pageImportName}, { __name: ${metaRouteName} })`; if (page.mode === "server") return `() => createIslandPage(${routeName}, import.meta.server ? ${islandKey} : undefined)`; if (page.mode === "client") return `() => createClientPage(${onlyOnClient(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(); const dynamicPageMeta = dynamicPageMetaCache(nuxt); return { imports: metaImports, routes: genArrayFromRaw(routes.map((page) => { const markedDynamic = getDynamicPageMeta(page, dynamicPageMeta); const metaFiltered = {}; let skipMeta = true; for (const key in page.meta || {}) if (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"; const metaImport = genImport(`${file}?macro=true`, [{ name: "default", as: metaImportName }]); const restriction = options.restrictedPages?.get(page); const stub = restriction ? PAGE_STUBS[restriction] : void 0; if (stub) metaImports.add(stub.declaration); const restrictToEnvironment = (loader) => stub ? `import.meta.${stub.env} ? ${loader} : () => Promise.resolve(${stub.name})` : loader; if (page._sync && !stub) metaImports.add(genImport(file, [{ name: "default", as: pageImportName }])); const isSyncImport = page._sync && page.mode !== "client" && !stub; const pageImport = isSyncImport ? pageImportName : genDynamicImport(file); const metaRouteName = options?.overrideMeta && !markedDynamic.has("name") ? route.name ?? `${metaImportName}?.name` : `${metaImportName}?.name ?? ${route.name}`; const islandKey = page.mode === "server" && page.file ? JSON.stringify(hash(relative(nuxt.options.rootDir, page.file))) : void 0; const component = restrictToEnvironment(nuxt.options.experimental.normalizePageNames ? normalizeComponentWithName(page, isSyncImport, pageImportName, pageImport, route.name, metaRouteName, islandKey) : normalizeComponent(page, pageImport, route.name, islandKey)); let componentsObject; if (page.components) { const viewEntries = []; for (const viewName in page.components) { if (viewName === "default") continue; const viewFile = normalize(page.components[viewName]); viewEntries.push(`${JSON.stringify(viewName)}: ${restrictToEnvironment(genDynamicImport(viewFile))}`); } if (viewEntries.length > 0) componentsObject = `{ default: ${component}, ${viewEntries.join(", ")} }`; } 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 (componentsObject) metaRoute.components = componentsObject; if (page.mode === "server") metaImports.add(` let _createIslandPage async function createIslandPage (name, islandKey) { _createIslandPage ||= await import(${JSON.stringify(options?.serverComponentRuntime)}).then(r => r.createIslandPage) return _createIslandPage(name, islandKey) };`); 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; } const { children, ...ownFields } = metaRoute; if (Object.values(ownFields).some((field) => field?.includes(metaImportName))) metaImports.add(metaImport); return metaRoute; })) }; } function resolveRoutePaths(page, parent = "/") { return [joinURL(parent, page.path), ...page.children?.flatMap((child) => resolveRoutePaths(child, joinURL(parent, page.path))) || []]; } const EMPTY_PARAM_REGEXP_RE = /:(\w+)\(\)([+*?]?)/g; function canonicalizeParams(path) { return path.replace(EMPTY_PARAM_REGEXP_RE, ":$1$2"); } /** * Strip the leading segments an absolute child path shares with its parent's `fullPath`. * Inserting only the remainder avoids re-declaring the parent's params on the child node. * The remainder only determines the node's own params, as the absolute child path is * re-applied as a path override afterwards and becomes the route's `fullPath`. * * Returns `undefined` when the child path does not extend the parent's full path, as there * is then no remainder that declares the child's own params and nothing else. */ function relativizeToParent(parentFullPath, childPath) { if (!childPath.startsWith("/")) return childPath; const parentSegments = canonicalizeParams(parentFullPath).replace(/\/$/, "").split("/"); const childSegments = childPath.split("/"); for (let index = 0; index < parentSegments.length; index++) if (canonicalizeParams(childSegments[index] ?? "") !== parentSegments[index]) return; return childSegments.slice(parentSegments.length).join("/"); } function isSerializable(node) { node = unwrapStaticExpression(node) || node; if (node.type === "Literal") { if (typeof node.value === "string" || typeof node.value === "number" || typeof node.value === "boolean" || node.value === null) return { value: node.value, serializable: true }; return { serializable: false }; } if (node.type === "UnaryExpression" && (node.operator === "-" || node.operator === "+")) { const arg = node.argument; if (arg.type === "Literal" && typeof arg.value === "number") return { value: node.operator === "-" ? -arg.value : arg.value, serializable: true }; return { serializable: false }; } if (node.type === "ArrayExpression") { const values = []; for (const element of node.elements) { if (!element || element.type === "SpreadElement") return { serializable: false }; const { serializable, value } = isSerializable(element); if (!serializable) return { serializable: false }; values.push(value); } return { value: values, serializable: true }; } if (node.type === "ObjectExpression") { const value = {}; for (const property of node.properties) { if (property.type !== "Property" || property.computed || property.kind !== "init" || property.method) return { serializable: false }; let key; if (property.key.type === "Identifier") key = property.key.name; else if (property.key.type === "Literal" && (typeof property.key.value === "string" || typeof property.key.value === "number")) key = String(property.key.value); else return { serializable: false }; const { serializable, value: propertyValue } = isSerializable(property.value); if (!serializable) return { serializable: false }; Object.defineProperty(value, key, { value: propertyValue, enumerable: true, writable: true, configurable: true }); } return { value, serializable: true }; } return { serializable: false }; } /** * Statically determine whether a `router.options` file may modify `routes`. * Returns `true` (conservatively) when the default export is not a plain object * literal, contains spreads or computed keys, or defines a `routes` property. */ function routerOptionsMayModifyRoutes(code, filename) { let mayModify = true; try { parseAndWalk(code, filename, (node) => { if (node.type !== "ExportDefaultDeclaration") return; let declaration = node.declaration; while (declaration.type === "TSAsExpression" || declaration.type === "TSTypeAssertion" || declaration.type === "TSSatisfiesExpression" || declaration.type === "ParenthesizedExpression") declaration = declaration.expression; if (declaration.type !== "ObjectExpression") return; mayModify = declaration.properties.some((property) => { if (property.type === "SpreadElement") return true; const key = property.key; const name = key.type === "Identifier" && !property.computed ? key.name : key.type === "Literal" ? String(key.value) : void 0; return name === void 0 || name === "routes"; }); }); } catch { return true; } return mayModify; } /** * Whether a route can be normalised to the same form as an incoming request path. * A path carrying a `%` that is not a valid escape sequence cannot be decoded, and is * matched in its raw form on both sides: the pattern keeps the authored characters while * the request keeps the encoding the client sent, so the two can never meet. */ function isDecodable(route) { if (!route.includes("%")) return true; try { return true; } catch { return false; } } /** * Collect a rou3 pattern for every path a page can be reached by: its canonical * path, each alias, and the whole subtree below it. Returns `undefined` when any * path cannot be converted, so callers can fall back to matching everything; * the routes that could not be converted are reported via `onUnconvertible`. */ function collectRou3PagePatterns(pages, prefixes = ["/"], onUnconvertible) { const patterns = []; let failed = false; for (const page of pages) { const routes = /* @__PURE__ */ new Set(); for (const route of [page.path, ...toArray(page.alias || [])]) if (route.startsWith("/")) routes.add(route); else for (const prefix of prefixes) routes.add(joinURL(prefix, route)); for (const route of routes) { if (!isDecodable(route)) { onUnconvertible?.(route); failed = true; continue; } const { patterns: converted } = vueRouterToRou3(route, { collapse: true }); if (!converted.length) { onUnconvertible?.(route); failed = true; continue; } patterns.push(...converted); } if (page.children?.length) { const childPatterns = collectRou3PagePatterns(page.children, [...routes], onUnconvertible); if (childPatterns) patterns.push(...childPatterns); else failed = true; } } return failed ? void 0 : patterns; } 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; } //#endregion //#region src/pages/route-rules.ts function globRouteRulesFromPages(pages) { return collectRouteRulesFromPages(pages, {}, ""); } function collectRouteRulesFromPages(pages, paths, prefix) { for (const page of pages) { if (page.rules) { if (Object.keys(page.rules).length) { const path = prefix + page.path; const { patterns, issues } = vueRouterToRou3(path, { collapse: true }); if (issues.length) for (const issue of issues) pageDiagnostics.NUXT_B4016({ path, detail: issue.message }); else for (const pattern of patterns) { if (pattern in paths && !isEqual(paths[pattern], page.rules)) pageDiagnostics.NUXT_B4017({ path, pattern }); paths[pattern] = page.rules; } } delete page.rules; } if (page.children?.length) collectRouteRulesFromPages(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); } } //#endregion //#region src/core/utils/route-rules.ts /** * Route-rule keys the client-side matcher is compiled from; anything else is server-only. * * @internal */ const VALID_MANIFEST_KEYS = [ "prerender", "redirect", "appMiddleware", "appLayout", "cache", "isr", "swr", "ssr", "noScripts" ]; /** * Normalise a route-rule key or lookup path: decode percent-encoding, then case-fold when * `fold` is set. Keys are matched in decoded form so that a rule may be authored either * decoded (`/测试`, the usual case) or encoded (`/%E6%B5%8B%E8%AF%95`, the form the page route * and the request path take). Decoding must precede case folding, or a percent-encoded * non-ASCII character would never fold. * * @internal */ function normalizeRouteRulePath(path, fold) { const decoded = decodeRoutePath(path); return fold ? decoded.toLowerCase() : decoded; } /** * Expand the rule shorthands a server runtime would expand: `redirect` as a string, and * `swr`/`cache` as the single `cache` rule everything downstream reads. * * @internal */ function normalizeRouteRules(config) { const normalized = {}; for (const key in config) { const { redirect, swr, cache, ...rules } = config[key] || {}; if (redirect) { const options = { to: "/", status: 307, ...typeof redirect === "string" ? { to: redirect } : redirect }; if (key.endsWith("/**")) options.base = key.slice(0, -3); rules.redirect = options; } else if (redirect === false) rules.redirect = false; if (swr !== void 0 && swr !== false) rules.cache = { ...cache || void 0, swr: true, ...typeof swr === "number" ? { maxAge: swr } : {} }; else if (swr === false && cache === void 0) rules.cache = false; else if (cache !== void 0) rules.cache = cache === false ? false : cache; normalized[key] = rules; } return normalized; } /** * Expand `appMiddleware` given as a name or a list of names into the record the server * runtime accepts. Matching entries are replaced, not mutated. * * @internal */ function normalizeAppMiddlewareRules(routeRules) { for (const route in routeRules) { const appMiddleware = routeRules[route]?.appMiddleware; if (typeof appMiddleware !== "string" && !Array.isArray(appMiddleware)) continue; routeRules[route] = { ...routeRules[route], appMiddleware: Object.fromEntries([appMiddleware].flat().map((name) => [name, true])) }; } } /** * The route rules the build has resolved, as router entries. * * A server builder resolves its own (a module may push rules through `nitro:config`, and the * server may add rules of its own), so its resolved set is preferred where there is one. * * @internal */ function resolveRouteRulesRoutes(nuxt) { const nitro = tryUseNitro(); const rules = nitro ? nitro.options.routeRules : normalizeRouteRules(nuxt.options.routeRules); const baseURL = (nitro ? nitro.options.baseURL : nuxt.options.app.baseURL) || ""; return { routes: Object.entries(rules).map(([route, data]) => ({ route, method: "", data })), baseURL }; } /** * Resolve the route rules that apply to `path`, merged most-specific-last. * * The router is expected to have been built with keys normalised by * {@link normalizeRouteRulePath} under the same `fold`, so that lookups match them. * * @internal */ function resolveRouteRules(router, path, fold) { return defu({}, ...router.matchAll("", normalizeRouteRulePath(path, fold)).reverse()); } /** * Build a route-rules router: patterns are registered under `baseURL`, and a lookup resolves * every pattern it matches, least specific first. * * @internal */ function createRouteRulesRouter(routes, baseURL) { const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL; const router = createRouter(); for (const route of routes) addRoute(router, route.method || "", base + route.route, route.data); return { routes, matchAll: (method, path) => findAllRoutes(router, method, path).map((route) => route.data), compileToString(options) { if (base || routes.length !== 1 || routes[0].route !== "/**" || !!routes[0].method) return compileRouterToString(router, void 0, options); const data = (options?.serialize || JSON.stringify)(routes[0].data); const match = `{data,params:{"_":p.slice(1)}}`; return `/* @__PURE__ */ (() => {const data=${data};return ((_m, p)=>(${options?.matchAll ? `[${match}]` : match}));})()`; } }; } /** * Source of the `normalizePath` helper a compiled matcher looks rules up through: `decodeRoutePath` * is inlined by source so that the runtime lookup and the build-time key normalisation cannot * drift apart. * * @internal */ const normalizePathCode = [ `const decodeRoutePath = ${decodeRoutePath.toString()}`, `const normalizePath = (path, fold) => {`, ` if (typeof path !== 'string') { return path }`, ` const decoded = decodeRoutePath(path)`, ` return fold ? decoded.toLowerCase() : decoded`, `}` ].join("\n"); /** * Build a route-rules router whose keys are decoded (and, when `fold` is set, lower-cased), * mirroring the normalised lookup path used at runtime. * * `onDuplicate` is called when two distinct keys normalise onto the same path; the later * key wins. * * @internal */ function createNormalizedRouteRulesRouter(routes, baseURL, fold, onDuplicate) { const normalizedKeys = /* @__PURE__ */ new Map(); return createRouteRulesRouter(routes.map((route) => { if (typeof route.route !== "string") return route; const key = normalizeRouteRulePath(route.route, fold); const existing = normalizedKeys.get(key); if (existing !== void 0 && existing !== route.route) onDuplicate?.(existing, route.route, key); normalizedKeys.set(key, route.route); return { ...route, route: key }; }), normalizeRouteRulePath(String(baseURL || ""), fold)); } //#endregion //#region src/pages/route-coverage.ts const UNESCAPE_RE = /\\(.)/g; /** * Derive a segment no literal rule can match, by extending a base until it collides with none of * the static segments in the rule set. Matching the probe then surfaces only the wildcard rules * that apply across a whole glob region. */ function createProbeSegment(routes) { const literals = /* @__PURE__ */ new Set(); for (const route of routes) for (const segment of route.split("/")) { literals.add(segment); literals.add(segment.replace(UNESCAPE_RE, "$1")); } let probe = "__nuxt_route_rule_probe__"; while (literals.has(probe)) probe += "_"; return probe; } const ROU3_PARAM_RE = /^:[\w-]+/; const ROU3_MODIFIER_RE = /^[?+*]/; /** Length of the `(...)` group starting at `index`, or `0` if there is none. */ function groupLength(segment, index) { if (segment[index] !== "(") return 0; let depth = 0; for (let cursor = index; cursor < segment.length; cursor++) { const char = segment[cursor]; if (char === "\\") cursor++; else if (char === "(") depth++; else if (char === ")" && --depth === 0) return cursor + 1 - index; } return segment.length - index; } /** * Turn one rou3 segment into the literal text it would match, substituting the probe for any * dynamic token. Escapes are honoured in both directions: `\:` is a literal `:` rather than a * param, and the backslash is dropped because rou3 matches against the unescaped path. */ function segmentToProbe(segment, probeSegment) { let probe = ""; let dynamic = false; let index = 0; while (index < segment.length) { const char = segment[index]; if (char === "\\") { probe += segment[index + 1] ?? ""; index += 2; continue; } let length = 0; if (char === "*") { length = segment.startsWith("**", index) ? 2 : 1; length += ROU3_PARAM_RE.exec(segment.slice(index + length))?.[0].length ?? 0; } else if (char === ":") length = ROU3_PARAM_RE.exec(segment.slice(index))?.[0].length ?? 0; else if (char === "(") length = groupLength(segment, index); if (!length) { probe += char; index++; continue; } dynamic = true; probe += probeSegment; index += length; index += groupLength(segment, index); index += ROU3_MODIFIER_RE.exec(segment.slice(index))?.[0].length ?? 0; } return { probe, dynamic }; } function patternToProbePath(pattern, probeSegment) { return pattern.split("/").map((segment) => segmentToProbe(segment, probeSegment).probe).join("/"); } const TRAILING_SLASH_RE = /\/$/; /** * Mark every page that route rules guarantee is never rendered in one of the two * bundles, so `normalizeRoutes` can stub its component out of that bundle. * * A page qualifies only when every path it can be reached by is covered: its * canonical path, each alias, and the whole subtree below it. Anything that * cannot be proven statically counts as uncovered. */ function markPagesCoveredByRouteRule(pages, nitro, options) { const fold = !options.sensitive; const matcher = createNormalizedRouteRulesRouter(Object.entries(nitro.options.routeRules).map(([route, data]) => ({ route, data })), "", fold); const PROBE_SEGMENT = createProbeSegment(matcher.routes.map(({ route }) => route)); const isPathCovered = (path) => options.isCovered(resolveRouteRules(matcher, path, fold)); function patternIsCovered(pattern) { const segments = pattern.split("/").map((segment) => segmentToProbe(segment, PROBE_SEGMENT)); const dynamicAt = segments.findIndex((segment) => segment.dynamic); if (dynamicAt === -1) { const path = segments.map((segment) => segment.probe).join("/"); return isPathCovered(path.length > 1 ? path.replace(TRAILING_SLASH_RE, "") : path); } const prefix = segments.slice(0, dynamicAt).map((segment) => segment.probe).join("/") || "/"; const probe = (prefix === "/" ? "" : prefix) + "/" + PROBE_SEGMENT; if (!isPathCovered(probe)) return false; const rawPrefix = pattern.split("/").slice(0, dynamicAt).join("/") || "/"; const within = rawPrefix === "/" ? "/" : rawPrefix + "/"; for (const { route } of matcher.routes) { if (route !== rawPrefix && !route.startsWith(within)) continue; if (!isPathCovered(patternToProbePath(route, PROBE_SEGMENT))) return false; } return true; } function pathIsCovered(path) { const { patterns } = vueRouterToRou3(path, { collapse: true }); return patterns.length > 0 && patterns.every(patternIsCovered); } function markPages(pages, prefix) { let allCovered = true; for (const page of pages) { const path = page.path.startsWith("/") ? page.path : joinURL(prefix, page.path); const aliases = toArray(page.alias || []).map((alias) => alias.startsWith("/") ? alias : joinURL(prefix, alias)); let covered = options.filter?.(page) === false ? false : [path, ...aliases].every(pathIsCovered); if (page.children?.length) covered = markPages(page.children, path) && covered; options.mark(page, covered, path); allCovered &&= covered; } return allCovered; } return markPages(pages, "/"); } //#endregion //#region src/pages/public-assets.ts const INDEX_HTML_SUFFIX = "/index.html"; function normalizeRoute(path) { return withoutTrailingSlash(withLeadingSlash(path)) || "/"; } /** * Map static page routes and aliases to their source files. * Dynamic routes are intentionally excluded to avoid noisy warnings. */ function collectStaticPageRoutes(pages, parent = "/", routes = /* @__PURE__ */ new Map()) { for (const page of pages) { const route = normalizeRoute(joinURL(parent, page.path)); if (!route.includes(":")) routes.set(route, page.file); const aliases = Array.isArray(page.alias) ? page.alias : page.alias ? [page.alias] : []; for (const alias of aliases) { const resolved = normalizeRoute(alias.startsWith("/") ? alias : joinURL(parent, alias)); if (!resolved.includes(":")) routes.set(resolved, page.file); } if (page.children) collectStaticPageRoutes(page.children, route, routes); } return routes; } /** * Return asset paths that Nitro can serve for a route, relative to the asset directory. */ function getAssetPathsForRoute(route, baseURL = "/") { const base = normalizeRoute(baseURL); if (base !== "/" && route !== base && !route.startsWith(base + "/")) return; const relativeRoute = route.slice(base === "/" ? 1 : base.length + 1); return relativeRoute ? [relativeRoute, relativeRoute + INDEX_HTML_SUFFIX] : [INDEX_HTML_SUFFIX.slice(1)]; } //#endregion //#region src/core/utils/parse.ts const LANG_RE = /\.(?:c|m)?(jsx?|tsx?)$/; /** * Parse a module, inferring the language from `filename`. * * `code` may be a `