Setting the file. One moment.
Package Loader · Hyperframes Animation · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page Blueprints Index
— line 233
This file
Number 18.6
Position 6 of 121
Type JavaScript
Size 14 KB
Lines 415 scripts/ package-loader.mjs
JavaScript · 415 lines · 14 KB
{ existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync }
from
"node:fs"
;
13 import { createRequire } from "node:module" ;
14 import { tmpdir } from "node:os" ;
15 import { basename, delimiter, dirname, join, parse, resolve, win32 as win32Path } from "node:path" ;
16 import { createInterface } from "node:readline/promises" ;
17 import { fileURLToPath, pathToFileURL } from "node:url" ;
18
19 const HERE = dirname ( fileURLToPath ( import . meta .url));
20 const VERSION_OVERRIDE_ENV = "HYPERFRAMES_SKILL_PKG_VERSION" ;
21 const BOOTSTRAP_ENV = "HYPERFRAMES_SKILL_DEPS_BOOTSTRAPPED" ;
22 const BOOTSTRAP_CONFIRM_ENV = "HYPERFRAMES_SKILL_BOOTSTRAP_DEPS" ;
23 const NODE_MODULES_ENV = "HYPERFRAMES_SKILL_NODE_MODULES" ;
24
25 export async function importPackagesOrBootstrap ( packageNames , options = {}) {
26 const entries = new Map ();
27 const missing = [];
28
29 for ( const packageName of packageNames) {
30 const entry = resolvePackageEntry (packageName);
31 if (entry) entries. set (packageName, entry);
32 else missing. push (packageName);
33 }
34
35 if (missing. length > 0 && ! process.env[ BOOTSTRAP_ENV ]) {
36 const npmPackages = options.npmPackages ?? missing;
37 assertPinnedPackageSpecs (npmPackages);
38 await confirmBootstrap (npmPackages);
39 bootstrapWithNpmInstall (npmPackages);
40 }
41
42 if (missing. length > 0 ) {
43 throw new Error (
44 [
45 `Could not resolve required package(s): ${ missing . join ( ", " ) }` ,
46 "Install them in this project, for example:" ,
47 ` npm install --save-dev ${ packageNames . map ( shellQuote ). join ( " " ) }` ,
48 ]. join ( " \n " ),
49 );
50 }
51
52 const modules = {};
53 for ( const [ packageName , entry ] of entries) {
54 modules[packageName] = await import ( pathToFileURL (entry).href);
55 }
56 return modules;
57 }
58
59 export async function bundleCompositionForCapture ( compiler , projectDir ) {
60 const compiledDir = mkdtempSync ( join ( tmpdir (), "hyperframes-skill-bundle-" ));
61 try {
62 const html = await compiler. bundleToSingleHtml (projectDir);
63 writeFileSync ( join (compiledDir, "index.html" ), html);
64 return {
65 compiledDir,
66 cleanup () {
67 rmSync (compiledDir, { recursive: true , force: true });
68 },
69 };
70 } catch (error) {
71 rmSync (compiledDir, { recursive: true , force: true });
72 throw error;
73 }
74 }
75
76 // ── Transient-init retry ─────────────────────────────────────────────────────
77 // Frozen snapshot of the engine's TRANSIENT_BROWSER_ERROR_PATTERNS (see
78 // packages/engine frameCapture.ts), used only when the imported
79 // @hyperframes/producer predates the isTransientBrowserError re-export. The
80 // last pattern is the load-bearing one for modular projects: sub-composition
81 // timelines register asynchronously, so a first init attempt can time out as
82 // "zero duration / Runtime ready: false" on a valid project.
83 const FALLBACK_TRANSIENT_PATTERNS = [
84 /Navigating frame was detached/ i ,
85 /Target closed/ i ,
86 /Session closed/ i ,
87 /browser has disconnected/ i ,
88 /Page crashed/ i ,
89 /Execution context was destroyed/ i ,
90 /Cannot find context with specified id/ i ,
91 /Failed to launch the browser process/ i ,
92 /Navigation timeout of \d + ms exceeded/ i ,
93 /ECONNREFUSED/ i ,
94 /net::ERR_NETWORK_CHANGED/ i ,
95 /Composition has zero duration [\s\S] * Runtime ready: false/ ,
96 ];
97
98 /**
99 * Create + initialize a capture session with the canonical transient-init
100 * retry/cleanup the render pipeline uses (see probeStage in
101 * @hyperframes/producer): on a transient failure, close the crashed session
102 * and retry ONCE with a fresh browser. Without this, a standalone helper
103 * false-fails valid modular projects whose sub-composition timelines land a
104 * beat after the first readiness deadline ("zero duration" with
105 * "Runtime ready: false").
106 *
107 * `producer` is the imported @hyperframes/producer namespace;
108 * `createSession` is a factory returning a fresh (uninitialized) session.
109 * Non-transient init failures (e.g. the "Runtime ready: true" zero-duration
110 * fast-fail — a genuine authoring bug) still throw on the first attempt.
111 */
112 export async function initializeSessionWithRetry ( producer , createSession , options = {}) {
113 const maxAttempts = options.maxAttempts ?? 2 ;
114 const log = options.log ?? (( message ) => console. error (message));
115 const isTransient =
116 typeof producer.isTransientBrowserError === "function"
117 ? producer.isTransientBrowserError
118 : ( err ) => {
119 const message = err instanceof Error ? err.message : String (err);
120 return FALLBACK_TRANSIENT_PATTERNS . some (( pattern ) => pattern. test (message));
121 };
122
123 for ( let attempt = 1 ; ; attempt ++ ) {
124 const session = await createSession ();
125 try {
126 await producer. initializeSession (session);
127 return session;
128 } catch (error) {
129 await producer. closeCaptureSession (session). catch (() => {});
130 if (attempt >= maxAttempts || ! isTransient (error)) throw error;
131 log (
132 `transient browser-init failure (attempt ${ attempt }/${ maxAttempts }): ${
133 error instanceof Error ? error . message : String ( error )
134 }` ,
135 );
136 log ( "retrying with a fresh browser session..." );
137 }
138 }
139 }
140
141 export function hyperframesPackageSpec ( packageName ) {
142 const override = process.env[ VERSION_OVERRIDE_ENV ]?. trim ();
143 if (override) return `${ packageName }@${ override }` ;
144
145 const version = readBundledHyperframesVersion ();
146 if (version) return `${ packageName }@${ version }` ;
147
148 // Global skill installs have no hyperframes package.json
149 // in their ancestor chain, so the bundled version is unknowable. Fall back to
150 // @latest instead of throwing: already-installed packages still import, and a
151 // bootstrap install can still proceed (@latest satisfies the pinned-spec guard).
152 process.stderr. write (
153 [
154 `hyperframes: could not determine the bundled version for ${ packageName }; using @latest.` ,
155 `Set ${ VERSION_OVERRIDE_ENV }=<version> to pin it.` ,
156 "" ,
157 ]. join ( " \n " ),
158 );
159 return `${ packageName }@latest` ;
160 }
161
162 function resolvePackageEntry ( packageName ) {
163 const bases = [process. cwd (), HERE , ... envNodeModulesDirs (), ... nodeModulesDirsFromPath ()];
164 const { rootName , subpath } = splitPackageSpecifier (packageName);
165
166 const seen = new Set ();
167 for ( const base of bases) {
168 const normalized = resolve (base);
169 if (seen. has (normalized)) continue ;
170 seen. add (normalized);
171
172 try {
173 return createRequire ( join (normalized, "__hyperframes_skill_loader__.cjs" )). resolve (
174 packageName,
175 );
176 } catch {
177 const packageDir = findPackageDir (normalized, rootName);
178 const packageEntry = packageDir ? readPackageEntry (packageDir, subpath) : null ;
179 if (packageEntry) return packageEntry;
180 }
181 }
182
183 return null ;
184 }
185
186 function splitPackageSpecifier ( packageName ) {
187 const segments = packageName. split ( "/" );
188 const rootLength = packageName. startsWith ( "@" ) ? 2 : 1 ;
189 return {
190 rootName: segments. slice ( 0 , rootLength). join ( "/" ),
191 subpath: segments. slice (rootLength). join ( "/" ),
192 };
193 }
194
195 function readBundledHyperframesVersion () {
196 for ( const ancestor of ancestors ( HERE )) {
197 const directVersion = readPackageVersion ( join (ancestor, "package.json" ));
198 if (directVersion) return directVersion;
199
200 const monorepoCliVersion = readPackageVersion (
201 join (ancestor, "packages" , "cli" , "package.json" ),
202 );
203 if (monorepoCliVersion) return monorepoCliVersion;
204 }
205 return null ;
206 }
207
208 function readPackageVersion ( packageJsonPath ) {
209 try {
210 const manifest = JSON . parse ( readFileSync (packageJsonPath, "utf8" ));
211 if (manifest.name === "hyperframes" || manifest.name === "@hyperframes/cli" ) {
212 return typeof manifest.version === "string" ? manifest.version : null ;
213 }
214 } catch {
215 // Keep searching ancestor package manifests.
216 }
217 return null ;
218 }
219
220 function envNodeModulesDirs () {
221 return (process.env[ NODE_MODULES_ENV ] ?? "" ). split (delimiter). filter (Boolean);
222 }
223
224 function nodeModulesDirsFromPath () {
225 const dirs = [];
226 for ( const entry of (process.env. PATH ?? "" ). split (delimiter)) {
227 if ( ! entry. endsWith ( `${ join ( "node_modules" , ".bin" ) }` )) continue ;
228 dirs. push ( dirname (entry));
229 }
230 return dirs;
231 }
232
233 function findPackageDir ( base , packageName ) {
234 const packageSegments = packageName. split ( "/" );
235 const roots =
236 basename (base) === "node_modules"
237 ? [base]
238 : ancestors (base). map (( ancestor ) => join (ancestor, "node_modules" ));
239
240 for ( const root of roots) {
241 const packageDir = join (root, ... packageSegments);
242 if ( existsSync ( join (packageDir, "package.json" ))) return packageDir;
243 }
244 return null ;
245 }
246
247 function readPackageEntry ( packageDir , subpath = "" ) {
248 try {
249 const manifest = JSON . parse ( readFileSync ( join (packageDir, "package.json" ), "utf8" ));
250 const requestedExport = subpath ? manifest.exports?.[ `./${ subpath }` ] : manifest.exports;
251 const entry =
252 exportEntry (requestedExport) ??
253 ( ! subpath ? (manifest.module ?? manifest.main ?? "index.js" ) : null );
254 if ( ! entry) return null ;
255 const entryPath = join (packageDir, entry);
256 return existsSync (entryPath) ? entryPath : null ;
257 } catch {
258 return null ;
259 }
260 }
261
262 function exportEntry ( exports ) {
263 const root =
264 typeof exports === "object" && exports !== null ? ( exports [ "." ] ?? exports ) : exports ;
265 if ( typeof root === "string" ) return root;
266 if ( typeof root !== "object" || root === null ) return null ;
267 if ( typeof root.import === "string" ) return root.import;
268 if ( typeof root.default === "string" ) return root.default;
269 if ( typeof root.node === "string" ) return root.node;
270 if ( typeof root.node === "object" && root.node !== null ) {
271 return root.node.import ?? root.node.default ?? null ;
272 }
273 return null ;
274 }
275
276 function assertPinnedPackageSpecs ( packageSpecs ) {
277 const unpinned = packageSpecs. filter (( spec ) => ! hasVersionSpec (spec));
278 if (unpinned. length === 0 ) return ;
279 throw new Error (
280 [
281 `Refusing to bootstrap unpinned package spec(s): ${ unpinned . join ( ", " ) }` ,
282 "Pass pinned npm package specs, for example:" ,
283 ` ${ packageSpecs . map (( spec ) => ( hasVersionSpec ( spec ) ? spec : `${ spec }@<version>` )). join ( " " ) }` ,
284 ]. join ( " \n " ),
285 );
286 }
287
288 function hasVersionSpec ( packageSpec ) {
289 if (packageSpec. startsWith ( "@" )) {
290 const slash = packageSpec. indexOf ( "/" );
291 return slash !== - 1 && packageSpec. indexOf ( "@" , slash + 1 ) !== - 1 ;
292 }
293 return packageSpec. includes ( "@" );
294 }
295
296 async function confirmBootstrap ( packageSpecs ) {
297 if (process.env[ BOOTSTRAP_CONFIRM_ENV ] === "1" ) return ;
298
299 const installLine = `npm install --ignore-scripts --no-save ${ packageSpecs . map ( shellQuote ). join ( " " ) }` ;
300 if ( ! process.stdin.isTTY) {
301 throw new Error (
302 [
303 "Required helper package(s) are missing." ,
304 "To allow a one-time temporary dependency bootstrap for this run, set:" ,
305 ` ${ BOOTSTRAP_CONFIRM_ENV }=1` ,
306 "The bootstrap command will be:" ,
307 ` ${ installLine }` ,
308 ]. join ( " \n " ),
309 );
310 }
311
312 const rl = createInterface ({ input: process.stdin, output: process.stderr });
313 try {
314 const answer = await rl. question (
315 [
316 "HyperFrames helper package(s) are missing." ,
317 `Run a temporary install with lifecycle scripts disabled?` ,
318 ` ${ installLine }` ,
319 "Proceed? [y/N] " ,
320 ]. join ( " \n " ),
321 );
322 if ( ! / ^ (y | yes) $ / i . test (answer. trim ())) {
323 throw new Error ( "Dependency bootstrap cancelled." );
324 }
325 } finally {
326 rl. close ();
327 }
328 }
329
330 function ancestors ( start ) {
331 const dirs = [];
332 let current = resolve (start);
333 const root = parse (current).root;
334 while (current && current !== root) {
335 dirs. push (current);
336 current = dirname (current);
337 }
338 dirs. push (root);
339 return dirs;
340 }
341
342 export function resolveNpmSpawnCommand (
343 args ,
344 platform = process.platform,
345 env = process.env,
346 nodeExecPath = process.execPath,
347 pathExists = existsSync,
348 ) {
349 if (platform !== "win32" ) {
350 return { cmd: "npm" , args, opts: { stdio: "inherit" } };
351 }
352
353 const bundledNpmCli = win32Path. join (
354 win32Path. dirname (nodeExecPath),
355 "node_modules" ,
356 "npm" ,
357 "bin" ,
358 "npm-cli.js" ,
359 );
360 const npmCli = [env.npm_execpath, bundledNpmCli]. find (
361 ( candidate ) => candidate && pathExists (candidate),
362 );
363 if ( ! npmCli) return null ;
364 return {
365 cmd: env.npm_node_execpath || nodeExecPath,
366 args: [npmCli, ... args],
367 opts: { stdio: "inherit" , windowsHide: true },
368 };
369 }
370
371 function bootstrapWithNpmInstall ( packageNames ) {
372 const installRoot = mkdtempSync ( join ( tmpdir (), "hyperframes-skill-deps-" ));
373 const npmArgs = [
374 "install" ,
375 "--silent" ,
376 "--no-audit" ,
377 "--no-fund" ,
378 "--ignore-scripts" ,
379 "--no-save" ,
380 "--prefix" ,
381 installRoot,
382 ... packageNames,
383 ];
384 const npmCommand = resolveNpmSpawnCommand (npmArgs);
385 if ( ! npmCommand) {
386 rmSync (installRoot, { recursive: true , force: true });
387 throw new Error ( "Could not locate npm-cli.js for dependency bootstrap on Windows." );
388 }
389 const installResult = spawnSync (npmCommand.cmd, npmCommand.args, npmCommand.opts);
390
391 if (installResult.error) throw installResult.error;
392 if (installResult.status !== 0 ) {
393 rmSync (installRoot, { recursive: true , force: true });
394 process. exit (installResult.status ?? 1 );
395 }
396
397 const args = [ ... process.argv. slice ( 1 )];
398 const result = spawnSync (process.execPath, args, {
399 stdio: "inherit" ,
400 env: {
401 ... process.env,
402 [ BOOTSTRAP_ENV ]: "1" ,
403 [ NODE_MODULES_ENV ]: join (installRoot, "node_modules" ),
404 },
405 });
406
407 rmSync (installRoot, { recursive: true , force: true });
408 if (result.error) throw result.error;
409 process. exit (result.status ?? 1 );
410 }
411
412 function shellQuote ( value ) {
413 if ( / ^ [A-Za-z0-9_./:@=-] +$ / . test (value)) return value;
414 return `'${ value . replace ( /'/ g , "' \\ ''" ) }'` ;
415 }