nuxt
Version:
[](https://nuxt.com)
1,257 lines (1,240 loc) • 146 kB
JavaScript
import { dirname, resolve, basename, extname, relative, normalize, isAbsolute, join } from 'pathe';
import { createHooks, createDebugger } from 'hookable';
import { useNuxt, resolveFiles, defineNuxtModule, addTemplate, addPlugin, addComponent, updateTemplates, addVitePlugin, addWebpackPlugin, findPath, addImportsSources, tryResolveModule, isIgnored, resolveAlias, addPluginTemplate, logger, normalizeModuleTranspilePath, resolvePath, createResolver, nuxtCtx, addBuildPlugin, installModule, loadNuxtConfig, normalizeTemplate, compileTemplate, normalizePlugin, templateUtils } from '@nuxt/kit';
import escapeRE from 'escape-string-regexp';
import fse from 'fs-extra';
import { parseURL, parseQuery, encodePath, joinURL, withTrailingSlash, withoutLeadingSlash } from 'ufo';
import { existsSync, readdirSync, statSync, readFileSync, promises } from 'node:fs';
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
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 { filename } from 'pathe/utils';
import { hash } from 'ohash';
import { kebabCase, splitByCase, pascalCase, camelCase } from 'scule';
import { createUnplugin } from 'unplugin';
import { findStaticImports, findExports, parseStaticImport, interopDefault, resolvePath as resolvePath$1 } from 'mlly';
import { walk } from 'estree-walker';
import MagicString from 'magic-string';
import { globby } from 'globby';
import { hyphenate } from '@vue/shared';
import { parse, walk as walk$1, ELEMENT_NODE } from 'ultrahtml';
import { createUnimport, defineUnimportPreset, scanDirExports } from 'unimport';
import { parseQuery as parseQuery$1 } from 'vue-router';
import { createRequire } from 'node:module';
import { createTransformer } from 'unctx/transform';
import { stripLiteral } from 'strip-literal';
import { createNitro, scanHandlers, writeTypes, build as build$1, prepare, copyPublicAssets, prerender, createDevServer } from 'nitropack';
import { defu } from 'defu';
import { dynamicEventHandler } from 'h3';
import { createHeadCore } from '@unhead/vue';
import { renderSSRHead } from '@unhead/ssr';
import { template } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs';
import chokidar from 'chokidar';
import { debounce } from 'perfect-debounce';
import { resolveSchema, generateTypes } from 'untyped';
import untypedPlugin from 'untyped/babel-plugin';
import jiti from 'jiti';
import { parse as parse$1 } from '@typescript-eslint/typescript-estree';
let _distDir = dirname(fileURLToPath(import.meta.url));
if (_distDir.match(/(chunks|shared)$/)) {
_distDir = dirname(_distDir);
}
const distDir = _distDir;
const pkgDir = resolve(distDir, "..");
resolve(distDir, "runtime");
function getNameFromPath(path) {
return kebabCase(basename(path).replace(extname(path), "")).replace(/["']/g, "");
}
function hasSuffix(path, suffix) {
return basename(path).replace(extname(path), "").endsWith(suffix);
}
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 && (!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 = /\.((c|m)?j|t)sx?$/;
function isJS(id) {
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href));
return JS_RE.test(pathname);
}
function uniqueBy(arr, key) {
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;
}
async function resolvePagesRoutes() {
const nuxt = useNuxt();
const pagesDirs = nuxt.options._layers.map(
(layer) => resolve(layer.config.srcDir, layer.config.dir?.pages || "pages")
);
const allRoutes = (await Promise.all(
pagesDirs.map(async (dir) => {
const files = await resolveFiles(dir, `**/*{${nuxt.options.extensions.join(",")}}`);
files.sort();
return generateRoutesFromFiles(files, dir);
})
)).flat();
return uniqueBy(allRoutes, "path");
}
function generateRoutesFromFiles(files, pagesDir) {
const routes = [];
for (const file of files) {
const segments = relative(pagesDir, file).replace(new RegExp(`${escapeRE(extname(file))}$`), "").split("/");
const route = {
name: "",
path: "",
file,
children: []
};
let parent = routes;
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 child = parent.find((parentRoute) => parentRoute.name === route.name && !parentRoute.path.endsWith("(.*)*"));
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);
}
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));
}, "/");
}
const PARAM_CHAR_RE = /[\w\d_.]/;
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 */ || buffer[buffer.length - 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";
console.warn(`[nuxt] 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.startsWith("/")) {
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 normalizeRoutes(routes, metaImports = /* @__PURE__ */ new Set()) {
return {
imports: metaImports,
routes: genArrayFromRaw(routes.map((page) => {
const route = Object.fromEntries(
Object.entries(page).filter(([key, value]) => key !== "file" && (Array.isArray(value) ? value.length : value)).map(([key, value]) => [key, JSON.stringify(value)])
);
if (page.children?.length) {
route.children = normalizeRoutes(page.children, metaImports).routes;
}
if (!page.file) {
for (const key of ["name", "path", "meta", "alias", "redirect"]) {
if (page[key]) {
route[key] = JSON.stringify(page[key]);
}
}
return route;
}
const file = normalize(page.file);
const metaImportName = genSafeVariableName(filename(file) + hash(file)) + "Meta";
metaImports.add(genImport(`${file}?macro=true`, [{ name: "default", as: metaImportName }]));
let aliasCode = `${metaImportName}?.alias || []`;
const alias = Array.isArray(page.alias) ? page.alias : [page.alias].filter(Boolean);
if (alias.length) {
aliasCode = `${JSON.stringify(alias)}.concat(${aliasCode})`;
}
route.name = `${metaImportName}?.name ?? ${page.name ? JSON.stringify(page.name) : "undefined"}`;
route.path = `${metaImportName}?.path ?? ${JSON.stringify(page.path)}`;
route.meta = page.meta && Object.values(page.meta).filter((value) => value !== void 0).length ? `{...(${metaImportName} || {}), ...${JSON.stringify(page.meta)}}` : `${metaImportName} || {}`;
route.alias = aliasCode;
route.redirect = page.redirect ? JSON.stringify(page.redirect) : `${metaImportName}?.redirect || undefined`;
route.component = genDynamicImport(file, { interopDefault: true });
return route;
}))
};
}
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) {
const query = parseMacroQuery(id);
id = normalize(id);
return !!query.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 specifier = rewriteQuery(scriptImport.specifier);
s.overwrite(0, code.length, `export { default } from ${JSON.stringify(specifier)}`);
return result();
}
const currentExports = findExports(code);
for (const match of currentExports) {
if (match.type !== "default" || !match.specifier) {
continue;
}
const specifier = rewriteQuery(match.specifier);
s.overwrite(0, code.length, `export { default } from ${JSON.stringify(specifier)}`);
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));
console.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;
}
const OPTIONAL_PARAM_RE = /^\/?:.*(\?|\(\.\*\)\*)$/;
const pagesModule = defineNuxtModule({
meta: {
name: "pages"
},
async setup(_options, nuxt) {
const useExperimentalTypedPages = nuxt.options.experimental.typedPages;
const pagesDirs = nuxt.options._layers.map(
(layer) => resolve(layer.config.srcDir, layer.config.dir?.pages || "pages")
);
const isNonEmptyDir = (dir) => existsSync(dir) && readdirSync(dir).length;
const userPreference = nuxt.options.pages;
const isPagesEnabled = async () => {
if (typeof userPreference === "boolean") {
return userPreference;
}
if (nuxt.options._layers.some((layer) => existsSync(resolve(layer.config.srcDir, "app/router.options.ts")))) {
return true;
}
if (pagesDirs.some((dir) => isNonEmptyDir(dir))) {
return true;
}
const pages = await resolvePagesRoutes();
await nuxt.callHook("pages:extend", pages);
if (pages.length) {
return true;
}
return false;
};
nuxt.options.pages = await isPagesEnabled();
const restartPaths = nuxt.options._layers.flatMap((layer) => [
join(layer.config.srcDir, "app/router.options.ts"),
join(layer.config.srcDir, layer.config.dir?.pages || "pages")
]);
nuxt.hooks.hook("builder:watch", async (event, path) => {
const fullPath = join(nuxt.options.srcDir, path);
if (restartPaths.some((path2) => path2 === fullPath || fullPath.startsWith(path2 + "/"))) {
const newSetting = await isPagesEnabled();
if (nuxt.options.pages !== newSetting) {
console.info("Pages", newSetting ? "enabled" : "disabled");
return nuxt.callHook("restart");
}
}
});
addTemplate({
filename: "vue-router.d.ts",
getContents: () => `export * from '${useExperimentalTypedPages ? "vue-router/auto" : "vue-router"}'`
});
nuxt.options.alias["#vue-router"] = join(nuxt.options.buildDir, "vue-router");
if (!nuxt.options.pages) {
addPlugin(resolve(distDir, "app/plugins/router"));
addTemplate({
filename: "pages.mjs",
getContents: () => "export { useRoute } from '#app'"
});
addComponent({
name: "NuxtPage",
priority: 10,
// built-in that we do not expect the user to override
filePath: resolve(distDir, "pages/runtime/page-placeholder")
});
return;
}
addTemplate({
filename: "vue-router.mjs",
// TODO: use `vue-router/auto` when we have support for page metadata
getContents: () => "export * from 'vue-router';"
});
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 = await resolvePagesRoutes();
await nuxt.callHook("pages:extend", 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 });
});
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) {
const dts = await readFile(dtsFile, "utf-8");
addTemplate({
filename: "types/typed-router.d.ts",
getContents: () => dts
});
}
nuxt.hook("builder:generateApp", async (options2) => {
if (!options2?.filter || options2.filter({ filename: "routes.mjs" })) {
await context.scanPages();
}
});
}
const runtimeDir = resolve(distDir, "pages/runtime");
nuxt.hook("prepare:types", ({ references }) => {
references.push({ types: useExperimentalTypedPages ? "vue-router/auto" : "vue-router" });
});
nuxt.hook("imports:sources", (sources) => {
const routerImports = sources.find((s) => s.from === "#app" && s.imports.includes("onBeforeRouteLeave"));
if (routerImports) {
routerImports.from = "#vue-router";
}
});
nuxt.hook("builder:watch", async (event, path) => {
const dirs = [
nuxt.options.dir.pages,
nuxt.options.dir.layouts,
nuxt.options.dir.middleware
].filter(Boolean);
const pathPattern = new RegExp(`(^|\\/)(${dirs.map(escapeRE).join("|")})/`);
if (event !== "change" && pathPattern.test(path)) {
await updateTemplates({
filter: (template) => template.filename === "routes.mjs"
});
}
});
nuxt.hook("app:resolve", (app) => {
if (app.mainComponent.includes("@nuxt/ui-templates")) {
app.mainComponent = resolve(runtimeDir, "app.vue");
}
app.middleware.unshift({
name: "validate",
path: resolve(runtimeDir, "validate"),
global: true
});
});
if (!nuxt.options.dev && nuxt.options._generate) {
const prerenderRoutes = /* @__PURE__ */ new Set();
nuxt.hook("modules:done", () => {
nuxt.hook("pages:extend", (pages) => {
prerenderRoutes.clear();
const processPages = (pages2, currentPath = "/") => {
for (const page of pages2) {
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);
}
}
};
processPages(pages);
});
});
nuxt.hook("nitro:build:before", (nitro) => {
for (const route of nitro.options.prerender.routes || []) {
if (route === "/") {
continue;
}
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" }
);
});
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"));
addPlugin(resolve(runtimeDir, "plugins/router"));
const getSources = (pages) => pages.filter((p) => Boolean(p.file)).flatMap(
(p) => [relative(nuxt.options.srcDir, p.file), ...getSources(p.children || [])]
);
nuxt.hook("build:manifest", async (manifest) => {
if (nuxt.options.dev) {
return;
}
const pages = await resolvePagesRoutes();
await nuxt.callHook("pages:extend", pages);
const sourceFiles = getSources(pages);
for (const key in manifest) {
if (manifest[key].isEntry) {
manifest[key].dynamicImports = manifest[key].dynamicImports?.filter((i) => !sourceFiles.includes(i));
}
}
});
addTemplate({
filename: "routes.mjs",
async getContents() {
const pages = await resolvePagesRoutes();
await nuxt.callHook("pages:extend", pages);
const { routes, imports } = normalizeRoutes(pages);
return [...imports, `export default ${routes}`].join("\n");
}
});
addTemplate({
filename: "pages.mjs",
getContents: () => "export { useRoute } from 'vue-router'"
});
nuxt.options.vite.optimizeDeps = nuxt.options.vite.optimizeDeps || {};
nuxt.options.vite.optimizeDeps.include = nuxt.options.vite.optimizeDeps.include || [];
nuxt.options.vite.optimizeDeps.include.push("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 Promise.all(nuxt.options._layers.map(
async (layer) => await findPath(resolve(layer.config.srcDir, "app/router.options"))
))).filter(Boolean);
routerOptionsFiles.push(resolve(runtimeDir, "router.options"));
const configRouterOptions = genObjectFromRawEntries(Object.entries(nuxt.options.router.options).map(([key, value]) => [key, genString(value)]));
return [
...routerOptionsFiles.map((file, index) => genImport(file, `routerOptions${index}`)),
`const configRouterOptions = ${configRouterOptions}`,
"export default {",
"...configRouterOptions,",
// We need to reverse spreading order to respect layers priority
...routerOptionsFiles.map((_, index) => `...routerOptions${index},`).reverse(),
"}"
].join("\n");
}
});
addTemplate({
filename: "types/middleware.d.ts",
getContents: ({ app }) => {
const composablesFile = 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>",
" }",
"}"
].join("\n");
}
});
addTemplate({
filename: "types/layouts.d.ts",
getContents: ({ app }) => {
const composablesFile = resolve(runtimeDir, "composables");
return [
"import { ComputedRef, Ref } from 'vue'",
`export type LayoutKey = ${Object.keys(app.layouts).map((name) => genString(name)).join(" | ") || "string"}`,
`declare module ${genString(composablesFile)} {`,
" interface PageMeta {",
" layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>",
" }",
"}"
].join("\n");
}
});
addComponent({
name: "NuxtPage",
priority: 10,
// built-in that we do not expect the user to override
filePath: resolve(distDir, "pages/runtime/page")
});
nuxt.hook("prepare:types", ({ references }) => {
references.push({ path: resolve(nuxt.options.buildDir, "types/middleware.d.ts") });
references.push({ path: resolve(nuxt.options.buildDir, "types/layouts.d.ts") });
references.push({ path: resolve(nuxt.options.buildDir, "vue-router.d.ts") });
});
}
});
const components = ["NoScript", "Link", "Base", "Title", "Meta", "Style", "Head", "Html", "Body"];
const metaModule = defineNuxtModule({
meta: {
name: "meta"
},
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 override
priority: 10,
// kebab case version of these tags is not valid
kebabName: componentName
});
}
nuxt.options.optimization.treeShake.composables.client["@unhead/vue"] = [
"useServerHead",
"useServerSeoMeta",
"useServerHeadSafe"
];
addImportsSources({
from: "@unhead/vue",
// hard-coded for now we so don't support auto-imports on the deprecated composables
imports: [
"injectHead",
"useHead",
"useSeoMeta",
"useHeadSafe",
"useServerHead",
"useServerSeoMeta",
"useServerHeadSafe"
]
});
if (nuxt.options.experimental.polyfillVueUseHead) {
nuxt.options.alias["@vueuse/head"] = await tryResolveModule("@unhead/vue", nuxt.options.modulesDir) || "@unhead/vue";
addPlugin({ src: resolve(runtimeDir, "plugins/vueuse-head-polyfill") });
}
addPlugin({ src: resolve(runtimeDir, "plugins/unhead") });
}
});
const CLIENT_FALLBACK_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/;
const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g;
const clientFallbackAutoIdPlugin = createUnplugin((options) => {
const exclude = options.transform?.exclude || [];
const include = options.transform?.include || [];
return {
name: "nuxt:client-fallback-auto-id",
enforce: "pre",
transformInclude(id) {
if (exclude.some((pattern) => pattern.test(id))) {
return false;
}
if (include.some((pattern) => pattern.test(id))) {
return true;
}
return isVue(id);
},
transform(code, id) {
if (!CLIENT_FALLBACK_RE.test(code)) {
return;
}
const s = new MagicString(code);
const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id;
let count = 0;
s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => {
count++;
if (/ :?uid=/g.test(attrs)) {
return full;
}
return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ""}>`;
});
if (s.hasChanged()) {
return {
code: s.toString(),
map: options.sourcemap ? s.generateMap({ hires: true }) : void 0
};
}
}
};
});
const createImportMagicComments = (options) => {
const { chunkName, prefetch, preload } = options;
return [
`webpackChunkName: "${chunkName}"`,
prefetch === true || typeof prefetch === "number" ? `webpackPrefetch: ${prefetch}` : false,
preload === true || typeof preload === "number" ? `webpackPreload: ${preload}` : false
].filter(Boolean).join(", ");
};
const emptyComponentsPlugin = `
import { defineNuxtPlugin } from '#app/nuxt'
export default defineNuxtPlugin({
name: 'nuxt:global-components',
})
`;
const componentsPluginTemplate = {
filename: "components.plugin.mjs",
getContents({ app }) {
const globalComponents = /* @__PURE__ */ new Set();
for (const component of app.components) {
if (component.global) {
globalComponents.add(component.pascalName);
}
}
if (!globalComponents.size) {
return emptyComponentsPlugin;
}
const components = [...globalComponents];
return `import { defineNuxtPlugin } from '#app/nuxt'
import { ${components.map((c) => "Lazy" + c).join(", ")} } from '#components'
const lazyGlobalComponents = [
${components.map((c) => `["${c}", Lazy${c}]`).join(",\n")}
]
export default defineNuxtPlugin({
name: 'nuxt:global-components',
setup (nuxtApp) {
for (const [name, component] of lazyGlobalComponents) {
nuxtApp.vueApp.component(name, component)
nuxtApp.vueApp.component('Lazy' + name, component)
}
}
})
`;
}
};
const componentNamesTemplate = {
filename: "component-names.mjs",
getContents({ app }) {
return `export const componentNames = ${JSON.stringify(app.components.filter((c) => !c.island).map((c) => c.pascalName))}`;
}
};
const componentsIslandsTemplate = {
// components.islands.mjs'
getContents({ app }) {
const components = app.components;
const islands = components.filter(
(component) => component.island || // .server components without a corresponding .client component will need to be rendered as an island
component.mode === "server" && !components.some((c) => c.pascalName === component.pascalName && c.mode === "client")
);
return ["import { defineAsyncComponent } from 'vue'", ...islands.map(
(c) => {
const exp = c.export === "default" ? "c.default || c" : `c['${c.export}']`;
const comment = createImportMagicComments(c);
return `export const ${c.pascalName} = /* #__PURE__ */ defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`;
}
)].join("\n");
}
};
const componentsTypeTemplate = {
filename: "components.d.ts",
getContents: ({ app, nuxt }) => {
const buildDir = nuxt.options.buildDir;
const componentTypes = app.components.filter((c) => !c.island).map((c) => [
c.pascalName,
`typeof ${genDynamicImport(isAbsolute(c.filePath) ? relative(buildDir, c.filePath).replace(/(?<=\w)\.(?!vue)\w+$/g, "") : c.filePath.replace(/(?<=\w)\.(?!vue)\w+$/g, ""), { wrapper: false })}['${c.export}']`
]);
return `// Generated by components discovery
declare module 'vue' {
export interface GlobalComponents {
${componentTypes.map(([pascalName, type]) => ` '${pascalName}': ${type}`).join("\n")}
${componentTypes.map(([pascalName, type]) => ` 'Lazy${pascalName}': ${type}`).join("\n")}
}
}
${componentTypes.map(([pascalName, type]) => `export const ${pascalName}: ${type}`).join("\n")}
${componentTypes.map(([pascalName, type]) => `export const Lazy${pascalName}: ${type}`).join("\n")}
export const componentNames: string[]
`;
}
};
async function scanComponents(dirs, srcDir) {
const components = [];
const filePaths = /* @__PURE__ */ new Set();
const scannedPaths = [];
for (const dir of dirs) {
const resolvedNames = /* @__PURE__ */ new Map();
const files = (await globby(dir.pattern, { cwd: dir.path, ignore: dir.ignore })).sort();
if (files.length) {
const siblings = await readdir(dirname(dir.path)).catch(() => []);
const directory = basename(dir.path);
if (!siblings.includes(directory)) {
const caseCorrected = siblings.find((sibling) => sibling.toLowerCase() === directory.toLowerCase());
if (caseCorrected) {
const nuxt = useNuxt();
const original = relative(nuxt.options.srcDir, dir.path);
const corrected = relative(nuxt.options.srcDir, join(dirname(dir.path), caseCorrected));
console.warn(`[nuxt] Components not scanned from \`~/${corrected}\`. Did you mean to name the directory \`~/${original}\` instead?`);
continue;
}
}
}
for (const _file of files) {
const filePath = join(dir.path, _file);
if (scannedPaths.find((d) => filePath.startsWith(withTrailingSlash(d))) || isIgnored(filePath)) {
continue;
}
if (filePaths.has(filePath)) {
continue;
}
filePaths.add(filePath);
const prefixParts = [].concat(
dir.prefix ? splitByCase(dir.prefix) : [],
dir.pathPrefix !== false ? splitByCase(relative(dir.path, dirname(filePath))) : []
);
let fileName = basename(filePath, extname(filePath));
const island = /\.(island)(\.global)?$/.test(fileName) || dir.island;
const global = /\.(global)(\.island)?$/.test(fileName) || dir.global;
const mode = island ? "server" : fileName.match(/(?<=\.)(client|server)(\.global|\.island)*$/)?.[1] || "all";
fileName = fileName.replace(/(\.(client|server))?(\.global|\.island)*$/, "");
if (fileName.toLowerCase() === "index") {
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : "";
}
const suffix = mode !== "all" ? `-${mode}` : "";
const componentName = resolveComponentName(fileName, prefixParts);
if (resolvedNames.has(componentName + suffix) || resolvedNames.has(componentName)) {
console.warn(
`Two component files resolving to the same name \`${componentName}\`:
- ${filePath}
- ${resolvedNames.get(componentName)}`
);
continue;
}
resolvedNames.set(componentName + suffix, filePath);
const pascalName = pascalCase(componentName).replace(/["']/g, "");
const kebabName = hyphenate(componentName);
const shortPath = relative(srcDir, filePath);
const chunkName = "components/" + kebabName + suffix;
let component = {
// inheritable from directory configuration
mode,
global,
island,
prefetch: Boolean(dir.prefetch),
preload: Boolean(dir.preload),
// specific to the file
filePath,
pascalName,
kebabName,
chunkName,
shortPath,
export: "default",
// by default, give priority to scanned components
priority: 1
};
if (typeof dir.extendComponent === "function") {
component = await dir.extendComponent(component) || component;
}
if (!components.some((c) => c.pascalName === component.pascalName && ["all", component.mode].includes(c.mode))) {
components.push(component);
}
}
scannedPaths.push(dir.path);
}
return components;
}
function resolveComponentName(fileName, prefixParts) {
const fileNameParts = splitByCase(fileName);
const fileNamePartsContent = fileNameParts.join("/").toLowerCase();
const componentNameParts = [...prefixParts];
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 pascalCase(componentNameParts) + pascalCase(fileNameParts);
}
const loaderPlugin = createUnplugin((options) => {
const exclude = options.transform?.exclude || [];
const include = options.transform?.include || [];
const serverComponentRuntime = resolve(distDir, "components/runtime/server-component");
return {
name: "nuxt:components-loader",
enforce: "post",
transformInclude(id) {
if (exclude.some((pattern) => pattern.test(id))) {
return false;
}
if (include.some((pattern) => pattern.test(id))) {
return true;
}
return isVue(id, { type: ["template", "script"] });
},
transform(code) {
const components = options.getComponents();
let num = 0;
const imports = /* @__PURE__ */ new Set();
const map = /* @__PURE__ */ new Map();
const s = new MagicString(code);
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy)?([^'"]*?)["'][\s,]*[^)]*\)/g, (full, lazy, name) => {
const component = findComponent(components, name, options.mode);
if (component) {
let identifier = map.get(component) || `__nuxt_component_${num++}`;
map.set(component, identifier);
const isServerOnly = component.mode === "server" && !components.some((c) => c.pascalName === component.pascalName && c.mode === "client");
if (isServerOnly) {
imports.add(genImport(serverComponentRuntime, [{ name: "createServerComponent" }]));
imports.add(`const ${identifier} = createServerComponent(${JSON.stringify(name)})`);
if (!options.experimentalComponentIslands) {
console.warn(`Standalone server components (\`${name}\`) are not yet supported without enabling \`experimental.componentIslands\`.`);
}
return identifier;
}
const isClientOnly = component.mode === "client" && component.pascalName !== "NuxtClientFallback";
if (isClientOnly) {
imports.add(genImport("#app/components/client-only", [{ name: "createClientOnly" }]));
identifier += "_client";
}
if (lazy) {
imports.add(genImport("vue", [{ name: "defineAsyncComponent", as: "__defineAsyncComponent" }]));
identifier += "_lazy";
imports.add(`const ${identifier} = /*#__PURE__*/ __defineAsyncComponent(${genDynamicImport(component.filePath, { interopDefault: true })}${isClientOnly ? ".then(c => createClientOnly(c))" : ""})`);
} else {
imports.add(genImport(component.filePath, [{ name: component.export, as: identifier }]));
if (isClientOnly) {
imports.add(`const ${identifier}_wrapped = /*#__PURE__*/ createClientOnly(${identifier})`);
identifier += "_wrapped";
}
}
return identifier;
}
return full;
});
if (imports.size) {
s.prepend([...imports, ""].join("\n"));
}
if (s.hasChanged()) {
return {
code: s.toString(),
map: options.sourcemap ? s.generateMap({ hires: true }) : void 0
};
}
}
};
});
function findComponent(components, name, mode) {
const id = pascalCase(name).replace(/["']/g, "");
const component = components.find((component2) => id === component2.pascalName && ["all", mode, void 0].includes(component2.mode));
if (component) {
return component;
}
const otherModeComponent = components.find((component2) => id === component2.pascalName);
if (mode === "server" && otherModeComponent) {
return components.find((c) => c.pascalName === "ServerPlaceholder");
}
return otherModeComponent;
}
const SSR_RENDER_RE = /ssrRenderComponent/;
const PLACEHOLDER_EXACT_RE = /^(fallback|placeholder)$/;
const CLIENT_ONLY_NAME_RE = /^(?:_unref\()?(?:_component_)?(?:Lazy|lazy_)?(?:client_only|ClientOnly\)?)$/;
const PARSER_OPTIONS = { sourceType: "module", ecmaVersion: "latest" };
const TreeShakeTemplatePlugin = createUnplugin((options) => {
const regexpMap = /* @__PURE__ */ new WeakMap();
return {
name: "nuxt:tree-shake-template",
enforce: "post",
transformInclude(id) {
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href));
return pathname.endsWith(".vue");
},
transform(code) {
const components = options.getComponents();
if (!regexpMap.has(components)) {
const clientOnlyComponents = components.filter((c) => c.mode === "client" && !components.some((other) => other.mode !== "client" && other.pascalName === c.pascalName && other.filePath !== resolve(distDir, "app/components/server-placeholder"))).flatMap((c) => [c.pascalName, c.kebabName.replaceAll("-", "_")]).concat(["ClientOnly", "client_only"]);
regexpMap.set(components, [new RegExp(`(${clientOnlyComponents.join("|")})`), new RegExp(`^(${clientOnlyComponents.map((c) => `(?:(?:_unref\\()?(?:_component_)?(?:Lazy|lazy_)?${c}\\)?)`).join("|")})$`), clientOnlyComponents]);
}
const s = new MagicString(code);
const [COMPONENTS_RE, COMPONENTS_IDENTIFIERS_RE] = regexpMap.get(components);
if (!COMPONENTS_RE.test(code)) {
return;
}
const codeAst = this.parse(code, PARSER_OPTIONS);
const componentsToRemoveSet = /* @__PURE__ */ new Set();
walk(codeAst, {
enter: (_node) => {
const node = _node;
if (isSsrRender(node)) {
const [componentCall, _, children] = node.arguments;
if (componentCall.type === "Identifier" || componentCall.type === "MemberExpression" || componentCall.type === "CallExpression") {
const componentName = getComponentName(node);
const isClientComponent = COMPONENTS_IDENTIFIERS_RE.test(componentName);
const isClientOnlyComponent = CLIENT_ONLY_NAME_RE.test(componentName);
if (isClientComponent && children?.type === "ObjectExpression") {
const slotsToRemove = isClientOnlyComponent ? children.properties.filter((prop) => prop.type === "Property" && prop.key.type === "Identifier" && !PLACEHOLDER_EXACT_RE.test(prop.key.name)) : children.properties;
for (const slot of slotsToRemove) {
s.remove(slot.start, slot.end + 1);
const removedCode = `({${code.slice(slot.start, slot.end + 1)}})`;
const currentCodeAst = this.parse(s.toString(), PARSER_OPTIONS);
walk(this.parse(removedCode, PARSER_OPTIONS), {
enter: (_node2) => {
const node2 = _node2;
if (isSsrRender(node2)) {
const name = getComponentName(node2);
const nameToRemove = isComponentNotCalledInSetup(currentCodeAst, name);
if (nameToRemove) {
componentsToRemoveSet.add(nameToRemove);
}
}
}
});
}
}
}
}
}
});
const componentsToRemove = [...componentsToRemoveSet];
const removedNodes = /* @__PURE__ */ new WeakSet();
for (const componentName of componentsToRemove) {
removeImportDeclaration(codeAst, componentName, s);
removeVariableDeclarator(codeAst, componentName, s, removedNodes);
removeFromSetupReturn(codeAst, componentName, s);
}
if (s.hasChanged()) {
return {
code: s.toString(),
map: options.sourcemap ? s.generateMap({ hires: true }) : void 0
};
}
}
};
});
function removeFromSetupReturn(codeAst, name, magicString) {
let walkedInSetup = false;
walk(codeAst, {
enter(node) {
if (walkedInSetup) {
this.skip();
} else if (node.type === "Property" && node.key.type === "Identifier" && node.key.name === "setup" && (node.value.type === "FunctionExpression" || node.value.type === "ArrowFunctionExpression")) {
walkedInSetup = true;
if (node.value.body.type === "BlockStatement") {
const returnStatement = node.value.body.body.find((statement) => statement.type === "ReturnStatement");
if (returnStatement && returnStatement.argument?.type === "ObjectExpression") {
removePropertyFromObject(returnStatement.argument, name, magicString);
}
const variableList = node.value.body.body.filter((statement) => statement.type === "VariableDeclaration");
const returnedVariableDeclaration = variableList.find((declaration) => declaration.declarations[0]?.id.type === "Identifier" && declaration.declarations[0]?.id.name === "__returned__" && declaration.declarations[0]?.init?.type === "ObjectExpression");
if (returnedVariableDeclaration) {
const init = returnedVariableDeclaration.declarations[0].init;
removePropertyFromObject(init, name, magicString);
}
}
}
}
});
}
function removePropertyFromObject(node, name, magicString) {
for (const property of node.properties) {
if (property.type === "Property" && property.key.type === "Identifier" && property.key.name === name) {
magicString.remove(property.start, property.end + 1);
return true;
}
}
return false;
}
function isSsrRender(node) {
return node.type === "CallExpression" && node.callee.type === "Identifier" && SSR_RENDER_RE.test(node.callee.name);
}
function removeImportDeclaration(ast, importName, magicString) {
for (const node of ast.body) {
if (node.type === "ImportDeclaration") {
const specifier = node.specifiers.find((s) => s.local.name === importName);
if (specifier) {
if (node.specifiers.length > 1) {
const specifierIndex = node.specifiers.findIndex((s) => s.local.name === importName);
if (specifierIndex > -1) {
magicString.remove(node.specifiers[specifierIndex].start, node.specifiers[specifierIndex].end + 1);
node.specifiers.splice(specifierIndex, 1);
}
} else {
magicString.remove(node.start, node.end);
}
return true;
}
}
}
return false;
}
function isComponentNotCalledInSetup(codeAst, name) {
if (name) {
let found = false;
walk(codeAst, {
enter(node) {
if (node.type === "Property" && node.key.type === "Identifier" && node.value.type === "FunctionExpression" && node.key.name === "setup" || node.type === "FunctionDeclaration" && node.id?.name === "_sfc_ssrRender") {
walk(node, {
enter(node2) {
if (found || node2.type === "VariableDeclaration") {
this.skip();
} else if (node2.type === "Identifier" && node2.name === name) {
found = true;
} else if (node2.type === "MemberExpression") {
found = node2.property.type === "Literal" && node2.property.value === name || node2.property.type === "Identifier" && node2.property.name === name;
}
}
});
}
}
});
if (!found) {
return name;
}
}
}
function getComponentName(ssrRenderNode) {
const componentCall = ssrRenderNode.arguments[0];
if (componentCall.type === "Identifier") {
return componentCall.name;
} else if (componentCall.type === "MemberExpression