create-analog 3.0.0-alpha.2 → 3.0.0-alpha.20

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