@webority/sandbox-gate 0.0.0-stage → 0.129.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +8 -3
- package/dist/index.d.ts +91 -0
- package/dist/sandbox-gate.js +11 -0
- package/package.json +30 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Copyright (c) Webority Technologies. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its NuGet packages are the proprietary and confidential property
|
|
4
|
+
of Webority Technologies. They are published publicly only for the convenience of
|
|
5
|
+
Webority's own projects and internal consumption.
|
|
6
|
+
|
|
7
|
+
No license or right to use, copy, modify, merge, publish, distribute, sublicense,
|
|
8
|
+
or create derivative works is granted to any third party. Unauthorized use,
|
|
9
|
+
reproduction, or distribution is prohibited.
|
package/README.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @webority/sandbox-gate
|
|
2
|
+
The gate runner the Webority repos share: sandbox boot, a parallel task runner, gate Chrome ownership, accessibility trees and changed-file helpers.
|
|
3
|
+
- `derivePorts(root, count)`: the port block a checkout owns.
|
|
4
|
+
- `runTasks(tasks, options)`: runs tasks in parallel and buffers each one's output.
|
|
5
|
+
- `bootSandboxes(options)`: starts the sandboxes on the given ports.
|
|
6
|
+
- `stopAll(options)`: stops the children, frees their ports and reaps gate Chrome.
|
|
7
|
+
- `accessibilityTree(page, options)`: the flat accessibility tree of an element.
|
|
8
|
+
Node only; not for browsers.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export type SandboxName = "react" | "razor" | "phoneWeb";
|
|
4
|
+
|
|
5
|
+
export interface GateTask {
|
|
6
|
+
command: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
label: string;
|
|
9
|
+
retryArgs?: string[];
|
|
10
|
+
after?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TaskResult {
|
|
14
|
+
label: string;
|
|
15
|
+
code: number;
|
|
16
|
+
ms: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BufferedResult extends TaskResult {
|
|
20
|
+
output: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RunTasksOptions {
|
|
24
|
+
env?: Record<string, string>;
|
|
25
|
+
heartbeatMs?: number;
|
|
26
|
+
quick?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BootSandboxesOptions {
|
|
30
|
+
root: string;
|
|
31
|
+
which: SandboxName[];
|
|
32
|
+
ports: Partial<Record<SandboxName, number>>;
|
|
33
|
+
origins: Partial<Record<SandboxName, string>>;
|
|
34
|
+
readyUrls?: Partial<Record<SandboxName, string>>;
|
|
35
|
+
reactFilter?: string;
|
|
36
|
+
razorProject?: string;
|
|
37
|
+
phoneWebDir?: string;
|
|
38
|
+
/** Added to each sandbox's log file name, so several pairs booted by one run keep separate logs. */
|
|
39
|
+
tag?: string;
|
|
40
|
+
/** Merged over process.env for both children. */
|
|
41
|
+
env?: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BootedSandboxes {
|
|
45
|
+
children: ChildProcess[];
|
|
46
|
+
logs: Partial<Record<SandboxName, string>>;
|
|
47
|
+
waitReady(options?: { attempts?: number; intervalMs?: number }): Promise<boolean>;
|
|
48
|
+
finish(code: number): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AriaNode {
|
|
52
|
+
depth: number;
|
|
53
|
+
role: string;
|
|
54
|
+
name: string;
|
|
55
|
+
states: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AriaPage {
|
|
59
|
+
locator(selector: string): { ariaSnapshot(): Promise<string> };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function derivePorts(root: string, count?: number): number[];
|
|
63
|
+
export function runTasks(tasks: GateTask[], options?: RunTasksOptions): Promise<TaskResult[]>;
|
|
64
|
+
export function runBuffered(task: GateTask, env?: Record<string, string>): Promise<BufferedResult>;
|
|
65
|
+
export function bootSandboxes(options: BootSandboxesOptions): BootedSandboxes;
|
|
66
|
+
export function stopAll(options?: { ports?: number[]; children?: ChildProcess[] }): number;
|
|
67
|
+
export function accessibilityTree(page: AriaPage, options?: { selector?: string }): Promise<AriaNode[]>;
|
|
68
|
+
export function parseAriaSnapshot(yaml: string): AriaNode[];
|
|
69
|
+
|
|
70
|
+
export function killPort(port: number): boolean;
|
|
71
|
+
export function reachable(url: string): Promise<boolean>;
|
|
72
|
+
export function secs(ms: number): string;
|
|
73
|
+
export function shellLine(command: string, args?: string[]): string;
|
|
74
|
+
export function exitAfterOutput(code: number): Promise<void>;
|
|
75
|
+
|
|
76
|
+
export function gitLines(root: string, args: string[]): string[] | null;
|
|
77
|
+
export function changedFiles(root: string, base?: string | null): string[] | null;
|
|
78
|
+
export function baseCommitOf(root: string, base: string | null): string | null;
|
|
79
|
+
export function versionStampOnly(root: string, file: string, base: string | null): boolean;
|
|
80
|
+
|
|
81
|
+
export const GATE_CHROME_MARKER: string;
|
|
82
|
+
export function gateOwnerPid(): number;
|
|
83
|
+
export function profileDir(): string;
|
|
84
|
+
export function playwrightGateArgs(): string[];
|
|
85
|
+
export function reapOrphanGateChrome(): number;
|
|
86
|
+
export function reapOwnGateChrome(ownerPid?: number): number;
|
|
87
|
+
export const LIGHT_SCHEME_ARG: string;
|
|
88
|
+
|
|
89
|
+
export const REACT_ORIGIN: string;
|
|
90
|
+
export const RAZOR_ORIGIN: string;
|
|
91
|
+
export const PHONE_WEB_ORIGIN: string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import{spawn as e,spawnSync as t}from"node:child_process";import{existsSync as n,mkdtempSync as r,openSync as i,readFileSync as a,readdirSync as o,rmSync as s}from"node:fs";import{tmpdir as c}from"node:os";import{join as l,resolve as u}from"node:path";import{createHash as d}from"node:crypto";async function f(e,t={}){return p(await e.locator(t.selector??`body`).ariaSnapshot())}function p(e){let t=[];for(let n of e.split(`
|
|
2
|
+
`)){let e=n.trimStart();if(!e.startsWith(`- `))continue;let r=(n.length-e.length)/2,i=e.slice(2).trimEnd();if(i.startsWith(`/`))continue;i.endsWith(`:`)&&(i=i.slice(0,-1));let a=[];for(;i.endsWith(`]`);){let e=i.lastIndexOf(` [`);if(e===-1)break;a.unshift(i.slice(e+2,-1)),i=i.slice(0,e)}let o=i,s=``,c=i.indexOf(` `),l=i.indexOf(`:`);l!==-1&&(c===-1||l<c)?(o=i.slice(0,l),s=m(i.slice(l+1).trim())):c!==-1&&(o=i.slice(0,c),s=m(i.slice(c+1).trim())),t.push({depth:r,role:o,name:s,states:a})}return t}function m(e){return e.length>=2&&e.startsWith(`"`)&&e.endsWith(`"`)?e.slice(1,-1):e}var h=`webority-ui-gate-chrome`,g=RegExp(`${h}[=-](\\d+)`);function _(){let e=Number(process.env.WEBORITY_UI_GATE_PID);return Number.isFinite(e)&&e>0?e:process.pid}function v(){return r(l(c(),`${h}-${_()}-`))}function y(){return[`--${h}=${_()}`]}function b(e){try{return process.kill(e,0),!0}catch{return!1}}function x(){return process.platform===`win32`?(t(`powershell`,[`-NoProfile`,`-Command`,`Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" | ForEach-Object { '{0} {1}' -f $_.ProcessId, $_.CommandLine }`],{encoding:`utf8`}).stdout||``).split(/\r?\n/).map(e=>e.trim()).filter(Boolean).map(e=>{let t=e.indexOf(` `);return t<0?null:{pid:e.slice(0,t).trim(),commandLine:e.slice(t+1)}}).filter(Boolean):(t(`ps`,[`-A`,`-o`,`pid=,args=`],{encoding:`utf8`}).stdout||``).split(`
|
|
3
|
+
`).map(e=>e.trim()).filter(e=>/(?:^|[/\s])(chrome|chromium)(?:\s|$|\.exe)/i.test(e)).map(e=>{let t=e.match(/^(\d+)\s+(.*)$/);return t?{pid:t[1],commandLine:t[2]}:null}).filter(Boolean)}function S(e){process.platform===`win32`?t(`taskkill`,[`/PID`,e,`/T`,`/F`],{stdio:`ignore`}):t(`kill`,[`-9`,e],{stdio:`ignore`})}function C(){let e=[];for(let{pid:t,commandLine:n}of x()){if(!n?.includes(`webority-ui-gate-chrome`))continue;let r=n.match(g);if(!r)continue;let i=Number(r[1]);!Number.isFinite(i)||i<=0||i!==process.pid&&(b(i)||e.push(t))}for(let t of e)S(t);return T(),e.length}function w(e=process.pid){let t=[];for(let{pid:n,commandLine:r}of x()){let i=r?.match(g);i&&Number(i[1])===e&&t.push(n)}for(let e of t)S(e);return t.length}function T(){let e=RegExp(`^${h}-(\\d+)-`),t;try{t=o(c())}catch{return}for(let n of t){let t=n.match(e);if(!t)continue;let r=Number(t[1]);if(!(r===process.pid||b(r)))try{s(l(c(),n),{recursive:!0,force:!0})}catch{}}}var E=`--blink-settings=preferredColorScheme=1`,D=(e,t=[])=>[e,...t].join(` `);async function O(e){await new Promise(e=>process.stdout.write(``,e)),await new Promise(e=>process.stderr.write(``,e)),process.exit(e)}var k=e=>`${(e/1e3).toFixed(1)}s`;async function A(e){try{return(await fetch(e)).ok}catch{return!1}}function j(e){if(process.platform===`win32`){let n=t(`netstat`,[`-ano`],{encoding:`utf8`}).stdout||``,r=new Set(n.split(`
|
|
4
|
+
`).filter(e=>e.includes(`LISTENING`)).filter(t=>t.trim().split(/\s+/)[1]?.endsWith(`:${e}`)).map(e=>e.trim().split(/\s+/).pop()).filter(e=>e&&e!==`0`));for(let e of r)t(`taskkill`,[`/PID`,e,`/T`,`/F`],{stdio:`ignore`});return r.size>0}let n=(t(`lsof`,[`-ti`,`tcp:${e}`,`-sTCP:LISTEN`],{encoding:`utf8`}).stdout||``).split(`
|
|
5
|
+
`).filter(Boolean);for(let e of n)t(`kill`,[`-9`,e],{stdio:`ignore`});return n.length>0}var M=[`react`,`razor`,`phoneWeb`];function N(t){let{which:r,ports:o,origins:l,readyUrls:d={},reactFilter:f=`@webority/react-sandbox`,razorProject:p=`samples/Razor.Sandbox`,phoneWebDir:m=`samples/mobile-sandbox-web`,tag:h,env:g={}}=t;for(let e of r){if(!M.includes(e))throw Error(`bootSandboxes: unknown sandbox "${e}"`);if(o[e]===void 0)throw Error(`bootSandboxes: no port for "${e}"`);if(l[e]===void 0)throw Error(`bootSandboxes: no origin for "${e}"`)}let _={react:[`corepack`,[`pnpm`,`--filter`,f,`dev`,`--port`,String(o.react),`--strictPort`]],razor:[`dotnet`,[`run`,`--project`,p,`--no-build`,`--`,`--urls`,l.razor]],phoneWeb:[`corepack`,[`pnpm`,`--dir`,m,`exec`,`vite`,`--port`,String(o.phoneWeb),`--strictPort`]]},v={};for(let e of r)v[e]=u(c(),`webority-gate-${h?`${e}-${h}`:e}-sandbox-${process.pid}.log`);console.log(`\nStarting the sandboxes (${r.join(`, `)})... (output: ${r.map(e=>v[e]).join(`, `)})`);let y=r.map(t=>{let[n,r]=_[t],a=i(v[t],`w`);return e(D(n,r),{stdio:[`ignore`,a,a],shell:!0,env:{...process.env,...g}})}),b=r.map(e=>d[e]??l[e]);async function x(){for(let e of b)if(!await A(e))return!1;return!0}return{children:y,logs:v,async waitReady({attempts:e=600,intervalMs:t=300}={}){for(let n=0;n<e;n+=1){if(await x())return!0;await new Promise(e=>setTimeout(e,t))}return!1},finish(e){if(e===0)for(let e of Object.values(v))s(e,{force:!0});else for(let[e,t]of Object.entries(v)){if(!n(t))continue;let r=a(t,`utf8`).trimEnd().split(`
|
|
6
|
+
`),i=r.findIndex(e=>/Exception: |^\s*Error: /.test(e));i!==-1&&i<r.length-25&&console.log(`\n──── ${e} sandbox log, first error (${t}:${i+1}) ────\n${r.slice(i,i+3).join(`
|
|
7
|
+
`)}`),console.log(`\n──── ${e} sandbox log, last 25 lines (${t}) ────\n${r.slice(-25).join(`
|
|
8
|
+
`)}`)}}}}function P({ports:e=[],children:t=[]}={}){for(let e of t)e.kill();for(let t of e)j(t);return w()}var F=process.env.WUI_REACT_ORIGIN||`http://localhost:5173`,I=process.env.WUI_RAZOR_ORIGIN||`http://localhost:5299`,L=process.env.WUI_PHONE_WEB_ORIGIN||`http://localhost:5174`;function R(e,t=2){if(!Number.isInteger(t)||t<1||t>8)throw RangeError(`derivePorts: count must be 1 to 8, got ${t}`);let n=2e4+d(`sha1`).update(e).digest().readUInt16BE(0)%1e3*8;return Array.from({length:t},(e,t)=>n+t)}function z({command:t,args:n,label:r},i={}){return new Promise(a=>{let o=performance.now(),s=[],c=e(D(t,n),{shell:!0,env:{...process.env,...i}});c.stdout.on(`data`,e=>s.push(e)),c.stderr.on(`data`,e=>s.push(e)),c.on(`close`,e=>a({label:r,code:e??1,output:Buffer.concat(s).toString(),ms:Math.round(performance.now()-o)}))})}var B=/\[vitest-pool\]: Failed to start \w+ worker|\[vitest-pool-runner\]: Timeout waiting for worker to respond/,V=/^\s*(Test Files|Tests)\s.*\b\d+ failed/m;async function H(e,t){let n=await z(e,t);if(n.code===0||!e.retryArgs||!B.test(n.output)||V.test(n.output))return n;console.log(`\n──── RETRY ${e.label}: the vitest worker pool did not start in time (machine load, ${k(n.ms)}); running it once more with half the workers ────`);let r=await z({...e,args:e.retryArgs},t);return{...r,ms:n.ms+r.ms,output:`${n.output}\n──── retry ────\n${r.output}`}}async function U(e,t={}){if(e.length===0)return[];let{env:n={},heartbeatMs:r=6e4}=t;console.log(`\n──── running in parallel: ${e.map(e=>e.label).join(` · `)} ────`);let i=performance.now(),a=/* @__PURE__ */ new Set,o=setInterval(()=>{let t=e.map(e=>e.label).filter(e=>!a.has(e));console.log(`still running after ${Math.round((performance.now()-i)/1e3)} s: ${t.join(`, `)}`)},r),s=/* @__PURE__ */ new Map,c;try{c=await Promise.all(e.map(e=>{let t=(async()=>{await null,e.after&&s.has(e.after)&&await s.get(e.after),console.log(`\n──── start ${e.label} ────`);let t=await H(e,n);return a.add(t.label),console.log(`\n──── ${t.label} → ${t.code===0?`ok`:`FAIL`} (${k(t.ms)}) ────`),process.stdout.write(t.output.endsWith(`
|
|
9
|
+
`)?t.output:`${t.output}\n`),{label:t.label,code:t.code,ms:t.ms}})();return s.set(e.label,t),t}))}finally{clearInterval(o)}return console.log(`──── group wall time: ${k(Math.round(performance.now()-i))} ────`),c}function W(e,n){let r=t(`git`,n,{cwd:e,encoding:`utf8`});return r.status===0?r.stdout.split(`
|
|
10
|
+
`).filter(Boolean):null}function G(e,t=process.env.GATE_BASE||null){let n=t?W(e,[`diff`,`--name-only`,`--no-renames`,t,`HEAD`]):W(e,[`diff`,`--name-only`,`--no-renames`,`origin/development...HEAD`]);if(n===null)return null;let r=W(e,[`diff`,`--name-only`,`--no-renames`,`HEAD`])??[],i=W(e,[`ls-files`,`--others`,`--exclude-standard`])??[];return[.../* @__PURE__ */ new Set([...n,...r,...i])].sort()}function K(e,t){return t||W(e,[`merge-base`,`origin/development`,`HEAD`])?.[0]||null}function q(e,t){let n=W(e,[`show`,`${t}:version.json`]);if(n===null)return null;try{let e=JSON.parse(n.join(`
|
|
11
|
+
`)).Version;return typeof e==`string`?e:null}catch{return null}}function J(e,t,n){let r=K(e,n);if(!r)return!1;let i=q(e,r),a=q(e,`HEAD`);if(i===null||a===null||i===a)return!1;let o=W(e,[`status`,`--porcelain`,`--`,t]);if(o===null||o.length>0)return!1;let s=W(e,[`diff`,`-U0`,`--no-renames`,r,`HEAD`,`--`,t]);if(s===null)return!1;let c=[];for(let e of s)e.startsWith(`@@`)?c.push({removed:[],added:[]}):c.length&&e.startsWith(`-`)?c.at(-1).removed.push(e.slice(1)):c.length&&e.startsWith(`+`)&&c.at(-1).added.push(e.slice(1));return c.length!==0&&c.every(({removed:e,added:t})=>e.length===t.length&&e.every((e,n)=>e.split(i).join(a)===t[n]))}export{h as GATE_CHROME_MARKER,E as LIGHT_SCHEME_ARG,L as PHONE_WEB_ORIGIN,I as RAZOR_ORIGIN,F as REACT_ORIGIN,f as accessibilityTree,K as baseCommitOf,N as bootSandboxes,G as changedFiles,R as derivePorts,O as exitAfterOutput,_ as gateOwnerPid,W as gitLines,j as killPort,p as parseAriaSnapshot,y as playwrightGateArgs,v as profileDir,A as reachable,C as reapOrphanGateChrome,w as reapOwnGateChrome,z as runBuffered,U as runTasks,k as secs,D as shellLine,P as stopAll,J as versionStampOnly};
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority/sandbox-gate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.129.0",
|
|
4
|
+
"description": "The gate runner Webority repos share: sandbox boot on ports derived from the checkout, a buffered parallel task runner, headless Chrome ownership and orphan reaping, accessibility trees from Playwright, and changed-file helpers for a quick scope. Node only.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"registry": "https://registry.npmjs.org/"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"main": "./dist/sandbox-gate.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/sandbox-gate.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=24.15 <25"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"vite": "^8.0.12"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "vite build && node scripts/copy-types.mjs",
|
|
31
|
+
"test": "node --test"
|
|
32
|
+
}
|
|
6
33
|
}
|