@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 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
- const resolver = new Resolver({ ...paths }, { includeSite: false });
69
- resolver.removePath('site');
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
- // Run tasks sequentially for memory optimization (unless --parallel)
363
- if (options.parallel) {
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
-
@@ -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 getGemPaths('jade_?', sitePath);
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
+ });
@@ -90,11 +90,16 @@ function createWebpackConfig(config, options = {}) {
90
90
  const resolveAlias = {
91
91
  lodash: lodashDir,
92
92
  };
93
- if (paths.jadechild) {
94
- resolveAlias['jade-child'] = path.join(paths.jadechild, paths.js_src || 'app/js', 'jade-child.js');
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.join(paths.jade, paths.js_src || 'app/js', 'jade-engine.js');
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@epublishing/grunt-epublishing",
3
3
  "description": "Modern front-end build tools for ePublishing Jade and client sites.",
4
- "version": "1.1.3",
4
+ "version": "1.2.0",
5
5
  "homepage": "https://www.epublishing.com",
6
6
  "contributors": [
7
7
  {