const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
let compileTemplate, compileScript, compileStyle, compileStyleAsync, parse;
try {
const sfc = require("@vue/compiler-sfc");
compileTemplate = sfc.compileTemplate;
compileScript = sfc.compileScript;
compileStyle = sfc.compileStyle;
compileStyleAsync = sfc.compileStyleAsync;
parse = sfc.parse;
} catch (e) {
console.error("Error: @vue/compiler-sfc not found.");
console.error("Please install it with: npm install @vue/compiler-sfc");
process.exit(1);
}
function getHash(text) {
return crypto.createHash("sha256").update(text).digest("hex").substring(0, 8);
}
function generateComponentId(filepath, source, isProd) {
const normalizedPath = filepath.replace(/\\/g, "/");
if (isProd) {
return getHash(normalizedPath);
} else {
return getHash(normalizedPath + source);
}
}
const scriptIdentifier = "_sfc_main";
function isUseInlineTemplate(descriptor, isProd) {
return (
isProd && !!descriptor.scriptSetup && !descriptor.template?.src );
}
function canInlineMain(descriptor) {
if (descriptor.script?.src || descriptor.scriptSetup?.src) {
return false;
}
const lang = descriptor.script?.lang || descriptor.scriptSetup?.lang;
if (!lang || lang === "js" || lang === "ts") {
return true;
}
return false;
}
function resolveTemplateCompilerOptions(descriptor, filename, isProd, ssr, resolvedScript) {
const block = descriptor.template;
if (!block) return undefined;
const hasScoped = descriptor.styles.some((s) => s.scoped);
const id = descriptor.id;
const expressionPlugins = [];
const lang = descriptor.scriptSetup?.lang || descriptor.script?.lang;
if (lang && /tsx?$/.test(lang) && !expressionPlugins.includes("typescript")) {
expressionPlugins.push("typescript");
}
return {
id,
filename,
scoped: hasScoped,
slotted: descriptor.slotted,
isProd,
ssr,
ssrCssVars: descriptor.cssVars,
sourceMap: false,
compilerOptions: {
scopeId: hasScoped ? `data-v-${id}` : undefined,
bindingMetadata: resolvedScript ? resolvedScript.bindings : undefined,
expressionPlugins,
sourceMap: false,
},
};
}
const CODEGEN_DIR = path.join(__dirname, "check");
const SOURCE_DIR = path.join(CODEGEN_DIR, "source");
const GENERATED_DIR = path.join(CODEGEN_DIR, "generated");
if (!fs.existsSync(SOURCE_DIR)) {
fs.mkdirSync(SOURCE_DIR, { recursive: true });
}
if (!fs.existsSync(GENERATED_DIR)) {
fs.mkdirSync(GENERATED_DIR, { recursive: true });
}
const vueFiles = fs.existsSync(SOURCE_DIR)
? fs.readdirSync(SOURCE_DIR).filter((file) => file.endsWith(".vue"))
: [];
if (vueFiles.length === 0) {
console.log("No .vue files found in source/ directory");
console.log("Run the Rust example first to create sample files:");
console.log(" cargo run --example check");
process.exit(0);
}
console.log(`Found ${vueFiles.length} .vue file(s) to process`);
console.log("Generating per-block dev, prod, and SSR builds...\n");
async function compileVueSFC(source, filename, filepath, isProd, ssr = false) {
const id = generateComponentId(filepath, source, isProd);
const blocks = {};
const { descriptor, errors: parseErrors } = parse(source, {
filename,
sourceMap: false,
});
if (parseErrors.length > 0) {
return { blocks, id };
}
descriptor.id = id;
const hasScoped = descriptor.styles.some((s) => s.scoped);
const inlineTemplate = isUseInlineTemplate(descriptor, isProd);
let resolvedScript = null;
if (descriptor.script || descriptor.scriptSetup) {
try {
resolvedScript = compileScript(descriptor, {
id,
isProd,
sourceMap: false,
inlineTemplate,
templateOptions: resolveTemplateCompilerOptions(descriptor, filename, isProd, ssr, null),
genDefaultAs: canInlineMain(descriptor) ? scriptIdentifier : undefined,
propsDestructure: true,
});
blocks["script"] = resolvedScript.content;
} catch {
}
}
if (descriptor.template && !inlineTemplate) {
try {
const templateOpts = resolveTemplateCompilerOptions(
descriptor,
filename,
isProd,
ssr,
resolvedScript,
);
const templateResult = compileTemplate({
...templateOpts,
source: descriptor.template.content,
});
if (!templateResult.errors || templateResult.errors.length === 0) {
blocks["render"] = templateResult.code;
}
} catch {
}
}
for (let i = 0; i < descriptor.styles.length; i++) {
const style = descriptor.styles[i];
const isModule = !!style.module;
try {
const styleOptions = {
source: style.content,
filename,
id,
scoped: style.scoped,
isProd,
modules: isModule,
preprocessLang: style.lang,
};
const styleResult = isModule
? await compileStyleAsync(styleOptions)
: compileStyle(styleOptions);
if (!styleResult.errors || styleResult.errors.length === 0) {
blocks[`style${i}`] = styleResult.code;
}
} catch {
}
}
for (const block of descriptor.customBlocks) {
blocks[block.type] = block.content;
}
return { blocks, id };
}
function writeBlocks(baseName, blocks, mode) {
let totalSize = 0;
for (const [block, content] of Object.entries(blocks)) {
const outputPath = path.join(GENERATED_DIR, `${baseName}.${block}.${mode}.vue.js`);
fs.writeFileSync(outputPath, content);
totalSize += content.length;
}
return totalSize;
}
async function processFiles() {
const start = Date.now();
let erroredFiles = 0;
let totalDevSize = 0;
let totalProdSize = 0;
let totalSsrSize = 0;
for (const file of vueFiles) {
const filePath = path.join(SOURCE_DIR, file);
const baseName = path.basename(file, ".vue");
try {
const source = fs.readFileSync(filePath, "utf-8");
const devResult = await compileVueSFC(source, file, filePath, false);
if (Object.keys(devResult.blocks).length > 0) {
totalDevSize += writeBlocks(baseName, devResult.blocks, "dev");
} else {
erroredFiles++;
}
const prodResult = await compileVueSFC(source, file, filePath, true);
if (Object.keys(prodResult.blocks).length > 0) {
totalProdSize += writeBlocks(baseName, prodResult.blocks, "prod");
}
const ssrResult = await compileVueSFC(source, file, filePath, false, true);
if (Object.keys(ssrResult.blocks).length > 0) {
totalSsrSize += writeBlocks(baseName, ssrResult.blocks, "ssr");
}
} catch {
erroredFiles++;
}
}
const elapsed = Date.now() - start;
console.log(`Done! ${elapsed}ms`);
console.log(` DEV total: ${totalDevSize} bytes`);
console.log(` PROD total: ${totalProdSize} bytes`);
console.log(` SSR total: ${totalSsrSize} bytes`);
console.log(` Errors: ${erroredFiles} file(s)`);
console.log("\nOutput format: {name}.{block}.{dev|prod|ssr}.vue.js");
console.log(" Blocks: script, render, style0..styleN, <custom>");
}
processFiles().catch(console.error);