@sanity/runtime-cli 14.4.0 → 14.5.0

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/README.md CHANGED
@@ -20,7 +20,7 @@ $ npm install -g @sanity/runtime-cli
20
20
  $ sanity-run COMMAND
21
21
  running command...
22
22
  $ sanity-run (--version)
23
- @sanity/runtime-cli/14.4.0 linux-x64 node-v24.14.0
23
+ @sanity/runtime-cli/14.5.0 linux-x64 node-v24.14.0
24
24
  $ sanity-run --help [COMMAND]
25
25
  USAGE
26
26
  $ sanity-run COMMAND
@@ -98,7 +98,7 @@ EXAMPLES
98
98
  $ sanity-run blueprints add function --name my-function --fn-type document-create --fn-type document-update --lang js
99
99
  ```
100
100
 
101
- _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/blueprints/add.ts)_
101
+ _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/blueprints/add.ts)_
102
102
 
103
103
  ## `sanity-run blueprints config`
104
104
 
@@ -133,7 +133,7 @@ EXAMPLES
133
133
  $ sanity-run blueprints config --edit --project-id <projectId> --stack <name-or-id>
134
134
  ```
135
135
 
136
- _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/blueprints/config.ts)_
136
+ _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/blueprints/config.ts)_
137
137
 
138
138
  ## `sanity-run blueprints deploy`
139
139
 
@@ -158,15 +158,19 @@ DESCRIPTION
158
158
 
159
159
  Use --no-wait to queue the deployment and return immediately without waiting for completion.
160
160
 
161
+ Use --fn-installer to force which package manager to use when deploying functions.
162
+
161
163
  Set SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.
162
164
 
163
165
  EXAMPLES
164
166
  $ sanity-run blueprints deploy
165
167
 
166
168
  $ sanity-run blueprints deploy --no-wait
169
+
170
+ $ sanity-run blueprints deploy --fn-installer npm
167
171
  ```
168
172
 
169
- _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/blueprints/deploy.ts)_
173
+ _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/blueprints/deploy.ts)_
170
174
 
171
175
  ## `sanity-run blueprints destroy`
172
176
 
@@ -198,7 +202,7 @@ EXAMPLES
198
202
  $ sanity-run blueprints destroy --stack <name-or-id> --project-id <projectId> --force --no-wait
199
203
  ```
200
204
 
201
- _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/blueprints/destroy.ts)_
205
+ _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/blueprints/destroy.ts)_
202
206
 
203
207
  ## `sanity-run blueprints doctor`
204
208
 
@@ -224,7 +228,7 @@ DESCRIPTION
224
228
  issues.
225
229
  ```
226
230
 
227
- _See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/blueprints/doctor.ts)_
231
+ _See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/blueprints/doctor.ts)_
228
232
 
229
233
  ## `sanity-run blueprints info`
230
234
 
@@ -254,7 +258,7 @@ EXAMPLES
254
258
  $ sanity-run blueprints info --stack <name-or-id>
255
259
  ```
256
260
 
257
- _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/blueprints/info.ts)_
261
+ _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/blueprints/info.ts)_
258
262
 
259
263
  ## `sanity-run blueprints init [DIR]`
260
264
 
@@ -304,7 +308,7 @@ EXAMPLES
304
308
  $ sanity-run blueprints init --blueprint-type <json|js|ts> --stack-name <stackName>
305
309
  ```
306
310
 
307
- _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/blueprints/init.ts)_
311
+ _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/blueprints/init.ts)_
308
312
 
309
313
  ## `sanity-run blueprints logs`
310
314
 
@@ -333,7 +337,7 @@ EXAMPLES
333
337
  $ sanity-run blueprints logs --watch
334
338
  ```
335
339
 
336
- _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/blueprints/logs.ts)_
340
+ _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/blueprints/logs.ts)_
337
341
 
338
342
  ## `sanity-run blueprints plan`
339
343
 
@@ -359,7 +363,7 @@ EXAMPLES
359
363
  $ sanity-run blueprints plan
360
364
  ```
361
365
 
362
- _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/blueprints/plan.ts)_
366
+ _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/blueprints/plan.ts)_
363
367
 
364
368
  ## `sanity-run blueprints stacks`
365
369
 
@@ -388,7 +392,7 @@ EXAMPLES
388
392
  $ sanity-run blueprints stacks --organization-id <organizationId>
389
393
  ```
390
394
 
391
- _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/blueprints/stacks.ts)_
395
+ _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/blueprints/stacks.ts)_
392
396
 
393
397
  ## `sanity-run functions add`
394
398
 
@@ -437,7 +441,7 @@ EXAMPLES
437
441
  $ sanity-run functions add --name my-function --type document-create --type document-update --lang js
438
442
  ```
439
443
 
440
- _See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/functions/add.ts)_
444
+ _See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/functions/add.ts)_
441
445
 
442
446
  ## `sanity-run functions dev`
443
447
 
@@ -471,7 +475,7 @@ EXAMPLES
471
475
  $ sanity-run functions dev --timeout 60
472
476
  ```
473
477
 
474
- _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/functions/dev.ts)_
478
+ _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/functions/dev.ts)_
475
479
 
476
480
  ## `sanity-run functions env add NAME KEY VALUE`
477
481
 
@@ -498,7 +502,7 @@ EXAMPLES
498
502
  $ sanity-run functions env add MyFunction API_URL https://api.example.com/
499
503
  ```
500
504
 
501
- _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/functions/env/add.ts)_
505
+ _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/functions/env/add.ts)_
502
506
 
503
507
  ## `sanity-run functions env list NAME`
504
508
 
@@ -522,7 +526,7 @@ EXAMPLES
522
526
  $ sanity-run functions env list MyFunction
523
527
  ```
524
528
 
525
- _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/functions/env/list.ts)_
529
+ _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/functions/env/list.ts)_
526
530
 
527
531
  ## `sanity-run functions env remove NAME KEY`
528
532
 
@@ -548,7 +552,7 @@ EXAMPLES
548
552
  $ sanity-run functions env remove MyFunction API_URL
549
553
  ```
550
554
 
551
- _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/functions/env/remove.ts)_
555
+ _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/functions/env/remove.ts)_
552
556
 
553
557
  ## `sanity-run functions logs [NAME]`
554
558
 
@@ -588,7 +592,7 @@ EXAMPLES
588
592
  $ sanity-run functions logs <name> --delete
589
593
  ```
590
594
 
591
- _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/functions/logs.ts)_
595
+ _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/functions/logs.ts)_
592
596
 
593
597
  ## `sanity-run functions test [NAME]`
594
598
 
@@ -642,7 +646,7 @@ EXAMPLES
642
646
  $ sanity-run functions test <name> --event update --data-before '{ "title": "before" }' --data-after '{ "title": "after" }'
643
647
  ```
644
648
 
645
- _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v14.4.0/src/commands/functions/test.ts)_
649
+ _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v14.5.0/src/commands/functions/test.ts)_
646
650
 
647
651
  ## `sanity-run help [COMMAND]`
648
652
 
@@ -1,12 +1,13 @@
1
1
  import AdmZip from 'adm-zip';
2
2
  import type { Logger } from '../../utils/logger.js';
3
- import type { AuthParams, CollectionFunction, FunctionResource } from '../../utils/types.js';
3
+ import type { AuthParams, CollectionFunction, FunctionResource, InstallerType } from '../../utils/types.js';
4
4
  export declare const ASSET_CHECK_URL: string;
5
5
  export declare const ASSET_STASH_URL: string;
6
- export declare function stashAsset({ resource, auth, logger, }: {
6
+ export declare function stashAsset({ resource, auth, logger, installer, }: {
7
7
  resource: FunctionResource | CollectionFunction;
8
8
  auth: AuthParams;
9
9
  logger: ReturnType<typeof Logger>;
10
+ installer?: InstallerType;
10
11
  }): Promise<{
11
12
  success: boolean;
12
13
  assetId?: string;
@@ -14,14 +14,14 @@ const { apiUrl } = config;
14
14
  const ASSETS_URL = `${apiUrl}vX/blueprints/assets`;
15
15
  export const ASSET_CHECK_URL = `${ASSETS_URL}/check`;
16
16
  export const ASSET_STASH_URL = `${ASSETS_URL}/stash`;
17
- export async function stashAsset({ resource, auth, logger, }) {
17
+ export async function stashAsset({ resource, auth, logger, installer, }) {
18
18
  const isCollection = isLocalFunctionCollection(resource);
19
19
  const functions = isCollection ? resource.functions : [resource];
20
20
  const combinedAsset = [];
21
21
  const prepareErrors = [];
22
22
  try {
23
23
  for (const func of functions) {
24
- const prepResult = await prepareAsset({ resource: func });
24
+ const prepResult = await prepareAsset({ resource: func }, { installer });
25
25
  if (prepResult.success && prepResult.outputPath && prepResult.cleanup) {
26
26
  combinedAsset.push({
27
27
  name: func.name,
@@ -175,7 +175,18 @@ export function hashBuffer(buffer) {
175
175
  }
176
176
  export async function pathToZip(path) {
177
177
  const stats = await fs.promises.stat(path);
178
- const zip = new AdmZip();
178
+ const zip = new AdmZip(undefined, {
179
+ fs: {
180
+ ...fs,
181
+ // @ts-expect-error monkey patching to return matching mtime for builds
182
+ statSync(path, options) {
183
+ const s = fs.statSync(path, options);
184
+ if (s)
185
+ s.mtime = new Date(0);
186
+ return s;
187
+ },
188
+ },
189
+ });
179
190
  if (stats.isDirectory())
180
191
  zip.addLocalFolder(path);
181
192
  else
@@ -8,6 +8,7 @@ import { LocalBlueprintCommand } from '../../baseCommands.js';
8
8
  import { FUNCTION_TYPES } from '../../constants.js';
9
9
  import { functionAddCore } from '../../cores/functions/index.js';
10
10
  import { Logger } from '../../utils/logger.js';
11
+ import { INSTALLER_OPTIONS } from '../../utils/types.js';
11
12
  // import {warn} from '../../utils/display/presenters.js'
12
13
  export default class AddCommand extends LocalBlueprintCommand {
13
14
  // static state = 'deprecated'
@@ -69,7 +70,7 @@ After adding a function, use 'functions dev' to test locally, then 'blueprints d
69
70
  'fn-installer': Flags.string({
70
71
  description: 'Which package manager to use when installing the @sanity/functions helpers',
71
72
  aliases: ['function-installer', 'installer'],
72
- options: ['skip', 'npm', 'pnpm', 'yarn'],
73
+ options: ['skip', ...INSTALLER_OPTIONS],
73
74
  }),
74
75
  install: Flags.boolean({
75
76
  description: 'Shortcut for --fn-installer npm',
@@ -5,6 +5,7 @@ export default class DeployCommand extends DeployedStackCommand<typeof DeployCom
5
5
  static examples: string[];
6
6
  static flags: {
7
7
  stack: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'fn-installer': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
10
  'new-stack-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  };
@@ -2,6 +2,7 @@ import { Flags } from '@oclif/core';
2
2
  import { DeployedStackCommand, stackFlag, unhide } from '../../baseCommands.js';
3
3
  import { blueprintDeployCore } from '../../cores/blueprints/deploy.js';
4
4
  import { Logger } from '../../utils/logger.js';
5
+ import { INSTALLER_OPTIONS } from '../../utils/types.js';
5
6
  export default class DeployCommand extends DeployedStackCommand {
6
7
  static summary = 'Deploy the local Blueprint to the remote Stack';
7
8
  static description = `Pushes your local Blueprint configuration to the remote Stack; provisioning, updating, or destroying resources as needed. This is the primary command for applying infrastructure changes.
@@ -10,13 +11,22 @@ Before deploying, run 'blueprints plan' to preview changes. After deployment, us
10
11
 
11
12
  Use --no-wait to queue the deployment and return immediately without waiting for completion.
12
13
 
14
+ Use --fn-installer to force which package manager to use when deploying functions.
15
+
13
16
  Set SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.`;
14
17
  static examples = [
15
18
  '<%= config.bin %> <%= command.id %>',
16
19
  '<%= config.bin %> <%= command.id %> --no-wait',
20
+ '<%= config.bin %> <%= command.id %> --fn-installer npm',
17
21
  ];
18
22
  static flags = {
19
23
  stack: unhide(stackFlag),
24
+ 'fn-installer': Flags.string({
25
+ description: 'Which package manager to use when installing Function dependencies',
26
+ aliases: ['function-installer', 'installer'],
27
+ options: [...INSTALLER_OPTIONS],
28
+ hidden: true,
29
+ }),
20
30
  'no-wait': Flags.boolean({
21
31
  description: 'Do not wait for Stack deployment to complete',
22
32
  default: false,
@@ -3,6 +3,7 @@ import { LocalBlueprintCommand } from '../../baseCommands.js';
3
3
  import { FUNCTION_TYPES } from '../../constants.js';
4
4
  import { functionAddCore } from '../../cores/functions/index.js';
5
5
  import { Logger } from '../../utils/logger.js';
6
+ import { INSTALLER_OPTIONS } from '../../utils/types.js';
6
7
  export default class AddCommand extends LocalBlueprintCommand {
7
8
  static summary = 'Add a Function to your Blueprint';
8
9
  static description = `Scaffolds a new Function in the functions/ folder and templates a resource for your Blueprint manifest.
@@ -51,7 +52,7 @@ After adding, use 'functions dev' to test locally, then 'blueprints deploy' to p
51
52
  }),
52
53
  installer: Flags.string({
53
54
  description: 'How to install the @sanity/functions helpers',
54
- options: ['skip', 'npm', 'pnpm', 'yarn'],
55
+ options: ['skip', ...INSTALLER_OPTIONS],
55
56
  }),
56
57
  install: Flags.boolean({
57
58
  description: 'Shortcut for --fn-installer npm',
@@ -6,6 +6,7 @@ export default class BuildCommand extends LocalBlueprintCommand<typeof BuildComm
6
6
  name: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
7
  };
8
8
  static flags: {
9
+ 'fn-installer': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  'out-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  };
11
12
  run(): Promise<void>;
@@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core';
2
2
  import { LocalBlueprintCommand } from '../../baseCommands.js';
3
3
  import { functionBuildCore } from '../../cores/functions/build.js';
4
4
  import { Logger } from '../../utils/logger.js';
5
+ import { INSTALLER_OPTIONS } from '../../utils/types.js';
5
6
  export default class BuildCommand extends LocalBlueprintCommand {
6
7
  static summary = 'Build Sanity Function(s) to zip archives';
7
8
  static hidden = true;
@@ -9,6 +10,12 @@ export default class BuildCommand extends LocalBlueprintCommand {
9
10
  name: Args.string({ description: 'The name of the Sanity Function', required: false }),
10
11
  };
11
12
  static flags = {
13
+ 'fn-installer': Flags.string({
14
+ description: 'Which package manager to use when building Functions',
15
+ aliases: ['function-installer', 'installer'],
16
+ options: [...INSTALLER_OPTIONS],
17
+ hidden: true,
18
+ }),
12
19
  'out-dir': Flags.string({ char: 'o', description: 'Output directory for zip files' }),
13
20
  };
14
21
  async run() {
@@ -9,6 +9,7 @@ export interface BlueprintDeployOptions extends CoreConfig {
9
9
  deployedStack: Stack;
10
10
  blueprint: ReadBlueprintResult;
11
11
  flags: {
12
+ 'fn-installer'?: string;
12
13
  'no-wait'?: boolean;
13
14
  'new-stack-name'?: string;
14
15
  verbose?: boolean;
@@ -14,6 +14,7 @@ export async function blueprintDeployCore(options) {
14
14
  const { verbose } = flags;
15
15
  const noWait = flags['no-wait'] || false;
16
16
  const stackName = flags['new-stack-name'] ?? deployedStack.name;
17
+ const installer = flags['fn-installer'] || undefined;
17
18
  log(`Deploying "${stackName}" ${niceId(deployedStack.id)}...`);
18
19
  try {
19
20
  const { ok: checkOk, stack: currentStack } = await getStack({ stackId, auth, logger: log });
@@ -44,7 +45,7 @@ export async function blueprintDeployCore(options) {
44
45
  let result;
45
46
  try {
46
47
  result = await Promise.race([
47
- stashAsset({ resource, auth, logger: log }),
48
+ stashAsset({ resource, auth, logger: log, installer }),
48
49
  new Promise((_, reject) => {
49
50
  assetTimeoutTimer = setTimeout(() => {
50
51
  reject(new Error(`Processing ${resource.name} timed out after ${assetTimeoutS}s`));
@@ -6,6 +6,7 @@ export interface FunctionBuildOptions extends CoreConfig {
6
6
  name: string | undefined;
7
7
  };
8
8
  flags: {
9
+ 'fn-installer'?: string;
9
10
  'out-dir'?: string;
10
11
  };
11
12
  }
@@ -20,13 +20,14 @@ export async function functionBuildCore(options) {
20
20
  else {
21
21
  functions = allFunctions;
22
22
  }
23
+ const installer = flags['fn-installer'] || undefined;
23
24
  const outDir = flags['out-dir'] || path.join(path.dirname(blueprint.fileInfo.blueprintFilePath), '.build');
24
25
  await fs.promises.mkdir(outDir, { recursive: true });
25
26
  const errors = [];
26
27
  const built = [];
27
28
  for (const resource of functions) {
28
29
  const spinner = log.ora(`Building ${resource.name}...`).start();
29
- const prepResult = await prepareAsset({ resource });
30
+ const prepResult = await prepareAsset({ resource }, { installer });
30
31
  if (!prepResult.success || !prepResult.outputPath) {
31
32
  spinner.fail(`Failed to build ${resource.name}`);
32
33
  errors.push({ name: resource.name, error: prepResult.error ?? 'Unknown error' });
@@ -19,7 +19,7 @@ const RESOURCE_CATEGORIES = {
19
19
  },
20
20
  },
21
21
  [SANITY_ACCESS_ROBOT]: {
22
- label: 'Robots',
22
+ label: 'Robot Tokens',
23
23
  displayNameAttribute: 'label',
24
24
  formatDetails(res) {
25
25
  return isRobotResource(res) ? arrayifyRobot(res) : [];
@@ -1,6 +1,12 @@
1
1
  import type AdmZip from 'adm-zip';
2
+ import type { FunctionResource } from '../types.js';
2
3
  /**
3
- * Scan zip entries of dependencies for native modules.
4
+ * Recurse node_modules for native modules.
4
5
  * Native modules built on one platform may not work on another (e.g., macOS vs Linux)
5
6
  */
6
7
  export declare const detectNativeModules: (zip: AdmZip) => string[];
8
+ /**
9
+ * Finds the dependencies of the function and compares to the workspace root to see if any native modules are included.
10
+ * Due to how PNPM with workspaces symlinks dependencies, we have to check the workspace root for native modules prior to the bundle step.
11
+ */
12
+ export declare const detectNativeModulesForBundle: (resource: FunctionResource) => string[];
@@ -1,9 +1,13 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { cwd } from 'node:process';
4
+ import { createPnpmRequire } from '../pnpm.js';
1
5
  /**
2
6
  * Patterns to identify native modules based on common file types and build configurations.
3
7
  */
4
8
  const KNOWN_MODULE_PATTERNS = [/binding\.gyp$/, /\.node$/];
5
9
  /**
6
- * Scan zip entries of dependencies for native modules.
10
+ * Recurse node_modules for native modules.
7
11
  * Native modules built on one platform may not work on another (e.g., macOS vs Linux)
8
12
  */
9
13
  export const detectNativeModules = (zip) => {
@@ -24,3 +28,67 @@ export const detectNativeModules = (zip) => {
24
28
  throw new Error(`Failed to scan zip for native modules: ${message}`);
25
29
  }
26
30
  };
31
+ /**
32
+ * Recursively checks a directory for files that match known native module patterns.
33
+ */
34
+ const hasNativeFiles = (directory) => {
35
+ for (const name of readdirSync(directory)) {
36
+ // don't recurse back into node_modules.
37
+ if (name === 'node_modules')
38
+ continue;
39
+ const fullPath = path.join(directory, name);
40
+ if (statSync(fullPath).isDirectory()) {
41
+ if (hasNativeFiles(fullPath))
42
+ return true;
43
+ }
44
+ else if (KNOWN_MODULE_PATTERNS.some((pattern) => pattern.test(name))) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ };
50
+ /**
51
+ * Finds the dependencies of the function and compares to the workspace root to see if any native modules are included.
52
+ * Due to how PNPM with workspaces symlinks dependencies, we have to check the workspace root for native modules prior to the bundle step.
53
+ */
54
+ export const detectNativeModulesForBundle = (resource) => {
55
+ if (!resource.src)
56
+ throw new Error('Resource src is required');
57
+ const hasWorkspace = existsSync(path.join(cwd(), 'pnpm-workspace.yaml'));
58
+ if (!hasWorkspace) {
59
+ return [];
60
+ }
61
+ const sourcePath = path.resolve(cwd(), resource.src);
62
+ const stats = statSync(sourcePath);
63
+ const entryDir = stats.isFile() ? path.dirname(sourcePath) : sourcePath;
64
+ const hasPackageJson = existsSync(path.join(entryDir, 'package.json'));
65
+ if (!hasPackageJson) {
66
+ throw new Error('pnpm workspace detected but no package.json found in function src');
67
+ }
68
+ // gets package.json dependencies of the function. We use these function deps to compare against the workspace root modules.
69
+ const pkgJsonPath = path.join(entryDir, 'package.json');
70
+ // a function should always have deps, but this will just be a little safer.
71
+ const functionDeps = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')).dependencies ?? {};
72
+ const workspaceModules = createPnpmRequire(cwd());
73
+ const nativeModules = [];
74
+ for (const depName of Object.keys(functionDeps)) {
75
+ try {
76
+ const resolved = workspaceModules.resolve(depName);
77
+ let currentDir = path.dirname(resolved);
78
+ while (currentDir !== cwd() && currentDir !== path.parse(currentDir).root) {
79
+ // look for node_module package.json to locate top level
80
+ if (existsSync(path.join(currentDir, 'package.json'))) {
81
+ if (hasNativeFiles(currentDir)) {
82
+ nativeModules.push(depName);
83
+ break;
84
+ }
85
+ }
86
+ currentDir = path.dirname(currentDir);
87
+ }
88
+ }
89
+ catch (_e) {
90
+ // If we fail to resolve, just skip
91
+ }
92
+ }
93
+ return nativeModules;
94
+ };
@@ -1,6 +1,8 @@
1
- import type { CollectionFunction, FunctionResource } from '../types.js';
1
+ import type { CollectionFunction, FunctionResource, InstallerType } from '../types.js';
2
2
  export declare function prepareAsset({ resource, }: {
3
3
  resource: FunctionResource | CollectionFunction;
4
+ }, { installer }?: {
5
+ installer?: InstallerType;
4
6
  }): Promise<{
5
7
  success: boolean;
6
8
  outputPath?: string;
@@ -1,12 +1,14 @@
1
+ import { rm, stat } from 'node:fs/promises';
1
2
  import path from 'node:path';
2
3
  import { cwd } from 'node:process';
3
4
  import { MAX_ASSET_SIZE } from '../../constants.js';
4
5
  import { transpileFunction } from '../transpile/transpile-function.js';
6
+ import { detectNativeModulesForBundle } from './detect-native-modules.js';
5
7
  import { getFolderSize } from './getFolderSize.js';
6
8
  import { resolveResourceDependencies } from './resolve-dependencies.js';
7
9
  import { shouldAutoResolveDependencies } from './should-auto-resolve-deps.js';
8
10
  import { shouldTranspileFunction } from './should-transpile.js';
9
- export async function prepareAsset({ resource, }) {
11
+ export async function prepareAsset({ resource, }, { installer } = {}) {
10
12
  if (!resource.src)
11
13
  throw new Error('Resource src is required');
12
14
  let functionPath = path.join(cwd(), resource.src);
@@ -15,7 +17,13 @@ export async function prepareAsset({ resource, }) {
15
17
  const shouldTranspile = await shouldTranspileFunction(resource);
16
18
  if (shouldTranspile) {
17
19
  try {
18
- const result = await transpileFunction(resource);
20
+ const hasNativeModules = detectNativeModulesForBundle(resource);
21
+ if (hasNativeModules.length) {
22
+ const errorMsg = `Native modules detected:\n${hasNativeModules.join('\n')}\n\n` +
23
+ `Please replace with JavaScript alternatives.`;
24
+ throw new Error(errorMsg);
25
+ }
26
+ const result = await transpileFunction(resource, { installer });
19
27
  functionPath = result.outputDir;
20
28
  cleanup = result.cleanup;
21
29
  wasBundled = result.bundled;
@@ -26,7 +34,8 @@ export async function prepareAsset({ resource, }) {
26
34
  }
27
35
  const shouldResolveDependencies = await shouldAutoResolveDependencies(resource);
28
36
  if (shouldResolveDependencies && !wasBundled) {
29
- await resolveResourceDependencies(resource, shouldTranspile);
37
+ await resolveResourceDependencies(resource, { transpiled: shouldTranspile, installer });
38
+ await removeArcAutoInstallPackageJson(functionPath);
30
39
  }
31
40
  try {
32
41
  const size = getFolderSize(functionPath);
@@ -39,3 +48,19 @@ export async function prepareAsset({ resource, }) {
39
48
  return { success: false, error: err instanceof Error ? err.message : `${err}` };
40
49
  }
41
50
  }
51
+ /**
52
+ * Remove file that contains the current date to prevent the hash from changing in the final asset.
53
+ * @param functionPath The path to the built function
54
+ */
55
+ async function removeArcAutoInstallPackageJson(functionPath) {
56
+ const arcAutoInstallPackageJson = path.join(functionPath, 'node_modules', '_arc-autoinstall', 'package.json');
57
+ try {
58
+ const fileStat = await stat(arcAutoInstallPackageJson);
59
+ if (fileStat.isFile()) {
60
+ await rm(arcAutoInstallPackageJson);
61
+ }
62
+ }
63
+ catch {
64
+ // ignore errors
65
+ }
66
+ }
@@ -1,2 +1,5 @@
1
- import type { FunctionResource } from '../types.js';
2
- export declare function resolveResourceDependencies(resource: FunctionResource, transpiled: boolean): Promise<void>;
1
+ import type { FunctionResource, InstallerType } from '../types.js';
2
+ export declare function resolveResourceDependencies(resource: FunctionResource, { transpiled, installer, }: {
3
+ transpiled: boolean;
4
+ installer?: InstallerType;
5
+ }): Promise<void>;
@@ -3,25 +3,22 @@ import { join } from 'node:path';
3
3
  import hydrate from '@architect/hydrate';
4
4
  import inventory from '@architect/inventory';
5
5
  import { convertResourceToArcFormat } from './resource-to-arc.js';
6
- export async function resolveResourceDependencies(resource, transpiled) {
6
+ export async function resolveResourceDependencies(resource, { transpiled, installer, }) {
7
7
  const rawArc = await convertResourceToArcFormat(resource, transpiled);
8
8
  const inv = await inventory({ rawArc });
9
9
  const cwd = inv.inv._project.cwd;
10
- const installOptions = {
11
- inventory: inv,
12
- hydrateShared: false,
13
- quiet: true,
14
- pnpm: false,
15
- yarn: false,
16
- };
17
- if (existsSync(join(cwd, 'pnpm-lock.yaml'))) {
18
- installOptions.pnpm = true;
10
+ let installType = 'npm';
11
+ if (installer) {
12
+ installType = installer;
13
+ }
14
+ else if (existsSync(join(cwd, 'pnpm-lock.yaml'))) {
15
+ installType = 'pnpm';
19
16
  }
20
17
  else if (existsSync(join(cwd, 'yarn.lock'))) {
21
- installOptions.yarn = true;
18
+ installType = 'yarn';
22
19
  }
23
20
  try {
24
- await hydrate.install(installOptions);
21
+ await hydrate.install(toInstallerOptions(inv, installType));
25
22
  }
26
23
  catch (err) {
27
24
  // This is a temporary fix.
@@ -32,3 +29,13 @@ export async function resolveResourceDependencies(resource, transpiled) {
32
29
  }
33
30
  }
34
31
  }
32
+ function toInstallerOptions(inv, type) {
33
+ return {
34
+ inventory: inv,
35
+ hydrateShared: false,
36
+ quiet: true,
37
+ npm: type === 'npm',
38
+ pnpm: type === 'pnpm',
39
+ yarn: type === 'yarn',
40
+ };
41
+ }
@@ -84,7 +84,7 @@ export default async function invoke(resource, payload, context, options) {
84
84
  }
85
85
  const shouldResolveDependencies = await shouldAutoResolveDependencies(resource);
86
86
  if (shouldResolveDependencies && existingPackageJson) {
87
- await resolveResourceDependencies(resource, shouldTranspile);
87
+ await resolveResourceDependencies(resource, { transpiled: shouldTranspile });
88
88
  }
89
89
  if (!existingPackageJson) {
90
90
  createTempPackageJson(functionPath);
@@ -0,0 +1 @@
1
+ export declare const createPnpmRequire: (workspaceRoot: string) => NodeJS.Require;
@@ -0,0 +1,9 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ export const createPnpmRequire = (workspaceRoot) => {
4
+ const pnpmModulesPath = path.join(workspaceRoot, 'node_modules', '.pnpm', 'node_modules');
5
+ // createRequire gives us Node's full module resolution algorithm rooted at
6
+ // the pnpm virtual store — it reads package.json exports/main and returns
7
+ // the absolute path to the actual entry file, not just the directory.
8
+ return createRequire(path.join(pnpmModulesPath, '_virtual.js'));
9
+ };
@@ -1,5 +1,7 @@
1
- import type { FunctionResource } from '../types.js';
2
- export declare function transpileFunction(resource: FunctionResource): Promise<{
1
+ import type { FunctionResource, InstallerType } from '../types.js';
2
+ export declare function transpileFunction(resource: FunctionResource, { installer }?: {
3
+ installer?: InstallerType;
4
+ }): Promise<{
3
5
  type: string;
4
6
  outputDir: string;
5
7
  warnings: string[];
@@ -1,6 +1,5 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
3
- import { createRequire } from 'node:module';
4
3
  import path from 'node:path';
5
4
  import { performance } from 'node:perf_hooks';
6
5
  import { cwd } from 'node:process';
@@ -8,9 +7,10 @@ import * as find from 'empathic/find';
8
7
  import { build as viteBuild } from 'vite';
9
8
  import tsConfigPaths from 'vite-tsconfig-paths';
10
9
  import { findFunctionEntryPoint } from '../functions/find-entry-point.js';
10
+ import { createPnpmRequire } from '../pnpm.js';
11
11
  import { cleanupSourceMaps } from './cleanup-source-maps.js';
12
12
  import { verifyHandler } from './verify-handler.js';
13
- export async function transpileFunction(resource) {
13
+ export async function transpileFunction(resource, { installer } = {}) {
14
14
  if (!resource.src)
15
15
  throw new Error('Resource src is required');
16
16
  if (!resource.name)
@@ -35,7 +35,8 @@ export async function transpileFunction(resource) {
35
35
  }
36
36
  try {
37
37
  const viteStart = performance.now();
38
- const bundle = existsSync(path.join(cwd(), 'pnpm-workspace.yaml'));
38
+ // Always bundle the code if pnpm usage is detected or specified by the user
39
+ const bundle = existsSync(path.join(cwd(), 'pnpm-workspace.yaml')) || installer === 'pnpm';
39
40
  const result = await viteBuild({
40
41
  root: fnRootDir,
41
42
  logLevel: 'silent',
@@ -140,11 +141,7 @@ function logCleanupFailure(err) {
140
141
  * which isn't reachable via standard Node resolution.
141
142
  */
142
143
  function pnpmResolvePlugin(workspaceRoot) {
143
- const pnpmModulesPath = path.join(workspaceRoot, 'node_modules', '.pnpm', 'node_modules');
144
- // createRequire gives us Node's full module resolution algorithm rooted at
145
- // the pnpm virtual store — it reads package.json exports/main and returns
146
- // the absolute path to the actual entry file, not just the directory.
147
- const pnpmRequire = createRequire(path.join(pnpmModulesPath, '_virtual.js'));
144
+ const pnpmRequire = createPnpmRequire(workspaceRoot);
148
145
  return {
149
146
  name: 'pnpm-resolve',
150
147
  async resolveId(source, importer, options) {
@@ -255,4 +255,6 @@ export declare interface FetchConfig {
255
255
  apiHost?: string;
256
256
  apiVersion?: string;
257
257
  }
258
+ export declare const INSTALLER_OPTIONS: readonly ["npm", "pnpm", "yarn"];
259
+ export type InstallerType = 'npm' | 'pnpm' | 'yarn';
258
260
  export {};
@@ -57,3 +57,4 @@ export var BlueprintParserErrorType;
57
57
  BlueprintParserErrorType["InvalidInput"] = "invalid_input";
58
58
  BlueprintParserErrorType["MissingParameter"] = "missing_parameter";
59
59
  })(BlueprintParserErrorType || (BlueprintParserErrorType = {}));
60
+ export const INSTALLER_OPTIONS = ['npm', 'pnpm', 'yarn'];
@@ -319,10 +319,11 @@
319
319
  "blueprints:deploy": {
320
320
  "aliases": [],
321
321
  "args": {},
322
- "description": "Pushes your local Blueprint configuration to the remote Stack; provisioning, updating, or destroying resources as needed. This is the primary command for applying infrastructure changes.\n\nBefore deploying, run 'blueprints plan' to preview changes. After deployment, use 'blueprints info' to verify Stack status or 'blueprints logs' to monitor activity.\n\nUse --no-wait to queue the deployment and return immediately without waiting for completion.\n\nSet SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.",
322
+ "description": "Pushes your local Blueprint configuration to the remote Stack; provisioning, updating, or destroying resources as needed. This is the primary command for applying infrastructure changes.\n\nBefore deploying, run 'blueprints plan' to preview changes. After deployment, use 'blueprints info' to verify Stack status or 'blueprints logs' to monitor activity.\n\nUse --no-wait to queue the deployment and return immediately without waiting for completion.\n\nUse --fn-installer to force which package manager to use when deploying functions.\n\nSet SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.",
323
323
  "examples": [
324
324
  "<%= config.bin %> <%= command.id %>",
325
- "<%= config.bin %> <%= command.id %> --no-wait"
325
+ "<%= config.bin %> <%= command.id %> --no-wait",
326
+ "<%= config.bin %> <%= command.id %> --fn-installer npm"
326
327
  ],
327
328
  "flags": {
328
329
  "json": {
@@ -374,6 +375,23 @@
374
375
  "multiple": false,
375
376
  "type": "option"
376
377
  },
378
+ "fn-installer": {
379
+ "aliases": [
380
+ "function-installer",
381
+ "installer"
382
+ ],
383
+ "description": "Which package manager to use when installing Function dependencies",
384
+ "hidden": true,
385
+ "name": "fn-installer",
386
+ "hasDynamicHelp": false,
387
+ "multiple": false,
388
+ "options": [
389
+ "npm",
390
+ "pnpm",
391
+ "yarn"
392
+ ],
393
+ "type": "option"
394
+ },
377
395
  "no-wait": {
378
396
  "description": "Do not wait for Stack deployment to complete",
379
397
  "name": "no-wait",
@@ -1336,6 +1354,23 @@
1336
1354
  "allowNo": false,
1337
1355
  "type": "boolean"
1338
1356
  },
1357
+ "fn-installer": {
1358
+ "aliases": [
1359
+ "function-installer",
1360
+ "installer"
1361
+ ],
1362
+ "description": "Which package manager to use when building Functions",
1363
+ "hidden": true,
1364
+ "name": "fn-installer",
1365
+ "hasDynamicHelp": false,
1366
+ "multiple": false,
1367
+ "options": [
1368
+ "npm",
1369
+ "pnpm",
1370
+ "yarn"
1371
+ ],
1372
+ "type": "option"
1373
+ },
1339
1374
  "out-dir": {
1340
1375
  "char": "o",
1341
1376
  "description": "Output directory for zip files",
@@ -2163,5 +2198,5 @@
2163
2198
  ]
2164
2199
  }
2165
2200
  },
2166
- "version": "14.4.0"
2201
+ "version": "14.5.0"
2167
2202
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanity/runtime-cli",
3
3
  "description": "Sanity's Runtime CLI for Blueprints and Functions",
4
- "version": "14.4.0",
4
+ "version": "14.5.0",
5
5
  "author": "Sanity Runtime Team",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -90,9 +90,10 @@
90
90
  "lint:fix": "npm run lint:write",
91
91
  "prepare": "npm run build",
92
92
  "postpack": "shx rm -f oclif.manifest.json",
93
- "test": "vitest run && npm run lint",
94
- "test:depmgmt": "vitest run --config ./test-depmgmt/vitest.config.ts",
93
+ "test": "npm run test:vitest && npm run lint",
95
94
  "test:api": "cd test/api-types && npm it",
95
+ "test:depmgmt": "vitest run --config ./test-depmgmt/vitest.config.ts",
96
+ "test:vitest": "vitest run",
96
97
  "typecheck": "tsc --project tsconfig.typecheck.json",
97
98
  "watch": "tsc --watch"
98
99
  },