create-analog 3.0.0-alpha.6 → 3.0.0-alpha.61

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