create-gasket-app 7.0.3-cli.0 → 7.0.4
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 +223 -3
- package/lib/commands/create.js +149 -0
- package/lib/config/default-plugins.js +15 -0
- package/lib/index.d.ts +310 -0
- package/lib/index.js +24 -9
- package/lib/scaffold/action-wrapper.js +37 -0
- package/lib/scaffold/actions/create-hooks.js +27 -0
- package/lib/scaffold/actions/generate-files.js +189 -0
- package/lib/scaffold/actions/global-prompts.js +162 -0
- package/lib/scaffold/actions/index.js +33 -0
- package/lib/scaffold/actions/install-modules.js +15 -0
- package/lib/scaffold/actions/link-modules.js +19 -0
- package/lib/scaffold/actions/load-preset.js +60 -0
- package/lib/scaffold/actions/mkdir.js +30 -0
- package/lib/scaffold/actions/post-create-hooks.js +31 -0
- package/lib/scaffold/actions/preset-config-hooks.js +13 -0
- package/lib/scaffold/actions/preset-prompt-hooks.js +17 -0
- package/lib/scaffold/actions/print-report.js +89 -0
- package/lib/scaffold/actions/prompt-hooks.js +43 -0
- package/lib/scaffold/actions/setup-pkg.js +34 -0
- package/lib/scaffold/actions/write-gasket-config.js +119 -0
- package/lib/scaffold/actions/write-pkg.js +19 -0
- package/lib/scaffold/config-builder.js +526 -0
- package/lib/scaffold/create-context.js +188 -0
- package/lib/scaffold/dump-error-context.js +28 -0
- package/lib/scaffold/files.js +30 -0
- package/lib/scaffold/readme.js +51 -0
- package/lib/scaffold/utils.js +22 -0
- package/lib/utils/create-option.js +18 -0
- package/lib/utils/index.js +5 -0
- package/lib/utils/logo.js +12 -0
- package/lib/utils/process-args.js +37 -0
- package/lib/utils/process-command.js +50 -0
- package/lib/utils/process-options.js +44 -0
- package/package.json +36 -9
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { GasketConfigDefinition, MaybeAsync, Plugin, GasketEngine } from '@gasket/core';
|
|
2
|
+
import type { PackageManager } from '@gasket/utils';
|
|
3
|
+
import type { PromptModule } from 'inquirer';
|
|
4
|
+
|
|
5
|
+
export interface Dependencies {
|
|
6
|
+
dependencies?: Record<string, string>;
|
|
7
|
+
devDependencies?: Record<string, string>;
|
|
8
|
+
peerDependencies?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PackageJson extends Dependencies {
|
|
12
|
+
name: string;
|
|
13
|
+
version?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
license?: string;
|
|
16
|
+
type?: 'commonjs' | 'module';
|
|
17
|
+
repository?:
|
|
18
|
+
| string
|
|
19
|
+
| {
|
|
20
|
+
type: 'git';
|
|
21
|
+
url: string;
|
|
22
|
+
};
|
|
23
|
+
scripts?: Record<string, string>;
|
|
24
|
+
optionalDependencies?: Record<string, string>;
|
|
25
|
+
homepage?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ModuleInfo {
|
|
29
|
+
name: string;
|
|
30
|
+
module: any;
|
|
31
|
+
path?: string;
|
|
32
|
+
package?: PackageJson;
|
|
33
|
+
version?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PresetInfo extends ModuleInfo { }
|
|
37
|
+
|
|
38
|
+
export interface PluginInfo extends ModuleInfo { }
|
|
39
|
+
|
|
40
|
+
export interface ConfigBuilder<Config> {
|
|
41
|
+
/**
|
|
42
|
+
* fields object
|
|
43
|
+
*/
|
|
44
|
+
fields: { [key: string]: any };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Adds all `[key, value]` pairs in the `fields` provided.
|
|
48
|
+
* @param fields - Object to merge. Can be a function that accepts the current fields and object to merge.
|
|
49
|
+
*/
|
|
50
|
+
extend(
|
|
51
|
+
fields: Partial<Config> | ((current: Config) => Partial<Config>)
|
|
52
|
+
): void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Performs an intelligent, domain-aware merge of the `value` for
|
|
56
|
+
* the given `key` into the package.json fields associated with this instance.
|
|
57
|
+
*
|
|
58
|
+
* @param key - Field in package.json to add or extend.
|
|
59
|
+
* @param value - Target value to set for key provided.
|
|
60
|
+
* @param [options] - Optional arguments for add behavior
|
|
61
|
+
* @param [options.force] - Should the semver version override other attempts
|
|
62
|
+
*
|
|
63
|
+
* Adapted from @vue/cli under MIT License:
|
|
64
|
+
* https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/GeneratorAPI.js#L117-L150
|
|
65
|
+
*/
|
|
66
|
+
add<Key extends keyof Config>(
|
|
67
|
+
key: Key,
|
|
68
|
+
value: Config[Key],
|
|
69
|
+
options?: { force?: boolean }
|
|
70
|
+
): void;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* addPlugin - Add plugin import to the gasket file and use the value in the plugins array
|
|
74
|
+
* @param {string} pluginImport - name of the import used as a value - `import pluginImport...`
|
|
75
|
+
* @param {string} pluginName - name of the plugin import/package - `from 'pluginName'`
|
|
76
|
+
* @example
|
|
77
|
+
* addPlugin('pluginA', '@gasket/plugin-a')
|
|
78
|
+
*
|
|
79
|
+
* // gasket.js
|
|
80
|
+
* import pluginA from '@gasket/plugin-a';
|
|
81
|
+
*
|
|
82
|
+
* export default makeGasket({
|
|
83
|
+
* plugins: [
|
|
84
|
+
* pluginA
|
|
85
|
+
* ]
|
|
86
|
+
* });
|
|
87
|
+
*/
|
|
88
|
+
addPlugin(pluginImport: string, pluginName: string): void;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* addImport - Add a non-plugin import to the gasket file
|
|
92
|
+
* @param {string} importName - name of the import used as a value - `import fs...`
|
|
93
|
+
* @param {string} importPath - path of the import - `from 'fs'`
|
|
94
|
+
* @returns {ConfigBuilder} - instance for chaining
|
|
95
|
+
* @example
|
|
96
|
+
* Can be default or named import
|
|
97
|
+
* addImport('{ readFileSync }', 'fs')
|
|
98
|
+
* addImport('fs', 'fs')
|
|
99
|
+
*
|
|
100
|
+
* // gasket.js
|
|
101
|
+
* import { readFileSync } from 'fs';
|
|
102
|
+
* import fs from 'fs';
|
|
103
|
+
*/
|
|
104
|
+
addImport(importName: string, importPath: string): ConfigBuilder<Config>;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* addExpression - add programmatic expression to the gasket file
|
|
108
|
+
* @param {string} expression - expression to add after imports
|
|
109
|
+
* @returns {ConfigBuilder} - instance for chaining
|
|
110
|
+
* @example
|
|
111
|
+
*
|
|
112
|
+
* .addImport('fs', 'fs')
|
|
113
|
+
* .addExpression('const file = fs.readFileSync(\'./file.txt\')')
|
|
114
|
+
*
|
|
115
|
+
* // gasket.js
|
|
116
|
+
* import fs from 'fs';
|
|
117
|
+
* const file = fs.readFileSync('./file.txt');
|
|
118
|
+
*/
|
|
119
|
+
addExpression(expression: string): ConfigBuilder<Config>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* injectValue - Inject a value into the gasket config object
|
|
123
|
+
* @param {string} configKey - collapsed object path to inject value into - `express.config.routes`
|
|
124
|
+
* @param {string} injectedValue - string used as a value
|
|
125
|
+
* @example
|
|
126
|
+
* .addImport('{ routes }', './routes')
|
|
127
|
+
* .injectValue('express.routes', 'routes');
|
|
128
|
+
*
|
|
129
|
+
* // gasket.js
|
|
130
|
+
* export default makeGasket({
|
|
131
|
+
* express: {
|
|
132
|
+
* routes: routes
|
|
133
|
+
* }
|
|
134
|
+
* });
|
|
135
|
+
*/
|
|
136
|
+
injectValue(configKey: string, injectedValue: string): void;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface PackageJsonBuilder extends ConfigBuilder<PackageJson> {
|
|
140
|
+
/**
|
|
141
|
+
* Checks if a property exists on the package.json fields.
|
|
142
|
+
* @param key - Field to search
|
|
143
|
+
* @param value - Value to search for
|
|
144
|
+
* @returns True if the property exists and matches the value
|
|
145
|
+
*/
|
|
146
|
+
has(key: keyof PackageJson, value: string): boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface Files {
|
|
150
|
+
add(...args: string[]): void;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface Readme {
|
|
154
|
+
/** Markdown content to be injected into the app readme */
|
|
155
|
+
markdown: string[];
|
|
156
|
+
|
|
157
|
+
/** Links to be added to the footer */
|
|
158
|
+
links: string[];
|
|
159
|
+
|
|
160
|
+
/** Add a markdown heading */
|
|
161
|
+
heading(content: string, level?: number): Readme;
|
|
162
|
+
|
|
163
|
+
/** Add a markdown sub-heading */
|
|
164
|
+
subHeading(content: string): Readme;
|
|
165
|
+
|
|
166
|
+
/** Add markdown content */
|
|
167
|
+
content(markdown: string);
|
|
168
|
+
|
|
169
|
+
/** Add a markdown list */
|
|
170
|
+
list(items: string[]): Readme;
|
|
171
|
+
|
|
172
|
+
/** Add a markdown link - printed in footer */
|
|
173
|
+
link(content: string, href: string): Readme;
|
|
174
|
+
|
|
175
|
+
/** Add a markdown code block */
|
|
176
|
+
codeBlock(content: string, syntax?: string): Readme;
|
|
177
|
+
|
|
178
|
+
/** Add markdown file */
|
|
179
|
+
markdownFile(path: string): Promise<Readme>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type CreatePrompt = PromptModule;
|
|
183
|
+
|
|
184
|
+
declare module 'create-gasket-app' {
|
|
185
|
+
export interface CreateContext {
|
|
186
|
+
/** Short name of the app */
|
|
187
|
+
appName: string;
|
|
188
|
+
|
|
189
|
+
/** Current work directory */
|
|
190
|
+
cwd: string;
|
|
191
|
+
|
|
192
|
+
/** Path to the target app (Default: cwd/appName) */
|
|
193
|
+
dest: string;
|
|
194
|
+
|
|
195
|
+
/** Relative path to the target app */
|
|
196
|
+
relDest: string;
|
|
197
|
+
|
|
198
|
+
/** Whether or not target directory already exists */
|
|
199
|
+
extant: boolean;
|
|
200
|
+
|
|
201
|
+
/** paths to the local presets packages */
|
|
202
|
+
localPresets: Array<string>;
|
|
203
|
+
|
|
204
|
+
/** Raw preset desc from args. Can include version constraint. Added by
|
|
205
|
+
* load-preset if using localPresets. */
|
|
206
|
+
rawPresets: Array<string>;
|
|
207
|
+
|
|
208
|
+
/** Local packages that should be linked */
|
|
209
|
+
pkgLinks: Array<string>;
|
|
210
|
+
|
|
211
|
+
/** non-error/warning messages to report */
|
|
212
|
+
messages: Array<string>;
|
|
213
|
+
|
|
214
|
+
/** warnings messages to report */
|
|
215
|
+
warnings: Array<string>;
|
|
216
|
+
|
|
217
|
+
/** error messages to report but do not exit process */
|
|
218
|
+
errors: Array<string>;
|
|
219
|
+
|
|
220
|
+
/** any next steps to report for user */
|
|
221
|
+
nextSteps: Array<string>;
|
|
222
|
+
|
|
223
|
+
/** any generated files to show in report */
|
|
224
|
+
generatedFiles: Set<string>;
|
|
225
|
+
|
|
226
|
+
/** Default empty array, populated by load-preset with actual imports */
|
|
227
|
+
presets: Array<Plugin>;
|
|
228
|
+
|
|
229
|
+
/** Default to object w/empty plugins array to be populated by `presetConfig` hook */
|
|
230
|
+
presetConfig: GasketConfigDefinition;
|
|
231
|
+
|
|
232
|
+
// Added by `global-prompts`
|
|
233
|
+
|
|
234
|
+
/** Description of app */
|
|
235
|
+
appDescription: string;
|
|
236
|
+
|
|
237
|
+
/** Should a git repo be initialized and first commit */
|
|
238
|
+
gitInit: boolean;
|
|
239
|
+
|
|
240
|
+
/** Names of the plugins that add unit and integration tests */
|
|
241
|
+
testPlugins: Array<string>;
|
|
242
|
+
|
|
243
|
+
/** Which package manager to use (Default: 'npm') */
|
|
244
|
+
packageManager: string;
|
|
245
|
+
|
|
246
|
+
/** Derived install command (Default: 'npm install') */
|
|
247
|
+
installCmd: string;
|
|
248
|
+
|
|
249
|
+
/** Derived local run command (Default: 'npx gasket local') */
|
|
250
|
+
localCmd: string;
|
|
251
|
+
|
|
252
|
+
/** Whether or not the user wants to override an extant directory */
|
|
253
|
+
destOverride: boolean;
|
|
254
|
+
|
|
255
|
+
// Added by `setup-pkg`
|
|
256
|
+
|
|
257
|
+
/** package.json builder */
|
|
258
|
+
pkg: PackageJsonBuilder;
|
|
259
|
+
|
|
260
|
+
/** manager to execute npm or yarn commands */
|
|
261
|
+
pkgManager: PackageManager;
|
|
262
|
+
|
|
263
|
+
// Added by `setup-gasket-config`
|
|
264
|
+
|
|
265
|
+
/** gasket.config builder */
|
|
266
|
+
gasketConfig: ConfigBuilder<GasketConfigDefinition>;
|
|
267
|
+
|
|
268
|
+
// Added by `create-hooks`
|
|
269
|
+
|
|
270
|
+
/** Use to add files and templates to generate */
|
|
271
|
+
files: Files;
|
|
272
|
+
|
|
273
|
+
/** Use to add content to the README.md */
|
|
274
|
+
readme: Readme;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface ActionWrapperParams {
|
|
279
|
+
gasket: GasketEngine;
|
|
280
|
+
context: CreateContext;
|
|
281
|
+
spinner?: any;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
declare module '@gasket/core' {
|
|
285
|
+
|
|
286
|
+
export interface HookExecTypes {
|
|
287
|
+
presetPrompt(
|
|
288
|
+
context: CreateContext,
|
|
289
|
+
utils: {
|
|
290
|
+
prompt: CreatePrompt;
|
|
291
|
+
}
|
|
292
|
+
): Promise<void>;
|
|
293
|
+
presetConfig(context: CreateContext): Promise<CreateContext['presetConfig']>;
|
|
294
|
+
prompt(
|
|
295
|
+
context: CreateContext,
|
|
296
|
+
utils: {
|
|
297
|
+
prompt: CreatePrompt;
|
|
298
|
+
}
|
|
299
|
+
): MaybeAsync<CreateContext>;
|
|
300
|
+
|
|
301
|
+
create(context: CreateContext): MaybeAsync<void>;
|
|
302
|
+
|
|
303
|
+
postCreate(
|
|
304
|
+
context: CreateContext,
|
|
305
|
+
utils: {
|
|
306
|
+
runScript: (script: string) => Promise<void>;
|
|
307
|
+
}
|
|
308
|
+
): MaybeAsync<void>;
|
|
309
|
+
}
|
|
310
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { createCommand } from './commands/create.js';
|
|
5
|
+
import { processCommand, logo } from './utils/index.js';
|
|
6
|
+
import { warnIfOutdated } from '@gasket/utils';
|
|
2
7
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const pkg = require('../package.json');
|
|
10
|
+
const program = new Command();
|
|
5
11
|
|
|
6
|
-
|
|
12
|
+
// Create Gasket CLI
|
|
13
|
+
const gasketBin = program
|
|
14
|
+
.name('create-gasket-app')
|
|
15
|
+
.description(pkg.description)
|
|
16
|
+
.option('--gasket-config [gasket-config-path]', 'Fully qualified Gasket config to load', 'gasket.config')
|
|
17
|
+
.version(pkg.version)
|
|
18
|
+
.addHelpText('beforeAll', logo);
|
|
7
19
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
const { command, hidden, isDefault } = processCommand(createCommand);
|
|
23
|
+
await warnIfOutdated(pkg.name, pkg.version);
|
|
24
|
+
gasketBin.addCommand(command, { hidden, isDefault });
|
|
25
|
+
|
|
26
|
+
process.argv.splice(2, 0, 'create');
|
|
27
|
+
|
|
28
|
+
return await gasketBin.parseAsync();
|
|
14
29
|
}
|
|
15
30
|
|
|
16
31
|
main();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decorate a create action with spinner.
|
|
5
|
+
* If an action throws, a fail spinner will render for the step, regardless of
|
|
6
|
+
* if the spinner was started or not.
|
|
7
|
+
*
|
|
8
|
+
* @param {String} label - Label for the spinner
|
|
9
|
+
* @param {Function} fn - Action to wrap
|
|
10
|
+
* @param {Boolean} startSpinner - Should the spinner start
|
|
11
|
+
* @returns {Function} decorated action
|
|
12
|
+
*/
|
|
13
|
+
export default function withSpinner(label, fn, { startSpinner = true } = {}) {
|
|
14
|
+
/**
|
|
15
|
+
* Decorated function
|
|
16
|
+
*
|
|
17
|
+
* @param {ActionWrapperParams} params - ActionWrapperParams
|
|
18
|
+
* @param {GasketEngine} params.gasket - Gasket config
|
|
19
|
+
* @param {CreateContext} params.context - Create context
|
|
20
|
+
* @returns {Promise} promise
|
|
21
|
+
*/
|
|
22
|
+
async function wrapper({ gasket = {}, context }) {
|
|
23
|
+
const spinner = ora(label);
|
|
24
|
+
if (startSpinner) spinner.start();
|
|
25
|
+
try {
|
|
26
|
+
await fn({ gasket, context, spinner });
|
|
27
|
+
if (spinner.isSpinning) spinner.succeed();
|
|
28
|
+
} catch (err) {
|
|
29
|
+
spinner.fail();
|
|
30
|
+
context.errors.push(err.stack);
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
wrapper.wrapped = fn;
|
|
36
|
+
return wrapper;
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import action from '../action-wrapper.js';
|
|
2
|
+
import { ConfigBuilder } from '../config-builder.js';
|
|
3
|
+
import { Files } from '../files.js';
|
|
4
|
+
import Readme from '../readme.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Executes the `create` hook for all registered plugins.
|
|
8
|
+
* Adds `files` to context for plugins to add their files and templates.
|
|
9
|
+
*
|
|
10
|
+
* @param {GasketEngine} gasket - Gasket API
|
|
11
|
+
* @param {CreateContext} context - Create context
|
|
12
|
+
* @returns {Promise} promise
|
|
13
|
+
*/
|
|
14
|
+
async function createHooks({ gasket, context }) {
|
|
15
|
+
const { plugins = [], warnings } = context;
|
|
16
|
+
|
|
17
|
+
const files = new Files();
|
|
18
|
+
const readme = new Readme();
|
|
19
|
+
const gasketConfig = ConfigBuilder.create({}, { orderBy: ['plugins'], warnings, plugins });
|
|
20
|
+
Object.assign(context, { files, readme, gasketConfig });
|
|
21
|
+
|
|
22
|
+
await gasket.execApply('create', async function applyCreate(plugin, handler) {
|
|
23
|
+
await handler(context.runWith(plugin));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default action('Execute create hooks', createHooks);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { default as diagnostics } from 'diagnostics';
|
|
2
|
+
const debug = diagnostics('gasket:cli:generate-files');
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import Handlebars from 'handlebars';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import mkdirp from 'mkdirp';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
const { dim } = chalk;
|
|
10
|
+
import action from '../action-wrapper.js';
|
|
11
|
+
import * as syncGlob from 'glob';
|
|
12
|
+
const glob = promisify(syncGlob.default);
|
|
13
|
+
|
|
14
|
+
const flatten = (acc, values) => (acc || []).concat(values);
|
|
15
|
+
const reSep = /[/\\]+/;
|
|
16
|
+
const joinSep = pthArr => pthArr.join(path.sep);
|
|
17
|
+
const splitSep = pthStr => pthStr.split(reSep);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Find all duplicate target files and reduce to single descriptor.
|
|
21
|
+
* Keeps track of overrides.
|
|
22
|
+
* Last in wins.
|
|
23
|
+
*
|
|
24
|
+
* @param {object[]} descriptors - Details about files to generate
|
|
25
|
+
* @returns {object[]} descriptors
|
|
26
|
+
*/
|
|
27
|
+
function reduceDescriptors(descriptors) {
|
|
28
|
+
const reduced = descriptors.reduce((acc, desc) => {
|
|
29
|
+
const { target } = desc;
|
|
30
|
+
if (acc.has(target)) {
|
|
31
|
+
const prev = acc.get(target);
|
|
32
|
+
if (prev.from !== desc.from) {
|
|
33
|
+
debug(`Using "${target}" from ${desc.from} instead of ${prev.from}`);
|
|
34
|
+
desc.overrides = prev.from;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
acc.set(target, desc);
|
|
38
|
+
return acc;
|
|
39
|
+
}, new Map());
|
|
40
|
+
|
|
41
|
+
return Array.from(reduced.values());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Assemble the description objects from glob results
|
|
46
|
+
*
|
|
47
|
+
* @param {string} dest - app destination
|
|
48
|
+
* @param {string} from - Plugin source name
|
|
49
|
+
* @param {string} pattern - pattern to glob
|
|
50
|
+
* @param {string[]} srcPaths - resulting paths from glob
|
|
51
|
+
* @returns {object[]} descriptors
|
|
52
|
+
*/
|
|
53
|
+
function assembleDescriptors(dest, from, pattern, srcPaths) {
|
|
54
|
+
const output = joinSep(splitSep(dest));
|
|
55
|
+
const baseParts = splitSep(path.resolve(pattern.replace(/[/\\]+\.?\*.*$/, '')));
|
|
56
|
+
const base = joinSep(baseParts);
|
|
57
|
+
return srcPaths.map((srcPath) => {
|
|
58
|
+
const parts = splitSep(srcPath);
|
|
59
|
+
const srcFile = joinSep(parts);
|
|
60
|
+
const target = joinSep(parts.slice(baseParts.length)).replace('.template', '');
|
|
61
|
+
return {
|
|
62
|
+
pattern,
|
|
63
|
+
base,
|
|
64
|
+
srcFile,
|
|
65
|
+
targetFile: joinSep([output, target]),
|
|
66
|
+
target,
|
|
67
|
+
from
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build a list of descriptions of all files we want to generate
|
|
74
|
+
*
|
|
75
|
+
* @param {CreateContext} context - Create context
|
|
76
|
+
* @returns {object[]} descriptors
|
|
77
|
+
*/
|
|
78
|
+
async function getDescriptors(context) {
|
|
79
|
+
const { dest, files } = context;
|
|
80
|
+
const descriptors = (
|
|
81
|
+
await Promise.all(files.globSets.map(async set => {
|
|
82
|
+
const { globs, source } = set;
|
|
83
|
+
const matches = await Promise.all(globs.map(async pattern => {
|
|
84
|
+
pattern = splitSep(pattern).join('/'); // Glob uses / for all OS's
|
|
85
|
+
const srcPaths = await glob(pattern, { nodir: true });
|
|
86
|
+
return assembleDescriptors(dest, source.name, pattern, srcPaths);
|
|
87
|
+
}));
|
|
88
|
+
return matches.reduce(flatten);
|
|
89
|
+
}))
|
|
90
|
+
).reduce(flatten);
|
|
91
|
+
|
|
92
|
+
return reduceDescriptors(descriptors);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read file content, apply templating, then write out target file.
|
|
97
|
+
*
|
|
98
|
+
* @param {CreateContext} context - Create context
|
|
99
|
+
* @param {object[]} descriptors - Details about files to generate
|
|
100
|
+
* @returns {boolean} hasWarnings
|
|
101
|
+
*/
|
|
102
|
+
async function performGenerate(context, descriptors) {
|
|
103
|
+
const { warnings, errors, generatedFiles } = context;
|
|
104
|
+
let hasWarning = false;
|
|
105
|
+
let hasError = false;
|
|
106
|
+
|
|
107
|
+
const handlebars = Handlebars.create();
|
|
108
|
+
handlebars.registerHelper('json', JSON.stringify);
|
|
109
|
+
handlebars.registerHelper('jspretty', (data) => {
|
|
110
|
+
return JSON.stringify(data).replace(/"/g, '\'');
|
|
111
|
+
});
|
|
112
|
+
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
113
|
+
handlebars.registerHelper('markdownCompile', (data) => {
|
|
114
|
+
return handlebars.compile(data)(context);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
debug('descriptors', JSON.stringify(descriptors, null, 2));
|
|
118
|
+
|
|
119
|
+
await Promise.all(descriptors.map(async desc => {
|
|
120
|
+
const targetDir = path.dirname(desc.targetFile);
|
|
121
|
+
let content;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const source = await fs.readFile(desc.srcFile, { encoding: 'utf8' });
|
|
125
|
+
content = source;
|
|
126
|
+
try {
|
|
127
|
+
content = handlebars.compile(source)(context);
|
|
128
|
+
} catch (compileErr) {
|
|
129
|
+
hasWarning = true;
|
|
130
|
+
warnings.push(`Error templating ${desc.targetFile}: ${compileErr.message}`);
|
|
131
|
+
}
|
|
132
|
+
} catch (readErr) {
|
|
133
|
+
if (readErr.code === 'EISDIR') {
|
|
134
|
+
// Skipping directory errors.
|
|
135
|
+
// node-glob seems to incorrectly captures some directories on Windows
|
|
136
|
+
hasWarning = true;
|
|
137
|
+
warnings.push(`Directory matched as template file: ${desc.targetFile}`);
|
|
138
|
+
} else {
|
|
139
|
+
hasError = true;
|
|
140
|
+
errors.push(`Error reading template ${desc.srcFile}: ${readErr.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!content) return;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await mkdirp(targetDir);
|
|
147
|
+
try {
|
|
148
|
+
await fs.writeFile(desc.targetFile, content, { encoding: 'utf8' });
|
|
149
|
+
const message = desc.target + (desc.overrides ? dim(` – from ${desc.from}`) : '');
|
|
150
|
+
generatedFiles.add(message);
|
|
151
|
+
} catch (writeErr) {
|
|
152
|
+
hasError = true;
|
|
153
|
+
errors.push(`Error writing ${desc.targetFile}: ${writeErr.message}`);
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
hasError = true;
|
|
157
|
+
errors.push(`Error creating directory ${targetDir}: ${err.message}`);
|
|
158
|
+
}
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
return [hasWarning, hasError];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Generate the app files and templates using context
|
|
166
|
+
*
|
|
167
|
+
* @param {CreateContext} context - Create context
|
|
168
|
+
* @param {Spinner} spinner - Spinner
|
|
169
|
+
* @returns {Promise} promise
|
|
170
|
+
*/
|
|
171
|
+
async function generateFiles({ context, spinner }) {
|
|
172
|
+
const { files } = context;
|
|
173
|
+
|
|
174
|
+
if (!files.globSets.length) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let descriptors = await getDescriptors(context);
|
|
179
|
+
descriptors = reduceDescriptors(descriptors);
|
|
180
|
+
const [hasWarnings, hasError] = await performGenerate(context, descriptors);
|
|
181
|
+
|
|
182
|
+
if (hasWarnings) spinner.warn();
|
|
183
|
+
if (hasError) spinner.fail();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export default action('Generate app contents', generateFiles);
|
|
187
|
+
// exported for unit testing
|
|
188
|
+
export const _getDescriptors = getDescriptors;
|
|
189
|
+
export const _assembleDescriptors = assembleDescriptors;
|