nuxt
Version:
1,299 lines (1,287 loc) • 221 kB
JavaScript
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(/¯o=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