@vamship/build-utils 1.0.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ };