import esbuild from "esbuild"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { nodeExternalsPlugin } from "esbuild-node-externals"; import path from "path"; import fs from "fs"; // import { glob } from "glob"; const banner = ` // patch __dirname // import { fileURLToPath } from "url"; // import path from "path"; // const __filename = fileURLToPath(import.meta.url); // const __dirname = path.dirname(__filename); // allow top level await import { createRequire as topLevelCreateRequire } from "module"; const require = topLevelCreateRequire(import.meta.url); `; const argv = yargs(hideBin(process.argv)) .usage("Usage: $0 -entry [string] -out [string] -build [string]") .option("entry", { alias: "e", describe: "Entry point file", type: "string", demandOption: true, }) .option("out", { alias: "o", describe: "Output file path", type: "string", demandOption: true, }) .option("build", { alias: "b", describe: "Build type (oss, saas, enterprise)", type: "string", choices: ["oss", "saas", "enterprise"], default: "oss", }) .help() .alias("help", "h").argv; // generate a list of all package.json files in the monorepo function getPackagePaths() { // const packagePaths = []; // const packageGlob = "package.json"; // const packageJsonFiles = glob.sync(packageGlob); // for (const packageJsonFile of packageJsonFiles) { // packagePaths.push(path.dirname(packageJsonFile) + "/package.json"); // } // return packagePaths; return ["package.json"]; } // Plugin to guard against bad imports from #private function privateImportGuardPlugin() { return { name: "private-import-guard", setup(build) { const violations = []; build.onResolve({ filter: /^#private\// }, (args) => { const importingFile = args.importer; // Check if the importing file is NOT in server/private const normalizedImporter = path.normalize(importingFile); const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private")); if (!isInServerPrivate) { const violation = { file: importingFile, importPath: args.path, resolveDir: args.resolveDir }; violations.push(violation); console.log(`PRIVATE IMPORT VIOLATION:`); console.log(` File: ${importingFile}`); console.log(` Import: ${args.path}`); console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`); console.log(''); } // Return null to let the default resolver handle it return null; }); build.onEnd((result) => { if (violations.length > 0) { console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`); violations.forEach((v, i) => { console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`); }); console.log(''); result.errors.push({ text: `Private import violations detected: ${violations.length} violation(s) found`, location: null, notes: violations.map(v => ({ text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`, location: null })) }); } }); } }; } // Plugin to guard against bad imports from #private function dynamicImportGuardPlugin() { return { name: "dynamic-import-guard", setup(build) { const violations = []; build.onResolve({ filter: /^#dynamic\// }, (args) => { const importingFile = args.importer; // Check if the importing file is NOT in server/private const normalizedImporter = path.normalize(importingFile); const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private")); if (isInServerPrivate) { const violation = { file: importingFile, importPath: args.path, resolveDir: args.resolveDir }; violations.push(violation); console.log(`DYNAMIC IMPORT VIOLATION:`); console.log(` File: ${importingFile}`); console.log(` Import: ${args.path}`); console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`); console.log(''); } // Return null to let the default resolver handle it return null; }); build.onEnd((result) => { if (violations.length > 0) { console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`); violations.forEach((v, i) => { console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`); }); console.log(''); result.errors.push({ text: `Dynamic import violations detected: ${violations.length} violation(s) found`, location: null, notes: violations.map(v => ({ text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`, location: null })) }); } }); } }; } // Plugin to dynamically switch imports based on build type function dynamicImportSwitcherPlugin(buildValue) { return { name: "dynamic-import-switcher", setup(build) { const switches = []; build.onStart(() => { console.log(`Dynamic import switcher using build type: ${buildValue}`); }); build.onResolve({ filter: /^#dynamic\// }, (args) => { // Extract the path after #dynamic/ const dynamicPath = args.path.replace(/^#dynamic\//, ''); // Determine the replacement based on build type let replacement; if (buildValue === "oss") { replacement = `#open/${dynamicPath}`; } else if (buildValue === "saas" || buildValue === "enterprise") { replacement = `#closed/${dynamicPath}`; // We use #closed here so that the route guards dont complain after its been changed but this is the same as #private } else { console.warn(`Unknown build type '${buildValue}', defaulting to #open/`); replacement = `#open/${dynamicPath}`; } const switchInfo = { file: args.importer, originalPath: args.path, replacementPath: replacement, buildType: buildValue }; switches.push(switchInfo); console.log(`DYNAMIC IMPORT SWITCH:`); console.log(` File: ${args.importer}`); console.log(` Original: ${args.path}`); console.log(` Switched to: ${replacement} (build: ${buildValue})`); console.log(''); // Rewrite the import path and let the normal resolution continue return build.resolve(replacement, { importer: args.importer, namespace: args.namespace, resolveDir: args.resolveDir, kind: args.kind }); }); build.onEnd((result) => { if (switches.length > 0) { console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`); switches.forEach((s, i) => { console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`); console.log(` ${s.originalPath} → ${s.replacementPath}`); }); console.log(''); } }); } }; } esbuild .build({ entryPoints: [argv.entry], bundle: true, outfile: argv.out, format: "esm", minify: false, banner: { js: banner, }, platform: "node", external: ["body-parser"], plugins: [ privateImportGuardPlugin(), dynamicImportGuardPlugin(), dynamicImportSwitcherPlugin(argv.build), nodeExternalsPlugin({ packagePath: getPackagePaths(), }), ], sourcemap: "inline", target: "node22", }) .then((result) => { // Check if there were any errors in the build result if (result.errors && result.errors.length > 0) { console.error(`Build failed with ${result.errors.length} error(s):`); result.errors.forEach((error, i) => { console.error(`${i + 1}. ${error.text}`); if (error.notes) { error.notes.forEach(note => { console.error(` - ${note.text}`); }); } }); // remove the output file if it was created if (fs.existsSync(argv.out)) { fs.unlinkSync(argv.out); } process.exit(1); } console.log("Build completed successfully"); }) .catch((error) => { console.error("Build failed:", error); process.exit(1); });