Skip to content

feat(create-sanity): standalone bundled create-sanity package#761

Closed
rexxars wants to merge 27 commits intomainfrom
feat/bundled-create-sanity
Closed

feat(create-sanity): standalone bundled create-sanity package#761
rexxars wants to merge 27 commits intomainfrom
feat/bundled-create-sanity

Conversation

@rexxars
Copy link
Copy Markdown
Member

@rexxars rexxars commented Mar 23, 2026

Description

Note

This builds on top of #748 and #753, which are included in this branch.

This is the culmination of the init refactor series. The goal: ship create-sanity as a standalone, dependency-free bundled package so that npm create sanity is fast and lightweight, while sanity init continues to work as before.

What this does:

  • Moves the entire init command logic out of the oclif command and into a plain action function (initAction) that takes a simple options object
  • Replaces the oclif flag parser with node:util parseArgs for the standalone entry point - zero framework dependency
  • Bundles everything into a single file using Rollup with tree-shaking, aliasing @sanity/cli-core source directly for better dead code elimination
  • Adds CLIError and warn() utilities to @sanity/cli-core to replace oclif UX imports in the init code path
  • Decouples dataset import from the oclif ImportDatasetCommand (now uses execa to call the local sanity binary)
  • Defines init flags as plain POJOs with an oclif adapter layer, so both the oclif command and standalone entry can share the same flag definitions

Key changes by area:

  • @sanity/cli-core: New CLIError/CLIWarning error classes, reorganized exports, inlined indent-string dependency
  • @sanity/cli init action: Extracted from the 1700-line init.ts command into actions/init/ with separate files for each flow (studio, nextjs, app, scaffolding, helpers)
  • @sanity/cli init command: Now a thin oclif wrapper that converts flags and delegates to initAction()
  • create-sanity: New standalone entry point (src/index.ts), Rollup bundling config, proper arg parsing with help output, tests
  • Flag system: flags.ts defines flags as POJOs, flagAdapter.ts converts them to oclif format, flagsToInitOptions.ts converts parsed values to InitOptions

What to review

  • The Rollup config and bundling approach (packages/create-sanity/rollup.config.ts)
  • The standalone entry point and arg parsing (packages/create-sanity/src/index.ts, src/parseArgs.ts)
  • The flag definition system and oclif adapter (packages/@sanity/cli/src/actions/init/flags.ts, src/util/flagAdapter.ts)
  • The initAction extraction and flow separation (packages/@sanity/cli/src/actions/init/)
  • Test coverage for the standalone bundle and flag parsing

Notes

  • Bundle is currently ~2.8 MB unminified. There is significant room for improvement (tree-shaking failures with barrel exports, unnecessary transitive deps). A separate DEP-REPORT.md analysis has been prepared with a roadmap to get this under 1 MB.
  • The sanity init command continues to work exactly as before - this is purely additive
  • The standalone entry replicates oclif's flag parsing behavior (aliases, negation, exclusive constraints, validation) using node:util parseArgs

Test plan

  • pnpm test passes
  • pnpm check:types passes
  • pnpm check:lint passes
  • pnpm build:cli succeeds
  • node packages/create-sanity/dist/index.js --help shows correct help output
  • sanity init still works end-to-end
  • npm create sanity works end-to-end with the bundled package

🤖 Generated with Claude Code

rexxars and others added 27 commits March 23, 2026 12:37
…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>
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>
- 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>
- 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>
@claude
Copy link
Copy Markdown

claude bot commented Mar 23, 2026

Claude finished @rexxars's task in 5m 31s —— View job


Code Review

  • Gather context
  • Review key files
  • Post findings

Issues Found

1. Exclusive constraint check fires on defaulted flags — parseArgs.ts:107-115

node:util parseArgs includes default values in the returned values object. The exclusive check uses merged[name] === undefined to decide if a flag was "set", but flags with defaults are never undefined in merged.

Concrete breakage: auto-updates has default: true and exclusive: ['bare']. When a user runs npm create sanity -- --bare, merged['auto-updates'] is true (from default), merged['bare'] is true (from input), and the check fires: --auto-updates cannot be used with --bare — even though the user never passed --auto-updates. The same applies for boolean defaults of false: since false !== undefined, any defaulted flag with an exclusive list will spuriously conflict.

The existing tests don't catch this because they only assert code !== 0 and stderr contains or doesn't contain specific strings, which pass regardless of which exclusive violation is reported. Fix this →

2. Dead code in getCreateCommandparseArgs.ts:19

const pm = getRunningPackageManager() ?? 'npm'
const sep = options?.withFlagSeparator && (pm === 'npm' || !pm) ? ' --' : ''

pm is always a non-empty string (defaults to 'npm'), so !pm is always false. Fix this →

3. output.error type mismatch in index.ts:36-39

The output object's error callback is typed as (msg) => never, but InitContext.output (defined in types.ts) uses the shared Output type from cli-core. If Output.error is typed as (msg: Error | string) => void, this cast is unsound and TypeScript may silently accept calls in initAction that assume error() doesn't return. Verify the Output type's error signature matches never. Fix this →

4. rollup-plugin-visualizer artifact committed to dist/rollup.config.ts:105-109

visualizer writes dist/stats.html to disk on every build. It's excluded from the npm package via !dist/stats.html in package.json, but the file will be present in the working tree after every build, showing up as an untracked file in git status. Consider gating it with an env flag (process.env.ANALYZE) so it's opt-in. Fix this →

5. Missing test for --bare with no other flags

The test suite doesn't cover the happy path of runCreateSanity(['--bare', '--yes']) (or ['--bare'] alone) to confirm that passing --bare without --auto-updates does not produce a flag conflict error. This would catch the issue in point 1 above. Fix this →

@socket-security
Copy link
Copy Markdown

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
Addednpm/​rollup-plugin-visualizer@​7.0.19910010091100

View full report

@rexxars rexxars closed this Mar 23, 2026
@github-actions
Copy link
Copy Markdown
Contributor

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/index.ts 0.0% (new)
packages/create-sanity/src/noopTelemetry.ts 0.0% (new)
packages/create-sanity/src/parseArgs.ts 0.0% (new)

Comparing 30 changed files against main @ 34577da2b6d07747e07360a32556a261e32b7adb

Overall Coverage

Metric Coverage
Statements 81.9% (- 1.1%)
Branches 71.3% (- 1.4%)
Functions 82.1% (- 1.1%)
Lines 82.4% (- 1.0%)

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