Setting the file. One moment.
Browser Tooling · Wix Headless Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page 28.10
Workflow
async function tryLoadPackage
— line 235
This file
Number 28.37
Position 37 of 89
Type JavaScript
Size 19 KB
Lines 570 scripts/lib/ browser-tooling.mjs
JavaScript · 570 lines · 19 KB
export
async
function
resolveBrowserToolingContext
({
startDir
=
process.
cwd
() }
=
{}) {
11 const projectRoot = await findNearestPackageRoot (startDir);
12 if ( ! projectRoot) {
13 return {
14 ok: false ,
15 projectRoot: null ,
16 packageManager: "unknown" ,
17 manifest: null ,
18 requireFromRoot: null ,
19 reason: `No package.json was found above ${ startDir }.` ,
20 };
21 }
22
23 const manifest = await readManifest (projectRoot);
24 const packageManager = detectPackageManager (manifest);
25 const nodeLinker = await detectNodeLinker (projectRoot);
26 return {
27 ok: true ,
28 projectRoot,
29 packageManager,
30 nodeLinker,
31 manifest,
32 requireFromRoot: createRequire (path. join (projectRoot, "package.json" )),
33 };
34 }
35
36 export async function runBrowserExtractionPreflight ({ startDir = process. cwd (), fix = false } = {}) {
37 const context = await resolveBrowserToolingContext ({ startDir });
38 const report = {
39 ok: false ,
40 context: {
41 projectRoot: context.projectRoot,
42 packageManager: context.packageManager,
43 nodeLinker: context.nodeLinker || "unknown" ,
44 },
45 checks: [],
46 remediation: {
47 commands: [],
48 notes: [],
49 autoFixAttempted: false ,
50 autoFixSucceeded: false ,
51 },
52 };
53
54 if ( ! context.ok) {
55 report.checks. push ( failCheck ( "project_root" , context.reason));
56 report.remediation.notes. push (
57 "Create or choose a project root with a package.json, then add playwright and provide a runnable design-md-generator before retrying browser extraction." ,
58 );
59 return report;
60 }
61
62 const declaredPackages = getDeclaredPackages (context.manifest);
63 const requiredPackages = await requiredPackageSpecsForContext (context);
64 const missingDeclarations = Object. keys (requiredPackages). filter (( name ) => ! declaredPackages. has (name));
65 report.checks. push (
66 missingDeclarations. length === 0
67 ? passCheck ( "dependency_declarations" , "Required browser-extraction packages are declared in package.json." )
68 : failCheck ( "dependency_declarations" , `Missing package.json declarations for: ${ missingDeclarations . join ( ", " ) }` ),
69 );
70
71 report.checks. push (
72 context.packageManager !== "yarn" || context.nodeLinker === "node-modules"
73 ? passCheck ( "package_manager_layout" , context.packageManager === "yarn"
74 ? "Yarn nodeLinker is set to node-modules."
75 : `Package manager ${ context . packageManager } does not require a Yarn nodeLinker check.` )
76 : failCheck (
77 "package_manager_layout" ,
78 `Yarn project ${ context . projectRoot } is using nodeLinker=${ context . nodeLinker || "unknown"}. Browser extraction requires node-modules so host binaries and browser/design tooling resolve consistently.` ,
79 ),
80 );
81
82 const installDeclaredCommand = installCommandForContext (context, missingDeclarations, requiredPackages);
83 if (installDeclaredCommand) {
84 report.remediation.commands. push (installDeclaredCommand);
85 }
86 const linkerRemediationCommand = nodeLinkerRemediationCommand (context);
87 if (linkerRemediationCommand) {
88 report.remediation.commands. push (linkerRemediationCommand);
89 report.remediation.notes. push (
90 "Yarn Plug'n'Play is not compatible with the current browser/design extraction toolchain because the host repo needs a real node_modules layout for host binaries and runtime resolution." ,
91 );
92 }
93
94 let playwright = null ;
95 const playwrightLoad = await tryLoadPackage (context, "playwright" );
96 if (playwrightLoad.ok) {
97 playwright = playwrightLoad.module;
98 report.checks. push ( passCheck ( "playwright_package" , `Resolved playwright from ${ playwrightLoad . resolvedPath }` ));
99 } else {
100 report.checks. push ( failCheck ( "playwright_package" , playwrightLoad.message));
101 }
102
103 const designMdGenerator = await resolveDesignMdGeneratorExecutionFromContext (context);
104 if (designMdGenerator.ok) {
105 report.checks. push (
106 passCheck (
107 "design_md_generator" ,
108 `Resolved design-md-generator in ${ designMdGenerator . mode } mode at ${ designMdGenerator . rootDir } using ${ designMdGenerator . commandDescription }.` ,
109 ),
110 );
111 } else {
112 report.checks. push ( failCheck ( "design_md_generator" , designMdGenerator.message));
113 report.remediation.notes. push ( ... (designMdGenerator.notes || []));
114 }
115
116 const browserInstallCommand = browserInstallCommandForContext (context);
117 let chromiumOkay = false ;
118 if (playwright) {
119 const launch = await tryLaunchChromium (playwright);
120 chromiumOkay = launch.ok;
121 report.checks. push (
122 launch.ok
123 ? passCheck ( "chromium_runtime" , "Chromium launched successfully." )
124 : failCheck ( "chromium_runtime" , launch.message),
125 );
126 if ( ! launch.ok && browserInstallCommand && ! launch.environmentBlocked) {
127 report.remediation.commands. push (browserInstallCommand);
128 }
129 if ( ! launch.ok && launch.environmentBlocked) {
130 report.remediation.notes. push (
131 "Chromium is installed but the current execution environment blocked browser launch. Retry from a normal local shell/session or rerun with the browser-launch permission/escalation your agent runtime requires before treating this as a hard blocker." ,
132 );
133 }
134 }
135
136 const needsInstall = missingDeclarations. length > 0 || ! playwrightLoad.ok;
137 const needsNodeModulesLayout = Boolean (linkerRemediationCommand);
138 if (fix && (needsInstall || ! chromiumOkay || needsNodeModulesLayout)) {
139 report.remediation.autoFixAttempted = true ;
140 const succeeded = await attemptAutoFix ({
141 context,
142 installDeclaredCommand,
143 runInstall: needsInstall,
144 runBrowserInstall: ! chromiumOkay,
145 updateNodeLinker: needsNodeModulesLayout,
146 });
147 report.remediation.autoFixSucceeded = succeeded.ok;
148 report.remediation.notes. push ( ... succeeded.notes);
149 if (succeeded.ok) {
150 return runBrowserExtractionPreflight ({ startDir, fix: false });
151 }
152 }
153
154 report.ok = report.checks. every (( check ) => check.status !== "fail" );
155 return report;
156 }
157
158 export async function ensureBrowserExtractionReady ({ startDir = process. cwd (), fix = false } = {}) {
159 const report = await runBrowserExtractionPreflight ({ startDir, fix });
160 if ( ! report.ok) {
161 const lines = [];
162 for ( const check of report.checks. filter (( item ) => item.status === "fail" )) {
163 lines. push ( `- ${ check . id }: ${ check . message }` );
164 }
165 const commands = report.remediation.commands. length
166 ? `Suggested fixes: \n ${ report . remediation . commands . map (( command ) => ` ${ command }` ). join ( " \n " ) }`
167 : "Suggested fixes: none were auto-derived; inspect the failing checks." ;
168 throw new Error (
169 `Browser extraction preflight failed. \n ${ lines . join ( " \n " ) } \n ${ commands } \n ` +
170 "If browser launch was blocked by the execution environment, rerun with the required browser-launch escalation before treating the run as blocked. \n " +
171 "If this repo uses the root helper scripts, run `corepack yarn browser-extraction:preflight --fix` from the project root." ,
172 );
173 }
174 return resolveBrowserToolingContext ({ startDir });
175 }
176
177 export async function loadPlaywrightFromContext ( context ) {
178 const loaded = await tryLoadPackage (context, "playwright" );
179 if ( ! loaded.ok) {
180 throw new Error (loaded.message);
181 }
182 return loaded.module;
183 }
184
185 export async function resolveDesignMdGeneratorFromContext ( context ) {
186 const result = await resolveDesignMdGeneratorExecutionFromContext (context);
187 if ( ! result.ok) {
188 throw new Error (result.message);
189 }
190 return result;
191 }
192
193 async function findNearestPackageRoot ( startDir ) {
194 let current = path. resolve (startDir);
195 while ( true ) {
196 if ( await pathExists (path. join (current, "package.json" ))) {
197 return current;
198 }
199 const parent = path. dirname (current);
200 if (parent === current) return null ;
201 current = parent;
202 }
203 }
204
205 async function readManifest ( projectRoot ) {
206 return JSON . parse ( await readFile (path. join (projectRoot, "package.json" ), "utf8" ));
207 }
208
209 async function detectNodeLinker ( projectRoot ) {
210 const yarnrcPath = path. join (projectRoot, ".yarnrc.yml" );
211 if ( ! ( await pathExists (yarnrcPath))) return null ;
212 const yarnrc = await readFile (yarnrcPath, "utf8" );
213 const match = yarnrc. match ( / ^ \s * nodeLinker: \s * ( [ ^ \s#] + ) \s *$ / m );
214 return match ? match[ 1 ] : null ;
215 }
216
217 function detectPackageManager ( manifest ) {
218 const raw = String (manifest?.packageManager || "" ). trim ();
219 if (raw. startsWith ( "yarn@" )) return "yarn" ;
220 if (raw. startsWith ( "pnpm@" )) return "pnpm" ;
221 if (raw. startsWith ( "npm@" )) return "npm" ;
222 return "npm" ;
223 }
224
225 function getDeclaredPackages ( manifest ) {
226 return new Set (
227 [
228 ... Object. keys (manifest?.dependencies || {}),
229 ... Object. keys (manifest?.devDependencies || {}),
230 ... Object. keys (manifest?.optionalDependencies || {}),
231 ],
232 );
233 }
234
235 async function tryLoadPackage ( context , packageName ) {
236 try {
237 const resolvedPath = context.requireFromRoot. resolve (packageName);
238 const module = context. requireFromRoot (packageName);
239 return { ok: true , resolvedPath, module };
240 } catch (error) {
241 return {
242 ok: false ,
243 message: `Could not resolve ${ packageName } from ${ context . projectRoot }. Install project dependencies first. Details: ${ error . message }` ,
244 };
245 }
246 }
247
248 async function resolvePackageBinary ( context , packageName , binName ) {
249 try {
250 const pkgDir = await resolvePackageDir (context, packageName);
251 if ( ! pkgDir) {
252 return {
253 ok: false ,
254 message: `Could not resolve ${ packageName } from ${ context . projectRoot }. Install project dependencies first.` ,
255 };
256 }
257 const binPath = path. join (context.projectRoot, "node_modules" , ".bin" , binName);
258 if ( await pathExists (binPath)) {
259 return { ok: true , path: binPath, pkgDir };
260 }
261 return {
262 ok: false ,
263 message: `${ packageName } was resolved, but ${ binName } was not found at ${ binPath }. Use a node-modules install (for example Yarn with nodeLinker: node-modules) and reinstall dependencies.` ,
264 };
265 } catch (error) {
266 return {
267 ok: false ,
268 message: `Could not resolve ${ packageName } from ${ context . projectRoot }. Details: ${ error . message }` ,
269 };
270 }
271 }
272
273 export async function resolveDesignMdGeneratorExecutionFromContext ( context ) {
274 const packageDir = await resolvePackageDir (context, "design-md-generator" );
275 const localCandidates = [
276 process.env. DESIGN_MD_GENERATOR_DIR ,
277 path. join (context.projectRoot, "tools" , "design-md-generator" ),
278 path. join (context.projectRoot, "test-designmd" , "design-md-generator" ),
279 ]. filter (Boolean);
280 let lastFailure = null ;
281
282 for ( const candidate of localCandidates) {
283 const checkout = await resolveDesignMdGeneratorCheckout (candidate);
284 if (checkout.ok) {
285 return checkout;
286 }
287 const packageMode = await resolveDesignMdGeneratorPackageMode (candidate, context.projectRoot);
288 if (packageMode.ok) {
289 return packageMode;
290 }
291 if (packageMode.message) {
292 lastFailure = packageMode;
293 }
294 }
295
296 const packageMode = await resolveDesignMdGeneratorPackageMode (packageDir, context.projectRoot);
297 if (packageMode.ok) {
298 return packageMode;
299 }
300 if (packageMode.message) {
301 return packageMode;
302 }
303 if (lastFailure) {
304 return lastFailure;
305 }
306
307 return {
308 ok: false ,
309 message: "design-md-generator was not found in a runnable form. Declare it in package.json and install a built CLI, or set DESIGN_MD_GENERATOR_DIR to a local checkout." ,
310 notes: [
311 `Repo-default checkout path: ${ path . join ( context . projectRoot , "tools" , "design-md-generator" ) }` ,
312 "External repos can either install a built design-md-generator package or provide DESIGN_MD_GENERATOR_DIR=/absolute/path/to/design-md-generator." ,
313 ],
314 };
315 }
316
317 async function resolveDesignMdGeneratorCheckout ( candidate ) {
318 if ( ! candidate) return { ok: false };
319 const extractScript = path. join (candidate, "scripts" , "extract.ts" );
320 const packageJsonPath = path. join (candidate, "package.json" );
321 if ( !await pathExists (extractScript) || !await pathExists (packageJsonPath)) {
322 return { ok: false };
323 }
324 return {
325 ok: true ,
326 mode: "checkout" ,
327 rootDir: candidate,
328 command: "npx" ,
329 args: [ "ts-node" , "scripts/extract.ts" ],
330 retryCommand: "corepack" ,
331 retryArgs: [ "yarn" , "ts-node" , "scripts/extract.ts" ],
332 commandDescription: "npx ts-node scripts/extract.ts" ,
333 };
334 }
335
336 async function resolveDesignMdGeneratorPackageMode ( packageDir , projectRoot ) {
337 if ( ! packageDir) return { ok: false };
338 const packageJsonPath = path. join (packageDir, "package.json" );
339 if ( !await pathExists (packageJsonPath)) return { ok: false };
340
341 let manifest;
342 try {
343 manifest = JSON . parse ( await readFile (packageJsonPath, "utf8" ));
344 } catch (error) {
345 return {
346 ok: false ,
347 message: `design-md-generator package metadata at ${ packageJsonPath } could not be read: ${ error . message }` ,
348 };
349 }
350
351 const binField = manifest?.bin;
352 const binEntries = typeof binField === "string" ? [binField] : Object. values (binField || {});
353 for ( const relativeBin of binEntries) {
354 const cliPath = path. join (packageDir, relativeBin);
355 if ( await pathExists (cliPath)) {
356 return {
357 ok: true ,
358 mode: "package" ,
359 rootDir: packageDir,
360 command: "node" ,
361 args: [cliPath],
362 retryCommand: null ,
363 retryArgs: null ,
364 commandDescription: `node ${ path . relative ( packageDir , cliPath ) }` ,
365 };
366 }
367 }
368
369 const distCli = path. join (packageDir, "dist" , "cli.js" );
370 if ( await pathExists (distCli)) {
371 return {
372 ok: true ,
373 mode: "package" ,
374 rootDir: packageDir,
375 command: "node" ,
376 args: [distCli],
377 retryCommand: null ,
378 retryArgs: null ,
379 commandDescription: `node ${ path . relative ( packageDir , distCli ) }` ,
380 };
381 }
382
383 return {
384 ok: false ,
385 message: `design-md-generator is installed at ${ packageDir }, but it does not contain a runnable extractor entrypoint. Expected either a built CLI from package.json/bin or a checkout entrypoint such as scripts/extract.ts.` ,
386 notes: [
387 `For this repo, prefer a real checkout at ${ path . join ( projectRoot , "tools" , "design-md-generator" ) }.` ,
388 "Outside this repo, set DESIGN_MD_GENERATOR_DIR to a local checkout or install a built package that exposes a working CLI." ,
389 ],
390 };
391 }
392
393 async function resolvePackageDir ( context , packageName ) {
394 try {
395 const resolvedEntry = context.requireFromRoot. resolve (packageName);
396 return await findPackageDirFromResolvedPath (resolvedEntry, packageName);
397 } catch {
398 try {
399 const pkgJsonPath = context.requireFromRoot. resolve ( `${ packageName }/package.json` );
400 return path. dirname (pkgJsonPath);
401 } catch {
402 return null ;
403 }
404 }
405 }
406
407 async function findPackageDirFromResolvedPath ( resolvedPath , packageName ) {
408 let current = path. dirname (resolvedPath);
409 while ( true ) {
410 const pkgJsonPath = path. join (current, "package.json" );
411 if ( await pathExists (pkgJsonPath)) {
412 try {
413 const manifest = JSON . parse ( await readFile (pkgJsonPath, "utf8" ));
414 if (manifest?.name === packageName) {
415 return current;
416 }
417 } catch {}
418 }
419 const parent = path. dirname (current);
420 if (parent === current) return null ;
421 current = parent;
422 }
423 }
424
425 async function tryLaunchChromium ( playwright ) {
426 let browser;
427 try {
428 browser = await playwright.chromium. launch ({ headless: true });
429 return { ok: true };
430 } catch (error) {
431 const details = String (error?.message || error);
432 const environmentBlocked = /Permission denied \( 1100 \) | MachPortRendezvousServer | bootstrap_check_in/ i . test (details);
433 return {
434 ok: false ,
435 environmentBlocked,
436 message: environmentBlocked
437 ? `Chromium could not launch because the current execution environment blocked Playwright from starting the browser. Details: ${ details }`
438 : `Chromium could not launch. Install the browser runtime and retry. Details: ${ details }` ,
439 };
440 } finally {
441 await browser?. close ();
442 }
443 }
444
445 function installCommandForContext ( context , missingDeclarations , requiredPackages ) {
446 if (missingDeclarations. length > 0 ) {
447 const pkgSpecs = missingDeclarations. map (( name ) => `${ name }@${ requiredPackages [ name ] }` );
448 switch (context.packageManager) {
449 case "yarn" :
450 return `corepack yarn add -D ${ pkgSpecs . join ( " " ) }` ;
451 case "pnpm" :
452 return `corepack pnpm add -D ${ pkgSpecs . join ( " " ) }` ;
453 default :
454 return `npm install --save-dev ${ pkgSpecs . join ( " " ) }` ;
455 }
456 }
457
458 switch (context.packageManager) {
459 case "yarn" :
460 return `corepack yarn install` ;
461 case "pnpm" :
462 return `corepack pnpm install` ;
463 default :
464 return `npm install` ;
465 }
466 }
467
468 async function requiredPackageSpecsForContext ( context ) {
469 const specs = { ... PLAYWRIGHT_PACKAGE };
470 const repoLocalTool = path. join (context.projectRoot, "tools" , "design-md-generator" , "package.json" );
471 if ( await pathExists (repoLocalTool)) {
472 specs[ "design-md-generator" ] = "file:tools/design-md-generator" ;
473 }
474 return specs;
475 }
476
477 function browserInstallCommandForContext ( context ) {
478 switch (context.packageManager) {
479 case "yarn" :
480 return `npx playwright install chromium` ;
481 case "pnpm" :
482 return `npx playwright install chromium` ;
483 default :
484 return `npx playwright install chromium` ;
485 }
486 }
487
488 function nodeLinkerRemediationCommand ( context ) {
489 if (context.packageManager !== "yarn" || context.nodeLinker === "node-modules" ) {
490 return null ;
491 }
492 return "printf 'nodeLinker: node-modules \\ n' > .yarnrc.yml" ;
493 }
494
495 async function attemptAutoFix ({ context , installDeclaredCommand , runInstall , runBrowserInstall , updateNodeLinker }) {
496 const notes = [];
497 if (updateNodeLinker) {
498 const linkerUpdate = await runShellCommand ( "printf 'nodeLinker: node-modules \\ n' > .yarnrc.yml" , context.projectRoot);
499 if ( ! linkerUpdate.ok) {
500 notes. push ( `Yarn nodeLinker remediation failed: ${ linkerUpdate . summary }` );
501 return { ok: false , notes };
502 }
503 notes. push ( "Updated .yarnrc.yml to use nodeLinker: node-modules" );
504 }
505 if (runInstall && installDeclaredCommand) {
506 const install = await runShellCommand (installDeclaredCommand, context.projectRoot);
507 if ( ! install.ok) {
508 notes. push ( `Dependency remediation failed: ${ install . summary }` );
509 return { ok: false , notes };
510 }
511 notes. push ( `Ran dependency remediation: ${ installDeclaredCommand }` );
512 }
513 if (runBrowserInstall) {
514 const browserInstallCommand = browserInstallCommandForContext (context);
515 const browserInstall = await runShellCommand (browserInstallCommand, context.projectRoot);
516 if ( ! browserInstall.ok) {
517 notes. push ( `Chromium remediation failed: ${ browserInstall . summary }` );
518 return { ok: false , notes };
519 }
520 notes. push ( `Ran browser remediation: ${ browserInstallCommand }` );
521 }
522 return { ok: true , notes };
523 }
524
525 function runShellCommand ( command , cwd ) {
526 return new Promise (( resolve , reject ) => {
527 const child = spawn (command, {
528 cwd,
529 shell: true ,
530 stdio: [ "ignore" , "pipe" , "pipe" ],
531 });
532 let stdout = "" ;
533 let stderr = "" ;
534 child.stdout. on ( "data" , ( chunk ) => {
535 stdout += chunk;
536 });
537 child.stderr. on ( "data" , ( chunk ) => {
538 stderr += chunk;
539 });
540 child. on ( "error" , reject);
541 child. on ( "close" , ( code ) => {
542 resolve ({
543 ok: code === 0 ,
544 code,
545 summary: code === 0 ? "ok" : `exit ${ code }: ${ String ( stderr || stdout ). slice ( - 1000 ) }` ,
546 });
547 });
548 });
549 }
550
551 function passCheck ( id , message ) {
552 return { id, status: "pass" , message };
553 }
554
555 function failCheck ( id , message ) {
556 return { id, status: "fail" , message };
557 }
558
559 function skippedCheck ( id , message ) {
560 return { id, status: "skip" , message };
561 }
562
563 async function pathExists ( filePath ) {
564 try {
565 await access (filePath);
566 return true ;
567 } catch {
568 return false ;
569 }
570 }