2023-03-20 22:24:32 -08:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
2024-07-09 22:14:13 -08:00
|
|
|
import child_process from 'node:child_process';
|
|
|
|
|
import fs from 'node:fs';
|
2025-11-20 21:26:57 -09:00
|
|
|
import { createRequire } from 'node:module';
|
2024-07-09 22:14:13 -08:00
|
|
|
import os from 'node:os';
|
|
|
|
|
import path from 'node:path';
|
|
|
|
|
import process from 'node:process';
|
2023-03-20 22:24:32 -08:00
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
import { getFiles, getTestFiles, all_subdirs } from './files.js';
|
|
|
|
|
|
2026-11-02 02:48:21 -09:00
|
|
|
const srcdir = import.meta.dirname;
|
2023-03-21 07:51:20 -08:00
|
|
|
const production = process.env.NODE_ENV === 'production';
|
2026-10-30 02:34:14 -08:00
|
|
|
const useWasm = os.arch() !== 'x64';
|
2023-03-21 07:51:20 -08:00
|
|
|
|
2023-03-20 22:24:32 -08:00
|
|
|
// ensure node_modules is present and up to date
|
2026-11-02 02:48:21 -09:00
|
|
|
child_process.spawnSync('tools/node-modules', ['make_package_lock_json'], { stdio: 'inherit', cwd: srcdir });
|
2023-03-20 22:24:32 -08:00
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
// List of directories to use when resolving import statements
|
2026-11-02 02:48:21 -09:00
|
|
|
const nodePaths = [`${srcdir}/pkg/lib`];
|
2023-03-21 07:51:20 -08:00
|
|
|
|
2026-11-02 02:59:49 -09:00
|
|
|
const parser = (await import('argparse')).default.ArgumentParser();
|
|
|
|
|
parser.add_argument('-o', '--outdir', { help: "base output directory", default: ".", metavar: "DIR" });
|
|
|
|
|
parser.add_argument('-r', '--rsync', { help: "rsync bundles to ssh target after build", metavar: "HOST" });
|
|
|
|
|
parser.add_argument('-w', '--watch', { action: 'store_true', help: "Enable watch mode" });
|
|
|
|
|
parser.add_argument('onlydir', { nargs: '?', help: "The pkg/<DIRECTORY> to build (eg. base1, shell, ...)", metavar: "DIRECTORY" });
|
|
|
|
|
const args = parser.parse_args();
|
|
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
// context options for distributed pages in dist/
|
|
|
|
|
const pkgOptions = {
|
2023-04-18 01:51:23 -08:00
|
|
|
...!production ? { sourcemap: "linked" } : {},
|
2023-03-21 07:51:20 -08:00
|
|
|
bundle: true,
|
|
|
|
|
external: ['*.woff', '*.woff2', '*.jpg', '*.svg', '../../assets*'], // Allow external font files which live in ../../static/fonts
|
|
|
|
|
legalComments: 'external', // Move all legal comments to a .LEGAL.txt file
|
|
|
|
|
loader: {
|
|
|
|
|
".js": "jsx",
|
|
|
|
|
".py": "text",
|
|
|
|
|
".sh": "text",
|
|
|
|
|
},
|
2025-11-18 10:42:57 -09:00
|
|
|
metafile: true,
|
2023-03-21 07:51:20 -08:00
|
|
|
minify: production,
|
|
|
|
|
nodePaths,
|
2026-11-02 02:48:21 -09:00
|
|
|
outbase: `${srcdir}/pkg`,
|
2026-11-02 02:59:49 -09:00
|
|
|
outdir: `${args.outdir}/dist`,
|
2024-12-05 19:28:26 -09:00
|
|
|
target: ['es2021'],
|
2023-03-21 07:51:20 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// context options for qunit tests in qunit/
|
|
|
|
|
const qunitOptions = {
|
2024-08-27 00:36:40 -08:00
|
|
|
sourcemap: "linked",
|
2023-03-21 07:51:20 -08:00
|
|
|
bundle: true,
|
|
|
|
|
minify: false,
|
|
|
|
|
nodePaths,
|
2026-11-02 02:48:21 -09:00
|
|
|
outbase: `${srcdir}/pkg`,
|
2026-11-02 02:59:49 -09:00
|
|
|
outdir: `${args.outdir}/qunit`,
|
2023-03-21 07:51:20 -08:00
|
|
|
loader: {
|
|
|
|
|
".sh": "text",
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (args.onlydir?.includes('/'))
|
2023-03-20 22:24:32 -08:00
|
|
|
parser.error("Directory must not contain '/'");
|
2023-03-21 07:51:20 -08:00
|
|
|
|
|
|
|
|
if (useWasm && args.watch)
|
|
|
|
|
parser.error("watch mode is not supported with esbuild-wasm");
|
2023-03-20 22:24:32 -08:00
|
|
|
|
|
|
|
|
if (args.onlydir)
|
|
|
|
|
process.env.ONLYDIR = args.onlydir;
|
2023-03-21 07:51:20 -08:00
|
|
|
if (args.rsync)
|
|
|
|
|
process.env.RSYNC = args.rsync;
|
2023-03-20 22:24:32 -08:00
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
// keep cockpit.js as global external, except on base1 (as that's what exports it), and kdump (for testing that bundling works)
|
|
|
|
|
const cockpitJSResolvePlugin = {
|
|
|
|
|
name: 'cockpit-js-resolve',
|
|
|
|
|
setup(build) {
|
|
|
|
|
build.onResolve({ filter: /^cockpit$/ }, args => {
|
|
|
|
|
if (args.resolveDir.endsWith('/base1') || args.resolveDir.endsWith('/kdump'))
|
|
|
|
|
return null;
|
|
|
|
|
return { path: args.path, namespace: 'external-global' };
|
|
|
|
|
});
|
2023-03-20 22:24:32 -08:00
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
build.onLoad({ filter: /.*/, namespace: 'external-global' },
|
|
|
|
|
args => ({ contents: `module.exports = ${args.path}` }));
|
|
|
|
|
},
|
|
|
|
|
};
|
2023-03-20 22:24:32 -08:00
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
// similar to fs.watch(), but recursively watches all subdirectories
|
|
|
|
|
function watch_dirs(dir, on_change) {
|
|
|
|
|
const callback = (ev, dir, fname) => {
|
|
|
|
|
// only listen for "change" events, as renames are noisy
|
|
|
|
|
if (ev !== "change")
|
|
|
|
|
return;
|
|
|
|
|
on_change(path.join(dir, fname));
|
|
|
|
|
};
|
2023-03-20 22:24:32 -08:00
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
fs.watch(dir, {}, (ev, path) => callback(ev, dir, path));
|
2023-03-20 22:24:32 -08:00
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
// watch all subdirectories in dir
|
|
|
|
|
const d = fs.opendirSync(dir);
|
|
|
|
|
let dirent;
|
|
|
|
|
while ((dirent = d.readSync()) !== null) {
|
|
|
|
|
if (dirent.isDirectory())
|
|
|
|
|
watch_dirs(path.join(dir, dirent.name), on_change);
|
2023-03-20 22:24:32 -08:00
|
|
|
}
|
2023-03-21 07:51:20 -08:00
|
|
|
d.closeSync();
|
2023-03-20 22:24:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function build() {
|
|
|
|
|
// dynamic imports which need node_modules
|
2025-11-20 21:26:57 -09:00
|
|
|
const esbuild = await (async () => {
|
|
|
|
|
try {
|
|
|
|
|
// Try node_modules first for installs with devDependencies
|
|
|
|
|
return (await import(useWasm ? 'esbuild-wasm' : 'esbuild')).default;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (e.code !== 'ERR_MODULE_NOT_FOUND')
|
|
|
|
|
throw e;
|
|
|
|
|
|
|
|
|
|
// Fall back to distro package (e.g. Debian's /usr/lib/*/nodejs/esbuild)
|
|
|
|
|
// Use createRequire to leverage Node's module resolution which searches system paths
|
|
|
|
|
// Use require.resolve to find esbuild in system paths, then import it
|
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
|
return (await import(require.resolve('esbuild'))).default;
|
|
|
|
|
}
|
|
|
|
|
})();
|
2023-03-20 22:24:32 -08:00
|
|
|
|
2025-11-20 21:40:35 -09:00
|
|
|
// Check if qunit is available
|
|
|
|
|
const qunitAvailable = await (async () => {
|
|
|
|
|
try {
|
|
|
|
|
await import('qunit');
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
|
|
|
console.log('qunit not found, skipping test builds');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
2025-08-08 04:09:52 -08:00
|
|
|
const sassPlugin = (await import('esbuild-sass-plugin')).sassPlugin;
|
|
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
const cleanPlugin = (await import('./pkg/lib/esbuild-cleanup-plugin.js')).cleanPlugin;
|
|
|
|
|
const cockpitCompressPlugin = (await import('./pkg/lib/esbuild-compress-plugin.js')).cockpitCompressPlugin;
|
|
|
|
|
const cockpitPoEsbuildPlugin = (await import('./pkg/lib/cockpit-po-plugin.js')).cockpitPoEsbuildPlugin;
|
|
|
|
|
const cockpitRsyncEsbuildPlugin = (await import('./pkg/lib/cockpit-rsync-plugin.js')).cockpitRsyncEsbuildPlugin;
|
|
|
|
|
const cockpitTestHtmlPlugin = (await import('./pkg/lib/esbuild-test-html-plugin.js')).cockpitTestHtmlPlugin;
|
2026-05-28 09:09:14 -08:00
|
|
|
const cockpitRemoveRHIconsPlugin = (await import('./pkg/lib/cockpit-remove-rhicons-plugin.js')).cockpitRemoveRHIconsPlugin;
|
2023-03-20 22:24:32 -08:00
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
const { entryPoints, assetFiles, redhat_fonts } = getFiles(args.onlydir);
|
|
|
|
|
const tests = getTestFiles();
|
2026-11-02 02:48:21 -09:00
|
|
|
const testEntryPoints = tests.map(test => `${srcdir}/pkg/${test}`);
|
2023-03-20 22:24:32 -08:00
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
const pkgFirstPlugins = [
|
|
|
|
|
cleanPlugin({ subdir: args.onlydir }),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const pkgPlugins = [
|
|
|
|
|
cockpitJSResolvePlugin,
|
2025-08-08 04:09:52 -08:00
|
|
|
sassPlugin({
|
2026-11-02 02:48:21 -09:00
|
|
|
loadPaths: [...nodePaths, `${srcdir}/node_modules`],
|
2025-08-08 04:09:52 -08:00
|
|
|
filter: /\.scss/,
|
|
|
|
|
quietDeps: true,
|
2026-03-17 02:20:23 -08:00
|
|
|
silenceDeprecations: [
|
|
|
|
|
"if-function" // https://github.com/patternfly/patternfly/issues/8077
|
|
|
|
|
]
|
2025-08-08 04:09:52 -08:00
|
|
|
})
|
2023-03-21 07:51:20 -08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const getTime = () => new Date().toTimeString().split(' ')[0];
|
|
|
|
|
|
|
|
|
|
const pkgLastPlugins = [
|
|
|
|
|
cockpitPoEsbuildPlugin({
|
2026-11-02 05:47:45 -09:00
|
|
|
podir: path.join(srcdir, 'po'),
|
2023-03-21 07:51:20 -08:00
|
|
|
subdirs: args.onlydir ? [args.onlydir] : all_subdirs,
|
|
|
|
|
// login page does not have cockpit.js, but reads window.cockpit_po
|
2026-10-30 02:34:14 -08:00
|
|
|
wrapper: subdir => subdir === "static" ? "window.cockpit_po = PO_DATA;" : undefined,
|
2023-03-21 07:51:20 -08:00
|
|
|
}),
|
2025-08-08 01:13:27 -08:00
|
|
|
|
|
|
|
|
// copy the static asset files determined in ./files.js
|
|
|
|
|
{
|
|
|
|
|
name: 'copy-assets',
|
|
|
|
|
setup(build) {
|
|
|
|
|
build.onEnd(() => {
|
|
|
|
|
for (const file of [...assetFiles, ...redhat_fonts]) {
|
2026-11-02 02:44:14 -09:00
|
|
|
const destDir = path.join(pkgOptions.outdir, file.to);
|
2025-08-08 01:13:27 -08:00
|
|
|
fs.mkdirSync(destDir, { recursive: true });
|
|
|
|
|
fs.copyFileSync(file.from, path.join(destDir, path.basename(file.from)));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2026-05-28 09:09:14 -08:00
|
|
|
// HACK: filter out RH icons until they become default
|
|
|
|
|
// Reduces PF icon byte size by >50% per icon
|
|
|
|
|
cockpitRemoveRHIconsPlugin(),
|
|
|
|
|
|
2023-03-21 07:51:20 -08:00
|
|
|
// cockpit-ws cannot currently serve compressed login page
|
|
|
|
|
...production ? [cockpitCompressPlugin({ subdir: args.onlydir, exclude: /\/static/ })] : [],
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
name: 'notify-end',
|
|
|
|
|
setup(build) {
|
|
|
|
|
build.onEnd(() => console.log(`${getTime()}: Build finished`));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2023-05-25 22:18:55 -08:00
|
|
|
...(args.rsync || process.env.RSYNC)
|
2026-11-02 02:44:14 -09:00
|
|
|
? [cockpitRsyncEsbuildPlugin({ source: `${pkgOptions.outdir}/${args.onlydir || ''}` })]
|
2023-05-25 22:18:55 -08:00
|
|
|
: [],
|
2023-03-21 07:51:20 -08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (useWasm) {
|
|
|
|
|
// build each entry point individually, as otherwise it runs out of memory
|
|
|
|
|
// See https://github.com/evanw/esbuild/issues/3006
|
|
|
|
|
const numEntries = entryPoints.length;
|
|
|
|
|
for (const [index, entryPoint] of entryPoints.entries()) {
|
|
|
|
|
console.log("building", entryPoint);
|
|
|
|
|
const context = await esbuild.context({
|
|
|
|
|
...pkgOptions,
|
|
|
|
|
entryPoints: [entryPoint],
|
|
|
|
|
plugins: [
|
|
|
|
|
...(index === 0 ? pkgFirstPlugins : []),
|
|
|
|
|
...pkgPlugins,
|
|
|
|
|
...(index === numEntries - 1 ? pkgLastPlugins : []),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await context.rebuild();
|
|
|
|
|
context.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// build all tests in one go, they are small enough
|
2025-11-20 21:40:35 -09:00
|
|
|
if (qunitAvailable) {
|
|
|
|
|
console.log("building qunit tests");
|
|
|
|
|
const context = await esbuild.context({
|
|
|
|
|
...qunitOptions,
|
|
|
|
|
entryPoints: testEntryPoints,
|
|
|
|
|
plugins: [
|
|
|
|
|
cockpitTestHtmlPlugin({ testFiles: tests }),
|
|
|
|
|
],
|
|
|
|
|
});
|
2023-03-21 07:51:20 -08:00
|
|
|
|
2025-11-20 21:40:35 -09:00
|
|
|
await context.rebuild();
|
|
|
|
|
context.dispose();
|
|
|
|
|
}
|
2023-03-20 22:24:32 -08:00
|
|
|
} else {
|
2023-03-21 07:51:20 -08:00
|
|
|
// with native esbuild, build everything in one go, that's fastest
|
|
|
|
|
const pkgContext = await esbuild.context({
|
|
|
|
|
...pkgOptions,
|
|
|
|
|
entryPoints,
|
|
|
|
|
plugins: [...pkgFirstPlugins, ...pkgPlugins, ...pkgLastPlugins],
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-20 21:40:35 -09:00
|
|
|
const qunitContext = qunitAvailable
|
|
|
|
|
? await esbuild.context({
|
|
|
|
|
...qunitOptions,
|
|
|
|
|
entryPoints: testEntryPoints,
|
|
|
|
|
plugins: [
|
|
|
|
|
cockpitTestHtmlPlugin({ testFiles: tests }),
|
|
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
: null;
|
2023-03-21 07:51:20 -08:00
|
|
|
|
|
|
|
|
try {
|
2025-11-20 21:40:35 -09:00
|
|
|
const results = await Promise.all([pkgContext.rebuild(), qunitContext?.rebuild()]);
|
2025-11-18 10:42:57 -09:00
|
|
|
// skip metafile and runtime module calculation in watch and onlydir modes
|
|
|
|
|
if (!args.watch && !args.onlydir) {
|
2026-11-02 02:59:49 -09:00
|
|
|
fs.writeFileSync(`${args.outdir}/metafile.json`, JSON.stringify(results[0].metafile));
|
2025-11-18 10:42:57 -09:00
|
|
|
|
|
|
|
|
// Extract bundled npm packages for dependency tracking
|
|
|
|
|
const bundledPackages = new Set();
|
|
|
|
|
for (const inputPath of Object.keys(results[0].metafile.inputs)) {
|
2026-11-02 02:48:21 -09:00
|
|
|
const rel = path.relative(srcdir, inputPath);
|
|
|
|
|
|
2025-11-18 10:42:57 -09:00
|
|
|
// Match paths like node_modules/package-name/ or node_modules/@scope/package-name/
|
2026-11-02 02:48:21 -09:00
|
|
|
const match = rel.match(/^node_modules\/(@[^/]+\/[^/]+|[^/]+)\//);
|
2025-11-18 10:42:57 -09:00
|
|
|
if (match)
|
|
|
|
|
bundledPackages.add(match[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Look up versions from package-lock.json and output simple format
|
2026-11-02 02:48:21 -09:00
|
|
|
const packageLock = JSON.parse(fs.readFileSync(`${srcdir}/package-lock.json`, 'utf8'));
|
2025-11-18 10:42:57 -09:00
|
|
|
const deps = [];
|
|
|
|
|
for (const pkgName of Array.from(bundledPackages).sort()) {
|
|
|
|
|
const lockKey = `node_modules/${pkgName}`;
|
|
|
|
|
const pkgInfo = packageLock.packages?.[lockKey];
|
|
|
|
|
if (pkgInfo?.version)
|
|
|
|
|
deps.push(`${pkgName} ${pkgInfo.version}`);
|
|
|
|
|
else
|
|
|
|
|
console.error(`Warning: Could not find version for ${pkgName}`);
|
|
|
|
|
}
|
2026-11-02 02:59:49 -09:00
|
|
|
fs.writeFileSync(`${args.outdir}/runtime-npm-modules.txt`, deps.join('\n') + '\n');
|
2025-11-18 10:42:57 -09:00
|
|
|
}
|
2023-03-21 07:51:20 -08:00
|
|
|
} catch (e) {
|
2026-06-02 07:56:03 -08:00
|
|
|
const msg = String(e);
|
|
|
|
|
if (!msg.includes("Build failed"))
|
|
|
|
|
console.error("build.js:", msg);
|
2023-03-21 07:51:20 -08:00
|
|
|
if (!args.watch)
|
|
|
|
|
process.exit(1);
|
|
|
|
|
// ignore errors in watch mode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (args.watch) {
|
|
|
|
|
const on_change = async path => {
|
|
|
|
|
console.log("change detected:", path);
|
2025-11-20 21:40:35 -09:00
|
|
|
await Promise.all([pkgContext.cancel(), qunitContext?.cancel()]);
|
2023-03-21 07:51:20 -08:00
|
|
|
try {
|
2025-11-20 21:40:35 -09:00
|
|
|
await Promise.all([pkgContext.rebuild(), qunitContext?.rebuild()]);
|
2023-03-21 07:51:20 -08:00
|
|
|
} catch (e) {} // ignore in watch mode
|
|
|
|
|
};
|
|
|
|
|
|
2026-11-02 02:48:21 -09:00
|
|
|
watch_dirs(`${srcdir}/pkg`, on_change);
|
2023-03-21 07:51:20 -08:00
|
|
|
// wait forever until Control-C
|
|
|
|
|
await new Promise(() => {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pkgContext.dispose();
|
2025-11-20 21:40:35 -09:00
|
|
|
qunitContext?.dispose();
|
2023-03-20 22:24:32 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
build();
|