UNPKG

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