Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/provider-hyperframes-local/src/capture-bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Preloaded into the disposable capture process, which never enters `bin/hypit.mjs`.
*
* `module.registerHooks` is process-local, so the resolver the launcher installs does not
* reach a child that Node starts directly. This registers the same resolution environment
* in the child, so its view of `@hypit/*` and of the machine packages those modules declare
* is identical to its parent's.
*
* Only relative paths can be used here: no hook exists yet, and a Distribution ships its
* package sources without any `node_modules` link between them.
*/
import { resolve } from "node:path";

// `bin/hypit.mjs` publishes this for every process it starts; the fallback mirrors
// `packages/video-cli/src/distribution.ts` for callers that import a Distribution directly.
const distributionRoot = resolve(
process.env.HYPIT_DISTRIBUTION_ROOT ?? resolve(import.meta.dirname, "../../.."),
);

const { installDistributionPackageResolution, installExternalPackageResolution } =
await import("../../package-loader-node/src/distribution-resolution.js");
installDistributionPackageResolution([distributionRoot]);

const { hypitHostPackageRoot } = await import("../../runtime-host-node/src/index.js");
installExternalPackageResolution([hypitHostPackageRoot()]);
8 changes: 7 additions & 1 deletion packages/provider-hyperframes-local/src/capture-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ export async function runCaptureProcess(
): Promise<void> {
signal.throwIfAborted();
await new Promise<void>((resolve, reject) => {
const child = spawn(process.execPath, ["--import", import.meta.resolve("tsx"), fileURLToPath(entry)], {
// The launcher's resolver hooks are process-local, so this child preloads the same
// environment before it imports any package the Distribution owns.
const child = spawn(process.execPath, [
"--import", import.meta.resolve("tsx"),
"--import", new URL("./capture-bootstrap.ts", import.meta.url).href,
fileURLToPath(entry),
], {
detached: process.platform !== "win32", windowsHide: true,
stdio: ["ignore", "pipe", "pipe", "ipc"],
});
Expand Down
39 changes: 38 additions & 1 deletion packages/provider-hyperframes-local/test/capture-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import { execFileSync } from "node:child_process";
import { execFileSync, spawnSync } from "node:child_process";
import { runCaptureProcess } from "../src/capture-process.js";
import { resolveExecutionOptions } from "../src/render.js";
import type { CaptureInput } from "../src/capture.js";
Expand Down Expand Up @@ -41,6 +41,43 @@ setInterval(() => {}, 1000);
}
});

test("a capture process resolves Distribution packages without workspace links", async () => {
// A packed Distribution ships its package sources with no node_modules link between them,
// and the launcher's resolver hooks are process-local. Starting this entry outside the
// checkout reproduces exactly that: nothing above it declares @hypit/hyperframes.
const root = await mkdtemp(join(tmpdir(), "hypit-capture-resolution-"));
const tsx = import.meta.resolve("tsx");
const bootstrap = new URL("../src/capture-bootstrap.ts", import.meta.url).href;
try {
const probe = join(root, "probe.mjs");
await writeFile(probe, `const module = await import("@hypit/hyperframes/project");
process.stdout.write("resolved:" + Object.keys(module).join(",") + "\\n");`);

// Control: the preload is the only difference between the two arms below.
const unresolved = spawnSync(process.execPath, ["--import", tsx, probe], { encoding: "utf8", timeout: 120_000 });
assert.notEqual(unresolved.status, 0);
assert.match(unresolved.stderr, /ERR_MODULE_NOT_FOUND/u);

const resolved = spawnSync(process.execPath, ["--import", tsx, "--import", bootstrap, probe],
{ encoding: "utf8", timeout: 120_000 });
assert.equal(resolved.status, 0, resolved.stderr);
assert.match(resolved.stdout, /stageHyperframesProject/u);

// The render entry point must actually pass that preload to its child.
const worker = join(root, "worker.mjs");
await writeFile(worker, `process.once("message", async () => {
const module = await import("@hypit/hyperframes/project");
process.stdout.write("resolved:" + Object.keys(module).join(",") + "\\n");
process.send({ type: "completed" });
});`);
const diagnostics: string[] = [];
await runCaptureProcess({ config: resolveExecutionOptions({}) } as CaptureInput,
new AbortController().signal, () => {}, pathToFileURL(worker),
async (message) => { diagnostics.push(message.message); });
assert.ok(diagnostics.some((message) => message.startsWith("resolved:")), diagnostics.join("\n"));
} finally { await rm(root, { recursive: true, force: true }); }
});

test("renderer stdout and stderr diagnostics are drained before reporting success", async () => {
const root = await mkdtemp(join(tmpdir(), "hypit-render-diagnostics-"));
const messages: import("@hypit/runtime").ExecutionDiagnostic[] = [];
Expand Down
Loading