UNPKG

nuxt-component-meta

Version:

[![npm version][npm-version-src]][npm-version-href] [![npm downloads][npm-downloads-src]][npm-downloads-href]

264 lines (259 loc) 9.28 kB
import { readFileSync } from 'fs'; import { createResolver, resolveAlias, logger, defineNuxtModule, tryResolveModule, addImportsDir, addTemplate, addServerHandler } from '@nuxt/kit'; import { join } from 'pathe'; import { createUnplugin } from 'unplugin'; import { useComponentMetaParser } from './parser.mjs'; import 'perf_hooks'; import 'vue-component-meta'; import 'mlly'; import 'ohash'; import 'defu'; const metaPlugin = createUnplugin(({ parser, parserOptions }) => { const instance = parser || useComponentMetaParser(parserOptions); let _configResolved; return { name: "vite-plugin-nuxt-component-meta", enforce: "post", buildStart() { if (_configResolved?.build.ssr) { return; } instance.fetchComponents(); instance.updateOutput(); }, vite: { configResolved(config) { _configResolved = config; }, handleHotUpdate({ file }) { if (Object.entries(instance.components).some(([, comp]) => comp.fullPath === file)) { instance.fetchComponent(file); instance.updateOutput(); } } } }; }); async function loadExternalSources(sources = []) { const resolver = createResolver(import.meta.url); const components = {}; for (const src of sources) { if (typeof src === "string") { try { let modulePath = ""; const alias = resolveAlias(src); if (alias !== src) { modulePath = alias; } else { modulePath = await resolver.resolvePath(src); } const definition = await import(modulePath).then((m) => m.default || m); for (const [name, meta] of Object.entries(definition)) { components[name] = meta; } } catch (error) { logger.error(`Unable to load static components definitions from "${src}"`, error); } } else { for (const [name, meta] of Object.entries(src)) { if (meta) { components[name] = meta; } } } } return components; } const slotReplacer = (_, _before, slotName, _rest) => `<slot ${_before || ""}${slotName === "default" ? "" : `name="${slotName}"`}`; const module = defineNuxtModule({ meta: { name: "nuxt-component-meta", configKey: "componentMeta" }, defaults: (nuxt) => ({ outputDir: nuxt.options.buildDir, rootDir: nuxt.options.rootDir, componentDirs: [], components: [], metaSources: [], silent: true, exclude: [ "nuxt/dist/app/components/welcome", "nuxt/dist/app/components/client-only", "nuxt/dist/app/components/dev-only", "@nuxtjs/mdc/dist/runtime/components/MDC", "nuxt/dist/app/components/nuxt-layout", "nuxt/dist/app/components/nuxt-error-boundary", "nuxt/dist/app/components/server-placeholder", "nuxt/dist/app/components/nuxt-loading-indicator", "nuxt/dist/app/components/nuxt-route-announcer", "nuxt/dist/app/components/nuxt-stubs" ], include: [], metaFields: { type: true, props: true, slots: true, events: true, exposed: true }, transformers: [ // @nuxt/content support (component, code) => { if (code.includes("MDCSlot")) { code = code.replace(/<MDCSlot\s*([^>]*)?:use="\$slots\.([a-zA-Z0-9_]+)"/gm, slotReplacer); code = code.replace(/<MDCSlot\s*([^>]*)?name="([a-zA-Z0-9_]+)"/gm, slotReplacer); code = code.replace(/<\/MDCSlot>/gm, "</slot>"); } if (code.includes("ContentSlot")) { code = code.replace(/<ContentSlot\s*([^>]*)?:use="\$slots\.([a-zA-Z0-9_]+)"/gm, slotReplacer); code = code.replace(/<ContentSlot\s*([^>]*)?name="([a-zA-Z0-9_]+)"/gm, slotReplacer); code = code.replace(/<\/ContentSlot>/gm, "</slot>"); } const name = code.match(/(const|let|var) ([a-zA-Z][a-zA-Z-_0-9]*) = useSlots\(\)/)?.[2] || "$slots"; const _slots = code.match(new RegExp(`${name}\\.[a-zA-Z]+`, "gm")); if (_slots) { const slots = _slots.map((s) => s.replace(name + ".", "")).map((s) => `<slot name="${s}" />`); code = code.replace(/<template>/, `<template> ${slots.join("\n")} `); } const slotNames = code.match(/(const|let|var) {([^}]+)}\s*=\s*useSlots\(\)/)?.[2]; if (slotNames) { const slots = slotNames.trim().split(",").map((s) => s.trim().split(":")[0].trim()).map((s) => `<slot name="${s}" />`); code = code.replace(/<template>/, `<template> ${slots.join("\n")} `); } return { component, code }; } ], checkerOptions: { forceUseTs: true, schema: { ignore: [ "NuxtComponentMetaNames", // avoid loop "RouteLocationRaw", // vue router "RouteLocationPathRaw", // vue router "RouteLocationNamedRaw" // vue router ] } }, globalsOnly: false }), async setup(options, nuxt) { const resolver = createResolver(import.meta.url); const isComponentIncluded = (component) => { if (!options?.globalsOnly) { return true; } if (component.global) { return true; } return (options.include || []).find((excludeRule) => { switch (typeof excludeRule) { case "string": return component.pascalName === excludeRule || component.filePath.includes(excludeRule); case "object": return excludeRule instanceof RegExp ? excludeRule.test(component.filePath) : false; case "function": return excludeRule(component); default: return false; } }); }; let transformers = options?.transformers || []; transformers = await nuxt.callHook("component-meta:transformers", transformers) || transformers; let parser; const parserOptions = { ...options, components: [], metaSources: {}, transformers }; let componentDirs = [...options?.componentDirs || []]; let components = []; let metaSources = {}; const uiTemplatesPath = await tryResolveModule("@nuxt/ui-templates"); nuxt.hook("components:dirs", (dirs) => { componentDirs = [ ...componentDirs, ...dirs, { path: nuxt.options.appDir } ]; if (uiTemplatesPath) { componentDirs.push({ path: uiTemplatesPath.replace("/index.mjs", "/templates") }); } parserOptions.componentDirs = componentDirs; }); nuxt.hook("components:extend", (_components) => { _components.forEach((c) => { if (c.global) { parserOptions.componentDirs.push(c.filePath); } }); }); nuxt.hook("components:extend", async (_components) => { components = _components.filter(isComponentIncluded); metaSources = await loadExternalSources(options.metaSources); parserOptions.components = components; parserOptions.metaSources = metaSources; await nuxt.callHook("component-meta:extend", parserOptions); parser = useComponentMetaParser(parserOptions); await Promise.all([ parser.init(), parser.stubOutput() ]); }); addImportsDir(resolver.resolve("./runtime/composables")); addTemplate({ filename: "component-meta.d.ts", getContents: () => [ "import type { ComponentData } from 'nuxt-component-meta'", `export type NuxtComponentMetaNames = ${[...components, ...Object.values(metaSources)].map((c) => `'${c.pascalName}'`).join(" | ")}`, "export type NuxtComponentMeta = Record<NuxtComponentMetaNames, ComponentData>", "declare const components: NuxtComponentMeta", "export { components as default, components }" ].join("\n"), write: true }); nuxt.hook("vite:extend", (vite) => { vite.config.plugins = vite.config.plugins || []; vite.config.plugins.push(metaPlugin.vite({ parser, parserOptions })); }); nuxt.options.alias = nuxt.options.alias || {}; nuxt.options.alias["#nuxt-component-meta"] = join(nuxt.options.buildDir, "component-meta.mjs"); nuxt.options.alias["#nuxt-component-meta/types"] = join(nuxt.options.buildDir, "component-meta.d.ts"); nuxt.hook("prepare:types", ({ references }) => { references.push({ path: join(nuxt.options.buildDir, "component-meta.d.ts") }); }); nuxt.hook("nitro:config", (nitroConfig) => { nitroConfig.handlers = nitroConfig.handlers || []; nitroConfig.virtual = nitroConfig.virtual || {}; nitroConfig.virtual["#nuxt-component-meta/nitro"] = () => readFileSync(join(nuxt.options.buildDir, "/component-meta.mjs"), "utf-8"); }); addServerHandler({ method: "get", route: "/api/component-meta", handler: resolver.resolve("./runtime/server/api/component-meta.get") }); addServerHandler({ method: "get", route: "/api/component-meta.json", handler: resolver.resolve("./runtime/server/api/component-meta.json.get") }); addServerHandler({ method: "get", route: "/api/component-meta/:component?", handler: resolver.resolve("./runtime/server/api/component-meta-component.get") }); } }); export { module as default };