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,299 lines (1,287 loc) 221 kB
import fs, { promises, existsSync, readdirSync, statSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'; import { mkdir, readFile, readdir, writeFile, rm } from 'node:fs/promises'; import { dirname, resolve, normalize, basename, extname, relative, isAbsolute, join } from 'pathe'; import { createHooks, createDebugger } from 'hookable'; import ignore from 'ignore'; import { tryUseNuxt, useNuxt, resolveFiles, resolvePath as resolvePath$1, logger, defineNuxtModule, addPlugin, addTemplate, addTypeTemplate, addComponent, updateTemplates, useNitro, addVitePlugin, addWebpackPlugin, addBuildPlugin, findPath, addImportsSources, tryResolveModule, isIgnored, resolveAlias, addPluginTemplate, normalizeModuleTranspilePath, resolveNuxtModule, resolveIgnorePatterns, createResolver, nuxtCtx, addServerPlugin, installModule, addRouteMiddleware, loadNuxtConfig, normalizeTemplate, compileTemplate, normalizePlugin, templateUtils } from '@nuxt/kit'; import { resolvePackageJSON, readPackageJSON } from 'pkg-types'; import { hash } from 'ohash'; import consola from 'consola'; import { colorize } from 'consola/utils'; import { updateConfig } from 'c12/update'; import { resolveCompatibilityDatesFromEnv, formatDate } from 'compatx'; import escapeRE from 'escape-string-regexp'; import { withTrailingSlash, parseURL, parseQuery, withLeadingSlash, joinURL, encodePath, withoutLeadingSlash } from 'ufo'; import defu$1, { defu } from 'defu'; import { satisfies, gt } from 'semver'; import { isWindows, hasTTY, isCI } from 'std-env'; import { genArrayFromRaw, genSafeVariableName, genImport, genDynamicImport, genObjectFromRawEntries, genString, genExport } from 'knitwork'; import { createRoutesContext } from 'unplugin-vue-router'; import { resolveOptions } from 'unplugin-vue-router/options'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { resolvePath, findStaticImports, findExports, parseStaticImport, parseNodeModulePath, lookupNodeModuleSubpath, interopDefault } from 'mlly'; import { runInNewContext } from 'node:vm'; import { filename } from 'pathe/utils'; import { transform } from 'esbuild'; import { parse } from 'acorn'; import { walk } from 'estree-walker'; import { splitByCase, kebabCase, pascalCase, camelCase } from 'scule'; import { createUnplugin } from 'unplugin'; import MagicString from 'magic-string'; import { stripLiteral } from 'strip-literal'; import { globby } from 'globby'; import { parse as parse$1, walk as walk$1, ELEMENT_NODE } from 'ultrahtml'; import { createUnimport, defineUnimportPreset, toExports, scanDirExports } from 'unimport'; import { parseQuery as parseQuery$1 } from 'vue-router'; import { createTransformer } from 'unctx/transform'; import { cpus } from 'node:os'; import { toRouteMatcher, createRouter, exportMatcher } from 'radix3'; import { createNitro, scanHandlers, writeTypes, copyPublicAssets, build as build$1, prepare, prerender, createDevServer } from 'nitropack'; import { dynamicEventHandler } from 'h3'; import chokidar from 'chokidar'; import { debounce } from 'perfect-debounce'; import { resolveSchema, generateTypes } from 'untyped'; import untypedPlugin from 'untyped/babel-plugin'; import jiti from 'jiti'; let _distDir = dirname(fileURLToPath(import.meta.url)); if (_distDir.match(/(chunks|shared)$/)) { _distDir = dirname(_distDir); } const distDir = _distDir; const pkgDir = resolve(distDir, ".."); async function resolveTypePath(path, subpath, searchPaths = tryUseNuxt()?.options.modulesDir) { try { const r = await resolvePath(path, { url: searchPaths, conditions: ["types", "import", "require"] }); 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(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(/["']/g, ""); } 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) { matchedSuffix.unshift(...splitByCase(prefixParts[index]).map((p) => p.toLowerCase())); const matchedSuffixContent = matchedSuffix.join("/"); if (fileNamePartsContent === matchedSuffixContent || fileNamePartsContent.startsWith(matchedSuffixContent + "/") || // e.g Item/Item/Item.vue -> Item prefixParts[index].toLowerCase() === fileNamePartsContent && prefixParts[index + 1] && prefixParts[index] === prefixParts[index + 1]) { componentNameParts.length = index; } index--; } return [...componentNameParts, ...fileNameParts]; } function isVue(id, opts = {}) { const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href)); if (id.endsWith(".vue") && !search) { return true; } if (!search) { return false; } const query = parseQuery(search); if (query.nuxt_component) { return false; } if (query.macro && (search === "?macro=true" || !opts.type || opts.type.includes("script"))) { return true; } const type = "setup" in query ? "script" : query.type; if (!("vue" in query) || opts.type && !opts.type.includes(type)) { return false; } return true; } const JS_RE = /\.(?:[cm]?j|t)sx?$/; function isJS(id) { const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href)); return JS_RE.test(pathname); } 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; } function toArray(value) { return Array.isArray(value) ? value : [value]; } async function isDirectory$1(path) { return (await promises.lstat(path)).isDirectory(); } async function resolvePagesRoutes() { const nuxt = useNuxt(); const pagesDirs = nuxt.options._layers.map( (layer) => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || "pages") ); const scannedFiles = []; for (const dir of pagesDirs) { const files = await resolveFiles(dir, `**/*{${nuxt.options.extensions.join(",")}}`); scannedFiles.push(...files.map((file) => ({ relativePath: relative(dir, file), absolutePath: file }))); } scannedFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath, "en-US")); const allRoutes = await generateRoutesFromFiles(uniqueBy(scannedFiles, "relativePath"), { shouldUseServerComponents: !!nuxt.options.experimental.componentIslands }); const pages = uniqueBy(allRoutes, "path"); const shouldAugment = nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages; if (shouldAugment) { const augmentedPages = await augmentPages(pages, nuxt.vfs); await nuxt.callHook("pages:extend", pages); await augmentPages(pages, nuxt.vfs, augmentedPages); augmentedPages.clear(); } else { await nuxt.callHook("pages:extend", pages); } return pages; } function generateRoutesFromFiles(files, options = {}) { const routes = []; for (const file of files) { const segments = file.relativePath.replace(new RegExp(`${escapeRE(extname(file.relativePath))}$`), "").split("/"); const route = { name: "", path: "", file: file.absolutePath, children: [] }; let parent = routes; const lastSegment = segments[segments.length - 1]; if (lastSegment.endsWith(".server")) { segments[segments.length - 1] = lastSegment.replace(".server", ""); if (options.shouldUseServerComponents) { route.mode = "server"; } } else if (lastSegment.endsWith(".client")) { segments[segments.length - 1] = lastSegment.replace(".client", ""); route.mode = "client"; } for (let i = 0; i < segments.length; i++) { const segment = segments[i]; const tokens = parseSegment(segment); const segmentName = tokens.map(({ value }) => value).join(""); route.name += (route.name && "/") + segmentName; const path = withLeadingSlash(joinURL(route.path, getRoutePath(tokens).replace(/\/index$/, "/"))); const child = parent.find((parentRoute) => parentRoute.name === route.name && parentRoute.path === path); if (child && child.children) { parent = child.children; route.path = ""; } else if (segmentName === "index" && !route.path) { route.path += "/"; } else if (segmentName !== "index") { route.path += getRoutePath(tokens); } } parent.push(route); } return prepareRoutes(routes); } async function augmentPages(routes, vfs, augmentedPages = /* @__PURE__ */ new Set()) { for (const route of routes) { if (route.file && !augmentedPages.has(route.file)) { const fileContent = route.file in vfs ? vfs[route.file] : fs.readFileSync(await resolvePath$1(route.file), "utf-8"); const routeMeta = await getRouteMeta(fileContent, route.file); if (route.meta) { routeMeta.meta = { ...routeMeta.meta, ...route.meta }; } Object.assign(route, routeMeta); augmentedPages.add(route.file); } if (route.children && route.children.length > 0) { await augmentPages(route.children, vfs, augmentedPages); } } return augmentedPages; } const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/i; function extractScriptContent(html) { const groups = html.match(SFC_SCRIPT_RE)?.groups || {}; if (groups.content) { return { loader: groups.attrs.includes("tsx") ? "tsx" : "ts", code: groups.content.trim() }; } return null; } const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/; const DYNAMIC_META_KEY = "__nuxt_dynamic_meta_key"; const pageContentsCache = {}; const metaCache$1 = {}; async function getRouteMeta(contents, absolutePath) { if (!(absolutePath in pageContentsCache) || pageContentsCache[absolutePath] !== contents) { pageContentsCache[absolutePath] = contents; delete metaCache$1[absolutePath]; } if (absolutePath in metaCache$1) { return metaCache$1[absolutePath]; } const script = extractScriptContent(contents); if (!script) { metaCache$1[absolutePath] = {}; return {}; } if (!PAGE_META_RE.test(script.code)) { metaCache$1[absolutePath] = {}; return {}; } const js = await transform(script.code, { loader: script.loader }); const ast = parse(js.code, { sourceType: "module", ecmaVersion: "latest", ranges: true }); const extractedMeta = {}; const extractionKeys = ["name", "path", "alias", "redirect"]; const dynamicProperties = /* @__PURE__ */ new Set(); let foundMeta = false; walk(ast, { enter(node) { if (foundMeta) { return; } if (node.type !== "ExpressionStatement" || node.expression.type !== "CallExpression" || node.expression.callee.type !== "Identifier" || node.expression.callee.name !== "definePageMeta") { return; } foundMeta = true; const pageMetaArgument = node.expression.arguments[0]; for (const key of extractionKeys) { const property = pageMetaArgument.properties.find((property2) => property2.type === "Property" && property2.key.type === "Identifier" && property2.key.name === key); if (!property) { continue; } if (property.value.type === "ObjectExpression") { const valueString = js.code.slice(property.value.range[0], property.value.range[1]); try { extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {})); } catch { console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`); dynamicProperties.add(key); continue; } } if (property.value.type === "ArrayExpression") { const values = []; for (const element of property.value.elements) { if (!element) { continue; } if (element.type !== "Literal" || typeof element.value !== "string") { console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`); dynamicProperties.add(key); continue; } values.push(element.value); } extractedMeta[key] = values; continue; } if (property.value.type !== "Literal" || typeof property.value.value !== "string") { console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`); dynamicProperties.add(key); continue; } extractedMeta[key] = property.value.value; } for (const property of pageMetaArgument.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.includes(name)) { dynamicProperties.add("meta"); break; } } if (dynamicProperties.size) { extractedMeta.meta ?? (extractedMeta.meta = {}); extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties; } } }); metaCache$1[absolutePath] = extractedMeta; return extractedMeta; } function getRoutePath(tokens) { return tokens.reduce((path, token) => { return path + (token.type === 2 /* optional */ ? `:${token.value}?` : token.type === 1 /* dynamic */ ? `:${token.value}()` : token.type === 3 /* catchall */ ? `:${token.value}(.*)*` : encodePath(token.value).replace(/:/g, "\\:")); }, "/"); } const PARAM_CHAR_RE = /[\w.]/; function parseSegment(segment) { let state = 0 /* initial */; let i = 0; let buffer = ""; const tokens = []; function consumeBuffer() { if (!buffer) { return; } if (state === 0 /* initial */) { throw new Error("wrong state"); } tokens.push({ type: state === 1 /* static */ ? 0 /* static */ : state === 2 /* dynamic */ ? 1 /* dynamic */ : state === 3 /* optional */ ? 2 /* optional */ : 3 /* catchall */, value: buffer }); buffer = ""; } while (i < segment.length) { const c = segment[i]; switch (state) { case 0 /* initial */: buffer = ""; if (c === "[") { state = 2 /* dynamic */; } else { i--; state = 1 /* static */; } break; case 1 /* static */: if (c === "[") { consumeBuffer(); state = 2 /* dynamic */; } else { buffer += c; } break; case 4 /* catchall */: case 2 /* dynamic */: case 3 /* optional */: if (buffer === "...") { buffer = ""; state = 4 /* catchall */; } if (c === "[" && state === 2 /* dynamic */) { state = 3 /* optional */; } if (c === "]" && (state !== 3 /* optional */ || segment[i - 1] === "]")) { if (!buffer) { throw new Error("Empty param"); } else { consumeBuffer(); } state = 0 /* initial */; } else if (PARAM_CHAR_RE.test(c)) { buffer += c; } else ; break; } i++; } if (state === 2 /* dynamic */) { throw new Error(`Unfinished param "${buffer}"`); } consumeBuffer(); return tokens; } function findRouteByName(name, routes) { for (const route of routes) { if (route.name === name) { return route; } } return findRouteByName(name, routes); } function prepareRoutes(routes, parent, names = /* @__PURE__ */ new Set()) { for (const route of routes) { if (route.name) { route.name = route.name.replace(/\/index$/, "").replace(/\//g, "-"); if (names.has(route.name)) { const existingRoute = findRouteByName(route.name, routes); const extra = existingRoute?.name ? `is the same as \`${existingRoute.file}\`` : "is a duplicate"; logger.warn(`Route name generated for \`${route.file}\` ${extra}. You may wish to set a custom name using \`definePageMeta\` within the page file.`); } } if (parent && route.path[0] === "/") { route.path = route.path.slice(1); } if (route.children?.length) { route.children = prepareRoutes(route.children, route, names); } if (route.children?.find((childRoute) => childRoute.path === "")) { delete route.name; } if (route.name) { names.add(route.name); } } return routes; } function serializeRouteValue(value, skipSerialisation = false) { if (skipSerialisation || value === void 0) { return void 0; } return JSON.stringify(value); } function normalizeRoutes(routes, metaImports = /* @__PURE__ */ new Set(), overrideMeta = false) { 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), name: serializeRouteValue(page.name), meta: serializeRouteValue(metaFiltered, skipMeta), alias: serializeRouteValue(toArray(page.alias), skipAlias), redirect: serializeRouteValue(page.redirect) }; for (const key of ["path", "name", "meta", "alias", "redirect"]) { if (route[key] === void 0) { delete route[key]; } } if (page.children?.length) { route.children = normalizeRoutes(page.children, metaImports, overrideMeta).routes; } if (!page.file) { return route; } const file = normalize(page.file); const pageImportName = genSafeVariableName(filename(file) + hash(file)); 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 pageImport = page._sync && page.mode !== "client" ? pageImportName : genDynamicImport(file, { interopDefault: true }); const metaRoute = { name: `${metaImportName}?.name ?? ${route.name}`, path: `${metaImportName}?.path ?? ${route.path}`, meta: `${metaImportName} || {}`, alias: `${metaImportName}?.alias || []`, redirect: `${metaImportName}?.redirect`, component: page.mode === "server" ? `() => createIslandPage(${route.name})` : page.mode === "client" ? `() => createClientPage(${pageImport})` : pageImport }; if (page.mode === "server") { metaImports.add(` let _createIslandPage async function createIslandPage (name) { _createIslandPage ||= await import(${JSON.stringify(resolve(distDir, "components/runtime/server-component"))}).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(resolve(distDir, "components/runtime/client-component"))}).then(r => r.createClientPage) return _createClientPage(loader); }`); } if (route.children) { metaRoute.children = route.children; } if (route.meta) { metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`; } if (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"]) { 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; })) }; } function pathToNitroGlob(path) { if (!path) { return null; } if (path.indexOf(":") !== path.lastIndexOf(":")) { return null; } return path.replace(/\/[^:/]*:\w.*$/, "/**"); } function resolveRoutePaths(page, parent = "/") { return [ joinURL(parent, page.path), ...page.children?.flatMap((child) => resolveRoutePaths(child, joinURL(parent, page.path))) || [] ]; } const ROUTE_RULE_RE = /\bdefineRouteRules\(/; const ruleCache = {}; async function extractRouteRules(code) { if (code in ruleCache) { return ruleCache[code]; } if (!ROUTE_RULE_RE.test(code)) { return null; } const script = extractScriptContent(code); code = script?.code || code; let rule = null; const js = await transform(code, { loader: script?.loader || "ts" }); walk(parse(js.code, { sourceType: "module", ecmaVersion: "latest" }), { enter(_node) { if (_node.type !== "CallExpression" || _node.callee.type !== "Identifier") { return; } const node = _node; const name = "name" in node.callee && node.callee.name; if (name === "defineRouteRules") { const rulesString = js.code.slice(node.start, node.end); try { rule = JSON.parse(runInNewContext(rulesString.replace("defineRouteRules", "JSON.stringify"), {})); } catch { throw new Error("[nuxt] Error parsing route rules. They should be JSON-serializable."); } } } }); ruleCache[code] = rule; return rule; } function getMappedPages(pages, paths = {}, prefix = "") { for (const page of pages) { if (page.file) { const filename = normalize(page.file); paths[filename] = pathToNitroGlob(prefix + page.path); } if (page.children) { getMappedPages(page.children, paths, page.path + "/"); } } return paths; } const HAS_MACRO_RE = /\bdefinePageMeta\s*\(\s*/; const CODE_EMPTY = ` const __nuxt_page_meta = null 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 = createUnplugin((options) => { return { name: "nuxt:pages-macros-transform", enforce: "post", transformInclude(id) { return !!parseMacroQuery(id).macro; }, transform(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(CODE_EMPTY + (options.dev ? CODE_HMR : "")); const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href)); logger.error(`The file \`${pathname}\` is not a valid page as it has no content.`); } else { s.overwrite(0, code.length, CODE_EMPTY + (options.dev ? CODE_HMR : "")); } 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); } } walk(this.parse(code, { sourceType: "module", ecmaVersion: "latest" }), { enter(_node) { if (_node.type !== "CallExpression" || _node.callee.type !== "Identifier") { return; } const node = _node; const name = "name" in node.callee && node.callee.name; if (name !== "definePageMeta") { return; } const meta = node.arguments[0]; let contents = `const __nuxt_page_meta = ${code.slice(meta.start, meta.end) || "null"} export default __nuxt_page_meta` + (options.dev ? CODE_HMR : ""); function addImport(name2) { if (name2 && importMap.has(name2)) { const importValue = importMap.get(name2).code; if (!addedImports.has(importValue)) { contents = importMap.get(name2).code + "\n" + contents; addedImports.add(importValue); } } } walk(meta, { enter(_node2) { if (_node2.type === "CallExpression") { const node2 = _node2; addImport("name" in node2.callee && node2.callee.name); } if (_node2.type === "Identifier") { const node2 = _node2; addImport(node2.name); } } }); s.overwrite(0, code.length, contents); } }); if (!s.hasChanged() && !code.includes("__nuxt_page_meta")) { s.overwrite(0, code.length, CODE_EMPTY + (options.dev ? CODE_HMR : "")); } return result(); }, vite: { handleHotUpdate: { order: "pre", handler: ({ modules }) => { const index = modules.findIndex((i) => i.id?.includes("?macro=true")); if (index !== -1) { modules.splice(index, 1); } } } } }; }); function rewriteQuery(id) { return id.replace(/\?.+$/, (r) => "?macro=true&" + r.replace(/^\?/, "").replace(/&macro=true/, "")); } function parseMacroQuery(id) { const { search } = parseURL(decodeURIComponent(isAbsolute(id) ? pathToFileURL(id).href : id).replace(/\?macro=true$/, "")); const query = parseQuery(search); if (id.includes("?macro=true")) { return { macro: "true", ...query }; } return query; } function getQuotedSpecifier(id) { return id.match(/(["']).*\1/)?.[0]; } const INJECTION_RE_TEMPLATE = /\b_ctx\.\$route\b/g; const INJECTION_RE_SCRIPT = /\bthis\.\$route\b/g; 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(code) { if (!INJECTION_SINGLE_RE.test(code) || code.includes("_ctx._.provides[__nuxt_route_symbol") || code.includes("this._.provides[__nuxt_route_symbol")) { return; } let replaced = false; const s = new MagicString(code); const strippedCode = stripLiteral(code); const replaceMatches = (regExp, replacement) => { for (const match of strippedCode.matchAll(regExp)) { const start = match.index; const end = start + match[0].length; s.overwrite(start, end, replacement); if (!replaced) { replaced = true; } } }; replaceMatches(INJECTION_RE_TEMPLATE, "(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)"); replaceMatches(INJECTION_RE_SCRIPT, "(this._.provides[__nuxt_route_symbol] || this.$route)"); if (replaced) { s.prepend("import { PageRouteSymbol as __nuxt_route_symbol } from '#app/components/injections';\n"); } if (s.hasChanged()) { return { code: s.toString(), map: nuxt.options.sourcemap.client || nuxt.options.sourcemap.server ? s.generateMap({ hires: true }) : void 0 }; } } }; }); const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/; const pagesModule = defineNuxtModule({ meta: { name: "pages" }, async setup(_options, nuxt) { const useExperimentalTypedPages = nuxt.options.experimental.typedPages; const runtimeDir = resolve(distDir, "pages/runtime"); const pagesDirs = nuxt.options._layers.map( (layer) => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || "pages") ); nuxt.options.alias["#vue-router"] = "vue-router"; const routerPath = await resolveTypePath("vue-router", "", nuxt.options.modulesDir) || "vue-router"; nuxt.hook("prepare:types", ({ tsConfig }) => { var _a; tsConfig.compilerOptions || (tsConfig.compilerOptions = {}); (_a = tsConfig.compilerOptions).paths || (_a.paths = {}); tsConfig.compilerOptions.paths["#vue-router"] = [routerPath]; delete tsConfig.compilerOptions.paths["#vue-router/*"]; }); async function resolveRouterOptions() { 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: resolve(runtimeDir, "router.options"), optional: true }); await nuxt.callHook("pages:routerOptions", context); return context.files; } const isNonEmptyDir = (dir) => existsSync(dir) && readdirSync(dir).length; const userPreference = nuxt.options.pages; const isPagesEnabled = async () => { if (typeof userPreference === "boolean") { return userPreference; } const routerOptionsFiles = await resolveRouterOptions(); if (routerOptionsFiles.filter((p) => !p.optional).length > 0) { return true; } if (pagesDirs.some((dir) => isNonEmptyDir(dir))) { return true; } const pages = await resolvePagesRoutes(); if (pages.length) { if (nuxt.apps.default) { nuxt.apps.default.pages = pages; } return true; } return false; }; nuxt.options.pages = await isPagesEnabled(); if (nuxt.options.dev && nuxt.options.pages) { addPlugin(resolve(runtimeDir, "plugins/check-if-page-unused")); } nuxt.hook("app:templates", async (app) => { app.pages = await resolvePagesRoutes(); 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 : 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 (nuxt.options.pages !== newSetting) { logger.info("Pages", newSetting ? "enabled" : "disabled"); return nuxt.callHook("restart"); } } }); if (!nuxt.options.pages) { 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") }); addTypeTemplate({ filename: "types/middleware.d.ts", getContents: () => [ "declare module 'nitropack' {", " interface NitroRouteConfig {", " appMiddleware?: string | string[] | Record<string, boolean>", " }", "}", "export {}" ].join("\n") }); 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 = "./types/typed-router.d.ts"; const options = { routesFolder: [], dts: resolve(nuxt.options.buildDir, declarationFile), logs: nuxt.options.debug, async beforeWriteFiles(rootPage) { rootPage.children.forEach((child) => child.delete()); const pages = nuxt.apps.default?.pages || await resolvePagesRoutes(); if (nuxt.apps.default) { nuxt.apps.default.pages = pages; } function addPage(parent, page) { const route = parent.insert(page.path, page.file); if (page.meta) { route.addToMeta(page.meta); } if (page.alias) { route.addAlias(page.alias); } if (page.name) { route.name = page.name; } if (page.children) { page.children.forEach((child) => addPage(route, child)); } } for (const page of pages) { addPage(rootPage, page); } } }; nuxt.hook("prepare:types", ({ references }) => { references.push({ path: declarationFile }); references.push({ types: "unplugin-vue-router/client" }); }); const context = createRoutesContext(resolveOptions(options)); const dtsFile = resolve(nuxt.options.buildDir, declarationFile); await mkdir(dirname(dtsFile), { recursive: true }); await context.scanPages(false); if (nuxt.options._prepare || !nuxt.options.dev) { const dts = await readFile(dtsFile, "utf-8"); addTemplate({ filename: "types/typed-router.d.ts", getContents: () => dts }); } nuxt.hook("app:templatesGenerated", async (_app, _templates, options2) => { if (!options2?.filter || options2.filter({ filename: "routes.mjs" })) { await context.scanPages(); } }); } nuxt.hook("prepare:types", ({ references }) => { references.push({ types: useExperimentalTypedPages ? "vue-router/auto-routes" : "vue-router" }); }); nuxt.hook("imports:sources", (sources) => { const routerImports = sources.find((s) => s.from === "#app/composables/router" && s.imports.includes("onBeforeRouteLeave")); if (routerImports) { routerImports.from = "vue-router"; } }); const updateTemplatePaths = nuxt.options._layers.flatMap((l) => { const dir = (l.config.rootDir === nuxt.options.rootDir ? nuxt.options : l.config).dir; return [ resolve(l.config.srcDir || l.cwd, dir?.pages || "pages") + "/", resolve(l.config.srcDir || l.cwd, dir?.layouts || "layouts") + "/", resolve(l.config.srcDir || l.cwd, dir?.middleware || "middleware") + "/" ]; }); function isPage(file, pages = nuxt.apps.default.pages) { if (!pages) { return false; } return pages.some((page) => page.file === file) || pages.some((page) => page.children && isPage(file, page.children)); } nuxt.hook("builder:watch", async (event, relativePath) => { const path = resolve(nuxt.options.srcDir, relativePath); const shouldAlwaysRegenerate = nuxt.options.experimental.scanPageMeta && isPage(path); if (event === "change" && !shouldAlwaysRegenerate) { return; } if (shouldAlwaysRegenerate || updateTemplatePaths.some((dir) => path.startsWith(dir))) { await updateTemplates({ filter: (template) => template.filename === "routes.mjs" }); } }); nuxt.hook("app:resolve", (app) => { if (app.mainComponent === resolve(nuxt.options.appDir, "components/welcome.vue")) { app.mainComponent = resolve(runtimeDir, "app.vue"); } app.middleware.unshift({ name: "validate", path: resolve(runtimeDir, "validate"), global: true }); }); nuxt.hook("app:resolve", (app) => { const nitro = useNitro(); if (nitro.options.prerender.crawlLinks) { app.plugins.push({ src: resolve(runtimeDir, "plugins/prerender.server"), mode: "server" }); } }); const prerenderRoutes = /* @__PURE__ */ new Set(); function processPages(pages, currentPath = "/") { for (const page of pages) { if (OPTIONAL_PARAM_RE.test(page.path) && !page.children?.length) { prerenderRoutes.add(currentPath); } if (page.path.includes(":")) { continue; } const route = joinURL(currentPath, page.path); prerenderRoutes.add(route); if (page.children) { processPages(page.children, route); } } } nuxt.hook("pages:extend", (pages) => { if (nuxt.options.dev) { return; } prerenderRoutes.clear(); processPages(pages); }); nuxt.hook("nitro:build:before", (nitro) => { if (nuxt.options.dev || !nitro.options.static || nuxt.options.router.options.hashMode || !nitro.options.prerender.crawlLinks) { return; } if (nuxt.options.ssr) { const [firstPage] = [...prerenderRoutes].sort(); nitro.options.prerender.routes.push(firstPage || "/"); return; } for (const route of nitro.options.prerender.routes || []) { prerenderRoutes.add(route); } nitro.options.prerender.routes = Array.from(prerenderRoutes); }); nuxt.hook("imports:extend", (imports) => { imports.push( { name: "definePageMeta", as: "definePageMeta", from: resolve(runtimeDir, "composables") }, { name: "useLink", as: "useLink", from: "vue-router" } ); if (nuxt.options.experimental.inlineRouteRules) { imports.push({ name: "defineRouteRules", as: "defineRouteRules", from: resolve(runtimeDir, "composables") }); } }); if (nuxt.options.experimental.inlineRouteRules) { let pageToGlobMap = {}; nuxt.hook("pages:extend", (pages) => { pageToGlobMap = getMappedPages(pages); }); const inlineRules = {}; let updateRouteConfig; nuxt.hook("nitro:init", (nitro) => { updateRouteConfig = () => nitro.updateConfig({ routeRules: defu(inlineRules, nitro.options._config.routeRules) }); }); const updatePage = async function updatePage2(path) { const glob = pageToGlobMap[path]; const code = path in nuxt.vfs ? nuxt.vfs[path] : await readFile(path, "utf-8"); try { const extractedRule = await extractRouteRules(code); if (extractedRule) { if (!glob) { const relativePath = relative(nuxt.options.srcDir, path); logger.error(`Could not set inline route rules in \`~/${relativePath}\` as it could not be mapped to a Nitro route.`); return; } inlineRules[glob] = extractedRule; } else if (glob) { delete inlineRules[glob]; } } catch (e) { if (e.toString().includes("Error parsing route rules")) { const relativePath = relative(nuxt.options.srcDir, path); logger.error(`Error parsing route rules within \`~/${relativePath}\`. They should be JSON-serializable.`); } else { logger.error(e); } } }; nuxt.hook("builder:watch", async (event, relativePath) => { const path = resolve(nuxt.options.srcDir, relativePath); if (!(path in pageToGlobMap)) { return; } if (event === "unlink") { delete inlineRules[path]; delete pageToGlobMap[path]; } else { await updatePage(path); } await updateRouteConfig?.(); }); nuxt.hooks.hookOnce("pages:extend", async () => { for (const page in pageToGlobMap) { await updatePage(page); } await updateRouteConfig?.(); }); } if (nuxt.options.experimental.appManifest) { const componentStubPath = await resolvePath$1(resolve(runtimeDir, "component-stub")); nuxt.hook("pages:extend", (routes) => { const nitro = useNitro(); let resolvedRoutes; for (const path in nitro.options.routeRules) { const rule = nitro.options.routeRules[path]; if (!rule.redirect) { continue; } resolvedRoutes || (resolvedRoutes = routes.flatMap((route) => resolveRoutePaths(route))); if (resolvedRoutes.includes(path)) { continue; } routes.push({ _sync: true, path: path.replace(/\/[^/]*\*\*/, "/:pathMatch(.*)"), file: componentStubPath }); } }); } const pageMetaOptions = { dev: nuxt.options.dev, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client }; nuxt.hook("modules:done", () => { addVitePlugin(() => PageMetaPlugin.vite(pageMetaOptions)); addWebpackPlugin(() => PageMetaPlugin.webpack(pageMetaOptions)); }); addPlugin(resolve(runtimeDir, "plugins/prefetch.client")); if (nuxt.options.experimental.templateRouteInjection) { addBuildPlugin(RouteInjectionPlugin(nuxt), { server: false }); } addPlugin(resolve(runtimeDir, "plugins/router")); const getSources = (pages) => pages.filter((p) => Boolean(p.file)).flatMap( (p) => [relative(nuxt.options.srcDir, p.file), ...p.children?.length ? getSources(p.children) : []] ); nuxt.hook("build:manifest", (manifest) => { if (nuxt.options.dev) { return; } const sourceFiles = nuxt.apps.default?.pages?.length ? getSources(nuxt.apps.default.pages) : []; for (const key in manifest) { if (manifest[key].src && Object.values(nuxt.apps).some((app) => app.pages?.some((page) => page.mode === "server" && page.file === join(nuxt.options.srcDir, manifest[key].src)))) { delete manifest[key]; continue; } if (manifest[key].isEntry) { manifest[key].dynamicImports = manifest[key].dynamicImports?.filter((i) => !sourceFiles.includes(i)); } } }); addTemplate({ filename: "routes.mjs", getContents({ app }) { if (!app.pages) { return "export default []"; } const { routes, imports } = normalizeRoutes(app.pages, /* @__PURE__ */ new Set(), nuxt.options.experimental.scanPageMeta); return [...imports, `export default ${routes}`].join("\n"); } }); addTemplate({ filename: "pages.mjs", getContents: () => "export { START_LOCATION, useRoute } from 'vue-router'" }); nuxt.options.vite.resolve = nuxt.options.vite.resolve || {}; nuxt.options.vite.resolve.dedupe = nuxt.options.vite.resolve.dedupe || []; nuxt.options.vite.resolve.dedupe.push("vue-router"); addTemplate({ filename: "router.options.mjs", getContents: async () => { const routerOptionsFiles = await resolveRouterOptions(); const configRouterOptions = genObjectFromRawEntries(Object.entries(nuxt.options.router.options).map(([key, value]) => [key, genString(value)])); return [ ...routerOptionsFiles.map((file, index) => genImport(file.path, `routerOptions${index}`)), `const configRouterOptions = ${configRouterOptions}`, "export default {", "...configRouterOptions,", ...routerOptionsFiles.map((_, index) => `...routerOptions${index},`), "}" ].join("\n"); } }); addTypeTemplate({ filename: "types/middleware.d.ts", getContents: ({ nuxt: nuxt2, app }) => { const composablesFile = relative(join(nuxt2.options.buildDir, "types"), resolve(runtimeDir, "composables")); const namedMiddleware = app.middleware.filter((mw) => !mw.global); return [ "import type { NavigationGuard } from 'vue-router'", `export type MiddlewareKey = ${namedMiddleware.map((mw) => genString(mw.name)).join(" | ") || "string"}`, `declare module ${genString(composablesFile)} {`, " interface PageMeta {", " middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>", " }", "}", "declare module 'nitropack' {", " interface NitroRouteConfig {", " appMiddleware?: MiddlewareKey | MiddlewareKey[] | Record<MiddlewareKey, boolean>", " }", "}" ].join("\n"); } }); addTypeTemplate({ filename: "types/layouts.d.ts", getContents: ({ nuxt: nuxt2, app }) => { const composablesFile = relative(join(nuxt2.options.buildDir, "types"), resolve(runtimeDir, "composables")); return [ "import type { ComputedRef, MaybeRef } from 'vue'", `export type LayoutKey = ${Object.keys(app.layouts).map((name) => genString(name)).join(" | ") || "string"}`, `declare module ${genString(composablesFile)} {`, " interface PageMeta {", " layout?: MaybeRef<LayoutKey | false> | ComputedRef<LayoutKey | false>", " }", "}" ].join("\n"); } }); if (nuxt.options.experimental.viewTransition) { addTypeTemplate({ filename: "types/view-transitions.d.ts", getContents: ({ nuxt: nuxt2 }) => { const runtimeDir2 = resolve(distDir, "pages/runtime"); const composablesFile = relative(join(nuxt2.options.buildDir, "types"), resolve(runtimeDir2, "composables")); return [ "import type { ComputedRef, MaybeRef } from 'vue'", `declare module ${genString(composablesFile)} {`, " interface PageMeta {", " viewTransition?: boolean | 'always'", " }", "}" ].join("\n"); } }); } addComponent({ name: "NuxtPage", priority: 10, // built-in that we do not expect the user to override filePath: resolve(distDir, "pages/runtime/page") }); } }); const components = ["NoScript", "Link", "Base", "Title", "Meta", "Style", "Head", "Html", "Body"]; const metaModule = defineNuxtModule({ meta: { name: "meta", configKey: "unhead" }, async setup(options, nuxt) { const runtimeDir = resolve(distDir, "head/runtime"); nuxt.options.build.transpile.push("@unhead/vue"); const componentsPath = resolve(runtimeDir, "components"); for (const componentName of components) { addComponent({ name: componentName, filePath: componentsPath, export: componentName, // built-in that we do not expect the user to ov