@rshono/create 1.0.0-rc.12 → 1.0.0-rc.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lasse Tange
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- // Thin launcher, kept deliberately old-syntax and dependency-free: it is the one file that has to
3
- // parse and run on whatever Node the user happens to have. The CLI itself is a bundle — its imports
4
- // are evaluated before any statement in it, so a version check *inside* it would be too late to
5
- // produce anything friendlier than a stack trace.
2
+ // Thin launcher, deliberately old-syntax and dependency-free: it is the one file that has to parse and run on
3
+ // whatever Node the user happens to have. A version check inside the CLI bundle would be too late — its imports
4
+ // evaluate before any statement in it.
5
+ //
6
6
  // Kept in step with `engines.node` in @rshono/core's manifest, which is what a scaffolded app pins.
7
7
  var MINIMUM = [22, 18, 0];
8
8
 
package/dist/api.mjs CHANGED
@@ -52,12 +52,12 @@ __webpack_require__.d(__webpack_exports__, {
52
52
 
53
53
  ;// CONCATENATED MODULE: ./src/features/combinations.ts
54
54
  /**
55
- * Biome's CSS parser rejects Tailwind's syntax — `@apply` and a `@layer` block are parse errors, not
56
- * unknown-at-rule warnings — so a project with both needs Biome pointed away from stylesheets. The
57
- * overlay is a second `biome.json` listed after the first, which is all "later overlay wins" means.
55
+ * Biome's CSS parser rejects Tailwind's syntax — `@apply` is a parse error, not an unknown-at-rule warning — so
56
+ * a project with both needs Biome pointed away from stylesheets. The overlay is a second `biome.json` listed
57
+ * after the first.
58
58
  *
59
- * Narrow on purpose: the alternative was excluding CSS for everybody, which would quietly give up
60
- * formatting the plain-CSS template that Biome handles perfectly well.
59
+ * Narrow on purpose: excluding CSS for everybody would give up formatting the plain-CSS template that Biome
60
+ * handles perfectly well.
61
61
  */ const BIOME_TAILWIND = {
62
62
  id: 'biome-tailwind',
63
63
  overlays: [
@@ -83,14 +83,12 @@ __webpack_require__.d(__webpack_exports__, {
83
83
  /**
84
84
  * The scripts every app gets, whatever it targets.
85
85
  *
86
- * `start`, `preview` and `deploy` are deliberately not here: each one means something different per
87
- * platform, so the deploy target contributes its own. The three names are a contract the targets keep,
88
- * and the reason the README can describe an app it was not written for:
86
+ * `start`, `preview` and `deploy` are not here: each means something different per platform, so the deploy
87
+ * target contributes its own. The three names are a contract the targets keep, which is what lets the README
88
+ * describe an app it was not written for:
89
89
  *
90
- * - **`start`** runs a build that already exists and never makes one — what a host's own start command
91
- * calls. Only the target whose build is a server has one.
92
- * - **`preview`** builds, then runs the result here — for the targets where that is not the same two
93
- * commands, because otherwise the production build is unanswerable without deploying it.
90
+ * - **`start`** runs a build that already exists and never makes one — what a host's own start command calls.
91
+ * - **`preview`** builds, then runs the result here, so the production build is answerable without deploying.
94
92
  * - **`deploy`** builds, then ships it — where the platform has one command that does the shipping.
95
93
  */ const BASE_SCRIPTS = {
96
94
  dev: 'rshono dev',
@@ -98,9 +96,8 @@ __webpack_require__.d(__webpack_exports__, {
98
96
  typecheck: 'tsc --noEmit'
99
97
  };
100
98
  /**
101
- * Every script the app gets, in the order they are written: the base ones, then each feature's, in the order
102
- * the features were selected. The manifest and the README's command table are both this, so neither can
103
- * document a script the other does not have.
99
+ * Every script the app gets, in the order they are written: the base ones, then each feature's. The manifest and
100
+ * the README's command table are both this, so neither can document a script the other lacks.
104
101
  */ function buildScripts(features) {
105
102
  const scripts = {
106
103
  ...BASE_SCRIPTS
@@ -116,9 +113,8 @@ __webpack_require__.d(__webpack_exports__, {
116
113
  };
117
114
  }
118
115
  /**
119
- * Script names a package manager has a command of its own for. `pnpm deploy` runs pnpm's workspace-deploy
120
- * command and never looks at the manifest, so printing it would hand somebody a line that quietly does
121
- * something else. The explicit `run` form is what all four managers accept, so those names get it.
116
+ * Script names a package manager has a command of its own for: `pnpm deploy` runs pnpm's workspace-deploy and
117
+ * never looks at the manifest. These get the explicit `run` form, which all four managers accept.
122
118
  */ const SHADOWED = new Set([
123
119
  'deploy'
124
120
  ]);
@@ -126,10 +122,9 @@ __webpack_require__.d(__webpack_exports__, {
126
122
  return `${SHADOWED.has(script) ? `${pm.name} run` : pm.run} ${script}`;
127
123
  }
128
124
  /**
129
- * The README's command table: one line per script, with the command to type and a one-line gloss.
130
- *
131
- * A script whose feature supplies no `scriptHelp` is left out and covered by the "package.json has the
132
- * rest" line — that is how a formatter's `format:check` stays out of a table about running the app.
125
+ * The README's command table: one line per script, with the command to type and a one-line gloss. A script whose
126
+ * feature supplies no `scriptHelp` is left out — that is how `format:check` stays out of a table about running
127
+ * the app and covered by the line pointing at `package.json`.
133
128
  */ function scriptTable(answers, features, pm) {
134
129
  const help = baseScriptHelp(answers.deploy);
135
130
  for (const feature of features)Object.assign(help, feature.scriptHelp);
@@ -138,31 +133,28 @@ __webpack_require__.d(__webpack_exports__, {
138
133
  return documented.map((name)=>`${invoke(pm, name).padEnd(width)} # ${help[name]}`).join('\n');
139
134
  }
140
135
  /**
141
- * The command that gets this app into production, as a sentence — the README's deploy step, and the same
142
- * choice the closing summary makes, so the two cannot name different commands.
136
+ * The command that gets this app into production, as a sentence — the README's deploy step and the closing
137
+ * summary's, so the two cannot name different commands.
143
138
  *
144
139
  * Read off the scripts rather than the target, so a target that gains a `deploy` gains the sentence with it.
145
- * `start` is the answer where there is no `deploy`: it ships nothing itself, but it is what the host runs.
146
140
  */ function deployStep(features, pm) {
147
141
  const scripts = buildScripts(features);
148
142
  if (scripts.deploy) return `\`${invoke(pm, 'deploy')}\` does the build and the upload in one step.`;
149
143
  if (scripts.start) return `\`${invoke(pm, 'start')}\` runs that build wherever you host it, and never makes one.`;
150
- // Every branch names a script the app has, so a target that contributes none of the three still reads true.
151
144
  if (scripts.preview) return `\`${invoke(pm, 'preview')}\` runs the build here, so you can check it first.`;
152
145
  return `\`${invoke(pm, 'build')}\` produces it; getting it there is yours to script.`;
153
146
  }
154
147
 
155
148
  ;// CONCATENATED MODULE: ./src/generated/framework.ts
156
149
  // GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
157
- /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.12';
150
+ /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.14';
158
151
  /**
159
- * The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
160
- * or a contributor on an older Node hears it from their package manager rather than from a stack trace.
152
+ * The Node range rshono declares, restated in every scaffolded app's `engines` — so a CI image or a contributor
153
+ * on an older Node hears it from their package manager rather than from a stack trace.
161
154
  */ const NODE_ENGINE = '>=22.18.0';
162
155
  /**
163
- * The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
164
- * exact — React's RSC internals are coupled across builds, and a generated app has no workspace
165
- * overrides to fall back on.
156
+ * The dependency versions rshono is tested against, copied from its own manifest. Exact where it is exact:
157
+ * React's RSC internals are coupled across builds, and a generated app has no workspace overrides.
166
158
  */ const FRAMEWORK_DEPS = {
167
159
  hono: '^4.12.31',
168
160
  react: '19.2.8',
@@ -195,14 +187,12 @@ __webpack_require__.d(__webpack_exports__, {
195
187
  /** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
196
188
  /** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
197
189
  /**
198
- * Versions for the optional tooling the features can add — the one place in this package where a
199
- * dependency range is typed out by hand, because none of these are things the framework itself
200
- * declares. Everything a *scaffolded app* needs to run rshono comes from {@link FRAMEWORK_DEPS},
201
- * which is generated from rshono's own manifest and must not be edited here.
190
+ * Versions for the optional tooling the features can add — the one place in this package where a range is typed
191
+ * by hand, because the framework declares none of these. Everything a scaffolded app needs to *run* rshono comes
192
+ * from {@link FRAMEWORK_DEPS}, generated from rshono's own manifest.
202
193
  *
203
- * Ranges are caret, not exact: these are the app's own dev tools, and a scaffold made six months from
204
- * now should pick up their patch releases rather than pinning whatever was current the day this file
205
- * was last touched.
194
+ * Caret ranges, not exact: these are the app's own dev tools, and a scaffold made six months from now should
195
+ * pick up their patch releases.
206
196
  */ const TOOL_VERSIONS = {
207
197
  tailwindcss: '^4.3.3',
208
198
  '@tailwindcss/postcss': '^4.3.3',
@@ -214,23 +204,20 @@ __webpack_require__.d(__webpack_exports__, {
214
204
  oxlint: '^1.76.0',
215
205
  oxfmt: '^0.61.0',
216
206
  eslint: '^10.8.0',
217
- // ESLint's own recommended JavaScript rules, which typescript-eslint layers on top of rather than
218
- // replaces, and the rules of hooks — the one class of React mistake no type checker sees.
207
+ // ESLint's recommended JavaScript rules, which typescript-eslint layers on rather than replaces, and the rules
208
+ // of hooks — the one class of React mistake no type checker sees.
219
209
  '@eslint/js': '^10.0.1',
220
210
  'typescript-eslint': '^8.65.0',
221
211
  'eslint-plugin-react-hooks': '^7.1.1',
222
212
  wrangler: '^4.115.0'
223
213
  };
224
214
  /**
225
- * The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
215
+ * The TypeScript an ESLint app pins in place of the framework's own — the one deliberate exception to
226
216
  * {@link FRAMEWORK_DEPS}.
227
217
  *
228
- * typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface,
229
- * so it accepts `typescript >=4.8.4 <6.1.0` and nothing above; `~6.0.3` is the newest that satisfies
230
- * it. rshono's declarations compile the same under either, which is what makes this an app's business
231
- * and not the framework's.
232
- *
233
- * When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
218
+ * typescript-eslint reads TypeScript's compiler API directly, so it accepts `>=4.8.4 <6.1.0` and nothing above;
219
+ * `~6.0.3` is the newest that satisfies it. rshono's declarations compile the same under either, which is what
220
+ * makes this the app's business. When upstream widens its range, this constant is what to delete.
234
221
  */ const ESLINT_TYPESCRIPT = '~6.0.3';
235
222
 
236
223
  ;// CONCATENATED MODULE: ./src/features/deploy.ts
@@ -239,22 +226,17 @@ __webpack_require__.d(__webpack_exports__, {
239
226
  /**
240
227
  * What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
241
228
  *
242
- * Deliberately thin: the framework arranges its own output for every platform, and `rshono build`
243
- * writes the one platform config that has to exist (`wrangler.jsonc`) if the project has none a second
244
- * copy generated here would only go stale. So a target contributes the commands that run and ship the
245
- * build, the CLI they need, the directories to gitignore, and a note for the step no command covers.
246
- *
247
- * What the three script names promise is documented once, above `BASE_SCRIPTS` in `scripts.ts`. Only `node`
248
- * has a `start`, because `rshono start` refuses a bundle built for anywhere else, and it needs no `preview`:
249
- * `build` then `start` already is one.
229
+ * Thin on purpose: the framework arranges its own output for every platform, and `rshono build` writes the one
230
+ * platform config that has to exist. So a target contributes the commands that run and ship the build, the CLI
231
+ * they need, the directories to gitignore, and a note for the step no command covers.
250
232
  *
251
- * `pm` is here because two things have to be spelled for the app's package manager — the runner that fetches
252
- * the uninstalled Vercel CLI ({@link PackageManager.dlx}), and every command in {@link Feature.platformSetup}.
233
+ * What the three script names promise is documented above `BASE_SCRIPTS` in `scripts.ts`. Only `node` has a
234
+ * `start` `rshono start` refuses a bundle built for anywhere else — and it needs no `preview`, since `build`
235
+ * then `start` already is one.
253
236
  */ function deployFeatures(pm) {
254
237
  const build = invoke(pm, 'build');
255
238
  return {
256
- // Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
257
- // guess — so the target contributes only the command that runs what was built.
239
+ // Where a Node build goes next is a Dockerfile or a process manager, neither of which this can guess.
258
240
  node: {
259
241
  id: 'deploy-node',
260
242
  scripts: {
@@ -277,16 +259,14 @@ __webpack_require__.d(__webpack_exports__, {
277
259
  devDependencies: {
278
260
  wrangler: TOOL_VERSIONS.wrangler
279
261
  },
280
- // The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each
281
- // one merely picks the platform binary out of the optional dependency that already carries it, so
282
- // neither needs to run — `workerd --version` and `esbuild --version` both answer without it.
262
+ // The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each merely
263
+ // picks the platform binary out of the optional dependency already carrying it, so neither needs to run.
283
264
  allowBuilds: {
284
265
  esbuild: false,
285
266
  workerd: false
286
267
  },
287
- // `wrangler dev` is the one preview that runs the code in the runtime it will actually run in:
288
- // workerd, not Node, serving the assets the build assembled. Both scripts read the wrangler.jsonc the
289
- // build wrote, so nothing here has to know where the bundle or the assets went.
268
+ // `wrangler dev` is the one preview that runs the code in workerd rather than Node. Both scripts read the
269
+ // wrangler.jsonc the build wrote, so nothing here has to know where the bundle went.
290
270
  scripts: {
291
271
  preview: 'rshono build && wrangler dev',
292
272
  deploy: 'rshono build && wrangler deploy'
@@ -308,9 +288,8 @@ __webpack_require__.d(__webpack_exports__, {
308
288
  },
309
289
  vercel: {
310
290
  id: 'deploy-vercel',
311
- // `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview
312
- // URL, which is a useful thing to have but not what the word means. The local `preview` is a Node
313
- // build run here — the platform has no way to run its own prebuilt output on your machine.
291
+ // `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview URL.
292
+ // `preview` is a Node build run here the platform cannot run its own prebuilt output on your machine.
314
293
  scripts: {
315
294
  preview: 'rshono build --deploy node && rshono start',
316
295
  deploy: `rshono build && ${pm.dlx} vercel deploy --prebuilt --prod`
@@ -336,8 +315,7 @@ __webpack_require__.d(__webpack_exports__, {
336
315
  },
337
316
  'aws-lambda': {
338
317
  id: 'deploy-aws-lambda',
339
- // No CLI to wrap, and no upload this could guess at. `preview` still applies the bundle is a Node
340
- // handler, so it runs here.
318
+ // No CLI to wrap and no upload to guess at, but `preview` still applies: the bundle is a Node handler.
341
319
  scripts: {
342
320
  preview: 'rshono build --deploy node && rshono start'
343
321
  },
@@ -349,8 +327,9 @@ __webpack_require__.d(__webpack_exports__, {
349
327
  ],
350
328
  platformSetup: [
351
329
  `There is no settings page here; the upload is yours to script. A job needs \`${pm.name} install && ${build}\`,`,
352
- 'then the function package: `dist/`, plus `node_modules` for any dependency of your own, which the server',
353
- 'bundle leaves external.'
330
+ 'then the function package: `dist/` and nothing else, with the handler at `dist/server/main.mjs`. Your',
331
+ 'dependencies are compiled into the bundle for this target, so no `node_modules` is uploaded — which also',
332
+ 'means a native addon fails the build here rather than the deploy.'
354
333
  ].join('\n')
355
334
  }
356
335
  };
@@ -362,14 +341,11 @@ function deployFeature(target, pm) {
362
341
  ;// CONCATENATED MODULE: ./src/features/quality.ts
363
342
 
364
343
  /**
365
- * The formatter and linter features. Biome answers to both slots and appears once `selectFeatures`
366
- * deduplicates by `id`, so `formatter: 'biome', linter: 'biome'` contributes one set of files, one
367
- * dependency and one pair of scripts.
344
+ * The formatter and linter features. Biome answers to both slots and appears once, because `selectFeatures`
345
+ * deduplicates by `id`.
368
346
  *
369
- * A formatter brings `format:check` beside `format`, because the writing half and the CI half want
370
- * different exit-code behaviour: `format` rewrites files, `format:check` fails instead. A linter brings
371
- * `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
372
- * one. Biome adds a `check` of its own, which is the pair of them in a single pass.
347
+ * Each brings a pair of scripts, since the writing half and the CI half want different exit codes: `format`
348
+ * rewrites and `format:check` fails, `lint:fix` rewrites and `lint` fails. Biome's `check` is both in one pass.
373
349
  */ const PRETTIER = {
374
350
  id: 'prettier',
375
351
  overlays: [
@@ -410,11 +386,10 @@ const OXLINT = {
410
386
  }
411
387
  };
412
388
  /**
413
- * The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
414
- * be installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one
415
- * its peer range accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to
416
- * reach for ESLint over a syntax-only linter — so the config it ships hands the parser the whole
417
- * program rather than linting file by file.
389
+ * The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
390
+ * installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest its peer range
391
+ * accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to reach for ESLint at all —
392
+ * so the config it ships hands the parser the whole program.
418
393
  */ const ESLINT = {
419
394
  id: 'eslint',
420
395
  overlays: [
@@ -430,6 +405,12 @@ const OXLINT = {
430
405
  scripts: {
431
406
  lint: 'eslint .',
432
407
  'lint:fix': 'eslint . --fix'
408
+ },
409
+ // The pin is invisible from the command line, and its consequence is not: the rules reason about the
410
+ // program through an older compiler than the one that builds it, so a fix can produce code `tsc` rejects.
411
+ // The generated eslint.config.mjs explains why; the README's table is where someone reads what to type.
412
+ scriptHelp: {
413
+ 'lint:fix': 'apply what it can — then run typecheck, see eslint.config.mjs'
433
414
  }
434
415
  };
435
416
  const BIOME = {
@@ -470,13 +451,12 @@ function linterFeature(linter) {
470
451
  ;// CONCATENATED MODULE: ./src/features/styling.ts
471
452
 
472
453
  /**
473
- * Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
474
- * plus an overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts
475
- * postcss-loader in front of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten
476
- * in utilities.
454
+ * Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages, plus an
455
+ * overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts postcss-loader in front
456
+ * of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten in utilities.
477
457
  *
478
- * `postcss` and `postcss-loader` are the app's dependencies rather than the framework's rshono
479
- * compiles CSS natively, so an app that does not want a plugin chain does not install one.
458
+ * `postcss` is the app's dependency rather than the framework's: rshono compiles CSS natively, so an app that
459
+ * wants no plugin chain installs none.
480
460
  */ const TAILWIND = {
481
461
  id: 'tailwind',
482
462
  overlays: [
@@ -499,12 +479,11 @@ function stylingFeature(styling) {
499
479
 
500
480
 
501
481
  /**
502
- * The features a set of answers selects, in application order — so an overlay listed later wins a file
503
- * both of them ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome
504
- * is both the formatter and the linter) without contributing twice.
482
+ * The features a set of answers selects, in application order — so an overlay listed later wins a file both
483
+ * ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome is both formatter and
484
+ * linter) without contributing twice.
505
485
  *
506
- * `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled
507
- * CLI; nothing else here depends on which package manager the app is for.
486
+ * `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled CLI.
508
487
  */ function selectFeatures(answers, pm) {
509
488
  const selected = [
510
489
  deployFeature(answers.deploy, pm),
@@ -526,9 +505,8 @@ function stylingFeature(styling) {
526
505
  ;// CONCATENATED MODULE: ./src/options.ts
527
506
 
528
507
  /*
529
- * The names each option accepts, spelled once. The types below are derived from them, the CLI validates
530
- * its flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them
531
- * — so a name added here reaches all three without a second list to remember.
508
+ * The names each option accepts, spelled once: the types below are derived from them, the CLI validates its
509
+ * flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them.
532
510
  */ const FORMATTER_NAMES = [
533
511
  'prettier',
534
512
  'biome',
@@ -589,14 +567,13 @@ const QUALITY_PRESETS = [
589
567
  }
590
568
  ];
591
569
  /**
592
- * Turns whatever the user typed into a name npm will accept, or returns `null` when nothing usable is
593
- * left. Lowercasing and replacing runs of invalid characters covers the ordinary cases (`My App`,
594
- * `my_app`); a scoped name is kept intact, since `@scope/name` is legal.
570
+ * Turns whatever the user typed into a name npm will accept, or `null` when nothing usable is left.
571
+ * Lowercasing and replacing runs of invalid characters covers the ordinary cases (`My App`, `my_app`).
595
572
  */ function toPackageName(input) {
596
573
  const trimmed = input.trim().replace(/^\.\/+/, '').replace(/\/+$/, '');
597
574
  if (!trimmed || trimmed === '.') return null;
598
- // A scoped name is a name, not a path: `@scope/pkg` stays whole rather than becoming `pkg`. Anything
599
- // else is a path, and the last segment is the one that names the project.
575
+ // A scoped name is a name, not a path: `@scope/pkg` stays whole. Anything else is a path, whose last
576
+ // segment names the project.
600
577
  const scoped = /^@[^\\/]+[\\/][^\\/]+$/.test(trimmed);
601
578
  const base = scoped ? trimmed : trimmed.split(/[\\/]/).filter(Boolean).pop() ?? '';
602
579
  if (!base) return null;
@@ -630,15 +607,11 @@ function sorted(record) {
630
607
  return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
631
608
  }
632
609
  /**
633
- * pnpm's settings for the new app, written only when a feature has something to put in them. `null`
634
- * means there is nothing to say, so no file is written.
635
- *
636
- * It exists for one field, `allowBuilds`. pnpm fails an install — and every `pnpm dev` after it —
637
- * until the project has said whether a dependency's install script should run. Nothing rshono itself
638
- * installs has one, so most apps get no file; the packages that do (wrangler's esbuild and workerd)
639
- * declare their answer on the feature that brings them.
610
+ * pnpm's settings for the new app, or `null` when no feature has anything to put in them.
640
611
  *
641
- * A file rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
612
+ * It exists for one field, `allowBuilds`: pnpm fails an install until the project has said whether a
613
+ * dependency's install script should run. Nothing rshono installs has one, so most apps get no file. A file
614
+ * rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
642
615
  */ function buildPnpmSettings(features) {
643
616
  const allowBuilds = {};
644
617
  for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
@@ -654,10 +627,8 @@ function sorted(record) {
654
627
  ].join('\n');
655
628
  }
656
629
  /**
657
- * Assembles `package.json` from the answers and whatever the selected features contribute.
658
- *
659
- * Dependencies are sorted by name and scripts keep the order {@link buildScripts} gives them — so two runs
660
- * with the same answers produce byte-identical output, which is what makes the manifest snapshot-testable.
630
+ * Assembles `package.json` from the answers and whatever the selected features contribute. Dependencies are
631
+ * sorted and scripts keep {@link buildScripts}'s order, so two runs with the same answers are byte-identical.
661
632
  */ function buildPackageJson(answers, features, pm) {
662
633
  const dependencies = {
663
634
  '@rshono/core': RSHONO_RANGE,
@@ -687,8 +658,8 @@ function sorted(record) {
687
658
  dependencies: sorted(dependencies),
688
659
  devDependencies: sorted(devDependencies)
689
660
  };
690
- // Only when the environment told us the exact version: `packageManager` pins the tool for Corepack,
691
- // and a guess at the version is worse than leaving the field out.
661
+ // Only when the environment told us the exact version: this field pins the tool for Corepack, and a guess is
662
+ // worse than leaving it out.
692
663
  if (pm.version) manifest.packageManager = `${pm.name}@${pm.version}`;
693
664
  const ordered = Object.fromEntries(FIELD_ORDER.filter((field)=>field in manifest).map((field)=>[
694
665
  field,
@@ -700,25 +671,22 @@ function sorted(record) {
700
671
  ;// CONCATENATED MODULE: ./src/render.ts
701
672
 
702
673
  /**
703
- * `{{NAME}}`, deliberately not `__NAME__`: templates are real files that real tools run over, and in
704
- * markdown `__NAME__` *is* strong emphasis Prettier rewrites it to `**NAME**` and the token stops
705
- * matching. `{{…}}` means nothing to any format these templates are written in.
674
+ * `{{NAME}}`, not `__NAME__`: in markdown the latter is strong emphasis, so Prettier rewrites it to `**NAME**`
675
+ * and the token stops matching. `{{…}}` means nothing to any format these templates are written in.
706
676
  */ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
707
677
  function tokensFor(answers, features, pm) {
708
678
  return {
709
679
  '{{PROJECT_NAME}}': answers.packageName,
710
680
  '{{DEPLOY_TARGET}}': answers.deploy,
711
- // Derived from the features rather than the answers, because all three are about the scripts the app
712
- // actually got: its command table, the one command that ships it, and what its platform asks for.
681
+ // From the features rather than the answers, because all three are about the scripts the app actually got.
713
682
  '{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
714
683
  '{{DEPLOY_STEP}}': deployStep(features, pm),
715
684
  '{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
716
685
  };
717
686
  }
718
687
  /**
719
- * Substitutes tokens, and throws on one it doesn't know — a typo in a template would otherwise ship a
720
- * literal `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would
721
- * catch.
688
+ * Substitutes tokens, and throws on one it does not know — a typo in a template would otherwise ship a literal
689
+ * `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would catch.
722
690
  */ function render(contents, tokens, source) {
723
691
  return contents.replace(TOKEN_PATTERN, (token)=>{
724
692
  const value = tokens[token];
@@ -748,11 +716,9 @@ function readTemplateDir(dir) {
748
716
  return files;
749
717
  }
750
718
  /**
751
- * `_gitignore` → `.gitignore`, and so on for every dotfile.
752
- *
753
- * npm strips a literal `.gitignore` out of a published tarball, so a template cannot contain one — it
754
- * would exist in the repo, pass every local test, and be missing from the package everybody installs.
755
- * Applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
719
+ * `_gitignore` → `.gitignore`, and so on for every dotfile: npm strips a literal `.gitignore` out of a
720
+ * published tarball, so a template that contained one would pass every local test and be missing from the
721
+ * package everybody installs. The basename only, so `templates/_x/y.ts` is left alone.
756
722
  */ function undotted(path) {
757
723
  const segments = path.split(posix.sep);
758
724
  const name = segments.pop();
@@ -761,19 +727,16 @@ function readTemplateDir(dir) {
761
727
  name.startsWith('_') ? `.${name.slice(1)}` : name
762
728
  ].join(posix.sep);
763
729
  }
764
- /**
765
- * A feature's `.gitignore` lines, appended under a heading naming it — so somebody reading the file six
766
- * months later can tell why `.wrangler/` is in there.
767
- */ function appendGitignore(existing, features) {
730
+ /** A feature's `.gitignore` lines, under a heading naming it — so a reader can tell why `.wrangler/` is there. */ function appendGitignore(existing, features) {
768
731
  const additions = features.filter((feature)=>feature.gitignore?.length);
769
732
  if (additions.length === 0) return existing;
770
733
  const blocks = additions.map((feature)=>`\n# ${feature.id}\n${feature.gitignore.join('\n')}\n`);
771
734
  return existing + blocks.join('');
772
735
  }
773
736
  /**
774
- * Turns answers into the exact set of files to write, without touching the target directory the
775
- * decisions and the I/O are separated so the whole matrix of answers can be asserted on in a test, and
776
- * so `--dry-run` is the same code path minus the last step.
737
+ * Turns answers into the exact set of files to write, without touching the target directory. Separating the
738
+ * decisions from the I/O is what lets a test assert on the whole matrix of answers, and makes `--dry-run` the
739
+ * same code path minus the last step.
777
740
  */ function plan_plan(answers, pm) {
778
741
  const features = selectFeatures(answers, pm);
779
742
  const tokens = tokensFor(answers, features, pm);
@@ -847,11 +810,9 @@ function packageManager(name, version) {
847
810
  };
848
811
  }
849
812
  /**
850
- * Which package manager invoked us. Every one of them sets `npm_config_user_agent` on the process it
851
- * spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm
852
- * project without asking, and the exact version comes along for the `packageManager` field.
853
- *
854
- * Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
813
+ * Which package manager invoked us. All four set `npm_config_user_agent` on the process they spawn — `pnpm/11.9.0
814
+ * npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm project without asking, exact
815
+ * version included. Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
855
816
  */ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
856
817
  const [spec] = (userAgent ?? '').split(' ');
857
818
  const [name, version] = (spec ?? '').split('/');
@@ -859,9 +820,8 @@ function packageManager(name, version) {
859
820
  return packageManager('npm');
860
821
  }
861
822
  /**
862
- * Runs the install, streaming its output. `shell: true` on Windows because npm, pnpm and yarn are all
863
- * `.cmd` shims there, which `spawn` cannot execute directly — and with a shell involved the arguments
864
- * are all fixed strings from the tables above, never anything the user typed.
823
+ * Runs the install, streaming its output. `shell: true` on Windows, where npm, pnpm and yarn are `.cmd` shims
824
+ * `spawn` cannot execute directly — safe because every argument is a fixed string from the tables above.
865
825
  */ function runInstall(pm, cwd) {
866
826
  return run(pm, pm.install, cwd);
867
827
  }
@@ -877,10 +837,7 @@ function run(pm, args, cwd) {
877
837
  ;// CONCATENATED MODULE: ./src/write.ts
878
838
 
879
839
 
880
- /**
881
- * Files that do not make a directory "occupied". A user who ran `git init` or opened the folder in an
882
- * editor before scaffolding has not put anything in it that we would overwrite.
883
- */ const IGNORED_ENTRIES = new Set([
840
+ /** Files that do not make a directory "occupied": a `git init` or an editor has put nothing there to overwrite. */ const IGNORED_ENTRIES = new Set([
884
841
  '.git',
885
842
  '.DS_Store',
886
843
  '.idea',
@@ -888,12 +845,9 @@ function run(pm, args, cwd) {
888
845
  'Thumbs.db'
889
846
  ]);
890
847
  /**
891
- * What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind —
892
- * which is what decides whether scaffolding into it is safe.
893
- *
894
- * A path that does not exist yet is no conflict. A path that exists and is *not* a directory throws
895
- * rather than reporting an empty list, since `--force` should not write into one either — otherwise
896
- * `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
848
+ * What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind — which is
849
+ * what decides whether scaffolding into it is safe. A path that does not exist yet is no conflict; one that
850
+ * exists and is not a directory throws, since `--force` should not write into it either.
897
851
  */ function conflictingEntries(dir) {
898
852
  const stats = statSync(dir, {
899
853
  throwIfNoEntry: false
@@ -903,8 +857,8 @@ function run(pm, args, cwd) {
903
857
  return readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
904
858
  }
905
859
  /**
906
- * Writes the plan. Directories are created as needed, and files are written with the plan's own
907
- * ordering so a failure part-way through leaves something a person can make sense of.
860
+ * Writes the plan, creating directories as needed and keeping the plan's own ordering — so a failure part-way
861
+ * through leaves something a person can make sense of.
908
862
  */ function writePlan(plan, targetDir) {
909
863
  mkdirSync(targetDir, {
910
864
  recursive: true
package/dist/cli.mjs CHANGED
@@ -2906,8 +2906,8 @@ function hasGit(cwd) {
2906
2906
  ], cwd).ok;
2907
2907
  }
2908
2908
  /**
2909
- * Whether the new project already sits inside somebody's repository — scaffolding into a monorepo or an
2910
- * existing checkout, where a nested repo would be a surprise rather than a convenience.
2909
+ * Whether the new project already sits inside somebody's repository — a monorepo or an existing checkout, where
2910
+ * a nested repo would be a surprise rather than a convenience.
2911
2911
  */ function isInsideRepo(cwd) {
2912
2912
  const { ok, stdout } = git_git([
2913
2913
  'rev-parse',
@@ -2916,11 +2916,9 @@ function hasGit(cwd) {
2916
2916
  return ok && stdout === 'true';
2917
2917
  }
2918
2918
  /**
2919
- * Initializes a repository and makes the first commit.
2920
- *
2921
- * `initialized` rather than `committed` is the honest answer when the commit itself fails, which it does
2922
- * on a machine with no `user.email` configured. That is not a reason to fail the scaffold — the files
2923
- * are all there and staged — so the caller reports it and moves on.
2919
+ * Initializes a repository and makes the first commit. `initialized` is the honest answer when the commit itself
2920
+ * fails, as it does with no `user.email` configured — the files are all there and staged, so the caller reports
2921
+ * it and moves on rather than failing the scaffold.
2924
2922
  */ function initRepo(cwd) {
2925
2923
  const init = git_git([
2926
2924
  'init',
@@ -2943,15 +2941,14 @@ function hasGit(cwd) {
2943
2941
 
2944
2942
  ;// CONCATENATED MODULE: ./src/generated/framework.ts
2945
2943
  // GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
2946
- /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.12';
2944
+ /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.14';
2947
2945
  /**
2948
- * The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
2949
- * or a contributor on an older Node hears it from their package manager rather than from a stack trace.
2946
+ * The Node range rshono declares, restated in every scaffolded app's `engines` — so a CI image or a contributor
2947
+ * on an older Node hears it from their package manager rather than from a stack trace.
2950
2948
  */ const NODE_ENGINE = '>=22.18.0';
2951
2949
  /**
2952
- * The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
2953
- * exact — React's RSC internals are coupled across builds, and a generated app has no workspace
2954
- * overrides to fall back on.
2950
+ * The dependency versions rshono is tested against, copied from its own manifest. Exact where it is exact:
2951
+ * React's RSC internals are coupled across builds, and a generated app has no workspace overrides.
2955
2952
  */ const FRAMEWORK_DEPS = {
2956
2953
  hono: '^4.12.31',
2957
2954
  react: '19.2.8',
@@ -2982,9 +2979,8 @@ function hasGit(cwd) {
2982
2979
  ;// CONCATENATED MODULE: ./src/options.ts
2983
2980
 
2984
2981
  /*
2985
- * The names each option accepts, spelled once. The types below are derived from them, the CLI validates
2986
- * its flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them
2987
- * — so a name added here reaches all three without a second list to remember.
2982
+ * The names each option accepts, spelled once: the types below are derived from them, the CLI validates its
2983
+ * flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them.
2988
2984
  */ const FORMATTER_NAMES = [
2989
2985
  'prettier',
2990
2986
  'biome',
@@ -3045,14 +3041,13 @@ const QUALITY_PRESETS = [
3045
3041
  }
3046
3042
  ];
3047
3043
  /**
3048
- * Turns whatever the user typed into a name npm will accept, or returns `null` when nothing usable is
3049
- * left. Lowercasing and replacing runs of invalid characters covers the ordinary cases (`My App`,
3050
- * `my_app`); a scoped name is kept intact, since `@scope/name` is legal.
3044
+ * Turns whatever the user typed into a name npm will accept, or `null` when nothing usable is left.
3045
+ * Lowercasing and replacing runs of invalid characters covers the ordinary cases (`My App`, `my_app`).
3051
3046
  */ function toPackageName(input) {
3052
3047
  const trimmed = input.trim().replace(/^\.\/+/, '').replace(/\/+$/, '');
3053
3048
  if (!trimmed || trimmed === '.') return null;
3054
- // A scoped name is a name, not a path: `@scope/pkg` stays whole rather than becoming `pkg`. Anything
3055
- // else is a path, and the last segment is the one that names the project.
3049
+ // A scoped name is a name, not a path: `@scope/pkg` stays whole. Anything else is a path, whose last
3050
+ // segment names the project.
3056
3051
  const scoped = /^@[^\\/]+[\\/][^\\/]+$/.test(trimmed);
3057
3052
  const base = scoped ? trimmed : trimmed.split(/[\\/]/).filter(Boolean).pop() ?? '';
3058
3053
  if (!base) return null;
@@ -3066,12 +3061,12 @@ const QUALITY_PRESETS = [
3066
3061
 
3067
3062
  ;// CONCATENATED MODULE: ./src/features/combinations.ts
3068
3063
  /**
3069
- * Biome's CSS parser rejects Tailwind's syntax — `@apply` and a `@layer` block are parse errors, not
3070
- * unknown-at-rule warnings — so a project with both needs Biome pointed away from stylesheets. The
3071
- * overlay is a second `biome.json` listed after the first, which is all "later overlay wins" means.
3064
+ * Biome's CSS parser rejects Tailwind's syntax — `@apply` is a parse error, not an unknown-at-rule warning — so
3065
+ * a project with both needs Biome pointed away from stylesheets. The overlay is a second `biome.json` listed
3066
+ * after the first.
3072
3067
  *
3073
- * Narrow on purpose: the alternative was excluding CSS for everybody, which would quietly give up
3074
- * formatting the plain-CSS template that Biome handles perfectly well.
3068
+ * Narrow on purpose: excluding CSS for everybody would give up formatting the plain-CSS template that Biome
3069
+ * handles perfectly well.
3075
3070
  */ const BIOME_TAILWIND = {
3076
3071
  id: 'biome-tailwind',
3077
3072
  overlays: [
@@ -3097,14 +3092,12 @@ const QUALITY_PRESETS = [
3097
3092
  /**
3098
3093
  * The scripts every app gets, whatever it targets.
3099
3094
  *
3100
- * `start`, `preview` and `deploy` are deliberately not here: each one means something different per
3101
- * platform, so the deploy target contributes its own. The three names are a contract the targets keep,
3102
- * and the reason the README can describe an app it was not written for:
3095
+ * `start`, `preview` and `deploy` are not here: each means something different per platform, so the deploy
3096
+ * target contributes its own. The three names are a contract the targets keep, which is what lets the README
3097
+ * describe an app it was not written for:
3103
3098
  *
3104
- * - **`start`** runs a build that already exists and never makes one — what a host's own start command
3105
- * calls. Only the target whose build is a server has one.
3106
- * - **`preview`** builds, then runs the result here — for the targets where that is not the same two
3107
- * commands, because otherwise the production build is unanswerable without deploying it.
3099
+ * - **`start`** runs a build that already exists and never makes one — what a host's own start command calls.
3100
+ * - **`preview`** builds, then runs the result here, so the production build is answerable without deploying.
3108
3101
  * - **`deploy`** builds, then ships it — where the platform has one command that does the shipping.
3109
3102
  */ const BASE_SCRIPTS = {
3110
3103
  dev: 'rshono dev',
@@ -3112,9 +3105,8 @@ const QUALITY_PRESETS = [
3112
3105
  typecheck: 'tsc --noEmit'
3113
3106
  };
3114
3107
  /**
3115
- * Every script the app gets, in the order they are written: the base ones, then each feature's, in the order
3116
- * the features were selected. The manifest and the README's command table are both this, so neither can
3117
- * document a script the other does not have.
3108
+ * Every script the app gets, in the order they are written: the base ones, then each feature's. The manifest and
3109
+ * the README's command table are both this, so neither can document a script the other lacks.
3118
3110
  */ function buildScripts(features) {
3119
3111
  const scripts = {
3120
3112
  ...BASE_SCRIPTS
@@ -3130,9 +3122,8 @@ const QUALITY_PRESETS = [
3130
3122
  };
3131
3123
  }
3132
3124
  /**
3133
- * Script names a package manager has a command of its own for. `pnpm deploy` runs pnpm's workspace-deploy
3134
- * command and never looks at the manifest, so printing it would hand somebody a line that quietly does
3135
- * something else. The explicit `run` form is what all four managers accept, so those names get it.
3125
+ * Script names a package manager has a command of its own for: `pnpm deploy` runs pnpm's workspace-deploy and
3126
+ * never looks at the manifest. These get the explicit `run` form, which all four managers accept.
3136
3127
  */ const SHADOWED = new Set([
3137
3128
  'deploy'
3138
3129
  ]);
@@ -3140,10 +3131,9 @@ const QUALITY_PRESETS = [
3140
3131
  return `${SHADOWED.has(script) ? `${pm.name} run` : pm.run} ${script}`;
3141
3132
  }
3142
3133
  /**
3143
- * The README's command table: one line per script, with the command to type and a one-line gloss.
3144
- *
3145
- * A script whose feature supplies no `scriptHelp` is left out and covered by the "package.json has the
3146
- * rest" line — that is how a formatter's `format:check` stays out of a table about running the app.
3134
+ * The README's command table: one line per script, with the command to type and a one-line gloss. A script whose
3135
+ * feature supplies no `scriptHelp` is left out — that is how `format:check` stays out of a table about running
3136
+ * the app and covered by the line pointing at `package.json`.
3147
3137
  */ function scriptTable(answers, features, pm) {
3148
3138
  const help = baseScriptHelp(answers.deploy);
3149
3139
  for (const feature of features)Object.assign(help, feature.scriptHelp);
@@ -3152,16 +3142,14 @@ const QUALITY_PRESETS = [
3152
3142
  return documented.map((name)=>`${invoke(pm, name).padEnd(width)} # ${help[name]}`).join('\n');
3153
3143
  }
3154
3144
  /**
3155
- * The command that gets this app into production, as a sentence — the README's deploy step, and the same
3156
- * choice the closing summary makes, so the two cannot name different commands.
3145
+ * The command that gets this app into production, as a sentence — the README's deploy step and the closing
3146
+ * summary's, so the two cannot name different commands.
3157
3147
  *
3158
3148
  * Read off the scripts rather than the target, so a target that gains a `deploy` gains the sentence with it.
3159
- * `start` is the answer where there is no `deploy`: it ships nothing itself, but it is what the host runs.
3160
3149
  */ function deployStep(features, pm) {
3161
3150
  const scripts = buildScripts(features);
3162
3151
  if (scripts.deploy) return `\`${invoke(pm, 'deploy')}\` does the build and the upload in one step.`;
3163
3152
  if (scripts.start) return `\`${invoke(pm, 'start')}\` runs that build wherever you host it, and never makes one.`;
3164
- // Every branch names a script the app has, so a target that contributes none of the three still reads true.
3165
3153
  if (scripts.preview) return `\`${invoke(pm, 'preview')}\` runs the build here, so you can check it first.`;
3166
3154
  return `\`${invoke(pm, 'build')}\` produces it; getting it there is yours to script.`;
3167
3155
  }
@@ -3171,14 +3159,12 @@ const QUALITY_PRESETS = [
3171
3159
  /** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
3172
3160
  /** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
3173
3161
  /**
3174
- * Versions for the optional tooling the features can add — the one place in this package where a
3175
- * dependency range is typed out by hand, because none of these are things the framework itself
3176
- * declares. Everything a *scaffolded app* needs to run rshono comes from {@link FRAMEWORK_DEPS},
3177
- * which is generated from rshono's own manifest and must not be edited here.
3162
+ * Versions for the optional tooling the features can add — the one place in this package where a range is typed
3163
+ * by hand, because the framework declares none of these. Everything a scaffolded app needs to *run* rshono comes
3164
+ * from {@link FRAMEWORK_DEPS}, generated from rshono's own manifest.
3178
3165
  *
3179
- * Ranges are caret, not exact: these are the app's own dev tools, and a scaffold made six months from
3180
- * now should pick up their patch releases rather than pinning whatever was current the day this file
3181
- * was last touched.
3166
+ * Caret ranges, not exact: these are the app's own dev tools, and a scaffold made six months from now should
3167
+ * pick up their patch releases.
3182
3168
  */ const TOOL_VERSIONS = {
3183
3169
  tailwindcss: '^4.3.3',
3184
3170
  '@tailwindcss/postcss': '^4.3.3',
@@ -3190,23 +3176,20 @@ const QUALITY_PRESETS = [
3190
3176
  oxlint: '^1.76.0',
3191
3177
  oxfmt: '^0.61.0',
3192
3178
  eslint: '^10.8.0',
3193
- // ESLint's own recommended JavaScript rules, which typescript-eslint layers on top of rather than
3194
- // replaces, and the rules of hooks — the one class of React mistake no type checker sees.
3179
+ // ESLint's recommended JavaScript rules, which typescript-eslint layers on rather than replaces, and the rules
3180
+ // of hooks — the one class of React mistake no type checker sees.
3195
3181
  '@eslint/js': '^10.0.1',
3196
3182
  'typescript-eslint': '^8.65.0',
3197
3183
  'eslint-plugin-react-hooks': '^7.1.1',
3198
3184
  wrangler: '^4.115.0'
3199
3185
  };
3200
3186
  /**
3201
- * The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
3187
+ * The TypeScript an ESLint app pins in place of the framework's own — the one deliberate exception to
3202
3188
  * {@link FRAMEWORK_DEPS}.
3203
3189
  *
3204
- * typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface,
3205
- * so it accepts `typescript >=4.8.4 <6.1.0` and nothing above; `~6.0.3` is the newest that satisfies
3206
- * it. rshono's declarations compile the same under either, which is what makes this an app's business
3207
- * and not the framework's.
3208
- *
3209
- * When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
3190
+ * typescript-eslint reads TypeScript's compiler API directly, so it accepts `>=4.8.4 <6.1.0` and nothing above;
3191
+ * `~6.0.3` is the newest that satisfies it. rshono's declarations compile the same under either, which is what
3192
+ * makes this the app's business. When upstream widens its range, this constant is what to delete.
3210
3193
  */ const ESLINT_TYPESCRIPT = '~6.0.3';
3211
3194
 
3212
3195
  ;// CONCATENATED MODULE: ./src/features/deploy.ts
@@ -3215,22 +3198,17 @@ const QUALITY_PRESETS = [
3215
3198
  /**
3216
3199
  * What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
3217
3200
  *
3218
- * Deliberately thin: the framework arranges its own output for every platform, and `rshono build`
3219
- * writes the one platform config that has to exist (`wrangler.jsonc`) if the project has none a second
3220
- * copy generated here would only go stale. So a target contributes the commands that run and ship the
3221
- * build, the CLI they need, the directories to gitignore, and a note for the step no command covers.
3201
+ * Thin on purpose: the framework arranges its own output for every platform, and `rshono build` writes the one
3202
+ * platform config that has to exist. So a target contributes the commands that run and ship the build, the CLI
3203
+ * they need, the directories to gitignore, and a note for the step no command covers.
3222
3204
  *
3223
- * What the three script names promise is documented once, above `BASE_SCRIPTS` in `scripts.ts`. Only `node`
3224
- * has a `start`, because `rshono start` refuses a bundle built for anywhere else, and it needs no `preview`:
3225
- * `build` then `start` already is one.
3226
- *
3227
- * `pm` is here because two things have to be spelled for the app's package manager — the runner that fetches
3228
- * the uninstalled Vercel CLI ({@link PackageManager.dlx}), and every command in {@link Feature.platformSetup}.
3205
+ * What the three script names promise is documented above `BASE_SCRIPTS` in `scripts.ts`. Only `node` has a
3206
+ * `start` `rshono start` refuses a bundle built for anywhere else and it needs no `preview`, since `build`
3207
+ * then `start` already is one.
3229
3208
  */ function deployFeatures(pm) {
3230
3209
  const build = invoke(pm, 'build');
3231
3210
  return {
3232
- // Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
3233
- // guess — so the target contributes only the command that runs what was built.
3211
+ // Where a Node build goes next is a Dockerfile or a process manager, neither of which this can guess.
3234
3212
  node: {
3235
3213
  id: 'deploy-node',
3236
3214
  scripts: {
@@ -3253,16 +3231,14 @@ const QUALITY_PRESETS = [
3253
3231
  devDependencies: {
3254
3232
  wrangler: TOOL_VERSIONS.wrangler
3255
3233
  },
3256
- // The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each
3257
- // one merely picks the platform binary out of the optional dependency that already carries it, so
3258
- // neither needs to run — `workerd --version` and `esbuild --version` both answer without it.
3234
+ // The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each merely
3235
+ // picks the platform binary out of the optional dependency already carrying it, so neither needs to run.
3259
3236
  allowBuilds: {
3260
3237
  esbuild: false,
3261
3238
  workerd: false
3262
3239
  },
3263
- // `wrangler dev` is the one preview that runs the code in the runtime it will actually run in:
3264
- // workerd, not Node, serving the assets the build assembled. Both scripts read the wrangler.jsonc the
3265
- // build wrote, so nothing here has to know where the bundle or the assets went.
3240
+ // `wrangler dev` is the one preview that runs the code in workerd rather than Node. Both scripts read the
3241
+ // wrangler.jsonc the build wrote, so nothing here has to know where the bundle went.
3266
3242
  scripts: {
3267
3243
  preview: 'rshono build && wrangler dev',
3268
3244
  deploy: 'rshono build && wrangler deploy'
@@ -3284,9 +3260,8 @@ const QUALITY_PRESETS = [
3284
3260
  },
3285
3261
  vercel: {
3286
3262
  id: 'deploy-vercel',
3287
- // `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview
3288
- // URL, which is a useful thing to have but not what the word means. The local `preview` is a Node
3289
- // build run here — the platform has no way to run its own prebuilt output on your machine.
3263
+ // `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview URL.
3264
+ // `preview` is a Node build run here the platform cannot run its own prebuilt output on your machine.
3290
3265
  scripts: {
3291
3266
  preview: 'rshono build --deploy node && rshono start',
3292
3267
  deploy: `rshono build && ${pm.dlx} vercel deploy --prebuilt --prod`
@@ -3312,8 +3287,7 @@ const QUALITY_PRESETS = [
3312
3287
  },
3313
3288
  'aws-lambda': {
3314
3289
  id: 'deploy-aws-lambda',
3315
- // No CLI to wrap, and no upload this could guess at. `preview` still applies the bundle is a Node
3316
- // handler, so it runs here.
3290
+ // No CLI to wrap and no upload to guess at, but `preview` still applies: the bundle is a Node handler.
3317
3291
  scripts: {
3318
3292
  preview: 'rshono build --deploy node && rshono start'
3319
3293
  },
@@ -3325,8 +3299,9 @@ const QUALITY_PRESETS = [
3325
3299
  ],
3326
3300
  platformSetup: [
3327
3301
  `There is no settings page here; the upload is yours to script. A job needs \`${pm.name} install && ${build}\`,`,
3328
- 'then the function package: `dist/`, plus `node_modules` for any dependency of your own, which the server',
3329
- 'bundle leaves external.'
3302
+ 'then the function package: `dist/` and nothing else, with the handler at `dist/server/main.mjs`. Your',
3303
+ 'dependencies are compiled into the bundle for this target, so no `node_modules` is uploaded — which also',
3304
+ 'means a native addon fails the build here rather than the deploy.'
3330
3305
  ].join('\n')
3331
3306
  }
3332
3307
  };
@@ -3338,14 +3313,11 @@ function deployFeature(target, pm) {
3338
3313
  ;// CONCATENATED MODULE: ./src/features/quality.ts
3339
3314
 
3340
3315
  /**
3341
- * The formatter and linter features. Biome answers to both slots and appears once `selectFeatures`
3342
- * deduplicates by `id`, so `formatter: 'biome', linter: 'biome'` contributes one set of files, one
3343
- * dependency and one pair of scripts.
3316
+ * The formatter and linter features. Biome answers to both slots and appears once, because `selectFeatures`
3317
+ * deduplicates by `id`.
3344
3318
  *
3345
- * A formatter brings `format:check` beside `format`, because the writing half and the CI half want
3346
- * different exit-code behaviour: `format` rewrites files, `format:check` fails instead. A linter brings
3347
- * `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
3348
- * one. Biome adds a `check` of its own, which is the pair of them in a single pass.
3319
+ * Each brings a pair of scripts, since the writing half and the CI half want different exit codes: `format`
3320
+ * rewrites and `format:check` fails, `lint:fix` rewrites and `lint` fails. Biome's `check` is both in one pass.
3349
3321
  */ const PRETTIER = {
3350
3322
  id: 'prettier',
3351
3323
  overlays: [
@@ -3386,11 +3358,10 @@ const OXLINT = {
3386
3358
  }
3387
3359
  };
3388
3360
  /**
3389
- * The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
3390
- * be installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one
3391
- * its peer range accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to
3392
- * reach for ESLint over a syntax-only linter — so the config it ships hands the parser the whole
3393
- * program rather than linting file by file.
3361
+ * The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
3362
+ * installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest its peer range
3363
+ * accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to reach for ESLint at all —
3364
+ * so the config it ships hands the parser the whole program.
3394
3365
  */ const ESLINT = {
3395
3366
  id: 'eslint',
3396
3367
  overlays: [
@@ -3406,6 +3377,12 @@ const OXLINT = {
3406
3377
  scripts: {
3407
3378
  lint: 'eslint .',
3408
3379
  'lint:fix': 'eslint . --fix'
3380
+ },
3381
+ // The pin is invisible from the command line, and its consequence is not: the rules reason about the
3382
+ // program through an older compiler than the one that builds it, so a fix can produce code `tsc` rejects.
3383
+ // The generated eslint.config.mjs explains why; the README's table is where someone reads what to type.
3384
+ scriptHelp: {
3385
+ 'lint:fix': 'apply what it can — then run typecheck, see eslint.config.mjs'
3409
3386
  }
3410
3387
  };
3411
3388
  const BIOME = {
@@ -3446,13 +3423,12 @@ function linterFeature(linter) {
3446
3423
  ;// CONCATENATED MODULE: ./src/features/styling.ts
3447
3424
 
3448
3425
  /**
3449
- * Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
3450
- * plus an overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts
3451
- * postcss-loader in front of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten
3452
- * in utilities.
3426
+ * Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages, plus an
3427
+ * overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts postcss-loader in front
3428
+ * of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten in utilities.
3453
3429
  *
3454
- * `postcss` and `postcss-loader` are the app's dependencies rather than the framework's rshono
3455
- * compiles CSS natively, so an app that does not want a plugin chain does not install one.
3430
+ * `postcss` is the app's dependency rather than the framework's: rshono compiles CSS natively, so an app that
3431
+ * wants no plugin chain installs none.
3456
3432
  */ const TAILWIND = {
3457
3433
  id: 'tailwind',
3458
3434
  overlays: [
@@ -3475,12 +3451,11 @@ function stylingFeature(styling) {
3475
3451
 
3476
3452
 
3477
3453
  /**
3478
- * The features a set of answers selects, in application order — so an overlay listed later wins a file
3479
- * both of them ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome
3480
- * is both the formatter and the linter) without contributing twice.
3454
+ * The features a set of answers selects, in application order — so an overlay listed later wins a file both
3455
+ * ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome is both formatter and
3456
+ * linter) without contributing twice.
3481
3457
  *
3482
- * `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled
3483
- * CLI; nothing else here depends on which package manager the app is for.
3458
+ * `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled CLI.
3484
3459
  */ function selectFeatures(answers, pm) {
3485
3460
  const selected = [
3486
3461
  deployFeature(answers.deploy, pm),
@@ -3517,15 +3492,11 @@ function sorted(record) {
3517
3492
  return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
3518
3493
  }
3519
3494
  /**
3520
- * pnpm's settings for the new app, written only when a feature has something to put in them. `null`
3521
- * means there is nothing to say, so no file is written.
3522
- *
3523
- * It exists for one field, `allowBuilds`. pnpm fails an install — and every `pnpm dev` after it —
3524
- * until the project has said whether a dependency's install script should run. Nothing rshono itself
3525
- * installs has one, so most apps get no file; the packages that do (wrangler's esbuild and workerd)
3526
- * declare their answer on the feature that brings them.
3495
+ * pnpm's settings for the new app, or `null` when no feature has anything to put in them.
3527
3496
  *
3528
- * A file rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
3497
+ * It exists for one field, `allowBuilds`: pnpm fails an install until the project has said whether a
3498
+ * dependency's install script should run. Nothing rshono installs has one, so most apps get no file. A file
3499
+ * rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
3529
3500
  */ function buildPnpmSettings(features) {
3530
3501
  const allowBuilds = {};
3531
3502
  for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
@@ -3541,10 +3512,8 @@ function sorted(record) {
3541
3512
  ].join('\n');
3542
3513
  }
3543
3514
  /**
3544
- * Assembles `package.json` from the answers and whatever the selected features contribute.
3545
- *
3546
- * Dependencies are sorted by name and scripts keep the order {@link buildScripts} gives them — so two runs
3547
- * with the same answers produce byte-identical output, which is what makes the manifest snapshot-testable.
3515
+ * Assembles `package.json` from the answers and whatever the selected features contribute. Dependencies are
3516
+ * sorted and scripts keep {@link buildScripts}'s order, so two runs with the same answers are byte-identical.
3548
3517
  */ function buildPackageJson(answers, features, pm) {
3549
3518
  const dependencies = {
3550
3519
  '@rshono/core': RSHONO_RANGE,
@@ -3574,8 +3543,8 @@ function sorted(record) {
3574
3543
  dependencies: sorted(dependencies),
3575
3544
  devDependencies: sorted(devDependencies)
3576
3545
  };
3577
- // Only when the environment told us the exact version: `packageManager` pins the tool for Corepack,
3578
- // and a guess at the version is worse than leaving the field out.
3546
+ // Only when the environment told us the exact version: this field pins the tool for Corepack, and a guess is
3547
+ // worse than leaving it out.
3579
3548
  if (pm.version) manifest.packageManager = `${pm.name}@${pm.version}`;
3580
3549
  const ordered = Object.fromEntries(FIELD_ORDER.filter((field)=>field in manifest).map((field)=>[
3581
3550
  field,
@@ -3587,25 +3556,22 @@ function sorted(record) {
3587
3556
  ;// CONCATENATED MODULE: ./src/render.ts
3588
3557
 
3589
3558
  /**
3590
- * `{{NAME}}`, deliberately not `__NAME__`: templates are real files that real tools run over, and in
3591
- * markdown `__NAME__` *is* strong emphasis Prettier rewrites it to `**NAME**` and the token stops
3592
- * matching. `{{…}}` means nothing to any format these templates are written in.
3559
+ * `{{NAME}}`, not `__NAME__`: in markdown the latter is strong emphasis, so Prettier rewrites it to `**NAME**`
3560
+ * and the token stops matching. `{{…}}` means nothing to any format these templates are written in.
3593
3561
  */ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
3594
3562
  function tokensFor(answers, features, pm) {
3595
3563
  return {
3596
3564
  '{{PROJECT_NAME}}': answers.packageName,
3597
3565
  '{{DEPLOY_TARGET}}': answers.deploy,
3598
- // Derived from the features rather than the answers, because all three are about the scripts the app
3599
- // actually got: its command table, the one command that ships it, and what its platform asks for.
3566
+ // From the features rather than the answers, because all three are about the scripts the app actually got.
3600
3567
  '{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
3601
3568
  '{{DEPLOY_STEP}}': deployStep(features, pm),
3602
3569
  '{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
3603
3570
  };
3604
3571
  }
3605
3572
  /**
3606
- * Substitutes tokens, and throws on one it doesn't know — a typo in a template would otherwise ship a
3607
- * literal `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would
3608
- * catch.
3573
+ * Substitutes tokens, and throws on one it does not know — a typo in a template would otherwise ship a literal
3574
+ * `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would catch.
3609
3575
  */ function render(contents, tokens, source) {
3610
3576
  return contents.replace(TOKEN_PATTERN, (token)=>{
3611
3577
  const value = tokens[token];
@@ -3635,11 +3601,9 @@ function readTemplateDir(dir) {
3635
3601
  return files;
3636
3602
  }
3637
3603
  /**
3638
- * `_gitignore` → `.gitignore`, and so on for every dotfile.
3639
- *
3640
- * npm strips a literal `.gitignore` out of a published tarball, so a template cannot contain one — it
3641
- * would exist in the repo, pass every local test, and be missing from the package everybody installs.
3642
- * Applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
3604
+ * `_gitignore` → `.gitignore`, and so on for every dotfile: npm strips a literal `.gitignore` out of a
3605
+ * published tarball, so a template that contained one would pass every local test and be missing from the
3606
+ * package everybody installs. The basename only, so `templates/_x/y.ts` is left alone.
3643
3607
  */ function undotted(path) {
3644
3608
  const segments = path.split(posix.sep);
3645
3609
  const name = segments.pop();
@@ -3648,19 +3612,16 @@ function readTemplateDir(dir) {
3648
3612
  name.startsWith('_') ? `.${name.slice(1)}` : name
3649
3613
  ].join(posix.sep);
3650
3614
  }
3651
- /**
3652
- * A feature's `.gitignore` lines, appended under a heading naming it — so somebody reading the file six
3653
- * months later can tell why `.wrangler/` is in there.
3654
- */ function appendGitignore(existing, features) {
3615
+ /** A feature's `.gitignore` lines, under a heading naming it — so a reader can tell why `.wrangler/` is there. */ function appendGitignore(existing, features) {
3655
3616
  const additions = features.filter((feature)=>feature.gitignore?.length);
3656
3617
  if (additions.length === 0) return existing;
3657
3618
  const blocks = additions.map((feature)=>`\n# ${feature.id}\n${feature.gitignore.join('\n')}\n`);
3658
3619
  return existing + blocks.join('');
3659
3620
  }
3660
3621
  /**
3661
- * Turns answers into the exact set of files to write, without touching the target directory the
3662
- * decisions and the I/O are separated so the whole matrix of answers can be asserted on in a test, and
3663
- * so `--dry-run` is the same code path minus the last step.
3622
+ * Turns answers into the exact set of files to write, without touching the target directory. Separating the
3623
+ * decisions from the I/O is what lets a test assert on the whole matrix of answers, and makes `--dry-run` the
3624
+ * same code path minus the last step.
3664
3625
  */ function plan_plan(answers, pm) {
3665
3626
  const features = selectFeatures(answers, pm);
3666
3627
  const tokens = tokensFor(answers, features, pm);
@@ -3732,11 +3693,9 @@ function packageManager(name, version) {
3732
3693
  };
3733
3694
  }
3734
3695
  /**
3735
- * Which package manager invoked us. Every one of them sets `npm_config_user_agent` on the process it
3736
- * spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm
3737
- * project without asking, and the exact version comes along for the `packageManager` field.
3738
- *
3739
- * Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
3696
+ * Which package manager invoked us. All four set `npm_config_user_agent` on the process they spawn — `pnpm/11.9.0
3697
+ * npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm project without asking, exact
3698
+ * version included. Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
3740
3699
  */ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
3741
3700
  const [spec] = (userAgent ?? '').split(' ');
3742
3701
  const [name, version] = (spec ?? '').split('/');
@@ -3744,9 +3703,8 @@ function packageManager(name, version) {
3744
3703
  return packageManager('npm');
3745
3704
  }
3746
3705
  /**
3747
- * Runs the install, streaming its output. `shell: true` on Windows because npm, pnpm and yarn are all
3748
- * `.cmd` shims there, which `spawn` cannot execute directly — and with a shell involved the arguments
3749
- * are all fixed strings from the tables above, never anything the user typed.
3706
+ * Runs the install, streaming its output. `shell: true` on Windows, where npm, pnpm and yarn are `.cmd` shims
3707
+ * `spawn` cannot execute directly — safe because every argument is a fixed string from the tables above.
3750
3708
  */ function runInstall(pm, cwd) {
3751
3709
  return run(pm, pm.install, cwd);
3752
3710
  }
@@ -3764,9 +3722,8 @@ function run(pm, args, cwd) {
3764
3722
 
3765
3723
 
3766
3724
  /**
3767
- * The two halves of the closing line, in the words of the app's own scripts: what produces something
3768
- * shippable, and where it goes from there. The target with no command to give keeps the framework's hint,
3769
- * which is a sentence about the platform rather than something to type.
3725
+ * The two halves of the closing line, in the words of the app's own scripts: what produces something shippable,
3726
+ * and where it goes from there. A target with no command to give falls back to the framework's hint.
3770
3727
  */ function productionSteps(answers, plan, pm) {
3771
3728
  const scripts = buildScripts(plan.features);
3772
3729
  const check = scripts.preview ? `${invoke(pm, 'preview')} to run the production build` : `${pm.run} build`;
@@ -3774,8 +3731,8 @@ function run(pm, args, cwd) {
3774
3731
  return `${check}, and ${ship}`;
3775
3732
  }
3776
3733
  /**
3777
- * What to do next, in the order to do it — the last thing the user reads, and for most people the only
3778
- * documentation they will read today. `installed` decides whether the install step is still theirs.
3734
+ * What to do next, in the order to do it — the last thing the user reads. `installed` decides whether the install
3735
+ * step is still theirs.
3779
3736
  */ function nextSteps(answers, plan, pm, options) {
3780
3737
  const steps = [];
3781
3738
  if (options.directory !== '.') steps.push(`cd ${options.directory}`);
@@ -3810,10 +3767,7 @@ function run(pm, args, cwd) {
3810
3767
  ;// CONCATENATED MODULE: ./src/write.ts
3811
3768
 
3812
3769
 
3813
- /**
3814
- * Files that do not make a directory "occupied". A user who ran `git init` or opened the folder in an
3815
- * editor before scaffolding has not put anything in it that we would overwrite.
3816
- */ const IGNORED_ENTRIES = new Set([
3770
+ /** Files that do not make a directory "occupied": a `git init` or an editor has put nothing there to overwrite. */ const IGNORED_ENTRIES = new Set([
3817
3771
  '.git',
3818
3772
  '.DS_Store',
3819
3773
  '.idea',
@@ -3821,12 +3775,9 @@ function run(pm, args, cwd) {
3821
3775
  'Thumbs.db'
3822
3776
  ]);
3823
3777
  /**
3824
- * What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind —
3825
- * which is what decides whether scaffolding into it is safe.
3826
- *
3827
- * A path that does not exist yet is no conflict. A path that exists and is *not* a directory throws
3828
- * rather than reporting an empty list, since `--force` should not write into one either — otherwise
3829
- * `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
3778
+ * What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind — which is
3779
+ * what decides whether scaffolding into it is safe. A path that does not exist yet is no conflict; one that
3780
+ * exists and is not a directory throws, since `--force` should not write into it either.
3830
3781
  */ function conflictingEntries(dir) {
3831
3782
  const stats = statSync(dir, {
3832
3783
  throwIfNoEntry: false
@@ -3836,8 +3787,8 @@ function run(pm, args, cwd) {
3836
3787
  return external_node_fs_readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
3837
3788
  }
3838
3789
  /**
3839
- * Writes the plan. Directories are created as needed, and files are written with the plan's own
3840
- * ordering so a failure part-way through leaves something a person can make sense of.
3790
+ * Writes the plan, creating directories as needed and keeping the plan's own ordering — so a failure part-way
3791
+ * through leaves something a person can make sense of.
3841
3792
  */ function writePlan(plan, targetDir) {
3842
3793
  mkdirSync(targetDir, {
3843
3794
  recursive: true
@@ -3969,10 +3920,9 @@ function fail(message) {
3969
3920
  async function main() {
3970
3921
  const { values, positionals } = parse();
3971
3922
  if (values.help) return console.log(HELP);
3972
- if (values.version) return console.log("1.0.0-rc.12");
3973
- // A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both
3974
- // streams have to be a terminal: the prompts draw on stdout but *read from stdin*, so
3975
- // `echo | npx @rshono/create` would otherwise ask a question with nothing behind the keyboard.
3923
+ if (values.version) return console.log("1.0.0-rc.14");
3924
+ // A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both streams have to
3925
+ // be a terminal: the prompts draw on stdout but read from stdin.
3976
3926
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
3977
3927
  const pmFlag = oneOf(values.pm, PACKAGE_MANAGERS, 'pm');
3978
3928
  const pm = pmFlag ? packageManager(pmFlag) : detectPackageManager();
@@ -3983,8 +3933,7 @@ async function main() {
3983
3933
  const tailwindFlag = tristate(values.tailwind, values['no-tailwind'], 'tailwind');
3984
3934
  const installFlag = tristate(values.install, values['no-install'], 'install');
3985
3935
  const gitFlag = tristate(values.git, values['no-git'], 'git');
3986
- // The framework version, not this package's: it is the one the app will be pinned to, and the one
3987
- // worth reading here. `--version` reports create-rshono's own.
3936
+ // The framework version, not this package's: it is the one the app gets pinned to. `--version` reports ours.
3988
3937
  intro(`create-rshono · rshono ${RSHONO_VERSION}`);
3989
3938
  // ── Where ───────────────────────────────────────────────────────────────────────────────────────
3990
3939
  let directory = positionals[0];
@@ -4044,8 +3993,8 @@ async function main() {
4044
3993
  ]
4045
3994
  }));
4046
3995
  }
4047
- // One question instead of two. The axes stay independent underneath: a `--formatter` or `--linter`
4048
- // flag addresses either on its own, and skips the question entirely.
3996
+ // One question instead of two; the axes stay independent underneath, and a `--formatter` or `--linter` flag
3997
+ // addresses either on its own.
4049
3998
  let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
4050
3999
  if (!preset && !formatterFlag && !linterFlag) {
4051
4000
  const fallback = QUALITY_PRESETS["0"];
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@rshono/create",
3
- "version": "1.0.0-rc.12",
3
+ "version": "1.0.0-rc.14",
4
4
  "description": "Scaffold a new rshono app — Hono + Rspack + React Server Components",
5
5
  "author": "Lasse <lasse@lassetange.com> (https://www.lassetange.com)",
6
- "license": "ISC",
6
+ "license": "MIT",
7
7
  "keywords": [
8
8
  "create",
9
9
  "scaffold",
@@ -46,7 +46,7 @@
46
46
  "@rspack/core": "2.1.7",
47
47
  "@types/node": "^26.1.1",
48
48
  "typescript": "^7.0.2",
49
- "@rshono/core": "1.0.0-rc.12"
49
+ "@rshono/core": "1.0.0-rc.14"
50
50
  },
51
51
  "scripts": {
52
52
  "build": "node scripts/codegen.mjs && node scripts/build.mjs",
@@ -1,9 +1,24 @@
1
+ /**
2
+ * ESLint with type-aware rules, which is the reason to reach for it over a linter that only reads syntax.
3
+ *
4
+ * **This app's `typescript` is pinned below the one rshono is built with.** typescript-eslint reads the
5
+ * compiler API directly and accepts nothing from TypeScript 6.1 up, so `package.json` holds the newest
6
+ * version it takes. Your editor and `tsc` are unaffected — rshono's declarations compile the same under
7
+ * either — but the rules below reason about your program through the *older* compiler.
8
+ *
9
+ * The practical consequence: `lint:fix` can rewrite code that `tsc` then rejects. Run `typecheck` after it,
10
+ * not instead of it. When typescript-eslint widens its peer range, drop the `typescript` pin from
11
+ * `package.json` and this note with it.
12
+ */
1
13
  import js from '@eslint/js';
2
14
  import reactHooks from 'eslint-plugin-react-hooks';
3
15
  import tseslint from 'typescript-eslint';
4
16
 
5
17
  export default tseslint.config(
6
- { ignores: ['dist/**', '.wrangler/**', '.vercel/**', '.netlify/**'] },
18
+ // Build output, all of it generated: `dist/` from `rshono build`, `.rshono/` from `rshono dev`, and
19
+ // whatever the deploy target assembles beside them. Without this, linting after a dev run reports on
20
+ // bundles rather than on anything you wrote.
21
+ { ignores: ['dist/**', '.rshono/**', '.wrangler/**', '.vercel/**', '.netlify/**'] },
7
22
  js.configs.recommended,
8
23
  tseslint.configs.recommendedTypeChecked,
9
24
  reactHooks.configs.flat['recommended-latest'],