@epublishing/grunt-epublishing 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli.js +161 -66
- package/lib/config-loader.js +40 -3
- package/lib/task-worker.js +75 -0
- package/lib/webpack.config.js +9 -4
- package/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI Implementation
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Main entry point for the build tools.
|
|
5
5
|
* Orchestrates webpack, sass, concat/minify tasks.
|
|
6
|
+
*
|
|
7
|
+
* Heavy modules (webpack, sass-embedded, terser) are lazy-loaded inside
|
|
8
|
+
* their task functions so that subprocess workers only pay for what they use.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
'use strict';
|
|
9
12
|
|
|
13
|
+
const os = require('os');
|
|
10
14
|
const fs = require('fs');
|
|
11
15
|
const path = require('path');
|
|
12
16
|
const { spawn } = require('child_process');
|
|
13
17
|
const { program } = require('commander');
|
|
14
18
|
const chalk = require('chalk');
|
|
15
19
|
const ora = require('ora');
|
|
16
|
-
const Resolver = require('@epublishing/jade-resolver');
|
|
17
20
|
const { loadConfig } = require('./config-loader');
|
|
18
|
-
const { configureWebpack, runWebpack, watchWebpack, splitEntries } = require('./webpack.config');
|
|
19
|
-
const { compileSass } = require('./sass-compiler');
|
|
20
|
-
const { runConcatMinify } = require('./concat-minify');
|
|
21
|
-
const { writeTsConfigs, cleanTsConfigs } = require('./tsconfig-gen');
|
|
22
21
|
|
|
23
22
|
// Package info for banner
|
|
24
23
|
const pkg = require('../package.json');
|
|
@@ -28,7 +27,7 @@ const pkg = require('../package.json');
|
|
|
28
27
|
*/
|
|
29
28
|
function printBanner(options) {
|
|
30
29
|
if (options.noBanner) return;
|
|
31
|
-
|
|
30
|
+
|
|
32
31
|
console.log(chalk.cyan(`
|
|
33
32
|
╔═══════════════════════════════════════════╗
|
|
34
33
|
║ ║
|
|
@@ -43,7 +42,7 @@ function printBanner(options) {
|
|
|
43
42
|
*/
|
|
44
43
|
function printOptions(options) {
|
|
45
44
|
if (options.noBanner) return;
|
|
46
|
-
|
|
45
|
+
|
|
47
46
|
const flags = [];
|
|
48
47
|
if (options.watch) flags.push('watch');
|
|
49
48
|
if (options.noMinify) flags.push('no-minify');
|
|
@@ -51,7 +50,8 @@ function printOptions(options) {
|
|
|
51
50
|
if (options.lint) flags.push('lint');
|
|
52
51
|
if (options.verbose) flags.push('verbose');
|
|
53
52
|
if (options.parallel) flags.push('parallel');
|
|
54
|
-
|
|
53
|
+
if (options.isolateTasks) flags.push('isolate-tasks');
|
|
54
|
+
|
|
55
55
|
if (flags.length > 0) {
|
|
56
56
|
console.log(chalk.gray(` Options: ${flags.join(', ')}\n`));
|
|
57
57
|
}
|
|
@@ -64,16 +64,27 @@ function printOptions(options) {
|
|
|
64
64
|
* @param {Object} options - Build options
|
|
65
65
|
*/
|
|
66
66
|
async function runNpmInstall(config, options) {
|
|
67
|
+
const Resolver = require('@epublishing/jade-resolver');
|
|
67
68
|
const paths = config.paths || {};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// Build paths object with ONLY jade and jade child gem directories (absolute paths)
|
|
70
|
+
const gemPaths = {};
|
|
71
|
+
if (paths.jade && path.isAbsolute(paths.jade)) {
|
|
72
|
+
gemPaths.jade = paths.jade;
|
|
73
|
+
}
|
|
74
|
+
if (paths.jadechild && path.isAbsolute(paths.jadechild)) {
|
|
75
|
+
gemPaths.jadechild = paths.jadechild;
|
|
76
|
+
}
|
|
77
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
78
|
+
if (key.startsWith('jade_') && value && path.isAbsolute(value)) {
|
|
79
|
+
gemPaths[key] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const resolver = new Resolver(gemPaths, { includeSite: false });
|
|
71
83
|
const packages = resolver.find('package.json');
|
|
72
84
|
if (!packages || packages.length === 0) {
|
|
73
85
|
return;
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
const origCwd = process.cwd();
|
|
77
88
|
const dirs = [...new Set(packages.map((p) => path.dirname(p)))];
|
|
78
89
|
|
|
79
90
|
for (const dir of dirs) {
|
|
@@ -84,13 +95,12 @@ async function runNpmInstall(config, options) {
|
|
|
84
95
|
const hasLock = fs.existsSync(path.join(dir, 'package-lock.json'));
|
|
85
96
|
const cmd = hasLock ? 'ci' : 'install';
|
|
86
97
|
await new Promise((resolve, reject) => {
|
|
87
|
-
process.chdir(dir);
|
|
88
98
|
const proc = spawn('npm', [cmd], {
|
|
99
|
+
cwd: dir,
|
|
89
100
|
stdio: options.verbose ? 'inherit' : 'pipe',
|
|
90
101
|
shell: true,
|
|
91
102
|
});
|
|
92
103
|
proc.on('close', (code) => {
|
|
93
|
-
process.chdir(origCwd);
|
|
94
104
|
if (code === 0) {
|
|
95
105
|
spinner.succeed(`npm ${cmd} completed in ${basename}`);
|
|
96
106
|
resolve();
|
|
@@ -100,13 +110,11 @@ async function runNpmInstall(config, options) {
|
|
|
100
110
|
}
|
|
101
111
|
});
|
|
102
112
|
proc.on('error', (err) => {
|
|
103
|
-
process.chdir(origCwd);
|
|
104
113
|
spinner.fail(`npm ${cmd} failed in ${basename}`);
|
|
105
114
|
reject(err);
|
|
106
115
|
});
|
|
107
116
|
});
|
|
108
117
|
} catch (err) {
|
|
109
|
-
process.chdir(origCwd);
|
|
110
118
|
throw err;
|
|
111
119
|
}
|
|
112
120
|
}
|
|
@@ -118,35 +126,36 @@ async function runNpmInstall(config, options) {
|
|
|
118
126
|
* @param {Object} options - Build options
|
|
119
127
|
*/
|
|
120
128
|
async function runWebpackBuild(config, options) {
|
|
129
|
+
const { configureWebpack, runWebpack, splitEntries } = require('./webpack.config');
|
|
121
130
|
const spinner = ora('Building webpack bundles...').start();
|
|
122
|
-
|
|
131
|
+
|
|
123
132
|
try {
|
|
124
133
|
const webpackConfigs = configureWebpack(config, options);
|
|
125
134
|
const targets = Object.keys(webpackConfigs);
|
|
126
|
-
|
|
135
|
+
|
|
127
136
|
if (targets.length === 0) {
|
|
128
137
|
spinner.info('No webpack targets configured');
|
|
129
138
|
return;
|
|
130
139
|
}
|
|
131
|
-
|
|
140
|
+
|
|
132
141
|
for (const targetName of targets) {
|
|
133
142
|
const targetConfig = webpackConfigs[targetName];
|
|
134
|
-
|
|
143
|
+
|
|
135
144
|
// Split entries if there are many (memory optimization)
|
|
136
145
|
if (targetConfig.entry && Object.keys(targetConfig.entry).length > 10 && !options.parallel) {
|
|
137
146
|
const batches = splitEntries(targetConfig.entry, 10);
|
|
138
147
|
spinner.text = `Building ${targetName} (${batches.length} batches)...`;
|
|
139
|
-
|
|
148
|
+
|
|
140
149
|
for (let i = 0; i < batches.length; i++) {
|
|
141
150
|
const batchConfig = { ...targetConfig, entry: batches[i] };
|
|
142
151
|
spinner.text = `Building ${targetName} batch ${i + 1}/${batches.length}...`;
|
|
143
|
-
|
|
152
|
+
|
|
144
153
|
const stats = await runWebpack(batchConfig);
|
|
145
|
-
|
|
154
|
+
|
|
146
155
|
if (options.verbose) {
|
|
147
156
|
console.log(stats.toString({ colors: true }));
|
|
148
157
|
}
|
|
149
|
-
|
|
158
|
+
|
|
150
159
|
// Allow GC between batches
|
|
151
160
|
if (global.gc) {
|
|
152
161
|
global.gc();
|
|
@@ -155,13 +164,13 @@ async function runWebpackBuild(config, options) {
|
|
|
155
164
|
} else {
|
|
156
165
|
spinner.text = `Building ${targetName}...`;
|
|
157
166
|
const stats = await runWebpack(targetConfig);
|
|
158
|
-
|
|
167
|
+
|
|
159
168
|
if (options.verbose) {
|
|
160
169
|
console.log(stats.toString({ colors: true }));
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
}
|
|
164
|
-
|
|
173
|
+
|
|
165
174
|
spinner.succeed(`Webpack: ${targets.length} target(s) built`);
|
|
166
175
|
} catch (error) {
|
|
167
176
|
spinner.fail('Webpack build failed');
|
|
@@ -175,56 +184,57 @@ async function runWebpackBuild(config, options) {
|
|
|
175
184
|
* @param {Object} options - Build options
|
|
176
185
|
*/
|
|
177
186
|
async function runWebpackWatch(config, options) {
|
|
187
|
+
const { configureWebpack, watchWebpack } = require('./webpack.config');
|
|
178
188
|
const webpackConfigs = configureWebpack(config, { ...options, watch: true });
|
|
179
189
|
const targets = Object.keys(webpackConfigs);
|
|
180
|
-
|
|
190
|
+
|
|
181
191
|
if (targets.length === 0) {
|
|
182
192
|
console.log(chalk.yellow('No webpack targets configured'));
|
|
183
193
|
return;
|
|
184
194
|
}
|
|
185
|
-
|
|
195
|
+
|
|
186
196
|
console.log(chalk.cyan('Starting webpack watch mode...'));
|
|
187
|
-
|
|
197
|
+
|
|
188
198
|
const watchers = [];
|
|
189
|
-
|
|
199
|
+
|
|
190
200
|
for (const targetName of targets) {
|
|
191
201
|
const targetConfig = webpackConfigs[targetName];
|
|
192
|
-
|
|
202
|
+
|
|
193
203
|
const watcher = watchWebpack(targetConfig, (err, stats) => {
|
|
194
204
|
if (err) {
|
|
195
205
|
console.error(chalk.red(`[${targetName}] Error:`, err.message));
|
|
196
206
|
return;
|
|
197
207
|
}
|
|
198
|
-
|
|
208
|
+
|
|
199
209
|
const info = stats.toJson();
|
|
200
|
-
|
|
210
|
+
|
|
201
211
|
if (stats.hasErrors()) {
|
|
202
212
|
console.error(chalk.red(`[${targetName}] Errors:`));
|
|
203
213
|
info.errors.forEach(e => console.error(e.message));
|
|
204
214
|
return;
|
|
205
215
|
}
|
|
206
|
-
|
|
216
|
+
|
|
207
217
|
if (stats.hasWarnings() && options.verbose) {
|
|
208
218
|
console.warn(chalk.yellow(`[${targetName}] Warnings:`));
|
|
209
219
|
info.warnings.forEach(w => console.warn(w.message));
|
|
210
220
|
}
|
|
211
|
-
|
|
221
|
+
|
|
212
222
|
console.log(chalk.green(`[${targetName}] Rebuilt in ${info.time}ms`));
|
|
213
223
|
});
|
|
214
|
-
|
|
224
|
+
|
|
215
225
|
watchers.push(watcher);
|
|
216
226
|
}
|
|
217
|
-
|
|
227
|
+
|
|
218
228
|
// Handle process termination
|
|
219
229
|
const cleanup = () => {
|
|
220
230
|
console.log(chalk.yellow('\nStopping watchers...'));
|
|
221
231
|
watchers.forEach(w => w.close());
|
|
222
232
|
process.exit(0);
|
|
223
233
|
};
|
|
224
|
-
|
|
234
|
+
|
|
225
235
|
process.on('SIGINT', cleanup);
|
|
226
236
|
process.on('SIGTERM', cleanup);
|
|
227
|
-
|
|
237
|
+
|
|
228
238
|
// Keep process alive
|
|
229
239
|
await new Promise(() => {});
|
|
230
240
|
}
|
|
@@ -235,17 +245,18 @@ async function runWebpackWatch(config, options) {
|
|
|
235
245
|
* @param {Object} options - Build options
|
|
236
246
|
*/
|
|
237
247
|
async function runSassBuild(config, options) {
|
|
248
|
+
const { compileSass } = require('./sass-compiler');
|
|
238
249
|
const spinner = ora('Compiling Sass...').start();
|
|
239
|
-
|
|
250
|
+
|
|
240
251
|
try {
|
|
241
252
|
const results = await compileSass(config, {
|
|
242
253
|
verbose: options.verbose,
|
|
243
254
|
maxConcurrency: options.parallel ? 4 : 2,
|
|
244
255
|
});
|
|
245
|
-
|
|
256
|
+
|
|
246
257
|
const successful = results.filter(r => r.success).length;
|
|
247
258
|
const failed = results.filter(r => !r.success).length;
|
|
248
|
-
|
|
259
|
+
|
|
249
260
|
if (failed > 0) {
|
|
250
261
|
spinner.warn(`Sass: ${successful} compiled, ${failed} failed`);
|
|
251
262
|
results.filter(r => !r.success).forEach(r => {
|
|
@@ -268,29 +279,30 @@ async function runSassBuild(config, options) {
|
|
|
268
279
|
* @param {Object} options - Build options
|
|
269
280
|
*/
|
|
270
281
|
async function runConcatMinifyBuild(config, options) {
|
|
282
|
+
const { runConcatMinify } = require('./concat-minify');
|
|
271
283
|
const spinner = ora('Concatenating and minifying...').start();
|
|
272
|
-
|
|
284
|
+
|
|
273
285
|
try {
|
|
274
286
|
const results = await runConcatMinify(config, {
|
|
275
287
|
verbose: options.verbose,
|
|
276
288
|
noMinify: options.noMinify,
|
|
277
289
|
});
|
|
278
|
-
|
|
290
|
+
|
|
279
291
|
const concatCount = results.concat.filter(r => r.success).length;
|
|
280
292
|
const minifyCount = results.minify.filter(r => r.success).length;
|
|
281
|
-
|
|
293
|
+
|
|
282
294
|
if (concatCount > 0 || minifyCount > 0) {
|
|
283
295
|
spinner.succeed(`Concat: ${concatCount} bundles, Minify: ${minifyCount} files`);
|
|
284
296
|
} else {
|
|
285
297
|
spinner.info('No concat/minify targets configured');
|
|
286
298
|
}
|
|
287
|
-
|
|
299
|
+
|
|
288
300
|
// Report any failures
|
|
289
301
|
const failures = [
|
|
290
302
|
...results.concat.filter(r => !r.success),
|
|
291
303
|
...results.minify.filter(r => !r.success),
|
|
292
304
|
];
|
|
293
|
-
|
|
305
|
+
|
|
294
306
|
if (failures.length > 0) {
|
|
295
307
|
console.warn(chalk.yellow(' Some targets failed:'));
|
|
296
308
|
failures.forEach(f => {
|
|
@@ -310,30 +322,30 @@ async function runConcatMinifyBuild(config, options) {
|
|
|
310
322
|
*/
|
|
311
323
|
async function runClean(config, options) {
|
|
312
324
|
const spinner = ora('Cleaning...').start();
|
|
313
|
-
|
|
325
|
+
|
|
314
326
|
try {
|
|
315
327
|
const cleanConfig = config.clean || {};
|
|
316
328
|
let cleaned = 0;
|
|
317
|
-
|
|
329
|
+
|
|
318
330
|
for (const [targetName, targetConfig] of Object.entries(cleanConfig)) {
|
|
319
331
|
const src = Array.isArray(targetConfig.src) ? targetConfig.src : [targetConfig.src];
|
|
320
|
-
|
|
332
|
+
|
|
321
333
|
for (const pattern of src) {
|
|
322
334
|
const resolvedPath = path.resolve(config.paths?.site || process.cwd(), pattern);
|
|
323
|
-
|
|
335
|
+
|
|
324
336
|
// Simple glob handling for clean
|
|
325
337
|
if (pattern.includes('*')) {
|
|
326
338
|
// Skip complex globs for now
|
|
327
339
|
continue;
|
|
328
340
|
}
|
|
329
|
-
|
|
341
|
+
|
|
330
342
|
if (fs.existsSync(resolvedPath)) {
|
|
331
343
|
await fs.promises.rm(resolvedPath, { recursive: true, force: true });
|
|
332
344
|
cleaned++;
|
|
333
345
|
}
|
|
334
346
|
}
|
|
335
347
|
}
|
|
336
|
-
|
|
348
|
+
|
|
337
349
|
spinner.succeed(`Cleaned ${cleaned} path(s)`);
|
|
338
350
|
} catch (error) {
|
|
339
351
|
spinner.fail('Clean failed');
|
|
@@ -341,6 +353,60 @@ async function runClean(config, options) {
|
|
|
341
353
|
}
|
|
342
354
|
}
|
|
343
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Run a single build task in an isolated subprocess with a controlled heap.
|
|
358
|
+
* When the subprocess exits, the OS fully reclaims all its memory.
|
|
359
|
+
*
|
|
360
|
+
* @param {string} taskName - Task to run (webpack, sass, concat)
|
|
361
|
+
* @param {Object} configPaths - config.paths object (all strings, serializable)
|
|
362
|
+
* @param {Object} options - Serializable build options
|
|
363
|
+
* @param {number} heapSize - Max heap size in MB for the subprocess
|
|
364
|
+
* @returns {Promise<void>}
|
|
365
|
+
*/
|
|
366
|
+
function runTaskInSubprocess(taskName, configPaths, options, heapSize) {
|
|
367
|
+
return new Promise((resolve, reject) => {
|
|
368
|
+
const tmpFile = path.join(os.tmpdir(), `epb-paths-${process.pid}-${taskName}.json`);
|
|
369
|
+
fs.writeFileSync(tmpFile, JSON.stringify(configPaths));
|
|
370
|
+
|
|
371
|
+
const workerPath = path.join(__dirname, 'task-worker.js');
|
|
372
|
+
const serializableOptions = {
|
|
373
|
+
verbose: options.verbose || false,
|
|
374
|
+
noMinify: options.noMinify || false,
|
|
375
|
+
analyze: options.analyze || false,
|
|
376
|
+
lint: options.lint || false,
|
|
377
|
+
parallel: false, // never parallel inside a subprocess
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const nodeArgs = [
|
|
381
|
+
`--max-old-space-size=${heapSize}`,
|
|
382
|
+
workerPath,
|
|
383
|
+
taskName,
|
|
384
|
+
tmpFile,
|
|
385
|
+
JSON.stringify(serializableOptions),
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
const proc = spawn(process.execPath, nodeArgs, {
|
|
389
|
+
stdio: 'inherit',
|
|
390
|
+
// Clear NODE_OPTIONS so our --max-old-space-size takes effect
|
|
391
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
proc.on('close', (code) => {
|
|
395
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
396
|
+
if (code === 0) {
|
|
397
|
+
resolve();
|
|
398
|
+
} else {
|
|
399
|
+
reject(new Error(`${taskName} subprocess exited with code ${code}`));
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
proc.on('error', (err) => {
|
|
404
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
405
|
+
reject(err);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
344
410
|
/**
|
|
345
411
|
* Run full build
|
|
346
412
|
* @param {Object} config - Build configuration
|
|
@@ -348,9 +414,10 @@ async function runClean(config, options) {
|
|
|
348
414
|
*/
|
|
349
415
|
async function runFullBuild(config, options) {
|
|
350
416
|
const startTime = Date.now();
|
|
351
|
-
|
|
417
|
+
|
|
352
418
|
// Generate tsconfig files first
|
|
353
419
|
try {
|
|
420
|
+
const { writeTsConfigs } = require('./tsconfig-gen');
|
|
354
421
|
const written = await writeTsConfigs(process.cwd());
|
|
355
422
|
if (options.verbose && written.length > 0) {
|
|
356
423
|
console.log(chalk.gray(` Generated ${written.length} tsconfig.json file(s)`));
|
|
@@ -358,9 +425,19 @@ async function runFullBuild(config, options) {
|
|
|
358
425
|
} catch {
|
|
359
426
|
// TSConfig generation is optional, continue on error
|
|
360
427
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
428
|
+
|
|
429
|
+
if (options.isolateTasks) {
|
|
430
|
+
// Subprocess isolation: each task runs in its own Node process.
|
|
431
|
+
// When the subprocess exits, the OS fully reclaims all memory.
|
|
432
|
+
const heapSize = options.heapSize || 512;
|
|
433
|
+
|
|
434
|
+
console.log(chalk.gray(` Running tasks in isolated subprocesses (heap: ${heapSize}MB)\n`));
|
|
435
|
+
|
|
436
|
+
await runTaskInSubprocess('webpack', config.paths, options, heapSize);
|
|
437
|
+
await runTaskInSubprocess('sass', config.paths, options, heapSize);
|
|
438
|
+
await runTaskInSubprocess('concat', config.paths, options, heapSize);
|
|
439
|
+
} else if (options.parallel) {
|
|
440
|
+
// Run tasks in parallel (uses more memory)
|
|
364
441
|
await Promise.all([
|
|
365
442
|
runWebpackBuild(config, options),
|
|
366
443
|
runSassBuild(config, options),
|
|
@@ -370,20 +447,21 @@ async function runFullBuild(config, options) {
|
|
|
370
447
|
// Sequential execution with GC hints between tasks
|
|
371
448
|
await runWebpackBuild(config, options);
|
|
372
449
|
if (global.gc) global.gc();
|
|
373
|
-
|
|
450
|
+
|
|
374
451
|
await runSassBuild(config, options);
|
|
375
452
|
if (global.gc) global.gc();
|
|
376
|
-
|
|
453
|
+
|
|
377
454
|
await runConcatMinifyBuild(config, options);
|
|
378
455
|
}
|
|
379
|
-
|
|
456
|
+
|
|
380
457
|
// Clean tsconfig files after build
|
|
381
458
|
try {
|
|
459
|
+
const { cleanTsConfigs } = require('./tsconfig-gen');
|
|
382
460
|
await cleanTsConfigs(process.cwd());
|
|
383
461
|
} catch {
|
|
384
462
|
// Cleanup is optional
|
|
385
463
|
}
|
|
386
|
-
|
|
464
|
+
|
|
387
465
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
388
466
|
console.log(chalk.green(`\n✓ Build completed in ${duration}s\n`));
|
|
389
467
|
}
|
|
@@ -403,6 +481,8 @@ async function run(argv) {
|
|
|
403
481
|
.option('--lint', 'Run ESLint')
|
|
404
482
|
.option('-v, --verbose', 'Verbose output')
|
|
405
483
|
.option('--parallel', 'Run tasks in parallel (uses more memory)')
|
|
484
|
+
.option('--isolate-tasks', 'Run each task in an isolated subprocess for lower memory usage')
|
|
485
|
+
.option('--heap-size <mb>', 'Max heap size in MB per subprocess (default: 512)', parseInt)
|
|
406
486
|
.option('--no-banner', 'Hide banner')
|
|
407
487
|
.option('--env <env>', 'Set NODE_ENV')
|
|
408
488
|
.option('--gc-between-tasks', 'Force garbage collection between tasks')
|
|
@@ -412,6 +492,9 @@ async function run(argv) {
|
|
|
412
492
|
const options = program.opts();
|
|
413
493
|
const task = program.args[0] || 'all';
|
|
414
494
|
|
|
495
|
+
// Debug: print paths and webpack alias (with -v or DEBUG=1)
|
|
496
|
+
const debug = options.verbose || process.env.DEBUG;
|
|
497
|
+
|
|
415
498
|
// Set NODE_ENV if specified
|
|
416
499
|
if (options.env) {
|
|
417
500
|
process.env.NODE_ENV = options.env;
|
|
@@ -427,7 +510,7 @@ async function run(argv) {
|
|
|
427
510
|
|
|
428
511
|
// Load configuration
|
|
429
512
|
const spinner = ora('Loading configuration...').start();
|
|
430
|
-
|
|
513
|
+
|
|
431
514
|
let config;
|
|
432
515
|
try {
|
|
433
516
|
config = await loadConfig(process.cwd(), { verbose: options.verbose });
|
|
@@ -437,6 +520,15 @@ async function run(argv) {
|
|
|
437
520
|
throw error;
|
|
438
521
|
}
|
|
439
522
|
|
|
523
|
+
if (debug && config.paths) {
|
|
524
|
+
console.log(chalk.gray(' Paths: jade=') + (config.paths.jade || '(none)'));
|
|
525
|
+
console.log(chalk.gray(' Paths: jadechild=') + (config.paths.jadechild || '(none)'));
|
|
526
|
+
const jadeChildKeys = Object.keys(config.paths).filter((k) => k.startsWith('jade_'));
|
|
527
|
+
jadeChildKeys.forEach((k) => {
|
|
528
|
+
console.log(chalk.gray(` Paths: ${k}=`) + config.paths[k]);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
440
532
|
// Install npm packages from jade hierarchy (jade, jadechild) before build
|
|
441
533
|
if (task === 'webpack' || task === 'all') {
|
|
442
534
|
try {
|
|
@@ -468,7 +560,8 @@ async function run(argv) {
|
|
|
468
560
|
await runClean(config, options);
|
|
469
561
|
break;
|
|
470
562
|
|
|
471
|
-
case 'tsconfig':
|
|
563
|
+
case 'tsconfig': {
|
|
564
|
+
const { writeTsConfigs } = require('./tsconfig-gen');
|
|
472
565
|
const tsconfigSpinner = ora('Generating tsconfig.json files...').start();
|
|
473
566
|
try {
|
|
474
567
|
const written = await writeTsConfigs(process.cwd());
|
|
@@ -478,8 +571,10 @@ async function run(argv) {
|
|
|
478
571
|
throw error;
|
|
479
572
|
}
|
|
480
573
|
break;
|
|
574
|
+
}
|
|
481
575
|
|
|
482
|
-
case 'clean-tsconfig':
|
|
576
|
+
case 'clean-tsconfig': {
|
|
577
|
+
const { cleanTsConfigs } = require('./tsconfig-gen');
|
|
483
578
|
const cleanTsconfigSpinner = ora('Cleaning tsconfig.json files...').start();
|
|
484
579
|
try {
|
|
485
580
|
const removed = await cleanTsConfigs(process.cwd());
|
|
@@ -489,6 +584,7 @@ async function run(argv) {
|
|
|
489
584
|
throw error;
|
|
490
585
|
}
|
|
491
586
|
break;
|
|
587
|
+
}
|
|
492
588
|
|
|
493
589
|
case 'all':
|
|
494
590
|
default:
|
|
@@ -513,4 +609,3 @@ module.exports = {
|
|
|
513
609
|
runClean,
|
|
514
610
|
runFullBuild,
|
|
515
611
|
};
|
|
516
|
-
|
package/lib/config-loader.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
+
const { spawn } = require('child_process');
|
|
14
15
|
const _ = require('lodash');
|
|
15
|
-
const getGemPaths = require('@epublishing/get-gem-paths');
|
|
16
16
|
const { resolveConfig, resolveTemplates } = require('./template-resolver');
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -41,6 +41,43 @@ function getFilename(fullPath) {
|
|
|
41
41
|
return path.basename(fullPath);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Get gem paths by running the get-gem-paths Ruby script via bundle exec.
|
|
46
|
+
* This ensures Bundler has loaded the site's Gemfile and gem specs.
|
|
47
|
+
* @param {string} sitePath - Path to the site (with Gemfile)
|
|
48
|
+
* @param {string} pattern - Gem name pattern (e.g. 'jade')
|
|
49
|
+
* @returns {Promise<Array<{name: string, path: string, version: string}>>}
|
|
50
|
+
*/
|
|
51
|
+
function getGemPathsFromBundler(sitePath, pattern = 'jade') {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const rubyScript = path.join(
|
|
54
|
+
path.dirname(require.resolve('@epublishing/get-gem-paths')),
|
|
55
|
+
'scripts',
|
|
56
|
+
'get-gem-paths.rb'
|
|
57
|
+
);
|
|
58
|
+
const proc = spawn('bundle', ['exec', 'ruby', rubyScript, pattern], {
|
|
59
|
+
cwd: sitePath || process.cwd(),
|
|
60
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
|
+
});
|
|
62
|
+
let stdout = '';
|
|
63
|
+
let stderr = '';
|
|
64
|
+
proc.stdout.on('data', (d) => { stdout += d; });
|
|
65
|
+
proc.stderr.on('data', (d) => { stderr += d; });
|
|
66
|
+
proc.on('close', (code) => {
|
|
67
|
+
if (code !== 0) {
|
|
68
|
+
reject(new Error(`get-gem-paths failed (${code}): ${stderr || stdout}`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
resolve(JSON.parse(stdout));
|
|
73
|
+
} catch (e) {
|
|
74
|
+
reject(new Error(`get-gem-paths invalid JSON: ${stdout}`));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
proc.on('error', reject);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
44
81
|
/**
|
|
45
82
|
* Custom merger for lodash merge
|
|
46
83
|
* Handles array merging with JS file override logic
|
|
@@ -127,8 +164,8 @@ async function loadConfig(sitePath = process.cwd(), options = {}) {
|
|
|
127
164
|
const jadeChildPaths = {};
|
|
128
165
|
|
|
129
166
|
try {
|
|
130
|
-
const gems = await
|
|
131
|
-
|
|
167
|
+
const gems = await getGemPathsFromBundler(sitePath, 'jade');
|
|
168
|
+
|
|
132
169
|
for (const gem of gems) {
|
|
133
170
|
if (gem.name === 'jade') {
|
|
134
171
|
jadePath = gem.path;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Task Worker - Isolated subprocess for a single build task.
|
|
4
|
+
*
|
|
5
|
+
* Invoked by the main CLI in --isolate-tasks mode. Each task runs in
|
|
6
|
+
* its own Node process with a controlled heap size (via --max-old-space-size).
|
|
7
|
+
* When the subprocess exits, the OS fully reclaims all its memory.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node --max-old-space-size=512 task-worker.js <task> <paths-file> [options-json]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const taskName = args[0];
|
|
19
|
+
const pathsFile = args[1];
|
|
20
|
+
const optionsJson = args[2] || '{}';
|
|
21
|
+
|
|
22
|
+
if (!taskName || !pathsFile) {
|
|
23
|
+
console.error('Usage: task-worker.js <task> <paths-file> [options-json]');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Read paths (written by the main process after bundle exec discovery)
|
|
28
|
+
const paths = JSON.parse(fs.readFileSync(pathsFile, 'utf8'));
|
|
29
|
+
const options = JSON.parse(optionsJson);
|
|
30
|
+
|
|
31
|
+
// Rebuild config from paths — re-reads grunt-config.js files but skips
|
|
32
|
+
// the expensive `bundle exec` gem-path discovery (already done by parent).
|
|
33
|
+
const { baseConfig, mergeConfig } = require('./config-loader');
|
|
34
|
+
const { resolveTemplates } = require('./template-resolver');
|
|
35
|
+
|
|
36
|
+
let config = JSON.parse(JSON.stringify(baseConfig));
|
|
37
|
+
config.paths = paths;
|
|
38
|
+
|
|
39
|
+
if (paths.jade) config = mergeConfig(config, paths.jade);
|
|
40
|
+
for (const [key, val] of Object.entries(paths)) {
|
|
41
|
+
if (key.startsWith('jade_') && val) config = mergeConfig(config, val);
|
|
42
|
+
}
|
|
43
|
+
if (paths.site) config = mergeConfig(config, paths.site);
|
|
44
|
+
config = resolveTemplates(config, config.paths);
|
|
45
|
+
|
|
46
|
+
// Import only the task function we need (lazy loading keeps memory low)
|
|
47
|
+
const {
|
|
48
|
+
runWebpackBuild,
|
|
49
|
+
runSassBuild,
|
|
50
|
+
runConcatMinifyBuild,
|
|
51
|
+
} = require('./cli');
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
switch (taskName) {
|
|
55
|
+
case 'webpack':
|
|
56
|
+
await runWebpackBuild(config, options);
|
|
57
|
+
break;
|
|
58
|
+
case 'sass':
|
|
59
|
+
await runSassBuild(config, options);
|
|
60
|
+
break;
|
|
61
|
+
case 'concat':
|
|
62
|
+
await runConcatMinifyBuild(config, options);
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
throw new Error(`Unknown task: ${taskName}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main()
|
|
70
|
+
.then(() => process.exit(0))
|
|
71
|
+
.catch((err) => {
|
|
72
|
+
console.error(`[task-worker] ${taskName} failed:`, err.message);
|
|
73
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
package/lib/webpack.config.js
CHANGED
|
@@ -90,11 +90,16 @@ function createWebpackConfig(config, options = {}) {
|
|
|
90
90
|
const resolveAlias = {
|
|
91
91
|
lodash: lodashDir,
|
|
92
92
|
};
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const jadeChildPath = paths.jadechild || (() => {
|
|
94
|
+
const jadeChildKey = Object.keys(paths).find((k) => k.startsWith('jade_') && k !== 'jadechild');
|
|
95
|
+
return jadeChildKey ? paths[jadeChildKey] : null;
|
|
96
|
+
})();
|
|
97
|
+
if (jadeChildPath && typeof jadeChildPath === 'string') {
|
|
98
|
+
const jadeChildJs = path.resolve(jadeChildPath, paths.js_src || 'app/js', 'jade-child.js');
|
|
99
|
+
resolveAlias['jade-child'] = jadeChildJs;
|
|
95
100
|
}
|
|
96
|
-
if (paths.jade) {
|
|
97
|
-
resolveAlias['jade-engine'] = path.
|
|
101
|
+
if (paths.jade && typeof paths.jade === 'string') {
|
|
102
|
+
resolveAlias['jade-engine'] = path.resolve(paths.jade, paths.js_src || 'app/js', 'jade-engine.js');
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
// Build plugins array
|
package/package.json
CHANGED