UNPKG

nuxt

Version:

[![Nuxt banner](./.github/assets/banner.png)](https://nuxt.com)

1,257 lines (1,240 loc) 146 kB
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(/&macro=true/, "")); } function parseMacroQuery(id) { const { search } = parseURL(decodeURIComponent(isAbsolute(id) ? pathToFileURL(id).href : id).replace(/\?macro=true$/, "")); const query = parseQuery(search); if (id.includes("?macro=true")) { return { macro: "true", ...query }; } return query; } 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