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
JavaScript
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 };