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

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
@@ -51,7 +51,7 @@ a target added to rshono appears here with no edit.
51
51
  ## What you get
52
52
 
53
53
  ```
54
- package.json scripts, and the exact dependency versions rshono is tested against
54
+ package.json scripts, including the target's own, and the exact pins rshono is tested against
55
55
  rshono.config.ts the chosen deploy target; every other setting takes its default
56
56
  tsconfig.json strict, with @/* → ./src/*
57
57
  .env committed defaults; secrets go in .env.local
@@ -71,6 +71,21 @@ Scaffolding runs the install and nothing else — no formatter, no linter. The t
71
71
  every formatter option at its configured width, so a fresh project passes its own `format:check` without a
72
72
  write pass first.
73
73
 
74
+ Beside `dev`, `build` and `typecheck`, the deploy target contributes the scripts only it can define, under three
75
+ names that mean the same thing in every app. **`deploy`** builds and ships it, where the platform has one
76
+ command that does the shipping (`cloudflare`, `vercel`). **`preview`** builds and runs the result here — in
77
+ workerd for `cloudflare`, and as a Node build for `vercel` and `aws-lambda`, since neither can run its own
78
+ output locally. **`start`** runs a build that already exists and never makes one, which is what a host's start
79
+ command needs — so only `node` has one, and needs no `preview`: `build` then `start` already is that.
80
+
81
+ Every command is spelled for the package manager the project got. That includes the runner for the one CLI a
82
+ scaffold does not install, Vercel's: `npx`, `pnpm dlx`, `yarn dlx` or `bunx`. Wrangler is a devDependency
83
+ instead, since `wrangler dev` is how a Cloudflare app previews.
84
+
85
+ Those scripts are for a laptop, and a platform building from git asks a different question — what to type into
86
+ two fields on a settings page, where a `deploy` script would build twice. So each target also spells out the
87
+ commands its own platform asks for, in the scaffolded README's Deploying section.
88
+
74
89
  `react` and `react-dom` are pinned **exactly**, at the versions the framework is tested against, and those
75
90
  pins are generated from rshono's own manifest. That is not tidiness: the RSC runtime reaches into React's
76
91
  internals, and an app installed with npm or bun has no workspace overrides to keep a single copy of it.
package/dist/api.mjs CHANGED
@@ -77,9 +77,84 @@ __webpack_require__.d(__webpack_exports__, {
77
77
  ] : [];
78
78
  }
79
79
 
80
+ ;// CONCATENATED MODULE: ./src/scripts.ts
81
+ // `features/types.js` rather than `features/index.js`: the deploy feature imports `invoke` from here, and
82
+ // going through the barrel would make that a cycle — types.ts imports nothing.
83
+ /**
84
+ * The scripts every app gets, whatever it targets.
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:
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.
94
+ * - **`deploy`** builds, then ships it — where the platform has one command that does the shipping.
95
+ */ const BASE_SCRIPTS = {
96
+ dev: 'rshono dev',
97
+ build: 'rshono build',
98
+ typecheck: 'tsc --noEmit'
99
+ };
100
+ /**
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.
104
+ */ function buildScripts(features) {
105
+ const scripts = {
106
+ ...BASE_SCRIPTS
107
+ };
108
+ for (const feature of features)Object.assign(scripts, feature.scripts);
109
+ return scripts;
110
+ }
111
+ /** The gloss for each base script, in the README's command table. `build` names the target it is for. */ function baseScriptHelp(deploy) {
112
+ return {
113
+ dev: 'dev server with HMR, http://localhost:3000',
114
+ build: `production build for ${deploy}`,
115
+ typecheck: 'tsc --noEmit'
116
+ };
117
+ }
118
+ /**
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.
122
+ */ const SHADOWED = new Set([
123
+ 'deploy'
124
+ ]);
125
+ /** How to type one of the app's scripts with this package manager — `pnpm dev`, but `npm run dev`. */ function invoke(pm, script) {
126
+ return `${SHADOWED.has(script) ? `${pm.name} run` : pm.run} ${script}`;
127
+ }
128
+ /**
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.
133
+ */ function scriptTable(answers, features, pm) {
134
+ const help = baseScriptHelp(answers.deploy);
135
+ for (const feature of features)Object.assign(help, feature.scriptHelp);
136
+ const documented = Object.keys(buildScripts(features)).filter((name)=>help[name]);
137
+ const width = Math.max(...documented.map((name)=>invoke(pm, name).length));
138
+ return documented.map((name)=>`${invoke(pm, name).padEnd(width)} # ${help[name]}`).join('\n');
139
+ }
140
+ /**
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.
143
+ *
144
+ * 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
+ */ function deployStep(features, pm) {
147
+ const scripts = buildScripts(features);
148
+ if (scripts.deploy) return `\`${invoke(pm, 'deploy')}\` does the build and the upload in one step.`;
149
+ 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
+ if (scripts.preview) return `\`${invoke(pm, 'preview')}\` runs the build here, so you can check it first.`;
152
+ return `\`${invoke(pm, 'build')}\` produces it; getting it there is yours to script.`;
153
+ }
154
+
80
155
  ;// CONCATENATED MODULE: ./src/generated/framework.ts
81
156
  // 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.10';
157
+ /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.12';
83
158
  /**
84
159
  * The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
85
160
  * or a contributor on an older Node hears it from their package manager rather than from a stack trace.
@@ -160,66 +235,128 @@ __webpack_require__.d(__webpack_exports__, {
160
235
 
161
236
  ;// CONCATENATED MODULE: ./src/features/deploy.ts
162
237
 
238
+
163
239
  /**
164
240
  * What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
165
241
  *
166
242
  * Deliberately thin: the framework arranges its own output for every platform, and `rshono build`
167
- * writes the one platform config that has to exist (`wrangler.jsonc`, dated the day it ran) if the
168
- * project has none — a second copy generated here would only go stale. So a target contributes the
169
- * command that ships the build, the CLI that command needs, the directories to gitignore, and a note
170
- * for the step no command covers. Several contribute just one of those.
171
- */ const DEPLOY_FEATURES = {
172
- // Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
173
- // guess so the target contributes only the command that runs what was built.
174
- node: {
175
- id: 'deploy-node',
176
- scripts: {
177
- start: 'rshono start'
178
- }
179
- },
180
- cloudflare: {
181
- id: 'deploy-cloudflare',
182
- devDependencies: {
183
- wrangler: TOOL_VERSIONS.wrangler
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.
250
+ *
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}.
253
+ */ function deployFeatures(pm) {
254
+ const build = invoke(pm, 'build');
255
+ 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.
258
+ node: {
259
+ id: 'deploy-node',
260
+ scripts: {
261
+ start: 'rshono start'
262
+ },
263
+ scriptHelp: {
264
+ start: 'run the build that exists — what your host calls'
265
+ },
266
+ platformSetup: [
267
+ 'The two commands a host asks for:',
268
+ '',
269
+ `- **Build** — \`${pm.name} install && ${build}\``,
270
+ `- **Start** — \`${invoke(pm, 'start')}\``,
271
+ '',
272
+ `In a Dockerfile, the same pair: \`RUN ${build}\`, then \`CMD ["${pm.name}", "start"]\`.`
273
+ ].join('\n')
184
274
  },
185
- // The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each
186
- // one merely picks the platform binary out of the optional dependency that already carries it, so
187
- // neither needs to run — `workerd --version` and `esbuild --version` both answer without it.
188
- allowBuilds: {
189
- esbuild: false,
190
- workerd: false
275
+ cloudflare: {
276
+ id: 'deploy-cloudflare',
277
+ devDependencies: {
278
+ wrangler: TOOL_VERSIONS.wrangler
279
+ },
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.
283
+ allowBuilds: {
284
+ esbuild: false,
285
+ workerd: false
286
+ },
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.
290
+ scripts: {
291
+ preview: 'rshono build && wrangler dev',
292
+ deploy: 'rshono build && wrangler deploy'
293
+ },
294
+ scriptHelp: {
295
+ preview: 'build, then run it in workerd — port 8787',
296
+ deploy: 'build, then ship it to Cloudflare'
297
+ },
298
+ gitignore: [
299
+ '.wrangler/'
300
+ ],
301
+ notes: [
302
+ 'The first build writes wrangler.jsonc — yours to edit after that.'
303
+ ],
304
+ platformSetup: [
305
+ `Building from a git repo instead: set Workers Builds' **Build command** to \`${build}\`.`,
306
+ 'Its deploy command already defaults to `npx wrangler deploy`, and it installs dependencies itself.'
307
+ ].join('\n')
191
308
  },
192
- scripts: {
193
- deploy: 'rshono build && wrangler deploy'
309
+ vercel: {
310
+ 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.
314
+ scripts: {
315
+ preview: 'rshono build --deploy node && rshono start',
316
+ deploy: `rshono build && ${pm.dlx} vercel deploy --prebuilt --prod`
317
+ },
318
+ scriptHelp: {
319
+ preview: 'build for Node and run that here',
320
+ deploy: 'build, then upload it to production'
321
+ },
322
+ gitignore: [
323
+ '.vercel/'
324
+ ],
325
+ notes: [
326
+ '--prebuilt uploads what rshono build assembled; the platform must not rebuild it.',
327
+ 'Drop --prod from the deploy script for a preview URL instead.'
328
+ ],
329
+ platformSetup: [
330
+ `Deploying from CI instead: the same \`${invoke(pm, 'deploy')}\`, with \`VERCEL_ORG_ID\`, \`VERCEL_PROJECT_ID\``,
331
+ 'and a CLI token in the environment.',
332
+ '',
333
+ `If you let Vercel build the repo itself, set **Framework Preset** to Other — \`hono\` is otherwise`,
334
+ `detected as the Hono preset — and **Build Command** to \`${build}\`.`
335
+ ].join('\n')
194
336
  },
195
- gitignore: [
196
- '.wrangler/'
197
- ],
198
- notes: [
199
- 'The first build writes wrangler.jsonc — yours to edit after that.'
200
- ]
201
- },
202
- vercel: {
203
- id: 'deploy-vercel',
204
- scripts: {
205
- deploy: 'rshono build && vercel deploy --prebuilt'
206
- },
207
- gitignore: [
208
- '.vercel/'
209
- ],
210
- notes: [
211
- '--prebuilt uploads what rshono build assembled; the platform must not rebuild it.'
212
- ]
213
- },
214
- 'aws-lambda': {
215
- id: 'deploy-aws-lambda',
216
- notes: [
217
- 'Use a Function URL in RESPONSE_STREAM mode — a buffered invoke mode drops the streaming.'
218
- ]
219
- }
220
- };
221
- function deployFeature(target) {
222
- return DEPLOY_FEATURES[target];
337
+ 'aws-lambda': {
338
+ 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.
341
+ scripts: {
342
+ preview: 'rshono build --deploy node && rshono start'
343
+ },
344
+ scriptHelp: {
345
+ preview: 'build for Node and run that here'
346
+ },
347
+ notes: [
348
+ 'Use a Function URL in RESPONSE_STREAM mode — a buffered invoke mode drops the streaming.'
349
+ ],
350
+ platformSetup: [
351
+ `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.'
354
+ ].join('\n')
355
+ }
356
+ };
357
+ }
358
+ function deployFeature(target, pm) {
359
+ return deployFeatures(pm)[target];
223
360
  }
224
361
 
225
362
  ;// CONCATENATED MODULE: ./src/features/quality.ts
@@ -365,9 +502,12 @@ function stylingFeature(styling) {
365
502
  * The features a set of answers selects, in application order — so an overlay listed later wins a file
366
503
  * both of them ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome
367
504
  * is both the formatter and the linter) without contributing twice.
368
- */ function selectFeatures(answers) {
505
+ *
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.
508
+ */ function selectFeatures(answers, pm) {
369
509
  const selected = [
370
- deployFeature(answers.deploy),
510
+ deployFeature(answers.deploy, pm),
371
511
  stylingFeature(answers.styling),
372
512
  formatterFeature(answers.formatter),
373
513
  linterFeature(answers.linter),
@@ -474,15 +614,7 @@ const QUALITY_PRESETS = [
474
614
 
475
615
  ;// CONCATENATED MODULE: ./src/pkg.ts
476
616
 
477
- /**
478
- * The scripts every app gets. `start` is not among them, because it means something different per
479
- * platform: `node` is the one target that runs the build itself, so it contributes its own, and a
480
- * platform target contributes a `deploy` instead, where its platform has one command to give.
481
- */ const BASE_SCRIPTS = {
482
- dev: 'rshono dev',
483
- build: 'rshono build',
484
- typecheck: 'tsc --noEmit'
485
- };
617
+
486
618
  /** Field order in the emitted file — the conventional reading order, and stable so snapshots are too. */ const FIELD_ORDER = [
487
619
  'name',
488
620
  'version',
@@ -524,13 +656,9 @@ function sorted(record) {
524
656
  /**
525
657
  * Assembles `package.json` from the answers and whatever the selected features contribute.
526
658
  *
527
- * Dependencies are sorted by name and scripts are left in contribution order (the base ones, then each
528
- * feature's, in the order features were selected) so two runs with the same answers produce byte-
529
- * identical output, which is what makes the generated manifest snapshot-testable.
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.
530
661
  */ function buildPackageJson(answers, features, pm) {
531
- const scripts = {
532
- ...BASE_SCRIPTS
533
- };
534
662
  const dependencies = {
535
663
  '@rshono/core': RSHONO_RANGE,
536
664
  hono: FRAMEWORK_DEPS.hono,
@@ -543,7 +671,6 @@ function sorted(record) {
543
671
  typescript: FRAMEWORK_DEPS.typescript
544
672
  };
545
673
  for (const feature of features){
546
- Object.assign(scripts, feature.scripts);
547
674
  Object.assign(dependencies, feature.dependencies);
548
675
  Object.assign(devDependencies, feature.devDependencies);
549
676
  }
@@ -556,7 +683,7 @@ function sorted(record) {
556
683
  engines: {
557
684
  node: NODE_ENGINE
558
685
  },
559
- scripts,
686
+ scripts: buildScripts(features),
560
687
  dependencies: sorted(dependencies),
561
688
  devDependencies: sorted(devDependencies)
562
689
  };
@@ -577,12 +704,15 @@ function sorted(record) {
577
704
  * markdown `__NAME__` *is* strong emphasis — Prettier rewrites it to `**NAME**` and the token stops
578
705
  * matching. `{{…}}` means nothing to any format these templates are written in.
579
706
  */ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
580
- function tokensFor(answers, pm) {
707
+ function tokensFor(answers, features, pm) {
581
708
  return {
582
709
  '{{PROJECT_NAME}}': answers.packageName,
583
710
  '{{DEPLOY_TARGET}}': answers.deploy,
584
- '{{DEPLOY_HINT}}': deployHint(answers.deploy),
585
- '{{PM_RUN}}': pm.run
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.
713
+ '{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
714
+ '{{DEPLOY_STEP}}': deployStep(features, pm),
715
+ '{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
586
716
  };
587
717
  }
588
718
  /**
@@ -645,8 +775,8 @@ function readTemplateDir(dir) {
645
775
  * decisions and the I/O are separated so the whole matrix of answers can be asserted on in a test, and
646
776
  * so `--dry-run` is the same code path minus the last step.
647
777
  */ function plan_plan(answers, pm) {
648
- const features = selectFeatures(answers);
649
- const tokens = tokensFor(answers, pm);
778
+ const features = selectFeatures(answers, pm);
779
+ const tokens = tokensFor(answers, features, pm);
650
780
  const raw = readTemplateDir(join(TEMPLATES_DIR, 'base'));
651
781
  for (const feature of features){
652
782
  for (const overlay of feature.overlays ?? []){
@@ -698,6 +828,12 @@ const RUN = {
698
828
  yarn: 'yarn',
699
829
  bun: 'bun'
700
830
  };
831
+ const DLX = {
832
+ npm: 'npx',
833
+ pnpm: 'pnpm dlx',
834
+ yarn: 'yarn dlx',
835
+ bun: 'bunx'
836
+ };
701
837
  function isKnown(name) {
702
838
  return PACKAGE_MANAGERS.includes(name);
703
839
  }
@@ -706,7 +842,8 @@ function packageManager(name, version) {
706
842
  name,
707
843
  version,
708
844
  install: INSTALL[name],
709
- run: RUN[name]
845
+ run: RUN[name],
846
+ dlx: DLX[name]
710
847
  };
711
848
  }
712
849
  /**
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.10';
2946
+ /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.12';
2947
2947
  /**
2948
2948
  * The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
2949
2949
  * or a contributor on an older Node hears it from their package manager rather than from a stack trace.
@@ -3091,6 +3091,81 @@ const QUALITY_PRESETS = [
3091
3091
  ] : [];
3092
3092
  }
3093
3093
 
3094
+ ;// CONCATENATED MODULE: ./src/scripts.ts
3095
+ // `features/types.js` rather than `features/index.js`: the deploy feature imports `invoke` from here, and
3096
+ // going through the barrel would make that a cycle — types.ts imports nothing.
3097
+ /**
3098
+ * The scripts every app gets, whatever it targets.
3099
+ *
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:
3103
+ *
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.
3108
+ * - **`deploy`** builds, then ships it — where the platform has one command that does the shipping.
3109
+ */ const BASE_SCRIPTS = {
3110
+ dev: 'rshono dev',
3111
+ build: 'rshono build',
3112
+ typecheck: 'tsc --noEmit'
3113
+ };
3114
+ /**
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.
3118
+ */ function buildScripts(features) {
3119
+ const scripts = {
3120
+ ...BASE_SCRIPTS
3121
+ };
3122
+ for (const feature of features)Object.assign(scripts, feature.scripts);
3123
+ return scripts;
3124
+ }
3125
+ /** The gloss for each base script, in the README's command table. `build` names the target it is for. */ function baseScriptHelp(deploy) {
3126
+ return {
3127
+ dev: 'dev server with HMR, http://localhost:3000',
3128
+ build: `production build for ${deploy}`,
3129
+ typecheck: 'tsc --noEmit'
3130
+ };
3131
+ }
3132
+ /**
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.
3136
+ */ const SHADOWED = new Set([
3137
+ 'deploy'
3138
+ ]);
3139
+ /** How to type one of the app's scripts with this package manager — `pnpm dev`, but `npm run dev`. */ function invoke(pm, script) {
3140
+ return `${SHADOWED.has(script) ? `${pm.name} run` : pm.run} ${script}`;
3141
+ }
3142
+ /**
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.
3147
+ */ function scriptTable(answers, features, pm) {
3148
+ const help = baseScriptHelp(answers.deploy);
3149
+ for (const feature of features)Object.assign(help, feature.scriptHelp);
3150
+ const documented = Object.keys(buildScripts(features)).filter((name)=>help[name]);
3151
+ const width = Math.max(...documented.map((name)=>invoke(pm, name).length));
3152
+ return documented.map((name)=>`${invoke(pm, name).padEnd(width)} # ${help[name]}`).join('\n');
3153
+ }
3154
+ /**
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.
3157
+ *
3158
+ * 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
+ */ function deployStep(features, pm) {
3161
+ const scripts = buildScripts(features);
3162
+ if (scripts.deploy) return `\`${invoke(pm, 'deploy')}\` does the build and the upload in one step.`;
3163
+ 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
+ if (scripts.preview) return `\`${invoke(pm, 'preview')}\` runs the build here, so you can check it first.`;
3166
+ return `\`${invoke(pm, 'build')}\` produces it; getting it there is yours to script.`;
3167
+ }
3168
+
3094
3169
  ;// CONCATENATED MODULE: ./src/versions.ts
3095
3170
 
3096
3171
  /** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
@@ -3136,66 +3211,128 @@ const QUALITY_PRESETS = [
3136
3211
 
3137
3212
  ;// CONCATENATED MODULE: ./src/features/deploy.ts
3138
3213
 
3214
+
3139
3215
  /**
3140
3216
  * What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
3141
3217
  *
3142
3218
  * Deliberately thin: the framework arranges its own output for every platform, and `rshono build`
3143
- * writes the one platform config that has to exist (`wrangler.jsonc`, dated the day it ran) if the
3144
- * project has none — a second copy generated here would only go stale. So a target contributes the
3145
- * command that ships the build, the CLI that command needs, the directories to gitignore, and a note
3146
- * for the step no command covers. Several contribute just one of those.
3147
- */ const DEPLOY_FEATURES = {
3148
- // Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
3149
- // guess so the target contributes only the command that runs what was built.
3150
- node: {
3151
- id: 'deploy-node',
3152
- scripts: {
3153
- start: 'rshono start'
3154
- }
3155
- },
3156
- cloudflare: {
3157
- id: 'deploy-cloudflare',
3158
- devDependencies: {
3159
- wrangler: TOOL_VERSIONS.wrangler
3160
- },
3161
- // The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each
3162
- // one merely picks the platform binary out of the optional dependency that already carries it, so
3163
- // neither needs to run — `workerd --version` and `esbuild --version` both answer without it.
3164
- allowBuilds: {
3165
- esbuild: false,
3166
- workerd: false
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.
3222
+ *
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}.
3229
+ */ function deployFeatures(pm) {
3230
+ const build = invoke(pm, 'build');
3231
+ 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.
3234
+ node: {
3235
+ id: 'deploy-node',
3236
+ scripts: {
3237
+ start: 'rshono start'
3238
+ },
3239
+ scriptHelp: {
3240
+ start: 'run the build that exists — what your host calls'
3241
+ },
3242
+ platformSetup: [
3243
+ 'The two commands a host asks for:',
3244
+ '',
3245
+ `- **Build** — \`${pm.name} install && ${build}\``,
3246
+ `- **Start** — \`${invoke(pm, 'start')}\``,
3247
+ '',
3248
+ `In a Dockerfile, the same pair: \`RUN ${build}\`, then \`CMD ["${pm.name}", "start"]\`.`
3249
+ ].join('\n')
3167
3250
  },
3168
- scripts: {
3169
- deploy: 'rshono build && wrangler deploy'
3251
+ cloudflare: {
3252
+ id: 'deploy-cloudflare',
3253
+ devDependencies: {
3254
+ wrangler: TOOL_VERSIONS.wrangler
3255
+ },
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.
3259
+ allowBuilds: {
3260
+ esbuild: false,
3261
+ workerd: false
3262
+ },
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.
3266
+ scripts: {
3267
+ preview: 'rshono build && wrangler dev',
3268
+ deploy: 'rshono build && wrangler deploy'
3269
+ },
3270
+ scriptHelp: {
3271
+ preview: 'build, then run it in workerd — port 8787',
3272
+ deploy: 'build, then ship it to Cloudflare'
3273
+ },
3274
+ gitignore: [
3275
+ '.wrangler/'
3276
+ ],
3277
+ notes: [
3278
+ 'The first build writes wrangler.jsonc — yours to edit after that.'
3279
+ ],
3280
+ platformSetup: [
3281
+ `Building from a git repo instead: set Workers Builds' **Build command** to \`${build}\`.`,
3282
+ 'Its deploy command already defaults to `npx wrangler deploy`, and it installs dependencies itself.'
3283
+ ].join('\n')
3170
3284
  },
3171
- gitignore: [
3172
- '.wrangler/'
3173
- ],
3174
- notes: [
3175
- 'The first build writes wrangler.jsoncyours to edit after that.'
3176
- ]
3177
- },
3178
- vercel: {
3179
- id: 'deploy-vercel',
3180
- scripts: {
3181
- deploy: 'rshono build && vercel deploy --prebuilt'
3285
+ vercel: {
3286
+ 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 herethe platform has no way to run its own prebuilt output on your machine.
3290
+ scripts: {
3291
+ preview: 'rshono build --deploy node && rshono start',
3292
+ deploy: `rshono build && ${pm.dlx} vercel deploy --prebuilt --prod`
3293
+ },
3294
+ scriptHelp: {
3295
+ preview: 'build for Node and run that here',
3296
+ deploy: 'build, then upload it to production'
3297
+ },
3298
+ gitignore: [
3299
+ '.vercel/'
3300
+ ],
3301
+ notes: [
3302
+ '--prebuilt uploads what rshono build assembled; the platform must not rebuild it.',
3303
+ 'Drop --prod from the deploy script for a preview URL instead.'
3304
+ ],
3305
+ platformSetup: [
3306
+ `Deploying from CI instead: the same \`${invoke(pm, 'deploy')}\`, with \`VERCEL_ORG_ID\`, \`VERCEL_PROJECT_ID\``,
3307
+ 'and a CLI token in the environment.',
3308
+ '',
3309
+ `If you let Vercel build the repo itself, set **Framework Preset** to Other — \`hono\` is otherwise`,
3310
+ `detected as the Hono preset — and **Build Command** to \`${build}\`.`
3311
+ ].join('\n')
3182
3312
  },
3183
- gitignore: [
3184
- '.vercel/'
3185
- ],
3186
- notes: [
3187
- '--prebuilt uploads what rshono build assembled; the platform must not rebuild it.'
3188
- ]
3189
- },
3190
- 'aws-lambda': {
3191
- id: 'deploy-aws-lambda',
3192
- notes: [
3193
- 'Use a Function URL in RESPONSE_STREAM mode — a buffered invoke mode drops the streaming.'
3194
- ]
3195
- }
3196
- };
3197
- function deployFeature(target) {
3198
- return DEPLOY_FEATURES[target];
3313
+ 'aws-lambda': {
3314
+ 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.
3317
+ scripts: {
3318
+ preview: 'rshono build --deploy node && rshono start'
3319
+ },
3320
+ scriptHelp: {
3321
+ preview: 'build for Node and run that here'
3322
+ },
3323
+ notes: [
3324
+ 'Use a Function URL in RESPONSE_STREAM mode — a buffered invoke mode drops the streaming.'
3325
+ ],
3326
+ platformSetup: [
3327
+ `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.'
3330
+ ].join('\n')
3331
+ }
3332
+ };
3333
+ }
3334
+ function deployFeature(target, pm) {
3335
+ return deployFeatures(pm)[target];
3199
3336
  }
3200
3337
 
3201
3338
  ;// CONCATENATED MODULE: ./src/features/quality.ts
@@ -3341,9 +3478,12 @@ function stylingFeature(styling) {
3341
3478
  * The features a set of answers selects, in application order — so an overlay listed later wins a file
3342
3479
  * both of them ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome
3343
3480
  * is both the formatter and the linter) without contributing twice.
3344
- */ function selectFeatures(answers) {
3481
+ *
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.
3484
+ */ function selectFeatures(answers, pm) {
3345
3485
  const selected = [
3346
- deployFeature(answers.deploy),
3486
+ deployFeature(answers.deploy, pm),
3347
3487
  stylingFeature(answers.styling),
3348
3488
  formatterFeature(answers.formatter),
3349
3489
  linterFeature(answers.linter),
@@ -3361,15 +3501,7 @@ function stylingFeature(styling) {
3361
3501
 
3362
3502
  ;// CONCATENATED MODULE: ./src/pkg.ts
3363
3503
 
3364
- /**
3365
- * The scripts every app gets. `start` is not among them, because it means something different per
3366
- * platform: `node` is the one target that runs the build itself, so it contributes its own, and a
3367
- * platform target contributes a `deploy` instead, where its platform has one command to give.
3368
- */ const BASE_SCRIPTS = {
3369
- dev: 'rshono dev',
3370
- build: 'rshono build',
3371
- typecheck: 'tsc --noEmit'
3372
- };
3504
+
3373
3505
  /** Field order in the emitted file — the conventional reading order, and stable so snapshots are too. */ const FIELD_ORDER = [
3374
3506
  'name',
3375
3507
  'version',
@@ -3411,13 +3543,9 @@ function sorted(record) {
3411
3543
  /**
3412
3544
  * Assembles `package.json` from the answers and whatever the selected features contribute.
3413
3545
  *
3414
- * Dependencies are sorted by name and scripts are left in contribution order (the base ones, then each
3415
- * feature's, in the order features were selected) so two runs with the same answers produce byte-
3416
- * identical output, which is what makes the generated manifest snapshot-testable.
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.
3417
3548
  */ function buildPackageJson(answers, features, pm) {
3418
- const scripts = {
3419
- ...BASE_SCRIPTS
3420
- };
3421
3549
  const dependencies = {
3422
3550
  '@rshono/core': RSHONO_RANGE,
3423
3551
  hono: FRAMEWORK_DEPS.hono,
@@ -3430,7 +3558,6 @@ function sorted(record) {
3430
3558
  typescript: FRAMEWORK_DEPS.typescript
3431
3559
  };
3432
3560
  for (const feature of features){
3433
- Object.assign(scripts, feature.scripts);
3434
3561
  Object.assign(dependencies, feature.dependencies);
3435
3562
  Object.assign(devDependencies, feature.devDependencies);
3436
3563
  }
@@ -3443,7 +3570,7 @@ function sorted(record) {
3443
3570
  engines: {
3444
3571
  node: NODE_ENGINE
3445
3572
  },
3446
- scripts,
3573
+ scripts: buildScripts(features),
3447
3574
  dependencies: sorted(dependencies),
3448
3575
  devDependencies: sorted(devDependencies)
3449
3576
  };
@@ -3464,12 +3591,15 @@ function sorted(record) {
3464
3591
  * markdown `__NAME__` *is* strong emphasis — Prettier rewrites it to `**NAME**` and the token stops
3465
3592
  * matching. `{{…}}` means nothing to any format these templates are written in.
3466
3593
  */ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
3467
- function tokensFor(answers, pm) {
3594
+ function tokensFor(answers, features, pm) {
3468
3595
  return {
3469
3596
  '{{PROJECT_NAME}}': answers.packageName,
3470
3597
  '{{DEPLOY_TARGET}}': answers.deploy,
3471
- '{{DEPLOY_HINT}}': deployHint(answers.deploy),
3472
- '{{PM_RUN}}': pm.run
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.
3600
+ '{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
3601
+ '{{DEPLOY_STEP}}': deployStep(features, pm),
3602
+ '{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
3473
3603
  };
3474
3604
  }
3475
3605
  /**
@@ -3532,8 +3662,8 @@ function readTemplateDir(dir) {
3532
3662
  * decisions and the I/O are separated so the whole matrix of answers can be asserted on in a test, and
3533
3663
  * so `--dry-run` is the same code path minus the last step.
3534
3664
  */ function plan_plan(answers, pm) {
3535
- const features = selectFeatures(answers);
3536
- const tokens = tokensFor(answers, pm);
3665
+ const features = selectFeatures(answers, pm);
3666
+ const tokens = tokensFor(answers, features, pm);
3537
3667
  const raw = readTemplateDir(external_node_path_join(TEMPLATES_DIR, 'base'));
3538
3668
  for (const feature of features){
3539
3669
  for (const overlay of feature.overlays ?? []){
@@ -3583,6 +3713,12 @@ const RUN = {
3583
3713
  yarn: 'yarn',
3584
3714
  bun: 'bun'
3585
3715
  };
3716
+ const DLX = {
3717
+ npm: 'npx',
3718
+ pnpm: 'pnpm dlx',
3719
+ yarn: 'yarn dlx',
3720
+ bun: 'bunx'
3721
+ };
3586
3722
  function isKnown(name) {
3587
3723
  return PACKAGE_MANAGERS.includes(name);
3588
3724
  }
@@ -3591,7 +3727,8 @@ function packageManager(name, version) {
3591
3727
  name,
3592
3728
  version,
3593
3729
  install: INSTALL[name],
3594
- run: RUN[name]
3730
+ run: RUN[name],
3731
+ dlx: DLX[name]
3595
3732
  };
3596
3733
  }
3597
3734
  /**
@@ -3625,6 +3762,17 @@ function run(pm, args, cwd) {
3625
3762
  ;// CONCATENATED MODULE: ./src/ui.ts
3626
3763
 
3627
3764
 
3765
+
3766
+ /**
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.
3770
+ */ function productionSteps(answers, plan, pm) {
3771
+ const scripts = buildScripts(plan.features);
3772
+ const check = scripts.preview ? `${invoke(pm, 'preview')} to run the production build` : `${pm.run} build`;
3773
+ const ship = scripts.deploy ? `${invoke(pm, 'deploy')} to ship it` : scripts.start ? `${invoke(pm, 'start')} on the host that runs it` : deployHint(answers.deploy);
3774
+ return `${check}, and ${ship}`;
3775
+ }
3628
3776
  /**
3629
3777
  * What to do next, in the order to do it — the last thing the user reads, and for most people the only
3630
3778
  * documentation they will read today. `installed` decides whether the install step is still theirs.
@@ -3636,7 +3784,7 @@ function run(pm, args, cwd) {
3636
3784
  const lines = [
3637
3785
  steps.join('\n')
3638
3786
  ];
3639
- lines.push(`\nThen ${pm.run} build, and ${deployHint(answers.deploy)}.`);
3787
+ lines.push(`\nThen ${productionSteps(answers, plan, pm)}.`);
3640
3788
  if (plan.notes.length > 0) lines.push(`\n${plan.notes.join('\n')}`);
3641
3789
  return lines.join('\n');
3642
3790
  }
@@ -3821,7 +3969,7 @@ function fail(message) {
3821
3969
  async function main() {
3822
3970
  const { values, positionals } = parse();
3823
3971
  if (values.help) return console.log(HELP);
3824
- if (values.version) return console.log("1.0.0-rc.10");
3972
+ if (values.version) return console.log("1.0.0-rc.12");
3825
3973
  // A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both
3826
3974
  // streams have to be a terminal: the prompts draw on stdout but *read from stdin*, so
3827
3975
  // `echo | npx @rshono/create` would otherwise ask a question with nothing behind the keyboard.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rshono/create",
3
- "version": "1.0.0-rc.10",
3
+ "version": "1.0.0-rc.12",
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",
@@ -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.10"
49
+ "@rshono/core": "1.0.0-rc.12"
50
50
  },
51
51
  "scripts": {
52
52
  "build": "node scripts/codegen.mjs && node scripts/build.mjs",
@@ -1,9 +1,7 @@
1
1
  # {{PROJECT_NAME}}
2
2
 
3
3
  ```bash
4
- {{PM_RUN}} dev # dev server with HMR, http://localhost:3000
5
- {{PM_RUN}} build # production build for {{DEPLOY_TARGET}}
6
- {{PM_RUN}} typecheck # tsc --noEmit
4
+ {{SCRIPT_TABLE}}
7
5
  ```
8
6
 
9
7
  `package.json` has the rest, including whatever your formatter and linter added.
@@ -31,8 +29,12 @@ compiles to `undefined` rather than shipping. `src/components/layout.tsx` reads
31
29
 
32
30
  ## Deploying
33
31
 
34
- This app is built for `{{DEPLOY_TARGET}}`: after `{{PM_RUN}} build`, {{DEPLOY_HINT}}.
32
+ This app is built for `{{DEPLOY_TARGET}}`. {{DEPLOY_STEP}}
33
+
34
+ {{PLATFORM_SETUP}}
35
35
 
36
36
  Change `deploy` in `rshono.config.ts` to target somewhere else, or build for one place without editing the
37
37
  file: `rshono build --deploy vercel`, or `RSHONO_DEPLOY=vercel` in CI. `dev` always runs the Node dev
38
38
  server, whatever the target — it is a property of the build, not of developing.
39
+
40
+ Every deployment target, and what each one needs: <https://www.rshono.com/docs/deployment>
@@ -1,5 +1,6 @@
1
1
  node_modules/
2
2
  dist/
3
+ .rshono/
3
4
  *.tsbuildinfo
4
5
  .DS_Store
5
6