create-analog 3.0.0-alpha.4 → 3.0.0-alpha.41

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.
Files changed (32) hide show
  1. package/index.js +328 -153
  2. package/package.json +7 -4
  3. package/template-angular-v17/package.json +8 -8
  4. package/template-angular-v17/tsconfig.json +1 -4
  5. package/template-angular-v17/tsconfig.spec.json +2 -1
  6. package/template-angular-v17/vite.config.ts +4 -2
  7. package/template-angular-v18/package.json +9 -9
  8. package/template-angular-v18/tsconfig.json +1 -4
  9. package/template-angular-v18/tsconfig.spec.json +2 -1
  10. package/template-angular-v18/vite.config.ts +4 -2
  11. package/template-angular-v19/package.json +9 -9
  12. package/template-angular-v19/tsconfig.json +0 -6
  13. package/template-angular-v19/tsconfig.spec.json +2 -1
  14. package/template-angular-v19/vite.config.ts +2 -2
  15. package/template-angular-v20/package.json +9 -9
  16. package/template-angular-v20/tsconfig.json +0 -5
  17. package/template-angular-v20/tsconfig.spec.json +2 -1
  18. package/template-angular-v20/vite.config.ts +2 -2
  19. package/template-blog/package.json +11 -10
  20. package/template-blog/src/app/app.config.ts +6 -2
  21. package/template-blog/src/test-setup.ts +1 -0
  22. package/template-blog/tsconfig.json +3 -8
  23. package/template-blog/tsconfig.spec.json +2 -1
  24. package/template-blog/vite.config.ts +3 -3
  25. package/template-latest/package.json +11 -10
  26. package/template-latest/src/test-setup.ts +1 -0
  27. package/template-latest/tsconfig.json +3 -8
  28. package/template-latest/tsconfig.spec.json +2 -1
  29. package/template-latest/vite.config.ts +3 -3
  30. package/template-minimal/package.json +11 -10
  31. package/template-minimal/tsconfig.json +3 -9
  32. package/template-minimal/vite.config.ts +3 -3
package/index.js CHANGED
@@ -3,17 +3,73 @@
3
3
  // @ts-check
4
4
  import { blue, green, red, reset, yellow } from 'kolorist';
5
5
  import minimist from 'minimist';
6
- import { execSync } from 'node:child_process';
6
+ import { execFileSync } from 'node:child_process';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import prompts from 'prompts';
11
11
 
12
+ /**
13
+ * @typedef {'latest' | 'blog' | 'minimal'} Template
14
+ * @typedef {'prismjs' | 'shiki'} HighlighterId
15
+ * @typedef {(value: string) => string} Colorizer
16
+ *
17
+ * @typedef {object} Variant
18
+ * @property {string} name
19
+ * @property {Template} template
20
+ * @property {Colorizer} color
21
+ *
22
+ * @typedef {object} AppDefinition
23
+ * @property {string} name
24
+ * @property {Colorizer} color
25
+ * @property {readonly Variant[]} variants
26
+ *
27
+ * @typedef {object} HighlighterConfig
28
+ * @property {string} highlighter
29
+ * @property {string} entryPoint
30
+ * @property {Record<string, string>} dependencies
31
+ *
32
+ * @typedef {object} PackageJson
33
+ * @property {string} [name]
34
+ * @property {Record<string, string>} [scripts]
35
+ * @property {Record<string, string>} [dependencies]
36
+ * @property {Record<string, string>} [devDependencies]
37
+ *
38
+ * @typedef {object} PromptAnswers
39
+ * @property {string} [projectName]
40
+ * @property {boolean} [overwrite]
41
+ * @property {string} [packageName]
42
+ * @property {Template} [variant]
43
+ * @property {boolean} [tailwind]
44
+ * @property {HighlighterId} [syntaxHighlighter]
45
+ *
46
+ * @typedef {object} UserAgentPackage
47
+ * @property {string} name
48
+ * @property {string} version
49
+ *
50
+ * @typedef {{
51
+ * _: string[];
52
+ * template?: string;
53
+ * t?: string;
54
+ * skipTailwind?: boolean | string;
55
+ * skipGit?: boolean | string;
56
+ * } & Record<string, unknown>} CliArgv
57
+ */
58
+
59
+ const CLI_DIR = path.dirname(fileURLToPath(import.meta.url));
60
+ const DEFAULT_TARGET_DIR = 'analog-project';
61
+ const DEFAULT_BLOG_HIGHLIGHTER = 'prismjs';
62
+
63
+ /** @type {readonly Template[]} */
64
+ const H3_TEMPLATES = ['latest', 'blog', 'minimal'];
65
+
12
66
  // Avoids autoconversion to number of the project name by defining that the args
13
67
  // non associated with an option ( _ ) needs to be parsed as a string. See #4606
68
+ /** @type {CliArgv} */
14
69
  const argv = minimist(process.argv.slice(2), { string: ['_'] });
15
70
  const cwd = process.cwd();
16
71
 
72
+ /** @type {readonly AppDefinition[]} */
17
73
  const APPS = [
18
74
  {
19
75
  name: 'Analog',
@@ -37,6 +93,8 @@ const APPS = [
37
93
  ],
38
94
  },
39
95
  ];
96
+
97
+ /** @type {Readonly<Record<HighlighterId, HighlighterConfig>>} */
40
98
  const HIGHLIGHTERS = {
41
99
  prismjs: {
42
100
  highlighter: 'withPrismHighlighter',
@@ -50,118 +108,134 @@ const HIGHLIGHTERS = {
50
108
  highlighter: 'withShikiHighlighter',
51
109
  entryPoint: 'shiki-highlighter',
52
110
  dependencies: {
53
- marked: '^15.0.7',
111
+ marked: '^18.0.0',
54
112
  'marked-shiki': '^1.1.0',
55
113
  shiki: '^1.6.1',
56
114
  },
57
115
  },
58
116
  };
59
117
 
118
+ /** @type {Readonly<Record<string, string>>} */
60
119
  const renameFiles = {
61
120
  _gitignore: '.gitignore',
62
121
  };
63
122
 
123
+ const TAILWIND_POSTCSS_CONFIG = `export default {
124
+ plugins: {
125
+ '@tailwindcss/postcss': {},
126
+ },
127
+ };
128
+ `;
129
+
64
130
  async function init() {
65
131
  let targetDir = formatTargetDir(argv._[0]);
66
- let template = argv.template || argv.t;
132
+ let template = resolveTemplate(argv.template ?? argv.t);
67
133
  let skipTailwind = fromBoolArg(argv.skipTailwind);
134
+ const skipGit = fromBoolArg(argv.skipGit ?? argv['skip-git']) ?? false;
68
135
 
69
- const defaultTargetDir = 'analog-project';
70
136
  const getProjectName = () =>
71
- targetDir === '.' ? path.basename(path.resolve()) : targetDir;
137
+ targetDir === '.' ? path.basename(path.resolve()) : (targetDir ?? '');
72
138
 
139
+ /** @type {PromptAnswers} */
73
140
  let result = {};
74
141
 
75
142
  try {
76
- result = await prompts(
77
- [
78
- {
79
- type: targetDir ? null : 'text',
80
- name: 'projectName',
81
- message: reset('Project name:'),
82
- initial: defaultTargetDir,
83
- onState: (state) => {
84
- targetDir = formatTargetDir(state.value) || defaultTargetDir;
143
+ result = /** @type {PromptAnswers} */ (
144
+ await prompts(
145
+ [
146
+ {
147
+ type: targetDir ? null : 'text',
148
+ name: 'projectName',
149
+ message: reset('Project name:'),
150
+ initial: DEFAULT_TARGET_DIR,
151
+ onState: (state) => {
152
+ targetDir =
153
+ formatTargetDir(String(state.value ?? '')) ||
154
+ DEFAULT_TARGET_DIR;
155
+ },
85
156
  },
86
- },
87
- {
88
- type: () =>
89
- !fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm',
90
- name: 'overwrite',
91
- message: () =>
92
- (targetDir === '.'
93
- ? 'Current directory'
94
- : `Target directory "${targetDir}"`) +
95
- ` is not empty. Remove existing files and continue?`,
96
- },
97
- {
98
- type: (_, { overwrite } = {}) => {
99
- if (overwrite === false) {
100
- throw new Error(red('✖') + ' Operation cancelled');
101
- }
102
- return null;
157
+ {
158
+ type: () =>
159
+ !targetDir || !fs.existsSync(targetDir) || isEmpty(targetDir)
160
+ ? null
161
+ : 'confirm',
162
+ name: 'overwrite',
163
+ message: () =>
164
+ (targetDir === '.'
165
+ ? 'Current directory'
166
+ : `Target directory "${targetDir}"`) +
167
+ ' is not empty. Remove existing files and continue?',
103
168
  },
104
- name: 'overwriteChecker',
105
- },
106
- {
107
- type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
108
- name: 'packageName',
109
- message: reset('Package name:'),
110
- initial: () => toValidPackageName(getProjectName()),
111
- validate: (dir) =>
112
- isValidPackageName(dir) || 'Invalid package.json name',
113
- },
114
- {
115
- type: template ? null : 'select',
116
- name: 'variant',
117
- message: reset('What would you like to start?:'),
118
- // @ts-ignore
119
- choices: APPS[0].variants.map((variant) => {
120
- const variantColor = variant.color;
121
- return {
122
- title: variantColor(variant.name),
169
+ {
170
+ type: (_, promptState = {}) => {
171
+ if (promptState.overwrite === false) {
172
+ throw new Error(`${red('✖')} Operation cancelled`);
173
+ }
174
+ return null;
175
+ },
176
+ name: 'overwriteChecker',
177
+ },
178
+ {
179
+ type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
180
+ name: 'packageName',
181
+ message: reset('Package name:'),
182
+ initial: () => toValidPackageName(getProjectName()),
183
+ validate: (dir) =>
184
+ isValidPackageName(String(dir)) || 'Invalid package.json name',
185
+ },
186
+ {
187
+ type: template ? null : 'select',
188
+ name: 'variant',
189
+ message: reset('What would you like to start?:'),
190
+ choices: APPS[0].variants.map((variant) => ({
191
+ title: variant.color(variant.name),
123
192
  value: variant.template,
124
- };
125
- }),
126
- },
127
- {
128
- type: (prev) => (prev === 'blog' ? 'select' : null),
129
- name: 'syntaxHighlighter',
130
- message: reset('Choose a syntax highlighter:'),
131
- choices: Object.keys(HIGHLIGHTERS).map((highlighter) => ({
132
- title: highlighter,
133
- value: highlighter,
134
- })),
135
- initial: 1,
136
- },
193
+ })),
194
+ },
195
+ {
196
+ type: (prev) => (prev === 'blog' ? 'select' : null),
197
+ name: 'syntaxHighlighter',
198
+ message: reset('Choose a syntax highlighter:'),
199
+ choices:
200
+ /** @type {{ title: HighlighterId; value: HighlighterId }[]} */ (
201
+ Object.keys(HIGHLIGHTERS).map((highlighter) => ({
202
+ title: /** @type {HighlighterId} */ (highlighter),
203
+ value: /** @type {HighlighterId} */ (highlighter),
204
+ }))
205
+ ),
206
+ initial: 1,
207
+ },
208
+ {
209
+ type: skipTailwind === undefined ? 'confirm' : null,
210
+ name: 'tailwind',
211
+ message: 'Would you like to add Tailwind to your project?',
212
+ },
213
+ ],
137
214
  {
138
- type: skipTailwind === undefined ? 'confirm' : null,
139
- name: 'tailwind',
140
- message: 'Would you like to add Tailwind to your project?',
141
- },
142
- ],
143
- {
144
- onCancel: () => {
145
- throw new Error(red('✖') + ' Operation cancelled');
215
+ onCancel: () => {
216
+ throw new Error(`${red('')} Operation cancelled`);
217
+ },
146
218
  },
147
- },
219
+ )
148
220
  );
149
- } catch (cancelled) {
150
- console.log(cancelled.message);
221
+ } catch (error) {
222
+ console.log(error instanceof Error ? error.message : String(error));
151
223
  return;
152
224
  }
153
225
 
154
- // user choice associated with prompts
155
- const {
156
- framework,
157
- overwrite,
158
- packageName,
159
- variant,
160
- tailwind,
161
- syntaxHighlighter,
162
- } = result;
226
+ const { overwrite, packageName, variant, tailwind, syntaxHighlighter } =
227
+ result;
163
228
 
164
- const root = path.join(cwd, targetDir);
229
+ template = variant ?? template;
230
+ if (!template) {
231
+ throw new Error('A project template must be selected.');
232
+ }
233
+
234
+ const highlighter =
235
+ syntaxHighlighter ??
236
+ (template === 'blog' ? DEFAULT_BLOG_HIGHLIGHTER : undefined);
237
+
238
+ const root = path.join(cwd, targetDir ?? DEFAULT_TARGET_DIR);
165
239
 
166
240
  if (overwrite) {
167
241
  emptyDir(root);
@@ -169,58 +243,58 @@ async function init() {
169
243
  fs.mkdirSync(root, { recursive: true });
170
244
  }
171
245
 
172
- // determine template
173
- template = variant || framework || template;
174
- // determine syntax highlighter
175
- const highlighter =
176
- syntaxHighlighter ?? (template === 'blog' ? 'prism' : null);
177
246
  skipTailwind = skipTailwind ?? !tailwind;
178
247
 
179
248
  console.log(`\nScaffolding project in ${root}...`);
180
249
 
181
- const templateDir = path.resolve(
182
- fileURLToPath(import.meta.url),
183
- '..',
184
- `template-${template}`,
185
- );
186
-
187
- const filesDir = path.resolve(fileURLToPath(import.meta.url), '..', `files`);
250
+ const templateDir = path.resolve(CLI_DIR, `template-${template}`);
251
+ const filesDir = path.resolve(CLI_DIR, 'files');
188
252
 
253
+ /**
254
+ * @param {string} file
255
+ * @param {string | undefined} [content]
256
+ */
189
257
  const write = (file, content) => {
190
258
  const targetPath = renameFiles[file]
191
259
  ? path.join(root, renameFiles[file])
192
260
  : path.join(root, file);
193
261
 
194
- if (content) {
262
+ if (typeof content === 'string') {
195
263
  fs.writeFileSync(targetPath, content);
196
- } else {
197
- copy(path.join(templateDir, file), targetPath);
264
+ return;
198
265
  }
266
+
267
+ copy(path.join(templateDir, file), targetPath);
199
268
  };
200
269
 
201
270
  const files = fs.readdirSync(templateDir);
202
- for (const file of files.filter((f) => f !== 'package.json')) {
271
+ for (const file of files.filter((entry) => entry !== 'package.json')) {
203
272
  write(file);
204
273
  }
205
274
 
206
275
  if (!skipTailwind) {
207
276
  addTailwindDirectives(write, filesDir);
277
+ write('postcss.config.mjs', TAILWIND_POSTCSS_CONFIG);
208
278
  }
209
279
 
210
280
  replacePlaceholders(root, 'vite.config.ts', {
211
281
  __TAILWIND_IMPORT__: !skipTailwind
212
- ? `\nimport tailwindcss from '@tailwindcss/vite';`
282
+ ? "import tailwindcss from '@tailwindcss/vite';\n"
213
283
  : '',
214
- __TAILWIND_PLUGIN__: !skipTailwind ? '\n tailwindcss()' : '',
284
+ __TAILWIND_PLUGIN__: !skipTailwind ? ' tailwindcss(),\n' : '',
215
285
  });
216
286
 
217
- const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
218
- const pkgManager = pkgInfo ? pkgInfo.name : 'npm';
287
+ /** @type {PackageJson} */
219
288
  const pkg = JSON.parse(
220
- fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8'),
289
+ fs.readFileSync(path.join(templateDir, 'package.json'), 'utf-8'),
221
290
  );
291
+ const pkgManager =
292
+ pkgFromUserAgent(process.env.npm_config_user_agent)?.name ?? 'npm';
222
293
 
223
294
  pkg.name = packageName || getProjectName();
295
+ pkg.scripts ??= {};
296
+ pkg.dependencies ??= {};
297
+ pkg.devDependencies ??= {};
224
298
  pkg.scripts.start = getStartCommand(pkgManager);
225
299
 
226
300
  if (template === 'blog' && highlighter) {
@@ -246,18 +320,22 @@ async function init() {
246
320
 
247
321
  setProjectTitle(root, getProjectName());
248
322
 
249
- console.log(`\nInitializing git repository:`);
250
- execSync(`git init ${targetDir} && cd ${targetDir} && git add .`);
251
-
252
- // Fail Silent
253
- // Can fail when user does not have global git credentials
254
- try {
255
- execSync(`cd ${targetDir} && git commit -m "initial commit"`);
256
- } catch {
257
- /* ignore */
323
+ if (!skipGit) {
324
+ console.log('\nInitializing git repository:');
325
+ execFileSync('git', ['init', targetDir], { stdio: 'inherit' });
326
+ execFileSync('git', ['-C', targetDir, 'add', '.'], { stdio: 'inherit' });
327
+
328
+ // Can fail when the user does not have global git credentials.
329
+ try {
330
+ execFileSync('git', ['-C', targetDir, 'commit', '-m', 'initial commit'], {
331
+ stdio: 'inherit',
332
+ });
333
+ } catch {
334
+ /* ignore */
335
+ }
258
336
  }
259
337
 
260
- console.log(`\nDone. Now run:\n`);
338
+ console.log('\nDone. Now run:\n');
261
339
  if (root !== cwd) {
262
340
  console.log(` cd ${path.relative(cwd, root)}`);
263
341
  }
@@ -268,22 +346,30 @@ async function init() {
268
346
 
269
347
  /**
270
348
  * @param {string | undefined} targetDir
349
+ * @returns {string | undefined}
271
350
  */
272
351
  function formatTargetDir(targetDir) {
273
352
  return targetDir?.trim().replace(/\/+$/g, '');
274
353
  }
275
354
 
355
+ /**
356
+ * @param {string} src
357
+ * @param {string} dest
358
+ * @returns {void}
359
+ */
276
360
  function copy(src, dest) {
277
361
  const stat = fs.statSync(src);
278
362
  if (stat.isDirectory()) {
279
363
  copyDir(src, dest);
280
- } else {
281
- fs.copyFileSync(src, dest);
364
+ return;
282
365
  }
366
+
367
+ fs.copyFileSync(src, dest);
283
368
  }
284
369
 
285
370
  /**
286
371
  * @param {string} projectName
372
+ * @returns {boolean}
287
373
  */
288
374
  function isValidPackageName(projectName) {
289
375
  return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
@@ -293,6 +379,7 @@ function isValidPackageName(projectName) {
293
379
 
294
380
  /**
295
381
  * @param {string} projectName
382
+ * @returns {string}
296
383
  */
297
384
  function toValidPackageName(projectName) {
298
385
  return projectName
@@ -306,6 +393,7 @@ function toValidPackageName(projectName) {
306
393
  /**
307
394
  * @param {string} srcDir
308
395
  * @param {string} destDir
396
+ * @returns {void}
309
397
  */
310
398
  function copyDir(srcDir, destDir) {
311
399
  fs.mkdirSync(destDir, { recursive: true });
@@ -317,42 +405,49 @@ function copyDir(srcDir, destDir) {
317
405
  }
318
406
 
319
407
  /**
320
- * @param {string} path
408
+ * @param {string} directoryPath
409
+ * @returns {boolean}
321
410
  */
322
- function isEmpty(path) {
323
- const files = fs.readdirSync(path);
411
+ function isEmpty(directoryPath) {
412
+ const files = fs.readdirSync(directoryPath);
324
413
  return files.length === 0 || (files.length === 1 && files[0] === '.git');
325
414
  }
326
415
 
327
416
  /**
328
417
  * @param {string} dir
418
+ * @returns {void}
329
419
  */
330
420
  function emptyDir(dir) {
331
421
  if (!fs.existsSync(dir)) {
332
422
  return;
333
423
  }
424
+
334
425
  for (const file of fs.readdirSync(dir)) {
335
426
  fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });
336
427
  }
337
428
  }
338
429
 
339
430
  /**
340
- * @param {string | undefined} userAgent process.env.npm_config_user_agent
341
- * @returns object | undefined
431
+ * @param {string | undefined} userAgent
432
+ * @returns {UserAgentPackage | undefined}
342
433
  */
343
434
  function pkgFromUserAgent(userAgent) {
344
- if (!userAgent) return undefined;
435
+ if (!userAgent) {
436
+ return undefined;
437
+ }
438
+
345
439
  const pkgSpec = userAgent.split(' ')[0];
346
- const pkgSpecArr = pkgSpec.split('/');
347
- return {
348
- name: pkgSpecArr[0],
349
- version: pkgSpecArr[1],
350
- };
440
+ const [name, version] = pkgSpec.split('/');
441
+ if (!name || !version) {
442
+ return undefined;
443
+ }
444
+
445
+ return { name, version };
351
446
  }
352
447
 
353
448
  /**
354
449
  * @param {string} pkgManager
355
- * @returns string
450
+ * @returns {string}
356
451
  */
357
452
  function getInstallCommand(pkgManager) {
358
453
  return pkgManager === 'yarn' ? 'yarn' : `${pkgManager} install`;
@@ -360,46 +455,76 @@ function getInstallCommand(pkgManager) {
360
455
 
361
456
  /**
362
457
  * @param {string} pkgManager
363
- * @returns string
458
+ * @returns {string}
364
459
  */
365
460
  function getStartCommand(pkgManager) {
366
461
  return pkgManager === 'yarn' ? 'yarn dev' : `${pkgManager} run dev`;
367
462
  }
368
463
 
464
+ /**
465
+ * @param {(file: string, content?: string) => void} write
466
+ * @param {string} filesDir
467
+ * @returns {void}
468
+ */
369
469
  function addTailwindDirectives(write, filesDir) {
370
470
  write(
371
471
  'src/styles.css',
372
- fs.readFileSync(path.join(filesDir, `styles.css`), 'utf-8'),
472
+ fs.readFileSync(path.join(filesDir, 'styles.css'), 'utf-8'),
373
473
  );
374
474
  }
375
475
 
476
+ /**
477
+ * @param {PackageJson} pkg
478
+ * @returns {void}
479
+ */
376
480
  function addTailwindDependencies(pkg) {
377
- pkg.dependencies['tailwindcss'] = '^4.1.4';
378
- pkg.dependencies['postcss'] = '^8.5.3';
379
- pkg.dependencies['@tailwindcss/vite'] = '^4.1.4';
481
+ pkg.devDependencies ??= {};
482
+ pkg.devDependencies.postcss = '^8.5.6';
483
+ pkg.devDependencies.tailwindcss = '^4.2.2';
484
+ pkg.devDependencies['@tailwindcss/postcss'] = '^4.2.2';
485
+ pkg.devDependencies['@tailwindcss/vite'] = '^4.2.2';
380
486
  }
381
487
 
488
+ /**
489
+ * @param {PackageJson} pkg
490
+ * @param {Template} template
491
+ * @returns {void}
492
+ */
382
493
  function addYarnDevDependencies(pkg, template) {
383
- // v18
384
- if (template === 'latest' || template === 'blog' || template === 'minimal') {
385
- pkg.devDependencies['h3'] = '^1.13.0';
494
+ if (H3_TEMPLATES.includes(template)) {
495
+ pkg.devDependencies ??= {};
496
+ pkg.devDependencies.h3 = '^1.13.0';
386
497
  }
387
498
  }
388
499
 
500
+ /**
501
+ * @param {PackageJson} pkg
502
+ * @param {Template} template
503
+ * @returns {void}
504
+ */
389
505
  function addPnpmDependencies(pkg, template) {
390
- if (template === 'latest' || template === 'blog' || template === 'minimal') {
391
- pkg.dependencies['h3'] = '^1.13.0';
506
+ if (H3_TEMPLATES.includes(template)) {
507
+ pkg.dependencies ??= {};
508
+ pkg.dependencies.h3 = '^1.13.0';
392
509
  }
393
510
  }
394
511
 
512
+ /**
513
+ * @param {string} root
514
+ * @param {PackageJson} pkg
515
+ * @param {HighlighterId} highlighter
516
+ * @returns {void}
517
+ */
395
518
  function ensureSyntaxHighlighter(root, pkg, highlighter) {
519
+ const config = HIGHLIGHTERS[highlighter];
520
+
396
521
  replacePlaceholders(root, 'src/app/app.config.ts', {
397
- __HIGHLIGHTER__: HIGHLIGHTERS[highlighter].highlighter,
398
- __HIGHLIGHTER_ENTRY_POINT__: HIGHLIGHTERS[highlighter].entryPoint,
522
+ __HIGHLIGHTER__: config.highlighter,
523
+ __HIGHLIGHTER_ENTRY_POINT__: config.entryPoint,
399
524
  });
400
525
 
401
- const dependencies = HIGHLIGHTERS[highlighter].dependencies;
402
- for (const [name, version] of Object.entries(dependencies)) {
526
+ pkg.dependencies ??= {};
527
+ for (const [name, version] of Object.entries(config.dependencies)) {
403
528
  pkg.dependencies[name] = version;
404
529
  }
405
530
 
@@ -408,44 +533,94 @@ function ensureSyntaxHighlighter(root, pkg, highlighter) {
408
533
  });
409
534
  }
410
535
 
536
+ /**
537
+ * @param {Record<string, string>} obj
538
+ * @returns {Record<string, string>}
539
+ */
411
540
  function sortObjectKeys(obj) {
412
541
  return Object.keys(obj)
413
542
  .sort()
414
543
  .reduce((result, key) => {
415
544
  result[key] = obj[key];
416
545
  return result;
417
- }, {});
546
+ }, /** @type {Record<string, string>} */ ({}));
418
547
  }
419
548
 
549
+ /**
550
+ * @param {string} root
551
+ * @param {string} title
552
+ * @returns {void}
553
+ */
420
554
  function setProjectTitle(root, title) {
421
555
  replacePlaceholders(root, ['index.html', 'README.md'], {
422
556
  __PROJECT_TITLE__: title,
423
557
  });
424
558
  }
425
559
 
560
+ /**
561
+ * @param {string} root
562
+ * @param {string | readonly string[]} files
563
+ * @param {Record<string, string>} config
564
+ * @returns {void}
565
+ */
426
566
  function replacePlaceholders(root, files, config) {
427
- for (const file of toFlatArray(files)) {
567
+ for (const file of toArray(files)) {
428
568
  const filePath = path.join(root, file);
429
569
  const fileContent = fs.readFileSync(filePath, 'utf-8');
430
570
  const newFileContent = Object.keys(config).reduce(
431
571
  (content, placeholder) =>
432
- content.replace(RegExp(placeholder, 'g'), config[placeholder]),
572
+ content.replaceAll(placeholder, config[placeholder]),
433
573
  fileContent,
434
574
  );
435
575
  fs.writeFileSync(filePath, newFileContent);
436
576
  }
437
577
  }
438
578
 
439
- function toFlatArray(value) {
440
- return (Array.isArray(value) ? value : [value]).filter(Boolean).flat();
579
+ /**
580
+ * @param {string | readonly string[] | undefined | null} value
581
+ * @returns {string[]}
582
+ */
583
+ function toArray(value) {
584
+ if (value == null) {
585
+ return [];
586
+ }
587
+
588
+ return Array.isArray(value) ? [...value] : [/** @type {string} */ (value)];
441
589
  }
442
590
 
591
+ /**
592
+ * @param {unknown} arg
593
+ * @returns {boolean | undefined}
594
+ */
443
595
  function fromBoolArg(arg) {
444
- return ['boolean', 'undefined'].includes(typeof arg)
445
- ? arg
446
- : ['', 'true'].includes(arg);
596
+ if (typeof arg === 'boolean' || typeof arg === 'undefined') {
597
+ return arg;
598
+ }
599
+
600
+ if (typeof arg !== 'string') {
601
+ return undefined;
602
+ }
603
+
604
+ return arg === '' || arg === 'true';
605
+ }
606
+
607
+ /**
608
+ * @param {string | undefined} value
609
+ * @returns {Template | undefined}
610
+ */
611
+ function resolveTemplate(value) {
612
+ return isTemplate(value) ? value : undefined;
613
+ }
614
+
615
+ /**
616
+ * @param {string | undefined} value
617
+ * @returns {value is Template}
618
+ */
619
+ function isTemplate(value) {
620
+ return value === 'latest' || value === 'blog' || value === 'minimal';
447
621
  }
448
622
 
449
- init().catch((e) => {
450
- console.error(e);
623
+ init().catch((error) => {
624
+ console.error(error);
625
+ process.exitCode = 1;
451
626
  });