@rshono/create 1.0.0-rc.11 → 1.0.0-rc.13
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/bin/create-rshono.mjs +4 -4
- package/dist/api.mjs +103 -156
- package/dist/cli.mjs +118 -176
- package/package.json +2 -2
- package/templates/base/README.md +1 -1
- package/templates/base/_gitignore +1 -0
- package/templates/eslint/eslint.config.mjs +4 -1
package/bin/create-rshono.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Thin launcher,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// Thin launcher, deliberately old-syntax and dependency-free: it is the one file that has to parse and run on
|
|
3
|
+
// whatever Node the user happens to have. A version check inside the CLI bundle would be too late — its imports
|
|
4
|
+
// evaluate before any statement in it.
|
|
5
|
+
//
|
|
6
6
|
// Kept in step with `engines.node` in @rshono/core's manifest, which is what a scaffolded app pins.
|
|
7
7
|
var MINIMUM = [22, 18, 0];
|
|
8
8
|
|
package/dist/api.mjs
CHANGED
|
@@ -52,12 +52,12 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
52
52
|
|
|
53
53
|
;// CONCATENATED MODULE: ./src/features/combinations.ts
|
|
54
54
|
/**
|
|
55
|
-
* Biome's CSS parser rejects Tailwind's syntax — `@apply`
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* Biome's CSS parser rejects Tailwind's syntax — `@apply` is a parse error, not an unknown-at-rule warning — so
|
|
56
|
+
* a project with both needs Biome pointed away from stylesheets. The overlay is a second `biome.json` listed
|
|
57
|
+
* after the first.
|
|
58
58
|
*
|
|
59
|
-
* Narrow on purpose:
|
|
60
|
-
*
|
|
59
|
+
* Narrow on purpose: excluding CSS for everybody would give up formatting the plain-CSS template that Biome
|
|
60
|
+
* handles perfectly well.
|
|
61
61
|
*/ const BIOME_TAILWIND = {
|
|
62
62
|
id: 'biome-tailwind',
|
|
63
63
|
overlays: [
|
|
@@ -83,14 +83,12 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
83
83
|
/**
|
|
84
84
|
* The scripts every app gets, whatever it targets.
|
|
85
85
|
*
|
|
86
|
-
* `start`, `preview` and `deploy` are
|
|
87
|
-
*
|
|
88
|
-
*
|
|
86
|
+
* `start`, `preview` and `deploy` are not here: each means something different per platform, so the deploy
|
|
87
|
+
* target contributes its own. The three names are a contract the targets keep, which is what lets the README
|
|
88
|
+
* describe an app it was not written for:
|
|
89
89
|
*
|
|
90
|
-
* - **`start`** runs a build that already exists and never makes one — what a host's own start command
|
|
91
|
-
*
|
|
92
|
-
* - **`preview`** builds, then runs the result here — for the targets where that is not the same two
|
|
93
|
-
* commands, because otherwise the production build is unanswerable without deploying it.
|
|
90
|
+
* - **`start`** runs a build that already exists and never makes one — what a host's own start command calls.
|
|
91
|
+
* - **`preview`** builds, then runs the result here, so the production build is answerable without deploying.
|
|
94
92
|
* - **`deploy`** builds, then ships it — where the platform has one command that does the shipping.
|
|
95
93
|
*/ const BASE_SCRIPTS = {
|
|
96
94
|
dev: 'rshono dev',
|
|
@@ -98,9 +96,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
98
96
|
typecheck: 'tsc --noEmit'
|
|
99
97
|
};
|
|
100
98
|
/**
|
|
101
|
-
* Every script the app gets, in the order they are written: the base ones, then each feature's
|
|
102
|
-
* the
|
|
103
|
-
* document a script the other does not have.
|
|
99
|
+
* Every script the app gets, in the order they are written: the base ones, then each feature's. The manifest and
|
|
100
|
+
* the README's command table are both this, so neither can document a script the other lacks.
|
|
104
101
|
*/ function buildScripts(features) {
|
|
105
102
|
const scripts = {
|
|
106
103
|
...BASE_SCRIPTS
|
|
@@ -116,9 +113,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
116
113
|
};
|
|
117
114
|
}
|
|
118
115
|
/**
|
|
119
|
-
* Script names a package manager has a command of its own for
|
|
120
|
-
*
|
|
121
|
-
* something else. The explicit `run` form is what all four managers accept, so those names get it.
|
|
116
|
+
* Script names a package manager has a command of its own for: `pnpm deploy` runs pnpm's workspace-deploy and
|
|
117
|
+
* never looks at the manifest. These get the explicit `run` form, which all four managers accept.
|
|
122
118
|
*/ const SHADOWED = new Set([
|
|
123
119
|
'deploy'
|
|
124
120
|
]);
|
|
@@ -126,10 +122,9 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
126
122
|
return `${SHADOWED.has(script) ? `${pm.name} run` : pm.run} ${script}`;
|
|
127
123
|
}
|
|
128
124
|
/**
|
|
129
|
-
* The README's command table: one line per script, with the command to type and a one-line gloss.
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* rest" line — that is how a formatter's `format:check` stays out of a table about running the app.
|
|
125
|
+
* The README's command table: one line per script, with the command to type and a one-line gloss. A script whose
|
|
126
|
+
* feature supplies no `scriptHelp` is left out — that is how `format:check` stays out of a table about running
|
|
127
|
+
* the app — and covered by the line pointing at `package.json`.
|
|
133
128
|
*/ function scriptTable(answers, features, pm) {
|
|
134
129
|
const help = baseScriptHelp(answers.deploy);
|
|
135
130
|
for (const feature of features)Object.assign(help, feature.scriptHelp);
|
|
@@ -138,31 +133,28 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
138
133
|
return documented.map((name)=>`${invoke(pm, name).padEnd(width)} # ${help[name]}`).join('\n');
|
|
139
134
|
}
|
|
140
135
|
/**
|
|
141
|
-
* The command that gets this app into production, as a sentence — the README's deploy step
|
|
142
|
-
*
|
|
136
|
+
* The command that gets this app into production, as a sentence — the README's deploy step and the closing
|
|
137
|
+
* summary's, so the two cannot name different commands.
|
|
143
138
|
*
|
|
144
139
|
* Read off the scripts rather than the target, so a target that gains a `deploy` gains the sentence with it.
|
|
145
|
-
* `start` is the answer where there is no `deploy`: it ships nothing itself, but it is what the host runs.
|
|
146
140
|
*/ function deployStep(features, pm) {
|
|
147
141
|
const scripts = buildScripts(features);
|
|
148
142
|
if (scripts.deploy) return `\`${invoke(pm, 'deploy')}\` does the build and the upload in one step.`;
|
|
149
143
|
if (scripts.start) return `\`${invoke(pm, 'start')}\` runs that build wherever you host it, and never makes one.`;
|
|
150
|
-
// Every branch names a script the app has, so a target that contributes none of the three still reads true.
|
|
151
144
|
if (scripts.preview) return `\`${invoke(pm, 'preview')}\` runs the build here, so you can check it first.`;
|
|
152
145
|
return `\`${invoke(pm, 'build')}\` produces it; getting it there is yours to script.`;
|
|
153
146
|
}
|
|
154
147
|
|
|
155
148
|
;// CONCATENATED MODULE: ./src/generated/framework.ts
|
|
156
149
|
// GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
|
|
157
|
-
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.
|
|
150
|
+
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.13';
|
|
158
151
|
/**
|
|
159
|
-
* The Node range rshono
|
|
160
|
-
*
|
|
152
|
+
* The Node range rshono declares, restated in every scaffolded app's `engines` — so a CI image or a contributor
|
|
153
|
+
* on an older Node hears it from their package manager rather than from a stack trace.
|
|
161
154
|
*/ const NODE_ENGINE = '>=22.18.0';
|
|
162
155
|
/**
|
|
163
|
-
* The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
|
|
164
|
-
*
|
|
165
|
-
* overrides to fall back on.
|
|
156
|
+
* The dependency versions rshono is tested against, copied from its own manifest. Exact where it is exact:
|
|
157
|
+
* React's RSC internals are coupled across builds, and a generated app has no workspace overrides.
|
|
166
158
|
*/ const FRAMEWORK_DEPS = {
|
|
167
159
|
hono: '^4.12.31',
|
|
168
160
|
react: '19.2.8',
|
|
@@ -195,14 +187,12 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
195
187
|
/** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
|
|
196
188
|
/** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
|
|
197
189
|
/**
|
|
198
|
-
* Versions for the optional tooling the features can add — the one place in this package where a
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* which is generated from rshono's own manifest and must not be edited here.
|
|
190
|
+
* Versions for the optional tooling the features can add — the one place in this package where a range is typed
|
|
191
|
+
* by hand, because the framework declares none of these. Everything a scaffolded app needs to *run* rshono comes
|
|
192
|
+
* from {@link FRAMEWORK_DEPS}, generated from rshono's own manifest.
|
|
202
193
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* was last touched.
|
|
194
|
+
* Caret ranges, not exact: these are the app's own dev tools, and a scaffold made six months from now should
|
|
195
|
+
* pick up their patch releases.
|
|
206
196
|
*/ const TOOL_VERSIONS = {
|
|
207
197
|
tailwindcss: '^4.3.3',
|
|
208
198
|
'@tailwindcss/postcss': '^4.3.3',
|
|
@@ -214,23 +204,20 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
214
204
|
oxlint: '^1.76.0',
|
|
215
205
|
oxfmt: '^0.61.0',
|
|
216
206
|
eslint: '^10.8.0',
|
|
217
|
-
// ESLint's
|
|
218
|
-
//
|
|
207
|
+
// ESLint's recommended JavaScript rules, which typescript-eslint layers on rather than replaces, and the rules
|
|
208
|
+
// of hooks — the one class of React mistake no type checker sees.
|
|
219
209
|
'@eslint/js': '^10.0.1',
|
|
220
210
|
'typescript-eslint': '^8.65.0',
|
|
221
211
|
'eslint-plugin-react-hooks': '^7.1.1',
|
|
222
212
|
wrangler: '^4.115.0'
|
|
223
213
|
};
|
|
224
214
|
/**
|
|
225
|
-
* The TypeScript an ESLint app pins
|
|
215
|
+
* The TypeScript an ESLint app pins in place of the framework's own — the one deliberate exception to
|
|
226
216
|
* {@link FRAMEWORK_DEPS}.
|
|
227
217
|
*
|
|
228
|
-
* typescript-eslint reads TypeScript's compiler API directly
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* and not the framework's.
|
|
232
|
-
*
|
|
233
|
-
* When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
|
|
218
|
+
* typescript-eslint reads TypeScript's compiler API directly, so it accepts `>=4.8.4 <6.1.0` and nothing above;
|
|
219
|
+
* `~6.0.3` is the newest that satisfies it. rshono's declarations compile the same under either, which is what
|
|
220
|
+
* makes this the app's business. When upstream widens its range, this constant is what to delete.
|
|
234
221
|
*/ const ESLINT_TYPESCRIPT = '~6.0.3';
|
|
235
222
|
|
|
236
223
|
;// CONCATENATED MODULE: ./src/features/deploy.ts
|
|
@@ -239,22 +226,17 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
239
226
|
/**
|
|
240
227
|
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
|
|
241
228
|
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* build, the CLI they need, the directories to gitignore, and a note for the step no command covers.
|
|
246
|
-
*
|
|
247
|
-
* What the three script names promise is documented once, above `BASE_SCRIPTS` in `scripts.ts`. Only `node`
|
|
248
|
-
* has a `start`, because `rshono start` refuses a bundle built for anywhere else, and it needs no `preview`:
|
|
249
|
-
* `build` then `start` already is one.
|
|
229
|
+
* Thin on purpose: the framework arranges its own output for every platform, and `rshono build` writes the one
|
|
230
|
+
* platform config that has to exist. So a target contributes the commands that run and ship the build, the CLI
|
|
231
|
+
* they need, the directories to gitignore, and a note for the step no command covers.
|
|
250
232
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
233
|
+
* What the three script names promise is documented above `BASE_SCRIPTS` in `scripts.ts`. Only `node` has a
|
|
234
|
+
* `start` — `rshono start` refuses a bundle built for anywhere else — and it needs no `preview`, since `build`
|
|
235
|
+
* then `start` already is one.
|
|
253
236
|
*/ function deployFeatures(pm) {
|
|
254
237
|
const build = invoke(pm, 'build');
|
|
255
238
|
return {
|
|
256
|
-
// Where a Node build goes
|
|
257
|
-
// guess — so the target contributes only the command that runs what was built.
|
|
239
|
+
// Where a Node build goes next is a Dockerfile or a process manager, neither of which this can guess.
|
|
258
240
|
node: {
|
|
259
241
|
id: 'deploy-node',
|
|
260
242
|
scripts: {
|
|
@@ -277,16 +259,14 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
277
259
|
devDependencies: {
|
|
278
260
|
wrangler: TOOL_VERSIONS.wrangler
|
|
279
261
|
},
|
|
280
|
-
// The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each
|
|
281
|
-
//
|
|
282
|
-
// neither needs to run — `workerd --version` and `esbuild --version` both answer without it.
|
|
262
|
+
// The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each merely
|
|
263
|
+
// picks the platform binary out of the optional dependency already carrying it, so neither needs to run.
|
|
283
264
|
allowBuilds: {
|
|
284
265
|
esbuild: false,
|
|
285
266
|
workerd: false
|
|
286
267
|
},
|
|
287
|
-
// `wrangler dev` is the one preview that runs the code in
|
|
288
|
-
//
|
|
289
|
-
// build wrote, so nothing here has to know where the bundle or the assets went.
|
|
268
|
+
// `wrangler dev` is the one preview that runs the code in workerd rather than Node. Both scripts read the
|
|
269
|
+
// wrangler.jsonc the build wrote, so nothing here has to know where the bundle went.
|
|
290
270
|
scripts: {
|
|
291
271
|
preview: 'rshono build && wrangler dev',
|
|
292
272
|
deploy: 'rshono build && wrangler deploy'
|
|
@@ -308,9 +288,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
308
288
|
},
|
|
309
289
|
vercel: {
|
|
310
290
|
id: 'deploy-vercel',
|
|
311
|
-
// `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview
|
|
312
|
-
//
|
|
313
|
-
// build run here — the platform has no way to run its own prebuilt output on your machine.
|
|
291
|
+
// `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview URL.
|
|
292
|
+
// `preview` is a Node build run here — the platform cannot run its own prebuilt output on your machine.
|
|
314
293
|
scripts: {
|
|
315
294
|
preview: 'rshono build --deploy node && rshono start',
|
|
316
295
|
deploy: `rshono build && ${pm.dlx} vercel deploy --prebuilt --prod`
|
|
@@ -336,8 +315,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
336
315
|
},
|
|
337
316
|
'aws-lambda': {
|
|
338
317
|
id: 'deploy-aws-lambda',
|
|
339
|
-
// No CLI to wrap
|
|
340
|
-
// handler, so it runs here.
|
|
318
|
+
// No CLI to wrap and no upload to guess at, but `preview` still applies: the bundle is a Node handler.
|
|
341
319
|
scripts: {
|
|
342
320
|
preview: 'rshono build --deploy node && rshono start'
|
|
343
321
|
},
|
|
@@ -362,14 +340,11 @@ function deployFeature(target, pm) {
|
|
|
362
340
|
;// CONCATENATED MODULE: ./src/features/quality.ts
|
|
363
341
|
|
|
364
342
|
/**
|
|
365
|
-
* The formatter and linter features. Biome answers to both slots and appears once
|
|
366
|
-
* deduplicates by `id
|
|
367
|
-
* dependency and one pair of scripts.
|
|
343
|
+
* The formatter and linter features. Biome answers to both slots and appears once, because `selectFeatures`
|
|
344
|
+
* deduplicates by `id`.
|
|
368
345
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
|
|
372
|
-
* one. Biome adds a `check` of its own, which is the pair of them in a single pass.
|
|
346
|
+
* Each brings a pair of scripts, since the writing half and the CI half want different exit codes: `format`
|
|
347
|
+
* rewrites and `format:check` fails, `lint:fix` rewrites and `lint` fails. Biome's `check` is both in one pass.
|
|
373
348
|
*/ const PRETTIER = {
|
|
374
349
|
id: 'prettier',
|
|
375
350
|
overlays: [
|
|
@@ -410,11 +385,10 @@ const OXLINT = {
|
|
|
410
385
|
}
|
|
411
386
|
};
|
|
412
387
|
/**
|
|
413
|
-
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* program rather than linting file by file.
|
|
388
|
+
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
|
|
389
|
+
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest its peer range
|
|
390
|
+
* accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to reach for ESLint at all —
|
|
391
|
+
* so the config it ships hands the parser the whole program.
|
|
418
392
|
*/ const ESLINT = {
|
|
419
393
|
id: 'eslint',
|
|
420
394
|
overlays: [
|
|
@@ -470,13 +444,12 @@ function linterFeature(linter) {
|
|
|
470
444
|
;// CONCATENATED MODULE: ./src/features/styling.ts
|
|
471
445
|
|
|
472
446
|
/**
|
|
473
|
-
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
* in utilities.
|
|
447
|
+
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages, plus an
|
|
448
|
+
* overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts postcss-loader in front
|
|
449
|
+
* of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten in utilities.
|
|
477
450
|
*
|
|
478
|
-
* `postcss`
|
|
479
|
-
*
|
|
451
|
+
* `postcss` is the app's dependency rather than the framework's: rshono compiles CSS natively, so an app that
|
|
452
|
+
* wants no plugin chain installs none.
|
|
480
453
|
*/ const TAILWIND = {
|
|
481
454
|
id: 'tailwind',
|
|
482
455
|
overlays: [
|
|
@@ -499,12 +472,11 @@ function stylingFeature(styling) {
|
|
|
499
472
|
|
|
500
473
|
|
|
501
474
|
/**
|
|
502
|
-
* The features a set of answers selects, in application order — so an overlay listed later wins a file
|
|
503
|
-
*
|
|
504
|
-
*
|
|
475
|
+
* The features a set of answers selects, in application order — so an overlay listed later wins a file both
|
|
476
|
+
* ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome is both formatter and
|
|
477
|
+
* linter) without contributing twice.
|
|
505
478
|
*
|
|
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.
|
|
479
|
+
* `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled CLI.
|
|
508
480
|
*/ function selectFeatures(answers, pm) {
|
|
509
481
|
const selected = [
|
|
510
482
|
deployFeature(answers.deploy, pm),
|
|
@@ -526,9 +498,8 @@ function stylingFeature(styling) {
|
|
|
526
498
|
;// CONCATENATED MODULE: ./src/options.ts
|
|
527
499
|
|
|
528
500
|
/*
|
|
529
|
-
* The names each option accepts, spelled once
|
|
530
|
-
*
|
|
531
|
-
* — so a name added here reaches all three without a second list to remember.
|
|
501
|
+
* The names each option accepts, spelled once: the types below are derived from them, the CLI validates its
|
|
502
|
+
* flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them.
|
|
532
503
|
*/ const FORMATTER_NAMES = [
|
|
533
504
|
'prettier',
|
|
534
505
|
'biome',
|
|
@@ -589,14 +560,13 @@ const QUALITY_PRESETS = [
|
|
|
589
560
|
}
|
|
590
561
|
];
|
|
591
562
|
/**
|
|
592
|
-
* Turns whatever the user typed into a name npm will accept, or
|
|
593
|
-
*
|
|
594
|
-
* `my_app`); a scoped name is kept intact, since `@scope/name` is legal.
|
|
563
|
+
* Turns whatever the user typed into a name npm will accept, or `null` when nothing usable is left.
|
|
564
|
+
* Lowercasing and replacing runs of invalid characters covers the ordinary cases (`My App`, `my_app`).
|
|
595
565
|
*/ function toPackageName(input) {
|
|
596
566
|
const trimmed = input.trim().replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
597
567
|
if (!trimmed || trimmed === '.') return null;
|
|
598
|
-
// A scoped name is a name, not a path: `@scope/pkg` stays whole
|
|
599
|
-
//
|
|
568
|
+
// A scoped name is a name, not a path: `@scope/pkg` stays whole. Anything else is a path, whose last
|
|
569
|
+
// segment names the project.
|
|
600
570
|
const scoped = /^@[^\\/]+[\\/][^\\/]+$/.test(trimmed);
|
|
601
571
|
const base = scoped ? trimmed : trimmed.split(/[\\/]/).filter(Boolean).pop() ?? '';
|
|
602
572
|
if (!base) return null;
|
|
@@ -630,15 +600,11 @@ function sorted(record) {
|
|
|
630
600
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
631
601
|
}
|
|
632
602
|
/**
|
|
633
|
-
* pnpm's settings for the new app,
|
|
634
|
-
* means there is nothing to say, so no file is written.
|
|
603
|
+
* pnpm's settings for the new app, or `null` when no feature has anything to put in them.
|
|
635
604
|
*
|
|
636
|
-
* It exists for one field, `allowBuilds
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
* declare their answer on the feature that brings them.
|
|
640
|
-
*
|
|
641
|
-
* A file rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
605
|
+
* It exists for one field, `allowBuilds`: pnpm fails an install until the project has said whether a
|
|
606
|
+
* dependency's install script should run. Nothing rshono installs has one, so most apps get no file. A file
|
|
607
|
+
* rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
642
608
|
*/ function buildPnpmSettings(features) {
|
|
643
609
|
const allowBuilds = {};
|
|
644
610
|
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
@@ -654,10 +620,8 @@ function sorted(record) {
|
|
|
654
620
|
].join('\n');
|
|
655
621
|
}
|
|
656
622
|
/**
|
|
657
|
-
* Assembles `package.json` from the answers and whatever the selected features contribute.
|
|
658
|
-
*
|
|
659
|
-
* Dependencies are sorted by name and scripts keep the order {@link buildScripts} gives them — so two runs
|
|
660
|
-
* with the same answers produce byte-identical output, which is what makes the manifest snapshot-testable.
|
|
623
|
+
* Assembles `package.json` from the answers and whatever the selected features contribute. Dependencies are
|
|
624
|
+
* sorted and scripts keep {@link buildScripts}'s order, so two runs with the same answers are byte-identical.
|
|
661
625
|
*/ function buildPackageJson(answers, features, pm) {
|
|
662
626
|
const dependencies = {
|
|
663
627
|
'@rshono/core': RSHONO_RANGE,
|
|
@@ -687,8 +651,8 @@ function sorted(record) {
|
|
|
687
651
|
dependencies: sorted(dependencies),
|
|
688
652
|
devDependencies: sorted(devDependencies)
|
|
689
653
|
};
|
|
690
|
-
// Only when the environment told us the exact version:
|
|
691
|
-
//
|
|
654
|
+
// Only when the environment told us the exact version: this field pins the tool for Corepack, and a guess is
|
|
655
|
+
// worse than leaving it out.
|
|
692
656
|
if (pm.version) manifest.packageManager = `${pm.name}@${pm.version}`;
|
|
693
657
|
const ordered = Object.fromEntries(FIELD_ORDER.filter((field)=>field in manifest).map((field)=>[
|
|
694
658
|
field,
|
|
@@ -700,25 +664,22 @@ function sorted(record) {
|
|
|
700
664
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
701
665
|
|
|
702
666
|
/**
|
|
703
|
-
* `{{NAME}}`,
|
|
704
|
-
*
|
|
705
|
-
* matching. `{{…}}` means nothing to any format these templates are written in.
|
|
667
|
+
* `{{NAME}}`, not `__NAME__`: in markdown the latter is strong emphasis, so Prettier rewrites it to `**NAME**`
|
|
668
|
+
* and the token stops matching. `{{…}}` means nothing to any format these templates are written in.
|
|
706
669
|
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
707
670
|
function tokensFor(answers, features, pm) {
|
|
708
671
|
return {
|
|
709
672
|
'{{PROJECT_NAME}}': answers.packageName,
|
|
710
673
|
'{{DEPLOY_TARGET}}': answers.deploy,
|
|
711
|
-
//
|
|
712
|
-
// actually got: its command table, the one command that ships it, and what its platform asks for.
|
|
674
|
+
// From the features rather than the answers, because all three are about the scripts the app actually got.
|
|
713
675
|
'{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
|
|
714
676
|
'{{DEPLOY_STEP}}': deployStep(features, pm),
|
|
715
677
|
'{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
|
|
716
678
|
};
|
|
717
679
|
}
|
|
718
680
|
/**
|
|
719
|
-
* Substitutes tokens, and throws on one it
|
|
720
|
-
*
|
|
721
|
-
* catch.
|
|
681
|
+
* Substitutes tokens, and throws on one it does not know — a typo in a template would otherwise ship a literal
|
|
682
|
+
* `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would catch.
|
|
722
683
|
*/ function render(contents, tokens, source) {
|
|
723
684
|
return contents.replace(TOKEN_PATTERN, (token)=>{
|
|
724
685
|
const value = tokens[token];
|
|
@@ -748,11 +709,9 @@ function readTemplateDir(dir) {
|
|
|
748
709
|
return files;
|
|
749
710
|
}
|
|
750
711
|
/**
|
|
751
|
-
* `_gitignore` → `.gitignore`, and so on for every dotfile
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
* would exist in the repo, pass every local test, and be missing from the package everybody installs.
|
|
755
|
-
* Applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
712
|
+
* `_gitignore` → `.gitignore`, and so on for every dotfile: npm strips a literal `.gitignore` out of a
|
|
713
|
+
* published tarball, so a template that contained one would pass every local test and be missing from the
|
|
714
|
+
* package everybody installs. The basename only, so `templates/_x/y.ts` is left alone.
|
|
756
715
|
*/ function undotted(path) {
|
|
757
716
|
const segments = path.split(posix.sep);
|
|
758
717
|
const name = segments.pop();
|
|
@@ -761,19 +720,16 @@ function readTemplateDir(dir) {
|
|
|
761
720
|
name.startsWith('_') ? `.${name.slice(1)}` : name
|
|
762
721
|
].join(posix.sep);
|
|
763
722
|
}
|
|
764
|
-
/**
|
|
765
|
-
* A feature's `.gitignore` lines, appended under a heading naming it — so somebody reading the file six
|
|
766
|
-
* months later can tell why `.wrangler/` is in there.
|
|
767
|
-
*/ function appendGitignore(existing, features) {
|
|
723
|
+
/** A feature's `.gitignore` lines, under a heading naming it — so a reader can tell why `.wrangler/` is there. */ function appendGitignore(existing, features) {
|
|
768
724
|
const additions = features.filter((feature)=>feature.gitignore?.length);
|
|
769
725
|
if (additions.length === 0) return existing;
|
|
770
726
|
const blocks = additions.map((feature)=>`\n# ${feature.id}\n${feature.gitignore.join('\n')}\n`);
|
|
771
727
|
return existing + blocks.join('');
|
|
772
728
|
}
|
|
773
729
|
/**
|
|
774
|
-
* Turns answers into the exact set of files to write, without touching the target directory
|
|
775
|
-
* decisions
|
|
776
|
-
*
|
|
730
|
+
* Turns answers into the exact set of files to write, without touching the target directory. Separating the
|
|
731
|
+
* decisions from the I/O is what lets a test assert on the whole matrix of answers, and makes `--dry-run` the
|
|
732
|
+
* same code path minus the last step.
|
|
777
733
|
*/ function plan_plan(answers, pm) {
|
|
778
734
|
const features = selectFeatures(answers, pm);
|
|
779
735
|
const tokens = tokensFor(answers, features, pm);
|
|
@@ -847,11 +803,9 @@ function packageManager(name, version) {
|
|
|
847
803
|
};
|
|
848
804
|
}
|
|
849
805
|
/**
|
|
850
|
-
* Which package manager invoked us.
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
* Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
806
|
+
* Which package manager invoked us. All four set `npm_config_user_agent` on the process they spawn — `pnpm/11.9.0
|
|
807
|
+
* npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm project without asking, exact
|
|
808
|
+
* version included. Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
855
809
|
*/ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
856
810
|
const [spec] = (userAgent ?? '').split(' ');
|
|
857
811
|
const [name, version] = (spec ?? '').split('/');
|
|
@@ -859,9 +813,8 @@ function packageManager(name, version) {
|
|
|
859
813
|
return packageManager('npm');
|
|
860
814
|
}
|
|
861
815
|
/**
|
|
862
|
-
* Runs the install, streaming its output. `shell: true` on Windows
|
|
863
|
-
*
|
|
864
|
-
* are all fixed strings from the tables above, never anything the user typed.
|
|
816
|
+
* Runs the install, streaming its output. `shell: true` on Windows, where npm, pnpm and yarn are `.cmd` shims
|
|
817
|
+
* `spawn` cannot execute directly — safe because every argument is a fixed string from the tables above.
|
|
865
818
|
*/ function runInstall(pm, cwd) {
|
|
866
819
|
return run(pm, pm.install, cwd);
|
|
867
820
|
}
|
|
@@ -877,10 +830,7 @@ function run(pm, args, cwd) {
|
|
|
877
830
|
;// CONCATENATED MODULE: ./src/write.ts
|
|
878
831
|
|
|
879
832
|
|
|
880
|
-
/**
|
|
881
|
-
* Files that do not make a directory "occupied". A user who ran `git init` or opened the folder in an
|
|
882
|
-
* editor before scaffolding has not put anything in it that we would overwrite.
|
|
883
|
-
*/ const IGNORED_ENTRIES = new Set([
|
|
833
|
+
/** Files that do not make a directory "occupied": a `git init` or an editor has put nothing there to overwrite. */ const IGNORED_ENTRIES = new Set([
|
|
884
834
|
'.git',
|
|
885
835
|
'.DS_Store',
|
|
886
836
|
'.idea',
|
|
@@ -888,12 +838,9 @@ function run(pm, args, cwd) {
|
|
|
888
838
|
'Thumbs.db'
|
|
889
839
|
]);
|
|
890
840
|
/**
|
|
891
|
-
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind —
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory throws
|
|
895
|
-
* rather than reporting an empty list, since `--force` should not write into one either — otherwise
|
|
896
|
-
* `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
841
|
+
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind — which is
|
|
842
|
+
* what decides whether scaffolding into it is safe. A path that does not exist yet is no conflict; one that
|
|
843
|
+
* exists and is not a directory throws, since `--force` should not write into it either.
|
|
897
844
|
*/ function conflictingEntries(dir) {
|
|
898
845
|
const stats = statSync(dir, {
|
|
899
846
|
throwIfNoEntry: false
|
|
@@ -903,8 +850,8 @@ function run(pm, args, cwd) {
|
|
|
903
850
|
return readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
|
|
904
851
|
}
|
|
905
852
|
/**
|
|
906
|
-
* Writes the plan
|
|
907
|
-
*
|
|
853
|
+
* Writes the plan, creating directories as needed and keeping the plan's own ordering — so a failure part-way
|
|
854
|
+
* through leaves something a person can make sense of.
|
|
908
855
|
*/ function writePlan(plan, targetDir) {
|
|
909
856
|
mkdirSync(targetDir, {
|
|
910
857
|
recursive: true
|
package/dist/cli.mjs
CHANGED
|
@@ -2906,8 +2906,8 @@ function hasGit(cwd) {
|
|
|
2906
2906
|
], cwd).ok;
|
|
2907
2907
|
}
|
|
2908
2908
|
/**
|
|
2909
|
-
* Whether the new project already sits inside somebody's repository —
|
|
2910
|
-
*
|
|
2909
|
+
* Whether the new project already sits inside somebody's repository — a monorepo or an existing checkout, where
|
|
2910
|
+
* a nested repo would be a surprise rather than a convenience.
|
|
2911
2911
|
*/ function isInsideRepo(cwd) {
|
|
2912
2912
|
const { ok, stdout } = git_git([
|
|
2913
2913
|
'rev-parse',
|
|
@@ -2916,11 +2916,9 @@ function hasGit(cwd) {
|
|
|
2916
2916
|
return ok && stdout === 'true';
|
|
2917
2917
|
}
|
|
2918
2918
|
/**
|
|
2919
|
-
* Initializes a repository and makes the first commit.
|
|
2920
|
-
*
|
|
2921
|
-
*
|
|
2922
|
-
* on a machine with no `user.email` configured. That is not a reason to fail the scaffold — the files
|
|
2923
|
-
* are all there and staged — so the caller reports it and moves on.
|
|
2919
|
+
* Initializes a repository and makes the first commit. `initialized` is the honest answer when the commit itself
|
|
2920
|
+
* fails, as it does with no `user.email` configured — the files are all there and staged, so the caller reports
|
|
2921
|
+
* it and moves on rather than failing the scaffold.
|
|
2924
2922
|
*/ function initRepo(cwd) {
|
|
2925
2923
|
const init = git_git([
|
|
2926
2924
|
'init',
|
|
@@ -2943,15 +2941,14 @@ function hasGit(cwd) {
|
|
|
2943
2941
|
|
|
2944
2942
|
;// CONCATENATED MODULE: ./src/generated/framework.ts
|
|
2945
2943
|
// GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
|
|
2946
|
-
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.
|
|
2944
|
+
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.13';
|
|
2947
2945
|
/**
|
|
2948
|
-
* The Node range rshono
|
|
2949
|
-
*
|
|
2946
|
+
* The Node range rshono declares, restated in every scaffolded app's `engines` — so a CI image or a contributor
|
|
2947
|
+
* on an older Node hears it from their package manager rather than from a stack trace.
|
|
2950
2948
|
*/ const NODE_ENGINE = '>=22.18.0';
|
|
2951
2949
|
/**
|
|
2952
|
-
* The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
|
|
2953
|
-
*
|
|
2954
|
-
* overrides to fall back on.
|
|
2950
|
+
* The dependency versions rshono is tested against, copied from its own manifest. Exact where it is exact:
|
|
2951
|
+
* React's RSC internals are coupled across builds, and a generated app has no workspace overrides.
|
|
2955
2952
|
*/ const FRAMEWORK_DEPS = {
|
|
2956
2953
|
hono: '^4.12.31',
|
|
2957
2954
|
react: '19.2.8',
|
|
@@ -2982,9 +2979,8 @@ function hasGit(cwd) {
|
|
|
2982
2979
|
;// CONCATENATED MODULE: ./src/options.ts
|
|
2983
2980
|
|
|
2984
2981
|
/*
|
|
2985
|
-
* The names each option accepts, spelled once
|
|
2986
|
-
*
|
|
2987
|
-
* — so a name added here reaches all three without a second list to remember.
|
|
2982
|
+
* The names each option accepts, spelled once: the types below are derived from them, the CLI validates its
|
|
2983
|
+
* flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them.
|
|
2988
2984
|
*/ const FORMATTER_NAMES = [
|
|
2989
2985
|
'prettier',
|
|
2990
2986
|
'biome',
|
|
@@ -3045,14 +3041,13 @@ const QUALITY_PRESETS = [
|
|
|
3045
3041
|
}
|
|
3046
3042
|
];
|
|
3047
3043
|
/**
|
|
3048
|
-
* Turns whatever the user typed into a name npm will accept, or
|
|
3049
|
-
*
|
|
3050
|
-
* `my_app`); a scoped name is kept intact, since `@scope/name` is legal.
|
|
3044
|
+
* Turns whatever the user typed into a name npm will accept, or `null` when nothing usable is left.
|
|
3045
|
+
* Lowercasing and replacing runs of invalid characters covers the ordinary cases (`My App`, `my_app`).
|
|
3051
3046
|
*/ function toPackageName(input) {
|
|
3052
3047
|
const trimmed = input.trim().replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
3053
3048
|
if (!trimmed || trimmed === '.') return null;
|
|
3054
|
-
// A scoped name is a name, not a path: `@scope/pkg` stays whole
|
|
3055
|
-
//
|
|
3049
|
+
// A scoped name is a name, not a path: `@scope/pkg` stays whole. Anything else is a path, whose last
|
|
3050
|
+
// segment names the project.
|
|
3056
3051
|
const scoped = /^@[^\\/]+[\\/][^\\/]+$/.test(trimmed);
|
|
3057
3052
|
const base = scoped ? trimmed : trimmed.split(/[\\/]/).filter(Boolean).pop() ?? '';
|
|
3058
3053
|
if (!base) return null;
|
|
@@ -3066,12 +3061,12 @@ const QUALITY_PRESETS = [
|
|
|
3066
3061
|
|
|
3067
3062
|
;// CONCATENATED MODULE: ./src/features/combinations.ts
|
|
3068
3063
|
/**
|
|
3069
|
-
* Biome's CSS parser rejects Tailwind's syntax — `@apply`
|
|
3070
|
-
*
|
|
3071
|
-
*
|
|
3064
|
+
* Biome's CSS parser rejects Tailwind's syntax — `@apply` is a parse error, not an unknown-at-rule warning — so
|
|
3065
|
+
* a project with both needs Biome pointed away from stylesheets. The overlay is a second `biome.json` listed
|
|
3066
|
+
* after the first.
|
|
3072
3067
|
*
|
|
3073
|
-
* Narrow on purpose:
|
|
3074
|
-
*
|
|
3068
|
+
* Narrow on purpose: excluding CSS for everybody would give up formatting the plain-CSS template that Biome
|
|
3069
|
+
* handles perfectly well.
|
|
3075
3070
|
*/ const BIOME_TAILWIND = {
|
|
3076
3071
|
id: 'biome-tailwind',
|
|
3077
3072
|
overlays: [
|
|
@@ -3097,14 +3092,12 @@ const QUALITY_PRESETS = [
|
|
|
3097
3092
|
/**
|
|
3098
3093
|
* The scripts every app gets, whatever it targets.
|
|
3099
3094
|
*
|
|
3100
|
-
* `start`, `preview` and `deploy` are
|
|
3101
|
-
*
|
|
3102
|
-
*
|
|
3095
|
+
* `start`, `preview` and `deploy` are not here: each means something different per platform, so the deploy
|
|
3096
|
+
* target contributes its own. The three names are a contract the targets keep, which is what lets the README
|
|
3097
|
+
* describe an app it was not written for:
|
|
3103
3098
|
*
|
|
3104
|
-
* - **`start`** runs a build that already exists and never makes one — what a host's own start command
|
|
3105
|
-
*
|
|
3106
|
-
* - **`preview`** builds, then runs the result here — for the targets where that is not the same two
|
|
3107
|
-
* commands, because otherwise the production build is unanswerable without deploying it.
|
|
3099
|
+
* - **`start`** runs a build that already exists and never makes one — what a host's own start command calls.
|
|
3100
|
+
* - **`preview`** builds, then runs the result here, so the production build is answerable without deploying.
|
|
3108
3101
|
* - **`deploy`** builds, then ships it — where the platform has one command that does the shipping.
|
|
3109
3102
|
*/ const BASE_SCRIPTS = {
|
|
3110
3103
|
dev: 'rshono dev',
|
|
@@ -3112,9 +3105,8 @@ const QUALITY_PRESETS = [
|
|
|
3112
3105
|
typecheck: 'tsc --noEmit'
|
|
3113
3106
|
};
|
|
3114
3107
|
/**
|
|
3115
|
-
* Every script the app gets, in the order they are written: the base ones, then each feature's
|
|
3116
|
-
* the
|
|
3117
|
-
* document a script the other does not have.
|
|
3108
|
+
* Every script the app gets, in the order they are written: the base ones, then each feature's. The manifest and
|
|
3109
|
+
* the README's command table are both this, so neither can document a script the other lacks.
|
|
3118
3110
|
*/ function buildScripts(features) {
|
|
3119
3111
|
const scripts = {
|
|
3120
3112
|
...BASE_SCRIPTS
|
|
@@ -3130,9 +3122,8 @@ const QUALITY_PRESETS = [
|
|
|
3130
3122
|
};
|
|
3131
3123
|
}
|
|
3132
3124
|
/**
|
|
3133
|
-
* Script names a package manager has a command of its own for
|
|
3134
|
-
*
|
|
3135
|
-
* something else. The explicit `run` form is what all four managers accept, so those names get it.
|
|
3125
|
+
* Script names a package manager has a command of its own for: `pnpm deploy` runs pnpm's workspace-deploy and
|
|
3126
|
+
* never looks at the manifest. These get the explicit `run` form, which all four managers accept.
|
|
3136
3127
|
*/ const SHADOWED = new Set([
|
|
3137
3128
|
'deploy'
|
|
3138
3129
|
]);
|
|
@@ -3140,10 +3131,9 @@ const QUALITY_PRESETS = [
|
|
|
3140
3131
|
return `${SHADOWED.has(script) ? `${pm.name} run` : pm.run} ${script}`;
|
|
3141
3132
|
}
|
|
3142
3133
|
/**
|
|
3143
|
-
* The README's command table: one line per script, with the command to type and a one-line gloss.
|
|
3144
|
-
*
|
|
3145
|
-
*
|
|
3146
|
-
* rest" line — that is how a formatter's `format:check` stays out of a table about running the app.
|
|
3134
|
+
* The README's command table: one line per script, with the command to type and a one-line gloss. A script whose
|
|
3135
|
+
* feature supplies no `scriptHelp` is left out — that is how `format:check` stays out of a table about running
|
|
3136
|
+
* the app — and covered by the line pointing at `package.json`.
|
|
3147
3137
|
*/ function scriptTable(answers, features, pm) {
|
|
3148
3138
|
const help = baseScriptHelp(answers.deploy);
|
|
3149
3139
|
for (const feature of features)Object.assign(help, feature.scriptHelp);
|
|
@@ -3152,16 +3142,14 @@ const QUALITY_PRESETS = [
|
|
|
3152
3142
|
return documented.map((name)=>`${invoke(pm, name).padEnd(width)} # ${help[name]}`).join('\n');
|
|
3153
3143
|
}
|
|
3154
3144
|
/**
|
|
3155
|
-
* The command that gets this app into production, as a sentence — the README's deploy step
|
|
3156
|
-
*
|
|
3145
|
+
* The command that gets this app into production, as a sentence — the README's deploy step and the closing
|
|
3146
|
+
* summary's, so the two cannot name different commands.
|
|
3157
3147
|
*
|
|
3158
3148
|
* Read off the scripts rather than the target, so a target that gains a `deploy` gains the sentence with it.
|
|
3159
|
-
* `start` is the answer where there is no `deploy`: it ships nothing itself, but it is what the host runs.
|
|
3160
3149
|
*/ function deployStep(features, pm) {
|
|
3161
3150
|
const scripts = buildScripts(features);
|
|
3162
3151
|
if (scripts.deploy) return `\`${invoke(pm, 'deploy')}\` does the build and the upload in one step.`;
|
|
3163
3152
|
if (scripts.start) return `\`${invoke(pm, 'start')}\` runs that build wherever you host it, and never makes one.`;
|
|
3164
|
-
// Every branch names a script the app has, so a target that contributes none of the three still reads true.
|
|
3165
3153
|
if (scripts.preview) return `\`${invoke(pm, 'preview')}\` runs the build here, so you can check it first.`;
|
|
3166
3154
|
return `\`${invoke(pm, 'build')}\` produces it; getting it there is yours to script.`;
|
|
3167
3155
|
}
|
|
@@ -3171,14 +3159,12 @@ const QUALITY_PRESETS = [
|
|
|
3171
3159
|
/** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
|
|
3172
3160
|
/** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
|
|
3173
3161
|
/**
|
|
3174
|
-
* Versions for the optional tooling the features can add — the one place in this package where a
|
|
3175
|
-
*
|
|
3176
|
-
*
|
|
3177
|
-
* which is generated from rshono's own manifest and must not be edited here.
|
|
3162
|
+
* Versions for the optional tooling the features can add — the one place in this package where a range is typed
|
|
3163
|
+
* by hand, because the framework declares none of these. Everything a scaffolded app needs to *run* rshono comes
|
|
3164
|
+
* from {@link FRAMEWORK_DEPS}, generated from rshono's own manifest.
|
|
3178
3165
|
*
|
|
3179
|
-
*
|
|
3180
|
-
*
|
|
3181
|
-
* was last touched.
|
|
3166
|
+
* Caret ranges, not exact: these are the app's own dev tools, and a scaffold made six months from now should
|
|
3167
|
+
* pick up their patch releases.
|
|
3182
3168
|
*/ const TOOL_VERSIONS = {
|
|
3183
3169
|
tailwindcss: '^4.3.3',
|
|
3184
3170
|
'@tailwindcss/postcss': '^4.3.3',
|
|
@@ -3190,23 +3176,20 @@ const QUALITY_PRESETS = [
|
|
|
3190
3176
|
oxlint: '^1.76.0',
|
|
3191
3177
|
oxfmt: '^0.61.0',
|
|
3192
3178
|
eslint: '^10.8.0',
|
|
3193
|
-
// ESLint's
|
|
3194
|
-
//
|
|
3179
|
+
// ESLint's recommended JavaScript rules, which typescript-eslint layers on rather than replaces, and the rules
|
|
3180
|
+
// of hooks — the one class of React mistake no type checker sees.
|
|
3195
3181
|
'@eslint/js': '^10.0.1',
|
|
3196
3182
|
'typescript-eslint': '^8.65.0',
|
|
3197
3183
|
'eslint-plugin-react-hooks': '^7.1.1',
|
|
3198
3184
|
wrangler: '^4.115.0'
|
|
3199
3185
|
};
|
|
3200
3186
|
/**
|
|
3201
|
-
* The TypeScript an ESLint app pins
|
|
3187
|
+
* The TypeScript an ESLint app pins in place of the framework's own — the one deliberate exception to
|
|
3202
3188
|
* {@link FRAMEWORK_DEPS}.
|
|
3203
3189
|
*
|
|
3204
|
-
* typescript-eslint reads TypeScript's compiler API directly
|
|
3205
|
-
*
|
|
3206
|
-
*
|
|
3207
|
-
* and not the framework's.
|
|
3208
|
-
*
|
|
3209
|
-
* When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
|
|
3190
|
+
* typescript-eslint reads TypeScript's compiler API directly, so it accepts `>=4.8.4 <6.1.0` and nothing above;
|
|
3191
|
+
* `~6.0.3` is the newest that satisfies it. rshono's declarations compile the same under either, which is what
|
|
3192
|
+
* makes this the app's business. When upstream widens its range, this constant is what to delete.
|
|
3210
3193
|
*/ const ESLINT_TYPESCRIPT = '~6.0.3';
|
|
3211
3194
|
|
|
3212
3195
|
;// CONCATENATED MODULE: ./src/features/deploy.ts
|
|
@@ -3215,22 +3198,17 @@ const QUALITY_PRESETS = [
|
|
|
3215
3198
|
/**
|
|
3216
3199
|
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
|
|
3217
3200
|
*
|
|
3218
|
-
*
|
|
3219
|
-
*
|
|
3220
|
-
*
|
|
3221
|
-
* build, the CLI they need, the directories to gitignore, and a note for the step no command covers.
|
|
3201
|
+
* Thin on purpose: the framework arranges its own output for every platform, and `rshono build` writes the one
|
|
3202
|
+
* platform config that has to exist. So a target contributes the commands that run and ship the build, the CLI
|
|
3203
|
+
* they need, the directories to gitignore, and a note for the step no command covers.
|
|
3222
3204
|
*
|
|
3223
|
-
* What the three script names promise is documented
|
|
3224
|
-
*
|
|
3225
|
-
*
|
|
3226
|
-
*
|
|
3227
|
-
* `pm` is here because two things have to be spelled for the app's package manager — the runner that fetches
|
|
3228
|
-
* the uninstalled Vercel CLI ({@link PackageManager.dlx}), and every command in {@link Feature.platformSetup}.
|
|
3205
|
+
* What the three script names promise is documented above `BASE_SCRIPTS` in `scripts.ts`. Only `node` has a
|
|
3206
|
+
* `start` — `rshono start` refuses a bundle built for anywhere else — and it needs no `preview`, since `build`
|
|
3207
|
+
* then `start` already is one.
|
|
3229
3208
|
*/ function deployFeatures(pm) {
|
|
3230
3209
|
const build = invoke(pm, 'build');
|
|
3231
3210
|
return {
|
|
3232
|
-
// Where a Node build goes
|
|
3233
|
-
// guess — so the target contributes only the command that runs what was built.
|
|
3211
|
+
// Where a Node build goes next is a Dockerfile or a process manager, neither of which this can guess.
|
|
3234
3212
|
node: {
|
|
3235
3213
|
id: 'deploy-node',
|
|
3236
3214
|
scripts: {
|
|
@@ -3253,16 +3231,14 @@ const QUALITY_PRESETS = [
|
|
|
3253
3231
|
devDependencies: {
|
|
3254
3232
|
wrangler: TOOL_VERSIONS.wrangler
|
|
3255
3233
|
},
|
|
3256
|
-
// The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each
|
|
3257
|
-
//
|
|
3258
|
-
// neither needs to run — `workerd --version` and `esbuild --version` both answer without it.
|
|
3234
|
+
// The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each merely
|
|
3235
|
+
// picks the platform binary out of the optional dependency already carrying it, so neither needs to run.
|
|
3259
3236
|
allowBuilds: {
|
|
3260
3237
|
esbuild: false,
|
|
3261
3238
|
workerd: false
|
|
3262
3239
|
},
|
|
3263
|
-
// `wrangler dev` is the one preview that runs the code in
|
|
3264
|
-
//
|
|
3265
|
-
// build wrote, so nothing here has to know where the bundle or the assets went.
|
|
3240
|
+
// `wrangler dev` is the one preview that runs the code in workerd rather than Node. Both scripts read the
|
|
3241
|
+
// wrangler.jsonc the build wrote, so nothing here has to know where the bundle went.
|
|
3266
3242
|
scripts: {
|
|
3267
3243
|
preview: 'rshono build && wrangler dev',
|
|
3268
3244
|
deploy: 'rshono build && wrangler deploy'
|
|
@@ -3284,9 +3260,8 @@ const QUALITY_PRESETS = [
|
|
|
3284
3260
|
},
|
|
3285
3261
|
vercel: {
|
|
3286
3262
|
id: 'deploy-vercel',
|
|
3287
|
-
// `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview
|
|
3288
|
-
//
|
|
3289
|
-
// build run here — the platform has no way to run its own prebuilt output on your machine.
|
|
3263
|
+
// `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview URL.
|
|
3264
|
+
// `preview` is a Node build run here — the platform cannot run its own prebuilt output on your machine.
|
|
3290
3265
|
scripts: {
|
|
3291
3266
|
preview: 'rshono build --deploy node && rshono start',
|
|
3292
3267
|
deploy: `rshono build && ${pm.dlx} vercel deploy --prebuilt --prod`
|
|
@@ -3312,8 +3287,7 @@ const QUALITY_PRESETS = [
|
|
|
3312
3287
|
},
|
|
3313
3288
|
'aws-lambda': {
|
|
3314
3289
|
id: 'deploy-aws-lambda',
|
|
3315
|
-
// No CLI to wrap
|
|
3316
|
-
// handler, so it runs here.
|
|
3290
|
+
// No CLI to wrap and no upload to guess at, but `preview` still applies: the bundle is a Node handler.
|
|
3317
3291
|
scripts: {
|
|
3318
3292
|
preview: 'rshono build --deploy node && rshono start'
|
|
3319
3293
|
},
|
|
@@ -3338,14 +3312,11 @@ function deployFeature(target, pm) {
|
|
|
3338
3312
|
;// CONCATENATED MODULE: ./src/features/quality.ts
|
|
3339
3313
|
|
|
3340
3314
|
/**
|
|
3341
|
-
* The formatter and linter features. Biome answers to both slots and appears once
|
|
3342
|
-
* deduplicates by `id
|
|
3343
|
-
* dependency and one pair of scripts.
|
|
3315
|
+
* The formatter and linter features. Biome answers to both slots and appears once, because `selectFeatures`
|
|
3316
|
+
* deduplicates by `id`.
|
|
3344
3317
|
*
|
|
3345
|
-
*
|
|
3346
|
-
*
|
|
3347
|
-
* `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
|
|
3348
|
-
* one. Biome adds a `check` of its own, which is the pair of them in a single pass.
|
|
3318
|
+
* Each brings a pair of scripts, since the writing half and the CI half want different exit codes: `format`
|
|
3319
|
+
* rewrites and `format:check` fails, `lint:fix` rewrites and `lint` fails. Biome's `check` is both in one pass.
|
|
3349
3320
|
*/ const PRETTIER = {
|
|
3350
3321
|
id: 'prettier',
|
|
3351
3322
|
overlays: [
|
|
@@ -3386,11 +3357,10 @@ const OXLINT = {
|
|
|
3386
3357
|
}
|
|
3387
3358
|
};
|
|
3388
3359
|
/**
|
|
3389
|
-
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
3390
|
-
*
|
|
3391
|
-
*
|
|
3392
|
-
*
|
|
3393
|
-
* program rather than linting file by file.
|
|
3360
|
+
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
|
|
3361
|
+
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest its peer range
|
|
3362
|
+
* accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to reach for ESLint at all —
|
|
3363
|
+
* so the config it ships hands the parser the whole program.
|
|
3394
3364
|
*/ const ESLINT = {
|
|
3395
3365
|
id: 'eslint',
|
|
3396
3366
|
overlays: [
|
|
@@ -3446,13 +3416,12 @@ function linterFeature(linter) {
|
|
|
3446
3416
|
;// CONCATENATED MODULE: ./src/features/styling.ts
|
|
3447
3417
|
|
|
3448
3418
|
/**
|
|
3449
|
-
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
3450
|
-
*
|
|
3451
|
-
*
|
|
3452
|
-
* in utilities.
|
|
3419
|
+
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages, plus an
|
|
3420
|
+
* overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts postcss-loader in front
|
|
3421
|
+
* of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten in utilities.
|
|
3453
3422
|
*
|
|
3454
|
-
* `postcss`
|
|
3455
|
-
*
|
|
3423
|
+
* `postcss` is the app's dependency rather than the framework's: rshono compiles CSS natively, so an app that
|
|
3424
|
+
* wants no plugin chain installs none.
|
|
3456
3425
|
*/ const TAILWIND = {
|
|
3457
3426
|
id: 'tailwind',
|
|
3458
3427
|
overlays: [
|
|
@@ -3475,12 +3444,11 @@ function stylingFeature(styling) {
|
|
|
3475
3444
|
|
|
3476
3445
|
|
|
3477
3446
|
/**
|
|
3478
|
-
* The features a set of answers selects, in application order — so an overlay listed later wins a file
|
|
3479
|
-
*
|
|
3480
|
-
*
|
|
3447
|
+
* The features a set of answers selects, in application order — so an overlay listed later wins a file both
|
|
3448
|
+
* ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome is both formatter and
|
|
3449
|
+
* linter) without contributing twice.
|
|
3481
3450
|
*
|
|
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.
|
|
3451
|
+
* `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled CLI.
|
|
3484
3452
|
*/ function selectFeatures(answers, pm) {
|
|
3485
3453
|
const selected = [
|
|
3486
3454
|
deployFeature(answers.deploy, pm),
|
|
@@ -3517,15 +3485,11 @@ function sorted(record) {
|
|
|
3517
3485
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
3518
3486
|
}
|
|
3519
3487
|
/**
|
|
3520
|
-
* pnpm's settings for the new app,
|
|
3521
|
-
* means there is nothing to say, so no file is written.
|
|
3522
|
-
*
|
|
3523
|
-
* It exists for one field, `allowBuilds`. pnpm fails an install — and every `pnpm dev` after it —
|
|
3524
|
-
* until the project has said whether a dependency's install script should run. Nothing rshono itself
|
|
3525
|
-
* installs has one, so most apps get no file; the packages that do (wrangler's esbuild and workerd)
|
|
3526
|
-
* declare their answer on the feature that brings them.
|
|
3488
|
+
* pnpm's settings for the new app, or `null` when no feature has anything to put in them.
|
|
3527
3489
|
*
|
|
3528
|
-
*
|
|
3490
|
+
* It exists for one field, `allowBuilds`: pnpm fails an install until the project has said whether a
|
|
3491
|
+
* dependency's install script should run. Nothing rshono installs has one, so most apps get no file. A file
|
|
3492
|
+
* rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
3529
3493
|
*/ function buildPnpmSettings(features) {
|
|
3530
3494
|
const allowBuilds = {};
|
|
3531
3495
|
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
@@ -3541,10 +3505,8 @@ function sorted(record) {
|
|
|
3541
3505
|
].join('\n');
|
|
3542
3506
|
}
|
|
3543
3507
|
/**
|
|
3544
|
-
* Assembles `package.json` from the answers and whatever the selected features contribute.
|
|
3545
|
-
*
|
|
3546
|
-
* Dependencies are sorted by name and scripts keep the order {@link buildScripts} gives them — so two runs
|
|
3547
|
-
* with the same answers produce byte-identical output, which is what makes the manifest snapshot-testable.
|
|
3508
|
+
* Assembles `package.json` from the answers and whatever the selected features contribute. Dependencies are
|
|
3509
|
+
* sorted and scripts keep {@link buildScripts}'s order, so two runs with the same answers are byte-identical.
|
|
3548
3510
|
*/ function buildPackageJson(answers, features, pm) {
|
|
3549
3511
|
const dependencies = {
|
|
3550
3512
|
'@rshono/core': RSHONO_RANGE,
|
|
@@ -3574,8 +3536,8 @@ function sorted(record) {
|
|
|
3574
3536
|
dependencies: sorted(dependencies),
|
|
3575
3537
|
devDependencies: sorted(devDependencies)
|
|
3576
3538
|
};
|
|
3577
|
-
// Only when the environment told us the exact version:
|
|
3578
|
-
//
|
|
3539
|
+
// Only when the environment told us the exact version: this field pins the tool for Corepack, and a guess is
|
|
3540
|
+
// worse than leaving it out.
|
|
3579
3541
|
if (pm.version) manifest.packageManager = `${pm.name}@${pm.version}`;
|
|
3580
3542
|
const ordered = Object.fromEntries(FIELD_ORDER.filter((field)=>field in manifest).map((field)=>[
|
|
3581
3543
|
field,
|
|
@@ -3587,25 +3549,22 @@ function sorted(record) {
|
|
|
3587
3549
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
3588
3550
|
|
|
3589
3551
|
/**
|
|
3590
|
-
* `{{NAME}}`,
|
|
3591
|
-
*
|
|
3592
|
-
* matching. `{{…}}` means nothing to any format these templates are written in.
|
|
3552
|
+
* `{{NAME}}`, not `__NAME__`: in markdown the latter is strong emphasis, so Prettier rewrites it to `**NAME**`
|
|
3553
|
+
* and the token stops matching. `{{…}}` means nothing to any format these templates are written in.
|
|
3593
3554
|
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
3594
3555
|
function tokensFor(answers, features, pm) {
|
|
3595
3556
|
return {
|
|
3596
3557
|
'{{PROJECT_NAME}}': answers.packageName,
|
|
3597
3558
|
'{{DEPLOY_TARGET}}': answers.deploy,
|
|
3598
|
-
//
|
|
3599
|
-
// actually got: its command table, the one command that ships it, and what its platform asks for.
|
|
3559
|
+
// From the features rather than the answers, because all three are about the scripts the app actually got.
|
|
3600
3560
|
'{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
|
|
3601
3561
|
'{{DEPLOY_STEP}}': deployStep(features, pm),
|
|
3602
3562
|
'{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
|
|
3603
3563
|
};
|
|
3604
3564
|
}
|
|
3605
3565
|
/**
|
|
3606
|
-
* Substitutes tokens, and throws on one it
|
|
3607
|
-
*
|
|
3608
|
-
* catch.
|
|
3566
|
+
* Substitutes tokens, and throws on one it does not know — a typo in a template would otherwise ship a literal
|
|
3567
|
+
* `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would catch.
|
|
3609
3568
|
*/ function render(contents, tokens, source) {
|
|
3610
3569
|
return contents.replace(TOKEN_PATTERN, (token)=>{
|
|
3611
3570
|
const value = tokens[token];
|
|
@@ -3635,11 +3594,9 @@ function readTemplateDir(dir) {
|
|
|
3635
3594
|
return files;
|
|
3636
3595
|
}
|
|
3637
3596
|
/**
|
|
3638
|
-
* `_gitignore` → `.gitignore`, and so on for every dotfile
|
|
3639
|
-
*
|
|
3640
|
-
*
|
|
3641
|
-
* would exist in the repo, pass every local test, and be missing from the package everybody installs.
|
|
3642
|
-
* Applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
3597
|
+
* `_gitignore` → `.gitignore`, and so on for every dotfile: npm strips a literal `.gitignore` out of a
|
|
3598
|
+
* published tarball, so a template that contained one would pass every local test and be missing from the
|
|
3599
|
+
* package everybody installs. The basename only, so `templates/_x/y.ts` is left alone.
|
|
3643
3600
|
*/ function undotted(path) {
|
|
3644
3601
|
const segments = path.split(posix.sep);
|
|
3645
3602
|
const name = segments.pop();
|
|
@@ -3648,19 +3605,16 @@ function readTemplateDir(dir) {
|
|
|
3648
3605
|
name.startsWith('_') ? `.${name.slice(1)}` : name
|
|
3649
3606
|
].join(posix.sep);
|
|
3650
3607
|
}
|
|
3651
|
-
/**
|
|
3652
|
-
* A feature's `.gitignore` lines, appended under a heading naming it — so somebody reading the file six
|
|
3653
|
-
* months later can tell why `.wrangler/` is in there.
|
|
3654
|
-
*/ function appendGitignore(existing, features) {
|
|
3608
|
+
/** A feature's `.gitignore` lines, under a heading naming it — so a reader can tell why `.wrangler/` is there. */ function appendGitignore(existing, features) {
|
|
3655
3609
|
const additions = features.filter((feature)=>feature.gitignore?.length);
|
|
3656
3610
|
if (additions.length === 0) return existing;
|
|
3657
3611
|
const blocks = additions.map((feature)=>`\n# ${feature.id}\n${feature.gitignore.join('\n')}\n`);
|
|
3658
3612
|
return existing + blocks.join('');
|
|
3659
3613
|
}
|
|
3660
3614
|
/**
|
|
3661
|
-
* Turns answers into the exact set of files to write, without touching the target directory
|
|
3662
|
-
* decisions
|
|
3663
|
-
*
|
|
3615
|
+
* Turns answers into the exact set of files to write, without touching the target directory. Separating the
|
|
3616
|
+
* decisions from the I/O is what lets a test assert on the whole matrix of answers, and makes `--dry-run` the
|
|
3617
|
+
* same code path minus the last step.
|
|
3664
3618
|
*/ function plan_plan(answers, pm) {
|
|
3665
3619
|
const features = selectFeatures(answers, pm);
|
|
3666
3620
|
const tokens = tokensFor(answers, features, pm);
|
|
@@ -3732,11 +3686,9 @@ function packageManager(name, version) {
|
|
|
3732
3686
|
};
|
|
3733
3687
|
}
|
|
3734
3688
|
/**
|
|
3735
|
-
* Which package manager invoked us.
|
|
3736
|
-
*
|
|
3737
|
-
*
|
|
3738
|
-
*
|
|
3739
|
-
* Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
3689
|
+
* Which package manager invoked us. All four set `npm_config_user_agent` on the process they spawn — `pnpm/11.9.0
|
|
3690
|
+
* npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm project without asking, exact
|
|
3691
|
+
* version included. Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
3740
3692
|
*/ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
3741
3693
|
const [spec] = (userAgent ?? '').split(' ');
|
|
3742
3694
|
const [name, version] = (spec ?? '').split('/');
|
|
@@ -3744,9 +3696,8 @@ function packageManager(name, version) {
|
|
|
3744
3696
|
return packageManager('npm');
|
|
3745
3697
|
}
|
|
3746
3698
|
/**
|
|
3747
|
-
* Runs the install, streaming its output. `shell: true` on Windows
|
|
3748
|
-
*
|
|
3749
|
-
* are all fixed strings from the tables above, never anything the user typed.
|
|
3699
|
+
* Runs the install, streaming its output. `shell: true` on Windows, where npm, pnpm and yarn are `.cmd` shims
|
|
3700
|
+
* `spawn` cannot execute directly — safe because every argument is a fixed string from the tables above.
|
|
3750
3701
|
*/ function runInstall(pm, cwd) {
|
|
3751
3702
|
return run(pm, pm.install, cwd);
|
|
3752
3703
|
}
|
|
@@ -3764,9 +3715,8 @@ function run(pm, args, cwd) {
|
|
|
3764
3715
|
|
|
3765
3716
|
|
|
3766
3717
|
/**
|
|
3767
|
-
* The two halves of the closing line, in the words of the app's own scripts: what produces something
|
|
3768
|
-
*
|
|
3769
|
-
* which is a sentence about the platform rather than something to type.
|
|
3718
|
+
* The two halves of the closing line, in the words of the app's own scripts: what produces something shippable,
|
|
3719
|
+
* and where it goes from there. A target with no command to give falls back to the framework's hint.
|
|
3770
3720
|
*/ function productionSteps(answers, plan, pm) {
|
|
3771
3721
|
const scripts = buildScripts(plan.features);
|
|
3772
3722
|
const check = scripts.preview ? `${invoke(pm, 'preview')} to run the production build` : `${pm.run} build`;
|
|
@@ -3774,8 +3724,8 @@ function run(pm, args, cwd) {
|
|
|
3774
3724
|
return `${check}, and ${ship}`;
|
|
3775
3725
|
}
|
|
3776
3726
|
/**
|
|
3777
|
-
* What to do next, in the order to do it — the last thing the user reads
|
|
3778
|
-
*
|
|
3727
|
+
* What to do next, in the order to do it — the last thing the user reads. `installed` decides whether the install
|
|
3728
|
+
* step is still theirs.
|
|
3779
3729
|
*/ function nextSteps(answers, plan, pm, options) {
|
|
3780
3730
|
const steps = [];
|
|
3781
3731
|
if (options.directory !== '.') steps.push(`cd ${options.directory}`);
|
|
@@ -3810,10 +3760,7 @@ function run(pm, args, cwd) {
|
|
|
3810
3760
|
;// CONCATENATED MODULE: ./src/write.ts
|
|
3811
3761
|
|
|
3812
3762
|
|
|
3813
|
-
/**
|
|
3814
|
-
* Files that do not make a directory "occupied". A user who ran `git init` or opened the folder in an
|
|
3815
|
-
* editor before scaffolding has not put anything in it that we would overwrite.
|
|
3816
|
-
*/ const IGNORED_ENTRIES = new Set([
|
|
3763
|
+
/** Files that do not make a directory "occupied": a `git init` or an editor has put nothing there to overwrite. */ const IGNORED_ENTRIES = new Set([
|
|
3817
3764
|
'.git',
|
|
3818
3765
|
'.DS_Store',
|
|
3819
3766
|
'.idea',
|
|
@@ -3821,12 +3768,9 @@ function run(pm, args, cwd) {
|
|
|
3821
3768
|
'Thumbs.db'
|
|
3822
3769
|
]);
|
|
3823
3770
|
/**
|
|
3824
|
-
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind —
|
|
3825
|
-
*
|
|
3826
|
-
*
|
|
3827
|
-
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory throws
|
|
3828
|
-
* rather than reporting an empty list, since `--force` should not write into one either — otherwise
|
|
3829
|
-
* `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
3771
|
+
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind — which is
|
|
3772
|
+
* what decides whether scaffolding into it is safe. A path that does not exist yet is no conflict; one that
|
|
3773
|
+
* exists and is not a directory throws, since `--force` should not write into it either.
|
|
3830
3774
|
*/ function conflictingEntries(dir) {
|
|
3831
3775
|
const stats = statSync(dir, {
|
|
3832
3776
|
throwIfNoEntry: false
|
|
@@ -3836,8 +3780,8 @@ function run(pm, args, cwd) {
|
|
|
3836
3780
|
return external_node_fs_readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
|
|
3837
3781
|
}
|
|
3838
3782
|
/**
|
|
3839
|
-
* Writes the plan
|
|
3840
|
-
*
|
|
3783
|
+
* Writes the plan, creating directories as needed and keeping the plan's own ordering — so a failure part-way
|
|
3784
|
+
* through leaves something a person can make sense of.
|
|
3841
3785
|
*/ function writePlan(plan, targetDir) {
|
|
3842
3786
|
mkdirSync(targetDir, {
|
|
3843
3787
|
recursive: true
|
|
@@ -3969,10 +3913,9 @@ function fail(message) {
|
|
|
3969
3913
|
async function main() {
|
|
3970
3914
|
const { values, positionals } = parse();
|
|
3971
3915
|
if (values.help) return console.log(HELP);
|
|
3972
|
-
if (values.version) return console.log("1.0.0-rc.
|
|
3973
|
-
// A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both
|
|
3974
|
-
//
|
|
3975
|
-
// `echo | npx @rshono/create` would otherwise ask a question with nothing behind the keyboard.
|
|
3916
|
+
if (values.version) return console.log("1.0.0-rc.13");
|
|
3917
|
+
// A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both streams have to
|
|
3918
|
+
// be a terminal: the prompts draw on stdout but read from stdin.
|
|
3976
3919
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
|
|
3977
3920
|
const pmFlag = oneOf(values.pm, PACKAGE_MANAGERS, 'pm');
|
|
3978
3921
|
const pm = pmFlag ? packageManager(pmFlag) : detectPackageManager();
|
|
@@ -3983,8 +3926,7 @@ async function main() {
|
|
|
3983
3926
|
const tailwindFlag = tristate(values.tailwind, values['no-tailwind'], 'tailwind');
|
|
3984
3927
|
const installFlag = tristate(values.install, values['no-install'], 'install');
|
|
3985
3928
|
const gitFlag = tristate(values.git, values['no-git'], 'git');
|
|
3986
|
-
// The framework version, not this package's: it is the one the app
|
|
3987
|
-
// worth reading here. `--version` reports create-rshono's own.
|
|
3929
|
+
// The framework version, not this package's: it is the one the app gets pinned to. `--version` reports ours.
|
|
3988
3930
|
intro(`create-rshono · rshono ${RSHONO_VERSION}`);
|
|
3989
3931
|
// ── Where ───────────────────────────────────────────────────────────────────────────────────────
|
|
3990
3932
|
let directory = positionals[0];
|
|
@@ -4044,8 +3986,8 @@ async function main() {
|
|
|
4044
3986
|
]
|
|
4045
3987
|
}));
|
|
4046
3988
|
}
|
|
4047
|
-
// One question instead of two
|
|
4048
|
-
//
|
|
3989
|
+
// One question instead of two; the axes stay independent underneath, and a `--formatter` or `--linter` flag
|
|
3990
|
+
// addresses either on its own.
|
|
4049
3991
|
let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
|
|
4050
3992
|
if (!preset && !formatterFlag && !linterFlag) {
|
|
4051
3993
|
const fallback = QUALITY_PRESETS["0"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rshono/create",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.13",
|
|
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.
|
|
49
|
+
"@rshono/core": "1.0.0-rc.13"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "node scripts/codegen.mjs && node scripts/build.mjs",
|
package/templates/base/README.md
CHANGED
|
@@ -37,4 +37,4 @@ Change `deploy` in `rshono.config.ts` to target somewhere else, or build for one
|
|
|
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
39
|
|
|
40
|
-
Every target, and what each one needs: <https://www.rshono.com/docs/deployment>
|
|
40
|
+
Every deployment target, and what each one needs: <https://www.rshono.com/docs/deployment>
|
|
@@ -3,7 +3,10 @@ import reactHooks from 'eslint-plugin-react-hooks';
|
|
|
3
3
|
import tseslint from 'typescript-eslint';
|
|
4
4
|
|
|
5
5
|
export default tseslint.config(
|
|
6
|
-
|
|
6
|
+
// Build output, all of it generated: `dist/` from `rshono build`, `.rshono/` from `rshono dev`, and
|
|
7
|
+
// whatever the deploy target assembles beside them. Without this, linting after a dev run reports on
|
|
8
|
+
// bundles rather than on anything you wrote.
|
|
9
|
+
{ ignores: ['dist/**', '.rshono/**', '.wrangler/**', '.vercel/**', '.netlify/**'] },
|
|
7
10
|
js.configs.recommended,
|
|
8
11
|
tseslint.configs.recommendedTypeChecked,
|
|
9
12
|
reactHooks.configs.flat['recommended-latest'],
|