@openparachute/app 0.2.0-rc.4 → 0.2.0-rc.5

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/CHANGELOG.md CHANGED
@@ -9,6 +9,21 @@ side-by-side:
9
9
  The admin SPA at `web/admin/` ships inside the host package as
10
10
  `dist/admin/`; its version mirrors the host's version.
11
11
 
12
+ ## [app 0.2.0-rc.5] - 2026-05-22
13
+
14
+ fix(app): self-register uses `manifestName` as services.json row key
15
+ (matches hub install path; closes duplicate-port bug).
16
+
17
+ Hub installs modules under `manifest.manifestName` (`"parachute-app"`),
18
+ but the boot-time self-registration was writing under the short name
19
+ `"app"`. The two writes left services.json with two rows on the same
20
+ port, which trips hub's duplicate-port detector on re-read
21
+ (`duplicate port 1946 — claimed by both "parachute-app" and "app"`).
22
+
23
+ The row key is now sourced from `.parachute/module.json#manifestName`,
24
+ so the install path and the runtime path converge to one row. Mirrors
25
+ the fix landed in parachute-runner.
26
+
12
27
  ## [app 0.2.0-rc.1] + [app-client 0.1.0-rc.1] - 2026-05-21
13
28
 
14
29
  feat(app): Phase 2.0 — extract `@openparachute/app-client` shared
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/app",
3
- "version": "0.2.0-rc.4",
3
+ "version": "0.2.0-rc.5",
4
4
  "description": "Host module for custom Parachute UIs — drop a built bundle in and serve it under one origin.",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -31,11 +31,40 @@
31
31
  * When per-UI `uis` map lands (design doc section 12), the caller assembles
32
32
  * the `uis` field and passes it through via `extraFields`.
33
33
  */
34
+ import { readFileSync } from "node:fs";
34
35
  import * as path from "node:path";
35
36
 
36
37
  import pkg from "../package.json" with { type: "json" };
37
38
  import { type ServiceEntry, readServiceEntry, upsertService } from "./services-manifest.ts";
38
39
 
40
+ /**
41
+ * The canonical services.json row key for the app module — sourced from
42
+ * `.parachute/module.json#manifestName`. Hub looks up modules in
43
+ * services.json by `manifestName` (vault + scribe use this convention), so
44
+ * self-registering under the short name "app" would create a duplicate row
45
+ * alongside the hub-installed `parachute-app` row and trip hub's
46
+ * duplicate-port detector on re-read. See parachute-patterns or the
47
+ * `manifestName` field on .parachute/module.json files. */
48
+ const ROW_NAME = resolveManifestName();
49
+
50
+ function resolveManifestName(): string {
51
+ // Read once at module load — `.parachute/module.json` ships in the
52
+ // package and doesn't change at runtime.
53
+ try {
54
+ const manifestPath = path.resolve(import.meta.dir, "..", ".parachute", "module.json");
55
+ const raw = JSON.parse(readFileSync(manifestPath, "utf8")) as { manifestName?: unknown };
56
+ if (typeof raw.manifestName === "string" && raw.manifestName.length > 0) {
57
+ return raw.manifestName;
58
+ }
59
+ } catch {
60
+ // Fall through to the conservative fallback. The module.json is part
61
+ // of the published package, so this branch is effectively unreachable
62
+ // in production — it exists so tests that mount a stub package layout
63
+ // don't hard-crash on import.
64
+ }
65
+ return "parachute-app";
66
+ }
67
+
39
68
  export type SelfRegisterOpts = {
40
69
  /**
41
70
  * The port app just bound. Used only as the first-run fallback — if
@@ -68,7 +97,7 @@ export type SelfRegisterResult = {
68
97
  ok: boolean;
69
98
  /** The path we wrote to (or attempted to write to). */
70
99
  manifestPath: string;
71
- /** True when services.json already had a row for `app` before we wrote. */
100
+ /** True when services.json already had a row for `parachute-app` before we wrote. */
72
101
  hadExistingEntry: boolean;
73
102
  /** The port we ended up stamping (existing-entry port or boundPort). */
74
103
  portWritten: number;
@@ -92,7 +121,7 @@ export function selfRegister(opts: SelfRegisterOpts): SelfRegisterResult {
92
121
 
93
122
  let existing: ServiceEntry | undefined;
94
123
  try {
95
- existing = readServiceEntry("app", manifestPath);
124
+ existing = readServiceEntry(ROW_NAME, manifestPath);
96
125
  } catch (e) {
97
126
  const err = e as Error;
98
127
  logger.warn(`[app] skipped self-register: ${err.message}`);
@@ -107,7 +136,7 @@ export function selfRegister(opts: SelfRegisterOpts): SelfRegisterResult {
107
136
 
108
137
  const portToWrite = existing?.port ?? opts.boundPort;
109
138
  const entry: ServiceEntry = {
110
- name: "app",
139
+ name: ROW_NAME,
111
140
  port: portToWrite,
112
141
  paths: ["/app", "/.parachute"],
113
142
  health: "/app/healthz",