1import { build } from 'esbuild'; 2import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; 3import path from 'node:path'; 4import { fileURLToPath } from 'node:url'; 5 6const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); 7const nodeModulesDir = path.join(rootDir, 'node_modules'); 8const vendorDir = path.join(rootDir, 'vendor'); 9const fontDir = path.join(rootDir, 'font'); 10 11const packages = [ 12 { 13 name: 'bpmn-js', 14 globalName: 'BpmnJS', 15 assetsDir: path.join('dist', 'assets'), 16 fontsDir: path.join('dist', 'assets', 'bpmn-font', 'font'), 17 fontFiles: ['bpmn.woff', 'bpmn.woff2'], 18 outputs: [ 19 { 20 entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'bpmn-viewer.js'), 21 outFile: path.join(vendorDir, 'bpmn-js', 'dist', 'bpmn-viewer.production.min.js'), 22 }, 23 { 24 entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'bpmn-modeler.js'), 25 outFile: path.join(vendorDir, 'bpmn-js', 'dist', 'bpmn-modeler.production.min.js'), 26 }, 27 ], 28 }, 29 { 30 name: 'dmn-js', 31 globalName: 'DmnJS', 32 assetsDir: path.join('dist', 'assets'), 33 fontsDir: path.join('dist', 'assets', 'dmn-font', 'font'), 34 fontFiles: ['dmn.woff', 'dmn.woff2'], 35 outputs: [ 36 { 37 entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'dmn-viewer.js'), 38 outFile: path.join(vendorDir, 'dmn-js', 'dist', 'dmn-viewer.production.min.js'), 39 }, 40 { 41 entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'dmn-modeler.js'), 42 outFile: path.join(vendorDir, 'dmn-js', 'dist', 'dmn-modeler.production.min.js'), 43 }, 44 ], 45 }, 46]; 47 48async function pathExists(targetPath) { 49 try { 50 await stat(targetPath); 51 return true; 52 } catch { 53 return false; 54 } 55} 56 57async function ensureInstalled(packageName) { 58 const packagePath = path.join(nodeModulesDir, packageName, 'package.json'); 59 60 if (!(await pathExists(packagePath))) { 61 throw new Error( 62 `Missing npm dependency ${packageName}. Run npm install before building vendor bundles.` 63 ); 64 } 65 66 return packagePath; 67} 68 69async function readPackageMetadata(packageName) { 70 const packagePath = await ensureInstalled(packageName); 71 return JSON.parse(await readFile(packagePath, 'utf8')); 72} 73 74function createBanner(metadata) { 75 return `/*! ${metadata.name} - ${metadata.version} | generated for dokuwiki-plugin-bpmnio | ${metadata.license} */`; 76} 77 78async function copyFileEnsuringDir(sourcePath, targetPath) { 79 await mkdir(path.dirname(targetPath), { recursive: true }); 80 await cp(sourcePath, targetPath, { force: true }); 81} 82 83async function copyAssets(sourceDir, targetDir) { 84 await mkdir(targetDir, { recursive: true }); 85 86 for (const entry of await readdir(sourceDir, { withFileTypes: true })) { 87 const sourcePath = path.join(sourceDir, entry.name); 88 const normalizedPath = sourcePath.split(path.sep).join('/'); 89 90 if (normalizedPath.includes('/font/')) { 91 continue; 92 } 93 94 if (entry.isDirectory()) { 95 await copyAssets(sourcePath, path.join(targetDir, entry.name)); 96 continue; 97 } 98 99 const targetName = entry.name.endsWith('.css') 100 ? entry.name.replace(/\.css$/u, '.less') 101 : entry.name; 102 103 await copyFileEnsuringDir(sourcePath, path.join(targetDir, targetName)); 104 } 105} 106 107async function copyFonts(sourceDir, files) { 108 await mkdir(fontDir, { recursive: true }); 109 110 for (const file of files) { 111 await copyFileEnsuringDir(path.join(sourceDir, file), path.join(fontDir, file)); 112 } 113} 114 115async function cleanPackageOutput(packageName) { 116 await rm(path.join(vendorDir, packageName), { recursive: true, force: true }); 117} 118 119async function copyMetadata(packageName) { 120 const sourceDir = path.join(nodeModulesDir, packageName); 121 const targetDir = path.join(vendorDir, packageName); 122 123 for (const file of ['LICENSE', 'README.md', 'package.json']) { 124 const sourcePath = path.join(sourceDir, file); 125 if (await pathExists(sourcePath)) { 126 await copyFileEnsuringDir(sourcePath, path.join(targetDir, file)); 127 } 128 } 129} 130 131async function buildBundle({ entryPoint, outFile, metadata, packageName }) { 132 await mkdir(path.dirname(outFile), { recursive: true }); 133 134 await build({ 135 entryPoints: [entryPoint], 136 outfile: outFile, 137 bundle: true, 138 minify: true, 139 platform: 'browser', 140 format: 'iife', 141 target: ['es2019'], 142 legalComments: 'inline', 143 banner: { 144 js: createBanner(metadata), 145 }, 146 define: { 147 'process.env.NODE_ENV': '"production"', 148 global: 'window', 149 }, 150 logLevel: 'info', 151 }); 152 153 console.log(`Built ${packageName} bundle: ${path.relative(rootDir, outFile)}`); 154} 155 156async function main() { 157 for (const pkg of packages) { 158 const metadata = await readPackageMetadata(pkg.name); 159 const sourceDir = path.join(nodeModulesDir, pkg.name); 160 const sourceAssetsDir = path.join(sourceDir, pkg.assetsDir); 161 const sourceFontsDir = path.join(sourceDir, pkg.fontsDir); 162 const targetAssetsDir = path.join(vendorDir, pkg.name, pkg.assetsDir); 163 164 await cleanPackageOutput(pkg.name); 165 await copyMetadata(pkg.name); 166 await copyAssets(sourceAssetsDir, targetAssetsDir); 167 await copyFonts(sourceFontsDir, pkg.fontFiles); 168 169 for (const output of pkg.outputs) { 170 await buildBundle({ 171 entryPoint: output.entryPoint, 172 outFile: output.outFile, 173 metadata, 174 packageName: pkg.name, 175 }); 176 } 177 } 178 179 const generatedMetadata = { 180 generatedAt: new Date().toISOString(), 181 packages: Object.fromEntries( 182 await Promise.all( 183 packages.map(async (pkg) => [pkg.name, (await readPackageMetadata(pkg.name)).version]) 184 ) 185 ), 186 }; 187 188 await writeFile( 189 path.join(vendorDir, 'build-manifest.json'), 190 `${JSON.stringify(generatedMetadata, null, 2)}\n` 191 ); 192} 193 194main().catch((error) => { 195 console.error(error.message); 196 process.exitCode = 1; 197}); 198