create-analog 3.0.0-alpha.5 → 3.0.0-alpha.50

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 +330 -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 +12 -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 +12 -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 +12 -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,78 @@ 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';
509
+ pkg.dependencies.nitro = '3.0.260415-beta';
510
+ pkg.dependencies.ofetch = '2.0.0-alpha.3';
392
511
  }
393
512
  }
394
513
 
514
+ /**
515
+ * @param {string} root
516
+ * @param {PackageJson} pkg
517
+ * @param {HighlighterId} highlighter
518
+ * @returns {void}
519
+ */
395
520
  function ensureSyntaxHighlighter(root, pkg, highlighter) {
521
+ const config = HIGHLIGHTERS[highlighter];
522
+
396
523
  replacePlaceholders(root, 'src/app/app.config.ts', {
397
- __HIGHLIGHTER__: HIGHLIGHTERS[highlighter].highlighter,
398
- __HIGHLIGHTER_ENTRY_POINT__: HIGHLIGHTERS[highlighter].entryPoint,
524
+ __HIGHLIGHTER__: config.highlighter,
525
+ __HIGHLIGHTER_ENTRY_POINT__: config.entryPoint,
399
526
  });
400
527
 
401
- const dependencies = HIGHLIGHTERS[highlighter].dependencies;
402
- for (const [name, version] of Object.entries(dependencies)) {
528
+ pkg.dependencies ??= {};
529
+ for (const [name, version] of Object.entries(config.dependencies)) {
403
530
  pkg.dependencies[name] = version;
404
531
  }
405
532
 
@@ -408,44 +535,94 @@ function ensureSyntaxHighlighter(root, pkg, highlighter) {
408
535
  });
409
536
  }
410
537
 
538
+ /**
539
+ * @param {Record<string, string>} obj
540
+ * @returns {Record<string, string>}
541
+ */
411
542
  function sortObjectKeys(obj) {
412
543
  return Object.keys(obj)
413
544
  .sort()
414
545
  .reduce((result, key) => {
415
546
  result[key] = obj[key];
416
547
  return result;
417
- }, {});
548
+ }, /** @type {Record<string, string>} */ ({}));
418
549
  }
419
550
 
551
+ /**
552
+ * @param {string} root
553
+ * @param {string} title
554
+ * @returns {void}
555
+ */
420
556
  function setProjectTitle(root, title) {
421
557
  replacePlaceholders(root, ['index.html', 'README.md'], {
422
558
  __PROJECT_TITLE__: title,
423
559
  });
424
560
  }
425
561
 
562
+ /**
563
+ * @param {string} root
564
+ * @param {string | readonly string[]} files
565
+ * @param {Record<string, string>} config
566
+ * @returns {void}
567
+ */
426
568
  function replacePlaceholders(root, files, config) {
427
- for (const file of toFlatArray(files)) {
569
+ for (const file of toArray(files)) {
428
570
  const filePath = path.join(root, file);
429
571
  const fileContent = fs.readFileSync(filePath, 'utf-8');
430
572
  const newFileContent = Object.keys(config).reduce(
431
573
  (content, placeholder) =>
432
- content.replace(RegExp(placeholder, 'g'), config[placeholder]),
574
+ content.replaceAll(placeholder, config[placeholder]),
433
575
  fileContent,
434
576
  );
435
577
  fs.writeFileSync(filePath, newFileContent);
436
578
  }
437
579
  }
438
580
 
439
- function toFlatArray(value) {
440
- return (Array.isArray(value) ? value : [value]).filter(Boolean).flat();
581
+ /**
582
+ * @param {string | readonly string[] | undefined | null} value
583
+ * @returns {string[]}
584
+ */
585
+ function toArray(value) {
586
+ if (value == null) {
587
+ return [];
588
+ }
589
+
590
+ return Array.isArray(value) ? [...value] : [/** @type {string} */ (value)];
441
591
  }
442
592
 
593
+ /**
594
+ * @param {unknown} arg
595
+ * @returns {boolean | undefined}
596
+ */
443
597
  function fromBoolArg(arg) {
444
- return ['boolean', 'undefined'].includes(typeof arg)
445
- ? arg
446
- : ['', 'true'].includes(arg);
598
+ if (typeof arg === 'boolean' || typeof arg === 'undefined') {
599
+ return arg;
600
+ }
601
+
602
+ if (typeof arg !== 'string') {
603
+ return undefined;
604
+ }
605
+
606
+ return arg === '' || arg === 'true';
607
+ }
608
+
609
+ /**
610
+ * @param {string | undefined} value
611
+ * @returns {Template | undefined}
612
+ */
613
+ function resolveTemplate(value) {
614
+ return isTemplate(value) ? value : undefined;
615
+ }
616
+
617
+ /**
618
+ * @param {string | undefined} value
619
+ * @returns {value is Template}
620
+ */
621
+ function isTemplate(value) {
622
+ return value === 'latest' || value === 'blog' || value === 'minimal';
447
623
  }
448
624
 
449
- init().catch((e) => {
450
- console.error(e);
625
+ init().catch((error) => {
626
+ console.error(error);
627
+ process.exitCode = 1;
451
628
  });