@buildinternet/uploads 0.12.1 → 0.13.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/dist/client.d.ts +14 -5
- package/dist/client.js +8 -1
- package/dist/commands/screenshot.js +33 -2
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +6 -1
- package/dist/mcp/tools.js +16 -0
- package/dist/screenshot-local.d.ts +8 -0
- package/dist/screenshot-local.js +20 -2
- package/dist/screenshot-remote.d.ts +8 -0
- package/dist/screenshot.d.ts +34 -0
- package/dist/screenshot.js +48 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { UploadsClientConfig } from "./config.js";
|
|
2
|
+
/** Scoped-operator-token permission scope. */
|
|
3
|
+
export type TokenScope = "files:read" | "files:write" | "files:delete" | "operator:read" | "operator:write" | "workspace:invite" | "workspace:manage";
|
|
2
4
|
/** Allowlisted object provenance (maps to X-Uploads-Meta-* on put). */
|
|
3
5
|
export type ProvenanceInput = {
|
|
4
6
|
client?: string;
|
|
@@ -212,7 +214,7 @@ export interface EnrollmentExchangeResult {
|
|
|
212
214
|
apiUrl?: string;
|
|
213
215
|
workspace: string;
|
|
214
216
|
token: string;
|
|
215
|
-
scopes?: Array<
|
|
217
|
+
scopes?: Array<TokenScope>;
|
|
216
218
|
expiresAt?: string;
|
|
217
219
|
}
|
|
218
220
|
export interface EnrollmentCreateResult {
|
|
@@ -229,9 +231,16 @@ export declare function createEnrollment(apiUrl: string, adminToken: string, inp
|
|
|
229
231
|
email?: string;
|
|
230
232
|
enrollmentSeconds?: number;
|
|
231
233
|
tokenExpiresInSeconds?: number;
|
|
232
|
-
scopes?: Array<
|
|
234
|
+
scopes?: Array<TokenScope>;
|
|
233
235
|
}): Promise<EnrollmentCreateResult>;
|
|
234
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* OAuth client id for the device flow. Registered server-side as a managed
|
|
238
|
+
* official `oauth_client` row (seeded by apps/auth migration
|
|
239
|
+
* 20260719000000_seed_cli_oauth_client.sql — issue #251): public PKCE client,
|
|
240
|
+
* no secret, device-code grant only. The auth worker's device endpoints
|
|
241
|
+
* validate this id against that table, so operators can disable it from
|
|
242
|
+
* /admin/oauth. The literal must match the seeded row's client_id.
|
|
243
|
+
*/
|
|
235
244
|
export declare const DEVICE_CLIENT_ID = "uploads-cli";
|
|
236
245
|
/**
|
|
237
246
|
* User-Agent for device-flow requests. Stored on the Better Auth session row
|
|
@@ -302,7 +311,7 @@ export declare function createWorkspaceRequest(apiUrl: string, accessToken: stri
|
|
|
302
311
|
export interface MintTokenResult {
|
|
303
312
|
token: string;
|
|
304
313
|
workspace: string;
|
|
305
|
-
scopes: Array<
|
|
314
|
+
scopes: Array<TokenScope>;
|
|
306
315
|
label: string | null;
|
|
307
316
|
expiresAt: string | null;
|
|
308
317
|
}
|
|
@@ -332,7 +341,7 @@ export declare function createWorkspaceInvite(apiUrl: string, accessToken: strin
|
|
|
332
341
|
*/
|
|
333
342
|
export declare function mintWorkspaceToken(apiUrl: string, accessToken: string, input: {
|
|
334
343
|
workspace: string;
|
|
335
|
-
scopes?: Array<
|
|
344
|
+
scopes?: Array<TokenScope>;
|
|
336
345
|
label?: string;
|
|
337
346
|
ttlSeconds?: number;
|
|
338
347
|
}): Promise<MintTokenResult>;
|
package/dist/client.js
CHANGED
|
@@ -37,7 +37,14 @@ export function createEnrollment(apiUrl, adminToken, input) {
|
|
|
37
37
|
// D5). Better Auth's `device.code`/`device.token` endpoints take
|
|
38
38
|
// `application/json` bodies, NOT the RFC's form-encoding — the JSON shapes
|
|
39
39
|
// below are what the worker expects.
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* OAuth client id for the device flow. Registered server-side as a managed
|
|
42
|
+
* official `oauth_client` row (seeded by apps/auth migration
|
|
43
|
+
* 20260719000000_seed_cli_oauth_client.sql — issue #251): public PKCE client,
|
|
44
|
+
* no secret, device-code grant only. The auth worker's device endpoints
|
|
45
|
+
* validate this id against that table, so operators can disable it from
|
|
46
|
+
* /admin/oauth. The literal must match the seeded row's client_id.
|
|
47
|
+
*/
|
|
41
48
|
export const DEVICE_CLIENT_ID = "uploads-cli";
|
|
42
49
|
/**
|
|
43
50
|
* User-Agent for device-flow requests. Stored on the Better Auth session row
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { flagBool, flagInt, flagString, flagValues, parseCommandArgs, UsageError, } from "../cli-args.js";
|
|
4
4
|
import { writeCommandHelp } from "../cli-style.js";
|
|
@@ -10,7 +10,7 @@ import { ghMetadataFromTarget } from "../github.js";
|
|
|
10
10
|
import { execRunner } from "../github-gh.js";
|
|
11
11
|
import { parseMetaFlags, validateMetaMap } from "../metadata.js";
|
|
12
12
|
import { writeJson, writeStdout } from "../io.js";
|
|
13
|
-
import { captureScreenshot, parseViewport, parseWaitUntil, } from "../screenshot.js";
|
|
13
|
+
import { assertHideSelector, captureScreenshot, parseViewport, parseWaitUntil, } from "../screenshot.js";
|
|
14
14
|
const SCREENSHOT_HELP = `uploads screenshot <target> [options]
|
|
15
15
|
|
|
16
16
|
Capture a URL or a local .html file and host it — a hosted, PR-embeddable
|
|
@@ -43,6 +43,12 @@ Options:
|
|
|
43
43
|
own prefers-color-scheme queries won't flip)
|
|
44
44
|
--wait <load|domcontentloaded|networkidle|ms> Settle strategy (default: load); a millisecond
|
|
45
45
|
count is local-only — use --via local
|
|
46
|
+
--hide <css> Hide matching elements before capture (repeatable)
|
|
47
|
+
--no-hide-dev-tools Don't auto-hide framework dev toolbars (auto-hidden on localhost/private)
|
|
48
|
+
--reduced-motion Emulate prefers-reduced-motion: reduce so animations settle (best-effort
|
|
49
|
+
on --via remote — neutralizes animations via injected CSS)
|
|
50
|
+
--eval <js> Run JS in the page after settle, before capture (--via local only)
|
|
51
|
+
--init-script <file> Inject a JS file before navigation (--via local only)
|
|
46
52
|
--out <file> Also write the PNG to a local file
|
|
47
53
|
--no-upload Skip hosting; requires --out (local file only)
|
|
48
54
|
--destination <id> Typed root: screenshots | gh | f
|
|
@@ -129,6 +135,26 @@ captureImpl = captureScreenshot) {
|
|
|
129
135
|
const fullPage = flagBool(parsed.flags, "--full-page");
|
|
130
136
|
const colorScheme = colorSchemeFromFlags(parsed.flags);
|
|
131
137
|
const waitUntil = parseWaitUntil(flagString(parsed.flags, "--wait"));
|
|
138
|
+
const hide = flagValues(parsed.flags, "--hide");
|
|
139
|
+
// Fail fast before capture, using the shared policy (throws UploadsError
|
|
140
|
+
// code USAGE → exit 2, same as UsageError) so there's one source of truth.
|
|
141
|
+
for (const sel of hide)
|
|
142
|
+
assertHideSelector(sel);
|
|
143
|
+
// --no-hide-dev-tools opts out of auto-hiding framework toolbars; undefined
|
|
144
|
+
// lets captureScreenshot apply its localhost-aware default.
|
|
145
|
+
const hideDevTools = flagBool(parsed.flags, "--no-hide-dev-tools") ? false : undefined;
|
|
146
|
+
const reducedMotion = flagBool(parsed.flags, "--reduced-motion");
|
|
147
|
+
const evalJs = flagString(parsed.flags, "--eval");
|
|
148
|
+
const initScriptPath = flagString(parsed.flags, "--init-script");
|
|
149
|
+
let initScript;
|
|
150
|
+
if (initScriptPath !== undefined) {
|
|
151
|
+
try {
|
|
152
|
+
initScript = readFileSync(initScriptPath, "utf8");
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
throw new UsageError(`could not read --init-script ${initScriptPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
132
158
|
const outFile = flagString(parsed.flags, "--out");
|
|
133
159
|
const noUpload = flagBool(parsed.flags, "--no-upload");
|
|
134
160
|
if (noUpload && !outFile)
|
|
@@ -207,6 +233,11 @@ captureImpl = captureScreenshot) {
|
|
|
207
233
|
fullPage,
|
|
208
234
|
colorScheme,
|
|
209
235
|
waitUntil,
|
|
236
|
+
hide,
|
|
237
|
+
hideDevTools,
|
|
238
|
+
reducedMotion,
|
|
239
|
+
evalJs,
|
|
240
|
+
initScript,
|
|
210
241
|
apiUrl: ctx.config.apiUrl,
|
|
211
242
|
token: ctx.config.token,
|
|
212
243
|
});
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { METADATA_DESCRIPTION, metadataProp, optPosInt, optString, optStringArray, optStringRecord, usage, type ToolArgs, } from "./args.js";
|
|
2
2
|
export { ToolBatchError, batchFailureMessage } from "./batch-error.js";
|
|
3
|
+
export { mapBounded } from "../async.js";
|
|
3
4
|
export interface McpTool {
|
|
4
5
|
name: string;
|
|
5
6
|
description: string;
|
package/dist/mcp/server.js
CHANGED
|
@@ -12,6 +12,7 @@ import { errorCodeFromUnknown, recordEvent } from "../telemetry.js";
|
|
|
12
12
|
import { ToolBatchError } from "./batch-error.js";
|
|
13
13
|
export { METADATA_DESCRIPTION, metadataProp, optPosInt, optString, optStringArray, optStringRecord, usage, } from "./args.js";
|
|
14
14
|
export { ToolBatchError, batchFailureMessage } from "./batch-error.js";
|
|
15
|
+
export { mapBounded } from "../async.js";
|
|
15
16
|
const SUPPORTED_PROTOCOL_VERSIONS = new Set(["2025-06-18", "2025-03-26", "2024-11-05"]);
|
|
16
17
|
const LATEST_PROTOCOL_VERSION = "2025-06-18";
|
|
17
18
|
function response(id, result) {
|
|
@@ -115,7 +116,11 @@ export function createMcpServer(opts) {
|
|
|
115
116
|
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.has(requested)
|
|
116
117
|
? requested
|
|
117
118
|
: LATEST_PROTOCOL_VERSION;
|
|
118
|
-
return response(id, {
|
|
119
|
+
return response(id, {
|
|
120
|
+
protocolVersion,
|
|
121
|
+
capabilities: { tools: {} },
|
|
122
|
+
serverInfo,
|
|
123
|
+
});
|
|
119
124
|
}
|
|
120
125
|
case "ping":
|
|
121
126
|
return response(id, {});
|
package/dist/mcp/tools.js
CHANGED
|
@@ -546,6 +546,19 @@ export function createUploadsMcpTools(opts) {
|
|
|
546
546
|
type: "string",
|
|
547
547
|
description: 'Settle strategy: load (default) | domcontentloaded | networkidle | a millisecond count (millisecond counts are local-only — via: "local").',
|
|
548
548
|
},
|
|
549
|
+
hide: {
|
|
550
|
+
type: "array",
|
|
551
|
+
items: { type: "string" },
|
|
552
|
+
description: "CSS selectors to hide (display:none) before capture. Works on both backends.",
|
|
553
|
+
},
|
|
554
|
+
noHideDevTools: {
|
|
555
|
+
type: "boolean",
|
|
556
|
+
description: "Don't auto-hide framework dev toolbars (Astro/Next/Nuxt/Vite), which are hidden by default for localhost/private-network targets.",
|
|
557
|
+
},
|
|
558
|
+
reducedMotion: {
|
|
559
|
+
type: "boolean",
|
|
560
|
+
description: 'Emulate prefers-reduced-motion: reduce so animations settle. Best-effort on via: "remote" (neutralizes animations via injected CSS).',
|
|
561
|
+
},
|
|
549
562
|
key: {
|
|
550
563
|
type: "string",
|
|
551
564
|
description: "Explicit object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.png). Cannot be combined with pr/issue.",
|
|
@@ -684,6 +697,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
684
697
|
fullPage: optBool(args, "fullPage"),
|
|
685
698
|
colorScheme: colorSchemeArg,
|
|
686
699
|
waitUntil: screenshotModule.parseWaitUntil(optString(args, "wait")),
|
|
700
|
+
hide: optStringArray(args, "hide"),
|
|
701
|
+
hideDevTools: optBool(args, "noHideDevTools") ? false : undefined,
|
|
702
|
+
reducedMotion: optBool(args, "reducedMotion"),
|
|
687
703
|
apiUrl: config.apiUrl,
|
|
688
704
|
token: config.token,
|
|
689
705
|
});
|
|
@@ -51,6 +51,14 @@ export interface LocalCaptureOptions {
|
|
|
51
51
|
colorScheme?: "dark" | "light";
|
|
52
52
|
/** "load" | "domcontentloaded" | "networkidle", or a millisecond settle delay. */
|
|
53
53
|
waitUntil: "load" | "domcontentloaded" | "networkidle" | number;
|
|
54
|
+
/** CSS selectors hidden (display:none) just before capture. */
|
|
55
|
+
hide?: string[];
|
|
56
|
+
/** Emulate prefers-reduced-motion: reduce so animations settle deterministically. */
|
|
57
|
+
reducedMotion?: boolean;
|
|
58
|
+
/** JS run via page.evaluate after settle, before capture. */
|
|
59
|
+
evalJs?: string;
|
|
60
|
+
/** JS injected via addInitScript before navigation. */
|
|
61
|
+
initScript?: string;
|
|
54
62
|
timeoutMs?: number;
|
|
55
63
|
detectRoots?: DetectRoots;
|
|
56
64
|
/**
|
package/dist/screenshot-local.js
CHANGED
|
@@ -285,19 +285,37 @@ export async function captureLocal(opts) {
|
|
|
285
285
|
viewport: { width: opts.viewport.width, height: opts.viewport.height },
|
|
286
286
|
deviceScaleFactor: opts.viewport.deviceScaleFactor,
|
|
287
287
|
colorScheme: opts.colorScheme,
|
|
288
|
+
reducedMotion: opts.reducedMotion ? "reduce" : undefined,
|
|
288
289
|
});
|
|
290
|
+
// Runs before every navigation on this context — must be registered before
|
|
291
|
+
// the goto below so it's present when the page's own scripts first run.
|
|
292
|
+
if (opts.initScript)
|
|
293
|
+
await context.addInitScript({ content: opts.initScript });
|
|
289
294
|
const page = usingCdp
|
|
290
295
|
? await context.newPage()
|
|
291
296
|
: (context.pages()[0] ?? (await context.newPage()));
|
|
292
297
|
if (usingCdp) {
|
|
293
298
|
await page.setViewportSize({ width: opts.viewport.width, height: opts.viewport.height });
|
|
294
|
-
|
|
295
|
-
|
|
299
|
+
// deviceScaleFactor + reducedMotion can't be set on an already-launched
|
|
300
|
+
// CDP context, but emulateMedia (colorScheme + reduced-motion) still can.
|
|
301
|
+
if (opts.colorScheme || opts.reducedMotion) {
|
|
302
|
+
await page.emulateMedia({
|
|
303
|
+
colorScheme: opts.colorScheme,
|
|
304
|
+
reducedMotion: opts.reducedMotion ? "reduce" : undefined,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
296
307
|
}
|
|
297
308
|
const waitUntil = typeof opts.waitUntil === "string" ? opts.waitUntil : "load";
|
|
298
309
|
await page.goto(opts.url, { waitUntil, timeout: opts.timeoutMs ?? 30_000 });
|
|
299
310
|
if (typeof opts.waitUntil === "number")
|
|
300
311
|
await page.waitForTimeout(opts.waitUntil);
|
|
312
|
+
// Hide overlays first, then run any user eval (which may depend on, or
|
|
313
|
+
// deliberately override, the hidden state).
|
|
314
|
+
if (opts.hide && opts.hide.length > 0) {
|
|
315
|
+
await page.addStyleTag({ content: `${opts.hide.join(",")}{display:none !important}` });
|
|
316
|
+
}
|
|
317
|
+
if (opts.evalJs)
|
|
318
|
+
await page.evaluate(opts.evalJs);
|
|
301
319
|
const png = opts.selector
|
|
302
320
|
? await page.locator(opts.selector).screenshot({ timeout: opts.timeoutMs ?? 30_000 })
|
|
303
321
|
: await page.screenshot({ fullPage: opts.fullPage === true });
|
|
@@ -12,6 +12,14 @@ export interface RemoteRenderRequest {
|
|
|
12
12
|
fullPage?: boolean;
|
|
13
13
|
colorScheme?: "dark" | "light";
|
|
14
14
|
waitUntil?: "load" | "domcontentloaded" | "networkidle" | number;
|
|
15
|
+
/** CSS selectors hidden (display:none) server-side before capture. */
|
|
16
|
+
hide?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Best-effort reduced-motion on the remote backend: the render endpoint has
|
|
19
|
+
* no true media-feature emulation, so it neutralizes animations/transitions
|
|
20
|
+
* with an injected stylesheet (documented gap, like colorScheme).
|
|
21
|
+
*/
|
|
22
|
+
reducedMotion?: boolean;
|
|
15
23
|
}
|
|
16
24
|
export interface RemoteRenderOptions {
|
|
17
25
|
apiUrl: string;
|
package/dist/screenshot.d.ts
CHANGED
|
@@ -2,6 +2,23 @@ import { captureRemote } from "./screenshot-remote.js";
|
|
|
2
2
|
import type { DetectRoots } from "./screenshot-local.js";
|
|
3
3
|
export type ScreenshotBackend = "auto" | "local" | "remote";
|
|
4
4
|
export type WaitUntil = "load" | "domcontentloaded" | "networkidle" | number;
|
|
5
|
+
/**
|
|
6
|
+
* Host selectors for framework dev toolbars/overlays that otherwise pollute a
|
|
7
|
+
* screenshot of a running dev server. Hidden (display:none) automatically when
|
|
8
|
+
* the target is a localhost/private URL, unless `--no-hide-dev-tools`. Hiding
|
|
9
|
+
* the custom-element host also hides its shadow-DOM contents, so a single
|
|
10
|
+
* host-level rule is enough for the web-component toolbars.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEV_TOOLBAR_SELECTORS: readonly string[];
|
|
13
|
+
/**
|
|
14
|
+
* Reject a `--hide` selector that could break out of the generated
|
|
15
|
+
* `selector{display:none}` rule (or inject markup server-side). The selectors
|
|
16
|
+
* are the caller's own, so this guards against footguns, not a trust boundary.
|
|
17
|
+
* `@` is rejected too: a leading at-rule (e.g. `@import url(...);*`) needs no
|
|
18
|
+
* braces to smuggle an `@import` into the injected stylesheet — and `@` is
|
|
19
|
+
* never valid in a CSS selector anyway.
|
|
20
|
+
*/
|
|
21
|
+
export declare function assertHideSelector(selector: string): void;
|
|
5
22
|
export interface ScreenshotViewport {
|
|
6
23
|
width: number;
|
|
7
24
|
height: number;
|
|
@@ -41,6 +58,19 @@ export interface CaptureScreenshotOptions {
|
|
|
41
58
|
fullPage?: boolean;
|
|
42
59
|
colorScheme?: "dark" | "light";
|
|
43
60
|
waitUntil?: WaitUntil;
|
|
61
|
+
/** Extra CSS selectors to hide (display:none) before capture. */
|
|
62
|
+
hide?: string[];
|
|
63
|
+
/**
|
|
64
|
+
* Auto-hide known framework dev toolbars. Defaults to on for localhost/
|
|
65
|
+
* private-network targets and off otherwise; pass `false` to opt out.
|
|
66
|
+
*/
|
|
67
|
+
hideDevTools?: boolean;
|
|
68
|
+
/** Emulate prefers-reduced-motion: reduce so CSS/JS animations settle. */
|
|
69
|
+
reducedMotion?: boolean;
|
|
70
|
+
/** Run this JS in the page after settle, before capture (local backend only). */
|
|
71
|
+
evalJs?: string;
|
|
72
|
+
/** Inject this JS as an init script before navigation (local backend only). */
|
|
73
|
+
initScript?: string;
|
|
44
74
|
apiUrl: string;
|
|
45
75
|
token: string;
|
|
46
76
|
/** Injectable for tests; forwarded to detectLocalBrowser. */
|
|
@@ -55,6 +85,10 @@ export interface CaptureScreenshotOptions {
|
|
|
55
85
|
fullPage?: boolean;
|
|
56
86
|
colorScheme?: "dark" | "light";
|
|
57
87
|
waitUntil: WaitUntil;
|
|
88
|
+
hide?: string[];
|
|
89
|
+
reducedMotion?: boolean;
|
|
90
|
+
evalJs?: string;
|
|
91
|
+
initScript?: string;
|
|
58
92
|
detectRoots?: DetectRoots;
|
|
59
93
|
/** Pre-computed detection result from auto-routing, to avoid a second fs scan. */
|
|
60
94
|
detectResult?: import("./screenshot-local.js").DetectResult;
|
package/dist/screenshot.js
CHANGED
|
@@ -13,6 +13,35 @@ import { basename, resolve as resolvePath } from "node:path";
|
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
14
14
|
import { UploadsError } from "./errors.js";
|
|
15
15
|
import { captureRemote, MAX_REMOTE_HTML_BYTES } from "./screenshot-remote.js";
|
|
16
|
+
/**
|
|
17
|
+
* Host selectors for framework dev toolbars/overlays that otherwise pollute a
|
|
18
|
+
* screenshot of a running dev server. Hidden (display:none) automatically when
|
|
19
|
+
* the target is a localhost/private URL, unless `--no-hide-dev-tools`. Hiding
|
|
20
|
+
* the custom-element host also hides its shadow-DOM contents, so a single
|
|
21
|
+
* host-level rule is enough for the web-component toolbars.
|
|
22
|
+
*/
|
|
23
|
+
export const DEV_TOOLBAR_SELECTORS = [
|
|
24
|
+
"astro-dev-toolbar", // Astro
|
|
25
|
+
"#__next-build-watcher", // Next.js (legacy build-activity indicator)
|
|
26
|
+
"nextjs-portal", // Next.js dev overlay / indicators (App Router)
|
|
27
|
+
"#nuxt-devtools-anchor", // Nuxt DevTools launcher
|
|
28
|
+
"#nuxt-devtools-container",
|
|
29
|
+
"vite-plugin-checker-error-overlay", // vite-plugin-checker overlay
|
|
30
|
+
"vite-error-overlay", // Vite HMR error overlay
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Reject a `--hide` selector that could break out of the generated
|
|
34
|
+
* `selector{display:none}` rule (or inject markup server-side). The selectors
|
|
35
|
+
* are the caller's own, so this guards against footguns, not a trust boundary.
|
|
36
|
+
* `@` is rejected too: a leading at-rule (e.g. `@import url(...);*`) needs no
|
|
37
|
+
* braces to smuggle an `@import` into the injected stylesheet — and `@` is
|
|
38
|
+
* never valid in a CSS selector anyway.
|
|
39
|
+
*/
|
|
40
|
+
export function assertHideSelector(selector) {
|
|
41
|
+
if (selector.length === 0 || /[@{}<>]/.test(selector)) {
|
|
42
|
+
throw new UploadsError(`invalid hide selector: ${JSON.stringify(selector)} (a CSS selector, no @, {, }, <, or >)`, "USAGE");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
16
45
|
export const DEFAULT_SCREENSHOT_VIEWPORT = {
|
|
17
46
|
width: 1280,
|
|
18
47
|
height: 800,
|
|
@@ -159,6 +188,13 @@ export async function captureScreenshot(opts) {
|
|
|
159
188
|
// is sent to the remote backend as an inline `html` body (though anything
|
|
160
189
|
// it references via file:// or relative paths won't resolve there).
|
|
161
190
|
const localOnly = target.kind === "url" && target.localOnly;
|
|
191
|
+
// Auto-hide framework dev toolbars only makes sense for a running dev server
|
|
192
|
+
// (a localhost/private URL); default off elsewhere. Combine with any
|
|
193
|
+
// explicit --hide selectors into one list shared by both backends.
|
|
194
|
+
const autoHideDevTools = opts.hideDevTools ?? localOnly;
|
|
195
|
+
for (const sel of opts.hide ?? [])
|
|
196
|
+
assertHideSelector(sel);
|
|
197
|
+
const hide = [...(opts.hide ?? []), ...(autoHideDevTools ? DEV_TOOLBAR_SELECTORS : [])];
|
|
162
198
|
// Populated only when auto-routing actually probes the filesystem, so it
|
|
163
199
|
// can be threaded into captureLocalImpl below to avoid a second scan.
|
|
164
200
|
let detected;
|
|
@@ -192,6 +228,12 @@ export async function captureScreenshot(opts) {
|
|
|
192
228
|
if (backend === "remote" && typeof waitUntil === "number") {
|
|
193
229
|
throw new UploadsError(`numeric --wait (${waitUntil}ms) is local-only — use --via local, or one of load/domcontentloaded/networkidle for the remote backend`, "USAGE");
|
|
194
230
|
}
|
|
231
|
+
// Arbitrary pre-capture JS runs only on the local backend — the shared
|
|
232
|
+
// remote renderer intentionally has no eval escape hatch (different security
|
|
233
|
+
// posture). Fail fast rather than silently dropping it.
|
|
234
|
+
if (backend === "remote" && (opts.evalJs !== undefined || opts.initScript !== undefined)) {
|
|
235
|
+
throw new UploadsError("--eval and --init-script are local-only — use --via local", "USAGE");
|
|
236
|
+
}
|
|
195
237
|
if (backend === "local") {
|
|
196
238
|
const captureLocalImpl = opts.captureLocalImpl ??
|
|
197
239
|
(async (localOpts) => {
|
|
@@ -207,6 +249,10 @@ export async function captureScreenshot(opts) {
|
|
|
207
249
|
fullPage: opts.fullPage,
|
|
208
250
|
colorScheme: opts.colorScheme,
|
|
209
251
|
waitUntil,
|
|
252
|
+
hide,
|
|
253
|
+
reducedMotion: opts.reducedMotion,
|
|
254
|
+
evalJs: opts.evalJs,
|
|
255
|
+
initScript: opts.initScript,
|
|
210
256
|
detectRoots: opts.detectRoots,
|
|
211
257
|
detectResult: detected,
|
|
212
258
|
});
|
|
@@ -226,6 +272,8 @@ export async function captureScreenshot(opts) {
|
|
|
226
272
|
fullPage: opts.fullPage,
|
|
227
273
|
colorScheme: opts.colorScheme,
|
|
228
274
|
waitUntil,
|
|
275
|
+
...(hide.length > 0 ? { hide } : {}),
|
|
276
|
+
...(opts.reducedMotion ? { reducedMotion: true } : {}),
|
|
229
277
|
}, { apiUrl: opts.apiUrl, token: opts.token });
|
|
230
278
|
return { png, filename, backend };
|
|
231
279
|
}
|