Skip to content

Commit 34db66e

Browse files
fix(cli): prevent esbuild runtime error in global/npx installs (#452)
* fix(cli): resolve runtime fallback for globally-installed hyperframes When hyperframes is installed globally via npm, the `loadRuntimeSourceFallback()` path that dynamically imports @hyperframes/core and runs esbuild fails because @hyperframes/core is inlined into cli.js and import.meta.url resolves to the wrong location for the entry.ts source file. Add a disk-based fallback that searches for the pre-built IIFE runtime artifact in multiple locations: - Alongside the bundled CLI (dist/hyperframe-runtime.js, dist/hyperframe.runtime.iife.js) - Walking up from __dirname through node_modules The esbuild path is tried first to preserve live-rebuild behavior in dev, with the pre-built artifact search as a safety net for the bundled context. Also adds the IIFE artifact name variant to resolveRuntimePath() in the studio server so it checks both naming conventions. * fix(cli): gate esbuild fallback on source availability The previous fix still triggered esbuild's stderr output before the catch could suppress it. Now check whether the runtime entry.ts source file actually exists before attempting the on-the-fly build, avoiding the noisy error in global installs entirely. * fix(cli): remove noisy console.warn from runtime fallback The caller already handles a null return — no need to warn about something the user can't act on. If both paths fail, the /api/runtime.js route returns a 404 which the studio handles gracefully. * style(engine): fix oxfmt trailing blank line in chunkEncoder test * fix(cli): guard against null/undefined from loadHyperframeRuntimeSource Fall through to the pre-built artifact if the function returns a falsy value without throwing. * refactor(cli): consolidate runtime source resolution into single module Replace the scattered path-probing logic with a single loadRuntimeSource() that encodes the full priority chain: esbuild from source (dev only, gated on entry.ts existence) → pre-built artifact alongside cli.js → core/dist artifact → node_modules walk. Rename loadRuntimeSourceFallback → loadRuntimeSource since it's now the primary resolution function, not a fallback.
1 parent 147bb73 commit 34db66e

4 files changed

Lines changed: 77 additions & 14 deletions

File tree

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,77 @@
1-
export async function loadRuntimeSourceFallback(): Promise<string | null> {
1+
import { existsSync, readFileSync } from "node:fs";
2+
import { resolve, dirname } from "node:path";
3+
4+
const ARTIFACT_NAMES = ["hyperframe-runtime.js", "hyperframe.runtime.iife.js"];
5+
6+
/**
7+
* Resolve the runtime JS source for the studio preview server.
8+
*
9+
* Two contexts exist:
10+
*
11+
* Dev (monorepo workspace) — `entry.ts` exists next to `@hyperframes/core`
12+
* source. We build from source via esbuild so edits to the runtime are
13+
* reflected without a manual `bun run build`.
14+
*
15+
* Installed (npm global / npx) — only `dist/` ships. We read the pre-built
16+
* IIFE artifact that `build:runtime` copies alongside `cli.js`.
17+
*
18+
* The priority chain:
19+
* 1. esbuild from source (dev only — gated on entry.ts existence)
20+
* 2. pre-built artifact (alongside cli.js in dist/)
21+
* 3. core/dist artifact (dev fallback if build:runtime already ran)
22+
* 4. node_modules walk (nested install edge cases)
23+
*/
24+
export async function loadRuntimeSource(): Promise<string | null> {
25+
return (await buildFromSource()) ?? readPrebuiltArtifact();
26+
}
27+
28+
// ── Strategy 1: live build from source (dev only) ──────────────────────────
29+
30+
const ENTRY_TS = resolve(__dirname, "..", "..", "..", "core", "src", "runtime", "entry.ts");
31+
32+
async function buildFromSource(): Promise<string | null> {
33+
if (!existsSync(ENTRY_TS)) return null;
234
try {
335
const mod = await import("@hyperframes/core");
436
if (typeof mod.loadHyperframeRuntimeSource === "function") {
5-
return mod.loadHyperframeRuntimeSource();
37+
const source = mod.loadHyperframeRuntimeSource();
38+
if (source) return source;
39+
}
40+
} catch {
41+
// esbuild failed — fall through to artifact
42+
}
43+
return null;
44+
}
45+
46+
// ── Strategy 2-4: pre-built IIFE artifact ──────────────────────────────────
47+
48+
function readPrebuiltArtifact(): string | null {
49+
return readFromDir(__dirname) ?? readFromCoreDistDir() ?? readFromNodeModules();
50+
}
51+
52+
function readFromDir(dir: string): string | null {
53+
for (const name of ARTIFACT_NAMES) {
54+
const path = resolve(dir, name);
55+
if (existsSync(path)) return readFileSync(path, "utf-8");
56+
}
57+
return null;
58+
}
59+
60+
function readFromCoreDistDir(): string | null {
61+
return readFromDir(resolve(__dirname, "..", "..", "..", "core", "dist"));
62+
}
63+
64+
function readFromNodeModules(): string | null {
65+
const subPaths = ["node_modules/hyperframes/dist", "node_modules/@hyperframes/core/dist"];
66+
let dir = __dirname;
67+
for (;;) {
68+
for (const sub of subPaths) {
69+
const result = readFromDir(resolve(dir, sub));
70+
if (result) return result;
671
}
7-
} catch (err) {
8-
console.warn("[studio] Failed to load runtime source fallback:", err);
72+
const parent = dirname(dir);
73+
if (parent === dir) break;
74+
dir = parent;
975
}
1076
return null;
1177
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { describe, expect, it } from "vitest";
22
import { loadHyperframeRuntimeSource } from "@hyperframes/core";
3-
import { loadRuntimeSourceFallback } from "./runtimeSource.js";
3+
import { loadRuntimeSource } from "./runtimeSource.js";
44

5-
describe("loadRuntimeSourceFallback", () => {
5+
describe("loadRuntimeSource", () => {
66
it("loads runtime source from the published core entrypoint", async () => {
7-
await expect(loadRuntimeSourceFallback()).resolves.toBe(loadHyperframeRuntimeSource());
7+
await expect(loadRuntimeSource()).resolves.toBe(loadHyperframeRuntimeSource());
88
});
99
});

packages/cli/src/server/studioServer.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { streamSSE } from "hono/streaming";
1010
import { existsSync, readFileSync, writeFileSync, statSync } from "node:fs";
1111
import { resolve, join, basename } from "node:path";
1212
import { createProjectWatcher, type ProjectWatcher } from "./fileWatcher.js";
13-
import { loadRuntimeSourceFallback } from "./runtimeSource.js";
13+
import { loadRuntimeSource } from "./runtimeSource.js";
1414
import { VERSION as version } from "../version.js";
1515
import {
1616
createStudioApi,
@@ -33,6 +33,8 @@ function resolveDistDir(): string {
3333
function resolveRuntimePath(): string {
3434
const builtPath = resolve(__dirname, "hyperframe-runtime.js");
3535
if (existsSync(builtPath)) return builtPath;
36+
const iifePath = resolve(__dirname, "hyperframe.runtime.iife.js");
37+
if (existsSync(iifePath)) return iifePath;
3638
const devPath = resolve(
3739
__dirname,
3840
"..",
@@ -282,12 +284,8 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
282284
// CLI-specific routes (before shared API)
283285
app.get("/api/runtime.js", (c) => {
284286
const serve = async () => {
285-
// Prefer the runtime generated from the current core source over a
286-
// potentially stale copied artifact. This keeps local studio/preview
287-
// sessions aligned with source edits without requiring a manual
288-
// rebuild of the CLI runtime bundle first.
289287
const runtimeSource =
290-
(await loadRuntimeSourceFallback()) ??
288+
(await loadRuntimeSource()) ??
291289
(existsSync(runtimePath) ? readFileSync(runtimePath, "utf-8") : null);
292290
if (!runtimeSource) return c.text("runtime not available", 404);
293291
return c.body(runtimeSource, 200, {

packages/engine/src/services/chunkEncoder.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,6 @@ describe("buildEncoderArgs HDR color space", () => {
460460
expect.stringContaining("HDR is not supported with codec=h264"),
461461
);
462462
warnSpy.mockRestore();
463-
464463
});
465464

466465
it("uses range conversion for HDR CPU encoding", () => {

0 commit comments

Comments
 (0)