@legendapp/spark 0.0.1-next.3 → 0.0.1-next.4

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.
Files changed (83) hide show
  1. package/camera.ts +13 -0
  2. package/package.json +21 -11
  3. package/patches/camera/react-native-nitro-image.patch +320 -0
  4. package/patches/camera/react-native-nitro-modules.patch +50 -0
  5. package/patches/camera/react-native-vision-camera.patch +2662 -0
  6. package/patches/camera/upstream.json +22 -0
  7. package/patches/dependencies.json +8 -0
  8. package/patches/react-native-enriched-markdown@1.0.2.patch +8684 -0
  9. package/postinstall.cjs +1 -0
  10. package/ui-swipe.ts +1 -0
  11. package/vendor/node_modules/@legendapp/spark-ai/package.json +3 -3
  12. package/vendor/node_modules/@legendapp/spark-audio/package.json +2 -2
  13. package/vendor/node_modules/@legendapp/spark-auth-session/package.json +3 -3
  14. package/vendor/node_modules/@legendapp/spark-cli/dist/build.js +62 -11
  15. package/vendor/node_modules/@legendapp/spark-cli/dist/cli-arguments.js +2 -1
  16. package/vendor/node_modules/@legendapp/spark-cli/dist/commands.js +3 -2
  17. package/vendor/node_modules/@legendapp/spark-cli/dist/create.js +6 -3
  18. package/vendor/node_modules/@legendapp/spark-cli/dist/credentials.js +41 -26
  19. package/vendor/node_modules/@legendapp/spark-cli/dist/dependency-pins.js +132 -0
  20. package/vendor/node_modules/@legendapp/spark-cli/dist/dev-command.js +1 -1
  21. package/vendor/node_modules/@legendapp/spark-cli/dist/dev.js +1 -1
  22. package/vendor/node_modules/@legendapp/spark-cli/dist/errors.js +100 -0
  23. package/vendor/node_modules/@legendapp/spark-cli/dist/examples.js +1 -1
  24. package/vendor/node_modules/@legendapp/spark-cli/dist/expo-native.cjs +5 -1
  25. package/vendor/node_modules/@legendapp/spark-cli/dist/helpers.js +1 -1
  26. package/vendor/node_modules/@legendapp/spark-cli/dist/index.js +43 -7
  27. package/vendor/node_modules/@legendapp/spark-cli/dist/json.js +16 -0
  28. package/vendor/node_modules/@legendapp/spark-cli/dist/native-patch-preflight.js +3 -1
  29. package/vendor/node_modules/@legendapp/spark-cli/dist/process.js +7 -0
  30. package/vendor/node_modules/@legendapp/spark-cli/dist/project.js +27 -22
  31. package/vendor/node_modules/@legendapp/spark-cli/dist/release.js +2 -1
  32. package/vendor/node_modules/@legendapp/spark-cli/dist/release.json +19 -19
  33. package/vendor/node_modules/@legendapp/spark-cli/dist/universal.cjs +2 -1
  34. package/vendor/node_modules/@legendapp/spark-cli/dist/windows.js +1 -8
  35. package/vendor/node_modules/@legendapp/spark-cli/package.json +3 -3
  36. package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/package.json +2 -2
  37. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/package.json +2 -2
  38. package/vendor/node_modules/@legendapp/spark-cli/templates/windows/package.json +2 -2
  39. package/vendor/node_modules/@legendapp/spark-clipboard/package.json +2 -2
  40. package/vendor/node_modules/@legendapp/spark-codex/package.json +3 -3
  41. package/vendor/node_modules/@legendapp/spark-commands/package.json +5 -5
  42. package/vendor/node_modules/@legendapp/spark-context-menu/package.json +2 -2
  43. package/vendor/node_modules/@legendapp/spark-desktop-app/package.json +1 -1
  44. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/menu.ts +6 -4
  45. package/vendor/node_modules/@legendapp/spark-desktop-config/config.cjs +2 -0
  46. package/vendor/node_modules/@legendapp/spark-desktop-config/expo.cjs +1 -0
  47. package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.cjs +106 -0
  48. package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.d.cts +1 -0
  49. package/vendor/node_modules/@legendapp/spark-desktop-config/package.json +5 -4
  50. package/vendor/node_modules/@legendapp/spark-desktop-host/package.json +2 -2
  51. package/vendor/node_modules/@legendapp/spark-desktop-links/package.json +2 -2
  52. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/package.json +2 -2
  53. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManager.mm +39 -0
  54. package/vendor/node_modules/@legendapp/spark-desktop-windows/package.json +3 -3
  55. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/api.ts +3 -1
  56. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/macos-adapter.ts +1 -1
  57. package/vendor/node_modules/@legendapp/spark-documents/package.json +7 -7
  58. package/vendor/node_modules/@legendapp/spark-drag-drop/package.json +2 -2
  59. package/vendor/node_modules/@legendapp/spark-file-dialog/package.json +2 -2
  60. package/vendor/node_modules/@legendapp/spark-file-system/package.json +2 -2
  61. package/vendor/node_modules/@legendapp/spark-global-shortcuts/package.json +2 -2
  62. package/vendor/node_modules/@legendapp/spark-message-dialog/package.json +2 -2
  63. package/vendor/node_modules/@legendapp/spark-native-menu/package.json +2 -2
  64. package/vendor/node_modules/@legendapp/spark-notifications/package.json +2 -2
  65. package/vendor/node_modules/@legendapp/spark-notifications/windows/SparkNotifications/SparkNotifications.h +16 -11
  66. package/vendor/node_modules/@legendapp/spark-processes/package.json +2 -2
  67. package/vendor/node_modules/@legendapp/spark-secure-storage/package.json +2 -2
  68. package/vendor/node_modules/@legendapp/spark-settings/package.json +3 -3
  69. package/vendor/node_modules/@legendapp/spark-settings-ui/package.json +4 -4
  70. package/vendor/node_modules/@legendapp/spark-sqlite/package.json +3 -3
  71. package/vendor/node_modules/@legendapp/spark-system/package.json +2 -2
  72. package/vendor/node_modules/@legendapp/spark-tray/package.json +2 -2
  73. package/vendor/node_modules/@legendapp/spark-ui/macos/appkit-split-view/RNSidebarSplitViewComponent.mm +263 -26
  74. package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.h +10 -0
  75. package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.mm +422 -0
  76. package/vendor/node_modules/@legendapp/spark-ui/package.json +3 -2
  77. package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/SidebarSplitViewNativeComponent.ts +6 -0
  78. package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/index.tsx +20 -11
  79. package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/SwipeActionsNativeComponent.ts +11 -0
  80. package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/index.tsx +41 -0
  81. package/vendor/node_modules/@legendapp/spark-updates/package.json +2 -2
  82. package/vendor/node_modules/@legendapp/spark-webview/package.json +2 -2
  83. package/vendor/node_modules/@legendapp/spark-window-options/package.json +1 -1
@@ -0,0 +1 @@
1
+ require("./vendor/node_modules/@legendapp/spark-desktop-config/native-patches.cjs").prepareNativePatches(__dirname);
package/ui-swipe.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/index.tsx";
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@legendapp/spark-ai",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "private": true,
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
7
7
  "dependencies": {
8
- "@legendapp/spark-desktop-app": "0.0.1-next.3",
9
- "@legendapp/spark-processes": "0.0.1-next.3"
8
+ "@legendapp/spark-desktop-app": "0.0.1-next.4",
9
+ "@legendapp/spark-processes": "0.0.1-next.4"
10
10
  },
11
11
  "peerDependencies": {
12
12
  "react": "19.1.4",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-audio",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index",
6
6
  "react-native": "src/index",
@@ -47,6 +47,6 @@
47
47
  },
48
48
  "private": true,
49
49
  "dependencies": {
50
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
50
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
51
51
  }
52
52
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-auth-session",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index",
6
6
  "react-native": "src/index",
@@ -8,9 +8,9 @@
8
8
  "src"
9
9
  ],
10
10
  "dependencies": {
11
- "@legendapp/spark-desktop-links": "0.0.1-next.3",
11
+ "@legendapp/spark-desktop-links": "0.0.1-next.4",
12
12
  "base64-js": "1.5.1",
13
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
13
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
14
14
  },
15
15
  "peerDependencies": {
16
16
  "react-native": "0.81.6",
@@ -12,10 +12,11 @@ import path from "node:path";
12
12
  import { registerRuntime } from "./local.js";
13
13
  import { binary, doctor, run } from "./commands.js";
14
14
  import { prepareConfig, entryFile, digest, hashFiles, nativePreparationInputs, nativeProjectFingerprint, localSigningIdentity, nativePackages, readJson, runtimeFor, selection, stateFile, writeJson, validateBuildModules, goConfigurationIssues, } from "./project.js";
15
- export async function analyze(root, packages = nativePackages(root)) {
15
+ export async function analyze(root) {
16
16
  assertNativePatchPreflight(root);
17
17
  if (projectPlatform(root) === "windows")
18
18
  throw new Error("Windows production analysis is not implemented; use spark build --dev.");
19
+ const packages = nativePackages(root, { applyExcludes: false });
19
20
  prepareConfig(root);
20
21
  const dir = stateFile(root, "analysis");
21
22
  mkdirSync(dir, { recursive: true });
@@ -57,24 +58,70 @@ export async function analyze(root, packages = nativePackages(root)) {
57
58
  (!source.includes("/node_modules/") || /["']@react-native-runtimes\/core["']/.test(readFileSync(source, "utf8")))) });
58
59
  await exportBundle({ SPARK_RUNTIME_SOURCES: runtimeSources });
59
60
  const map = readJson(path.join(dir, "app.map"));
61
+ const sourceFile = (source) => source.includes("\0") ? undefined
62
+ : [path.resolve(root, source), path.resolve(root, source.replace(/^\//, ""))].find(file => existsSync(file));
60
63
  const used = new Set();
61
64
  for (const source of map.sources) {
65
+ const file = sourceFile(source);
66
+ const resolved = file ? realpathSync(file) : source;
62
67
  for (const pkg of packages) {
63
68
  // Metro's source map can contain absolute paths or project-relative node_modules paths.
64
69
  if (source.startsWith(pkg.root + "/") ||
70
+ resolved.startsWith(pkg.root + "/") ||
65
71
  source.includes(`/node_modules/${pkg.name}/`) ||
66
72
  source.startsWith(`node_modules/${pkg.name}/`))
67
73
  used.add(pkg.name);
68
74
  }
69
75
  }
70
76
  const config = readAppConfig(root);
71
- const result = selection(packages, used, config.expo?.extra?.spark?.include ?? []);
77
+ const explicitExcluded = config.expo?.autolinking?.exclude ?? [];
78
+ const include = config.expo?.extra?.spark?.include ?? [];
79
+ const result = selection(packages, used, include, explicitExcluded);
80
+ const violations = packages.filter(pkg => explicitExcluded.includes(pkg.name)).flatMap(pkg => {
81
+ const reasons = [];
82
+ if (used.has(pkg.name))
83
+ reasons.push("used by the production bundle");
84
+ if (include.includes(pkg.name))
85
+ reasons.push("explicit native include");
86
+ for (const included of result.included) {
87
+ if (included.requires.includes(pkg.name))
88
+ reasons.push(`required by included ${included.name} (spark.requires)`);
89
+ if (Object.hasOwn(included.json.dependencies ?? {}, pkg.name) && !Object.hasOwn(included.json.optionalDependencies ?? {}, pkg.name))
90
+ reasons.push(`required by included ${included.name} (dependencies)`);
91
+ if (Object.hasOwn(included.json.peerDependencies ?? {}, pkg.name) && !included.json.peerDependenciesMeta?.[pkg.name]?.optional)
92
+ reasons.push(`required by included ${included.name} (peerDependencies)`);
93
+ }
94
+ if (!reasons.length)
95
+ return [];
96
+ const names = [pkg.name];
97
+ if (pkg.name.startsWith("@legendapp/spark-"))
98
+ names.push(`@legendapp/spark/${pkg.name.slice("@legendapp/spark-".length).replace(/^desktop-/, "")}`);
99
+ const importedFrom = map.sources.find(source => {
100
+ if (source.includes("/node_modules/") || source.startsWith("node_modules/"))
101
+ return false;
102
+ const file = sourceFile(source);
103
+ return file !== undefined && names.some(name => readFileSync(file, "utf8").includes(name));
104
+ });
105
+ return [{ name: pkg.name, reasons, ...(importedFrom ? { importedFrom } : {}) }];
106
+ });
72
107
  writeJson(stateFile(root, "selection-report.json"), {
73
108
  included: result.included.map((p) => p.name),
74
109
  excluded: result.excluded.map((p) => p.name),
75
110
  reasons: result.reasons,
76
111
  sources: map.sources,
112
+ nativeCheck: { violations },
77
113
  });
114
+ if (violations.length)
115
+ throw new Error([
116
+ `Release native-module check failed: ${violations.length} excluded native package${violations.length === 1 ? " is" : "s are"} required.`,
117
+ ...violations.flatMap(violation => [
118
+ ` ${violation.name}`,
119
+ ...violation.reasons.map(reason => ` ${reason}${reason === "used by the production bundle" && violation.importedFrom ? ` (e.g. ${violation.importedFrom})` : ""}`),
120
+ " excluded by desktop.config.json expo.autolinking.exclude",
121
+ ]),
122
+ "Fix: stop excluding it, or remove the import. In legend-apps, add the package (or its @legendapp/spark/<subpath>) to apps/<app>/app.manifest.ts nativeModules.macos.",
123
+ "Details: .spark/selection-report.json (nativeCheck)",
124
+ ].join("\n"));
78
125
  return result;
79
126
  }
80
127
  export async function build(root, mode, force = false) {
@@ -125,11 +172,10 @@ async function buildUnlocked(root, mode, force) {
125
172
  throw new Error(`Build the Spark Runner from a generic SDK starter: ${issues.join("; ")}`);
126
173
  }
127
174
  await doctor(root);
128
- const all = nativePackages(root);
129
175
  const productionGraph = mode === "release" || mode === "preview";
130
176
  const chosen = productionGraph
131
- ? await analyze(root, all)
132
- : { included: all, excluded: [] };
177
+ ? await analyze(root)
178
+ : { included: nativePackages(root), excluded: [] };
133
179
  validateBuildModules(mode, chosen.included);
134
180
  const runtime = runtimeFor(root, chosen.included, mode);
135
181
  const productionHash = productionGraph
@@ -148,9 +194,9 @@ async function buildUnlocked(root, mode, force) {
148
194
  });
149
195
  const rnConfig = path.join(root, "react-native.config.js");
150
196
  const marker = "// Generated by spark; extend with configuration plugins.";
151
- if (existsSync(rnConfig) &&
197
+ if (!isUniversal(root) && (existsSync(rnConfig) &&
152
198
  !readFileSync(rnConfig, "utf8").startsWith(marker) &&
153
- !(isExpoProject(root) && readFileSync(rnConfig, "utf8").includes("// spark: existing Expo project")))
199
+ !(isExpoProject(root) && readFileSync(rnConfig, "utf8").includes("// spark: existing Expo project"))))
154
200
  throw new Error("Existing react-native.config.js is not spark-managed. Compose the native selection explicitly before building.");
155
201
  if (!isUniversal(root))
156
202
  writeFileSync(rnConfig, `${marker}\nconst selection = require('./.spark/native-selection.json');\nmodule.exports = { dependencies: Object.fromEntries([\n...selection.included.map(p => [p.name, { root: p.root }]),\n...selection.excluded.map(name => [name, { platforms: { ios: null, macos: null, android: null } }])\n]) };\n`);
@@ -175,6 +221,7 @@ async function buildUnlocked(root, mode, force) {
175
221
  if (!force && existing) {
176
222
  if (existing.runtime.fingerprint === runtime.fingerprint &&
177
223
  existing.preparation?.fingerprint === preparation.fingerprint &&
224
+ existing.product === path.basename(existing.app) &&
178
225
  existsSync(existing.app)) {
179
226
  console.log(`Reusing ${existing.app}`);
180
227
  return existing;
@@ -184,6 +231,7 @@ async function buildUnlocked(root, mode, force) {
184
231
  ...preparationChanges(existing.preparation, preparation),
185
232
  ...(existing.runtime.fingerprint !== runtime.fingerprint ? ["runtime inputs changed"] : []),
186
233
  ...(!existsSync(existing.app) ? ["cached app missing"] : []),
234
+ ...(existing.product !== path.basename(existing.app) ? ["cached app product is unverified"] : []),
187
235
  ];
188
236
  console.log(`Building ${mode} runtime: ${reasons.join("; ")}.`);
189
237
  const nativeConfig = readAppConfig(root).expo;
@@ -273,11 +321,12 @@ async function buildUnlocked(root, mode, force) {
273
321
  label: `Building ${mode === "go" ? "Spark Runner" : mode} runtime (${configuration}, ${runtime.arch})`,
274
322
  });
275
323
  const products = path.join(derived, "Build", "Products", configuration);
276
- const product = readdirSync(products).find((name) => name.endsWith(".app"));
277
- if (!product)
278
- throw new Error("Build completed without an app product.");
324
+ // The scheme's product, not the first .app: a renamed app leaves its old bundle in DerivedData.
325
+ const product = `${name}.app`;
326
+ if (!existsSync(path.join(products, product)))
327
+ throw new Error(`Build completed without ${product}.`);
279
328
  const destination = stateFile(root, `products/macos-${runtime.arch}/${mode}/${product}`);
280
- rmSync(destination, { recursive: true, force: true });
329
+ rmSync(path.dirname(destination), { recursive: true, force: true });
281
330
  mkdirSync(path.dirname(destination), { recursive: true });
282
331
  cpSync(path.join(products, product), destination, { recursive: true, verbatimSymlinks: true });
283
332
  if (mode === "release" &&
@@ -285,6 +334,8 @@ async function buildUnlocked(root, mode, force) {
285
334
  throw new Error("Standalone build is missing its JavaScript bundle.");
286
335
  const result = {
287
336
  app: destination,
337
+ // Older receipts may record a stale bundle selected from DerivedData.
338
+ product,
288
339
  runtime: runtimeFor(root, chosen.included, mode),
289
340
  preparation,
290
341
  };
@@ -6,7 +6,7 @@ const options = {
6
6
  device: { type: "string" }, project: { type: "string" }, platform: { type: "string" },
7
7
  packages: { type: "string" }, port: { type: "string" }, runner: { type: "boolean" },
8
8
  dev: { type: "boolean" }, release: { type: "boolean" }, preview: { type: "boolean" },
9
- force: { type: "boolean" }, "submission-id": { type: "string" }, help: { type: "boolean", short: "h" },
9
+ force: { type: "boolean" }, "dry-run": { type: "boolean" }, "submission-id": { type: "string" }, help: { type: "boolean", short: "h" },
10
10
  };
11
11
  const commands = {
12
12
  create: { flags: ["example", "package-manager", "universal", "platform", "packages"], arguments: [1, 1] },
@@ -20,6 +20,7 @@ const commands = {
20
20
  build: { flags: ["project", "platform", "device", "port", "runner", "dev", "release", "preview", "force"], arguments: [0, 0] },
21
21
  prebuild: { flags: ["project", "platform"], arguments: [0, 0] },
22
22
  doctor: { flags: ["project"], arguments: [0, 0] },
23
+ upgrade: { flags: ["project", "dry-run"], arguments: [0, 0] },
23
24
  "updates init": { flags: ["project", "platform"], arguments: [1, 1] },
24
25
  credentials: { flags: ["project", "platform"], arguments: [0, 0] },
25
26
  package: { flags: ["project", "platform", "force", "submission-id"], arguments: [0, 0] },
@@ -3,7 +3,8 @@ import { checkExpoDesktopNode } from "./expo-node.js";
3
3
  import { appendFileSync, mkdirSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { createRequire } from "node:module";
6
- import { stateFile } from "./project.js";
6
+ import { statePath as stateFile } from "@legendapp/spark-desktop-config/config.cjs";
7
+ import { CommandError } from "./errors.js";
7
8
  const running = new Map();
8
9
  const secretFlags = new Set(["--password", "--apple-id-password", "--token", "--secret", "--api-key", "-P"]);
9
10
  export function commandRedactor(argv, secrets = []) {
@@ -74,7 +75,7 @@ export async function run(root, argv, options = {}) {
74
75
  process.stdout.write(redact(output));
75
76
  }
76
77
  if (code)
77
- throw new Error(`${argv.map(redact).join(" ")} exited ${code}. See ${log}\n${redact(output).slice(-1800)}`);
78
+ throw new CommandError(argv.map(redact), code, redact(output), log);
78
79
  if (options.label)
79
80
  console.log(`${options.label} finished (${((Date.now() - started) / 1000).toFixed(1)}s).`);
80
81
  return output;
@@ -1,15 +1,15 @@
1
+ import { creationManifest } from "./local.js";
1
2
  import { installedRelease, materializeReleasePackages, packageSources, verifyMaterializedReleasePackages } from "./release.js";
2
3
  import { packageManager, managerCommand, applyOverrides, localArchive } from "./package-manager.js";
3
4
  import { packArchive } from "./pack-archive.js";
4
- import { spawnProcess } from "./process.js";
5
+ import { nodeCommand, spawnProcess } from "./process.js";
5
6
  import { checkExpoDesktopNode } from "./expo-node.js";
6
7
  import { configureExample } from "./examples.js";
7
8
  import { hostPlatform } from "./platform.js";
8
- import { nodeCommand } from "./windows.js";
9
9
  import { cpSync, existsSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
10
10
  import path from "node:path";
11
11
  import os from "node:os";
12
- import { readJson, writeJson } from "./project.js";
12
+ import { readJson, writeJson } from "./json.js";
13
13
  import { run } from "./commands.js";
14
14
  // Resolve relative to the installed CLI so packed consumers use the same starter.
15
15
  const template = path.resolve(import.meta.dirname, "../templates/blank-typescript");
@@ -37,6 +37,9 @@ export async function refreshLocalPackages(root, manifest, selectedManager) {
37
37
  upgradeManagedEntry(root);
38
38
  }
39
39
  export async function create(root, archiveManifest, platform = hostPlatform(), universal = false, example, selectedManager) {
40
+ if (!(universal || example) && !["macos", "windows"].includes(platform))
41
+ throw new Error("Use create --universal for mobile/web targets");
42
+ archiveManifest ??= creationManifest();
40
43
  const manager = packageManager(process.cwd(), selectedManager);
41
44
  await checkExpoDesktopNode(path.resolve(import.meta.dirname, ".."));
42
45
  const variant = universal || example ? "universal" : platform === "macos" ? "blank-typescript" : "windows";
@@ -5,6 +5,7 @@ import { createInterface } from "node:readline/promises";
5
5
  import { run } from "./commands.js";
6
6
  import { readJson, stateFile, writeJson } from "./project.js";
7
7
  import { listNotaryProfiles } from "./notary-profiles.js";
8
+ import { isNetworkError } from "./errors.js";
8
9
  export function parseIdentities(output) {
9
10
  return [...output.matchAll(/\b([A-Fa-f0-9]{40})\s+"(Developer ID Application: [^"\n]+ \(([A-Z0-9]{10})\))"/g)]
10
11
  .map((match) => ({ hash: match[1].toUpperCase(), name: match[2], teamId: match[3] }));
@@ -27,6 +28,17 @@ async function ask(question) {
27
28
  input.close();
28
29
  }
29
30
  }
31
+ async function choose(question, defaultChoice, count) {
32
+ let choice;
33
+ while (choice === undefined) {
34
+ const answer = Number(await ask(`${question} [${defaultChoice}]: `) || String(defaultChoice));
35
+ if (Number.isInteger(answer) && answer >= 1 && answer <= count)
36
+ choice = answer;
37
+ else
38
+ console.error(`Choose a number from 1 to ${count}.`);
39
+ }
40
+ return choice;
41
+ }
30
42
  export async function credentials(root, reset = false, execute = run) {
31
43
  const file = stateFile(root, "signing.json");
32
44
  const saved = !reset && existsSync(file) ? readJson(file) : {};
@@ -41,9 +53,7 @@ export async function credentials(root, reset = false, execute = run) {
41
53
  if (process.stdin.isTTY) {
42
54
  console.log("Available Developer ID signing certificates:");
43
55
  console.log(matches.map((item, index) => `${index + 1}. ${item.name} [${item.hash}]`).join("\n"));
44
- const choice = Number(await ask(`Signing certificate [${defaultChoice}]: `) || String(defaultChoice));
45
- if (!Number.isInteger(choice) || choice < 1 || choice > matches.length)
46
- throw new Error("Invalid signing identity selection.");
56
+ const choice = await choose("Signing certificate", defaultChoice, matches.length);
47
57
  selected = matches[choice - 1];
48
58
  }
49
59
  else if (matches.length > 1) {
@@ -66,14 +76,14 @@ export async function credentials(root, reset = false, execute = run) {
66
76
  console.log(`Available saved notarization Keychain profiles${keychain ? ` in ${keychain}` : ""}:`);
67
77
  console.log(profiles.length ? profiles.map((profile, index) => `${index + 1}. ${profile}`).join("\n") : "No saved notarization profiles found.");
68
78
  console.log(`${manualChoice}. Enter an existing profile name\n${createChoice}. Create a notarization Keychain profile`);
69
- const choice = Number(await ask(`Notarization profile [${defaultProfileChoice}]: `) || String(defaultProfileChoice));
79
+ const choice = await choose("Notarization profile", defaultProfileChoice, createChoice);
70
80
  if (Number.isInteger(choice) && choice >= 1 && choice <= profiles.length) {
71
81
  keychainProfile = profiles[choice - 1];
72
82
  }
73
83
  else if (choice === manualChoice) {
74
84
  keychainProfile = await ask("Existing notarization Keychain profile: ");
75
85
  if (!keychainProfile)
76
- throw new Error("Enter an existing profile name or select the create option.");
86
+ console.error("Enter an existing profile name or select the create option.");
77
87
  }
78
88
  else if (choice === createChoice) {
79
89
  keychainProfile = await ask(`New notarization Keychain profile [${suggestedProfile}]: `) || suggestedProfile;
@@ -84,29 +94,34 @@ export async function credentials(root, reset = false, execute = run) {
84
94
  if (await child.exited)
85
95
  throw new Error("Notarization credential setup failed. Run spark credentials to retry.");
86
96
  }
87
- else {
88
- throw new Error("Invalid notarization setup selection.");
89
- }
90
- needsSelection = false;
91
- }
92
- result = { ...selected, keychainProfile, ...(keychain ? { keychain } : {}) };
93
- // Validate the Keychain profile before native compilation or signing.
94
- try {
95
- await execute(root, ["xcrun", "notarytool", "history", ...notaryAuth(result), "--output-format", "json"], { capture: true });
96
- validated = true;
97
+ needsSelection = !keychainProfile;
97
98
  }
98
- catch (error) {
99
- if (error instanceof Error && error.message.includes("No Keychain password item found for profile:")) {
100
- const message = `Notarization Keychain profile ${JSON.stringify(keychainProfile)} was not found${keychain ? ` in ${JSON.stringify(keychain)}` : ""}. A signing certificate name does not create a notarization profile.`;
101
- if (!process.stdin.isTTY)
102
- throw new Error(`${message} Run spark credentials interactively to select an existing profile or explicitly create one.`, { cause: error });
103
- console.error(message);
104
- suggestedProfile = keychainProfile;
105
- keychainProfile = undefined;
106
- needsSelection = true;
99
+ if (keychainProfile) {
100
+ result = { ...selected, keychainProfile, ...(keychain ? { keychain } : {}) };
101
+ // Validate the Keychain profile before native compilation or signing.
102
+ try {
103
+ await execute(root, ["xcrun", "notarytool", "history", ...notaryAuth(result), "--output-format", "json"], { capture: true });
104
+ validated = true;
107
105
  }
108
- else {
109
- throw error;
106
+ catch (error) {
107
+ if (error instanceof Error && error.message.includes("No Keychain password item found for profile:")) {
108
+ const message = `Notarization Keychain profile ${JSON.stringify(keychainProfile)} was not found${keychain ? ` in ${JSON.stringify(keychain)}` : ""}. A signing certificate name does not create a notarization profile.`;
109
+ if (!process.stdin.isTTY)
110
+ throw new Error(`${message} Run spark credentials interactively to select an existing profile or explicitly create one.`, { cause: error });
111
+ console.error(message);
112
+ suggestedProfile = keychainProfile;
113
+ keychainProfile = undefined;
114
+ needsSelection = true;
115
+ }
116
+ else if (process.stdin.isTTY && isNetworkError(error)) {
117
+ console.error("Could not reach Apple’s notarization service. Check your internet connection or VPN.");
118
+ const retry = await ask("Retry validation with the same profile? [Y/n]: ");
119
+ if (retry && !/^y(?:es)?$/i.test(retry))
120
+ throw error;
121
+ }
122
+ else {
123
+ throw error;
124
+ }
110
125
  }
111
126
  }
112
127
  }
@@ -0,0 +1,132 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import ts from "typescript";
4
+ import { supportedPlatforms } from "@legendapp/spark-desktop-config/config.cjs";
5
+ const packages = ["react", "react-native", "react-native-macos", "expo"];
6
+ // create/refreshLocalPackages use this starter's tested compatibility matrix too.
7
+ const defaults = JSON.parse(readFileSync(new URL("../templates/blank-typescript/package.json", import.meta.url), "utf8"));
8
+ export const expectedDependencyPins = Object.fromEntries(packages.map(name => [name, defaults.dependencies[name]]));
9
+ export function dependencyPinIssues(pkg, platforms) {
10
+ const required = packages.filter(name => name !== "react-native-macos" || platforms.includes("macos"));
11
+ const issues = [];
12
+ function check(name, value, where) {
13
+ // npm and pnpm permit an override to reference a direct dependency's spec.
14
+ const resolved = ["overrides", "pnpm"].includes(where[0]) && typeof value === "string" && value.startsWith("$")
15
+ ? pkg.dependencies?.[value.slice(1)] ?? pkg.devDependencies?.[value.slice(1)] : value;
16
+ if (typeof resolved === "string" && /^(catalog|workspace):/.test(resolved)) {
17
+ console.log(`Skipping dependency pin check for ${where.join(" → ")}: ${resolved} is managed by the catalog or workspace.`);
18
+ return;
19
+ }
20
+ if (resolved !== expectedDependencyPins[name])
21
+ issues.push({ package: name, found: typeof value === "string" ? value : JSON.stringify(value), expected: expectedDependencyPins[name], where });
22
+ }
23
+ for (const name of required) {
24
+ let present = false;
25
+ for (const section of ["dependencies", "devDependencies"]) {
26
+ if (Object.hasOwn(pkg[section] ?? {}, name)) {
27
+ present = true;
28
+ check(name, pkg[section][name], [section, name]);
29
+ }
30
+ }
31
+ if (!present)
32
+ issues.push({ package: name, found: "(missing)", expected: expectedDependencyPins[name], where: ["dependencies", name], missing: true });
33
+ }
34
+ function overrides(entries, where, parent) {
35
+ for (const [selector, value] of Object.entries(entries ?? {})) {
36
+ // Yarn paths/globs, version-qualified npm selectors and pnpm parent>child selectors.
37
+ // Comparators after @, whitespace or || belong to the version range.
38
+ const child = selector.split(/(?<![@\s|])>(?=[@a-zA-Z])/).at(-1);
39
+ const segments = child.split("/");
40
+ const name = selector === "." ? parent : segments.at(-2)?.startsWith("@") ? undefined : required.find(name => new RegExp(`^${name}(?:@[^/]*)?$`).test(segments.at(-1)));
41
+ const location = [...where, selector];
42
+ if (value && typeof value === "object" && !Array.isArray(value))
43
+ overrides(value, location, name);
44
+ else if (name)
45
+ check(name, value, location);
46
+ }
47
+ }
48
+ overrides(pkg.overrides, ["overrides"]);
49
+ overrides(pkg.resolutions, ["resolutions"]);
50
+ overrides(pkg.pnpm?.overrides, ["pnpm", "overrides"]);
51
+ return issues;
52
+ }
53
+ export function printDependencyPins(issues) {
54
+ if (!issues.length) {
55
+ console.log("Dependency pins match Spark's expected versions.");
56
+ return;
57
+ }
58
+ const rows = [["Package", "Found", "Expected", "Where"], ...issues.map(issue => [issue.package, issue.found, issue.expected, issue.where.join(" → ")])];
59
+ const widths = rows[0].map((_, column) => Math.max(...rows.map(row => row[column].length)));
60
+ for (const row of rows)
61
+ console.log(row.map((value, column) => value.padEnd(widths[column])).join(" ").trimEnd());
62
+ }
63
+ /** Edit only mismatched values and insert missing pins, preserving existing formatting. */
64
+ export function upgradeDependencyPins(root, dryRun = false) {
65
+ const file = path.join(root, "package.json");
66
+ let source = readFileSync(file, "utf8");
67
+ const pkg = JSON.parse(source);
68
+ const parsed = ts.parseJsonText(file, source);
69
+ const object = parsed.statements[0].expression;
70
+ function property(object, key) {
71
+ return object.properties.find(p => ts.isPropertyAssignment(p) && ts.isStringLiteral(p.name) && p.name.text === key);
72
+ }
73
+ function rejectDuplicates(node, where, nested = true) {
74
+ if (!node || !ts.isObjectLiteralExpression(node))
75
+ return;
76
+ const keys = new Set();
77
+ for (const entry of node.properties) {
78
+ if (!ts.isPropertyAssignment(entry) || !ts.isStringLiteral(entry.name))
79
+ continue;
80
+ const key = entry.name.text;
81
+ if (keys.has(key))
82
+ throw new Error(`package.json has duplicate key ${JSON.stringify(key)} in ${where.join(" → ") || "root"}. No files were changed.`);
83
+ keys.add(key);
84
+ if (nested)
85
+ rejectDuplicates(entry.initializer, [...where, key]);
86
+ }
87
+ }
88
+ rejectDuplicates(object, [], false);
89
+ for (const section of ["dependencies", "devDependencies", "overrides", "resolutions"])
90
+ rejectDuplicates(property(object, section)?.initializer, [section]);
91
+ const pnpm = property(object, "pnpm")?.initializer;
92
+ rejectDuplicates(pnpm, ["pnpm"], false);
93
+ if (pnpm && ts.isObjectLiteralExpression(pnpm))
94
+ rejectDuplicates(property(pnpm, "overrides")?.initializer, ["pnpm", "overrides"]);
95
+ const issues = dependencyPinIssues(pkg, supportedPlatforms(root));
96
+ if (!issues.length || dryRun)
97
+ return issues;
98
+ const edits = [];
99
+ const missing = issues.filter(issue => issue.missing);
100
+ if (missing.length) {
101
+ if ("dependencies" in pkg && (!pkg.dependencies || typeof pkg.dependencies !== "object" || Array.isArray(pkg.dependencies)))
102
+ throw new Error("package.json dependencies must be an object. No files were changed.");
103
+ const indent = source.match(/^[\t ]+(?=")/m)?.[0] ?? " ";
104
+ const newline = source.includes("\r\n") ? "\r\n" : "\n";
105
+ const dependencies = property(object, "dependencies")?.initializer;
106
+ const target = dependencies ?? object;
107
+ const entries = dependencies ? missing.map(issue => `${JSON.stringify(issue.package)}: ${JSON.stringify(issue.expected)}`)
108
+ : [`"dependencies": ${JSON.stringify(Object.fromEntries(missing.map(issue => [issue.package, issue.expected])), null, indent)}`];
109
+ const last = target.properties.at(-1);
110
+ const baseIndent = source.slice(0, target.getStart(parsed)).match(/(?:^|\n)([\t ]*)[^\n]*$/)[1];
111
+ const multiline = !last || source.slice(target.getStart(parsed), target.end).includes("\n");
112
+ const separator = multiline ? newline + baseIndent + indent : " ";
113
+ const text = entries.map(entry => entry.replaceAll("\n", newline + baseIndent + indent)).join("," + separator);
114
+ edits.push(last ? { start: last.end, end: last.end, text: "," + separator + text }
115
+ : { start: target.getStart(parsed) + 1, end: target.end - 1, text: separator + text + newline + baseIndent });
116
+ }
117
+ for (const issue of issues) {
118
+ if (issue.missing)
119
+ continue;
120
+ // Fixing the referenced direct dependency also fixes npm/pnpm's $ref override.
121
+ if (["overrides", "pnpm"].includes(issue.where[0]) && issue.found.startsWith("$") && issues.some(direct => ["dependencies", "devDependencies"].includes(direct.where[0]) && direct.package === issue.found.slice(1) && direct.expected === issue.expected))
122
+ continue;
123
+ let node = object;
124
+ for (const key of issue.where)
125
+ node = property(node, key).initializer;
126
+ edits.push({ start: node.getStart(parsed), end: node.end, text: JSON.stringify(issue.expected) });
127
+ }
128
+ for (const edit of edits.sort((a, b) => b.start - a.start))
129
+ source = source.slice(0, edit.start) + edit.text + source.slice(edit.end);
130
+ writeFileSync(file, source);
131
+ return issues;
132
+ }
@@ -4,7 +4,7 @@ import { prepareConfig, selectTarget, supportedPlatforms } from "@legendapp/spar
4
4
  import { dev } from "./dev.js";
5
5
  import { findProject } from "./local.js";
6
6
  import { hostPlatform } from "./platform.js";
7
- import { nodeCommand } from "./windows.js";
7
+ import { nodeCommand } from "./process.js";
8
8
  // Consume only spark options. Expo validates its flags, aliases and values.
9
9
  export function devArguments(args) {
10
10
  const spark = {};
@@ -3,7 +3,7 @@ import { acquireRunner } from "./runner-install.js";
3
3
  import { spawnProcess } from "./process.js";
4
4
  import { windowsMetroPort, stopWindowsMetro } from "./windows-metro.js";
5
5
  import { architecture, projectPlatform } from "./platform.js";
6
- import { nodeCommand } from "./windows.js";
6
+ import { nodeCommand } from "./process.js";
7
7
  import { readAppConfig } from "./project.js";
8
8
  import { existsSync, rmSync, watch } from "node:fs";
9
9
  import path from "node:path";