Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3b3a947
refactor(init): move init logic to action pattern (#748)
rexxars Mar 23, 2026
d7c72fb
refactor(init): split init flows into separate functions (#753)
rexxars Mar 23, 2026
1b1a2a1
feat(create-sanity): add standalone entry point calling initAction di…
rexxars Mar 20, 2026
3d5d02f
feat(create-sanity): replace esbuild with Rollup for bundling
rexxars Mar 20, 2026
3490d89
feat(cli-core): add CLIError and warn() utilities to replace oclif ux
rexxars Mar 23, 2026
09d27f4
refactor(cli): remove oclif imports from init-reachable code paths
rexxars Mar 23, 2026
ec1570b
refactor(cli): define init flags as POJOs with oclif adapter
rexxars Mar 23, 2026
63746a8
feat(create-sanity): replace oclif parser with node:util parseArgs
rexxars Mar 23, 2026
324f52f
chore: clean up lint, type errors, and remove old create-sanity files
rexxars Mar 23, 2026
2b6da42
test(create-sanity): add tests for standalone bundle
rexxars Mar 23, 2026
381b61e
chore(create-sanity): fix dep check issues
rexxars Mar 23, 2026
fcbf022
refactor(init): decouple dataset import from oclif ImportDatasetCommand
rexxars Mar 23, 2026
a1347fe
fix: enable but dont publish source map
rexxars Mar 23, 2026
b3d7af4
chore(cli-core): inline indent-string, drop dependency
rexxars Mar 23, 2026
c79ce09
refactor(create-sanity): reuse getRunningPackageManager from cli-core
rexxars Mar 23, 2026
3c01413
refactor: remove --from-create flag (dead code)
rexxars Mar 23, 2026
07fa322
fix(create-sanity): add -- flag separator for npm in help output
rexxars Mar 23, 2026
01a93e4
refactor(init): extract flagsToInitOptions into its own file
rexxars Mar 23, 2026
020c660
refactor: reorganize source, errors
rexxars Mar 23, 2026
b5d2669
fix(init): guard against missing sanity binary before dataset import
rexxars Mar 23, 2026
78a0c89
fix: address code review findings
rexxars Mar 23, 2026
a54fd9d
fix(create-sanity): handle flag aliases, option validation, exclusive…
rexxars Mar 23, 2026
e6848b3
refactor(create-sanity): extract flag parsing to parseArgs.ts
rexxars Mar 23, 2026
feee49c
fix(test): update init tests for execa-based dataset import
rexxars Mar 23, 2026
1cafe70
fix: normalize exit codes for flag validation errors
rexxars Mar 23, 2026
0e16428
fix: pretest build step, cleaner dataset import errors, boolean defaults
rexxars Mar 23, 2026
f14e46e
fix(create-sanity): improve help output and alias conflict detection
rexxars Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {type KnipConfig} from 'knip'
const project = ['src/**/*.{js,jsx,ts,tsx}', '!**/docs/**']

const baseConfig = {
// For now only care about cli package
ignore: [
'tmp/**',

'packages/@sanity/cli-test/fixtures/**',

// See `helpClass` in `oclif.config.js`
Expand Down Expand Up @@ -72,6 +73,8 @@ const baseConfig = {
project,
},
'packages/create-sanity': {
// @sanity/cli is imported via relative source paths (../../@sanity/cli/src/...)
// for bundling, but the workspace link is still needed for resolution
ignoreDependencies: ['@sanity/cli'],
},
},
Expand Down
14 changes: 10 additions & 4 deletions packages/@sanity/cli-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@
],
"type": "module",
"sideEffects": false,
"main": "./dist/index.js",
"types": "dist/index.d.ts",
"main": "./dist/_exports/index.js",
"types": "dist/_exports/index.d.ts",
"exports": {
".": {
"source": "./src/index.ts",
"default": "./dist/index.js"
"source": "./src/_exports/index.ts",
"default": "./dist/_exports/index.js"
},
"./errors": {
"source": "./src/_exports/errors.ts",
"default": "./dist/_exports/errors.js"
},
"./ux": {
"source": "./src/_exports/ux.ts",
Expand Down Expand Up @@ -68,6 +72,7 @@
"@sanity/client": "catalog:",
"babel-plugin-react-compiler": "^1.0.0",
"boxen": "^8.0.1",
"clean-stack": "^6.0.0",
"configstore": "^7.0.0",
"debug": "catalog:",
"get-it": "^8.7.0",
Expand All @@ -82,6 +87,7 @@
"tsx": "catalog:",
"vite": "catalog:",
"vite-node": "^5.3.0",
"wrap-ansi": "^10.0.0",
"zod": "catalog:"
},
"devDependencies": {
Expand Down
5 changes: 5 additions & 0 deletions packages/@sanity/cli-core/src/_exports/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from '../errors/CLIError.js'
export * from '../errors/CLIWarning.js'
export * from '../errors/NonInteractiveError.js'
export * from '../errors/NotFoundError.js'
export * from '../errors/ProjectRootNotFoundError.js'
67 changes: 67 additions & 0 deletions packages/@sanity/cli-core/src/_exports/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {deprecate} from 'node:util'

import {NonInteractiveError as _NonInteractiveError} from '../errors/NonInteractiveError.js'
import {NotFoundError as _NotFoundError} from '../errors/NotFoundError.js'
import {ProjectRootNotFoundError as _ProjectRootNotFoundError} from '../errors/ProjectRootNotFoundError.js'

export * from '../config/cli/getCliConfig.js'
export * from '../config/cli/getCliConfigSync.js'
export {type CliConfig} from '../config/cli/types/cliConfig.js'
export {type UserViteConfig} from '../config/cli/types/userViteConfig.js'
export * from '../config/findProjectRoot.js'
export * from '../config/findProjectRootSync.js'
export * from '../config/studio/getStudioConfig.js'
export * from '../config/studio/getStudioWorkspaces.js'
export * from '../config/studio/isStudioConfig.js'
export * from '../config/util/findConfigsPaths.js'
export * from '../config/util/findStudioConfigPath.js'
export {type ProjectRootResult} from '../config/util/recursivelyResolveProjectRoot.js'
export * from '../debug.js'
export * from '../loaders/studio/studioWorkerTask.js'
export * from '../loaders/tsx/tsxWorkerTask.js'
export * from '../SanityCommand.js'
export * from '../services/apiClient.js'
export * from '../services/cliUserConfig.js'
export * from '../services/getCliToken.js'
export * from '../telemetry/getTelemetryBaseInfo.js'
export {
type CLITelemetryStore,
type ConsentInformation,
type TelemetryUserProperties,
} from '../telemetry/types.js'
export {type Output, type SanityOrgUser} from '../types.js'
export {doImport} from '../util/doImport.js'
export * from '../util/environment/mockBrowserEnvironment.js'
export {
clearCliTelemetry,
CLI_TELEMETRY_SYMBOL,
getCliTelemetry,
setCliTelemetry,
} from '../util/getCliTelemetry.js'
export * from '../util/getSanityEnvVar.js'
export * from '../util/getSanityUrl.js'
export * from '../util/getUserConfig.js'
export * from '../util/importModule.js'
export * from '../util/isCi.js'
export * from '../util/isInteractive.js'
export * from '../util/isStaging.js'
export * from '../util/normalizePath.js'
export * from '../util/promisifyWorker.js'
export * from '../util/readPackageJson.js'
export * from '../util/resolveLocalPackage.js'
export * from '../util/safeStructuredClone.js'
export * from '../ux/colorizeJson.js'
export * from '../ux/timer.js'

export const NonInteractiveError = deprecate(
_NonInteractiveError,
'Import `NonInteractiveError` from `@sanity/cli-core/errors`',
)
export const NotFoundError = deprecate(
_NotFoundError,
'Import `NotFoundError` from `@sanity/cli-core/errors`',
)
export const ProjectRootNotFoundError = deprecate(
_ProjectRootNotFoundError,
'Import `ProjectRootNotFoundError` from `@sanity/cli-core/errors`',
)
2 changes: 2 additions & 0 deletions packages/@sanity/cli-core/src/_exports/ux.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export {NonInteractiveError} from '../errors/NonInteractiveError.js'
export * from '../ux/boxen.js'
export * from '../ux/errors.js'
export * from '../ux/logSymbols.js'
export * from '../ux/output.js'
export * from '../ux/prompts.js'
export * from '../ux/spinner.js'
46 changes: 46 additions & 0 deletions packages/@sanity/cli-core/src/errors/CLIError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {styleText} from 'node:util'

import {type PrettyPrintableError} from '@oclif/core/interfaces'
import cleanStack from 'clean-stack'

/**
* A formatted CLI error that pretty-prints to stderr.
*
* This is a lightweight reimplementation of `@oclif/core`'s `CLIError`.
* We can't import the original because `@oclif/core` is a CJS barrel that
* pulls in the entire oclif runtime (~10MB) when bundled - defeating
* tree-shaking in the standalone `create-sanity` bundle. By owning the
* error class here, code in `@sanity/cli-core` and the init action tree
* can throw formatted errors without depending on oclif at all.
*
* The `oclif` property is shaped so oclif's error handler still recognises
* these errors when thrown inside an oclif command, preserving the correct
* exit code and suppressing redundant stack traces.
*/
export class CLIError extends Error {
code?: string
oclif: {exit?: number} = {exit: 2}
ref?: string
skipOclifErrorHandling?: boolean
suggestions?: string[]

constructor(error: Error | string, options: PrettyPrintableError & {exit?: false | number} = {}) {
super(error instanceof Error ? error.message : error)
if (error instanceof Error && error.stack) {
this.stack = error.stack
}
if (options.exit !== undefined)
this.oclif.exit = options.exit === false ? undefined : options.exit
this.code = options.code
this.suggestions = options.suggestions
this.ref = options.ref
}

get bang(): string | undefined {
return styleText('red', process.platform === 'win32' ? '»' : '›')
}

get prettyStack(): string {
return cleanStack(super.stack!, {pretty: true})
}
}
18 changes: 18 additions & 0 deletions packages/@sanity/cli-core/src/errors/CLIWarning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {styleText} from 'node:util'

import {CLIError} from './CLIError.js'

/**
* A warning-level CLI error. Identical to {@link CLIError} except the
* bang prefix is yellow instead of red.
*/
export class CLIWarning extends CLIError {
constructor(input: Error | string) {
super(input instanceof Error ? input.message : input)
this.name = 'Warning'
}

override get bang(): string | undefined {
return styleText('yellow', process.platform === 'win32' ? '»' : '›')
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {CLIError} from '@oclif/core/errors'
import {CLIError} from './CLIError.js'

/**
* Error thrown when a prompt is attempted in a non-interactive environment
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {CLIError} from '@oclif/core/errors'

import {isRecord} from '../util/isRecord.js'
import {CLIError} from './CLIError.js'

/**
* Error thrown when a project root directory cannot be found.
Expand Down
51 changes: 0 additions & 51 deletions packages/@sanity/cli-core/src/index.ts

This file was deleted.

5 changes: 3 additions & 2 deletions packages/@sanity/cli-core/src/services/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {ux} from '@oclif/core'
import {styleText} from 'node:util'

import {
type ClientConfig,
type ClientError,
Expand Down Expand Up @@ -168,7 +169,7 @@ function authErrors() {

const statusCode = isHttpError(err) && err.response.body.statusCode
if (statusCode === 401) {
err.message = `${err.message}. You may need to login again with ${ux.colorize('cyan', 'sanity login')}.\nFor more information, see ${generateHelpUrl('cli-errors')}.`
err.message = `${err.message}. You may need to login again with ${styleText('cyan', 'sanity login')}.\nFor more information, see ${generateHelpUrl('cli-errors')}.`
}

return err
Expand Down
4 changes: 2 additions & 2 deletions packages/@sanity/cli-core/src/util/getCliTelemetry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {ux} from '@oclif/core'
import {noopLogger} from '@sanity/telemetry'

import {type CLITelemetryStore} from '../telemetry/types.js'
import {warn} from '../ux/errors.js'

/**
* @public
Expand Down Expand Up @@ -33,7 +33,7 @@ export function getCliTelemetry(): CLITelemetryStore {
const state = getState()
// This should never happen, but if it does, we return a noop logger to avoid errors.
if (!state) {
ux.warn('CLI telemetry not initialized, returning noop logger')
warn('CLI telemetry not initialized, returning noop logger')
return noopLogger
}

Expand Down
84 changes: 84 additions & 0 deletions packages/@sanity/cli-core/src/ux/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* eslint-disable no-console -- this is the error output layer */
import {type PrettyPrintableError} from '@oclif/core/interfaces'
import wrapAnsi from 'wrap-ansi'

import {CLIError} from '../errors/CLIError.js'
import {CLIWarning} from '../errors/CLIWarning.js'

const settings: {debug?: boolean} = (globalThis as Record<string, unknown>).oclif ?? {}

/**
* Print a formatted error to stderr without throwing, when `exit: false`.
*/
export function error(input: Error | string, options: PrettyPrintableError & {exit: false}): void
/**
* Throw a formatted {@link CLIError}.
*/
export function error(
input: Error | string,
options?: PrettyPrintableError & {exit?: number},
): never
export function error(
input: Error | string,
options: PrettyPrintableError & {exit?: false | number} = {},
): void {
const err = new CLIError(input, options)

if (options.exit === false) {
const message = prettyPrint(err)
if (message) console.error(message)
} else {
throw err
}
}

/**
* Print a formatted warning to stderr.
*/
export function warn(input: Error | string): void {
const err = new CLIWarning(input)
const message = prettyPrint(err)
if (message) console.error(message)
}

function indentString(str: string, count: number, options?: {indent?: string}): string {
const indent = options?.indent ?? ' '
if (count === 0) return str
return str.replaceAll(/^(?!\s*$)/gm, indent.repeat(count))
}

function stderrWidth(): number {
const env = Number.parseInt(process.env.OCLIF_COLUMNS!, 10)
if (env) return env
if (!process.stderr.isTTY) return 80
const w = (process.stderr as {getWindowSize?: () => number[]}).getWindowSize?.()[0] ?? 80
return Math.max(w < 1 ? 80 : w, 40)
}

function formatSuggestions(suggestions?: string[]): string | undefined {
const label = 'Try this:'
if (!suggestions || suggestions.length === 0) return undefined
if (suggestions.length === 1) return `${label} ${suggestions[0]}`
return `${label}\n${indentString(suggestions.map((s) => `* ${s}`).join('\n'), 2)}`
}

function prettyPrint(error: CLIError): string | undefined {
if (settings.debug) return error.prettyStack

const {bang: prefix, code, message, name: errorSuffix, ref, suggestions} = error
const formattedHeader = message ? `${errorSuffix || 'Error'}: ${message}` : undefined
const formattedCode = code ? `Code: ${code}` : undefined
const formattedSuggestions = formatSuggestions(suggestions)
const formattedReference = ref ? `Reference: ${ref}` : undefined

const formatted = [formattedHeader, formattedCode, formattedSuggestions, formattedReference]
.filter(Boolean)
.join('\n')

const width = stderrWidth()
let output = wrapAnsi(formatted, width - 6, {hard: true, trim: false})
output = indentString(output, 3)
output = indentString(output, 1, {indent: prefix || ''})
output = indentString(output, 1)
return output
}
Loading
Loading