@rshono/create 1.0.0-rc.6 → 1.0.0-rc.8
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 +54 -45
- package/bin/create-rshono.mjs +5 -3
- package/dist/api.mjs +53 -68
- package/dist/cli.mjs +62 -83
- package/package.json +4 -4
- package/templates/base/src/server.ts +15 -1
package/README.md
CHANGED
|
@@ -26,49 +26,72 @@ arrived in pnpm 10.16, before which it is `pnpm dlx`.
|
|
|
26
26
|
|
|
27
27
|
Six, each with a default, and each answerable by a flag instead:
|
|
28
28
|
|
|
29
|
-
| Question | Default | Flag
|
|
30
|
-
| --------------------- | ------------------ |
|
|
31
|
-
| Where should it go? | `my-rshono-app` | first positional argument, or `.`
|
|
32
|
-
| Where is it deployed? | `node` |
|
|
33
|
-
| Styling | plain CSS | `--tailwind` / `--no-tailwind`
|
|
34
|
-
| Formatting & linting | Prettier + oxlint | `--quality prettier-oxlint\|prettier-eslint\|biome\|oxc\|none`
|
|
35
|
-
| Install dependencies? | yes | `--no-install`
|
|
36
|
-
| Initialize git? | yes, unless nested | `--no-git`
|
|
37
|
-
|
|
38
|
-
Plus `--formatter` and `--linter` to set either half
|
|
39
|
-
the
|
|
40
|
-
`--
|
|
41
|
-
given. **A non-interactive terminal implies `-y`**,
|
|
29
|
+
| Question | Default | Flag |
|
|
30
|
+
| --------------------- | ------------------ | -------------------------------------------------------------- |
|
|
31
|
+
| Where should it go? | `my-rshono-app` | first positional argument, or `.` |
|
|
32
|
+
| Where is it deployed? | `node` | `-d, --deploy node\|cloudflare\|vercel\|aws-lambda` |
|
|
33
|
+
| Styling | plain CSS | `--tailwind` / `--no-tailwind` |
|
|
34
|
+
| Formatting & linting | Prettier + oxlint | `--quality prettier-oxlint\|prettier-eslint\|biome\|oxc\|none` |
|
|
35
|
+
| Install dependencies? | yes | `--no-install` |
|
|
36
|
+
| Initialize git? | yes, unless nested | `--no-git` |
|
|
37
|
+
|
|
38
|
+
Plus `--formatter prettier\|biome\|oxfmt\|none` and `--linter oxlint\|eslint\|biome\|none` to set either half
|
|
39
|
+
of the quality preset on its own, `--pm npm\|pnpm\|yarn\|bun` to override the package manager that was
|
|
40
|
+
detected, `--force` to scaffold into a directory that is not empty, `--dry-run` to see the file list and write
|
|
41
|
+
nothing, and `-y` to take the defaults for everything not given. **A non-interactive terminal implies `-y`**,
|
|
42
|
+
so this is one command in CI or from an agent:
|
|
42
43
|
|
|
43
44
|
```bash
|
|
44
45
|
npx @rshono/create@latest my-app -y --deploy cloudflare --tailwind --quality biome
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
The deploy targets, and the deploy command each one prints, are generated from the framework's own
|
|
48
|
-
|
|
48
|
+
The deploy targets, and the deploy command each one prints, are generated from the framework's own presets —
|
|
49
|
+
a target added to rshono appears here with no edit.
|
|
49
50
|
|
|
50
51
|
## What you get
|
|
51
52
|
|
|
52
53
|
```
|
|
54
|
+
package.json scripts, and the exact dependency versions rshono is tested against
|
|
53
55
|
rshono.config.ts the chosen deploy target; every other setting takes its default
|
|
54
56
|
tsconfig.json strict, with @/* → ./src/*
|
|
55
57
|
.env committed defaults; secrets go in .env.local
|
|
58
|
+
.gitignore plus whatever each chosen feature adds, under a heading naming it
|
|
59
|
+
README.md the app's own commands, layout and deploy step
|
|
56
60
|
public/ favicon.svg, robots.txt
|
|
57
61
|
src/routes.ts one page, a 404 and a 500
|
|
58
62
|
src/server.ts error reporting, trailing-slash and old-path redirects, AppType
|
|
59
63
|
src/components/ layout, home, 404, 500
|
|
60
64
|
src/styles.css element-level CSS, or the Tailwind entry
|
|
65
|
+
src/env.d.ts the CSS-import declaration the compiler needs
|
|
61
66
|
pnpm-workspace.yaml pnpm only: which dependency install scripts this app runs (none of them)
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
Scaffolding runs the install and nothing else — no formatter, no linter. The templates are written to
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
Scaffolding runs the install and nothing else — no formatter, no linter. The templates are written to satisfy
|
|
70
|
+
every formatter option at its configured width, so a fresh project passes its own `format:check` without a
|
|
71
|
+
write pass first.
|
|
67
72
|
|
|
68
73
|
`react` and `react-dom` are pinned **exactly**, at the versions the framework is tested against, and those
|
|
69
74
|
pins are generated from rshono's own manifest. That is not tidiness: the RSC runtime reaches into React's
|
|
70
75
|
internals, and an app installed with npm or bun has no workspace overrides to keep a single copy of it.
|
|
71
76
|
|
|
77
|
+
## Two things worth knowing
|
|
78
|
+
|
|
79
|
+
**The ESLint preset pins TypeScript 6.** Linting TypeScript with ESLint means `typescript-eslint`, which reads
|
|
80
|
+
the compiler API directly rather than through a stable interface, so its peer range is
|
|
81
|
+
`typescript >=4.8.4 <6.1.0` — below the TypeScript rshono is built and tested against. An app that chooses
|
|
82
|
+
ESLint therefore gets `typescript ~6.0.3`, the newest that range allows: `npm install` would otherwise fail
|
|
83
|
+
outright on the conflict, and forcing past it hands you a linter running against a compiler API it was never
|
|
84
|
+
built for. The framework's declarations compile identically under either version, which is what makes this the
|
|
85
|
+
app's pin and not the framework's — every other preset leaves TypeScript alone. What you trade for type-aware
|
|
86
|
+
rules is a compiler one major behind, and the JavaScript implementation rather than the native one, so
|
|
87
|
+
`typecheck` on a large app is several times slower. When upstream widens the range, the pin in
|
|
88
|
+
`features/quality.ts` is the only thing to delete.
|
|
89
|
+
|
|
90
|
+
**Tailwind is four packages, a `postcss.config.mjs` and one rule in `rshono.config.ts`.** rshono compiles CSS
|
|
91
|
+
natively and has no PostCSS in it — that is deliberate, and it means an app that wants a plugin chain brings
|
|
92
|
+
its own, through the `rspack` hook. The overlay writes all of it, with a comment saying what to delete to go
|
|
93
|
+
back to plain CSS.
|
|
94
|
+
|
|
72
95
|
## Adding an option
|
|
73
96
|
|
|
74
97
|
Every difference between two scaffolds is a `Feature` — files to overlay, dependencies, scripts,
|
|
@@ -77,37 +100,19 @@ edits and no new machinery:
|
|
|
77
100
|
|
|
78
101
|
1. A `Feature` in `src/features/` — `templates/<id>` for the files it brings, plus whatever it adds to the
|
|
79
102
|
manifest.
|
|
80
|
-
2. The template directory itself, as **real files**. They are copied over the base, so an overlay replaces
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
2. The template directory itself, as **real files**. They are copied over the base, so an overlay replaces a
|
|
104
|
+
file rather than patching it, and template files stay valid TypeScript that an editor can check. `_name`
|
|
105
|
+
becomes `.name` on write, because npm strips a literal `.gitignore` out of a published tarball.
|
|
83
106
|
3. A prompt in `src/cli.ts`, if it deserves a question of its own — and a flag, which it always does.
|
|
84
107
|
|
|
85
|
-
`plan(answers)` is pure: answers in, a `Map` of path → contents out, no directory touched. That is what
|
|
86
|
-
makes the whole matrix of options testable in milliseconds (`test/plan.test.mjs`) and `--dry-run` free. It
|
|
87
|
-
|
|
108
|
+
`plan(answers, pm)` is pure: answers in, a `Map` of path → contents out, no directory touched. That is what
|
|
109
|
+
makes the whole matrix of options testable in milliseconds (`test/plan.test.mjs`) and `--dry-run` free. It is
|
|
110
|
+
also exported, for a tool that wants to scaffold without the prompts:
|
|
88
111
|
|
|
89
112
|
```ts
|
|
90
113
|
import { plan, writePlan } from '@rshono/create';
|
|
91
114
|
```
|
|
92
115
|
|
|
93
|
-
## Two things worth knowing
|
|
94
|
-
|
|
95
|
-
**The ESLint preset pins TypeScript 6.** Linting TypeScript with ESLint means `typescript-eslint`, which
|
|
96
|
-
reads the compiler API directly rather than through a stable interface, so its peer range is
|
|
97
|
-
`typescript >=4.8.4 <6.1.0` — below the TypeScript rshono is built and tested against. An app that chooses
|
|
98
|
-
ESLint therefore gets `typescript ~6.0.3`, the newest that range allows: `npm install` would otherwise fail
|
|
99
|
-
outright on the conflict, and forcing past it hands you a linter running against a compiler API it was never
|
|
100
|
-
built for. The framework's declarations compile identically under either version, which is what makes this
|
|
101
|
-
the app's pin and not the framework's — every other preset leaves TypeScript alone. What you trade for
|
|
102
|
-
type-aware rules is a compiler one major behind, and the JavaScript implementation rather than the native
|
|
103
|
-
one, so `typecheck` on a large app is several times slower. When upstream widens the range, the pin in
|
|
104
|
-
`features/quality.ts` is the only thing to delete.
|
|
105
|
-
|
|
106
|
-
**Tailwind is four packages, a `postcss.config.mjs` and one rule in `rshono.config.ts`.** rshono compiles
|
|
107
|
-
CSS natively and has no PostCSS in it — that is deliberate, and it means an app that wants a plugin chain
|
|
108
|
-
brings its own, through the `rspack` hook. The overlay writes all of it, with a comment saying what to
|
|
109
|
-
delete to go back to plain CSS.
|
|
110
|
-
|
|
111
116
|
## Development
|
|
112
117
|
|
|
113
118
|
```bash
|
|
@@ -117,9 +122,13 @@ CREATE_RSHONO_E2E=1 pnpm --filter @rshono/create test # also: pack, install an
|
|
|
117
122
|
```
|
|
118
123
|
|
|
119
124
|
`plan.test.mjs` is the fast one — the whole matrix of answers in memory, no directory touched.
|
|
120
|
-
`cli.test.mjs` spawns the real bin against temp directories, which is where argument parsing and the
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
`cli.test.mjs` spawns the real bin against temp directories, which is where argument parsing and the refusal
|
|
126
|
+
to overwrite somebody's files are checked. `e2e.test.mjs` is the opt-in one, and the only one that installs
|
|
127
|
+
anything.
|
|
128
|
+
|
|
129
|
+
`src/generated/framework.ts` — the rshono version, the Node range, the dependency pins and the deploy target
|
|
130
|
+
list — is written by `scripts/codegen.mjs` from `packages/core`, so `pnpm --filter @rshono/create codegen` is
|
|
131
|
+
what picks up a change there.
|
|
123
132
|
|
|
124
133
|
`@clack/prompts` (MIT) is bundled rather than depended on, so `npx @rshono/create` downloads one tarball
|
|
125
134
|
before it can ask its first question.
|
package/bin/create-rshono.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// parse and run on whatever Node the user happens to have. The CLI itself is a bundle — its imports
|
|
4
4
|
// are evaluated before any statement in it, so a version check *inside* it would be too late to
|
|
5
5
|
// produce anything friendlier than a stack trace.
|
|
6
|
-
|
|
6
|
+
// Kept in step with `engines.node` in @rshono/core's manifest, which is what a scaffolded app pins.
|
|
7
|
+
var MINIMUM = [22, 18, 0];
|
|
7
8
|
|
|
8
9
|
var current = process.versions.node.split('.').map(Number);
|
|
9
10
|
var older = false;
|
|
@@ -20,8 +21,9 @@ if (older) {
|
|
|
20
21
|
' or newer — you are on ' +
|
|
21
22
|
process.versions.node +
|
|
22
23
|
'.\n' +
|
|
23
|
-
'rshono itself requires it
|
|
24
|
-
'so scaffolding on this version
|
|
24
|
+
'rshono itself requires it (native TypeScript stripping loads rshono.config.ts, and the dev\n' +
|
|
25
|
+
'server needs process.loadEnvFile and Promise.withResolvers), so scaffolding on this version\n' +
|
|
26
|
+
'would produce an app that cannot start.\n',
|
|
25
27
|
);
|
|
26
28
|
process.exit(1);
|
|
27
29
|
}
|
package/dist/api.mjs
CHANGED
|
@@ -79,7 +79,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
79
79
|
|
|
80
80
|
;// CONCATENATED MODULE: ./src/generated/framework.ts
|
|
81
81
|
// GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
|
|
82
|
-
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.
|
|
82
|
+
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.8';
|
|
83
83
|
/**
|
|
84
84
|
* The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
|
|
85
85
|
* or a contributor on an older Node hears it from their package manager rather than from a stack trace.
|
|
@@ -148,12 +148,11 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
148
148
|
};
|
|
149
149
|
/**
|
|
150
150
|
* The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
|
|
151
|
-
* {@link FRAMEWORK_DEPS}
|
|
151
|
+
* {@link FRAMEWORK_DEPS}.
|
|
152
152
|
*
|
|
153
|
-
* typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface,
|
|
154
|
-
* it accepts `typescript >=4.8.4 <6.1.0` and nothing above
|
|
155
|
-
*
|
|
156
|
-
* rshono's declarations compile the same under either, which is what makes this pin an app's business
|
|
153
|
+
* typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface,
|
|
154
|
+
* so it accepts `typescript >=4.8.4 <6.1.0` and nothing above; `~6.0.3` is the newest that satisfies
|
|
155
|
+
* it. rshono's declarations compile the same under either, which is what makes this an app's business
|
|
157
156
|
* and not the framework's.
|
|
158
157
|
*
|
|
159
158
|
* When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
|
|
@@ -162,17 +161,13 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
162
161
|
;// CONCATENATED MODULE: ./src/features/deploy.ts
|
|
163
162
|
|
|
164
163
|
/**
|
|
165
|
-
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts
|
|
166
|
-
* to read, and the template's to carry.
|
|
164
|
+
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
|
|
167
165
|
*
|
|
168
|
-
* Deliberately thin
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* So a target contributes only what is true of it: the command that runs or ships the build, the CLI
|
|
174
|
-
* that command needs installed locally, the directories its platform leaves behind for `.gitignore`, and
|
|
175
|
-
* a note for the step no command covers. Several contribute just one of those.
|
|
166
|
+
* Deliberately thin: the framework arranges its own output for every platform, and `rshono build`
|
|
167
|
+
* writes the one platform config that has to exist (`wrangler.jsonc`, dated the day it ran) if the
|
|
168
|
+
* project has none — a second copy generated here would only go stale. So a target contributes the
|
|
169
|
+
* command that ships the build, the CLI that command needs, the directories to gitignore, and a note
|
|
170
|
+
* for the step no command covers. Several contribute just one of those.
|
|
176
171
|
*/ const DEPLOY_FEATURES = {
|
|
177
172
|
// Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
|
|
178
173
|
// guess — so the target contributes only the command that runs what was built.
|
|
@@ -187,9 +182,11 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
187
182
|
devDependencies: {
|
|
188
183
|
wrangler: TOOL_VERSIONS.wrangler
|
|
189
184
|
},
|
|
190
|
-
//
|
|
191
|
-
//
|
|
185
|
+
// The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each
|
|
186
|
+
// one merely picks the platform binary out of the optional dependency that already carries it, so
|
|
187
|
+
// neither needs to run — `workerd --version` and `esbuild --version` both answer without it.
|
|
192
188
|
allowBuilds: {
|
|
189
|
+
esbuild: false,
|
|
193
190
|
workerd: false
|
|
194
191
|
},
|
|
195
192
|
scripts: {
|
|
@@ -276,12 +273,11 @@ const OXLINT = {
|
|
|
276
273
|
}
|
|
277
274
|
};
|
|
278
275
|
/**
|
|
279
|
-
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
280
|
-
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one
|
|
281
|
-
* peer range accepts (see {@link ESLINT_TYPESCRIPT}).
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* so the config it ships hands the whole program to the parser rather than linting file by file.
|
|
276
|
+
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
277
|
+
* be installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one
|
|
278
|
+
* its peer range accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to
|
|
279
|
+
* reach for ESLint over a syntax-only linter — so the config it ships hands the parser the whole
|
|
280
|
+
* program rather than linting file by file.
|
|
285
281
|
*/ const ESLINT = {
|
|
286
282
|
id: 'eslint',
|
|
287
283
|
overlays: [
|
|
@@ -337,14 +333,13 @@ function linterFeature(linter) {
|
|
|
337
333
|
;// CONCATENATED MODULE: ./src/features/styling.ts
|
|
338
334
|
|
|
339
335
|
/**
|
|
340
|
-
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
341
|
-
* an overlay carrying
|
|
342
|
-
*
|
|
343
|
-
*
|
|
336
|
+
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
337
|
+
* plus an overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts
|
|
338
|
+
* postcss-loader in front of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten
|
|
339
|
+
* in utilities.
|
|
344
340
|
*
|
|
345
341
|
* `postcss` and `postcss-loader` are the app's dependencies rather than the framework's — rshono
|
|
346
|
-
* compiles CSS natively
|
|
347
|
-
* install one.
|
|
342
|
+
* compiles CSS natively, so an app that does not want a plugin chain does not install one.
|
|
348
343
|
*/ const TAILWIND = {
|
|
349
344
|
id: 'tailwind',
|
|
350
345
|
overlays: [
|
|
@@ -503,35 +498,26 @@ function sorted(record) {
|
|
|
503
498
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
504
499
|
}
|
|
505
500
|
/**
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
* the optional dependency that already carries it — rshono's own repo denies it for the same reason.
|
|
509
|
-
*/ const BASE_ALLOW_BUILDS = {
|
|
510
|
-
esbuild: false
|
|
511
|
-
};
|
|
512
|
-
/**
|
|
513
|
-
* pnpm's settings for the new app — written for pnpm and for nobody else.
|
|
501
|
+
* pnpm's settings for the new app, written only when a feature has something to put in them. `null`
|
|
502
|
+
* means there is nothing to say, so no file is written.
|
|
514
503
|
*
|
|
515
|
-
* It exists for one field
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
* `pnpm approve-builds` before it has rendered a page once.
|
|
504
|
+
* It exists for one field, `allowBuilds`. pnpm fails an install — and every `pnpm dev` after it —
|
|
505
|
+
* until the project has said whether a dependency's install script should run. Nothing rshono itself
|
|
506
|
+
* installs has one, so most apps get no file; the packages that do (wrangler's esbuild and workerd)
|
|
507
|
+
* declare their answer on the feature that brings them.
|
|
520
508
|
*
|
|
521
|
-
*
|
|
522
|
-
* package projects included. What lands here is a decision about *this* app: a scaffolded file the app
|
|
523
|
-
* owns from then on, not something the framework reaches back into.
|
|
509
|
+
* A file rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
524
510
|
*/ function buildPnpmSettings(features) {
|
|
525
|
-
const allowBuilds = {
|
|
526
|
-
...BASE_ALLOW_BUILDS
|
|
527
|
-
};
|
|
511
|
+
const allowBuilds = {};
|
|
528
512
|
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
513
|
+
const entries = Object.entries(sorted(allowBuilds));
|
|
514
|
+
if (entries.length === 0) return null;
|
|
529
515
|
return [
|
|
530
516
|
'# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
|
|
531
517
|
'# fails the install rather than skip one quietly — so anything added later belongs here too.',
|
|
532
518
|
'# `false` means the script was looked at: these ship their real binary as an optional dependency.',
|
|
533
519
|
'allowBuilds:',
|
|
534
|
-
...
|
|
520
|
+
...entries.map(([name, allowed])=>` ${name}: ${allowed}`),
|
|
535
521
|
''
|
|
536
522
|
].join('\n');
|
|
537
523
|
}
|
|
@@ -587,10 +573,9 @@ function sorted(record) {
|
|
|
587
573
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
588
574
|
|
|
589
575
|
/**
|
|
590
|
-
* `{{NAME}}`,
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
* any of the formats these templates are written in.
|
|
576
|
+
* `{{NAME}}`, deliberately not `__NAME__`: templates are real files that real tools run over, and in
|
|
577
|
+
* markdown `__NAME__` *is* strong emphasis — Prettier rewrites it to `**NAME**` and the token stops
|
|
578
|
+
* matching. `{{…}}` means nothing to any format these templates are written in.
|
|
594
579
|
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
595
580
|
function tokensFor(answers, pm) {
|
|
596
581
|
return {
|
|
@@ -635,10 +620,9 @@ function readTemplateDir(dir) {
|
|
|
635
620
|
/**
|
|
636
621
|
* `_gitignore` → `.gitignore`, and so on for every dotfile.
|
|
637
622
|
*
|
|
638
|
-
* npm strips a literal `.gitignore` out of a published tarball, so a template cannot
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
* fix. It applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
623
|
+
* npm strips a literal `.gitignore` out of a published tarball, so a template cannot contain one — it
|
|
624
|
+
* would exist in the repo, pass every local test, and be missing from the package everybody installs.
|
|
625
|
+
* Applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
642
626
|
*/ function undotted(path) {
|
|
643
627
|
const segments = path.split(posix.sep);
|
|
644
628
|
const name = segments.pop();
|
|
@@ -678,7 +662,9 @@ function readTemplateDir(dir) {
|
|
|
678
662
|
const gitignore = files.get('.gitignore');
|
|
679
663
|
if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
|
|
680
664
|
files.set('package.json', buildPackageJson(answers, features, pm));
|
|
681
|
-
|
|
665
|
+
// Only for pnpm, and only when a feature brought an install script to answer for — see `buildPnpmSettings`.
|
|
666
|
+
const pnpmSettings = pm.name === 'pnpm' ? buildPnpmSettings(features) : null;
|
|
667
|
+
if (pnpmSettings) files.set('pnpm-workspace.yaml', pnpmSettings);
|
|
682
668
|
return {
|
|
683
669
|
// Sorted, so both the write order and a test's snapshot are stable.
|
|
684
670
|
files: new Map([
|
|
@@ -724,10 +710,9 @@ function packageManager(name, version) {
|
|
|
724
710
|
};
|
|
725
711
|
}
|
|
726
712
|
/**
|
|
727
|
-
* Which package manager invoked us. Every one of them sets `npm_config_user_agent`
|
|
728
|
-
* spawns
|
|
729
|
-
*
|
|
730
|
-
* `packageManager` field.
|
|
713
|
+
* Which package manager invoked us. Every one of them sets `npm_config_user_agent` on the process it
|
|
714
|
+
* spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm
|
|
715
|
+
* project without asking, and the exact version comes along for the `packageManager` field.
|
|
731
716
|
*
|
|
732
717
|
* Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
733
718
|
*/ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
@@ -766,12 +751,12 @@ function run(pm, args, cwd) {
|
|
|
766
751
|
'Thumbs.db'
|
|
767
752
|
]);
|
|
768
753
|
/**
|
|
769
|
-
* What is already at the target path,
|
|
770
|
-
*
|
|
754
|
+
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind —
|
|
755
|
+
* which is what decides whether scaffolding into it is safe.
|
|
771
756
|
*
|
|
772
|
-
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory
|
|
773
|
-
*
|
|
774
|
-
*
|
|
757
|
+
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory throws
|
|
758
|
+
* rather than reporting an empty list, since `--force` should not write into one either — otherwise
|
|
759
|
+
* `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
775
760
|
*/ function conflictingEntries(dir) {
|
|
776
761
|
const stats = statSync(dir, {
|
|
777
762
|
throwIfNoEntry: false
|
package/dist/cli.mjs
CHANGED
|
@@ -2943,7 +2943,7 @@ function hasGit(cwd) {
|
|
|
2943
2943
|
|
|
2944
2944
|
;// CONCATENATED MODULE: ./src/generated/framework.ts
|
|
2945
2945
|
// GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
|
|
2946
|
-
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.
|
|
2946
|
+
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.8';
|
|
2947
2947
|
/**
|
|
2948
2948
|
* The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
|
|
2949
2949
|
* or a contributor on an older Node hears it from their package manager rather than from a stack trace.
|
|
@@ -3124,12 +3124,11 @@ const QUALITY_PRESETS = [
|
|
|
3124
3124
|
};
|
|
3125
3125
|
/**
|
|
3126
3126
|
* The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
|
|
3127
|
-
* {@link FRAMEWORK_DEPS}
|
|
3127
|
+
* {@link FRAMEWORK_DEPS}.
|
|
3128
3128
|
*
|
|
3129
|
-
* typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface,
|
|
3130
|
-
* it accepts `typescript >=4.8.4 <6.1.0` and nothing above
|
|
3131
|
-
*
|
|
3132
|
-
* rshono's declarations compile the same under either, which is what makes this pin an app's business
|
|
3129
|
+
* typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface,
|
|
3130
|
+
* so it accepts `typescript >=4.8.4 <6.1.0` and nothing above; `~6.0.3` is the newest that satisfies
|
|
3131
|
+
* it. rshono's declarations compile the same under either, which is what makes this an app's business
|
|
3133
3132
|
* and not the framework's.
|
|
3134
3133
|
*
|
|
3135
3134
|
* When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
|
|
@@ -3138,17 +3137,13 @@ const QUALITY_PRESETS = [
|
|
|
3138
3137
|
;// CONCATENATED MODULE: ./src/features/deploy.ts
|
|
3139
3138
|
|
|
3140
3139
|
/**
|
|
3141
|
-
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts
|
|
3142
|
-
* to read, and the template's to carry.
|
|
3140
|
+
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
|
|
3143
3141
|
*
|
|
3144
|
-
* Deliberately thin
|
|
3145
|
-
*
|
|
3146
|
-
*
|
|
3147
|
-
*
|
|
3148
|
-
*
|
|
3149
|
-
* So a target contributes only what is true of it: the command that runs or ships the build, the CLI
|
|
3150
|
-
* that command needs installed locally, the directories its platform leaves behind for `.gitignore`, and
|
|
3151
|
-
* a note for the step no command covers. Several contribute just one of those.
|
|
3142
|
+
* Deliberately thin: the framework arranges its own output for every platform, and `rshono build`
|
|
3143
|
+
* writes the one platform config that has to exist (`wrangler.jsonc`, dated the day it ran) if the
|
|
3144
|
+
* project has none — a second copy generated here would only go stale. So a target contributes the
|
|
3145
|
+
* command that ships the build, the CLI that command needs, the directories to gitignore, and a note
|
|
3146
|
+
* for the step no command covers. Several contribute just one of those.
|
|
3152
3147
|
*/ const DEPLOY_FEATURES = {
|
|
3153
3148
|
// Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
|
|
3154
3149
|
// guess — so the target contributes only the command that runs what was built.
|
|
@@ -3163,9 +3158,11 @@ const QUALITY_PRESETS = [
|
|
|
3163
3158
|
devDependencies: {
|
|
3164
3159
|
wrangler: TOOL_VERSIONS.wrangler
|
|
3165
3160
|
},
|
|
3166
|
-
//
|
|
3167
|
-
//
|
|
3161
|
+
// The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each
|
|
3162
|
+
// one merely picks the platform binary out of the optional dependency that already carries it, so
|
|
3163
|
+
// neither needs to run — `workerd --version` and `esbuild --version` both answer without it.
|
|
3168
3164
|
allowBuilds: {
|
|
3165
|
+
esbuild: false,
|
|
3169
3166
|
workerd: false
|
|
3170
3167
|
},
|
|
3171
3168
|
scripts: {
|
|
@@ -3252,12 +3249,11 @@ const OXLINT = {
|
|
|
3252
3249
|
}
|
|
3253
3250
|
};
|
|
3254
3251
|
/**
|
|
3255
|
-
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
3256
|
-
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one
|
|
3257
|
-
* peer range accepts (see {@link ESLINT_TYPESCRIPT}).
|
|
3258
|
-
*
|
|
3259
|
-
*
|
|
3260
|
-
* so the config it ships hands the whole program to the parser rather than linting file by file.
|
|
3252
|
+
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
3253
|
+
* be installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one
|
|
3254
|
+
* its peer range accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to
|
|
3255
|
+
* reach for ESLint over a syntax-only linter — so the config it ships hands the parser the whole
|
|
3256
|
+
* program rather than linting file by file.
|
|
3261
3257
|
*/ const ESLINT = {
|
|
3262
3258
|
id: 'eslint',
|
|
3263
3259
|
overlays: [
|
|
@@ -3313,14 +3309,13 @@ function linterFeature(linter) {
|
|
|
3313
3309
|
;// CONCATENATED MODULE: ./src/features/styling.ts
|
|
3314
3310
|
|
|
3315
3311
|
/**
|
|
3316
|
-
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
3317
|
-
* an overlay carrying
|
|
3318
|
-
*
|
|
3319
|
-
*
|
|
3312
|
+
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
3313
|
+
* plus an overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts
|
|
3314
|
+
* postcss-loader in front of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten
|
|
3315
|
+
* in utilities.
|
|
3320
3316
|
*
|
|
3321
3317
|
* `postcss` and `postcss-loader` are the app's dependencies rather than the framework's — rshono
|
|
3322
|
-
* compiles CSS natively
|
|
3323
|
-
* install one.
|
|
3318
|
+
* compiles CSS natively, so an app that does not want a plugin chain does not install one.
|
|
3324
3319
|
*/ const TAILWIND = {
|
|
3325
3320
|
id: 'tailwind',
|
|
3326
3321
|
overlays: [
|
|
@@ -3390,35 +3385,26 @@ function sorted(record) {
|
|
|
3390
3385
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
3391
3386
|
}
|
|
3392
3387
|
/**
|
|
3393
|
-
*
|
|
3394
|
-
*
|
|
3395
|
-
* the optional dependency that already carries it — rshono's own repo denies it for the same reason.
|
|
3396
|
-
*/ const BASE_ALLOW_BUILDS = {
|
|
3397
|
-
esbuild: false
|
|
3398
|
-
};
|
|
3399
|
-
/**
|
|
3400
|
-
* pnpm's settings for the new app — written for pnpm and for nobody else.
|
|
3388
|
+
* pnpm's settings for the new app, written only when a feature has something to put in them. `null`
|
|
3389
|
+
* means there is nothing to say, so no file is written.
|
|
3401
3390
|
*
|
|
3402
|
-
* It exists for one field
|
|
3403
|
-
*
|
|
3404
|
-
*
|
|
3405
|
-
*
|
|
3406
|
-
* `pnpm approve-builds` before it has rendered a page once.
|
|
3391
|
+
* It exists for one field, `allowBuilds`. pnpm fails an install — and every `pnpm dev` after it —
|
|
3392
|
+
* until the project has said whether a dependency's install script should run. Nothing rshono itself
|
|
3393
|
+
* installs has one, so most apps get no file; the packages that do (wrangler's esbuild and workerd)
|
|
3394
|
+
* declare their answer on the feature that brings them.
|
|
3407
3395
|
*
|
|
3408
|
-
*
|
|
3409
|
-
* package projects included. What lands here is a decision about *this* app: a scaffolded file the app
|
|
3410
|
-
* owns from then on, not something the framework reaches back into.
|
|
3396
|
+
* A file rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
3411
3397
|
*/ function buildPnpmSettings(features) {
|
|
3412
|
-
const allowBuilds = {
|
|
3413
|
-
...BASE_ALLOW_BUILDS
|
|
3414
|
-
};
|
|
3398
|
+
const allowBuilds = {};
|
|
3415
3399
|
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
3400
|
+
const entries = Object.entries(sorted(allowBuilds));
|
|
3401
|
+
if (entries.length === 0) return null;
|
|
3416
3402
|
return [
|
|
3417
3403
|
'# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
|
|
3418
3404
|
'# fails the install rather than skip one quietly — so anything added later belongs here too.',
|
|
3419
3405
|
'# `false` means the script was looked at: these ship their real binary as an optional dependency.',
|
|
3420
3406
|
'allowBuilds:',
|
|
3421
|
-
...
|
|
3407
|
+
...entries.map(([name, allowed])=>` ${name}: ${allowed}`),
|
|
3422
3408
|
''
|
|
3423
3409
|
].join('\n');
|
|
3424
3410
|
}
|
|
@@ -3474,10 +3460,9 @@ function sorted(record) {
|
|
|
3474
3460
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
3475
3461
|
|
|
3476
3462
|
/**
|
|
3477
|
-
* `{{NAME}}`,
|
|
3478
|
-
*
|
|
3479
|
-
*
|
|
3480
|
-
* any of the formats these templates are written in.
|
|
3463
|
+
* `{{NAME}}`, deliberately not `__NAME__`: templates are real files that real tools run over, and in
|
|
3464
|
+
* markdown `__NAME__` *is* strong emphasis — Prettier rewrites it to `**NAME**` and the token stops
|
|
3465
|
+
* matching. `{{…}}` means nothing to any format these templates are written in.
|
|
3481
3466
|
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
3482
3467
|
function tokensFor(answers, pm) {
|
|
3483
3468
|
return {
|
|
@@ -3522,10 +3507,9 @@ function readTemplateDir(dir) {
|
|
|
3522
3507
|
/**
|
|
3523
3508
|
* `_gitignore` → `.gitignore`, and so on for every dotfile.
|
|
3524
3509
|
*
|
|
3525
|
-
* npm strips a literal `.gitignore` out of a published tarball, so a template cannot
|
|
3526
|
-
*
|
|
3527
|
-
*
|
|
3528
|
-
* fix. It applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
3510
|
+
* npm strips a literal `.gitignore` out of a published tarball, so a template cannot contain one — it
|
|
3511
|
+
* would exist in the repo, pass every local test, and be missing from the package everybody installs.
|
|
3512
|
+
* Applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
3529
3513
|
*/ function undotted(path) {
|
|
3530
3514
|
const segments = path.split(posix.sep);
|
|
3531
3515
|
const name = segments.pop();
|
|
@@ -3565,7 +3549,9 @@ function readTemplateDir(dir) {
|
|
|
3565
3549
|
const gitignore = files.get('.gitignore');
|
|
3566
3550
|
if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
|
|
3567
3551
|
files.set('package.json', buildPackageJson(answers, features, pm));
|
|
3568
|
-
|
|
3552
|
+
// Only for pnpm, and only when a feature brought an install script to answer for — see `buildPnpmSettings`.
|
|
3553
|
+
const pnpmSettings = pm.name === 'pnpm' ? buildPnpmSettings(features) : null;
|
|
3554
|
+
if (pnpmSettings) files.set('pnpm-workspace.yaml', pnpmSettings);
|
|
3569
3555
|
return {
|
|
3570
3556
|
// Sorted, so both the write order and a test's snapshot are stable.
|
|
3571
3557
|
files: new Map([
|
|
@@ -3609,10 +3595,9 @@ function packageManager(name, version) {
|
|
|
3609
3595
|
};
|
|
3610
3596
|
}
|
|
3611
3597
|
/**
|
|
3612
|
-
* Which package manager invoked us. Every one of them sets `npm_config_user_agent`
|
|
3613
|
-
* spawns
|
|
3614
|
-
*
|
|
3615
|
-
* `packageManager` field.
|
|
3598
|
+
* Which package manager invoked us. Every one of them sets `npm_config_user_agent` on the process it
|
|
3599
|
+
* spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm
|
|
3600
|
+
* project without asking, and the exact version comes along for the `packageManager` field.
|
|
3616
3601
|
*
|
|
3617
3602
|
* Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
3618
3603
|
*/ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
@@ -3688,12 +3673,12 @@ function run(pm, args, cwd) {
|
|
|
3688
3673
|
'Thumbs.db'
|
|
3689
3674
|
]);
|
|
3690
3675
|
/**
|
|
3691
|
-
* What is already at the target path,
|
|
3692
|
-
*
|
|
3676
|
+
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind —
|
|
3677
|
+
* which is what decides whether scaffolding into it is safe.
|
|
3693
3678
|
*
|
|
3694
|
-
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory
|
|
3695
|
-
*
|
|
3696
|
-
*
|
|
3679
|
+
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory throws
|
|
3680
|
+
* rather than reporting an empty list, since `--force` should not write into one either — otherwise
|
|
3681
|
+
* `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
3697
3682
|
*/ function conflictingEntries(dir) {
|
|
3698
3683
|
const stats = statSync(dir, {
|
|
3699
3684
|
throwIfNoEntry: false
|
|
@@ -3770,11 +3755,7 @@ function fail(message) {
|
|
|
3770
3755
|
if (off) return false;
|
|
3771
3756
|
return undefined;
|
|
3772
3757
|
}
|
|
3773
|
-
/**
|
|
3774
|
-
* `parseArgs` rejects an unknown flag with a message that names it but nothing else — so a mistyped
|
|
3775
|
-
* `--tailwnid` reads as a wall of text about positional arguments. Pointing at `--help` is the whole
|
|
3776
|
-
* addition.
|
|
3777
|
-
*/ function parse() {
|
|
3758
|
+
/** `parseArgs` names an unknown flag but says nothing about what to do next; this points at `--help`. */ function parse() {
|
|
3778
3759
|
try {
|
|
3779
3760
|
return parseArgs({
|
|
3780
3761
|
options: {
|
|
@@ -3840,12 +3821,11 @@ function fail(message) {
|
|
|
3840
3821
|
async function main() {
|
|
3841
3822
|
const { values, positionals } = parse();
|
|
3842
3823
|
if (values.help) return console.log(HELP);
|
|
3843
|
-
if (values.version) return console.log("1.0.0-rc.
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
*/ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
|
|
3824
|
+
if (values.version) return console.log("1.0.0-rc.8");
|
|
3825
|
+
// A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both
|
|
3826
|
+
// streams have to be a terminal: the prompts draw on stdout but *read from stdin*, so
|
|
3827
|
+
// `echo | npx @rshono/create` would otherwise ask a question with nothing behind the keyboard.
|
|
3828
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
|
|
3849
3829
|
const pmFlag = oneOf(values.pm, PACKAGE_MANAGERS, 'pm');
|
|
3850
3830
|
const pm = pmFlag ? packageManager(pmFlag) : detectPackageManager();
|
|
3851
3831
|
const deployFlag = oneOf(values.deploy, DEPLOY_TARGET_NAMES, 'deploy');
|
|
@@ -3916,10 +3896,9 @@ async function main() {
|
|
|
3916
3896
|
]
|
|
3917
3897
|
}));
|
|
3918
3898
|
}
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
*/ let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
|
|
3899
|
+
// One question instead of two. The axes stay independent underneath: a `--formatter` or `--linter`
|
|
3900
|
+
// flag addresses either on its own, and skips the question entirely.
|
|
3901
|
+
let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
|
|
3923
3902
|
if (!preset && !formatterFlag && !linterFlag) {
|
|
3924
3903
|
const fallback = QUALITY_PRESETS["0"];
|
|
3925
3904
|
if (interactive) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rshono/create",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.8",
|
|
4
4
|
"description": "Scaffold a new rshono app — Hono + Rspack + React Server Components",
|
|
5
5
|
"author": "Lasse <lasse@lassetange.com> (https://www.lassetange.com)",
|
|
6
6
|
"license": "ISC",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"templates"
|
|
37
37
|
],
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": ">=22.
|
|
39
|
+
"node": ">=22.18.0"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@clack/prompts": "^1.7.0",
|
|
46
|
-
"@rspack/core": "2.1.
|
|
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.8"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "node scripts/codegen.mjs && node scripts/build.mjs",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { onServerError } from '@rshono/core/server';
|
|
1
|
+
import { onServerError, publicUrl } from '@rshono/core/server';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
4
|
+
import { csrf } from 'hono/csrf';
|
|
3
5
|
import { trimTrailingSlash } from 'hono/trailing-slash';
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -14,6 +16,15 @@ onServerError((error, { source, request }) => {
|
|
|
14
16
|
console.error(`[error] ${source} ${new URL(request.url).pathname}: ${message}`);
|
|
15
17
|
});
|
|
16
18
|
|
|
19
|
+
/** Caps every request body before anything downstream buffers it. Raise it where you accept uploads. */
|
|
20
|
+
server.use(bodyLimit({ maxSize: 1024 * 1024 }));
|
|
21
|
+
|
|
22
|
+
/** Cross-origin hosts allowed to post server actions, alongside this app's own. */
|
|
23
|
+
const ALLOWED_ORIGINS: string[] = [];
|
|
24
|
+
|
|
25
|
+
/** Rejects a cross-origin POST before it reaches a server action. `publicUrl(c)`, not `c.req.url`: behind a proxy those differ. */
|
|
26
|
+
server.use(csrf({ origin: (origin, c) => origin === publicUrl(c).origin || ALLOWED_ORIGINS.includes(origin) }));
|
|
27
|
+
|
|
17
28
|
/** `/about/` and `/about` should not be two pages. */
|
|
18
29
|
server.use(trimTrailingSlash({ alwaysRedirect: true }));
|
|
19
30
|
|
|
@@ -24,6 +35,9 @@ for (const [from, to] of Object.entries(REDIRECTS)) {
|
|
|
24
35
|
server.get(from, (c) => c.redirect(to, 301));
|
|
25
36
|
}
|
|
26
37
|
|
|
38
|
+
/** A JSON API route, with no page involved. The layout links to it. */
|
|
39
|
+
server.get('/api/health', (c) => c.json({ status: 'ok' }));
|
|
40
|
+
|
|
27
41
|
export default server;
|
|
28
42
|
|
|
29
43
|
/** `hc<AppType>('/')` from `hono/client` gives paths, params and responses typed against the handlers above. */
|