UNPKG

17.5 kBPlain TextView Raw
1import { glob, saferRemove } from 'backlib';
2import * as chokidar from 'chokidar';
3import * as fs from 'fs-extra';
4import { spawn } from 'p-spawn';
5import * as Path from 'path';
6import { BaseObj } from './base';
7import { pcssFiles, rollupFiles, tmplFiles } from './processors';
8import { asNames, now, Partial, printLog, readJsonFileWithComments } from './utils';
9import { loadVdevConfig } from './vdev-config';
10
11
12// --------- Public Types --------- //
13export interface Block extends BaseObj {
14 name: string;
15 dir: string;
16 webBundles?: WebBundle[];
17 baseDistDir?: string;
18 dbuildDependencies?: string | string[];
19 [key: string]: any;
20}
21
22export interface WebBundle {
23 name: string;
24 /** The entries, support wild cards, to be compiled */
25 entries: string | string[];
26
27 /** To specify glob/files to watch in place of the entries (only used in watch mode, and not for .js/.ts) */
28 watch?: string | string[];
29
30 rollupOptions?: any;
31 dist: string; // distribution file path (from .dir)
32
33
34 type?: string; // set in the buildWebBundles
35 dir?: string; // set in the initWebBundle
36}
37
38export type BlockByName = { [name: string]: Block };
39// --------- /Public Types --------- //
40
41export async function updateVersions(config?: any) {
42 if (!config) {
43 config = await loadVdevConfig();
44 }
45 const versionFiles = config.version?.files as string[] ?? null;
46
47 // if we do not have version files, we skip.
48 if (versionFiles == null) {
49 return;
50
51 }
52 const newVersion = config.__version__;
53
54
55 let firstUpdate = false; // flag that will be set
56 try {
57 for (let file of versionFiles) {
58 const originalContent = (await fs.readFile(file, 'utf8')).toString();
59 const isHTML = file.toLowerCase().endsWith('html');
60
61 let fileAppVersion = getVersion(originalContent, isHTML);
62
63 if (newVersion !== fileAppVersion) {
64 // On the first update needed, we start the log section for the version update
65 if (!firstUpdate) {
66 console.log(`----- Version Update: Updating '__version__ = ".."' or '?v=..' to ${newVersion} `);
67 firstUpdate = true;
68 }
69
70 console.log(`Changing ${fileAppVersion} -> ${newVersion} in file: ${file}`);
71 let newContent = replaceVersion(originalContent, newVersion, isHTML);
72 await fs.writeFile(file, newContent, 'utf8');
73 } else {
74 // Note: for now, we do not log when nothing to do.
75 }
76 }
77 } catch (ex) {
78 throw new Error(`ERROR while doing versioning files - ${ex.message}`);
79 }
80 // if we have at least one update, we close the log section.
81 if (firstUpdate) {
82 console.log('----- /Version Update: done');
83 }
84}
85
86
87// FIXME: Needs to look at the "blocks" from the config
88export async function cleanNodeFiles() {
89 const filesToDelete = ['./package-lock.json', './node_blockules'];
90
91 const blocks = await loadBlocks();
92
93 // dirs.unshift('./'); // we do not clean the base dir, as it is easy to do by hand, and then, scripts won't work
94 // TODO: probably need to add web-server as well
95 for (const block of Object.values(blocks)) {
96 const dir = block.dir;
97 for (let fName of filesToDelete) {
98 const fileToDelete = Path.join(dir, fName);
99 if ((await fs.pathExists(fileToDelete))) {
100 await saferRemove(fileToDelete);
101 }
102 }
103 }
104}
105
106/**
107 *
108 * @param names List of block name or block/bundle names
109 */
110export async function buildBlocksOrBundles(names: string[]) {
111 if (names.length === 0) {
112 const blocks = await loadBlocks();
113 for (let block of Object.values(blocks)) {
114 await _buildBlock(block);
115 }
116 } else {
117 for (let name of names) {
118 const blockAndBundle = name.split('/');
119 // if only .length === 1 then blockAndBundle[1] which is fine
120 await buildBlock(blockAndBundle[0], { onlyBundleName: blockAndBundle[1] });
121 }
122 }
123}
124
125
126// TODO: needs to add support for any block rule watch
127// TODO: needs to add support for only one bundle watch
128export async function watchBlock(blockName: string) {
129 const blocks = await loadBlocks();
130 const block = blocks[blockName];
131
132 const webBundles = (block) ? block.webBundles : null;
133 if (webBundles == null) {
134 throw new Error(`Block ${blockName} not found or does not have a '.webBundles'. As of now, can only watch webBundles`)
135 }
136
137 for (let bundle of webBundles) {
138 await initWebBundle(block, bundle);
139
140 // The rollup have a watch block, so we use it
141 if (bundle.type === 'js' || bundle.type === 'ts') {
142 if (bundle.watch) {
143 console.log(`WARNING - Ignoring '.watch' property for bundle ${bundle.name} for .js/.ts processing
144 as rollup watches dependencies. (advice: remove this .watch property for clarity)`);
145 }
146 await _buildBlock(block, bundle, { watch: true });
147 }
148
149 // Otherwise, we just watch the entries or the watch, and rebuild everything
150 else {
151 await _buildBlock(block, bundle);
152 const toWatch = bundle.watch ?? bundle.entries;
153 let watcher = chokidar.watch(toWatch, { persistent: true });
154 // TODO: Needs to use a call reducer
155 watcher.on('change', async function (filePath: string, stats) {
156 if (filePath.endsWith(`.${bundle.type}`)) {
157 await _buildBlock(block, bundle);
158 }
159 });
160 }
161 }
162}
163
164export interface BuildOptions {
165 watch?: boolean; // for rollup bundles for now
166 full?: boolean; // for maven for including unit test (not implemented yet)
167 onlyBundleName?: string; // A>W
168}
169
170
171const blockBuilt = new Set<string>();
172/**
173 * Build a and block and eventually a bundle
174 * @param blockName
175 * @param opts
176 */
177export async function buildBlock(blockName: string, opts?: BuildOptions) {
178 const blockByName = await loadBlocks();
179 const block = blockByName[blockName];
180
181 if (blockBuilt.has(block.name)) {
182 console.log(`------ Block ${block.name} already built - SKIP\n`);
183 return;
184 }
185
186 if (!block) {
187 throw new Error(`No block found for blockeName ${blockName}.`);
188 }
189
190 let bundle: WebBundle | undefined;
191 const onlyBundleName = opts?.onlyBundleName;
192 if (onlyBundleName && block.webBundles) {
193 bundle = block.webBundles.find((b) => (b.name == onlyBundleName));
194 if (!bundle) {
195 throw new Error(`No webBundle ${onlyBundleName} found in block ${block.name}`);
196 }
197 await initWebBundle(block, bundle);
198 }
199
200 await _buildBlock(block, bundle, opts);
201 blockBuilt.add(block.name);
202}
203
204async function _buildBlock(block: Block, bundle?: WebBundle, opts?: BuildOptions) {
205
206 const hasPomXml = await fs.pathExists(Path.join(block.dir, 'pom.xml'));
207 const hasPackageJson = await fs.pathExists(Path.join(block.dir, 'package.json'));
208 const hasTsConfig = await fs.pathExists(Path.join(block.dir, 'tsconfig.json'));
209 const hasWebBundles = (block.webBundles) ? true : false;
210
211 // Note: if we have a bundleName, then, just the bundle log will be enough.
212 const start = now();
213 if (!bundle) {
214 console.log(`------ Building Block ${block.name} ${block.dir}`);
215 }
216
217 // no matter what, if we have a pckageJson, we make sure we do a npm install
218 if (hasPackageJson) {
219 await npmInstall(block);
220 }
221
222 // if we have a webBundles, we build it
223 if (hasWebBundles) {
224 // TODO: need to allow to give a bundle name to just build it
225 await buildWebBundles(block, bundle, opts);
226 }
227
228 // run tsc (with clean dist/ folder), if it is not a webBundle (assume rollup will take care of the ts when webBundles)
229 if (!hasWebBundles && hasTsConfig) {
230 await buildTsSrc(block);
231 }
232
233 if (hasPomXml) {
234 await runMvn(block, opts ? opts.full : false);
235 }
236
237 if (!bundle) {
238 await printLog(`------ Building Block ${block.name} DONE`, start);
239 console.log();
240 }
241}
242
243
244async function npmInstall(block: Block) {
245 await spawn('npm', ['install'], { cwd: block.dir });
246}
247
248async function buildTsSrc(block: Block) {
249 const distDirNeedsDelete = false;
250
251 const distDir = Path.join(block.dir, '/dist/');
252 const distDirExist = await fs.pathExists(distDir);
253
254 // if we have distDirExist, check that it define as compileOptions.outDir in
255 if (distDirExist) {
256 const tsconfigObj = await readJsonFileWithComments(Path.join(block.dir, 'tsconfig.json'));
257 let outDir = tsconfigObj.compilerOptions.outDir as string | undefined | null;
258 outDir = (outDir) ? Path.join(block.dir, outDir, '/') : null; // add a ending '/' to normalize all of the dir path with ending / (join will remove duplicate)
259 if (outDir === distDir) {
260 console.log(`tsc prep - deleting tsc distDir ${distDir}`);
261 await saferRemove(distDir);
262 } else {
263 console.log(`tss prep - skipping tsc distDir ${distDir} because does not match tsconfig.json compilerOptions.outDir ${outDir}`);
264 }
265 }
266
267 // Assume there is a typescript installed in the root project
268 await spawn('./node_modules/.bin/tsc', ['-p', block.dir]);
269}
270
271async function runMvn(block: Block, full?: boolean) {
272 const args = ['clean', 'package'];
273 if (!full) {
274 args.push('-Dmaven.test.skip=true');
275 }
276
277 var start = now();
278
279 await spawn('mvn', args, {
280 toConsole: false,
281 cwd: block.dir,
282 onStderr: function (data: any) {
283 process.stdout.write(data);
284 }
285 });
286
287 await printLog(`maven build ${full ? 'with test' : ''}`, start);
288}
289
290
291// --------- Private WebBundle Utils --------- //
292type WebBundler = (block: Block, webBundle: WebBundle, opts?: BuildOptions) => void;
293
294// bundlers by type (which is file extension without '.')
295const bundlers: { [type: string]: WebBundler } = {
296 ts: buildTsBundler,
297 pcss: buildPcssBundler,
298 tmpl: buildTmplBundler,
299 html: buildTmplBundler,
300 js: buildJsBundler
301}
302
303async function buildWebBundles(block: Block, onlyBundle?: WebBundle, opts?: BuildOptions) {
304
305 let webBundles: WebBundle[];
306
307 if (onlyBundle) {
308 // the onlyBundle is already initialized
309 webBundles = [onlyBundle];
310 } else {
311 webBundles = block.webBundles!;
312 // we need to initialize the webBundles
313 for (let bundle of webBundles) {
314 await initWebBundle(block, bundle);
315 }
316 }
317
318 for (let bundle of webBundles!) {
319 await ensureDist(bundle);
320 var start = now();
321 await bundlers[bundle.type!](block, bundle, opts);
322 if (opts?.watch) {
323 await printLog(`Starting watch mode for ${block.name}/${bundle.name} (${bundle.dist})`, start);
324 } else {
325 await printLog(`Building bundle ${block.name}/${bundle.name}`, start, bundle.dist);
326 }
327
328 }
329}
330
331const rollupOptionsDefaults = {
332 ts: {
333 watch: false,
334 ts: true,
335 tsconfig: './tsconfig.json'
336 },
337 js: {
338 watch: false,
339 ts: false
340 }
341}
342
343/**
344 * Initialize all of the bundle properties accordingly.
345 * This allow the bundlers and other logic to not have to worry about default values and path resolution.
346 */
347async function initWebBundle(block: Block, bundle: WebBundle) {
348
349 bundle.type = Path.extname(asNames(bundle.entries)[0]).substring(1);
350
351 // for now, just take the block.dir
352 bundle.dir = specialPathResolve('', block.dir, bundle.dir);
353
354 // Make the entries relative to the Block
355 bundle.entries = asNames(bundle.entries).map((f) => specialPathResolve('', bundle.dir!, f));
356
357 // If bundle.watch, same as entries above
358 if (bundle.watch) {
359 // Make the watch relative to the Block
360 bundle.watch = asNames(bundle.watch).map((f) => specialPathResolve('', bundle.dir!, f));
361 }
362
363 // resolve the dist
364 bundle.dist = specialPathResolve('', block.baseDistDir!, bundle.dist);
365
366
367 // --------- rollupOptions initialization --------- //
368 if (bundle.type === 'ts' || bundle.type === 'js') {
369 // get the base default options for this type
370 const rollupOptionsDefault = rollupOptionsDefaults[bundle.type];
371
372 // override default it if bundle has one
373 const rollupOptions = (bundle.rollupOptions) ? { ...rollupOptionsDefault, ...bundle.rollupOptions }
374 : { ...rollupOptionsDefault };
375
376 // resolve tsconfig
377 if (rollupOptions.tsconfig) {
378 rollupOptions.tsconfig = specialPathResolve('', bundle.dir, rollupOptions.tsconfig);
379 }
380
381 // set the new optoins back.
382 bundle.rollupOptions = rollupOptions;
383 }
384 // --------- /rollupOptions initialization --------- //
385
386}
387// --------- /Private WebBundle Utils --------- //
388
389// --------- Private Bundlers --------- //
390async function buildTsBundler(block: Block, bundle: WebBundle, opts?: BuildOptions) {
391 // TODO: need to re-enable watch
392 try {
393 if (opts && opts.watch) {
394 bundle.rollupOptions.watch = true;
395 }
396 // resolve all of the entries (with glob)
397 const allEntries = await resolveGlobs(bundle.entries);
398 await rollupFiles(allEntries, bundle.dist, bundle.rollupOptions);
399 } catch (ex) {
400 // TODO: need to move exception ahndle to the caller
401 console.log("BUILD ERROR - something when wrong on rollup\n\t", ex);
402 console.log("Empty string was save to the app bundle");
403 console.log("Trying saving again...");
404 return;
405 }
406}
407
408async function buildPcssBundler(block: Block, bundle: WebBundle, opts?: BuildOptions) {
409 const allEntries = await resolveGlobs(bundle.entries);
410 await pcssFiles(allEntries, bundle.dist);
411}
412
413async function buildTmplBundler(block: Block, bundle: WebBundle, opts?: BuildOptions) {
414 const allEntries = await resolveGlobs(bundle.entries);
415 await tmplFiles(allEntries, bundle.dist);
416}
417
418async function buildJsBundler(block: Block, bundle: WebBundle, opts?: BuildOptions) {
419 if (opts && opts.watch) {
420 bundle.rollupOptions.watch = true;
421 }
422 const allEntries = await resolveGlobs(bundle.entries);
423 await rollupFiles(allEntries, bundle.dist, bundle.rollupOptions);
424}
425// --------- /Private Bundlers --------- //
426
427// --------- Public Loaders --------- //
428export async function loadDockerBlocks(): Promise<BlockByName> {
429 const blocks = await loadBlocks();
430 const dockerBlocks: BlockByName = {};
431 for (let block of Object.values(blocks)) {
432 const hasDockerfile = await fs.pathExists(Path.join(block.dir, 'Dockerfile'));
433 if (hasDockerfile) {
434 dockerBlocks[block.name] = block;
435 }
436 }
437 return dockerBlocks;
438}
439
440export async function loadBlocks(): Promise<BlockByName> {
441 const rawConfig = await loadVdevConfig();
442
443 const rawBlocks = rawConfig.blocks;
444
445 const base = {
446 system: rawConfig.system,
447 __version__: rawConfig.__version__,
448 imageTag: rawConfig.imageTag
449 }
450
451 // build the services map from the raw services list
452 const blockByName: { [name: string]: Block } = rawBlocks.map((item: string | any) => {
453 let block: Partial<Block>;
454 let name: string;
455
456 // initialize the object
457 if (typeof item === 'string') {
458 block = {
459 name: item
460 }
461 } else {
462 if (!item.name) {
463 throw new Error(`the build config file vdev.yaml has a block without '.name'`);
464 }
465 // we do a shallow clone
466 block = { ...item };
467 }
468
469 //// add the system/version (they should not been set in the block anyway)
470 if (block.system != null || block.__version__ != null) {
471 throw new Error(`.system or .__version__ cannot be set at the block level (but found in ${block.name})`);
472 }
473 Object.assign(block, base);
474
475
476 // if the block does not have a dir, then, build it with the parent one
477 if (!block.dir) {
478 block.dir = Path.join(rawConfig.baseBlockDir, `${block.name}/`);
479 }
480 return block as Block;
481 }).reduce((map: { [key: string]: Block }, block: Block) => {
482 map[block.name] = block;
483 return map;
484 }, {});
485
486 return blockByName;
487}
488
489export async function loadBlock(name: string): Promise<Block> {
490 const blocks = await loadBlocks();
491 const block = blocks[name];
492
493 if (!block) {
494 throw new Error(`Block ${name} not found`);
495 }
496
497 return block;
498}
499
500
501// --------- /Public Loaders --------- //
502
503// --------- Private Utils --------- //
504
505/** Since 0.11.18 each string glob is sorted within their match, but if globs is an array, the order of each result glob result is preserved. */
506async function resolveGlobs(globs: string | string[]) {
507 if (typeof globs === 'string') {
508 return glob(globs);
509 } else {
510 const lists: string[][] = [];
511 for (const globStr of globs) {
512 const list = await glob(globStr);
513 lists.push(list);
514 }
515 return lists.flat();
516 }
517
518}
519
520
521//#region ---------- version Utils ----------
522
523/** Return the first version found. For html, looks for the `src|href=....?v=___` and for other files the version = ... */
524function getVersion(content: string, isHtml = false) {
525 // look for the href or src ?v=...
526 if (isHtml) {
527 const rgx = /<.*(href|src).*?v=(.*?)(\"|\&)/gm;
528 const r = rgx.exec(content);
529 if (r != null && r.length > 2) {
530 return r[2];
531 } else {
532 return null;
533 }
534 }
535 // look for the version = ...
536 else {
537 var rx = new RegExp('__version__' + '\\s*[=:]\\s*[\'"](.*)[\'"]', 'i');
538 var match = content.match(rx);
539 return (match) ? match[1] : null;
540 }
541}
542
543function replaceVersion(content: string, value: string, isHtml = false) {
544 if (isHtml) {
545 const rgxRep = /(<.*(href|src).*?v=).*?(\"|\&.*)/g;
546 // $2 not is not used because it is included as part of $1
547 return content.replace(rgxRep, '$1' + value + '$3');
548 }
549 else {
550 var rx = new RegExp('(.*' + '__version__' + '\\s*[=:]\\s*["\']).*([\'"].*)', 'i');
551 content = content.replace(rx, '$1' + value + '$2');
552 return content;
553 }
554}
555
556//#endregion ---------- /version Utils ----------
557
558async function ensureDist(bundle: WebBundle) {
559 const distDir = Path.dirname(bundle.dist);
560 await fs.ensureDir(distDir);
561}
562
563/**
564 * Special resolve that
565 * - if finalPath null, then, return dir.
566 * - resolve any path to dir if it starts with './' or '../',
567 * - absolute path if it starts with '/'
568 * - baseDir if the path does not start with either '/' or './'
569 * @param baseDir
570 * @param dir
571 * @param finalPath dir or file path
572 */
573export function specialPathResolve(baseDir: string, dir: string, finalPath?: string): string {
574 if (finalPath == null) {
575 return dir;
576 }
577
578 if (finalPath.startsWith('/')) {
579 return finalPath;
580 }
581 if (finalPath.startsWith('./') || finalPath.startsWith('../')) {
582 return Path.join(dir, finalPath)
583 }
584 return Path.join(baseDir, finalPath);
585}
586// --------- /Private Utils --------- //