Skip to content

Commit e6848b3

Browse files
rexxarsclaude
andcommitted
refactor(create-sanity): extract flag parsing to parseArgs.ts
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>
1 parent a54fd9d commit e6848b3

2 files changed

Lines changed: 162 additions & 141 deletions

File tree

packages/create-sanity/src/index.ts

Lines changed: 6 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,154 +1,19 @@
11
/* eslint-disable no-console */
2-
import {parseArgs} from 'node:util'
32

43
// eslint-disable-next-line import-x/no-extraneous-dependencies -- bundled, not a runtime dep
54
import {isInteractive} from '@sanity/cli-core'
6-
// eslint-disable-next-line import-x/no-extraneous-dependencies -- bundled, not a runtime dep
7-
import {getRunningPackageManager} from '@sanity/cli-core/package-manager'
85

9-
import {type FlagDef, initFlagDefs} from '../../@sanity/cli/src/actions/init/flags.js'
10-
import {initAction} from '../../@sanity/cli/src/actions/init/initAction.js'
11-
import {InitError} from '../../@sanity/cli/src/actions/init/initError.js'
126
import {
137
flagsToInitOptions,
148
type InitCommandFlags,
159
} from '../../@sanity/cli/src/actions/init/flagsToInitOptions.js'
10+
import {initAction} from '../../@sanity/cli/src/actions/init/initAction.js'
11+
import {InitError} from '../../@sanity/cli/src/actions/init/initError.js'
1612
import {createNoopTelemetryStore} from './noopTelemetry.js'
17-
18-
function getCreateCommand(options?: {withFlagSeparator?: boolean}): string {
19-
const pm = getRunningPackageManager() ?? 'npm'
20-
// npm requires `--` to forward flags to the create script, other PMs don't
21-
const sep = options?.withFlagSeparator && (pm === 'npm' || !pm) ? ' --' : ''
22-
if (pm === 'bun') return `bun create sanity@latest${sep}`
23-
if (pm === 'pnpm') return `pnpm create sanity@latest${sep}`
24-
if (pm === 'yarn') return `yarn create sanity@latest${sep}`
25-
return `npm create sanity@latest${sep}`
26-
}
27-
28-
type ParseArgsOption = {
29-
default?: boolean | string
30-
multiple?: boolean
31-
short?: string
32-
type: 'boolean' | 'string'
33-
}
34-
35-
function buildParseArgsOptions() {
36-
const options: Record<string, ParseArgsOption> = {}
37-
const allowNoFlags = new Set<string>()
38-
// Maps alias name → canonical flag name (e.g. 'project-id' → 'project')
39-
const aliasMap = new Map<string, string>()
40-
41-
for (const [name, def] of Object.entries<FlagDef>(initFlagDefs)) {
42-
if (def.type !== 'boolean' && def.type !== 'string') {
43-
throw new Error(`Unknown flag type "${def.type}" for flag "${name}"`)
44-
}
45-
46-
options[name] = {type: def.type}
47-
if (def.short) options[name].short = def.short
48-
if (def.default !== undefined) options[name].default = def.default
49-
50-
if (def.type === 'boolean' && def.allowNo) {
51-
allowNoFlags.add(name)
52-
options[`no-${name}`] = {type: 'boolean'}
53-
}
54-
55-
// Register aliases as separate parseArgs options that map back to the canonical name
56-
if (def.aliases) {
57-
for (const alias of def.aliases) {
58-
options[alias] = {type: def.type}
59-
aliasMap.set(alias, name)
60-
}
61-
}
62-
}
63-
64-
// Built-in --help support
65-
options.help = {short: 'h', type: 'boolean'}
66-
67-
return {aliasMap, allowNoFlags, options}
68-
}
69-
70-
/**
71-
* Merge --no-<flag> companions back into the base flag, resolve aliases
72-
* to canonical names, and validate option constraints.
73-
*/
74-
function normalizeFlags(
75-
values: Record<string, unknown>,
76-
allowNoFlags: Set<string>,
77-
aliasMap: Map<string, string>,
78-
): Record<string, unknown> {
79-
const merged = {...values}
80-
81-
// Resolve aliases to canonical names
82-
for (const [alias, canonical] of aliasMap) {
83-
if (merged[alias] !== undefined) {
84-
merged[canonical] = merged[alias]
85-
delete merged[alias]
86-
}
87-
}
88-
89-
// Merge --no-<flag> companions
90-
for (const name of allowNoFlags) {
91-
const noKey = `no-${name}`
92-
if (merged[noKey] === true) {
93-
merged[name] = false
94-
}
95-
delete merged[noKey]
96-
}
97-
98-
// Validate string flags with `options` constraints
99-
for (const [name, def] of Object.entries<FlagDef>(initFlagDefs)) {
100-
if (def.options && merged[name] !== undefined) {
101-
const value = String(merged[name])
102-
if (!def.options.includes(value)) {
103-
console.error(
104-
`Invalid value "${value}" for --${name}. ` + `Allowed: ${def.options.join(', ')}`,
105-
)
106-
process.exit(1)
107-
}
108-
}
109-
}
110-
111-
// Validate exclusive constraints
112-
for (const [name, def] of Object.entries<FlagDef>(initFlagDefs)) {
113-
if (!def.exclusive || merged[name] === undefined) continue
114-
for (const other of def.exclusive) {
115-
if (merged[other] !== undefined) {
116-
console.error(`--${name} cannot be used with --${other}`)
117-
process.exit(1)
118-
}
119-
}
120-
}
121-
122-
return merged
123-
}
13+
import {getCreateCommand, parseInitArgs} from './parseArgs.js'
12414

12515
try {
126-
const {aliasMap, allowNoFlags, options} = buildParseArgsOptions()
127-
const {positionals, values} = parseArgs({
128-
allowPositionals: true,
129-
args: process.argv.slice(2),
130-
options,
131-
strict: true,
132-
})
133-
134-
if (values.help) {
135-
const cmd = getCreateCommand({withFlagSeparator: true})
136-
console.log(`Usage: ${cmd} [options]`)
137-
console.log('')
138-
console.log('Initialize a new Sanity project')
139-
console.log('')
140-
console.log('Options:')
141-
for (const [name, def] of Object.entries<FlagDef>(initFlagDefs)) {
142-
if (def.hidden) continue
143-
const flag = def.short ? `-${def.short}, --${name}` : ` --${name}`
144-
const val = def.type === 'string' && def.helpValue ? ` ${def.helpValue}` : ''
145-
console.log(` ${(flag + val).padEnd(36)} ${def.description || ''}`)
146-
}
147-
process.exit(0)
148-
}
149-
150-
const flags = normalizeFlags(values, allowNoFlags, aliasMap)
151-
const args = {type: positionals[0]}
16+
const {args, flags} = parseInitArgs(process.argv.slice(2))
15217

15318
let mcpMode: 'auto' | 'prompt' | 'skip' = 'prompt'
15419
if (!flags.mcp || !isInteractive()) {
@@ -168,8 +33,8 @@ try {
16833

16934
await initAction(initOptions, {
17035
output: {
171-
error: (msg: string): never => {
172-
console.error(msg)
36+
error: (msg: Error | string): never => {
37+
console.error(msg instanceof Error ? msg.message : msg)
17338
process.exit(1)
17439
},
17540
log: console.log,
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/* eslint-disable no-console */
2+
import {parseArgs} from 'node:util'
3+
4+
// eslint-disable-next-line import-x/no-extraneous-dependencies -- bundled, not a runtime dep
5+
import {getRunningPackageManager} from '@sanity/cli-core/package-manager'
6+
7+
import {type FlagDef, initFlagDefs} from '../../@sanity/cli/src/actions/init/flags.js'
8+
9+
type ParseArgsOption = {
10+
default?: boolean | string
11+
multiple?: boolean
12+
short?: string
13+
type: 'boolean' | 'string'
14+
}
15+
16+
export function getCreateCommand(options?: {withFlagSeparator?: boolean}): string {
17+
const pm = getRunningPackageManager() ?? 'npm'
18+
// npm requires `--` to forward flags to the create script, other PMs don't
19+
const sep = options?.withFlagSeparator && (pm === 'npm' || !pm) ? ' --' : ''
20+
if (pm === 'bun') return `bun create sanity@latest${sep}`
21+
if (pm === 'pnpm') return `pnpm create sanity@latest${sep}`
22+
if (pm === 'yarn') return `yarn create sanity@latest${sep}`
23+
return `npm create sanity@latest${sep}`
24+
}
25+
26+
function buildParseArgsOptions() {
27+
const options: Record<string, ParseArgsOption> = {}
28+
const allowNoFlags = new Set<string>()
29+
// Maps alias name → canonical flag name (e.g. 'project-id' → 'project')
30+
const aliasMap = new Map<string, string>()
31+
32+
for (const [name, def] of Object.entries<FlagDef>(initFlagDefs)) {
33+
if (def.type !== 'boolean' && def.type !== 'string') {
34+
throw new Error(`Unknown flag type "${def.type}" for flag "${name}"`)
35+
}
36+
37+
options[name] = {type: def.type}
38+
if (def.short) options[name].short = def.short
39+
if (def.default !== undefined) options[name].default = def.default
40+
41+
if (def.type === 'boolean' && def.allowNo) {
42+
allowNoFlags.add(name)
43+
options[`no-${name}`] = {type: 'boolean'}
44+
}
45+
46+
// Register aliases as separate parseArgs options that map back to the canonical name
47+
if (def.aliases) {
48+
for (const alias of def.aliases) {
49+
options[alias] = {type: def.type}
50+
aliasMap.set(alias, name)
51+
}
52+
}
53+
}
54+
55+
// Built-in --help support
56+
options.help = {short: 'h', type: 'boolean'}
57+
58+
return {aliasMap, allowNoFlags, options}
59+
}
60+
61+
/**
62+
* Merge --no-<flag> companions back into the base flag, resolve aliases
63+
* to canonical names, and validate option constraints.
64+
*/
65+
function normalizeFlags(
66+
values: Record<string, unknown>,
67+
allowNoFlags: Set<string>,
68+
aliasMap: Map<string, string>,
69+
): Record<string, unknown> {
70+
const merged = {...values}
71+
72+
// Resolve aliases to canonical names
73+
for (const [alias, canonical] of aliasMap) {
74+
if (merged[alias] !== undefined) {
75+
merged[canonical] = merged[alias]
76+
delete merged[alias]
77+
}
78+
}
79+
80+
// Merge --no-<flag> companions
81+
for (const name of allowNoFlags) {
82+
const noKey = `no-${name}`
83+
if (merged[noKey] === true) {
84+
merged[name] = false
85+
}
86+
delete merged[noKey]
87+
}
88+
89+
// Validate string flags with `options` constraints
90+
for (const [name, def] of Object.entries<FlagDef>(initFlagDefs)) {
91+
if (def.options && merged[name] !== undefined) {
92+
const value = String(merged[name])
93+
if (!def.options.includes(value)) {
94+
console.error(
95+
`Invalid value "${value}" for --${name}. ` + `Allowed: ${def.options.join(', ')}`,
96+
)
97+
process.exit(1)
98+
}
99+
}
100+
}
101+
102+
// Validate exclusive constraints
103+
for (const [name, def] of Object.entries<FlagDef>(initFlagDefs)) {
104+
if (!def.exclusive || merged[name] === undefined) continue
105+
for (const other of def.exclusive) {
106+
if (merged[other] !== undefined) {
107+
console.error(`--${name} cannot be used with --${other}`)
108+
process.exit(1)
109+
}
110+
}
111+
}
112+
113+
return merged
114+
}
115+
116+
function printHelp(): never {
117+
const cmd = getCreateCommand({withFlagSeparator: true})
118+
console.log(`Usage: ${cmd} [options]`)
119+
console.log('')
120+
console.log('Initialize a new Sanity project')
121+
console.log('')
122+
console.log('Options:')
123+
for (const [name, def] of Object.entries<FlagDef>(initFlagDefs)) {
124+
if (def.hidden) continue
125+
const flag = def.short ? `-${def.short}, --${name}` : ` --${name}`
126+
const val = def.type === 'string' && def.helpValue ? ` ${def.helpValue}` : ''
127+
console.log(` ${(flag + val).padEnd(36)} ${def.description || ''}`)
128+
}
129+
process.exit(0)
130+
}
131+
132+
/**
133+
* Parse process.argv using node:util parseArgs with the init flag definitions.
134+
* Handles --help, aliases, --no-* negation, option validation, and exclusive constraints.
135+
*/
136+
export function parseInitArgs(argv: string[]): {
137+
args: {type?: string}
138+
flags: Record<string, unknown>
139+
} {
140+
const {aliasMap, allowNoFlags, options} = buildParseArgsOptions()
141+
const {positionals, values} = parseArgs({
142+
allowPositionals: true,
143+
args: argv,
144+
options,
145+
strict: true,
146+
})
147+
148+
if (values.help) {
149+
printHelp()
150+
}
151+
152+
return {
153+
args: {type: positionals[0]},
154+
flags: normalizeFlags(values, allowNoFlags, aliasMap),
155+
}
156+
}

0 commit comments

Comments
 (0)