@nuxt/module-builder
Version:
Complete solution for building Nuxt modules
246 lines (241 loc) • 8.65 kB
JavaScript
import { promises, existsSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
import { resolve, basename, join, dirname } from 'pathe';
import { filename } from 'pathe/utils';
import { readPackageJSON } from 'pkg-types';
import { parse } from 'tsconfck';
import { defu } from 'defu';
import { createRegExp, anyOf } from 'magic-regexp';
import { consola } from 'consola';
import { resolvePath, findExports } from 'mlly';
import { defineCommand } from 'citty';
import { n as name, v as version } from '../shared/module-builder.617a9be8.mjs';
const build = defineCommand({
meta: {
name: "build",
description: "Build module for distribution"
},
args: {
cwd: {
type: "string",
description: "Current working directory"
},
rootDir: {
type: "positional",
description: "Root directory",
required: false
},
outDir: {
type: "string"
},
sourcemap: {
type: "boolean"
},
stub: {
type: "boolean"
}
},
async run(context) {
const { build } = await import('unbuild');
const cwd = resolve(context.args.cwd || context.args.rootDir || ".");
const outDir = context.args.outDir || "dist";
await build(cwd, false, {
declaration: true,
sourcemap: context.args.sourcemap,
stub: context.args.stub,
outDir,
entries: [
"src/module",
{
input: "src/runtime/",
outDir: `${outDir}/runtime`,
addRelativeDeclarationExtensions: true,
ext: "js",
esbuild: {
jsxImportSource: "vue",
jsx: "automatic",
jsxFactory: "h"
}
}
],
rollup: {
esbuild: {
target: "esnext"
},
emitCJS: false,
cjsBridge: true
},
externals: [
/dist\/runtime\//,
"@nuxt/schema",
"@nuxt/schema-nightly",
"@nuxt/schema-edge",
"@nuxt/kit",
"@nuxt/kit-nightly",
"@nuxt/kit-edge",
"nuxt",
"nuxt-nightly",
"nuxt-edge",
"nuxt3",
"vue",
"vue-demi"
],
hooks: {
async "mkdist:entry:options"(_ctx, entry, options) {
options.typescript = defu(options.typescript, {
compilerOptions: await loadTSCompilerOptions(entry.input)
});
},
async "rollup:options"(ctx, options) {
options.plugins || (options.plugins = []);
if (!Array.isArray(options.plugins))
options.plugins = [options.plugins];
const runtimeEntries = ctx.options.entries.filter((entry) => entry.builder === "mkdist");
const runtimeDirs = runtimeEntries.map((entry) => basename(entry.input));
const RUNTIME_RE = createRegExp(anyOf(...runtimeDirs).and("/"));
options.plugins.unshift({
name: "nuxt-module-builder:runtime-externals",
async resolveId(id, importer) {
if (!RUNTIME_RE.test(id))
return;
const resolved = await this.resolve(id, importer, { skipSelf: true });
if (!resolved)
return;
for (const entry of runtimeEntries) {
if (!resolved.id.includes(entry.input))
continue;
const distFile = await resolvePath(join(dirname(resolved.id.replace(entry.input, entry.outDir)), filename(resolved.id)));
if (distFile) {
return {
external: true,
id: distFile
};
}
}
}
});
},
async "rollup:done"(ctx) {
await writeCJSStub(ctx.options.outDir);
const moduleEntryPath = resolve(ctx.options.outDir, "module.mjs");
const moduleFn = await import(pathToFileURL(moduleEntryPath).toString()).then((r) => r.default || r).catch((err) => {
consola.error(err);
consola.error("Cannot load module. Please check dist:", moduleEntryPath);
return null;
});
if (!moduleFn) {
return;
}
const moduleMeta = await moduleFn.getMeta?.() || {};
if (ctx.pkg) {
if (!moduleMeta.name) {
moduleMeta.name = ctx.pkg.name;
}
if (!moduleMeta.version) {
moduleMeta.version = ctx.pkg.version;
}
}
moduleMeta.builder = {
[name]: version,
unbuild: await readPackageJSON("unbuild").then((r) => r.version).catch(() => "unknown")
};
const metaFile = resolve(ctx.options.outDir, "module.json");
await promises.writeFile(metaFile, JSON.stringify(moduleMeta, null, 2), "utf8");
await writeTypes(ctx.options.outDir, ctx.options.stub);
}
}
});
}
});
async function writeTypes(distDir, isStub) {
const dtsFile = resolve(distDir, "types.d.ts");
const dtsFileMts = resolve(distDir, "types.d.mts");
if (existsSync(dtsFile)) {
return;
}
const moduleReExports = [];
if (!isStub) {
const moduleTypesFile = resolve(distDir, "module.d.ts");
const moduleTypes = await promises.readFile(moduleTypesFile, "utf8").catch(() => "");
const normalisedModuleTypes = moduleTypes.replace(/export\s*\{.*?\}/gs, (match) => match.replace(/\b(type|interface)\b/g, ""));
for (const e of findExports(normalisedModuleTypes)) {
moduleReExports.push(e);
}
}
const appShims = [];
const schemaShims = [];
const moduleImports = [];
const schemaImports = [];
const moduleExports = [];
const hasTypeExport = (name2) => isStub || moduleReExports.find((exp) => exp.names?.includes(name2));
if (!hasTypeExport("ModuleOptions")) {
schemaImports.push("NuxtModule");
moduleImports.push("default as Module");
moduleExports.push(`export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>`);
}
if (hasTypeExport("ModuleHooks")) {
moduleImports.push("ModuleHooks");
schemaShims.push(" interface NuxtHooks extends ModuleHooks {}");
}
if (hasTypeExport("ModuleRuntimeHooks")) {
moduleImports.push("ModuleRuntimeHooks");
appShims.push(` interface RuntimeNuxtHooks extends ModuleRuntimeHooks {}`);
}
if (hasTypeExport("ModuleRuntimeConfig")) {
moduleImports.push("ModuleRuntimeConfig");
schemaShims.push(" interface RuntimeConfig extends ModuleRuntimeConfig {}");
}
if (hasTypeExport("ModulePublicRuntimeConfig")) {
moduleImports.push("ModulePublicRuntimeConfig");
schemaShims.push(" interface PublicRuntimeConfig extends ModulePublicRuntimeConfig {}");
}
const dtsContents = `
${schemaImports.length ? `import type { ${schemaImports.join(", ")} } from '@nuxt/schema'` : ""}
${moduleImports.length ? `import type { ${moduleImports.join(", ")} } from './module'` : ""}
${appShims.length ? `declare module '#app' {
${appShims.join("\n")}
}
` : ""}
${schemaShims.length ? `declare module '@nuxt/schema' {
${schemaShims.join("\n")}
}
` : ""}
${moduleExports.length ? `
${moduleExports.join("\n")}` : ""}
${isStub ? 'export * from "./module"' : ""}
${moduleReExports[0] ? `
export { ${moduleReExports[0].names.map((n) => (n === "default" ? "" : "type ") + n).join(", ")} } from './module'` : ""}
`.trim().replace(/[\n\r]{3,}/g, "\n\n") + "\n";
await promises.writeFile(dtsFile, dtsContents, "utf8");
if (!existsSync(dtsFileMts)) {
await promises.writeFile(dtsFileMts, dtsContents.replace(/\.\/module/g, "./module.js"), "utf8");
}
}
async function writeCJSStub(distDir) {
const cjsStubFile = resolve(distDir, "module.cjs");
if (existsSync(cjsStubFile)) {
return;
}
const cjsStub = `module.exports = function(...args) {
return import('./module.mjs').then(m => m.default.call(this, ...args))
}
const _meta = module.exports.meta = require('./module.json')
module.exports.getMeta = () => Promise.resolve(_meta)
`;
await promises.writeFile(cjsStubFile, cjsStub, "utf8");
}
async function loadTSCompilerOptions(path) {
const config = await parse(path);
const resolvedCompilerOptions = config?.tsconfig.compilerOptions || {};
for (const { tsconfig, tsconfigFile } of config.extended || []) {
for (const alias in tsconfig.compilerOptions?.paths || {}) {
resolvedCompilerOptions.paths[alias] = resolvedCompilerOptions.paths[alias].map((p) => {
if (!/^\.{1,2}(?:\/|$)/.test(p))
return p;
return resolve(dirname(tsconfigFile), p);
});
}
}
return resolvedCompilerOptions;
}
export { build as default };