@ultimat3/cli 20.2.1 → 22.0.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/CLAUDE.md +84 -1582
- package/README.md +40 -2
- package/package.json +31 -31
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- package/src/app-env.ts +2 -2
- package/src/app-load.ts +37 -5
- package/src/app-openapi.ts +27 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-root.ts +1 -1
- package/src/async-pages.ts +32 -0
- package/src/bin.ts +7 -1
- package/src/boundary-findings.ts +36 -0
- package/src/browser-launcher-fake-html.ts +80 -0
- package/src/browser-launcher-fake.ts +165 -0
- package/src/browser-launcher-port.ts +159 -0
- package/src/browser-launcher.ts +26 -133
- package/src/budgets.ts +56 -13
- package/src/build-errors.ts +54 -0
- package/src/cdp-shot-a11y.ts +77 -0
- package/src/cdp-shot-clock.ts +14 -0
- package/src/cdp-shot-driver.ts +150 -0
- package/src/cdp-shot-element.ts +147 -0
- package/src/cdp-shot-errors.ts +62 -0
- package/src/cdp-shot-keys.ts +152 -0
- package/src/cdp-shot-page.ts +230 -0
- package/src/cdp-shot-watch.ts +241 -0
- package/src/cmd-affected-spec.ts +24 -0
- package/src/cmd-affected.ts +3 -19
- package/src/cmd-build-spec.ts +16 -0
- package/src/cmd-build.ts +46 -20
- package/src/cmd-ci-spec.ts +24 -0
- package/src/cmd-ci.ts +17 -21
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db-spec.ts +99 -0
- package/src/cmd-db.ts +15 -94
- package/src/cmd-deploy-helm.ts +136 -0
- package/src/cmd-deploy-spec.ts +40 -0
- package/src/cmd-deploy.ts +117 -42
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +78 -122
- package/src/cmd-docs-spec.ts +16 -0
- package/src/cmd-docs.ts +2 -11
- package/src/cmd-doctor-spec.ts +21 -0
- package/src/cmd-doctor.ts +4 -18
- package/src/cmd-env-spec.ts +18 -0
- package/src/cmd-env.ts +4 -13
- package/src/cmd-errors-spec.ts +23 -0
- package/src/cmd-errors.ts +3 -17
- package/src/cmd-fix-spec.ts +18 -0
- package/src/cmd-fix.ts +4 -13
- package/src/cmd-generate-spec.ts +31 -0
- package/src/cmd-generate.ts +46 -42
- package/src/cmd-i18n-spec.ts +16 -0
- package/src/cmd-i18n.ts +7 -14
- package/src/cmd-jobs-spec.ts +56 -0
- package/src/cmd-jobs.ts +3 -50
- package/src/cmd-manifest-spec.ts +15 -0
- package/src/cmd-manifest.ts +19 -20
- package/src/cmd-mcp-spec.ts +21 -0
- package/src/cmd-mcp.ts +2 -15
- package/src/cmd-new-spec.ts +33 -0
- package/src/cmd-new.ts +38 -33
- package/src/cmd-policy-spec.ts +13 -0
- package/src/cmd-policy.ts +2 -9
- package/src/cmd-pr-spec.ts +39 -0
- package/src/cmd-pr.ts +5 -65
- package/src/cmd-registries-spec.ts +32 -0
- package/src/cmd-registries.ts +4 -25
- package/src/cmd-routes-spec.ts +12 -0
- package/src/cmd-routes.ts +2 -7
- package/src/cmd-secrets-spec.ts +19 -0
- package/src/cmd-secrets.ts +22 -34
- package/src/cmd-shot-island.ts +12 -13
- package/src/cmd-shot-spec.ts +53 -0
- package/src/cmd-shot.ts +11 -60
- package/src/cmd-tasks-spec.ts +21 -0
- package/src/cmd-tasks.ts +2 -16
- package/src/cmd-test-spec.ts +54 -0
- package/src/cmd-test.ts +4 -49
- package/src/cmd-verify-spec.ts +28 -0
- package/src/cmd-verify.ts +3 -23
- package/src/db-seed.ts +4 -2
- package/src/dev-dashboard.ts +4 -4
- package/src/dev-lock.ts +18 -2
- package/src/dev-port.ts +21 -0
- package/src/dev-route-table.ts +119 -0
- package/src/dispatch.ts +12 -5
- package/src/document-styles.ts +1 -1
- package/src/drift.ts +4 -18
- package/src/error-catalog.ts +16 -9
- package/src/error-codes.ts +33 -28
- package/src/error-pages.ts +19 -7
- package/src/error-unthrown.ts +130 -0
- package/src/errors.ts +22 -29
- package/src/favicon.ts +2 -2
- package/src/fix-imports.ts +1 -1
- package/src/fix-scan.ts +2 -9
- package/src/flag-reads.ts +1 -2
- package/src/foreign-text.ts +36 -0
- package/src/framework-schema.ts +5 -6
- package/src/generate-feature.ts +42 -0
- package/src/generate-files.ts +7 -3
- package/src/generate-grants.ts +83 -0
- package/src/generate-kinds.ts +56 -4
- package/src/i18n-index.ts +59 -10
- package/src/icon-assets.ts +1 -1
- package/src/image-prepare.ts +14 -0
- package/src/import-scan.ts +63 -0
- package/src/index.ts +67 -334
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +38 -11
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +98 -0
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +2 -2
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/live-routes.ts +82 -42
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +30 -26
- package/src/mcp-host.ts +27 -12
- package/src/mcp-ui-diff.ts +27 -0
- package/src/mcp-ui-inspect.ts +4 -4
- package/src/mcp-ui-interact.ts +21 -15
- package/src/mcp-ui.ts +19 -15
- package/src/measure-database.ts +73 -0
- package/src/measure-paths.ts +74 -0
- package/src/measure-scope.ts +74 -0
- package/src/messages.ts +1 -3
- package/src/metrics-endpoint.ts +1 -1
- package/src/otlp-export.ts +1 -1
- package/src/output.ts +6 -0
- package/src/page-sync.ts +54 -0
- package/src/permission-grants.ts +86 -0
- package/src/prerender-out.ts +25 -0
- package/src/prerender.ts +121 -102
- package/src/pwa-artifacts.ts +3 -3
- package/src/realtime-browser-probe-fixture.ts +2 -2
- package/src/reexport-manifest.ts +2 -1
- package/src/registry.ts +80 -56
- package/src/role-realtime.ts +36 -0
- package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
- package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
- package/src/role-start-types.ts +112 -0
- package/src/{dev-roles.ts → role-start.ts} +42 -115
- package/src/{dev-sync.ts → role-sync.ts} +23 -11
- package/src/root-env.ts +67 -0
- package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
- package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
- package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
- package/src/runtime-jobs.ts +87 -0
- package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
- package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
- package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
- package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
- package/src/runtime-realtime.ts +55 -0
- package/src/{dev-render.ts → runtime-render.ts} +119 -20
- package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
- package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
- package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
- package/src/scaffold-fixture.ts +28 -6
- package/src/scaffold-typecheck.ts +6 -3
- package/src/schema-drift.ts +7 -1
- package/src/script-csp.ts +5 -2
- package/src/secrets-rotation.ts +59 -0
- package/src/serve-boot.ts +192 -0
- package/src/serve-drain.ts +24 -0
- package/src/serve-entry.ts +6 -0
- package/src/serve-env.ts +116 -0
- package/src/serve-types.ts +55 -0
- package/src/serve.ts +44 -338
- package/src/shot-server.ts +2 -2
- package/src/shot-settle.ts +10 -1
- package/src/shot-theme.ts +3 -3
- package/src/shot-verdict.ts +16 -7
- package/src/signal-shred.ts +27 -0
- package/src/solid-loader.ts +26 -2
- package/src/static-report.ts +8 -1
- package/src/sw-artifacts.ts +13 -3
- package/src/sync-url.ts +31 -0
- package/src/templates/action.ts +30 -16
- package/src/templates/entity.ts +12 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/job.ts +10 -7
- package/src/templates/policy.ts +20 -2
- package/src/templates/resource-create.ts +127 -0
- package/src/templates/resource-form-island.ts +76 -30
- package/src/templates/resource.ts +10 -4
- package/src/templates/route.ts +3 -0
- package/src/templates/scaffold-app.ts +4 -1
- package/src/templates/scaffold-auth.ts +3 -1
- package/src/templates/scaffold-container-compose.ts +184 -0
- package/src/templates/scaffold-container.ts +25 -145
- package/src/templates/scaffold-dashboard-example.ts +2 -2
- package/src/templates/scaffold-db-package.ts +16 -3
- package/src/templates/scaffold-demo-org.ts +41 -0
- package/src/templates/scaffold-entries.ts +1 -1
- package/src/templates/scaffold-env.ts +6 -0
- package/src/templates/scaffold-helm-templates.ts +66 -7
- package/src/templates/scaffold-helm.ts +27 -5
- package/src/templates/scaffold-i18n.ts +15 -10
- package/src/templates/scaffold-repo.ts +13 -10
- package/src/templates/scaffold-roles.ts +38 -10
- package/src/templates/slice-foundation.ts +1 -1
- package/src/templates/wrap.ts +4 -1
- package/src/test-passes.ts +2 -1
- package/src/test-workers.ts +26 -0
- package/src/ts-scan.ts +3 -6
- package/src/tsconfig-references.ts +1 -2
- package/src/verify-checks.ts +41 -39
- package/src/verify-e2e.ts +41 -0
- package/src/verify-run.ts +115 -50
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +22 -30
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/worker-bundle.ts +192 -0
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -100
- package/src/cdp-connection.ts +0 -211
- package/src/cdp-e2e-page.ts +0 -209
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -139
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -97
- package/src/e2e-errors.ts +0 -103
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -150
- package/src/e2e-selection.ts +0 -182
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// CSS selectors over an HTML string, on Bun's own `HTMLRewriter`: the `query()` the offline
|
|
2
|
+
// `fakeShotDriver` answers from markup. The selector subset is lol-html's — type, `#id`, `.class`,
|
|
3
|
+
// `[attr]`, `[attr=value]`, descendant and child combinators. No box and no hit-target on any
|
|
4
|
+
// answer: this has no layout engine, and a fabricated box would make a covered button clickable.
|
|
5
|
+
import type { ElementSnapshot } from './browser-launcher-port';
|
|
6
|
+
|
|
7
|
+
/** Elements with no end tag: `onEndTag` never fires for them, so they close on open. */
|
|
8
|
+
const VOID_TAGS = new Set([
|
|
9
|
+
'area',
|
|
10
|
+
'base',
|
|
11
|
+
'br',
|
|
12
|
+
'col',
|
|
13
|
+
'embed',
|
|
14
|
+
'hr',
|
|
15
|
+
'img',
|
|
16
|
+
'input',
|
|
17
|
+
'link',
|
|
18
|
+
'meta',
|
|
19
|
+
'source',
|
|
20
|
+
'track',
|
|
21
|
+
'wbr',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const HIDDEN_STYLE = /(?:^|;)\s*(?:display\s*:\s*none|visibility\s*:\s*hidden)\s*(?:;|$)/i;
|
|
25
|
+
|
|
26
|
+
const visible = (tag: string, attrs: ReadonlyMap<string, string>): boolean =>
|
|
27
|
+
!attrs.has('hidden') &&
|
|
28
|
+
!(tag === 'input' && attrs.get('type')?.toLowerCase() === 'hidden') &&
|
|
29
|
+
!HIDDEN_STYLE.test(attrs.get('style') ?? '') &&
|
|
30
|
+
attrs.get('aria-hidden') !== 'true';
|
|
31
|
+
|
|
32
|
+
interface Open {
|
|
33
|
+
readonly tag: string;
|
|
34
|
+
readonly attrs: Map<string, string>;
|
|
35
|
+
readonly parts: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const finish = (open: Open): ElementSnapshot => ({
|
|
39
|
+
tag: open.tag,
|
|
40
|
+
attrs: Object.fromEntries(open.attrs),
|
|
41
|
+
text: open.parts.join('').replaceAll(/\s+/g, ' ').trim(),
|
|
42
|
+
value: open.attrs.get('value') ?? '',
|
|
43
|
+
visible: visible(open.tag, open.attrs),
|
|
44
|
+
enabled: !open.attrs.has('disabled') && open.attrs.get('aria-disabled') !== 'true',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** Every element matching `selector`, as the markup states it. A selector lol-html refuses throws. */
|
|
48
|
+
export async function queryHtml(
|
|
49
|
+
html: string,
|
|
50
|
+
selector: string,
|
|
51
|
+
): Promise<readonly ElementSnapshot[]> {
|
|
52
|
+
const done: ElementSnapshot[] = [];
|
|
53
|
+
const open: Open[] = [];
|
|
54
|
+
const rewriter = new HTMLRewriter().on(selector, {
|
|
55
|
+
element(element): void {
|
|
56
|
+
const tag = element.tagName.toLowerCase();
|
|
57
|
+
const attrs = new Map<string, string>();
|
|
58
|
+
for (const [name, value] of element.attributes) attrs.set(name.toLowerCase(), value);
|
|
59
|
+
const record: Open = { tag, attrs, parts: [] };
|
|
60
|
+
if (VOID_TAGS.has(tag)) {
|
|
61
|
+
done.push(finish(record));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
open.push(record);
|
|
65
|
+
element.onEndTag(() => {
|
|
66
|
+
const index = open.lastIndexOf(record);
|
|
67
|
+
if (index !== -1) open.splice(index, 1);
|
|
68
|
+
done.push(finish(record));
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
text(chunk): void {
|
|
72
|
+
// Every OPEN match: `div, span` matches both, and both contain the text.
|
|
73
|
+
for (const record of open) record.parts.push(chunk.text);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
await rewriter.transform(new Response(html)).text();
|
|
77
|
+
// Never closed is malformed markup; the element still matched, so it is still reported.
|
|
78
|
+
for (const record of open) done.push(finish(record));
|
|
79
|
+
return done;
|
|
80
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// `fakeShotDriver`: a `ShotDriver` over recorded pages, so every `x shot` and `ui.*` test runs with
|
|
2
|
+
// no Chrome. It parses markup and runs none of it — `query()` is answered from the HTML,
|
|
3
|
+
// `evaluate()` from a recorded table, and a click follows `data-goto` or an `<a href>`. What it
|
|
4
|
+
// cannot know it refuses by name rather than inventing: no layout box, and no accessibility tree.
|
|
5
|
+
import { hostDecision, notImplemented } from '@ultimat3/core';
|
|
6
|
+
import { CdpCallFailedError } from '@ultimat3/testing';
|
|
7
|
+
import { queryHtml } from './browser-launcher-fake-html';
|
|
8
|
+
import type {
|
|
9
|
+
CaptureClip,
|
|
10
|
+
ElementSnapshot,
|
|
11
|
+
NetworkEntry,
|
|
12
|
+
ShotColorScheme,
|
|
13
|
+
ShotDriver,
|
|
14
|
+
ShotPage,
|
|
15
|
+
ShotSession,
|
|
16
|
+
ShotSessionInit,
|
|
17
|
+
} from './browser-launcher-port';
|
|
18
|
+
import { awaitReady } from './cdp-shot-element';
|
|
19
|
+
import { ShotHostRefusedError } from './cdp-shot-errors';
|
|
20
|
+
import { parseKeyChord } from './cdp-shot-keys';
|
|
21
|
+
|
|
22
|
+
export const FAKE_SHOT_DRIVER = 'fake';
|
|
23
|
+
|
|
24
|
+
/** One recorded page: its url, its markup, and `expression -> JSON text` for `evaluate()`. */
|
|
25
|
+
export interface FakeShotPage {
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly html: string;
|
|
28
|
+
readonly evaluate?: Readonly<Record<string, string>> | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A PNG signature, and nothing behind it: deterministic bytes, never a render. */
|
|
32
|
+
export const FAKE_PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Different deterministic bytes for every framing fact that was set — one per rectangle, one per
|
|
36
|
+
* colour preference — so a caller that dropped the clip or the scheme fails a test here, which is
|
|
37
|
+
* the only place a framing knob can be proved without a browser (issue #338).
|
|
38
|
+
*/
|
|
39
|
+
export const framedPng = (clip: CaptureClip | undefined, scheme: ShotColorScheme): Uint8Array => {
|
|
40
|
+
const notes =
|
|
41
|
+
(clip === undefined
|
|
42
|
+
? ''
|
|
43
|
+
: ` clip ${String(clip.x)},${String(clip.y)},${String(clip.width)},${String(clip.height)}`) +
|
|
44
|
+
(scheme === 'no-preference' ? '' : ` scheme ${scheme}`);
|
|
45
|
+
if (notes === '') return FAKE_PNG;
|
|
46
|
+
const suffix = new TextEncoder().encode(notes);
|
|
47
|
+
const out = new Uint8Array(FAKE_PNG.length + suffix.length);
|
|
48
|
+
out.set(FAKE_PNG);
|
|
49
|
+
out.set(suffix, FAKE_PNG.length);
|
|
50
|
+
return out;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const normalise = (url: string): string => {
|
|
54
|
+
try {
|
|
55
|
+
return new URL(url).toString();
|
|
56
|
+
} catch {
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function fakePage(byUrl: ReadonlyMap<string, FakeShotPage>, init: ShotSessionInit): ShotPage {
|
|
62
|
+
let current: FakeShotPage | undefined;
|
|
63
|
+
let scheme: ShotColorScheme = 'no-preference';
|
|
64
|
+
const network: NetworkEntry[] = [];
|
|
65
|
+
const url = (): string => current?.url ?? 'about:blank';
|
|
66
|
+
|
|
67
|
+
const navigate = (to: string): void => {
|
|
68
|
+
const page = byUrl.get(normalise(to));
|
|
69
|
+
if (page === undefined) {
|
|
70
|
+
throw new CdpCallFailedError({
|
|
71
|
+
method: `Page.navigate to ${to}`,
|
|
72
|
+
detail: 'fakeShotDriver() has no page recorded at that url',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
current = page;
|
|
76
|
+
network.push({
|
|
77
|
+
method: 'GET',
|
|
78
|
+
url: page.url,
|
|
79
|
+
status: 200,
|
|
80
|
+
resourceType: 'document',
|
|
81
|
+
at: init.clock.now().getTime(),
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const query = (selector: string): Promise<readonly ElementSnapshot[]> =>
|
|
86
|
+
queryHtml(current?.html ?? '', selector);
|
|
87
|
+
|
|
88
|
+
const ready = (selector: string, options?: Parameters<ShotPage['waitFor']>[1]) =>
|
|
89
|
+
awaitReady({
|
|
90
|
+
selector,
|
|
91
|
+
state: options?.state ?? 'actionable',
|
|
92
|
+
timeoutMs: options?.timeout ?? init.timeoutMs,
|
|
93
|
+
clock: init.clock,
|
|
94
|
+
url,
|
|
95
|
+
snapshot: async () => (await query(selector))[0],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
url,
|
|
100
|
+
async goto(to) {
|
|
101
|
+
if (!hostDecision(to, init.rules.allowHosts).allowed) {
|
|
102
|
+
throw new ShotHostRefusedError({ url: to, allowHosts: init.rules.allowHosts });
|
|
103
|
+
}
|
|
104
|
+
navigate(to);
|
|
105
|
+
},
|
|
106
|
+
waitFor: (selector, options) => ready(selector, options),
|
|
107
|
+
async click(selector, options) {
|
|
108
|
+
const target = await ready(selector, options);
|
|
109
|
+
// What the browser would do with the click, as far as markup can say: follow the link.
|
|
110
|
+
const next =
|
|
111
|
+
target.attrs['data-goto'] ?? (target.tag === 'a' ? target.attrs['href'] : undefined);
|
|
112
|
+
if (next !== undefined) navigate(new URL(next, url()).toString());
|
|
113
|
+
},
|
|
114
|
+
async type(selector, _text, options) {
|
|
115
|
+
await ready(selector, options);
|
|
116
|
+
},
|
|
117
|
+
async focus(selector, options) {
|
|
118
|
+
await ready(selector, options);
|
|
119
|
+
},
|
|
120
|
+
async press(chord) {
|
|
121
|
+
parseKeyChord(chord);
|
|
122
|
+
},
|
|
123
|
+
async accessibility() {
|
|
124
|
+
return notImplemented(
|
|
125
|
+
'accessibility() on fakeShotDriver()',
|
|
126
|
+
'assert on the markup with page.query(selector) — a role read off the HTML would hide the element a screen reader cannot name, which is the finding this read exists for',
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
query,
|
|
130
|
+
async evaluate(expression) {
|
|
131
|
+
const table = current?.evaluate ?? {};
|
|
132
|
+
const recorded = Object.hasOwn(table, expression) ? table[expression] : undefined;
|
|
133
|
+
if (recorded === undefined) {
|
|
134
|
+
throw new CdpCallFailedError({
|
|
135
|
+
method: 'Runtime.evaluate',
|
|
136
|
+
detail: 'fakeShotDriver() has no answer recorded for that expression on this page',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return JSON.parse(recorded) as unknown;
|
|
140
|
+
},
|
|
141
|
+
async screenshot(options) {
|
|
142
|
+
return framedPng(options?.clip, scheme);
|
|
143
|
+
},
|
|
144
|
+
async colorScheme(next) {
|
|
145
|
+
scheme = next;
|
|
146
|
+
},
|
|
147
|
+
async prepare() {},
|
|
148
|
+
console: () => [],
|
|
149
|
+
pageErrors: () => [],
|
|
150
|
+
pageErrorsDropped: () => 0,
|
|
151
|
+
network: () => [...network],
|
|
152
|
+
networkDropped: () => 0,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A driver whose every session reads from `pages`. An unrecorded url is refused, never fetched. */
|
|
157
|
+
export function fakeShotDriver(pages: readonly FakeShotPage[]): ShotDriver {
|
|
158
|
+
const byUrl = new Map(pages.map((page) => [normalise(page.url), page]));
|
|
159
|
+
return {
|
|
160
|
+
name: FAKE_SHOT_DRIVER,
|
|
161
|
+
async open(init): Promise<ShotSession> {
|
|
162
|
+
return { page: fakePage(byUrl, init), close: async () => undefined };
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// The browser surface `x shot` and the dev MCP server's `ui.*` tools drive, declared here and
|
|
2
|
+
// nowhere else. `cdp-shot-driver.ts` implements it over raw CDP; a test hands in a fake. Every
|
|
3
|
+
// member is one a shot or a `ui.*` call reads. A member nothing reads is one the raw driver would
|
|
4
|
+
// have to build and nothing would test, and that is how a port rots.
|
|
5
|
+
import type { Clock } from '@ultimat3/core';
|
|
6
|
+
|
|
7
|
+
/** What the browser is told the OS prefers. `'no-preference'` CLEARS the override. */
|
|
8
|
+
export type ShotColorScheme = 'light' | 'dark' | 'no-preference';
|
|
9
|
+
|
|
10
|
+
/** CSS pixels, top-left origin: the space `getBoundingClientRect()` answers in. */
|
|
11
|
+
export interface CaptureClip {
|
|
12
|
+
readonly x: number;
|
|
13
|
+
readonly y: number;
|
|
14
|
+
readonly width: number;
|
|
15
|
+
readonly height: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `fullPage`, or a `clip` — never both. */
|
|
19
|
+
export interface ShotCapture {
|
|
20
|
+
readonly fullPage?: boolean | undefined;
|
|
21
|
+
readonly clip?: CaptureClip | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ConsoleLine {
|
|
25
|
+
readonly level: 'log' | 'info' | 'warn' | 'error' | 'debug';
|
|
26
|
+
readonly text: string;
|
|
27
|
+
readonly at: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** An uncaught exception the page threw. Not a `ConsoleLine`: a throw calls no console method. */
|
|
31
|
+
export interface PageError {
|
|
32
|
+
readonly message: string;
|
|
33
|
+
/** Absent when the exception carried no stack, never `''`. */
|
|
34
|
+
readonly stack?: string | undefined;
|
|
35
|
+
readonly at: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ShotResourceType =
|
|
39
|
+
| 'document'
|
|
40
|
+
| 'stylesheet'
|
|
41
|
+
| 'image'
|
|
42
|
+
| 'media'
|
|
43
|
+
| 'font'
|
|
44
|
+
| 'script'
|
|
45
|
+
| 'xhr'
|
|
46
|
+
| 'fetch'
|
|
47
|
+
| 'websocket'
|
|
48
|
+
| 'other';
|
|
49
|
+
|
|
50
|
+
export interface NetworkEntry {
|
|
51
|
+
readonly method: string;
|
|
52
|
+
readonly url: string;
|
|
53
|
+
/** Absent while in flight, or when the request was refused. */
|
|
54
|
+
readonly status?: number | undefined;
|
|
55
|
+
readonly resourceType: ShotResourceType;
|
|
56
|
+
readonly at: number;
|
|
57
|
+
/** `host`: the allow list refused it. The other two are what a scraping driver can also say. */
|
|
58
|
+
readonly refused?: 'blocked' | 'host' | 'robots' | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What a screen reader is told about one element — the browser's computed role and name. */
|
|
62
|
+
export interface AxNode {
|
|
63
|
+
readonly role: string;
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly description?: string | undefined;
|
|
66
|
+
readonly value?: string | undefined;
|
|
67
|
+
readonly focused?: boolean | undefined;
|
|
68
|
+
readonly disabled?: boolean | undefined;
|
|
69
|
+
readonly ignored: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ElementBox {
|
|
73
|
+
readonly x: number;
|
|
74
|
+
readonly y: number;
|
|
75
|
+
readonly width: number;
|
|
76
|
+
readonly height: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** One element as of one observation — a value, never a live handle. */
|
|
80
|
+
export interface ElementSnapshot {
|
|
81
|
+
readonly tag: string;
|
|
82
|
+
readonly attrs: Readonly<Record<string, string>>;
|
|
83
|
+
readonly text: string;
|
|
84
|
+
readonly value: string;
|
|
85
|
+
readonly visible: boolean;
|
|
86
|
+
readonly enabled: boolean;
|
|
87
|
+
/** Absent on a driver with no layout engine; never a fabricated zero box. */
|
|
88
|
+
readonly box?: ElementBox | undefined;
|
|
89
|
+
/** Whether a click at the element's centre lands on it. Absent: no layout. */
|
|
90
|
+
readonly hitTarget?: boolean | undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Each level implies the ones before it. */
|
|
94
|
+
export type ActionabilityState = 'attached' | 'visible' | 'enabled' | 'actionable';
|
|
95
|
+
|
|
96
|
+
export interface WaitOptions {
|
|
97
|
+
readonly state?: ActionabilityState | undefined;
|
|
98
|
+
/** Milliseconds. Falls back to the session's `timeoutMs`. */
|
|
99
|
+
readonly timeout?: number | undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface AccessibilityOptions {
|
|
103
|
+
readonly max?: number | undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Every wait goes through this, so a test of a 30-second deadline finishes in microseconds. */
|
|
107
|
+
export interface ShotClock extends Clock {
|
|
108
|
+
sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface ShotPage {
|
|
112
|
+
url(): string;
|
|
113
|
+
/** Refused before a byte leaves when the host is outside the session's `allowHosts`. */
|
|
114
|
+
goto(url: string, options?: { readonly timeout?: number | undefined }): Promise<void>;
|
|
115
|
+
/** Blocks until the first match reaches `state` (default `actionable`), then answers it. */
|
|
116
|
+
waitFor(selector: string, options?: WaitOptions): Promise<ElementSnapshot>;
|
|
117
|
+
click(selector: string, options?: WaitOptions): Promise<void>;
|
|
118
|
+
/** Appends. */
|
|
119
|
+
type(selector: string, text: string, options?: WaitOptions): Promise<void>;
|
|
120
|
+
focus(selector: string, options?: WaitOptions): Promise<void>;
|
|
121
|
+
/** `'Meta+K'`, `'Escape'`, `'Shift+Tab'` — on whatever holds focus. */
|
|
122
|
+
press(chord: string): Promise<void>;
|
|
123
|
+
accessibility(selector: string, options?: AccessibilityOptions): Promise<readonly AxNode[]>;
|
|
124
|
+
query(selector: string): Promise<readonly ElementSnapshot[]>;
|
|
125
|
+
/** The expression's result, `unknown` — parse it, never cast it. */
|
|
126
|
+
evaluate(expression: string): Promise<unknown>;
|
|
127
|
+
/** PNG bytes. */
|
|
128
|
+
screenshot(options?: ShotCapture): Promise<Uint8Array>;
|
|
129
|
+
colorScheme(scheme: ShotColorScheme): Promise<void>;
|
|
130
|
+
/** Runs in every document this page navigates to, before the document's own scripts. */
|
|
131
|
+
prepare(expression: string): Promise<void>;
|
|
132
|
+
/** The bounded tails, and how many entries each bound threw away. */
|
|
133
|
+
console(): readonly ConsoleLine[];
|
|
134
|
+
pageErrors(): readonly PageError[];
|
|
135
|
+
pageErrorsDropped(): number;
|
|
136
|
+
network(): readonly NetworkEntry[];
|
|
137
|
+
networkDropped(): number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface ShotSessionInit {
|
|
141
|
+
/** Every error cause raised inside the session carries it. */
|
|
142
|
+
readonly name: string;
|
|
143
|
+
/** Never `*` from a shot: a headless browser inside your network is an SSRF surface. */
|
|
144
|
+
readonly rules: { readonly allowHosts: readonly string[] };
|
|
145
|
+
readonly clock: ShotClock;
|
|
146
|
+
/** Per-operation default, in ms. */
|
|
147
|
+
readonly timeoutMs: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ShotSession {
|
|
151
|
+
readonly page: ShotPage;
|
|
152
|
+
/** Idempotent and never throws: it runs in a `finally`, beside the run's real failure. */
|
|
153
|
+
close(): Promise<void>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface ShotDriver {
|
|
157
|
+
readonly name: string;
|
|
158
|
+
open(init: ShotSessionInit): Promise<ShotSession>;
|
|
159
|
+
}
|
package/src/browser-launcher.ts
CHANGED
|
@@ -1,26 +1,14 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// Two ways to get a browser, and the second is the one production uses. `localBrowser()` starts
|
|
7
|
-
// Chrome in this container; `remoteBrowser({ cdpUrl })` ATTACHES to one somebody else is running,
|
|
8
|
-
// which is what every stealth provider sells — a session created over their API answers with a
|
|
9
|
-
// `wss://` CDP endpoint and a real, unfingerprintable Chromium behind it. `driver-cdp.ts` has
|
|
10
|
-
// called attach its primary path since it shipped, and until now no CLI command could reach it.
|
|
1
|
+
// Which browser `x shot` drives, and the raw-CDP driver over it (`cdp-shot-driver.ts`). No browser
|
|
2
|
+
// library, in the app or here: Chrome is launched on its debugging pipe by `@ultimat3/testing`'s
|
|
3
|
+
// launcher — the one `x verify`'s e2e step already runs on — or ATTACHED to over `--cdp-url`,
|
|
4
|
+
// which is what every stealth provider sells. Until 22.0.0 this resolved the APP's `puppeteer-core`,
|
|
5
|
+
// so `x shot` needed an install the framework could not make, and two launchers drifted.
|
|
11
6
|
|
|
12
7
|
import { existsSync } from 'node:fs';
|
|
13
8
|
import { UltimateError } from '@ultimat3/core';
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The one library this works against. Playwright is not an alternative and is not a flag:
|
|
20
|
-
* `packages/scraping/src/cdp-port.ts` records that its `connectOverCDP` cannot perform the
|
|
21
|
-
* WebSocket upgrade under Bun (oven-sh/bun#9911), verified against puppeteer-core 25.8.0.
|
|
22
|
-
*/
|
|
23
|
-
export const BROWSER_PACKAGE = 'puppeteer-core';
|
|
9
|
+
import { CHROME_CANDIDATES } from '@ultimat3/testing';
|
|
10
|
+
import type { ShotDriver } from './browser-launcher-port';
|
|
11
|
+
import { cdpShotDriver } from './cdp-shot-driver';
|
|
24
12
|
|
|
25
13
|
/** Where a browser binary is named when the flag does not name one. Read in this order. */
|
|
26
14
|
export const BROWSER_PATH_VARS = ['PUPPETEER_EXECUTABLE_PATH', 'CHROME_PATH'] as const;
|
|
@@ -37,33 +25,7 @@ export const BROWSER_CDP_URL_VAR = 'SCRAPE_CDP_URL';
|
|
|
37
25
|
const CDP_SCHEMES = ['ws:', 'wss:', 'http:', 'https:'] as const;
|
|
38
26
|
|
|
39
27
|
/**
|
|
40
|
-
*
|
|
41
|
-
* shapes — nothing resolved, or something resolved that is not a launcher — while the fix is the
|
|
42
|
-
* same install either way, because both are answered by putting the real package in the app.
|
|
43
|
-
*/
|
|
44
|
-
export class ShotBrowserMissingError extends UltimateError {
|
|
45
|
-
constructor(input: { root: string; detail: string }) {
|
|
46
|
-
super({
|
|
47
|
-
code: 'X_SHOT_BROWSER_MISSING',
|
|
48
|
-
// The same install either way: attaching over CDP needs no Chrome on this box, but it still
|
|
49
|
-
// needs a client that speaks the protocol, and `puppeteer-core` is that client.
|
|
50
|
-
cause: `x shot drives a real browser and ${BROWSER_PACKAGE} ${input.detail} from ${input.root}`,
|
|
51
|
-
// One literal, not `bun add -d ${BROWSER_PACKAGE}`: `fix-scan.ts` can only read a fix that IS
|
|
52
|
-
// one literal, and a fix line the gate cannot read is a fix line nothing holds to the
|
|
53
|
-
// contract. `browser-launcher.test.ts` pins it against the constant instead.
|
|
54
|
-
fix: 'bun add -d puppeteer-core',
|
|
55
|
-
meta: { root: input.root, package: BROWSER_PACKAGE },
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* The OTHER half of "no browser": the library is installed and there is no Chrome for it to start.
|
|
62
|
-
*
|
|
63
|
-
* The same code as the class above because it is the same question to a reader — `x shot` has no
|
|
64
|
-
* browser — and a code is a stable public name, not a taxonomy. The cause and the fix are what
|
|
65
|
-
* differ, and they are the half that is acted on: `bun add -d puppeteer-core` cannot install
|
|
66
|
-
* Chrome, and `export CHROME_PATH=…` cannot install a client that speaks CDP.
|
|
28
|
+
* `x shot` has no browser: none was named and none of the probed paths is on disk.
|
|
67
29
|
*
|
|
68
30
|
* Raised BEFORE the dev server, which is the whole point of it: the launch that used to report this
|
|
69
31
|
* happens inside `driver.open()`, one embedded Postgres past the point where the answer was already
|
|
@@ -74,8 +36,7 @@ export class ShotChromeMissingError extends UltimateError {
|
|
|
74
36
|
super({
|
|
75
37
|
code: 'X_SHOT_BROWSER_MISSING',
|
|
76
38
|
cause: `x shot launches a browser here and none was named or found — no ${BROWSER_PATH_VARS.join(' or ')} is set, and none of ${CHROME_CANDIDATES.join(', ')} is on disk`,
|
|
77
|
-
// One literal,
|
|
78
|
-
// export rather than an install because a path is the one repair that works whatever the
|
|
39
|
+
// One literal, so `fix-scan.ts` can read it. A path is the one repair that works whatever the
|
|
79
40
|
// browser is and wherever the distribution put it; `--cdp-url` is the answer for a box that
|
|
80
41
|
// will never have one, and it is a flag `x shot` already ships.
|
|
81
42
|
fix: 'export CHROME_PATH=/usr/bin/google-chrome # any Chrome or Chromium binary; a box that will never have one attaches instead: x shot / --cdp-url wss://cdp.example.com/session/abc',
|
|
@@ -84,47 +45,20 @@ export class ShotChromeMissingError extends UltimateError {
|
|
|
84
45
|
}
|
|
85
46
|
}
|
|
86
47
|
|
|
87
|
-
/**
|
|
88
|
-
* Structural, because this is somebody else's module: a namespace object, a CJS `default`, or a
|
|
89
|
-
* transpiled interop wrapper are all shapes `import()` legitimately hands back, and only one
|
|
90
|
-
* question decides — is the method this run needs there to call?
|
|
91
|
-
*
|
|
92
|
-
* WHICH method is the run's, not this function's. `cdp-port.ts` declares `launch` and `connect`
|
|
93
|
-
* both optional precisely so an attach-only provider SDK satisfies the port, and asking for
|
|
94
|
-
* `launch` when the run is going to `connect` would refuse exactly the library that works.
|
|
95
|
-
*/
|
|
96
|
-
const launcherIn = (module: unknown, method: 'launch' | 'connect'): CdpLauncherLike | undefined => {
|
|
97
|
-
if (typeof module !== 'object' || module === null) return undefined;
|
|
98
|
-
const candidate = module as Record<string, unknown>;
|
|
99
|
-
if (typeof candidate[method] === 'function') return candidate as CdpLauncherLike;
|
|
100
|
-
// `module.exports.default = module.exports` is a real CJS interop shape, so the self-reference is
|
|
101
|
-
// refused rather than followed: one unbounded recursion here is a stack overflow instead of the
|
|
102
|
-
// instruction this whole function exists to produce.
|
|
103
|
-
const inner = candidate['default'];
|
|
104
|
-
if (inner === undefined || inner === candidate) return undefined;
|
|
105
|
-
return launcherIn(inner, method);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
48
|
export interface AppBrowserOptions {
|
|
109
|
-
|
|
110
|
-
/** `--browser`, then `PUPPETEER_EXECUTABLE_PATH`, then `CHROME_PATH`. */
|
|
49
|
+
/** `--browser`, then `PUPPETEER_EXECUTABLE_PATH`, then `CHROME_PATH`, then the probe. */
|
|
111
50
|
readonly executablePath?: string | undefined;
|
|
112
51
|
/**
|
|
113
52
|
* A CDP endpoint to ATTACH to. When it is set nothing is launched here and `executablePath` is
|
|
114
|
-
* not read: the browser is somebody else's, and closing it ends their session too —
|
|
115
|
-
*
|
|
53
|
+
* not read: the browser is somebody else's, and closing it ends their session too — deliberately,
|
|
54
|
+
* so a provider stops billing for a run that ended.
|
|
116
55
|
*/
|
|
117
56
|
readonly cdpUrl?: string | undefined;
|
|
118
57
|
/**
|
|
119
|
-
* The page size this browser lays out at
|
|
120
|
-
*
|
|
121
|
-
* (`packages/scraping/src/page.ts`), so the viewport IS the frame of every picture taken with
|
|
122
|
-
* this driver — which is why `x shot --island` builds one browser per declared viewport.
|
|
58
|
+
* The page size this browser lays out at — the frame of every viewport picture, which is why
|
|
59
|
+
* `x shot --island` builds one browser per declared viewport.
|
|
123
60
|
*/
|
|
124
61
|
readonly viewport?: { readonly width: number; readonly height: number } | undefined;
|
|
125
|
-
/** Test seam: the resolver and the loader, so a test proves the refusal without an install. */
|
|
126
|
-
readonly resolve?: (specifier: string, from: string) => string;
|
|
127
|
-
readonly load?: (path: string) => Promise<unknown>;
|
|
128
62
|
}
|
|
129
63
|
|
|
130
64
|
/** True when a named executable is really there — a bad `--browser` is refused before a boot. */
|
|
@@ -133,12 +67,9 @@ export const browserBinaryExists = (path: string): boolean => existsSync(path);
|
|
|
133
67
|
/**
|
|
134
68
|
* The path a run will launch, or `undefined` when this machine has no browser to launch.
|
|
135
69
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* knowable. So the last step is a PROBE — the same four paths `cdp-launch.ts` already tries for the
|
|
140
|
-
* e2e driver, imported rather than restated, because two lists of Chrome locations that must agree
|
|
141
|
-
* is the drift axiom 2 refuses.
|
|
70
|
+
* The last step is a PROBE — the four paths `cdp-launch.ts` tries for the e2e driver, imported
|
|
71
|
+
* rather than restated, because two lists of Chrome locations that must agree is the drift axiom 2
|
|
72
|
+
* refuses — so a box with no browser is refused before a dev-server boot, not one launch later.
|
|
142
73
|
*
|
|
143
74
|
* A value NAMED in the flag or the environment is answered without touching the filesystem, even
|
|
144
75
|
* when nothing is there: an operator who typed a path has a belief about which binary runs, and
|
|
@@ -192,54 +123,16 @@ export const cdpUrlProblem = (url: string): string | undefined => {
|
|
|
192
123
|
};
|
|
193
124
|
|
|
194
125
|
/**
|
|
195
|
-
* The
|
|
196
|
-
*
|
|
197
|
-
* no such dependency, and would answer "missing" for an app that installed it correctly.
|
|
126
|
+
* The driver a run photographs with. Refused HERE when there is nothing to launch and nothing to
|
|
127
|
+
* attach to, so a missing browser costs no dev-server boot.
|
|
198
128
|
*/
|
|
199
|
-
export async function appBrowser(options: AppBrowserOptions): Promise<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const method = options.cdpUrl === undefined ? 'launch' : 'connect';
|
|
203
|
-
let entry: string;
|
|
204
|
-
try {
|
|
205
|
-
entry = resolve(BROWSER_PACKAGE, options.root);
|
|
206
|
-
} catch {
|
|
207
|
-
throw new ShotBrowserMissingError({ root: options.root, detail: 'does not resolve' });
|
|
208
|
-
}
|
|
209
|
-
const launcher = launcherIn(await load(entry), method);
|
|
210
|
-
if (launcher === undefined) {
|
|
211
|
-
throw new ShotBrowserMissingError({
|
|
212
|
-
root: options.root,
|
|
213
|
-
detail: `resolved to ${entry}, which exports no ${method}()`,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
if (options.cdpUrl !== undefined) {
|
|
217
|
-
return remoteBrowser({
|
|
218
|
-
launcher,
|
|
219
|
-
cdpUrl: options.cdpUrl,
|
|
220
|
-
// The viewport reaches `connect()` the same way it reaches `launch()` — through the
|
|
221
|
-
// pass-through slot — so `x shot --island`'s one-browser-per-viewport loop is unchanged by
|
|
222
|
-
// which half of the port a run took.
|
|
223
|
-
...(options.viewport === undefined
|
|
224
|
-
? {}
|
|
225
|
-
: { options: { defaultViewport: { ...options.viewport } } }),
|
|
226
|
-
});
|
|
129
|
+
export async function appBrowser(options: AppBrowserOptions): Promise<ShotDriver> {
|
|
130
|
+
if (options.cdpUrl === undefined && options.executablePath === undefined) {
|
|
131
|
+
throw new ShotChromeMissingError();
|
|
227
132
|
}
|
|
228
|
-
return
|
|
229
|
-
|
|
230
|
-
headless: true,
|
|
133
|
+
return cdpShotDriver({
|
|
134
|
+
...(options.cdpUrl === undefined ? {} : { cdpUrl: options.cdpUrl }),
|
|
231
135
|
...(options.executablePath === undefined ? {} : { executablePath: options.executablePath }),
|
|
232
|
-
|
|
233
|
-
// that lets the CLI size a browser without `@ultimat3/scraping` naming a puppeteer type.
|
|
234
|
-
//
|
|
235
|
-
// `args` carries `CONTAINER_CHROME_ARGS` on EVERY local launch, viewport or not — the same
|
|
236
|
-
// `--no-sandbox` / `--disable-dev-shm-usage` `cdp-launch.ts`'s e2e driver already needed for
|
|
237
|
-
// this container, read from the one export rather than restated. Before this, `x shot` was the
|
|
238
|
-
// only browser-launching command in the tree with neither, so a box where the e2e gate ran
|
|
239
|
-
// green could not run `x shot` at all — Chrome exits "No usable sandbox".
|
|
240
|
-
options: {
|
|
241
|
-
args: [...CONTAINER_CHROME_ARGS],
|
|
242
|
-
...(options.viewport === undefined ? {} : { defaultViewport: { ...options.viewport } }),
|
|
243
|
-
},
|
|
136
|
+
...(options.viewport === undefined ? {} : { viewport: options.viewport }),
|
|
244
137
|
});
|
|
245
138
|
}
|