Environment
|
|
| Hypit |
@hypit/hypit 0.1.8 (npm install); also present on current main = v0.1.9 (2f2bb329) |
| OS |
Windows 11 (10.0.26200.9445) |
| Node |
v22.22.2 |
| npm |
10.9.7 |
| ffmpeg / ffprobe |
8.1.1 |
| Chrome |
C:\Program Files\Google\Chrome\Application\chrome.exe |
| Runtime Profile |
only media.local + hyperframes.local, no managed endpoints |
| Machine package root |
HYPIT_STATE_HOME set |
Reproduction
Same Source that renders fine from a source checkout:
$ hypit check productions/hello-world/chat.svml # OK
$ hypit plan productions/hello-world/chat.svrun # OK · 3 requests · all local · no Provider charge
$ hypit build productions/hello-world/chat.svrun \
--runtime productions/hello-world/hello.runtime.json --follow
Progress then fails:
· Working · 1/3 steps complete · 1 staging / starting browsers / rendering frames # visual render starts
x Render failed
Cannot find package '@hypit/hyperframes'
Worth noting the order: Cannot find package '@hyperframes/engine' is reported first
(it goes away once the package is installed into the machine npm root), then
Cannot find package '@hypit/hyperframes' appears — and that one cannot be installed,
because @hypit/hyperframes is not published on npm.
Expected
The same Source completes local visual render → audio → mux → MP4 under the npm distribution,
with plan still reporting no Provider charge.
Actual
The local render child process never starts. build fails at the visual-render stage.
(Audio and muxing stages do work.)
Root cause
1. The resolver hooks live in the parent process; rendering happens in a child process
bin/hypit.mjs:
24 register(); // tsx: allow importing TS sources
29 installDistributionPackageResolution([distributionRoot]); // resolver for @hypit/*
31 installExternalPackageResolution([hypitHostPackageRoot()]); // machine-npm fallback
packages/package-loader-node/src/distribution-resolution.ts:87 registers these via
module.registerHooks(...). module.registerHooks is process-local and synchronous —
a brand-new Node process started with child_process.spawn does not inherit it (Node has no
mechanism for passing hooks through spawn).
2. The render child process happens to not go through the launcher
packages/provider-hyperframes-local/src/capture-process.ts:
const child = spawn(process.execPath, ["--import", import.meta.resolve("tsx"), fileURLToPath(entry)], {
detached: process.platform !== "win32", windowsHide: true,
stdio: ["ignore", "pipe", "pipe", "ipc"],
});
--import tsx installs TypeScript support only — no Hypit resolver hooks. That child is
left with nothing but Node's native "walk up looking for node_modules".
3. The child's dependency graph has runtime @hypit/* imports
capture-worker.ts
└─ import { captureStagedVisual } from "./capture.js"
└─ capture.ts:10 import { renderWorkerLimit } from "./render.js" <== runtime import, not type-only
└─ render.ts:6 import { stageHyperframesProject } from "@hypit/hyperframes/project"
render.ts:7 import { sealRenderedVisual } from "@hypit/media"
render.ts:9 import { verifyCompositableSurfaceBytes } from "@hypit/media-execution"
render.ts:10 import { verifyHyperframesVisualRequest } from "@hypit/render-hyperframes"
render.ts:12 import { isStreamingResourceStore } from "@hypit/runtime"
(capture.ts:3,4 and output.ts:1 import @hypit/hyperframes as import type, which is erased.
The fatal one is capture.ts:10, a runtime import that drags render.ts into the child graph.)
4. So "works from source, fails from npm" reduces to node_modules symlinks
| Resolution start point |
import('@hypit/hyperframes/project') |
source <repo>/packages/provider-hyperframes-local/ |
RESOLVED ok · exports = stageHyperframesProject |
npm distribution .../node_modules/@hypit/hypit/packages/provider-hyperframes-local/ |
FAILED ERR_MODULE_NOT_FOUND: Cannot find package '@hypit/hyperframes' |
Why:
# source: pnpm symlinks workspace deps into each package's own node_modules
packages/provider-hyperframes-local/node_modules/@hypit/
hyperframes -> <repo>/packages/hyperframes (13 @hypit/* symlinks)
packages/provider-hyperframes-local/node_modules/@hyperframes/
engine -> .../@hyperframes+engine@0.7.101/...
producer -> .../@hyperframes+producer@0.7.101/...
# npm distribution: no node_modules inside the embedded package dirs at all
MISSING <project>/node_modules/@hypit/hypit/packages/provider-hyperframes-local/node_modules
<project>/node_modules/@hypit/ -> contains only `hypit`
Extra measurement (import.meta.resolve in the same process, before vs after registering hooks):
=== before hooks (== child process situation) ===
before @hyperframes/engine -> FAILED ERR_MODULE_NOT_FOUND
before @hypit/hyperframes -> FAILED ERR_MODULE_NOT_FOUND
=== after hooks (== parent process situation) ===
after @hyperframes/engine -> file:///<HYPIT_STATE_HOME>/packages/@hyperframes/engine/0.7.101/...
after @hypit/hyperframes -> file:///<project>/node_modules/@hypit/hypit/packages/hyperframes/...
In short: the parent is fine; the child is simply missing its copy of the hooks.
Classification
A packaging bug with a spawn-environment bug on top.
- packaging:
package.json files packs packages/*/src/**/* and packages/*/package.json
into the tarball, but nothing gives those embedded packages a node_modules resolution
context. @hypit/* belongs to the Distribution and is owned by the hooks — and the hooks
only exist in the parent.
- spawn:
capture-process.ts starts the child with tsx but without a bootstrap module that
installs the resolver hooks. That implicitly requires the runtime to already have symlinks,
which is only true in a pnpm/workspace checkout.
- docs: the local-rendering docs target the source checkout (
node bin/hypit.mjs +
pnpm build:public-types), while the README Quickstart also offers npx @hypit/hypit.
Under that documented consumption path, local rendering cannot work by construction —
the published shape and the docs disagree.
Suggested fix (option A, recommended)
Give the child process the hooks too. Add a tiny internal bootstrap module, e.g.
packages/package-loader-node/src/child-bootstrap.ts:
import { installDistributionPackageResolution, installExternalPackageResolution } from "./distribution-resolution.js";
import { hypitHostPackageRoot } from "@hypit/runtime-host-node";
and change the spawn args to:
spawn(process.execPath, [
"--import", import.meta.resolve("tsx"),
"--import", import.meta.resolve("@hypit/package-loader-node/child-bootstrap"), // <== new
fileURLToPath(entry),
], { /* unchanged */ });
The hooks locate the Distribution root via HYPIT_DISTRIBUTION_ROOT
(bin/hypit.mjs:23 already exports it into process.env, so the child inherits it) —
no new protocol needed.
Note: in a real fix the bootstrap module has to live somewhere that can depend on both
package-loader-node and runtime-host-node; package-loader-node itself cannot, because
runtime-host-node already depends on it (cycle). Placing it in
provider-hyperframes-local/src/ is the smallest change that compiles today.
Alternatives considered:
- Option B (weaker): move pure helpers like
renderWorkerLimit out of render.ts so
capture.ts stops pulling it in. This only postpones the problem — the child must
eventually execute Distribution code.
- Option C (release-side): make embedded packages genuinely resolvable (e.g. generate a
runtime resolution map for packages/* in bin/hypit.mjs). Larger blast radius than A.
Option A turns "Distribution resolution" from a per-process fact into part of the child-process
launch protocol — the same idea already used by
workerLaunch.args = [installedLauncher] (packages/video-cli/src/distribution.ts:60-68),
which solves the identical problem by re-entering the launcher. This spawn site was missed.
Status on my side
I have implemented option A locally and verified it end to end against a real packed
tarball (npm run pack:distribution → hypit-hypit-0.1.9.tgz, 1041 entries, 0 node_modules,
0 symlinks) installed into an isolated directory:
| Stage |
Result |
check |
✅ valid |
plan --runtime |
✅ 3 requests / 3 local / no Provider charge ×3 |
build --follow |
✅ 240/240 frames |
| MP4 |
h264 High 540×960 30fps 240 frames + aac stereo, zero decode errors |
| Depends on source tree |
no — zero references to the checkout inside the install, NODE_PATH unset |
| Regression test |
new case not ok before the fix (byte-identical error) / ok after |
A PR follows. Happy to move the bootstrap module wherever maintainers prefer.
Notes
- Evidence comes from read-only investigation plus an isolated A/B child process; no installed
files were modified.
- Independent of the
optionalDependencies font-resolution issue. Fixing either does not fix
the other.
- The source-checkout path is not blocked by this bug: fully-local $0 rendering works there
(measured: 240 frames → MP4). Only the npm consumption path is blocked.
Environment
@hypit/hypit0.1.8 (npm install); also present on currentmain= v0.1.9 (2f2bb329)C:\Program Files\Google\Chrome\Application\chrome.exemedia.local+hyperframes.local, no managed endpointsHYPIT_STATE_HOMEsetReproduction
Same Source that renders fine from a source checkout:
Progress then fails:
Worth noting the order:
Cannot find package '@hyperframes/engine'is reported first(it goes away once the package is installed into the machine npm root), then
Cannot find package '@hypit/hyperframes'appears — and that one cannot be installed,because
@hypit/hyperframesis not published on npm.Expected
The same Source completes local visual render → audio → mux → MP4 under the npm distribution,
with
planstill reporting no Provider charge.Actual
The local render child process never starts.
buildfails at the visual-render stage.(Audio and muxing stages do work.)
Root cause
1. The resolver hooks live in the parent process; rendering happens in a child process
bin/hypit.mjs:packages/package-loader-node/src/distribution-resolution.ts:87registers these viamodule.registerHooks(...).module.registerHooksis process-local and synchronous —a brand-new Node process started with
child_process.spawndoes not inherit it (Node has nomechanism for passing hooks through
spawn).2. The render child process happens to not go through the launcher
packages/provider-hyperframes-local/src/capture-process.ts:--import tsxinstalls TypeScript support only — no Hypit resolver hooks. That child isleft with nothing but Node's native "walk up looking for
node_modules".3. The child's dependency graph has runtime
@hypit/*imports(
capture.ts:3,4andoutput.ts:1import@hypit/hyperframesasimport type, which is erased.The fatal one is
capture.ts:10, a runtime import that dragsrender.tsinto the child graph.)4. So "works from source, fails from npm" reduces to
node_modulessymlinksimport('@hypit/hyperframes/project')<repo>/packages/provider-hyperframes-local/exports = stageHyperframesProject.../node_modules/@hypit/hypit/packages/provider-hyperframes-local/ERR_MODULE_NOT_FOUND: Cannot find package '@hypit/hyperframes'Why:
Extra measurement (
import.meta.resolvein the same process, before vs after registering hooks):In short: the parent is fine; the child is simply missing its copy of the hooks.
Classification
A packaging bug with a spawn-environment bug on top.
package.jsonfilespackspackages/*/src/**/*andpackages/*/package.jsoninto the tarball, but nothing gives those embedded packages a
node_modulesresolutioncontext.
@hypit/*belongs to the Distribution and is owned by the hooks — and the hooksonly exist in the parent.
capture-process.tsstarts the child with tsx but without a bootstrap module thatinstalls the resolver hooks. That implicitly requires the runtime to already have symlinks,
which is only true in a pnpm/workspace checkout.
node bin/hypit.mjs+pnpm build:public-types), while the README Quickstart also offersnpx @hypit/hypit.Under that documented consumption path, local rendering cannot work by construction —
the published shape and the docs disagree.
Suggested fix (option A, recommended)
Give the child process the hooks too. Add a tiny internal bootstrap module, e.g.
packages/package-loader-node/src/child-bootstrap.ts:and change the spawn args to:
The hooks locate the Distribution root via
HYPIT_DISTRIBUTION_ROOT(
bin/hypit.mjs:23already exports it intoprocess.env, so the child inherits it) —no new protocol needed.
Note: in a real fix the bootstrap module has to live somewhere that can depend on both
package-loader-nodeandruntime-host-node;package-loader-nodeitself cannot, becauseruntime-host-nodealready depends on it (cycle). Placing it inprovider-hyperframes-local/src/is the smallest change that compiles today.Alternatives considered:
renderWorkerLimitout ofrender.tssocapture.tsstops pulling it in. This only postpones the problem — the child musteventually execute Distribution code.
runtime resolution map for
packages/*inbin/hypit.mjs). Larger blast radius than A.Option A turns "Distribution resolution" from a per-process fact into part of the child-process
launch protocol — the same idea already used by
workerLaunch.args = [installedLauncher](packages/video-cli/src/distribution.ts:60-68),which solves the identical problem by re-entering the launcher. This spawn site was missed.
Status on my side
I have implemented option A locally and verified it end to end against a real packed
tarball (
npm run pack:distribution→hypit-hypit-0.1.9.tgz, 1041 entries, 0node_modules,0 symlinks) installed into an isolated directory:
checkplan --runtimeno Provider charge×3build --followh264 High 540×960 30fps 240 frames + aac stereo, zero decode errorsNODE_PATHunsetnot okbefore the fix (byte-identical error) /okafterA PR follows. Happy to move the bootstrap module wherever maintainers prefer.
Notes
files were modified.
optionalDependenciesfont-resolution issue. Fixing either does not fixthe other.
(measured: 240 frames → MP4). Only the npm consumption path is blocked.