UNPKG

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