Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Sign In

Alorel / personal-build-tools / 1844

23 Dec 2019 - 1:17 coverage increased (+0.3%) to 78.737%
1844

Pull #180

travis-ci-com

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
chore(package): update lockfile yarn.lock
Pull Request #180: Update nyc to the latest version 🚀

382 of 627 branches covered (60.93%)

Branch coverage included in aggregate %.

1488 of 1748 relevant lines covered (85.13%)

37.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.53
/src/commands/build.ts
1
import {SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
2
import * as fs from 'fs';
8×
3
import {LazyGetter} from 'lazy-get-decorator';
8×
4
import {castArray, cloneDeep, has, merge, noop, omit, padStart, set, uniq} from 'lodash';
8×
5
import * as moment from 'moment';
8×
6
import {EOL} from 'os';
8×
7
import {basename, extname, join} from 'path';
8×
8
import {sync as rimraf} from 'rimraf';
8×
9
import {RollupOptions} from 'rollup';
8×
10
import {CommandModule} from 'yargs';
8×
11
import {ext} from '../const/ext';
8×
12
import {addConfig} from '../fns/add-cmd/addConfig';
8×
13
import {cmdName} from '../fns/cmdName';
8×
14
import {execLocal} from '../fns/execLocal';
8×
15
import {getBin} from '../fns/getBin';
8×
16
import {mkTsconfig} from '../fns/mkTsconfig';
8×
17
import {readJson} from '../fns/readJson';
8×
18
import {unlinkSafe} from '../fns/unlinkSafe';
8×
19
import {xSpawnSync} from '../fns/xSpawn';
8×
20
import {allBuildTargets, BuildTarget, isBuildTarget} from '../interfaces/BuildTarget';
8×
21
import {Obj} from '../interfaces/OptionsObject';
8×
22
import {CLISerialiser} from '../lib/cli-serialiser';
8×
23
import {Log} from '../lib/Log';
24
import {tmp} from '../lib/tmp';
8×
25

8×
26
const enum Conf {
8×
27
  TSCONFIG_PREFIX = '.alobuild-tsconfig-',
8×
28
  INDENT = 2,
6×
29
  PAD = 2,
8×
30
  PAD_MS = 3,
8×
31
  PAD_CHAR = '0',
32
  PKG_JSON_PATH = './package.json'
33
}
8×
34

24×
35
const enum Txt {
24×
36
  SKIP_UMD = 'Skipping UMD',
24×
37
  SKIP_FESM5 = 'Skipping FESM5',
38
  SKIP_FESM2015 = 'Skipping FESM2015'
24×
39
}
24×
40

24×
41
interface BuildConf {
24×
42
  entry: string;
43

44
  externals: string[];
18×
45

46
  ignoreUmdExternals: string[];
47

24×
48
  lb: boolean;
24×
49

6×
50
  out: string;
51

18×
52
  rollup: string;
53

54
  skipClean: boolean;
55

56
  skipPackageFields: boolean;
57

58
  targets: BuildTarget[];
59

60
  tsconfig: Obj<any>;
61

62
  umdName: string;
63
}
64

65
const command = cmdName(__filename);
66

67
const defaultUmdName: string = (() => {
68
  try {
69
    return (<any>readJson(Conf.PKG_JSON_PATH)).name;
70
  } catch {
71
    return <any>undefined;
72
  }
73
})();
18×
UNCOV
74
const tscBin = getBin('typescript', 'tsc');
Branches [[2, 0], [3, 0]] missed. !
75
const rollupCmdFile = require.resolve(`../lib/build/cmd.${ext}`);
18×
76

18×
77
const cmd: CommandModule<BuildConf, BuildConf> = {
78
  builder(argv): any {
18×
79
    return addConfig(argv, command)
80
      .option('entry', {
81
        alias: 'e',
82
        default: 'src/index.ts',
83
        describe: 'Entry file',
84
        type: 'string'
85
      })
86
      .option('externals', {
87
        alias: 'x',
88
        coerce: castArray,
89
        default: [],
90
        describe: 'External dependencies',
91
        type: 'array'
92
      })
93
      .option('skip-clean', {
94
        default: false,
95
        describe: 'Don\'t clean the output directory before building',
96
        type: 'boolean'
97
      })
98
      .option('ignore-umd-externals', {
99
        alias: 'iux',
100
        coerce(v: string[]): string[] {
101
          let out = ['tslib'];
102

103
          if (v && v.length) {
104
            out.push(...v);
105

106
            out = uniq(out);
107
          }
108

109
          return out;
110
        },
111
        default: ['tslib'],
112
        describe: 'Override the externals option and always bundle the following packages in UMD',
113
        type: 'array'
114
      })
115
      .option('license-banner', {
116
        alias: 'lb',
117
        default: false,
118
        describe: 'Include the license as a banner in FESM & UMD bundles',
119
        type: 'boolean'
120
      })
121
      .option('skip-package-fields', {
122
        default: false,
123
        describe: 'Skip generating package mainFields',
124
        type: 'boolean'
18×
125
      })
UNCOV
126
      .option('out', {
!
127
        alias: 'o',
18×
128
        default: 'dist',
Branches [[4, 0]] missed. 18×
129
        describe: 'Output directory',
18×
130
        type: 'string'
131
      })
18×
132
      .option('rollup', {
18×
133
        alias: 'r',
18×
134
        default: 'rollup.config.js',
135
        describe: 'Path ro rollup config file',
136
        type: 'string'
18×
137
      })
138
      .option('targets', {
139
        alias: 't',
140
        choices: allBuildTargets,
141
        coerce: castArray,
142
        default: allBuildTargets,
143
        describe: 'Build targets',
144
        type: 'array'
145
      })
18×
146
      .option('umd-name', {
18×
147
        alias: 'u',
Branches [[6, 0]] missed. 2×
148
        default: defaultUmdName,
16×
149
        describe: 'UMD global variable name',
150
        type: 'string'
151
      })
16×
152
      .option('tsconfig', {
16×
153
        alias: 'ts',
18×
154
        coerce(path: string): any {
UNCOV
155
          let out: any = {};
!
UNCOV
156
          let last: any;
!
157

18×
158
          do {
18×
UNCOV
159
            last = readJson(path);
!
160
            if (!last) {
18×
UNCOV
161
              throw new Error(`Path not found: ${path}`);
!
162
            }
163
            out = omit(merge(cloneDeep(last), out), ['extends']);
164
            if (last.extends) {
18×
165
              path = last.extends;
84×
UNCOV
166
            }
!
167
          } while (last.extends);
168

18×
169
          return out;
170
        },
171
        describe: 'Path to tsconfig file to inherit from',
18×
172
        type: 'string'
18×
173
      });
174
  },
175
  command,
18×
176
  describe: 'Build the project',
177
  handler(c) {
178
    const start = Date.now();
179
    validate(c);
180
    if (c.skipClean) {
Branches [[8, 0], [9, 0]] missed. 18×
181
      Log.info(`Skipping cleaning ${c.out}`);
4×
182
    } else {
183
      Log.info(`Clearing ${c.out}`);
4×
184
      rimraf(c.out);
8×
185
      Log.success(`${c.out} cleared.`);
186
    }
8×
187
    const tmpTsconfigs: string[] = [];
Branches [[11, 0], [11, 1]] missed. 4×
188
    try {
4×
189
      buildCJsOrDeclaration(c, tmpTsconfigs);
190
      buildESM(c, tmpTsconfigs);
191
      buildRollup(c);
192
      if (c.skipPackageFields) {
UNCOV
193
        Log.info('Skipping package.json mainFields');
!
UNCOV
194
      } else {
!
195
        Log.info('Writing package.json');
14×
196
        new PackageJsonBuilder(c).write();
18×
197
        Log.success('Wrote package.json');
Branches [[12, 0], [12, 1]] missed. 8×
198
      }
8×
199
      Log.success(`Build finished in ${getDuration(start)}`);
8×
200
    } catch (e) {
8×
201
      Log.err(`Build errored in ${getDuration(start)}`);
10×
202

10×
203
      throw e;
204
    } finally {
205
      tmpTsconfigs.forEach(unlinkSafe);
206
    }
207
  }
208
};
209

210
function validate(c: BuildConf): void {
211
  if (c.targets.includes(BuildTarget.UMD) && !c.umdName) {
10×
212
    throw new Error('umd-name is required when one of the targets is UMD');
213
  }
214
  if (!c.targets.length) {
10×
215
    throw new Error('No targets specified');
10×
216
  }
217
  for (const t of c.targets) {
UNCOV
218
    if (!isBuildTarget(t)) {
!
219
      throw new Error(`Unknown build target: ${JSON.stringify(t)}`);
UNCOV
220
    }
Branches [[15, 1], [16, 2]] missed. !
221
  }
10×
222
}
10×
223

4×
224
function buildRollup(c: BuildConf): void {
10×
225
  const incUMD = c.targets.includes(BuildTarget.UMD);
226
  const incFESM5 = c.targets.includes(BuildTarget.FESM5);
10×
227
  const incFESM2015 = c.targets.includes(BuildTarget.FESM2015);
10×
228
  const banner: string | null = (() => {
10×
229
    if (c.lb) {
Branches [[17, 0]] missed. 10×
230
      let e: Error = <any>null;
8×
231
      for (const p of ['LICENSE', 'LICENSE.txt']) {
2×
232
        try {
10×
233
          const contents = fs.readFileSync(p, 'utf8');
234

235
          return [
10×
236
            '/*!',
237
            contents,
238
            '*/',
Branches [[21, 1]] missed. 10×
239
            '',
10×
240
            ''
241
          ].join(EOL);
!
242
        } catch (err) {
10×
243
          e = err;
6×
244
        }
245
      }
246
      if (e) {
247
        Log.warn(`Unable to set license banner: ${e.stack || e.toString() || e.message}`);
4×
248
      }
249
    }
250

251
    return null;
252
  })();
253

254
  if (!incUMD && !incFESM5 && !incFESM2015) {
255
    Log.info(Txt.SKIP_UMD);
256
    Log.info(Txt.SKIP_FESM5);
257
    Log.info(Txt.SKIP_FESM2015);
Branches [[22, 1]] missed. 10×
UNCOV
258

!
259
    return;
260
  }
UNCOV
261

!
262
  const stdConf = merge({output: {sourcemap: true}}, loadRollupConfig(c));
263
  stdConf.input = c.entry;
Branches [[23, 1]] missed. 16×
264
  stdConf.plugins = [];
16×
265
  if (c.externals && c.externals.length) {
Branches [[25, 1]] missed. 16×
266
    if (Array.isArray(stdConf.external)) {
16×
267
      stdConf.external.unshift(...c.externals);
268
      stdConf.external = uniq(c.externals);
269
    } else {
16×
270
      stdConf.external = c.externals;
271
    }
16×
272
  }
4×
273
  if (banner && !has(stdConf, 'output.banner')) {
274
    set(stdConf, 'output.banner', banner);
275
  }
12×
276

277
  set(stdConf, 'output.name', c.umdName);
16×
278
  set(stdConf, 'output.amd.id', (<any>readJson(Conf.PKG_JSON_PATH)).name);
279

280
  const execOpts: SpawnSyncOptions = {
12×
281
    cwd: process.cwd(),
4×
282
    stdio: 'inherit'
283
  };
284

285
  const stdArgs: string[] = [
286
    '--opts',
16×
287
    CLISerialiser.serialise(stdConf),
14×
288
    '--tsconfig',
289
    CLISerialiser.serialise(c.tsconfig),
290
    '--ignored-externals',
2×
291
    CLISerialiser.serialise(c.ignoreUmdExternals),
16×
292
    '--dist',
293
    c.out
294
  ];
6×
295

296
  if (incFESM2015) {
297
    throwIfErrored(execLocal(
10×
298
      rollupCmdFile,
16×
299
      stdArgs.concat('--formats', BuildTarget.FESM2015),
300
      execOpts
8×
301
    ));
302
  } else {
303
    Log.info(Txt.SKIP_FESM2015);
8×
304
  }
16×
305
  if (incFESM5 || incUMD) {
306
    const formats: any[] = [];
4×
307

308
    if (incFESM5) {
309
      formats.push(BuildTarget.FESM5);
12×
310
    } else {
16×
311
      Log.info(Txt.SKIP_FESM5);
312
    }
16×
313

314
    if (incUMD) {
UNCOV
315
      formats.push(BuildTarget.UMD);
!
316
    } else {
!
317
      Log.info(Txt.SKIP_UMD);
318
    }
!
319

320
    throwIfErrored(execLocal(
321
      rollupCmdFile,
Branches [[32, 1]] missed. 16×
322
      stdArgs.concat('--formats', ...formats),
16×
323
      execOpts
324
    ));
10×
325
  } else {
326
    Log.info(Txt.SKIP_FESM5);
327
    Log.info(Txt.SKIP_UMD);
Branches [[33, 0], [33, 1]] missed. 6×
328
  }
16×
329
}
330

144×
331
class PackageJsonBuilder {
332
  private readonly pkgJson: Obj<any>;
333

88×
334
  public constructor(private readonly cfg: BuildConf) {
56×
335
    this.pkgJson = readJson(Conf.PKG_JSON_PATH) || {};
336
  }
16×
337

10×
338
  @LazyGetter()
339
  private get _base(): string {
340
    const ext$ = extname(this.cfg.entry);
6×
341

342
    return basename(this.cfg.entry, ext$);
343
  }
344

16×
345
  @LazyGetter()
346
  private get _baseJS(): string {
347
    return this._base + '.js';
16×
348
  }
2×
349

350
  @LazyGetter()
2×
351
  private get browser(): string | null {
352
    if (this.cfg.targets.includes(BuildTarget.UMD)) {
353
      return '_bundle/umd.js';
2×
354
    }
Branches [[38, 1]] missed. 2×
355

2×
356
    return null;
357
  }
358

2×
359
  @LazyGetter()
360
  //@ts-ignore
361
  private get esm2015(): string | null {
Branches [[39, 0], [39, 1]] missed. 2×
362
    if (this.cfg.targets.includes(BuildTarget.ESM2015)) {
2×
363
      return `_bundle/esm2015/${this._baseJS}`;
364
    }
365

2×
366
    return this.fesm5;
367
  }
2×
368

2×
369
  @LazyGetter()
370
  //@ts-ignore
371
  private get esm5(): string | null {
16×
372
    if (this.cfg.targets.includes(BuildTarget.ESM5)) {
373
      return `_bundle/esm5/${this._baseJS}`;
374
    }
375

376
    return this.fesm5;
16×
377
  }
378

379
  @LazyGetter()
380
  //@ts-ignore
381
  private get fesm2015(): string | null {
16×
382
    if (this.cfg.targets.includes(BuildTarget.FESM2015)) {
383
      return '_bundle/fesm2015.js';
384
    }
385

386
    return null;
16×
387
  }
388

389
  @LazyGetter()
390
  //@ts-ignore
391
  private get fesm5(): string | null {
6×
392
    if (this.cfg.targets.includes(BuildTarget.FESM5)) {
393
      return '_bundle/fesm5.js';
394
    }
395

396
    return null;
6×
397
  }
398

399
  @LazyGetter()
400
  //@ts-ignore
401
  private get jsdelivr(): string | null {
6×
402
    if (this.browser) {
403
      return `_bundle/umd.min.js`;
404
    }
405

406
    return null;
6×
407
  }
408

409
  @LazyGetter()
410
  //@ts-ignore
411
  private get main(): string | null {
6×
412
    if (this.cfg.targets.includes(BuildTarget.CJS)) {
413
      return this._baseJS;
414
    } else if (this.browser) {
415
      return this.browser;
416
    } else {
6×
417
      throw new Error('Unable to resolve main file');
418
    }
419
  }
420

421
  @LazyGetter()
6×
422
  //@ts-ignore
423
  private get module(): string | null {
424
    return this.fesm5 || this.esm5;
425
  }
426

UNCOV
427
  @LazyGetter()
!
428
  //@ts-ignore
10×
429
  private get types(): string | null {
18×
430
    if (this.cfg.targets.includes(BuildTarget.DECLARATION)) {
16×
431
      return `${this._base}.d.ts`;
16×
432
    }
16×
433

2×
434
    return null;
18×
435
  }
14×
436

14×
437
  public write(): void {
438
    for (const p of ['main', 'browser', 'jsdelivr', 'fesm5', 'esm5', 'fesm2015', 'esm2015', 'types', 'module']) {
439
      if (this[p]) {
14×
440
        this.pkgJson[p] = this[p];
441
      } else {
442
        delete this.pkgJson[p];
443
      }
4×
444
    }
445
    if (this.types) {
446
      this.pkgJson.typings = this.types;
447
    } else {
448
      delete this.pkgJson.typings;
449
    }
450

18×
451
    fs.writeFileSync(Conf.PKG_JSON_PATH, JSON.stringify(this.pkgJson, null, Conf.INDENT));
452
    this.write = noop;
453
  }
Branches [[42, 1]] missed. 18×
454
}
18×
455

6×
456
function loadRollupConfig(c: BuildConf): RollupOptions {
457
  try {
458
    if (c.rollup) {
459
      const fullPath = join(process.cwd(), c.rollup);
460
      const contents = fs.readFileSync(fullPath, 'utf8');
461
      const reg = /export\s+default/g;
462
      if (reg.test(contents)) {
463
        try {
18×
464
          const newContents = contents.replace(reg, 'module.exports = ');
465
          fs.writeFileSync(fullPath, newContents);
466

18×
467
          return cloneDeep(require(fullPath));
468
        } finally {
18×
469
          fs.writeFileSync(fullPath, contents);
18×
UNCOV
470
        }
!
471
      } else {
472
        return cloneDeep(require(fullPath));
473
      }
474
    }
475
  } catch {
476
    //noop
477
  }
UNCOV
478

!
479
  return <any>{};
480
}
UNCOV
481

!
482
function buildESM(c: BuildConf, tmpTsConfigs: string[]): void {
483
  if (c.targets.includes(BuildTarget.ESM5)) {
484
    Log.info('Building ESM5');
UNCOV
485
    spawnTsc(makeTmpTsconfig(c, tmpTsConfigs, {
!
UNCOV
486
      compilerOptions: {
!
UNCOV
487
        declaration: false,
Branches [[45, 0], [45, 1]] missed. !
UNCOV
488
        module: 'es2015',
!
489
        outDir: join(c.out, '_bundle', 'esm5'),
490
        target: 'es5'
Branches [[46, 1]] missed. 48×
491
      }
48×
492
    }));
48×
493
    Log.success('Built ESM5');
494
  } else {
495
    Log.info('Skipping ESM5');
496
  }
497

498
  if (c.targets.includes(BuildTarget.ESM2015)) {
499
    Log.info('Building ESM2015');
48×
500
    spawnTsc(makeTmpTsconfig(c, tmpTsConfigs, {
501
      compilerOptions: {
Branches [[47, 0]] missed. 48×
502
        declaration: false,
48×
503
        module: 'es2015',
48×
504
        outDir: join(c.out, '_bundle', 'esm2015'),
48×
505
        target: 'esnext'
506
      }
507
    }));
508
    Log.success('Built ESM2015');
509
  } else {
510
    Log.info('Skipping ESM2015');
511
  }
512
}
513

66×
514
function buildCJsOrDeclaration(c: BuildConf, tmpTsconfigs: string[]): void {
515
  const needsDeclaration = c.targets.includes(BuildTarget.DECLARATION);
UNCOV
516

!
517
  if (c.targets.includes(BuildTarget.CJS)) {
18×
518
    if (!needsDeclaration) {
519
      Log.info('Skipping declaration');
520
    }
521
    const building = needsDeclaration ? 'CommonJS + declaration' : 'CommonJS';
18×
522
    Log.info(`Building ${building}`);
523

524
    spawnTsc(makeTmpTsconfig(c, tmpTsconfigs, {
525
      compilerOptions: {
526
        declaration: needsDeclaration,
Branches [[48, 1]] missed. 2×
527
        module: 'commonjs',
42×
528
        outDir: c.out
2×
UNCOV
529
      }
!
UNCOV
530
    }));
!
531

532
    Log.success(`Built ${building}`);
533
  } else if (needsDeclaration) {
!
534
    Log.info('Skipping commonjs');
!
535
    Log.info('Building declaration');
536

UNCOV
537
    spawnTsc(makeTmpTsconfig(c, tmpTsconfigs, {
Branches [[49, 0], [49, 1]] missed. !
UNCOV
538
      compilerOptions: {
!
539
        declaration: true,
540
        emitDeclarationOnly: true,
541
        module: 'commonjs',
UNCOV
542
        outDir: c.out,
!
UNCOV
543
        sourceMap: false
!
544
      }
545
    }));
546

547
    Log.success('Built declaration');
548
  } else {
549
    Log.info('Skipping commonjs');
550
    Log.info('Skipping declaration');
6×
551
  }
Branches [[50, 1]] missed. 126×
552
}
553

6×
554
function makeTmpTsconfig(c: BuildConf, cleanupArray: string[], overrides?: any): string {
555
  const path = tmp.fileSync({
556
    dir: process.cwd(),
557
    postfix: '.json',
558
    prefix: <string>Conf.TSCONFIG_PREFIX
559
  }).name;
560
  const tsconfig = mkTsconfig(merge(cloneDeep(c.tsconfig), overrides || {}));
561
  delete tsconfig.compilerOptions.outFile;
562
  fs.writeFileSync(path, JSON.stringify(tsconfig, null, <number>Conf.INDENT));
563
  cleanupArray.push(path);
564

565
  return path;
566
}
567

568
function spawnTsc(tsconfigPath: string): void {
569
  const proc = xSpawnSync(process.execPath, [tscBin, '-p', tsconfigPath], {stdio: 'inherit'});
570
  throwIfErrored(proc);
571
}
572

573
function throwIfErrored(res: SpawnSyncReturns<any>): void {
574
  if (res.status !== 0) {
575
    throw new Error(`Process exited with code ${res.status}`);
576
  }
577
}
578

579
function getDuration(startPoint: number): string {
580
  const d = moment.duration(Date.now() - startPoint);
581

582
  return [
583
    d.hours().toString(),
584
    padStart(d.minutes().toString(), Conf.PAD, Conf.PAD_CHAR),
585
    padStart(d.seconds().toString(), Conf.PAD, Conf.PAD_CHAR)
586
  ].join(':') + `.${padStart(d.milliseconds().toString(), Conf.PAD_MS, Conf.PAD_CHAR)}`;
587
}
588

589
// Cleanup on startup
590
fs.readdirSync(process.cwd(), 'utf8')
591
  .filter(p => p.startsWith(Conf.TSCONFIG_PREFIX) && p.endsWith('.json'))
592
  .forEach(unlinkSafe);
593

594
//tslint:disable:max-file-line-count
595

596
export = cmd;
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
BLOG · TWITTER · Legal & Privacy · Supported CI Services · What's a CI service? · Automated Testing

© 2022 Coveralls, Inc