craft-native 0.0.499 → 0.0.502

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.
@@ -60,7 +60,6 @@ export declare function exec(command: string, options?: ExecOptions): Promise<Ex
60
60
  */
61
61
  export interface SpawnOptions {
62
62
  cwd?: string;
63
- env?: Record<string, string>;
64
63
  /**
65
64
  * Run `command` as a command line through a shell (cmd.exe on Windows,
66
65
  * /bin/sh elsewhere) instead of executing it directly.
@@ -74,45 +73,41 @@ export interface SpawnOptions {
74
73
  */
75
74
  shell?: boolean;
76
75
  }
76
+ /**
77
+ * A process started through the native bridge.
78
+ *
79
+ * What native `shell.spawn` does, and so all this class offers: start a
80
+ * program, and kill it. The child inherits Craft's own standard output and
81
+ * error. Nothing it writes is delivered to the page, and its exit is not
82
+ * reported.
83
+ *
84
+ * `onStdout`, `onStderr`, `onExit` and `write` used to be declared here. No
85
+ * native bridge ever emitted the events behind them or accepted input, so a
86
+ * handler registered with them was never called. They are removed rather than
87
+ * left as methods that accept a callback and stay silent. To read a child's
88
+ * output, run it in your app's backend process and send the result to the
89
+ * page; `exec()` covers a command that finishes and returns its output whole.
90
+ *
91
+ * Windows and Linux desktop builds do not serve the `shell` namespace at all:
92
+ * there `start()` rejects.
93
+ */
77
94
  export declare class ChildProcess {
78
95
  private command;
79
96
  private args;
80
97
  private options;
81
98
  private processId;
82
- private onStdoutHandlers;
83
- private onStderrHandlers;
84
- private onExitHandlers;
85
- private domListeners;
86
- private exited;
87
99
  constructor(command: string, args?: string[], options?: SpawnOptions);
88
- private addDomListener;
89
- private removeDomListeners;
90
- /** Pid assigned by the native bridge, or null before start(). */
100
+ /** Identifier this process is registered under, or null before start(). */
91
101
  get pid(): string | null;
92
102
  /**
93
- * Start the process
103
+ * Start the process. Resolves once native has started it.
94
104
  */
95
105
  start(): Promise<void>;
96
106
  /**
97
- * Write to stdin
98
- */
99
- write(data: string): Promise<void>;
100
- /**
101
- * Kill the process
102
- */
103
- kill(signal?: string): Promise<void>;
104
- /**
105
- * Listen for stdout data
106
- */
107
- onStdout(handler: (data: string) => void): () => void;
108
- /**
109
- * Listen for stderr data
110
- */
111
- onStderr(handler: (data: string) => void): () => void;
112
- /**
113
- * Listen for exit
107
+ * Kill the process. Native sends the platform's default termination; there
108
+ * is no signal to choose.
114
109
  */
115
- onExit(handler: (code: number) => void): () => void;
110
+ kill(): Promise<void>;
116
111
  }
117
112
  /**
118
113
  * Spawn a child process
@@ -46,6 +46,12 @@ export declare const BridgeErrorCodes: {
46
46
  readonly PROTOCOL_MISMATCH: -8;
47
47
  /** Bridge transitioned from connected to disconnected with requests in flight */
48
48
  readonly DISCONNECTED: -9;
49
+ /**
50
+ * The native side received the request and refused or failed it. Not
51
+ * retried: asking again gets the same answer. `data.nativeCode` carries
52
+ * the bridge's own string code when it sent one.
53
+ */
54
+ readonly NATIVE_ERROR: -10;
49
55
  };
50
56
  export type BridgeErrorCode = typeof BridgeErrorCodes[keyof typeof BridgeErrorCodes];
51
57
  /**
@@ -136,12 +142,12 @@ export declare class NativeBridge extends EventEmitter {
136
142
  * BridgeError(PROTOCOL_MISMATCH) if the native side reports a version we
137
143
  * don't speak.
138
144
  *
139
- * This is **opt-in** — you must call it explicitly. It assumes the host
140
- * registers a `_handshake` handler; the Zig core's
141
- * `Bridge.registerDefaults()` does this for you. If you're embedding Craft
142
- * inside a host that hasn't wired `_handshake`, do not call this method
143
- * (the timeout path will reject with TIMEOUT, which is louder than a
144
- * silent mismatch but still a failure).
145
+ * This is **opt-in** — you must call it explicitly, and only against a
146
+ * host of your own that answers a `_handshake` request. Craft's own
147
+ * webview hosts do not: inside one, requests go through
148
+ * `window.craft.invoke`, which takes `namespace.action` names, and this
149
+ * call is rejected immediately instead of negotiating anything. Use
150
+ * `window.craft.capabilities` to ask a Craft host what it serves.
145
151
  */
146
152
  handshake(): Promise<{
147
153
  nativeVersion: number;
@@ -203,6 +209,24 @@ export declare class NativeBridge extends EventEmitter {
203
209
  * use that to fail fast instead of waiting for the request timeout.
204
210
  */
205
211
  private send;
212
+ /**
213
+ * Hand a request to `window.craft.invoke`, the dispatcher that
214
+ * `craft-bridge.js` injects into every Craft webview, and settle the
215
+ * pending entry with what native answers.
216
+ *
217
+ * This is the only path by which `request()` reaches native code. The
218
+ * `{id, type: 'request', method, params}` envelope this class builds is not
219
+ * something any Craft host parses: posted raw to the `craft` message
220
+ * handler it was dropped, and every `request()` waited out its timeout and
221
+ * its retries. The method name is used as written, `namespace.action`, so
222
+ * it has to be one the native bridge serves (`fs.readFile`,
223
+ * `shell.openUrl`, `dialog.openFile`); one it does not serve is rejected
224
+ * with `NATIVE_ERROR` straight away.
225
+ *
226
+ * Returns false when the injected bridge is not present, so the older
227
+ * transports below still get their turn in a host that provides one.
228
+ */
229
+ private sendThroughInjectedBridge;
206
230
  /**
207
231
  * Reject the pending entry corresponding to `message.id` with `error`,
208
232
  * if any. Used by `send()` when serialization or transport throws so
package/dist/cli.js CHANGED
@@ -1395,6 +1395,33 @@ Description: ${description || opts.name}
1395
1395
  }
1396
1396
  });
1397
1397
  }
1398
+ function rpmSpec(options) {
1399
+ return [
1400
+ "%global debug_package %{nil}",
1401
+ "%global _build_id_links none",
1402
+ "%global __strip /bin/true",
1403
+ "%global __os_install_post %{nil}",
1404
+ `Name: ${options.name}`,
1405
+ `Version: ${options.version}`,
1406
+ "Release: 1",
1407
+ `Summary: ${options.summary}`,
1408
+ "License: MIT",
1409
+ `Source0: ${options.name}`,
1410
+ ...options.requires ? [`Requires: ${options.requires}`] : [],
1411
+ "",
1412
+ "%description",
1413
+ options.description,
1414
+ "",
1415
+ "%install",
1416
+ "mkdir -p %{buildroot}/usr/bin",
1417
+ `install -m 755 %{SOURCE0} %{buildroot}/usr/bin/${options.name}`,
1418
+ "",
1419
+ "%files",
1420
+ `/usr/bin/${options.name}`,
1421
+ ""
1422
+ ].join(`
1423
+ `);
1424
+ }
1398
1425
  async function createRPM(opts) {
1399
1426
  return new Promise((resolve) => {
1400
1427
  try {
@@ -1427,26 +1454,17 @@ async function createRPM(opts) {
1427
1454
  return r;
1428
1455
  }).join(", ");
1429
1456
  copyFileSync(opts.binaryPath, join3(sourcesDir, sanitizedName));
1430
- const specContent = `Name: ${sanitizedName}
1431
- Version: ${opts.version}
1432
- Release: 1%{?dist}
1433
- Summary: ${summary}
1434
- License: MIT
1435
- Requires: ${requires}
1436
-
1437
- %description
1438
- ${description}
1439
-
1440
- %install
1441
- mkdir -p %{buildroot}/usr/bin
1442
- install -m 755 %{SOURCE0} %{buildroot}/usr/bin/${sanitizedName}
1443
-
1444
- %files
1445
- /usr/bin/${sanitizedName}
1446
- `;
1457
+ const specContent = rpmSpec({ name: sanitizedName, version: opts.version, summary, description, requires });
1447
1458
  const specPath = join3(specDir, `${opts.name.toLowerCase()}.spec`);
1448
1459
  writeFileSync4(specPath, specContent);
1449
1460
  const proc = spawn("rpmbuild", ["-bb", specPath]);
1461
+ let said = "";
1462
+ proc.stdout?.on("data", (chunk) => {
1463
+ said += chunk.toString();
1464
+ });
1465
+ proc.stderr?.on("data", (chunk) => {
1466
+ said += chunk.toString();
1467
+ });
1450
1468
  proc.on("close", (code) => {
1451
1469
  if (code === 0) {
1452
1470
  const rpmName = `${opts.name.toLowerCase()}-${opts.version}-1.x86_64.rpm`;
@@ -1458,7 +1476,9 @@ install -m 755 %{SOURCE0} %{buildroot}/usr/bin/${sanitizedName}
1458
1476
  resolve({ success: false, error: "Failed to copy built RPM" });
1459
1477
  }
1460
1478
  } else {
1461
- resolve({ success: false, error: `rpmbuild exited with code ${code}` });
1479
+ resolve({ success: false, error: `rpmbuild exited with code ${code}: ${said.trim().split(`
1480
+ `).slice(-12).join(`
1481
+ `)}` });
1462
1482
  }
1463
1483
  });
1464
1484
  proc.on("error", (err) => {
@@ -3246,7 +3266,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3246
3266
  `);
3247
3267
  }
3248
3268
  // package.json
3249
- var version = "0.0.499";
3269
+ var version = "0.0.502";
3250
3270
 
3251
3271
  // bin/cli.ts
3252
3272
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -801,7 +801,7 @@ export interface TextInputProps extends BaseProps {
801
801
  autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
802
802
  /** Auto correct */
803
803
  autoCorrect?: boolean;
804
- /** Auto focus */
804
+ /** Focus the input when it mounts, or when this turns on; not on each render. */
805
805
  autoFocus?: boolean;
806
806
  /** Secure text entry (password) */
807
807
  secureTextEntry?: boolean;
package/dist/index.cjs CHANGED
@@ -376,7 +376,7 @@ function craftBinaryNotFoundMessage(triedPath) {
376
376
  `);
377
377
  }
378
378
  // package.json
379
- var version = "0.0.499";
379
+ var version = "0.0.502";
380
380
 
381
381
  // src/sdk-version.ts
382
382
  var SDK_VERSION = version;
@@ -1764,6 +1764,33 @@ Description: ${description || opts.name}
1764
1764
  }
1765
1765
  });
1766
1766
  }
1767
+ function rpmSpec(options) {
1768
+ return [
1769
+ "%global debug_package %{nil}",
1770
+ "%global _build_id_links none",
1771
+ "%global __strip /bin/true",
1772
+ "%global __os_install_post %{nil}",
1773
+ `Name: ${options.name}`,
1774
+ `Version: ${options.version}`,
1775
+ "Release: 1",
1776
+ `Summary: ${options.summary}`,
1777
+ "License: MIT",
1778
+ `Source0: ${options.name}`,
1779
+ ...options.requires ? [`Requires: ${options.requires}`] : [],
1780
+ "",
1781
+ "%description",
1782
+ options.description,
1783
+ "",
1784
+ "%install",
1785
+ "mkdir -p %{buildroot}/usr/bin",
1786
+ `install -m 755 %{SOURCE0} %{buildroot}/usr/bin/${options.name}`,
1787
+ "",
1788
+ "%files",
1789
+ `/usr/bin/${options.name}`,
1790
+ ""
1791
+ ].join(`
1792
+ `);
1793
+ }
1767
1794
  async function createRPM(opts) {
1768
1795
  return new Promise((resolve) => {
1769
1796
  try {
@@ -1796,26 +1823,17 @@ async function createRPM(opts) {
1796
1823
  return r;
1797
1824
  }).join(", ");
1798
1825
  import_fs.copyFileSync(opts.binaryPath, import_path.join(sourcesDir, sanitizedName));
1799
- const specContent = `Name: ${sanitizedName}
1800
- Version: ${opts.version}
1801
- Release: 1%{?dist}
1802
- Summary: ${summary}
1803
- License: MIT
1804
- Requires: ${requires}
1805
-
1806
- %description
1807
- ${description}
1808
-
1809
- %install
1810
- mkdir -p %{buildroot}/usr/bin
1811
- install -m 755 %{SOURCE0} %{buildroot}/usr/bin/${sanitizedName}
1812
-
1813
- %files
1814
- /usr/bin/${sanitizedName}
1815
- `;
1826
+ const specContent = rpmSpec({ name: sanitizedName, version: opts.version, summary, description, requires });
1816
1827
  const specPath = import_path.join(specDir, `${opts.name.toLowerCase()}.spec`);
1817
1828
  import_fs.writeFileSync(specPath, specContent);
1818
1829
  const proc = import_child_process.spawn("rpmbuild", ["-bb", specPath]);
1830
+ let said = "";
1831
+ proc.stdout?.on("data", (chunk) => {
1832
+ said += chunk.toString();
1833
+ });
1834
+ proc.stderr?.on("data", (chunk) => {
1835
+ said += chunk.toString();
1836
+ });
1819
1837
  proc.on("close", (code) => {
1820
1838
  if (code === 0) {
1821
1839
  const rpmName = `${opts.name.toLowerCase()}-${opts.version}-1.x86_64.rpm`;
@@ -1827,7 +1845,9 @@ install -m 755 %{SOURCE0} %{buildroot}/usr/bin/${sanitizedName}
1827
1845
  resolve({ success: false, error: "Failed to copy built RPM" });
1828
1846
  }
1829
1847
  } else {
1830
- resolve({ success: false, error: `rpmbuild exited with code ${code}` });
1848
+ resolve({ success: false, error: `rpmbuild exited with code ${code}: ${said.trim().split(`
1849
+ `).slice(-12).join(`
1850
+ `)}` });
1831
1851
  }
1832
1852
  });
1833
1853
  proc.on("error", (err) => {
@@ -2364,7 +2384,8 @@ var BridgeErrorCodes = {
2364
2384
  BRIDGE_DESTROYED: -6,
2365
2385
  BUSY: -7,
2366
2386
  PROTOCOL_MISMATCH: -8,
2367
- DISCONNECTED: -9
2387
+ DISCONNECTED: -9,
2388
+ NATIVE_ERROR: -10
2368
2389
  };
2369
2390
  var BRIDGE_PROTOCOL_VERSION = 1;
2370
2391
  function buildMessageId(prefix, counter) {
@@ -2772,6 +2793,9 @@ class NativeBridge extends import_events.EventEmitter {
2772
2793
  }
2773
2794
  try {
2774
2795
  if (typeof window !== "undefined") {
2796
+ if (message.type === "request" && this.sendThroughInjectedBridge(message)) {
2797
+ return true;
2798
+ }
2775
2799
  if (window.webkit?.messageHandlers?.craft) {
2776
2800
  window.webkit.messageHandlers.craft.postMessage(message);
2777
2801
  return true;
@@ -2799,6 +2823,31 @@ class NativeBridge extends import_events.EventEmitter {
2799
2823
  return false;
2800
2824
  }
2801
2825
  }
2826
+ sendThroughInjectedBridge(message) {
2827
+ const injected = window.craft;
2828
+ if (!injected || typeof injected.invoke !== "function" || typeof message.method !== "string")
2829
+ return false;
2830
+ let call;
2831
+ try {
2832
+ call = Promise.resolve(injected.invoke(message.method, message.params));
2833
+ } catch (e) {
2834
+ call = Promise.reject(e);
2835
+ }
2836
+ call.then((result) => this.handleResponse({ id: message.id, type: "response", result }), (error) => {
2837
+ const detail = error ?? {};
2838
+ const text = typeof detail.message === "string" && detail.message.length > 0 ? detail.message : `Native bridge refused ${message.method}`;
2839
+ this.handleResponse({
2840
+ id: message.id,
2841
+ type: "response",
2842
+ error: {
2843
+ code: BridgeErrorCodes.NATIVE_ERROR,
2844
+ message: text,
2845
+ data: { nativeCode: typeof detail.code === "string" ? detail.code : undefined, method: message.method }
2846
+ }
2847
+ });
2848
+ });
2849
+ return true;
2850
+ }
2802
2851
  failPendingForMessage(message, error) {
2803
2852
  const pending = this.pendingRequests.get(message.id);
2804
2853
  if (!pending)
@@ -5109,7 +5158,7 @@ async function getSystemInfo() {
5109
5158
  }
5110
5159
  async function exec(command, options) {
5111
5160
  if (typeof window !== "undefined" && window.craft) {
5112
- return callBridge3("process.exec", { command, ...options });
5161
+ return callBridge3("shell.exec", { command, cwd: options?.cwd });
5113
5162
  }
5114
5163
  const { exec: nodeExec } = await import("node:child_process");
5115
5164
  const { promisify } = await import("node:util");
@@ -5130,107 +5179,46 @@ async function exec(command, options) {
5130
5179
  };
5131
5180
  }
5132
5181
  }
5182
+ var nextProcessId = 0;
5183
+ function shellFacade() {
5184
+ return window.craft?.shell;
5185
+ }
5133
5186
 
5134
5187
  class ChildProcess {
5135
5188
  command;
5136
5189
  args;
5137
5190
  options;
5138
5191
  processId = null;
5139
- onStdoutHandlers = [];
5140
- onStderrHandlers = [];
5141
- onExitHandlers = [];
5142
- domListeners = [];
5143
- exited = false;
5144
5192
  constructor(command, args = [], options = {}) {
5145
5193
  this.command = command;
5146
5194
  this.args = args;
5147
5195
  this.options = options;
5148
5196
  }
5149
- addDomListener(type, handler) {
5150
- if (typeof window === "undefined")
5151
- return;
5152
- window.addEventListener(type, handler);
5153
- this.domListeners.push({ type, handler });
5154
- }
5155
- removeDomListeners() {
5156
- if (typeof window === "undefined")
5157
- return;
5158
- for (const { type, handler } of this.domListeners) {
5159
- window.removeEventListener(type, handler);
5160
- }
5161
- this.domListeners = [];
5162
- }
5163
5197
  get pid() {
5164
5198
  return this.processId;
5165
5199
  }
5166
5200
  async start() {
5167
5201
  if (typeof window !== "undefined" && window.craft) {
5168
- const result = await callBridge3("process.spawn", {
5202
+ nextProcessId += 1;
5203
+ const id = `sdk-${Date.now().toString(36)}-${nextProcessId}`;
5204
+ await callBridge3("shell.spawn", {
5205
+ id,
5169
5206
  command: this.command,
5170
5207
  args: this.args,
5171
- ...this.options
5208
+ cwd: this.options.cwd,
5209
+ shell: this.options.shell
5172
5210
  });
5173
- this.processId = result.pid;
5174
- const stdoutHandler = (event) => {
5175
- if (event.detail.pid === this.processId) {
5176
- this.onStdoutHandlers.forEach((h) => h(event.detail.data));
5177
- }
5178
- };
5179
- const stderrHandler = (event) => {
5180
- if (event.detail.pid === this.processId) {
5181
- this.onStderrHandlers.forEach((h) => h(event.detail.data));
5182
- }
5183
- };
5184
- const exitHandler = (event) => {
5185
- if (event.detail.pid === this.processId) {
5186
- this.exited = true;
5187
- this.onExitHandlers.forEach((h) => h(event.detail.code));
5188
- this.removeDomListeners();
5189
- }
5190
- };
5191
- this.addDomListener("craft:process:stdout", stdoutHandler);
5192
- this.addDomListener("craft:process:stderr", stderrHandler);
5193
- this.addDomListener("craft:process:exit", exitHandler);
5211
+ this.processId = id;
5194
5212
  return;
5195
5213
  }
5196
5214
  throw new Error("spawn() requires Craft environment");
5197
5215
  }
5198
- async write(data) {
5216
+ async kill() {
5199
5217
  if (typeof window !== "undefined" && window.craft && this.processId) {
5200
- await callBridge3("process.stdin", { pid: this.processId, data });
5218
+ await shellFacade()?.kill?.(this.processId);
5219
+ this.processId = null;
5201
5220
  }
5202
5221
  }
5203
- async kill(signal = "SIGTERM") {
5204
- if (typeof window !== "undefined" && window.craft && this.processId) {
5205
- await callBridge3("process.kill", { pid: this.processId, signal });
5206
- if (this.exited)
5207
- this.removeDomListeners();
5208
- }
5209
- }
5210
- onStdout(handler) {
5211
- this.onStdoutHandlers.push(handler);
5212
- return () => {
5213
- const idx = this.onStdoutHandlers.indexOf(handler);
5214
- if (idx >= 0)
5215
- this.onStdoutHandlers.splice(idx, 1);
5216
- };
5217
- }
5218
- onStderr(handler) {
5219
- this.onStderrHandlers.push(handler);
5220
- return () => {
5221
- const idx = this.onStderrHandlers.indexOf(handler);
5222
- if (idx >= 0)
5223
- this.onStderrHandlers.splice(idx, 1);
5224
- };
5225
- }
5226
- onExit(handler) {
5227
- this.onExitHandlers.push(handler);
5228
- return () => {
5229
- const idx = this.onExitHandlers.indexOf(handler);
5230
- if (idx >= 0)
5231
- this.onExitHandlers.splice(idx, 1);
5232
- };
5233
- }
5234
5222
  }
5235
5223
  function spawn2(command, args, options) {
5236
5224
  return new ChildProcess(command, args, options);
@@ -5243,7 +5231,7 @@ function cwd() {
5243
5231
  }
5244
5232
  async function homeDir() {
5245
5233
  if (typeof window !== "undefined" && window.craft) {
5246
- return callBridge3("process.homeDir");
5234
+ return (await callBridge3("fs.getHomeDir"))?.path || "/";
5247
5235
  }
5248
5236
  if (typeof process !== "undefined") {
5249
5237
  const os = await import("node:os");
@@ -5253,7 +5241,7 @@ async function homeDir() {
5253
5241
  }
5254
5242
  async function tempDir() {
5255
5243
  if (typeof window !== "undefined" && window.craft) {
5256
- return callBridge3("process.tempDir");
5244
+ return (await callBridge3("fs.getTempDir"))?.path || "/tmp";
5257
5245
  }
5258
5246
  if (typeof process !== "undefined") {
5259
5247
  const os = await import("node:os");
@@ -5281,7 +5269,10 @@ function argv() {
5281
5269
  }
5282
5270
  async function open(target) {
5283
5271
  if (typeof window !== "undefined" && window.craft) {
5284
- await callBridge3("process.open", { target });
5272
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target) && !/^[a-z]:[\\/]/i.test(target))
5273
+ await callBridge3("shell.openUrl", { url: target });
5274
+ else
5275
+ await shellFacade()?.openPath?.(target);
5285
5276
  return;
5286
5277
  }
5287
5278
  const platform = getPlatform();