@rshono/create 1.0.0-rc.1 → 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/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 { existsSync as external_node_fs_existsSync, mkdirSync, readFileSync, readdirSync as external_node_fs_readdirSync, writeFileSync } from "node:fs";
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.1';
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
- const PACKAGE_MANAGERS = (/* unused pure expression or super */ null && ([
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',
@@ -3088,7 +3093,7 @@ const QUALITY_PRESETS = [
3088
3093
 
3089
3094
  ;// CONCATENATED MODULE: ./src/versions.ts
3090
3095
 
3091
-
3096
+ /** Passed straight through, so everything generated from the framework reaches the rest of the package here. */
3092
3097
  /** The framework range a scaffolded app gets. The two packages are released together, so this is ours. */ const RSHONO_RANGE = `^${RSHONO_VERSION}`;
3093
3098
  /**
3094
3099
  * Versions for the optional tooling the features can add — the one place in this package where a
@@ -3119,12 +3124,11 @@ const QUALITY_PRESETS = [
3119
3124
  };
3120
3125
  /**
3121
3126
  * The TypeScript an ESLint app pins, in place of the framework's own — the one deliberate exception to
3122
- * {@link FRAMEWORK_DEPS}, and the reason it is spelled out here.
3127
+ * {@link FRAMEWORK_DEPS}.
3123
3128
  *
3124
- * typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface, so
3125
- * it accepts `typescript >=4.8.4 <6.1.0` and nothing above. `~6.0.3` is the newest that satisfies it:
3126
- * patch releases of 6.0, no minor. The framework itself stays on the TypeScript it is tested against —
3127
- * rshono's declarations compile the same under either, which is what makes this pin an app's business
3129
+ * typescript-eslint reads TypeScript's compiler API directly rather than through a stable interface,
3130
+ * so it accepts `typescript >=4.8.4 <6.1.0` and nothing above; `~6.0.3` is the newest that satisfies
3131
+ * it. rshono's declarations compile the same under either, which is what makes this an app's business
3128
3132
  * and not the framework's.
3129
3133
  *
3130
3134
  * When upstream widens the range, this constant and the ESLint feature's use of it are what to delete.
@@ -3133,14 +3137,13 @@ const QUALITY_PRESETS = [
3133
3137
  ;// CONCATENATED MODULE: ./src/features/deploy.ts
3134
3138
 
3135
3139
  /**
3136
- * What a deploy target adds beyond the `deploy` line in `rshono.config.ts` — which is the build's job
3137
- * 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.
3138
3141
  *
3139
- * Deliberately thin. The framework already knows how to arrange its own output for every platform, and
3140
- * `rshono build` writes the one platform config that has to exist (`wrangler.jsonc`, with the
3141
- * `compatibility_date` of the day it ran) if the project has none. Generating a second copy here would
3142
- * be a copy that goes stale. So a target contributes a `deploy` script, the CLI it needs locally, and
3143
- * the build artefacts its platform leaves in the project.
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.
3144
3147
  */ const DEPLOY_FEATURES = {
3145
3148
  // Where a Node build goes from here is a Dockerfile or a process manager, neither of which this can
3146
3149
  // guess — so the target contributes only the command that runs what was built.
@@ -3150,28 +3153,16 @@ const QUALITY_PRESETS = [
3150
3153
  start: 'rshono start'
3151
3154
  }
3152
3155
  },
3153
- // `rshono start` is the Node target's launcher and refuses a build made for another platform, so
3154
- // these two get the command their own runtime uses under the same script name.
3155
- bun: {
3156
- id: 'deploy-bun',
3157
- scripts: {
3158
- start: 'bun dist/server/main.mjs'
3159
- }
3160
- },
3161
- deno: {
3162
- id: 'deploy-deno',
3163
- scripts: {
3164
- start: 'deno serve -A dist/server/main.mjs'
3165
- }
3166
- },
3167
3156
  cloudflare: {
3168
3157
  id: 'deploy-cloudflare',
3169
3158
  devDependencies: {
3170
3159
  wrangler: TOOL_VERSIONS.wrangler
3171
3160
  },
3172
- // wrangler brings workerd, whose install script only picks the platform binary out of the optional
3173
- // dependency that already carries it — `workerd --version` answers without it having run.
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.
3174
3164
  allowBuilds: {
3165
+ esbuild: false,
3175
3166
  workerd: false
3176
3167
  },
3177
3168
  scripts: {
@@ -3196,15 +3187,6 @@ const QUALITY_PRESETS = [
3196
3187
  '--prebuilt uploads what rshono build assembled; the platform must not rebuild it.'
3197
3188
  ]
3198
3189
  },
3199
- netlify: {
3200
- id: 'deploy-netlify',
3201
- scripts: {
3202
- deploy: 'rshono build && netlify deploy --build=false --dir=.netlify/publish'
3203
- },
3204
- gitignore: [
3205
- '.netlify/'
3206
- ]
3207
- },
3208
3190
  'aws-lambda': {
3209
3191
  id: 'deploy-aws-lambda',
3210
3192
  notes: [
@@ -3223,8 +3205,10 @@ function deployFeature(target) {
3223
3205
  * deduplicates by `id`, so `formatter: 'biome', linter: 'biome'` contributes one set of files, one
3224
3206
  * dependency and one pair of scripts.
3225
3207
  *
3226
- * Each tool brings its own `check` script alongside `format`/`lint`, because the writing half and the
3227
- * CI half want different exit-code behaviour: `format` rewrites files, `check` fails instead.
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.
3228
3212
  */ const PRETTIER = {
3229
3213
  id: 'prettier',
3230
3214
  overlays: [
@@ -3265,12 +3249,11 @@ const OXLINT = {
3265
3249
  }
3266
3250
  };
3267
3251
  /**
3268
- * The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot be
3269
- * installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one its
3270
- * peer range accepts (see {@link ESLINT_TYPESCRIPT}). Every other preset leaves TypeScript alone.
3271
- *
3272
- * The rules are type-aware, which is the reason to reach for ESLint over a syntax-only linter at all —
3273
- * so the config it ships hands the whole program to the parser rather than linting file by file.
3252
+ * The one feature that changes a dependency the framework otherwise decides: typescript-eslint cannot
3253
+ * be installed alongside the TypeScript rshono is tested against, so an ESLint app pins the newest one
3254
+ * its peer range accepts (see {@link ESLINT_TYPESCRIPT}). Its rules are type-aware — the reason to
3255
+ * reach for ESLint over a syntax-only linter — so the config it ships hands the parser the whole
3256
+ * program rather than linting file by file.
3274
3257
  */ const ESLINT = {
3275
3258
  id: 'eslint',
3276
3259
  overlays: [
@@ -3326,14 +3309,13 @@ function linterFeature(linter) {
3326
3309
  ;// CONCATENATED MODULE: ./src/features/styling.ts
3327
3310
 
3328
3311
  /**
3329
- * Tailwind is a PostCSS plugin and nothing more, which is the whole of this feature: four packages, and
3330
- * an overlay carrying the `postcss.config.mjs` naming the plugin, an `rshono.config.ts` whose `rspack`
3331
- * hook puts postcss-loader in front of the CSS parser, a Tailwind entry stylesheet, and the two views
3332
- * written in utilities instead of classes of their own.
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.
3333
3316
  *
3334
3317
  * `postcss` and `postcss-loader` are the app's dependencies rather than the framework's — rshono
3335
- * compiles CSS natively and has no PostCSS in it, so an app that does not want a plugin chain does not
3336
- * install one.
3318
+ * compiles CSS natively, so an app that does not want a plugin chain does not install one.
3337
3319
  */ const TAILWIND = {
3338
3320
  id: 'tailwind',
3339
3321
  overlays: [
@@ -3380,8 +3362,9 @@ function stylingFeature(styling) {
3380
3362
  ;// CONCATENATED MODULE: ./src/pkg.ts
3381
3363
 
3382
3364
  /**
3383
- * The scripts every app gets. `start` is not among them: it is the *Node* launcher, and the deploy
3384
- * features each contribute the command their own platform runs what was built with.
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.
3385
3368
  */ const BASE_SCRIPTS = {
3386
3369
  dev: 'rshono dev',
3387
3370
  build: 'rshono build',
@@ -3402,35 +3385,26 @@ function sorted(record) {
3402
3385
  return Object.fromEntries(Object.entries(record).sort(([a], [b])=>a < b ? -1 : 1));
3403
3386
  }
3404
3387
  /**
3405
- * The install script every app inherits, from the framework rather than from anything it chose. tsx
3406
- * reads `rshono.config.ts` through esbuild, and esbuild's script only picks the platform binary out of
3407
- * the optional dependency that already carries it — rshono's own repo denies it for the same reason.
3408
- */ const BASE_ALLOW_BUILDS = {
3409
- esbuild: false
3410
- };
3411
- /**
3412
- * pnpm's settings for the new app — written for pnpm and for nobody else.
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.
3413
3390
  *
3414
- * It exists for one field. A dependency with an install script is a question pnpm will not answer on its
3415
- * own: it fails the install, and fails every `pnpm dev` after it, until the project has said whether the
3416
- * script should run. None of the ones an rshono app inherits need to (each is a native package whose
3417
- * binary arrives as an optional dependency), so a fresh app carries the answer rather than meeting
3418
- * `pnpm approve-builds` before it has rendered a page once.
3391
+ * It exists for one field, `allowBuilds`. pnpm fails an install — and every `pnpm dev` after it —
3392
+ * until the project has said whether a dependency's install script should run. Nothing rshono itself
3393
+ * installs has one, so most apps get no file; the packages that do (wrangler's esbuild and workerd)
3394
+ * declare their answer on the feature that brings them.
3419
3395
  *
3420
- * In this file rather than under a `pnpm` key in `package.json`, which pnpm 11 no longer reads, single-
3421
- * package projects included. What lands here is a decision about *this* app: a scaffolded file the app
3422
- * owns from then on, not something the framework reaches back into.
3396
+ * A file rather than a `pnpm` key in `package.json`, which pnpm 11 no longer reads.
3423
3397
  */ function buildPnpmSettings(features) {
3424
- const allowBuilds = {
3425
- ...BASE_ALLOW_BUILDS
3426
- };
3398
+ const allowBuilds = {};
3427
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;
3428
3402
  return [
3429
3403
  '# Which dependencies may run an install script. pnpm runs none it has not been told about, and',
3430
3404
  '# fails the install rather than skip one quietly — so anything added later belongs here too.',
3431
3405
  '# `false` means the script was looked at: these ship their real binary as an optional dependency.',
3432
3406
  'allowBuilds:',
3433
- ...Object.entries(sorted(allowBuilds)).map(([name, allowed])=>` ${name}: ${allowed}`),
3407
+ ...entries.map(([name, allowed])=>` ${name}: ${allowed}`),
3434
3408
  ''
3435
3409
  ].join('\n');
3436
3410
  }
@@ -3465,10 +3439,9 @@ function sorted(record) {
3465
3439
  version: '0.1.0',
3466
3440
  private: true,
3467
3441
  type: 'module',
3468
- // The floor the framework declares. Stated here too so a CI image or a contributor on an older
3469
- // 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.
3470
3443
  engines: {
3471
- node: '>=22.1.0'
3444
+ node: NODE_ENGINE
3472
3445
  },
3473
3446
  scripts,
3474
3447
  dependencies: sorted(dependencies),
@@ -3486,19 +3459,22 @@ function sorted(record) {
3486
3459
 
3487
3460
  ;// CONCATENATED MODULE: ./src/render.ts
3488
3461
 
3489
- const TOKEN_PATTERN = /__[A-Z][A-Z\d_]*__/g;
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;
3490
3467
  function tokensFor(answers, pm) {
3491
3468
  return {
3492
- __PROJECT_NAME__: answers.packageName,
3493
- __DEPLOY_TARGET__: answers.deploy,
3494
- __DEPLOY_HINT__: deployHint(answers.deploy),
3495
- __PM__: pm.name,
3496
- __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
3497
3473
  };
3498
3474
  }
3499
3475
  /**
3500
3476
  * Substitutes tokens, and throws on one it doesn't know — a typo in a template would otherwise ship a
3501
- * literal `__PORJECT_NAME__` into somebody's new app, which no test of the generator's logic would
3477
+ * literal `{{PORJECT_NAME}}` into somebody's new app, which no test of the generator's logic would
3502
3478
  * catch.
3503
3479
  */ function render(contents, tokens, source) {
3504
3480
  return contents.replace(TOKEN_PATTERN, (token)=>{
@@ -3531,10 +3507,9 @@ function readTemplateDir(dir) {
3531
3507
  /**
3532
3508
  * `_gitignore` → `.gitignore`, and so on for every dotfile.
3533
3509
  *
3534
- * npm strips a literal `.gitignore` out of a published tarball, so a template cannot simply contain
3535
- * one — the file would exist in the repo, pass every local test, and be missing from the package
3536
- * everybody actually installs. Naming them with an underscore and renaming here is the long-standing
3537
- * 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.
3538
3513
  */ function undotted(path) {
3539
3514
  const segments = path.split(posix.sep);
3540
3515
  const name = segments.pop();
@@ -3574,7 +3549,9 @@ function readTemplateDir(dir) {
3574
3549
  const gitignore = files.get('.gitignore');
3575
3550
  if (gitignore) files.set('.gitignore', appendGitignore(gitignore, features));
3576
3551
  files.set('package.json', buildPackageJson(answers, features, pm));
3577
- if (pm.name === 'pnpm') files.set('pnpm-workspace.yaml', buildPnpmSettings(features));
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);
3578
3555
  return {
3579
3556
  // Sorted, so both the write order and a test's snapshot are stable.
3580
3557
  files: new Map([
@@ -3587,6 +3564,7 @@ function readTemplateDir(dir) {
3587
3564
 
3588
3565
  ;// CONCATENATED MODULE: ./src/pm.ts
3589
3566
 
3567
+
3590
3568
  const INSTALL = {
3591
3569
  npm: [
3592
3570
  'install'
@@ -3606,7 +3584,7 @@ const RUN = {
3606
3584
  bun: 'bun'
3607
3585
  };
3608
3586
  function isKnown(name) {
3609
- return name === 'npm' || name === 'pnpm' || name === 'yarn' || name === 'bun';
3587
+ return PACKAGE_MANAGERS.includes(name);
3610
3588
  }
3611
3589
  function packageManager(name, version) {
3612
3590
  return {
@@ -3617,8 +3595,8 @@ function packageManager(name, version) {
3617
3595
  };
3618
3596
  }
3619
3597
  /**
3620
- * Which package manager invoked us. Every one of them sets `npm_config_user_agent` for the process it
3621
- * spawns — `pnpm/11.9.0 npm/? node/v22.14.0 darwin arm64` — so `pnpm create @rshono` scaffolds a pnpm
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
3622
3600
  * project without asking, and the exact version comes along for the `packageManager` field.
3623
3601
  *
3624
3602
  * Falls back to npm, which is also what a bare `node bin/create-rshono.mjs` gets.
@@ -3635,12 +3613,6 @@ function packageManager(name, version) {
3635
3613
  */ function runInstall(pm, cwd) {
3636
3614
  return run(pm, pm.install, cwd);
3637
3615
  }
3638
- /** `<pm> run <script>` — the one form every package manager accepts, Yarn v1 included. */ function runScript(pm, script, cwd) {
3639
- return run(pm, [
3640
- 'run',
3641
- script
3642
- ], cwd);
3643
- }
3644
3616
  function run(pm, args, cwd) {
3645
3617
  const result = spawnSync(pm.name, args, {
3646
3618
  cwd,
@@ -3700,15 +3672,20 @@ function run(pm, args, cwd) {
3700
3672
  '.vscode',
3701
3673
  'Thumbs.db'
3702
3674
  ]);
3703
- function inspectTarget(dir) {
3704
- if (!external_node_fs_existsSync(dir)) return {
3705
- exists: false,
3706
- conflicts: []
3707
- };
3708
- return {
3709
- exists: true,
3710
- conflicts: external_node_fs_readdirSync(dir).filter((entry)=>!IGNORED_ENTRIES.has(entry))
3711
- };
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));
3712
3689
  }
3713
3690
  /**
3714
3691
  * Writes the plan. Directories are created as needed, and files are written with the plan's own
@@ -3736,20 +3713,21 @@ function inspectTarget(dir) {
3736
3713
 
3737
3714
 
3738
3715
 
3716
+
3739
3717
  const DEFAULT_DIRECTORY = 'my-rshono-app';
3740
- 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
3741
3719
 
3742
3720
  Usage:
3743
- npm create @rshono@latest [directory] [options]
3721
+ npx @rshono/create@latest [directory] [options]
3744
3722
 
3745
3723
  Options:
3746
3724
  -y, --yes accept the default for every question not given as a flag
3747
3725
  -d, --deploy <target> ${DEPLOY_TARGET_NAMES.join(' | ')}
3748
3726
  --tailwind Tailwind CSS (--no-tailwind for plain CSS)
3749
3727
  --quality <preset> ${QUALITY_PRESETS.map((preset)=>preset.id).join(' | ')}
3750
- --formatter <name> prettier | biome | oxfmt | none (overrides --quality)
3751
- --linter <name> oxlint | eslint | biome | none (overrides --quality; eslint pins TypeScript 6)
3752
- --pm <name> npm | pnpm | yarn | bun (default: whatever ran this)
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)
3753
3731
  --no-install write the files and stop
3754
3732
  --no-git do not initialize a repository
3755
3733
  --force scaffold into a directory that is not empty
@@ -3760,7 +3738,7 @@ Options:
3760
3738
  Every question can be answered by a flag, and a non-interactive terminal implies --yes — so one command
3761
3739
  scaffolds without prompting:
3762
3740
 
3763
- npm create @rshono@latest my-app -y --deploy cloudflare --tailwind --quality biome
3741
+ npx @rshono/create@latest my-app -y --deploy cloudflare --tailwind --quality biome
3764
3742
  `;
3765
3743
  function fail(message) {
3766
3744
  log.error(message);
@@ -3777,93 +3755,89 @@ function fail(message) {
3777
3755
  if (off) return false;
3778
3756
  return undefined;
3779
3757
  }
3780
- async function main() {
3781
- const { values, positionals } = parseArgs({
3782
- options: {
3783
- yes: {
3784
- type: 'boolean',
3785
- short: 'y'
3786
- },
3787
- deploy: {
3788
- type: 'string',
3789
- short: 'd'
3790
- },
3791
- tailwind: {
3792
- type: 'boolean'
3793
- },
3794
- 'no-tailwind': {
3795
- type: 'boolean'
3796
- },
3797
- quality: {
3798
- type: 'string'
3799
- },
3800
- formatter: {
3801
- type: 'string'
3802
- },
3803
- linter: {
3804
- type: 'string'
3805
- },
3806
- pm: {
3807
- type: 'string'
3808
- },
3809
- install: {
3810
- type: 'boolean'
3811
- },
3812
- 'no-install': {
3813
- type: 'boolean'
3814
- },
3815
- git: {
3816
- type: 'boolean'
3817
- },
3818
- 'no-git': {
3819
- type: 'boolean'
3820
- },
3821
- force: {
3822
- type: 'boolean'
3823
- },
3824
- 'dry-run': {
3825
- type: 'boolean'
3826
- },
3827
- help: {
3828
- type: 'boolean',
3829
- short: 'h'
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
+ }
3830
3814
  },
3831
- version: {
3832
- type: 'boolean',
3833
- short: 'v'
3834
- }
3835
- },
3836
- allowPositionals: true
3837
- });
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();
3838
3823
  if (values.help) return console.log(HELP);
3839
- if (values.version) return console.log("1.0.0-rc.1");
3840
- // A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer.
3841
- const interactive = Boolean(process.stdout.isTTY) && !values.yes;
3842
- const pmFlag = oneOf(values.pm, [
3843
- 'npm',
3844
- 'pnpm',
3845
- 'yarn',
3846
- 'bun'
3847
- ], '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');
3848
3830
  const pm = pmFlag ? packageManager(pmFlag) : detectPackageManager();
3849
3831
  const deployFlag = oneOf(values.deploy, DEPLOY_TARGET_NAMES, 'deploy');
3850
- const formatterFlag = oneOf(values.formatter, [
3851
- 'prettier',
3852
- 'biome',
3853
- 'oxfmt',
3854
- 'none'
3855
- ], 'formatter');
3856
- const linterFlag = oneOf(values.linter, [
3857
- 'oxlint',
3858
- 'eslint',
3859
- 'biome',
3860
- 'none'
3861
- ], 'linter');
3832
+ const formatterFlag = oneOf(values.formatter, FORMATTER_NAMES, 'formatter');
3833
+ const linterFlag = oneOf(values.linter, LINTER_NAMES, 'linter');
3862
3834
  const qualityFlag = oneOf(values.quality, QUALITY_PRESETS.map((preset)=>preset.id), 'quality');
3863
3835
  const tailwindFlag = tristate(values.tailwind, values['no-tailwind'], 'tailwind');
3864
3836
  const installFlag = tristate(values.install, values['no-install'], 'install');
3865
3837
  const gitFlag = tristate(values.git, values['no-git'], 'git');
3866
- intro(`create-rshono · rshono ${"1.0.0-rc.1"}`);
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}`);
3867
3841
  // ── Where ───────────────────────────────────────────────────────────────────────────────────────
3868
3842
  let directory = positionals[0];
3869
3843
  if (!directory) {
@@ -3877,7 +3851,7 @@ async function main() {
3877
3851
  const targetDir = resolve(process.cwd(), directory);
3878
3852
  const packageName = toPackageName(directory === '.' ? basename(targetDir) : directory);
3879
3853
  if (!packageName || !isValidPackageName(packageName)) fail(`"${directory}" does not give a usable npm package name.`);
3880
- const conflicts = inspectTarget(targetDir).conflicts;
3854
+ const conflicts = conflictingEntries(targetDir);
3881
3855
  if (conflicts.length > 0 && !values.force) {
3882
3856
  const where = directory === '.' ? 'this directory' : `"${directory}"`;
3883
3857
  const listed = `${conflicts.slice(0, 3).join(', ')}${conflicts.length > 3 ? ', …' : ''}`;
@@ -3903,7 +3877,7 @@ async function main() {
3903
3877
  }))
3904
3878
  }));
3905
3879
  }
3906
- let styling = tailwindFlag === undefined ? 'css' : tailwindFlag ? 'tailwind' : 'css';
3880
+ let styling = tailwindFlag ? 'tailwind' : 'css';
3907
3881
  if (tailwindFlag === undefined && interactive) {
3908
3882
  styling = unwrap(await dist_select({
3909
3883
  message: 'Styling?',
@@ -3922,10 +3896,9 @@ async function main() {
3922
3896
  ]
3923
3897
  }));
3924
3898
  }
3925
- /*
3926
- * The preset asks one question instead of two. The two axes stay independent underneath — a
3927
- * `--formatter` or `--linter` flag addresses either on its own, and skips the question entirely.
3928
- */ 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);
3929
3902
  if (!preset && !formatterFlag && !linterFlag) {
3930
3903
  const fallback = QUALITY_PRESETS["0"];
3931
3904
  if (interactive) {
@@ -3953,9 +3926,10 @@ async function main() {
3953
3926
  initialValue: true
3954
3927
  }));
3955
3928
  }
3956
- let git = gitFlag ?? !isInsideRepo(process.cwd());
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;
3957
3932
  if (gitFlag === undefined && interactive) {
3958
- const nested = isInsideRepo(process.cwd());
3959
3933
  git = unwrap(await dist_confirm({
3960
3934
  message: nested ? 'Initialize a git repository? (this is already inside one)' : 'Initialize a git repository?',
3961
3935
  initialValue: !nested
@@ -3963,14 +3937,10 @@ async function main() {
3963
3937
  }
3964
3938
  const answers = {
3965
3939
  packageName,
3966
- targetDir,
3967
3940
  deploy,
3968
3941
  styling,
3969
3942
  formatter,
3970
- linter,
3971
- packageManager: pm.name,
3972
- install,
3973
- git
3943
+ linter
3974
3944
  };
3975
3945
  // ── Plan, then write ────────────────────────────────────────────────────────────────────────────
3976
3946
  const plan = plan_plan(answers, pm);
@@ -3987,14 +3957,7 @@ async function main() {
3987
3957
  if (install) {
3988
3958
  log.step(`Installing dependencies with ${pm.name}…`);
3989
3959
  installed = runInstall(pm, targetDir);
3990
- if (!installed) log.warn(`${pm.name} install failed — run it yourself and the rest will work.`);
3991
- }
3992
- /*
3993
- * Format the scaffold with the tool it was scaffolded with, so a fresh project passes its own
3994
- * `format:check` instead of reporting a diff nobody made. Needs the install, since the formatter is a
3995
- * devDependency — hence the skip, rather than a failure, when there is none.
3996
- */ if (installed && formatter !== 'none') {
3997
- if (!runScript(pm, 'format', targetDir)) log.warn(`\`${pm.run} format\` failed — the files are fine, the formatter is not.`);
3960
+ if (!installed) log.warn(`${pm.name} install failed — the files are all written, so run it yourself in the project.`);
3998
3961
  }
3999
3962
  if (git) {
4000
3963
  if (!hasGit(targetDir)) {