Skip to content

feat(create-sanity): bundle init logic into single file#759

Draft
rexxars wants to merge 30 commits intobundled-createfrom
feat/bundled-create-sanity
Draft

feat(create-sanity): bundle init logic into single file#759
rexxars wants to merge 30 commits intobundled-createfrom
feat/bundled-create-sanity

Conversation

@rexxars
Copy link
Copy Markdown
Member

@rexxars rexxars commented Mar 23, 2026

Description

Note

Part 3/4 of the ongoing work to make create-sanity faster by bundling the init logic

This mostly completes the work - the remaining part is the telemetry integration and an optional but wanted step at reducing the amount of dependencies we rely on for the init step.

The bundling script has a few hacks that would be nice to get rid of over time, but not the end of the world.

What to review

Testing

rexxars and others added 16 commits March 23, 2026 14:06
…rectly

Replaces the spawn-wrapper approach with a TypeScript entry point that
parses flags using @oclif/core/parser (reusing InitCommand.flags for
exact parity) and calls initAction() directly. Uses no-op telemetry stub.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch from esbuild to Rollup for the create-sanity standalone bundle
to enable better tree-shaking of barrel exports. Rollup resolves
@sanity/cli-core source directly via alias plugin so it can
tree-shake unused exports from the barrel.

Plugins: alias, node-resolve, commonjs, json, esbuild (transpile only).
Bundle size is ~13MB (same as esbuild - tree-shaking improvements
will come in subsequent tasks with stub removal).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add lightweight error formatting utilities that match oclif's
pretty-printed error output (bang prefix, word wrap, clean stacks,
suggestions) without depending on @oclif/core.

- CLIError: formatted error with oclif-compatible shape so oclif's
  error handler still recognizes it when thrown from commands
- CLIWarning: same but with yellow bang prefix
- error()/warn(): standalone functions for printing to stderr

Update NonInteractiveError and ProjectRootNotFoundError to extend
our CLIError instead of oclif's. Replace oclif ux imports in
apiClient.ts and getCliTelemetry.ts with node:util styleText
and our warn().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace oclif ux.warn/ux.stdout with our own warn() from
@sanity/cli-core/ux in setupMCP.ts. Replace console.log for
stdout output. Replace CLIError with InitError in
createOrAppendEnvVars.ts. Remove oclif import from
promptForDefaultConfig.ts.

These files are transitively bundled into create-sanity, so removing
oclif imports here prevents the entire oclif runtime from being
pulled into the standalone bundle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract flag definitions from InitCommand into plain objects in
flags.ts with zero @oclif/core imports. These can be consumed by
both the oclif command (via toOclifFlags adapter) and create-sanity's
standalone entry point (via node:util parseArgs).

- flags.ts: POJO flag/arg definitions shared across consumers
- flagAdapter.ts: toOclifFlags/toOclifArgs converters
- init.ts: uses adapter instead of direct oclif flag helpers
- types.ts: env flag validation moved to flagsToInitOptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite the standalone entry point to use parseArgs instead of
@oclif/core/parser, consuming the shared POJO flag definitions
directly. This eliminates @oclif/core from the bundle entirely.

- Handles --no-<flag> negation via companion boolean flags
- Adds --help with formatted flag listing
- Validates flag types at runtime
- Removes module stubs (no longer needed with Rollup tree-shaking)
- Simplifies Rollup config (no typescript external needed)

Bundle: 2.9 MB with zero oclif, zero typescript, zero stubs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add stdout/stderr output helpers to @sanity/cli-core/ux to avoid
  bare console.log/console.error in lint-strict packages
- Fix type errors: OclifChar narrowing, oclif overload disambiguation,
  FlagDef iteration, Output.warn/error signatures, noopTelemetry
- Fix lint: eslint-disable for error output layer, perfectionist sorting
- Use stdout() from cli-core/ux in setupMCP and promptForDefaultConfig
- Add package manager detection for help text (npm/pnpm/yarn)
- Handle ERR_PARSE_ARGS_UNKNOWN_OPTION with clean error message
- Delete old create-sanity files: build.ts, index.js, test.js,
  vitest.config.mjs (replaced by rollup config and new test suite)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests run against the built dist/index.js bundle and cover:
- --help output with flag listing
- Package manager detection (npm/pnpm/yarn) in help text
- Hidden flags not shown in help
- -h shorthand
- --help precedence over other flags
- Invalid flag produces clean error (no stack trace)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused devDeps: @oclif/core, tsx
- Add debug as devDep (used in rollup.config.ts alias)
- Remove unused InitFlagName type export
- Add knip ignore for @sanity/cli (imported via relative source paths)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace dynamic import of ImportDatasetCommand with spawning the
project's own sanity binary via execa. The dynamic import pulled the
entire oclif command tree into the create-sanity bundle because Rollup
inlines all dynamic imports.

The scaffolded project always has sanity as a dependency, so spawning
its binary for dataset import works in both standalone and oclif contexts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The function is 4 lines - no need for a separate package.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace custom detectPackageManager with the shared
getRunningPackageManager from @sanity/cli-core/package-manager.
Adds bun support for help text (`bun create sanity@latest`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
create-sanity now handles --help itself via parseArgs, so it never
delegates to `sanity init --help`. The --from-create flag, the
SanityHelp.replaceInitWithCreateCommand rewriting, and the fromCreate
option in InitOptions are all unused. Remove them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
npm requires `--` to forward flags to the create script (e.g.
`npm create sanity@latest -- --template moviedb`). Other package
managers don't need it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move flagsToInitOptions, InitCommandFlags, InitCommandArgs, and the
narrowing helpers out of types.ts into flagsToInitOptions.ts. types.ts
now only contains type definitions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 23, 2026

Claude finished @rexxars's task in 4m 9s —— View job


Code Review

  • Gather context and read CLAUDE.md
  • Review changed files
  • Post review feedback

packages/@sanity/cli/src/actions/init/flagsToInitOptions.ts:75 — Exit code regression for --env validation

The old oclif parse callback threw CLIError (exit 2). The new validation throws InitError with exit code 3:

throw new InitError('Env filename (`--env`) must start with `.env`', 3)

InitCommand.run() re-throws this via this.error(error.message, {exit: error.exitCode}), so sanity init --env invalid now exits 3 instead of 2. No existing test asserts the specific exit code. Fix this →


packages/@sanity/cli/src/actions/init/initStudio.ts:140 — Dataset import error is completely silenced

The catch block discards the underlying execa error:

} catch {
  output.warn('Sample dataset import failed. ...')
}

If the import fails due to auth, network, or an execa configuration issue, there's no way to diagnose it. Consider logging at debug level at minimum:

} catch (err) {
  subdebug('Dataset import failed: %O', err)
  output.warn('Sample dataset import failed. ...')
}

packages/create-sanity/src/index.ts:12-14 — Relative cross-package imports

Imports like '../../@sanity/cli/src/actions/init/flagsToInitOptions.js' cross package boundaries using relative paths. This is brittle if either package is moved or restructured. Acknowledged as a known hack in the PR description, but worth tracking as technical debt to resolve (e.g. via proper package exports or a shared internal package).


packages/create-sanity/package.json — No prepublish/prepare script

dist/ is gitignored but there is no prepublish or prepare script to ensure the bundle is built before publishing. The pretest hook covers local dev, but a publish without a prior build would ship an empty dist/. Verify this is handled at the workspace/CI level.

@rexxars rexxars force-pushed the feat/bundled-create-sanity branch from f51850a to 020c660 Compare March 23, 2026 22:43
@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 23, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​esbuild@​0.25.12911007391100
Addednpm/​clean-stack@​6.0.010010010081100
Addednpm/​wrap-ansi@​10.0.01001009386100

View full report

rexxars and others added 4 commits March 23, 2026 15:45
Add fs.access check before spawning the sanity binary for dataset
import. Gives a clear error message instead of a raw ENOENT if
dependencies weren't installed correctly.

Also fix misleading vitest config comment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use execa preferLocal instead of manual .bin path (Windows compat)
- Fix exit:0 being silently discarded in CLIError (0 || undefined bug)
- Remove duplicate --env validation from InitCommand (flagsToInitOptions
  is the canonical location, shared with create-sanity)
- Remove unused overrides parameter from toOclifFlags

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… constraints

parseArgs doesn't support oclif features like aliases, option
validation, or exclusive constraints. Add post-parse handling:

- Register flag aliases (e.g. --project-id) as separate parseArgs
  options, then resolve them back to canonical names
- Validate string flags with `options` against allowed values
  (--package-manager, --visibility)
- Check exclusive constraints and error if conflicting flags are passed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move all parseArgs logic (option building, alias resolution, negation
merging, option/exclusive validation, help printing) out of index.ts
into a dedicated parseArgs.ts module.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@rexxars rexxars force-pushed the feat/bundled-create-sanity branch from 895b6fa to e6848b3 Compare March 23, 2026 23:05
rexxars and others added 3 commits March 23, 2026 16:09
- Replace ImportDatasetCommand mock with execa mock
- Update assertion to match new call signature (binary name, args, options)
- Fix env validation exit code expectation (now InitError exit:1, not oclif exit:2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Establish consistent exit code semantics:
- 1: init action runtime failure (auth, missing deps, etc)
- 2: flag parsing/validation error (unknown flag, invalid option value,
  exclusive constraint violation)
- 3: domain-specific flag validation (e.g. --env must start with .env)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add pretest script so `pnpm test` builds the bundle before running
- Wrap execa call in initStudio with try/catch so dataset import
  failures produce a clean InitError instead of dumping ExecaError
- Add explicit default:false to bare, dataset-default, no-git flags
  so parseArgs always populates them, matching InitCommandFlags types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 23, 2026

Coverage Delta

File Statements
packages/@sanity/cli-core/src/_exports/index.ts 100.0% (new)
packages/@sanity/cli-core/src/errors/CLIError.ts 72.7% (new)
packages/@sanity/cli-core/src/errors/CLIWarning.ts 100.0% (new)
packages/@sanity/cli-core/src/errors/NonInteractiveError.ts 100.0% (±0%)
packages/@sanity/cli-core/src/errors/ProjectRootNotFoundError.ts 100.0% (±0%)
packages/@sanity/cli-core/src/services/apiClient.ts 100.0% (±0%)
packages/@sanity/cli-core/src/util/getCliTelemetry.ts 100.0% (±0%)
packages/@sanity/cli-core/src/ux/errors.ts 65.1% (new)
packages/@sanity/cli-core/src/ux/output.ts 50.0% (new)
packages/@sanity/cli/src/SanityHelp.ts 60.0% (- 10.4%)
packages/@sanity/cli/src/actions/init/bootstrapTemplate.ts 0.0% (±0%)
packages/@sanity/cli/src/actions/init/env/createOrAppendEnvVars.ts 0.0% (±0%)
packages/@sanity/cli/src/actions/init/flags.ts 100.0% (new)
packages/@sanity/cli/src/actions/init/flagsToInitOptions.ts 100.0% (new)
packages/@sanity/cli/src/actions/init/initAction.ts 94.8% (new)
packages/@sanity/cli/src/actions/init/initApp.ts 96.0% (new)
packages/@sanity/cli/src/actions/init/initError.ts 100.0% (new)
packages/@sanity/cli/src/actions/init/initHelpers.ts 100.0% (new)
packages/@sanity/cli/src/actions/init/initNextJs.ts 86.7% (new)
packages/@sanity/cli/src/actions/init/initStudio.ts 85.1% (new)
packages/@sanity/cli/src/actions/init/scaffoldTemplate.ts 88.0% (new)
packages/@sanity/cli/src/actions/mcp/setupMCP.ts 100.0% (±0%)
packages/@sanity/cli/src/commands/init.ts 100.0% (+ 7.4%)
packages/@sanity/cli/src/prompts/promptForDefaultConfig.ts 100.0% (±0%)
packages/@sanity/cli/src/telemetry/init.telemetry.ts 100.0% (±0%)
packages/@sanity/cli/src/util/flagAdapter.ts 91.7% (new)
packages/create-sanity/rollup.config.ts 0.0% (new)
packages/create-sanity/src/createCommand.ts 0.0% (new)
packages/create-sanity/src/help.ts 0.0% (new)
packages/create-sanity/src/index.ts 0.0% (new)
packages/create-sanity/src/noopTelemetry.ts 0.0% (new)
packages/create-sanity/src/parseArgs.ts 0.0% (new)

Comparing 32 changed files against main @ 56f2c0ad9debf4324b7f736ba2f19a2421f4eb7a

Overall Coverage

Metric Coverage
Statements 81.6% (- 1.4%)
Branches 71.1% (- 1.6%)
Functions 82.0% (- 1.2%)
Lines 82.1% (- 1.3%)

- Use helpLabel when defined (e.g. --[no-]git)
- Show --[no-] prefix for allowNo boolean flags
- Wrap long descriptions to terminal width
- Show --help in the options list
- Error when both a flag and its alias are passed (--project + --project-id)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rexxars and others added 3 commits March 23, 2026 16:56
On Windows, environment variables are case-insensitive but process.env
preserves the original casing. When overriding npm_config_user_agent
in tests, the original key (potentially different casing) wasn't being
removed, so the override was ineffective.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite printHelp to match oclif's help formatting:
- Terminal width detection: OCLIF_COLUMNS env > TTY width > 80 fallback,
  clamped to minimum 40
- Use wrap-ansi with {hard: true} for description wrapping
- Dynamic left column width based on widest flag label
- Automatic multiline format (flag on own line, description below) when
  any description wraps to >4 lines at current terminal width
- Wrap first, then indent (matching oclif's ordering)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@rexxars rexxars force-pushed the feat/bundled-create-sanity branch 2 times, most recently from 01fd21e to 3d2e1e9 Compare March 24, 2026 17:20
@rexxars rexxars force-pushed the feat/bundled-create-sanity branch from 3d2e1e9 to a27419e Compare March 24, 2026 18:27
…sanity

Move mcpMode computation into flagsToInitOptions so the identical logic
in init.ts and create-sanity/index.ts cannot diverge. The function now
takes an `interactive` boolean and derives both `isUnattended` and
`mcpMode` internally.

Add deprecation warnings in parseArgs.ts so that create-sanity users
see feedback when using --reconfigure, --create-project, or --quickstart
(oclif handles this automatically for `sanity init`, but the standalone
path was silent).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant