create-gasket-app 7.0.0-cli.7 → 7.0.0-next.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/README.md +222 -2
- package/lib/commands/create.js +192 -0
- package/lib/config/default-plugins.js +7 -0
- package/lib/index.d.ts +205 -0
- package/lib/index.js +22 -9
- package/lib/main.js +19 -0
- package/lib/scaffold/action-wrapper.js +36 -0
- package/lib/scaffold/actions/apply-preset-config.js +29 -0
- package/lib/scaffold/actions/cli-version.js +84 -0
- package/lib/scaffold/actions/create-hooks.js +26 -0
- package/lib/scaffold/actions/generate-files.js +183 -0
- package/lib/scaffold/actions/global-prompts.js +161 -0
- package/lib/scaffold/actions/index.js +36 -0
- package/lib/scaffold/actions/install-modules.js +18 -0
- package/lib/scaffold/actions/link-modules.js +21 -0
- package/lib/scaffold/actions/load-pkg-for-debug.js +24 -0
- package/lib/scaffold/actions/load-preset.js +82 -0
- package/lib/scaffold/actions/mkdir.js +30 -0
- package/lib/scaffold/actions/post-create-hooks.js +33 -0
- package/lib/scaffold/actions/print-report.js +88 -0
- package/lib/scaffold/actions/prompt-hooks.js +90 -0
- package/lib/scaffold/actions/setup-pkg.js +47 -0
- package/lib/scaffold/actions/write-gasket-config.js +32 -0
- package/lib/scaffold/actions/write-pkg.js +23 -0
- package/lib/scaffold/config-builder.js +448 -0
- package/lib/scaffold/create-context.js +193 -0
- package/lib/scaffold/create-engine.js +21 -0
- package/lib/scaffold/dump-error-context.js +28 -0
- package/lib/scaffold/fetcher.js +194 -0
- package/lib/scaffold/files.js +30 -0
- package/lib/scaffold/utils.js +115 -0
- package/lib/utils/create-option.js +20 -0
- package/lib/utils/index.js +16 -0
- package/lib/utils/logo.js +14 -0
- package/lib/utils/process-args.js +39 -0
- package/lib/utils/process-command.js +52 -0
- package/lib/utils/process-options.js +46 -0
- package/lib/utils/setup.js +25 -0
- package/lib/utils/warn-if-outdated.js +76 -0
- package/package.json +23 -3
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { rename, unlink } = fs.promises;
|
|
4
|
+
const tar = require('tar-fs');
|
|
5
|
+
const zlib = require('zlib');
|
|
6
|
+
const pump = require('pump');
|
|
7
|
+
const mkdirp = require('mkdirp');
|
|
8
|
+
const debug = require('diagnostics')('gasket:create-gasket-app:fetcher');
|
|
9
|
+
const { PackageManager } = require('@gasket/utils');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Simple helper class that can also be re-used in tests for similar
|
|
13
|
+
* file I/O operations
|
|
14
|
+
*/
|
|
15
|
+
const Fetcher = class Fetcher {
|
|
16
|
+
constructor(opts = {}) {
|
|
17
|
+
this.dir = path.join(opts.tmp || require('os').tmpdir(), this._id());
|
|
18
|
+
debug('tmpdir', this.dir);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ensures an eventually consistent rm -rf (aka rimraf) of
|
|
23
|
+
* the target sourceDir by atomically moving it into this
|
|
24
|
+
* tmp directory and then spawning a detached child process
|
|
25
|
+
* to remove it.
|
|
26
|
+
* @param {string} sourceDir Directory within this repository. *
|
|
27
|
+
* @returns {Promise} result
|
|
28
|
+
*/
|
|
29
|
+
async vacuum(sourceDir) {
|
|
30
|
+
const spawn = require('cross-spawn');
|
|
31
|
+
const basename = path.basename(sourceDir);
|
|
32
|
+
const rootDir = path.join(__dirname, '..', '..');
|
|
33
|
+
|
|
34
|
+
if (!sourceDir.startsWith(rootDir)) {
|
|
35
|
+
console.error('Refusing to rimraf vacuum unsafe directory: ', sourceDir);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await mkdirp(this.dir);
|
|
40
|
+
try {
|
|
41
|
+
await rename(sourceDir, path.join(this.dir, basename));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.code === 'ENOENT') return;
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rm = spawn(process.execPath, [
|
|
48
|
+
require.resolve('rimraf/bin.js'),
|
|
49
|
+
this.dir
|
|
50
|
+
], { detached: true });
|
|
51
|
+
|
|
52
|
+
rm.on('error', err => {
|
|
53
|
+
console.error(`Error removing ${this.dir}: ${err.message}`);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns a reasonably unique name for a subdirectory in `os.tmpdir()`
|
|
59
|
+
* @returns {String} Reasonably unique name
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
_id() {
|
|
63
|
+
return `gasket-${process.hrtime().join('-')}`;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Package fetcher that represents the core file system logic
|
|
70
|
+
* for getting npm packages without having to perform a
|
|
71
|
+
* configuration.
|
|
72
|
+
*
|
|
73
|
+
* @type {PackageFetcher}
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
76
|
+
module.exports = class PackageFetcher {
|
|
77
|
+
constructor(opts = {}) {
|
|
78
|
+
this.cwd = opts.cwd || process.cwd();
|
|
79
|
+
this.packageName = opts.packageName;
|
|
80
|
+
this.tmp = new Fetcher(opts);
|
|
81
|
+
debug('init', this.packageName, this.tmp.dir);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Simple wrapper around clone that also reads the package.json
|
|
86
|
+
* from disk once it has been downloaded.
|
|
87
|
+
*
|
|
88
|
+
* @returns {Promise<Object>} Parsed package.json for the fetched package.
|
|
89
|
+
*/
|
|
90
|
+
async readPackage() {
|
|
91
|
+
const dest = await this.clone();
|
|
92
|
+
return require(path.join(dest, 'package.json'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Critical path file system operations to pull down the npm package
|
|
97
|
+
* that represents the desired gasket scaffold, `this.packageName`. The logic
|
|
98
|
+
* revolves around the assumption:
|
|
99
|
+
*
|
|
100
|
+
* 1. Move (i.e. rename(2)) is the only truly atomic file system operation.
|
|
101
|
+
*
|
|
102
|
+
* Since we use `npm` as the baseline the algorithm becomes:
|
|
103
|
+
*
|
|
104
|
+
* 1. Create the target `tmp` directory to untar into
|
|
105
|
+
* 2. Fetch the tarball using `npm pack` directly as a child process
|
|
106
|
+
* 3. Unpack the tarball into the target `tmp` directory
|
|
107
|
+
* - All `npm` package tarbals have a nested "package" directory in them.
|
|
108
|
+
* 4. Remove the tarball created by `npm pack`.
|
|
109
|
+
*
|
|
110
|
+
* @returns {Promise<string>} Full path of the cloned destination on local disk.
|
|
111
|
+
* @public
|
|
112
|
+
*/
|
|
113
|
+
async clone() {
|
|
114
|
+
await mkdirp(this.tmp.dir);
|
|
115
|
+
|
|
116
|
+
const tarballFile = await this.fetch(this.packageName, this.tmp.dir);
|
|
117
|
+
const tarball = path.join(this.tmp.dir, tarballFile);
|
|
118
|
+
|
|
119
|
+
await this.unpack(tarball, this.tmp.dir);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await unlink(tarball);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// eslint ignore no-empty
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return path.join(this.tmp.dir, 'package');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Fetch the tarball for the scaffold
|
|
132
|
+
*
|
|
133
|
+
* @param {string} packageName An npm package name (e.g. @gasket/app-template).
|
|
134
|
+
* @param {string} [dir] Directory the package needs to be downloaded.
|
|
135
|
+
* @returns {Promise} A promise represents if fetch succeeds or fails
|
|
136
|
+
* @public
|
|
137
|
+
*/
|
|
138
|
+
async fetch(packageName = this.packageName, dir = this.cwd) {
|
|
139
|
+
const argv = [
|
|
140
|
+
'pack', packageName,
|
|
141
|
+
'--loglevel', 'error',
|
|
142
|
+
'--prefer-online'
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const { stdout } = await PackageManager.spawnNpm(argv, {
|
|
146
|
+
cwd: dir
|
|
147
|
+
}).catch(err => {
|
|
148
|
+
// TODO: dump npm stdout and stderr to a more useful location.
|
|
149
|
+
throw new Error(`Failed to fetch ${packageName}: npm exited with non-zero code\n${err.stdout}\n\n${err.stderr}`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Remark: we should **always** get a tarball, but is it
|
|
153
|
+
// worthwhile to validate that assumption?
|
|
154
|
+
return stdout.split('\n').filter(Boolean)[0];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Unpack the tarball into given directory.
|
|
159
|
+
*
|
|
160
|
+
* Used from https://github.com/ywangii/await-targz under MIT.
|
|
161
|
+
*
|
|
162
|
+
* @param {String} tarball Path for tarball to be extracted
|
|
163
|
+
* @param {String} dir Path for the file to be extracted
|
|
164
|
+
* @returns {Promise} A promise represents if unpack tarball succeeds or fails
|
|
165
|
+
* @public
|
|
166
|
+
*/
|
|
167
|
+
async unpack(tarball, dir) {
|
|
168
|
+
return {
|
|
169
|
+
then: async (fulfill, reject) => {
|
|
170
|
+
const logOpts = { tarball, dir };
|
|
171
|
+
const readableStream = fs.createReadStream(tarball).once('error', this._logError(`fs.createReadStream`, logOpts));
|
|
172
|
+
const unzip = zlib.createUnzip().once('error', this._logError(`zlib.createUnzip`, logOpts));
|
|
173
|
+
const extract = tar.extract(dir).once('error', this._logError(`tar.extract`, logOpts));
|
|
174
|
+
|
|
175
|
+
pump(readableStream, unzip, extract, err => {
|
|
176
|
+
if (err) return reject(err);
|
|
177
|
+
fulfill();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/*
|
|
184
|
+
* Simple logging helper for streams.
|
|
185
|
+
*/
|
|
186
|
+
_logError(msg, context) {
|
|
187
|
+
return err => { console.log(msg, context, err); };
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
//
|
|
192
|
+
// Expose Fetcher onto the exports for re-use
|
|
193
|
+
//
|
|
194
|
+
module.exports.Fetcher = Fetcher;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for plugins to add files and templates for generating
|
|
3
|
+
*
|
|
4
|
+
* @type {Files}
|
|
5
|
+
*/
|
|
6
|
+
module.exports = class Files {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.globSets = [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Return array of globs
|
|
13
|
+
*
|
|
14
|
+
* @deprecated
|
|
15
|
+
* @returns {string[]} `globby` compatible patterns
|
|
16
|
+
*/
|
|
17
|
+
get globs() {
|
|
18
|
+
return this.globSets.reduce((acc, cur) => [...acc, ...cur.globs], []);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Adds the specified `globby` compatible patterns, `globs`,
|
|
23
|
+
* into the set of all sources for this set of files.
|
|
24
|
+
* @param {string[]} globs - `globby` compatible patterns
|
|
25
|
+
* @param {Object} source - Plugin to blame if conflicts arise from this operation.
|
|
26
|
+
*/
|
|
27
|
+
add({ globs, source }) {
|
|
28
|
+
this.globSets.push({ globs, source });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const untildify = require('untildify');
|
|
3
|
+
const { pluginIdentifier } = require('@gasket/resolve');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pushes plugins short names for use in gasket.config.js
|
|
7
|
+
* Pushes the original plugin identifiers under rawPlugins
|
|
8
|
+
*
|
|
9
|
+
* @param {PluginDesc[]} plugins - Plugins names
|
|
10
|
+
* @param {CreateContext} context - Create context
|
|
11
|
+
*/
|
|
12
|
+
function addPluginsToContext(plugins, context) {
|
|
13
|
+
context.rawPlugins = context.rawPlugins || [];
|
|
14
|
+
context.plugins = context.plugins || [];
|
|
15
|
+
|
|
16
|
+
context.rawPlugins = plugins.reduce((acc, cur) => {
|
|
17
|
+
const next = pluginIdentifier(cur);
|
|
18
|
+
|
|
19
|
+
// get the index of existing plugins by comparing short names
|
|
20
|
+
const idx = acc.map(p => pluginIdentifier(p).shortName).indexOf(next.shortName);
|
|
21
|
+
|
|
22
|
+
if (idx > -1) {
|
|
23
|
+
// if the existing plugin does not specify a version, use the next one
|
|
24
|
+
if (!pluginIdentifier(acc[idx]).version) {
|
|
25
|
+
acc[idx] = cur;
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
// if we don't already have an entry, push the next one
|
|
29
|
+
acc.push(cur);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return acc;
|
|
33
|
+
}, context.rawPlugins);
|
|
34
|
+
|
|
35
|
+
context.plugins = plugins.reduce((acc, cur) => {
|
|
36
|
+
const next = pluginIdentifier(cur).shortName;
|
|
37
|
+
if (!acc.includes(next)) acc.push(next);
|
|
38
|
+
return acc;
|
|
39
|
+
}, context.plugins);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Adds plugins and dependencies of the app package
|
|
44
|
+
*
|
|
45
|
+
* @param {PluginDesc[]|pluginIdentifier[]} plugins - Plugins names
|
|
46
|
+
* @param {PackageJson} pkg - Package builder
|
|
47
|
+
* @param {String} [field] - Dependency type (Default: dependencies)
|
|
48
|
+
*/
|
|
49
|
+
function addPluginsToPkg(plugins, pkg, field = 'dependencies') {
|
|
50
|
+
const pluginIds = plugins.map(p => pluginIdentifier(p).withVersion());
|
|
51
|
+
pkg.add(field, pluginIds.reduce((acc, p) => {
|
|
52
|
+
if (!acc[p.fullName]) {
|
|
53
|
+
acc[p.fullName] = p.version;
|
|
54
|
+
}
|
|
55
|
+
return acc;
|
|
56
|
+
}, {}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Look up registry version of provided plugin descriptions w/o versions set.
|
|
61
|
+
*
|
|
62
|
+
* @param {PluginDesc[]|pluginIdentifier[]} plugins - Plugins names
|
|
63
|
+
* @param {PackageManager} pkgManager - Package manager instance
|
|
64
|
+
* @returns {Promise<pluginIdentifier[]>} plugins
|
|
65
|
+
*/
|
|
66
|
+
async function getPluginsWithVersions(plugins, pkgManager) {
|
|
67
|
+
return await Promise.all(
|
|
68
|
+
plugins.map(async name => {
|
|
69
|
+
const id = pluginIdentifier(name);
|
|
70
|
+
if (id.version != null) return id;
|
|
71
|
+
const version = (await pkgManager.info([id.fullName, 'version'])).data;
|
|
72
|
+
return id.withVersion(`^${version}`);
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Takes a file path and transforms it to be absolute if not already
|
|
79
|
+
*
|
|
80
|
+
* @param {string} filepath - Path to file that may be relative or have tildy
|
|
81
|
+
* @returns {string} absolute filepath
|
|
82
|
+
*/
|
|
83
|
+
function ensureAbsolute(filepath) {
|
|
84
|
+
filepath = untildify(filepath);
|
|
85
|
+
if (path.isAbsolute(filepath)) return filepath;
|
|
86
|
+
return path.resolve(process.cwd(), filepath);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parses JSON file or string to assign to context
|
|
92
|
+
*
|
|
93
|
+
* @param {CreateContext} context - Create context.
|
|
94
|
+
* @param {Object} configFlags - flags to read config from
|
|
95
|
+
* @param {string} configFlags.config - JSON string of config values
|
|
96
|
+
* @param {string} configFlags.configFile - path to JSON file of config values
|
|
97
|
+
*/
|
|
98
|
+
function readConfig(context, { config, configFile }) {
|
|
99
|
+
if (config) {
|
|
100
|
+
const parsedConfig = JSON.parse(config);
|
|
101
|
+
Object.assign(context, parsedConfig);
|
|
102
|
+
} else if (configFile) {
|
|
103
|
+
const parsedConfigFile = require(path.resolve(context.cwd, configFile));
|
|
104
|
+
Object.assign(context, parsedConfigFile);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
addPluginsToContext,
|
|
111
|
+
addPluginsToPkg,
|
|
112
|
+
getPluginsWithVersions,
|
|
113
|
+
ensureAbsolute,
|
|
114
|
+
readConfig
|
|
115
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const { Option } = require('commander');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* createOption - Create a commander option
|
|
5
|
+
* @param {CLICommandOption} definition - The option configuration
|
|
6
|
+
* @returns {CommnaderOption} option
|
|
7
|
+
*/
|
|
8
|
+
function createOption(definition) {
|
|
9
|
+
const option = new Option(...definition.options);
|
|
10
|
+
const { defaultValue, conflicts, parse, hidden, required } = definition;
|
|
11
|
+
|
|
12
|
+
if (conflicts.length) option.conflicts(definition.conflicts);
|
|
13
|
+
if (hidden) option.hideHelp();
|
|
14
|
+
if (typeof defaultValue !== 'undefined') option.default(defaultValue);
|
|
15
|
+
if (parse) option.argParser(parse);
|
|
16
|
+
if (required) option.makeOptionMandatory();
|
|
17
|
+
return option;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = createOption;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const processCommand = require('./process-command');
|
|
2
|
+
const processArgs = require('./process-args');
|
|
3
|
+
const processOptions = require('./process-options');
|
|
4
|
+
const createOption = require('./create-option');
|
|
5
|
+
const logo = require('./logo');
|
|
6
|
+
const warnIfOutdated = require('./warn-if-outdated');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
processCommand,
|
|
11
|
+
processArgs,
|
|
12
|
+
processOptions,
|
|
13
|
+
createOption,
|
|
14
|
+
logo,
|
|
15
|
+
warnIfOutdated
|
|
16
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const logo = `
|
|
2
|
+
▄▄▄▄████▀▀█▄
|
|
3
|
+
▄███▀▀▀▀▀▀▀█▄▄██
|
|
4
|
+
▄██▀ ▄▄▄▄▄ ▀██▌ ▄▄▄
|
|
5
|
+
▄██▀ ███▀▀▀▀█ ██ ██ ██
|
|
6
|
+
█▀▀█ ██▌ ▐█▌ ▄▀▀▀█▄ ▄█▀▀▀▄ ██ ▐██▀ ▄█▀▀█▄ ▀██▀▀
|
|
7
|
+
█▄▄█ ██▌ ▀▀██ ▐█▌ ▄▄▄▄██ ▀▀█▄▄▄ █████ ██▄▄██ ██
|
|
8
|
+
▀██▄ ███▄▄▄▄██ ██ ▀▄▄▄▀█▄ ▀▄▄▄█▀ ▄██▄ ██▄ ▀█▄▄▄▄ ▀█▄▄
|
|
9
|
+
▀██▄ ▀▀▀ ▀▀ ▄██▌
|
|
10
|
+
▀███▄▄▄▄▄▄▄█▀▀██
|
|
11
|
+
▀▀▀▀████▄▄█▀
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
module.exports = logo;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* isValidArg - Validates the argument configuration
|
|
3
|
+
* @param {CLICommandArg} arg The argument configuration
|
|
4
|
+
* @returns {boolean} True if valid, false otherwise
|
|
5
|
+
*/
|
|
6
|
+
function isValidArg(arg) {
|
|
7
|
+
const keys = Object.keys(arg);
|
|
8
|
+
if (arg.required && arg.default) return false;
|
|
9
|
+
return keys.length &&
|
|
10
|
+
keys.length <= 4 &&
|
|
11
|
+
arg.name && arg.description;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* processArgs - Process the arguments configuration
|
|
16
|
+
* @param {CLICommandArg[]} args Array of argument configurations
|
|
17
|
+
* @returns {ProccesedCLICommandArg[]} Array of argument definitions
|
|
18
|
+
*/
|
|
19
|
+
function processArgs(args) {
|
|
20
|
+
if (!Array.isArray(args) || !args.every(isValidArg)) throw new Error('Invalid argument(s) configuration');
|
|
21
|
+
|
|
22
|
+
return args.reduce((acc, arg) => {
|
|
23
|
+
const def = [];
|
|
24
|
+
const { name, description, required = false, default: defaultValue } = arg;
|
|
25
|
+
if (required) {
|
|
26
|
+
def.push(`<${name}>`, description);
|
|
27
|
+
} else {
|
|
28
|
+
def.push(`[${name}]`, description);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (defaultValue) def.push(defaultValue);
|
|
32
|
+
|
|
33
|
+
acc.push(def);
|
|
34
|
+
return acc;
|
|
35
|
+
|
|
36
|
+
}, []);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = processArgs;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const program = new Command();
|
|
3
|
+
const processArgs = require('./process-args');
|
|
4
|
+
const processOptions = require('./process-options');
|
|
5
|
+
const createOption = require('./create-option');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* isValidCommand - Validates the command configuration
|
|
9
|
+
* @param {CLICommand} command The command configuration
|
|
10
|
+
* @returns {boolean} True if valid, false otherwise
|
|
11
|
+
*/
|
|
12
|
+
function isValidCommand(command) {
|
|
13
|
+
const keys = Object.keys(command);
|
|
14
|
+
return keys.length &&
|
|
15
|
+
keys.length <= 7 &&
|
|
16
|
+
command &&
|
|
17
|
+
command.id &&
|
|
18
|
+
command.description &&
|
|
19
|
+
command.action &&
|
|
20
|
+
typeof command.action === 'function';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* processCommand - Process the command configuration
|
|
25
|
+
* @param {CLICommand} command The command configuration
|
|
26
|
+
* @returns {ProccesedCLICommand} The command instance
|
|
27
|
+
*/
|
|
28
|
+
function processCommand(command) {
|
|
29
|
+
if (!isValidCommand(command)) throw new Error('Invalid command configuration');
|
|
30
|
+
const { id, description, action, args, options, hidden = false, default: isDefault = false } = command;
|
|
31
|
+
const cmd = program
|
|
32
|
+
.command(id)
|
|
33
|
+
.description(description)
|
|
34
|
+
.action(action);
|
|
35
|
+
|
|
36
|
+
if (args) {
|
|
37
|
+
const cmdArgs = processArgs(args);
|
|
38
|
+
cmdArgs.forEach(arg => cmd.argument(...arg));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options) {
|
|
42
|
+
const cmdOpts = processOptions(options);
|
|
43
|
+
cmdOpts.forEach(o => {
|
|
44
|
+
const option = createOption(o);
|
|
45
|
+
cmd.addOption(option);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { command: cmd, hidden, isDefault };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = processCommand;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* isValidOption - Validate the option
|
|
3
|
+
* @param {CLICommandOption} option The option configuration
|
|
4
|
+
* @returns {boolean} True if valid, false otherwise
|
|
5
|
+
*/
|
|
6
|
+
function isValidOption(option) {
|
|
7
|
+
const keys = Object.keys(option);
|
|
8
|
+
return keys.length &&
|
|
9
|
+
keys.length <= 5 &&
|
|
10
|
+
option.name &&
|
|
11
|
+
option.description;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* processOptions - Process the options configuration
|
|
16
|
+
* @param {CLICommandOption[]} options Array of option configurations
|
|
17
|
+
* @returns {ProccesedCLICommandOption[]} Processed option definitions
|
|
18
|
+
*/
|
|
19
|
+
function processOptions(options) {
|
|
20
|
+
if (!Array.isArray(options) || !options.every(isValidOption)) throw new Error('Invalid option(s) configuration');
|
|
21
|
+
|
|
22
|
+
return options.reduce((acc, option) => {
|
|
23
|
+
const def = [];
|
|
24
|
+
const {
|
|
25
|
+
name,
|
|
26
|
+
description,
|
|
27
|
+
required = false,
|
|
28
|
+
short,
|
|
29
|
+
parse,
|
|
30
|
+
type = 'string',
|
|
31
|
+
conflicts = [],
|
|
32
|
+
hidden = false,
|
|
33
|
+
default: defaultValue
|
|
34
|
+
} = option;
|
|
35
|
+
|
|
36
|
+
const format = required ? `<${name}>` : `[${name}]`;
|
|
37
|
+
const flags = short ? `-${short}, --${name}` : `--${name}`;
|
|
38
|
+
|
|
39
|
+
def.push(`${flags}${type !== 'boolean' ? ` ${format}` : ''}`, description);
|
|
40
|
+
acc.push({ options: def, conflicts, hidden, defaultValue, parse, required });
|
|
41
|
+
|
|
42
|
+
return acc;
|
|
43
|
+
}, []);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = processOptions;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step which runs before oclif any everything else is required.
|
|
3
|
+
* Handles the `--require` flags to allow for early importing of instrumentation
|
|
4
|
+
* modules and the like.
|
|
5
|
+
*
|
|
6
|
+
* @param {string[]} [argv] - CLI arguments - defaults to process.argv
|
|
7
|
+
*/
|
|
8
|
+
module.exports = function setup(argv = process.argv) {
|
|
9
|
+
// Using to parse args before mounting all of oclif
|
|
10
|
+
const getopts = require('getopts');
|
|
11
|
+
const opts = getopts(argv, {
|
|
12
|
+
alias: {
|
|
13
|
+
require: ['r']
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (opts.require) {
|
|
18
|
+
const required = Array.isArray(opts.require) ? opts.require : [opts.require];
|
|
19
|
+
required.forEach(module => {
|
|
20
|
+
require(
|
|
21
|
+
require.resolve(module, { paths: [process.cwd()] })
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const { runShellCommand } = require('@gasket/utils');
|
|
2
|
+
const semver = require('semver');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { readFile, writeFile } = require('fs/promises');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const cachePath = path.join(__dirname, '..', '..', '.cache'); // Place at root of gasket-cli
|
|
8
|
+
const LATEST_VERSION = 'latestVersion';
|
|
9
|
+
const LATEST_VERSION_UPDATE_TIME = 'latestVersionUpdateTime';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks if the latest version update time is older than seven days
|
|
13
|
+
* @param {number} currentTime - current time in milliseconds
|
|
14
|
+
* @param {number} latestVersionUpdateTime - latest version update time in milliseconds
|
|
15
|
+
* @returns {boolean} - true if latestVersionUpdateTime is older than seven days
|
|
16
|
+
*/
|
|
17
|
+
function isOlderThanSevenDays(currentTime, latestVersionUpdateTime) {
|
|
18
|
+
const sevenDaysInMillis = 7 * 24 * 60 * 60 * 1000; // Seven days in milliseconds
|
|
19
|
+
const timeDifference = Math.abs(currentTime - latestVersionUpdateTime);
|
|
20
|
+
return timeDifference > sevenDaysInMillis;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fetches the latest version of a package from npm
|
|
25
|
+
* @param {string} pkgName - package name
|
|
26
|
+
* @param {number} currentTime - current time in milliseconds
|
|
27
|
+
* @param {object} cache - cache object
|
|
28
|
+
* @returns {string} - latest version of the package
|
|
29
|
+
*/
|
|
30
|
+
async function getLatestVersion(pkgName, currentTime, cache) {
|
|
31
|
+
if (
|
|
32
|
+
!cache[LATEST_VERSION_UPDATE_TIME] ||
|
|
33
|
+
!cache[LATEST_VERSION] ||
|
|
34
|
+
isOlderThanSevenDays(currentTime, cache[LATEST_VERSION_UPDATE_TIME])
|
|
35
|
+
) {
|
|
36
|
+
try {
|
|
37
|
+
const cmdResult = await runShellCommand('npm', ['view', pkgName, 'version'], {});
|
|
38
|
+
if (cmdResult?.stdout) {
|
|
39
|
+
const latestVersion = cmdResult.stdout.trim();
|
|
40
|
+
cache[LATEST_VERSION_UPDATE_TIME] = currentTime;
|
|
41
|
+
cache[LATEST_VERSION] = latestVersion;
|
|
42
|
+
await writeFile(cachePath, JSON.stringify(cache));
|
|
43
|
+
return latestVersion;
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Error fetching latest version:', error);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
return cache[LATEST_VERSION];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Warns if the package is outdated
|
|
55
|
+
* @param {string} pkgName - package name
|
|
56
|
+
* @param {string} currentVersion - current version of the package
|
|
57
|
+
*/
|
|
58
|
+
module.exports = async function warnIfOutdated(pkgName, currentVersion) {
|
|
59
|
+
const currentTime = new Date().getTime();
|
|
60
|
+
let cache = {};
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const file = await readFile(cachePath, 'utf8');
|
|
64
|
+
cache = JSON.parse(file);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// console.error('Error reading cache file:', error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const latestVersion = await getLatestVersion(pkgName, currentTime, cache);
|
|
70
|
+
|
|
71
|
+
if (typeof latestVersion === 'string' && semver.gt(latestVersion, currentVersion)) {
|
|
72
|
+
console.warn(
|
|
73
|
+
` ${chalk.yellow('›')} Warning: ${pkgName} update available from ${chalk.green(latestVersion)} to ${chalk.green(currentVersion)}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-gasket-app",
|
|
3
|
-
"version": "7.0.0-
|
|
3
|
+
"version": "7.0.0-next.0",
|
|
4
4
|
"description": "starter pack for creating a gasket app",
|
|
5
5
|
"main": "lib",
|
|
6
6
|
"bin": {
|
|
@@ -31,10 +31,30 @@
|
|
|
31
31
|
},
|
|
32
32
|
"homepage": "https://github.com/godaddy/gasket/tree/main/packages/create-gasket-app",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@gasket/
|
|
34
|
+
"@gasket/engine": "^7.0.0-next.0",
|
|
35
|
+
"@gasket/plugin-git": "^7.0.0-next.0",
|
|
36
|
+
"@gasket/plugin-lifecycle": "^7.0.0-next.0",
|
|
37
|
+
"@gasket/plugin-logger": "^7.0.0-next.0",
|
|
38
|
+
"@gasket/plugin-metadata": "^7.0.0-next.0",
|
|
39
|
+
"@gasket/plugin-start": "^7.0.0-next.0",
|
|
40
|
+
"@gasket/resolve": "^7.0.0-next.0",
|
|
41
|
+
"@gasket/utils": "^7.0.0-next.0",
|
|
42
|
+
"commander": "^12.0.0",
|
|
43
|
+
"deepmerge": "^4.3.1",
|
|
44
|
+
"handlebars": "^4.7.8",
|
|
45
|
+
"inquirer": "6.5.2",
|
|
46
|
+
"json5": "^2.2.3",
|
|
47
|
+
"mkdirp": "^1.0.4",
|
|
48
|
+
"ora": "3.4.0",
|
|
49
|
+
"pump": "^3.0.0",
|
|
50
|
+
"semver": "^7.5.4",
|
|
51
|
+
"tar-fs": "^2.0.0",
|
|
52
|
+
"untildify": "^4.0.0"
|
|
35
53
|
},
|
|
36
54
|
"devDependencies": {
|
|
37
55
|
"cross-env": "^7.0.3",
|
|
56
|
+
"diagnostics": "^2.0.2",
|
|
57
|
+
"dotenv": "^16.4.5",
|
|
38
58
|
"eslint": "^8.56.0",
|
|
39
59
|
"eslint-config-godaddy": "^7.0.2",
|
|
40
60
|
"eslint-plugin-jest": "^27.6.3",
|
|
@@ -57,5 +77,5 @@
|
|
|
57
77
|
"eslintIgnore": [
|
|
58
78
|
"test/fixtures"
|
|
59
79
|
],
|
|
60
|
-
"gitHead": "
|
|
80
|
+
"gitHead": "55ab680e659ef5315c1d82f511439f32cca5f2e4"
|
|
61
81
|
}
|