@warlock.js/core 5.33.0 → 5.34.1
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/CHANGELOG.md +36 -46
- package/esm/cli/commands/check/check-command.action.mjs +2 -1
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
- package/esm/cli/commands/check.command.mjs +2 -2
- package/esm/cli/commands/check.command.mjs.map +1 -1
- package/esm/cli/commands/e2e.command.mjs +63 -0
- package/esm/cli/commands/e2e.command.mjs.map +1 -0
- package/esm/cli/commands/load-e2e-spec.mjs +47 -0
- package/esm/cli/commands/load-e2e-spec.mjs.map +1 -0
- package/esm/cli/framework-cli-commands.mjs +2 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/dev-server/health-checker/checkers/oxlint-health-checker.mjs +1 -1
- package/esm/dev-server/health-checker/default-health-checkers.mjs +1 -6
- package/esm/dev-server/health-checker/default-health-checkers.mjs.map +1 -1
- package/esm/dev-server/health-checker/file-health-checker.contract.d.mts +0 -2
- package/esm/dev-server/health-checker/file-health-checker.contract.d.mts.map +1 -1
- package/esm/dev-server/health-checker/file-health-result.d.mts +1 -1
- package/esm/dev-server/health-checker/file-health-result.mjs.map +1 -1
- package/esm/e2e/browser.d.mts +4 -0
- package/esm/e2e/browser.d.mts.map +1 -0
- package/esm/e2e/browser.mjs +88 -0
- package/esm/e2e/browser.mjs.map +1 -0
- package/esm/e2e/cdp.d.mts +152 -0
- package/esm/e2e/cdp.d.mts.map +1 -0
- package/esm/e2e/cdp.mjs +106 -0
- package/esm/e2e/cdp.mjs.map +1 -0
- package/esm/e2e/fixture.mjs +195 -0
- package/esm/e2e/fixture.mjs.map +1 -0
- package/esm/e2e/index.d.mts +5 -0
- package/esm/e2e/index.mjs +6 -0
- package/esm/e2e/owned-process.mjs +193 -0
- package/esm/e2e/owned-process.mjs.map +1 -0
- package/esm/e2e/page.d.mts +64 -0
- package/esm/e2e/page.d.mts.map +1 -0
- package/esm/e2e/page.mjs +376 -0
- package/esm/e2e/page.mjs.map +1 -0
- package/esm/e2e/run-e2e.d.mts +17 -0
- package/esm/e2e/run-e2e.d.mts.map +1 -0
- package/esm/e2e/run-e2e.mjs +143 -0
- package/esm/e2e/run-e2e.mjs.map +1 -0
- package/esm/e2e/types.d.mts +123 -0
- package/esm/e2e/types.d.mts.map +1 -0
- package/esm/e2e/types.mjs +24 -0
- package/esm/e2e/types.mjs.map +1 -0
- package/esm/generations/features/web.feature.mjs +7 -4
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +18 -35
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/secrets/errors.d.mts +1 -1
- package/esm/secrets/errors.d.mts.map +1 -1
- package/esm/secrets/errors.mjs +2 -0
- package/esm/secrets/errors.mjs.map +1 -1
- package/esm/secrets/file-backend.mjs +1 -0
- package/esm/secrets/file-backend.mjs.map +1 -1
- package/esm/secrets/index.d.mts +2 -2
- package/esm/secrets/keyring-backend.mjs +1 -0
- package/esm/secrets/keyring-backend.mjs.map +1 -1
- package/esm/secrets/memory-backend.d.mts +1 -0
- package/esm/secrets/memory-backend.d.mts.map +1 -1
- package/esm/secrets/memory-backend.mjs +1 -0
- package/esm/secrets/memory-backend.mjs.map +1 -1
- package/esm/secrets/resolve-driver.mjs +14 -0
- package/esm/secrets/resolve-driver.mjs.map +1 -0
- package/esm/secrets/secret-store.d.mts +4 -2
- package/esm/secrets/secret-store.d.mts.map +1 -1
- package/esm/secrets/secret-store.mjs +22 -8
- package/esm/secrets/secret-store.mjs.map +1 -1
- package/esm/secrets/secrets.d.mts +2 -0
- package/esm/secrets/secrets.d.mts.map +1 -1
- package/esm/secrets/secrets.mjs +13 -1
- package/esm/secrets/secrets.mjs.map +1 -1
- package/esm/secrets/types.d.mts +8 -0
- package/esm/secrets/types.d.mts.map +1 -1
- package/esm/tooling/check/lint-step.d.mts +3 -3
- package/esm/tooling/check/lint-step.d.mts.map +1 -1
- package/esm/tooling/check/lint-step.mjs +3 -47
- package/esm/tooling/check/lint-step.mjs.map +1 -1
- package/esm/tooling/check/run-check.d.mts +2 -2
- package/esm/tooling/check/run-check.mjs +4 -2
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/problem.d.mts +1 -1
- package/esm/tooling/problem.d.mts.map +1 -1
- package/esm/tooling/problem.mjs.map +1 -1
- package/esm/updater/package-manager.mjs +5 -1
- package/esm/updater/package-manager.mjs.map +1 -1
- package/esm/upgrade/codemods/index.mjs +2 -1
- package/esm/upgrade/codemods/index.mjs.map +1 -1
- package/esm/upgrade/codemods/oxc-toolchain.mjs +59 -10
- package/esm/upgrade/codemods/oxc-toolchain.mjs.map +1 -1
- package/esm/upgrade/codemods/oxlint-rule-schemas.mjs +6 -0
- package/esm/upgrade/codemods/oxlint-rule-schemas.mjs.map +1 -0
- package/esm/upgrade/codemods/port-eslint-rules.mjs +343 -0
- package/esm/upgrade/codemods/port-eslint-rules.mjs.map +1 -0
- package/esm/upgrade/codemods/toolchain-templates.mjs +1 -1
- package/esm/upgrade/codemods/toolchain-templates.mjs.map +1 -1
- package/esm/upgrade/codemods/validate-oxlint-rule.mjs +60 -0
- package/esm/upgrade/codemods/validate-oxlint-rule.mjs.map +1 -0
- package/esm/upgrade/notes.mjs +14 -7
- package/esm/upgrade/notes.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +0 -1
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +269 -44
- package/llms.txt +4 -3
- package/package.json +19 -20
- package/skills/create-module/SKILL.md +2 -2
- package/skills/run-app/SKILL.md +3 -3
- package/skills/store-secrets/SKILL.md +60 -2
- package/skills/test-browser/SKILL.md +119 -0
- package/skills/upgrade-from-5-29/SKILL.md +70 -29
- package/skills/warlock-check/SKILL.md +3 -3
- package/skills/warlock-doctor/SKILL.md +2 -3
- package/skills/write-cli-command/SKILL.md +6 -2
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs +0 -152
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs.map +0 -1
- package/esm/dev-server/health-checker/eslint-support.mjs +0 -27
- package/esm/dev-server/health-checker/eslint-support.mjs.map +0 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.d.mts +0 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs +0 -183
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.mjs","names":["delay"],"sources":["../../../../../../../../packages/core/src/e2e/page.ts"],"sourcesContent":["import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { CdpConnection } from \"./cdp\";\nimport {\n defaultViewports,\n errorMessage,\n matches,\n type E2EEvidence,\n type E2ESpec,\n type Viewport,\n} from \"./types\";\n\ntype WaitOptions = { selector?: string; text?: string; url?: string; timeoutMs?: number };\ntype ClickBox = { x: number; y: number; width: number; height: number; reason: string };\ntype ViewportReport = {\n requestedWidth: number;\n innerWidth: number;\n documentWidth: number;\n bodyWidth: number;\n offenders: {\n tag: string;\n id: string;\n className: string;\n width: number;\n scrollWidth: number;\n right: number;\n left: number;\n }[];\n};\n\n/** Runs inside the browser. Two paints plus cross-probe box comparison detect\n * scroll/animation/layout races. The hit test also accepts target descendants.\n */\nfunction clickProbe(selector: string): Promise<ClickBox> {\n return (async () => {\n const empty = (reason: string): ClickBox => ({ x: 0, y: 0, width: 0, height: 0, reason });\n const element = document.querySelector(selector);\n if (!(element instanceof HTMLElement || element instanceof SVGElement))\n return empty(\"target is missing or is not an HTML/SVG element\");\n element.scrollIntoView({ block: \"center\", inline: \"center\", behavior: \"instant\" });\n await new Promise<void>((resolve) =>\n requestAnimationFrame(() => requestAnimationFrame(() => resolve())),\n );\n if (!element.isConnected || document.querySelector(selector) !== element)\n return empty(\"target was replaced during layout\");\n const box = element.getBoundingClientRect();\n const x = box.x + box.width / 2;\n const y = box.y + box.height / 2;\n const style = getComputedStyle(element);\n let reason = \"\";\n if (\n box.width <= 0 ||\n box.height <= 0 ||\n style.visibility !== \"visible\" ||\n style.display === \"none\" ||\n Number(style.opacity) === 0\n )\n reason = \"target is hidden or has an empty box\";\n else if (element.matches(\":disabled\") || element.closest('[inert], [aria-disabled=\"true\"]'))\n reason = \"target is disabled or inert\";\n else if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight)\n reason = \"target center is outside the viewport\";\n else {\n const top = document.elementFromPoint(x, y);\n if (!top || !element.contains(top))\n reason = `target is covered by ${top?.tagName ?? \"no element\"}${top?.id ? \"#\" + top.id : \"\"}`;\n }\n return { x, y, width: box.width, height: box.height, reason };\n })();\n}\n\nexport function boxesStable(first: ClickBox, second: ClickBox): boolean {\n return (\n !first.reason &&\n !second.reason &&\n ([\"x\", \"y\", \"width\", \"height\"] as const).every(\n (key) => Math.abs(first[key] - second[key]) < 0.25,\n )\n );\n}\n\nexport class BrowserPage {\n private viewport: Viewport = { ...defaultViewports[0] };\n private loads = 0;\n private pendingRequests = new Map<string, string>();\n private requestUrls = new Map<string, string>();\n private lastNetworkActivity = Date.now();\n\n constructor(\n private cdp: CdpConnection,\n private origin: string,\n private outputDirectory: string,\n private evidence: E2EEvidence,\n private spec: E2ESpec,\n ) {\n cdp.on(\"Page.loadEventFired\", () => {\n this.loads++;\n });\n cdp.on(\"Runtime.consoleAPICalled\", ({ type, args }) =>\n evidence.console.push({\n at: new Date().toISOString(),\n level: type === \"assert\" ? \"error\" : type,\n text: args.map((arg) => String(arg.value ?? arg.description ?? \"\")).join(\" \"),\n }),\n );\n cdp.on(\"Runtime.exceptionThrown\", ({ exceptionDetails }) =>\n evidence.console.push({\n at: new Date().toISOString(),\n level: \"error\",\n text: exceptionDetails.exception?.description ?? exceptionDetails.text,\n }),\n );\n cdp.on(\"Log.entryAdded\", ({ entry }) =>\n evidence.console.push({ at: new Date().toISOString(), level: entry.level, text: entry.text }),\n );\n cdp.on(\"Network.requestWillBeSent\", ({ requestId, request }) => {\n this.pendingRequests.set(requestId, request.url);\n this.requestUrls.set(requestId, request.url);\n this.lastNetworkActivity = Date.now();\n });\n cdp.on(\"Network.responseReceived\", ({ requestId, response }) => {\n this.requestUrls.set(requestId, response.url);\n if (response.status >= 400)\n evidence.networkFailures.push({\n at: new Date().toISOString(),\n url: response.url,\n status: response.status,\n });\n });\n cdp.on(\"Network.loadingFailed\", ({ requestId, errorText, canceled }) => {\n evidence.networkFailures.push({\n at: new Date().toISOString(),\n url: this.requestUrls.get(requestId) ?? `unknown request ${requestId}`,\n errorText,\n canceled,\n });\n this.completeRequest(requestId);\n });\n cdp.on(\"Network.loadingFinished\", ({ requestId }) => this.completeRequest(requestId));\n }\n\n private completeRequest(requestId: string): void {\n this.pendingRequests.delete(requestId);\n this.requestUrls.delete(requestId);\n this.lastNetworkActivity = Date.now();\n }\n\n async initialize(): Promise<void> {\n await this.cdp.command(\"Page.enable\", {});\n await this.cdp.command(\"Runtime.enable\", {});\n await this.cdp.command(\"Network.enable\", {});\n await this.cdp.command(\"Log.enable\", {});\n await this.setViewport(this.spec.viewports?.[0] ?? defaultViewports[0]);\n }\n\n async step<T>(name: string, action: () => Promise<T>): Promise<T> {\n const start = performance.now();\n const entry = {\n name,\n startedAt: new Date().toISOString(),\n durationMs: 0,\n status: \"passed\" as \"passed\" | \"failed\",\n error: undefined as string | undefined,\n };\n this.evidence.steps.push(entry);\n try {\n return await action();\n } catch (error) {\n entry.status = \"failed\";\n entry.error = errorMessage(error);\n throw error;\n } finally {\n entry.durationMs = Math.round(performance.now() - start);\n }\n }\n\n async evaluate<T = unknown>(expression: string, timeoutMs = 10_000): Promise<T> {\n const response = await this.cdp.command(\n \"Runtime.evaluate\",\n { expression, returnByValue: true, awaitPromise: true, silent: true },\n timeoutMs,\n );\n if (response.exceptionDetails)\n throw new Error(\n `Browser evaluation failed: ${response.exceptionDetails.exception?.description ?? response.exceptionDetails.text}`,\n );\n return response.result.value as T;\n }\n\n private async poll(\n description: string,\n check: (remainingMs: number) => Promise<boolean>,\n timeoutMs: number,\n ): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n let reason = \"condition stayed false\";\n while (Date.now() < deadline) {\n try {\n if (await check(Math.max(1, deadline - Date.now()))) return;\n } catch (error) {\n reason = errorMessage(error);\n }\n await delay(Math.min(50, Math.max(0, deadline - Date.now())));\n }\n throw new Error(`${description} timed out after ${timeoutMs}ms: ${reason}`);\n }\n\n async goto(path: string, timeoutMs = 10_000): Promise<void> {\n await this.step(`goto ${path}`, async () => {\n const url = new URL(path, this.origin).href;\n const before = this.loads;\n const navigation = await this.cdp.command(\"Page.navigate\", { url }, timeoutMs);\n if (navigation.errorText) throw new Error(`Navigation failed: ${navigation.errorText}`);\n await this.poll(\n `Navigation to ${url}`,\n (remaining) =>\n this.evaluate<boolean>(\n `document.readyState === 'complete' && ${navigation.loaderId ? this.loads > before : \"true\"}`,\n remaining,\n ),\n timeoutMs,\n );\n });\n }\n\n async waitFor(options: WaitOptions): Promise<void> {\n if (options.selector === undefined && options.text === undefined && options.url === undefined)\n throw new Error(\"waitFor requires a selector, text, or URL\");\n await this.step(`waitFor ${JSON.stringify(options)}`, () =>\n this.poll(\n \"waitFor \" + JSON.stringify(options),\n (remaining) =>\n this.evaluate<boolean>(\n `(() => {\n const options = ${JSON.stringify(options)};\n const element = options.selector ? document.querySelector(options.selector) : document.body;\n if (!element) return false;\n if (options.selector) { const box=element.getBoundingClientRect(); const css=getComputedStyle(element); if (!box.width || !box.height || css.visibility !== 'visible') return false; }\n return (options.text === undefined || (element.innerText ?? element.textContent ?? '').includes(options.text)) &&\n (options.url === undefined || location.href === new URL(options.url, ${JSON.stringify(this.origin)}).href);\n })()`,\n remaining,\n ),\n options.timeoutMs ?? 10_000,\n ),\n );\n }\n\n /** Arms the document/URL comparison BEFORE invoking the navigation action,\n * covering full loads, reloads, hash changes and client-side routing.\n */\n async waitForNavigation(\n action: () => Promise<unknown>,\n options: { url?: string; timeoutMs?: number } = {},\n ): Promise<void> {\n await this.step(\"waitForNavigation\", async () => {\n const before = await this.evaluate<{ href: string; timeOrigin: number }>(\n \"({href:location.href,timeOrigin:performance.timeOrigin})\",\n );\n await action();\n await this.poll(\n \"Navigation\",\n (remaining) =>\n this.evaluate<boolean>(\n `document.readyState === 'complete' && (location.href !== ${JSON.stringify(before.href)} || performance.timeOrigin !== ${before.timeOrigin}) && ${options.url ? `location.href === ${JSON.stringify(new URL(options.url, this.origin).href)}` : \"true\"}`,\n remaining,\n ),\n options.timeoutMs ?? 10_000,\n );\n });\n }\n\n async click(selector: string, timeoutMs = 10_000): Promise<void> {\n await this.step(`click ${selector}`, async () => {\n const deadline = Date.now() + timeoutMs;\n let previous: ClickBox | undefined;\n let reason = \"layout has not settled\";\n while (Date.now() < deadline) {\n const remaining = () => Math.max(1, deadline - Date.now());\n let box: ClickBox;\n try {\n box = await this.evaluate<ClickBox>(\n `(${clickProbe.toString()})(${JSON.stringify(selector)})`,\n remaining(),\n );\n } catch (error) {\n reason = errorMessage(error);\n previous = undefined;\n await delay(25);\n continue;\n }\n reason = box.reason || \"target box is moving\";\n if (previous && boxesStable(previous, box)) {\n // Hover can move menus and overlays. Re-measure and hit-test after it.\n let hovered: ClickBox;\n try {\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n { type: \"mouseMoved\", x: box.x, y: box.y },\n remaining(),\n );\n hovered = await this.evaluate<ClickBox>(\n `(${clickProbe.toString()})(${JSON.stringify(selector)})`,\n remaining(),\n );\n } catch (error) {\n reason = errorMessage(error);\n previous = undefined;\n await delay(Math.min(25, remaining()));\n continue;\n }\n if (Date.now() < deadline && boxesStable(box, hovered)) {\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n {\n type: \"mousePressed\",\n x: hovered.x,\n y: hovered.y,\n button: \"left\",\n buttons: 1,\n clickCount: 1,\n },\n remaining(),\n );\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n {\n type: \"mouseReleased\",\n x: hovered.x,\n y: hovered.y,\n button: \"left\",\n buttons: 0,\n clickCount: 1,\n },\n remaining(),\n );\n return;\n }\n reason = hovered.reason || \"hover changed the target box\";\n previous = undefined;\n } else previous = box;\n await delay(Math.min(50, remaining()));\n }\n throw new Error(`Cannot click ${JSON.stringify(selector)} within ${timeoutMs}ms: ${reason}`);\n });\n }\n\n async fill(selector: string, value: string): Promise<void> {\n await this.step(`fill ${selector}`, async () => {\n await this.click(selector);\n // The native value setter works with controlled inputs; input/change are\n // emitted after clearing. CDP insertText then performs actual text input.\n await this.evaluate(`(() => {\n const element=document.querySelector(${JSON.stringify(selector)});\n if (element !== document.activeElement) throw new Error('Fill target lost focus');\n if ((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled) {\n const prototype=element instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;\n const setter=Object.getOwnPropertyDescriptor(prototype,'value')?.set;\n if (!setter) throw new Error('Fill target has no native value setter');\n setter.call(element,'');\n } else if (element instanceof HTMLElement && element.isContentEditable) element.textContent='';\n else throw new Error('Fill target must be an editable input, textarea or contenteditable');\n element.dispatchEvent(new Event('input',{bubbles:true}));\n })()`);\n await this.evaluate(\n `document.activeElement === document.querySelector(${JSON.stringify(selector)}) || (() => { throw new Error(\"Fill target lost focus after input\"); })()`,\n );\n if (value) await this.cdp.command(\"Input.insertText\", { text: value });\n await this.evaluate(\n \"document.activeElement?.dispatchEvent(new Event('change',{bubbles:true}))\",\n );\n });\n }\n\n /** Appends Unicode text through the browser's text-input path. */\n async type(selector: string, text: string): Promise<void> {\n await this.step(`type ${selector}`, async () => {\n await this.click(selector);\n const needsEndKey = await this.evaluate<boolean>(\n `(() => { const element=document.querySelector(${JSON.stringify(selector)}); if (element !== document.activeElement || !((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled || element instanceof HTMLElement && element.isContentEditable)) throw new Error('Type target is not focused and editable'); if(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { if (typeof element.selectionStart !== \"number\") return true; element.setSelectionRange(element.value.length,element.value.length); } else { const range=document.createRange(); range.selectNodeContents(element); range.collapse(false); const selection=getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } return false; })()`,\n );\n // Email/number inputs do not expose selectionRange; use their native\n // keyboard editing path instead of throwing on setSelectionRange.\n if (needsEndKey) {\n await this.cdp.command(\"Input.dispatchKeyEvent\", {\n type: \"keyDown\",\n key: \"End\",\n code: \"End\",\n windowsVirtualKeyCode: 35,\n });\n await this.cdp.command(\"Input.dispatchKeyEvent\", {\n type: \"keyUp\",\n key: \"End\",\n code: \"End\",\n windowsVirtualKeyCode: 35,\n });\n }\n await this.cdp.command(\"Input.insertText\", { text });\n });\n }\n\n async setViewport(viewport: Viewport): Promise<void> {\n if (\n !/^[a-z0-9_-]+$/i.test(viewport.name) ||\n !Number.isInteger(viewport.width) ||\n !Number.isInteger(viewport.height) ||\n viewport.width <= 0 ||\n viewport.height <= 0\n )\n throw new Error(\"Viewport requires a safe name and positive integer width/height\");\n await this.step(`viewport ${viewport.name} ${viewport.width}x${viewport.height}`, async () => {\n await this.cdp.command(\"Emulation.setDeviceMetricsOverride\", {\n width: viewport.width,\n height: viewport.height,\n deviceScaleFactor: 1,\n mobile: viewport.mobile ?? false,\n });\n this.viewport = { ...viewport };\n await this.evaluate(\n \"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))\",\n );\n });\n }\n\n async assertViewport(): Promise<ViewportReport> {\n return this.step(`assertViewport ${this.viewport.name}`, async () => {\n const report = await this.evaluate<ViewportReport>(`({\n requestedWidth:${this.viewport.width},innerWidth,documentWidth:document.documentElement.scrollWidth,bodyWidth:document.body?.scrollWidth ?? 0,\n offenders:[...document.querySelectorAll('*')].map(element=>{const box=element.getBoundingClientRect();return {tag:element.tagName,id:element.id,className:element.getAttribute('class')??'',width:box.width,scrollWidth:element.scrollWidth,right:box.right,left:box.left}})\n .filter(element=>element.right>${this.viewport.width} || element.left<0 || element.width>${this.viewport.width} || element.scrollWidth>${this.viewport.width})\n .sort((a,b)=>Math.max(b.width,b.scrollWidth,b.right)-Math.max(a.width,a.scrollWidth,a.right)).slice(0,10)\n })`);\n if (\n report.innerWidth !== report.requestedWidth ||\n report.documentWidth > report.requestedWidth ||\n report.bodyWidth > report.requestedWidth\n )\n throw new Error(\n `Horizontal overflow or expanded viewport at ${this.viewport.name}: ${JSON.stringify(report)}`,\n );\n return report;\n });\n }\n\n async screenshot(name: string): Promise<string> {\n if (!/^[a-z0-9_-]+$/i.test(name))\n throw new Error(\"Screenshot names must contain only letters, digits, '_' or '-'\");\n return this.step(`screenshot ${name}`, async () => {\n const file = `${String(this.evidence.screenshots.length + 1).padStart(3, \"0\")}-${name}-${this.viewport.name}.png`;\n const { data } = await this.cdp.command(\"Page.captureScreenshot\", { format: \"png\" });\n await writeFile(join(this.outputDirectory, file), Buffer.from(data, \"base64\"));\n this.evidence.screenshots.push({ name, file, viewport: { ...this.viewport } });\n return join(this.outputDirectory, file);\n });\n }\n\n async captureViewports(\n name: string,\n viewports = this.spec.viewports ?? defaultViewports,\n ): Promise<void> {\n const failures: unknown[] = [];\n const original = this.viewport;\n for (const viewport of viewports) {\n try {\n await this.setViewport(viewport);\n try {\n await this.assertViewport();\n } finally {\n await this.screenshot(name);\n }\n } catch (error) {\n failures.push(error);\n }\n }\n try {\n await this.setViewport(original);\n } catch (error) {\n failures.push(error);\n }\n if (failures.length) throw new AggregateError(failures, failures.map(errorMessage).join(\"; \"));\n }\n\n async waitForNetworkIdle(timeoutMs = 10_000): Promise<void> {\n await this.step(\"waitForNetworkIdle\", () =>\n this.poll(\n \"Network idle\",\n async () => {\n const pending = [...this.pendingRequests.values()].filter(\n (url) => !this.spec.allowPendingRequests?.some((pattern) => matches(pattern, url)),\n );\n if (pending.length) throw new Error(`Pending requests: ${pending.join(\", \")}`);\n return Date.now() - this.lastNetworkActivity >= 300;\n },\n timeoutMs,\n ),\n );\n }\n\n assertHealthy(): void {\n for (const entry of this.evidence.console)\n if (entry.level === \"error\")\n entry.allowed =\n this.spec.allowConsoleErrors?.some((pattern) => matches(pattern, entry.text)) ?? false;\n for (const entry of this.evidence.networkFailures)\n entry.allowed =\n this.spec.allowNetworkFailures?.some(\n (rule) =>\n matches(rule.url, entry.url) &&\n (rule.status === undefined || rule.status === entry.status) &&\n (rule.errorText === undefined || matches(rule.errorText, entry.errorText ?? \"\")),\n ) ?? false;\n const consoleErrors = this.evidence.console.filter(\n (entry) => entry.level === \"error\" && !entry.allowed,\n );\n const networkErrors = this.evidence.networkFailures.filter((entry) => !entry.allowed);\n if (consoleErrors.length || networkErrors.length)\n throw new Error(\n `Browser health failed: ${consoleErrors.length} console errors, ${networkErrors.length} failed HTTP/network requests\\n${JSON.stringify({ console: consoleErrors, network: networkErrors })}`,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { E2EEvidence, E2ESpec, RunE2EOptions } from "./types.mjs";
|
|
2
|
+
//#region ../../packages/core/src/e2e/run-e2e.d.ts
|
|
3
|
+
export type E2EResult = {
|
|
4
|
+
outputDirectory: string;
|
|
5
|
+
evidenceFile: string;
|
|
6
|
+
evidence: E2EEvidence;
|
|
7
|
+
};
|
|
8
|
+
export declare class E2ERunError extends Error {
|
|
9
|
+
readonly result: E2EResult;
|
|
10
|
+
constructor(result: E2EResult);
|
|
11
|
+
}
|
|
12
|
+
/** Each invocation owns a fresh app copy, data root, process groups, profile,
|
|
13
|
+
* screenshots and evidence file. Failures still publish evidence after cleanup.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runE2E(spec: E2ESpec, options?: RunE2EOptions): Promise<E2EResult>;
|
|
16
|
+
//#endregion
|
|
17
|
+
//# sourceMappingURL=run-e2e.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-e2e.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/run-e2e.ts"],"mappings":";;YAOY;EAAc;EAAyB;EAAsB,UAAU;;qBACtE,oBAAoB;WACV,QAAQ;EAA7B,YAAqB,QAAQ;;;;;wBAWT,OAAO,MAAM,SAAS,UAAS,gBAAqB,QAAQ"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { AppFixture } from "./fixture.mjs";
|
|
2
|
+
import { OwnedBrowser } from "./browser.mjs";
|
|
3
|
+
import { errorMessage } from "./types.mjs";
|
|
4
|
+
import { BrowserPage } from "./page.mjs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
//#region ../../packages/core/src/e2e/run-e2e.ts
|
|
9
|
+
var E2ERunError = class extends Error {
|
|
10
|
+
constructor(result) {
|
|
11
|
+
super(`Browser e2e failed: ${result.evidence.errors.join("; ")}\nEvidence: ${result.evidenceFile}`);
|
|
12
|
+
this.result = result;
|
|
13
|
+
this.name = "E2ERunError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** Each invocation owns a fresh app copy, data root, process groups, profile,
|
|
17
|
+
* screenshots and evidence file. Failures still publish evidence after cleanup.
|
|
18
|
+
*/
|
|
19
|
+
async function runE2E(spec, options = {}) {
|
|
20
|
+
if (!spec || !spec.app || typeof spec.run !== "function") throw new Error("An e2e spec must default-export defineE2E({ app, run })");
|
|
21
|
+
const base = resolve(options.outputDirectory ?? ".warlock/e2e");
|
|
22
|
+
await mkdir(base, { recursive: true });
|
|
23
|
+
const outputDirectory = await mkdtemp(join(base, "run-"));
|
|
24
|
+
const started = performance.now();
|
|
25
|
+
const evidence = {
|
|
26
|
+
schemaVersion: 1,
|
|
27
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28
|
+
durationMs: 0,
|
|
29
|
+
status: "running",
|
|
30
|
+
steps: [],
|
|
31
|
+
console: [],
|
|
32
|
+
networkFailures: [],
|
|
33
|
+
screenshots: [],
|
|
34
|
+
processes: [],
|
|
35
|
+
errors: []
|
|
36
|
+
};
|
|
37
|
+
const result = {
|
|
38
|
+
outputDirectory,
|
|
39
|
+
evidenceFile: join(outputDirectory, "evidence.json"),
|
|
40
|
+
evidence
|
|
41
|
+
};
|
|
42
|
+
let fixture;
|
|
43
|
+
let browser;
|
|
44
|
+
let page;
|
|
45
|
+
const abort = new AbortController();
|
|
46
|
+
const forwardAbort = () => abort.abort(options.signal?.reason ?? /* @__PURE__ */ new Error("Browser e2e aborted"));
|
|
47
|
+
options.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
48
|
+
if (options.signal?.aborted) forwardAbort();
|
|
49
|
+
const onInterrupt = () => abort.abort(/* @__PURE__ */ new Error("Browser e2e interrupted (SIGINT)"));
|
|
50
|
+
const onTerminate = () => abort.abort(/* @__PURE__ */ new Error("Browser e2e interrupted (SIGTERM)"));
|
|
51
|
+
process.on("SIGINT", onInterrupt);
|
|
52
|
+
process.on("SIGTERM", onTerminate);
|
|
53
|
+
const timeout = setTimeout(() => abort.abort(/* @__PURE__ */ new Error(`Browser e2e timed out after ${spec.timeoutMs ?? 12e4}ms`)), spec.timeoutMs ?? 12e4);
|
|
54
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
55
|
+
if (abort.signal.aborted) reject(abort.signal.reason);
|
|
56
|
+
else abort.signal.addEventListener("abort", () => reject(abort.signal.reason), { once: true });
|
|
57
|
+
});
|
|
58
|
+
aborted.catch(() => {});
|
|
59
|
+
try {
|
|
60
|
+
abort.signal.throwIfAborted();
|
|
61
|
+
fixture = await AppFixture.create(evidence);
|
|
62
|
+
const context = await fixture.start(spec.app, {
|
|
63
|
+
...options,
|
|
64
|
+
signal: abort.signal
|
|
65
|
+
});
|
|
66
|
+
abort.signal.throwIfAborted();
|
|
67
|
+
browser = new OwnedBrowser(fixture.root, evidence);
|
|
68
|
+
const connection = await browser.start({
|
|
69
|
+
...spec.browser,
|
|
70
|
+
binary: options.browser ?? spec.browser?.binary
|
|
71
|
+
}, abort.signal);
|
|
72
|
+
abort.signal.throwIfAborted();
|
|
73
|
+
page = new BrowserPage(connection, context.url, outputDirectory, evidence, spec);
|
|
74
|
+
await page.initialize();
|
|
75
|
+
const activePage = page;
|
|
76
|
+
await Promise.race([aborted, spec.run({
|
|
77
|
+
...context,
|
|
78
|
+
page: activePage,
|
|
79
|
+
evidence,
|
|
80
|
+
outputDirectory,
|
|
81
|
+
signal: abort.signal,
|
|
82
|
+
step: (name, action) => activePage.step(name, action)
|
|
83
|
+
})]);
|
|
84
|
+
await page.waitForNetworkIdle();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
evidence.errors.push(errorMessage(error));
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
if (page) {
|
|
90
|
+
try {
|
|
91
|
+
await page.captureViewports("final");
|
|
92
|
+
} catch (error) {
|
|
93
|
+
evidence.errors.push(errorMessage(error));
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await page.waitForNetworkIdle();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
evidence.errors.push(errorMessage(error));
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
page.assertHealthy();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
evidence.errors.push(errorMessage(error));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (fixture) for (const owned of fixture.processes) try {
|
|
107
|
+
await writeFile(join(outputDirectory, `${owned.record.role}-${owned.record.pid}.log`), owned.logs());
|
|
108
|
+
} catch (error) {
|
|
109
|
+
evidence.errors.push(`Writing process log: ${errorMessage(error)}`);
|
|
110
|
+
}
|
|
111
|
+
if (browser?.process) try {
|
|
112
|
+
await writeFile(join(outputDirectory, `browser-${browser.process.record.pid}.log`), browser.process.logs());
|
|
113
|
+
} catch (error) {
|
|
114
|
+
evidence.errors.push(`Writing browser log: ${errorMessage(error)}`);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await browser?.stop();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
evidence.errors.push(`Browser teardown: ${errorMessage(error)}`);
|
|
120
|
+
}
|
|
121
|
+
if (browser?.process && !browser.process.record.stopped) {
|
|
122
|
+
evidence.errors.push(`Fixture files retained at ${fixture?.root}: browser could not be safely stopped`);
|
|
123
|
+
const stopped = await Promise.allSettled(fixture?.processes.map((owned) => owned.stop()) ?? []);
|
|
124
|
+
for (const entry of stopped) if (entry.status === "rejected") evidence.errors.push(errorMessage(entry.reason));
|
|
125
|
+
} else try {
|
|
126
|
+
await fixture?.stop();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
evidence.errors.push(`App teardown: ${errorMessage(error)}`);
|
|
129
|
+
}
|
|
130
|
+
options.signal?.removeEventListener("abort", forwardAbort);
|
|
131
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
132
|
+
process.removeListener("SIGTERM", onTerminate);
|
|
133
|
+
evidence.durationMs = Math.round(performance.now() - started);
|
|
134
|
+
evidence.status = evidence.errors.length ? "failed" : "passed";
|
|
135
|
+
await writeFile(result.evidenceFile, JSON.stringify(evidence, null, 2) + "\n");
|
|
136
|
+
}
|
|
137
|
+
if (evidence.status === "failed") throw new E2ERunError(result);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
export { E2ERunError, runE2E };
|
|
143
|
+
//# sourceMappingURL=run-e2e.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-e2e.mjs","names":[],"sources":["../../../../../../../../packages/core/src/e2e/run-e2e.ts"],"sourcesContent":["import { mkdir, mkdtemp, writeFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { OwnedBrowser } from \"./browser\";\nimport { AppFixture } from \"./fixture\";\nimport { BrowserPage } from \"./page\";\nimport { errorMessage, type E2EEvidence, type E2ESpec, type RunE2EOptions } from \"./types\";\n\nexport type E2EResult = { outputDirectory: string; evidenceFile: string; evidence: E2EEvidence };\nexport class E2ERunError extends Error {\n constructor(readonly result: E2EResult) {\n super(\n `Browser e2e failed: ${result.evidence.errors.join(\"; \")}\\nEvidence: ${result.evidenceFile}`,\n );\n this.name = \"E2ERunError\";\n }\n}\n\n/** Each invocation owns a fresh app copy, data root, process groups, profile,\n * screenshots and evidence file. Failures still publish evidence after cleanup.\n */\nexport async function runE2E(spec: E2ESpec, options: RunE2EOptions = {}): Promise<E2EResult> {\n if (!spec || !spec.app || typeof spec.run !== \"function\")\n throw new Error(\"An e2e spec must default-export defineE2E({ app, run })\");\n const base = resolve(options.outputDirectory ?? \".warlock/e2e\");\n await mkdir(base, { recursive: true });\n const outputDirectory = await mkdtemp(join(base, \"run-\"));\n const started = performance.now();\n const evidence: E2EEvidence = {\n schemaVersion: 1,\n startedAt: new Date().toISOString(),\n durationMs: 0,\n status: \"running\",\n steps: [],\n console: [],\n networkFailures: [],\n screenshots: [],\n processes: [],\n errors: [],\n };\n const result: E2EResult = {\n outputDirectory,\n evidenceFile: join(outputDirectory, \"evidence.json\"),\n evidence,\n };\n let fixture: AppFixture | undefined;\n let browser: OwnedBrowser | undefined;\n let page: BrowserPage | undefined;\n const abort = new AbortController();\n const forwardAbort = () =>\n abort.abort(options.signal?.reason ?? new Error(\"Browser e2e aborted\"));\n options.signal?.addEventListener(\"abort\", forwardAbort, { once: true });\n if (options.signal?.aborted) forwardAbort();\n const onInterrupt = () => abort.abort(new Error(\"Browser e2e interrupted (SIGINT)\"));\n const onTerminate = () => abort.abort(new Error(\"Browser e2e interrupted (SIGTERM)\"));\n process.on(\"SIGINT\", onInterrupt);\n process.on(\"SIGTERM\", onTerminate);\n const timeout = setTimeout(\n () => abort.abort(new Error(`Browser e2e timed out after ${spec.timeoutMs ?? 120_000}ms`)),\n spec.timeoutMs ?? 120_000,\n );\n const aborted = new Promise<never>((_resolve, reject) => {\n if (abort.signal.aborted) reject(abort.signal.reason);\n else abort.signal.addEventListener(\"abort\", () => reject(abort.signal.reason), { once: true });\n });\n // Attach immediately: fixture startup may take time before the race is awaited.\n void aborted.catch(() => {});\n try {\n abort.signal.throwIfAborted();\n fixture = await AppFixture.create(evidence);\n const context = await fixture.start(spec.app, { ...options, signal: abort.signal });\n abort.signal.throwIfAborted();\n browser = new OwnedBrowser(fixture.root, evidence);\n const connection = await browser.start(\n { ...spec.browser, binary: options.browser ?? spec.browser?.binary },\n abort.signal,\n );\n abort.signal.throwIfAborted();\n page = new BrowserPage(connection, context.url, outputDirectory, evidence, spec);\n await page.initialize();\n const activePage = page;\n await Promise.race([\n aborted,\n spec.run({\n ...context,\n page: activePage,\n evidence,\n outputDirectory,\n signal: abort.signal,\n step: (name, action) => activePage.step(name, action),\n }),\n ]);\n await page.waitForNetworkIdle();\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n } finally {\n clearTimeout(timeout);\n // Capture both widths even when the spec failed. An overflow failure still\n // captures the offending layout and remains a run failure.\n if (page) {\n try {\n await page.captureViewports(\"final\");\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n }\n try {\n await page.waitForNetworkIdle();\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n }\n try {\n page.assertHealthy();\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n }\n }\n if (fixture) {\n for (const owned of fixture.processes) {\n try {\n await writeFile(\n join(outputDirectory, `${owned.record.role}-${owned.record.pid}.log`),\n owned.logs(),\n );\n } catch (error) {\n evidence.errors.push(`Writing process log: ${errorMessage(error)}`);\n }\n }\n }\n if (browser?.process) {\n try {\n await writeFile(\n join(outputDirectory, `browser-${browser.process.record.pid}.log`),\n browser.process.logs(),\n );\n } catch (error) {\n evidence.errors.push(`Writing browser log: ${errorMessage(error)}`);\n }\n }\n try {\n await browser?.stop();\n } catch (error) {\n evidence.errors.push(`Browser teardown: ${errorMessage(error)}`);\n }\n // Never remove a live browser profile after an identity verification failure.\n if (browser?.process && !browser.process.record.stopped) {\n evidence.errors.push(\n `Fixture files retained at ${fixture?.root}: browser could not be safely stopped`,\n );\n const stopped = await Promise.allSettled(\n fixture?.processes.map((owned) => owned.stop()) ?? [],\n );\n for (const entry of stopped)\n if (entry.status === \"rejected\") evidence.errors.push(errorMessage(entry.reason));\n } else {\n try {\n await fixture?.stop();\n } catch (error) {\n evidence.errors.push(`App teardown: ${errorMessage(error)}`);\n }\n }\n options.signal?.removeEventListener(\"abort\", forwardAbort);\n process.removeListener(\"SIGINT\", onInterrupt);\n process.removeListener(\"SIGTERM\", onTerminate);\n evidence.durationMs = Math.round(performance.now() - started);\n evidence.status = evidence.errors.length ? \"failed\" : \"passed\";\n await writeFile(result.evidenceFile, JSON.stringify(evidence, null, 2) + \"\\n\");\n }\n if (evidence.status === \"failed\") throw new E2ERunError(result);\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BrowserPage } from "./page.mjs";
|
|
2
|
+
//#region ../../packages/core/src/e2e/types.d.ts
|
|
3
|
+
export type Command = readonly [executable: string, ...args: string[]];
|
|
4
|
+
export type Pattern = string | RegExp;
|
|
5
|
+
export type Viewport = {
|
|
6
|
+
name: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
mobile?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare const defaultViewports: readonly Viewport[];
|
|
12
|
+
export type FixtureContext = {
|
|
13
|
+
/** Disposable copy of the app. All commands run here. */
|
|
14
|
+
directory: string;
|
|
15
|
+
dataRoot: string;
|
|
16
|
+
/** OS-selected ephemeral port, reserved until immediately before app startup. */
|
|
17
|
+
port: number;
|
|
18
|
+
url: string;
|
|
19
|
+
/** Write an actual port here when the app binds port 0 itself. */
|
|
20
|
+
portFile: string;
|
|
21
|
+
};
|
|
22
|
+
export type AppFixtureOptions = {
|
|
23
|
+
directory?: string;
|
|
24
|
+
/** Optional build, executed inside the disposable copy before start. */
|
|
25
|
+
build?: Command | ((fixture: FixtureContext) => Command);
|
|
26
|
+
/** Defaults to [process.execPath, "dist/app.js"]. Must stay in the foreground. */
|
|
27
|
+
start?: Command | ((fixture: FixtureContext) => Command);
|
|
28
|
+
/** Only these app variables are added to the isolated environment. */
|
|
29
|
+
env?: Record<string, string> | ((fixture: FixtureContext) => Record<string, string>);
|
|
30
|
+
/** Directories prepended to PATH, for fixture-owned fake engines/binaries. */
|
|
31
|
+
path?: string[];
|
|
32
|
+
/** Prepare data/fake binaries in the disposable fixture before build/start. */
|
|
33
|
+
prepare?: (fixture: FixtureContext) => void | Promise<void>;
|
|
34
|
+
/** Additional relative directories to omit when copying app files. */
|
|
35
|
+
exclude?: string[];
|
|
36
|
+
readyPath?: string;
|
|
37
|
+
/** If true, await portFile instead of using the preallocated port. */
|
|
38
|
+
portFromFile?: boolean;
|
|
39
|
+
startupTimeoutMs?: number;
|
|
40
|
+
};
|
|
41
|
+
export type BrowserOptions = {
|
|
42
|
+
binary?: string;
|
|
43
|
+
/** Additional Chrome flags, e.g. --no-sandbox in a controlled root container. */
|
|
44
|
+
args?: string[];
|
|
45
|
+
};
|
|
46
|
+
export type NetworkFailure = {
|
|
47
|
+
at: string;
|
|
48
|
+
url: string;
|
|
49
|
+
status?: number;
|
|
50
|
+
errorText?: string;
|
|
51
|
+
canceled?: boolean;
|
|
52
|
+
allowed?: boolean;
|
|
53
|
+
};
|
|
54
|
+
export type ConsoleEntry = {
|
|
55
|
+
at: string;
|
|
56
|
+
level: string;
|
|
57
|
+
text: string;
|
|
58
|
+
allowed?: boolean;
|
|
59
|
+
};
|
|
60
|
+
export type StepEvidence = {
|
|
61
|
+
name: string;
|
|
62
|
+
startedAt: string;
|
|
63
|
+
durationMs: number;
|
|
64
|
+
status: "passed" | "failed";
|
|
65
|
+
error?: string;
|
|
66
|
+
};
|
|
67
|
+
export type ProcessEvidence = {
|
|
68
|
+
role: string;
|
|
69
|
+
pid: number;
|
|
70
|
+
childPid?: number;
|
|
71
|
+
command: string[];
|
|
72
|
+
ownerFile: string;
|
|
73
|
+
stopped?: boolean;
|
|
74
|
+
};
|
|
75
|
+
export type E2EEvidence = {
|
|
76
|
+
schemaVersion: 1;
|
|
77
|
+
startedAt: string;
|
|
78
|
+
durationMs: number;
|
|
79
|
+
status: "running" | "passed" | "failed";
|
|
80
|
+
url?: string;
|
|
81
|
+
steps: StepEvidence[];
|
|
82
|
+
console: ConsoleEntry[];
|
|
83
|
+
networkFailures: NetworkFailure[];
|
|
84
|
+
screenshots: {
|
|
85
|
+
name: string;
|
|
86
|
+
file: string;
|
|
87
|
+
viewport: Viewport;
|
|
88
|
+
}[];
|
|
89
|
+
processes: ProcessEvidence[];
|
|
90
|
+
errors: string[];
|
|
91
|
+
};
|
|
92
|
+
export type NetworkAllowance = {
|
|
93
|
+
url: Pattern;
|
|
94
|
+
status?: number;
|
|
95
|
+
errorText?: Pattern;
|
|
96
|
+
};
|
|
97
|
+
export type E2EContext = FixtureContext & {
|
|
98
|
+
page: BrowserPage;
|
|
99
|
+
evidence: E2EEvidence;
|
|
100
|
+
outputDirectory: string;
|
|
101
|
+
signal: AbortSignal;
|
|
102
|
+
step: <T>(name: string, action: () => Promise<T>) => Promise<T>;
|
|
103
|
+
};
|
|
104
|
+
export type E2ESpec = {
|
|
105
|
+
app: AppFixtureOptions;
|
|
106
|
+
browser?: BrowserOptions;
|
|
107
|
+
viewports?: readonly Viewport[];
|
|
108
|
+
allowConsoleErrors?: Pattern[];
|
|
109
|
+
allowNetworkFailures?: NetworkAllowance[];
|
|
110
|
+
/** Long-lived HTTP requests excluded from the final network-idle barrier. */
|
|
111
|
+
allowPendingRequests?: Pattern[];
|
|
112
|
+
timeoutMs?: number;
|
|
113
|
+
run: (context: E2EContext) => Promise<void>;
|
|
114
|
+
};
|
|
115
|
+
export type RunE2EOptions = {
|
|
116
|
+
browser?: string;
|
|
117
|
+
path?: string[];
|
|
118
|
+
outputDirectory?: string;
|
|
119
|
+
signal?: AbortSignal;
|
|
120
|
+
};
|
|
121
|
+
export declare function defineE2E(spec: E2ESpec): E2ESpec;
|
|
122
|
+
//#endregion
|
|
123
|
+
//# sourceMappingURL=types.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/types.ts"],"mappings":";;YAEY,oBAAoB,uBAAuB;YAC3C,mBAAmB;YACnB;EAAa;EAAc;EAAe;EAAgB;;qBACzD,2BAA2B;YAK5B;;EAEV;EACA;;EAEA;EACA;;EAEA;;YAEU;EACV;;EAEA,QAAQ,YAAY,SAAS,mBAAmB;;EAEhD,QAAQ,YAAY,SAAS,mBAAmB;;EAEhD,MAAM,2BAA2B,SAAS,mBAAmB;;EAE7D;;EAEA,WAAW,SAAS,0BAA0B;;EAE9C;EACA;;EAEA;EACA;;YAEU;EACV;;EAEA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA;;YAEU;EAAiB;EAAY;EAAe;EAAc;;YAC1D;EACV;EACA;EACA;EACA;EACA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA,OAAO;EACP,SAAS;EACT,iBAAiB;EACjB;IAAe;IAAc;IAAc,UAAU;;EACrD,WAAW;EACX;;YAEU;EAAqB,KAAK;EAAS;EAAiB,YAAY;;YAChE,aAAa;EACvB,MAAM;EACN,UAAU;EACV;EACA,QAAQ;EACR,OAAO,GAAG,cAAc,cAAc,QAAQ,OAAO,QAAQ;;YAEnD;EACV,KAAK;EACL,UAAU;EACV,qBAAqB;EACrB,qBAAqB;EACrB,uBAAuB;;EAEvB,uBAAuB;EACvB;EACA,MAAM,SAAS,eAAe;;YAEpB;EACV;EACA;EACA;EACA,SAAS;;wBAGK,UAAU,MAAM,UAAU"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region ../../packages/core/src/e2e/types.ts
|
|
2
|
+
const defaultViewports = [{
|
|
3
|
+
name: "desktop",
|
|
4
|
+
width: 1280,
|
|
5
|
+
height: 800
|
|
6
|
+
}, {
|
|
7
|
+
name: "phone",
|
|
8
|
+
width: 390,
|
|
9
|
+
height: 844,
|
|
10
|
+
mobile: true
|
|
11
|
+
}];
|
|
12
|
+
function defineE2E(spec) {
|
|
13
|
+
return spec;
|
|
14
|
+
}
|
|
15
|
+
function errorMessage(error) {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
function matches(pattern, value) {
|
|
19
|
+
return typeof pattern === "string" ? value.includes(pattern) : new RegExp(pattern.source, pattern.flags).test(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { defaultViewports, defineE2E, errorMessage, matches };
|
|
24
|
+
//# sourceMappingURL=types.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../../packages/core/src/e2e/types.ts"],"sourcesContent":["import type { BrowserPage } from \"./page\";\n\nexport type Command = readonly [executable: string, ...args: string[]];\nexport type Pattern = string | RegExp;\nexport type Viewport = { name: string; width: number; height: number; mobile?: boolean };\nexport const defaultViewports: readonly Viewport[] = [\n { name: \"desktop\", width: 1280, height: 800 },\n { name: \"phone\", width: 390, height: 844, mobile: true },\n];\n\nexport type FixtureContext = {\n /** Disposable copy of the app. All commands run here. */\n directory: string;\n dataRoot: string;\n /** OS-selected ephemeral port, reserved until immediately before app startup. */\n port: number;\n url: string;\n /** Write an actual port here when the app binds port 0 itself. */\n portFile: string;\n};\nexport type AppFixtureOptions = {\n directory?: string;\n /** Optional build, executed inside the disposable copy before start. */\n build?: Command | ((fixture: FixtureContext) => Command);\n /** Defaults to [process.execPath, \"dist/app.js\"]. Must stay in the foreground. */\n start?: Command | ((fixture: FixtureContext) => Command);\n /** Only these app variables are added to the isolated environment. */\n env?: Record<string, string> | ((fixture: FixtureContext) => Record<string, string>);\n /** Directories prepended to PATH, for fixture-owned fake engines/binaries. */\n path?: string[];\n /** Prepare data/fake binaries in the disposable fixture before build/start. */\n prepare?: (fixture: FixtureContext) => void | Promise<void>;\n /** Additional relative directories to omit when copying app files. */\n exclude?: string[];\n readyPath?: string;\n /** If true, await portFile instead of using the preallocated port. */\n portFromFile?: boolean;\n startupTimeoutMs?: number;\n};\nexport type BrowserOptions = {\n binary?: string;\n /** Additional Chrome flags, e.g. --no-sandbox in a controlled root container. */\n args?: string[];\n};\nexport type NetworkFailure = {\n at: string;\n url: string;\n status?: number;\n errorText?: string;\n canceled?: boolean;\n allowed?: boolean;\n};\nexport type ConsoleEntry = { at: string; level: string; text: string; allowed?: boolean };\nexport type StepEvidence = {\n name: string;\n startedAt: string;\n durationMs: number;\n status: \"passed\" | \"failed\";\n error?: string;\n};\nexport type ProcessEvidence = {\n role: string;\n pid: number;\n childPid?: number;\n command: string[];\n ownerFile: string;\n stopped?: boolean;\n};\nexport type E2EEvidence = {\n schemaVersion: 1;\n startedAt: string;\n durationMs: number;\n status: \"running\" | \"passed\" | \"failed\";\n url?: string;\n steps: StepEvidence[];\n console: ConsoleEntry[];\n networkFailures: NetworkFailure[];\n screenshots: { name: string; file: string; viewport: Viewport }[];\n processes: ProcessEvidence[];\n errors: string[];\n};\nexport type NetworkAllowance = { url: Pattern; status?: number; errorText?: Pattern };\nexport type E2EContext = FixtureContext & {\n page: BrowserPage;\n evidence: E2EEvidence;\n outputDirectory: string;\n signal: AbortSignal;\n step: <T>(name: string, action: () => Promise<T>) => Promise<T>;\n};\nexport type E2ESpec = {\n app: AppFixtureOptions;\n browser?: BrowserOptions;\n viewports?: readonly Viewport[];\n allowConsoleErrors?: Pattern[];\n allowNetworkFailures?: NetworkAllowance[];\n /** Long-lived HTTP requests excluded from the final network-idle barrier. */\n allowPendingRequests?: Pattern[];\n timeoutMs?: number;\n run: (context: E2EContext) => Promise<void>;\n};\nexport type RunE2EOptions = {\n browser?: string;\n path?: string[];\n outputDirectory?: string;\n signal?: AbortSignal;\n};\n\nexport function defineE2E(spec: E2ESpec): E2ESpec {\n return spec;\n}\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\nexport function matches(pattern: Pattern, value: string): boolean {\n // A fresh regexp avoids stateful /g and /y allowlists changing their verdict.\n return typeof pattern === \"string\"\n ? value.includes(pattern)\n : new RegExp(pattern.source, pattern.flags).test(value);\n}\n"],"mappings":";AAKA,MAAa,mBAAwC,CACnD;CAAE,MAAM;CAAW,OAAO;CAAM,QAAQ;AAAI,GAC5C;CAAE,MAAM;CAAS,OAAO;CAAK,QAAQ;CAAK,QAAQ;AAAK,CACzD;AAmGA,SAAgB,UAAU,MAAwB;CAChD,OAAO;AACT;AACA,SAAgB,aAAa,OAAwB;CACnD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AACA,SAAgB,QAAQ,SAAkB,OAAwB;CAEhE,OAAO,OAAO,YAAY,WACtB,MAAM,SAAS,OAAO,IACtB,IAAI,OAAO,QAAQ,QAAQ,QAAQ,KAAK,CAAC,CAAC,KAAK,KAAK;AAC1D"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { rootPath, srcPath } from "../../utils/paths.mjs";
|
|
2
2
|
import "../../utils/index.mjs";
|
|
3
3
|
import { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from "../generation-fs.mjs";
|
|
4
|
-
import { webContactControllerStub, webContactRoutesStub, webHomePageStub,
|
|
4
|
+
import { webContactControllerStub, webContactRoutesStub, webHomePageStub, webHomeSetupStub, webRootSetupStub, webRootStub } from "../stubs.mjs";
|
|
5
5
|
import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
|
|
6
6
|
import { insertConnectorEntry } from "./shared/insert-connector-entry.mjs";
|
|
7
7
|
import { relocateConflictingHomeRoute } from "./shared/relocate-conflicting-home-route.mjs";
|
|
@@ -65,17 +65,20 @@ async function completeWebInstallation(_options) {
|
|
|
65
65
|
else {
|
|
66
66
|
await ensureDirectoryAsync(srcPath("web"));
|
|
67
67
|
await putFileAsync(rootFile, webRootStub);
|
|
68
|
+
const rootSetupFile = srcPath("web/root.setup.ts");
|
|
69
|
+
if (!await fileExistsAsync(rootSetupFile)) await putFileAsync(rootSetupFile, webRootSetupStub);
|
|
68
70
|
console.log(`${colors.green("✓")} Created src/web/root.tsx`);
|
|
69
71
|
const collision = await relocateConflictingHomeRoute();
|
|
70
72
|
if (collision.outcome === "relocated") console.log(`${colors.green("✓")} Moved the existing ${colors.yellowBright("GET \"/\"")} route to ${colors.yellowBright("\"/welcome\"")} in ${colors.yellowBright(`src/${collision.relativePath}`)} — the new page owns \`/\` now, and the JSON welcome route still answers at /welcome.`);
|
|
71
73
|
if (collision.outcome === "conflict" || collision.outcome === "failed") {
|
|
72
74
|
const verb = collision.outcome === "failed" ? colors.redBright("✗") : colors.yellowBright("!");
|
|
73
|
-
console.log(`${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\n The page
|
|
74
|
-
Free up ${colors.yellowBright("GET \"/\"")} under src/app — move it to a path of its own, or remove it — then create src/web/index.page.tsx yourself. Giving the
|
|
75
|
+
console.log(`${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\n The page setup declares ${colors.yellowBright("route.path = \"/\"")}, and two handlers on one path is a 500 at request time, not a startup error.
|
|
76
|
+
Free up ${colors.yellowBright("GET \"/\"")} under src/app — move it to a path of its own, or remove it — then create src/web/index.page.tsx yourself. Giving the setup file a \`config.route\` other than \`/\` works too.`);
|
|
75
77
|
process.exitCode = 1;
|
|
76
78
|
} else {
|
|
77
79
|
await putFileAsync(srcPath("web/index.page.tsx"), webHomePageStub);
|
|
78
|
-
|
|
80
|
+
const homeSetupFile = srcPath("web/index.setup.ts");
|
|
81
|
+
if (!await fileExistsAsync(homeSetupFile)) await putFileAsync(homeSetupFile, webHomeSetupStub);
|
|
79
82
|
const contactPlan = resolveContactScaffold({
|
|
80
83
|
controllerExists: await fileExistsAsync(srcPath("app/contact/controllers/contact.controller.ts")),
|
|
81
84
|
routesExists: await fileExistsAsync(srcPath("app/contact/routes.ts"))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n putFileAsync,\n} from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport { insertConnectorEntry } from \"./shared/insert-connector-entry\";\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\nimport { resolveContactScaffold } from \"./shared/resolve-contact-scaffold\";\nimport {\n webContactControllerStub,\n webContactRoutesStub,\n webHomePageStub,\n webHomeRegisterStub,\n webRootStub,\n} from \"../stubs\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\n/** The one vite range shared by every feature (web's peer range is `>=7.3.5 <8`). */\nexport const VITE_RANGE = \"^7.3.5\";\n\n/**\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\n *\n * It belongs to the config array or to app code, never both. Both halves are\n * registered before app code loads — the CLI preloader in dev, the generated\n * entry in production — so also calling `connectorsManager.register(...)` in\n * `src/app/main.ts` boots the connector twice and installs every page route\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\n * taken`, because pages and API routes share one route-name namespace.\n *\n * The config array is the half to prefer: `warlock build` reads the same array\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\n * cannot drift.\n *\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\n * app-owned file that may carry any formatting, and a parse-and-print would\n * reformat the parts we did not come to change.\n */\nasync function registerWebConnector(): Promise<void> {\n const configPath = rootPath(\"warlock.config.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\n ` export default defineConfig({ connectors: [webConnector()] });`,\n );\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n\n if (current.includes(\"webConnector\")) {\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\n\n return;\n }\n\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\n\n // An existing `connectors: [` gains one entry; otherwise the key is added to\n // the object `defineConfig` receives.\n const insertion = insertConnectorEntry(next, \"webConnector()\");\n\n if (insertion.status === \"already-present\") {\n return;\n }\n\n if (insertion.status === \"added\") {\n next = insertion.next;\n } else if (next.includes(\"defineConfig({\")) {\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\n } else {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\n \"add `connectors: [webConnector()]` yourself.\",\n );\n\n return;\n }\n\n await putFileAsync(configPath, next);\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\n}\n\n/**\n * Scaffold the smallest page layer that renders, and register the connector.\n *\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\n * ships a default root, so its presence means a human has been here.\n */\nasync function completeWebInstallation(_options: CommandActionData) {\n const rootFile = srcPath(\"web/root.tsx\");\n\n if (await fileExistsAsync(rootFile)) {\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\n } else {\n await ensureDirectoryAsync(srcPath(\"web\"));\n await putFileAsync(rootFile, webRootStub);\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\n\n const collision = await relocateConflictingHomeRoute();\n\n if (collision.outcome === \"relocated\") {\n console.log(\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\n );\n }\n\n // The page is written ONLY when `/` is provably free. Writing it while\n // another handler holds `/` produces a homepage that 500s on first request,\n // which is precisely the outcome a scaffolder must never hand back.\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\n const verb =\n collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\n\n console.log(\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\n ` The page stub declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\n \"path is a 500 at request time, not a startup error.\\n\" +\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the page a `route` other \" +\n \"than `/` works too.\",\n );\n\n // Non-zero on BOTH branches. The page layer this command exists to\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\n // worked\" signal that put `/` in this state to begin with — a conflict we\n // declined to guess at is still an incomplete install, not a success.\n //\n // `exitCode` rather than `exit(1)`: the connector below still has to be\n // registered, and any other feature in the same `warlock add` invocation\n // still has to install, or the project is left half-wired on top of this.\n process.exitCode = 1;\n } else {\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\n await putFileAsync(srcPath(\"web/index.register.ts\"), webHomeRegisterStub);\n\n // Unlike root.tsx above, the contact controller and its routes file are\n // ordinary application files a project can already have — independently\n // of ever having run `warlock add web`. Each is guarded on its OWN\n // existence, not on the (already-consumed) root.tsx sentinel, so an\n // existing `contact` module is skipped rather than clobbered. See\n // resolveContactScaffold's doc comment for why.\n const contactPlan = resolveContactScaffold({\n controllerExists: await fileExistsAsync(\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\n ),\n routesExists: await fileExistsAsync(srcPath(\"app/contact/routes.ts\")),\n });\n\n if (contactPlan.writeController) {\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\n await putFileAsync(\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\n webContactControllerStub,\n );\n }\n\n if (contactPlan.writeRoutes) {\n await ensureDirectoryAsync(srcPath(\"app/contact\"));\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\n }\n\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\n\n for (const message of contactPlan.messages) {\n console.log(message);\n }\n }\n }\n\n await registerWebConnector();\n}\n\nexport const webFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\n dependencies: {\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\n \"@mongez/http\": \"^3.5.0\",\n \"@mongez/react-form\": \"^4.0.0\",\n \"@mongez/react-localization\": \"^3.4.7\",\n react: \"^19.2.3\",\n \"react-dom\": \"^19.2.3\",\n },\n devDependencies: {\n \"@types/react\": \"^19.2.7\",\n \"@types/react-dom\": \"^19.2.3\",\n // Loaded through `await import()` by the dev server only, so both are\n // optional peers of `web` rather than hard dependencies.\n vite: VITE_RANGE,\n \"@vitejs/plugin-react\": \"^5.2.0\",\n },\n onExecuting: completeWebInstallation,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n putFileAsync,\n} from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport { insertConnectorEntry } from \"./shared/insert-connector-entry\";\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\nimport { resolveContactScaffold } from \"./shared/resolve-contact-scaffold\";\nimport {\n webContactControllerStub,\n webContactRoutesStub,\n webHomePageStub,\n webHomeSetupStub,\n webRootStub,\n webRootSetupStub,\n} from \"../stubs\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\n/** The one vite range shared by every feature (web's peer range is `>=7.3.5 <8`). */\nexport const VITE_RANGE = \"^7.3.5\";\n\n/**\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\n *\n * It belongs to the config array or to app code, never both. Both halves are\n * registered before app code loads — the CLI preloader in dev, the generated\n * entry in production — so also calling `connectorsManager.register(...)` in\n * `src/app/main.ts` boots the connector twice and installs every page route\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\n * taken`, because pages and API routes share one route-name namespace.\n *\n * The config array is the half to prefer: `warlock build` reads the same array\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\n * cannot drift.\n *\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\n * app-owned file that may carry any formatting, and a parse-and-print would\n * reformat the parts we did not come to change.\n */\nasync function registerWebConnector(): Promise<void> {\n const configPath = rootPath(\"warlock.config.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\n ` export default defineConfig({ connectors: [webConnector()] });`,\n );\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n\n if (current.includes(\"webConnector\")) {\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\n\n return;\n }\n\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\n\n // An existing `connectors: [` gains one entry; otherwise the key is added to\n // the object `defineConfig` receives.\n const insertion = insertConnectorEntry(next, \"webConnector()\");\n\n if (insertion.status === \"already-present\") {\n return;\n }\n\n if (insertion.status === \"added\") {\n next = insertion.next;\n } else if (next.includes(\"defineConfig({\")) {\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\n } else {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\n \"add `connectors: [webConnector()]` yourself.\",\n );\n\n return;\n }\n\n await putFileAsync(configPath, next);\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\n}\n\n/**\n * Scaffold the smallest page layer that renders, and register the connector.\n *\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\n * ships a default root, so its presence means a human has been here.\n */\nasync function completeWebInstallation(_options: CommandActionData) {\n const rootFile = srcPath(\"web/root.tsx\");\n\n if (await fileExistsAsync(rootFile)) {\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\n } else {\n await ensureDirectoryAsync(srcPath(\"web\"));\n await putFileAsync(rootFile, webRootStub);\n const rootSetupFile = srcPath(\"web/root.setup.ts\");\n if (!(await fileExistsAsync(rootSetupFile))) {\n await putFileAsync(rootSetupFile, webRootSetupStub);\n }\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\n\n const collision = await relocateConflictingHomeRoute();\n\n if (collision.outcome === \"relocated\") {\n console.log(\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\n );\n }\n\n // The page is written ONLY when `/` is provably free. Writing it while\n // another handler holds `/` produces a homepage that 500s on first request,\n // which is precisely the outcome a scaffolder must never hand back.\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\n const verb =\n collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\n\n console.log(\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\n ` The page setup declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\n \"path is a 500 at request time, not a startup error.\\n\" +\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the setup file a `config.route` other \" +\n \"than `/` works too.\",\n );\n\n // Non-zero on BOTH branches. The page layer this command exists to\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\n // worked\" signal that put `/` in this state to begin with — a conflict we\n // declined to guess at is still an incomplete install, not a success.\n //\n // `exitCode` rather than `exit(1)`: the connector below still has to be\n // registered, and any other feature in the same `warlock add` invocation\n // still has to install, or the project is left half-wired on top of this.\n process.exitCode = 1;\n } else {\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\n const homeSetupFile = srcPath(\"web/index.setup.ts\");\n if (!(await fileExistsAsync(homeSetupFile))) {\n await putFileAsync(homeSetupFile, webHomeSetupStub);\n }\n\n // Unlike root.tsx above, the contact controller and its routes file are\n // ordinary application files a project can already have — independently\n // of ever having run `warlock add web`. Each is guarded on its OWN\n // existence, not on the (already-consumed) root.tsx sentinel, so an\n // existing `contact` module is skipped rather than clobbered. See\n // resolveContactScaffold's doc comment for why.\n const contactPlan = resolveContactScaffold({\n controllerExists: await fileExistsAsync(\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\n ),\n routesExists: await fileExistsAsync(srcPath(\"app/contact/routes.ts\")),\n });\n\n if (contactPlan.writeController) {\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\n await putFileAsync(\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\n webContactControllerStub,\n );\n }\n\n if (contactPlan.writeRoutes) {\n await ensureDirectoryAsync(srcPath(\"app/contact\"));\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\n }\n\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\n\n for (const message of contactPlan.messages) {\n console.log(message);\n }\n }\n }\n\n await registerWebConnector();\n}\n\nexport const webFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\n dependencies: {\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\n \"@mongez/http\": \"^3.5.0\",\n \"@mongez/react-form\": \"^4.0.0\",\n \"@mongez/react-localization\": \"^3.4.7\",\n react: \"^19.2.3\",\n \"react-dom\": \"^19.2.3\",\n },\n devDependencies: {\n \"@types/react\": \"^19.2.7\",\n \"@types/react-dom\": \"^19.2.3\",\n // Loaded through `await import()` by the dev server only, so both are\n // optional peers of `web` rather than hard dependencies.\n vite: VITE_RANGE,\n \"@vitejs/plugin-react\": \"^5.2.0\",\n },\n onExecuting: completeWebInstallation,\n};\n"],"mappings":"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"}
|
|
@@ -825,13 +825,18 @@ ${signedInCases.replaceAll("fixture.", "fixture!.")}
|
|
|
825
825
|
});
|
|
826
826
|
`;
|
|
827
827
|
}
|
|
828
|
+
/** `src/web/root.setup.ts` — document policy. */
|
|
829
|
+
const webRootSetupStub = `import type { RootConfig } from "@warlock.js/web";
|
|
830
|
+
|
|
831
|
+
export const config: RootConfig = { strictMode: true };
|
|
832
|
+
`;
|
|
828
833
|
/**
|
|
829
834
|
* `src/web/root.tsx` — the application root for the SSR page layer.
|
|
830
835
|
*
|
|
831
836
|
* Deliberately minimal. The framework ships a default root, so this exists to
|
|
832
837
|
* give you a place to start rather than because anything requires it. The
|
|
833
|
-
*
|
|
834
|
-
*
|
|
838
|
+
* document component stays here; request policy and an app-level loader
|
|
839
|
+
* belong in `root.setup.ts`. Named ErrorBoundary components stay in the UI.
|
|
835
840
|
*/
|
|
836
841
|
const webRootStub = `import type { AppProps } from "@warlock.js/web";
|
|
837
842
|
import { Head, Scripts } from "@warlock.js/web";
|
|
@@ -925,14 +930,14 @@ import { contactController } from "./controllers/contact.controller";
|
|
|
925
930
|
|
|
926
931
|
router.post("/api/contact", contactController, { public: true });
|
|
927
932
|
`;
|
|
928
|
-
/**
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
933
|
+
/** `src/web/index.setup.ts` — page policy and universal registration. */
|
|
934
|
+
const webHomeSetupStub = `import { extend } from "@mongez/localization";
|
|
935
|
+
import type { PageConfig } from "@warlock.js/web";
|
|
936
|
+
|
|
937
|
+
export const config: PageConfig = {
|
|
938
|
+
route: { path: "/", name: "index" },
|
|
939
|
+
metadata: { title: "Home" },
|
|
940
|
+
};
|
|
936
941
|
|
|
937
942
|
export function register() {
|
|
938
943
|
extend("en", {
|
|
@@ -972,25 +977,9 @@ import { setCurrentLocaleCode } from "@mongez/localization";
|
|
|
972
977
|
import { Form, useFormControl, type FormControlProps } from "@mongez/react-form";
|
|
973
978
|
import { transX } from "@mongez/react-localization";
|
|
974
979
|
import { v } from "@warlock.js/seal";
|
|
975
|
-
import { Link, type
|
|
980
|
+
import { Link, type PageProps } from "@warlock.js/web";
|
|
976
981
|
import { useState } from "react";
|
|
977
982
|
|
|
978
|
-
export { register } from "./index.register";
|
|
979
|
-
|
|
980
|
-
/**
|
|
981
|
-
* A page route is an ordinary Warlock route whose handler renders React
|
|
982
|
-
* instead of returning JSON.
|
|
983
|
-
*
|
|
984
|
-
* The URL and stable hydration name are the ones this file DECLARES below.
|
|
985
|
-
* This page answers \`GET "/"\` because \`config.route.path = "/"\`, not because of
|
|
986
|
-
* where the file lives. A page file with
|
|
987
|
-
* no \`config.route\` declaration derives its URL from the filesystem.
|
|
988
|
-
*/
|
|
989
|
-
export const config: PageConfig = {
|
|
990
|
-
route: { path: "/", name: "index" },
|
|
991
|
-
metadata: { title: "Home" },
|
|
992
|
-
};
|
|
993
|
-
|
|
994
983
|
const contactSchema = v.object({
|
|
995
984
|
name: v.string().min(2),
|
|
996
985
|
email: v.email(),
|
|
@@ -1009,13 +998,7 @@ function TextInput({ label, ...controlProps }: FormControlProps & { label: strin
|
|
|
1009
998
|
);
|
|
1010
999
|
}
|
|
1011
1000
|
|
|
1012
|
-
/**
|
|
1013
|
-
* Add a \`loader\` export to fetch data on the server, and it arrives here as
|
|
1014
|
-
* \`data\`, typed:
|
|
1015
|
-
*
|
|
1016
|
-
* export const loader = (async () => ({ items: await itemsRepository.all() }));
|
|
1017
|
-
* export default function HomePage({ data }: PageProps<typeof loader>) { ... }
|
|
1018
|
-
*/
|
|
1001
|
+
/** Fetch data in index.setup.ts; use a type-only loader import for PageProps. */
|
|
1019
1002
|
export default function HomePage(_props: PageProps) {
|
|
1020
1003
|
// Live state. If the button below does nothing, the page rendered on the
|
|
1021
1004
|
// server but never hydrated — the runtime never mounted at \`#vessel\`. This is
|
|
@@ -1189,5 +1172,5 @@ function ContactMessage() {
|
|
|
1189
1172
|
`;
|
|
1190
1173
|
|
|
1191
1174
|
//#endregion
|
|
1192
|
-
export { accessConfigStub, accessResolverStub, accessRoleMigrationStub, accessRoleModelIndexStub, accessRoleModelStub, accessUserRoleMigrationStub, accessUserRoleModelIndexStub, accessUserRoleModelStub, aiConfigStub, communicatorsConfigStub, listNotificationsControllerStub, listNotificationsSchemaStub, markAllNotificationsReadControllerStub, markNotificationReadControllerStub, markNotificationReadServiceStub, notificationControllerSpecStub, notificationLocalesStub, notificationMigrationStub, notificationModelStub, notificationResourceStub, notificationRoutesStub, notificationTestFixtureStub, notificationsConfigStub, notificationsRequestTypeStub, socketConfigStub, unreadNotificationsCountControllerStub, webContactControllerStub, webContactRoutesStub, webHomePageStub,
|
|
1175
|
+
export { accessConfigStub, accessResolverStub, accessRoleMigrationStub, accessRoleModelIndexStub, accessRoleModelStub, accessUserRoleMigrationStub, accessUserRoleModelIndexStub, accessUserRoleModelStub, aiConfigStub, communicatorsConfigStub, listNotificationsControllerStub, listNotificationsSchemaStub, markAllNotificationsReadControllerStub, markNotificationReadControllerStub, markNotificationReadServiceStub, notificationControllerSpecStub, notificationLocalesStub, notificationMigrationStub, notificationModelStub, notificationResourceStub, notificationRoutesStub, notificationTestFixtureStub, notificationsConfigStub, notificationsRequestTypeStub, socketConfigStub, unreadNotificationsCountControllerStub, webContactControllerStub, webContactRoutesStub, webHomePageStub, webHomeSetupStub, webRootSetupStub, webRootStub };
|
|
1193
1176
|
//# sourceMappingURL=stubs.mjs.map
|