@vamship/build-utils 1.0.0-0

Sign up to get free protection for your applications and to get access to all the features.
package/src/project.js ADDED
@@ -0,0 +1,564 @@
1
+ 'use strict';
2
+
3
+ const _camelcase = require('camelcase');
4
+ const Directory = require('./directory');
5
+ const _dotEnv = require('dotenv');
6
+ const _dotEnvExpand = require('dotenv-expand');
7
+ const _fs = require('fs');
8
+
9
+ const SUPPORTED_PROJECT_TYPES = [
10
+ 'lib',
11
+ 'cli',
12
+ 'api',
13
+ 'aws-microservice',
14
+ 'container',
15
+ ];
16
+ const SUPPORTED_LANGUAGES = ['js', 'ts'];
17
+
18
+ /**
19
+ * Represents project configuration. This class will encapsulate information
20
+ * about projects that should help automate the build/test/deploy toolchain for
21
+ * a project.
22
+ */
23
+ module.exports = class Project {
24
+ /**
25
+ * @param {Object} packageConfig Reference to the project configuration.
26
+ * This is typically the contents of package.json, with an additional
27
+ * set of properties called `buildMetadata`.
28
+ * @param {Object} [buildMetadata] An optional build metadata object that
29
+ * will override the build metadata defined within packageConfig.
30
+ */
31
+ constructor(packageConfig, buildMetadata) {
32
+ if (!packageConfig || typeof packageConfig !== 'object') {
33
+ throw new Error('Invalid packageConfig (arg #1)');
34
+ }
35
+
36
+ const config = Object.assign({}, packageConfig);
37
+ config.buildMetadata = Object.assign(
38
+ {},
39
+ config.buildMetadata,
40
+ buildMetadata
41
+ );
42
+
43
+ this._name = config.name;
44
+ this._license = config.license;
45
+ this._keywords = (config.keywords || []).slice();
46
+ this._unscopedName = config.name.replace(/^@[^/]*\//, '');
47
+ this._snakeCasedName = config.name
48
+ .replace(/^@/, '')
49
+ .replace(/\//g, '-');
50
+ this._version = config.version;
51
+ this._description = config.description;
52
+ this._initProjectProperties(config.buildMetadata);
53
+
54
+ const tree = {
55
+ src: null,
56
+ test: {
57
+ unit: null,
58
+ api: null,
59
+ },
60
+ infra: null,
61
+ working: {
62
+ src: null,
63
+ test: {
64
+ unit: null,
65
+ api: null,
66
+ },
67
+ infra: null,
68
+ node_modules: null,
69
+ },
70
+ dist: null,
71
+ docs: null,
72
+ node_modules: null,
73
+ coverage: null,
74
+ '.gulp': null,
75
+ '.tscache': null,
76
+ logs: null,
77
+ 'cdk.out': null,
78
+ };
79
+
80
+ if (this._hasExportedTypes) {
81
+ let rootParentDir = tree;
82
+ let workingParentDir = tree.working;
83
+
84
+ const exportedTypesDirs = this._exportedTypes.split('/');
85
+ const lastIndex = exportedTypesDirs.length - 1;
86
+
87
+ exportedTypesDirs.forEach((dirName, index) => {
88
+ const isLastIndex = index === lastIndex;
89
+
90
+ if (!rootParentDir[dirName]) {
91
+ rootParentDir[dirName] = isLastIndex ? null : {};
92
+ }
93
+ rootParentDir = rootParentDir[dirName];
94
+
95
+ if (!workingParentDir[dirName]) {
96
+ workingParentDir[dirName] = isLastIndex ? null : {};
97
+ }
98
+ workingParentDir = workingParentDir[dirName];
99
+ });
100
+ }
101
+
102
+ this._rootDir = Directory.createTree('./', tree);
103
+ }
104
+
105
+ /**
106
+ * Initializes project properties using values from a metadata object.
107
+ *
108
+ * @private
109
+ * @param {Object} buildMetadata The metadata to use when initializing
110
+ * properties.
111
+ */
112
+ _initProjectProperties(buildMetadata) {
113
+ if (!buildMetadata || typeof buildMetadata !== 'object') {
114
+ throw new Error('Invalid buildMetadata (arg #1)');
115
+ }
116
+
117
+ const {
118
+ projectType,
119
+ language,
120
+ docker,
121
+ requiredEnv,
122
+ exportedTypes,
123
+ aws,
124
+ } = buildMetadata;
125
+
126
+ if (SUPPORTED_PROJECT_TYPES.indexOf(projectType) < 0) {
127
+ throw new Error(
128
+ `Invalid projectType (buildMetadata.projectType).\n\tMust be one of: [${SUPPORTED_PROJECT_TYPES}]`
129
+ );
130
+ }
131
+
132
+ if (SUPPORTED_LANGUAGES.indexOf(language) < 0) {
133
+ throw new Error(
134
+ `Invalid language (buildMetadata.language)\n\tMust be one of: [${SUPPORTED_LANGUAGES}]`
135
+ );
136
+ }
137
+
138
+ this._requiredEnv = [];
139
+ if (requiredEnv instanceof Array) {
140
+ this._requiredEnv = requiredEnv.concat([]);
141
+ }
142
+
143
+ this._projectType = projectType;
144
+ this._language = language;
145
+ this._exportedTypes = exportedTypes;
146
+
147
+ this._hasTypescript = this._language === 'ts';
148
+ this._hasServer = this._projectType === 'api';
149
+ this._hasExportedTypes =
150
+ typeof exportedTypes === 'string' && exportedTypes.length > 0;
151
+
152
+ if (this._projectType === 'aws-microservice') {
153
+ if (!aws || typeof aws !== 'object') {
154
+ throw new Error(
155
+ 'The project is an AWS microservice, but does not define AWS configuration'
156
+ );
157
+ }
158
+
159
+ if (!aws.stacks || typeof aws.stacks !== 'object') {
160
+ throw new Error(
161
+ 'The project is an AWS microservice, but does not define AWS stacks'
162
+ );
163
+ }
164
+
165
+ this._awsRegion = aws.region;
166
+ this._awsProfile = aws.profile;
167
+ this._cdkStacks = aws.stacks;
168
+ } else {
169
+ this._awsRegion = undefined;
170
+ this._awsProfile = undefined;
171
+ this._cdkStacks = {};
172
+ }
173
+
174
+ this._hasDocker =
175
+ this._projectType !== 'lib' &&
176
+ this._projectType !== 'aws-microservice' &&
177
+ docker &&
178
+ typeof docker === 'object';
179
+
180
+ this._dockerTargets = this._initDockerTargets(docker);
181
+ }
182
+
183
+ /**
184
+ * Initialize docker targets for the project.
185
+ *
186
+ * @param docker The docker configuration section for the project.
187
+ */
188
+ _initDockerTargets(docker) {
189
+ if (!this._hasDocker) return [];
190
+
191
+ if (docker.repo || docker.registry || docker.buildArgs) {
192
+ // Deprecated settings
193
+ let repo = docker.repo;
194
+ if (!repo) {
195
+ repo = docker.registry
196
+ ? `${docker.registry}/${this._unscopedName}`
197
+ : this._unscopedName;
198
+ }
199
+ return [
200
+ {
201
+ repo,
202
+ name: 'default',
203
+ buildFile: 'Dockerfile',
204
+ buildArgs: this._initializeFromEnv(docker.buildArgs),
205
+ isDefault: true,
206
+ isDeprecated: true,
207
+ },
208
+ ];
209
+ }
210
+
211
+ return Object.keys(docker).map((key) => {
212
+ const config = docker[key];
213
+ if (!config || typeof config !== 'object') {
214
+ throw new Error(
215
+ `Docker target configuration is invalid for target: [${key}]`
216
+ );
217
+ }
218
+ if (typeof config.repo !== 'string' || config.repo.length <= 0) {
219
+ throw new Error(
220
+ `Docker target does not define a valid repo: [${key}]`
221
+ );
222
+ }
223
+
224
+ const { repo, buildFile, buildArgs } = config;
225
+ return {
226
+ repo,
227
+ name: key,
228
+ buildFile: buildFile || 'Dockerfile',
229
+ buildArgs: this._initializeFromEnv(buildArgs),
230
+ isDefault: key === 'default',
231
+ isDeprecated: false,
232
+ };
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Loops through the specified object, and replaces specific values from
238
+ * those defined in the current environment. Returns a new object with the
239
+ * replaced values. Only properties whose values are '__ENV__' will be
240
+ * replaced with environment equivalents.
241
+ *
242
+ * @param map The initial set of key value mappings.
243
+ *
244
+ * @returns {Array} An array of objects containing "name" and "value"
245
+ * properties that contain the key and the values (replaced from
246
+ * environment if applicable)
247
+ */
248
+ _initializeFromEnv(map) {
249
+ if (!map || map instanceof Array || typeof map !== 'object') {
250
+ return [];
251
+ }
252
+ return Object.keys(map).map((name) => {
253
+ let value = map[name];
254
+ if (value == '__ENV__') {
255
+ value = process.env[name];
256
+ }
257
+ return { name, value };
258
+ });
259
+ }
260
+
261
+ /**
262
+ * Initializes a directory tree for the project based on project properties.
263
+ *
264
+ * @private
265
+ */
266
+ _initProjectTree() {
267
+ const tree = {
268
+ src: null,
269
+ test: {
270
+ unit: null,
271
+ api: null,
272
+ },
273
+ infra: null,
274
+ working: {
275
+ src: null,
276
+ test: {
277
+ unit: null,
278
+ api: null,
279
+ },
280
+ infra: null,
281
+ node_modules: null,
282
+ },
283
+ dist: null,
284
+ docs: null,
285
+ node_modules: null,
286
+ coverage: null,
287
+ '.gulp': null,
288
+ '.tscache': null,
289
+ logs: null,
290
+ 'cdk.out': null,
291
+ };
292
+
293
+ if (this._projectType === 'aws-microservice') {
294
+ tree.infra = null;
295
+ tree.working.infra = null;
296
+ tree.working.node_modules = null;
297
+ tree['cdk.out'] = null;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * An object representation of the project's root directory.
303
+ *
304
+ * @returns {Directory}
305
+ */
306
+ get rootDir() {
307
+ return this._rootDir;
308
+ }
309
+
310
+ /**
311
+ * An object representation of the root directory for all javascript files.
312
+ * For typescript projects, this would be the directory containing the
313
+ * transpiled files.
314
+ *
315
+ * @returns {Directory}
316
+ */
317
+ get jsRootDir() {
318
+ return this._hasTypescript
319
+ ? this._rootDir.getChild('working')
320
+ : this._rootDir;
321
+ }
322
+
323
+ /**
324
+ * The name of the project as defined in package.json.
325
+ *
326
+ * @return {String}
327
+ */
328
+ get name() {
329
+ return this._name;
330
+ }
331
+
332
+ /**
333
+ * The license of the project as defined in package.json.
334
+ *
335
+ * @return {String}
336
+ */
337
+ get license() {
338
+ return this._license;
339
+ }
340
+
341
+ /**
342
+ * The keywords for the project as defined in package.json.
343
+ *
344
+ * @return {String}
345
+ */
346
+ get keywords() {
347
+ return this._keywords;
348
+ }
349
+
350
+ /**
351
+ * The name of the project without including its scope. IF the project has
352
+ * no scope, the unscoped name will match the project name.
353
+ *
354
+ * @return {String}
355
+ */
356
+ get unscopedName() {
357
+ return this._unscopedName;
358
+ }
359
+
360
+ /**
361
+ * The name of the project formatted in snake case. Ideal for use when
362
+ * generating package names. This property does not include the file
363
+ * extension (.tgz).
364
+ */
365
+ get snakeCasedName() {
366
+ return this._snakeCasedName;
367
+ }
368
+
369
+ /**
370
+ * The version of the project as defined in package.json.
371
+ *
372
+ * @return {String}
373
+ */
374
+ get version() {
375
+ return this._version;
376
+ }
377
+
378
+ /**
379
+ * The description of the project as defined in package.json.
380
+ *
381
+ * @return {String}
382
+ */
383
+ get description() {
384
+ return this._description;
385
+ }
386
+
387
+ /**
388
+ * Gets the name of the expected configuration file name based on the name
389
+ * of the project.
390
+ */
391
+ get configFileName() {
392
+ return `.${_camelcase(this._unscopedName)}rc`;
393
+ }
394
+
395
+ /**
396
+ * The project type of the project (lib/api/cli).
397
+ *
398
+ * @return {String}
399
+ */
400
+ get projectType() {
401
+ return this._projectType;
402
+ }
403
+
404
+ /**
405
+ * The language used by the project (js/ts).
406
+ *
407
+ * @return {String}
408
+ */
409
+ get language() {
410
+ return this._language;
411
+ }
412
+
413
+ /**
414
+ * Returns the AWS region configured for the project.
415
+ *
416
+ * @return {String}
417
+ */
418
+ get awsRegion() {
419
+ return this._awsRegion;
420
+ }
421
+
422
+ /**
423
+ * Returns the AWS profile configured for the project.
424
+ *
425
+ * @return {String}
426
+ */
427
+ get awsProfile() {
428
+ return this._awsProfile;
429
+ }
430
+
431
+ /**
432
+ * The path to the directory that contains the types exported by this
433
+ * project.
434
+ *
435
+ * @return {String}
436
+ */
437
+ get exportedTypes() {
438
+ return this._exportedTypes;
439
+ }
440
+
441
+ /**
442
+ * Determines whether or not the project can be packaged up as a docker
443
+ * image.
444
+ *
445
+ * @return {Boolean}
446
+ */
447
+ get hasDocker() {
448
+ return this._hasDocker;
449
+ }
450
+
451
+ /**
452
+ * Determines whether or not the project contains typescript files.
453
+ *
454
+ * @return {Boolean}
455
+ */
456
+ get hasTypescript() {
457
+ return this._hasTypescript;
458
+ }
459
+
460
+ /**
461
+ * Determines whether or not the project has a server component that might
462
+ * require API tests or the ability to host a local server.
463
+ *
464
+ * @return {Boolean}
465
+ */
466
+ get hasServer() {
467
+ return this._hasServer;
468
+ }
469
+
470
+ /**
471
+ * Determines if the project has any types to export.
472
+ *
473
+ * @return {String}
474
+ */
475
+ get hasExportedTypes() {
476
+ return this._hasExportedTypes;
477
+ }
478
+
479
+ /**
480
+ * Initializes a list of environment variables from the specified files.
481
+ * If environment variables are repeated in files, the declaration in the
482
+ * first file takes precedence over the others.
483
+ *
484
+ * @param {Array} [files=[]] A list of files to load environment variables
485
+ * from.
486
+ */
487
+ initEnv(envFiles) {
488
+ if (!(envFiles instanceof Array)) {
489
+ envFiles = [];
490
+ }
491
+ envFiles
492
+ .filter((file) => _fs.existsSync(file))
493
+ .forEach((file) => _dotEnvExpand(_dotEnv.config({ path: file })));
494
+ }
495
+
496
+ /**
497
+ * Returns a list of required environment variables. These parameters can
498
+ * be checked during build/package time to ensure that they exist, before
499
+ * performing any actions.
500
+ *
501
+ * @return {Array}
502
+ */
503
+ getRequiredEnv() {
504
+ return this._requiredEnv.concat([]);
505
+ }
506
+
507
+ /**
508
+ * Checks to see if all required variables have been defined in the
509
+ * environment. This is typically a runtime call, executed prior to
510
+ * building/packaging a project.
511
+ */
512
+ validateRequiredEnv() {
513
+ const missingVars = [];
514
+ this._requiredEnv.forEach((param) => {
515
+ if (!process.env[param]) {
516
+ missingVars.push(param);
517
+ }
518
+ });
519
+ if (missingVars.length > 0) {
520
+ throw new Error(
521
+ `Required environment variables not defined: ${missingVars}`
522
+ );
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Returns a list of docker targets defined for the project. Every target
528
+ * will define the following properties:
529
+ * - repo: The docker repo
530
+ * - buildFile: The name of the build file to use
531
+ * - buildArgs: Arguments to be passed to the docker build
532
+ * - isDefault: Determines if the current target is the default one
533
+ * - isDeprecated: Determines if the target uses a deprecated configuration.
534
+ *
535
+ * @return {Array}
536
+ */
537
+ getDockerTargets() {
538
+ return this._dockerTargets.concat([]);
539
+ }
540
+
541
+ /**
542
+ * Returns a list of CDK stack keys defined for the project. These stack
543
+ * keys will be used to generate deploy tasks for each. Each key maps to a
544
+ * specific CDK stack that can be deployed.
545
+ *
546
+ * @return {Array} A list of stack keys
547
+ */
548
+ getCdkStacks() {
549
+ return Object.keys(this._cdkStacks);
550
+ }
551
+
552
+ /**
553
+ * Returns the name of the stack corresponding to the stack key.
554
+ *
555
+ * @param {String} key The CDK stack key to use when looking up the name.
556
+ * @return {String} The stack name that maps to the key.
557
+ */
558
+ getCdkStackName(key) {
559
+ if (typeof key !== 'string' || key.length <= 0) {
560
+ throw new Error('Invalid stack key (arg #1)');
561
+ }
562
+ return this._cdkStacks[key];
563
+ }
564
+ };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const _gulp = require('gulp');
4
+
5
+ /**
6
+ * Sub builder that creates a task that will copy javascript files from source
7
+ * to build directories. This method will return a watcher if the watch option
8
+ * is set to true.
9
+ *
10
+ * @private
11
+ * @param {Object} project Reference to an object that contains project metadata
12
+ * that can be used to customize build outputs.
13
+ * @param {Object} options An options object that can be used to customize the
14
+ * task.
15
+ *
16
+ * @returns {Function} A gulp task.
17
+ */
18
+ module.exports = (project, options) => {
19
+ const { watch } = Object.assign({ watch: false }, options);
20
+ const rootDir = project.rootDir;
21
+ const workingDir = rootDir.getChild('working');
22
+
23
+ const dirs = ['src', 'test'];
24
+ if (project.projectType === 'aws-microservice') {
25
+ dirs.push('infra');
26
+ }
27
+
28
+ const extras = [
29
+ project.configFileName,
30
+ 'package.json',
31
+ '.npmignore',
32
+ '.npmrc',
33
+ ];
34
+
35
+ if (project.hasDocker) {
36
+ extras.push('Dockerfile*');
37
+ }
38
+
39
+ const paths = dirs
40
+ .map((dir) => rootDir.getChild(dir))
41
+ .map((dir) => ['js', 'json'].map((ext) => dir.getAllFilesGlob(ext)))
42
+ .reduce((result, arr) => result.concat(arr), [])
43
+ .concat(extras.map((item) => rootDir.getFileGlob(item)));
44
+
45
+ const task = () =>
46
+ _gulp
47
+ .src(paths, { allowEmpty: true, base: rootDir.globPath })
48
+ .pipe(_gulp.dest(workingDir.absolutePath));
49
+
50
+ task.displayName = 'build-js';
51
+ task.description = 'Copy javascript files from source to build directory';
52
+
53
+ if (watch) {
54
+ const watchTask = () => _gulp.watch(paths, task);
55
+ watchTask.displayName = 'watch-build-js';
56
+ watchTask.description =
57
+ 'Automatically copy javascript files to build directory on change';
58
+
59
+ return watchTask;
60
+ }
61
+ return task;
62
+ };
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const _gulp = require('gulp');
4
+ const _typescript = require('gulp-typescript');
5
+
6
+ /**
7
+ * Sub builder that creates a task that will transpile typescript files into
8
+ * javascript files. This method will return a watcher if the watch option
9
+ * is set to true.
10
+ *
11
+ * @private
12
+ * @param {Object} project Reference to an object that contains project metadata
13
+ * that can be used to customize build outputs.
14
+ * @param {Object} options An options object that can be used to customize the
15
+ * task.
16
+ *
17
+ * @returns {Function} A gulp task.
18
+ */
19
+ module.exports = (project, options) => {
20
+ const { watch } = Object.assign({ watch: false }, options);
21
+ const rootDir = project.rootDir;
22
+ const workingDir = rootDir.getChild('working');
23
+
24
+ const dirs = ['src', 'test'];
25
+ if (project.projectType === 'aws-microservice') {
26
+ dirs.push('infra');
27
+ }
28
+
29
+ const tsProject = _typescript.createProject('tsconfig.json');
30
+
31
+ const paths = dirs
32
+ .map((dir) => rootDir.getChild(dir))
33
+ .map((dir) => ['ts'].map((ext) => dir.getAllFilesGlob(ext)))
34
+ .reduce((result, arr) => result.concat(arr), []);
35
+
36
+ const distFiles = [
37
+ rootDir.getFileGlob('package-lock.json'),
38
+ rootDir.getFileGlob('Dockerfile*'),
39
+ rootDir.getFileGlob('LICENSE'),
40
+ rootDir.getFileGlob('README.md'),
41
+ rootDir.getFileGlob('.env'),
42
+ rootDir.getFileGlob(project.configFileName),
43
+ ];
44
+
45
+ const buildTask = () =>
46
+ _gulp
47
+ .src(paths, { base: rootDir.globPath })
48
+ .pipe(tsProject())
49
+ .pipe(_gulp.dest(workingDir.absolutePath));
50
+
51
+ const copyTask = () =>
52
+ _gulp
53
+ .src(distFiles, { allowEmpty: true })
54
+ .pipe(_gulp.dest(workingDir.absolutePath));
55
+
56
+ const task = _gulp.parallel([copyTask, buildTask]);
57
+
58
+ task.displayName = 'build-ts';
59
+ task.description = 'Build typescript source files to javascript files';
60
+
61
+ if (watch) {
62
+ const watchTask = () => _gulp.watch(paths, task);
63
+ watchTask.displayName = 'watch-build-ts';
64
+ watchTask.description =
65
+ 'Automatically build typescript files on change';
66
+
67
+ return watchTask;
68
+ }
69
+ return task;
70
+ };