UNPKG

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