@rshono/create 1.0.0-rc.0 → 1.0.0-rc.10
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 +83 -53
- package/bin/create-rshono.mjs +5 -3
- package/dist/api.mjs +181 -105
- package/dist/cli.mjs +250 -197
- package/package.json +11 -12
- package/templates/base/AGENTS.md +5 -0
- package/templates/base/CLAUDE.md +1 -0
- package/templates/base/README.md +6 -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 +16 -0
- 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 -19
- 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.10';
|
|
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',
|
|
@@ -3010,6 +3015,13 @@ const QUALITY_PRESETS = [
|
|
|
3010
3015
|
formatter: 'prettier',
|
|
3011
3016
|
linter: 'oxlint'
|
|
3012
3017
|
},
|
|
3018
|
+
{
|
|
3019
|
+
id: 'prettier-eslint',
|
|
3020
|
+
label: 'Prettier + ESLint',
|
|
3021
|
+
hint: 'type-aware rules — pins TypeScript 6, which is all typescript-eslint accepts',
|
|
3022
|
+
formatter: 'prettier',
|
|
3023
|
+
linter: 'eslint'
|
|
3024
|
+
},
|
|
3013
3025
|
{
|
|
3014
3026
|
id: 'biome',
|
|
3015
3027
|
label: 'Biome',
|
|
@@ -3081,7 +3093,7 @@ const QUALITY_PRESETS = [
|
|
|
3081
3093
|
|
|
3082
3094
|
;// CONCATENATED MODULE: ./src/versions.ts
|
|
3083
3095
|
|
|
3084
|
-
|
|
3096
|
+
/** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
|
|
3085
3097
|
/** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
|
|
3086
3098
|
/**
|
|
3087
3099
|
* Versions for the optional tooling the features can add — the one place in this package where a
|
|
@@ -3102,20 +3114,36 @@ const QUALITY_PRESETS = [
|
|
|
3102
3114
|
'@biomejs/biome': '^2.5.6',
|
|
3103
3115
|
oxlint: '^1.76.0',
|
|
3104
3116
|
oxfmt: '^0.61.0',
|
|
3117
|
+
eslint: '^10.8.0',
|
|
3118
|
+
// ESLint's own recommended JavaScript rules, which typescript-eslint layers on top of rather than
|
|
3119
|
+
// replaces, and the rules of hooks — the one class of React mistake no type checker sees.
|
|
3120
|
+
'@eslint/js': '^10.0.1',
|
|
3121
|
+
'typescript-eslint': '^8.65.0',
|
|
3122
|
+
'eslint-plugin-react-hooks': '^7.1.1',
|
|
3105
3123
|
wrangler: '^4.115.0'
|
|
3106
3124
|
};
|
|
3125
|
+
/**
|
|
3126
|
+
* The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
|
|
3127
|
+
* {@link FRAMEWORK_DEPS}.
|
|
3128
|
+
*
|
|
3129
|
+
* typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface,
|
|
3130
|
+
* so it accepts `typescript >=4.8.4 <6.1.0` and nothing above; `~6.0.3` is the newest that satisfies
|
|
3131
|
+
* it. rshono's declarations compile the same under either, which is what makes this an app's business
|
|
3132
|
+
* and not the framework's.
|
|
3133
|
+
*
|
|
3134
|
+
* When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
|
|
3135
|
+
*/ const ESLINT_TYPESCRIPT = '~6.0.3';
|
|
3107
3136
|
|
|
3108
3137
|
;// CONCATENATED MODULE: ./src/features/deploy.ts
|
|
3109
3138
|
|
|
3110
3139
|
/**
|
|
3111
|
-
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts
|
|
3112
|
-
* to read, and the template's to carry.
|
|
3140
|
+
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
|
|
3113
3141
|
*
|
|
3114
|
-
* Deliberately thin
|
|
3115
|
-
*
|
|
3116
|
-
*
|
|
3117
|
-
*
|
|
3118
|
-
* the
|
|
3142
|
+
* Deliberately thin: the framework arranges its own output for every platform, and `rshono build`
|
|
3143
|
+
* writes the one platform config that has to exist (`wrangler.jsonc`, dated the day it ran) if the
|
|
3144
|
+
* project has none — a second copy generated here would only go stale. So a target contributes the
|
|
3145
|
+
* command that ships the build, the CLI that command needs, the directories to gitignore, and a note
|
|
3146
|
+
* for the step no command covers. Several contribute just one of those.
|
|
3119
3147
|
*/ const DEPLOY_FEATURES = {
|
|
3120
3148
|
// Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
|
|
3121
3149
|
// guess — so the target contributes only the command that runs what was built.
|
|
@@ -3125,25 +3153,18 @@ const QUALITY_PRESETS = [
|
|
|
3125
3153
|
start: 'rshono start'
|
|
3126
3154
|
}
|
|
3127
3155
|
},
|
|
3128
|
-
// `rshono start` is the Node target's launcher and refuses a build made for another platform, so
|
|
3129
|
-
// these two get the command their own runtime uses under the same script name.
|
|
3130
|
-
bun: {
|
|
3131
|
-
id: 'deploy-bun',
|
|
3132
|
-
scripts: {
|
|
3133
|
-
start: 'bun dist/server/main.mjs'
|
|
3134
|
-
}
|
|
3135
|
-
},
|
|
3136
|
-
deno: {
|
|
3137
|
-
id: 'deploy-deno',
|
|
3138
|
-
scripts: {
|
|
3139
|
-
start: 'deno serve -A dist/server/main.mjs'
|
|
3140
|
-
}
|
|
3141
|
-
},
|
|
3142
3156
|
cloudflare: {
|
|
3143
3157
|
id: 'deploy-cloudflare',
|
|
3144
3158
|
devDependencies: {
|
|
3145
3159
|
wrangler: TOOL_VERSIONS.wrangler
|
|
3146
3160
|
},
|
|
3161
|
+
// The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each
|
|
3162
|
+
// one merely picks the platform binary out of the optional dependency that already carries it, so
|
|
3163
|
+
// neither needs to run — `workerd --version` and `esbuild --version` both answer without it.
|
|
3164
|
+
allowBuilds: {
|
|
3165
|
+
esbuild: false,
|
|
3166
|
+
workerd: false
|
|
3167
|
+
},
|
|
3147
3168
|
scripts: {
|
|
3148
3169
|
deploy: 'rshono build && wrangler deploy'
|
|
3149
3170
|
},
|
|
@@ -3166,15 +3187,6 @@ const QUALITY_PRESETS = [
|
|
|
3166
3187
|
'--prebuilt uploads what rshono build assembled; the platform must not rebuild it.'
|
|
3167
3188
|
]
|
|
3168
3189
|
},
|
|
3169
|
-
netlify: {
|
|
3170
|
-
id: 'deploy-netlify',
|
|
3171
|
-
scripts: {
|
|
3172
|
-
deploy: 'rshono build && netlify deploy --build=false --dir=.netlify/publish'
|
|
3173
|
-
},
|
|
3174
|
-
gitignore: [
|
|
3175
|
-
'.netlify/'
|
|
3176
|
-
]
|
|
3177
|
-
},
|
|
3178
3190
|
'aws-lambda': {
|
|
3179
3191
|
id: 'deploy-aws-lambda',
|
|
3180
3192
|
notes: [
|
|
@@ -3193,8 +3205,10 @@ function deployFeature(target) {
|
|
|
3193
3205
|
* deduplicates by `id`, so `formatter: 'biome', linter: 'biome'` contributes one set of files, one
|
|
3194
3206
|
* dependency and one pair of scripts.
|
|
3195
3207
|
*
|
|
3196
|
-
*
|
|
3197
|
-
*
|
|
3208
|
+
* A formatter brings `format:check` beside `format`, because the writing half and the CI half want
|
|
3209
|
+
* different exit-code behaviour: `format` rewrites files, `format:check` fails instead. A linter brings
|
|
3210
|
+
* `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
|
|
3211
|
+
* one. Biome adds a `check` of its own, which is the pair of them in a single pass.
|
|
3198
3212
|
*/ const PRETTIER = {
|
|
3199
3213
|
id: 'prettier',
|
|
3200
3214
|
overlays: [
|
|
@@ -3234,6 +3248,29 @@ const OXLINT = {
|
|
|
3234
3248
|
'lint:fix': 'oxlint --fix'
|
|
3235
3249
|
}
|
|
3236
3250
|
};
|
|
3251
|
+
/**
|
|
3252
|
+
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
|
|
3253
|
+
* be installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one
|
|
3254
|
+
* its peer range accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to
|
|
3255
|
+
* reach for ESLint over a syntax-only linter — so the config it ships hands the parser the whole
|
|
3256
|
+
* program rather than linting file by file.
|
|
3257
|
+
*/ const ESLINT = {
|
|
3258
|
+
id: 'eslint',
|
|
3259
|
+
overlays: [
|
|
3260
|
+
'eslint'
|
|
3261
|
+
],
|
|
3262
|
+
devDependencies: {
|
|
3263
|
+
eslint: TOOL_VERSIONS.eslint,
|
|
3264
|
+
'@eslint/js': TOOL_VERSIONS["@eslint/js"],
|
|
3265
|
+
'typescript-eslint': TOOL_VERSIONS["typescript-eslint"],
|
|
3266
|
+
'eslint-plugin-react-hooks': TOOL_VERSIONS["eslint-plugin-react-hooks"],
|
|
3267
|
+
typescript: (/* inlined export .ESLINT_TYPESCRIPT */"~6.0.3")
|
|
3268
|
+
},
|
|
3269
|
+
scripts: {
|
|
3270
|
+
lint: 'eslint .',
|
|
3271
|
+
'lint:fix': 'eslint . --fix'
|
|
3272
|
+
}
|
|
3273
|
+
};
|
|
3237
3274
|
const BIOME = {
|
|
3238
3275
|
id: 'biome',
|
|
3239
3276
|
overlays: [
|
|
@@ -3258,6 +3295,7 @@ const FORMATTERS = {
|
|
|
3258
3295
|
};
|
|
3259
3296
|
const LINTERS = {
|
|
3260
3297
|
oxlint: OXLINT,
|
|
3298
|
+
eslint: ESLINT,
|
|
3261
3299
|
biome: BIOME,
|
|
3262
3300
|
none: null
|
|
3263
3301
|
};
|
|
@@ -3271,14 +3309,13 @@ function linterFeature(linter) {
|
|
|
3271
3309
|
;// CONCATENATED MODULE: ./src/features/styling.ts
|
|
3272
3310
|
|
|
3273
3311
|
/**
|
|
3274
|
-
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
3275
|
-
* an overlay carrying
|
|
3276
|
-
*
|
|
3277
|
-
*
|
|
3312
|
+
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
3313
|
+
* plus an overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts
|
|
3314
|
+
* postcss-loader in front of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten
|
|
3315
|
+
* in utilities.
|
|
3278
3316
|
*
|
|
3279
3317
|
* `postcss` and `postcss-loader` are the app's dependencies rather than the framework's — rshono
|
|
3280
|
-
* compiles CSS natively
|
|
3281
|
-
* install one.
|
|
3318
|
+
* compiles CSS natively, so an app that does not want a plugin chain does not install one.
|
|
3282
3319
|
*/ const TAILWIND = {
|
|
3283
3320
|
id: 'tailwind',
|
|
3284
3321
|
overlays: [
|
|
@@ -3325,8 +3362,9 @@ function stylingFeature(styling) {
|
|
|
3325
3362
|
;// CONCATENATED MODULE: ./src/pkg.ts
|
|
3326
3363
|
|
|
3327
3364
|
/**
|
|
3328
|
-
* The scripts every app gets. `start` is not among them
|
|
3329
|
-
*
|
|
3365
|
+
* The scripts every app gets. `start` is not among them, because it means something different per
|
|
3366
|
+
* platform: `node` is the one target that runs the build itself, so it contributes its own, and a
|
|
3367
|
+
* platform target contributes a `deploy` instead, where its platform has one command to give.
|
|
3330
3368
|
*/ const BASE_SCRIPTS = {
|
|
3331
3369
|
dev: 'rshono dev',
|
|
3332
3370
|
build: 'rshono build',
|
|
@@ -3346,6 +3384,30 @@ function stylingFeature(styling) {
|
|
|
3346
3384
|
function sorted(record) {
|
|
3347
3385
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
3348
3386
|
}
|
|
3387
|
+
/**
|
|
3388
|
+
* pnpm's settings for the new app, written only when a feature has something to put in them. `null`
|
|
3389
|
+
* means there is nothing to say, so no file is written.
|
|
3390
|
+
*
|
|
3391
|
+
* It exists for one field, `allowBuilds`. pnpm fails an install — and every `pnpm dev` after it —
|
|
3392
|
+
* until the project has said whether a dependency's install script should run. Nothing rshono itself
|
|
3393
|
+
* installs has one, so most apps get no file; the packages that do (wrangler's esbuild and workerd)
|
|
3394
|
+
* declare their answer on the feature that brings them.
|
|
3395
|
+
*
|
|
3396
|
+
* A file rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
3397
|
+
*/ function buildPnpmSettings(features) {
|
|
3398
|
+
const allowBuilds = {};
|
|
3399
|
+
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
3400
|
+
const entries = Object.entries(sorted(allowBuilds));
|
|
3401
|
+
if (entries.length === 0) return null;
|
|
3402
|
+
return [
|
|
3403
|
+
'# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
|
|
3404
|
+
'# fails the install rather than skip one quietly — so anything added later belongs here too.',
|
|
3405
|
+
'# `false` means the script was looked at: these ship their real binary as an optional dependency.',
|
|
3406
|
+
'allowBuilds:',
|
|
3407
|
+
...entries.map(([name, allowed])=>` ${name}: ${allowed}`),
|
|
3408
|
+
''
|
|
3409
|
+
].join('\n');
|
|
3410
|
+
}
|
|
3349
3411
|
/**
|
|
3350
3412
|
* Assembles `package.json` from the answers and whatever the selected features contribute.
|
|
3351
3413
|
*
|
|
@@ -3377,10 +3439,9 @@ function sorted(record) {
|
|
|
3377
3439
|
version: '0.1.0',
|
|
3378
3440
|
private: true,
|
|
3379
3441
|
type: 'module',
|
|
3380
|
-
//
|
|
3381
|
-
// Node finds out from their package manager rather than from a stack trace.
|
|
3442
|
+
// Generated from the framework's own manifest, so the app's floor cannot drift below rshono's.
|
|
3382
3443
|
engines: {
|
|
3383
|
-
node:
|
|
3444
|
+
node: NODE_ENGINE
|
|
3384
3445
|
},
|
|
3385
3446
|
scripts,
|
|
3386
3447
|
dependencies: sorted(dependencies),
|
|
@@ -3398,19 +3459,22 @@ function sorted(record) {
|
|
|
3398
3459
|
|
|
3399
3460
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
3400
3461
|
|
|
3401
|
-
|
|
3462
|
+
/**
|
|
3463
|
+
* `{{NAME}}`, deliberately not `__NAME__`: templates are real files that real tools run over, and in
|
|
3464
|
+
* markdown `__NAME__` *is* strong emphasis — Prettier rewrites it to `**NAME**` and the token stops
|
|
3465
|
+
* matching. `{{…}}` means nothing to any format these templates are written in.
|
|
3466
|
+
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
3402
3467
|
function tokensFor(answers, pm) {
|
|
3403
3468
|
return {
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
__PM_RUN__: pm.run
|
|
3469
|
+
'{{PROJECT_NAME}}': answers.packageName,
|
|
3470
|
+
'{{DEPLOY_TARGET}}': answers.deploy,
|
|
3471
|
+
'{{DEPLOY_HINT}}': deployHint(answers.deploy),
|
|
3472
|
+
'{{PM_RUN}}': pm.run
|
|
3409
3473
|
};
|
|
3410
3474
|
}
|
|
3411
3475
|
/**
|
|
3412
3476
|
* Substitutes tokens, and throws on one it doesn't know — a typo in a template would otherwise ship a
|
|
3413
|
-
* literal `
|
|
3477
|
+
* literal `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would
|
|
3414
3478
|
* catch.
|
|
3415
3479
|
*/ function render(contents, tokens, source) {
|
|
3416
3480
|
return contents.replace(TOKEN_PATTERN, (token)=>{
|
|
@@ -3443,10 +3507,9 @@ function readTemplateDir(dir) {
|
|
|
3443
3507
|
/**
|
|
3444
3508
|
* `_gitignore` → `.gitignore`, and so on for every dotfile.
|
|
3445
3509
|
*
|
|
3446
|
-
* npm strips a literal `.gitignore` out of a published tarball, so a template cannot
|
|
3447
|
-
*
|
|
3448
|
-
*
|
|
3449
|
-
* fix. It applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
3510
|
+
* npm strips a literal `.gitignore` out of a published tarball, so a template cannot contain one — it
|
|
3511
|
+
* would exist in the repo, pass every local test, and be missing from the package everybody installs.
|
|
3512
|
+
* Applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
3450
3513
|
*/ function undotted(path) {
|
|
3451
3514
|
const segments = path.split(posix.sep);
|
|
3452
3515
|
const name = segments.pop();
|
|
@@ -3486,6 +3549,9 @@ function readTemplateDir(dir) {
|
|
|
3486
3549
|
const gitignore = files.get('.gitignore');
|
|
3487
3550
|
if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
|
|
3488
3551
|
files.set('package.json', buildPackageJson(answers, features, pm));
|
|
3552
|
+
// Only for pnpm, and only when a feature brought an install script to answer for — see `buildPnpmSettings`.
|
|
3553
|
+
const pnpmSettings = pm.name === 'pnpm' ? buildPnpmSettings(features) : null;
|
|
3554
|
+
if (pnpmSettings) files.set('pnpm-workspace.yaml', pnpmSettings);
|
|
3489
3555
|
return {
|
|
3490
3556
|
// Sorted, so both the write order and a test's snapshot are stable.
|
|
3491
3557
|
files: new Map([
|
|
@@ -3498,6 +3564,7 @@ function readTemplateDir(dir) {
|
|
|
3498
3564
|
|
|
3499
3565
|
;// CONCATENATED MODULE: ./src/pm.ts
|
|
3500
3566
|
|
|
3567
|
+
|
|
3501
3568
|
const INSTALL = {
|
|
3502
3569
|
npm: [
|
|
3503
3570
|
'install'
|
|
@@ -3517,7 +3584,7 @@ const RUN = {
|
|
|
3517
3584
|
bun: 'bun'
|
|
3518
3585
|
};
|
|
3519
3586
|
function isKnown(name) {
|
|
3520
|
-
return name
|
|
3587
|
+
return PACKAGE_MANAGERS.includes(name);
|
|
3521
3588
|
}
|
|
3522
3589
|
function packageManager(name, version) {
|
|
3523
3590
|
return {
|
|
@@ -3528,8 +3595,8 @@ function packageManager(name, version) {
|
|
|
3528
3595
|
};
|
|
3529
3596
|
}
|
|
3530
3597
|
/**
|
|
3531
|
-
* Which package manager invoked us. Every one of them sets `npm_config_user_agent`
|
|
3532
|
-
* spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `
|
|
3598
|
+
* Which package manager invoked us. Every one of them sets `npm_config_user_agent` on the process it
|
|
3599
|
+
* spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm
|
|
3533
3600
|
* project without asking, and the exact version comes along for the `packageManager` field.
|
|
3534
3601
|
*
|
|
3535
3602
|
* Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
@@ -3546,12 +3613,6 @@ function packageManager(name, version) {
|
|
|
3546
3613
|
*/ function runInstall(pm, cwd) {
|
|
3547
3614
|
return run(pm, pm.install, cwd);
|
|
3548
3615
|
}
|
|
3549
|
-
/** `<pm> run <script>` — the one form every package manager accepts, Yarn v1 included. */ function runScript(pm, script, cwd) {
|
|
3550
|
-
return run(pm, [
|
|
3551
|
-
'run',
|
|
3552
|
-
script
|
|
3553
|
-
], cwd);
|
|
3554
|
-
}
|
|
3555
3616
|
function run(pm, args, cwd) {
|
|
3556
3617
|
const result = spawnSync(pm.name, args, {
|
|
3557
3618
|
cwd,
|
|
@@ -3611,15 +3672,20 @@ function run(pm, args, cwd) {
|
|
|
3611
3672
|
'.vscode',
|
|
3612
3673
|
'Thumbs.db'
|
|
3613
3674
|
]);
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3675
|
+
/**
|
|
3676
|
+
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind —
|
|
3677
|
+
* which is what decides whether scaffolding into it is safe.
|
|
3678
|
+
*
|
|
3679
|
+
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory throws
|
|
3680
|
+
* rather than reporting an empty list, since `--force` should not write into one either — otherwise
|
|
3681
|
+
* `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
3682
|
+
*/ function conflictingEntries(dir) {
|
|
3683
|
+
const stats = statSync(dir, {
|
|
3684
|
+
throwIfNoEntry: false
|
|
3685
|
+
});
|
|
3686
|
+
if (!stats) return [];
|
|
3687
|
+
if (!stats.isDirectory()) throw new Error(`${dir} already exists and is not a directory.`);
|
|
3688
|
+
return external_node_fs_readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
|
|
3623
3689
|
}
|
|
3624
3690
|
/**
|
|
3625
3691
|
* Writes the plan. Directories are created as needed, and files are written with the plan's own
|
|
@@ -3647,20 +3713,21 @@ function inspectTarget(dir) {
|
|
|
3647
3713
|
|
|
3648
3714
|
|
|
3649
3715
|
|
|
3716
|
+
|
|
3650
3717
|
const DEFAULT_DIRECTORY = 'my-rshono-app';
|
|
3651
|
-
const HELP = `create-rshono — scaffold a new rshono app
|
|
3718
|
+
/** Every accepted name comes from `options.ts`, so the help cannot promise one the validation refuses. */ const HELP = `create-rshono — scaffold a new rshono app
|
|
3652
3719
|
|
|
3653
3720
|
Usage:
|
|
3654
|
-
|
|
3721
|
+
npx @rshono/create@latest [directory] [options]
|
|
3655
3722
|
|
|
3656
3723
|
Options:
|
|
3657
3724
|
-y, --yes accept the default for every question not given as a flag
|
|
3658
3725
|
-d, --deploy <target> ${DEPLOY_TARGET_NAMES.join(' | ')}
|
|
3659
3726
|
--tailwind Tailwind CSS (--no-tailwind for plain CSS)
|
|
3660
3727
|
--quality <preset> ${QUALITY_PRESETS.map((preset)=>preset.id).join(' | ')}
|
|
3661
|
-
--formatter <name>
|
|
3662
|
-
--linter <name>
|
|
3663
|
-
--pm <name>
|
|
3728
|
+
--formatter <name> ${FORMATTER_NAMES.join(' | ')} (overrides --quality)
|
|
3729
|
+
--linter <name> ${LINTER_NAMES.join(' | ')} (overrides --quality; eslint pins TypeScript 6)
|
|
3730
|
+
--pm <name> ${PACKAGE_MANAGERS.join(' | ')} (default: whatever ran this)
|
|
3664
3731
|
--no-install write the files and stop
|
|
3665
3732
|
--no-git do not initialize a repository
|
|
3666
3733
|
--force scaffold into a directory that is not empty
|
|
@@ -3671,7 +3738,7 @@ Options:
|
|
|
3671
3738
|
Every question can be answered by a flag, and a non-interactive terminal implies --yes — so one command
|
|
3672
3739
|
scaffolds without prompting:
|
|
3673
3740
|
|
|
3674
|
-
|
|
3741
|
+
npx @rshono/create@latest my-app -y --deploy cloudflare --tailwind --quality biome
|
|
3675
3742
|
`;
|
|
3676
3743
|
function fail(message) {
|
|
3677
3744
|
log.error(message);
|
|
@@ -3688,92 +3755,89 @@ function fail(message) {
|
|
|
3688
3755
|
if (off) return false;
|
|
3689
3756
|
return undefined;
|
|
3690
3757
|
}
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3758
|
+
/** `parseArgs` names an unknown flag but says nothing about what to do next; this points at `--help`. */ function parse() {
|
|
3759
|
+
try {
|
|
3760
|
+
return parseArgs({
|
|
3761
|
+
options: {
|
|
3762
|
+
yes: {
|
|
3763
|
+
type: 'boolean',
|
|
3764
|
+
short: 'y'
|
|
3765
|
+
},
|
|
3766
|
+
deploy: {
|
|
3767
|
+
type: 'string',
|
|
3768
|
+
short: 'd'
|
|
3769
|
+
},
|
|
3770
|
+
tailwind: {
|
|
3771
|
+
type: 'boolean'
|
|
3772
|
+
},
|
|
3773
|
+
'no-tailwind': {
|
|
3774
|
+
type: 'boolean'
|
|
3775
|
+
},
|
|
3776
|
+
quality: {
|
|
3777
|
+
type: 'string'
|
|
3778
|
+
},
|
|
3779
|
+
formatter: {
|
|
3780
|
+
type: 'string'
|
|
3781
|
+
},
|
|
3782
|
+
linter: {
|
|
3783
|
+
type: 'string'
|
|
3784
|
+
},
|
|
3785
|
+
pm: {
|
|
3786
|
+
type: 'string'
|
|
3787
|
+
},
|
|
3788
|
+
install: {
|
|
3789
|
+
type: 'boolean'
|
|
3790
|
+
},
|
|
3791
|
+
'no-install': {
|
|
3792
|
+
type: 'boolean'
|
|
3793
|
+
},
|
|
3794
|
+
git: {
|
|
3795
|
+
type: 'boolean'
|
|
3796
|
+
},
|
|
3797
|
+
'no-git': {
|
|
3798
|
+
type: 'boolean'
|
|
3799
|
+
},
|
|
3800
|
+
force: {
|
|
3801
|
+
type: 'boolean'
|
|
3802
|
+
},
|
|
3803
|
+
'dry-run': {
|
|
3804
|
+
type: 'boolean'
|
|
3805
|
+
},
|
|
3806
|
+
help: {
|
|
3807
|
+
type: 'boolean',
|
|
3808
|
+
short: 'h'
|
|
3809
|
+
},
|
|
3810
|
+
version: {
|
|
3811
|
+
type: 'boolean',
|
|
3812
|
+
short: 'v'
|
|
3813
|
+
}
|
|
3741
3814
|
},
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3815
|
+
allowPositionals: true
|
|
3816
|
+
});
|
|
3817
|
+
} catch (error) {
|
|
3818
|
+
fail(`${error instanceof Error ? error.message : String(error)}\n\nRun with --help to see the options.`);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
async function main() {
|
|
3822
|
+
const { values, positionals } = parse();
|
|
3749
3823
|
if (values.help) return console.log(HELP);
|
|
3750
|
-
if (values.version) return console.log("1.0.0-rc.
|
|
3751
|
-
// A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer.
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
'yarn',
|
|
3757
|
-
'bun'
|
|
3758
|
-
], 'pm');
|
|
3824
|
+
if (values.version) return console.log("1.0.0-rc.10");
|
|
3825
|
+
// A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both
|
|
3826
|
+
// streams have to be a terminal: the prompts draw on stdout but *read from stdin*, so
|
|
3827
|
+
// `echo | npx @rshono/create` would otherwise ask a question with nothing behind the keyboard.
|
|
3828
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
|
|
3829
|
+
const pmFlag = oneOf(values.pm, PACKAGE_MANAGERS, 'pm');
|
|
3759
3830
|
const pm = pmFlag ? packageManager(pmFlag) : detectPackageManager();
|
|
3760
3831
|
const deployFlag = oneOf(values.deploy, DEPLOY_TARGET_NAMES, 'deploy');
|
|
3761
|
-
const formatterFlag = oneOf(values.formatter,
|
|
3762
|
-
|
|
3763
|
-
'biome',
|
|
3764
|
-
'oxfmt',
|
|
3765
|
-
'none'
|
|
3766
|
-
], 'formatter');
|
|
3767
|
-
const linterFlag = oneOf(values.linter, [
|
|
3768
|
-
'oxlint',
|
|
3769
|
-
'biome',
|
|
3770
|
-
'none'
|
|
3771
|
-
], 'linter');
|
|
3832
|
+
const formatterFlag = oneOf(values.formatter, FORMATTER_NAMES, 'formatter');
|
|
3833
|
+
const linterFlag = oneOf(values.linter, LINTER_NAMES, 'linter');
|
|
3772
3834
|
const qualityFlag = oneOf(values.quality, QUALITY_PRESETS.map((preset)=>preset.id), 'quality');
|
|
3773
3835
|
const tailwindFlag = tristate(values.tailwind, values['no-tailwind'], 'tailwind');
|
|
3774
3836
|
const installFlag = tristate(values.install, values['no-install'], 'install');
|
|
3775
3837
|
const gitFlag = tristate(values.git, values['no-git'], 'git');
|
|
3776
|
-
|
|
3838
|
+
// The framework version, not this package's: it is the one the app will be pinned to, and the one
|
|
3839
|
+
// worth reading here. `--version` reports create-rshono's own.
|
|
3840
|
+
intro(`create-rshono · rshono ${RSHONO_VERSION}`);
|
|
3777
3841
|
// ── Where ───────────────────────────────────────────────────────────────────────────────────────
|
|
3778
3842
|
let directory = positionals[0];
|
|
3779
3843
|
if (!directory) {
|
|
@@ -3787,7 +3851,7 @@ async function main() {
|
|
|
3787
3851
|
const targetDir = resolve(process.cwd(), directory);
|
|
3788
3852
|
const packageName = toPackageName(directory === '.' ? basename(targetDir) : directory);
|
|
3789
3853
|
if (!packageName || !isValidPackageName(packageName)) fail(`"${directory}" does not give a usable npm package name.`);
|
|
3790
|
-
const conflicts =
|
|
3854
|
+
const conflicts = conflictingEntries(targetDir);
|
|
3791
3855
|
if (conflicts.length > 0 && !values.force) {
|
|
3792
3856
|
const where = directory === '.' ? 'this directory' : `"${directory}"`;
|
|
3793
3857
|
const listed = `${conflicts.slice(0, 3).join(', ')}${conflicts.length > 3 ? ', …' : ''}`;
|
|
@@ -3813,7 +3877,7 @@ async function main() {
|
|
|
3813
3877
|
}))
|
|
3814
3878
|
}));
|
|
3815
3879
|
}
|
|
3816
|
-
let styling = tailwindFlag
|
|
3880
|
+
let styling = tailwindFlag ? 'tailwind' : 'css';
|
|
3817
3881
|
if (tailwindFlag === undefined && interactive) {
|
|
3818
3882
|
styling = unwrap(await dist_select({
|
|
3819
3883
|
message: 'Styling?',
|
|
@@ -3832,10 +3896,9 @@ async function main() {
|
|
|
3832
3896
|
]
|
|
3833
3897
|
}));
|
|
3834
3898
|
}
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
*/ let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
|
|
3899
|
+
// One question instead of two. The axes stay independent underneath: a `--formatter` or `--linter`
|
|
3900
|
+
// flag addresses either on its own, and skips the question entirely.
|
|
3901
|
+
let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
|
|
3839
3902
|
if (!preset && !formatterFlag && !linterFlag) {
|
|
3840
3903
|
const fallback = QUALITY_PRESETS["0"];
|
|
3841
3904
|
if (interactive) {
|
|
@@ -3863,9 +3926,10 @@ async function main() {
|
|
|
3863
3926
|
initialValue: true
|
|
3864
3927
|
}));
|
|
3865
3928
|
}
|
|
3866
|
-
|
|
3929
|
+
// Asked once, not once per use: this shells out to `git rev-parse`.
|
|
3930
|
+
const nested = isInsideRepo(process.cwd());
|
|
3931
|
+
let git = gitFlag ?? !nested;
|
|
3867
3932
|
if (gitFlag === undefined && interactive) {
|
|
3868
|
-
const nested = isInsideRepo(process.cwd());
|
|
3869
3933
|
git = unwrap(await dist_confirm({
|
|
3870
3934
|
message: nested ? 'Initialize a git repository? (this is already inside one)' : 'Initialize a git repository?',
|
|
3871
3935
|
initialValue: !nested
|
|
@@ -3873,14 +3937,10 @@ async function main() {
|
|
|
3873
3937
|
}
|
|
3874
3938
|
const answers = {
|
|
3875
3939
|
packageName,
|
|
3876
|
-
targetDir,
|
|
3877
3940
|
deploy,
|
|
3878
3941
|
styling,
|
|
3879
3942
|
formatter,
|
|
3880
|
-
linter
|
|
3881
|
-
packageManager: pm.name,
|
|
3882
|
-
install,
|
|
3883
|
-
git
|
|
3943
|
+
linter
|
|
3884
3944
|
};
|
|
3885
3945
|
// ── Plan, then write ────────────────────────────────────────────────────────────────────────────
|
|
3886
3946
|
const plan = plan_plan(answers, pm);
|
|
@@ -3897,14 +3957,7 @@ async function main() {
|
|
|
3897
3957
|
if (install) {
|
|
3898
3958
|
log.step(`Installing dependencies with ${pm.name}…`);
|
|
3899
3959
|
installed = runInstall(pm, targetDir);
|
|
3900
|
-
if (!installed) log.warn(`${pm.name} install failed — run it yourself
|
|
3901
|
-
}
|
|
3902
|
-
/*
|
|
3903
|
-
* Format the scaffold with the tool it was scaffolded with, so a fresh project passes its own
|
|
3904
|
-
* `format:check` instead of reporting a diff nobody made. Needs the install, since the formatter is a
|
|
3905
|
-
* devDependency — hence the skip, rather than a failure, when there is none.
|
|
3906
|
-
*/ if (installed && formatter !== 'none') {
|
|
3907
|
-
if (!runScript(pm, 'format', targetDir)) log.warn(`\`${pm.run} format\` failed — the files are fine, the formatter is not.`);
|
|
3960
|
+
if (!installed) log.warn(`${pm.name} install failed — the files are all written, so run it yourself in the project.`);
|
|
3908
3961
|
}
|
|
3909
3962
|
if (git) {
|
|
3910
3963
|
if (!hasGit(targetDir)) {
|