@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/cli.mjs
CHANGED
|
@@ -846,7 +846,7 @@ function dist_m(l, e) {
|
|
|
846
846
|
if (e)
|
|
847
847
|
return l ? e : e[0];
|
|
848
848
|
}
|
|
849
|
-
let T$1 = class T extends
|
|
849
|
+
let T$1 = class T extends dist_V {
|
|
850
850
|
filteredOptions;
|
|
851
851
|
multiple;
|
|
852
852
|
isNavigating = false;
|
|
@@ -862,11 +862,11 @@ let T$1 = class T extends (/* unused pure expression or super */ null && (dist_V
|
|
|
862
862
|
}
|
|
863
863
|
get userInputWithCursor() {
|
|
864
864
|
if (!this.userInput)
|
|
865
|
-
return
|
|
865
|
+
return external_node_util_styleText(["inverse", "hidden"], "_");
|
|
866
866
|
if (this._cursor >= this.userInput.length)
|
|
867
867
|
return `${this.userInput}\u2588`;
|
|
868
868
|
const e = this.userInput.slice(0, this.cursor), t = this.userInput.slice(this.cursor, this.cursor + 1), i = this.userInput.slice(this.cursor + 1);
|
|
869
|
-
return `${e}${
|
|
869
|
+
return `${e}${external_node_util_styleText("inverse", t)}${i}`;
|
|
870
870
|
}
|
|
871
871
|
get options() {
|
|
872
872
|
return typeof this.#i == "function" ? this.#i() : this.#i;
|
|
@@ -1160,7 +1160,7 @@ class U extends dist_V {
|
|
|
1160
1160
|
}
|
|
1161
1161
|
}
|
|
1162
1162
|
|
|
1163
|
-
let u$2 = class u extends
|
|
1163
|
+
let u$2 = class u extends dist_V {
|
|
1164
1164
|
options;
|
|
1165
1165
|
cursor = 0;
|
|
1166
1166
|
#t;
|
|
@@ -1364,7 +1364,7 @@ class dist_a extends dist_V {
|
|
|
1364
1364
|
}
|
|
1365
1365
|
}
|
|
1366
1366
|
|
|
1367
|
-
let u$1 = class u extends
|
|
1367
|
+
let u$1 = class u extends dist_V {
|
|
1368
1368
|
_mask = "\u2022";
|
|
1369
1369
|
get cursor() {
|
|
1370
1370
|
return this._cursor;
|
|
@@ -1377,9 +1377,9 @@ let u$1 = class u extends (/* unused pure expression or super */ null && (dist_V
|
|
|
1377
1377
|
return this.masked;
|
|
1378
1378
|
const t = this.userInput;
|
|
1379
1379
|
if (this.cursor >= t.length)
|
|
1380
|
-
return `${this.masked}${
|
|
1380
|
+
return `${this.masked}${external_node_util_styleText(["inverse", "hidden"], "_")}`;
|
|
1381
1381
|
const s = this.masked, r = s.slice(0, this.cursor), i = s.slice(this.cursor, this.cursor + 1), o = s.slice(this.cursor + 1);
|
|
1382
|
-
return `${r}${
|
|
1382
|
+
return `${r}${external_node_util_styleText("inverse", i)}${o}`;
|
|
1383
1383
|
}
|
|
1384
1384
|
clear() {
|
|
1385
1385
|
this._clearUserInput();
|
|
@@ -2906,8 +2906,8 @@ function hasGit(cwd) {
|
|
|
2906
2906
|
], cwd).ok;
|
|
2907
2907
|
}
|
|
2908
2908
|
/**
|
|
2909
|
-
* Whether the new project already sits inside somebody's repository —
|
|
2910
|
-
*
|
|
2909
|
+
* Whether the new project already sits inside somebody's repository — a monorepo or an existing checkout, where
|
|
2910
|
+
* a nested repo would be a surprise rather than a convenience.
|
|
2911
2911
|
*/ function isInsideRepo(cwd) {
|
|
2912
2912
|
const { ok, stdout } = git_git([
|
|
2913
2913
|
'rev-parse',
|
|
@@ -2916,11 +2916,9 @@ function hasGit(cwd) {
|
|
|
2916
2916
|
return ok && stdout === 'true';
|
|
2917
2917
|
}
|
|
2918
2918
|
/**
|
|
2919
|
-
* Initializes a repository and makes the first commit.
|
|
2920
|
-
*
|
|
2921
|
-
*
|
|
2922
|
-
* on a machine with no `user.email` configured. That is not a reason to fail the scaffold — the files
|
|
2923
|
-
* are all there and staged — so the caller reports it and moves on.
|
|
2919
|
+
* Initializes a repository and makes the first commit. `initialized` is the honest answer when the commit itself
|
|
2920
|
+
* fails, as it does with no `user.email` configured — the files are all there and staged, so the caller reports
|
|
2921
|
+
* it and moves on rather than failing the scaffold.
|
|
2924
2922
|
*/ function initRepo(cwd) {
|
|
2925
2923
|
const init = git_git([
|
|
2926
2924
|
'init',
|
|
@@ -2943,22 +2941,21 @@ function hasGit(cwd) {
|
|
|
2943
2941
|
|
|
2944
2942
|
;// CONCATENATED MODULE: ./src/generated/framework.ts
|
|
2945
2943
|
// 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.
|
|
2944
|
+
/** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.20';
|
|
2947
2945
|
/**
|
|
2948
|
-
* The Node range rshono
|
|
2949
|
-
*
|
|
2950
|
-
*/ const NODE_ENGINE = '>=22.
|
|
2946
|
+
* The Node range rshono declares, restated in every scaffolded app's `engines` — so a CI image or a contributor
|
|
2947
|
+
* on an older Node hears it from their package manager rather than from a stack trace.
|
|
2948
|
+
*/ const NODE_ENGINE = '>=22.18.0';
|
|
2951
2949
|
/**
|
|
2952
|
-
* The dependency versions rshono is tested against, copied from its own manifest. Exact where it is
|
|
2953
|
-
*
|
|
2954
|
-
* overrides to fall back on.
|
|
2950
|
+
* The dependency versions rshono is tested against, copied from its own manifest. Exact where it is exact:
|
|
2951
|
+
* React's RSC internals are coupled across builds, and a generated app has no workspace overrides.
|
|
2955
2952
|
*/ const FRAMEWORK_DEPS = {
|
|
2956
|
-
hono: '^4.
|
|
2953
|
+
hono: '^4.13.5',
|
|
2957
2954
|
react: '19.2.8',
|
|
2958
2955
|
'react-dom': '19.2.8',
|
|
2959
2956
|
typescript: '^7.0.2',
|
|
2960
|
-
'@types/node': '^26.
|
|
2961
|
-
'@types/react': '^19.2.
|
|
2957
|
+
'@types/node': '^26.4.0',
|
|
2958
|
+
'@types/react': '^19.2.18'
|
|
2962
2959
|
};
|
|
2963
2960
|
/** Every `deploy` target the installed framework knows, with the command that ships what it built. */ const DEPLOY_TARGETS = [
|
|
2964
2961
|
{
|
|
@@ -2969,22 +2966,10 @@ function hasGit(cwd) {
|
|
|
2969
2966
|
name: 'cloudflare',
|
|
2970
2967
|
hint: 'deploy with `wrangler deploy`'
|
|
2971
2968
|
},
|
|
2972
|
-
{
|
|
2973
|
-
name: 'bun',
|
|
2974
|
-
hint: 'run `bun dist/server/main.mjs`'
|
|
2975
|
-
},
|
|
2976
|
-
{
|
|
2977
|
-
name: 'deno',
|
|
2978
|
-
hint: 'run `deno serve -A dist/server/main.mjs`'
|
|
2979
|
-
},
|
|
2980
2969
|
{
|
|
2981
2970
|
name: 'vercel',
|
|
2982
2971
|
hint: 'deploy with `vercel deploy --prebuilt`'
|
|
2983
2972
|
},
|
|
2984
|
-
{
|
|
2985
|
-
name: 'netlify',
|
|
2986
|
-
hint: 'deploy with `netlify deploy --build=false --dir=.netlify/publish`'
|
|
2987
|
-
},
|
|
2988
2973
|
{
|
|
2989
2974
|
name: 'aws-lambda',
|
|
2990
2975
|
hint: 'zip dist/ with the handler at dist/server/main.mjs'
|
|
@@ -2994,9 +2979,8 @@ function hasGit(cwd) {
|
|
|
2994
2979
|
;// CONCATENATED MODULE: ./src/options.ts
|
|
2995
2980
|
|
|
2996
2981
|
/*
|
|
2997
|
-
* The names each option accepts, spelled once
|
|
2998
|
-
*
|
|
2999
|
-
* — so a name added here reaches all three without a second list to remember.
|
|
2982
|
+
* The names each option accepts, spelled once: the types below are derived from them, the CLI validates its
|
|
2983
|
+
* flags against them and prints them in `--help`, and `pm.ts` recognises a package manager by them.
|
|
3000
2984
|
*/ const FORMATTER_NAMES = [
|
|
3001
2985
|
'prettier',
|
|
3002
2986
|
'biome',
|
|
@@ -3057,20 +3041,26 @@ const QUALITY_PRESETS = [
|
|
|
3057
3041
|
}
|
|
3058
3042
|
];
|
|
3059
3043
|
/**
|
|
3060
|
-
* Turns whatever the user typed into a name npm will accept, or
|
|
3061
|
-
*
|
|
3062
|
-
*
|
|
3044
|
+
* Turns whatever the user typed into a name npm will accept, or `null` when nothing usable is left.
|
|
3045
|
+
* Lowercasing and replacing runs of invalid characters covers the ordinary cases (`My App`, `my_app`).
|
|
3046
|
+
*
|
|
3047
|
+
* The promise is checked rather than assumed: the result goes through {@link isValidPackageName} on the way
|
|
3048
|
+
* out. It used to be spelled as a rule per character class here and the check made again by the caller, which
|
|
3049
|
+
* left the exported function able to return a name npm refuses — `_leading` was one, because a leading
|
|
3050
|
+
* underscore is stripped by npm's rule and not by this one.
|
|
3063
3051
|
*/ function toPackageName(input) {
|
|
3064
3052
|
const trimmed = input.trim().replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
3065
3053
|
if (!trimmed || trimmed === '.') return null;
|
|
3066
|
-
// A scoped name is a name, not a path: `@scope/pkg` stays whole
|
|
3067
|
-
//
|
|
3054
|
+
// A scoped name is a name, not a path: `@scope/pkg` stays whole. Anything else is a path, whose last
|
|
3055
|
+
// segment names the project.
|
|
3068
3056
|
const scoped = /^@[^\\/]+[\\/][^\\/]+$/.test(trimmed);
|
|
3069
3057
|
const base = scoped ? trimmed : trimmed.split(/[\\/]/).filter(Boolean).pop() ?? '';
|
|
3070
3058
|
if (!base) return null;
|
|
3071
|
-
const name = base.toLowerCase().replace(/[\\/]/g, '/').replace(/[^a-z\d\-._~/@]+/g, '-')
|
|
3072
|
-
|
|
3073
|
-
|
|
3059
|
+
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
|
|
3060
|
+
// behind once the invalid runs are replaced.
|
|
3061
|
+
.replace(/^[-_]+/, '').replace(/-+$/, '');
|
|
3062
|
+
const trimmedName = name.slice(0, 214);
|
|
3063
|
+
return isValidPackageName(trimmedName) ? trimmedName : null;
|
|
3074
3064
|
}
|
|
3075
3065
|
/** npm's own rule, narrowed to what we ever generate: no uppercase, no leading dot or underscore. */ function isValidPackageName(name) {
|
|
3076
3066
|
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(name) && name.length <= 214;
|
|
@@ -3078,12 +3068,12 @@ const QUALITY_PRESETS = [
|
|
|
3078
3068
|
|
|
3079
3069
|
;// CONCATENATED MODULE: ./src/features/combinations.ts
|
|
3080
3070
|
/**
|
|
3081
|
-
* Biome's CSS parser rejects Tailwind's syntax — `@apply`
|
|
3082
|
-
*
|
|
3083
|
-
*
|
|
3071
|
+
* Biome's CSS parser rejects Tailwind's syntax — `@apply` is a parse error, not an unknown-at-rule warning — so
|
|
3072
|
+
* a project with both needs Biome pointed away from stylesheets. The overlay is a second `biome.json` listed
|
|
3073
|
+
* after the first.
|
|
3084
3074
|
*
|
|
3085
|
-
* Narrow on purpose:
|
|
3086
|
-
*
|
|
3075
|
+
* Narrow on purpose: excluding CSS for everybody would give up formatting the plain-CSS template that Biome
|
|
3076
|
+
* handles perfectly well.
|
|
3087
3077
|
*/ const BIOME_TAILWIND = {
|
|
3088
3078
|
id: 'biome-tailwind',
|
|
3089
3079
|
overlays: [
|
|
@@ -3103,19 +3093,85 @@ const QUALITY_PRESETS = [
|
|
|
3103
3093
|
] : [];
|
|
3104
3094
|
}
|
|
3105
3095
|
|
|
3096
|
+
;// CONCATENATED MODULE: ./src/scripts.ts
|
|
3097
|
+
// `features/types.js` rather than `features/index.js`: the deploy feature imports `invoke` from here, and
|
|
3098
|
+
// going through the barrel would make that a cycle — types.ts imports nothing.
|
|
3099
|
+
/**
|
|
3100
|
+
* The scripts every app gets, whatever it targets.
|
|
3101
|
+
*
|
|
3102
|
+
* `start`, `preview` and `deploy` are not here: each means something different per platform, so the deploy
|
|
3103
|
+
* target contributes its own. The three names are a contract the targets keep, which is what lets the README
|
|
3104
|
+
* describe an app it was not written for:
|
|
3105
|
+
*
|
|
3106
|
+
* - **`start`** runs a build that already exists and never makes one — what a host's own start command calls.
|
|
3107
|
+
* - **`preview`** builds, then runs the result here, so the production build is answerable without deploying.
|
|
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. The manifest and
|
|
3116
|
+
* the README's command table are both this, so neither can document a script the other lacks.
|
|
3117
|
+
*/ function buildScripts(features) {
|
|
3118
|
+
const scripts = {
|
|
3119
|
+
...BASE_SCRIPTS
|
|
3120
|
+
};
|
|
3121
|
+
for (const feature of features)Object.assign(scripts, feature.scripts);
|
|
3122
|
+
return scripts;
|
|
3123
|
+
}
|
|
3124
|
+
/** The gloss for each base script, in the README's command table. `build` names the target it is for. */ function baseScriptHelp(deploy) {
|
|
3125
|
+
return {
|
|
3126
|
+
dev: 'dev server with HMR, http://localhost:3000',
|
|
3127
|
+
build: `production build for ${deploy}`,
|
|
3128
|
+
typecheck: 'tsc --noEmit'
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Script names a package manager has a command of its own for: `pnpm deploy` runs pnpm's workspace-deploy and
|
|
3133
|
+
* never looks at the manifest. These get the explicit `run` form, which all four managers accept.
|
|
3134
|
+
*/ const SHADOWED = new Set([
|
|
3135
|
+
'deploy'
|
|
3136
|
+
]);
|
|
3137
|
+
/** How to type one of the app's scripts with this package manager — `pnpm dev`, but `npm run dev`. */ function invoke(pm, script) {
|
|
3138
|
+
return `${SHADOWED.has(script) ? `${pm.name} run` : pm.run} ${script}`;
|
|
3139
|
+
}
|
|
3140
|
+
/**
|
|
3141
|
+
* The README's command table: one line per script, with the command to type and a one-line gloss. A script whose
|
|
3142
|
+
* feature supplies no `scriptHelp` is left out — that is how `format:check` stays out of a table about running
|
|
3143
|
+
* the app — and covered by the line pointing at `package.json`.
|
|
3144
|
+
*/ function scriptTable(answers, features, pm) {
|
|
3145
|
+
const help = baseScriptHelp(answers.deploy);
|
|
3146
|
+
for (const feature of features)Object.assign(help, feature.scriptHelp);
|
|
3147
|
+
const documented = Object.keys(buildScripts(features)).filter((name)=>help[name]);
|
|
3148
|
+
const width = Math.max(...documented.map((name)=>invoke(pm, name).length));
|
|
3149
|
+
return documented.map((name)=>`${invoke(pm, name).padEnd(width)} # ${help[name]}`).join('\n');
|
|
3150
|
+
}
|
|
3151
|
+
/**
|
|
3152
|
+
* The command that gets this app into production, as a sentence — the README's deploy step and the closing
|
|
3153
|
+
* summary's, so the two cannot name different commands.
|
|
3154
|
+
*
|
|
3155
|
+
* Read off the scripts rather than the target, so a target that gains a `deploy` gains the sentence with it.
|
|
3156
|
+
*/ function deployStep(features, pm) {
|
|
3157
|
+
const scripts = buildScripts(features);
|
|
3158
|
+
if (scripts.deploy) return `\`${invoke(pm, 'deploy')}\` does the build and the upload in one step.`;
|
|
3159
|
+
if (scripts.start) return `\`${invoke(pm, 'start')}\` runs that build wherever you host it, and never makes one.`;
|
|
3160
|
+
if (scripts.preview) return `\`${invoke(pm, 'preview')}\` runs the build here, so you can check it first.`;
|
|
3161
|
+
return `\`${invoke(pm, 'build')}\` produces it; getting it there is yours to script.`;
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3106
3164
|
;// CONCATENATED MODULE: ./src/versions.ts
|
|
3107
3165
|
|
|
3108
3166
|
/** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
|
|
3109
3167
|
/** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
|
|
3110
3168
|
/**
|
|
3111
|
-
* Versions for the optional tooling the features can add — the one place in this package where a
|
|
3112
|
-
*
|
|
3113
|
-
*
|
|
3114
|
-
* which is generated from rshono's own manifest and must not be edited here.
|
|
3169
|
+
* Versions for the optional tooling the features can add — the one place in this package where a range is typed
|
|
3170
|
+
* by hand, because the framework declares none of these. Everything a scaffolded app needs to *run* rshono comes
|
|
3171
|
+
* from {@link FRAMEWORK_DEPS}, generated from rshono's own manifest.
|
|
3115
3172
|
*
|
|
3116
|
-
*
|
|
3117
|
-
*
|
|
3118
|
-
* was last touched.
|
|
3173
|
+
* Caret ranges, not exact: these are the app's own dev tools, and a scaffold made six months from now should
|
|
3174
|
+
* pick up their patch releases.
|
|
3119
3175
|
*/ const TOOL_VERSIONS = {
|
|
3120
3176
|
tailwindcss: '^4.3.3',
|
|
3121
3177
|
'@tailwindcss/postcss': '^4.3.3',
|
|
@@ -3127,126 +3183,148 @@ const QUALITY_PRESETS = [
|
|
|
3127
3183
|
oxlint: '^1.76.0',
|
|
3128
3184
|
oxfmt: '^0.61.0',
|
|
3129
3185
|
eslint: '^10.8.0',
|
|
3130
|
-
// ESLint's
|
|
3131
|
-
//
|
|
3186
|
+
// ESLint's recommended JavaScript rules, which typescript-eslint layers on rather than replaces, and the rules
|
|
3187
|
+
// of hooks — the one class of React mistake no type checker sees.
|
|
3132
3188
|
'@eslint/js': '^10.0.1',
|
|
3133
3189
|
'typescript-eslint': '^8.65.0',
|
|
3134
3190
|
'eslint-plugin-react-hooks': '^7.1.1',
|
|
3135
3191
|
wrangler: '^4.115.0'
|
|
3136
3192
|
};
|
|
3137
3193
|
/**
|
|
3138
|
-
* The TypeScript an ESLint app pins
|
|
3139
|
-
* {@link FRAMEWORK_DEPS}
|
|
3194
|
+
* The TypeScript an ESLint app pins in place of the framework's own — the one deliberate exception to
|
|
3195
|
+
* {@link FRAMEWORK_DEPS}.
|
|
3140
3196
|
*
|
|
3141
|
-
* typescript-eslint reads TypeScript's compiler API directly
|
|
3142
|
-
*
|
|
3143
|
-
*
|
|
3144
|
-
* rshono's declarations compile the same under either, which is what makes this pin an app's business
|
|
3145
|
-
* and not the framework's.
|
|
3146
|
-
*
|
|
3147
|
-
* When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
|
|
3197
|
+
* typescript-eslint reads TypeScript's compiler API directly, so it accepts `>=4.8.4 <6.1.0` and nothing above;
|
|
3198
|
+
* `~6.0.3` is the newest that satisfies it. rshono's declarations compile the same under either, which is what
|
|
3199
|
+
* makes this the app's business. When upstream widens its range, this constant is what to delete.
|
|
3148
3200
|
*/ const ESLINT_TYPESCRIPT = '~6.0.3';
|
|
3149
3201
|
|
|
3150
3202
|
;// CONCATENATED MODULE: ./src/features/deploy.ts
|
|
3151
3203
|
|
|
3204
|
+
|
|
3152
3205
|
/**
|
|
3153
|
-
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts
|
|
3154
|
-
* to read, and the template's to carry.
|
|
3206
|
+
* What a deploy target adds beyond the `deploy` line in `rshono.config.ts`, which the template carries.
|
|
3155
3207
|
*
|
|
3156
|
-
*
|
|
3157
|
-
*
|
|
3158
|
-
*
|
|
3159
|
-
* be a copy that goes stale.
|
|
3208
|
+
* Thin on purpose: the framework arranges its own output for every platform, and `rshono build` writes the one
|
|
3209
|
+
* platform config that has to exist. So a target contributes the commands that run and ship the build, the CLI
|
|
3210
|
+
* they need, the directories to gitignore, and a note for the step no command covers.
|
|
3160
3211
|
*
|
|
3161
|
-
*
|
|
3162
|
-
*
|
|
3163
|
-
*
|
|
3164
|
-
*/
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
start: 'deno serve -A dist/server/main.mjs'
|
|
3185
|
-
}
|
|
3186
|
-
},
|
|
3187
|
-
cloudflare: {
|
|
3188
|
-
id: 'deploy-cloudflare',
|
|
3189
|
-
devDependencies: {
|
|
3190
|
-
wrangler: TOOL_VERSIONS.wrangler
|
|
3191
|
-
},
|
|
3192
|
-
// wrangler brings workerd, whose install script only picks the platform binary out of the optional
|
|
3193
|
-
// dependency that already carries it — `workerd --version` answers without it having run.
|
|
3194
|
-
allowBuilds: {
|
|
3195
|
-
workerd: false
|
|
3196
|
-
},
|
|
3197
|
-
scripts: {
|
|
3198
|
-
deploy: 'rshono build && wrangler deploy'
|
|
3212
|
+
* What the three script names promise is documented above `BASE_SCRIPTS` in `scripts.ts`. Only `node` has a
|
|
3213
|
+
* `start` — `rshono start` refuses a bundle built for anywhere else — and it needs no `preview`, since `build`
|
|
3214
|
+
* then `start` already is one.
|
|
3215
|
+
*/ function deployFeatures(pm) {
|
|
3216
|
+
const build = invoke(pm, 'build');
|
|
3217
|
+
return {
|
|
3218
|
+
// Where a Node build goes next is a Dockerfile or a process manager, neither of which this can guess.
|
|
3219
|
+
node: {
|
|
3220
|
+
id: 'deploy-node',
|
|
3221
|
+
scripts: {
|
|
3222
|
+
start: 'rshono start'
|
|
3223
|
+
},
|
|
3224
|
+
scriptHelp: {
|
|
3225
|
+
start: 'run the build that exists — what your host calls'
|
|
3226
|
+
},
|
|
3227
|
+
platformSetup: [
|
|
3228
|
+
'The two commands a host asks for:',
|
|
3229
|
+
'',
|
|
3230
|
+
`- **Build** — \`${pm.name} install && ${build}\``,
|
|
3231
|
+
`- **Start** — \`${invoke(pm, 'start')}\``,
|
|
3232
|
+
'',
|
|
3233
|
+
`In a Dockerfile, the same pair: \`RUN ${build}\`, then \`CMD ["${pm.name}", "start"]\`.`
|
|
3234
|
+
].join('\n')
|
|
3199
3235
|
},
|
|
3200
|
-
|
|
3201
|
-
'
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3236
|
+
cloudflare: {
|
|
3237
|
+
id: 'deploy-cloudflare',
|
|
3238
|
+
devDependencies: {
|
|
3239
|
+
wrangler: TOOL_VERSIONS.wrangler
|
|
3240
|
+
},
|
|
3241
|
+
// The only two install scripts a scaffolded app can end up with, and wrangler brings both. Each merely
|
|
3242
|
+
// picks the platform binary out of the optional dependency already carrying it, so neither needs to run.
|
|
3243
|
+
allowBuilds: {
|
|
3244
|
+
esbuild: false,
|
|
3245
|
+
workerd: false
|
|
3246
|
+
},
|
|
3247
|
+
// `wrangler dev` is the one preview that runs the code in workerd rather than Node. Both scripts read the
|
|
3248
|
+
// wrangler.jsonc the build wrote, so nothing here has to know where the bundle went.
|
|
3249
|
+
scripts: {
|
|
3250
|
+
preview: 'rshono build && wrangler dev',
|
|
3251
|
+
deploy: 'rshono build && wrangler deploy'
|
|
3252
|
+
},
|
|
3253
|
+
scriptHelp: {
|
|
3254
|
+
preview: 'build, then run it in workerd — port 8787',
|
|
3255
|
+
deploy: 'build, then ship it to Cloudflare'
|
|
3256
|
+
},
|
|
3257
|
+
gitignore: [
|
|
3258
|
+
'.wrangler/'
|
|
3259
|
+
],
|
|
3260
|
+
notes: [
|
|
3261
|
+
'The first build writes wrangler.jsonc — yours to edit after that.'
|
|
3262
|
+
],
|
|
3263
|
+
platformSetup: [
|
|
3264
|
+
`Building from a git repo instead: set Workers Builds' **Build command** to \`${build}\`.`,
|
|
3265
|
+
'Its deploy command already defaults to `npx wrangler deploy`, and it installs dependencies itself.'
|
|
3266
|
+
].join('\n')
|
|
3211
3267
|
},
|
|
3212
|
-
|
|
3213
|
-
'
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3268
|
+
vercel: {
|
|
3269
|
+
id: 'deploy-vercel',
|
|
3270
|
+
// `--prod` because the script is called `deploy`: without it the CLI uploads to a throwaway preview URL.
|
|
3271
|
+
// `preview` is a Node build run here — the platform cannot run its own prebuilt output on your machine.
|
|
3272
|
+
scripts: {
|
|
3273
|
+
preview: 'rshono build --deploy node && rshono start',
|
|
3274
|
+
deploy: `rshono build && ${pm.dlx} vercel deploy --prebuilt --prod`
|
|
3275
|
+
},
|
|
3276
|
+
scriptHelp: {
|
|
3277
|
+
preview: 'build for Node and run that here',
|
|
3278
|
+
deploy: 'build, then upload it to production'
|
|
3279
|
+
},
|
|
3280
|
+
gitignore: [
|
|
3281
|
+
'.vercel/'
|
|
3282
|
+
],
|
|
3283
|
+
notes: [
|
|
3284
|
+
'--prebuilt uploads what rshono build assembled; the platform must not rebuild it.',
|
|
3285
|
+
'Drop --prod from the deploy script for a preview URL instead.'
|
|
3286
|
+
],
|
|
3287
|
+
platformSetup: [
|
|
3288
|
+
`Deploying from CI instead: the same \`${invoke(pm, 'deploy')}\`, with \`VERCEL_ORG_ID\`, \`VERCEL_PROJECT_ID\``,
|
|
3289
|
+
'and a CLI token in the environment.',
|
|
3290
|
+
'',
|
|
3291
|
+
`If you let Vercel build the repo itself, set **Framework Preset** to Other — \`hono\` is otherwise`,
|
|
3292
|
+
`detected as the Hono preset — and **Build Command** to \`${build}\`.`
|
|
3293
|
+
].join('\n')
|
|
3223
3294
|
},
|
|
3224
|
-
|
|
3225
|
-
'
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3295
|
+
'aws-lambda': {
|
|
3296
|
+
id: 'deploy-aws-lambda',
|
|
3297
|
+
// No CLI to wrap and no upload to guess at, but `preview` still applies: the bundle is a Node handler.
|
|
3298
|
+
scripts: {
|
|
3299
|
+
preview: 'rshono build --deploy node && rshono start'
|
|
3300
|
+
},
|
|
3301
|
+
scriptHelp: {
|
|
3302
|
+
preview: 'build for Node and run that here'
|
|
3303
|
+
},
|
|
3304
|
+
notes: [
|
|
3305
|
+
'Use a Function URL in RESPONSE_STREAM mode — a buffered invoke mode drops the streaming.'
|
|
3306
|
+
],
|
|
3307
|
+
platformSetup: [
|
|
3308
|
+
`There is no settings page here; the upload is yours to script. A job needs \`${pm.name} install && ${build}\`,`,
|
|
3309
|
+
'then the function package: `dist/` and nothing else, with the handler at `dist/server/main.mjs`. Your',
|
|
3310
|
+
'dependencies are compiled into the bundle for this target, so no `node_modules` is uploaded — which also',
|
|
3311
|
+
'means a native addon fails the build here rather than the deploy.'
|
|
3312
|
+
].join('\n')
|
|
3313
|
+
}
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
function deployFeature(target, pm) {
|
|
3317
|
+
return deployFeatures(pm)[target];
|
|
3237
3318
|
}
|
|
3238
3319
|
|
|
3239
3320
|
;// CONCATENATED MODULE: ./src/features/quality.ts
|
|
3240
3321
|
|
|
3241
3322
|
/**
|
|
3242
|
-
* The formatter and linter features. Biome answers to both slots and appears once
|
|
3243
|
-
* deduplicates by `id
|
|
3244
|
-
* dependency and one pair of scripts.
|
|
3323
|
+
* The formatter and linter features. Biome answers to both slots and appears once, because `selectFeatures`
|
|
3324
|
+
* deduplicates by `id`.
|
|
3245
3325
|
*
|
|
3246
|
-
*
|
|
3247
|
-
*
|
|
3248
|
-
* `lint:fix` beside `lint`, for the same reason in the other direction — `lint` is already the failing
|
|
3249
|
-
* one. Biome adds a `check` of its own, which is the pair of them in a single pass.
|
|
3326
|
+
* Each brings a pair of scripts, since the writing half and the CI half want different exit codes: `format`
|
|
3327
|
+
* rewrites and `format:check` fails, `lint:fix` rewrites and `lint` fails. Biome's `check` is both in one pass.
|
|
3250
3328
|
*/ const PRETTIER = {
|
|
3251
3329
|
id: 'prettier',
|
|
3252
3330
|
overlays: [
|
|
@@ -3288,11 +3366,9 @@ const OXLINT = {
|
|
|
3288
3366
|
};
|
|
3289
3367
|
/**
|
|
3290
3368
|
* The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
|
|
3291
|
-
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest
|
|
3292
|
-
*
|
|
3293
|
-
*
|
|
3294
|
-
* The rules are type-aware, which is the reason to reach for ESLint over a syntax-only linter at all —
|
|
3295
|
-
* so the config it ships hands the whole program to the parser rather than linting file by file.
|
|
3369
|
+
* installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest its peer range
|
|
3370
|
+
* accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to reach for ESLint at all —
|
|
3371
|
+
* so the config it ships hands the parser the whole program.
|
|
3296
3372
|
*/ const ESLINT = {
|
|
3297
3373
|
id: 'eslint',
|
|
3298
3374
|
overlays: [
|
|
@@ -3308,6 +3384,12 @@ const OXLINT = {
|
|
|
3308
3384
|
scripts: {
|
|
3309
3385
|
lint: 'eslint .',
|
|
3310
3386
|
'lint:fix': 'eslint . --fix'
|
|
3387
|
+
},
|
|
3388
|
+
// The pin is invisible from the command line, and its consequence is not: the rules reason about the
|
|
3389
|
+
// program through an older compiler than the one that builds it, so a fix can produce code `tsc` rejects.
|
|
3390
|
+
// The generated eslint.config.mjs explains why; the README's table is where someone reads what to type.
|
|
3391
|
+
scriptHelp: {
|
|
3392
|
+
'lint:fix': 'apply what it can — then run typecheck, see eslint.config.mjs'
|
|
3311
3393
|
}
|
|
3312
3394
|
};
|
|
3313
3395
|
const BIOME = {
|
|
@@ -3348,14 +3430,12 @@ function linterFeature(linter) {
|
|
|
3348
3430
|
;// CONCATENATED MODULE: ./src/features/styling.ts
|
|
3349
3431
|
|
|
3350
3432
|
/**
|
|
3351
|
-
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages,
|
|
3352
|
-
*
|
|
3353
|
-
*
|
|
3354
|
-
* written in utilities instead of classes of their own.
|
|
3433
|
+
* Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages, plus an
|
|
3434
|
+
* overlay carrying `postcss.config.mjs`, an `rshono.config.ts` whose `rspack` hook puts postcss-loader in front
|
|
3435
|
+
* of the CSS parser, a Tailwind entry stylesheet, and the two views rewritten in utilities.
|
|
3355
3436
|
*
|
|
3356
|
-
* `postcss`
|
|
3357
|
-
*
|
|
3358
|
-
* install one.
|
|
3437
|
+
* `postcss` is the app's dependency rather than the framework's: rshono compiles CSS natively, so an app that
|
|
3438
|
+
* wants no plugin chain installs none.
|
|
3359
3439
|
*/ const TAILWIND = {
|
|
3360
3440
|
id: 'tailwind',
|
|
3361
3441
|
overlays: [
|
|
@@ -3378,12 +3458,14 @@ function stylingFeature(styling) {
|
|
|
3378
3458
|
|
|
3379
3459
|
|
|
3380
3460
|
/**
|
|
3381
|
-
* The features a set of answers selects, in application order — so an overlay listed later wins a file
|
|
3382
|
-
*
|
|
3383
|
-
*
|
|
3384
|
-
|
|
3461
|
+
* The features a set of answers selects, in application order — so an overlay listed later wins a file both
|
|
3462
|
+
* ship. Deduplicated by `id`, which is what lets one feature answer two questions (Biome is both formatter and
|
|
3463
|
+
* linter) without contributing twice.
|
|
3464
|
+
*
|
|
3465
|
+
* `pm` reaches the deploy target because one script has to name the runner that fetches an uninstalled CLI.
|
|
3466
|
+
*/ function selectFeatures(answers, pm) {
|
|
3385
3467
|
const selected = [
|
|
3386
|
-
deployFeature(answers.deploy),
|
|
3468
|
+
deployFeature(answers.deploy, pm),
|
|
3387
3469
|
stylingFeature(answers.styling),
|
|
3388
3470
|
formatterFeature(answers.formatter),
|
|
3389
3471
|
linterFeature(answers.linter),
|
|
@@ -3401,15 +3483,7 @@ function stylingFeature(styling) {
|
|
|
3401
3483
|
|
|
3402
3484
|
;// CONCATENATED MODULE: ./src/pkg.ts
|
|
3403
3485
|
|
|
3404
|
-
|
|
3405
|
-
* The scripts every app gets. `start` is not among them, because it means something different per
|
|
3406
|
-
* platform: the three targets that run the build themselves — node, bun, deno — each contribute their
|
|
3407
|
-
* own, and a platform target contributes a `deploy` instead, where its platform has one command to give.
|
|
3408
|
-
*/ const BASE_SCRIPTS = {
|
|
3409
|
-
dev: 'rshono dev',
|
|
3410
|
-
build: 'rshono build',
|
|
3411
|
-
typecheck: 'tsc --noEmit'
|
|
3412
|
-
};
|
|
3486
|
+
|
|
3413
3487
|
/** Field order in the emitted file — the conventional reading order, and stable so snapshots are too. */ const FIELD_ORDER = [
|
|
3414
3488
|
'name',
|
|
3415
3489
|
'version',
|
|
@@ -3425,48 +3499,29 @@ function sorted(record) {
|
|
|
3425
3499
|
return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
|
|
3426
3500
|
}
|
|
3427
3501
|
/**
|
|
3428
|
-
*
|
|
3429
|
-
* reads `rshono.config.ts` through esbuild, and esbuild's script only picks the platform binary out of
|
|
3430
|
-
* the optional dependency that already carries it — rshono's own repo denies it for the same reason.
|
|
3431
|
-
*/ const BASE_ALLOW_BUILDS = {
|
|
3432
|
-
esbuild: false
|
|
3433
|
-
};
|
|
3434
|
-
/**
|
|
3435
|
-
* pnpm's settings for the new app — written for pnpm and for nobody else.
|
|
3436
|
-
*
|
|
3437
|
-
* It exists for one field. A dependency with an install script is a question pnpm will not answer on its
|
|
3438
|
-
* own: it fails the install, and fails every `pnpm dev` after it, until the project has said whether the
|
|
3439
|
-
* script should run. None of the ones an rshono app inherits need to (each is a native package whose
|
|
3440
|
-
* binary arrives as an optional dependency), so a fresh app carries the answer rather than meeting
|
|
3441
|
-
* `pnpm approve-builds` before it has rendered a page once.
|
|
3502
|
+
* pnpm's settings for the new app, or `null` when no feature has anything to put in them.
|
|
3442
3503
|
*
|
|
3443
|
-
*
|
|
3444
|
-
*
|
|
3445
|
-
*
|
|
3504
|
+
* It exists for one field, `allowBuilds`: pnpm fails an install until the project has said whether a
|
|
3505
|
+
* dependency's install script should run. Nothing rshono installs has one, so most apps get no file. A file
|
|
3506
|
+
* rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
|
|
3446
3507
|
*/ function buildPnpmSettings(features) {
|
|
3447
|
-
const allowBuilds = {
|
|
3448
|
-
...BASE_ALLOW_BUILDS
|
|
3449
|
-
};
|
|
3508
|
+
const allowBuilds = {};
|
|
3450
3509
|
for (const feature of features)Object.assign(allowBuilds, feature.allowBuilds);
|
|
3510
|
+
const entries = Object.entries(sorted(allowBuilds));
|
|
3511
|
+
if (entries.length === 0) return null;
|
|
3451
3512
|
return [
|
|
3452
3513
|
'# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
|
|
3453
3514
|
'# fails the install rather than skip one quietly — so anything added later belongs here too.',
|
|
3454
3515
|
'# `false` means the script was looked at: these ship their real binary as an optional dependency.',
|
|
3455
3516
|
'allowBuilds:',
|
|
3456
|
-
...
|
|
3517
|
+
...entries.map(([name, allowed])=>` ${name}: ${allowed}`),
|
|
3457
3518
|
''
|
|
3458
3519
|
].join('\n');
|
|
3459
3520
|
}
|
|
3460
3521
|
/**
|
|
3461
|
-
* Assembles `package.json` from the answers and whatever the selected features contribute.
|
|
3462
|
-
*
|
|
3463
|
-
* Dependencies are sorted by name and scripts are left in contribution order (the base ones, then each
|
|
3464
|
-
* feature's, in the order features were selected) — so two runs with the same answers produce byte-
|
|
3465
|
-
* identical output, which is what makes the generated manifest snapshot-testable.
|
|
3522
|
+
* Assembles `package.json` from the answers and whatever the selected features contribute. Dependencies are
|
|
3523
|
+
* sorted and scripts keep {@link buildScripts}'s order, so two runs with the same answers are byte-identical.
|
|
3466
3524
|
*/ function buildPackageJson(answers, features, pm) {
|
|
3467
|
-
const scripts = {
|
|
3468
|
-
...BASE_SCRIPTS
|
|
3469
|
-
};
|
|
3470
3525
|
const dependencies = {
|
|
3471
3526
|
'@rshono/core': RSHONO_RANGE,
|
|
3472
3527
|
hono: FRAMEWORK_DEPS.hono,
|
|
@@ -3479,7 +3534,6 @@ function sorted(record) {
|
|
|
3479
3534
|
typescript: FRAMEWORK_DEPS.typescript
|
|
3480
3535
|
};
|
|
3481
3536
|
for (const feature of features){
|
|
3482
|
-
Object.assign(scripts, feature.scripts);
|
|
3483
3537
|
Object.assign(dependencies, feature.dependencies);
|
|
3484
3538
|
Object.assign(devDependencies, feature.devDependencies);
|
|
3485
3539
|
}
|
|
@@ -3492,12 +3546,12 @@ function sorted(record) {
|
|
|
3492
3546
|
engines: {
|
|
3493
3547
|
node: NODE_ENGINE
|
|
3494
3548
|
},
|
|
3495
|
-
scripts,
|
|
3549
|
+
scripts: buildScripts(features),
|
|
3496
3550
|
dependencies: sorted(dependencies),
|
|
3497
3551
|
devDependencies: sorted(devDependencies)
|
|
3498
3552
|
};
|
|
3499
|
-
// Only when the environment told us the exact version:
|
|
3500
|
-
//
|
|
3553
|
+
// Only when the environment told us the exact version: this field pins the tool for Corepack, and a guess is
|
|
3554
|
+
// worse than leaving it out.
|
|
3501
3555
|
if (pm.version) manifest.packageManager = `${pm.name}@${pm.version}`;
|
|
3502
3556
|
const ordered = Object.fromEntries(FIELD_ORDER.filter((field)=>field in manifest).map((field)=>[
|
|
3503
3557
|
field,
|
|
@@ -3509,23 +3563,22 @@ function sorted(record) {
|
|
|
3509
3563
|
;// CONCATENATED MODULE: ./src/render.ts
|
|
3510
3564
|
|
|
3511
3565
|
/**
|
|
3512
|
-
* `{{NAME}}`,
|
|
3513
|
-
*
|
|
3514
|
-
* turns a token into literal text that no substitution will ever match again. `{{…}}` means nothing to
|
|
3515
|
-
* any of the formats these templates are written in.
|
|
3566
|
+
* `{{NAME}}`, not `__NAME__`: in markdown the latter is strong emphasis, so Prettier rewrites it to `**NAME**`
|
|
3567
|
+
* and the token stops matching. `{{…}}` means nothing to any format these templates are written in.
|
|
3516
3568
|
*/ const TOKEN_PATTERN = /\{\{[A-Z][A-Z\d_]*\}\}/g;
|
|
3517
|
-
function tokensFor(answers, pm) {
|
|
3569
|
+
function tokensFor(answers, features, pm) {
|
|
3518
3570
|
return {
|
|
3519
3571
|
'{{PROJECT_NAME}}': answers.packageName,
|
|
3520
3572
|
'{{DEPLOY_TARGET}}': answers.deploy,
|
|
3521
|
-
|
|
3522
|
-
'{{
|
|
3573
|
+
// From the features rather than the answers, because all three are about the scripts the app actually got.
|
|
3574
|
+
'{{SCRIPT_TABLE}}': scriptTable(answers, features, pm),
|
|
3575
|
+
'{{DEPLOY_STEP}}': deployStep(features, pm),
|
|
3576
|
+
'{{PLATFORM_SETUP}}': features.map((feature)=>feature.platformSetup ?? '').join('')
|
|
3523
3577
|
};
|
|
3524
3578
|
}
|
|
3525
3579
|
/**
|
|
3526
|
-
* Substitutes tokens, and throws on one it
|
|
3527
|
-
*
|
|
3528
|
-
* catch.
|
|
3580
|
+
* Substitutes tokens, and throws on one it does not know — a typo in a template would otherwise ship a literal
|
|
3581
|
+
* `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would catch.
|
|
3529
3582
|
*/ function render(contents, tokens, source) {
|
|
3530
3583
|
return contents.replace(TOKEN_PATTERN, (token)=>{
|
|
3531
3584
|
const value = tokens[token];
|
|
@@ -3555,12 +3608,9 @@ function readTemplateDir(dir) {
|
|
|
3555
3608
|
return files;
|
|
3556
3609
|
}
|
|
3557
3610
|
/**
|
|
3558
|
-
* `_gitignore` → `.gitignore`, and so on for every dotfile
|
|
3559
|
-
*
|
|
3560
|
-
*
|
|
3561
|
-
* one — the file would exist in the repo, pass every local test, and be missing from the package
|
|
3562
|
-
* everybody actually installs. Naming them with an underscore and renaming here is the long-standing
|
|
3563
|
-
* fix. It applies to the basename only, so `src/lib/_x.ts` is a dotfile but `templates/_x/y.ts` is not.
|
|
3611
|
+
* `_gitignore` → `.gitignore`, and so on for every dotfile: npm strips a literal `.gitignore` out of a
|
|
3612
|
+
* published tarball, so a template that contained one would pass every local test and be missing from the
|
|
3613
|
+
* package everybody installs. The basename only, so `templates/_x/y.ts` is left alone.
|
|
3564
3614
|
*/ function undotted(path) {
|
|
3565
3615
|
const segments = path.split(posix.sep);
|
|
3566
3616
|
const name = segments.pop();
|
|
@@ -3569,22 +3619,19 @@ function readTemplateDir(dir) {
|
|
|
3569
3619
|
name.startsWith('_') ? `.${name.slice(1)}` : name
|
|
3570
3620
|
].join(posix.sep);
|
|
3571
3621
|
}
|
|
3572
|
-
/**
|
|
3573
|
-
* A feature's `.gitignore` lines, appended under a heading naming it — so somebody reading the file six
|
|
3574
|
-
* months later can tell why `.wrangler/` is in there.
|
|
3575
|
-
*/ function appendGitignore(existing, features) {
|
|
3622
|
+
/** A feature's `.gitignore` lines, under a heading naming it — so a reader can tell why `.wrangler/` is there. */ function appendGitignore(existing, features) {
|
|
3576
3623
|
const additions = features.filter((feature)=>feature.gitignore?.length);
|
|
3577
3624
|
if (additions.length === 0) return existing;
|
|
3578
3625
|
const blocks = additions.map((feature)=>`\n# ${feature.id}\n${feature.gitignore.join('\n')}\n`);
|
|
3579
3626
|
return existing + blocks.join('');
|
|
3580
3627
|
}
|
|
3581
3628
|
/**
|
|
3582
|
-
* Turns answers into the exact set of files to write, without touching the target directory
|
|
3583
|
-
* decisions
|
|
3584
|
-
*
|
|
3629
|
+
* Turns answers into the exact set of files to write, without touching the target directory. Separating the
|
|
3630
|
+
* decisions from the I/O is what lets a test assert on the whole matrix of answers, and makes `--dry-run` the
|
|
3631
|
+
* same code path minus the last step.
|
|
3585
3632
|
*/ function plan_plan(answers, pm) {
|
|
3586
|
-
const features = selectFeatures(answers);
|
|
3587
|
-
const tokens = tokensFor(answers, pm);
|
|
3633
|
+
const features = selectFeatures(answers, pm);
|
|
3634
|
+
const tokens = tokensFor(answers, features, pm);
|
|
3588
3635
|
const raw = readTemplateDir(external_node_path_join(TEMPLATES_DIR, 'base'));
|
|
3589
3636
|
for (const feature of features){
|
|
3590
3637
|
for (const overlay of feature.overlays ?? []){
|
|
@@ -3600,7 +3647,9 @@ function readTemplateDir(dir) {
|
|
|
3600
3647
|
const gitignore = files.get('.gitignore');
|
|
3601
3648
|
if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
|
|
3602
3649
|
files.set('package.json', buildPackageJson(answers, features, pm));
|
|
3603
|
-
|
|
3650
|
+
// Only for pnpm, and only when a feature brought an install script to answer for — see `buildPnpmSettings`.
|
|
3651
|
+
const pnpmSettings = pm.name === 'pnpm' ? buildPnpmSettings(features) : null;
|
|
3652
|
+
if (pnpmSettings) files.set('pnpm-workspace.yaml', pnpmSettings);
|
|
3604
3653
|
return {
|
|
3605
3654
|
// Sorted, so both the write order and a test's snapshot are stable.
|
|
3606
3655
|
files: new Map([
|
|
@@ -3632,6 +3681,12 @@ const RUN = {
|
|
|
3632
3681
|
yarn: 'yarn',
|
|
3633
3682
|
bun: 'bun'
|
|
3634
3683
|
};
|
|
3684
|
+
const DLX = {
|
|
3685
|
+
npm: 'npx',
|
|
3686
|
+
pnpm: 'pnpm dlx',
|
|
3687
|
+
yarn: 'yarn dlx',
|
|
3688
|
+
bun: 'bunx'
|
|
3689
|
+
};
|
|
3635
3690
|
function isKnown(name) {
|
|
3636
3691
|
return PACKAGE_MANAGERS.includes(name);
|
|
3637
3692
|
}
|
|
@@ -3640,15 +3695,14 @@ function packageManager(name, version) {
|
|
|
3640
3695
|
name,
|
|
3641
3696
|
version,
|
|
3642
3697
|
install: INSTALL[name],
|
|
3643
|
-
run: RUN[name]
|
|
3698
|
+
run: RUN[name],
|
|
3699
|
+
dlx: DLX[name]
|
|
3644
3700
|
};
|
|
3645
3701
|
}
|
|
3646
3702
|
/**
|
|
3647
|
-
* Which package manager invoked us.
|
|
3648
|
-
*
|
|
3649
|
-
*
|
|
3650
|
-
*
|
|
3651
|
-
* Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
3703
|
+
* Which package manager invoked us. All four set `npm_config_user_agent` on the process they spawn — `pnpm/11.9.0
|
|
3704
|
+
* npm/? node/v22.14.0 darwin arm64` — so `pnx @rshono/create` scaffolds a pnpm project without asking, exact
|
|
3705
|
+
* version included. Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
|
|
3652
3706
|
*/ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
3653
3707
|
const [spec] = (userAgent ?? '').split(' ');
|
|
3654
3708
|
const [name, version] = (spec ?? '').split('/');
|
|
@@ -3656,18 +3710,11 @@ function packageManager(name, version) {
|
|
|
3656
3710
|
return packageManager('npm');
|
|
3657
3711
|
}
|
|
3658
3712
|
/**
|
|
3659
|
-
* Runs the install, streaming its output. `shell: true` on Windows
|
|
3660
|
-
*
|
|
3661
|
-
* are all fixed strings from the tables above, never anything the user typed.
|
|
3713
|
+
* Runs the install, streaming its output. `shell: true` on Windows, where npm, pnpm and yarn are `.cmd` shims
|
|
3714
|
+
* `spawn` cannot execute directly — safe because every argument is a fixed string from the tables above.
|
|
3662
3715
|
*/ function runInstall(pm, cwd) {
|
|
3663
3716
|
return run(pm, pm.install, cwd);
|
|
3664
3717
|
}
|
|
3665
|
-
/** `<pm> run <script>` — the one form every package manager accepts, Yarn v1 included. */ function runScript(pm, script, cwd) {
|
|
3666
|
-
return run(pm, [
|
|
3667
|
-
'run',
|
|
3668
|
-
script
|
|
3669
|
-
], cwd);
|
|
3670
|
-
}
|
|
3671
3718
|
function run(pm, args, cwd) {
|
|
3672
3719
|
const result = spawnSync(pm.name, args, {
|
|
3673
3720
|
cwd,
|
|
@@ -3680,9 +3727,19 @@ function run(pm, args, cwd) {
|
|
|
3680
3727
|
;// CONCATENATED MODULE: ./src/ui.ts
|
|
3681
3728
|
|
|
3682
3729
|
|
|
3730
|
+
|
|
3683
3731
|
/**
|
|
3684
|
-
*
|
|
3685
|
-
*
|
|
3732
|
+
* The two halves of the closing line, in the words of the app's own scripts: what produces something shippable,
|
|
3733
|
+
* and where it goes from there. A target with no command to give falls back to the framework's hint.
|
|
3734
|
+
*/ function productionSteps(answers, plan, pm) {
|
|
3735
|
+
const scripts = buildScripts(plan.features);
|
|
3736
|
+
const check = scripts.preview ? `${invoke(pm, 'preview')} to run the production build` : `${pm.run} build`;
|
|
3737
|
+
const ship = scripts.deploy ? `${invoke(pm, 'deploy')} to ship it` : scripts.start ? `${invoke(pm, 'start')} on the host that runs it` : deployHint(answers.deploy);
|
|
3738
|
+
return `${check}, and ${ship}`;
|
|
3739
|
+
}
|
|
3740
|
+
/**
|
|
3741
|
+
* What to do next, in the order to do it — the last thing the user reads. `installed` decides whether the install
|
|
3742
|
+
* step is still theirs.
|
|
3686
3743
|
*/ function nextSteps(answers, plan, pm, options) {
|
|
3687
3744
|
const steps = [];
|
|
3688
3745
|
if (options.directory !== '.') steps.push(`cd ${options.directory}`);
|
|
@@ -3691,7 +3748,7 @@ function run(pm, args, cwd) {
|
|
|
3691
3748
|
const lines = [
|
|
3692
3749
|
steps.join('\n')
|
|
3693
3750
|
];
|
|
3694
|
-
lines.push(`\nThen ${
|
|
3751
|
+
lines.push(`\nThen ${productionSteps(answers, plan, pm)}.`);
|
|
3695
3752
|
if (plan.notes.length > 0) lines.push(`\n${plan.notes.join('\n')}`);
|
|
3696
3753
|
return lines.join('\n');
|
|
3697
3754
|
}
|
|
@@ -3717,10 +3774,7 @@ function run(pm, args, cwd) {
|
|
|
3717
3774
|
;// CONCATENATED MODULE: ./src/write.ts
|
|
3718
3775
|
|
|
3719
3776
|
|
|
3720
|
-
/**
|
|
3721
|
-
* Files that do not make a directory "occupied". A user who ran `git init` or opened the folder in an
|
|
3722
|
-
* editor before scaffolding has not put anything in it that we would overwrite.
|
|
3723
|
-
*/ const IGNORED_ENTRIES = new Set([
|
|
3777
|
+
/** 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([
|
|
3724
3778
|
'.git',
|
|
3725
3779
|
'.DS_Store',
|
|
3726
3780
|
'.idea',
|
|
@@ -3728,12 +3782,9 @@ function run(pm, args, cwd) {
|
|
|
3728
3782
|
'Thumbs.db'
|
|
3729
3783
|
]);
|
|
3730
3784
|
/**
|
|
3731
|
-
* What is already at the target path,
|
|
3732
|
-
*
|
|
3733
|
-
*
|
|
3734
|
-
* A path that does not exist yet is no conflict. A path that exists and is *not* a directory is not
|
|
3735
|
-
* something `--force` should be able to write into, so it throws rather than reporting an empty list —
|
|
3736
|
-
* otherwise `create-rshono README.md` gets as far as `mkdir` before failing on a raw ENOTDIR.
|
|
3785
|
+
* What is already at the target path, ignoring the entries a fresh clone or an editor leaves behind — which is
|
|
3786
|
+
* what decides whether scaffolding into it is safe. A path that does not exist yet is no conflict; one that
|
|
3787
|
+
* exists and is not a directory throws, since `--force` should not write into it either.
|
|
3737
3788
|
*/ function conflictingEntries(dir) {
|
|
3738
3789
|
const stats = statSync(dir, {
|
|
3739
3790
|
throwIfNoEntry: false
|
|
@@ -3743,8 +3794,8 @@ function run(pm, args, cwd) {
|
|
|
3743
3794
|
return external_node_fs_readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry));
|
|
3744
3795
|
}
|
|
3745
3796
|
/**
|
|
3746
|
-
* Writes the plan
|
|
3747
|
-
*
|
|
3797
|
+
* Writes the plan, creating directories as needed and keeping the plan's own ordering — so a failure part-way
|
|
3798
|
+
* through leaves something a person can make sense of.
|
|
3748
3799
|
*/ function writePlan(plan, targetDir) {
|
|
3749
3800
|
mkdirSync(targetDir, {
|
|
3750
3801
|
recursive: true
|
|
@@ -3773,7 +3824,7 @@ const DEFAULT_DIRECTORY = 'my-rshono-app';
|
|
|
3773
3824
|
/** 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
|
|
3774
3825
|
|
|
3775
3826
|
Usage:
|
|
3776
|
-
|
|
3827
|
+
npx @rshono/create@latest [directory] [options]
|
|
3777
3828
|
|
|
3778
3829
|
Options:
|
|
3779
3830
|
-y, --yes accept the default for every question not given as a flag
|
|
@@ -3793,7 +3844,7 @@ Options:
|
|
|
3793
3844
|
Every question can be answered by a flag, and a non-interactive terminal implies --yes — so one command
|
|
3794
3845
|
scaffolds without prompting:
|
|
3795
3846
|
|
|
3796
|
-
|
|
3847
|
+
npx @rshono/create@latest my-app -y --deploy cloudflare --tailwind --quality biome
|
|
3797
3848
|
`;
|
|
3798
3849
|
function fail(message) {
|
|
3799
3850
|
log.error(message);
|
|
@@ -3810,11 +3861,7 @@ function fail(message) {
|
|
|
3810
3861
|
if (off) return false;
|
|
3811
3862
|
return undefined;
|
|
3812
3863
|
}
|
|
3813
|
-
/**
|
|
3814
|
-
* `parseArgs` rejects an unknown flag with a message that names it but nothing else — so a mistyped
|
|
3815
|
-
* `--tailwnid` reads as a wall of text about positional arguments. Pointing at `--help` is the whole
|
|
3816
|
-
* addition.
|
|
3817
|
-
*/ function parse() {
|
|
3864
|
+
/** `parseArgs` names an unknown flag but says nothing about what to do next; this points at `--help`. */ function parse() {
|
|
3818
3865
|
try {
|
|
3819
3866
|
return parseArgs({
|
|
3820
3867
|
options: {
|
|
@@ -3880,12 +3927,10 @@ function fail(message) {
|
|
|
3880
3927
|
async function main() {
|
|
3881
3928
|
const { values, positionals } = parse();
|
|
3882
3929
|
if (values.help) return console.log(HELP);
|
|
3883
|
-
if (values.version) return console.log("1.0.0-rc.
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
* stdout still attached would otherwise ask a question with nothing behind the keyboard.
|
|
3888
|
-
*/ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
|
|
3930
|
+
if (values.version) return console.log("1.0.0-rc.20");
|
|
3931
|
+
// A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both streams have to
|
|
3932
|
+
// be a terminal: the prompts draw on stdout but read from stdin.
|
|
3933
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !values.yes;
|
|
3889
3934
|
const pmFlag = oneOf(values.pm, PACKAGE_MANAGERS, 'pm');
|
|
3890
3935
|
const pm = pmFlag ? packageManager(pmFlag) : detectPackageManager();
|
|
3891
3936
|
const deployFlag = oneOf(values.deploy, DEPLOY_TARGET_NAMES, 'deploy');
|
|
@@ -3895,8 +3940,7 @@ async function main() {
|
|
|
3895
3940
|
const tailwindFlag = tristate(values.tailwind, values['no-tailwind'], 'tailwind');
|
|
3896
3941
|
const installFlag = tristate(values.install, values['no-install'], 'install');
|
|
3897
3942
|
const gitFlag = tristate(values.git, values['no-git'], 'git');
|
|
3898
|
-
// The framework version, not this package's: it is the one the app
|
|
3899
|
-
// worth reading here. `--version` reports create-rshono's own.
|
|
3943
|
+
// The framework version, not this package's: it is the one the app gets pinned to. `--version` reports ours.
|
|
3900
3944
|
intro(`create-rshono · rshono ${RSHONO_VERSION}`);
|
|
3901
3945
|
// ── Where ───────────────────────────────────────────────────────────────────────────────────────
|
|
3902
3946
|
let directory = positionals[0];
|
|
@@ -3909,11 +3953,17 @@ async function main() {
|
|
|
3909
3953
|
})) : DEFAULT_DIRECTORY;
|
|
3910
3954
|
}
|
|
3911
3955
|
const targetDir = resolve(process.cwd(), directory);
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3956
|
+
// Resolved rather than compared as text: `.`, `./`, `foo/..` and the cwd's own absolute path all name this
|
|
3957
|
+
// directory, and its own basename is the only thing left to call the package. Matching the literal `'.'`
|
|
3958
|
+
// covered one spelling and left `./` failing on "does not give a usable npm package name".
|
|
3959
|
+
const intoCwd = targetDir === process.cwd();
|
|
3960
|
+
const packageName = toPackageName(intoCwd ? basename(targetDir) : directory);
|
|
3961
|
+
if (!packageName) fail(`"${directory}" does not give a usable npm package name.`);
|
|
3962
|
+
// Not under --dry-run: nothing is written, so there is nothing to conflict with, and the advice it would
|
|
3963
|
+
// give (`--force`) describes an action the user did not ask for.
|
|
3964
|
+
const conflicts = values['dry-run'] ? [] : conflictingEntries(targetDir);
|
|
3915
3965
|
if (conflicts.length > 0 && !values.force) {
|
|
3916
|
-
const where =
|
|
3966
|
+
const where = intoCwd ? 'this directory' : `"${directory}"`;
|
|
3917
3967
|
const listed = `${conflicts.slice(0, 3).join(', ')}${conflicts.length > 3 ? ', …' : ''}`;
|
|
3918
3968
|
if (!interactive) fail(`${where} is not empty (${listed}) — pass --force to scaffold into it anyway.`);
|
|
3919
3969
|
const proceed = unwrap(await dist_confirm({
|
|
@@ -3956,10 +4006,9 @@ async function main() {
|
|
|
3956
4006
|
]
|
|
3957
4007
|
}));
|
|
3958
4008
|
}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
*/ let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
|
|
4009
|
+
// One question instead of two; the axes stay independent underneath, and a `--formatter` or `--linter` flag
|
|
4010
|
+
// addresses either on its own.
|
|
4011
|
+
let preset = QUALITY_PRESETS.find((candidate)=>candidate.id === qualityFlag);
|
|
3963
4012
|
if (!preset && !formatterFlag && !linterFlag) {
|
|
3964
4013
|
const fallback = QUALITY_PRESETS["0"];
|
|
3965
4014
|
if (interactive) {
|
|
@@ -4020,16 +4069,6 @@ async function main() {
|
|
|
4020
4069
|
installed = runInstall(pm, targetDir);
|
|
4021
4070
|
if (!installed) log.warn(`${pm.name} install failed — the files are all written, so run it yourself in the project.`);
|
|
4022
4071
|
}
|
|
4023
|
-
/*
|
|
4024
|
-
* Format the scaffold with the tool it was scaffolded with, so a fresh project passes its own
|
|
4025
|
-
* `format:check` instead of reporting a diff nobody made. Needs the install, since the formatter is a
|
|
4026
|
-
* devDependency — hence the skip, rather than a failure, when there is none.
|
|
4027
|
-
*
|
|
4028
|
-
* Whether there is a `format` script is the plan's answer, not the formatter answer's: `--formatter
|
|
4029
|
-
* none --linter biome` has no formatter and a `format` script all the same, because Biome brings one.
|
|
4030
|
-
*/ if (installed && plan.features.some((feature)=>feature.scripts?.format)) {
|
|
4031
|
-
if (!runScript(pm, 'format', targetDir)) log.warn(`\`${pm.run} format\` failed — the scaffold is written, just not formatted.`);
|
|
4032
|
-
}
|
|
4033
4072
|
if (git) {
|
|
4034
4073
|
if (!hasGit(targetDir)) {
|
|
4035
4074
|
log.warn('git was not found on PATH — skipped.');
|