@rshono/create 1.0.0-rc.1 → 1.0.0-rc.11
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 +97 -59
- package/bin/create-rshono.mjs +5 -3
- package/dist/api.mjs +324 -202
- package/dist/cli.mjs +409 -298
- package/package.json +11 -12
- package/templates/base/AGENTS.md +5 -0
- package/templates/base/CLAUDE.md +1 -0
- package/templates/base/README.md +8 -10
- package/templates/base/_env +2 -5
- package/templates/base/public/favicon.svg +2 -1
- package/templates/base/rshono.config.ts +1 -31
- 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 +0 -15
- package/templates/oxfmt/_oxfmtrc.json +1 -1
- package/templates/oxlint/_oxlintrc.json +1 -1
- package/templates/prettier/_prettierignore +6 -1
- package/templates/tailwind/postcss.config.mjs +0 -4
- package/templates/tailwind/rshono.config.ts +2 -38
- 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/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.11';
|
|
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.18.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
|
|
@@ -2965,22 +2969,10 @@ function hasGit(cwd) {
|
|
|
2965
2969
|
name: 'cloudflare',
|
|
2966
2970
|
hint: 'deploy with `wrangler deploy`'
|
|
2967
2971
|
},
|
|
2968
|
-
{
|
|
2969
|
-
name: 'bun',
|
|
2970
|
-
hint: 'run `bun dist/server/main.mjs`'
|
|
2971
|
-
},
|
|
2972
|
-
{
|
|
2973
|
-
name: 'deno',
|
|
2974
|
-
hint: 'run `deno serve -A dist/server/main.mjs`'
|
|
2975
|
-
},
|
|
2976
2972
|
{
|
|
2977
2973
|
name: 'vercel',
|
|
2978
2974
|
hint: 'deploy with `vercel deploy --prebuilt`'
|
|
2979
2975
|
},
|
|
2980
|
-
{
|
|
2981
|
-
name: 'netlify',
|
|
2982
|
-
hint: 'deploy with `netlify deploy --build=false --dir=.netlify/publish`'
|
|
2983
|
-
},
|
|
2984
2976
|
{
|
|
2985
2977
|
name: 'aws-lambda',
|
|
2986
2978
|
hint: 'zip dist/ with the handler at dist/server/main.mjs'
|
|
@@ -2989,19 +2981,32 @@ function hasGit(cwd) {
|
|
|
2989
2981
|
|
|
2990
2982
|
;// CONCATENATED MODULE: ./src/options.ts
|
|
2991
2983
|
|
|
2992
|
-
|
|
2984
|
+
/*
|
|
2985
|
+
* The names each option accepts, spelled once. The types below are derived from them, the CLI validates
|
|
2986
|
+
* its flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them
|
|
2987
|
+
* — so a name added here reaches all three without a second list to remember.
|
|
2988
|
+
*/ const FORMATTER_NAMES = [
|
|
2989
|
+
'prettier',
|
|
2990
|
+
'biome',
|
|
2991
|
+
'oxfmt',
|
|
2992
|
+
'none'
|
|
2993
|
+
];
|
|
2994
|
+
const LINTER_NAMES = [
|
|
2995
|
+
'oxlint',
|
|
2996
|
+
'eslint',
|
|
2997
|
+
'biome',
|
|
2998
|
+
'none'
|
|
2999
|
+
];
|
|
3000
|
+
const PACKAGE_MANAGERS = [
|
|
2993
3001
|
'npm',
|
|
2994
3002
|
'pnpm',
|
|
2995
3003
|
'yarn',
|
|
2996
3004
|
'bun'
|
|
2997
|
-
]
|
|
3005
|
+
];
|
|
2998
3006
|
const DEPLOY_TARGET_NAMES = DEPLOY_TARGETS.map((target)=>target.name);
|
|
2999
3007
|
function deployHint(name) {
|
|
3000
3008
|
return DEPLOY_TARGETS.find((target)=>target.name === name)?.hint ?? '';
|
|
3001
3009
|
}
|
|
3002
|
-
function isDeployTarget(value) {
|
|
3003
|
-
return DEPLOY_TARGET_NAMES.includes(value);
|
|
3004
|
-
}
|
|
3005
3010
|
const QUALITY_PRESETS = [
|
|
3006
3011
|
{
|
|
3007
3012
|
id: 'prettier-oxlint',
|
|
@@ -3086,9 +3091,84 @@ const QUALITY_PRESETS = [
|
|
|
3086
3091
|
] : [];
|
|
3087
3092
|
}
|
|
3088
3093
|
|
|
3089
|
-
;// CONCATENATED MODULE: ./src/
|
|
3094
|
+
;// CONCATENATED MODULE: ./src/scripts.ts
|
|
3095
|
+
// `features/types.js` rather than `features/index.js`: the deploy feature imports `invoke` from here, and
|
|
3096
|
+
// going through the barrel would make that a cycle — types.ts imports nothing.
|
|
3097
|
+
/**
|
|
3098
|
+
* The scripts every app gets, whatever it targets.
|
|
3099
|
+
*
|
|
3100
|
+
* `start`, `preview` and `deploy` are deliberately not here: each one means something different per
|
|
3101
|
+
* platform, so the deploy target contributes its own. The three names are a contract the targets keep,
|
|
3102
|
+
* and the reason the README can describe an app it was not written for:
|
|
3103
|
+
*
|
|
3104
|
+
* - **`start`** runs a build that already exists and never makes one — what a host's own start command
|
|
3105
|
+
* calls. Only the target whose build is a server has one.
|
|
3106
|
+
* - **`preview`** builds, then runs the result here — for the targets where that is not the same two
|
|
3107
|
+
* commands, because otherwise the production build is unanswerable without deploying it.
|
|
3108
|
+
* - **`deploy`** builds, then ships it — where the platform has one command that does the shipping.
|
|
3109
|
+
*/ const BASE_SCRIPTS = {
|
|
3110
|
+
dev: 'rshono dev',
|
|
3111
|
+
build: 'rshono build',
|
|
3112
|
+
typecheck: 'tsc --noEmit'
|
|
3113
|
+
};
|
|
3114
|
+
/**
|
|
3115
|
+
* Every script the app gets, in the order they are written: the base ones, then each feature's, in the order
|
|
3116
|
+
* the features were selected. The manifest and the README's command table are both this, so neither can
|
|
3117
|
+
* document a script the other does not have.
|
|
3118
|
+
*/ function buildScripts(features) {
|
|
3119
|
+
const scripts = {
|
|
3120
|
+
...BASE_SCRIPTS
|
|
3121
|
+
};
|
|
3122
|
+
for (const feature of features)Object.assign(scripts, feature.scripts);
|
|
3123
|
+
return scripts;
|
|
3124
|
+
}
|
|
3125
|
+
/** The gloss for each base script, in the README's command table. `build` names the target it is for. */ function baseScriptHelp(deploy) {
|
|
3126
|
+
return {
|
|
3127
|
+
dev: 'dev server with HMR, http://localhost:3000',
|
|
3128
|
+
build: `production build for ${deploy}`,
|
|
3129
|
+
typecheck: 'tsc --noEmit'
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
/**
|
|
3133
|
+
* Script names a package manager has a command of its own for. `pnpm deploy` runs pnpm's workspace-deploy
|
|
3134
|
+
* command and never looks at the manifest, so printing it would hand somebody a line that quietly does
|
|
3135
|
+
* something else. The explicit `run` form is what all four managers accept, so those names get it.
|
|
3136
|
+
*/ const SHADOWED = new Set([
|
|
3137
|
+
'deploy'
|
|
3138
|
+
]);
|
|
3139
|
+
/** How to type one of the app's scripts with this package manager — `pnpm dev`, but `npm run dev`. */ function invoke(pm, script) {
|
|
3140
|
+
return `${SHADOWED.has(script) ? `${pm.name} run` : pm.run} ${script}`;
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* The README's command table: one line per script, with the command to type and a one-line gloss.
|
|
3144
|
+
*
|
|
3145
|
+
* A script whose feature supplies no `scriptHelp` is left out and covered by the "package.json has the
|
|
3146
|
+
* rest" line — that is how a formatter's `format:check` stays out of a table about running the app.
|
|
3147
|
+
*/ function scriptTable(answers, features, pm) {
|
|
3148
|
+
const help = baseScriptHelp(answers.deploy);
|
|
3149
|
+
for (const feature of features)Object.assign(help, feature.scriptHelp);
|
|
3150
|
+
const documented = Object.keys(buildScripts(features)).filter((name)=>help[name]);
|
|
3151
|
+
const width = Math.max(...documented.map((name)=>invoke(pm, name).length));
|
|
3152
|
+
return documented.map((name)=>`${invoke(pm, name).padEnd(width)} # ${help[name]}`).join('\n');
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* The command that gets this app into production, as a sentence — the README's deploy step, and the same
|
|
3156
|
+
* choice the closing summary makes, so the two cannot name different commands.
|
|
3157
|
+
*
|
|
3158
|
+
* Read off the scripts rather than the target, so a target that gains a `deploy` gains the sentence with it.
|
|
3159
|
+
* `start` is the answer where there is no `deploy`: it ships nothing itself, but it is what the host runs.
|
|
3160
|
+
*/ function deployStep(features, pm) {
|
|
3161
|
+
const scripts = buildScripts(features);
|
|
3162
|
+
if (scripts.deploy) return `\`${invoke(pm, 'deploy')}\` does the build and the upload in one step.`;
|
|
3163
|
+
if (scripts.start) return `\`${invoke(pm, 'start')}\` runs that build wherever you host it, and never makes one.`;
|
|
3164
|
+
// Every branch names a script the app has, so a target that contributes none of the three still reads true.
|
|
3165
|
+
if (scripts.preview) return `\`${invoke(pm, 'preview')}\` runs the build here, so you can check it first.`;
|
|
3166
|
+
return `\`${invoke(pm, 'build')}\` produces it; getting it there is yours to script.`;
|
|
3167
|
+
}
|
|
3090
3168
|
|
|
3169
|
+
;// CONCATENATED MODULE: ./src/versions.ts
|
|
3091
3170
|
|
|
3171
|
+
/** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
|
|
3092
3172
|
/** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
|
|
3093
3173
|
/**
|
|
3094
3174
|
* Versions for the optional tooling the features can add — the one place in this package where a
|
|
@@ -3119,12 +3199,11 @@ const QUALITY_PRESETS = [
|
|
|
3119
3199
|
};
|
|
3120
3200
|
/**
|
|
3121
3201
|
* The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
|
|
3122
|
-
* {@link FRAMEWORK_DEPS}
|
|
3202
|
+
* {@link FRAMEWORK_DEPS}.
|
|
3123
3203
|
*
|
|
3124
|
-
* typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface,
|
|
3125
|
-
* it accepts `typescript >=4.8.4 <6.1.0` and nothing above
|
|
3126
|
-
*
|
|
3127
|
-
* rshono's declarations compile the same under either, which is what makes this pin an app's business
|
|
3204
|
+
* typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface,
|
|
3205
|
+
* so it accepts `typescript >=4.8.4 <6.1.0` and nothing above; `~6.0.3` is the newest that satisfies
|
|
3206
|
+
* it. rshono's declarations compile the same under either, which is what makes this an app's business
|
|
3128
3207
|
* and not the framework's.
|
|
3129
3208
|
*
|
|
3130
3209
|
* When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
|
|
@@ -3132,88 +3211,128 @@ const QUALITY_PRESETS = [
|
|
|
3132
3211
|
|
|
3133
3212
|
;// CONCATENATED MODULE: ./src/features/deploy.ts
|
|
3134
3213
|
|
|
3214
|
+
|
|
3135
3215
|
/**
|
|
3136
|
-
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts
|
|
3137
|
-
* to read, and the template's to carry.
|
|
3216
|
+
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
|
|
3138
3217
|
*
|
|
3139
|
-
* Deliberately thin
|
|
3140
|
-
*
|
|
3141
|
-
*
|
|
3142
|
-
*
|
|
3143
|
-
*
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
},
|
|
3172
|
-
// wrangler brings workerd, whose install script only picks the platform binary out of the optional
|
|
3173
|
-
// dependency that already carries it — `workerd --version` answers without it having run.
|
|
3174
|
-
allowBuilds: {
|
|
3175
|
-
workerd: false
|
|
3176
|
-
},
|
|
3177
|
-
scripts: {
|
|
3178
|
-
deploy: 'rshono build && wrangler deploy'
|
|
3218
|
+
* Deliberately thin: the framework arranges its own output for every platform, and `rshono build`
|
|
3219
|
+
* writes the one platform config that has to exist (`wrangler.jsonc`) if the project has none — a second
|
|
3220
|
+
* copy generated here would only go stale. So a target contributes the commands that run and ship the
|
|
3221
|
+
* build, the CLI they need, the directories to gitignore, and a note for the step no command covers.
|
|
3222
|
+
*
|
|
3223
|
+
* What the three script names promise is documented once, above `BASE_SCRIPTS` in `scripts.ts`. Only `node`
|
|
3224
|
+
* has a `start`, because `rshono start` refuses a bundle built for anywhere else, and it needs no `preview`:
|
|
3225
|
+
* `build` then `start` already is one.
|
|
3226
|
+
*
|
|
3227
|
+
* `pm` is here because two things have to be spelled for the app's package manager — the runner that fetches
|
|
3228
|
+
* the uninstalled Vercel CLI ({@link PackageManager.dlx}), and every command in {@link Feature.platformSetup}.
|
|
3229
|
+
*/ function deployFeatures(pm) {
|
|
3230
|
+
const build = invoke(pm, 'build');
|
|
3231
|
+
return {
|
|
3232
|
+
// Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
|
|
3233
|
+
// guess — so the target contributes only the command that runs what was built.
|
|
3234
|
+
node: {
|
|
3235
|
+
id: 'deploy-node',
|
|
3236
|
+
scripts: {
|
|
3237
|
+
start: 'rshono start'
|
|
3238
|
+
},
|
|
3239
|
+
scriptHelp: {
|
|
3240
|
+
start: 'run the build that exists — what your host calls'
|
|
3241
|
+
},
|
|
3242
|
+
platformSetup: [
|
|
3243
|
+
'The two commands a host asks for:',
|
|
3244
|
+
'',
|
|
3245
|
+
`- **Build** — \`${pm.name} install && ${build}\``,
|
|
3246
|
+
`- **Start** — \`${invoke(pm, 'start')}\``,
|
|
3247
|
+
'',
|
|
3248
|
+
`In a Dockerfile, the same pair: \`RUN ${build}\`, then \`CMD ["${pm.name}", "start"]\`.`
|
|
3249
|
+
].join('\n')
|
|
3179
3250
|
},
|
|
3180
|
-
|
|
3181
|
-
'
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3251
|
+
cloudflare: {
|
|
3252
|
+
id: 'deploy-cloudflare',
|
|
3253
|
+
devDependencies: {
|
|
3254
|
+
wrangler: TOOL_VERSIONS.wrangler
|
|
3255
|
+
},
|
|
3256
|
+
// The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each
|
|
3257
|
+
// one merely picks the platform binary out of the optional dependency that already carries it, so
|
|
3258
|
+
// neither needs to run — `workerd --version` and `esbuild --version` both answer without it.
|
|
3259
|
+
allowBuilds: {
|
|
3260
|
+
esbuild: false,
|
|
3261
|
+
workerd: false
|
|
3262
|
+
},
|
|
3263
|
+
// `wrangler dev` is the one preview that runs the code in the runtime it will actually run in:
|
|
3264
|
+
// workerd, not Node, serving the assets the build assembled. Both scripts read the wrangler.jsonc the
|
|
3265
|
+
// build wrote, so nothing here has to know where the bundle or the assets went.
|
|
3266
|
+
scripts: {
|
|
3267
|
+
preview: 'rshono build && wrangler dev',
|
|
3268
|
+
deploy: 'rshono build && wrangler deploy'
|
|
3269
|
+
},
|
|
3270
|
+
scriptHelp: {
|
|
3271
|
+
preview: 'build, then run it in workerd — port 8787',
|
|
3272
|
+
deploy: 'build, then ship it to Cloudflare'
|
|
3273
|
+
},
|
|
3274
|
+
gitignore: [
|
|
3275
|
+
'.wrangler/'
|
|
3276
|
+
],
|
|
3277
|
+
notes: [
|
|
3278
|
+
'The first build writes wrangler.jsonc — yours to edit after that.'
|
|
3279
|
+
],
|
|
3280
|
+
platformSetup: [
|
|
3281
|
+
`Building from a git repo instead: set Workers Builds' **Build command** to \`${build}\`.`,
|
|
3282
|
+
'Its deploy command already defaults to `npx wrangler deploy`, and it installs dependencies itself.'
|
|
3283
|
+
].join('\n')
|
|
3191
3284
|
},
|
|
3192
|
-
|
|
3193
|
-
'
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3285
|
+
vercel: {
|
|
3286
|
+
id: 'deploy-vercel',
|
|
3287
|
+
// `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview
|
|
3288
|
+
// URL, which is a useful thing to have but not what the word means. The local `preview` is a Node
|
|
3289
|
+
// build run here — the platform has no way to run its own prebuilt output on your machine.
|
|
3290
|
+
scripts: {
|
|
3291
|
+
preview: 'rshono build --deploy node && rshono start',
|
|
3292
|
+
deploy: `rshono build && ${pm.dlx} vercel deploy --prebuilt --prod`
|
|
3293
|
+
},
|
|
3294
|
+
scriptHelp: {
|
|
3295
|
+
preview: 'build for Node and run that here',
|
|
3296
|
+
deploy: 'build, then upload it to production'
|
|
3297
|
+
},
|
|
3298
|
+
gitignore: [
|
|
3299
|
+
'.vercel/'
|
|
3300
|
+
],
|
|
3301
|
+
notes: [
|
|
3302
|
+
'--prebuilt uploads what rshono build assembled; the platform must not rebuild it.',
|
|
3303
|
+
'Drop --prod from the deploy script for a preview URL instead.'
|
|
3304
|
+
],
|
|
3305
|
+
platformSetup: [
|
|
3306
|
+
`Deploying from CI instead: the same \`${invoke(pm, 'deploy')}\`, with \`VERCEL_ORG_ID\`, \`VERCEL_PROJECT_ID\``,
|
|
3307
|
+
'and a CLI token in the environment.',
|
|
3308
|
+
'',
|
|
3309
|
+
`If you let Vercel build the repo itself, set **Framework Preset** to Other — \`hono\` is otherwise`,
|
|
3310
|
+
`detected as the Hono preset — and **Build Command** to \`${build}\`.`
|
|
3311
|
+
].join('\n')
|
|
3203
3312
|
},
|
|
3204
|
-
|
|
3205
|
-
'
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3313
|
+
'aws-lambda': {
|
|
3314
|
+
id: 'deploy-aws-lambda',
|
|
3315
|
+
// No CLI to wrap, and no upload this could guess at. `preview` still applies — the bundle is a Node
|
|
3316
|
+
// handler, so it runs here.
|
|
3317
|
+
scripts: {
|
|
3318
|
+
preview: 'rshono build --deploy node && rshono start'
|
|
3319
|
+
},
|
|
3320
|
+
scriptHelp: {
|
|
3321
|
+
preview: 'build for Node and run that here'
|
|
3322
|
+
},
|
|
3323
|
+
notes: [
|
|
3324
|
+
'Use a Function URL in RESPONSE_STREAM mode — a buffered invoke mode drops the streaming.'
|
|
3325
|
+
],
|
|
3326
|
+
platformSetup: [
|
|
3327
|
+
`There is no settings page here; the upload is yours to script. A job needs \`${pm.name} install && ${build}\`,`,
|
|
3328
|
+
'then the function package: `dist/`, plus `node_modules` for any dependency of your own, which the server',
|
|
3329
|
+
'bundle leaves external.'
|
|
3330
|
+
].join('\n')
|
|
3331
|
+
}
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
function deployFeature(target, pm) {
|
|
3335
|
+
return deployFeatures(pm)[target];
|
|
3217
3336
|
}
|
|
3218
3337
|
|
|
3219
3338
|
;// CONCATENATED MODULE: ./src/features/quality.ts
|
|
@@ -3223,8 +3342,10 @@ function deployFeature(target) {
|
|
|
3223
3342
|
* deduplicates by `id`, so `formatter: 'biome', linter: 'biome'` contributes one set of files, one
|
|
3224
3343
|
* dependency and one pair of scripts.
|
|
3225
3344
|
*
|
|
3226
|
-
*
|
|
3227
|
-
*
|
|
3345
|
+
* A formatter brings `format:check` beside `format`, because the writing half and the CI half want
|
|
3346
|
+
* different exit-code behaviour: `format` rewrites files, `format:check` fails instead. A linter brings
|
|
3347
|
+
* `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
|
|
3348
|
+
* one. Biome adds a `check` of its own, which is the pair of them in a single pass.
|
|
3228
3349
|
*/ const PRETTIER = {
|
|
3229
3350
|
id: 'prettier',
|
|
3230
3351
|
overlays: [
|
|
@@ -3265,12 +3386,11 @@ const OXLINT = {
|
|
|
3265
3386
|
}
|
|
3266
3387
|
};
|
|
3267
3388
|
/**
|
|
3268
|
-
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
3269
|
-
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one
|
|
3270
|
-
* peer range accepts (see {@link ESLINT_TYPESCRIPT}).
|
|
3271
|
-
*
|
|
3272
|
-
*
|
|
3273
|
-
* so the config it ships hands the whole program to the parser rather than linting file by file.
|
|
3389
|
+
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
3390
|
+
* be installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one
|
|
3391
|
+
* its peer range accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to
|
|
3392
|
+
* reach for ESLint over a syntax-only linter — so the config it ships hands the parser the whole
|
|
3393
|
+
* program rather than linting file by file.
|
|
3274
3394
|
*/ const ESLINT = {
|
|
3275
3395
|
id: 'eslint',
|
|
3276
3396
|
overlays: [
|
|
@@ -3326,14 +3446,13 @@ function linterFeature(linter) {
|
|
|
3326
3446
|
;// CONCATENATED MODULE: ./src/features/styling.ts
|
|
3327
3447
|
|
|
3328
3448
|
/**
|
|
3329
|
-
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
3330
|
-
* an overlay carrying
|
|
3331
|
-
*
|
|
3332
|
-
*
|
|
3449
|
+
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
3450
|
+
* plus an overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts
|
|
3451
|
+
* postcss-loader in front of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten
|
|
3452
|
+
* in utilities.
|
|
3333
3453
|
*
|
|
3334
3454
|
* `postcss` and `postcss-loader` are the app's dependencies rather than the framework's — rshono
|
|
3335
|
-
* compiles CSS natively
|
|
3336
|
-
* install one.
|
|
3455
|
+
* compiles CSS natively, so an app that does not want a plugin chain does not install one.
|
|
3337
3456
|
*/ const TAILWIND = {
|
|
3338
3457
|
id: 'tailwind',
|
|
3339
3458
|
overlays: [
|
|
@@ -3359,9 +3478,12 @@ function stylingFeature(styling) {
|
|
|
3359
3478
|
* The features a set of answers selects, in application order — so an overlay listed later wins a file
|
|
3360
3479
|
* both of them ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome
|
|
3361
3480
|
* is both the formatter and the linter) without contributing twice.
|
|
3362
|
-
|
|
3481
|
+
*
|
|
3482
|
+
* `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled
|
|
3483
|
+
* CLI; nothing else here depends on which package manager the app is for.
|
|
3484
|
+
*/ function selectFeatures(answers, pm) {
|
|
3363
3485
|
const selected = [
|
|
3364
|
-
deployFeature(answers.deploy),
|
|
3486
|
+
deployFeature(answers.deploy, pm),
|
|
3365
3487
|
stylingFeature(answers.styling),
|
|
3366
3488
|
formatterFeature(answers.formatter),
|
|
3367
3489
|
linterFeature(answers.linter),
|
|
@@ -3379,14 +3501,7 @@ function stylingFeature(styling) {
|
|
|
3379
3501
|
|
|
3380
3502
|
;// CONCATENATED MODULE: ./src/pkg.ts
|
|
3381
3503
|
|
|
3382
|
-
|
|
3383
|
-
* The scripts every app gets. `start` is not among them: it is the *Node* launcher, and the deploy
|
|
3384
|
-
* features each contribute the command their own platform runs what was built with.
|
|
3385
|
-
*/ const BASE_SCRIPTS = {
|
|
3386
|
-
dev: 'rshono dev',
|
|
3387
|
-
build: 'rshono build',
|
|
3388
|
-
typecheck: 'tsc --noEmit'
|
|
3389
|
-
};
|
|
3504
|
+
|
|
3390
3505
|
/** Field order in the emitted file — the conventional reading order, and stable so snapshots are too. */ const FIELD_ORDER = [
|
|
3391
3506
|
'name',
|
|
3392
3507
|
'version',
|
|
@@ -3402,48 +3517,35 @@ function sorted(record) {
|
|
|
3402
3517
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
3403
3518
|
}
|
|
3404
3519
|
/**
|
|
3405
|
-
*
|
|
3406
|
-
*
|
|
3407
|
-
* the optional dependency that already carries it — rshono's own repo denies it for the same reason.
|
|
3408
|
-
*/ const BASE_ALLOW_BUILDS = {
|
|
3409
|
-
esbuild: false
|
|
3410
|
-
};
|
|
3411
|
-
/**
|
|
3412
|
-
* pnpm's settings for the new app — written for pnpm and for nobody else.
|
|
3520
|
+
* pnpm's settings for the new app, written only when a feature has something to put in them. `null`
|
|
3521
|
+
* means there is nothing to say, so no file is written.
|
|
3413
3522
|
*
|
|
3414
|
-
* It exists for one field
|
|
3415
|
-
*
|
|
3416
|
-
*
|
|
3417
|
-
*
|
|
3418
|
-
* `pnpm approve-builds` before it has rendered a page once.
|
|
3523
|
+
* It exists for one field, `allowBuilds`. pnpm fails an install — and every `pnpm dev` after it —
|
|
3524
|
+
* until the project has said whether a dependency's install script should run. Nothing rshono itself
|
|
3525
|
+
* installs has one, so most apps get no file; the packages that do (wrangler's esbuild and workerd)
|
|
3526
|
+
* declare their answer on the feature that brings them.
|
|
3419
3527
|
*
|
|
3420
|
-
*
|
|
3421
|
-
* package projects included. What lands here is a decision about *this* app: a scaffolded file the app
|
|
3422
|
-
* owns from then on, not something the framework reaches back into.
|
|
3528
|
+
* A file rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
3423
3529
|
*/ function buildPnpmSettings(features) {
|
|
3424
|
-
const allowBuilds = {
|
|
3425
|
-
...BASE_ALLOW_BUILDS
|
|
3426
|
-
};
|
|
3530
|
+
const allowBuilds = {};
|
|
3427
3531
|
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
3532
|
+
const entries = Object.entries(sorted(allowBuilds));
|
|
3533
|
+
if (entries.length === 0) return null;
|
|
3428
3534
|
return [
|
|
3429
3535
|
'# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
|
|
3430
3536
|
'# fails the install rather than skip one quietly — so anything added later belongs here too.',
|
|
3431
3537
|
'# `false` means the script was looked at: these ship their real binary as an optional dependency.',
|
|
3432
3538
|
'allowBuilds:',
|
|
3433
|
-
...
|
|
3539
|
+
...entries.map(([name, allowed])=>` ${name}: ${allowed}`),
|
|
3434
3540
|
''
|
|
3435
3541
|
].join('\n');
|
|
3436
3542
|
}
|
|
3437
3543
|
/**
|
|
3438
3544
|
* Assembles `package.json` from the answers and whatever the selected features contribute.
|
|
3439
3545
|
*
|
|
3440
|
-
* Dependencies are sorted by name and scripts
|
|
3441
|
-
*
|
|
3442
|
-
* identical output, which is what makes the generated manifest snapshot-testable.
|
|
3546
|
+
* Dependencies are sorted by name and scripts keep the order {@link buildScripts} gives them — so two runs
|
|
3547
|
+
* with the same answers produce byte-identical output, which is what makes the manifest snapshot-testable.
|
|
3443
3548
|
*/ function buildPackageJson(answers, features, pm) {
|
|
3444
|
-
const scripts = {
|
|
3445
|
-
...BASE_SCRIPTS
|
|
3446
|
-
};
|
|
3447
3549
|
const dependencies = {
|
|
3448
3550
|
'@rshono/core': RSHONO_RANGE,
|
|
3449
3551
|
hono: FRAMEWORK_DEPS.hono,
|
|
@@ -3456,7 +3558,6 @@ function sorted(record) {
|
|
|
3456
3558
|
typescript: FRAMEWORK_DEPS.typescript
|
|
3457
3559
|
};
|
|
3458
3560
|
for (const feature of features){
|
|
3459
|
-
Object.assign(scripts, feature.scripts);
|
|
3460
3561
|
Object.assign(dependencies, feature.dependencies);
|
|
3461
3562
|
Object.assign(devDependencies, feature.devDependencies);
|
|
3462
3563
|
}
|
|
@@ -3465,12 +3566,11 @@ function sorted(record) {
|
|
|
3465
3566
|
version: '0.1.0',
|
|
3466
3567
|
private: true,
|
|
3467
3568
|
type: 'module',
|
|
3468
|
-
//
|
|
3469
|
-
// Node finds out from their package manager rather than from a stack trace.
|
|
3569
|
+
// Generated from the framework's own manifest, so the app's floor cannot drift below rshono's.
|
|
3470
3570
|
engines: {
|
|
3471
|
-
node:
|
|
3571
|
+
node: NODE_ENGINE
|
|
3472
3572
|
},
|
|
3473
|
-
scripts,
|
|
3573
|
+
scripts: buildScripts(features),
|
|
3474
3574
|
dependencies: sorted(dependencies),
|
|
3475
3575
|
devDependencies: sorted(devDependencies)
|
|
3476
3576
|
};
|
|
@@ -3486,19 +3586,25 @@ function sorted(record) {
|
|
|
3486
3586
|
|
|
3487
3587
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
3488
3588
|
|
|
3489
|
-
|
|
3490
|
-
|
|
3589
|
+
/**
|
|
3590
|
+
* `{{NAME}}`, deliberately not `__NAME__`: templates are real files that real tools run over, and in
|
|
3591
|
+
* markdown `__NAME__` *is* strong emphasis — Prettier rewrites it to `**NAME**` and the token stops
|
|
3592
|
+
* matching. `{{…}}` means nothing to any format these templates are written in.
|
|
3593
|
+
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
3594
|
+
function tokensFor(answers, features, pm) {
|
|
3491
3595
|
return {
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3596
|
+
'{{PROJECT_NAME}}': answers.packageName,
|
|
3597
|
+
'{{DEPLOY_TARGET}}': answers.deploy,
|
|
3598
|
+
// Derived from the features rather than the answers, because all three are about the scripts the app
|
|
3599
|
+
// actually got: its command table, the one command that ships it, and what its platform asks for.
|
|
3600
|
+
'{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
|
|
3601
|
+
'{{DEPLOY_STEP}}': deployStep(features, pm),
|
|
3602
|
+
'{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
|
|
3497
3603
|
};
|
|
3498
3604
|
}
|
|
3499
3605
|
/**
|
|
3500
3606
|
* Substitutes tokens, and throws on one it doesn't know — a typo in a template would otherwise ship a
|
|
3501
|
-
* literal `
|
|
3607
|
+
* literal `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would
|
|
3502
3608
|
* catch.
|
|
3503
3609
|
*/ function render(contents, tokens, source) {
|
|
3504
3610
|
return contents.replace(TOKEN_PATTERN, (token)=>{
|
|
@@ -3531,10 +3637,9 @@ function readTemplateDir(dir) {
|
|
|
3531
3637
|
/**
|
|
3532
3638
|
* `_gitignore` → `.gitignore`, and so on for every dotfile.
|
|
3533
3639
|
*
|
|
3534
|
-
* npm strips a literal `.gitignore` out of a published tarball, so a template cannot
|
|
3535
|
-
*
|
|
3536
|
-
*
|
|
3537
|
-
* fix. It applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
3640
|
+
* npm strips a literal `.gitignore` out of a published tarball, so a template cannot contain one — it
|
|
3641
|
+
* would exist in the repo, pass every local test, and be missing from the package everybody installs.
|
|
3642
|
+
* Applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
3538
3643
|
*/ function undotted(path) {
|
|
3539
3644
|
const segments = path.split(posix.sep);
|
|
3540
3645
|
const name = segments.pop();
|
|
@@ -3557,8 +3662,8 @@ function readTemplateDir(dir) {
|
|
|
3557
3662
|
* decisions and the I/O are separated so the whole matrix of answers can be asserted on in a test, and
|
|
3558
3663
|
* so `--dry-run` is the same code path minus the last step.
|
|
3559
3664
|
*/ function plan_plan(answers, pm) {
|
|
3560
|
-
const features = selectFeatures(answers);
|
|
3561
|
-
const tokens = tokensFor(answers, pm);
|
|
3665
|
+
const features = selectFeatures(answers, pm);
|
|
3666
|
+
const tokens = tokensFor(answers, features, pm);
|
|
3562
3667
|
const raw = readTemplateDir(external_node_path_join(TEMPLATES_DIR, 'base'));
|
|
3563
3668
|
for (const feature of features){
|
|
3564
3669
|
for (const overlay of feature.overlays ?? []){
|
|
@@ -3574,7 +3679,9 @@ function readTemplateDir(dir) {
|
|
|
3574
3679
|
const gitignore = files.get('.gitignore');
|
|
3575
3680
|
if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
|
|
3576
3681
|
files.set('package.json', buildPackageJson(answers, features, pm));
|
|
3577
|
-
|
|
3682
|
+
// Only for pnpm, and only when a feature brought an install script to answer for — see `buildPnpmSettings`.
|
|
3683
|
+
const pnpmSettings = pm.name === 'pnpm' ? buildPnpmSettings(features) : null;
|
|
3684
|
+
if (pnpmSettings) files.set('pnpm-workspace.yaml', pnpmSettings);
|
|
3578
3685
|
return {
|
|
3579
3686
|
// Sorted, so both the write order and a test's snapshot are stable.
|
|
3580
3687
|
files: new Map([
|
|
@@ -3587,6 +3694,7 @@ function readTemplateDir(dir) {
|
|
|
3587
3694
|
|
|
3588
3695
|
;// CONCATENATED MODULE: ./src/pm.ts
|
|
3589
3696
|
|
|
3697
|
+
|
|
3590
3698
|
const INSTALL = {
|
|
3591
3699
|
npm: [
|
|
3592
3700
|
'install'
|
|
@@ -3605,20 +3713,27 @@ const RUN = {
|
|
|
3605
3713
|
yarn: 'yarn',
|
|
3606
3714
|
bun: 'bun'
|
|
3607
3715
|
};
|
|
3716
|
+
const DLX = {
|
|
3717
|
+
npm: 'npx',
|
|
3718
|
+
pnpm: 'pnpm dlx',
|
|
3719
|
+
yarn: 'yarn dlx',
|
|
3720
|
+
bun: 'bunx'
|
|
3721
|
+
};
|
|
3608
3722
|
function isKnown(name) {
|
|
3609
|
-
return name
|
|
3723
|
+
return PACKAGE_MANAGERS.includes(name);
|
|
3610
3724
|
}
|
|
3611
3725
|
function packageManager(name, version) {
|
|
3612
3726
|
return {
|
|
3613
3727
|
name,
|
|
3614
3728
|
version,
|
|
3615
3729
|
install: INSTALL[name],
|
|
3616
|
-
run: RUN[name]
|
|
3730
|
+
run: RUN[name],
|
|
3731
|
+
dlx: DLX[name]
|
|
3617
3732
|
};
|
|
3618
3733
|
}
|
|
3619
3734
|
/**
|
|
3620
|
-
* Which package manager invoked us. Every one of them sets `npm_config_user_agent`
|
|
3621
|
-
* spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `
|
|
3735
|
+
* Which package manager invoked us. Every one of them sets `npm_config_user_agent` on the process it
|
|
3736
|
+
* spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm
|
|
3622
3737
|
* project without asking, and the exact version comes along for the `packageManager` field.
|
|
3623
3738
|
*
|
|
3624
3739
|
* Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
@@ -3635,12 +3750,6 @@ function packageManager(name, version) {
|
|
|
3635
3750
|
*/ function runInstall(pm, cwd) {
|
|
3636
3751
|
return run(pm, pm.install, cwd);
|
|
3637
3752
|
}
|
|
3638
|
-
/** `<pm> run <script>` — the one form every package manager accepts, Yarn v1 included. */ function runScript(pm, script, cwd) {
|
|
3639
|
-
return run(pm, [
|
|
3640
|
-
'run',
|
|
3641
|
-
script
|
|
3642
|
-
], cwd);
|
|
3643
|
-
}
|
|
3644
3753
|
function run(pm, args, cwd) {
|
|
3645
3754
|
const result = spawnSync(pm.name, args, {
|
|
3646
3755
|
cwd,
|
|
@@ -3653,6 +3762,17 @@ function run(pm, args, cwd) {
|
|
|
3653
3762
|
;// CONCATENATED MODULE: ./src/ui.ts
|
|
3654
3763
|
|
|
3655
3764
|
|
|
3765
|
+
|
|
3766
|
+
/**
|
|
3767
|
+
* The two halves of the closing line, in the words of the app's own scripts: what produces something
|
|
3768
|
+
* shippable, and where it goes from there. The target with no command to give keeps the framework's hint,
|
|
3769
|
+
* which is a sentence about the platform rather than something to type.
|
|
3770
|
+
*/ function productionSteps(answers, plan, pm) {
|
|
3771
|
+
const scripts = buildScripts(plan.features);
|
|
3772
|
+
const check = scripts.preview ? `${invoke(pm, 'preview')} to run the production build` : `${pm.run} build`;
|
|
3773
|
+
const ship = scripts.deploy ? `${invoke(pm, 'deploy')} to ship it` : scripts.start ? `${invoke(pm, 'start')} on the host that runs it` : deployHint(answers.deploy);
|
|
3774
|
+
return `${check}, and ${ship}`;
|
|
3775
|
+
}
|
|
3656
3776
|
/**
|
|
3657
3777
|
* What to do next, in the order to do it — the last thing the user reads, and for most people the only
|
|
3658
3778
|
* documentation they will read today. `installed` decides whether the install step is still theirs.
|
|
@@ -3664,7 +3784,7 @@ function run(pm, args, cwd) {
|
|
|
3664
3784
|
const lines = [
|
|
3665
3785
|
steps.join('\n')
|
|
3666
3786
|
];
|
|
3667
|
-
lines.push(`\nThen ${
|
|
3787
|
+
lines.push(`\nThen ${productionSteps(answers, plan, pm)}.`);
|
|
3668
3788
|
if (plan.notes.length > 0) lines.push(`\n${plan.notes.join('\n')}`);
|
|
3669
3789
|
return lines.join('\n');
|
|
3670
3790
|
}
|
|
@@ -3700,15 +3820,20 @@ function run(pm, args, cwd) {
|
|
|
3700
3820
|
'.vscode',
|
|
3701
3821
|
'Thumbs.db'
|
|
3702
3822
|
]);
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3823
|
+
/**
|
|
3824
|
+
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind —
|
|
3825
|
+
* which is what decides whether scaffolding into it is safe.
|
|
3826
|
+
*
|
|
3827
|
+
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory throws
|
|
3828
|
+
* rather than reporting an empty list, since `--force` should not write into one either — otherwise
|
|
3829
|
+
* `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
3830
|
+
*/ function conflictingEntries(dir) {
|
|
3831
|
+
const stats = statSync(dir, {
|
|
3832
|
+
throwIfNoEntry: false
|
|
3833
|
+
});
|
|
3834
|
+
if (!stats) return [];
|
|
3835
|
+
if (!stats.isDirectory()) throw new Error(`${dir} already exists and is not a directory.`);
|
|
3836
|
+
return external_node_fs_readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
|
|
3712
3837
|
}
|
|
3713
3838
|
/**
|
|
3714
3839
|
* Writes the plan. Directories are created as needed, and files are written with the plan's own
|
|
@@ -3736,20 +3861,21 @@ function inspectTarget(dir) {
|
|
|
3736
3861
|
|
|
3737
3862
|
|
|
3738
3863
|
|
|
3864
|
+
|
|
3739
3865
|
const DEFAULT_DIRECTORY = 'my-rshono-app';
|
|
3740
|
-
const HELP = `create-rshono — scaffold a new rshono app
|
|
3866
|
+
/** 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
|
|
3741
3867
|
|
|
3742
3868
|
Usage:
|
|
3743
|
-
|
|
3869
|
+
npx @rshono/create@latest [directory] [options]
|
|
3744
3870
|
|
|
3745
3871
|
Options:
|
|
3746
3872
|
-y, --yes accept the default for every question not given as a flag
|
|
3747
3873
|
-d, --deploy <target> ${DEPLOY_TARGET_NAMES.join(' | ')}
|
|
3748
3874
|
--tailwind Tailwind CSS (--no-tailwind for plain CSS)
|
|
3749
3875
|
--quality <preset> ${QUALITY_PRESETS.map((preset)=>preset.id).join(' | ')}
|
|
3750
|
-
--formatter <name>
|
|
3751
|
-
--linter <name>
|
|
3752
|
-
--pm <name>
|
|
3876
|
+
--formatter <name> ${FORMATTER_NAMES.join(' | ')} (overrides --quality)
|
|
3877
|
+
--linter <name> ${LINTER_NAMES.join(' | ')} (overrides --quality; eslint pins TypeScript 6)
|
|
3878
|
+
--pm <name> ${PACKAGE_MANAGERS.join(' | ')} (default: whatever ran this)
|
|
3753
3879
|
--no-install write the files and stop
|
|
3754
3880
|
--no-git do not initialize a repository
|
|
3755
3881
|
--force scaffold into a directory that is not empty
|
|
@@ -3760,7 +3886,7 @@ Options:
|
|
|
3760
3886
|
Every question can be answered by a flag, and a non-interactive terminal implies --yes — so one command
|
|
3761
3887
|
scaffolds without prompting:
|
|
3762
3888
|
|
|
3763
|
-
|
|
3889
|
+
npx @rshono/create@latest my-app -y --deploy cloudflare --tailwind --quality biome
|
|
3764
3890
|
`;
|
|
3765
3891
|
function fail(message) {
|
|
3766
3892
|
log.error(message);
|
|
@@ -3777,93 +3903,89 @@ function fail(message) {
|
|
|
3777
3903
|
if (off) return false;
|
|
3778
3904
|
return undefined;
|
|
3779
3905
|
}
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3906
|
+
/** `parseArgs` names an unknown flag but says nothing about what to do next; this points at `--help`. */ function parse() {
|
|
3907
|
+
try {
|
|
3908
|
+
return parseArgs({
|
|
3909
|
+
options: {
|
|
3910
|
+
yes: {
|
|
3911
|
+
type: 'boolean',
|
|
3912
|
+
short: 'y'
|
|
3913
|
+
},
|
|
3914
|
+
deploy: {
|
|
3915
|
+
type: 'string',
|
|
3916
|
+
short: 'd'
|
|
3917
|
+
},
|
|
3918
|
+
tailwind: {
|
|
3919
|
+
type: 'boolean'
|
|
3920
|
+
},
|
|
3921
|
+
'no-tailwind': {
|
|
3922
|
+
type: 'boolean'
|
|
3923
|
+
},
|
|
3924
|
+
quality: {
|
|
3925
|
+
type: 'string'
|
|
3926
|
+
},
|
|
3927
|
+
formatter: {
|
|
3928
|
+
type: 'string'
|
|
3929
|
+
},
|
|
3930
|
+
linter: {
|
|
3931
|
+
type: 'string'
|
|
3932
|
+
},
|
|
3933
|
+
pm: {
|
|
3934
|
+
type: 'string'
|
|
3935
|
+
},
|
|
3936
|
+
install: {
|
|
3937
|
+
type: 'boolean'
|
|
3938
|
+
},
|
|
3939
|
+
'no-install': {
|
|
3940
|
+
type: 'boolean'
|
|
3941
|
+
},
|
|
3942
|
+
git: {
|
|
3943
|
+
type: 'boolean'
|
|
3944
|
+
},
|
|
3945
|
+
'no-git': {
|
|
3946
|
+
type: 'boolean'
|
|
3947
|
+
},
|
|
3948
|
+
force: {
|
|
3949
|
+
type: 'boolean'
|
|
3950
|
+
},
|
|
3951
|
+
'dry-run': {
|
|
3952
|
+
type: 'boolean'
|
|
3953
|
+
},
|
|
3954
|
+
help: {
|
|
3955
|
+
type: 'boolean',
|
|
3956
|
+
short: 'h'
|
|
3957
|
+
},
|
|
3958
|
+
version: {
|
|
3959
|
+
type: 'boolean',
|
|
3960
|
+
short: 'v'
|
|
3961
|
+
}
|
|
3830
3962
|
},
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3963
|
+
allowPositionals: true
|
|
3964
|
+
});
|
|
3965
|
+
} catch (error) {
|
|
3966
|
+
fail(`${error instanceof Error ? error.message : String(error)}\n\nRun with --help to see the options.`);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
async function main() {
|
|
3970
|
+
const { values, positionals } = parse();
|
|
3838
3971
|
if (values.help) return console.log(HELP);
|
|
3839
|
-
if (values.version) return console.log("1.0.0-rc.
|
|
3840
|
-
// A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer.
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
'yarn',
|
|
3846
|
-
'bun'
|
|
3847
|
-
], 'pm');
|
|
3972
|
+
if (values.version) return console.log("1.0.0-rc.11");
|
|
3973
|
+
// A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both
|
|
3974
|
+
// streams have to be a terminal: the prompts draw on stdout but *read from stdin*, so
|
|
3975
|
+
// `echo | npx @rshono/create` would otherwise ask a question with nothing behind the keyboard.
|
|
3976
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
|
|
3977
|
+
const pmFlag = oneOf(values.pm, PACKAGE_MANAGERS, 'pm');
|
|
3848
3978
|
const pm = pmFlag ? packageManager(pmFlag) : detectPackageManager();
|
|
3849
3979
|
const deployFlag = oneOf(values.deploy, DEPLOY_TARGET_NAMES, 'deploy');
|
|
3850
|
-
const formatterFlag = oneOf(values.formatter,
|
|
3851
|
-
|
|
3852
|
-
'biome',
|
|
3853
|
-
'oxfmt',
|
|
3854
|
-
'none'
|
|
3855
|
-
], 'formatter');
|
|
3856
|
-
const linterFlag = oneOf(values.linter, [
|
|
3857
|
-
'oxlint',
|
|
3858
|
-
'eslint',
|
|
3859
|
-
'biome',
|
|
3860
|
-
'none'
|
|
3861
|
-
], 'linter');
|
|
3980
|
+
const formatterFlag = oneOf(values.formatter, FORMATTER_NAMES, 'formatter');
|
|
3981
|
+
const linterFlag = oneOf(values.linter, LINTER_NAMES, 'linter');
|
|
3862
3982
|
const qualityFlag = oneOf(values.quality, QUALITY_PRESETS.map((preset)=>preset.id), 'quality');
|
|
3863
3983
|
const tailwindFlag = tristate(values.tailwind, values['no-tailwind'], 'tailwind');
|
|
3864
3984
|
const installFlag = tristate(values.install, values['no-install'], 'install');
|
|
3865
3985
|
const gitFlag = tristate(values.git, values['no-git'], 'git');
|
|
3866
|
-
|
|
3986
|
+
// The framework version, not this package's: it is the one the app will be pinned to, and the one
|
|
3987
|
+
// worth reading here. `--version` reports create-rshono's own.
|
|
3988
|
+
intro(`create-rshono · rshono ${RSHONO_VERSION}`);
|
|
3867
3989
|
// ── Where ───────────────────────────────────────────────────────────────────────────────────────
|
|
3868
3990
|
let directory = positionals[0];
|
|
3869
3991
|
if (!directory) {
|
|
@@ -3877,7 +3999,7 @@ async function main() {
|
|
|
3877
3999
|
const targetDir = resolve(process.cwd(), directory);
|
|
3878
4000
|
const packageName = toPackageName(directory === '.' ? basename(targetDir) : directory);
|
|
3879
4001
|
if (!packageName || !isValidPackageName(packageName)) fail(`"${directory}" does not give a usable npm package name.`);
|
|
3880
|
-
const conflicts =
|
|
4002
|
+
const conflicts = conflictingEntries(targetDir);
|
|
3881
4003
|
if (conflicts.length > 0 && !values.force) {
|
|
3882
4004
|
const where = directory === '.' ? 'this directory' : `"${directory}"`;
|
|
3883
4005
|
const listed = `${conflicts.slice(0, 3).join(', ')}${conflicts.length > 3 ? ', …' : ''}`;
|
|
@@ -3903,7 +4025,7 @@ async function main() {
|
|
|
3903
4025
|
}))
|
|
3904
4026
|
}));
|
|
3905
4027
|
}
|
|
3906
|
-
let styling = tailwindFlag
|
|
4028
|
+
let styling = tailwindFlag ? 'tailwind' : 'css';
|
|
3907
4029
|
if (tailwindFlag === undefined && interactive) {
|
|
3908
4030
|
styling = unwrap(await dist_select({
|
|
3909
4031
|
message: 'Styling?',
|
|
@@ -3922,10 +4044,9 @@ async function main() {
|
|
|
3922
4044
|
]
|
|
3923
4045
|
}));
|
|
3924
4046
|
}
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
*/ let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
|
|
4047
|
+
// One question instead of two. The axes stay independent underneath: a `--formatter` or `--linter`
|
|
4048
|
+
// flag addresses either on its own, and skips the question entirely.
|
|
4049
|
+
let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
|
|
3929
4050
|
if (!preset && !formatterFlag && !linterFlag) {
|
|
3930
4051
|
const fallback = QUALITY_PRESETS["0"];
|
|
3931
4052
|
if (interactive) {
|
|
@@ -3953,9 +4074,10 @@ async function main() {
|
|
|
3953
4074
|
initialValue: true
|
|
3954
4075
|
}));
|
|
3955
4076
|
}
|
|
3956
|
-
|
|
4077
|
+
// Asked once, not once per use: this shells out to `git rev-parse`.
|
|
4078
|
+
const nested = isInsideRepo(process.cwd());
|
|
4079
|
+
let git = gitFlag ?? !nested;
|
|
3957
4080
|
if (gitFlag === undefined && interactive) {
|
|
3958
|
-
const nested = isInsideRepo(process.cwd());
|
|
3959
4081
|
git = unwrap(await dist_confirm({
|
|
3960
4082
|
message: nested ? 'Initialize a git repository? (this is already inside one)' : 'Initialize a git repository?',
|
|
3961
4083
|
initialValue: !nested
|
|
@@ -3963,14 +4085,10 @@ async function main() {
|
|
|
3963
4085
|
}
|
|
3964
4086
|
const answers = {
|
|
3965
4087
|
packageName,
|
|
3966
|
-
targetDir,
|
|
3967
4088
|
deploy,
|
|
3968
4089
|
styling,
|
|
3969
4090
|
formatter,
|
|
3970
|
-
linter
|
|
3971
|
-
packageManager: pm.name,
|
|
3972
|
-
install,
|
|
3973
|
-
git
|
|
4091
|
+
linter
|
|
3974
4092
|
};
|
|
3975
4093
|
// ── Plan, then write ────────────────────────────────────────────────────────────────────────────
|
|
3976
4094
|
const plan = plan_plan(answers, pm);
|
|
@@ -3987,14 +4105,7 @@ async function main() {
|
|
|
3987
4105
|
if (install) {
|
|
3988
4106
|
log.step(`Installing dependencies with ${pm.name}…`);
|
|
3989
4107
|
installed = runInstall(pm, targetDir);
|
|
3990
|
-
if (!installed) log.warn(`${pm.name} install failed — run it yourself
|
|
3991
|
-
}
|
|
3992
|
-
/*
|
|
3993
|
-
* Format the scaffold with the tool it was scaffolded with, so a fresh project passes its own
|
|
3994
|
-
* `format:check` instead of reporting a diff nobody made. Needs the install, since the formatter is a
|
|
3995
|
-
* devDependency — hence the skip, rather than a failure, when there is none.
|
|
3996
|
-
*/ if (installed && formatter !== 'none') {
|
|
3997
|
-
if (!runScript(pm, 'format', targetDir)) log.warn(`\`${pm.run} format\` failed — the files are fine, the formatter is not.`);
|
|
4108
|
+
if (!installed) log.warn(`${pm.name} install failed — the files are all written, so run it yourself in the project.`);
|
|
3998
4109
|
}
|
|
3999
4110
|
if (git) {
|
|
4000
4111
|
if (!hasGit(targetDir)) {
|