@nuxt/ui
Version:
A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.
479 lines (467 loc) • 16.9 kB
JavaScript
import { fileURLToPath } from 'node:url';
import { join, normalize, relative } from 'pathe';
import { createUnplugin } from 'unplugin';
import { defu } from 'defu';
import tailwind from '@tailwindcss/vite';
import { e as getTemplates, h as resolveExtraScanDirs, b as detectUsedComponents, f as publicComposables, g as getClientBundleIcons, p as parseIconName, d as defaultOptions, r as resolveColors, c as getDefaultConfig } from './shared/ui.DNq7CNkE.mjs';
import fs from 'node:fs';
import path from 'node:path';
import { consola } from 'consola';
import MagicString from 'magic-string';
import { genSafeVariableName } from 'knitwork';
import { resolvePathSync } from 'mlly';
import { globSync } from 'tinyglobby';
import AutoImportComponents from 'unplugin-vue-components';
import AutoImport from 'unplugin-auto-import';
import { IconUsageScanner, resolveBundleIcons, generateClientBundleCode, collectionNames } from '@nuxt/icon/utils';
import '../dist/runtime/utils/index.js';
import 'scule';
import 'tailwindcss/colors';
import '@nuxt/kit';
import 'node:fs/promises';
function TemplatePlugin(options, appConfig, componentDir) {
const vue = {};
const templates = getTemplates(options, appConfig.ui, void 0, void 0, vue);
const templateKeys = new Set(templates.map((t) => `#build/${t.filename}`));
async function writeTemplates(root) {
const map = {};
const dir = path.join(root, "node_modules", ".nuxt-ui");
const createdDirs = /* @__PURE__ */ new Set();
for (const template of templates) {
if (!template.write || !template.filename) {
continue;
}
const filePath = path.join(dir, template.filename);
const fileDir = path.dirname(filePath);
if (!createdDirs.has(fileDir)) {
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir, { recursive: true });
}
createdDirs.add(fileDir);
}
const contents = await template.getContents({});
let existing = null;
try {
existing = fs.readFileSync(filePath, "utf8");
} catch (error) {
if (error.code !== "ENOENT") {
throw error;
}
}
if (existing !== contents) {
fs.writeFileSync(filePath, contents);
}
map[`#build/${template.filename}`] = filePath;
}
return map;
}
return {
name: "nuxt:ui:templates",
enforce: "pre",
vite: {
async config(config) {
const root = path.resolve(options.root || config.root || ".");
if (options.experimental?.componentDetection && componentDir) {
const dirs = resolveExtraScanDirs(root, options.scanPackages, options.components ? options.components.dirs : void 0);
vue.detectedComponents = await detectUsedComponents(
[root, ...dirs],
options.prefix,
componentDir,
Array.isArray(options.experimental.componentDetection) ? options.experimental.componentDetection : void 0
);
if (vue.detectedComponents?.size) {
consola.success(`Nuxt UI detected ${vue.detectedComponents.size} components in use (including dependencies)`);
} else {
consola.info("Nuxt UI detected no components in use, including all components");
}
}
const alias = await writeTemplates(root);
return {
resolve: {
alias
}
};
}
},
resolveId(id) {
if (templateKeys.has(id + ".ts")) {
return id.replace("#build/", "virtual:nuxt-ui-templates/") + ".ts";
}
},
loadInclude: (id) => templateKeys.has(id.replace("virtual:nuxt-ui-templates/", "#build/")),
load(id) {
id = id.replace("virtual:nuxt-ui-templates/", "#build/");
return templates.find((t) => `#build/${t.filename}` === id).getContents({});
}
};
}
function PluginsPlugin(options) {
const plugins = globSync(["**/*", "!*.d.ts"], { cwd: join(runtimeDir, "plugins"), absolute: true });
plugins.unshift(resolvePathSync("../runtime/vue/plugins/router", { extensions: [".ts", ".mjs", ".js"], url: import.meta.url }));
plugins.unshift(resolvePathSync("../runtime/vue/plugins/head", { extensions: [".ts", ".mjs", ".js"], url: import.meta.url }));
plugins.unshift(resolvePathSync("../runtime/vue/plugins/icons", { extensions: [".ts", ".mjs", ".js"], url: import.meta.url }));
if (options.colorMode) {
plugins.push(resolvePathSync("../runtime/vue/plugins/color-mode", { extensions: [".ts", ".mjs", ".js"], url: import.meta.url }));
}
const proseComponents = options.prose || options.mdc ? globSync(["**/*.vue"], { cwd: join(runtimeDir, "components/prose"), absolute: true }) : [];
return {
name: "nuxt:ui:plugins",
enforce: "pre",
resolveId(id) {
if (id === "@nuxt/ui/vue-plugin") {
return "virtual:nuxt-ui-plugins";
}
},
transform(code, id) {
if (plugins.some((p) => id.startsWith(p)) && code.includes("import.meta.client")) {
const s = new MagicString(code);
s.replaceAll("import.meta.client", "true");
if (s.hasChanged()) {
return {
code: s.toString(),
map: s.generateMap({ hires: true })
};
}
}
},
loadInclude: (id) => id === "virtual:nuxt-ui-plugins",
load() {
const proseImports = proseComponents.map((p) => {
const name = `Prose${p.split("/").pop()?.replace(/\.vue$/, "")}`;
return { name, path: p };
});
return `
${plugins.map((p) => `import ${genSafeVariableName(p)} from "${p}"`).join("\n")}
${proseImports.map((c) => `import ${c.name} from "${c.path}"`).join("\n")}
export default {
install (app, pluginOptions = {}) {
${plugins.map((p) => ` app.use(${genSafeVariableName(p)}, pluginOptions)`).join("\n")}
${proseImports.map((c) => ` app.component('${c.name}', ${c.name})`).join("\n")}
}
}
`;
},
// Argument Vite specific configuration
vite: {
config() {
return {
// Opt-out Nuxt UI from Vite's pre-bundling,
// as we need Vite's pipeline to resolve imports like `#imports`
optimizeDeps: {
exclude: ["@nuxt/ui"]
}
};
}
}
};
}
function AppConfigPlugin(_options, appConfig) {
return {
name: "nuxt:ui:app-config",
enforce: "pre",
resolveId(id) {
if (id === "#build/app.config") {
return "virtual:nuxt-ui-app-config";
}
},
loadInclude: (id) => id === "virtual:nuxt-ui-app-config",
load() {
return `
export default ${JSON.stringify(appConfig)}
`;
},
vite: {
config() {
return {
test: {
server: {
deps: {
inline: ["@nuxt/ui"]
}
}
}
};
}
}
};
}
function resolveRouterMode(options) {
if (options.router === false) {
return "none";
}
if (options.router === "inertia") {
return "inertia";
}
if (options.router === void 0 && options.inertia === true) {
return "inertia";
}
return "vue-router";
}
function createComponentSource(cwd, prefix, ignore = []) {
const files = globSync("**/*.vue", { cwd, ignore: ignore.filter(Boolean) });
const names = new Set(files.map((c) => `${prefix}${c.split("/").pop()?.replace(/\.vue$/, "")}`));
const paths = new Map(files.map((c) => {
const componentName = `${prefix}${c.split("/").pop()?.replace(/\.vue$/, "")}`;
return [componentName, c];
}));
return {
has: (name) => names.has(name),
resolve: (componentName) => {
const relativePath = paths.get(componentName);
if (!relativePath) return;
return { name: "default", from: join(cwd, relativePath) };
},
resolveFile: (filename) => {
const componentName = `${prefix}${filename}`;
const relativePath = paths.get(componentName);
if (!relativePath) return;
return join(cwd, relativePath);
}
};
}
function ComponentImportPlugin(options, meta) {
const colorModeIgnore = !options.colorMode ? ["color-mode/**/*.vue"] : [];
const routerMode = resolveRouterMode(options);
const routerOverrides = {
"vue-router": createComponentSource(join(runtimeDir, "vue/overrides/vue-router"), options.prefix),
"inertia": createComponentSource(join(runtimeDir, "vue/overrides/inertia"), options.prefix),
"none": createComponentSource(join(runtimeDir, "vue/overrides/none"), options.prefix)
};
const unpluginComponents = createComponentSource(
join(runtimeDir, "vue/components"),
options.prefix,
colorModeIgnore
);
const overrideSources = [routerOverrides[routerMode], unpluginComponents].filter((s) => !!s);
const internalResolverPlugin = {
/**
* This plugin aims to ensure we override certain components with Vue-compatible versions:
* <UIcon> and <ULink> currently.
*/
name: "nuxt:ui:components",
enforce: "pre",
resolveId(id, importer) {
if (!importer || !normalize(importer).includes(runtimeDir)) {
return;
}
if (!RELATIVE_IMPORT_RE.test(id)) {
return;
}
const filename = id.match(/([^/]+)\.vue$/)?.[1];
if (filename) {
for (const source of overrideSources) {
const resolved = source.resolveFile(filename);
if (resolved) return resolved;
}
}
}
};
if (options.components === false) {
return [internalResolverPlugin];
}
const defaultComponents = createComponentSource(
join(runtimeDir, "components"),
options.prefix,
[...colorModeIgnore, "content/*.vue", "prose/**/*.vue"]
);
const proseComponents = options.prose || options.mdc ? createComponentSource(join(runtimeDir, "components/prose"), "Prose") : void 0;
const allSources = [routerOverrides[routerMode], unpluginComponents, defaultComponents, proseComponents];
const filteredSources = allSources.filter((s) => !!s);
const packagesToScan = [
"@nuxt/ui",
"@compodium/examples",
...Array.isArray(options.scanPackages) ? options.scanPackages : []
];
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const packagesRegex = packagesToScan.map(escapeRegex).join("|");
const excludeRegex = new RegExp(`[\\\\/]node_modules[\\\\/](?!\\.pnpm|${packagesRegex})`);
const pluginOptions = defu(options.components, {
dts: options.dts ?? true,
exclude: [
excludeRegex,
/[\\/]\.git[\\/]/,
/[\\/]\.nuxt[\\/]/
],
resolvers: [
(componentName) => {
for (const source of filteredSources) {
const resolved = source.resolve(componentName);
if (resolved) return resolved;
}
}
]
});
return [
internalResolverPlugin,
AutoImportComponents.raw(pluginOptions, meta)
];
}
const RELATIVE_IMPORT_RE = /^\.{1,2}\//;
function NuxtEnvironmentPlugin(options) {
const routerMode = resolveRouterMode(options);
const stubsPath = `../runtime/vue/stubs/${routerMode}`;
const stubPath = resolvePathSync(stubsPath, { extensions: [".ts", ".mjs", ".js"], url: import.meta.url });
return {
name: "nuxt:ui",
enforce: "pre",
resolveId(id) {
if (id === "#imports") {
return stubPath;
}
},
transformInclude(id) {
return normalize(id).includes(runtimeDir);
},
transform(code) {
if (code.includes("import.meta.client")) {
const s = new MagicString(code);
s.replaceAll("import.meta.client", "true");
if (s.hasChanged()) {
return {
code: s.toString(),
map: s.generateMap({ hires: true })
};
}
}
}
};
}
function AutoImportPlugin(options, meta) {
if (options.autoImport === false) {
return { name: "nuxt:ui:auto-import" };
}
const pluginOptions = defu(options.autoImport, {
dts: options.dts ?? true,
imports: [
...Object.entries(publicComposables).map(([file, names]) => ({
from: join(runtimeDir, "composables", file),
imports: names
}))
],
dirs: [join(runtimeDir, "vue/composables")]
});
return AutoImport.raw(pluginOptions, meta);
}
const VIRTUAL_ID = "virtual:nuxt-ui-icons";
function parseUserIcon(icon) {
const parsed = parseIconName(icon, collectionNames, true);
return parsed ? `${parsed[0]}:${parsed[1]}` : void 0;
}
function IconsPlugin(options, appConfig) {
let root = options.root || process.cwd();
let isBuild = false;
let source;
let scanner;
let scanned = false;
const scannedIcons = /* @__PURE__ */ new Set();
async function generate() {
const clientBundle = options.icon?.clientBundle;
if (clientBundle === false) {
return "export function init() {}";
}
const userIcons = /* @__PURE__ */ new Map();
for (const icon of clientBundle?.icons ?? []) {
userIcons.set(parseUserIcon(icon) ?? icon.replace(/^i[-:]/, ""), icon);
}
if (clientBundle?.scan) {
scanner ||= new IconUsageScanner(clientBundle.scan);
if (!scanned) {
await scanner.scanFiles(root, scannedIcons);
scanned = true;
}
}
const { collections, failed } = await resolveBundleIcons({
icons: userIcons.keys(),
scannedIcons,
extraIcons: getClientBundleIcons(appConfig.ui?.icons),
resolvePaths: [root]
});
if (failed.length) {
const names = failed.map((name) => userIcons.get(name) ?? name);
const message = `[Nuxt UI] Could not resolve the icons requested in \`icon.clientBundle.icons\`: ${names.join(", ")}. Install their \`@iconify-json/*\` collection, or remove them.`;
if (isBuild) {
throw new Error(message);
}
consola.warn(message);
}
return generateClientBundleCode(collections, { sizeLimitKb: clientBundle?.sizeLimitKb }).code;
}
return {
name: "nuxt:ui:icons",
enforce: "pre",
resolveId(id) {
if (id === VIRTUAL_ID) {
return VIRTUAL_ID;
}
},
loadInclude: (id) => id === VIRTUAL_ID,
load() {
source ||= generate().catch((error) => {
source = void 0;
throw error;
});
return source;
},
vite: {
configResolved(config) {
root = options.root || config.root || root;
isBuild = config.command === "build";
},
// Like `@nuxt/icon`'s vite plugin: when an edit introduces icons the scan hasn't seen,
// regenerate the virtual module so `scan: true` doesn't go stale until a restart.
async handleHotUpdate({ file, read, server }) {
if (!scanner) {
return;
}
const path = relative(root, file);
if (path.startsWith("..") || !scanner.isFileMatch(path)) {
return;
}
const sizeBefore = scannedIcons.size;
scanner.extractFromCode(await read(), scannedIcons);
if (scannedIcons.size === sizeBefore) {
return;
}
source = void 0;
const mod = server.moduleGraph.getModuleById(VIRTUAL_ID);
if (mod) {
server.moduleGraph.invalidateModule(mod);
await server.reloadModule(mod);
}
}
}
};
}
const runtimeDir = normalize(fileURLToPath(new URL("./runtime", import.meta.url)));
const NuxtUIPlugin = createUnplugin((_options = {}, meta) => {
const options = defu(_options, { fonts: false }, defaultOptions);
options.theme = options.theme || {};
options.theme.colors = resolveColors(options.theme.colors);
const { clientBundle, ...icon } = options.icon || {};
const appConfig = defu({ ui: options.ui, colorMode: options.colorMode, icon }, { ui: getDefaultConfig(options.theme) });
return [
NuxtEnvironmentPlugin(options),
ComponentImportPlugin(options, meta),
AutoImportPlugin(options, meta),
tailwind(),
IconsPlugin(options, appConfig),
PluginsPlugin(options),
TemplatePlugin(options, appConfig, join(runtimeDir, "components")),
AppConfigPlugin(options, appConfig),
{
name: "nuxt:ui:plugins-duplication-detection",
vite: {
configResolved(config) {
const plugins = config.plugins || [];
if (options.autoImport !== false && plugins.filter((i) => i.name === "unplugin-auto-import").length > 1) {
throw new Error("[Nuxt UI] Multiple instances of `unplugin-auto-import` detected. Nuxt UI includes `unplugin-auto-import` already, and you can configure it using `autoImport` option in Nuxt UI module options.");
}
if (options.components !== false && plugins.filter((i) => i.name === "unplugin-vue-components").length > 1) {
throw new Error("[Nuxt UI] Multiple instances of `unplugin-vue-components` detected. Nuxt UI includes `unplugin-vue-components` already, and you can configure it using `components` option in Nuxt UI module options.");
}
}
}
}
].flat(1);
});
export { NuxtUIPlugin, runtimeDir };