-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathbuild.js
More file actions
executable file
·309 lines (267 loc) · 11.2 KB
/
build.js
File metadata and controls
executable file
·309 lines (267 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#!/usr/bin/env node
import child_process from 'node:child_process';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { getFiles, getTestFiles, all_subdirs } from './files.js';
const production = process.env.NODE_ENV === 'production';
const useWasm = os.arch() != 'x64';
// ensure node_modules is present and up to date
child_process.spawnSync('tools/node-modules', ['make_package_lock_json'], { stdio: 'inherit' });
// List of directories to use when resolving import statements
const nodePaths = ['pkg/lib'];
// context options for distributed pages in dist/
const pkgOptions = {
...!production ? { sourcemap: "linked" } : {},
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",
},
metafile: true,
minify: production,
nodePaths,
outbase: './pkg',
outdir: "./dist",
target: ['es2021'],
};
// context options for qunit tests in qunit/
const qunitOptions = {
sourcemap: "linked",
bundle: true,
minify: false,
nodePaths,
outbase: './pkg',
outdir: "./qunit",
loader: {
".sh": "text",
},
};
const parser = (await import('argparse')).default.ArgumentParser();
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();
if (args.onlydir?.includes('/'))
parser.error("Directory must not contain '/'");
if (useWasm && args.watch)
parser.error("watch mode is not supported with esbuild-wasm");
if (args.onlydir)
process.env.ONLYDIR = args.onlydir;
if (args.rsync)
process.env.RSYNC = args.rsync;
// 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' };
});
build.onLoad({ filter: /.*/, namespace: 'external-global' },
args => ({ contents: `module.exports = ${args.path}` }));
},
};
// 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));
};
fs.watch(dir, {}, (ev, path) => callback(ev, dir, path));
// 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);
}
d.closeSync();
}
async function build() {
// dynamic imports which need node_modules
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;
}
})();
// 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;
}
})();
const sassPlugin = (await import('esbuild-sass-plugin')).sassPlugin;
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;
const { entryPoints, assetFiles, redhat_fonts } = getFiles(args.onlydir);
const tests = getTestFiles();
const testEntryPoints = tests.map(test => "pkg/" + test);
const pkgFirstPlugins = [
cleanPlugin({ subdir: args.onlydir }),
];
const pkgPlugins = [
cockpitJSResolvePlugin,
sassPlugin({
loadPaths: [...nodePaths, 'node_modules'],
filter: /\.scss/,
quietDeps: true,
silenceDeprecations: [
"if-function" // https://github.com/patternfly/patternfly/issues/8077
]
})
];
const getTime = () => new Date().toTimeString().split(' ')[0];
const pkgLastPlugins = [
cockpitPoEsbuildPlugin({
subdirs: args.onlydir ? [args.onlydir] : all_subdirs,
// login page does not have cockpit.js, but reads window.cockpit_po
wrapper: subdir => subdir == "static" ? "window.cockpit_po = PO_DATA;" : undefined,
}),
// copy the static asset files determined in ./files.js
{
name: 'copy-assets',
setup(build) {
build.onEnd(() => {
for (const file of [...assetFiles, ...redhat_fonts]) {
const destDir = path.join('./dist', file.to);
fs.mkdirSync(destDir, { recursive: true });
fs.copyFileSync(file.from, path.join(destDir, path.basename(file.from)));
}
});
}
},
// 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`));
}
},
...(args.rsync || process.env.RSYNC)
? [cockpitRsyncEsbuildPlugin({ source: "dist/" + (args.onlydir || '') })]
: [],
];
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
if (qunitAvailable) {
console.log("building qunit tests");
const context = await esbuild.context({
...qunitOptions,
entryPoints: testEntryPoints,
plugins: [
cockpitTestHtmlPlugin({ testFiles: tests }),
],
});
await context.rebuild();
context.dispose();
}
} else {
// with native esbuild, build everything in one go, that's fastest
const pkgContext = await esbuild.context({
...pkgOptions,
entryPoints,
plugins: [...pkgFirstPlugins, ...pkgPlugins, ...pkgLastPlugins],
});
const qunitContext = qunitAvailable
? await esbuild.context({
...qunitOptions,
entryPoints: testEntryPoints,
plugins: [
cockpitTestHtmlPlugin({ testFiles: tests }),
],
})
: null;
try {
const results = await Promise.all([pkgContext.rebuild(), qunitContext?.rebuild()]);
// skip metafile and runtime module calculation in watch and onlydir modes
if (!args.watch && !args.onlydir) {
fs.writeFileSync('metafile.json', JSON.stringify(results[0].metafile));
// Extract bundled npm packages for dependency tracking
const bundledPackages = new Set();
for (const inputPath of Object.keys(results[0].metafile.inputs)) {
// Match paths like node_modules/package-name/ or node_modules/@scope/package-name/
const match = inputPath.match(/^node_modules\/(@[^/]+\/[^/]+|[^/]+)\//);
if (match)
bundledPackages.add(match[1]);
}
// Look up versions from package-lock.json and output simple format
const packageLock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
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}`);
}
fs.writeFileSync('runtime-npm-modules.txt', deps.join('\n') + '\n');
}
} catch (e) {
if (!args.watch)
process.exit(1);
// ignore errors in watch mode
}
if (args.watch) {
const on_change = async path => {
console.log("change detected:", path);
await Promise.all([pkgContext.cancel(), qunitContext?.cancel()]);
try {
await Promise.all([pkgContext.rebuild(), qunitContext?.rebuild()]);
} catch (e) {} // ignore in watch mode
};
watch_dirs('pkg', on_change);
// wait forever until Control-C
await new Promise(() => {});
}
pkgContext.dispose();
qunitContext?.dispose();
}
}
build();