@rshono/create 1.0.0-rc.12 → 1.0.0-rc.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin/create-rshono.mjs +4 -4
- package/dist/api.mjs +112 -158
- package/dist/cli.mjs +127 -178
- package/package.json +3 -3
- package/templates/eslint/eslint.config.mjs +16 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lasse Tange
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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.14';
|
|
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
|
},
|
|
@@ -349,8 +327,9 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
349
327
|
],
|
|
350
328
|
platformSetup: [
|
|
351
329
|
`There is no settings page here; the upload is yours to script. A job needs \`${pm.name} install && ${build}\`,`,
|
|
352
|
-
'then the function package: `dist
|
|
353
|
-
'bundle
|
|
330
|
+
'then the function package: `dist/` and nothing else, with the handler at `dist/server/main.mjs`. Your',
|
|
331
|
+
'dependencies are compiled into the bundle for this target, so no `node_modules` is uploaded — which also',
|
|
332
|
+
'means a native addon fails the build here rather than the deploy.'
|
|
354
333
|
].join('\n')
|
|
355
334
|
}
|
|
356
335
|
};
|
|
@@ -362,14 +341,11 @@ function deployFeature(target, pm) {
|
|
|
362
341
|
;// CONCATENATED MODULE: ./src/features/quality.ts
|
|
363
342
|
|
|
364
343
|
/**
|
|
365
|
-
* The formatter and linter features. Biome answers to both slots and appears once
|
|
366
|
-
* deduplicates by `id
|
|
367
|
-
* dependency and one pair of scripts.
|
|
344
|
+
* The formatter and linter features. Biome answers to both slots and appears once, because `selectFeatures`
|
|
345
|
+
* deduplicates by `id`.
|
|
368
346
|
*
|
|
369
|
-
*
|
|
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.
|
|
347
|
+
* Each brings a pair of scripts, since the writing half and the CI half want different exit codes: `format`
|
|
348
|
+
* rewrites and `format:check` fails, `lint:fix` rewrites and `lint` fails. Biome's `check` is both in one pass.
|
|
373
349
|
*/ const PRETTIER = {
|
|
374
350
|
id: 'prettier',
|
|
375
351
|
overlays: [
|
|
@@ -410,11 +386,10 @@ const OXLINT = {
|
|
|
410
386
|
}
|
|
411
387
|
};
|
|
412
388
|
/**
|
|
413
|
-
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* program rather than linting file by file.
|
|
389
|
+
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
|
|
390
|
+
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest its peer range
|
|
391
|
+
* accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to reach for ESLint at all —
|
|
392
|
+
* so the config it ships hands the parser the whole program.
|
|
418
393
|
*/ const ESLINT = {
|
|
419
394
|
id: 'eslint',
|
|
420
395
|
overlays: [
|
|
@@ -430,6 +405,12 @@ const OXLINT = {
|
|
|
430
405
|
scripts: {
|
|
431
406
|
lint: 'eslint .',
|
|
432
407
|
'lint:fix': 'eslint . --fix'
|
|
408
|
+
},
|
|
409
|
+
// The pin is invisible from the command line, and its consequence is not: the rules reason about the
|
|
410
|
+
// program through an older compiler than the one that builds it, so a fix can produce code `tsc` rejects.
|
|
411
|
+
// The generated eslint.config.mjs explains why; the README's table is where someone reads what to type.
|
|
412
|
+
scriptHelp: {
|
|
413
|
+
'lint:fix': 'apply what it can — then run typecheck, see eslint.config.mjs'
|
|
433
414
|
}
|
|
434
415
|
};
|
|
435
416
|
const BIOME = {
|
|
@@ -470,13 +451,12 @@ function linterFeature(linter) {
|
|
|
470
451
|
;// CONCATENATED MODULE: ./src/features/styling.ts
|
|
471
452
|
|
|
472
453
|
/**
|
|
473
|
-
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
* in utilities.
|
|
454
|
+
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages, plus an
|
|
455
|
+
* overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts postcss-loader in front
|
|
456
|
+
* of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten in utilities.
|
|
477
457
|
*
|
|
478
|
-
* `postcss`
|
|
479
|
-
*
|
|
458
|
+
* `postcss` is the app's dependency rather than the framework's: rshono compiles CSS natively, so an app that
|
|
459
|
+
* wants no plugin chain installs none.
|
|
480
460
|
*/ const TAILWIND = {
|
|
481
461
|
id: 'tailwind',
|
|
482
462
|
overlays: [
|
|
@@ -499,12 +479,11 @@ function stylingFeature(styling) {
|
|
|
499
479
|
|
|
500
480
|
|
|
501
481
|
/**
|
|
502
|
-
* The features a set of answers selects, in application order — so an overlay listed later wins a file
|
|
503
|
-
*
|
|
504
|
-
*
|
|
482
|
+
* The features a set of answers selects, in application order — so an overlay listed later wins a file both
|
|
483
|
+
* ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome is both formatter and
|
|
484
|
+
* linter) without contributing twice.
|
|
505
485
|
*
|
|
506
|
-
* `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled
|
|
507
|
-
* CLI; nothing else here depends on which package manager the app is for.
|
|
486
|
+
* `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled CLI.
|
|
508
487
|
*/ function selectFeatures(answers, pm) {
|
|
509
488
|
const selected = [
|
|
510
489
|
deployFeature(answers.deploy, pm),
|
|
@@ -526,9 +505,8 @@ function stylingFeature(styling) {
|
|
|
526
505
|
;// CONCATENATED MODULE: ./src/options.ts
|
|
527
506
|
|
|
528
507
|
/*
|
|
529
|
-
* The names each option accepts, spelled once
|
|
530
|
-
*
|
|
531
|
-
* — so a name added here reaches all three without a second list to remember.
|
|
508
|
+
* The names each option accepts, spelled once: the types below are derived from them, the CLI validates its
|
|
509
|
+
* flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them.
|
|
532
510
|
*/ const FORMATTER_NAMES = [
|
|
533
511
|
'prettier',
|
|
534
512
|
'biome',
|
|
@@ -589,14 +567,13 @@ const QUALITY_PRESETS = [
|
|
|
589
567
|
}
|
|
590
568
|
];
|
|
591
569
|
/**
|
|
592
|
-
* Turns whatever the user typed into a name npm will accept, or
|
|
593
|
-
*
|
|
594
|
-
* `my_app`); a scoped name is kept intact, since `@scope/name` is legal.
|
|
570
|
+
* Turns whatever the user typed into a name npm will accept, or `null` when nothing usable is left.
|
|
571
|
+
* Lowercasing and replacing runs of invalid characters covers the ordinary cases (`My App`, `my_app`).
|
|
595
572
|
*/ function toPackageName(input) {
|
|
596
573
|
const trimmed = input.trim().replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
597
574
|
if (!trimmed || trimmed === '.') return null;
|
|
598
|
-
// A scoped name is a name, not a path: `@scope/pkg` stays whole
|
|
599
|
-
//
|
|
575
|
+
// A scoped name is a name, not a path: `@scope/pkg` stays whole. Anything else is a path, whose last
|
|
576
|
+
// segment names the project.
|
|
600
577
|
const scoped = /^@[^\\/]+[\\/][^\\/]+$/.test(trimmed);
|
|
601
578
|
const base = scoped ? trimmed : trimmed.split(/[\\/]/).filter(Boolean).pop() ?? '';
|
|
602
579
|
if (!base) return null;
|
|
@@ -630,15 +607,11 @@ function sorted(record) {
|
|
|
630
607
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
631
608
|
}
|
|
632
609
|
/**
|
|
633
|
-
* pnpm's settings for the new app,
|
|
634
|
-
* means there is nothing to say, so no file is written.
|
|
635
|
-
*
|
|
636
|
-
* It exists for one field, `allowBuilds`. pnpm fails an install — and every `pnpm dev` after it —
|
|
637
|
-
* until the project has said whether a dependency's install script should run. Nothing rshono itself
|
|
638
|
-
* installs has one, so most apps get no file; the packages that do (wrangler's esbuild and workerd)
|
|
639
|
-
* declare their answer on the feature that brings them.
|
|
610
|
+
* pnpm's settings for the new app, or `null` when no feature has anything to put in them.
|
|
640
611
|
*
|
|
641
|
-
*
|
|
612
|
+
* It exists for one field, `allowBuilds`: pnpm fails an install until the project has said whether a
|
|
613
|
+
* dependency's install script should run. Nothing rshono installs has one, so most apps get no file. A file
|
|
614
|
+
* rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
642
615
|
*/ function buildPnpmSettings(features) {
|
|
643
616
|
const allowBuilds = {};
|
|
644
617
|
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
@@ -654,10 +627,8 @@ function sorted(record) {
|
|
|
654
627
|
].join('\n');
|
|
655
628
|
}
|
|
656
629
|
/**
|
|
657
|
-
* Assembles `package.json` from the answers and whatever the selected features contribute.
|
|
658
|
-
*
|
|
659
|
-
* Dependencies are sorted by name and scripts keep the order {@link buildScripts} gives them — so two runs
|
|
660
|
-
* with the same answers produce byte-identical output, which is what makes the manifest snapshot-testable.
|
|
630
|
+
* Assembles `package.json` from the answers and whatever the selected features contribute. Dependencies are
|
|
631
|
+
* sorted and scripts keep {@link buildScripts}'s order, so two runs with the same answers are byte-identical.
|
|
661
632
|
*/ function buildPackageJson(answers, features, pm) {
|
|
662
633
|
const dependencies = {
|
|
663
634
|
'@rshono/core': RSHONO_RANGE,
|
|
@@ -687,8 +658,8 @@ function sorted(record) {
|
|
|
687
658
|
dependencies: sorted(dependencies),
|
|
688
659
|
devDependencies: sorted(devDependencies)
|
|
689
660
|
};
|
|
690
|
-
// Only when the environment told us the exact version:
|
|
691
|
-
//
|
|
661
|
+
// Only when the environment told us the exact version: this field pins the tool for Corepack, and a guess is
|
|
662
|
+
// worse than leaving it out.
|
|
692
663
|
if (pm.version) manifest.packageManager = `${pm.name}@${pm.version}`;
|
|
693
664
|
const ordered = Object.fromEntries(FIELD_ORDER.filter((field)=>field in manifest).map((field)=>[
|
|
694
665
|
field,
|
|
@@ -700,25 +671,22 @@ function sorted(record) {
|
|
|
700
671
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
701
672
|
|
|
702
673
|
/**
|
|
703
|
-
* `{{NAME}}`,
|
|
704
|
-
*
|
|
705
|
-
* matching. `{{…}}` means nothing to any format these templates are written in.
|
|
674
|
+
* `{{NAME}}`, not `__NAME__`: in markdown the latter is strong emphasis, so Prettier rewrites it to `**NAME**`
|
|
675
|
+
* and the token stops matching. `{{…}}` means nothing to any format these templates are written in.
|
|
706
676
|
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
707
677
|
function tokensFor(answers, features, pm) {
|
|
708
678
|
return {
|
|
709
679
|
'{{PROJECT_NAME}}': answers.packageName,
|
|
710
680
|
'{{DEPLOY_TARGET}}': answers.deploy,
|
|
711
|
-
//
|
|
712
|
-
// actually got: its command table, the one command that ships it, and what its platform asks for.
|
|
681
|
+
// From the features rather than the answers, because all three are about the scripts the app actually got.
|
|
713
682
|
'{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
|
|
714
683
|
'{{DEPLOY_STEP}}': deployStep(features, pm),
|
|
715
684
|
'{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
|
|
716
685
|
};
|
|
717
686
|
}
|
|
718
687
|
/**
|
|
719
|
-
* Substitutes tokens, and throws on one it
|
|
720
|
-
*
|
|
721
|
-
* catch.
|
|
688
|
+
* Substitutes tokens, and throws on one it does not know — a typo in a template would otherwise ship a literal
|
|
689
|
+
* `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would catch.
|
|
722
690
|
*/ function render(contents, tokens, source) {
|
|
723
691
|
return contents.replace(TOKEN_PATTERN, (token)=>{
|
|
724
692
|
const value = tokens[token];
|
|
@@ -748,11 +716,9 @@ function readTemplateDir(dir) {
|
|
|
748
716
|
return files;
|
|
749
717
|
}
|
|
750
718
|
/**
|
|
751
|
-
* `_gitignore` → `.gitignore`, and so on for every dotfile
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
* would exist in the repo, pass every local test, and be missing from the package everybody installs.
|
|
755
|
-
* Applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
719
|
+
* `_gitignore` → `.gitignore`, and so on for every dotfile: npm strips a literal `.gitignore` out of a
|
|
720
|
+
* published tarball, so a template that contained one would pass every local test and be missing from the
|
|
721
|
+
* package everybody installs. The basename only, so `templates/_x/y.ts` is left alone.
|
|
756
722
|
*/ function undotted(path) {
|
|
757
723
|
const segments = path.split(posix.sep);
|
|
758
724
|
const name = segments.pop();
|
|
@@ -761,19 +727,16 @@ function readTemplateDir(dir) {
|
|
|
761
727
|
name.startsWith('_') ? `.${name.slice(1)}` : name
|
|
762
728
|
].join(posix.sep);
|
|
763
729
|
}
|
|
764
|
-
/**
|
|
765
|
-
* A feature's `.gitignore` lines, appended under a heading naming it — so somebody reading the file six
|
|
766
|
-
* months later can tell why `.wrangler/` is in there.
|
|
767
|
-
*/ function appendGitignore(existing, features) {
|
|
730
|
+
/** A feature's `.gitignore` lines, under a heading naming it — so a reader can tell why `.wrangler/` is there. */ function appendGitignore(existing, features) {
|
|
768
731
|
const additions = features.filter((feature)=>feature.gitignore?.length);
|
|
769
732
|
if (additions.length === 0) return existing;
|
|
770
733
|
const blocks = additions.map((feature)=>`\n# ${feature.id}\n${feature.gitignore.join('\n')}\n`);
|
|
771
734
|
return existing + blocks.join('');
|
|
772
735
|
}
|
|
773
736
|
/**
|
|
774
|
-
* Turns answers into the exact set of files to write, without touching the target directory
|
|
775
|
-
* decisions
|
|
776
|
-
*
|
|
737
|
+
* Turns answers into the exact set of files to write, without touching the target directory. Separating the
|
|
738
|
+
* decisions from the I/O is what lets a test assert on the whole matrix of answers, and makes `--dry-run` the
|
|
739
|
+
* same code path minus the last step.
|
|
777
740
|
*/ function plan_plan(answers, pm) {
|
|
778
741
|
const features = selectFeatures(answers, pm);
|
|
779
742
|
const tokens = tokensFor(answers, features, pm);
|
|
@@ -847,11 +810,9 @@ function packageManager(name, version) {
|
|
|
847
810
|
};
|
|
848
811
|
}
|
|
849
812
|
/**
|
|
850
|
-
* Which package manager invoked us.
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
* Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
813
|
+
* Which package manager invoked us. All four set `npm_config_user_agent` on the process they spawn — `pnpm/11.9.0
|
|
814
|
+
* npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm project without asking, exact
|
|
815
|
+
* version included. Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
855
816
|
*/ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
856
817
|
const [spec] = (userAgent ?? '').split(' ');
|
|
857
818
|
const [name, version] = (spec ?? '').split('/');
|
|
@@ -859,9 +820,8 @@ function packageManager(name, version) {
|
|
|
859
820
|
return packageManager('npm');
|
|
860
821
|
}
|
|
861
822
|
/**
|
|
862
|
-
* Runs the install, streaming its output. `shell: true` on Windows
|
|
863
|
-
*
|
|
864
|
-
* are all fixed strings from the tables above, never anything the user typed.
|
|
823
|
+
* Runs the install, streaming its output. `shell: true` on Windows, where npm, pnpm and yarn are `.cmd` shims
|
|
824
|
+
* `spawn` cannot execute directly — safe because every argument is a fixed string from the tables above.
|
|
865
825
|
*/ function runInstall(pm, cwd) {
|
|
866
826
|
return run(pm, pm.install, cwd);
|
|
867
827
|
}
|
|
@@ -877,10 +837,7 @@ function run(pm, args, cwd) {
|
|
|
877
837
|
;// CONCATENATED MODULE: ./src/write.ts
|
|
878
838
|
|
|
879
839
|
|
|
880
|
-
/**
|
|
881
|
-
* Files that do not make a directory "occupied". A user who ran `git init` or opened the folder in an
|
|
882
|
-
* editor before scaffolding has not put anything in it that we would overwrite.
|
|
883
|
-
*/ const IGNORED_ENTRIES = new Set([
|
|
840
|
+
/** Files that do not make a directory "occupied": a `git init` or an editor has put nothing there to overwrite. */ const IGNORED_ENTRIES = new Set([
|
|
884
841
|
'.git',
|
|
885
842
|
'.DS_Store',
|
|
886
843
|
'.idea',
|
|
@@ -888,12 +845,9 @@ function run(pm, args, cwd) {
|
|
|
888
845
|
'Thumbs.db'
|
|
889
846
|
]);
|
|
890
847
|
/**
|
|
891
|
-
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind —
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory throws
|
|
895
|
-
* rather than reporting an empty list, since `--force` should not write into one either — otherwise
|
|
896
|
-
* `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
848
|
+
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind — which is
|
|
849
|
+
* what decides whether scaffolding into it is safe. A path that does not exist yet is no conflict; one that
|
|
850
|
+
* exists and is not a directory throws, since `--force` should not write into it either.
|
|
897
851
|
*/ function conflictingEntries(dir) {
|
|
898
852
|
const stats = statSync(dir, {
|
|
899
853
|
throwIfNoEntry: false
|
|
@@ -903,8 +857,8 @@ function run(pm, args, cwd) {
|
|
|
903
857
|
return readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
|
|
904
858
|
}
|
|
905
859
|
/**
|
|
906
|
-
* Writes the plan
|
|
907
|
-
*
|
|
860
|
+
* Writes the plan, creating directories as needed and keeping the plan's own ordering — so a failure part-way
|
|
861
|
+
* through leaves something a person can make sense of.
|
|
908
862
|
*/ function writePlan(plan, targetDir) {
|
|
909
863
|
mkdirSync(targetDir, {
|
|
910
864
|
recursive: true
|
package/dist/cli.mjs
CHANGED
|
@@ -2906,8 +2906,8 @@ function hasGit(cwd) {
|
|
|
2906
2906
|
], cwd).ok;
|
|
2907
2907
|
}
|
|
2908
2908
|
/**
|
|
2909
|
-
* Whether the new project already sits inside somebody's repository —
|
|
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.14';
|
|
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
|
},
|
|
@@ -3325,8 +3299,9 @@ const QUALITY_PRESETS = [
|
|
|
3325
3299
|
],
|
|
3326
3300
|
platformSetup: [
|
|
3327
3301
|
`There is no settings page here; the upload is yours to script. A job needs \`${pm.name} install && ${build}\`,`,
|
|
3328
|
-
'then the function package: `dist
|
|
3329
|
-
'bundle
|
|
3302
|
+
'then the function package: `dist/` and nothing else, with the handler at `dist/server/main.mjs`. Your',
|
|
3303
|
+
'dependencies are compiled into the bundle for this target, so no `node_modules` is uploaded — which also',
|
|
3304
|
+
'means a native addon fails the build here rather than the deploy.'
|
|
3330
3305
|
].join('\n')
|
|
3331
3306
|
}
|
|
3332
3307
|
};
|
|
@@ -3338,14 +3313,11 @@ function deployFeature(target, pm) {
|
|
|
3338
3313
|
;// CONCATENATED MODULE: ./src/features/quality.ts
|
|
3339
3314
|
|
|
3340
3315
|
/**
|
|
3341
|
-
* The formatter and linter features. Biome answers to both slots and appears once
|
|
3342
|
-
* deduplicates by `id
|
|
3343
|
-
* dependency and one pair of scripts.
|
|
3316
|
+
* The formatter and linter features. Biome answers to both slots and appears once, because `selectFeatures`
|
|
3317
|
+
* deduplicates by `id`.
|
|
3344
3318
|
*
|
|
3345
|
-
*
|
|
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.
|
|
3319
|
+
* Each brings a pair of scripts, since the writing half and the CI half want different exit codes: `format`
|
|
3320
|
+
* rewrites and `format:check` fails, `lint:fix` rewrites and `lint` fails. Biome's `check` is both in one pass.
|
|
3349
3321
|
*/ const PRETTIER = {
|
|
3350
3322
|
id: 'prettier',
|
|
3351
3323
|
overlays: [
|
|
@@ -3386,11 +3358,10 @@ const OXLINT = {
|
|
|
3386
3358
|
}
|
|
3387
3359
|
};
|
|
3388
3360
|
/**
|
|
3389
|
-
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
3390
|
-
*
|
|
3391
|
-
*
|
|
3392
|
-
*
|
|
3393
|
-
* program rather than linting file by file.
|
|
3361
|
+
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
|
|
3362
|
+
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest its peer range
|
|
3363
|
+
* accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to reach for ESLint at all —
|
|
3364
|
+
* so the config it ships hands the parser the whole program.
|
|
3394
3365
|
*/ const ESLINT = {
|
|
3395
3366
|
id: 'eslint',
|
|
3396
3367
|
overlays: [
|
|
@@ -3406,6 +3377,12 @@ const OXLINT = {
|
|
|
3406
3377
|
scripts: {
|
|
3407
3378
|
lint: 'eslint .',
|
|
3408
3379
|
'lint:fix': 'eslint . --fix'
|
|
3380
|
+
},
|
|
3381
|
+
// The pin is invisible from the command line, and its consequence is not: the rules reason about the
|
|
3382
|
+
// program through an older compiler than the one that builds it, so a fix can produce code `tsc` rejects.
|
|
3383
|
+
// The generated eslint.config.mjs explains why; the README's table is where someone reads what to type.
|
|
3384
|
+
scriptHelp: {
|
|
3385
|
+
'lint:fix': 'apply what it can — then run typecheck, see eslint.config.mjs'
|
|
3409
3386
|
}
|
|
3410
3387
|
};
|
|
3411
3388
|
const BIOME = {
|
|
@@ -3446,13 +3423,12 @@ function linterFeature(linter) {
|
|
|
3446
3423
|
;// CONCATENATED MODULE: ./src/features/styling.ts
|
|
3447
3424
|
|
|
3448
3425
|
/**
|
|
3449
|
-
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
3450
|
-
*
|
|
3451
|
-
*
|
|
3452
|
-
* in utilities.
|
|
3426
|
+
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages, plus an
|
|
3427
|
+
* overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts postcss-loader in front
|
|
3428
|
+
* of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten in utilities.
|
|
3453
3429
|
*
|
|
3454
|
-
* `postcss`
|
|
3455
|
-
*
|
|
3430
|
+
* `postcss` is the app's dependency rather than the framework's: rshono compiles CSS natively, so an app that
|
|
3431
|
+
* wants no plugin chain installs none.
|
|
3456
3432
|
*/ const TAILWIND = {
|
|
3457
3433
|
id: 'tailwind',
|
|
3458
3434
|
overlays: [
|
|
@@ -3475,12 +3451,11 @@ function stylingFeature(styling) {
|
|
|
3475
3451
|
|
|
3476
3452
|
|
|
3477
3453
|
/**
|
|
3478
|
-
* The features a set of answers selects, in application order — so an overlay listed later wins a file
|
|
3479
|
-
*
|
|
3480
|
-
*
|
|
3454
|
+
* The features a set of answers selects, in application order — so an overlay listed later wins a file both
|
|
3455
|
+
* ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome is both formatter and
|
|
3456
|
+
* linter) without contributing twice.
|
|
3481
3457
|
*
|
|
3482
|
-
* `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled
|
|
3483
|
-
* CLI; nothing else here depends on which package manager the app is for.
|
|
3458
|
+
* `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled CLI.
|
|
3484
3459
|
*/ function selectFeatures(answers, pm) {
|
|
3485
3460
|
const selected = [
|
|
3486
3461
|
deployFeature(answers.deploy, pm),
|
|
@@ -3517,15 +3492,11 @@ function sorted(record) {
|
|
|
3517
3492
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
3518
3493
|
}
|
|
3519
3494
|
/**
|
|
3520
|
-
* pnpm's settings for the new app,
|
|
3521
|
-
* means there is nothing to say, so no file is written.
|
|
3522
|
-
*
|
|
3523
|
-
* It exists for one field, `allowBuilds`. pnpm fails an install — and every `pnpm dev` after it —
|
|
3524
|
-
* until the project has said whether a dependency's install script should run. Nothing rshono itself
|
|
3525
|
-
* installs has one, so most apps get no file; the packages that do (wrangler's esbuild and workerd)
|
|
3526
|
-
* declare their answer on the feature that brings them.
|
|
3495
|
+
* pnpm's settings for the new app, or `null` when no feature has anything to put in them.
|
|
3527
3496
|
*
|
|
3528
|
-
*
|
|
3497
|
+
* It exists for one field, `allowBuilds`: pnpm fails an install until the project has said whether a
|
|
3498
|
+
* dependency's install script should run. Nothing rshono installs has one, so most apps get no file. A file
|
|
3499
|
+
* rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
3529
3500
|
*/ function buildPnpmSettings(features) {
|
|
3530
3501
|
const allowBuilds = {};
|
|
3531
3502
|
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
@@ -3541,10 +3512,8 @@ function sorted(record) {
|
|
|
3541
3512
|
].join('\n');
|
|
3542
3513
|
}
|
|
3543
3514
|
/**
|
|
3544
|
-
* Assembles `package.json` from the answers and whatever the selected features contribute.
|
|
3545
|
-
*
|
|
3546
|
-
* Dependencies are sorted by name and scripts keep the order {@link buildScripts} gives them — so two runs
|
|
3547
|
-
* with the same answers produce byte-identical output, which is what makes the manifest snapshot-testable.
|
|
3515
|
+
* Assembles `package.json` from the answers and whatever the selected features contribute. Dependencies are
|
|
3516
|
+
* sorted and scripts keep {@link buildScripts}'s order, so two runs with the same answers are byte-identical.
|
|
3548
3517
|
*/ function buildPackageJson(answers, features, pm) {
|
|
3549
3518
|
const dependencies = {
|
|
3550
3519
|
'@rshono/core': RSHONO_RANGE,
|
|
@@ -3574,8 +3543,8 @@ function sorted(record) {
|
|
|
3574
3543
|
dependencies: sorted(dependencies),
|
|
3575
3544
|
devDependencies: sorted(devDependencies)
|
|
3576
3545
|
};
|
|
3577
|
-
// Only when the environment told us the exact version:
|
|
3578
|
-
//
|
|
3546
|
+
// Only when the environment told us the exact version: this field pins the tool for Corepack, and a guess is
|
|
3547
|
+
// worse than leaving it out.
|
|
3579
3548
|
if (pm.version) manifest.packageManager = `${pm.name}@${pm.version}`;
|
|
3580
3549
|
const ordered = Object.fromEntries(FIELD_ORDER.filter((field)=>field in manifest).map((field)=>[
|
|
3581
3550
|
field,
|
|
@@ -3587,25 +3556,22 @@ function sorted(record) {
|
|
|
3587
3556
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
3588
3557
|
|
|
3589
3558
|
/**
|
|
3590
|
-
* `{{NAME}}`,
|
|
3591
|
-
*
|
|
3592
|
-
* matching. `{{…}}` means nothing to any format these templates are written in.
|
|
3559
|
+
* `{{NAME}}`, not `__NAME__`: in markdown the latter is strong emphasis, so Prettier rewrites it to `**NAME**`
|
|
3560
|
+
* and the token stops matching. `{{…}}` means nothing to any format these templates are written in.
|
|
3593
3561
|
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
3594
3562
|
function tokensFor(answers, features, pm) {
|
|
3595
3563
|
return {
|
|
3596
3564
|
'{{PROJECT_NAME}}': answers.packageName,
|
|
3597
3565
|
'{{DEPLOY_TARGET}}': answers.deploy,
|
|
3598
|
-
//
|
|
3599
|
-
// actually got: its command table, the one command that ships it, and what its platform asks for.
|
|
3566
|
+
// From the features rather than the answers, because all three are about the scripts the app actually got.
|
|
3600
3567
|
'{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
|
|
3601
3568
|
'{{DEPLOY_STEP}}': deployStep(features, pm),
|
|
3602
3569
|
'{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
|
|
3603
3570
|
};
|
|
3604
3571
|
}
|
|
3605
3572
|
/**
|
|
3606
|
-
* Substitutes tokens, and throws on one it
|
|
3607
|
-
*
|
|
3608
|
-
* catch.
|
|
3573
|
+
* Substitutes tokens, and throws on one it does not know — a typo in a template would otherwise ship a literal
|
|
3574
|
+
* `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would catch.
|
|
3609
3575
|
*/ function render(contents, tokens, source) {
|
|
3610
3576
|
return contents.replace(TOKEN_PATTERN, (token)=>{
|
|
3611
3577
|
const value = tokens[token];
|
|
@@ -3635,11 +3601,9 @@ function readTemplateDir(dir) {
|
|
|
3635
3601
|
return files;
|
|
3636
3602
|
}
|
|
3637
3603
|
/**
|
|
3638
|
-
* `_gitignore` → `.gitignore`, and so on for every dotfile
|
|
3639
|
-
*
|
|
3640
|
-
*
|
|
3641
|
-
* would exist in the repo, pass every local test, and be missing from the package everybody installs.
|
|
3642
|
-
* Applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
3604
|
+
* `_gitignore` → `.gitignore`, and so on for every dotfile: npm strips a literal `.gitignore` out of a
|
|
3605
|
+
* published tarball, so a template that contained one would pass every local test and be missing from the
|
|
3606
|
+
* package everybody installs. The basename only, so `templates/_x/y.ts` is left alone.
|
|
3643
3607
|
*/ function undotted(path) {
|
|
3644
3608
|
const segments = path.split(posix.sep);
|
|
3645
3609
|
const name = segments.pop();
|
|
@@ -3648,19 +3612,16 @@ function readTemplateDir(dir) {
|
|
|
3648
3612
|
name.startsWith('_') ? `.${name.slice(1)}` : name
|
|
3649
3613
|
].join(posix.sep);
|
|
3650
3614
|
}
|
|
3651
|
-
/**
|
|
3652
|
-
* A feature's `.gitignore` lines, appended under a heading naming it — so somebody reading the file six
|
|
3653
|
-
* months later can tell why `.wrangler/` is in there.
|
|
3654
|
-
*/ function appendGitignore(existing, features) {
|
|
3615
|
+
/** A feature's `.gitignore` lines, under a heading naming it — so a reader can tell why `.wrangler/` is there. */ function appendGitignore(existing, features) {
|
|
3655
3616
|
const additions = features.filter((feature)=>feature.gitignore?.length);
|
|
3656
3617
|
if (additions.length === 0) return existing;
|
|
3657
3618
|
const blocks = additions.map((feature)=>`\n# ${feature.id}\n${feature.gitignore.join('\n')}\n`);
|
|
3658
3619
|
return existing + blocks.join('');
|
|
3659
3620
|
}
|
|
3660
3621
|
/**
|
|
3661
|
-
* Turns answers into the exact set of files to write, without touching the target directory
|
|
3662
|
-
* decisions
|
|
3663
|
-
*
|
|
3622
|
+
* Turns answers into the exact set of files to write, without touching the target directory. Separating the
|
|
3623
|
+
* decisions from the I/O is what lets a test assert on the whole matrix of answers, and makes `--dry-run` the
|
|
3624
|
+
* same code path minus the last step.
|
|
3664
3625
|
*/ function plan_plan(answers, pm) {
|
|
3665
3626
|
const features = selectFeatures(answers, pm);
|
|
3666
3627
|
const tokens = tokensFor(answers, features, pm);
|
|
@@ -3732,11 +3693,9 @@ function packageManager(name, version) {
|
|
|
3732
3693
|
};
|
|
3733
3694
|
}
|
|
3734
3695
|
/**
|
|
3735
|
-
* Which package manager invoked us.
|
|
3736
|
-
*
|
|
3737
|
-
*
|
|
3738
|
-
*
|
|
3739
|
-
* Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
3696
|
+
* Which package manager invoked us. All four set `npm_config_user_agent` on the process they spawn — `pnpm/11.9.0
|
|
3697
|
+
* npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm project without asking, exact
|
|
3698
|
+
* version included. Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
3740
3699
|
*/ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
3741
3700
|
const [spec] = (userAgent ?? '').split(' ');
|
|
3742
3701
|
const [name, version] = (spec ?? '').split('/');
|
|
@@ -3744,9 +3703,8 @@ function packageManager(name, version) {
|
|
|
3744
3703
|
return packageManager('npm');
|
|
3745
3704
|
}
|
|
3746
3705
|
/**
|
|
3747
|
-
* Runs the install, streaming its output. `shell: true` on Windows
|
|
3748
|
-
*
|
|
3749
|
-
* are all fixed strings from the tables above, never anything the user typed.
|
|
3706
|
+
* Runs the install, streaming its output. `shell: true` on Windows, where npm, pnpm and yarn are `.cmd` shims
|
|
3707
|
+
* `spawn` cannot execute directly — safe because every argument is a fixed string from the tables above.
|
|
3750
3708
|
*/ function runInstall(pm, cwd) {
|
|
3751
3709
|
return run(pm, pm.install, cwd);
|
|
3752
3710
|
}
|
|
@@ -3764,9 +3722,8 @@ function run(pm, args, cwd) {
|
|
|
3764
3722
|
|
|
3765
3723
|
|
|
3766
3724
|
/**
|
|
3767
|
-
* The two halves of the closing line, in the words of the app's own scripts: what produces something
|
|
3768
|
-
*
|
|
3769
|
-
* which is a sentence about the platform rather than something to type.
|
|
3725
|
+
* The two halves of the closing line, in the words of the app's own scripts: what produces something shippable,
|
|
3726
|
+
* and where it goes from there. A target with no command to give falls back to the framework's hint.
|
|
3770
3727
|
*/ function productionSteps(answers, plan, pm) {
|
|
3771
3728
|
const scripts = buildScripts(plan.features);
|
|
3772
3729
|
const check = scripts.preview ? `${invoke(pm, 'preview')} to run the production build` : `${pm.run} build`;
|
|
@@ -3774,8 +3731,8 @@ function run(pm, args, cwd) {
|
|
|
3774
3731
|
return `${check}, and ${ship}`;
|
|
3775
3732
|
}
|
|
3776
3733
|
/**
|
|
3777
|
-
* What to do next, in the order to do it — the last thing the user reads
|
|
3778
|
-
*
|
|
3734
|
+
* What to do next, in the order to do it — the last thing the user reads. `installed` decides whether the install
|
|
3735
|
+
* step is still theirs.
|
|
3779
3736
|
*/ function nextSteps(answers, plan, pm, options) {
|
|
3780
3737
|
const steps = [];
|
|
3781
3738
|
if (options.directory !== '.') steps.push(`cd ${options.directory}`);
|
|
@@ -3810,10 +3767,7 @@ function run(pm, args, cwd) {
|
|
|
3810
3767
|
;// CONCATENATED MODULE: ./src/write.ts
|
|
3811
3768
|
|
|
3812
3769
|
|
|
3813
|
-
/**
|
|
3814
|
-
* Files that do not make a directory "occupied". A user who ran `git init` or opened the folder in an
|
|
3815
|
-
* editor before scaffolding has not put anything in it that we would overwrite.
|
|
3816
|
-
*/ const IGNORED_ENTRIES = new Set([
|
|
3770
|
+
/** Files that do not make a directory "occupied": a `git init` or an editor has put nothing there to overwrite. */ const IGNORED_ENTRIES = new Set([
|
|
3817
3771
|
'.git',
|
|
3818
3772
|
'.DS_Store',
|
|
3819
3773
|
'.idea',
|
|
@@ -3821,12 +3775,9 @@ function run(pm, args, cwd) {
|
|
|
3821
3775
|
'Thumbs.db'
|
|
3822
3776
|
]);
|
|
3823
3777
|
/**
|
|
3824
|
-
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind —
|
|
3825
|
-
*
|
|
3826
|
-
*
|
|
3827
|
-
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory throws
|
|
3828
|
-
* rather than reporting an empty list, since `--force` should not write into one either — otherwise
|
|
3829
|
-
* `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
3778
|
+
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind — which is
|
|
3779
|
+
* what decides whether scaffolding into it is safe. A path that does not exist yet is no conflict; one that
|
|
3780
|
+
* exists and is not a directory throws, since `--force` should not write into it either.
|
|
3830
3781
|
*/ function conflictingEntries(dir) {
|
|
3831
3782
|
const stats = statSync(dir, {
|
|
3832
3783
|
throwIfNoEntry: false
|
|
@@ -3836,8 +3787,8 @@ function run(pm, args, cwd) {
|
|
|
3836
3787
|
return external_node_fs_readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
|
|
3837
3788
|
}
|
|
3838
3789
|
/**
|
|
3839
|
-
* Writes the plan
|
|
3840
|
-
*
|
|
3790
|
+
* Writes the plan, creating directories as needed and keeping the plan's own ordering — so a failure part-way
|
|
3791
|
+
* through leaves something a person can make sense of.
|
|
3841
3792
|
*/ function writePlan(plan, targetDir) {
|
|
3842
3793
|
mkdirSync(targetDir, {
|
|
3843
3794
|
recursive: true
|
|
@@ -3969,10 +3920,9 @@ function fail(message) {
|
|
|
3969
3920
|
async function main() {
|
|
3970
3921
|
const { values, positionals } = parse();
|
|
3971
3922
|
if (values.help) return console.log(HELP);
|
|
3972
|
-
if (values.version) return console.log("1.0.0-rc.
|
|
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.
|
|
3923
|
+
if (values.version) return console.log("1.0.0-rc.14");
|
|
3924
|
+
// A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both streams have to
|
|
3925
|
+
// be a terminal: the prompts draw on stdout but read from stdin.
|
|
3976
3926
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
|
|
3977
3927
|
const pmFlag = oneOf(values.pm, PACKAGE_MANAGERS, 'pm');
|
|
3978
3928
|
const pm = pmFlag ? packageManager(pmFlag) : detectPackageManager();
|
|
@@ -3983,8 +3933,7 @@ async function main() {
|
|
|
3983
3933
|
const tailwindFlag = tristate(values.tailwind, values['no-tailwind'], 'tailwind');
|
|
3984
3934
|
const installFlag = tristate(values.install, values['no-install'], 'install');
|
|
3985
3935
|
const gitFlag = tristate(values.git, values['no-git'], 'git');
|
|
3986
|
-
// The framework version, not this package's: it is the one the app
|
|
3987
|
-
// worth reading here. `--version` reports create-rshono's own.
|
|
3936
|
+
// The framework version, not this package's: it is the one the app gets pinned to. `--version` reports ours.
|
|
3988
3937
|
intro(`create-rshono · rshono ${RSHONO_VERSION}`);
|
|
3989
3938
|
// ── Where ───────────────────────────────────────────────────────────────────────────────────────
|
|
3990
3939
|
let directory = positionals[0];
|
|
@@ -4044,8 +3993,8 @@ async function main() {
|
|
|
4044
3993
|
]
|
|
4045
3994
|
}));
|
|
4046
3995
|
}
|
|
4047
|
-
// One question instead of two
|
|
4048
|
-
//
|
|
3996
|
+
// One question instead of two; the axes stay independent underneath, and a `--formatter` or `--linter` flag
|
|
3997
|
+
// addresses either on its own.
|
|
4049
3998
|
let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
|
|
4050
3999
|
if (!preset && !formatterFlag && !linterFlag) {
|
|
4051
4000
|
const fallback = QUALITY_PRESETS["0"];
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rshono/create",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.14",
|
|
4
4
|
"description": "Scaffold a new rshono app — Hono + Rspack + React Server Components",
|
|
5
5
|
"author": "Lasse <lasse@lassetange.com> (https://www.lassetange.com)",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"create",
|
|
9
9
|
"scaffold",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@rspack/core": "2.1.7",
|
|
47
47
|
"@types/node": "^26.1.1",
|
|
48
48
|
"typescript": "^7.0.2",
|
|
49
|
-
"@rshono/core": "1.0.0-rc.
|
|
49
|
+
"@rshono/core": "1.0.0-rc.14"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "node scripts/codegen.mjs && node scripts/build.mjs",
|
|
@@ -1,9 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint with type-aware rules, which is the reason to reach for it over a linter that only reads syntax.
|
|
3
|
+
*
|
|
4
|
+
* **This app's `typescript` is pinned below the one rshono is built with.** typescript-eslint reads the
|
|
5
|
+
* compiler API directly and accepts nothing from TypeScript 6.1 up, so `package.json` holds the newest
|
|
6
|
+
* version it takes. Your editor and `tsc` are unaffected — rshono's declarations compile the same under
|
|
7
|
+
* either — but the rules below reason about your program through the *older* compiler.
|
|
8
|
+
*
|
|
9
|
+
* The practical consequence: `lint:fix` can rewrite code that `tsc` then rejects. Run `typecheck` after it,
|
|
10
|
+
* not instead of it. When typescript-eslint widens its peer range, drop the `typescript` pin from
|
|
11
|
+
* `package.json` and this note with it.
|
|
12
|
+
*/
|
|
1
13
|
import js from '@eslint/js';
|
|
2
14
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
3
15
|
import tseslint from 'typescript-eslint';
|
|
4
16
|
|
|
5
17
|
export default tseslint.config(
|
|
6
|
-
|
|
18
|
+
// Build output, all of it generated: `dist/` from `rshono build`, `.rshono/` from `rshono dev`, and
|
|
19
|
+
// whatever the deploy target assembles beside them. Without this, linting after a dev run reports on
|
|
20
|
+
// bundles rather than on anything you wrote.
|
|
21
|
+
{ ignores: ['dist/**', '.rshono/**', '.wrangler/**', '.vercel/**', '.netlify/**'] },
|
|
7
22
|
js.configs.recommended,
|
|
8
23
|
tseslint.configs.recommendedTypeChecked,
|
|
9
24
|
reactHooks.configs.flat['recommended-latest'],
|