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'; 5import packConfigModule from 'bpmnlint-pack-config'; 6 7const { packConfig } = packConfigModule; 8 9const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); 10const nodeModulesDir = path.join(rootDir, 'node_modules'); 11const vendorDir = path.join(rootDir, 'vendor'); 12const fontDir = path.join(rootDir, 'font'); 13const generatedDir = path.join(rootDir, 'build', 'generated'); 14 15// .bpmnlintrc is resolved into a self-contained { config, resolver } module at 16// build time. bpmnlint cannot resolve rule references in the browser, so the 17// packed module is what the bpmn-viewer / bpmn-modeler entry points import. 18const bpmnlintConfigPath = path.join(rootDir, '.bpmnlintrc'); 19const packedLintConfigPath = path.join(generatedDir, 'bpmnlintrc.packed.js'); 20 21const packages = [ 22 { 23 name: 'bpmn-js', 24 globalName: 'BpmnJS', 25 assetsDir: path.join('dist', 'assets'), 26 fontsDir: path.join('dist', 'assets', 'bpmn-font', 'font'), 27 fontFiles: ['bpmn.woff', 'bpmn.woff2'], 28 outputs: [ 29 { 30 entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'bpmn-viewer.js'), 31 outFile: path.join(vendorDir, 'bpmn-js', 'dist', 'bpmn-viewer.production.min.js'), 32 }, 33 { 34 entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'bpmn-modeler.js'), 35 outFile: path.join(vendorDir, 'bpmn-js', 'dist', 'bpmn-modeler.production.min.js'), 36 }, 37 ], 38 }, 39 { 40 name: 'dmn-js', 41 globalName: 'DmnJS', 42 assetsDir: path.join('dist', 'assets'), 43 fontsDir: path.join('dist', 'assets', 'dmn-font', 'font'), 44 fontFiles: ['dmn.woff', 'dmn.woff2'], 45 outputs: [ 46 { 47 entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'dmn-viewer.js'), 48 outFile: path.join(vendorDir, 'dmn-js', 'dist', 'dmn-viewer.production.min.js'), 49 }, 50 { 51 entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'dmn-modeler.js'), 52 outFile: path.join(vendorDir, 'dmn-js', 'dist', 'dmn-modeler.production.min.js'), 53 }, 54 ], 55 }, 56]; 57 58async function pathExists(targetPath) { 59 try { 60 await stat(targetPath); 61 return true; 62 } catch { 63 return false; 64 } 65} 66 67async function ensureInstalled(packageName) { 68 const packagePath = path.join(nodeModulesDir, packageName, 'package.json'); 69 70 if (!(await pathExists(packagePath))) { 71 throw new Error( 72 `Missing npm dependency ${packageName}. Run npm install before building vendor bundles.` 73 ); 74 } 75 76 return packagePath; 77} 78 79async function readPackageMetadata(packageName) { 80 const packagePath = await ensureInstalled(packageName); 81 return JSON.parse(await readFile(packagePath, 'utf8')); 82} 83 84function createBanner(metadata) { 85 return `/*! ${metadata.name} - ${metadata.version} | generated for dokuwiki-plugin-bpmnio | ${metadata.license} */`; 86} 87 88async function copyFileEnsuringDir(sourcePath, targetPath) { 89 await mkdir(path.dirname(targetPath), { recursive: true }); 90 await cp(sourcePath, targetPath, { force: true }); 91} 92 93async function copyAssets(sourceDir, targetDir) { 94 await mkdir(targetDir, { recursive: true }); 95 96 for (const entry of await readdir(sourceDir, { withFileTypes: true })) { 97 const sourcePath = path.join(sourceDir, entry.name); 98 const normalizedPath = sourcePath.split(path.sep).join('/'); 99 100 if (normalizedPath.includes('/font/')) { 101 continue; 102 } 103 104 if (entry.isDirectory()) { 105 await copyAssets(sourcePath, path.join(targetDir, entry.name)); 106 continue; 107 } 108 109 const targetName = entry.name.endsWith('.css') 110 ? entry.name.replace(/\.css$/u, '.less') 111 : entry.name; 112 113 await copyFileEnsuringDir(sourcePath, path.join(targetDir, targetName)); 114 } 115} 116 117async function copyFonts(sourceDir, files) { 118 await mkdir(fontDir, { recursive: true }); 119 120 for (const file of files) { 121 await copyFileEnsuringDir(path.join(sourceDir, file), path.join(fontDir, file)); 122 } 123} 124 125async function cleanPackageOutput(packageName) { 126 await rm(path.join(vendorDir, packageName), { recursive: true, force: true }); 127} 128 129async function copyMetadata(packageName) { 130 const sourceDir = path.join(nodeModulesDir, packageName); 131 const targetDir = path.join(vendorDir, packageName); 132 133 for (const file of ['LICENSE', 'README.md', 'package.json']) { 134 const sourcePath = path.join(sourceDir, file); 135 if (await pathExists(sourcePath)) { 136 await copyFileEnsuringDir(sourcePath, path.join(targetDir, file)); 137 } 138 } 139} 140 141async function buildBundle({ entryPoint, outFile, metadata, packageName }) { 142 await mkdir(path.dirname(outFile), { recursive: true }); 143 144 await build({ 145 entryPoints: [entryPoint], 146 outfile: outFile, 147 bundle: true, 148 minify: true, 149 platform: 'browser', 150 format: 'iife', 151 target: ['es2019'], 152 legalComments: 'inline', 153 banner: { 154 js: createBanner(metadata), 155 }, 156 define: { 157 'process.env.NODE_ENV': '"production"', 158 global: 'window', 159 }, 160 logLevel: 'info', 161 }); 162 163 console.log(`Built ${packageName} bundle: ${path.relative(rootDir, outFile)}`); 164} 165 166async function packLintConfig() { 167 if (!(await pathExists(bpmnlintConfigPath))) { 168 throw new Error( 169 `Missing .bpmnlintrc at repo root. It is required to build the linter bundle.` 170 ); 171 } 172 173 await mkdir(generatedDir, { recursive: true }); 174 175 // Produces an ES module exporting { config, resolver, ... } with every rule 176 // implementation inlined, so it can run in the browser without a resolver. 177 const output = await packConfig(bpmnlintConfigPath, 'es'); 178 const banner = '/*! generated from .bpmnlintrc for dokuwiki-plugin-bpmnio — do not edit by hand */\n'; 179 180 await writeFile(packedLintConfigPath, `${banner}${output.code}`); 181 182 console.log(`Packed lint config: ${path.relative(rootDir, packedLintConfigPath)}`); 183} 184 185async function copyLintAssets() { 186 const packageName = 'bpmn-js-bpmnlint'; 187 const sourceDir = path.join(nodeModulesDir, packageName); 188 189 await ensureInstalled(packageName); 190 await cleanPackageOutput(packageName); 191 await copyMetadata(packageName); 192 193 // Only the stylesheet is needed as a committed asset; the JS is bundled into 194 // the viewer/modeler entry points. The .css is renamed to .less so DokuWiki's 195 // LESS pipeline (all.less) can @import it like the other vendor stylesheets. 196 const cssSource = path.join(sourceDir, 'dist', 'assets', 'css', 'bpmn-js-bpmnlint.css'); 197 const cssTarget = path.join( 198 vendorDir, 199 packageName, 200 'dist', 201 'assets', 202 'css', 203 'bpmn-js-bpmnlint.less' 204 ); 205 206 await copyFileEnsuringDir(cssSource, cssTarget); 207 208 console.log(`Copied ${packageName} stylesheet: ${path.relative(rootDir, cssTarget)}`); 209} 210 211async function main() { 212 await packLintConfig(); 213 await copyLintAssets(); 214 215 for (const pkg of packages) { 216 const metadata = await readPackageMetadata(pkg.name); 217 const sourceDir = path.join(nodeModulesDir, pkg.name); 218 const sourceAssetsDir = path.join(sourceDir, pkg.assetsDir); 219 const sourceFontsDir = path.join(sourceDir, pkg.fontsDir); 220 const targetAssetsDir = path.join(vendorDir, pkg.name, pkg.assetsDir); 221 222 await cleanPackageOutput(pkg.name); 223 await copyMetadata(pkg.name); 224 await copyAssets(sourceAssetsDir, targetAssetsDir); 225 await copyFonts(sourceFontsDir, pkg.fontFiles); 226 227 for (const output of pkg.outputs) { 228 await buildBundle({ 229 entryPoint: output.entryPoint, 230 outFile: output.outFile, 231 metadata, 232 packageName: pkg.name, 233 }); 234 } 235 } 236 237 const lintPackages = ['bpmn-js-bpmnlint', 'bpmnlint', 'bpmnlint-pack-config']; 238 239 const generatedMetadata = { 240 generatedAt: new Date().toISOString(), 241 packages: Object.fromEntries( 242 await Promise.all([ 243 ...packages.map(async (pkg) => [pkg.name, (await readPackageMetadata(pkg.name)).version]), 244 ...lintPackages.map(async (name) => [name, (await readPackageMetadata(name)).version]), 245 ]) 246 ), 247 }; 248 249 await writeFile( 250 path.join(vendorDir, 'build-manifest.json'), 251 `${JSON.stringify(generatedMetadata, null, 2)}\n` 252 ); 253} 254 255main().catch((error) => { 256 console.error(error.message); 257 process.exitCode = 1; 258}); 259