sanity 4.22.0-next.9 → 4.22.1

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/bin/sanity CHANGED
@@ -1,57 +1,171 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable import/no-dynamic-require, no-sync, no-console, no-process-exit */
2
+ /**
3
+ * `npx sanity`: in order for this to work, the `sanity` module should be the one
4
+ * exposing a binary. However, the `@sanity/cli` module is the one who _actually_ ship
5
+ * the CLI binary.
6
+ *
7
+ * To solve this:
8
+ * 1. `@sanity/cli` is a dependency of the `sanity` module
9
+ * 2. The path to this file is configured as `bin.sanity` in the `sanity` module
10
+ * 3. This script resolves the `@sanity/cli` package, finds the path to the `sanity`
11
+ * binary from the `@sanity/cli` package declaration (`bin.sanity`)
12
+ * 4. Either imports the resolved path directly (if it's a Node.js script), or spawns it
13
+ * (if the path specified does not point to a Node.js script)
14
+ *
15
+ * See `runSanityCli()` for more details on the dual approach of importing vs spawning.
16
+ */
3
17
 
4
- import fs from 'node:fs'
5
- import path from 'node:path'
6
- import readPkgUp from 'read-pkg-up'
18
+ /* eslint-disable import/no-dynamic-require, no-process-exit */
19
+ import {readFile, open} from 'node:fs/promises'
7
20
  import {createRequire} from 'node:module'
8
- import {fileURLToPath} from 'node:url'
21
+ import {dirname, resolve} from 'node:path'
22
+ import execa from 'execa'
9
23
 
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
- const expectedPath = path.join(__dirname, '..', 'node_modules', '.bin', 'sanity')
12
24
  const require = createRequire(import.meta.url)
13
25
 
14
- if (fs.existsSync(expectedPath)) {
15
- isNodeScript(expectedPath).then((isScript) => {
16
- if (isScript) {
17
- require(expectedPath)
26
+ runSanityCli().catch((err) => {
27
+ // Use setImmediate/setTimeout to throw outside the promise chain
28
+ setImmediate(() => {
29
+ throw err
30
+ })
31
+ })
32
+
33
+ /**
34
+ * Resolves the Sanity CLI binary from `@sanity/cli` and executes it in the best
35
+ * possible way available to us, based on the module type:
36
+ *
37
+ * - If the binary is a Node.js script, we either `import()` or `require()` it based on
38
+ * the extension (cjs/mjs) or the package `type` field. This is prefered as it does
39
+ * not require spawning a new process.
40
+ * - If the binary is not a Node.js script, we spawn it using `execa`, which handles
41
+ * things correctly across platforms (windows doesn't enjoy shebangs etc). stdio is
42
+ * inherited from the parent process.
43
+ *
44
+ * Note:
45
+ * The "new" CLI is an ESM module, while the "old" CLI is CommonJS.
46
+ * In theory, Node.js now has support for transparently mixing CommonJS and ESM, but in
47
+ * practice we've seen some issues with this approach for the CLI specifically. Thus, we
48
+ * explicitly check the package type and use either `import()` or `require()` accordingly.
49
+ */
50
+ async function runSanityCli() {
51
+ const {bin, type} = await getSanityCliBin()
52
+
53
+ // The optimal case: the binary is a Node.js script - import/require it directly to
54
+ // prevent needing to spawn a new process
55
+ if (await isNodeScript(bin)) {
56
+ if (type === 'module') {
57
+ await import(bin)
18
58
  } else {
19
- runFromCli()
59
+ require(bin)
20
60
  }
61
+ return
62
+ }
63
+
64
+ // Fallback: spawn the binary as a child process
65
+ const child = execa(bin, process.argv.slice(2), {
66
+ stdio: 'inherit',
67
+ encoding: null,
68
+ buffer: false,
69
+ reject: false,
21
70
  })
22
- } else {
23
- runFromCli()
24
- }
25
71
 
26
- function runFromCli() {
27
- getSanityCliPath()
28
- .then((cliPath) => require(path.join(cliPath, 'bin', 'sanity')))
29
- .catch((err) => {
30
- console.error(err)
31
- process.exit(1)
32
- })
72
+ // Forward signals to the child process
73
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP']
74
+ const signalHandlers = signals.map((signal) => {
75
+ const handler = () => child.kill(signal)
76
+ process.on(signal, handler)
77
+ return {signal, handler}
78
+ })
79
+
80
+ try {
81
+ const {exitCode} = await child
82
+ process.exitCode = exitCode
83
+ } finally {
84
+ // Clean up signal handlers
85
+ for (const {signal, handler} of signalHandlers) {
86
+ process.off(signal, handler)
87
+ }
88
+ }
33
89
  }
34
90
 
35
- async function getSanityCliPath() {
36
- const modulePath = require.resolve(
37
- // Resolve package.json from `@sanity/cli` instead of the main export to allow resolving before
38
- // the package is built. Used when running the CLI from source by setting the env var SANITY_CLI_RUN_FROM_SOURCE=1
39
- '@sanity/cli/package.json',
40
- )
41
- const pkg = await readPkgUp({cwd: path.dirname(modulePath)})
42
- if (pkg) {
43
- return path.dirname(pkg.path)
91
+ async function getSanityCliBin() {
92
+ // Resolve package.json from `@sanity/cli` instead of the main export to allow
93
+ // resolving before the package is built. Note that this might fail if `@sanity/cli`
94
+ // does not explicitly list `./package.json` in the exports field, but since control
95
+ // this module, this feels like a safe assumption.
96
+ const pkgPath = require.resolve('@sanity/cli/package.json')
97
+
98
+ let pkg
99
+ try {
100
+ pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
101
+ } catch (err) {
102
+ throw new Error(`Failed to read @sanity/cli package.json: ${err.message}`)
103
+ }
104
+
105
+ if (!pkg.bin?.sanity) {
106
+ throw new Error('Failed to find `bin.sanity` field in @sanity/cli package.json')
44
107
  }
45
108
 
46
- throw new Error('Failed to resolve @sanity/cli path')
109
+ // Assuming `package.json` is located at the root of the package - anything else would
110
+ // be… wild, unconventional, unorthodox… I'd even say… heretical.
111
+ const cliDir = dirname(pkgPath)
112
+
113
+ // Note that npm normalizes bin paths: `./bin/sanity` -> `bin/sanity`
114
+ const bin = resolve(cliDir, pkg.bin.sanity)
115
+ const type = determineModuleType(bin, pkg)
116
+ return {bin, type}
47
117
  }
48
118
 
119
+ /**
120
+ * Determines if the file at `scriptPath` is a Node.js script by checking its shebang.
121
+ * Returns true if:
122
+ * - The shebang interpreter is `node` (e.g., `#!/usr/bin/node`, `#!/usr/bin/env node`)
123
+ * - The file has a `.js`, `.mjs`, or `.cjs` extension (fallback when no shebang)
124
+ */
49
125
  async function isNodeScript(scriptPath) {
50
- const file = await fs.promises.open(scriptPath)
51
- const {buffer} = await file.read({length: 256})
52
- await file.close()
126
+ let file
127
+ try {
128
+ file = await open(scriptPath)
129
+ } catch (err) {
130
+ throw new Error(
131
+ `Failed to read CLI binary at ${scriptPath}: ${err instanceof Error ? err.message : err}`,
132
+ )
133
+ }
134
+
135
+ try {
136
+ const {buffer, bytesRead} = await file.read({length: 256})
137
+ const content = buffer.toString('utf8', 0, bytesRead)
138
+
139
+ if (!content.startsWith('#!')) {
140
+ // No shebang - fall back to extension check (.js, .mjs, .cjs)
141
+ // This is not perfect - but it's better than nothing
142
+ return /\.(m?js|cjs)$/.test(scriptPath)
143
+ }
144
+
145
+ // Extract the interpreter, handling both direct paths and `env` invocations:
146
+ // `#!/usr/bin/node` => `node`
147
+ // `#!/usr/bin/env node` => `node`
148
+ // `#!/usr/bin/env -S node --flags` => `node`
149
+ const firstLine = content.slice(2, content.indexOf('\n')).trim()
150
+ const parts = firstLine.split(/\s+/)
151
+ const interpreter = parts[0].endsWith('/env')
152
+ ? parts.find((part, i) => i > 0 && !part.startsWith('-'))
153
+ : parts[0]
154
+
155
+ return interpreter && (interpreter.endsWith('node') || interpreter.endsWith('node.exe'))
156
+ } finally {
157
+ await file.close()
158
+ }
159
+ }
160
+
161
+ function determineModuleType(binPath, pkgJson) {
162
+ if (binPath.endsWith('.cjs')) {
163
+ return 'commonjs'
164
+ }
165
+
166
+ if (binPath.endsWith('.mjs')) {
167
+ return 'module'
168
+ }
53
169
 
54
- const content = buffer.toString('utf8')
55
- const firstLine = content.trimStart().slice(0, content.indexOf('\n'))
56
- return firstLine.endsWith('node')
170
+ return pkgJson.type === 'module' ? 'module' : 'commonjs'
57
171
  }
@@ -1682,7 +1682,7 @@ const MINIMUM_API_VERSION = "2025-02-19", importDatasetCommand = {
1682
1682
  skipCrossDatasetReferences,
1683
1683
  allowSystemDocuments,
1684
1684
  replaceAssets
1685
- } = flags, operation = getMutationOperation(args.extOptions), client = apiClient(), [file, target] = args.argsWithoutOptions;
1685
+ } = flags, operation = getMutationOperation(args.extOptions), releasesOperation = getReleasesOperation(flags), client = apiClient(), [file, target] = args.argsWithoutOptions;
1686
1686
  if (!file)
1687
1687
  throw new Error(`Source file name and target dataset must be specified ("sanity dataset import ${chalk.bold("[file]")} [dataset]")`);
1688
1688
  const targetDataset = await determineTargetDataset(target, context);
@@ -1742,6 +1742,7 @@ const MINIMUM_API_VERSION = "2025-02-19", importDatasetCommand = {
1742
1742
  client: importClient,
1743
1743
  assetsBase,
1744
1744
  operation,
1745
+ releasesOperation,
1745
1746
  onProgress,
1746
1747
  allowFailingAssets,
1747
1748
  allowAssetsInDifferentDataset,
@@ -1805,6 +1806,13 @@ function getMutationOperation(flags) {
1805
1806
  throw new Error("Cannot use both --replace and --missing");
1806
1807
  return flags.replace ? "createOrReplace" : flags.missing ? "createIfNotExists" : "create";
1807
1808
  }
1809
+ function getReleasesOperation(flags) {
1810
+ const {
1811
+ replace,
1812
+ missing
1813
+ } = flags;
1814
+ return replace ? "replace" : missing ? "ignore" : "fail";
1815
+ }
1808
1816
  function getPercentage(opts) {
1809
1817
  if (!opts.total || typeof opts.current > "u")
1810
1818
  return "";