@rshono/create 1.0.0-rc.2 → 1.0.0-rc.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -61
- package/bin/create-rshono.mjs +9 -7
- package/dist/api.mjs +297 -254
- package/dist/cli.mjs +343 -304
- package/package.json +12 -13
- package/templates/base/AGENTS.md +5 -0
- package/templates/base/CLAUDE.md +1 -0
- package/templates/base/README.md +11 -9
- package/templates/base/_env +1 -4
- package/templates/base/_gitignore +1 -0
- package/templates/base/public/favicon.svg +2 -1
- package/templates/base/rshono.config.ts +0 -30
- package/templates/base/src/components/404.tsx +0 -1
- package/templates/base/src/components/500.tsx +0 -6
- package/templates/base/src/components/home.tsx +6 -23
- package/templates/base/src/components/layout.tsx +4 -11
- package/templates/base/src/routes.ts +1 -26
- package/templates/base/src/server.ts +20 -35
- package/templates/base/src/styles.css +1 -64
- package/templates/biome/biome.json +2 -1
- package/templates/biome-tailwind/biome.json +1 -1
- package/templates/eslint/eslint.config.mjs +16 -16
- package/templates/oxfmt/_oxfmtrc.json +1 -1
- package/templates/tailwind/postcss.config.mjs +0 -4
- package/templates/tailwind/rshono.config.ts +1 -37
- package/templates/tailwind/src/components/home.tsx +6 -23
- package/templates/tailwind/src/components/layout.tsx +4 -11
- package/templates/tailwind/src/styles.css +0 -25
- package/templates/base/src/actions.ts +0 -22
- package/templates/base/src/components/greet-form.tsx +0 -27
- package/templates/base/src/lib/env.ts +0 -26
package/dist/api.mjs
CHANGED
|
@@ -52,12 +52,12 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
52
52
|
|
|
53
53
|
;// CONCATENATED MODULE: ./src/features/combinations.ts
|
|
54
54
|
/**
|
|
55
|
-
* Biome's CSS parser rejects Tailwind's syntax — `@apply`
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* Biome's CSS parser rejects Tailwind's syntax — `@apply` is a parse error, not an unknown-at-rule warning — so
|
|
56
|
+
* a project with both needs Biome pointed away from stylesheets. The overlay is a second `biome.json` listed
|
|
57
|
+
* after the first.
|
|
58
58
|
*
|
|
59
|
-
* Narrow on purpose:
|
|
60
|
-
*
|
|
59
|
+
* Narrow on purpose: excluding CSS for everybody would give up formatting the plain-CSS template that Biome
|
|
60
|
+
* handles perfectly well.
|
|
61
61
|
*/ const BIOME_TAILWIND = {
|
|
62
62
|
id: 'biome-tailwind',
|
|
63
63
|
overlays: [
|
|
@@ -77,24 +77,91 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
77
77
|
] : [];
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
;// CONCATENATED MODULE: ./src/scripts.ts
|
|
81
|
+
// `features/types.js` rather than `features/index.js`: the deploy feature imports `invoke` from here, and
|
|
82
|
+
// going through the barrel would make that a cycle — types.ts imports nothing.
|
|
83
|
+
/**
|
|
84
|
+
* The scripts every app gets, whatever it targets.
|
|
85
|
+
*
|
|
86
|
+
* `start`, `preview` and `deploy` are not here: each means something different per platform, so the deploy
|
|
87
|
+
* target contributes its own. The three names are a contract the targets keep, which is what lets the README
|
|
88
|
+
* describe an app it was not written for:
|
|
89
|
+
*
|
|
90
|
+
* - **`start`** runs a build that already exists and never makes one — what a host's own start command calls.
|
|
91
|
+
* - **`preview`** builds, then runs the result here, so the production build is answerable without deploying.
|
|
92
|
+
* - **`deploy`** builds, then ships it — where the platform has one command that does the shipping.
|
|
93
|
+
*/ const BASE_SCRIPTS = {
|
|
94
|
+
dev: 'rshono dev',
|
|
95
|
+
build: 'rshono build',
|
|
96
|
+
typecheck: 'tsc --noEmit'
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Every script the app gets, in the order they are written: the base ones, then each feature's. The manifest and
|
|
100
|
+
* the README's command table are both this, so neither can document a script the other lacks.
|
|
101
|
+
*/ function buildScripts(features) {
|
|
102
|
+
const scripts = {
|
|
103
|
+
...BASE_SCRIPTS
|
|
104
|
+
};
|
|
105
|
+
for (const feature of features)Object.assign(scripts, feature.scripts);
|
|
106
|
+
return scripts;
|
|
107
|
+
}
|
|
108
|
+
/** The gloss for each base script, in the README's command table. `build` names the target it is for. */ function baseScriptHelp(deploy) {
|
|
109
|
+
return {
|
|
110
|
+
dev: 'dev server with HMR, http://localhost:3000',
|
|
111
|
+
build: `production build for ${deploy}`,
|
|
112
|
+
typecheck: 'tsc --noEmit'
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Script names a package manager has a command of its own for: `pnpm deploy` runs pnpm's workspace-deploy and
|
|
117
|
+
* never looks at the manifest. These get the explicit `run` form, which all four managers accept.
|
|
118
|
+
*/ const SHADOWED = new Set([
|
|
119
|
+
'deploy'
|
|
120
|
+
]);
|
|
121
|
+
/** How to type one of the app's scripts with this package manager — `pnpm dev`, but `npm run dev`. */ function invoke(pm, script) {
|
|
122
|
+
return `${SHADOWED.has(script) ? `${pm.name} run` : pm.run} ${script}`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The README's command table: one line per script, with the command to type and a one-line gloss. A script whose
|
|
126
|
+
* feature supplies no `scriptHelp` is left out — that is how `format:check` stays out of a table about running
|
|
127
|
+
* the app — and covered by the line pointing at `package.json`.
|
|
128
|
+
*/ function scriptTable(answers, features, pm) {
|
|
129
|
+
const help = baseScriptHelp(answers.deploy);
|
|
130
|
+
for (const feature of features)Object.assign(help, feature.scriptHelp);
|
|
131
|
+
const documented = Object.keys(buildScripts(features)).filter((name)=>help[name]);
|
|
132
|
+
const width = Math.max(...documented.map((name)=>invoke(pm, name).length));
|
|
133
|
+
return documented.map((name)=>`${invoke(pm, name).padEnd(width)} # ${help[name]}`).join('\n');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The command that gets this app into production, as a sentence — the README's deploy step and the closing
|
|
137
|
+
* summary's, so the two cannot name different commands.
|
|
138
|
+
*
|
|
139
|
+
* Read off the scripts rather than the target, so a target that gains a `deploy` gains the sentence with it.
|
|
140
|
+
*/ function deployStep(features, pm) {
|
|
141
|
+
const scripts = buildScripts(features);
|
|
142
|
+
if (scripts.deploy) return `\`${invoke(pm, 'deploy')}\` does the build and the upload in one step.`;
|
|
143
|
+
if (scripts.start) return `\`${invoke(pm, 'start')}\` runs that build wherever you host it, and never makes one.`;
|
|
144
|
+
if (scripts.preview) return `\`${invoke(pm, 'preview')}\` runs the build here, so you can check it first.`;
|
|
145
|
+
return `\`${invoke(pm, 'build')}\` produces it; getting it there is yours to script.`;
|
|
146
|
+
}
|
|
147
|
+
|
|
80
148
|
;// CONCATENATED MODULE: ./src/generated/framework.ts
|
|
81
149
|
// 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.
|
|
150
|
+
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.20';
|
|
83
151
|
/**
|
|
84
|
-
* The Node range rshono
|
|
85
|
-
*
|
|
86
|
-
*/ const NODE_ENGINE = '>=22.
|
|
152
|
+
* The Node range rshono declares, restated in every scaffolded app's `engines` — so a CI image or a contributor
|
|
153
|
+
* on an older Node hears it from their package manager rather than from a stack trace.
|
|
154
|
+
*/ const NODE_ENGINE = '>=22.18.0';
|
|
87
155
|
/**
|
|
88
|
-
* The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
|
|
89
|
-
*
|
|
90
|
-
* overrides to fall back on.
|
|
156
|
+
* The dependency versions rshono is tested against, copied from its own manifest. Exact where it is exact:
|
|
157
|
+
* React's RSC internals are coupled across builds, and a generated app has no workspace overrides.
|
|
91
158
|
*/ const FRAMEWORK_DEPS = {
|
|
92
|
-
hono: '^4.
|
|
159
|
+
hono: '^4.13.5',
|
|
93
160
|
react: '19.2.8',
|
|
94
161
|
'react-dom': '19.2.8',
|
|
95
162
|
typescript: '^7.0.2',
|
|
96
|
-
'@types/node': '^26.
|
|
97
|
-
'@types/react': '^19.2.
|
|
163
|
+
'@types/node': '^26.4.0',
|
|
164
|
+
'@types/react': '^19.2.18'
|
|
98
165
|
};
|
|
99
166
|
/** Every `deploy` target the installed framework knows, with the command that ships what it built. */ const DEPLOY_TARGETS = [
|
|
100
167
|
{
|
|
@@ -105,22 +172,10 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
105
172
|
name: 'cloudflare',
|
|
106
173
|
hint: 'deploy with `wrangler deploy`'
|
|
107
174
|
},
|
|
108
|
-
{
|
|
109
|
-
name: 'bun',
|
|
110
|
-
hint: 'run `bun dist/server/main.mjs`'
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
name: 'deno',
|
|
114
|
-
hint: 'run `deno serve -A dist/server/main.mjs`'
|
|
115
|
-
},
|
|
116
175
|
{
|
|
117
176
|
name: 'vercel',
|
|
118
177
|
hint: 'deploy with `vercel deploy --prebuilt`'
|
|
119
178
|
},
|
|
120
|
-
{
|
|
121
|
-
name: 'netlify',
|
|
122
|
-
hint: 'deploy with `netlify deploy --build=false --dir=.netlify/publish`'
|
|
123
|
-
},
|
|
124
179
|
{
|
|
125
180
|
name: 'aws-lambda',
|
|
126
181
|
hint: 'zip dist/ with the handler at dist/server/main.mjs'
|
|
@@ -132,14 +187,12 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
132
187
|
/** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
|
|
133
188
|
/** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
|
|
134
189
|
/**
|
|
135
|
-
* Versions for the optional tooling the features can add — the one place in this package where a
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* which is generated from rshono's own manifest and must not be edited here.
|
|
190
|
+
* Versions for the optional tooling the features can add — the one place in this package where a range is typed
|
|
191
|
+
* by hand, because the framework declares none of these. Everything a scaffolded app needs to *run* rshono comes
|
|
192
|
+
* from {@link FRAMEWORK_DEPS}, generated from rshono's own manifest.
|
|
139
193
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* was last touched.
|
|
194
|
+
* Caret ranges, not exact: these are the app's own dev tools, and a scaffold made six months from now should
|
|
195
|
+
* pick up their patch releases.
|
|
143
196
|
*/ const TOOL_VERSIONS = {
|
|
144
197
|
tailwindcss: '^4.3.3',
|
|
145
198
|
'@tailwindcss/postcss': '^4.3.3',
|
|
@@ -151,126 +204,148 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
151
204
|
oxlint: '^1.76.0',
|
|
152
205
|
oxfmt: '^0.61.0',
|
|
153
206
|
eslint: '^10.8.0',
|
|
154
|
-
// ESLint's
|
|
155
|
-
//
|
|
207
|
+
// ESLint's recommended JavaScript rules, which typescript-eslint layers on rather than replaces, and the rules
|
|
208
|
+
// of hooks — the one class of React mistake no type checker sees.
|
|
156
209
|
'@eslint/js': '^10.0.1',
|
|
157
210
|
'typescript-eslint': '^8.65.0',
|
|
158
211
|
'eslint-plugin-react-hooks': '^7.1.1',
|
|
159
212
|
wrangler: '^4.115.0'
|
|
160
213
|
};
|
|
161
214
|
/**
|
|
162
|
-
* The TypeScript an ESLint app pins
|
|
163
|
-
* {@link FRAMEWORK_DEPS}
|
|
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.
|
|
215
|
+
* The TypeScript an ESLint app pins in place of the framework's own — the one deliberate exception to
|
|
216
|
+
* {@link FRAMEWORK_DEPS}.
|
|
170
217
|
*
|
|
171
|
-
*
|
|
218
|
+
* typescript-eslint reads TypeScript's compiler API directly, so it accepts `>=4.8.4 <6.1.0` and nothing above;
|
|
219
|
+
* `~6.0.3` is the newest that satisfies it. rshono's declarations compile the same under either, which is what
|
|
220
|
+
* makes this the app's business. When upstream widens its range, this constant is what to delete.
|
|
172
221
|
*/ const ESLINT_TYPESCRIPT = '~6.0.3';
|
|
173
222
|
|
|
174
223
|
;// CONCATENATED MODULE: ./src/features/deploy.ts
|
|
175
224
|
|
|
225
|
+
|
|
176
226
|
/**
|
|
177
|
-
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts
|
|
178
|
-
* to read, and the template's to carry.
|
|
227
|
+
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
|
|
179
228
|
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* be a copy that goes stale.
|
|
229
|
+
* Thin on purpose: the framework arranges its own output for every platform, and `rshono build` writes the one
|
|
230
|
+
* platform config that has to exist. So a target contributes the commands that run and ship the build, the CLI
|
|
231
|
+
* they need, the directories to gitignore, and a note for the step no command covers.
|
|
184
232
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*/
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
start: 'deno serve -A dist/server/main.mjs'
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
cloudflare: {
|
|
212
|
-
id: 'deploy-cloudflare',
|
|
213
|
-
devDependencies: {
|
|
214
|
-
wrangler: TOOL_VERSIONS.wrangler
|
|
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
|
-
},
|
|
221
|
-
scripts: {
|
|
222
|
-
deploy: 'rshono build && wrangler deploy'
|
|
233
|
+
* What the three script names promise is documented above `BASE_SCRIPTS` in `scripts.ts`. Only `node` has a
|
|
234
|
+
* `start` — `rshono start` refuses a bundle built for anywhere else — and it needs no `preview`, since `build`
|
|
235
|
+
* then `start` already is one.
|
|
236
|
+
*/ function deployFeatures(pm) {
|
|
237
|
+
const build = invoke(pm, 'build');
|
|
238
|
+
return {
|
|
239
|
+
// Where a Node build goes next is a Dockerfile or a process manager, neither of which this can guess.
|
|
240
|
+
node: {
|
|
241
|
+
id: 'deploy-node',
|
|
242
|
+
scripts: {
|
|
243
|
+
start: 'rshono start'
|
|
244
|
+
},
|
|
245
|
+
scriptHelp: {
|
|
246
|
+
start: 'run the build that exists — what your host calls'
|
|
247
|
+
},
|
|
248
|
+
platformSetup: [
|
|
249
|
+
'The two commands a host asks for:',
|
|
250
|
+
'',
|
|
251
|
+
`- **Build** — \`${pm.name} install && ${build}\``,
|
|
252
|
+
`- **Start** — \`${invoke(pm, 'start')}\``,
|
|
253
|
+
'',
|
|
254
|
+
`In a Dockerfile, the same pair: \`RUN ${build}\`, then \`CMD ["${pm.name}", "start"]\`.`
|
|
255
|
+
].join('\n')
|
|
223
256
|
},
|
|
224
|
-
|
|
225
|
-
'
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
257
|
+
cloudflare: {
|
|
258
|
+
id: 'deploy-cloudflare',
|
|
259
|
+
devDependencies: {
|
|
260
|
+
wrangler: TOOL_VERSIONS.wrangler
|
|
261
|
+
},
|
|
262
|
+
// The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each merely
|
|
263
|
+
// picks the platform binary out of the optional dependency already carrying it, so neither needs to run.
|
|
264
|
+
allowBuilds: {
|
|
265
|
+
esbuild: false,
|
|
266
|
+
workerd: false
|
|
267
|
+
},
|
|
268
|
+
// `wrangler dev` is the one preview that runs the code in workerd rather than Node. Both scripts read the
|
|
269
|
+
// wrangler.jsonc the build wrote, so nothing here has to know where the bundle went.
|
|
270
|
+
scripts: {
|
|
271
|
+
preview: 'rshono build && wrangler dev',
|
|
272
|
+
deploy: 'rshono build && wrangler deploy'
|
|
273
|
+
},
|
|
274
|
+
scriptHelp: {
|
|
275
|
+
preview: 'build, then run it in workerd — port 8787',
|
|
276
|
+
deploy: 'build, then ship it to Cloudflare'
|
|
277
|
+
},
|
|
278
|
+
gitignore: [
|
|
279
|
+
'.wrangler/'
|
|
280
|
+
],
|
|
281
|
+
notes: [
|
|
282
|
+
'The first build writes wrangler.jsonc — yours to edit after that.'
|
|
283
|
+
],
|
|
284
|
+
platformSetup: [
|
|
285
|
+
`Building from a git repo instead: set Workers Builds' **Build command** to \`${build}\`.`,
|
|
286
|
+
'Its deploy command already defaults to `npx wrangler deploy`, and it installs dependencies itself.'
|
|
287
|
+
].join('\n')
|
|
235
288
|
},
|
|
236
|
-
|
|
237
|
-
'
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
289
|
+
vercel: {
|
|
290
|
+
id: 'deploy-vercel',
|
|
291
|
+
// `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview URL.
|
|
292
|
+
// `preview` is a Node build run here — the platform cannot run its own prebuilt output on your machine.
|
|
293
|
+
scripts: {
|
|
294
|
+
preview: 'rshono build --deploy node && rshono start',
|
|
295
|
+
deploy: `rshono build && ${pm.dlx} vercel deploy --prebuilt --prod`
|
|
296
|
+
},
|
|
297
|
+
scriptHelp: {
|
|
298
|
+
preview: 'build for Node and run that here',
|
|
299
|
+
deploy: 'build, then upload it to production'
|
|
300
|
+
},
|
|
301
|
+
gitignore: [
|
|
302
|
+
'.vercel/'
|
|
303
|
+
],
|
|
304
|
+
notes: [
|
|
305
|
+
'--prebuilt uploads what rshono build assembled; the platform must not rebuild it.',
|
|
306
|
+
'Drop --prod from the deploy script for a preview URL instead.'
|
|
307
|
+
],
|
|
308
|
+
platformSetup: [
|
|
309
|
+
`Deploying from CI instead: the same \`${invoke(pm, 'deploy')}\`, with \`VERCEL_ORG_ID\`, \`VERCEL_PROJECT_ID\``,
|
|
310
|
+
'and a CLI token in the environment.',
|
|
311
|
+
'',
|
|
312
|
+
`If you let Vercel build the repo itself, set **Framework Preset** to Other — \`hono\` is otherwise`,
|
|
313
|
+
`detected as the Hono preset — and **Build Command** to \`${build}\`.`
|
|
314
|
+
].join('\n')
|
|
247
315
|
},
|
|
248
|
-
|
|
249
|
-
'
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
316
|
+
'aws-lambda': {
|
|
317
|
+
id: 'deploy-aws-lambda',
|
|
318
|
+
// No CLI to wrap and no upload to guess at, but `preview` still applies: the bundle is a Node handler.
|
|
319
|
+
scripts: {
|
|
320
|
+
preview: 'rshono build --deploy node && rshono start'
|
|
321
|
+
},
|
|
322
|
+
scriptHelp: {
|
|
323
|
+
preview: 'build for Node and run that here'
|
|
324
|
+
},
|
|
325
|
+
notes: [
|
|
326
|
+
'Use a Function URL in RESPONSE_STREAM mode — a buffered invoke mode drops the streaming.'
|
|
327
|
+
],
|
|
328
|
+
platformSetup: [
|
|
329
|
+
`There is no settings page here; the upload is yours to script. A job needs \`${pm.name} install && ${build}\`,`,
|
|
330
|
+
'then the function package: `dist/` and nothing else, with the handler at `dist/server/main.mjs`. Your',
|
|
331
|
+
'dependencies are compiled into the bundle for this target, so no `node_modules` is uploaded — which also',
|
|
332
|
+
'means a native addon fails the build here rather than the deploy.'
|
|
333
|
+
].join('\n')
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function deployFeature(target, pm) {
|
|
338
|
+
return deployFeatures(pm)[target];
|
|
261
339
|
}
|
|
262
340
|
|
|
263
341
|
;// CONCATENATED MODULE: ./src/features/quality.ts
|
|
264
342
|
|
|
265
343
|
/**
|
|
266
|
-
* The formatter and linter features. Biome answers to both slots and appears once
|
|
267
|
-
* deduplicates by `id
|
|
268
|
-
* dependency and one pair of scripts.
|
|
344
|
+
* The formatter and linter features. Biome answers to both slots and appears once, because `selectFeatures`
|
|
345
|
+
* deduplicates by `id`.
|
|
269
346
|
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
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.
|
|
347
|
+
* Each brings a pair of scripts, since the writing half and the CI half want different exit codes: `format`
|
|
348
|
+
* rewrites and `format:check` fails, `lint:fix` rewrites and `lint` fails. Biome's `check` is both in one pass.
|
|
274
349
|
*/ const PRETTIER = {
|
|
275
350
|
id: 'prettier',
|
|
276
351
|
overlays: [
|
|
@@ -312,11 +387,9 @@ const OXLINT = {
|
|
|
312
387
|
};
|
|
313
388
|
/**
|
|
314
389
|
* 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
|
|
316
|
-
*
|
|
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.
|
|
390
|
+
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest its peer range
|
|
391
|
+
* accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to reach for ESLint at all —
|
|
392
|
+
* so the config it ships hands the parser the whole program.
|
|
320
393
|
*/ const ESLINT = {
|
|
321
394
|
id: 'eslint',
|
|
322
395
|
overlays: [
|
|
@@ -332,6 +405,12 @@ const OXLINT = {
|
|
|
332
405
|
scripts: {
|
|
333
406
|
lint: 'eslint .',
|
|
334
407
|
'lint:fix': 'eslint . --fix'
|
|
408
|
+
},
|
|
409
|
+
// The pin is invisible from the command line, and its consequence is not: the rules reason about the
|
|
410
|
+
// program through an older compiler than the one that builds it, so a fix can produce code `tsc` rejects.
|
|
411
|
+
// The generated eslint.config.mjs explains why; the README's table is where someone reads what to type.
|
|
412
|
+
scriptHelp: {
|
|
413
|
+
'lint:fix': 'apply what it can — then run typecheck, see eslint.config.mjs'
|
|
335
414
|
}
|
|
336
415
|
};
|
|
337
416
|
const BIOME = {
|
|
@@ -372,14 +451,12 @@ function linterFeature(linter) {
|
|
|
372
451
|
;// CONCATENATED MODULE: ./src/features/styling.ts
|
|
373
452
|
|
|
374
453
|
/**
|
|
375
|
-
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* written in utilities instead of classes of their own.
|
|
454
|
+
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages, plus an
|
|
455
|
+
* overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts postcss-loader in front
|
|
456
|
+
* of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten in utilities.
|
|
379
457
|
*
|
|
380
|
-
* `postcss`
|
|
381
|
-
*
|
|
382
|
-
* install one.
|
|
458
|
+
* `postcss` is the app's dependency rather than the framework's: rshono compiles CSS natively, so an app that
|
|
459
|
+
* wants no plugin chain installs none.
|
|
383
460
|
*/ const TAILWIND = {
|
|
384
461
|
id: 'tailwind',
|
|
385
462
|
overlays: [
|
|
@@ -402,12 +479,14 @@ function stylingFeature(styling) {
|
|
|
402
479
|
|
|
403
480
|
|
|
404
481
|
/**
|
|
405
|
-
* The features a set of answers selects, in application order — so an overlay listed later wins a file
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
|
|
482
|
+
* The features a set of answers selects, in application order — so an overlay listed later wins a file both
|
|
483
|
+
* ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome is both formatter and
|
|
484
|
+
* linter) without contributing twice.
|
|
485
|
+
*
|
|
486
|
+
* `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled CLI.
|
|
487
|
+
*/ function selectFeatures(answers, pm) {
|
|
409
488
|
const selected = [
|
|
410
|
-
deployFeature(answers.deploy),
|
|
489
|
+
deployFeature(answers.deploy, pm),
|
|
411
490
|
stylingFeature(answers.styling),
|
|
412
491
|
formatterFeature(answers.formatter),
|
|
413
492
|
linterFeature(answers.linter),
|
|
@@ -426,9 +505,8 @@ function stylingFeature(styling) {
|
|
|
426
505
|
;// CONCATENATED MODULE: ./src/options.ts
|
|
427
506
|
|
|
428
507
|
/*
|
|
429
|
-
* The names each option accepts, spelled once
|
|
430
|
-
*
|
|
431
|
-
* — so a name added here reaches all three without a second list to remember.
|
|
508
|
+
* The names each option accepts, spelled once: the types below are derived from them, the CLI validates its
|
|
509
|
+
* flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them.
|
|
432
510
|
*/ const FORMATTER_NAMES = [
|
|
433
511
|
'prettier',
|
|
434
512
|
'biome',
|
|
@@ -489,20 +567,26 @@ const QUALITY_PRESETS = [
|
|
|
489
567
|
}
|
|
490
568
|
];
|
|
491
569
|
/**
|
|
492
|
-
* Turns whatever the user typed into a name npm will accept, or
|
|
493
|
-
*
|
|
494
|
-
*
|
|
570
|
+
* Turns whatever the user typed into a name npm will accept, or `null` when nothing usable is left.
|
|
571
|
+
* Lowercasing and replacing runs of invalid characters covers the ordinary cases (`My App`, `my_app`).
|
|
572
|
+
*
|
|
573
|
+
* The promise is checked rather than assumed: the result goes through {@link isValidPackageName} on the way
|
|
574
|
+
* out. It used to be spelled as a rule per character class here and the check made again by the caller, which
|
|
575
|
+
* left the exported function able to return a name npm refuses — `_leading` was one, because a leading
|
|
576
|
+
* underscore is stripped by npm's rule and not by this one.
|
|
495
577
|
*/ function toPackageName(input) {
|
|
496
578
|
const trimmed = input.trim().replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
497
579
|
if (!trimmed || trimmed === '.') return null;
|
|
498
|
-
// A scoped name is a name, not a path: `@scope/pkg` stays whole
|
|
499
|
-
//
|
|
580
|
+
// A scoped name is a name, not a path: `@scope/pkg` stays whole. Anything else is a path, whose last
|
|
581
|
+
// segment names the project.
|
|
500
582
|
const scoped = /^@[^\\/]+[\\/][^\\/]+$/.test(trimmed);
|
|
501
583
|
const base = scoped ? trimmed : trimmed.split(/[\\/]/).filter(Boolean).pop() ?? '';
|
|
502
584
|
if (!base) return null;
|
|
503
|
-
const name = base.toLowerCase().replace(/[\\/]/g, '/').replace(/[^a-z\d\-._~/@]+/g, '-')
|
|
504
|
-
|
|
505
|
-
|
|
585
|
+
const name = base.toLowerCase().replace(/[\\/]/g, '/').replace(/[^a-z\d\-._~/@]+/g, '-')// `_` alongside `-`: npm refuses a leading underscore too, and it is what `My_App` and `_internal` leave
|
|
586
|
+
// behind once the invalid runs are replaced.
|
|
587
|
+
.replace(/^[-_]+/, '').replace(/-+$/, '');
|
|
588
|
+
const trimmedName = name.slice(0, 214);
|
|
589
|
+
return isValidPackageName(trimmedName) ? trimmedName : null;
|
|
506
590
|
}
|
|
507
591
|
/** npm's own rule, narrowed to what we ever generate: no uppercase, no leading dot or underscore. */ function isValidPackageName(name) {
|
|
508
592
|
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(name) && name.length <= 214;
|
|
@@ -514,15 +598,7 @@ const QUALITY_PRESETS = [
|
|
|
514
598
|
|
|
515
599
|
;// CONCATENATED MODULE: ./src/pkg.ts
|
|
516
600
|
|
|
517
|
-
|
|
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.
|
|
521
|
-
*/ const BASE_SCRIPTS = {
|
|
522
|
-
dev: 'rshono dev',
|
|
523
|
-
build: 'rshono build',
|
|
524
|
-
typecheck: 'tsc --noEmit'
|
|
525
|
-
};
|
|
601
|
+
|
|
526
602
|
/** Field order in the emitted file — the conventional reading order, and stable so snapshots are too. */ const FIELD_ORDER = [
|
|
527
603
|
'name',
|
|
528
604
|
'version',
|
|
@@ -538,48 +614,29 @@ function sorted(record) {
|
|
|
538
614
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
539
615
|
}
|
|
540
616
|
/**
|
|
541
|
-
*
|
|
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.
|
|
617
|
+
* pnpm's settings for the new app, or `null` when no feature has anything to put in them.
|
|
555
618
|
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
619
|
+
* It exists for one field, `allowBuilds`: pnpm fails an install until the project has said whether a
|
|
620
|
+
* dependency's install script should run. Nothing rshono installs has one, so most apps get no file. A file
|
|
621
|
+
* rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
559
622
|
*/ function buildPnpmSettings(features) {
|
|
560
|
-
const allowBuilds = {
|
|
561
|
-
...BASE_ALLOW_BUILDS
|
|
562
|
-
};
|
|
623
|
+
const allowBuilds = {};
|
|
563
624
|
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
625
|
+
const entries = Object.entries(sorted(allowBuilds));
|
|
626
|
+
if (entries.length === 0) return null;
|
|
564
627
|
return [
|
|
565
628
|
'# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
|
|
566
629
|
'# fails the install rather than skip one quietly — so anything added later belongs here too.',
|
|
567
630
|
'# `false` means the script was looked at: these ship their real binary as an optional dependency.',
|
|
568
631
|
'allowBuilds:',
|
|
569
|
-
...
|
|
632
|
+
...entries.map(([name, allowed])=>` ${name}: ${allowed}`),
|
|
570
633
|
''
|
|
571
634
|
].join('\n');
|
|
572
635
|
}
|
|
573
636
|
/**
|
|
574
|
-
* Assembles `package.json` from the answers and whatever the selected features contribute.
|
|
575
|
-
*
|
|
576
|
-
* Dependencies are sorted by name and scripts are left in contribution order (the base ones, then each
|
|
577
|
-
* feature's, in the order features were selected) — so two runs with the same answers produce byte-
|
|
578
|
-
* identical output, which is what makes the generated manifest snapshot-testable.
|
|
637
|
+
* Assembles `package.json` from the answers and whatever the selected features contribute. Dependencies are
|
|
638
|
+
* sorted and scripts keep {@link buildScripts}'s order, so two runs with the same answers are byte-identical.
|
|
579
639
|
*/ function buildPackageJson(answers, features, pm) {
|
|
580
|
-
const scripts = {
|
|
581
|
-
...BASE_SCRIPTS
|
|
582
|
-
};
|
|
583
640
|
const dependencies = {
|
|
584
641
|
'@rshono/core': RSHONO_RANGE,
|
|
585
642
|
hono: FRAMEWORK_DEPS.hono,
|
|
@@ -592,7 +649,6 @@ function sorted(record) {
|
|
|
592
649
|
typescript: FRAMEWORK_DEPS.typescript
|
|
593
650
|
};
|
|
594
651
|
for (const feature of features){
|
|
595
|
-
Object.assign(scripts, feature.scripts);
|
|
596
652
|
Object.assign(dependencies, feature.dependencies);
|
|
597
653
|
Object.assign(devDependencies, feature.devDependencies);
|
|
598
654
|
}
|
|
@@ -605,12 +661,12 @@ function sorted(record) {
|
|
|
605
661
|
engines: {
|
|
606
662
|
node: NODE_ENGINE
|
|
607
663
|
},
|
|
608
|
-
scripts,
|
|
664
|
+
scripts: buildScripts(features),
|
|
609
665
|
dependencies: sorted(dependencies),
|
|
610
666
|
devDependencies: sorted(devDependencies)
|
|
611
667
|
};
|
|
612
|
-
// Only when the environment told us the exact version:
|
|
613
|
-
//
|
|
668
|
+
// Only when the environment told us the exact version: this field pins the tool for Corepack, and a guess is
|
|
669
|
+
// worse than leaving it out.
|
|
614
670
|
if (pm.version) manifest.packageManager = `${pm.name}@${pm.version}`;
|
|
615
671
|
const ordered = Object.fromEntries(FIELD_ORDER.filter((field)=>field in manifest).map((field)=>[
|
|
616
672
|
field,
|
|
@@ -622,23 +678,22 @@ function sorted(record) {
|
|
|
622
678
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
623
679
|
|
|
624
680
|
/**
|
|
625
|
-
* `{{NAME}}`,
|
|
626
|
-
*
|
|
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.
|
|
681
|
+
* `{{NAME}}`, not `__NAME__`: in markdown the latter is strong emphasis, so Prettier rewrites it to `**NAME**`
|
|
682
|
+
* and the token stops matching. `{{…}}` means nothing to any format these templates are written in.
|
|
629
683
|
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
630
|
-
function tokensFor(answers, pm) {
|
|
684
|
+
function tokensFor(answers, features, pm) {
|
|
631
685
|
return {
|
|
632
686
|
'{{PROJECT_NAME}}': answers.packageName,
|
|
633
687
|
'{{DEPLOY_TARGET}}': answers.deploy,
|
|
634
|
-
|
|
635
|
-
'{{
|
|
688
|
+
// From the features rather than the answers, because all three are about the scripts the app actually got.
|
|
689
|
+
'{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
|
|
690
|
+
'{{DEPLOY_STEP}}': deployStep(features, pm),
|
|
691
|
+
'{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
|
|
636
692
|
};
|
|
637
693
|
}
|
|
638
694
|
/**
|
|
639
|
-
* Substitutes tokens, and throws on one it
|
|
640
|
-
*
|
|
641
|
-
* catch.
|
|
695
|
+
* Substitutes tokens, and throws on one it does not know — a typo in a template would otherwise ship a literal
|
|
696
|
+
* `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would catch.
|
|
642
697
|
*/ function render(contents, tokens, source) {
|
|
643
698
|
return contents.replace(TOKEN_PATTERN, (token)=>{
|
|
644
699
|
const value = tokens[token];
|
|
@@ -668,12 +723,9 @@ function readTemplateDir(dir) {
|
|
|
668
723
|
return files;
|
|
669
724
|
}
|
|
670
725
|
/**
|
|
671
|
-
* `_gitignore` → `.gitignore`, and so on for every dotfile
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* one — the file would exist in the repo, pass every local test, and be missing from the package
|
|
675
|
-
* everybody actually installs. Naming them with an underscore and renaming here is the long-standing
|
|
676
|
-
* fix. It applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
726
|
+
* `_gitignore` → `.gitignore`, and so on for every dotfile: npm strips a literal `.gitignore` out of a
|
|
727
|
+
* published tarball, so a template that contained one would pass every local test and be missing from the
|
|
728
|
+
* package everybody installs. The basename only, so `templates/_x/y.ts` is left alone.
|
|
677
729
|
*/ function undotted(path) {
|
|
678
730
|
const segments = path.split(posix.sep);
|
|
679
731
|
const name = segments.pop();
|
|
@@ -682,22 +734,19 @@ function readTemplateDir(dir) {
|
|
|
682
734
|
name.startsWith('_') ? `.${name.slice(1)}` : name
|
|
683
735
|
].join(posix.sep);
|
|
684
736
|
}
|
|
685
|
-
/**
|
|
686
|
-
* A feature's `.gitignore` lines, appended under a heading naming it — so somebody reading the file six
|
|
687
|
-
* months later can tell why `.wrangler/` is in there.
|
|
688
|
-
*/ function appendGitignore(existing, features) {
|
|
737
|
+
/** A feature's `.gitignore` lines, under a heading naming it — so a reader can tell why `.wrangler/` is there. */ function appendGitignore(existing, features) {
|
|
689
738
|
const additions = features.filter((feature)=>feature.gitignore?.length);
|
|
690
739
|
if (additions.length === 0) return existing;
|
|
691
740
|
const blocks = additions.map((feature)=>`\n# ${feature.id}\n${feature.gitignore.join('\n')}\n`);
|
|
692
741
|
return existing + blocks.join('');
|
|
693
742
|
}
|
|
694
743
|
/**
|
|
695
|
-
* Turns answers into the exact set of files to write, without touching the target directory
|
|
696
|
-
* decisions
|
|
697
|
-
*
|
|
744
|
+
* Turns answers into the exact set of files to write, without touching the target directory. Separating the
|
|
745
|
+
* decisions from the I/O is what lets a test assert on the whole matrix of answers, and makes `--dry-run` the
|
|
746
|
+
* same code path minus the last step.
|
|
698
747
|
*/ function plan_plan(answers, pm) {
|
|
699
|
-
const features = selectFeatures(answers);
|
|
700
|
-
const tokens = tokensFor(answers, pm);
|
|
748
|
+
const features = selectFeatures(answers, pm);
|
|
749
|
+
const tokens = tokensFor(answers, features, pm);
|
|
701
750
|
const raw = readTemplateDir(join(TEMPLATES_DIR, 'base'));
|
|
702
751
|
for (const feature of features){
|
|
703
752
|
for (const overlay of feature.overlays ?? []){
|
|
@@ -713,7 +762,9 @@ function readTemplateDir(dir) {
|
|
|
713
762
|
const gitignore = files.get('.gitignore');
|
|
714
763
|
if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
|
|
715
764
|
files.set('package.json', buildPackageJson(answers, features, pm));
|
|
716
|
-
|
|
765
|
+
// Only for pnpm, and only when a feature brought an install script to answer for — see `buildPnpmSettings`.
|
|
766
|
+
const pnpmSettings = pm.name === 'pnpm' ? buildPnpmSettings(features) : null;
|
|
767
|
+
if (pnpmSettings) files.set('pnpm-workspace.yaml', pnpmSettings);
|
|
717
768
|
return {
|
|
718
769
|
// Sorted, so both the write order and a test's snapshot are stable.
|
|
719
770
|
files: new Map([
|
|
@@ -747,6 +798,12 @@ const RUN = {
|
|
|
747
798
|
yarn: 'yarn',
|
|
748
799
|
bun: 'bun'
|
|
749
800
|
};
|
|
801
|
+
const DLX = {
|
|
802
|
+
npm: 'npx',
|
|
803
|
+
pnpm: 'pnpm dlx',
|
|
804
|
+
yarn: 'yarn dlx',
|
|
805
|
+
bun: 'bunx'
|
|
806
|
+
};
|
|
750
807
|
function isKnown(name) {
|
|
751
808
|
return PACKAGE_MANAGERS.includes(name);
|
|
752
809
|
}
|
|
@@ -755,15 +812,14 @@ function packageManager(name, version) {
|
|
|
755
812
|
name,
|
|
756
813
|
version,
|
|
757
814
|
install: INSTALL[name],
|
|
758
|
-
run: RUN[name]
|
|
815
|
+
run: RUN[name],
|
|
816
|
+
dlx: DLX[name]
|
|
759
817
|
};
|
|
760
818
|
}
|
|
761
819
|
/**
|
|
762
|
-
* Which package manager invoked us.
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
* Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
820
|
+
* Which package manager invoked us. All four set `npm_config_user_agent` on the process they spawn — `pnpm/11.9.0
|
|
821
|
+
* npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm project without asking, exact
|
|
822
|
+
* version included. Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
767
823
|
*/ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
768
824
|
const [spec] = (userAgent ?? '').split(' ');
|
|
769
825
|
const [name, version] = (spec ?? '').split('/');
|
|
@@ -771,18 +827,11 @@ function packageManager(name, version) {
|
|
|
771
827
|
return packageManager('npm');
|
|
772
828
|
}
|
|
773
829
|
/**
|
|
774
|
-
* Runs the install, streaming its output. `shell: true` on Windows
|
|
775
|
-
*
|
|
776
|
-
* are all fixed strings from the tables above, never anything the user typed.
|
|
830
|
+
* Runs the install, streaming its output. `shell: true` on Windows, where npm, pnpm and yarn are `.cmd` shims
|
|
831
|
+
* `spawn` cannot execute directly — safe because every argument is a fixed string from the tables above.
|
|
777
832
|
*/ function runInstall(pm, cwd) {
|
|
778
833
|
return run(pm, pm.install, cwd);
|
|
779
834
|
}
|
|
780
|
-
/** `<pm> run <script>` — the one form every package manager accepts, Yarn v1 included. */ function runScript(pm, script, cwd) {
|
|
781
|
-
return run(pm, [
|
|
782
|
-
'run',
|
|
783
|
-
script
|
|
784
|
-
], cwd);
|
|
785
|
-
}
|
|
786
835
|
function run(pm, args, cwd) {
|
|
787
836
|
const result = spawnSync(pm.name, args, {
|
|
788
837
|
cwd,
|
|
@@ -795,10 +844,7 @@ function run(pm, args, cwd) {
|
|
|
795
844
|
;// CONCATENATED MODULE: ./src/write.ts
|
|
796
845
|
|
|
797
846
|
|
|
798
|
-
/**
|
|
799
|
-
* Files that do not make a directory "occupied". A user who ran `git init` or opened the folder in an
|
|
800
|
-
* editor before scaffolding has not put anything in it that we would overwrite.
|
|
801
|
-
*/ const IGNORED_ENTRIES = new Set([
|
|
847
|
+
/** Files that do not make a directory "occupied": a `git init` or an editor has put nothing there to overwrite. */ const IGNORED_ENTRIES = new Set([
|
|
802
848
|
'.git',
|
|
803
849
|
'.DS_Store',
|
|
804
850
|
'.idea',
|
|
@@ -806,12 +852,9 @@ function run(pm, args, cwd) {
|
|
|
806
852
|
'Thumbs.db'
|
|
807
853
|
]);
|
|
808
854
|
/**
|
|
809
|
-
* What is already at the target path,
|
|
810
|
-
*
|
|
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.
|
|
855
|
+
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind — which is
|
|
856
|
+
* what decides whether scaffolding into it is safe. A path that does not exist yet is no conflict; one that
|
|
857
|
+
* exists and is not a directory throws, since `--force` should not write into it either.
|
|
815
858
|
*/ function conflictingEntries(dir) {
|
|
816
859
|
const stats = statSync(dir, {
|
|
817
860
|
throwIfNoEntry: false
|
|
@@ -821,8 +864,8 @@ function run(pm, args, cwd) {
|
|
|
821
864
|
return readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
|
|
822
865
|
}
|
|
823
866
|
/**
|
|
824
|
-
* Writes the plan
|
|
825
|
-
*
|
|
867
|
+
* Writes the plan, creating directories as needed and keeping the plan's own ordering — so a failure part-way
|
|
868
|
+
* through leaves something a person can make sense of.
|
|
826
869
|
*/ function writePlan(plan, targetDir) {
|
|
827
870
|
mkdirSync(targetDir, {
|
|
828
871
|
recursive: true
|