xref: /plugin/bpmnio/build/build-vendor.mjs (revision c88bd154bd573c8ceefeb9b009eba97536aec54c)
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