import { build } from 'esbuild'; import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import packConfigModule from 'bpmnlint-pack-config'; const { packConfig } = packConfigModule; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const nodeModulesDir = path.join(rootDir, 'node_modules'); const vendorDir = path.join(rootDir, 'vendor'); const fontDir = path.join(rootDir, 'font'); const generatedDir = path.join(rootDir, 'build', 'generated'); // .bpmnlintrc is resolved into a self-contained { config, resolver } module at // build time. bpmnlint cannot resolve rule references in the browser, so the // packed module is what the bpmn-viewer / bpmn-modeler entry points import. const bpmnlintConfigPath = path.join(rootDir, '.bpmnlintrc'); const packedLintConfigPath = path.join(generatedDir, 'bpmnlintrc.packed.js'); const packages = [ { name: 'bpmn-js', globalName: 'BpmnJS', assetsDir: path.join('dist', 'assets'), fontsDir: path.join('dist', 'assets', 'bpmn-font', 'font'), fontFiles: ['bpmn.woff', 'bpmn.woff2'], outputs: [ { entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'bpmn-viewer.js'), outFile: path.join(vendorDir, 'bpmn-js', 'dist', 'bpmn-viewer.production.min.js'), }, { entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'bpmn-modeler.js'), outFile: path.join(vendorDir, 'bpmn-js', 'dist', 'bpmn-modeler.production.min.js'), }, ], }, { name: 'dmn-js', globalName: 'DmnJS', assetsDir: path.join('dist', 'assets'), fontsDir: path.join('dist', 'assets', 'dmn-font', 'font'), fontFiles: ['dmn.woff', 'dmn.woff2'], outputs: [ { entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'dmn-viewer.js'), outFile: path.join(vendorDir, 'dmn-js', 'dist', 'dmn-viewer.production.min.js'), }, { entryPoint: path.join(rootDir, 'build', 'vendor-entrypoints', 'dmn-modeler.js'), outFile: path.join(vendorDir, 'dmn-js', 'dist', 'dmn-modeler.production.min.js'), }, ], }, ]; async function pathExists(targetPath) { try { await stat(targetPath); return true; } catch { return false; } } async function ensureInstalled(packageName) { const packagePath = path.join(nodeModulesDir, packageName, 'package.json'); if (!(await pathExists(packagePath))) { throw new Error( `Missing npm dependency ${packageName}. Run npm install before building vendor bundles.` ); } return packagePath; } async function readPackageMetadata(packageName) { const packagePath = await ensureInstalled(packageName); return JSON.parse(await readFile(packagePath, 'utf8')); } function createBanner(metadata) { return `/*! ${metadata.name} - ${metadata.version} | generated for dokuwiki-plugin-bpmnio | ${metadata.license} */`; } async function copyFileEnsuringDir(sourcePath, targetPath) { await mkdir(path.dirname(targetPath), { recursive: true }); await cp(sourcePath, targetPath, { force: true }); } async function copyAssets(sourceDir, targetDir) { await mkdir(targetDir, { recursive: true }); for (const entry of await readdir(sourceDir, { withFileTypes: true })) { const sourcePath = path.join(sourceDir, entry.name); const normalizedPath = sourcePath.split(path.sep).join('/'); if (normalizedPath.includes('/font/')) { continue; } if (entry.isDirectory()) { await copyAssets(sourcePath, path.join(targetDir, entry.name)); continue; } const targetName = entry.name.endsWith('.css') ? entry.name.replace(/\.css$/u, '.less') : entry.name; await copyFileEnsuringDir(sourcePath, path.join(targetDir, targetName)); } } async function copyFonts(sourceDir, files) { await mkdir(fontDir, { recursive: true }); for (const file of files) { await copyFileEnsuringDir(path.join(sourceDir, file), path.join(fontDir, file)); } } async function cleanPackageOutput(packageName) { await rm(path.join(vendorDir, packageName), { recursive: true, force: true }); } async function copyMetadata(packageName) { const sourceDir = path.join(nodeModulesDir, packageName); const targetDir = path.join(vendorDir, packageName); for (const file of ['LICENSE', 'README.md', 'package.json']) { const sourcePath = path.join(sourceDir, file); if (await pathExists(sourcePath)) { await copyFileEnsuringDir(sourcePath, path.join(targetDir, file)); } } } async function buildBundle({ entryPoint, outFile, metadata, packageName }) { await mkdir(path.dirname(outFile), { recursive: true }); await build({ entryPoints: [entryPoint], outfile: outFile, bundle: true, minify: true, platform: 'browser', format: 'iife', target: ['es2019'], legalComments: 'inline', banner: { js: createBanner(metadata), }, define: { 'process.env.NODE_ENV': '"production"', global: 'window', }, logLevel: 'info', }); console.log(`Built ${packageName} bundle: ${path.relative(rootDir, outFile)}`); } async function packLintConfig() { if (!(await pathExists(bpmnlintConfigPath))) { throw new Error( `Missing .bpmnlintrc at repo root. It is required to build the linter bundle.` ); } await mkdir(generatedDir, { recursive: true }); // Produces an ES module exporting { config, resolver, ... } with every rule // implementation inlined, so it can run in the browser without a resolver. const output = await packConfig(bpmnlintConfigPath, 'es'); const banner = '/*! generated from .bpmnlintrc for dokuwiki-plugin-bpmnio — do not edit by hand */\n'; await writeFile(packedLintConfigPath, `${banner}${output.code}`); console.log(`Packed lint config: ${path.relative(rootDir, packedLintConfigPath)}`); } async function copyLintAssets() { const packageName = 'bpmn-js-bpmnlint'; const sourceDir = path.join(nodeModulesDir, packageName); await ensureInstalled(packageName); await cleanPackageOutput(packageName); await copyMetadata(packageName); // Only the stylesheet is needed as a committed asset; the JS is bundled into // the viewer/modeler entry points. The .css is renamed to .less so DokuWiki's // LESS pipeline (all.less) can @import it like the other vendor stylesheets. const cssSource = path.join(sourceDir, 'dist', 'assets', 'css', 'bpmn-js-bpmnlint.css'); const cssTarget = path.join( vendorDir, packageName, 'dist', 'assets', 'css', 'bpmn-js-bpmnlint.less' ); await copyFileEnsuringDir(cssSource, cssTarget); console.log(`Copied ${packageName} stylesheet: ${path.relative(rootDir, cssTarget)}`); } async function main() { await packLintConfig(); await copyLintAssets(); for (const pkg of packages) { const metadata = await readPackageMetadata(pkg.name); const sourceDir = path.join(nodeModulesDir, pkg.name); const sourceAssetsDir = path.join(sourceDir, pkg.assetsDir); const sourceFontsDir = path.join(sourceDir, pkg.fontsDir); const targetAssetsDir = path.join(vendorDir, pkg.name, pkg.assetsDir); await cleanPackageOutput(pkg.name); await copyMetadata(pkg.name); await copyAssets(sourceAssetsDir, targetAssetsDir); await copyFonts(sourceFontsDir, pkg.fontFiles); for (const output of pkg.outputs) { await buildBundle({ entryPoint: output.entryPoint, outFile: output.outFile, metadata, packageName: pkg.name, }); } } const lintPackages = ['bpmn-js-bpmnlint', 'bpmnlint', 'bpmnlint-pack-config']; const generatedMetadata = { generatedAt: new Date().toISOString(), packages: Object.fromEntries( await Promise.all([ ...packages.map(async (pkg) => [pkg.name, (await readPackageMetadata(pkg.name)).version]), ...lintPackages.map(async (name) => [name, (await readPackageMetadata(name)).version]), ]) ), }; await writeFile( path.join(vendorDir, 'build-manifest.json'), `${JSON.stringify(generatedMetadata, null, 2)}\n` ); } main().catch((error) => { console.error(error.message); process.exitCode = 1; });