mkdist
Version:
Lightweight file-to-file transformer
785 lines (773 loc) • 24.4 kB
JavaScript
import { basename, resolve, normalize, extname, join, dirname } from 'pathe';
import fsp from 'node:fs/promises';
import defu from 'defu';
import { pipeline } from 'node:stream';
import { createReadStream, createWriteStream, statSync } from 'node:fs';
import { transform } from 'esbuild';
import jiti from 'jiti';
import { pathToFileURL } from 'node:url';
import cssnano from 'cssnano';
import autoprefixer from 'autoprefixer';
import postcss from 'postcss';
import postcssNested from 'postcss-nested';
import { findStaticImports, findExports, findTypeExports, findDynamicImports } from 'mlly';
import { createRequire } from 'node:module';
import { satisfies } from 'semver';
import { glob } from 'tinyglobby';
function copyFileWithStream(sourcePath, outPath) {
const sourceStream = createReadStream(sourcePath);
const outStream = createWriteStream(outPath);
return new Promise((resolve, reject) => {
pipeline(sourceStream, outStream, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
const DECLARATION_RE = /\.d\.[cm]?ts$/;
const CM_LETTER_RE = /(?<=\.)(c|m)(?=[jt]s$)/;
const KNOWN_EXT_RE = /\.(c|m)?[jt]sx?$/;
const TS_EXTS = /* @__PURE__ */ new Set([".ts", ".mts", ".cts"]);
const jsLoader = async (input, { options }) => {
if (!KNOWN_EXT_RE.test(input.path) || DECLARATION_RE.test(input.path)) {
return;
}
const output = [];
let contents = await input.getContents();
if (options.declaration && !input.srcPath?.match(DECLARATION_RE)) {
const cm = input.srcPath?.match(CM_LETTER_RE)?.[0] || "";
const extension2 = `.d.${cm}ts`;
output.push({
contents,
srcPath: input.srcPath,
path: input.path,
extension: extension2,
declaration: true
});
}
if (TS_EXTS.has(input.extension)) {
contents = await transform(contents, {
...options.esbuild,
loader: "ts"
}).then((r) => r.code);
} else if ([".tsx", ".jsx"].includes(input.extension)) {
contents = await transform(contents, {
loader: input.extension === ".tsx" ? "tsx" : "jsx",
...options.esbuild
}).then((r) => r.code);
}
const isCjs = options.format === "cjs";
if (isCjs) {
contents = jiti("").transform({ source: contents, retainLines: false }).replace(/^exports.default = /gm, "module.exports = ").replace(/^var _default = exports.default = /gm, "module.exports = ").replace("module.exports = void 0;", "");
}
let extension = isCjs ? ".js" : ".mjs";
if (options.ext) {
extension = options.ext.startsWith(".") ? options.ext : `.${options.ext}`;
}
output.push({
contents,
path: input.path,
extension
});
return output;
};
let warnedTypescript = false;
function defineVueLoader(options) {
const blockLoaders = options?.blockLoaders || {};
return async (input, context) => {
if (input.extension !== ".vue") {
return;
}
const { parse } = await import('vue/compiler-sfc');
let modified = false;
const raw = await input.getContents();
const sfc = parse(raw, {
filename: input.srcPath,
ignoreEmpty: true
});
if (sfc.errors.length > 0) {
for (const error of sfc.errors) {
console.error(error);
}
return;
}
const isTs = [
sfc.descriptor.script?.lang,
sfc.descriptor.scriptSetup?.lang
].some((lang) => lang && lang.startsWith("ts"));
if (isTs && !warnedTypescript) {
console.warn(
"[mkdist] vue-sfc-transformer is not installed. mkdist will not transform typescript syntax in Vue SFCs."
);
warnedTypescript = true;
}
const output = [];
const addOutput = (...files) => output.push(...files);
const blocks = [
...sfc.descriptor.styles,
...sfc.descriptor.customBlocks
].filter((item) => !!item);
addOutput(
{
contents: "export default {}",
path: `${input.path}.js`,
srcPath: `${input.srcPath}.js`,
extension: ".d.ts",
declaration: true
},
{
contents: await input.getContents(),
path: input.path,
srcPath: input.srcPath,
extension: ".d.vue.ts",
declaration: true
}
);
const results = await Promise.all(
blocks.map(async (data) => {
const blockLoader = blockLoaders[data.type];
const result = await blockLoader?.(data, {
...context,
rawInput: input,
addOutput
});
if (result) {
modified = true;
}
return { block: result || data, offset: data.loc.start.offset };
})
);
if (!modified) {
addOutput({
path: input.path,
srcPath: input.srcPath,
extension: ".vue",
contents: raw,
declaration: false
});
return output;
}
if (sfc.descriptor.template) {
results.unshift({
block: sfc.descriptor.template,
offset: sfc.descriptor.template.loc.start.offset
});
}
if (sfc.descriptor.script) {
results.unshift({
block: sfc.descriptor.script,
offset: sfc.descriptor.script.loc.start.offset
});
}
if (sfc.descriptor.scriptSetup) {
results.unshift({
block: sfc.descriptor.scriptSetup,
offset: sfc.descriptor.scriptSetup.loc.start.offset
});
}
const contents = results.sort((a, b) => a.offset - b.offset).map(({ block }) => {
const attrs = Object.entries(block.attrs).map(([key, value]) => {
if (!value) {
return void 0;
}
return value === true ? key : `${key}="${value}"`;
}).filter((item) => !!item).join(" ");
const header = `<${`${block.type} ${attrs}`.trim()}>`;
const footer = `</${block.type}>`;
return `${header}
${block.content.trim()}
${footer}
`;
}).join("\n");
addOutput({
path: input.path,
srcPath: input.srcPath,
extension: ".vue",
contents,
declaration: false
});
return output;
};
}
function defineDefaultBlockLoader(options) {
return async (block, { loadFile, rawInput, addOutput }) => {
if (options.type !== block.type) {
return;
}
const lang = typeof block.attrs.lang === "string" ? block.attrs.lang : options.outputLang;
const extension = `.${lang}`;
const files = await loadFile({
getContents: () => block.content,
path: `${rawInput.path}${extension}`,
srcPath: `${rawInput.srcPath}${extension}`,
extension
}) || [];
const blockOutputFile = files.find(
(f) => f.extension === `.${options.outputLang}` || options.validExtensions?.includes(f.extension)
);
if (!blockOutputFile) {
return;
}
addOutput(...files.filter((f) => f !== blockOutputFile));
return {
type: block.type,
attrs: toOmit(block.attrs, "lang"),
content: blockOutputFile.contents
};
};
}
const styleLoader = defineDefaultBlockLoader({
outputLang: "css",
type: "style"
});
const fallbackVueLoader = defineVueLoader({
blockLoaders: {
style: styleLoader
}
});
let cachedVueLoader$1;
const vueLoader = async (file, ctx) => {
if (!cachedVueLoader$1) {
cachedVueLoader$1 = await import('vue-sfc-transformer/mkdist').then((r) => r.vueLoader).catch(() => fallbackVueLoader);
}
return cachedVueLoader$1(file, ctx);
};
function toOmit(record, toRemove) {
return Object.fromEntries(
Object.entries(record).filter(([key]) => key !== toRemove)
);
}
const sassLoader = async (input) => {
if (![".sass", ".scss"].includes(input.extension)) {
return;
}
if (basename(input.srcPath).startsWith("_")) {
return [
{
contents: "",
path: input.path,
skip: true
}
];
}
const compileString = await import('sass').then(
(r) => r.compileString || r.default.compileString
);
const output = [];
const contents = await input.getContents();
output.push({
contents: compileString(contents, {
loadPaths: ["node_modules"],
url: pathToFileURL(input.srcPath)
}).css,
path: input.path,
extension: ".css"
});
return output;
};
const postcssLoader = async (input, ctx) => {
if (ctx.options.postcss === false || ![".css"].includes(input.extension)) {
return;
}
const output = [];
const contents = await input.getContents();
const transformed = await postcss(
[
ctx.options.postcss?.nested !== false && postcssNested(ctx.options.postcss?.nested),
ctx.options.postcss?.autoprefixer !== false && autoprefixer(ctx.options.postcss?.autoprefixer),
ctx.options.postcss?.cssnano !== false && cssnano(ctx.options.postcss?.cssnano),
...ctx.options.postcss?.plugins || []
].filter(Boolean)
).process(contents, {
...ctx.options.postcss?.processOptions,
from: input.srcPath
});
output.push({
contents: transformed.content,
path: input.path,
extension: ".css"
});
return output;
};
let cachedVueLoader;
const loaders = {
js: jsLoader,
vue: cachedVueLoader || (async (...args) => {
cachedVueLoader = await import('vue-sfc-transformer/mkdist').then((r) => r.vueLoader).catch(() => vueLoader);
return cachedVueLoader(...args);
}),
sass: sassLoader,
postcss: postcssLoader
};
const defaultLoaders = ["js", "vue", "sass", "postcss"];
function resolveLoader(loader) {
if (typeof loader === "string") {
return loaders[loader];
}
return loader;
}
function resolveLoaders(loaders2 = defaultLoaders) {
return loaders2.map((loaderName) => {
const _loader = resolveLoader(loaderName);
if (!_loader) {
console.warn("Unknown loader:", loaderName);
}
return _loader;
}).filter(Boolean);
}
function createLoader(loaderOptions = {}) {
const loaders = resolveLoaders(loaderOptions.loaders);
const loadFile = async function(input) {
const context = {
loadFile,
options: loaderOptions
};
for (const loader of loaders) {
const outputs = await loader(input, context);
if (outputs?.length) {
return outputs;
}
}
return [
{
path: input.path,
srcPath: input.srcPath,
raw: true
}
];
};
return {
loadFile
};
}
async function normalizeCompilerOptions(_options) {
const ts = await import('typescript').then((r) => r.default || r);
return ts.convertCompilerOptionsFromJson(_options, process.cwd()).options;
}
const KNOWN_TS_SOURCE_EXT_RE = /\.[cm]?[jt]sx?$/;
async function getDeclarations(vfs, opts) {
const ts = await import('typescript').then((r) => r.default || r);
const inputFiles = [...vfs.keys()].filter(
(path) => path.match(KNOWN_TS_SOURCE_EXT_RE)
);
const tsHost = ts.createCompilerHost(opts.typescript.compilerOptions);
tsHost.writeFile = (fileName, declaration) => {
vfs.set(fileName, declaration);
};
const _readFile = tsHost.readFile;
tsHost.readFile = (filename) => {
if (vfs.has(filename)) {
return vfs.get(filename);
}
return _readFile(filename);
};
const program = ts.createProgram(
inputFiles,
opts.typescript.compilerOptions,
tsHost
);
const result = program.emit();
const output = extractDeclarations(vfs, inputFiles, opts);
augmentWithDiagnostics(result, output, tsHost, ts);
return output;
}
const JS_EXT_RE = /\.(m|c)?(ts|js)$/;
const JSX_EXT_RE = /\.(m|c)?(ts|js)x?$/;
const RELATIVE_RE = /^\.{1,2}[/\\]/;
function extractDeclarations(vfs, inputFiles, opts) {
const output = {};
for (const filename of inputFiles) {
const dtsFilename = filename.replace(JSX_EXT_RE, ".d.$1ts");
let contents = vfs.get(dtsFilename) || "";
if (opts?.addRelativeDeclarationExtensions) {
const ext = filename.match(JS_EXT_RE)?.[0].replace(/ts$/, "js") || ".js";
const imports = findStaticImports(contents);
const exports = findExports(contents);
const typeExports = findTypeExports(contents);
const dynamicImports = findDynamicImports(contents).map(
(dynamicImport) => {
let specifier;
try {
const value = JSON.parse(dynamicImport.expression);
if (typeof value === "string") {
specifier = value;
}
} catch {
}
return {
code: dynamicImport.code,
specifier
};
}
);
for (const spec of [
...exports,
...typeExports,
...imports,
...dynamicImports
]) {
if (!spec.specifier || !RELATIVE_RE.test(spec.specifier)) {
continue;
}
const srcPath = resolve(filename, "..", spec.specifier);
const srcDtsPath = srcPath + ext.replace(JS_EXT_RE, ".d.$1ts");
let specifier = spec.specifier;
try {
if (!vfs.get(srcDtsPath)) {
const stat = statSync(srcPath);
if (stat.isDirectory()) {
specifier += "/index";
}
}
} catch {
}
contents = contents.replace(
spec.code,
spec.code.replace(spec.specifier, specifier + ext)
);
}
}
output[filename] = { contents };
vfs.delete(filename);
}
return output;
}
function augmentWithDiagnostics(result, output, tsHost, ts) {
if (result.diagnostics?.length) {
for (const diagnostic of result.diagnostics) {
const filename = diagnostic.file?.fileName;
if (filename in output) {
output[filename].errors = output[filename].errors || [];
output[filename].errors.push(
new TypeError(ts.formatDiagnostics([diagnostic], tsHost), {
cause: diagnostic
})
);
}
}
console.error(ts.formatDiagnostics(result.diagnostics, tsHost));
}
}
const require = createRequire(import.meta.url);
async function getVueDeclarations(vfs, opts) {
const fileMapping = getFileMapping(vfs);
const srcFiles = Object.keys(fileMapping);
const originFiles = Object.values(fileMapping);
if (originFiles.length === 0) {
return;
}
const { readPackageJSON } = await import('pkg-types');
const pkgInfo = await readPackageJSON("vue-tsc").catch(() => {
});
if (!pkgInfo) {
console.warn(
"[mkdist] Please install `vue-tsc` to generate Vue SFC declarations."
);
return;
}
const { version } = pkgInfo;
let output;
switch (true) {
case satisfies(version, "^1.8.27"): {
output = await emitVueTscV1(vfs, srcFiles, originFiles, opts);
break;
}
case satisfies(version, "~v2.0.0"): {
output = await emitVueTscV2(vfs, srcFiles, originFiles, opts);
break;
}
default: {
output = await emitVueTscLatest(vfs, srcFiles, originFiles, opts);
}
}
for (const [vuePath, dtsSrcPath] of Object.entries(fileMapping)) {
output[vuePath] = output[dtsSrcPath];
}
return output;
}
const SFC_EXT_RE = /\.vue\.[cm]?[jt]s$/;
function getFileMapping(vfs) {
const files = /* @__PURE__ */ Object.create(null);
for (const [srcPath] of vfs) {
if (SFC_EXT_RE.test(srcPath)) {
files[srcPath.replace(SFC_EXT_RE, ".vue")] = srcPath;
}
}
return files;
}
async function emitVueTscV1(vfs, inputFiles, originFiles, opts) {
const vueTsc = await import('vue-tsc').then((r) => r.default || r).catch(() => void 0);
const ts = require("typescript");
const tsHost = ts.createCompilerHost(opts.typescript.compilerOptions);
const _tsSysWriteFile = ts.sys.writeFile;
ts.sys.writeFile = (filename, content) => {
vfs.set(filename, content);
};
const _tsSysReadFile = ts.sys.readFile;
ts.sys.readFile = (filename, encoding) => {
if (vfs.has(filename)) {
return vfs.get(filename);
}
return _tsSysReadFile(filename, encoding);
};
try {
const program = vueTsc.createProgram({
rootNames: inputFiles,
options: opts.typescript.compilerOptions,
host: tsHost
});
const result = program.emit();
const output = extractDeclarations(vfs, originFiles, opts);
augmentWithDiagnostics(result, output, tsHost, ts);
return output;
} finally {
ts.sys.writeFile = _tsSysWriteFile;
ts.sys.readFile = _tsSysReadFile;
}
}
async function emitVueTscV2(vfs, inputFiles, originFiles, opts) {
const { resolve: resolveModule } = await import('mlly');
const ts = await import('typescript').then(
(r) => r.default || r
);
const vueTsc = await import('vue-tsc');
const requireFromVueTsc = createRequire(await resolveModule("vue-tsc"));
const vueLanguageCore = requireFromVueTsc("@vue/language-core");
const volarTs = requireFromVueTsc("@volar/typescript");
const tsHost = ts.createCompilerHost(opts.typescript.compilerOptions);
tsHost.writeFile = (filename, content) => {
vfs.set(filename, vueTsc.removeEmitGlobalTypes(content));
};
const _tsReadFile = tsHost.readFile.bind(tsHost);
tsHost.readFile = (filename) => {
if (vfs.has(filename)) {
return vfs.get(filename);
}
return _tsReadFile(filename);
};
const _tsFileExist = tsHost.fileExists.bind(tsHost);
tsHost.fileExists = (filename) => {
return vfs.has(filename) || _tsFileExist(filename);
};
const programOptions = {
rootNames: inputFiles,
options: opts.typescript.compilerOptions,
host: tsHost
};
const createProgram = volarTs.proxyCreateProgram(
ts,
ts.createProgram,
(ts2, options) => {
const vueLanguagePlugin = vueLanguageCore.createVueLanguagePlugin(
ts2,
(id) => id,
() => "",
(fileName) => {
const fileMap = /* @__PURE__ */ new Set();
for (const vueFileName of options.rootNames.map(
(rootName) => normalize(rootName)
)) {
fileMap.add(vueFileName);
}
return fileMap.has(fileName);
},
options.options,
vueLanguageCore.resolveVueCompilerOptions({})
);
return [vueLanguagePlugin];
}
);
const program = createProgram(programOptions);
const result = program.emit();
const output = extractDeclarations(vfs, originFiles, opts);
augmentWithDiagnostics(result, output, tsHost, ts);
return output;
}
async function emitVueTscLatest(vfs, inputFiles, originFiles, opts) {
const { resolve: resolveModule } = await import('mlly');
const ts = await import('typescript').then(
(r) => r.default || r
);
const requireFromVueTsc = createRequire(await resolveModule("vue-tsc"));
const vueLanguageCore = requireFromVueTsc("@vue/language-core");
const volarTs = requireFromVueTsc("@volar/typescript");
const tsHost = ts.createCompilerHost(opts.typescript.compilerOptions);
tsHost.writeFile = (filename, content) => {
vfs.set(filename, content);
};
const _tsReadFile = tsHost.readFile.bind(tsHost);
tsHost.readFile = (filename) => {
if (vfs.has(filename)) {
return vfs.get(filename);
}
return _tsReadFile(filename);
};
const _tsFileExist = tsHost.fileExists.bind(tsHost);
tsHost.fileExists = (filename) => {
return vfs.has(filename) || _tsFileExist(filename);
};
const programOptions = {
rootNames: inputFiles,
options: opts.typescript.compilerOptions,
host: tsHost
};
const createProgram = volarTs.proxyCreateProgram(
ts,
ts.createProgram,
(ts2, options) => {
const vueLanguagePlugin = vueLanguageCore.createVueLanguagePlugin(
ts2,
options.options,
vueLanguageCore.createParsedCommandLineByJson(
ts2,
ts2.sys,
opts.rootDir,
{},
void 0,
true
).vueOptions,
(id) => id
);
return [vueLanguagePlugin];
}
);
const program = createProgram(programOptions);
const result = program.emit();
const output = extractDeclarations(vfs, originFiles, opts);
augmentWithDiagnostics(result, output, tsHost, ts);
return output;
}
async function mkdist(options = {}) {
options.rootDir = resolve(process.cwd(), options.rootDir || ".");
options.srcDir = resolve(options.rootDir, options.srcDir || "src");
options.distDir = resolve(options.rootDir, options.distDir || "dist");
if (options.cleanDist !== false) {
await fsp.unlink(options.distDir).catch(() => {
});
await fsp.rm(options.distDir, { recursive: true, force: true });
await fsp.mkdir(options.distDir, { recursive: true });
}
const filePaths = await glob(options.pattern || "**", {
absolute: false,
ignore: ["**/node_modules", "**/coverage", "**/.git"],
cwd: options.srcDir,
dot: true,
...options.globOptions
});
const files = filePaths.map((path) => {
const sourcePath = resolve(options.srcDir, path);
return {
path,
srcPath: sourcePath,
extension: extname(path),
getContents: () => fsp.readFile(sourcePath, { encoding: "utf8" })
};
});
options.typescript ||= {};
if (options.typescript.compilerOptions) {
options.typescript.compilerOptions = await normalizeCompilerOptions(
options.typescript.compilerOptions
);
}
options.typescript.compilerOptions = defu(
{ noEmit: false },
options.typescript.compilerOptions,
{
allowJs: true,
declaration: true,
skipLibCheck: true,
strictNullChecks: true,
emitDeclarationOnly: true,
allowImportingTsExtensions: true,
allowNonTsExtensions: true
}
);
const { loadFile } = createLoader(options);
const outputs = [];
for (const file of files) {
outputs.push(...await loadFile(file) || []);
}
for (const output of outputs.filter((o) => o.extension)) {
const renamed = basename(output.path, extname(output.path)) + output.extension;
output.path = join(dirname(output.path), renamed);
if (outputs.some(
(o) => o !== output && o.path === output.path && (!o.extension || o.extension === output.extension)
)) {
output.skip = true;
}
}
const dtsOutputs = outputs.filter((o) => o.declaration && !o.skip);
if (dtsOutputs.length > 0) {
const vfs = new Map(dtsOutputs.map((o) => [o.srcPath, o.contents || ""]));
const declarations = /* @__PURE__ */ Object.create(null);
for (const loader of [getVueDeclarations, getDeclarations]) {
Object.assign(declarations, await loader(vfs, options));
}
for (const output of dtsOutputs) {
const result = declarations[output.srcPath];
output.contents = result?.contents || "";
if (result.errors) {
output.errors = result.errors;
}
}
}
const outPaths = new Set(outputs.map((o) => o.path));
const resolveId = (from, id = "", resolveExtensions) => {
if (!id.startsWith(".")) {
return id;
}
for (const extension of resolveExtensions) {
if (outPaths.has(join(dirname(from), id + extension))) {
return id + extension;
}
}
return id;
};
const esmResolveExtensions = [
"",
"/index.mjs",
"/index.js",
".mjs",
".ts",
".js"
];
for (const output of outputs.filter(
(o) => o.extension === ".mjs" || o.extension === ".js"
)) {
output.contents = output.contents.replace(
/(import|export)(\s+(?:.+|{[\s\w,]+})\s+from\s+["'])(.*)(["'])/g,
(_, type, head, id, tail) => type + head + resolveId(output.path, id, esmResolveExtensions) + tail
).replace(
/import\((["'])(.*)(["'])\)/g,
(_, head, id, tail) => "import(" + head + resolveId(output.path, id, esmResolveExtensions) + tail + ")"
);
}
const cjsResolveExtensions = ["", "/index.cjs", ".cjs"];
for (const output of outputs.filter((o) => o.extension === ".cjs")) {
output.contents = output.contents.replace(
/require\((["'])(.*)(["'])\)/g,
(_, head, id, tail) => "require(" + head + resolveId(output.path, id, cjsResolveExtensions) + tail + ")"
);
}
const writtenFiles = [];
const errors = [];
await Promise.all(
outputs.filter((o) => !o.skip).map(async (output) => {
const outFile = join(options.distDir, output.path);
await fsp.mkdir(dirname(outFile), { recursive: true });
await (output.raw ? copyFileWithStream(output.srcPath, outFile) : fsp.writeFile(outFile, output.contents, "utf8"));
writtenFiles.push(outFile);
if (output.errors) {
errors.push({ filename: outFile, errors: output.errors });
}
})
);
return {
errors,
writtenFiles
};
}
export { mkdist as m };