@rshono/create 1.0.0-rc.0 → 1.0.0-rc.1

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/README.md CHANGED
@@ -22,7 +22,7 @@ Six, each with a default, and each answerable by a flag instead:
22
22
  | Where should it go? | `my-rshono-app` | first positional argument, or `.` |
23
23
  | Where is it deployed? | `node` | `--deploy node\|bun\|deno\|cloudflare\|vercel\|netlify\|aws-lambda` |
24
24
  | Styling | plain CSS | `--tailwind` / `--no-tailwind` |
25
- | Formatting & linting | Prettier + oxlint | `--quality prettier-oxlint\|biome\|oxc\|none` |
25
+ | Formatting & linting | Prettier + oxlint | `--quality prettier-oxlint\|prettier-eslint\|biome\|oxc\|none` |
26
26
  | Install dependencies? | yes | `--no-install` |
27
27
  | Initialize git? | yes, unless nested | `--no-git` |
28
28
 
@@ -51,6 +51,7 @@ src/actions.ts a 'use server' action, called from a form that works wit
51
51
  src/components/ layout, home, greet-form ('use client'), 404, 500
52
52
  src/lib/env.ts both sides of the PUBLIC_ boundary in one place
53
53
  src/styles.css element-level CSS, or the Tailwind entry
54
+ pnpm-workspace.yaml pnpm only: which dependency install scripts this app runs (none of them)
54
55
  ```
55
56
 
56
57
  Then, if the dependencies were installed, the scaffold is run through its own formatter — so a fresh
@@ -83,10 +84,16 @@ import { plan, writePlan } from '@rshono/create';
83
84
 
84
85
  ## Two things worth knowing
85
86
 
86
- **There is no ESLint option.** Linting TypeScript with ESLint means `typescript-eslint`, whose peer range is
87
- `typescript >=4.8.4 <6.1.0`; rshono is built and tested against TypeScript 7. `npm install` fails outright
88
- on the conflict, and forcing past it would hand you a linter running against a compiler API it was never
89
- built for. When upstream widens the range, ESLint becomes one more entry in `features/quality.ts`.
87
+ **The ESLint preset pins TypeScript 6.** Linting TypeScript with ESLint means `typescript-eslint`, which
88
+ reads the compiler API directly rather than through a stable interface, so its peer range is
89
+ `typescript >=4.8.4 <6.1.0` below the TypeScript rshono is built and tested against. An app that chooses
90
+ ESLint therefore gets `typescript ~6.0.3`, the newest that range allows: `npm install` would otherwise fail
91
+ outright on the conflict, and forcing past it hands you a linter running against a compiler API it was never
92
+ built for. The framework's declarations compile identically under either version, which is what makes this
93
+ the app's pin and not the framework's — every other preset leaves TypeScript alone. What you trade for
94
+ type-aware rules is a compiler one major behind, and the JavaScript implementation rather than the native
95
+ one, so `typecheck` on a large app is several times slower. When upstream widens the range, the pin in
96
+ `features/quality.ts` is the only thing to delete.
90
97
 
91
98
  **Tailwind is four packages, a `postcss.config.mjs` and one rule in `rshono.config.ts`.** rshono compiles
92
99
  CSS natively and has no PostCSS in it — that is deliberate, and it means an app that wants a plugin chain
package/dist/api.mjs CHANGED
@@ -31,6 +31,7 @@ var __webpack_exports__ = {};
31
31
  // EXPORTS
32
32
  __webpack_require__.d(__webpack_exports__, {
33
33
  bp: () => (/* reexport */ DEPLOY_TARGET_NAMES),
34
+ ml: () => (/* reexport */ (/* inlined export .ESLINT_TYPESCRIPT */"~6.0.3")),
34
35
  e7: () => (/* reexport */ FRAMEWORK_DEPS),
35
36
  Ab: () => (/* reexport */ PACKAGE_MANAGERS),
36
37
  pm: () => (/* reexport */ QUALITY_PRESETS),
@@ -42,7 +43,7 @@ __webpack_require__.d(__webpack_exports__, {
42
43
  WL: () => (/* reexport */ inspectTarget),
43
44
  p1: () => (/* reexport */ isDeployTarget),
44
45
  eW: () => (/* reexport */ isValidPackageName),
45
- ml: () => (/* reexport */ packageManager),
46
+ VZ: () => (/* reexport */ packageManager),
46
47
  et: () => (/* reexport */ plan_plan),
47
48
  N7: () => (/* reexport */ selectFeatures),
48
49
  PQ: () => (/* reexport */ toPackageName),
@@ -78,7 +79,7 @@ __webpack_require__.d(__webpack_exports__, {
78
79
 
79
80
  ;// CONCATENATED MODULE: ./src/generated/framework.ts
80
81
  // GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
81
- /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.0';
82
+ /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.1';
82
83
  /**
83
84
  * The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
84
85
  * exact — React's RSC internals are coupled across builds, and a generated app has no workspace
@@ -145,8 +146,26 @@ __webpack_require__.d(__webpack_exports__, {
145
146
  '@biomejs/biome': '^2.5.6',
146
147
  oxlint: '^1.76.0',
147
148
  oxfmt: '^0.61.0',
149
+ eslint: '^10.8.0',
150
+ // ESLint's own recommended JavaScript rules, which typescript-eslint layers on top of rather than
151
+ // replaces, and the rules of hooks — the one class of React mistake no type checker sees.
152
+ '@eslint/js': '^10.0.1',
153
+ 'typescript-eslint': '^8.65.0',
154
+ 'eslint-plugin-react-hooks': '^7.1.1',
148
155
  wrangler: '^4.115.0'
149
156
  };
157
+ /**
158
+ * The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
159
+ * {@link FRAMEWORK_DEPS}, and the reason it is spelled out here.
160
+ *
161
+ * typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface, so
162
+ * it accepts `typescript >=4.8.4 <6.1.0` and nothing above. `~6.0.3` is the newest that satisfies it:
163
+ * patch releases of 6.0, no minor. The framework itself stays on the TypeScript it is tested against —
164
+ * rshono's declarations compile the same under either, which is what makes this pin an app's business
165
+ * and not the framework's.
166
+ *
167
+ * When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
168
+ */ const ESLINT_TYPESCRIPT = '~6.0.3';
150
169
 
151
170
  ;// CONCATENATED MODULE: ./src/features/deploy.ts
152
171
 
@@ -187,6 +206,11 @@ __webpack_require__.d(__webpack_exports__, {
187
206
  devDependencies: {
188
207
  wrangler: TOOL_VERSIONS.wrangler
189
208
  },
209
+ // wrangler brings workerd, whose install script only picks the platform binary out of the optional
210
+ // dependency that already carries it — `workerd --version` answers without it having run.
211
+ allowBuilds: {
212
+ workerd: false
213
+ },
190
214
  scripts: {
191
215
  deploy: 'rshono build && wrangler deploy'
192
216
  },
@@ -277,6 +301,30 @@ const OXLINT = {
277
301
  'lint:fix': 'oxlint --fix'
278
302
  }
279
303
  };
304
+ /**
305
+ * The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
306
+ * installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one its
307
+ * peer range accepts (see {@link ESLINT_TYPESCRIPT}). Every other preset leaves TypeScript alone.
308
+ *
309
+ * The rules are type-aware, which is the reason to reach for ESLint over a syntax-only linter at all —
310
+ * so the config it ships hands the whole program to the parser rather than linting file by file.
311
+ */ const ESLINT = {
312
+ id: 'eslint',
313
+ overlays: [
314
+ 'eslint'
315
+ ],
316
+ devDependencies: {
317
+ eslint: TOOL_VERSIONS.eslint,
318
+ '@eslint/js': TOOL_VERSIONS["@eslint/js"],
319
+ 'typescript-eslint': TOOL_VERSIONS["typescript-eslint"],
320
+ 'eslint-plugin-react-hooks': TOOL_VERSIONS["eslint-plugin-react-hooks"],
321
+ typescript: (/* inlined export .ESLINT_TYPESCRIPT */"~6.0.3")
322
+ },
323
+ scripts: {
324
+ lint: 'eslint .',
325
+ 'lint:fix': 'eslint . --fix'
326
+ }
327
+ };
280
328
  const BIOME = {
281
329
  id: 'biome',
282
330
  overlays: [
@@ -301,6 +349,7 @@ const FORMATTERS = {
301
349
  };
302
350
  const LINTERS = {
303
351
  oxlint: OXLINT,
352
+ eslint: ESLINT,
304
353
  biome: BIOME,
305
354
  none: null
306
355
  };
@@ -388,6 +437,13 @@ const QUALITY_PRESETS = [
388
437
  formatter: 'prettier',
389
438
  linter: 'oxlint'
390
439
  },
440
+ {
441
+ id: 'prettier-eslint',
442
+ label: 'Prettier + ESLint',
443
+ hint: 'type-aware rules — pins TypeScript 6, which is all typescript-eslint accepts',
444
+ formatter: 'prettier',
445
+ linter: 'eslint'
446
+ },
391
447
  {
392
448
  id: 'biome',
393
449
  label: 'Biome',
@@ -454,6 +510,39 @@ const QUALITY_PRESETS = [
454
510
  function sorted(record) {
455
511
  return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
456
512
  }
513
+ /**
514
+ * The install script every app inherits, from the framework rather than from anything it chose. tsx
515
+ * reads `rshono.config.ts` through esbuild, and esbuild's script only picks the platform binary out of
516
+ * the optional dependency that already carries it — rshono's own repo denies it for the same reason.
517
+ */ const BASE_ALLOW_BUILDS = {
518
+ esbuild: false
519
+ };
520
+ /**
521
+ * pnpm's settings for the new app — written for pnpm and for nobody else.
522
+ *
523
+ * It exists for one field. A dependency with an install script is a question pnpm will not answer on its
524
+ * own: it fails the install, and fails every `pnpm dev` after it, until the project has said whether the
525
+ * script should run. None of the ones an rshono app inherits need to (each is a native package whose
526
+ * binary arrives as an optional dependency), so a fresh app carries the answer rather than meeting
527
+ * `pnpm approve-builds` before it has rendered a page once.
528
+ *
529
+ * In this file rather than under a `pnpm` key in `package.json`, which pnpm 11 no longer reads, single-
530
+ * package projects included. What lands here is a decision about *this* app: a scaffolded file the app
531
+ * owns from then on, not something the framework reaches back into.
532
+ */ function buildPnpmSettings(features) {
533
+ const allowBuilds = {
534
+ ...BASE_ALLOW_BUILDS
535
+ };
536
+ for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
537
+ return [
538
+ '# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
539
+ '# fails the install rather than skip one quietly — so anything added later belongs here too.',
540
+ '# `false` means the script was looked at: these ship their real binary as an optional dependency.',
541
+ 'allowBuilds:',
542
+ ...Object.entries(sorted(allowBuilds)).map(([name, allowed])=>` ${name}: ${allowed}`),
543
+ ''
544
+ ].join('\n');
545
+ }
457
546
  /**
458
547
  * Assembles `package.json` from the answers and whatever the selected features contribute.
459
548
  *
@@ -598,6 +687,7 @@ function readTemplateDir(dir) {
598
687
  const gitignore = files.get('.gitignore');
599
688
  if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
600
689
  files.set('package.json', buildPackageJson(answers, features, pm));
690
+ if (pm.name === 'pnpm') files.set('pnpm-workspace.yaml', buildPnpmSettings(features));
601
691
  return {
602
692
  // Sorted, so both the write order and a test's snapshot are stable.
603
693
  files: new Map([
@@ -730,6 +820,7 @@ function inspectTarget(dir) {
730
820
 
731
821
 
732
822
  var __webpack_exports__DEPLOY_TARGET_NAMES = __webpack_exports__.bp;
823
+ var __webpack_exports__ESLINT_TYPESCRIPT = __webpack_exports__.ml;
733
824
  var __webpack_exports__FRAMEWORK_DEPS = __webpack_exports__.e7;
734
825
  var __webpack_exports__PACKAGE_MANAGERS = __webpack_exports__.Ab;
735
826
  var __webpack_exports__QUALITY_PRESETS = __webpack_exports__.pm;
@@ -741,9 +832,9 @@ var __webpack_exports__detectPackageManager = __webpack_exports__.Lv;
741
832
  var __webpack_exports__inspectTarget = __webpack_exports__.WL;
742
833
  var __webpack_exports__isDeployTarget = __webpack_exports__.p1;
743
834
  var __webpack_exports__isValidPackageName = __webpack_exports__.eW;
744
- var __webpack_exports__packageManager = __webpack_exports__.ml;
835
+ var __webpack_exports__packageManager = __webpack_exports__.VZ;
745
836
  var __webpack_exports__plan = __webpack_exports__.et;
746
837
  var __webpack_exports__selectFeatures = __webpack_exports__.N7;
747
838
  var __webpack_exports__toPackageName = __webpack_exports__.PQ;
748
839
  var __webpack_exports__writePlan = __webpack_exports__.pD;
749
- export { __webpack_exports__DEPLOY_TARGET_NAMES as DEPLOY_TARGET_NAMES, __webpack_exports__FRAMEWORK_DEPS as FRAMEWORK_DEPS, __webpack_exports__PACKAGE_MANAGERS as PACKAGE_MANAGERS, __webpack_exports__QUALITY_PRESETS as QUALITY_PRESETS, __webpack_exports__RSHONO_RANGE as RSHONO_RANGE, __webpack_exports__TOOL_VERSIONS as TOOL_VERSIONS, __webpack_exports__buildPackageJson as buildPackageJson, __webpack_exports__deployHint as deployHint, __webpack_exports__detectPackageManager as detectPackageManager, __webpack_exports__inspectTarget as inspectTarget, __webpack_exports__isDeployTarget as isDeployTarget, __webpack_exports__isValidPackageName as isValidPackageName, __webpack_exports__packageManager as packageManager, __webpack_exports__plan as plan, __webpack_exports__selectFeatures as selectFeatures, __webpack_exports__toPackageName as toPackageName, __webpack_exports__writePlan as writePlan };
840
+ export { __webpack_exports__DEPLOY_TARGET_NAMES as DEPLOY_TARGET_NAMES, __webpack_exports__ESLINT_TYPESCRIPT as ESLINT_TYPESCRIPT, __webpack_exports__FRAMEWORK_DEPS as FRAMEWORK_DEPS, __webpack_exports__PACKAGE_MANAGERS as PACKAGE_MANAGERS, __webpack_exports__QUALITY_PRESETS as QUALITY_PRESETS, __webpack_exports__RSHONO_RANGE as RSHONO_RANGE, __webpack_exports__TOOL_VERSIONS as TOOL_VERSIONS, __webpack_exports__buildPackageJson as buildPackageJson, __webpack_exports__deployHint as deployHint, __webpack_exports__detectPackageManager as detectPackageManager, __webpack_exports__inspectTarget as inspectTarget, __webpack_exports__isDeployTarget as isDeployTarget, __webpack_exports__isValidPackageName as isValidPackageName, __webpack_exports__packageManager as packageManager, __webpack_exports__plan as plan, __webpack_exports__selectFeatures as selectFeatures, __webpack_exports__toPackageName as toPackageName, __webpack_exports__writePlan as writePlan };
package/dist/cli.mjs CHANGED
@@ -2943,7 +2943,7 @@ function hasGit(cwd) {
2943
2943
 
2944
2944
  ;// CONCATENATED MODULE: ./src/generated/framework.ts
2945
2945
  // 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.0';
2946
+ /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.1';
2947
2947
  /**
2948
2948
  * The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
2949
2949
  * exact — React's RSC internals are coupled across builds, and a generated app has no workspace
@@ -3010,6 +3010,13 @@ const QUALITY_PRESETS = [
3010
3010
  formatter: 'prettier',
3011
3011
  linter: 'oxlint'
3012
3012
  },
3013
+ {
3014
+ id: 'prettier-eslint',
3015
+ label: 'Prettier + ESLint',
3016
+ hint: 'type-aware rules — pins TypeScript 6, which is all typescript-eslint accepts',
3017
+ formatter: 'prettier',
3018
+ linter: 'eslint'
3019
+ },
3013
3020
  {
3014
3021
  id: 'biome',
3015
3022
  label: 'Biome',
@@ -3102,8 +3109,26 @@ const QUALITY_PRESETS = [
3102
3109
  '@biomejs/biome': '^2.5.6',
3103
3110
  oxlint: '^1.76.0',
3104
3111
  oxfmt: '^0.61.0',
3112
+ eslint: '^10.8.0',
3113
+ // ESLint's own recommended JavaScript rules, which typescript-eslint layers on top of rather than
3114
+ // replaces, and the rules of hooks — the one class of React mistake no type checker sees.
3115
+ '@eslint/js': '^10.0.1',
3116
+ 'typescript-eslint': '^8.65.0',
3117
+ 'eslint-plugin-react-hooks': '^7.1.1',
3105
3118
  wrangler: '^4.115.0'
3106
3119
  };
3120
+ /**
3121
+ * The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
3122
+ * {@link FRAMEWORK_DEPS}, and the reason it is spelled out here.
3123
+ *
3124
+ * typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface, so
3125
+ * it accepts `typescript >=4.8.4 <6.1.0` and nothing above. `~6.0.3` is the newest that satisfies it:
3126
+ * patch releases of 6.0, no minor. The framework itself stays on the TypeScript it is tested against —
3127
+ * rshono's declarations compile the same under either, which is what makes this pin an app's business
3128
+ * and not the framework's.
3129
+ *
3130
+ * When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
3131
+ */ const ESLINT_TYPESCRIPT = '~6.0.3';
3107
3132
 
3108
3133
  ;// CONCATENATED MODULE: ./src/features/deploy.ts
3109
3134
 
@@ -3144,6 +3169,11 @@ const QUALITY_PRESETS = [
3144
3169
  devDependencies: {
3145
3170
  wrangler: TOOL_VERSIONS.wrangler
3146
3171
  },
3172
+ // wrangler brings workerd, whose install script only picks the platform binary out of the optional
3173
+ // dependency that already carries it — `workerd --version` answers without it having run.
3174
+ allowBuilds: {
3175
+ workerd: false
3176
+ },
3147
3177
  scripts: {
3148
3178
  deploy: 'rshono build && wrangler deploy'
3149
3179
  },
@@ -3234,6 +3264,30 @@ const OXLINT = {
3234
3264
  'lint:fix': 'oxlint --fix'
3235
3265
  }
3236
3266
  };
3267
+ /**
3268
+ * The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
3269
+ * installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one its
3270
+ * peer range accepts (see {@link ESLINT_TYPESCRIPT}). Every other preset leaves TypeScript alone.
3271
+ *
3272
+ * The rules are type-aware, which is the reason to reach for ESLint over a syntax-only linter at all —
3273
+ * so the config it ships hands the whole program to the parser rather than linting file by file.
3274
+ */ const ESLINT = {
3275
+ id: 'eslint',
3276
+ overlays: [
3277
+ 'eslint'
3278
+ ],
3279
+ devDependencies: {
3280
+ eslint: TOOL_VERSIONS.eslint,
3281
+ '@eslint/js': TOOL_VERSIONS["@eslint/js"],
3282
+ 'typescript-eslint': TOOL_VERSIONS["typescript-eslint"],
3283
+ 'eslint-plugin-react-hooks': TOOL_VERSIONS["eslint-plugin-react-hooks"],
3284
+ typescript: (/* inlined export .ESLINT_TYPESCRIPT */"~6.0.3")
3285
+ },
3286
+ scripts: {
3287
+ lint: 'eslint .',
3288
+ 'lint:fix': 'eslint . --fix'
3289
+ }
3290
+ };
3237
3291
  const BIOME = {
3238
3292
  id: 'biome',
3239
3293
  overlays: [
@@ -3258,6 +3312,7 @@ const FORMATTERS = {
3258
3312
  };
3259
3313
  const LINTERS = {
3260
3314
  oxlint: OXLINT,
3315
+ eslint: ESLINT,
3261
3316
  biome: BIOME,
3262
3317
  none: null
3263
3318
  };
@@ -3346,6 +3401,39 @@ function stylingFeature(styling) {
3346
3401
  function sorted(record) {
3347
3402
  return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
3348
3403
  }
3404
+ /**
3405
+ * The install script every app inherits, from the framework rather than from anything it chose. tsx
3406
+ * reads `rshono.config.ts` through esbuild, and esbuild's script only picks the platform binary out of
3407
+ * the optional dependency that already carries it — rshono's own repo denies it for the same reason.
3408
+ */ const BASE_ALLOW_BUILDS = {
3409
+ esbuild: false
3410
+ };
3411
+ /**
3412
+ * pnpm's settings for the new app — written for pnpm and for nobody else.
3413
+ *
3414
+ * It exists for one field. A dependency with an install script is a question pnpm will not answer on its
3415
+ * own: it fails the install, and fails every `pnpm dev` after it, until the project has said whether the
3416
+ * script should run. None of the ones an rshono app inherits need to (each is a native package whose
3417
+ * binary arrives as an optional dependency), so a fresh app carries the answer rather than meeting
3418
+ * `pnpm approve-builds` before it has rendered a page once.
3419
+ *
3420
+ * In this file rather than under a `pnpm` key in `package.json`, which pnpm 11 no longer reads, single-
3421
+ * package projects included. What lands here is a decision about *this* app: a scaffolded file the app
3422
+ * owns from then on, not something the framework reaches back into.
3423
+ */ function buildPnpmSettings(features) {
3424
+ const allowBuilds = {
3425
+ ...BASE_ALLOW_BUILDS
3426
+ };
3427
+ for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
3428
+ return [
3429
+ '# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
3430
+ '# fails the install rather than skip one quietly — so anything added later belongs here too.',
3431
+ '# `false` means the script was looked at: these ship their real binary as an optional dependency.',
3432
+ 'allowBuilds:',
3433
+ ...Object.entries(sorted(allowBuilds)).map(([name, allowed])=>` ${name}: ${allowed}`),
3434
+ ''
3435
+ ].join('\n');
3436
+ }
3349
3437
  /**
3350
3438
  * Assembles `package.json` from the answers and whatever the selected features contribute.
3351
3439
  *
@@ -3486,6 +3574,7 @@ function readTemplateDir(dir) {
3486
3574
  const gitignore = files.get('.gitignore');
3487
3575
  if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
3488
3576
  files.set('package.json', buildPackageJson(answers, features, pm));
3577
+ if (pm.name === 'pnpm') files.set('pnpm-workspace.yaml', buildPnpmSettings(features));
3489
3578
  return {
3490
3579
  // Sorted, so both the write order and a test's snapshot are stable.
3491
3580
  files: new Map([
@@ -3658,9 +3747,9 @@ Options:
3658
3747
  -d, --deploy <target> ${DEPLOY_TARGET_NAMES.join(' | ')}
3659
3748
  --tailwind Tailwind CSS (--no-tailwind for plain CSS)
3660
3749
  --quality <preset> ${QUALITY_PRESETS.map((preset)=>preset.id).join(' | ')}
3661
- --formatter <name> prettier | biome | oxfmt | none (overrides --quality)
3662
- --linter <name> oxlint | biome | none (overrides --quality)
3663
- --pm <name> npm | pnpm | yarn | bun (default: whatever ran this)
3750
+ --formatter <name> prettier | biome | oxfmt | none (overrides --quality)
3751
+ --linter <name> oxlint | eslint | biome | none (overrides --quality; eslint pins TypeScript 6)
3752
+ --pm <name> npm | pnpm | yarn | bun (default: whatever ran this)
3664
3753
  --no-install write the files and stop
3665
3754
  --no-git do not initialize a repository
3666
3755
  --force scaffold into a directory that is not empty
@@ -3747,7 +3836,7 @@ async function main() {
3747
3836
  allowPositionals: true
3748
3837
  });
3749
3838
  if (values.help) return console.log(HELP);
3750
- if (values.version) return console.log("1.0.0-rc.0");
3839
+ if (values.version) return console.log("1.0.0-rc.1");
3751
3840
  // A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer.
3752
3841
  const interactive = Boolean(process.stdout.isTTY) && !values.yes;
3753
3842
  const pmFlag = oneOf(values.pm, [
@@ -3766,6 +3855,7 @@ async function main() {
3766
3855
  ], 'formatter');
3767
3856
  const linterFlag = oneOf(values.linter, [
3768
3857
  'oxlint',
3858
+ 'eslint',
3769
3859
  'biome',
3770
3860
  'none'
3771
3861
  ], 'linter');
@@ -3773,7 +3863,7 @@ async function main() {
3773
3863
  const tailwindFlag = tristate(values.tailwind, values['no-tailwind'], 'tailwind');
3774
3864
  const installFlag = tristate(values.install, values['no-install'], 'install');
3775
3865
  const gitFlag = tristate(values.git, values['no-git'], 'git');
3776
- intro(`create-rshono · rshono ${"1.0.0-rc.0"}`);
3866
+ intro(`create-rshono · rshono ${"1.0.0-rc.1"}`);
3777
3867
  // ── Where ───────────────────────────────────────────────────────────────────────────────────────
3778
3868
  let directory = positionals[0];
3779
3869
  if (!directory) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rshono/create",
3
- "version": "1.0.0-rc.0",
3
+ "version": "1.0.0-rc.1",
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
6
  "license": "ISC",
@@ -9,7 +9,10 @@
9
9
  * The signature is the one `useActionState` expects: previous state first, then the form data.
10
10
  */
11
11
  export async function greet(_previous: string | null, formData: FormData): Promise<string> {
12
- const name = String(formData.get('name') ?? '').trim();
12
+ // A form field is a string or a File, never only a string — a file input posted under this name would
13
+ // stringify to "[object File]" rather than fail. Narrowing is the validation the endpoint owes itself.
14
+ const field = formData.get('name');
15
+ const name = typeof field === 'string' ? field.trim() : '';
13
16
  if (!name) return 'Type a name first.';
14
17
 
15
18
  // Where real work goes — a database write, an email, a queue push.
@@ -0,0 +1,31 @@
1
+ import js from '@eslint/js';
2
+ import reactHooks from 'eslint-plugin-react-hooks';
3
+ import tseslint from 'typescript-eslint';
4
+
5
+ /**
6
+ * Flat config, with type-aware rules — the reason to run ESLint over a syntax-only linter. `projectService`
7
+ * builds the same program `tsc` does from tsconfig.json, so a rule can ask what a value actually *is*:
8
+ * an unawaited promise, a `catch` that swallows an error, a `String()` around something that is not one.
9
+ *
10
+ * The cost is that ESLint needs TypeScript to answer, which is why this app pins TypeScript 6 —
11
+ * typescript-eslint reads the compiler API directly and accepts nothing newer. `npm run typecheck` is
12
+ * still the thing that decides whether the app compiles; these rules only see what it sees.
13
+ */
14
+ export default tseslint.config(
15
+ // ESLint's own default ignores cover node_modules and nothing else, so the build output — and whatever
16
+ // the deploy target leaves beside it — would otherwise be linted as if you had written it.
17
+ { ignores: ['dist/**', '.wrangler/**', '.vercel/**', '.netlify/**'] },
18
+ js.configs.recommended,
19
+ tseslint.configs.recommendedTypeChecked,
20
+ // The rules of hooks: the one class of React mistake no type checker catches, and the reason a React
21
+ // app wants a linter at all.
22
+ reactHooks.configs.flat['recommended-latest'],
23
+ {
24
+ languageOptions: {
25
+ parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
26
+ },
27
+ },
28
+ // This file and any other plain JavaScript sits outside the TypeScript program, so the type-aware rules
29
+ // have nothing to run against and would report every file as unconfigured.
30
+ { files: ['**/*.{js,mjs,cjs}'], extends: [tseslint.configs.disableTypeChecked] },
31
+ );