nuxt
Version:
1,310 lines (1,297 loc) • 287 kB
JavaScript
import fs, { promises, existsSync, readdirSync, statSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
import { mkdir, readFile, readdir, writeFile, rm, stat, unlink, open } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import { AsyncLocalStorage } from 'node:async_hooks';
import { dirname, resolve, normalize, basename, extname, relative, isAbsolute, join } from 'pathe';
import { createHooks, createDebugger } from 'hookable';
import ignore from 'ignore';
import { useLogger, tryUseNuxt, useNuxt, directoryToURL, resolveFiles, resolvePath, defineNuxtModule, findPath, addPlugin, addTemplate, addTypeTemplate, addComponent, useNitro, addBuildPlugin, isIgnored, resolveAlias, addPluginTemplate, addImportsSources, addVitePlugin, createIsIgnored, updateTemplates, tryResolveModule, normalizeModuleTranspilePath, resolveNuxtModule, resolveIgnorePatterns, logger as logger$1, createResolver, importModule, tryImportModule, runWithNuxtContext, nuxtCtx, loadNuxtConfig, addWebpackPlugin, addServerPlugin, installModule, addServerTemplate, addServerHandler, addRouteMiddleware, normalizeTemplate, normalizePlugin } from '@nuxt/kit';
import { resolvePackageJSON, readPackageJSON } from 'pkg-types';
import { hash, serialize } from 'ohash';
import consola, { consola as consola$1 } from 'consola';
import onChange from 'on-change';
import { colors } from 'consola/utils';
import { resolveCompatibilityDatesFromEnv, formatDate } from 'compatx';
import escapeRE from 'escape-string-regexp';
import { withTrailingSlash, parseURL, parseQuery, joinURL, withLeadingSlash, encodePath, withoutLeadingSlash } from 'ufo';
import { ImpoundPlugin } from 'impound';
import defu$1, { defu } from 'defu';
import { satisfies, coerce } from 'semver';
import { isCI, provider, isWindows, hasTTY } from 'std-env';
import { genArrayFromRaw, genSafeVariableName, genImport, genDynamicImport, genObjectFromRawEntries, genString, genExport } from 'knitwork';
import { resolveModulePath } from 'exsolve';
import { addDependency } from 'nypm';
import { reverseResolveAlias, filename } from 'pathe/utils';
import { createRoutesContext } from 'unplugin-vue-router';
import { resolveOptions } from 'unplugin-vue-router/options';
import { toRouteMatcher, createRouter, exportMatcher } from 'radix3';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { runInNewContext } from 'node:vm';
import { klona } from 'klona';
import { parseAndWalk, ScopeTracker, walk, isBindingIdentifier, getUndeclaredIdentifiersInFunction } from 'oxc-walker';
import { splitByCase, kebabCase, pascalCase, camelCase, upperFirst } from 'scule';
import { createUnplugin } from 'unplugin';
import { findStaticImports, findExports, parseStaticImport, parseNodeModulePath, lookupNodeModuleSubpath } from 'mlly';
import MagicString from 'magic-string';
import { stripLiteral } from 'strip-literal';
import { unheadVueComposablesImports } from '@unhead/vue';
import { defineUnimportPreset, createUnimport, toExports, scanDirExports } from 'unimport';
import { glob } from 'tinyglobby';
import { parse, walk as walk$1, ELEMENT_NODE } from 'ultrahtml';
import { parseQuery as parseQuery$1 } from 'vue-router';
import { createTransformer } from 'unctx/transform';
import { cpus } from 'node:os';
import { createNitro, scanHandlers, writeTypes, copyPublicAssets, prepare, build as build$1, prerender, createDevServer } from 'nitropack';
import { dynamicEventHandler, defineEventHandler } from 'h3';
import { watch as watch$1 } from 'chokidar';
import { debounce } from 'perfect-debounce';
import { resolveSchema, generateTypes } from 'untyped';
import untypedPlugin from 'untyped/babel-plugin';
import { createJiti } from 'jiti';
import { transform } from 'oxc-transform';
import { minify } from 'oxc-minify';
import { resolve as resolve$1 } from 'node:path';
import { parseTar, createTar } from 'nanotar';
function toArray(value) {
return Array.isArray(value) ? value : [value];
}
async function isDirectory$1(path) {
return (await promises.lstat(path)).isDirectory();
}
const logger = useLogger("nuxt");
function resolveToAlias(path, nuxt = tryUseNuxt()) {
return reverseResolveAlias(path, { ...nuxt?.options.alias || {}, ...strippedAtAliases }).pop() || path;
}
const strippedAtAliases = {
"@": "",
"@@": ""
};
const isStackblitz = provider === "stackblitz";
async function promptToInstall(name, installCommand, options) {
for (const parent of options.searchPaths || []) {
if (await resolvePackageJSON(name, { parent }).catch(() => null)) {
return true;
}
}
logger.info(`Package ${name} is missing`);
if (isCI) {
return false;
}
if (options.prompt === true || options.prompt !== false && !isStackblitz) {
const confirm = await logger.prompt(`Do you want to install ${name} package?`, {
type: "confirm",
name: "confirm",
initial: true
});
if (!confirm) {
return false;
}
}
logger.info(`Installing ${name}...`);
try {
await installCommand();
logger.success(`Installed ${name}`);
return true;
} catch (err) {
logger.error(err);
return false;
}
}
const installPrompts = /* @__PURE__ */ new Set();
function installNuxtModule(name, options) {
if (installPrompts.has(name)) {
return;
}
installPrompts.add(name);
const nuxt = useNuxt();
return promptToInstall(name, async () => {
const { runCommand } = await import('@nuxt/cli');
await runCommand("module", ["add", name, "--cwd", nuxt.options.rootDir]);
}, { rootDir: nuxt.options.rootDir, searchPaths: nuxt.options.modulesDir, ...options });
}
function ensurePackageInstalled(name, options) {
return promptToInstall(name, () => addDependency(name, {
cwd: options.rootDir,
dev: true
}), options);
}
const features = {
__proto__: null,
ensurePackageInstalled: ensurePackageInstalled,
installNuxtModule: installNuxtModule
};
let _distDir = dirname(fileURLToPath(import.meta.url));
if (_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 = resolveModulePath(path, {
from: searchPaths?.map((d) => directoryToURL(d)),
conditions: ["types", "import", "require"],
extensions: [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]
});
if (subpath) {
return r.replace(/(?:\.d)?\.[mc]?[jt]s$/, "");
}
const rootPath = await resolvePackageJSON(r);
return dirname(rootPath);
} catch {
return null;
}
}
function getNameFromPath(path, relativeTo) {
const relativePath = relativeTo ? normalize(path).replace(withTrailingSlash(normalize(relativeTo)), "") : basename(path);
const prefixParts = splitByCase(dirname(relativePath));
const fileName = basename(relativePath, extname(relativePath));
const segments = resolveComponentNameSegments(fileName.toLowerCase() === "index" ? "" : fileName, prefixParts).filter(Boolean);
return kebabCase(segments).replace(QUOTE_RE, "");
}
function hasSuffix(path, suffix) {
return basename(path, extname(path)).endsWith(suffix);
}
function resolveComponentNameSegments(fileName, prefixParts) {
const fileNameParts = splitByCase(fileName);
const fileNamePartsContent = fileNameParts.join("/").toLowerCase();
const componentNameParts = prefixParts.flatMap((p) => splitByCase(p));
let index = prefixParts.length - 1;
const matchedSuffix = [];
while (index >= 0) {
const prefixPart = prefixParts[index];
matchedSuffix.unshift(...splitByCase(prefixPart).map((p) => p.toLowerCase()));
const matchedSuffixContent = matchedSuffix.join("/");
if (fileNamePartsContent === matchedSuffixContent || fileNamePartsContent.startsWith(matchedSuffixContent + "/") || // e.g. Item/Item/Item.vue -> Item
prefixPart.toLowerCase() === fileNamePartsContent && prefixParts[index + 1] && prefixParts[index] === prefixParts[index + 1]) {
componentNameParts.length = index;
}
index--;
}
return [...componentNameParts, ...fileNameParts];
}
function 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$1 = /\.(?:[cm]?j|t)sx?$/;
function isJS(id) {
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href));
return JS_RE$1.test(pathname);
}
function getLoader(id) {
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href));
const ext = extname(pathname);
if (ext === ".vue") {
return "vue";
}
if (!JS_RE$1.test(ext)) {
return null;
}
return ext.endsWith("x") ? "tsx" : "ts";
}
function matchWithStringOrRegex(value, matcher) {
if (typeof matcher === "string") {
return value === matcher;
} else if (matcher instanceof RegExp) {
return matcher.test(value);
}
return false;
}
function uniqueBy(arr, key) {
if (arr.length < 2) {
return arr;
}
const res = [];
const seen = /* @__PURE__ */ new Set();
for (const item of arr) {
if (seen.has(item[key])) {
continue;
}
seen.add(item[key]);
res.push(item);
}
return res;
}
const QUOTE_RE = /["']/g;
const EXTENSION_RE = /\b\.\w+$/g;
const SX_RE = /\.[tj]sx$/;
const enUSComparator = new Intl.Collator("en-US");
async function resolvePagesRoutes(pattern, nuxt = useNuxt()) {
const pagesDirs = nuxt.options._layers.map(
(layer) => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : layer.config.dir)?.pages || "pages")
);
const scannedFiles = [];
for (const dir of pagesDirs) {
const files = await resolveFiles(dir, pattern);
scannedFiles.push(...files.map((file) => ({ relativePath: relative(dir, file), absolutePath: file })));
}
scannedFiles.sort((a, b) => enUSComparator.compare(a.relativePath, b.relativePath));
const allRoutes = 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 === false) {
await nuxt.callHook("pages:extend", pages);
return pages;
}
const extraPageMetaExtractionKeys = nuxt.options?.experimental?.extraPageMetaExtractionKeys || [];
const augmentCtx = {
extraExtractionKeys: /* @__PURE__ */ new Set([
"middleware",
...extraPageMetaExtractionKeys
]),
fullyResolvedPaths: new Set(scannedFiles.map((file) => file.absolutePath))
};
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;
}
const INDEX_PAGE_RE = /\/index$/;
function generateRoutesFromFiles(files, options = {}) {
if (!files.length) {
return [];
}
const routes = [];
const sortedFiles = [...files].sort((a, b) => a.relativePath.length - b.relativePath.length);
for (const file of sortedFiles) {
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, file.absolutePath);
if (tokens.every((token) => token.type === 4 /* group */)) {
continue;
}
const segmentName = tokens.map(({ value, type }) => type === 4 /* group */ ? "" : value).join("");
route.name += (route.name && "/") + segmentName;
const routePath = getRoutePath(tokens, segments[i + 1] !== void 0 && segments[i + 1] !== "index");
const path = withLeadingSlash(joinURL(route.path, routePath.replace(INDEX_PAGE_RE, "/")));
const child = parent.find((parentRoute) => parentRoute.name === route.name && parentRoute.path === path.replace("([^/]*)*", "(.*)*"));
if (child && child.children) {
parent = child.children;
route.path = "";
} else if (segmentName === "index" && !route.path) {
route.path += "/";
} else if (segmentName !== "index") {
route.path += routePath;
}
}
parent.push(route);
}
return prepareRoutes(routes);
}
async function augmentPages(routes, vfs, ctx = {}) {
ctx.augmentedPages ??= /* @__PURE__ */ new Set();
for (const route of routes) {
if (route.file && !ctx.pagesToSkip?.has(route.file)) {
const fileContent = route.file in vfs ? vfs[route.file] : fs.readFileSync(ctx.fullyResolvedPaths?.has(route.file) ? route.file : await resolvePath(route.file), "utf-8");
const routeMeta = getRouteMeta(fileContent, route.file, ctx.extraExtractionKeys);
if (route.meta) {
routeMeta.meta = { ...routeMeta.meta, ...route.meta };
}
Object.assign(route, routeMeta);
ctx.augmentedPages.add(route.file);
}
if (route.children && route.children.length > 0) {
await augmentPages(route.children, vfs, ctx);
}
}
return ctx.augmentedPages;
}
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/gi;
function extractScriptContent(sfc) {
const contents = [];
for (const match of sfc.matchAll(SFC_SCRIPT_RE)) {
if (match?.groups?.content) {
contents.push({
loader: match.groups.attrs && /[tj]sx/.test(match.groups.attrs) ? "tsx" : "ts",
code: match.groups.content.trim()
});
}
}
return contents;
}
const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/;
const defaultExtractionKeys = ["name", "path", "props", "alias", "redirect", "middleware"];
const DYNAMIC_META_KEY = "__nuxt_dynamic_meta_key";
const pageContentsCache = {};
const metaCache$1 = {};
function getRouteMeta(contents, absolutePath, extraExtractionKeys = /* @__PURE__ */ new Set()) {
if (!(absolutePath in pageContentsCache) || pageContentsCache[absolutePath] !== contents) {
pageContentsCache[absolutePath] = contents;
delete metaCache$1[absolutePath];
}
if (absolutePath in metaCache$1 && metaCache$1[absolutePath]) {
return klona(metaCache$1[absolutePath]);
}
const loader = getLoader(absolutePath);
const scriptBlocks = !loader ? null : loader === "vue" ? extractScriptContent(contents) : [{ code: contents, loader }];
if (!scriptBlocks) {
metaCache$1[absolutePath] = {};
return {};
}
const extractedMeta = {};
const extractionKeys = /* @__PURE__ */ new Set([...defaultExtractionKeys, ...extraExtractionKeys]);
for (const script of scriptBlocks) {
if (!PAGE_META_RE.test(script.code)) {
continue;
}
const dynamicProperties = /* @__PURE__ */ new Set();
let foundMeta = false;
parseAndWalk(script.code, absolutePath.replace(/\.\w+$/, "." + script.loader), (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];
if (pageMetaArgument?.type !== "ObjectExpression") {
logger.warn(`\`definePageMeta\` must be called with an object literal (reading \`${absolutePath}\`).`);
return;
}
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;
}
const { value, serializable } = isSerializable(script.code, property.value);
if (!serializable) {
logger.debug(`Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`);
dynamicProperties.add(extraExtractionKeys.has(key) ? "meta" : key);
continue;
}
if (extraExtractionKeys.has(key)) {
extractedMeta.meta ??= {};
extractedMeta.meta[key] = value;
} else {
extractedMeta[key] = 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.has(name)) {
dynamicProperties.add("meta");
break;
}
}
if (dynamicProperties.size) {
extractedMeta.meta ??= {};
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties;
}
});
}
metaCache$1[absolutePath] = extractedMeta;
return klona(extractedMeta);
}
const COLON_RE = /:/g;
function getRoutePath(tokens, hasSucceedingSegment = false) {
return tokens.reduce((path, token) => {
return path + (token.type === 2 /* optional */ ? `:${token.value}?` : token.type === 1 /* dynamic */ ? `:${token.value}()` : token.type === 3 /* catchall */ ? hasSucceedingSegment ? `:${token.value}([^/]*)*` : `:${token.value}(.*)*` : token.type === 4 /* group */ ? "" : encodePath(token.value).replace(COLON_RE, "\\:"));
}, "/");
}
const PARAM_CHAR_RE = /[\w.]/;
function parseSegment(segment, absolutePath) {
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 */ : state === 4 /* catchall */ ? 3 /* catchall */ : 4 /* group */,
value: buffer
});
buffer = "";
}
while (i < segment.length) {
const c = segment[i];
switch (state) {
case 0 /* initial */:
buffer = "";
if (c === "[") {
state = 2 /* dynamic */;
} else if (c === "(") {
state = 5 /* group */;
} else {
i--;
state = 1 /* static */;
}
break;
case 1 /* static */:
if (c === "[") {
consumeBuffer();
state = 2 /* dynamic */;
} else if (c === "(") {
consumeBuffer();
state = 5 /* group */;
} else {
buffer += c;
}
break;
case 4 /* catchall */:
case 2 /* dynamic */:
case 3 /* optional */:
case 5 /* group */:
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 (c === ")" && state === 5 /* group */) {
if (!buffer) {
throw new Error("Empty group");
} else {
consumeBuffer();
}
state = 0 /* initial */;
} else if (c && PARAM_CHAR_RE.test(c)) {
buffer += c;
} else if (state === 2 /* dynamic */ || state === 3 /* optional */) {
if (c !== "[" && c !== "]") {
logger.warn(`'\`${c}\`' is not allowed in a dynamic route parameter and has been ignored. Consider renaming \`${absolutePath}\`.`);
}
}
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);
}
const NESTED_PAGE_RE = /\//g;
function prepareRoutes(routes, parent, names = /* @__PURE__ */ new Set()) {
for (const route of routes) {
if (route.name) {
route.name = route.name.replace(INDEX_PAGE_RE, "").replace(NESTED_PAGE_RE, "-");
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(), options) {
return {
imports: metaImports,
routes: genArrayFromRaw(routes.map((page) => {
const markedDynamic = page.meta?.[DYNAMIC_META_KEY] ?? /* @__PURE__ */ new Set();
const metaFiltered = {};
let skipMeta = true;
for (const key in page.meta || {}) {
if (key !== DYNAMIC_META_KEY && page.meta[key] !== void 0) {
skipMeta = false;
metaFiltered[key] = page.meta[key];
}
}
const skipAlias = toArray(page.alias).every((val) => !val);
const route = {
path: serializeRouteValue(page.path),
props: serializeRouteValue(page.props),
name: serializeRouteValue(page.name),
meta: serializeRouteValue(metaFiltered, skipMeta),
alias: serializeRouteValue(toArray(page.alias), skipAlias),
redirect: serializeRouteValue(page.redirect)
};
for (const key of [...defaultExtractionKeys, "meta"]) {
if (route[key] === void 0) {
delete route[key];
}
}
if (page.children?.length) {
route.children = normalizeRoutes(page.children, metaImports, options).routes;
}
if (!page.file) {
return route;
}
const file = normalize(page.file);
const pageImportName = genSafeVariableName(filename(file) + hash(file).replace(/-/g, "_"));
const metaImportName = pageImportName + "Meta";
metaImports.add(genImport(`${file}?macro=true`, [{ name: "default", as: metaImportName }]));
if (page._sync) {
metaImports.add(genImport(file, [{ name: "default", as: pageImportName }]));
}
const pageImport = page._sync && page.mode !== "client" ? pageImportName : genDynamicImport(file);
const metaRoute = {
name: `${metaImportName}?.name ?? ${route.name}`,
path: `${metaImportName}?.path ?? ${route.path}`,
props: `${metaImportName}?.props ?? ${route.props ?? false}`,
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(options?.serverComponentRuntime)}).then(r => r.createIslandPage)
return _createIslandPage(name)
};`);
} else if (page.mode === "client") {
metaImports.add(`
let _createClientPage
async function createClientPage(loader) {
_createClientPage ||= await import(${JSON.stringify(options?.clientComponentRuntime)}).then(r => r.createClientPage)
return _createClientPage(loader);
}`);
}
if (route.children) {
metaRoute.children = route.children;
}
if (route.meta) {
metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`;
}
if (options?.overrideMeta) {
for (const key of ["name", "path"]) {
if (markedDynamic.has(key)) {
continue;
}
metaRoute[key] = route[key] ?? `${metaImportName}?.${key}`;
}
for (const key of ["meta", "alias", "redirect", "props"]) {
if (markedDynamic.has(key)) {
continue;
}
if (route[key] == null) {
delete metaRoute[key];
continue;
}
metaRoute[key] = route[key];
}
} else {
if (route.alias != null) {
metaRoute.alias = `${route.alias}.concat(${metaImportName}?.alias || [])`;
}
if (route.redirect != null) {
metaRoute.redirect = route.redirect;
}
}
return metaRoute;
}))
};
}
const PATH_TO_NITRO_GLOB_RE = /\/[^:/]*:\w.*$/;
function pathToNitroGlob(path) {
if (!path) {
return null;
}
if (path.indexOf(":") !== path.lastIndexOf(":")) {
return null;
}
return path.replace(PATH_TO_NITRO_GLOB_RE, "/**");
}
function resolveRoutePaths(page, parent = "/") {
return [
joinURL(parent, page.path),
...page.children?.flatMap((child) => resolveRoutePaths(child, joinURL(parent, page.path))) || []
];
}
function isSerializable(code, node) {
if (node.type === "ObjectExpression") {
const valueString = code.slice(node.start, node.end);
try {
return {
value: JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {})),
serializable: true
};
} catch {
return {
serializable: false
};
}
}
if (node.type === "ArrayExpression") {
const values = [];
for (const element of node.elements) {
if (!element) {
continue;
}
const { serializable, value } = isSerializable(code, element);
if (!serializable) {
return {
serializable: false
};
}
values.push(value);
}
return {
value: values,
serializable: true
};
}
if (node.type === "Literal" && (typeof node.value === "string" || typeof node.value === "boolean" || typeof node.value === "number" || node.value === null)) {
return {
value: node.value,
serializable: true
};
}
return {
serializable: false
};
}
function toRou3Patterns(pages, prefix = "/") {
const routes = [];
for (const page of pages) {
const path = page.path.replace(/\([^)]*\)/g, "").replace(/:(\w+)\*.*/g, (_, name) => `**:${name}`).replace(/:([^/*]*)/g, (_, name) => `:${name.replace(/\W/g, (r) => r === "?" ? "" : "_")}`);
routes.push(joinURL(prefix, path));
if (page.children) {
routes.push(...toRou3Patterns(page.children, joinURL(prefix, path)));
}
}
return routes;
}
const ROUTE_RULE_RE = /\bdefineRouteRules\(/;
const ruleCache = {};
function extractRouteRules(code, path) {
if (!ROUTE_RULE_RE.test(code)) {
return null;
}
if (code in ruleCache) {
return ruleCache[code] || null;
}
const loader = getLoader(path);
if (!loader) {
return null;
}
let rule = null;
const contents = loader === "vue" ? extractScriptContent(code) : [{ code, loader }];
for (const script of contents) {
if (rule) {
break;
}
code = script?.code || code;
parseAndWalk(code, "file." + (script?.loader || "ts"), (node) => {
if (node.type !== "CallExpression" || node.callee.type !== "Identifier") {
return;
}
if (node.callee.name === "defineRouteRules") {
const rulesString = 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?.length) {
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_DEV_EMPTY = `
const __nuxt_page_meta = {}
export default __nuxt_page_meta
`;
const CODE_HMR = `
// Vite
if (import.meta.hot) {
import.meta.hot.accept(mod => {
Object.assign(__nuxt_page_meta, mod)
})
}
// webpack
if (import.meta.webpackHot) {
import.meta.webpackHot.accept((err) => {
if (err) { window.location = window.location.href }
})
}`;
const PageMetaPlugin = (options = {}) => createUnplugin(() => {
return {
name: "nuxt:pages-macros-transform",
enforce: "post",
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(options.dev ? CODE_DEV_EMPTY + CODE_HMR : CODE_EMPTY);
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, options.dev ? CODE_DEV_EMPTY + CODE_HMR : CODE_EMPTY);
}
return result();
}
const importMap = /* @__PURE__ */ new Map();
const addedImports = /* @__PURE__ */ new Set();
for (const i of imports) {
const parsed = parseStaticImport(i);
for (const name of [
parsed.defaultImport,
...Object.values(parsed.namedImports || {}),
parsed.namespacedImport
].filter(Boolean)) {
importMap.set(name, i);
}
}
function isStaticIdentifier(name) {
return !!(name && importMap.has(name));
}
function addImport(name) {
if (!isStaticIdentifier(name)) {
return;
}
const importValue = importMap.get(name).code.trim();
if (!addedImports.has(importValue)) {
addedImports.add(importValue);
}
}
const declarationNodes = [];
const addedDeclarations = /* @__PURE__ */ new Set();
function addDeclaration(node) {
const codeSectionKey = `${node.start}-${node.end}`;
if (addedDeclarations.has(codeSectionKey)) {
return;
}
addedDeclarations.add(codeSectionKey);
declarationNodes.push(node);
}
function addImportOrDeclaration(name, node) {
if (isStaticIdentifier(name)) {
addImport(name);
} else {
const declaration = scopeTracker.getDeclaration(name);
if (declaration && declaration !== node) {
processDeclaration(declaration);
}
}
}
const scopeTracker = new ScopeTracker({
preserveExitedScopes: true
});
function processDeclaration(scopeTrackerNode) {
if (scopeTrackerNode?.type === "Variable") {
addDeclaration(scopeTrackerNode);
for (const decl of scopeTrackerNode.variableNode.declarations) {
if (!decl.init) {
continue;
}
walk(decl.init, {
enter: (node, parent) => {
if (node.type === "AwaitExpression") {
logger.error(`Await expressions are not supported in definePageMeta. File: '${id}'`);
throw new Error("await in definePageMeta");
}
if (isBindingIdentifier(node, parent) || node.type !== "Identifier") {
return;
}
addImportOrDeclaration(node.name, scopeTrackerNode);
}
});
}
} else if (scopeTrackerNode?.type === "Function") {
if (scopeTrackerNode.node.type === "ArrowFunctionExpression") {
return;
}
const name = scopeTrackerNode.node.id?.name;
if (!name) {
return;
}
addDeclaration(scopeTrackerNode);
const undeclaredIdentifiers = getUndeclaredIdentifiersInFunction(scopeTrackerNode.node);
for (const name2 of undeclaredIdentifiers) {
addImportOrDeclaration(name2);
}
}
}
const { program: ast } = parseAndWalk(code, id, {
scopeTracker,
parseOptions: {
lang: query.lang ?? "ts"
}
});
scopeTracker.freeze();
let instances = 0;
walk(ast, {
scopeTracker,
enter: (node) => {
if (node.type !== "CallExpression" || node.callee.type !== "Identifier") {
return;
}
if (!("name" in node.callee) || node.callee.name !== "definePageMeta") {
return;
}
instances++;
const meta = node.arguments[0];
if (!meta) {
return;
}
const metaCode = code.slice(meta.start, meta.end);
const m = new MagicString(metaCode);
if (meta.type === "ObjectExpression") {
for (let i = 0; i < meta.properties.length; i++) {
const prop = meta.properties[i];
if (prop.type === "Property" && prop.key.type === "Identifier" && options.extractedKeys?.includes(prop.key.name)) {
const { serializable } = isSerializable(metaCode, prop.value);
if (!serializable) {
continue;
}
const nextProperty = meta.properties[i + 1];
if (nextProperty) {
m.overwrite(prop.start - meta.start, nextProperty.start - meta.start, "");
} else if (code[prop.end] === ",") {
m.overwrite(prop.start - meta.start, prop.end - meta.start + 1, "");
} else {
m.overwrite(prop.start - meta.start, prop.end - meta.start, "");
}
}
}
}
const definePageMetaScope = scopeTracker.getCurrentScope();
walk(meta, {
scopeTracker,
enter(node2, parent) {
if (isBindingIdentifier(node2, parent) || node2.type !== "Identifier") {
return;
}
const declaration = scopeTracker.getDeclaration(node2.name);
if (declaration) {
if (declaration.isUnderScope(definePageMetaScope) && (scopeTracker.isCurrentScopeUnder(declaration.scope) || declaration.start < node2.start)) {
return;
}
}
if (isStaticIdentifier(node2.name)) {
addImport(node2.name);
} else if (declaration) {
processDeclaration(declaration);
}
}
});
const importStatements = Array.from(addedImports).join("\n");
const declarations = declarationNodes.sort((a, b) => a.start - b.start).map((node2) => code.slice(node2.start, node2.end)).join("\n");
const extracted = [
importStatements,
declarations,
`const __nuxt_page_meta = ${m.toString() || "null"}
export default __nuxt_page_meta` + (options.dev ? CODE_HMR : "")
].join("\n");
s.overwrite(0, code.length, extracted.trim());
}
});
if (instances > 1) {
throw new Error("Multiple `definePageMeta` calls are not supported. File: " + id.replace(/\?.+$/, ""));
}
if (!s.hasChanged() && !code.includes("__nuxt_page_meta")) {
s.overwrite(0, code.length, options.dev ? CODE_DEV_EMPTY + CODE_HMR : CODE_EMPTY);
}
return result();
},
vite: {
handleHotUpdate: {
order: "post",
handler: ({ file, modules, server }) => {
if (options.routesPath && options.isPage?.(file)) {
const macroModule = server.moduleGraph.getModuleById(file + "?macro=true");
const routesModule = server.moduleGraph.getModuleById("virtual:nuxt:" + encodeURIComponent(options.routesPath));
return [
...modules,
...macroModule ? [macroModule] : [],
...routesModule ? [routesModule] : []
];
}
}
}
}
};
});
const QUERY_START_RE = /^\?/;
const MACRO_RE = /¯o=true/;
function rewriteQuery(id) {
return id.replace(/\?.+$/, (r) => "?macro=true&" + r.replace(QUERY_START_RE, "").replace(MACRO_RE, ""));
}
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;
}
const QUOTED_SPECIFIER_RE = /(["']).*\1/;
function getQuotedSpecifier(id) {
return id.match(QUOTED_SPECIFIER_RE)?.[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: {
filter: {
code: { include: INJECTION_SINGLE_RE }
},
handler(code) {
if (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);
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 runtimeDir = resolve(distDir, "pages/runtime");
async function resolveRouterOptions(nuxt, builtInRouterOptions) {
const context = {
files: []
};
for (const layer of nuxt.options._layers) {
const path = await findPath(resolve(layer.config.srcDir, layer.config.dir?.app || "app", "router.options"));
if (path) {
context.files.unshift({ path });
}
}
context.files.unshift({ path: builtInRouterOptions, optional: true });
await nuxt.callHook("pages:routerOptions", context);
return context.files;
}
const pagesModule = defineNuxtModule({
meta: {
name: "nuxt:pages",
configKey: "pages"
},
defaults: (nuxt) => ({
enabled: typeof nuxt.options.pages === "boolean" ? nuxt.options.pages : void 0,
pattern: `**/*{${nuxt.options.extensions.join(",")}}`
}),
async setup(_options, nuxt) {
const options = typeof _options === "boolean" ? { enabled: _options ?? nuxt.options.pages, pattern: `**/*{${nuxt.options.extensions.join(",")}}` } : { ..._options };
options.pattern = Array.isArray(options.pattern) ? [...new Set(options.pattern)] : options.pattern;
const useExperimentalTypedPages = nuxt.options.experimental.typedPages;
const builtInRouterOptions = await findPath(resolve(runtimeDir, "router.options")) || resolve(runtimeDir, "router.options");
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 }) => {
tsConfig.compilerOptions ||= {};
tsConfig.compilerOptions.paths ||= {};
tsConfig.compilerOptions.paths["#vue-router"] = [routerPath];
delete tsConfig.compilerOptions.paths["#vue-router/*"];
});
const isNonEmptyDir = (dir) => existsSync(dir) && readdirSync(dir).length;
const userPreference = options.enabled;
const isPagesEnabled = async () => {
if (typeof userPreference === "boolean") {
return userPreference;
}
const routerOptionsFiles = await resolveRouterOptions(nuxt, builtInRouterOptions);
if (routerOptionsFiles.filter((p) => !p.optional).length > 0) {
return true;
}
if (pagesDirs.some((dir) => isNonEmptyDir(dir))) {
return true;
}
const pages = await resolvePagesRoutes(options.pattern, nuxt);
if (pages.length) {
if (nuxt.apps.default) {
nuxt.apps.default.pages = pages;
}
return true;
}
return false;
};
options.enabled = await isPagesEnabled();
nuxt.options.pages = options;
Object.defineProperty(nuxt.options.pages, "toString", {
enumerable: false,
get: () => () => options.enabled
});
if (nuxt.options.dev && options.enabled) {
addPlugin(resolve(runtimeDir, "plugins/check-if-page-unused"));
}
nuxt.hook("app:templates", (app) => {
if (!nuxt.options.ssr && app.pages?.some((p) => p.mode === "server")) {
logger.warn("Using server pages with `ssr: false` is not supported with auto-detected component islands. Set `experimental.componentIslands` to `true`.");
}
});
const restartPaths = nuxt.options._layers.flatMap((layer) => {
const pagesDir = (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : layer.config.dir)?.pages || "pages";
return [
resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.app || "app", "router.options.ts"),
resolve(layer.config.srcDir || layer.cwd, pagesDir)
];
});
nuxt.hooks.hook("builder:watch", async (event, relativePath) => {
const path = resolve(nuxt.options.srcDir, relativePath);
if (restartPaths.some((p) => p === path || path.startsWith(p + "/"))) {
const newSetting = await isPagesEnabled();
if (options.enabled !== newSetting) {
logger.info("Pages", newSetting ? "enabled" : "disabled");
return nuxt.callHook("restart");
}
}
});
if (!options.enabled) {
addPlugin(resolve(distDir, "app/plugins/router"));
addTemplate({
filename: "pages.mjs",
getContents: () => [
"export { useRoute } from '#app/composables/router'",
"export const START_LOCATION = Symbol('router:start-location')"
].join("\n")
});
addTemplate({
filename: "router.options.mjs",
getContents: () => {
return [
"export const hashMode = false",
"export default {}"
].join("\n");
}
});
addTypeTemplate({
filename: "types/middleware.d.ts",
getContents: () => [
"declare module 'nitropack/types' {",
" interface NitroRouteConfig {",
" appMiddleware?: string | string[] | Record<string, boolean>",
" }",
"}",
"declare module 'nitropack' {",
" interface NitroRouteConfig {",
" appMiddleware?: string | string[] | Record<string, boolean>",
" }",
"}",
"export {}"
].join("\n")
}, { nuxt: true, nitro: true, node: true });
addComponent({
name: "NuxtPage",
priority: 10,
// built-in that we do not expect the user to override
filePath: resolve(distDir, "pages/runtime/page-placeholder")
});
nuxt.hook("nitro:init", (nitro) => {
if (nuxt.options.dev || !nuxt.options.ssr || !nitro.options.static || !nitro.options.prerender.crawlLinks) {
return;
}
nitro.options.prerender.routes.push("/");
});
return;
}
if (useExperimentalTypedPages) {
const declarationFile = "./types/typed-router.d.ts";
const typedRouterOptions = {
routesFolder: [],
dts: resolve(nuxt.options.buildDir, declarationFile),
logs: nuxt.options.debug && nuxt.options.debug.router,
async beforeWriteFiles(rootPage) {
rootPage.children.forEach((child) => child.delete());
const pages = nuxt.apps.default?.pages || await resolvePagesRoutes(options.pattern, nuxt);
if (nuxt.apps.default) {
nuxt.apps.default.pages = pages;
}
const addedPagePaths = /* @__PURE__ */ new Set();
function addPage(parent, page, basePath = "") {
const absolutePagePath = joinURL(basePath, page.path);
const route = addedPagePaths.has(absolutePagePath) ? parent : page.path[0] === "/" ? rootPage.insert(page.path, page.file) : parent.insert(page.path, page.file);
addedPagePaths.add(absolutePagePath);
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, absolutePagePath));
}
}
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(typedRouterOptions));
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