@frockbot/computer-host-runtime 0.3.16 → 0.3.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/computer-host-runtime",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,6 +14,7 @@ import { dirname, join } from "node:path";
14
14
  import { describe, expect, test } from "bun:test";
15
15
  import {
16
16
  APPLET_SDK_VERSION,
17
+ appletSdkInstallScript,
17
18
  APPLET_SHIM_PATH,
18
19
  appletShimScript,
19
20
  APPLETS_ROOT,
@@ -447,7 +448,7 @@ state running`);
447
448
  );
448
449
  });
449
450
 
450
- test("the update runner replaces files and installs nothing", () => {
451
+ test("the update runner replaces files and only repairs Applet dependencies", () => {
451
452
  expect(UPDATE_PHASES.map((phase) => phase.name)).toEqual([
452
453
  "runtime",
453
454
  "applets",
@@ -459,16 +460,16 @@ state running`);
459
460
  const updateDocument = UPDATE_PHASES.map((phase) => phase.body).join("\n");
460
461
  expect(updateDocument).not.toContain("apt-get");
461
462
  expect(updateDocument).not.toContain("playwright-core/cli.js install");
462
- // An in-place update swaps names over files it owns and leaves the SDK's
463
- // dependency tree which an `applet dev` may be running out of — alone.
464
- // The one install it carries is the SDK itself, and only while the SDK
465
- // is absent, which is exactly when nothing can be running out of it.
463
+ // An in-place update swaps names over files it owns. It installs the SDK
464
+ // only while absent, and fills the four shared React dependencies only
465
+ // when the resolution probe proves an older installation needs repair.
466
466
  expect(updateDocument).not.toContain("miniflare@");
467
467
  const installs = updateDocument
468
468
  .split("\n")
469
469
  .filter((line) => line.includes("npm install"));
470
470
  expect(installs).toEqual([
471
471
  ` if npm install --prefix ${APPLETS_ROOT} --no-audit --no-fund @frockbot/applet-sdk@${APPLET_SDK_VERSION}; then`,
472
+ ` npm install --prefix ${APPLETS_ROOT} --no-audit --no-fund react@19.2.8 react-dom@19.2.8 @types/react@19.2.18 @types/react-dom@19.2.4 || true`,
472
473
  ]);
473
474
  expect(updateDocument).toContain(
474
475
  `if [ ! -d ${APPLETS_ROOT}/node_modules/@frockbot/applet-sdk ]; then`,
@@ -509,18 +510,70 @@ describe("the applets phase", () => {
509
510
  expect(APPLETS_ROOT.startsWith(`${RUNTIME_ROOT}/`)).toBe(true);
510
511
  });
511
512
 
512
- test("an SDK that cannot be fetched leaves a record and not a failed run", () => {
513
- // `@frockbot/applet-sdk` is not on npm yet. A Computer whose SDK could not
514
- // be installed still browses, execs, and syncs, so the phase records the
515
- // failure for the doctor rather than failing provisioning.
513
+ test("recreates shared Applet dependencies outside every durable source root", () => {
514
+ const provision = PROVISION_PHASES.find(
515
+ (phase) => phase.name === "applets",
516
+ )!.body;
517
+ const update = UPDATE_PHASES.find(
518
+ (phase) => phase.name === "applets",
519
+ )!.body;
520
+
521
+ expect(APPLETS_ROOT).toBe(`${RUNTIME_ROOT}/applets`);
522
+ expect(APPLETS_ROOT).not.toContain("/agent-data/");
523
+ expect(provision).toContain(appletSdkInstallScript);
524
+ expect(update).toContain(appletSdkInstallScript);
525
+ expect(appletSdkInstallScript).toContain(
526
+ `npm install --prefix ${APPLETS_ROOT}`,
527
+ );
528
+ expect(appletSdkInstallScript).not.toContain("user-packages");
529
+ });
530
+
531
+ test("repairs old SDK installs and verifies every shared build import", () => {
532
+ const fallback =
533
+ `npm install --prefix ${APPLETS_ROOT} --no-audit --no-fund ` +
534
+ "react@19.2.8 react-dom@19.2.8 @types/react@19.2.18 @types/react-dom@19.2.4";
535
+ const fallbackProbe = `cd ${APPLETS_ROOT} && node -e "require.resolve('react-dom/client')"`;
536
+
537
+ expect(appletSdkInstallScript).toContain(fallback);
538
+ expect(appletSdkInstallScript).toContain(fallbackProbe);
539
+ expect(appletSdkInstallScript).toContain(
540
+ `SDK_RESOLUTION_ERROR=$(cd ${APPLETS_ROOT} && node -e`,
541
+ );
542
+ for (const specifier of [
543
+ "react-dom/client",
544
+ "react",
545
+ "@frockbot/applet-sdk/client",
546
+ ]) {
547
+ expect(appletSdkInstallScript).toContain(`'${specifier}'`);
548
+ }
549
+ expect(appletSdkInstallScript.indexOf(fallbackProbe)).toBeLessThan(
550
+ appletSdkInstallScript.indexOf(fallback),
551
+ );
552
+ expect(appletSdkInstallScript.indexOf(fallback)).toBeLessThan(
553
+ appletSdkInstallScript.indexOf("SDK_RESOLUTION_ERROR=$(cd"),
554
+ );
555
+ expect(appletSdkInstallScript).toContain(`> ${APPLETS_SDK_FAILURE_PATH}`);
556
+ });
557
+
558
+ test("an SDK that cannot be fetched or resolved leaves a record and not a failed run", () => {
559
+ // A Computer whose SDK could not be installed or resolved still browses,
560
+ // execs, and syncs, so the phase records the failure for the doctor rather
561
+ // than failing provisioning.
516
562
  const applets = PROVISION_PHASES.find((phase) => phase.name === "applets")!;
517
563
  expect(applets.body).toContain(`> ${APPLETS_SDK_FAILURE_PATH}`);
518
564
  expect(applets.body).toContain(`rm -f ${APPLETS_SDK_FAILURE_PATH}`);
519
565
  expect(boxDoctorScript).toContain("record applets-sdk fail");
520
566
  expect(boxDoctorScript).toContain("record applets-sdk pass");
567
+ expect(
568
+ boxDoctorScript.indexOf(`[ -f ${APPLETS_SDK_FAILURE_PATH} ]`),
569
+ ).toBeLessThan(
570
+ boxDoctorScript.indexOf(
571
+ `[ -d ${APPLETS_ROOT}/node_modules/@frockbot/applet-sdk ]`,
572
+ ),
573
+ );
521
574
  });
522
575
 
523
- test("runs again rather than once, because the SDK has not shipped", () => {
576
+ test("runs again rather than once so old SDK installs can be repaired", () => {
524
577
  const applets = PROVISION_PHASES.find((phase) => phase.name === "applets")!;
525
578
  expect(applets.always).toBe(true);
526
579
  expect(provisionScript).not.toContain('[ ! -f "$MARKERS/applets"');
package/src/runtime.ts CHANGED
@@ -1350,10 +1350,14 @@ ordinary file tools, then use \`applet\` — it is on your PATH:
1350
1350
  | \`applet dev\` | run it locally on this Computer and print a URL to open in the browser |
1351
1351
 
1352
1352
  The SDK and its runtime live under \`${APPLETS_ROOT}\`, installed when this
1353
- Computer was provisioned. An Applet **never runs for real here**: \`applet dev\`
1354
- is a preview, and publishing hands the built artifact to the kernel, which runs
1355
- it. If \`applet\` says the SDK is not installed, run the self-check and read the
1356
- \`applets-sdk\` line.
1353
+ Computer was provisioned and repaired by a runtime update when missing. Applet
1354
+ projects have no durable \`node_modules\`: dependency, cache, VCS, and ordinary
1355
+ build directories are excluded from Workspace sync, and the SDK resolves
1356
+ imports from this shared installation. The three files a publish explicitly
1357
+ requests from \`dist/\` are the only build-output exception. An Applet **never
1358
+ runs for real here**: \`applet dev\` is a preview, and publishing hands the
1359
+ built artifact to the kernel, which runs it. If \`applet\` says the SDK is not
1360
+ installed, run the self-check and read the \`applets-sdk\` line.
1357
1361
  `,
1358
1362
  },
1359
1363
  {
@@ -1643,19 +1647,22 @@ exec "$APPLET" "$@"
1643
1647
  `;
1644
1648
 
1645
1649
  /**
1646
- * Installs the Applets SDK when, and only when, it is absent.
1650
+ * Installs the Applets SDK when it is absent and verifies its shared imports.
1647
1651
  *
1648
1652
  * Guarded, and deliberately not fatal: a Computer that cannot fetch the SDK
1649
1653
  * is still a Computer — it browses, execs, and syncs — so the failure is
1650
1654
  * recorded as a file the doctor reports under `applets-sdk` rather than as a
1651
1655
  * phase that fails and leaves the whole run resumable-but-unfinished.
1652
1656
  *
1653
- * The `[ ! -d ]` guard is also what makes this safe inside an in-place
1654
- * update. The only thing that runs out of `${APPLETS_ROOT}` is `applet dev`,
1655
- * and the shim refuses to start it while the SDK is missing, so an install
1656
- * that runs only when the SDK is missing never touches a tree a process is
1657
- * using. Once the SDK is present neither provisioning nor an update installs
1658
- * again; a newer SDK arrives with the next provisioning adoption.
1657
+ * The React fallback repairs SDK releases published before those build-time
1658
+ * dependencies moved from development/peer metadata into dependencies. It is
1659
+ * idempotent and runs only when Node cannot resolve `react-dom/client` from
1660
+ * the shared prefix. The final probe covers React, React DOM, and the SDK's
1661
+ * client entry; its concise failure is durable input to box-doctor.
1662
+ *
1663
+ * Changing this script changes the runtime document digest, which is the
1664
+ * version marker that causes every existing Computer to run UPDATE_PHASES on
1665
+ * its next open.
1659
1666
  */
1660
1667
  export const appletSdkInstallScript = `if [ ! -d ${APPLETS_ROOT}/node_modules/@frockbot/applet-sdk ]; then
1661
1668
  if npm install --prefix ${APPLETS_ROOT} --no-audit --no-fund @frockbot/applet-sdk@${APPLET_SDK_VERSION}; then
@@ -1663,6 +1670,16 @@ export const appletSdkInstallScript = `if [ ! -d ${APPLETS_ROOT}/node_modules/@f
1663
1670
  else
1664
1671
  printf '%s\\n' "npm could not install @frockbot/applet-sdk@${APPLET_SDK_VERSION}" > ${APPLETS_SDK_FAILURE_PATH}
1665
1672
  fi
1673
+ fi
1674
+ if [ -d ${APPLETS_ROOT}/node_modules/@frockbot/applet-sdk ]; then
1675
+ if ! (cd ${APPLETS_ROOT} && node -e "require.resolve('react-dom/client')") >/dev/null 2>&1; then
1676
+ npm install --prefix ${APPLETS_ROOT} --no-audit --no-fund react@19.2.8 react-dom@19.2.8 @types/react@19.2.18 @types/react-dom@19.2.4 || true
1677
+ fi
1678
+ if SDK_RESOLUTION_ERROR=$(cd ${APPLETS_ROOT} && node -e "for (const id of ['react-dom/client', 'react', '@frockbot/applet-sdk/client']) { try { require.resolve(id) } catch { console.error('could not resolve ' + id); process.exitCode = 1 } }" 2>&1); then
1679
+ rm -f ${APPLETS_SDK_FAILURE_PATH}
1680
+ else
1681
+ printf '%s\\n' "Applets SDK dependency resolution failed: $SDK_RESOLUTION_ERROR" > ${APPLETS_SDK_FAILURE_PATH}
1682
+ fi
1666
1683
  fi`;
1667
1684
 
1668
1685
  /** The files the `applets` phase installs, with their modes. */
@@ -1883,13 +1900,12 @@ elif [ -x ${APPLET_SHIM_PATH} ]; then
1883
1900
  else
1884
1901
  record applets fail "no ${APPLET_SHIM_PATH}; provisioning installs it, and no Applet can be checked, built, or previewed without it"
1885
1902
  fi
1886
- # Named and non-fatal on purpose: \`@frockbot/applet-sdk\` is not published
1887
- # yet, so the provisioning phase's guarded install is expected to fail and
1888
- # leave this file rather than fail the whole run. This is where that shows up.
1889
- if [ -d ${APPLETS_ROOT}/node_modules/@frockbot/applet-sdk ]; then
1890
- record applets-sdk pass "the Applets SDK is installed under ${APPLETS_ROOT}"
1891
- elif [ -f ${APPLETS_SDK_FAILURE_PATH} ]; then
1903
+ # Named and non-fatal on purpose: an install or dependency-resolution failure
1904
+ # leaves this file rather than failing the whole Computer run.
1905
+ if [ -f ${APPLETS_SDK_FAILURE_PATH} ]; then
1892
1906
  record applets-sdk fail "$(head -n 1 ${APPLETS_SDK_FAILURE_PATH} 2>/dev/null); everything else on this Computer is unaffected"
1907
+ elif [ -d ${APPLETS_ROOT}/node_modules/@frockbot/applet-sdk ]; then
1908
+ record applets-sdk pass "the Applets SDK and its shared imports resolve under ${APPLETS_ROOT}"
1893
1909
  else
1894
1910
  record applets-sdk fail "no Applets SDK under ${APPLETS_ROOT} and no record of an attempt; it installs when this Computer is next provisioned"
1895
1911
  fi