@rshono/create 1.0.0-rc.0 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -10
- package/dist/api.mjs +178 -47
- package/dist/cli.mjs +252 -128
- package/package.json +1 -1
- package/templates/base/README.md +5 -5
- package/templates/base/_env +1 -1
- package/templates/base/rshono.config.ts +1 -1
- package/templates/base/src/actions.ts +4 -1
- package/templates/base/src/lib/env.ts +1 -1
- package/templates/biome/biome.json +1 -1
- package/templates/biome-tailwind/biome.json +1 -1
- package/templates/eslint/eslint.config.mjs +31 -0
- package/templates/oxfmt/_oxfmtrc.json +1 -1
- package/templates/oxlint/_oxlintrc.json +1 -1
- package/templates/prettier/_prettierignore +6 -1
- package/templates/tailwind/rshono.config.ts +1 -1
package/README.md
CHANGED
|
@@ -22,14 +22,14 @@ Six, each with a default, and each answerable by a flag instead:
|
|
|
22
22
|
| Where should it go? | `my-rshono-app` | first positional argument, or `.` |
|
|
23
23
|
| Where is it deployed? | `node` | `--deploy node\|bun\|deno\|cloudflare\|vercel\|netlify\|aws-lambda` |
|
|
24
24
|
| Styling | plain CSS | `--tailwind` / `--no-tailwind` |
|
|
25
|
-
| Formatting & linting | Prettier + oxlint | `--quality prettier-oxlint\|biome\|oxc\|none`
|
|
25
|
+
| Formatting & linting | Prettier + oxlint | `--quality prettier-oxlint\|prettier-eslint\|biome\|oxc\|none` |
|
|
26
26
|
| Install dependencies? | yes | `--no-install` |
|
|
27
27
|
| Initialize git? | yes, unless nested | `--no-git` |
|
|
28
28
|
|
|
29
|
-
Plus `--formatter` and `--linter` to set either half of the quality preset on its own, `--
|
|
30
|
-
|
|
31
|
-
to
|
|
32
|
-
command in CI or from an agent:
|
|
29
|
+
Plus `--formatter` and `--linter` to set either half of the quality preset on its own, `--pm` to override
|
|
30
|
+
the package manager that was detected, `--force` to scaffold into a directory that is not empty,
|
|
31
|
+
`--dry-run` to see the file list and write nothing, and `-y` to take the defaults for everything not
|
|
32
|
+
given. **A non-interactive terminal implies `-y`**, so this is one command in CI or from an agent:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
npm create @rshono@latest my-app -y --deploy cloudflare --tailwind --quality biome
|
|
@@ -51,6 +51,7 @@ src/actions.ts a 'use server' action, called from a form that works wit
|
|
|
51
51
|
src/components/ layout, home, greet-form ('use client'), 404, 500
|
|
52
52
|
src/lib/env.ts both sides of the PUBLIC_ boundary in one place
|
|
53
53
|
src/styles.css element-level CSS, or the Tailwind entry
|
|
54
|
+
pnpm-workspace.yaml pnpm only: which dependency install scripts this app runs (none of them)
|
|
54
55
|
```
|
|
55
56
|
|
|
56
57
|
Then, if the dependencies were installed, the scaffold is run through its own formatter — so a fresh
|
|
@@ -83,10 +84,16 @@ import { plan, writePlan } from '@rshono/create';
|
|
|
83
84
|
|
|
84
85
|
## Two things worth knowing
|
|
85
86
|
|
|
86
|
-
**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
**The ESLint preset pins TypeScript 6.** Linting TypeScript with ESLint means `typescript-eslint`, which
|
|
88
|
+
reads the compiler API directly rather than through a stable interface, so its peer range is
|
|
89
|
+
`typescript >=4.8.4 <6.1.0` — below the TypeScript rshono is built and tested against. An app that chooses
|
|
90
|
+
ESLint therefore gets `typescript ~6.0.3`, the newest that range allows: `npm install` would otherwise fail
|
|
91
|
+
outright on the conflict, and forcing past it hands you a linter running against a compiler API it was never
|
|
92
|
+
built for. The framework's declarations compile identically under either version, which is what makes this
|
|
93
|
+
the app's pin and not the framework's — every other preset leaves TypeScript alone. What you trade for
|
|
94
|
+
type-aware rules is a compiler one major behind, and the JavaScript implementation rather than the native
|
|
95
|
+
one, so `typecheck` on a large app is several times slower. When upstream widens the range, the pin in
|
|
96
|
+
`features/quality.ts` is the only thing to delete.
|
|
90
97
|
|
|
91
98
|
**Tailwind is four packages, a `postcss.config.mjs` and one rule in `rshono.config.ts`.** rshono compiles
|
|
92
99
|
CSS natively and has no PostCSS in it — that is deliberate, and it means an app that wants a plugin chain
|
|
@@ -97,9 +104,14 @@ delete to go back to plain CSS.
|
|
|
97
104
|
|
|
98
105
|
```bash
|
|
99
106
|
pnpm --filter @rshono/create build # codegen, then one bundled dist/cli.mjs with no runtime deps
|
|
100
|
-
pnpm --filter @rshono/create test # the plan matrix —
|
|
107
|
+
pnpm --filter @rshono/create test # builds, then the plan matrix and the CLI — seconds, no installs
|
|
101
108
|
CREATE_RSHONO_E2E=1 pnpm --filter @rshono/create test # also: pack, install and build real apps
|
|
102
109
|
```
|
|
103
110
|
|
|
111
|
+
`plan.test.mjs` is the fast one — the whole matrix of answers in memory, no directory touched.
|
|
112
|
+
`cli.test.mjs` spawns the real bin against temp directories, which is where argument parsing and the
|
|
113
|
+
refusal to overwrite somebody's files are checked. `e2e.test.mjs` is the opt-in one, and the only one that
|
|
114
|
+
installs anything.
|
|
115
|
+
|
|
104
116
|
`@clack/prompts` (MIT) is bundled rather than depended on, so `npm create` downloads one tarball before it
|
|
105
117
|
can ask its first question.
|
package/dist/api.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {fileURLToPath as __rspack_fileURLToPath} from "node:url";
|
|
3
3
|
import {dirname as __rspack_dirname} from "node:path";
|
|
4
4
|
var __rspack_import_meta_dirname__ = __rspack_dirname(__rspack_fileURLToPath(import.meta.url));
|
|
5
|
-
import {
|
|
5
|
+
import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join, posix, sep } from "node:path";
|
|
7
7
|
import "node:child_process";
|
|
8
8
|
// The require scope
|
|
@@ -31,18 +31,19 @@ var __webpack_exports__ = {};
|
|
|
31
31
|
// EXPORTS
|
|
32
32
|
__webpack_require__.d(__webpack_exports__, {
|
|
33
33
|
bp: () => (/* reexport */ DEPLOY_TARGET_NAMES),
|
|
34
|
+
ml: () => (/* reexport */ (/* inlined export .ESLINT_TYPESCRIPT */"~6.0.3")),
|
|
35
|
+
wC: () => (/* reexport */ FORMATTER_NAMES),
|
|
34
36
|
e7: () => (/* reexport */ FRAMEWORK_DEPS),
|
|
37
|
+
At: () => (/* reexport */ LINTER_NAMES),
|
|
35
38
|
Ab: () => (/* reexport */ PACKAGE_MANAGERS),
|
|
36
39
|
pm: () => (/* reexport */ QUALITY_PRESETS),
|
|
37
40
|
s_: () => (/* reexport */ RSHONO_RANGE),
|
|
38
41
|
RM: () => (/* reexport */ TOOL_VERSIONS),
|
|
39
|
-
|
|
42
|
+
dh: () => (/* reexport */ conflictingEntries),
|
|
40
43
|
P8: () => (/* reexport */ deployHint),
|
|
41
44
|
Lv: () => (/* reexport */ detectPackageManager),
|
|
42
|
-
WL: () => (/* reexport */ inspectTarget),
|
|
43
|
-
p1: () => (/* reexport */ isDeployTarget),
|
|
44
45
|
eW: () => (/* reexport */ isValidPackageName),
|
|
45
|
-
|
|
46
|
+
VZ: () => (/* reexport */ packageManager),
|
|
46
47
|
et: () => (/* reexport */ plan_plan),
|
|
47
48
|
N7: () => (/* reexport */ selectFeatures),
|
|
48
49
|
PQ: () => (/* reexport */ toPackageName),
|
|
@@ -78,7 +79,11 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
78
79
|
|
|
79
80
|
;// CONCATENATED MODULE: ./src/generated/framework.ts
|
|
80
81
|
// GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
|
|
81
|
-
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.
|
|
82
|
+
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.2';
|
|
83
|
+
/**
|
|
84
|
+
* The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
|
|
85
|
+
* or a contributor on an older Node hears it from their package manager rather than from a stack trace.
|
|
86
|
+
*/ const NODE_ENGINE = '>=22.1.0';
|
|
82
87
|
/**
|
|
83
88
|
* The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
|
|
84
89
|
* exact — React's RSC internals are coupled across builds, and a generated app has no workspace
|
|
@@ -124,7 +129,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
124
129
|
|
|
125
130
|
;// CONCATENATED MODULE: ./src/versions.ts
|
|
126
131
|
|
|
127
|
-
|
|
132
|
+
/** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
|
|
128
133
|
/** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
|
|
129
134
|
/**
|
|
130
135
|
* Versions for the optional tooling the features can add — the one place in this package where a
|
|
@@ -145,8 +150,26 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
145
150
|
'@biomejs/biome': '^2.5.6',
|
|
146
151
|
oxlint: '^1.76.0',
|
|
147
152
|
oxfmt: '^0.61.0',
|
|
153
|
+
eslint: '^10.8.0',
|
|
154
|
+
// ESLint's own recommended JavaScript rules, which typescript-eslint layers on top of rather than
|
|
155
|
+
// replaces, and the rules of hooks — the one class of React mistake no type checker sees.
|
|
156
|
+
'@eslint/js': '^10.0.1',
|
|
157
|
+
'typescript-eslint': '^8.65.0',
|
|
158
|
+
'eslint-plugin-react-hooks': '^7.1.1',
|
|
148
159
|
wrangler: '^4.115.0'
|
|
149
160
|
};
|
|
161
|
+
/**
|
|
162
|
+
* The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
|
|
163
|
+
* {@link FRAMEWORK_DEPS}, and the reason it is spelled out here.
|
|
164
|
+
*
|
|
165
|
+
* typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface, so
|
|
166
|
+
* it accepts `typescript >=4.8.4 <6.1.0` and nothing above. `~6.0.3` is the newest that satisfies it:
|
|
167
|
+
* patch releases of 6.0, no minor. The framework itself stays on the TypeScript it is tested against —
|
|
168
|
+
* rshono's declarations compile the same under either, which is what makes this pin an app's business
|
|
169
|
+
* and not the framework's.
|
|
170
|
+
*
|
|
171
|
+
* When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
|
|
172
|
+
*/ const ESLINT_TYPESCRIPT = '~6.0.3';
|
|
150
173
|
|
|
151
174
|
;// CONCATENATED MODULE: ./src/features/deploy.ts
|
|
152
175
|
|
|
@@ -157,8 +180,11 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
157
180
|
* Deliberately thin. The framework already knows how to arrange its own output for every platform, and
|
|
158
181
|
* `rshono build` writes the one platform config that has to exist (`wrangler.jsonc`, with the
|
|
159
182
|
* `compatibility_date` of the day it ran) if the project has none. Generating a second copy here would
|
|
160
|
-
* be a copy that goes stale.
|
|
161
|
-
*
|
|
183
|
+
* be a copy that goes stale.
|
|
184
|
+
*
|
|
185
|
+
* So a target contributes only what is true of it: the command that runs or ships the build, the CLI
|
|
186
|
+
* that command needs installed locally, the directories its platform leaves behind for `.gitignore`, and
|
|
187
|
+
* a note for the step no command covers. Several contribute just one of those.
|
|
162
188
|
*/ const DEPLOY_FEATURES = {
|
|
163
189
|
// Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
|
|
164
190
|
// guess — so the target contributes only the command that runs what was built.
|
|
@@ -187,6 +213,11 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
187
213
|
devDependencies: {
|
|
188
214
|
wrangler: TOOL_VERSIONS.wrangler
|
|
189
215
|
},
|
|
216
|
+
// wrangler brings workerd, whose install script only picks the platform binary out of the optional
|
|
217
|
+
// dependency that already carries it — `workerd --version` answers without it having run.
|
|
218
|
+
allowBuilds: {
|
|
219
|
+
workerd: false
|
|
220
|
+
},
|
|
190
221
|
scripts: {
|
|
191
222
|
deploy: 'rshono build && wrangler deploy'
|
|
192
223
|
},
|
|
@@ -236,8 +267,10 @@ function deployFeature(target) {
|
|
|
236
267
|
* deduplicates by `id`, so `formatter: 'biome', linter: 'biome'` contributes one set of files, one
|
|
237
268
|
* dependency and one pair of scripts.
|
|
238
269
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
270
|
+
* A formatter brings `format:check` beside `format`, because the writing half and the CI half want
|
|
271
|
+
* different exit-code behaviour: `format` rewrites files, `format:check` fails instead. A linter brings
|
|
272
|
+
* `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
|
|
273
|
+
* one. Biome adds a `check` of its own, which is the pair of them in a single pass.
|
|
241
274
|
*/ const PRETTIER = {
|
|
242
275
|
id: 'prettier',
|
|
243
276
|
overlays: [
|
|
@@ -277,6 +310,30 @@ const OXLINT = {
|
|
|
277
310
|
'lint:fix': 'oxlint --fix'
|
|
278
311
|
}
|
|
279
312
|
};
|
|
313
|
+
/**
|
|
314
|
+
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
|
|
315
|
+
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one its
|
|
316
|
+
* peer range accepts (see {@link ESLINT_TYPESCRIPT}). Every other preset leaves TypeScript alone.
|
|
317
|
+
*
|
|
318
|
+
* The rules are type-aware, which is the reason to reach for ESLint over a syntax-only linter at all —
|
|
319
|
+
* so the config it ships hands the whole program to the parser rather than linting file by file.
|
|
320
|
+
*/ const ESLINT = {
|
|
321
|
+
id: 'eslint',
|
|
322
|
+
overlays: [
|
|
323
|
+
'eslint'
|
|
324
|
+
],
|
|
325
|
+
devDependencies: {
|
|
326
|
+
eslint: TOOL_VERSIONS.eslint,
|
|
327
|
+
'@eslint/js': TOOL_VERSIONS["@eslint/js"],
|
|
328
|
+
'typescript-eslint': TOOL_VERSIONS["typescript-eslint"],
|
|
329
|
+
'eslint-plugin-react-hooks': TOOL_VERSIONS["eslint-plugin-react-hooks"],
|
|
330
|
+
typescript: (/* inlined export .ESLINT_TYPESCRIPT */"~6.0.3")
|
|
331
|
+
},
|
|
332
|
+
scripts: {
|
|
333
|
+
lint: 'eslint .',
|
|
334
|
+
'lint:fix': 'eslint . --fix'
|
|
335
|
+
}
|
|
336
|
+
};
|
|
280
337
|
const BIOME = {
|
|
281
338
|
id: 'biome',
|
|
282
339
|
overlays: [
|
|
@@ -301,6 +358,7 @@ const FORMATTERS = {
|
|
|
301
358
|
};
|
|
302
359
|
const LINTERS = {
|
|
303
360
|
oxlint: OXLINT,
|
|
361
|
+
eslint: ESLINT,
|
|
304
362
|
biome: BIOME,
|
|
305
363
|
none: null
|
|
306
364
|
};
|
|
@@ -367,6 +425,22 @@ function stylingFeature(styling) {
|
|
|
367
425
|
|
|
368
426
|
;// CONCATENATED MODULE: ./src/options.ts
|
|
369
427
|
|
|
428
|
+
/*
|
|
429
|
+
* The names each option accepts, spelled once. The types below are derived from them, the CLI validates
|
|
430
|
+
* its flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them
|
|
431
|
+
* — so a name added here reaches all three without a second list to remember.
|
|
432
|
+
*/ const FORMATTER_NAMES = [
|
|
433
|
+
'prettier',
|
|
434
|
+
'biome',
|
|
435
|
+
'oxfmt',
|
|
436
|
+
'none'
|
|
437
|
+
];
|
|
438
|
+
const LINTER_NAMES = [
|
|
439
|
+
'oxlint',
|
|
440
|
+
'eslint',
|
|
441
|
+
'biome',
|
|
442
|
+
'none'
|
|
443
|
+
];
|
|
370
444
|
const PACKAGE_MANAGERS = [
|
|
371
445
|
'npm',
|
|
372
446
|
'pnpm',
|
|
@@ -377,9 +451,6 @@ const DEPLOY_TARGET_NAMES = DEPLOY_TARGETS.map((target)=>target.name);
|
|
|
377
451
|
function deployHint(name) {
|
|
378
452
|
return DEPLOY_TARGETS.find((target)=>target.name === name)?.hint ?? '';
|
|
379
453
|
}
|
|
380
|
-
function isDeployTarget(value) {
|
|
381
|
-
return DEPLOY_TARGET_NAMES.includes(value);
|
|
382
|
-
}
|
|
383
454
|
const QUALITY_PRESETS = [
|
|
384
455
|
{
|
|
385
456
|
id: 'prettier-oxlint',
|
|
@@ -388,6 +459,13 @@ const QUALITY_PRESETS = [
|
|
|
388
459
|
formatter: 'prettier',
|
|
389
460
|
linter: 'oxlint'
|
|
390
461
|
},
|
|
462
|
+
{
|
|
463
|
+
id: 'prettier-eslint',
|
|
464
|
+
label: 'Prettier + ESLint',
|
|
465
|
+
hint: 'type-aware rules — pins TypeScript 6, which is all typescript-eslint accepts',
|
|
466
|
+
formatter: 'prettier',
|
|
467
|
+
linter: 'eslint'
|
|
468
|
+
},
|
|
391
469
|
{
|
|
392
470
|
id: 'biome',
|
|
393
471
|
label: 'Biome',
|
|
@@ -430,11 +508,16 @@ const QUALITY_PRESETS = [
|
|
|
430
508
|
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(name) && name.length <= 214;
|
|
431
509
|
}
|
|
432
510
|
|
|
511
|
+
;// CONCATENATED MODULE: external "node:fs"
|
|
512
|
+
|
|
513
|
+
;// CONCATENATED MODULE: external "node:path"
|
|
514
|
+
|
|
433
515
|
;// CONCATENATED MODULE: ./src/pkg.ts
|
|
434
516
|
|
|
435
517
|
/**
|
|
436
|
-
* The scripts every app gets. `start` is not among them
|
|
437
|
-
*
|
|
518
|
+
* The scripts every app gets. `start` is not among them, because it means something different per
|
|
519
|
+
* platform: the three targets that run the build themselves — node, bun, deno — each contribute their
|
|
520
|
+
* own, and a platform target contributes a `deploy` instead, where its platform has one command to give.
|
|
438
521
|
*/ const BASE_SCRIPTS = {
|
|
439
522
|
dev: 'rshono dev',
|
|
440
523
|
build: 'rshono build',
|
|
@@ -454,6 +537,39 @@ const QUALITY_PRESETS = [
|
|
|
454
537
|
function sorted(record) {
|
|
455
538
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
456
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* The install script every app inherits, from the framework rather than from anything it chose. tsx
|
|
542
|
+
* reads `rshono.config.ts` through esbuild, and esbuild's script only picks the platform binary out of
|
|
543
|
+
* the optional dependency that already carries it — rshono's own repo denies it for the same reason.
|
|
544
|
+
*/ const BASE_ALLOW_BUILDS = {
|
|
545
|
+
esbuild: false
|
|
546
|
+
};
|
|
547
|
+
/**
|
|
548
|
+
* pnpm's settings for the new app — written for pnpm and for nobody else.
|
|
549
|
+
*
|
|
550
|
+
* It exists for one field. A dependency with an install script is a question pnpm will not answer on its
|
|
551
|
+
* own: it fails the install, and fails every `pnpm dev` after it, until the project has said whether the
|
|
552
|
+
* script should run. None of the ones an rshono app inherits need to (each is a native package whose
|
|
553
|
+
* binary arrives as an optional dependency), so a fresh app carries the answer rather than meeting
|
|
554
|
+
* `pnpm approve-builds` before it has rendered a page once.
|
|
555
|
+
*
|
|
556
|
+
* In this file rather than under a `pnpm` key in `package.json`, which pnpm 11 no longer reads, single-
|
|
557
|
+
* package projects included. What lands here is a decision about *this* app: a scaffolded file the app
|
|
558
|
+
* owns from then on, not something the framework reaches back into.
|
|
559
|
+
*/ function buildPnpmSettings(features) {
|
|
560
|
+
const allowBuilds = {
|
|
561
|
+
...BASE_ALLOW_BUILDS
|
|
562
|
+
};
|
|
563
|
+
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
564
|
+
return [
|
|
565
|
+
'# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
|
|
566
|
+
'# fails the install rather than skip one quietly — so anything added later belongs here too.',
|
|
567
|
+
'# `false` means the script was looked at: these ship their real binary as an optional dependency.',
|
|
568
|
+
'allowBuilds:',
|
|
569
|
+
...Object.entries(sorted(allowBuilds)).map(([name, allowed])=>` ${name}: ${allowed}`),
|
|
570
|
+
''
|
|
571
|
+
].join('\n');
|
|
572
|
+
}
|
|
457
573
|
/**
|
|
458
574
|
* Assembles `package.json` from the answers and whatever the selected features contribute.
|
|
459
575
|
*
|
|
@@ -485,10 +601,9 @@ function sorted(record) {
|
|
|
485
601
|
version: '0.1.0',
|
|
486
602
|
private: true,
|
|
487
603
|
type: 'module',
|
|
488
|
-
//
|
|
489
|
-
// Node finds out from their package manager rather than from a stack trace.
|
|
604
|
+
// Generated from the framework's own manifest, so the app's floor cannot drift below rshono's.
|
|
490
605
|
engines: {
|
|
491
|
-
node:
|
|
606
|
+
node: NODE_ENGINE
|
|
492
607
|
},
|
|
493
608
|
scripts,
|
|
494
609
|
dependencies: sorted(dependencies),
|
|
@@ -504,25 +619,25 @@ function sorted(record) {
|
|
|
504
619
|
return `${JSON.stringify(ordered, null, 2)}\n`;
|
|
505
620
|
}
|
|
506
621
|
|
|
507
|
-
;// CONCATENATED MODULE: external "node:fs"
|
|
508
|
-
|
|
509
|
-
;// CONCATENATED MODULE: external "node:path"
|
|
510
|
-
|
|
511
622
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
512
623
|
|
|
513
|
-
|
|
624
|
+
/**
|
|
625
|
+
* `{{NAME}}`, and not the `__NAME__` this used to be. A template is a real file that real tools run
|
|
626
|
+
* over, and in markdown `__NAME__` *is* strong emphasis — Prettier rewrites it to `**NAME**`, which
|
|
627
|
+
* turns a token into literal text that no substitution will ever match again. `{{…}}` means nothing to
|
|
628
|
+
* any of the formats these templates are written in.
|
|
629
|
+
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
514
630
|
function tokensFor(answers, pm) {
|
|
515
631
|
return {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
__PM_RUN__: pm.run
|
|
632
|
+
'{{PROJECT_NAME}}': answers.packageName,
|
|
633
|
+
'{{DEPLOY_TARGET}}': answers.deploy,
|
|
634
|
+
'{{DEPLOY_HINT}}': deployHint(answers.deploy),
|
|
635
|
+
'{{PM_RUN}}': pm.run
|
|
521
636
|
};
|
|
522
637
|
}
|
|
523
638
|
/**
|
|
524
639
|
* Substitutes tokens, and throws on one it doesn't know — a typo in a template would otherwise ship a
|
|
525
|
-
* literal `
|
|
640
|
+
* literal `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would
|
|
526
641
|
* catch.
|
|
527
642
|
*/ function render(contents, tokens, source) {
|
|
528
643
|
return contents.replace(TOKEN_PATTERN, (token)=>{
|
|
@@ -598,6 +713,7 @@ function readTemplateDir(dir) {
|
|
|
598
713
|
const gitignore = files.get('.gitignore');
|
|
599
714
|
if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
|
|
600
715
|
files.set('package.json', buildPackageJson(answers, features, pm));
|
|
716
|
+
if (pm.name === 'pnpm') files.set('pnpm-workspace.yaml', buildPnpmSettings(features));
|
|
601
717
|
return {
|
|
602
718
|
// Sorted, so both the write order and a test's snapshot are stable.
|
|
603
719
|
files: new Map([
|
|
@@ -612,6 +728,7 @@ function readTemplateDir(dir) {
|
|
|
612
728
|
|
|
613
729
|
;// CONCATENATED MODULE: ./src/pm.ts
|
|
614
730
|
|
|
731
|
+
|
|
615
732
|
const INSTALL = {
|
|
616
733
|
npm: [
|
|
617
734
|
'install'
|
|
@@ -631,7 +748,7 @@ const RUN = {
|
|
|
631
748
|
bun: 'bun'
|
|
632
749
|
};
|
|
633
750
|
function isKnown(name) {
|
|
634
|
-
return name
|
|
751
|
+
return PACKAGE_MANAGERS.includes(name);
|
|
635
752
|
}
|
|
636
753
|
function packageManager(name, version) {
|
|
637
754
|
return {
|
|
@@ -688,15 +805,20 @@ function run(pm, args, cwd) {
|
|
|
688
805
|
'.vscode',
|
|
689
806
|
'Thumbs.db'
|
|
690
807
|
]);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
808
|
+
/**
|
|
809
|
+
* What is already at the target path, which is what decides whether scaffolding into it is safe: the
|
|
810
|
+
* entries already there, ignoring the ones a fresh clone or an editor leaves behind.
|
|
811
|
+
*
|
|
812
|
+
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory is not
|
|
813
|
+
* something `--force` should be able to write into, so it throws rather than reporting an empty list —
|
|
814
|
+
* otherwise `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
815
|
+
*/ function conflictingEntries(dir) {
|
|
816
|
+
const stats = statSync(dir, {
|
|
817
|
+
throwIfNoEntry: false
|
|
818
|
+
});
|
|
819
|
+
if (!stats) return [];
|
|
820
|
+
if (!stats.isDirectory()) throw new Error(`${dir} already exists and is not a directory.`);
|
|
821
|
+
return readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
|
|
700
822
|
}
|
|
701
823
|
/**
|
|
702
824
|
* Writes the plan. Directories are created as needed, and files are written with the plan's own
|
|
@@ -718,7 +840,16 @@ function inspectTarget(dir) {
|
|
|
718
840
|
/**
|
|
719
841
|
* The generator without the CLI around it: answers in, the exact set of files out. This is what the
|
|
720
842
|
* test suite asserts on — the whole matrix of options, in memory, with no directory to clean up — and
|
|
721
|
-
* what another tool would call to scaffold an app itself
|
|
843
|
+
* what another tool would call to scaffold an app itself:
|
|
844
|
+
*
|
|
845
|
+
* ```ts
|
|
846
|
+
* import { packageManager, plan, writePlan } from '@rshono/create';
|
|
847
|
+
*
|
|
848
|
+
* const answers = { packageName: 'my-app', deploy: 'node', styling: 'css', formatter: 'prettier', linter: 'oxlint' };
|
|
849
|
+
* writePlan(plan(answers, packageManager('npm')), './my-app');
|
|
850
|
+
* ```
|
|
851
|
+
*
|
|
852
|
+
* `package.json` is assembled inside `plan`, so nothing here needs to be composed by hand.
|
|
722
853
|
*
|
|
723
854
|
* @packageDocumentation
|
|
724
855
|
*/
|
|
@@ -728,22 +859,22 @@ function inspectTarget(dir) {
|
|
|
728
859
|
|
|
729
860
|
|
|
730
861
|
|
|
731
|
-
|
|
732
862
|
var __webpack_exports__DEPLOY_TARGET_NAMES = __webpack_exports__.bp;
|
|
863
|
+
var __webpack_exports__ESLINT_TYPESCRIPT = __webpack_exports__.ml;
|
|
864
|
+
var __webpack_exports__FORMATTER_NAMES = __webpack_exports__.wC;
|
|
733
865
|
var __webpack_exports__FRAMEWORK_DEPS = __webpack_exports__.e7;
|
|
866
|
+
var __webpack_exports__LINTER_NAMES = __webpack_exports__.At;
|
|
734
867
|
var __webpack_exports__PACKAGE_MANAGERS = __webpack_exports__.Ab;
|
|
735
868
|
var __webpack_exports__QUALITY_PRESETS = __webpack_exports__.pm;
|
|
736
869
|
var __webpack_exports__RSHONO_RANGE = __webpack_exports__.s_;
|
|
737
870
|
var __webpack_exports__TOOL_VERSIONS = __webpack_exports__.RM;
|
|
738
|
-
var
|
|
871
|
+
var __webpack_exports__conflictingEntries = __webpack_exports__.dh;
|
|
739
872
|
var __webpack_exports__deployHint = __webpack_exports__.P8;
|
|
740
873
|
var __webpack_exports__detectPackageManager = __webpack_exports__.Lv;
|
|
741
|
-
var __webpack_exports__inspectTarget = __webpack_exports__.WL;
|
|
742
|
-
var __webpack_exports__isDeployTarget = __webpack_exports__.p1;
|
|
743
874
|
var __webpack_exports__isValidPackageName = __webpack_exports__.eW;
|
|
744
|
-
var __webpack_exports__packageManager = __webpack_exports__.
|
|
875
|
+
var __webpack_exports__packageManager = __webpack_exports__.VZ;
|
|
745
876
|
var __webpack_exports__plan = __webpack_exports__.et;
|
|
746
877
|
var __webpack_exports__selectFeatures = __webpack_exports__.N7;
|
|
747
878
|
var __webpack_exports__toPackageName = __webpack_exports__.PQ;
|
|
748
879
|
var __webpack_exports__writePlan = __webpack_exports__.pD;
|
|
749
|
-
export { __webpack_exports__DEPLOY_TARGET_NAMES as DEPLOY_TARGET_NAMES, __webpack_exports__FRAMEWORK_DEPS as FRAMEWORK_DEPS, __webpack_exports__PACKAGE_MANAGERS as PACKAGE_MANAGERS, __webpack_exports__QUALITY_PRESETS as QUALITY_PRESETS, __webpack_exports__RSHONO_RANGE as RSHONO_RANGE, __webpack_exports__TOOL_VERSIONS as TOOL_VERSIONS,
|
|
880
|
+
export { __webpack_exports__DEPLOY_TARGET_NAMES as DEPLOY_TARGET_NAMES, __webpack_exports__ESLINT_TYPESCRIPT as ESLINT_TYPESCRIPT, __webpack_exports__FORMATTER_NAMES as FORMATTER_NAMES, __webpack_exports__FRAMEWORK_DEPS as FRAMEWORK_DEPS, __webpack_exports__LINTER_NAMES as LINTER_NAMES, __webpack_exports__PACKAGE_MANAGERS as PACKAGE_MANAGERS, __webpack_exports__QUALITY_PRESETS as QUALITY_PRESETS, __webpack_exports__RSHONO_RANGE as RSHONO_RANGE, __webpack_exports__TOOL_VERSIONS as TOOL_VERSIONS, __webpack_exports__conflictingEntries as conflictingEntries, __webpack_exports__deployHint as deployHint, __webpack_exports__detectPackageManager as detectPackageManager, __webpack_exports__isValidPackageName as isValidPackageName, __webpack_exports__packageManager as packageManager, __webpack_exports__plan as plan, __webpack_exports__selectFeatures as selectFeatures, __webpack_exports__toPackageName as toPackageName, __webpack_exports__writePlan as writePlan };
|
package/dist/cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { parseArgs, styleText as external_node_util_styleText } from "node:util"
|
|
|
6
6
|
import node_process, { stdin as external_node_process_stdin, stdout as external_node_process_stdout } from "node:process";
|
|
7
7
|
import node_readline from "node:readline";
|
|
8
8
|
import "node:tty";
|
|
9
|
-
import {
|
|
9
|
+
import { mkdirSync, readFileSync, readdirSync as external_node_fs_readdirSync, statSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { basename, dirname as external_node_path_dirname, join as external_node_path_join, posix, relative as external_node_path_relative, resolve, sep } from "node:path";
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
var __webpack_modules__ = ({
|
|
@@ -2943,7 +2943,11 @@ function hasGit(cwd) {
|
|
|
2943
2943
|
|
|
2944
2944
|
;// CONCATENATED MODULE: ./src/generated/framework.ts
|
|
2945
2945
|
// GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
|
|
2946
|
-
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.
|
|
2946
|
+
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.2';
|
|
2947
|
+
/**
|
|
2948
|
+
* The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
|
|
2949
|
+
* or a contributor on an older Node hears it from their package manager rather than from a stack trace.
|
|
2950
|
+
*/ const NODE_ENGINE = '>=22.1.0';
|
|
2947
2951
|
/**
|
|
2948
2952
|
* The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
|
|
2949
2953
|
* exact — React's RSC internals are coupled across builds, and a generated app has no workspace
|
|
@@ -2989,19 +2993,32 @@ function hasGit(cwd) {
|
|
|
2989
2993
|
|
|
2990
2994
|
;// CONCATENATED MODULE: ./src/options.ts
|
|
2991
2995
|
|
|
2992
|
-
|
|
2996
|
+
/*
|
|
2997
|
+
* The names each option accepts, spelled once. The types below are derived from them, the CLI validates
|
|
2998
|
+
* its flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them
|
|
2999
|
+
* — so a name added here reaches all three without a second list to remember.
|
|
3000
|
+
*/ const FORMATTER_NAMES = [
|
|
3001
|
+
'prettier',
|
|
3002
|
+
'biome',
|
|
3003
|
+
'oxfmt',
|
|
3004
|
+
'none'
|
|
3005
|
+
];
|
|
3006
|
+
const LINTER_NAMES = [
|
|
3007
|
+
'oxlint',
|
|
3008
|
+
'eslint',
|
|
3009
|
+
'biome',
|
|
3010
|
+
'none'
|
|
3011
|
+
];
|
|
3012
|
+
const PACKAGE_MANAGERS = [
|
|
2993
3013
|
'npm',
|
|
2994
3014
|
'pnpm',
|
|
2995
3015
|
'yarn',
|
|
2996
3016
|
'bun'
|
|
2997
|
-
]
|
|
3017
|
+
];
|
|
2998
3018
|
const DEPLOY_TARGET_NAMES = DEPLOY_TARGETS.map((target)=>target.name);
|
|
2999
3019
|
function deployHint(name) {
|
|
3000
3020
|
return DEPLOY_TARGETS.find((target)=>target.name === name)?.hint ?? '';
|
|
3001
3021
|
}
|
|
3002
|
-
function isDeployTarget(value) {
|
|
3003
|
-
return DEPLOY_TARGET_NAMES.includes(value);
|
|
3004
|
-
}
|
|
3005
3022
|
const QUALITY_PRESETS = [
|
|
3006
3023
|
{
|
|
3007
3024
|
id: 'prettier-oxlint',
|
|
@@ -3010,6 +3027,13 @@ const QUALITY_PRESETS = [
|
|
|
3010
3027
|
formatter: 'prettier',
|
|
3011
3028
|
linter: 'oxlint'
|
|
3012
3029
|
},
|
|
3030
|
+
{
|
|
3031
|
+
id: 'prettier-eslint',
|
|
3032
|
+
label: 'Prettier + ESLint',
|
|
3033
|
+
hint: 'type-aware rules — pins TypeScript 6, which is all typescript-eslint accepts',
|
|
3034
|
+
formatter: 'prettier',
|
|
3035
|
+
linter: 'eslint'
|
|
3036
|
+
},
|
|
3013
3037
|
{
|
|
3014
3038
|
id: 'biome',
|
|
3015
3039
|
label: 'Biome',
|
|
@@ -3081,7 +3105,7 @@ const QUALITY_PRESETS = [
|
|
|
3081
3105
|
|
|
3082
3106
|
;// CONCATENATED MODULE: ./src/versions.ts
|
|
3083
3107
|
|
|
3084
|
-
|
|
3108
|
+
/** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
|
|
3085
3109
|
/** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
|
|
3086
3110
|
/**
|
|
3087
3111
|
* Versions for the optional tooling the features can add — the one place in this package where a
|
|
@@ -3102,8 +3126,26 @@ const QUALITY_PRESETS = [
|
|
|
3102
3126
|
'@biomejs/biome': '^2.5.6',
|
|
3103
3127
|
oxlint: '^1.76.0',
|
|
3104
3128
|
oxfmt: '^0.61.0',
|
|
3129
|
+
eslint: '^10.8.0',
|
|
3130
|
+
// ESLint's own recommended JavaScript rules, which typescript-eslint layers on top of rather than
|
|
3131
|
+
// replaces, and the rules of hooks — the one class of React mistake no type checker sees.
|
|
3132
|
+
'@eslint/js': '^10.0.1',
|
|
3133
|
+
'typescript-eslint': '^8.65.0',
|
|
3134
|
+
'eslint-plugin-react-hooks': '^7.1.1',
|
|
3105
3135
|
wrangler: '^4.115.0'
|
|
3106
3136
|
};
|
|
3137
|
+
/**
|
|
3138
|
+
* The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
|
|
3139
|
+
* {@link FRAMEWORK_DEPS}, and the reason it is spelled out here.
|
|
3140
|
+
*
|
|
3141
|
+
* typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface, so
|
|
3142
|
+
* it accepts `typescript >=4.8.4 <6.1.0` and nothing above. `~6.0.3` is the newest that satisfies it:
|
|
3143
|
+
* patch releases of 6.0, no minor. The framework itself stays on the TypeScript it is tested against —
|
|
3144
|
+
* rshono's declarations compile the same under either, which is what makes this pin an app's business
|
|
3145
|
+
* and not the framework's.
|
|
3146
|
+
*
|
|
3147
|
+
* When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
|
|
3148
|
+
*/ const ESLINT_TYPESCRIPT = '~6.0.3';
|
|
3107
3149
|
|
|
3108
3150
|
;// CONCATENATED MODULE: ./src/features/deploy.ts
|
|
3109
3151
|
|
|
@@ -3114,8 +3156,11 @@ const QUALITY_PRESETS = [
|
|
|
3114
3156
|
* Deliberately thin. The framework already knows how to arrange its own output for every platform, and
|
|
3115
3157
|
* `rshono build` writes the one platform config that has to exist (`wrangler.jsonc`, with the
|
|
3116
3158
|
* `compatibility_date` of the day it ran) if the project has none. Generating a second copy here would
|
|
3117
|
-
* be a copy that goes stale.
|
|
3118
|
-
*
|
|
3159
|
+
* be a copy that goes stale.
|
|
3160
|
+
*
|
|
3161
|
+
* So a target contributes only what is true of it: the command that runs or ships the build, the CLI
|
|
3162
|
+
* that command needs installed locally, the directories its platform leaves behind for `.gitignore`, and
|
|
3163
|
+
* a note for the step no command covers. Several contribute just one of those.
|
|
3119
3164
|
*/ const DEPLOY_FEATURES = {
|
|
3120
3165
|
// Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
|
|
3121
3166
|
// guess — so the target contributes only the command that runs what was built.
|
|
@@ -3144,6 +3189,11 @@ const QUALITY_PRESETS = [
|
|
|
3144
3189
|
devDependencies: {
|
|
3145
3190
|
wrangler: TOOL_VERSIONS.wrangler
|
|
3146
3191
|
},
|
|
3192
|
+
// wrangler brings workerd, whose install script only picks the platform binary out of the optional
|
|
3193
|
+
// dependency that already carries it — `workerd --version` answers without it having run.
|
|
3194
|
+
allowBuilds: {
|
|
3195
|
+
workerd: false
|
|
3196
|
+
},
|
|
3147
3197
|
scripts: {
|
|
3148
3198
|
deploy: 'rshono build && wrangler deploy'
|
|
3149
3199
|
},
|
|
@@ -3193,8 +3243,10 @@ function deployFeature(target) {
|
|
|
3193
3243
|
* deduplicates by `id`, so `formatter: 'biome', linter: 'biome'` contributes one set of files, one
|
|
3194
3244
|
* dependency and one pair of scripts.
|
|
3195
3245
|
*
|
|
3196
|
-
*
|
|
3197
|
-
*
|
|
3246
|
+
* A formatter brings `format:check` beside `format`, because the writing half and the CI half want
|
|
3247
|
+
* different exit-code behaviour: `format` rewrites files, `format:check` fails instead. A linter brings
|
|
3248
|
+
* `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
|
|
3249
|
+
* one. Biome adds a `check` of its own, which is the pair of them in a single pass.
|
|
3198
3250
|
*/ const PRETTIER = {
|
|
3199
3251
|
id: 'prettier',
|
|
3200
3252
|
overlays: [
|
|
@@ -3234,6 +3286,30 @@ const OXLINT = {
|
|
|
3234
3286
|
'lint:fix': 'oxlint --fix'
|
|
3235
3287
|
}
|
|
3236
3288
|
};
|
|
3289
|
+
/**
|
|
3290
|
+
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
|
|
3291
|
+
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one its
|
|
3292
|
+
* peer range accepts (see {@link ESLINT_TYPESCRIPT}). Every other preset leaves TypeScript alone.
|
|
3293
|
+
*
|
|
3294
|
+
* The rules are type-aware, which is the reason to reach for ESLint over a syntax-only linter at all —
|
|
3295
|
+
* so the config it ships hands the whole program to the parser rather than linting file by file.
|
|
3296
|
+
*/ const ESLINT = {
|
|
3297
|
+
id: 'eslint',
|
|
3298
|
+
overlays: [
|
|
3299
|
+
'eslint'
|
|
3300
|
+
],
|
|
3301
|
+
devDependencies: {
|
|
3302
|
+
eslint: TOOL_VERSIONS.eslint,
|
|
3303
|
+
'@eslint/js': TOOL_VERSIONS["@eslint/js"],
|
|
3304
|
+
'typescript-eslint': TOOL_VERSIONS["typescript-eslint"],
|
|
3305
|
+
'eslint-plugin-react-hooks': TOOL_VERSIONS["eslint-plugin-react-hooks"],
|
|
3306
|
+
typescript: (/* inlined export .ESLINT_TYPESCRIPT */"~6.0.3")
|
|
3307
|
+
},
|
|
3308
|
+
scripts: {
|
|
3309
|
+
lint: 'eslint .',
|
|
3310
|
+
'lint:fix': 'eslint . --fix'
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3237
3313
|
const BIOME = {
|
|
3238
3314
|
id: 'biome',
|
|
3239
3315
|
overlays: [
|
|
@@ -3258,6 +3334,7 @@ const FORMATTERS = {
|
|
|
3258
3334
|
};
|
|
3259
3335
|
const LINTERS = {
|
|
3260
3336
|
oxlint: OXLINT,
|
|
3337
|
+
eslint: ESLINT,
|
|
3261
3338
|
biome: BIOME,
|
|
3262
3339
|
none: null
|
|
3263
3340
|
};
|
|
@@ -3325,8 +3402,9 @@ function stylingFeature(styling) {
|
|
|
3325
3402
|
;// CONCATENATED MODULE: ./src/pkg.ts
|
|
3326
3403
|
|
|
3327
3404
|
/**
|
|
3328
|
-
* The scripts every app gets. `start` is not among them
|
|
3329
|
-
*
|
|
3405
|
+
* The scripts every app gets. `start` is not among them, because it means something different per
|
|
3406
|
+
* platform: the three targets that run the build themselves — node, bun, deno — each contribute their
|
|
3407
|
+
* own, and a platform target contributes a `deploy` instead, where its platform has one command to give.
|
|
3330
3408
|
*/ const BASE_SCRIPTS = {
|
|
3331
3409
|
dev: 'rshono dev',
|
|
3332
3410
|
build: 'rshono build',
|
|
@@ -3346,6 +3424,39 @@ function stylingFeature(styling) {
|
|
|
3346
3424
|
function sorted(record) {
|
|
3347
3425
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
3348
3426
|
}
|
|
3427
|
+
/**
|
|
3428
|
+
* The install script every app inherits, from the framework rather than from anything it chose. tsx
|
|
3429
|
+
* reads `rshono.config.ts` through esbuild, and esbuild's script only picks the platform binary out of
|
|
3430
|
+
* the optional dependency that already carries it — rshono's own repo denies it for the same reason.
|
|
3431
|
+
*/ const BASE_ALLOW_BUILDS = {
|
|
3432
|
+
esbuild: false
|
|
3433
|
+
};
|
|
3434
|
+
/**
|
|
3435
|
+
* pnpm's settings for the new app — written for pnpm and for nobody else.
|
|
3436
|
+
*
|
|
3437
|
+
* It exists for one field. A dependency with an install script is a question pnpm will not answer on its
|
|
3438
|
+
* own: it fails the install, and fails every `pnpm dev` after it, until the project has said whether the
|
|
3439
|
+
* script should run. None of the ones an rshono app inherits need to (each is a native package whose
|
|
3440
|
+
* binary arrives as an optional dependency), so a fresh app carries the answer rather than meeting
|
|
3441
|
+
* `pnpm approve-builds` before it has rendered a page once.
|
|
3442
|
+
*
|
|
3443
|
+
* In this file rather than under a `pnpm` key in `package.json`, which pnpm 11 no longer reads, single-
|
|
3444
|
+
* package projects included. What lands here is a decision about *this* app: a scaffolded file the app
|
|
3445
|
+
* owns from then on, not something the framework reaches back into.
|
|
3446
|
+
*/ function buildPnpmSettings(features) {
|
|
3447
|
+
const allowBuilds = {
|
|
3448
|
+
...BASE_ALLOW_BUILDS
|
|
3449
|
+
};
|
|
3450
|
+
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
3451
|
+
return [
|
|
3452
|
+
'# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
|
|
3453
|
+
'# fails the install rather than skip one quietly — so anything added later belongs here too.',
|
|
3454
|
+
'# `false` means the script was looked at: these ship their real binary as an optional dependency.',
|
|
3455
|
+
'allowBuilds:',
|
|
3456
|
+
...Object.entries(sorted(allowBuilds)).map(([name, allowed])=>` ${name}: ${allowed}`),
|
|
3457
|
+
''
|
|
3458
|
+
].join('\n');
|
|
3459
|
+
}
|
|
3349
3460
|
/**
|
|
3350
3461
|
* Assembles `package.json` from the answers and whatever the selected features contribute.
|
|
3351
3462
|
*
|
|
@@ -3377,10 +3488,9 @@ function sorted(record) {
|
|
|
3377
3488
|
version: '0.1.0',
|
|
3378
3489
|
private: true,
|
|
3379
3490
|
type: 'module',
|
|
3380
|
-
//
|
|
3381
|
-
// Node finds out from their package manager rather than from a stack trace.
|
|
3491
|
+
// Generated from the framework's own manifest, so the app's floor cannot drift below rshono's.
|
|
3382
3492
|
engines: {
|
|
3383
|
-
node:
|
|
3493
|
+
node: NODE_ENGINE
|
|
3384
3494
|
},
|
|
3385
3495
|
scripts,
|
|
3386
3496
|
dependencies: sorted(dependencies),
|
|
@@ -3398,19 +3508,23 @@ function sorted(record) {
|
|
|
3398
3508
|
|
|
3399
3509
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
3400
3510
|
|
|
3401
|
-
|
|
3511
|
+
/**
|
|
3512
|
+
* `{{NAME}}`, and not the `__NAME__` this used to be. A template is a real file that real tools run
|
|
3513
|
+
* over, and in markdown `__NAME__` *is* strong emphasis — Prettier rewrites it to `**NAME**`, which
|
|
3514
|
+
* turns a token into literal text that no substitution will ever match again. `{{…}}` means nothing to
|
|
3515
|
+
* any of the formats these templates are written in.
|
|
3516
|
+
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
3402
3517
|
function tokensFor(answers, pm) {
|
|
3403
3518
|
return {
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
__PM_RUN__: pm.run
|
|
3519
|
+
'{{PROJECT_NAME}}': answers.packageName,
|
|
3520
|
+
'{{DEPLOY_TARGET}}': answers.deploy,
|
|
3521
|
+
'{{DEPLOY_HINT}}': deployHint(answers.deploy),
|
|
3522
|
+
'{{PM_RUN}}': pm.run
|
|
3409
3523
|
};
|
|
3410
3524
|
}
|
|
3411
3525
|
/**
|
|
3412
3526
|
* Substitutes tokens, and throws on one it doesn't know — a typo in a template would otherwise ship a
|
|
3413
|
-
* literal `
|
|
3527
|
+
* literal `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would
|
|
3414
3528
|
* catch.
|
|
3415
3529
|
*/ function render(contents, tokens, source) {
|
|
3416
3530
|
return contents.replace(TOKEN_PATTERN, (token)=>{
|
|
@@ -3486,6 +3600,7 @@ function readTemplateDir(dir) {
|
|
|
3486
3600
|
const gitignore = files.get('.gitignore');
|
|
3487
3601
|
if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
|
|
3488
3602
|
files.set('package.json', buildPackageJson(answers, features, pm));
|
|
3603
|
+
if (pm.name === 'pnpm') files.set('pnpm-workspace.yaml', buildPnpmSettings(features));
|
|
3489
3604
|
return {
|
|
3490
3605
|
// Sorted, so both the write order and a test's snapshot are stable.
|
|
3491
3606
|
files: new Map([
|
|
@@ -3498,6 +3613,7 @@ function readTemplateDir(dir) {
|
|
|
3498
3613
|
|
|
3499
3614
|
;// CONCATENATED MODULE: ./src/pm.ts
|
|
3500
3615
|
|
|
3616
|
+
|
|
3501
3617
|
const INSTALL = {
|
|
3502
3618
|
npm: [
|
|
3503
3619
|
'install'
|
|
@@ -3517,7 +3633,7 @@ const RUN = {
|
|
|
3517
3633
|
bun: 'bun'
|
|
3518
3634
|
};
|
|
3519
3635
|
function isKnown(name) {
|
|
3520
|
-
return name
|
|
3636
|
+
return PACKAGE_MANAGERS.includes(name);
|
|
3521
3637
|
}
|
|
3522
3638
|
function packageManager(name, version) {
|
|
3523
3639
|
return {
|
|
@@ -3611,15 +3727,20 @@ function run(pm, args, cwd) {
|
|
|
3611
3727
|
'.vscode',
|
|
3612
3728
|
'Thumbs.db'
|
|
3613
3729
|
]);
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3730
|
+
/**
|
|
3731
|
+
* What is already at the target path, which is what decides whether scaffolding into it is safe: the
|
|
3732
|
+
* entries already there, ignoring the ones a fresh clone or an editor leaves behind.
|
|
3733
|
+
*
|
|
3734
|
+
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory is not
|
|
3735
|
+
* something `--force` should be able to write into, so it throws rather than reporting an empty list —
|
|
3736
|
+
* otherwise `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
3737
|
+
*/ function conflictingEntries(dir) {
|
|
3738
|
+
const stats = statSync(dir, {
|
|
3739
|
+
throwIfNoEntry: false
|
|
3740
|
+
});
|
|
3741
|
+
if (!stats) return [];
|
|
3742
|
+
if (!stats.isDirectory()) throw new Error(`${dir} already exists and is not a directory.`);
|
|
3743
|
+
return external_node_fs_readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
|
|
3623
3744
|
}
|
|
3624
3745
|
/**
|
|
3625
3746
|
* Writes the plan. Directories are created as needed, and files are written with the plan's own
|
|
@@ -3647,8 +3768,9 @@ function inspectTarget(dir) {
|
|
|
3647
3768
|
|
|
3648
3769
|
|
|
3649
3770
|
|
|
3771
|
+
|
|
3650
3772
|
const DEFAULT_DIRECTORY = 'my-rshono-app';
|
|
3651
|
-
const HELP = `create-rshono — scaffold a new rshono app
|
|
3773
|
+
/** Every accepted name comes from `options.ts`, so the help cannot promise one the validation refuses. */ const HELP = `create-rshono — scaffold a new rshono app
|
|
3652
3774
|
|
|
3653
3775
|
Usage:
|
|
3654
3776
|
npm create @rshono@latest [directory] [options]
|
|
@@ -3658,9 +3780,9 @@ Options:
|
|
|
3658
3780
|
-d, --deploy <target> ${DEPLOY_TARGET_NAMES.join(' | ')}
|
|
3659
3781
|
--tailwind Tailwind CSS (--no-tailwind for plain CSS)
|
|
3660
3782
|
--quality <preset> ${QUALITY_PRESETS.map((preset)=>preset.id).join(' | ')}
|
|
3661
|
-
--formatter <name>
|
|
3662
|
-
--linter <name>
|
|
3663
|
-
--pm <name>
|
|
3783
|
+
--formatter <name> ${FORMATTER_NAMES.join(' | ')} (overrides --quality)
|
|
3784
|
+
--linter <name> ${LINTER_NAMES.join(' | ')} (overrides --quality; eslint pins TypeScript 6)
|
|
3785
|
+
--pm <name> ${PACKAGE_MANAGERS.join(' | ')} (default: whatever ran this)
|
|
3664
3786
|
--no-install write the files and stop
|
|
3665
3787
|
--no-git do not initialize a repository
|
|
3666
3788
|
--force scaffold into a directory that is not empty
|
|
@@ -3688,92 +3810,94 @@ function fail(message) {
|
|
|
3688
3810
|
if (off) return false;
|
|
3689
3811
|
return undefined;
|
|
3690
3812
|
}
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3813
|
+
/**
|
|
3814
|
+
* `parseArgs` rejects an unknown flag with a message that names it but nothing else — so a mistyped
|
|
3815
|
+
* `--tailwnid` reads as a wall of text about positional arguments. Pointing at `--help` is the whole
|
|
3816
|
+
* addition.
|
|
3817
|
+
*/ function parse() {
|
|
3818
|
+
try {
|
|
3819
|
+
return parseArgs({
|
|
3820
|
+
options: {
|
|
3821
|
+
yes: {
|
|
3822
|
+
type: 'boolean',
|
|
3823
|
+
short: 'y'
|
|
3824
|
+
},
|
|
3825
|
+
deploy: {
|
|
3826
|
+
type: 'string',
|
|
3827
|
+
short: 'd'
|
|
3828
|
+
},
|
|
3829
|
+
tailwind: {
|
|
3830
|
+
type: 'boolean'
|
|
3831
|
+
},
|
|
3832
|
+
'no-tailwind': {
|
|
3833
|
+
type: 'boolean'
|
|
3834
|
+
},
|
|
3835
|
+
quality: {
|
|
3836
|
+
type: 'string'
|
|
3837
|
+
},
|
|
3838
|
+
formatter: {
|
|
3839
|
+
type: 'string'
|
|
3840
|
+
},
|
|
3841
|
+
linter: {
|
|
3842
|
+
type: 'string'
|
|
3843
|
+
},
|
|
3844
|
+
pm: {
|
|
3845
|
+
type: 'string'
|
|
3846
|
+
},
|
|
3847
|
+
install: {
|
|
3848
|
+
type: 'boolean'
|
|
3849
|
+
},
|
|
3850
|
+
'no-install': {
|
|
3851
|
+
type: 'boolean'
|
|
3852
|
+
},
|
|
3853
|
+
git: {
|
|
3854
|
+
type: 'boolean'
|
|
3855
|
+
},
|
|
3856
|
+
'no-git': {
|
|
3857
|
+
type: 'boolean'
|
|
3858
|
+
},
|
|
3859
|
+
force: {
|
|
3860
|
+
type: 'boolean'
|
|
3861
|
+
},
|
|
3862
|
+
'dry-run': {
|
|
3863
|
+
type: 'boolean'
|
|
3864
|
+
},
|
|
3865
|
+
help: {
|
|
3866
|
+
type: 'boolean',
|
|
3867
|
+
short: 'h'
|
|
3868
|
+
},
|
|
3869
|
+
version: {
|
|
3870
|
+
type: 'boolean',
|
|
3871
|
+
short: 'v'
|
|
3872
|
+
}
|
|
3741
3873
|
},
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3874
|
+
allowPositionals: true
|
|
3875
|
+
});
|
|
3876
|
+
} catch (error) {
|
|
3877
|
+
fail(`${error instanceof Error ? error.message : String(error)}\n\nRun with --help to see the options.`);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
async function main() {
|
|
3881
|
+
const { values, positionals } = parse();
|
|
3749
3882
|
if (values.help) return console.log(HELP);
|
|
3750
|
-
if (values.version) return console.log("1.0.0-rc.
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
'bun'
|
|
3758
|
-
], 'pm');
|
|
3883
|
+
if (values.version) return console.log("1.0.0-rc.2");
|
|
3884
|
+
/*
|
|
3885
|
+
* A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both streams
|
|
3886
|
+
* have to be a terminal: the prompts draw on stdout but *read from stdin*, so `echo | npm create` with
|
|
3887
|
+
* stdout still attached would otherwise ask a question with nothing behind the keyboard.
|
|
3888
|
+
*/ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
|
|
3889
|
+
const pmFlag = oneOf(values.pm, PACKAGE_MANAGERS, 'pm');
|
|
3759
3890
|
const pm = pmFlag ? packageManager(pmFlag) : detectPackageManager();
|
|
3760
3891
|
const deployFlag = oneOf(values.deploy, DEPLOY_TARGET_NAMES, 'deploy');
|
|
3761
|
-
const formatterFlag = oneOf(values.formatter,
|
|
3762
|
-
|
|
3763
|
-
'biome',
|
|
3764
|
-
'oxfmt',
|
|
3765
|
-
'none'
|
|
3766
|
-
], 'formatter');
|
|
3767
|
-
const linterFlag = oneOf(values.linter, [
|
|
3768
|
-
'oxlint',
|
|
3769
|
-
'biome',
|
|
3770
|
-
'none'
|
|
3771
|
-
], 'linter');
|
|
3892
|
+
const formatterFlag = oneOf(values.formatter, FORMATTER_NAMES, 'formatter');
|
|
3893
|
+
const linterFlag = oneOf(values.linter, LINTER_NAMES, 'linter');
|
|
3772
3894
|
const qualityFlag = oneOf(values.quality, QUALITY_PRESETS.map((preset)=>preset.id), 'quality');
|
|
3773
3895
|
const tailwindFlag = tristate(values.tailwind, values['no-tailwind'], 'tailwind');
|
|
3774
3896
|
const installFlag = tristate(values.install, values['no-install'], 'install');
|
|
3775
3897
|
const gitFlag = tristate(values.git, values['no-git'], 'git');
|
|
3776
|
-
|
|
3898
|
+
// The framework version, not this package's: it is the one the app will be pinned to, and the one
|
|
3899
|
+
// worth reading here. `--version` reports create-rshono's own.
|
|
3900
|
+
intro(`create-rshono · rshono ${RSHONO_VERSION}`);
|
|
3777
3901
|
// ── Where ───────────────────────────────────────────────────────────────────────────────────────
|
|
3778
3902
|
let directory = positionals[0];
|
|
3779
3903
|
if (!directory) {
|
|
@@ -3787,7 +3911,7 @@ async function main() {
|
|
|
3787
3911
|
const targetDir = resolve(process.cwd(), directory);
|
|
3788
3912
|
const packageName = toPackageName(directory === '.' ? basename(targetDir) : directory);
|
|
3789
3913
|
if (!packageName || !isValidPackageName(packageName)) fail(`"${directory}" does not give a usable npm package name.`);
|
|
3790
|
-
const conflicts =
|
|
3914
|
+
const conflicts = conflictingEntries(targetDir);
|
|
3791
3915
|
if (conflicts.length > 0 && !values.force) {
|
|
3792
3916
|
const where = directory === '.' ? 'this directory' : `"${directory}"`;
|
|
3793
3917
|
const listed = `${conflicts.slice(0, 3).join(', ')}${conflicts.length > 3 ? ', …' : ''}`;
|
|
@@ -3813,7 +3937,7 @@ async function main() {
|
|
|
3813
3937
|
}))
|
|
3814
3938
|
}));
|
|
3815
3939
|
}
|
|
3816
|
-
let styling = tailwindFlag
|
|
3940
|
+
let styling = tailwindFlag ? 'tailwind' : 'css';
|
|
3817
3941
|
if (tailwindFlag === undefined && interactive) {
|
|
3818
3942
|
styling = unwrap(await dist_select({
|
|
3819
3943
|
message: 'Styling?',
|
|
@@ -3863,9 +3987,10 @@ async function main() {
|
|
|
3863
3987
|
initialValue: true
|
|
3864
3988
|
}));
|
|
3865
3989
|
}
|
|
3866
|
-
|
|
3990
|
+
// Asked once, not once per use: this shells out to `git rev-parse`.
|
|
3991
|
+
const nested = isInsideRepo(process.cwd());
|
|
3992
|
+
let git = gitFlag ?? !nested;
|
|
3867
3993
|
if (gitFlag === undefined && interactive) {
|
|
3868
|
-
const nested = isInsideRepo(process.cwd());
|
|
3869
3994
|
git = unwrap(await dist_confirm({
|
|
3870
3995
|
message: nested ? 'Initialize a git repository? (this is already inside one)' : 'Initialize a git repository?',
|
|
3871
3996
|
initialValue: !nested
|
|
@@ -3873,14 +3998,10 @@ async function main() {
|
|
|
3873
3998
|
}
|
|
3874
3999
|
const answers = {
|
|
3875
4000
|
packageName,
|
|
3876
|
-
targetDir,
|
|
3877
4001
|
deploy,
|
|
3878
4002
|
styling,
|
|
3879
4003
|
formatter,
|
|
3880
|
-
linter
|
|
3881
|
-
packageManager: pm.name,
|
|
3882
|
-
install,
|
|
3883
|
-
git
|
|
4004
|
+
linter
|
|
3884
4005
|
};
|
|
3885
4006
|
// ── Plan, then write ────────────────────────────────────────────────────────────────────────────
|
|
3886
4007
|
const plan = plan_plan(answers, pm);
|
|
@@ -3897,14 +4018,17 @@ async function main() {
|
|
|
3897
4018
|
if (install) {
|
|
3898
4019
|
log.step(`Installing dependencies with ${pm.name}…`);
|
|
3899
4020
|
installed = runInstall(pm, targetDir);
|
|
3900
|
-
if (!installed) log.warn(`${pm.name} install failed — run it yourself
|
|
4021
|
+
if (!installed) log.warn(`${pm.name} install failed — the files are all written, so run it yourself in the project.`);
|
|
3901
4022
|
}
|
|
3902
4023
|
/*
|
|
3903
4024
|
* Format the scaffold with the tool it was scaffolded with, so a fresh project passes its own
|
|
3904
4025
|
* `format:check` instead of reporting a diff nobody made. Needs the install, since the formatter is a
|
|
3905
4026
|
* devDependency — hence the skip, rather than a failure, when there is none.
|
|
3906
|
-
|
|
3907
|
-
|
|
4027
|
+
*
|
|
4028
|
+
* Whether there is a `format` script is the plan's answer, not the formatter answer's: `--formatter
|
|
4029
|
+
* none --linter biome` has no formatter and a `format` script all the same, because Biome brings one.
|
|
4030
|
+
*/ if (installed && plan.features.some((feature)=>feature.scripts?.format)) {
|
|
4031
|
+
if (!runScript(pm, 'format', targetDir)) log.warn(`\`${pm.run} format\` failed — the scaffold is written, just not formatted.`);
|
|
3908
4032
|
}
|
|
3909
4033
|
if (git) {
|
|
3910
4034
|
if (!hasGit(targetDir)) {
|
package/package.json
CHANGED
package/templates/base/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
3
|
A [rshono](https://github.com/rshono/rshono) app — [Hono](https://hono.dev) + [Rspack](https://rspack.rs) + [React Server Components](https://react.dev/reference/rsc/server-components).
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
{{PM_RUN}} dev # dev server with HMR, http://localhost:3000
|
|
7
|
+
{{PM_RUN}} build # production build for {{DEPLOY_TARGET}}
|
|
8
|
+
{{PM_RUN}} typecheck # tsc --noEmit
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
`package.json` has the rest, including whatever your formatter and linter added.
|
|
@@ -35,7 +35,7 @@ compiles to `undefined` rather than shipping. `src/lib/env.ts` is where both sid
|
|
|
35
35
|
|
|
36
36
|
## Deploying
|
|
37
37
|
|
|
38
|
-
This app is built for `
|
|
38
|
+
This app is built for `{{DEPLOY_TARGET}}`: after `{{PM_RUN}} build`, {{DEPLOY_HINT}}.
|
|
39
39
|
|
|
40
40
|
Change `deploy` in `rshono.config.ts` to target somewhere else, or build for one place without editing the
|
|
41
41
|
file: `rshono build --deploy vercel`, or `RSHONO_DEPLOY=vercel` in CI. `dev` always runs the Node dev
|
package/templates/base/_env
CHANGED
|
@@ -10,7 +10,7 @@ import { defineConfig } from '@rshono/core';
|
|
|
10
10
|
*/
|
|
11
11
|
export default defineConfig({
|
|
12
12
|
/** Where `build` targets. Overridable per build with `--deploy` or `RSHONO_DEPLOY`. */
|
|
13
|
-
deploy: '
|
|
13
|
+
deploy: '{{DEPLOY_TARGET}}',
|
|
14
14
|
|
|
15
15
|
// The public origin, baked into prerendered pages' absolute URLs. Set it if you use `render: 'static'`
|
|
16
16
|
// and build canonical tags, `og:url` or absolute links — there is no request to read a Host from.
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* The signature is the one `useActionState` expects: previous state first, then the form data.
|
|
10
10
|
*/
|
|
11
11
|
export async function greet(_previous: string | null, formData: FormData): Promise<string> {
|
|
12
|
-
|
|
12
|
+
// A form field is a string or a File, never only a string — a file input posted under this name would
|
|
13
|
+
// stringify to "[object File]" rather than fail. Narrowing is the validation the endpoint owes itself.
|
|
14
|
+
const field = formData.get('name');
|
|
15
|
+
const name = typeof field === 'string' ? field.trim() : '';
|
|
13
16
|
if (!name) return 'Type a name first.';
|
|
14
17
|
|
|
15
18
|
// Where real work goes — a database write, an email, a queue push.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
/** Safe anywhere: `PUBLIC_` variables are the ones compiled into the browser bundle. */
|
|
11
11
|
export const publicEnv = {
|
|
12
|
-
appName: process.env.PUBLIC_APP_NAME ?? '
|
|
12
|
+
appName: process.env.PUBLIC_APP_NAME ?? '{{PROJECT_NAME}}',
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
|
|
3
|
-
"files": { "includes": ["**", "!dist", "!wrangler.jsonc"] },
|
|
3
|
+
"files": { "includes": ["**", "!dist", "!.wrangler", "!.vercel", "!.netlify", "!wrangler.jsonc"] },
|
|
4
4
|
"formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 140 },
|
|
5
5
|
"javascript": { "formatter": { "quoteStyle": "single", "jsxQuoteStyle": "double" } },
|
|
6
6
|
"linter": { "enabled": true, "rules": { "preset": "recommended" } }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
|
|
3
|
-
"files": { "includes": ["**", "!dist", "!wrangler.jsonc", "!**/*.css"] },
|
|
3
|
+
"files": { "includes": ["**", "!dist", "!.wrangler", "!.vercel", "!.netlify", "!wrangler.jsonc", "!**/*.css"] },
|
|
4
4
|
"formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 140 },
|
|
5
5
|
"javascript": { "formatter": { "quoteStyle": "single", "jsxQuoteStyle": "double" } },
|
|
6
6
|
"linter": { "enabled": true, "rules": { "preset": "recommended" } }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
3
|
+
import tseslint from 'typescript-eslint';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Flat config, with type-aware rules — the reason to run ESLint over a syntax-only linter. `projectService`
|
|
7
|
+
* builds the same program `tsc` does from tsconfig.json, so a rule can ask what a value actually *is*:
|
|
8
|
+
* an unawaited promise, a `catch` that swallows an error, a `String()` around something that is not one.
|
|
9
|
+
*
|
|
10
|
+
* The cost is that ESLint needs TypeScript to answer, which is why this app pins TypeScript 6 —
|
|
11
|
+
* typescript-eslint reads the compiler API directly and accepts nothing newer. `npm run typecheck` is
|
|
12
|
+
* still the thing that decides whether the app compiles; these rules only see what it sees.
|
|
13
|
+
*/
|
|
14
|
+
export default tseslint.config(
|
|
15
|
+
// ESLint's own default ignores cover node_modules and nothing else, so the build output — and whatever
|
|
16
|
+
// the deploy target leaves beside it — would otherwise be linted as if you had written it.
|
|
17
|
+
{ ignores: ['dist/**', '.wrangler/**', '.vercel/**', '.netlify/**'] },
|
|
18
|
+
js.configs.recommended,
|
|
19
|
+
tseslint.configs.recommendedTypeChecked,
|
|
20
|
+
// The rules of hooks: the one class of React mistake no type checker catches, and the reason a React
|
|
21
|
+
// app wants a linter at all.
|
|
22
|
+
reactHooks.configs.flat['recommended-latest'],
|
|
23
|
+
{
|
|
24
|
+
languageOptions: {
|
|
25
|
+
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
// This file and any other plain JavaScript sits outside the TypeScript program, so the type-aware rules
|
|
29
|
+
// have nothing to run against and would report every file as unconfigured.
|
|
30
|
+
{ files: ['**/*.{js,mjs,cjs}'], extends: [tseslint.configs.disableTypeChecked] },
|
|
31
|
+
);
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
node_modules/
|
|
2
|
-
dist/
|
|
3
2
|
pnpm-lock.yaml
|
|
4
3
|
package-lock.json
|
|
5
4
|
yarn.lock
|
|
6
5
|
bun.lock
|
|
7
6
|
|
|
7
|
+
# Build output: `rshono build`'s own, and whatever the deploy target assembles beside it.
|
|
8
|
+
dist/
|
|
9
|
+
.wrangler/
|
|
10
|
+
.vercel/
|
|
11
|
+
.netlify/
|
|
12
|
+
|
|
8
13
|
# Written by `rshono build` for the Cloudflare target, in the shape the build decided. Yours to edit —
|
|
9
14
|
# just not the formatter's to reflow on every build.
|
|
10
15
|
wrangler.jsonc
|
|
@@ -10,7 +10,7 @@ import { defineConfig } from '@rshono/core';
|
|
|
10
10
|
*/
|
|
11
11
|
export default defineConfig({
|
|
12
12
|
/** Where `build` targets. Overridable per build with `--deploy` or `RSHONO_DEPLOY`. */
|
|
13
|
-
deploy: '
|
|
13
|
+
deploy: '{{DEPLOY_TARGET}}',
|
|
14
14
|
|
|
15
15
|
// The public origin, baked into prerendered pages' absolute URLs. Set it if you use `render: 'static'`
|
|
16
16
|
// and build canonical tags, `og:url` or absolute links — there is no request to read a Host from.
|