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

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
@@ -1,17 +1,26 @@
1
- # @rshono/create
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/rshono/rshono/main/logo.svg" alt="" width="72" height="72" />
3
+ </p>
2
4
 
3
- Scaffolds a new [rshono](https://github.com/rshono/rshono) app — Hono + Rspack + React Server Components.
5
+ <h1 align="center">@rshono/create</h1>
6
+
7
+ <p align="center">
8
+ Scaffolds a new <a href="https://github.com/rshono/rshono">rshono</a> app — Hono + Rspack + React Server Components.
9
+ </p>
4
10
 
5
11
  ```bash
6
- npm create @rshono@latest my-app
7
- pnpm create @rshono my-app
12
+ npx @rshono/create@latest my-app
13
+ pnx @rshono/create my-app
14
+ bunx @rshono/create my-app
8
15
  yarn create @rshono my-app
9
- bun create @rshono my-app
10
16
  ```
11
17
 
12
- The package manager that ran it is the one the project gets: it is read from `npm_config_user_agent`, used
13
- for the install, written into `packageManager` for Corepack, and used in every command the closing summary
14
- prints. Nothing asks you which one you meant.
18
+ The package manager that ran it is the one the project gets: it is read from `npm_config_user_agent`, which
19
+ every one of these runners sets, and it is used for the install, written into `packageManager` for Corepack,
20
+ and used in every command the closing summary prints. Nothing asks you which one you meant.
21
+
22
+ Yarn is the odd row out because `yarn dlx` is Berry-only, while `yarn create` works in either — and `pnx`
23
+ arrived in pnpm 10.16, before which it is `pnpm dlx`.
15
24
 
16
25
  ## The questions
17
26
 
@@ -26,13 +35,13 @@ Six, each with a default, and each answerable by a flag instead:
26
35
  | Install dependencies? | yes | `--no-install` |
27
36
  | Initialize git? | yes, unless nested | `--no-git` |
28
37
 
29
- Plus `--formatter` and `--linter` to set either half of the quality preset on its own, `--force` to
30
- scaffold into a directory that is not empty, `--dry-run` to see the file list and write nothing, and `-y`
31
- to take the defaults for everything not given. **A non-interactive terminal implies `-y`**, so this is one
32
- command in CI or from an agent:
38
+ Plus `--formatter` and `--linter` to set either half of the quality preset on its own, `--pm` to override
39
+ the package manager that was detected, `--force` to scaffold into a directory that is not empty,
40
+ `--dry-run` to see the file list and write nothing, and `-y` to take the defaults for everything not
41
+ given. **A non-interactive terminal implies `-y`**, so this is one command in CI or from an agent:
33
42
 
34
43
  ```bash
35
- npm create @rshono@latest my-app -y --deploy cloudflare --tailwind --quality biome
44
+ npx @rshono/create@latest my-app -y --deploy cloudflare --tailwind --quality biome
36
45
  ```
37
46
 
38
47
  The deploy targets, and the deploy command each one prints, are generated from the framework's own
@@ -104,9 +113,14 @@ delete to go back to plain CSS.
104
113
 
105
114
  ```bash
106
115
  pnpm --filter @rshono/create build # codegen, then one bundled dist/cli.mjs with no runtime deps
107
- pnpm --filter @rshono/create test # the plan matrix — fast, no I/O
116
+ pnpm --filter @rshono/create test # builds, then the plan matrix and the CLI seconds, no installs
108
117
  CREATE_RSHONO_E2E=1 pnpm --filter @rshono/create test # also: pack, install and build real apps
109
118
  ```
110
119
 
111
- `@clack/prompts` (MIT) is bundled rather than depended on, so `npm create` downloads one tarball before it
112
- can ask its first question.
120
+ `plan.test.mjs` is the fast one the whole matrix of answers in memory, no directory touched.
121
+ `cli.test.mjs` spawns the real bin against temp directories, which is where argument parsing and the
122
+ refusal to overwrite somebody's files are checked. `e2e.test.mjs` is the opt-in one, and the only one that
123
+ installs anything.
124
+
125
+ `@clack/prompts` (MIT) is bundled rather than depended on, so `npx @rshono/create` downloads one tarball
126
+ before it can ask its first question.
File without changes
package/dist/api.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {fileURLToPath as __rspack_fileURLToPath} from "node:url";
3
3
  import {dirname as __rspack_dirname} from "node:path";
4
4
  var __rspack_import_meta_dirname__ = __rspack_dirname(__rspack_fileURLToPath(import.meta.url));
5
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
+ import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join, posix, sep } from "node:path";
7
7
  import "node:child_process";
8
8
  // The require scope
@@ -32,16 +32,16 @@ var __webpack_exports__ = {};
32
32
  __webpack_require__.d(__webpack_exports__, {
33
33
  bp: () => (/* reexport */ DEPLOY_TARGET_NAMES),
34
34
  ml: () => (/* reexport */ (/* inlined export .ESLINT_TYPESCRIPT */"~6.0.3")),
35
+ wC: () => (/* reexport */ FORMATTER_NAMES),
35
36
  e7: () => (/* reexport */ FRAMEWORK_DEPS),
37
+ At: () => (/* reexport */ LINTER_NAMES),
36
38
  Ab: () => (/* reexport */ PACKAGE_MANAGERS),
37
39
  pm: () => (/* reexport */ QUALITY_PRESETS),
38
40
  s_: () => (/* reexport */ RSHONO_RANGE),
39
41
  RM: () => (/* reexport */ TOOL_VERSIONS),
40
- Jh: () => (/* reexport */ buildPackageJson),
42
+ dh: () => (/* reexport */ conflictingEntries),
41
43
  P8: () => (/* reexport */ deployHint),
42
44
  Lv: () => (/* reexport */ detectPackageManager),
43
- WL: () => (/* reexport */ inspectTarget),
44
- p1: () => (/* reexport */ isDeployTarget),
45
45
  eW: () => (/* reexport */ isValidPackageName),
46
46
  VZ: () => (/* reexport */ packageManager),
47
47
  et: () => (/* reexport */ plan_plan),
@@ -79,7 +79,11 @@ __webpack_require__.d(__webpack_exports__, {
79
79
 
80
80
  ;// CONCATENATED MODULE: ./src/generated/framework.ts
81
81
  // GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
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
+ /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.3';
83
+ /**
84
+ * The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
85
+ * or a contributor on an older Node hears it from their package manager rather than from a stack trace.
86
+ */ const NODE_ENGINE = '>=22.1.0';
83
87
  /**
84
88
  * The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
85
89
  * exact — React's RSC internals are coupled across builds, and a generated app has no workspace
@@ -125,7 +129,7 @@ __webpack_require__.d(__webpack_exports__, {
125
129
 
126
130
  ;// CONCATENATED MODULE: ./src/versions.ts
127
131
 
128
-
132
+ /** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
129
133
  /** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
130
134
  /**
131
135
  * Versions for the optional tooling the features can add — the one place in this package where a
@@ -176,8 +180,11 @@ __webpack_require__.d(__webpack_exports__, {
176
180
  * Deliberately thin. The framework already knows how to arrange its own output for every platform, and
177
181
  * `rshono build` writes the one platform config that has to exist (`wrangler.jsonc`, with the
178
182
  * `compatibility_date` of the day it ran) if the project has none. Generating a second copy here would
179
- * be a copy that goes stale. So a target contributes a `deploy` script, the CLI it needs locally, and
180
- * the build artefacts its platform leaves in the project.
183
+ * be a copy that goes stale.
184
+ *
185
+ * So a target contributes only what is true of it: the command that runs or ships the build, the CLI
186
+ * that command needs installed locally, the directories its platform leaves behind for `.gitignore`, and
187
+ * a note for the step no command covers. Several contribute just one of those.
181
188
  */ const DEPLOY_FEATURES = {
182
189
  // Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
183
190
  // guess — so the target contributes only the command that runs what was built.
@@ -260,8 +267,10 @@ function deployFeature(target) {
260
267
  * deduplicates by `id`, so `formatter: 'biome', linter: 'biome'` contributes one set of files, one
261
268
  * dependency and one pair of scripts.
262
269
  *
263
- * Each tool brings its own `check` script alongside `format`/`lint`, because the writing half and the
264
- * CI half want different exit-code behaviour: `format` rewrites files, `check` fails instead.
270
+ * A formatter brings `format:check` beside `format`, because the writing half and the CI half want
271
+ * different exit-code behaviour: `format` rewrites files, `format:check` fails instead. A linter brings
272
+ * `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
273
+ * one. Biome adds a `check` of its own, which is the pair of them in a single pass.
265
274
  */ const PRETTIER = {
266
275
  id: 'prettier',
267
276
  overlays: [
@@ -416,6 +425,22 @@ function stylingFeature(styling) {
416
425
 
417
426
  ;// CONCATENATED MODULE: ./src/options.ts
418
427
 
428
+ /*
429
+ * The names each option accepts, spelled once. The types below are derived from them, the CLI validates
430
+ * its flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them
431
+ * — so a name added here reaches all three without a second list to remember.
432
+ */ const FORMATTER_NAMES = [
433
+ 'prettier',
434
+ 'biome',
435
+ 'oxfmt',
436
+ 'none'
437
+ ];
438
+ const LINTER_NAMES = [
439
+ 'oxlint',
440
+ 'eslint',
441
+ 'biome',
442
+ 'none'
443
+ ];
419
444
  const PACKAGE_MANAGERS = [
420
445
  'npm',
421
446
  'pnpm',
@@ -426,9 +451,6 @@ const DEPLOY_TARGET_NAMES = DEPLOY_TARGETS.map((target)=>target.name);
426
451
  function deployHint(name) {
427
452
  return DEPLOY_TARGETS.find((target)=>target.name === name)?.hint ?? '';
428
453
  }
429
- function isDeployTarget(value) {
430
- return DEPLOY_TARGET_NAMES.includes(value);
431
- }
432
454
  const QUALITY_PRESETS = [
433
455
  {
434
456
  id: 'prettier-oxlint',
@@ -486,11 +508,16 @@ const QUALITY_PRESETS = [
486
508
  return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(name) && name.length <= 214;
487
509
  }
488
510
 
511
+ ;// CONCATENATED MODULE: external "node:fs"
512
+
513
+ ;// CONCATENATED MODULE: external "node:path"
514
+
489
515
  ;// CONCATENATED MODULE: ./src/pkg.ts
490
516
 
491
517
  /**
492
- * The scripts every app gets. `start` is not among them: it is the *Node* launcher, and the deploy
493
- * features each contribute the command their own platform runs what was built with.
518
+ * The scripts every app gets. `start` is not among them, because it means something different per
519
+ * platform: the three targets that run the build themselves node, bun, deno each contribute their
520
+ * own, and a platform target contributes a `deploy` instead, where its platform has one command to give.
494
521
  */ const BASE_SCRIPTS = {
495
522
  dev: 'rshono dev',
496
523
  build: 'rshono build',
@@ -574,10 +601,9 @@ function sorted(record) {
574
601
  version: '0.1.0',
575
602
  private: true,
576
603
  type: 'module',
577
- // The floor the framework declares. Stated here too so a CI image or a contributor on an older
578
- // Node finds out from their package manager rather than from a stack trace.
604
+ // Generated from the framework's own manifest, so the app's floor cannot drift below rshono's.
579
605
  engines: {
580
- node: '>=22.1.0'
606
+ node: NODE_ENGINE
581
607
  },
582
608
  scripts,
583
609
  dependencies: sorted(dependencies),
@@ -593,25 +619,25 @@ function sorted(record) {
593
619
  return `${JSON.stringify(ordered, null, 2)}\n`;
594
620
  }
595
621
 
596
- ;// CONCATENATED MODULE: external "node:fs"
597
-
598
- ;// CONCATENATED MODULE: external "node:path"
599
-
600
622
  ;// CONCATENATED MODULE: ./src/render.ts
601
623
 
602
- const TOKEN_PATTERN = /__[A-Z][A-Z\d_]*__/g;
624
+ /**
625
+ * `{{NAME}}`, and not the `__NAME__` this used to be. A template is a real file that real tools run
626
+ * over, and in markdown `__NAME__` *is* strong emphasis — Prettier rewrites it to `**NAME**`, which
627
+ * turns a token into literal text that no substitution will ever match again. `{{…}}` means nothing to
628
+ * any of the formats these templates are written in.
629
+ */ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
603
630
  function tokensFor(answers, pm) {
604
631
  return {
605
- __PROJECT_NAME__: answers.packageName,
606
- __DEPLOY_TARGET__: answers.deploy,
607
- __DEPLOY_HINT__: deployHint(answers.deploy),
608
- __PM__: pm.name,
609
- __PM_RUN__: pm.run
632
+ '{{PROJECT_NAME}}': answers.packageName,
633
+ '{{DEPLOY_TARGET}}': answers.deploy,
634
+ '{{DEPLOY_HINT}}': deployHint(answers.deploy),
635
+ '{{PM_RUN}}': pm.run
610
636
  };
611
637
  }
612
638
  /**
613
639
  * Substitutes tokens, and throws on one it doesn't know — a typo in a template would otherwise ship a
614
- * literal `__PORJECT_NAME__` into somebody's new app, which no test of the generator's logic would
640
+ * literal `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would
615
641
  * catch.
616
642
  */ function render(contents, tokens, source) {
617
643
  return contents.replace(TOKEN_PATTERN, (token)=>{
@@ -702,6 +728,7 @@ function readTemplateDir(dir) {
702
728
 
703
729
  ;// CONCATENATED MODULE: ./src/pm.ts
704
730
 
731
+
705
732
  const INSTALL = {
706
733
  npm: [
707
734
  'install'
@@ -721,7 +748,7 @@ const RUN = {
721
748
  bun: 'bun'
722
749
  };
723
750
  function isKnown(name) {
724
- return name === 'npm' || name === 'pnpm' || name === 'yarn' || name === 'bun';
751
+ return PACKAGE_MANAGERS.includes(name);
725
752
  }
726
753
  function packageManager(name, version) {
727
754
  return {
@@ -733,8 +760,9 @@ function packageManager(name, version) {
733
760
  }
734
761
  /**
735
762
  * Which package manager invoked us. Every one of them sets `npm_config_user_agent` for the process it
736
- * spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `pnpm create @rshono` scaffolds a pnpm
737
- * project without asking, and the exact version comes along for the `packageManager` field.
763
+ * spawns, from its runner as much as from `create` — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so
764
+ * `pnx @rshono/create` scaffolds a pnpm project without asking, and the exact version comes along for the
765
+ * `packageManager` field.
738
766
  *
739
767
  * Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
740
768
  */ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
@@ -778,15 +806,20 @@ function run(pm, args, cwd) {
778
806
  '.vscode',
779
807
  'Thumbs.db'
780
808
  ]);
781
- function inspectTarget(dir) {
782
- if (!existsSync(dir)) return {
783
- exists: false,
784
- conflicts: []
785
- };
786
- return {
787
- exists: true,
788
- conflicts: readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry))
789
- };
809
+ /**
810
+ * What is already at the target path, which is what decides whether scaffolding into it is safe: the
811
+ * entries already there, ignoring the ones a fresh clone or an editor leaves behind.
812
+ *
813
+ * A path that does not exist yet is no conflict. A path that exists and is *not* a directory is not
814
+ * something `--force` should be able to write into, so it throws rather than reporting an empty list —
815
+ * otherwise `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
816
+ */ function conflictingEntries(dir) {
817
+ const stats = statSync(dir, {
818
+ throwIfNoEntry: false
819
+ });
820
+ if (!stats) return [];
821
+ if (!stats.isDirectory()) throw new Error(`${dir} already exists and is not a directory.`);
822
+ return readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
790
823
  }
791
824
  /**
792
825
  * Writes the plan. Directories are created as needed, and files are written with the plan's own
@@ -808,7 +841,16 @@ function inspectTarget(dir) {
808
841
  /**
809
842
  * The generator without the CLI around it: answers in, the exact set of files out. This is what the
810
843
  * test suite asserts on — the whole matrix of options, in memory, with no directory to clean up — and
811
- * what another tool would call to scaffold an app itself.
844
+ * what another tool would call to scaffold an app itself:
845
+ *
846
+ * ```ts
847
+ * import { packageManager, plan, writePlan } from '@rshono/create';
848
+ *
849
+ * const answers = { packageName: 'my-app', deploy: 'node', styling: 'css', formatter: 'prettier', linter: 'oxlint' };
850
+ * writePlan(plan(answers, packageManager('npm')), './my-app');
851
+ * ```
852
+ *
853
+ * `package.json` is assembled inside `plan`, so nothing here needs to be composed by hand.
812
854
  *
813
855
  * @packageDocumentation
814
856
  */
@@ -818,23 +860,22 @@ function inspectTarget(dir) {
818
860
 
819
861
 
820
862
 
821
-
822
863
  var __webpack_exports__DEPLOY_TARGET_NAMES = __webpack_exports__.bp;
823
864
  var __webpack_exports__ESLINT_TYPESCRIPT = __webpack_exports__.ml;
865
+ var __webpack_exports__FORMATTER_NAMES = __webpack_exports__.wC;
824
866
  var __webpack_exports__FRAMEWORK_DEPS = __webpack_exports__.e7;
867
+ var __webpack_exports__LINTER_NAMES = __webpack_exports__.At;
825
868
  var __webpack_exports__PACKAGE_MANAGERS = __webpack_exports__.Ab;
826
869
  var __webpack_exports__QUALITY_PRESETS = __webpack_exports__.pm;
827
870
  var __webpack_exports__RSHONO_RANGE = __webpack_exports__.s_;
828
871
  var __webpack_exports__TOOL_VERSIONS = __webpack_exports__.RM;
829
- var __webpack_exports__buildPackageJson = __webpack_exports__.Jh;
872
+ var __webpack_exports__conflictingEntries = __webpack_exports__.dh;
830
873
  var __webpack_exports__deployHint = __webpack_exports__.P8;
831
874
  var __webpack_exports__detectPackageManager = __webpack_exports__.Lv;
832
- var __webpack_exports__inspectTarget = __webpack_exports__.WL;
833
- var __webpack_exports__isDeployTarget = __webpack_exports__.p1;
834
875
  var __webpack_exports__isValidPackageName = __webpack_exports__.eW;
835
876
  var __webpack_exports__packageManager = __webpack_exports__.VZ;
836
877
  var __webpack_exports__plan = __webpack_exports__.et;
837
878
  var __webpack_exports__selectFeatures = __webpack_exports__.N7;
838
879
  var __webpack_exports__toPackageName = __webpack_exports__.PQ;
839
880
  var __webpack_exports__writePlan = __webpack_exports__.pD;
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 };
881
+ export { __webpack_exports__DEPLOY_TARGET_NAMES as DEPLOY_TARGET_NAMES, __webpack_exports__ESLINT_TYPESCRIPT as ESLINT_TYPESCRIPT, __webpack_exports__FORMATTER_NAMES as FORMATTER_NAMES, __webpack_exports__FRAMEWORK_DEPS as FRAMEWORK_DEPS, __webpack_exports__LINTER_NAMES as LINTER_NAMES, __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__conflictingEntries as conflictingEntries, __webpack_exports__deployHint as deployHint, __webpack_exports__detectPackageManager as detectPackageManager, __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
@@ -6,7 +6,7 @@ import { parseArgs, styleText as external_node_util_styleText } from "node:util"
6
6
  import node_process, { stdin as external_node_process_stdin, stdout as external_node_process_stdout } from "node:process";
7
7
  import node_readline from "node:readline";
8
8
  import "node:tty";
9
- import { existsSync as external_node_fs_existsSync, mkdirSync, readFileSync, readdirSync as external_node_fs_readdirSync, writeFileSync } from "node:fs";
9
+ import { mkdirSync, readFileSync, readdirSync as external_node_fs_readdirSync, statSync, writeFileSync } from "node:fs";
10
10
  import { basename, dirname as external_node_path_dirname, join as external_node_path_join, posix, relative as external_node_path_relative, resolve, sep } from "node:path";
11
11
  import { spawnSync } from "node:child_process";
12
12
  var __webpack_modules__ = ({
@@ -2943,7 +2943,11 @@ 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.1';
2946
+ /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.3';
2947
+ /**
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.
2950
+ */ const NODE_ENGINE = '>=22.1.0';
2947
2951
  /**
2948
2952
  * The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
2949
2953
  * exact — React's RSC internals are coupled across builds, and a generated app has no workspace
@@ -2989,19 +2993,32 @@ function hasGit(cwd) {
2989
2993
 
2990
2994
  ;// CONCATENATED MODULE: ./src/options.ts
2991
2995
 
2992
- const PACKAGE_MANAGERS = (/* unused pure expression or super */ null && ([
2996
+ /*
2997
+ * The names each option accepts, spelled once. The types below are derived from them, the CLI validates
2998
+ * its flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them
2999
+ * — so a name added here reaches all three without a second list to remember.
3000
+ */ const FORMATTER_NAMES = [
3001
+ 'prettier',
3002
+ 'biome',
3003
+ 'oxfmt',
3004
+ 'none'
3005
+ ];
3006
+ const LINTER_NAMES = [
3007
+ 'oxlint',
3008
+ 'eslint',
3009
+ 'biome',
3010
+ 'none'
3011
+ ];
3012
+ const PACKAGE_MANAGERS = [
2993
3013
  'npm',
2994
3014
  'pnpm',
2995
3015
  'yarn',
2996
3016
  'bun'
2997
- ]));
3017
+ ];
2998
3018
  const DEPLOY_TARGET_NAMES = DEPLOY_TARGETS.map((target)=>target.name);
2999
3019
  function deployHint(name) {
3000
3020
  return DEPLOY_TARGETS.find((target)=>target.name === name)?.hint ?? '';
3001
3021
  }
3002
- function isDeployTarget(value) {
3003
- return DEPLOY_TARGET_NAMES.includes(value);
3004
- }
3005
3022
  const QUALITY_PRESETS = [
3006
3023
  {
3007
3024
  id: 'prettier-oxlint',
@@ -3088,7 +3105,7 @@ const QUALITY_PRESETS = [
3088
3105
 
3089
3106
  ;// CONCATENATED MODULE: ./src/versions.ts
3090
3107
 
3091
-
3108
+ /** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
3092
3109
  /** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
3093
3110
  /**
3094
3111
  * Versions for the optional tooling the features can add — the one place in this package where a
@@ -3139,8 +3156,11 @@ const QUALITY_PRESETS = [
3139
3156
  * Deliberately thin. The framework already knows how to arrange its own output for every platform, and
3140
3157
  * `rshono build` writes the one platform config that has to exist (`wrangler.jsonc`, with the
3141
3158
  * `compatibility_date` of the day it ran) if the project has none. Generating a second copy here would
3142
- * be a copy that goes stale. So a target contributes a `deploy` script, the CLI it needs locally, and
3143
- * the build artefacts its platform leaves in the project.
3159
+ * be a copy that goes stale.
3160
+ *
3161
+ * So a target contributes only what is true of it: the command that runs or ships the build, the CLI
3162
+ * that command needs installed locally, the directories its platform leaves behind for `.gitignore`, and
3163
+ * a note for the step no command covers. Several contribute just one of those.
3144
3164
  */ const DEPLOY_FEATURES = {
3145
3165
  // Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
3146
3166
  // guess — so the target contributes only the command that runs what was built.
@@ -3223,8 +3243,10 @@ function deployFeature(target) {
3223
3243
  * deduplicates by `id`, so `formatter: 'biome', linter: 'biome'` contributes one set of files, one
3224
3244
  * dependency and one pair of scripts.
3225
3245
  *
3226
- * Each tool brings its own `check` script alongside `format`/`lint`, because the writing half and the
3227
- * CI half want different exit-code behaviour: `format` rewrites files, `check` fails instead.
3246
+ * A formatter brings `format:check` beside `format`, because the writing half and the CI half want
3247
+ * different exit-code behaviour: `format` rewrites files, `format:check` fails instead. A linter brings
3248
+ * `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
3249
+ * one. Biome adds a `check` of its own, which is the pair of them in a single pass.
3228
3250
  */ const PRETTIER = {
3229
3251
  id: 'prettier',
3230
3252
  overlays: [
@@ -3380,8 +3402,9 @@ function stylingFeature(styling) {
3380
3402
  ;// CONCATENATED MODULE: ./src/pkg.ts
3381
3403
 
3382
3404
  /**
3383
- * The scripts every app gets. `start` is not among them: it is the *Node* launcher, and the deploy
3384
- * features each contribute the command their own platform runs what was built with.
3405
+ * The scripts every app gets. `start` is not among them, because it means something different per
3406
+ * platform: the three targets that run the build themselves node, bun, deno each contribute their
3407
+ * own, and a platform target contributes a `deploy` instead, where its platform has one command to give.
3385
3408
  */ const BASE_SCRIPTS = {
3386
3409
  dev: 'rshono dev',
3387
3410
  build: 'rshono build',
@@ -3465,10 +3488,9 @@ function sorted(record) {
3465
3488
  version: '0.1.0',
3466
3489
  private: true,
3467
3490
  type: 'module',
3468
- // The floor the framework declares. Stated here too so a CI image or a contributor on an older
3469
- // Node finds out from their package manager rather than from a stack trace.
3491
+ // Generated from the framework's own manifest, so the app's floor cannot drift below rshono's.
3470
3492
  engines: {
3471
- node: '>=22.1.0'
3493
+ node: NODE_ENGINE
3472
3494
  },
3473
3495
  scripts,
3474
3496
  dependencies: sorted(dependencies),
@@ -3486,19 +3508,23 @@ function sorted(record) {
3486
3508
 
3487
3509
  ;// CONCATENATED MODULE: ./src/render.ts
3488
3510
 
3489
- const TOKEN_PATTERN = /__[A-Z][A-Z\d_]*__/g;
3511
+ /**
3512
+ * `{{NAME}}`, and not the `__NAME__` this used to be. A template is a real file that real tools run
3513
+ * over, and in markdown `__NAME__` *is* strong emphasis — Prettier rewrites it to `**NAME**`, which
3514
+ * turns a token into literal text that no substitution will ever match again. `{{…}}` means nothing to
3515
+ * any of the formats these templates are written in.
3516
+ */ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
3490
3517
  function tokensFor(answers, pm) {
3491
3518
  return {
3492
- __PROJECT_NAME__: answers.packageName,
3493
- __DEPLOY_TARGET__: answers.deploy,
3494
- __DEPLOY_HINT__: deployHint(answers.deploy),
3495
- __PM__: pm.name,
3496
- __PM_RUN__: pm.run
3519
+ '{{PROJECT_NAME}}': answers.packageName,
3520
+ '{{DEPLOY_TARGET}}': answers.deploy,
3521
+ '{{DEPLOY_HINT}}': deployHint(answers.deploy),
3522
+ '{{PM_RUN}}': pm.run
3497
3523
  };
3498
3524
  }
3499
3525
  /**
3500
3526
  * Substitutes tokens, and throws on one it doesn't know — a typo in a template would otherwise ship a
3501
- * literal `__PORJECT_NAME__` into somebody's new app, which no test of the generator's logic would
3527
+ * literal `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would
3502
3528
  * catch.
3503
3529
  */ function render(contents, tokens, source) {
3504
3530
  return contents.replace(TOKEN_PATTERN, (token)=>{
@@ -3587,6 +3613,7 @@ function readTemplateDir(dir) {
3587
3613
 
3588
3614
  ;// CONCATENATED MODULE: ./src/pm.ts
3589
3615
 
3616
+
3590
3617
  const INSTALL = {
3591
3618
  npm: [
3592
3619
  'install'
@@ -3606,7 +3633,7 @@ const RUN = {
3606
3633
  bun: 'bun'
3607
3634
  };
3608
3635
  function isKnown(name) {
3609
- return name === 'npm' || name === 'pnpm' || name === 'yarn' || name === 'bun';
3636
+ return PACKAGE_MANAGERS.includes(name);
3610
3637
  }
3611
3638
  function packageManager(name, version) {
3612
3639
  return {
@@ -3618,8 +3645,9 @@ function packageManager(name, version) {
3618
3645
  }
3619
3646
  /**
3620
3647
  * Which package manager invoked us. Every one of them sets `npm_config_user_agent` for the process it
3621
- * spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `pnpm create @rshono` scaffolds a pnpm
3622
- * project without asking, and the exact version comes along for the `packageManager` field.
3648
+ * spawns, from its runner as much as from `create` — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so
3649
+ * `pnx @rshono/create` scaffolds a pnpm project without asking, and the exact version comes along for the
3650
+ * `packageManager` field.
3623
3651
  *
3624
3652
  * Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
3625
3653
  */ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
@@ -3700,15 +3728,20 @@ function run(pm, args, cwd) {
3700
3728
  '.vscode',
3701
3729
  'Thumbs.db'
3702
3730
  ]);
3703
- function inspectTarget(dir) {
3704
- if (!external_node_fs_existsSync(dir)) return {
3705
- exists: false,
3706
- conflicts: []
3707
- };
3708
- return {
3709
- exists: true,
3710
- conflicts: external_node_fs_readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry))
3711
- };
3731
+ /**
3732
+ * What is already at the target path, which is what decides whether scaffolding into it is safe: the
3733
+ * entries already there, ignoring the ones a fresh clone or an editor leaves behind.
3734
+ *
3735
+ * A path that does not exist yet is no conflict. A path that exists and is *not* a directory is not
3736
+ * something `--force` should be able to write into, so it throws rather than reporting an empty list —
3737
+ * otherwise `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
3738
+ */ function conflictingEntries(dir) {
3739
+ const stats = statSync(dir, {
3740
+ throwIfNoEntry: false
3741
+ });
3742
+ if (!stats) return [];
3743
+ if (!stats.isDirectory()) throw new Error(`${dir} already exists and is not a directory.`);
3744
+ return external_node_fs_readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
3712
3745
  }
3713
3746
  /**
3714
3747
  * Writes the plan. Directories are created as needed, and files are written with the plan's own
@@ -3736,20 +3769,21 @@ function inspectTarget(dir) {
3736
3769
 
3737
3770
 
3738
3771
 
3772
+
3739
3773
  const DEFAULT_DIRECTORY = 'my-rshono-app';
3740
- const HELP = `create-rshono — scaffold a new rshono app
3774
+ /** Every accepted name comes from `options.ts`, so the help cannot promise one the validation refuses. */ const HELP = `create-rshono — scaffold a new rshono app
3741
3775
 
3742
3776
  Usage:
3743
- npm create @rshono@latest [directory] [options]
3777
+ npx @rshono/create@latest [directory] [options]
3744
3778
 
3745
3779
  Options:
3746
3780
  -y, --yes accept the default for every question not given as a flag
3747
3781
  -d, --deploy <target> ${DEPLOY_TARGET_NAMES.join(' | ')}
3748
3782
  --tailwind Tailwind CSS (--no-tailwind for plain CSS)
3749
3783
  --quality <preset> ${QUALITY_PRESETS.map((preset)=>preset.id).join(' | ')}
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)
3784
+ --formatter <name> ${FORMATTER_NAMES.join(' | ')} (overrides --quality)
3785
+ --linter <name> ${LINTER_NAMES.join(' | ')} (overrides --quality; eslint pins TypeScript 6)
3786
+ --pm <name> ${PACKAGE_MANAGERS.join(' | ')} (default: whatever ran this)
3753
3787
  --no-install write the files and stop
3754
3788
  --no-git do not initialize a repository
3755
3789
  --force scaffold into a directory that is not empty
@@ -3760,7 +3794,7 @@ Options:
3760
3794
  Every question can be answered by a flag, and a non-interactive terminal implies --yes — so one command
3761
3795
  scaffolds without prompting:
3762
3796
 
3763
- npm create @rshono@latest my-app -y --deploy cloudflare --tailwind --quality biome
3797
+ npx @rshono/create@latest my-app -y --deploy cloudflare --tailwind --quality biome
3764
3798
  `;
3765
3799
  function fail(message) {
3766
3800
  log.error(message);
@@ -3777,93 +3811,94 @@ function fail(message) {
3777
3811
  if (off) return false;
3778
3812
  return undefined;
3779
3813
  }
3780
- async function main() {
3781
- const { values, positionals } = parseArgs({
3782
- options: {
3783
- yes: {
3784
- type: 'boolean',
3785
- short: 'y'
3786
- },
3787
- deploy: {
3788
- type: 'string',
3789
- short: 'd'
3790
- },
3791
- tailwind: {
3792
- type: 'boolean'
3793
- },
3794
- 'no-tailwind': {
3795
- type: 'boolean'
3796
- },
3797
- quality: {
3798
- type: 'string'
3799
- },
3800
- formatter: {
3801
- type: 'string'
3802
- },
3803
- linter: {
3804
- type: 'string'
3805
- },
3806
- pm: {
3807
- type: 'string'
3808
- },
3809
- install: {
3810
- type: 'boolean'
3811
- },
3812
- 'no-install': {
3813
- type: 'boolean'
3814
- },
3815
- git: {
3816
- type: 'boolean'
3817
- },
3818
- 'no-git': {
3819
- type: 'boolean'
3820
- },
3821
- force: {
3822
- type: 'boolean'
3823
- },
3824
- 'dry-run': {
3825
- type: 'boolean'
3826
- },
3827
- help: {
3828
- type: 'boolean',
3829
- short: 'h'
3814
+ /**
3815
+ * `parseArgs` rejects an unknown flag with a message that names it but nothing else — so a mistyped
3816
+ * `--tailwnid` reads as a wall of text about positional arguments. Pointing at `--help` is the whole
3817
+ * addition.
3818
+ */ function parse() {
3819
+ try {
3820
+ return parseArgs({
3821
+ options: {
3822
+ yes: {
3823
+ type: 'boolean',
3824
+ short: 'y'
3825
+ },
3826
+ deploy: {
3827
+ type: 'string',
3828
+ short: 'd'
3829
+ },
3830
+ tailwind: {
3831
+ type: 'boolean'
3832
+ },
3833
+ 'no-tailwind': {
3834
+ type: 'boolean'
3835
+ },
3836
+ quality: {
3837
+ type: 'string'
3838
+ },
3839
+ formatter: {
3840
+ type: 'string'
3841
+ },
3842
+ linter: {
3843
+ type: 'string'
3844
+ },
3845
+ pm: {
3846
+ type: 'string'
3847
+ },
3848
+ install: {
3849
+ type: 'boolean'
3850
+ },
3851
+ 'no-install': {
3852
+ type: 'boolean'
3853
+ },
3854
+ git: {
3855
+ type: 'boolean'
3856
+ },
3857
+ 'no-git': {
3858
+ type: 'boolean'
3859
+ },
3860
+ force: {
3861
+ type: 'boolean'
3862
+ },
3863
+ 'dry-run': {
3864
+ type: 'boolean'
3865
+ },
3866
+ help: {
3867
+ type: 'boolean',
3868
+ short: 'h'
3869
+ },
3870
+ version: {
3871
+ type: 'boolean',
3872
+ short: 'v'
3873
+ }
3830
3874
  },
3831
- version: {
3832
- type: 'boolean',
3833
- short: 'v'
3834
- }
3835
- },
3836
- allowPositionals: true
3837
- });
3875
+ allowPositionals: true
3876
+ });
3877
+ } catch (error) {
3878
+ fail(`${error instanceof Error ? error.message : String(error)}\n\nRun with --help to see the options.`);
3879
+ }
3880
+ }
3881
+ async function main() {
3882
+ const { values, positionals } = parse();
3838
3883
  if (values.help) return console.log(HELP);
3839
- if (values.version) return console.log("1.0.0-rc.1");
3840
- // A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer.
3841
- const interactive = Boolean(process.stdout.isTTY) && !values.yes;
3842
- const pmFlag = oneOf(values.pm, [
3843
- 'npm',
3844
- 'pnpm',
3845
- 'yarn',
3846
- 'bun'
3847
- ], 'pm');
3884
+ if (values.version) return console.log("1.0.0-rc.3");
3885
+ /*
3886
+ * A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both streams
3887
+ * have to be a terminal: the prompts draw on stdout but *read from stdin*, so `echo | npx @rshono/create`
3888
+ * with stdout still attached would otherwise ask a question with nothing behind the keyboard.
3889
+ */ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
3890
+ const pmFlag = oneOf(values.pm, PACKAGE_MANAGERS, 'pm');
3848
3891
  const pm = pmFlag ? packageManager(pmFlag) : detectPackageManager();
3849
3892
  const deployFlag = oneOf(values.deploy, DEPLOY_TARGET_NAMES, 'deploy');
3850
- const formatterFlag = oneOf(values.formatter, [
3851
- 'prettier',
3852
- 'biome',
3853
- 'oxfmt',
3854
- 'none'
3855
- ], 'formatter');
3856
- const linterFlag = oneOf(values.linter, [
3857
- 'oxlint',
3858
- 'eslint',
3859
- 'biome',
3860
- 'none'
3861
- ], 'linter');
3893
+ const formatterFlag = oneOf(values.formatter, FORMATTER_NAMES, 'formatter');
3894
+ const linterFlag = oneOf(values.linter, LINTER_NAMES, 'linter');
3862
3895
  const qualityFlag = oneOf(values.quality, QUALITY_PRESETS.map((preset)=>preset.id), 'quality');
3863
3896
  const tailwindFlag = tristate(values.tailwind, values['no-tailwind'], 'tailwind');
3864
3897
  const installFlag = tristate(values.install, values['no-install'], 'install');
3865
3898
  const gitFlag = tristate(values.git, values['no-git'], 'git');
3866
- intro(`create-rshono · rshono ${"1.0.0-rc.1"}`);
3899
+ // The framework version, not this package's: it is the one the app will be pinned to, and the one
3900
+ // worth reading here. `--version` reports create-rshono's own.
3901
+ intro(`create-rshono · rshono ${RSHONO_VERSION}`);
3867
3902
  // ── Where ───────────────────────────────────────────────────────────────────────────────────────
3868
3903
  let directory = positionals[0];
3869
3904
  if (!directory) {
@@ -3877,7 +3912,7 @@ async function main() {
3877
3912
  const targetDir = resolve(process.cwd(), directory);
3878
3913
  const packageName = toPackageName(directory === '.' ? basename(targetDir) : directory);
3879
3914
  if (!packageName || !isValidPackageName(packageName)) fail(`"${directory}" does not give a usable npm package name.`);
3880
- const conflicts = inspectTarget(targetDir).conflicts;
3915
+ const conflicts = conflictingEntries(targetDir);
3881
3916
  if (conflicts.length > 0 && !values.force) {
3882
3917
  const where = directory === '.' ? 'this directory' : `"${directory}"`;
3883
3918
  const listed = `${conflicts.slice(0, 3).join(', ')}${conflicts.length > 3 ? ', …' : ''}`;
@@ -3903,7 +3938,7 @@ async function main() {
3903
3938
  }))
3904
3939
  }));
3905
3940
  }
3906
- let styling = tailwindFlag === undefined ? 'css' : tailwindFlag ? 'tailwind' : 'css';
3941
+ let styling = tailwindFlag ? 'tailwind' : 'css';
3907
3942
  if (tailwindFlag === undefined && interactive) {
3908
3943
  styling = unwrap(await dist_select({
3909
3944
  message: 'Styling?',
@@ -3953,9 +3988,10 @@ async function main() {
3953
3988
  initialValue: true
3954
3989
  }));
3955
3990
  }
3956
- let git = gitFlag ?? !isInsideRepo(process.cwd());
3991
+ // Asked once, not once per use: this shells out to `git rev-parse`.
3992
+ const nested = isInsideRepo(process.cwd());
3993
+ let git = gitFlag ?? !nested;
3957
3994
  if (gitFlag === undefined && interactive) {
3958
- const nested = isInsideRepo(process.cwd());
3959
3995
  git = unwrap(await dist_confirm({
3960
3996
  message: nested ? 'Initialize a git repository? (this is already inside one)' : 'Initialize a git repository?',
3961
3997
  initialValue: !nested
@@ -3963,14 +3999,10 @@ async function main() {
3963
3999
  }
3964
4000
  const answers = {
3965
4001
  packageName,
3966
- targetDir,
3967
4002
  deploy,
3968
4003
  styling,
3969
4004
  formatter,
3970
- linter,
3971
- packageManager: pm.name,
3972
- install,
3973
- git
4005
+ linter
3974
4006
  };
3975
4007
  // ── Plan, then write ────────────────────────────────────────────────────────────────────────────
3976
4008
  const plan = plan_plan(answers, pm);
@@ -3987,14 +4019,17 @@ async function main() {
3987
4019
  if (install) {
3988
4020
  log.step(`Installing dependencies with ${pm.name}…`);
3989
4021
  installed = runInstall(pm, targetDir);
3990
- if (!installed) log.warn(`${pm.name} install failed — run it yourself and the rest will work.`);
4022
+ if (!installed) log.warn(`${pm.name} install failed — the files are all written, so run it yourself in the project.`);
3991
4023
  }
3992
4024
  /*
3993
4025
  * Format the scaffold with the tool it was scaffolded with, so a fresh project passes its own
3994
4026
  * `format:check` instead of reporting a diff nobody made. Needs the install, since the formatter is a
3995
4027
  * devDependency — hence the skip, rather than a failure, when there is none.
3996
- */ if (installed && formatter !== 'none') {
3997
- if (!runScript(pm, 'format', targetDir)) log.warn(`\`${pm.run} format\` failed the files are fine, the formatter is not.`);
4028
+ *
4029
+ * Whether there is a `format` script is the plan's answer, not the formatter answer's: `--formatter
4030
+ * none --linter biome` has no formatter and a `format` script all the same, because Biome brings one.
4031
+ */ if (installed && plan.features.some((feature)=>feature.scripts?.format)) {
4032
+ if (!runScript(pm, 'format', targetDir)) log.warn(`\`${pm.run} format\` failed — the scaffold is written, just not formatted.`);
3998
4033
  }
3999
4034
  if (git) {
4000
4035
  if (!hasGit(targetDir)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rshono/create",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.3",
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",
@@ -41,19 +41,18 @@
41
41
  "publishConfig": {
42
42
  "access": "public"
43
43
  },
44
+ "devDependencies": {
45
+ "@clack/prompts": "^1.7.0",
46
+ "@rspack/core": "2.1.5",
47
+ "@types/node": "^26.1.1",
48
+ "typescript": "^7.0.2",
49
+ "@rshono/core": "1.0.0-rc.3"
50
+ },
44
51
  "scripts": {
45
52
  "build": "node scripts/codegen.mjs && node scripts/build.mjs",
46
53
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
47
54
  "codegen": "node scripts/codegen.mjs",
48
55
  "typecheck": "node scripts/codegen.mjs && tsc --noEmit",
49
- "test": "npm run build && node --test --test-concurrency=1 \"test/*.test.mjs\"",
50
- "prepack": "npm run build"
51
- },
52
- "devDependencies": {
53
- "@clack/prompts": "^1.7.0",
54
- "@rspack/core": "2.1.5",
55
- "@types/node": "^26.1.1",
56
- "@rshono/core": "workspace:*",
57
- "typescript": "^7.0.2"
56
+ "test": "npm run build && node --test --test-concurrency=1 \"test/*.test.mjs\""
58
57
  }
59
- }
58
+ }
@@ -1,11 +1,11 @@
1
- # **PROJECT_NAME**
1
+ # {{PROJECT_NAME}}
2
2
 
3
3
  A [rshono](https://github.com/rshono/rshono) app — [Hono](https://hono.dev) + [Rspack](https://rspack.rs) + [React Server Components](https://react.dev/reference/rsc/server-components).
4
4
 
5
5
  ```bash
6
- __PM_RUN__ dev # dev server with HMR, http://localhost:3000
7
- __PM_RUN__ build # production build for __DEPLOY_TARGET__
8
- __PM_RUN__ typecheck # tsc --noEmit
6
+ {{PM_RUN}} dev # dev server with HMR, http://localhost:3000
7
+ {{PM_RUN}} build # production build for {{DEPLOY_TARGET}}
8
+ {{PM_RUN}} typecheck # tsc --noEmit
9
9
  ```
10
10
 
11
11
  `package.json` has the rest, including whatever your formatter and linter added.
@@ -35,7 +35,7 @@ compiles to `undefined` rather than shipping. `src/lib/env.ts` is where both sid
35
35
 
36
36
  ## Deploying
37
37
 
38
- This app is built for `__DEPLOY_TARGET__`: after `__PM_RUN__ build`, **DEPLOY_HINT**.
38
+ This app is built for `{{DEPLOY_TARGET}}`: after `{{PM_RUN}} build`, {{DEPLOY_HINT}}.
39
39
 
40
40
  Change `deploy` in `rshono.config.ts` to target somewhere else, or build for one place without editing the
41
41
  file: `rshono build --deploy vercel`, or `RSHONO_DEPLOY=vercel` in CI. `dev` always runs the Node dev
@@ -6,4 +6,4 @@
6
6
  # Only `PUBLIC_`-prefixed variables are compiled into the browser bundle. Anything else is server-only:
7
7
  # a stray read of it in client code compiles to `undefined` rather than shipping.
8
8
 
9
- PUBLIC_APP_NAME="__PROJECT_NAME__"
9
+ PUBLIC_APP_NAME="{{PROJECT_NAME}}"
@@ -1 +1,2 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><title>Lightning bolt</title><text y=".9em" font-size="90">⚡</text></svg>
1
+ <!-- The rshono mark, as a placeholder — replace it with your own and the tab is yours. -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><title>rshono</title><path fill="#087EA4" d="M12.4365 0.2520325c0.062375 -0.0080215 0.11745 0.0077075 0.16515 0.0471875 1.755075 2.142025 3.40665 4.359855 4.954725 6.65348 1.14615 1.725625 2.121325 3.550175 2.92565 5.4738 1.2845 3.426025 0.616 6.40675 -2.005475 8.9421 -2.293725 1.938575 -4.936225 2.693575 -7.927575 2.265025 -3.57555 -0.729025 -6.00575 -2.7974 -7.290525 -6.205225 -0.33465 -1.109425 -0.44475 -2.241925 -0.3303 -3.397525 0.19055 -1.9891 0.662425 -3.9081 1.415625 -5.7569 0.31385 -0.75435 0.722825 -1.4464 1.2269 -2.076275 0.411225 0.4898 0.80445 0.993175 1.179675 1.510025 0.17375 0.181625 0.354625 0.35465 0.542675 0.51905C8.728325 5.378325 10.44285 2.7201 12.4365 0.2520325Z"/><path fill="#61DAFB" d="M12.10625 4.07425c1.73145 2.008325 3.296525 4.1475 4.695175 6.41755 0.438525 0.75115 0.800275 1.537625 1.085325 2.3594 0.593825 2.336175 -0.043225 4.26305 -1.9111 5.7805 -1.80655 1.2712 -3.788425 1.6487 -5.945675 1.132525 -2.326325 -0.721875 -3.671175 -2.286975 -4.034575 -4.6952 -0.088175 -0.7593 -0.009525 -1.4986 0.23595 -2.217825 0.35005 -0.888875 0.774725 -1.73825 1.274075 -2.54815 0.471875 -0.6921 0.94375 -1.38415 1.415625 -2.07625 1.071925 -1.378375 2.13365 -2.762525 3.1852 -4.15255Z"/></svg>
@@ -10,7 +10,7 @@ import { defineConfig } from '@rshono/core';
10
10
  */
11
11
  export default defineConfig({
12
12
  /** Where `build` targets. Overridable per build with `--deploy` or `RSHONO_DEPLOY`. */
13
- deploy: '__DEPLOY_TARGET__',
13
+ deploy: '{{DEPLOY_TARGET}}',
14
14
 
15
15
  // The public origin, baked into prerendered pages' absolute URLs. Set it if you use `render: 'static'`
16
16
  // and build canonical tags, `og:url` or absolute links — there is no request to read a Host from.
@@ -9,7 +9,7 @@
9
9
 
10
10
  /** Safe anywhere: `PUBLIC_` variables are the ones compiled into the browser bundle. */
11
11
  export const publicEnv = {
12
- appName: process.env.PUBLIC_APP_NAME ?? '__PROJECT_NAME__',
12
+ appName: process.env.PUBLIC_APP_NAME ?? '{{PROJECT_NAME}}',
13
13
  };
14
14
 
15
15
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
3
- "files": { "includes": ["**", "!dist", "!wrangler.jsonc"] },
3
+ "files": { "includes": ["**", "!dist", "!.wrangler", "!.vercel", "!.netlify", "!wrangler.jsonc"] },
4
4
  "formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 140 },
5
5
  "javascript": { "formatter": { "quoteStyle": "single", "jsxQuoteStyle": "double" } },
6
6
  "linter": { "enabled": true, "rules": { "preset": "recommended" } }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
3
- "files": { "includes": ["**", "!dist", "!wrangler.jsonc", "!**/*.css"] },
3
+ "files": { "includes": ["**", "!dist", "!.wrangler", "!.vercel", "!.netlify", "!wrangler.jsonc", "!**/*.css"] },
4
4
  "formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 140 },
5
5
  "javascript": { "formatter": { "quoteStyle": "single", "jsxQuoteStyle": "double" } },
6
6
  "linter": { "enabled": true, "rules": { "preset": "recommended" } }
@@ -3,5 +3,5 @@
3
3
  "printWidth": 140,
4
4
  "singleQuote": true,
5
5
  "trailingComma": "all",
6
- "ignorePatterns": ["wrangler.jsonc"]
6
+ "ignorePatterns": ["dist", ".wrangler", ".vercel", ".netlify", "wrangler.jsonc"]
7
7
  }
@@ -3,5 +3,5 @@
3
3
  "categories": {
4
4
  "correctness": "error"
5
5
  },
6
- "ignorePatterns": ["dist"]
6
+ "ignorePatterns": ["dist", ".wrangler", ".vercel", ".netlify"]
7
7
  }
@@ -1,10 +1,15 @@
1
1
  node_modules/
2
- dist/
3
2
  pnpm-lock.yaml
4
3
  package-lock.json
5
4
  yarn.lock
6
5
  bun.lock
7
6
 
7
+ # Build output: `rshono build`'s own, and whatever the deploy target assembles beside it.
8
+ dist/
9
+ .wrangler/
10
+ .vercel/
11
+ .netlify/
12
+
8
13
  # Written by `rshono build` for the Cloudflare target, in the shape the build decided. Yours to edit —
9
14
  # just not the formatter's to reflow on every build.
10
15
  wrangler.jsonc
@@ -10,7 +10,7 @@ import { defineConfig } from '@rshono/core';
10
10
  */
11
11
  export default defineConfig({
12
12
  /** Where `build` targets. Overridable per build with `--deploy` or `RSHONO_DEPLOY`. */
13
- deploy: '__DEPLOY_TARGET__',
13
+ deploy: '{{DEPLOY_TARGET}}',
14
14
 
15
15
  // The public origin, baked into prerendered pages' absolute URLs. Set it if you use `render: 'static'`
16
16
  // and build canonical tags, `og:url` or absolute links — there is no request to read a Host from.