@salesforce/pwa-kit-create-app 3.0.0-preview.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/LICENSE +14 -0
- package/README.md +44 -0
- package/assets/bootstrap/js/.eslintignore +4 -0
- package/assets/bootstrap/js/.eslintrc.js +10 -0
- package/assets/bootstrap/js/.prettierrc.yaml +7 -0
- package/assets/bootstrap/js/babel.config.js +7 -0
- package/assets/bootstrap/js/config/default.js.hbs +77 -0
- package/assets/bootstrap/js/config/sites.js.hbs +26 -0
- package/assets/bootstrap/js/overrides/app/assets/svg/brand-logo.svg +5 -0
- package/assets/bootstrap/js/overrides/app/main.jsx +14 -0
- package/assets/bootstrap/js/overrides/app/request-processor.js +118 -0
- package/assets/bootstrap/js/overrides/app/routes.jsx.hbs +19 -0
- package/assets/bootstrap/js/overrides/app/ssr.js +67 -0
- package/assets/bootstrap/js/overrides/app/static/ico/favicon.ico +0 -0
- package/assets/bootstrap/js/overrides/app/static/img/global/app-icon-192.png +0 -0
- package/assets/bootstrap/js/overrides/app/static/img/global/app-icon-512.png +0 -0
- package/assets/bootstrap/js/overrides/app/static/img/global/apple-touch-icon.png +0 -0
- package/assets/bootstrap/js/overrides/app/static/img/hero.png +0 -0
- package/assets/bootstrap/js/overrides/app/static/manifest.json.hbs +19 -0
- package/assets/bootstrap/js/overrides/app/static/robots.txt +2 -0
- package/assets/bootstrap/js/package.json.hbs +36 -0
- package/assets/bootstrap/js/worker/main.js +6 -0
- package/assets/templates/retail-react-app/app/static/manifest.json.hbs +19 -0
- package/assets/templates/retail-react-app/config/default.js.hbs +77 -0
- package/assets/templates/retail-react-app/config/sites.js.hbs +26 -0
- package/package.json +50 -0
- package/scripts/create-mobify-app.js +764 -0
- package/templates/express-minimal.tar.gz +0 -0
- package/templates/mrt-reference-app.tar.gz +0 -0
- package/templates/retail-react-app.tar.gz +0 -0
- package/templates/typescript-minimal.tar.gz +0 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2023, Salesforce, Inc.
|
|
4
|
+
* All rights reserved.
|
|
5
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
6
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
7
|
+
*/
|
|
8
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* This is a generator for PWA Kit projects that run on the Managed Runtime.
|
|
12
|
+
*
|
|
13
|
+
* The output of this script is a copy of a project template with the following changes:
|
|
14
|
+
*
|
|
15
|
+
* 1) We update any monorepo-local dependencies to be installed through NPM.
|
|
16
|
+
*
|
|
17
|
+
* 2) We rename the template and configure the generated project based on answers to
|
|
18
|
+
* questions that we ask the user on the CLI.
|
|
19
|
+
*
|
|
20
|
+
* ## Basic usage
|
|
21
|
+
*
|
|
22
|
+
* We expect end-users to generate projects by running `npx @salesforce/pwa-kit-create-app` on
|
|
23
|
+
* the CLI and following the prompts. Users must be able to run that command without
|
|
24
|
+
* installing any dependencies first.
|
|
25
|
+
*
|
|
26
|
+
* ## Advanced usage and integration testing:
|
|
27
|
+
*
|
|
28
|
+
* For testing on CI we need to be able to generate projects without running
|
|
29
|
+
* the interactive prompts on the CLI. To support these cases, we have
|
|
30
|
+
* a few presets that are "private" and only usable through the GENERATOR_PRESET
|
|
31
|
+
* env var – this keeps them out of the --help docs.
|
|
32
|
+
*
|
|
33
|
+
* If both the GENERATOR_PRESET env var and --preset arguments are passed, the
|
|
34
|
+
* option set in --preset is used.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const p = require('path')
|
|
38
|
+
const fs = require('fs')
|
|
39
|
+
const os = require('os')
|
|
40
|
+
const child_proc = require('child_process')
|
|
41
|
+
const {Command} = require('commander')
|
|
42
|
+
const inquirer = require('inquirer')
|
|
43
|
+
const {URL} = require('url')
|
|
44
|
+
const deepmerge = require('deepmerge')
|
|
45
|
+
const sh = require('shelljs')
|
|
46
|
+
const tar = require('tar')
|
|
47
|
+
const semver = require('semver')
|
|
48
|
+
const slugify = require('slugify')
|
|
49
|
+
const generatorPkg = require('../package.json')
|
|
50
|
+
const Handlebars = require('handlebars')
|
|
51
|
+
|
|
52
|
+
const program = new Command()
|
|
53
|
+
|
|
54
|
+
sh.set('-e')
|
|
55
|
+
|
|
56
|
+
// Handlebars helpers
|
|
57
|
+
|
|
58
|
+
// Our eslint script uses exscaped double quotes to have windows compatibility. This helper
|
|
59
|
+
// will ensure those escaped double quotes are still escaped after processing the template.
|
|
60
|
+
Handlebars.registerHelper('script', (object) => object.replaceAll('"', '\\"'))
|
|
61
|
+
|
|
62
|
+
// Validations
|
|
63
|
+
const validPreset = (preset) => {
|
|
64
|
+
return ALL_PRESET_NAMES.includes(preset)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const validProjectName = (s) => {
|
|
68
|
+
const regex = new RegExp(`^[a-zA-Z0-9-\\s]{1,${PROJECT_ID_MAX_LENGTH}}$`)
|
|
69
|
+
return regex.test(s) || 'Value can only contain letters, numbers, space and hyphens.'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const validUrl = (s) => {
|
|
73
|
+
try {
|
|
74
|
+
new URL(s)
|
|
75
|
+
return true
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return 'Value must be an absolute URL'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const validSiteId = (s) =>
|
|
82
|
+
/^[a-z0-9_-]+$/i.test(s) || 'Valid characters are alphanumeric, hyphen, or underscore'
|
|
83
|
+
|
|
84
|
+
// To see definitions for Commerce API configuration values, go to
|
|
85
|
+
// https://developer.salesforce.com/docs/commerce/commerce-api/guide/commerce-api-configuration-values.
|
|
86
|
+
const defaultCommerceAPIError =
|
|
87
|
+
'Invalid format. Use docs to find more information about valid configurations: https://developer.salesforce.com/docs/commerce/commerce-api/guide/commerce-api-configuration-values'
|
|
88
|
+
const validShortCode = (s) => /(^[0-9A-Z]{8}$)/i.test(s) || defaultCommerceAPIError
|
|
89
|
+
|
|
90
|
+
const validClientId = (s) =>
|
|
91
|
+
/(^[0-9A-Z]{8}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{12}$)/i.test(s) ||
|
|
92
|
+
s === 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ||
|
|
93
|
+
defaultCommerceAPIError
|
|
94
|
+
const validOrganizationId = (s) =>
|
|
95
|
+
/^(f_ecom)_([A-Z]{4})_(prd|stg|dev|[0-9]{3}|s[0-9]{2})$/i.test(s) || defaultCommerceAPIError
|
|
96
|
+
|
|
97
|
+
// Globals
|
|
98
|
+
const GENERATED_PROJECT_VERSION = '0.0.1'
|
|
99
|
+
|
|
100
|
+
const INITIAL_CONTEXT = {
|
|
101
|
+
preset: undefined,
|
|
102
|
+
answers: {
|
|
103
|
+
general: {},
|
|
104
|
+
project: {}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const TEMPLATE_SOURCE_NPM = 'npm'
|
|
108
|
+
const TEMPLATE_SOURCE_BUNDLE = 'bundle'
|
|
109
|
+
const DEFAULT_TEMPLATE_VERSION = 'latest'
|
|
110
|
+
|
|
111
|
+
const EXTENSIBILITY_QUESTIONS = [
|
|
112
|
+
{
|
|
113
|
+
name: 'project.extend',
|
|
114
|
+
message: 'Do you wish to use template extensibility?',
|
|
115
|
+
type: 'list',
|
|
116
|
+
choices: [
|
|
117
|
+
{
|
|
118
|
+
name: 'No',
|
|
119
|
+
value: false
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Yes',
|
|
123
|
+
value: true
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
const MRT_REFERENCE_QUESTIONS = [
|
|
130
|
+
{
|
|
131
|
+
name: 'project.name',
|
|
132
|
+
validate: validProjectName,
|
|
133
|
+
message: 'What is the name of your Project?'
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const EXPRESS_MINIMAL_QUESTIONS = [
|
|
138
|
+
{
|
|
139
|
+
name: 'project.name',
|
|
140
|
+
validate: validProjectName,
|
|
141
|
+
message: 'What is the name of your Project?'
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
const TYPESCRIPT_MINIMAL_QUESTIONS = [
|
|
146
|
+
{
|
|
147
|
+
name: 'project.name',
|
|
148
|
+
validate: validProjectName,
|
|
149
|
+
message: 'What is the name of your Project?'
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
const RETAIL_REACT_APP_QUESTIONS = [
|
|
154
|
+
{
|
|
155
|
+
name: 'project.name',
|
|
156
|
+
validate: validProjectName,
|
|
157
|
+
message: 'What is the name of your Project?'
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'project.commerce.instanceUrl',
|
|
161
|
+
message: 'What is the URL for your Commerce Cloud instance?',
|
|
162
|
+
validate: validUrl
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'project.commerce.clientId',
|
|
166
|
+
message: 'What is your SLAS Client ID?',
|
|
167
|
+
validate: validClientId
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'project.commerce.siteId',
|
|
171
|
+
message: 'What is your Site ID in Business Manager?',
|
|
172
|
+
validate: validSiteId
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'project.commerce.organizationId',
|
|
176
|
+
message: 'What is your Commerce API organization ID in Business Manager?',
|
|
177
|
+
validate: validOrganizationId
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'project.commerce.shortCode',
|
|
181
|
+
message: 'What is your Commerce API short code in Business Manager?',
|
|
182
|
+
validate: validShortCode
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
// Project dictionary describing details and how the gerator should ask questions etc.
|
|
187
|
+
const PRESETS = [
|
|
188
|
+
{
|
|
189
|
+
id: 'retail-react-app',
|
|
190
|
+
name: 'Retail React App',
|
|
191
|
+
description: `
|
|
192
|
+
Generate a project using custom settings by answering questions about a
|
|
193
|
+
B2C Commerce instance.
|
|
194
|
+
|
|
195
|
+
Use this preset to connect to an existing instance, such as a sandbox.
|
|
196
|
+
`,
|
|
197
|
+
shortDescription: 'The Retail app using your own Commerce Cloud instance',
|
|
198
|
+
templateSource: {
|
|
199
|
+
type: TEMPLATE_SOURCE_NPM,
|
|
200
|
+
id: '@salesforce/retail-react-app'
|
|
201
|
+
},
|
|
202
|
+
questions: [...EXTENSIBILITY_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
|
|
203
|
+
assets: ['translations'],
|
|
204
|
+
private: false
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'retail-react-app-demo',
|
|
208
|
+
name: 'Retail React App Demo',
|
|
209
|
+
description: `
|
|
210
|
+
Generate a project using the settings for a special B2C Commerce
|
|
211
|
+
instance that is used for demo purposes. No questions are asked.
|
|
212
|
+
|
|
213
|
+
Use this preset to try out PWA Kit.
|
|
214
|
+
`,
|
|
215
|
+
shortDescription: 'The Retail app with demo Commerce Cloud instance',
|
|
216
|
+
templateSource: {
|
|
217
|
+
type: TEMPLATE_SOURCE_NPM,
|
|
218
|
+
id: '@salesforce/retail-react-app'
|
|
219
|
+
},
|
|
220
|
+
questions: [...EXTENSIBILITY_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
|
|
221
|
+
answers: {
|
|
222
|
+
['project.extend']: true,
|
|
223
|
+
['project.name']: 'demo-storefront',
|
|
224
|
+
['project.commerce.instanceUrl']: 'https://zzte-053.dx.commercecloud.salesforce.com',
|
|
225
|
+
['project.commerce.clientId']: '1d763261-6522-4913-9d52-5d947d3b94c4',
|
|
226
|
+
['project.commerce.siteId']: 'RefArch',
|
|
227
|
+
['project.commerce.organizationId']: 'f_ecom_zzte_053',
|
|
228
|
+
['project.commerce.shortCode']: 'kv7kzm78',
|
|
229
|
+
['project.einstein.clientId']: '1ea06c6e-c936-4324-bcf0-fada93f83bb1',
|
|
230
|
+
['project.einstein.siteId']: 'aaij-MobileFirst'
|
|
231
|
+
},
|
|
232
|
+
assets: ['translations'],
|
|
233
|
+
private: false
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 'retail-react-app-test-project',
|
|
237
|
+
name: 'Retail React App Test Project',
|
|
238
|
+
description: '',
|
|
239
|
+
templateSource: {
|
|
240
|
+
type: TEMPLATE_SOURCE_NPM,
|
|
241
|
+
id: '@salesforce/retail-react-app'
|
|
242
|
+
},
|
|
243
|
+
questions: [...EXTENSIBILITY_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
|
|
244
|
+
answers: {
|
|
245
|
+
['project.extend']: true,
|
|
246
|
+
['project.name']: 'retail-react-app',
|
|
247
|
+
['project.commerce.instanceUrl']: 'https://zzrf-001.dx.commercecloud.salesforce.com',
|
|
248
|
+
['project.commerce.clientId']: 'c9c45bfd-0ed3-4aa2-9971-40f88962b836',
|
|
249
|
+
['project.commerce.siteId']: 'RefArch',
|
|
250
|
+
['project.commerce.organizationId']: 'f_ecom_zzrf_001',
|
|
251
|
+
['project.commerce.shortCode']: 'kv7kzm78',
|
|
252
|
+
['project.einstein.clientId']: '1ea06c6e-c936-4324-bcf0-fada93f83bb1',
|
|
253
|
+
['project.einstein.siteId']: 'aaij-MobileFirst'
|
|
254
|
+
},
|
|
255
|
+
assets: ['translations'],
|
|
256
|
+
private: true
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: 'typescript-minimal-test-project',
|
|
260
|
+
name: 'Template Minimal Test Project',
|
|
261
|
+
description: '',
|
|
262
|
+
templateSource: {
|
|
263
|
+
type: TEMPLATE_SOURCE_BUNDLE,
|
|
264
|
+
id: 'typescript-minimal'
|
|
265
|
+
},
|
|
266
|
+
private: true
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: 'typescript-minimal',
|
|
270
|
+
name: 'Template Minimal Project',
|
|
271
|
+
description: `
|
|
272
|
+
Generate a project using a bare-bones TypeScript app template.
|
|
273
|
+
|
|
274
|
+
Use this as a TypeScript starting point or as a base on top of
|
|
275
|
+
which to build new TypeScript project templates for Managed Runtime.
|
|
276
|
+
`,
|
|
277
|
+
templateSource: {
|
|
278
|
+
type: TEMPLATE_SOURCE_BUNDLE,
|
|
279
|
+
id: 'typescript-minimal'
|
|
280
|
+
},
|
|
281
|
+
questions: TYPESCRIPT_MINIMAL_QUESTIONS,
|
|
282
|
+
private: true
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: 'express-minimal-test-project',
|
|
286
|
+
name: 'Express Minimal Test Project',
|
|
287
|
+
description: '',
|
|
288
|
+
templateSource: {
|
|
289
|
+
type: TEMPLATE_SOURCE_BUNDLE,
|
|
290
|
+
id: 'express-minimal'
|
|
291
|
+
},
|
|
292
|
+
questions: EXPRESS_MINIMAL_QUESTIONS,
|
|
293
|
+
answers: {
|
|
294
|
+
['project.name']: 'express-minimal'
|
|
295
|
+
},
|
|
296
|
+
private: true
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: 'express-minimal',
|
|
300
|
+
name: 'Express Minimal Project',
|
|
301
|
+
description: `
|
|
302
|
+
Generate a project using a bare-bones express app template.
|
|
303
|
+
|
|
304
|
+
Use this as a starting point for APIs or as a base on top of
|
|
305
|
+
which to build new project templates for Managed Runtime.
|
|
306
|
+
`,
|
|
307
|
+
templateSource: {
|
|
308
|
+
type: TEMPLATE_SOURCE_BUNDLE,
|
|
309
|
+
id: 'express-minimal'
|
|
310
|
+
},
|
|
311
|
+
questions: EXPRESS_MINIMAL_QUESTIONS,
|
|
312
|
+
private: true
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: 'mrt-reference-app',
|
|
316
|
+
name: 'Managed Runtime Reference App',
|
|
317
|
+
description: '',
|
|
318
|
+
templateSource: {
|
|
319
|
+
type: TEMPLATE_SOURCE_BUNDLE,
|
|
320
|
+
id: 'mrt-reference-app'
|
|
321
|
+
},
|
|
322
|
+
questions: MRT_REFERENCE_QUESTIONS,
|
|
323
|
+
answers: {
|
|
324
|
+
['project.name']: 'mrt-reference-app'
|
|
325
|
+
},
|
|
326
|
+
private: true
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
const PRESET_QUESTIONS = [
|
|
331
|
+
{
|
|
332
|
+
name: 'general.presetId',
|
|
333
|
+
message: 'Choose a project preset to get started:',
|
|
334
|
+
type: 'list',
|
|
335
|
+
choices: PRESETS.filter(({private}) => !private).map(({shortDescription, id}) => ({
|
|
336
|
+
name: shortDescription,
|
|
337
|
+
value: id
|
|
338
|
+
}))
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
const BOOTSTRAP_DIR = p.join(__dirname, '..', 'assets', 'bootstrap', 'js')
|
|
343
|
+
|
|
344
|
+
const ASSETS_TEMPLATES_DIR = p.join(__dirname, '..', 'assets', 'templates')
|
|
345
|
+
|
|
346
|
+
const PRIVATE_PRESET_NAMES = PRESETS.filter(({private}) => !!private).map(({id}) => id)
|
|
347
|
+
|
|
348
|
+
const PUBLIC_PRESET_NAMES = PRESETS.filter(({private}) => !private).map(({id}) => id)
|
|
349
|
+
|
|
350
|
+
const ALL_PRESET_NAMES = PRIVATE_PRESET_NAMES.concat(PUBLIC_PRESET_NAMES)
|
|
351
|
+
|
|
352
|
+
const PROJECT_ID_MAX_LENGTH = 20
|
|
353
|
+
|
|
354
|
+
// Utilities
|
|
355
|
+
|
|
356
|
+
const readJson = (path) => JSON.parse(sh.cat(path))
|
|
357
|
+
|
|
358
|
+
const writeJson = (path, data) => new sh.ShellString(JSON.stringify(data, null, 2)).to(path)
|
|
359
|
+
|
|
360
|
+
const slugifyName = (name) =>
|
|
361
|
+
slugify(name, {
|
|
362
|
+
lower: true,
|
|
363
|
+
strict: true
|
|
364
|
+
}).slice(0, PROJECT_ID_MAX_LENGTH)
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Check if the provided path is an empty directory.
|
|
368
|
+
* @param {*} path
|
|
369
|
+
* @returns
|
|
370
|
+
*/
|
|
371
|
+
const isDirEmpty = (path) => fs.readdirSync(path).length === 0
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Logs an error and exits the process if the provided path points at a
|
|
375
|
+
* non-empty directory.
|
|
376
|
+
*
|
|
377
|
+
* @param {*} path
|
|
378
|
+
*/
|
|
379
|
+
const checkOutputDir = (path) => {
|
|
380
|
+
if (sh.test('-e', path) && !isDirEmpty(path)) {
|
|
381
|
+
console.error(
|
|
382
|
+
`The output directory "${path}" already exists. Try, for example, ` +
|
|
383
|
+
`"~/Desktop/my-project" instead of "~/Desktop"`
|
|
384
|
+
)
|
|
385
|
+
process.exit(1)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Returns a list of absolute file paths for a given folder. This will recursively
|
|
391
|
+
* list files in child folders.
|
|
392
|
+
*
|
|
393
|
+
* @param {*} dirPath
|
|
394
|
+
* @param {*} arrayOfFiles
|
|
395
|
+
* @returns
|
|
396
|
+
*/
|
|
397
|
+
const getFiles = (dirPath, arrayOfFiles = []) => {
|
|
398
|
+
const files = fs.readdirSync(dirPath)
|
|
399
|
+
|
|
400
|
+
files.forEach((file) => {
|
|
401
|
+
if (fs.statSync(p.join(dirPath, file)).isDirectory()) {
|
|
402
|
+
arrayOfFiles = getFiles(p.join(dirPath, file), arrayOfFiles)
|
|
403
|
+
} else {
|
|
404
|
+
arrayOfFiles.push(p.join(dirPath, file))
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
return arrayOfFiles
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Deeply merge two objects in such a way that all array entries in b replace array
|
|
413
|
+
* entries in a, eg:
|
|
414
|
+
*
|
|
415
|
+
* merge(
|
|
416
|
+
* {foo: 'foo', items: [{thing: 'a'}]},
|
|
417
|
+
* {bar: 'bar', items: [{thing: 'b'}]}
|
|
418
|
+
* )
|
|
419
|
+
* > {foo: 'foo', bar: 'bar', items: [{thing: 'b'}]}
|
|
420
|
+
*
|
|
421
|
+
* @param a
|
|
422
|
+
* @param b
|
|
423
|
+
* @return {*}
|
|
424
|
+
*/
|
|
425
|
+
const merge = (a, b) => deepmerge(a, b, {arrayMerge: (orignal, replacement) => replacement})
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Provided a dot notation key, and a value, return an expanded object splitting
|
|
429
|
+
* the key.
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* const expandedObj = expand('parent.child.grandchild': { name: 'Preseley' })
|
|
433
|
+
* console.log(expandedObj) // {parent: { child: {grandchild: {name: 'Presley}}}}
|
|
434
|
+
*
|
|
435
|
+
* @param {string} key
|
|
436
|
+
* @param {Object} value
|
|
437
|
+
* @returns
|
|
438
|
+
*
|
|
439
|
+
*/
|
|
440
|
+
const expandKey = (key, value) =>
|
|
441
|
+
key
|
|
442
|
+
.split('.')
|
|
443
|
+
.reverse()
|
|
444
|
+
.reduce(
|
|
445
|
+
(acc, curr) =>
|
|
446
|
+
acc
|
|
447
|
+
? {
|
|
448
|
+
[curr]: acc
|
|
449
|
+
}
|
|
450
|
+
: {
|
|
451
|
+
[curr]: value
|
|
452
|
+
},
|
|
453
|
+
undefined
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Provided an object there the keys use "dot notation", expand each individual key.
|
|
458
|
+
* NOTE: This only expands keys at the root level, and not those nested.
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* const expandedObj = expand({'coolthings.babynames': 'Preseley', 'coolthings.cars': 'bmws'})
|
|
462
|
+
* console.log(expandedObj) // {coolthings: { babynames: 'Presley', cars: 'bmws'}}
|
|
463
|
+
*
|
|
464
|
+
* @param {Object} answers
|
|
465
|
+
* @returns {Object} The expanded object.
|
|
466
|
+
*
|
|
467
|
+
*/
|
|
468
|
+
const expandObject = (obj = {}) =>
|
|
469
|
+
Object.keys(obj).reduce((acc, curr) => merge(acc, expandKey(curr, obj[curr])), {})
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Envoke the "npm install" command for the provided project directory.
|
|
473
|
+
*
|
|
474
|
+
* @param {*} outputDir
|
|
475
|
+
* @param {*} param1
|
|
476
|
+
*/
|
|
477
|
+
const npmInstall = (outputDir, {verbose}) => {
|
|
478
|
+
console.log('Installing dependencies... This may take a few minutes.\n')
|
|
479
|
+
const npmLogLevel = verbose ? 'notice' : 'error'
|
|
480
|
+
const disableStdOut = ['inherit', 'ignore', 'inherit']
|
|
481
|
+
const stdio = verbose ? 'inherit' : disableStdOut
|
|
482
|
+
try {
|
|
483
|
+
child_proc.execSync(`npm install --color always --loglevel ${npmLogLevel}`, {
|
|
484
|
+
cwd: outputDir,
|
|
485
|
+
stdio,
|
|
486
|
+
env: {
|
|
487
|
+
...process.env,
|
|
488
|
+
OPENCOLLECTIVE_HIDE: 'true',
|
|
489
|
+
DISABLE_OPENCOLLECTIVE: 'true',
|
|
490
|
+
OPEN_SOURCE_CONTRIBUTOR: 'true'
|
|
491
|
+
}
|
|
492
|
+
})
|
|
493
|
+
} catch {
|
|
494
|
+
// error is already displayed on the console by child process.
|
|
495
|
+
// exit the program
|
|
496
|
+
process.exit(1)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Execute and copy the handlebars template to the output directory using
|
|
502
|
+
* the provided context object. If the file isn't a template, simply copy
|
|
503
|
+
* it to the destination.
|
|
504
|
+
*
|
|
505
|
+
* @param {string} inputFile
|
|
506
|
+
* @param {string} outputDir
|
|
507
|
+
* @param {Object} context
|
|
508
|
+
*/
|
|
509
|
+
const processTemplate = (relFile, inputDir, outputDir, context) => {
|
|
510
|
+
const inputFile = p.join(inputDir, relFile)
|
|
511
|
+
const outputFile = p.join(outputDir, relFile)
|
|
512
|
+
const destDir = p.join(outputFile, '..')
|
|
513
|
+
|
|
514
|
+
// Create folder if we are doing a deep copy
|
|
515
|
+
if (destDir) {
|
|
516
|
+
fs.mkdirSync(destDir, {recursive: true})
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (inputFile.endsWith('.hbs')) {
|
|
520
|
+
const template = sh.cat(inputFile).stdout
|
|
521
|
+
fs.writeFileSync(outputFile.replace('.hbs', ''), Handlebars.compile(template)(context))
|
|
522
|
+
} else {
|
|
523
|
+
fs.copyFileSync(inputFile, outputFile)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* This function does the bulk of the project generation given the project config
|
|
529
|
+
* object and the answers returned from the survey process.
|
|
530
|
+
*
|
|
531
|
+
* @param {*} preset
|
|
532
|
+
* @param {*} answers
|
|
533
|
+
* @param {*} param2
|
|
534
|
+
*/
|
|
535
|
+
const runGenerator = (context, {outputDir, templateVersion, verbose}) => {
|
|
536
|
+
const {answers, preset} = context
|
|
537
|
+
const {templateSource} = preset
|
|
538
|
+
const {extend = false} = answers.project
|
|
539
|
+
|
|
540
|
+
// Check if the output directory doesn't already exist.
|
|
541
|
+
checkOutputDir(outputDir)
|
|
542
|
+
|
|
543
|
+
// We need to get some assets from the base template. So extract it after
|
|
544
|
+
// downloading from NPM or copying from the template bundle folder.
|
|
545
|
+
const tmp = fs.mkdtempSync(p.resolve(os.tmpdir(), 'extract-template'))
|
|
546
|
+
const packagePath = p.join(tmp, 'package')
|
|
547
|
+
const {id, type} = templateSource
|
|
548
|
+
let tarPath
|
|
549
|
+
|
|
550
|
+
switch (type) {
|
|
551
|
+
case TEMPLATE_SOURCE_NPM: {
|
|
552
|
+
const tarFile = sh
|
|
553
|
+
.exec(`npm pack ${id}@${templateVersion} --pack-destination="${tmp}"`, {
|
|
554
|
+
silent: true
|
|
555
|
+
})
|
|
556
|
+
.stdout.trim()
|
|
557
|
+
tarPath = p.join(tmp, tarFile)
|
|
558
|
+
break
|
|
559
|
+
}
|
|
560
|
+
case TEMPLATE_SOURCE_BUNDLE:
|
|
561
|
+
tarPath = p.join(__dirname, '..', 'templates', `${id}.tar.gz`)
|
|
562
|
+
break
|
|
563
|
+
default: {
|
|
564
|
+
const msg = `Error: Cannot handle template source type ${type}.`
|
|
565
|
+
console.error(msg)
|
|
566
|
+
process.exit(1)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Extract the source
|
|
571
|
+
tar.x({
|
|
572
|
+
file: tarPath,
|
|
573
|
+
cwd: tmp,
|
|
574
|
+
sync: true
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
if (extend) {
|
|
578
|
+
// Bootstrap the projects.
|
|
579
|
+
getFiles(BOOTSTRAP_DIR)
|
|
580
|
+
.map((file) => file.replace(BOOTSTRAP_DIR, ''))
|
|
581
|
+
.forEach((relFilePath) =>
|
|
582
|
+
processTemplate(relFilePath, BOOTSTRAP_DIR, outputDir, context)
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
// Copy required assets defind on the preset level.
|
|
586
|
+
const {assets = []} = preset
|
|
587
|
+
assets.forEach((asset) => {
|
|
588
|
+
sh.cp('-rf', p.join(packagePath, asset), outputDir)
|
|
589
|
+
})
|
|
590
|
+
} else {
|
|
591
|
+
// Copy the base template either from the package or npm.
|
|
592
|
+
sh.cp('-rf', packagePath, outputDir)
|
|
593
|
+
|
|
594
|
+
// Copy template specific assets over.
|
|
595
|
+
const assetsDir = p.join(ASSETS_TEMPLATES_DIR, id)
|
|
596
|
+
if (sh.test('-e', assetsDir)) {
|
|
597
|
+
getFiles(assetsDir)
|
|
598
|
+
.map((file) => file.replace(assetsDir, ''))
|
|
599
|
+
.forEach((relFilePath) =>
|
|
600
|
+
processTemplate(relFilePath, assetsDir, outputDir, context)
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Update the generated projects version. NOTE: For bootstrapped projects this
|
|
605
|
+
// can be done in the template building. But since we have two types of project builds,
|
|
606
|
+
// (bootstrap/bundle) we'll do it here where it works in both scenarios.
|
|
607
|
+
const pkgJsonPath = p.resolve(outputDir, 'package.json')
|
|
608
|
+
const pkgJSON = readJson(pkgJsonPath)
|
|
609
|
+
const finalPkgData = merge(pkgJSON, {
|
|
610
|
+
name: slugifyName(context.answers.project.name || context.preset.id),
|
|
611
|
+
version: GENERATED_PROJECT_VERSION
|
|
612
|
+
})
|
|
613
|
+
writeJson(pkgJsonPath, finalPkgData)
|
|
614
|
+
|
|
615
|
+
// Clean up
|
|
616
|
+
sh.rm('-rf', tmp)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Install dependencies for the newly minted project.
|
|
620
|
+
npmInstall(outputDir, {verbose})
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const foundNode = process.versions.node
|
|
624
|
+
const requiredNode = generatorPkg.engines.node
|
|
625
|
+
const isUsingCompatibleNode = semver.satisfies(foundNode, new semver.Range(requiredNode))
|
|
626
|
+
|
|
627
|
+
const main = async (opts) => {
|
|
628
|
+
if (!isUsingCompatibleNode) {
|
|
629
|
+
console.log('')
|
|
630
|
+
console.warn(
|
|
631
|
+
`Warning: You are using Node ${foundNode}. ` +
|
|
632
|
+
`Your app may not work as expected when deployed to Managed ` +
|
|
633
|
+
`Runtime servers which are compatible with Node ${requiredNode}`
|
|
634
|
+
)
|
|
635
|
+
console.log('')
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// The context object will have all the current information, like the selected preset, the answers
|
|
639
|
+
// to "general" and "project" questions. It'll also be populated with details of the selected project,
|
|
640
|
+
// like its `package.json` value.
|
|
641
|
+
let context = INITIAL_CONTEXT
|
|
642
|
+
let {outputDir, verbose, preset, templateVersion} = opts
|
|
643
|
+
const {prompt} = inquirer
|
|
644
|
+
const OUTPUT_DIR_FLAG_ACTIVE = !!outputDir
|
|
645
|
+
const presetId = preset || process.env.GENERATOR_PRESET
|
|
646
|
+
|
|
647
|
+
// Exit if the preset provided is not valid.
|
|
648
|
+
if (presetId && !validPreset(presetId)) {
|
|
649
|
+
console.error(
|
|
650
|
+
`The preset "${presetId}" is not valid. Valid presets are: ${
|
|
651
|
+
process.env.GENERATOR_PRESET
|
|
652
|
+
? ALL_PRESET_NAMES.map((x) => `"${x}"`).join(' ')
|
|
653
|
+
: PUBLIC_PRESET_NAMES.map((x) => `"${x}"`).join(' ')
|
|
654
|
+
}.`
|
|
655
|
+
)
|
|
656
|
+
process.exit(1)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// If there is no preset arg, prompt the user with a selection of presets.
|
|
660
|
+
if (!presetId) {
|
|
661
|
+
context.answers = await prompt(PRESET_QUESTIONS)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Add the selected preset to the context object.
|
|
665
|
+
const selectedPreset = PRESETS.find(
|
|
666
|
+
({id}) => id === (presetId || context.answers.general.presetId)
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
// Add the preset to the context.
|
|
670
|
+
context.preset = selectedPreset
|
|
671
|
+
|
|
672
|
+
if (!OUTPUT_DIR_FLAG_ACTIVE) {
|
|
673
|
+
outputDir = p.join(process.cwd(), selectedPreset.id)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Ask preset specific questions and merge into the current context.
|
|
677
|
+
const {questions = {}, answers = {}} = selectedPreset
|
|
678
|
+
if (questions) {
|
|
679
|
+
const projectAnswers = await prompt(questions, answers)
|
|
680
|
+
|
|
681
|
+
context = merge(context, {
|
|
682
|
+
answers: expandObject(projectAnswers)
|
|
683
|
+
})
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Inject the packageJSON into the context for extensibile projects.
|
|
687
|
+
if (context.answers.project.extend) {
|
|
688
|
+
const pkgJSON = JSON.parse(
|
|
689
|
+
sh.exec(`npm view ${selectedPreset.templateSource.id}@${templateVersion} --json`, {
|
|
690
|
+
silent: true
|
|
691
|
+
}).stdout
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
// NOTE: Here we are rewriting a specific script (extract-default-translations) in order
|
|
695
|
+
// to update the script location for extensibility. In the future we'll hopefully
|
|
696
|
+
// move transations outside of the template and into the sdk where the script for
|
|
697
|
+
// building translations will ultimately live, meaning we won't have to do this. So
|
|
698
|
+
// its OK for now.
|
|
699
|
+
if (pkgJSON?.scripts['extract-default-translations']) {
|
|
700
|
+
pkgJSON.scripts['extract-default-translations'] = pkgJSON.scripts[
|
|
701
|
+
'extract-default-translations'
|
|
702
|
+
].replace('./', `./node_modules/${selectedPreset.templateSource.id}/`)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
context = merge(
|
|
706
|
+
context,
|
|
707
|
+
expandObject({
|
|
708
|
+
['answers.general.packageJSON']: pkgJSON
|
|
709
|
+
})
|
|
710
|
+
)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Generate the project.
|
|
714
|
+
runGenerator(context, {outputDir, templateVersion, verbose})
|
|
715
|
+
|
|
716
|
+
// Return the folder in which the project was generated in.
|
|
717
|
+
return outputDir
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (require.main === module) {
|
|
721
|
+
program.name(`pwa-kit-create-app`)
|
|
722
|
+
program.description(`Generate a new PWA Kit project, optionally using a preset.
|
|
723
|
+
|
|
724
|
+
Examples:
|
|
725
|
+
|
|
726
|
+
${PRESETS.filter(({private}) => !private).map(({id, description}) => {
|
|
727
|
+
return `
|
|
728
|
+
${program.name()} --preset "${id}"\n${description}
|
|
729
|
+
`
|
|
730
|
+
})}
|
|
731
|
+
|
|
732
|
+
`)
|
|
733
|
+
program
|
|
734
|
+
.option('--outputDir <path>', `Path to the output directory for the new project`)
|
|
735
|
+
.option(
|
|
736
|
+
'--preset <name>',
|
|
737
|
+
`The name of a project preset to use (choices: ${PUBLIC_PRESET_NAMES.map(
|
|
738
|
+
(x) => `"${x}"`
|
|
739
|
+
).join(', ')})`
|
|
740
|
+
)
|
|
741
|
+
.option(
|
|
742
|
+
'--templateVersion <version>',
|
|
743
|
+
`The version of the template to be generated when it's source is NPM.`,
|
|
744
|
+
DEFAULT_TEMPLATE_VERSION
|
|
745
|
+
)
|
|
746
|
+
.option('--verbose', `Print additional logging information to the console.`, false)
|
|
747
|
+
|
|
748
|
+
program.parse(process.argv)
|
|
749
|
+
|
|
750
|
+
Promise.resolve()
|
|
751
|
+
.then(() => main(program.opts()))
|
|
752
|
+
.then((outputDir) => {
|
|
753
|
+
console.log('')
|
|
754
|
+
console.log(
|
|
755
|
+
`Successfully generated a project in ${outputDir ? outputDir : program.outputDir}`
|
|
756
|
+
)
|
|
757
|
+
process.exit(0)
|
|
758
|
+
})
|
|
759
|
+
.catch((err) => {
|
|
760
|
+
console.error('Failed to generate a project')
|
|
761
|
+
console.error(err)
|
|
762
|
+
process.exit(1)
|
|
763
|
+
})
|
|
764
|
+
}
|