@datarails-ai/dr-appspace-cli 0.12.31 → 0.12.33
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/README.md +41 -0
- package/dist/cli.js +791 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -325,6 +325,47 @@ lives in dr-assets-service, attached to the agent by id — see the repo's
|
|
|
325
325
|
command calls a model: the builders write `contract.json` themselves and run
|
|
326
326
|
`app-space compile`.
|
|
327
327
|
|
|
328
|
+
### A picture of every page, as every role: `test --screenshot`
|
|
329
|
+
|
|
330
|
+
`app-space test --screenshot` draws, after the usual checks, every page of the
|
|
331
|
+
pushed package once per declared role in headless Google Chrome, and writes
|
|
332
|
+
`<out>/<page>.<role>.png` plus an `index.json` that a judge can read (page,
|
|
333
|
+
role, file, size, which handlers the page called, which had no fixture). It
|
|
334
|
+
judges nothing yet; the `look-*` findings come later.
|
|
335
|
+
|
|
336
|
+
| Flag | What it does | Default |
|
|
337
|
+
| --- | --- | --- |
|
|
338
|
+
| `--screenshot` | take the pictures after the checks | off |
|
|
339
|
+
| `--out <dir>` | where the PNGs and `index.json` go | `<dir>/.screenshots` |
|
|
340
|
+
| `--role <name>` | only this declared role (repeatable, or `a,b`) | every declared role (`default` for an app with none) |
|
|
341
|
+
| `--width <px>` / `--height <px>` | the window | `1400` × `900` |
|
|
342
|
+
| `--full` | grow the window to the page's full height (one extra load per picture) | off |
|
|
343
|
+
| `--shelf <path>` | serve `vendor/` and `ui-kit/` from this dr-app-space checkout | the server the app is pushed to |
|
|
344
|
+
| `--screenshot-budget <ms>` | the whole phase's ceiling; each picture is capped at 60 s | `180000` |
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
dr-appspace app-space test ./my-app --skip-handlers --interact never --screenshot -s DEV-1
|
|
348
|
+
dr-appspace app-space test ./my-app --screenshot --role approver --full --shelf ~/dr-app-space -s DEV-1
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The pages load from a stub on `127.0.0.1` (an ephemeral port), never from
|
|
352
|
+
the app's server, so taking pictures writes no row. The stub answers
|
|
353
|
+
`/access` as each role (the manifest's page and handler gates, under the
|
|
354
|
+
server's own answer for that role), each handler with its `tests.json`
|
|
355
|
+
fixture (the first `expect`, else a value built from the first `shape`),
|
|
356
|
+
`[]` when no fixture names it, and 403 when the role's gate refuses the
|
|
357
|
+
call. A page a role may not open is listed as skipped, not drawn. The shelf
|
|
358
|
+
comes from the server the app is pushed to (its public `/vendor/*` and
|
|
359
|
+
`/ui-kit/*`, fetched once per run). Use `--shelf` with a checkout to work
|
|
360
|
+
offline.
|
|
361
|
+
|
|
362
|
+
Chrome is found through `CHROME_PATH`, then `google-chrome`, `chromium` or
|
|
363
|
+
`chrome` on `PATH`, then the macOS app bundle and the usual Linux paths. With
|
|
364
|
+
none, the command exits 2 before anything runs. No browser is ever
|
|
365
|
+
downloaded. Each Chrome gets its own temporary profile, which is removed
|
|
366
|
+
afterwards, and its process group is killed once the file is written,
|
|
367
|
+
because Chrome 154 does not always exit after `--screenshot`.
|
|
368
|
+
|
|
328
369
|
## Development
|
|
329
370
|
|
|
330
371
|
This package lives inside the dr-app-space repo but is independent of the
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
|
|
|
13
13
|
// package.json
|
|
14
14
|
var package_default = {
|
|
15
15
|
name: "@datarails-ai/dr-appspace-cli",
|
|
16
|
-
version: "0.12.
|
|
16
|
+
version: "0.12.33",
|
|
17
17
|
description: "Datarails App Space CLI \u2014 build, try, push and publish App Space apps",
|
|
18
18
|
type: "module",
|
|
19
19
|
engines: {
|
|
@@ -57,7 +57,7 @@ var package_default = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
// src/buildInfo.ts
|
|
60
|
-
var BUILD_TIME = "2026-10-
|
|
60
|
+
var BUILD_TIME = "2026-10-26T12:07:09.192Z";
|
|
61
61
|
function formatBuildTime(iso) {
|
|
62
62
|
if (iso === "") return "";
|
|
63
63
|
const d = new Date(iso);
|
|
@@ -776,12 +776,12 @@ async function requestDeviceCode(host, options) {
|
|
|
776
776
|
interval: typeof obj4.interval === "number" ? obj4.interval : 0
|
|
777
777
|
};
|
|
778
778
|
}
|
|
779
|
-
var defaultSleep = (ms, signal) => new Promise((
|
|
779
|
+
var defaultSleep = (ms, signal) => new Promise((resolve17, reject) => {
|
|
780
780
|
if (signal?.aborted) {
|
|
781
781
|
reject(signal.reason ?? new Error("aborted"));
|
|
782
782
|
return;
|
|
783
783
|
}
|
|
784
|
-
const t = setTimeout(
|
|
784
|
+
const t = setTimeout(resolve17, ms);
|
|
785
785
|
signal?.addEventListener(
|
|
786
786
|
"abort",
|
|
787
787
|
() => {
|
|
@@ -827,7 +827,7 @@ async function pollForToken(host, deviceCode, options = {}) {
|
|
|
827
827
|
const interval = options.interval ?? 2e3;
|
|
828
828
|
const maxWait = options.maxWait ?? 6e5;
|
|
829
829
|
const now = options.now ?? Date.now;
|
|
830
|
-
const
|
|
830
|
+
const sleep3 = options.sleep ?? defaultSleep;
|
|
831
831
|
const signal = options.signal;
|
|
832
832
|
const base = host.replace(/\/+$/, "");
|
|
833
833
|
const deadline = now() + maxWait;
|
|
@@ -859,7 +859,7 @@ async function pollForToken(host, deviceCode, options = {}) {
|
|
|
859
859
|
}
|
|
860
860
|
const errCode = tryParseErrorCode(text3);
|
|
861
861
|
if (errCode === "authorization_pending" || errCode === "slow_down") {
|
|
862
|
-
await
|
|
862
|
+
await sleep3(interval, signal);
|
|
863
863
|
continue;
|
|
864
864
|
}
|
|
865
865
|
if (errCode) {
|
|
@@ -1407,19 +1407,19 @@ async function openBrowser(url) {
|
|
|
1407
1407
|
|
|
1408
1408
|
// src/commands/_serverPicker.ts
|
|
1409
1409
|
async function readLine(stdin) {
|
|
1410
|
-
return new Promise((
|
|
1410
|
+
return new Promise((resolve17, reject) => {
|
|
1411
1411
|
let data = "";
|
|
1412
1412
|
const onData = (chunk) => {
|
|
1413
1413
|
data += chunk.toString();
|
|
1414
1414
|
const i = data.indexOf("\n");
|
|
1415
1415
|
if (i >= 0) {
|
|
1416
1416
|
cleanup();
|
|
1417
|
-
|
|
1417
|
+
resolve17(data.slice(0, i + 1));
|
|
1418
1418
|
}
|
|
1419
1419
|
};
|
|
1420
1420
|
const onEnd = () => {
|
|
1421
1421
|
cleanup();
|
|
1422
|
-
|
|
1422
|
+
resolve17(data);
|
|
1423
1423
|
};
|
|
1424
1424
|
const onError = (err) => {
|
|
1425
1425
|
cleanup();
|
|
@@ -8636,10 +8636,10 @@ function pageCode(html, path3, modules, pageSource) {
|
|
|
8636
8636
|
continue;
|
|
8637
8637
|
}
|
|
8638
8638
|
if (/^(?:[a-z]+:)?\/\//i.test(src) || src.startsWith("/") || src.startsWith("../")) continue;
|
|
8639
|
-
const
|
|
8640
|
-
if (modules.has(`${path3}|${
|
|
8639
|
+
const relative4 = src.replace(/^\.\//, "");
|
|
8640
|
+
if (modules.has(`${path3}|${relative4}`)) continue;
|
|
8641
8641
|
const dir = path3.includes("/") ? path3.slice(0, path3.lastIndexOf("/") + 1) : "";
|
|
8642
|
-
const body = pageSource(`${dir}${
|
|
8642
|
+
const body = pageSource(`${dir}${relative4}`);
|
|
8643
8643
|
if (typeof body !== "string") return null;
|
|
8644
8644
|
out.push(body);
|
|
8645
8645
|
}
|
|
@@ -11859,7 +11859,7 @@ var CONTROLS = "dr-components-controls@1.2.0";
|
|
|
11859
11859
|
var DASHBOARD_RUNTIME = "dr-app-runtime@1.13.1";
|
|
11860
11860
|
var DASHBOARD_WIDGETS = "dr-app-widgets@1.6.0";
|
|
11861
11861
|
var DASHBOARD_BAR = "dr-app-dashboard@1.0.0";
|
|
11862
|
-
var ASSISTANT = "dr-insights@1.4.
|
|
11862
|
+
var ASSISTANT = "dr-insights@1.4.1";
|
|
11863
11863
|
var MARKDOWN = "dr-md@1.0.0";
|
|
11864
11864
|
var PART_FILES = {
|
|
11865
11865
|
"dr-components": { css: "components.css", js: "components.js" },
|
|
@@ -15696,9 +15696,9 @@ function whereProblems(where2, table, pointer2, who, { platform = false, noColum
|
|
|
15696
15696
|
const problems = [];
|
|
15697
15697
|
const say = (key, message, code = "app_model_mismatch") => problems.push({ code, pointer: pointer2(key), message: `${who} ${message}` });
|
|
15698
15698
|
for (const key of Object.keys(where2).sort()) {
|
|
15699
|
-
const
|
|
15700
|
-
const column =
|
|
15701
|
-
const op =
|
|
15699
|
+
const sep7 = key.indexOf("__");
|
|
15700
|
+
const column = sep7 < 0 ? key : key.slice(0, sep7);
|
|
15701
|
+
const op = sep7 < 0 ? "eq" : key.slice(sep7 + 2);
|
|
15702
15702
|
const value = where2[key];
|
|
15703
15703
|
if (!WHERE_OPS.includes(op)) {
|
|
15704
15704
|
say(key, `filters with "${key}", and "${op}" is no operator a where knows: ${WHERE_OPS.join(", ")}`);
|
|
@@ -21590,7 +21590,7 @@ var LiveContractMissing = class extends Error {
|
|
|
21590
21590
|
};
|
|
21591
21591
|
async function pullInto(client, appId, dir, {
|
|
21592
21592
|
onWait,
|
|
21593
|
-
sleep:
|
|
21593
|
+
sleep: sleep3 = (ms) => new Promise((done) => setTimeout(done, ms))
|
|
21594
21594
|
} = {}) {
|
|
21595
21595
|
for (let pull = 1; ; pull += 1) {
|
|
21596
21596
|
let pkg;
|
|
@@ -21612,7 +21612,7 @@ async function pullInto(client, appId, dir, {
|
|
|
21612
21612
|
if (pull >= contractWait.pulls) throw new LiveContractMissing(appId, pkg.revision, pull, read.missing);
|
|
21613
21613
|
const ms = contractWaitMs(pull);
|
|
21614
21614
|
onWait?.(pull, ms, read.missing);
|
|
21615
|
-
await
|
|
21615
|
+
await sleep3(ms);
|
|
21616
21616
|
}
|
|
21617
21617
|
}
|
|
21618
21618
|
async function runGet(appId, opts) {
|
|
@@ -26576,10 +26576,10 @@ function pageCode2(html, path3, modules, pageSource) {
|
|
|
26576
26576
|
continue;
|
|
26577
26577
|
}
|
|
26578
26578
|
if (/^(?:[a-z]+:)?\/\//i.test(src) || src.startsWith("/") || src.startsWith("../")) continue;
|
|
26579
|
-
const
|
|
26580
|
-
if (modules.has(`${path3}|${
|
|
26579
|
+
const relative4 = src.replace(/^\.\//, "");
|
|
26580
|
+
if (modules.has(`${path3}|${relative4}`)) continue;
|
|
26581
26581
|
const dir = path3.includes("/") ? path3.slice(0, path3.lastIndexOf("/") + 1) : "";
|
|
26582
|
-
const body = pageSource(`${dir}${
|
|
26582
|
+
const body = pageSource(`${dir}${relative4}`);
|
|
26583
26583
|
if (typeof body !== "string") return null;
|
|
26584
26584
|
out.push(body);
|
|
26585
26585
|
}
|
|
@@ -28986,7 +28986,7 @@ var kit_default = {
|
|
|
28986
28986
|
description: "Templates a dashboard app is built from, and the checks the Insights Builder runs on every build (live data only, widgets only, an Ask button per widget).",
|
|
28987
28987
|
checks: "checks/run.mjs",
|
|
28988
28988
|
templates: "templates/",
|
|
28989
|
-
assistant: "dr-insights@1.4.
|
|
28989
|
+
assistant: "dr-insights@1.4.1",
|
|
28990
28990
|
charts: "echarts@5.6.0",
|
|
28991
28991
|
fmt: "dr-fmt@2.1.0",
|
|
28992
28992
|
markdown: "dr-md@1.0.0",
|
|
@@ -30351,7 +30351,7 @@ async function runShipOntoLive(dir, opts) {
|
|
|
30351
30351
|
return { code, lost: code === 3 ? await lostRaceOf(dir) : null, creating: pulled === null };
|
|
30352
30352
|
}, opts);
|
|
30353
30353
|
}
|
|
30354
|
-
async function landWithRetries(tryOnce, opts,
|
|
30354
|
+
async function landWithRetries(tryOnce, opts, sleep3 = (ms) => new Promise((done) => setTimeout(done, ms))) {
|
|
30355
30355
|
let createGuarded = true;
|
|
30356
30356
|
for (let attempt = 1; attempt <= ONTO_LIVE_ATTEMPTS; attempt += 1) {
|
|
30357
30357
|
const { code, lost, creating } = await tryOnce(createGuarded);
|
|
@@ -30372,7 +30372,7 @@ async function landWithRetries(tryOnce, opts, sleep2 = (ms) => new Promise((done
|
|
|
30372
30372
|
`
|
|
30373
30373
|
);
|
|
30374
30374
|
}
|
|
30375
|
-
await
|
|
30375
|
+
await sleep3(ontoLiveBackoff(attempt));
|
|
30376
30376
|
}
|
|
30377
30377
|
process.stderr.write(
|
|
30378
30378
|
`ship --onto-live: ${String(ONTO_LIVE_ATTEMPTS)} attempts, and another landing got there first every time. Nothing of this pass is live; run the same command again.
|
|
@@ -30865,8 +30865,8 @@ function registerLogsCommand(parent) {
|
|
|
30865
30865
|
}
|
|
30866
30866
|
|
|
30867
30867
|
// src/commands/appspace/test.ts
|
|
30868
|
-
import { readFile as
|
|
30869
|
-
import { join as
|
|
30868
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
30869
|
+
import { join as join18, resolve as resolve16 } from "path";
|
|
30870
30870
|
|
|
30871
30871
|
// src/commands/appspace/_files.ts
|
|
30872
30872
|
import { readFile as readFile11, realpath } from "fs/promises";
|
|
@@ -31431,15 +31431,15 @@ function planInteractions(components, model, page, uidOf) {
|
|
|
31431
31431
|
for (const child of components) {
|
|
31432
31432
|
if (child.props.field === void 0 || child.props.field === "") continue;
|
|
31433
31433
|
if (!FORM_CONTROL_TAGS.has(child.tag)) continue;
|
|
31434
|
-
let
|
|
31434
|
+
let inside2 = false;
|
|
31435
31435
|
let hops = 0;
|
|
31436
31436
|
for (let up = child.parent === null ? void 0 : byIndex.get(child.parent); up !== void 0 && hops <= components.length; up = up.parent === null ? void 0 : byIndex.get(up.parent), hops += 1) {
|
|
31437
31437
|
if (up.n === component.n) {
|
|
31438
|
-
|
|
31438
|
+
inside2 = true;
|
|
31439
31439
|
break;
|
|
31440
31440
|
}
|
|
31441
31441
|
}
|
|
31442
|
-
if (!
|
|
31442
|
+
if (!inside2) continue;
|
|
31443
31443
|
const field = child.props.field;
|
|
31444
31444
|
const column = columnOf(model, component.props.entity, field);
|
|
31445
31445
|
const control = controlFor(column);
|
|
@@ -32372,13 +32372,13 @@ var defaultBlobReader = async (blob) => {
|
|
|
32372
32372
|
function blobReaderFor(window) {
|
|
32373
32373
|
const ctor = window?.FileReader;
|
|
32374
32374
|
if (typeof ctor !== "function") return defaultBlobReader;
|
|
32375
|
-
return (blob) => new Promise((
|
|
32375
|
+
return (blob) => new Promise((resolve17, reject) => {
|
|
32376
32376
|
const reader = new ctor();
|
|
32377
32377
|
reader.onload = () => {
|
|
32378
32378
|
const result = reader.result;
|
|
32379
|
-
if (result === null || result === void 0)
|
|
32380
|
-
else if (typeof result === "string")
|
|
32381
|
-
else
|
|
32379
|
+
if (result === null || result === void 0) resolve17(Buffer.alloc(0));
|
|
32380
|
+
else if (typeof result === "string") resolve17(Buffer.from(result, "utf8"));
|
|
32381
|
+
else resolve17(Buffer.from(result));
|
|
32382
32382
|
};
|
|
32383
32383
|
reader.onerror = () => reject(new Error("the attached file could not be read"));
|
|
32384
32384
|
reader.onabort = () => reject(new Error("reading the attached file was aborted"));
|
|
@@ -32665,7 +32665,7 @@ async function loadJsdom() {
|
|
|
32665
32665
|
);
|
|
32666
32666
|
}
|
|
32667
32667
|
}
|
|
32668
|
-
var sleep = (ms) => new Promise((
|
|
32668
|
+
var sleep = (ms) => new Promise((resolve17) => setTimeout(resolve17, ms));
|
|
32669
32669
|
var DRAW_GRACE_MS = 3e3;
|
|
32670
32670
|
async function runUiSmoke(opts) {
|
|
32671
32671
|
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
@@ -33168,6 +33168,649 @@ async function runUiSmoke(opts) {
|
|
|
33168
33168
|
};
|
|
33169
33169
|
}
|
|
33170
33170
|
|
|
33171
|
+
// src/commands/appspace/_screenshot.ts
|
|
33172
|
+
import { spawn } from "child_process";
|
|
33173
|
+
import { existsSync as existsSync3, statSync as statSync3 } from "fs";
|
|
33174
|
+
import { mkdir as mkdir8, mkdtemp as mkdtemp4, readFile as readFile12, readdir as readdir3, rm as rm5, stat as stat2, writeFile as writeFile9 } from "fs/promises";
|
|
33175
|
+
import { createServer } from "http";
|
|
33176
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
33177
|
+
import { delimiter, extname as extname2, join as join17, relative as relative3, resolve as resolve15, sep as sep6 } from "path";
|
|
33178
|
+
var SHOT_TIMEOUT_MS = 6e4;
|
|
33179
|
+
var SHOT_BUDGET_MS = 18e4;
|
|
33180
|
+
var SHOT_CONCURRENCY = 4;
|
|
33181
|
+
var VIRTUAL_TIME_MS = 8e3;
|
|
33182
|
+
var ATTEMPT_MS = 25e3;
|
|
33183
|
+
var MEASURE_MS = 15e3;
|
|
33184
|
+
var FULL_HEIGHT_CAP = 8e3;
|
|
33185
|
+
var NO_ROLE = "default";
|
|
33186
|
+
var CHROME_MISSING = "app-space test --screenshot needs Google Chrome: install Google Chrome or set CHROME_PATH to a Chrome or Chromium binary. Nothing is downloaded.";
|
|
33187
|
+
var CHROME_APP_PATHS = [
|
|
33188
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
33189
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
33190
|
+
"/usr/bin/google-chrome",
|
|
33191
|
+
"/usr/bin/google-chrome-stable",
|
|
33192
|
+
"/usr/bin/chromium",
|
|
33193
|
+
"/usr/bin/chromium-browser"
|
|
33194
|
+
];
|
|
33195
|
+
var CHROME_NAMES = ["google-chrome", "chromium", "chrome"];
|
|
33196
|
+
function isFile(path3) {
|
|
33197
|
+
try {
|
|
33198
|
+
return statSync3(path3).isFile();
|
|
33199
|
+
} catch {
|
|
33200
|
+
return false;
|
|
33201
|
+
}
|
|
33202
|
+
}
|
|
33203
|
+
function findChrome(env = process.env, exists = isFile, appPaths = CHROME_APP_PATHS) {
|
|
33204
|
+
const pinned = env["CHROME_PATH"]?.trim();
|
|
33205
|
+
if (pinned) return pinned;
|
|
33206
|
+
const dirs = (env["PATH"] ?? "").split(delimiter).filter((dir) => dir !== "");
|
|
33207
|
+
for (const name of CHROME_NAMES) {
|
|
33208
|
+
for (const dir of dirs) {
|
|
33209
|
+
const path3 = join17(dir, name);
|
|
33210
|
+
if (exists(path3)) return path3;
|
|
33211
|
+
}
|
|
33212
|
+
}
|
|
33213
|
+
return appPaths.find((path3) => exists(path3)) ?? null;
|
|
33214
|
+
}
|
|
33215
|
+
function rolesOf6(manifest) {
|
|
33216
|
+
if (!Array.isArray(manifest?.roles)) return [];
|
|
33217
|
+
return manifest.roles.filter(
|
|
33218
|
+
(one) => typeof one === "object" && one !== null && typeof one.name === "string"
|
|
33219
|
+
);
|
|
33220
|
+
}
|
|
33221
|
+
function pageGates(manifest) {
|
|
33222
|
+
const out = /* @__PURE__ */ new Map();
|
|
33223
|
+
if (!Array.isArray(manifest?.pages)) return out;
|
|
33224
|
+
for (const page of manifest.pages) {
|
|
33225
|
+
if (typeof page !== "object" || page === null) continue;
|
|
33226
|
+
const { path: path3, roles } = page;
|
|
33227
|
+
if (typeof path3 !== "string" || !Array.isArray(roles)) continue;
|
|
33228
|
+
const names = roles.filter((r) => typeof r === "string");
|
|
33229
|
+
if (names.length > 0) out.set(path3, names);
|
|
33230
|
+
}
|
|
33231
|
+
return out;
|
|
33232
|
+
}
|
|
33233
|
+
function handlerGates(manifest) {
|
|
33234
|
+
const out = /* @__PURE__ */ new Map();
|
|
33235
|
+
const raw = manifest?.handlers;
|
|
33236
|
+
if (typeof raw !== "object" || raw === null) return out;
|
|
33237
|
+
for (const [name, row] of Object.entries(raw)) {
|
|
33238
|
+
const gate = typeof row === "object" && row !== null ? row.requiresRole : void 0;
|
|
33239
|
+
out.set(name, Array.isArray(gate) ? gate.filter((r) => typeof r === "string") : null);
|
|
33240
|
+
}
|
|
33241
|
+
return out;
|
|
33242
|
+
}
|
|
33243
|
+
function shotRoles(manifest, only = []) {
|
|
33244
|
+
const declared = rolesOf6(manifest).map((role) => role.name);
|
|
33245
|
+
const all = declared.length === 0 ? [NO_ROLE] : declared;
|
|
33246
|
+
if (only.length === 0) return all;
|
|
33247
|
+
const unknown = only.filter((role) => !all.includes(role));
|
|
33248
|
+
if (unknown.length > 0) {
|
|
33249
|
+
throw new Error(
|
|
33250
|
+
`--role ${unknown.join(", ")}: this app ${declared.length === 0 ? 'declares no roles (its one render is "default")' : `declares ${declared.join(", ")}`}.`
|
|
33251
|
+
);
|
|
33252
|
+
}
|
|
33253
|
+
return all.filter((role) => only.includes(role));
|
|
33254
|
+
}
|
|
33255
|
+
function seesOf(roles, role) {
|
|
33256
|
+
const declared = new Set(roles.map((r) => r.name));
|
|
33257
|
+
const raw = roles.find((r) => r.name === role)?.sees;
|
|
33258
|
+
if (!Array.isArray(raw)) return [];
|
|
33259
|
+
return raw.filter((s) => typeof s === "string" && declared.has(s) && s !== role);
|
|
33260
|
+
}
|
|
33261
|
+
function maySeePage(manifest, role, entry) {
|
|
33262
|
+
const roles = rolesOf6(manifest);
|
|
33263
|
+
const gate = pageGates(manifest).get(`ui/${entry}`);
|
|
33264
|
+
if (gate === void 0 || roles.length === 0) return true;
|
|
33265
|
+
const visible = /* @__PURE__ */ new Set([role, ...seesOf(roles, role)]);
|
|
33266
|
+
return gate.some((r) => visible.has(r));
|
|
33267
|
+
}
|
|
33268
|
+
function accessFor(manifest, role, overlay, sources = []) {
|
|
33269
|
+
const roles = rolesOf6(manifest);
|
|
33270
|
+
const names = roles.map((r) => r.name);
|
|
33271
|
+
const held = names.length === 0 ? null : role;
|
|
33272
|
+
const visible = new Set(held === null ? [] : [held, ...seesOf(roles, held)]);
|
|
33273
|
+
const allowed = {};
|
|
33274
|
+
for (const name of names) allowed[name] = visible.has(name);
|
|
33275
|
+
const gates = pageGates(manifest);
|
|
33276
|
+
for (const [path3, gate] of gates) {
|
|
33277
|
+
allowed[gate.join(" ")] = maySeePage(manifest, role, path3.replace(/^ui\//, ""));
|
|
33278
|
+
}
|
|
33279
|
+
for (const source of sources) allowed[source] = true;
|
|
33280
|
+
for (const [handler, gate] of handlerGates(manifest)) {
|
|
33281
|
+
allowed[handler] = gate === null || gate.length === 0 || held === null || gate.includes(held);
|
|
33282
|
+
}
|
|
33283
|
+
Object.assign(allowed, overlay ?? {});
|
|
33284
|
+
const payload = {
|
|
33285
|
+
role: held,
|
|
33286
|
+
asRole: null,
|
|
33287
|
+
defaultRole: roles.find((r) => r.default === true)?.name ?? null,
|
|
33288
|
+
roles: names,
|
|
33289
|
+
allowed,
|
|
33290
|
+
user: { email: "screenshot@app-space.test" }
|
|
33291
|
+
};
|
|
33292
|
+
if (gates.size > 0 || roles.some((r) => typeof r.home === "string")) {
|
|
33293
|
+
payload.pages = Object.fromEntries(
|
|
33294
|
+
[...gates.keys()].sort().map((path3) => [path3, maySeePage(manifest, role, path3.replace(/^ui\//, ""))])
|
|
33295
|
+
);
|
|
33296
|
+
const home = roles.find((r) => r.name === held)?.home;
|
|
33297
|
+
payload.home = typeof home === "string" && /^ui\/[A-Za-z0-9_-]+\.html$/.test(home) ? home : null;
|
|
33298
|
+
}
|
|
33299
|
+
return payload;
|
|
33300
|
+
}
|
|
33301
|
+
function valueOfShape(shape) {
|
|
33302
|
+
if (typeof shape === "string") {
|
|
33303
|
+
const want = shape.endsWith("?") ? shape.slice(0, -1) : shape;
|
|
33304
|
+
switch (want) {
|
|
33305
|
+
case "string":
|
|
33306
|
+
return "";
|
|
33307
|
+
case "number":
|
|
33308
|
+
return 0;
|
|
33309
|
+
case "boolean":
|
|
33310
|
+
return false;
|
|
33311
|
+
case "object":
|
|
33312
|
+
return {};
|
|
33313
|
+
case "array":
|
|
33314
|
+
return [];
|
|
33315
|
+
default:
|
|
33316
|
+
return null;
|
|
33317
|
+
}
|
|
33318
|
+
}
|
|
33319
|
+
if (Array.isArray(shape)) return [];
|
|
33320
|
+
if (typeof shape === "object" && shape !== null) {
|
|
33321
|
+
return Object.fromEntries(Object.entries(shape).map(([key, sub]) => [key, valueOfShape(sub)]));
|
|
33322
|
+
}
|
|
33323
|
+
return null;
|
|
33324
|
+
}
|
|
33325
|
+
function fixtureAnswers(cases) {
|
|
33326
|
+
const out = /* @__PURE__ */ new Map();
|
|
33327
|
+
for (const kase of cases) {
|
|
33328
|
+
if (kase.error !== void 0 || kase.expect === void 0) continue;
|
|
33329
|
+
if (!out.has(kase.handler)) out.set(kase.handler, { body: kase.expect, from: "expect" });
|
|
33330
|
+
}
|
|
33331
|
+
for (const kase of cases) {
|
|
33332
|
+
if (kase.error !== void 0 || kase.shape === void 0 || out.has(kase.handler)) continue;
|
|
33333
|
+
out.set(kase.handler, { body: valueOfShape(kase.shape), from: "shape" });
|
|
33334
|
+
}
|
|
33335
|
+
return out;
|
|
33336
|
+
}
|
|
33337
|
+
var TYPES = {
|
|
33338
|
+
".html": "text/html; charset=utf-8",
|
|
33339
|
+
".htm": "text/html; charset=utf-8",
|
|
33340
|
+
".js": "text/javascript; charset=utf-8",
|
|
33341
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
33342
|
+
".css": "text/css; charset=utf-8",
|
|
33343
|
+
".json": "application/json",
|
|
33344
|
+
".svg": "image/svg+xml",
|
|
33345
|
+
".png": "image/png",
|
|
33346
|
+
".jpg": "image/jpeg",
|
|
33347
|
+
".jpeg": "image/jpeg",
|
|
33348
|
+
".gif": "image/gif",
|
|
33349
|
+
".webp": "image/webp",
|
|
33350
|
+
".ico": "image/x-icon",
|
|
33351
|
+
".woff": "font/woff",
|
|
33352
|
+
".woff2": "font/woff2",
|
|
33353
|
+
".ttf": "font/ttf",
|
|
33354
|
+
".otf": "font/otf",
|
|
33355
|
+
".map": "application/json"
|
|
33356
|
+
};
|
|
33357
|
+
function contentType(path3) {
|
|
33358
|
+
return TYPES[extname2(path3).toLowerCase()] ?? "application/octet-stream";
|
|
33359
|
+
}
|
|
33360
|
+
function inside(root, rel) {
|
|
33361
|
+
const base = resolve15(root);
|
|
33362
|
+
const full = resolve15(base, rel);
|
|
33363
|
+
return full === base || !full.startsWith(base + sep6) ? null : full;
|
|
33364
|
+
}
|
|
33365
|
+
var Shelf = class {
|
|
33366
|
+
constructor(source, fetchImpl = fetch) {
|
|
33367
|
+
this.source = source;
|
|
33368
|
+
this.fetchImpl = fetchImpl;
|
|
33369
|
+
}
|
|
33370
|
+
source;
|
|
33371
|
+
fetchImpl;
|
|
33372
|
+
cache = /* @__PURE__ */ new Map();
|
|
33373
|
+
misses = /* @__PURE__ */ new Set();
|
|
33374
|
+
get(path3) {
|
|
33375
|
+
let hit = this.cache.get(path3);
|
|
33376
|
+
if (hit === void 0) {
|
|
33377
|
+
hit = this.load(path3).then((served) => {
|
|
33378
|
+
if (served.status !== 200) this.misses.add(path3);
|
|
33379
|
+
return served;
|
|
33380
|
+
});
|
|
33381
|
+
this.cache.set(path3, hit);
|
|
33382
|
+
}
|
|
33383
|
+
return hit;
|
|
33384
|
+
}
|
|
33385
|
+
async load(path3) {
|
|
33386
|
+
const missing = { status: 404, type: "text/plain", body: Buffer.from("not on the shelf") };
|
|
33387
|
+
if (this.source.kind === "server") {
|
|
33388
|
+
try {
|
|
33389
|
+
const res = await this.fetchImpl(`${this.source.baseUrl.replace(/\/+$/, "")}/${path3}`, {
|
|
33390
|
+
signal: AbortSignal.timeout(3e4)
|
|
33391
|
+
});
|
|
33392
|
+
if (!res.ok) return { ...missing, status: res.status };
|
|
33393
|
+
return {
|
|
33394
|
+
status: 200,
|
|
33395
|
+
type: res.headers.get("content-type") ?? contentType(path3),
|
|
33396
|
+
body: Buffer.from(await res.arrayBuffer())
|
|
33397
|
+
};
|
|
33398
|
+
} catch {
|
|
33399
|
+
return { ...missing, status: 502 };
|
|
33400
|
+
}
|
|
33401
|
+
}
|
|
33402
|
+
const candidates = path3.startsWith("ui-kit/") ? [path3, `appspace/ui_kit/${path3.slice("ui-kit/".length)}`] : [path3];
|
|
33403
|
+
for (const rel of candidates) {
|
|
33404
|
+
const full = inside(this.source.root, rel);
|
|
33405
|
+
if (full === null) continue;
|
|
33406
|
+
try {
|
|
33407
|
+
return { status: 200, type: contentType(full), body: await readFile12(full) };
|
|
33408
|
+
} catch {
|
|
33409
|
+
}
|
|
33410
|
+
}
|
|
33411
|
+
return missing;
|
|
33412
|
+
}
|
|
33413
|
+
};
|
|
33414
|
+
var MEASURE_SCRIPT = '<script>addEventListener("load",function(){setTimeout(function(){var d=document.documentElement,b=document.body||d,extra=0;document.querySelectorAll("*").forEach(function(e){var s=getComputedStyle(e).overflowY;if((s==="auto"||s==="scroll")&&e.scrollHeight>e.clientHeight)extra=Math.max(extra,e.scrollHeight-e.clientHeight)});var h=Math.max(d.scrollHeight,b.scrollHeight,innerHeight+extra);var m=document.createElement("meta");m.id="dr-shot-height";m.content=String(h);document.head.appendChild(m)},3000)})</script>';
|
|
33415
|
+
function injectMeasure(html) {
|
|
33416
|
+
const head = /<head\b[^>]*>/i.exec(html);
|
|
33417
|
+
if (head === null) return MEASURE_SCRIPT + html;
|
|
33418
|
+
return html.slice(0, head.index + head[0].length) + MEASURE_SCRIPT + html.slice(head.index + head[0].length);
|
|
33419
|
+
}
|
|
33420
|
+
function send(res, status, type, body) {
|
|
33421
|
+
res.writeHead(status, { "Content-Type": type, "Cache-Control": "no-store" });
|
|
33422
|
+
res.end(body);
|
|
33423
|
+
}
|
|
33424
|
+
function sendJson(res, status, value) {
|
|
33425
|
+
send(res, status, "application/json", JSON.stringify(value));
|
|
33426
|
+
}
|
|
33427
|
+
function drain(req) {
|
|
33428
|
+
return new Promise((done) => {
|
|
33429
|
+
req.on("data", () => void 0);
|
|
33430
|
+
req.on("end", () => done());
|
|
33431
|
+
req.on("error", () => done());
|
|
33432
|
+
});
|
|
33433
|
+
}
|
|
33434
|
+
async function startStubServer(opts) {
|
|
33435
|
+
const shots = /* @__PURE__ */ new Map();
|
|
33436
|
+
const logs = /* @__PURE__ */ new Map();
|
|
33437
|
+
const logOf = (id) => {
|
|
33438
|
+
let log = logs.get(id);
|
|
33439
|
+
if (log === void 0) {
|
|
33440
|
+
log = { called: [], answered: [], noFixture: [], refused: [] };
|
|
33441
|
+
logs.set(id, log);
|
|
33442
|
+
}
|
|
33443
|
+
return log;
|
|
33444
|
+
};
|
|
33445
|
+
const note = (list3, name) => {
|
|
33446
|
+
if (!list3.includes(name)) list3.push(name);
|
|
33447
|
+
};
|
|
33448
|
+
const appPrefix = /^apps\/[^/]+\//;
|
|
33449
|
+
const handle = async (req, res) => {
|
|
33450
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
33451
|
+
let path3;
|
|
33452
|
+
try {
|
|
33453
|
+
path3 = decodeURIComponent(url.pathname);
|
|
33454
|
+
} catch {
|
|
33455
|
+
return send(res, 400, "text/plain", "bad path");
|
|
33456
|
+
}
|
|
33457
|
+
const match = /^\/s\/(\d+)\/(.*)$/.exec(path3);
|
|
33458
|
+
const id = match === null ? -1 : Number(match[1]);
|
|
33459
|
+
const rest = match === null ? path3.replace(/^\/+/, "") : match[2] ?? "";
|
|
33460
|
+
const shot = shots.get(id);
|
|
33461
|
+
if (req.method === "GET" && (rest.startsWith("vendor/") || rest.startsWith("ui-kit/"))) {
|
|
33462
|
+
const served = await opts.shelf.get(rest);
|
|
33463
|
+
return send(res, served.status, served.type, served.body);
|
|
33464
|
+
}
|
|
33465
|
+
const prefix = appPrefix.exec(rest);
|
|
33466
|
+
if (shot === void 0 || prefix === null) {
|
|
33467
|
+
await drain(req);
|
|
33468
|
+
return send(res, 404, "text/plain", "not found");
|
|
33469
|
+
}
|
|
33470
|
+
const inApp = rest.slice(prefix[0].length);
|
|
33471
|
+
if (req.method === "GET" && inApp === "access") {
|
|
33472
|
+
return sendJson(
|
|
33473
|
+
res,
|
|
33474
|
+
200,
|
|
33475
|
+
accessFor(opts.manifest, shot.role, opts.allowedByRole?.[shot.role], opts.sources ?? [])
|
|
33476
|
+
);
|
|
33477
|
+
}
|
|
33478
|
+
if (req.method === "GET" && inApp.startsWith("ui/")) {
|
|
33479
|
+
const entry = inApp.slice("ui/".length);
|
|
33480
|
+
const body = Object.prototype.hasOwnProperty.call(opts.files, entry) ? opts.files[entry] : void 0;
|
|
33481
|
+
if (body === void 0) return send(res, 404, "text/plain", "no such UI file");
|
|
33482
|
+
const type = contentType(entry);
|
|
33483
|
+
if (shot.measure === true && type.startsWith("text/html")) {
|
|
33484
|
+
return send(res, 200, type, injectMeasure(body.toString()));
|
|
33485
|
+
}
|
|
33486
|
+
return send(res, 200, type, body);
|
|
33487
|
+
}
|
|
33488
|
+
if (req.method === "POST" && /^[^/]+$/.test(inApp)) {
|
|
33489
|
+
await drain(req);
|
|
33490
|
+
if (inApp === "uploads") return sendJson(res, 200, { upload: "screenshot-stub" });
|
|
33491
|
+
const log = logOf(id);
|
|
33492
|
+
note(log.called, inApp);
|
|
33493
|
+
const access = accessFor(opts.manifest, shot.role, opts.allowedByRole?.[shot.role], opts.sources ?? []);
|
|
33494
|
+
if (access.allowed[inApp] === false) {
|
|
33495
|
+
note(log.refused, inApp);
|
|
33496
|
+
return sendJson(res, 403, { detail: `role ${shot.role} may not call ${inApp}` });
|
|
33497
|
+
}
|
|
33498
|
+
const answer = opts.answers.get(inApp);
|
|
33499
|
+
if (answer === void 0) {
|
|
33500
|
+
note(log.noFixture, inApp);
|
|
33501
|
+
return sendJson(res, 200, []);
|
|
33502
|
+
}
|
|
33503
|
+
note(log.answered, inApp);
|
|
33504
|
+
return sendJson(res, 200, answer.body);
|
|
33505
|
+
}
|
|
33506
|
+
await drain(req);
|
|
33507
|
+
return send(res, 404, "text/plain", "not found");
|
|
33508
|
+
};
|
|
33509
|
+
const server = createServer((req, res) => {
|
|
33510
|
+
handle(req, res).catch(() => {
|
|
33511
|
+
if (!res.headersSent) send(res, 500, "text/plain", "stub error");
|
|
33512
|
+
else res.end();
|
|
33513
|
+
});
|
|
33514
|
+
});
|
|
33515
|
+
await new Promise((ok, fail) => {
|
|
33516
|
+
server.once("error", fail);
|
|
33517
|
+
server.listen(0, "127.0.0.1", () => ok());
|
|
33518
|
+
});
|
|
33519
|
+
const port = server.address().port;
|
|
33520
|
+
return {
|
|
33521
|
+
port,
|
|
33522
|
+
urlFor: (id, entry) => `http://127.0.0.1:${String(port)}/s/${String(id)}/apps/${encodeURIComponent(opts.appId)}/ui/${entry.split("/").map(encodeURIComponent).join("/")}`,
|
|
33523
|
+
register: (id, shot) => {
|
|
33524
|
+
shots.set(id, shot);
|
|
33525
|
+
},
|
|
33526
|
+
calls: (id) => {
|
|
33527
|
+
const log = logOf(id);
|
|
33528
|
+
return { called: [...log.called], answered: [...log.answered], noFixture: [...log.noFixture], refused: [...log.refused] };
|
|
33529
|
+
},
|
|
33530
|
+
close: () => new Promise((done) => {
|
|
33531
|
+
server.closeAllConnections?.();
|
|
33532
|
+
server.close(() => done());
|
|
33533
|
+
})
|
|
33534
|
+
};
|
|
33535
|
+
}
|
|
33536
|
+
function killGroup(child) {
|
|
33537
|
+
if (child.pid === void 0) return;
|
|
33538
|
+
try {
|
|
33539
|
+
process.kill(-child.pid, "SIGKILL");
|
|
33540
|
+
} catch {
|
|
33541
|
+
try {
|
|
33542
|
+
child.kill("SIGKILL");
|
|
33543
|
+
} catch {
|
|
33544
|
+
}
|
|
33545
|
+
}
|
|
33546
|
+
}
|
|
33547
|
+
var sleep2 = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
33548
|
+
async function runChrome(run) {
|
|
33549
|
+
const profile = await mkdtemp4(join17(tmpdir5(), "dr-appspace-shot-"));
|
|
33550
|
+
const args = [
|
|
33551
|
+
"--headless=new",
|
|
33552
|
+
"--disable-gpu",
|
|
33553
|
+
"--hide-scrollbars",
|
|
33554
|
+
"--no-first-run",
|
|
33555
|
+
"--no-default-browser-check",
|
|
33556
|
+
"--disable-extensions",
|
|
33557
|
+
"--disable-background-networking",
|
|
33558
|
+
"--disable-sync",
|
|
33559
|
+
"--mute-audio",
|
|
33560
|
+
`--user-data-dir=${profile}`,
|
|
33561
|
+
`--window-size=${String(run.width)},${String(run.height)}`,
|
|
33562
|
+
`--virtual-time-budget=${String(VIRTUAL_TIME_MS)}`,
|
|
33563
|
+
run.out === void 0 ? "--dump-dom" : `--screenshot=${run.out}`,
|
|
33564
|
+
run.url
|
|
33565
|
+
];
|
|
33566
|
+
let stdout = "";
|
|
33567
|
+
let exited = false;
|
|
33568
|
+
let spawnError;
|
|
33569
|
+
const child = spawn(run.chrome, args, {
|
|
33570
|
+
detached: true,
|
|
33571
|
+
stdio: ["ignore", run.out === void 0 ? "pipe" : "ignore", "ignore"]
|
|
33572
|
+
});
|
|
33573
|
+
child.stdout?.on("data", (chunk) => {
|
|
33574
|
+
stdout += chunk.toString();
|
|
33575
|
+
});
|
|
33576
|
+
child.on("exit", () => {
|
|
33577
|
+
exited = true;
|
|
33578
|
+
});
|
|
33579
|
+
child.on("error", (err) => {
|
|
33580
|
+
exited = true;
|
|
33581
|
+
spawnError = `could not start ${run.chrome}: ${err.message}`;
|
|
33582
|
+
});
|
|
33583
|
+
const deadline = Date.now() + run.timeoutMs;
|
|
33584
|
+
let lastSize = -1;
|
|
33585
|
+
let ok = false;
|
|
33586
|
+
try {
|
|
33587
|
+
while (Date.now() < deadline) {
|
|
33588
|
+
if (spawnError !== void 0) break;
|
|
33589
|
+
if (run.out === void 0) {
|
|
33590
|
+
if (/<\/html>\s*$/i.test(stdout) || exited) {
|
|
33591
|
+
ok = stdout.trim() !== "";
|
|
33592
|
+
break;
|
|
33593
|
+
}
|
|
33594
|
+
} else {
|
|
33595
|
+
let size2 = -1;
|
|
33596
|
+
try {
|
|
33597
|
+
size2 = (await stat2(run.out)).size;
|
|
33598
|
+
} catch {
|
|
33599
|
+
size2 = -1;
|
|
33600
|
+
}
|
|
33601
|
+
if (size2 > 0 && size2 === lastSize) {
|
|
33602
|
+
ok = true;
|
|
33603
|
+
break;
|
|
33604
|
+
}
|
|
33605
|
+
lastSize = size2;
|
|
33606
|
+
if (exited && size2 <= 0) break;
|
|
33607
|
+
}
|
|
33608
|
+
await sleep2(exited ? 50 : 150);
|
|
33609
|
+
}
|
|
33610
|
+
} finally {
|
|
33611
|
+
killGroup(child);
|
|
33612
|
+
await rm5(profile, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }).catch(() => void 0);
|
|
33613
|
+
}
|
|
33614
|
+
if (ok) return { ok, stdout, profile };
|
|
33615
|
+
const error = spawnError ?? (Date.now() >= deadline ? `timed out after ${String(Math.round(run.timeoutMs / 1e3))}s` : run.out === void 0 ? "Chrome printed no DOM" : "Chrome exited without writing the screenshot");
|
|
33616
|
+
return { ok, stdout, profile, error };
|
|
33617
|
+
}
|
|
33618
|
+
function pngSize(bytes) {
|
|
33619
|
+
if (bytes.length < 24 || bytes.readUInt32BE(0) !== 2303741511 || bytes.toString("ascii", 12, 16) !== "IHDR") {
|
|
33620
|
+
return null;
|
|
33621
|
+
}
|
|
33622
|
+
return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
|
|
33623
|
+
}
|
|
33624
|
+
function pageName(entry) {
|
|
33625
|
+
return entry.replace(/\.html?$/i, "").replace(/\//g, "_");
|
|
33626
|
+
}
|
|
33627
|
+
function safeRole(role) {
|
|
33628
|
+
return role.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
33629
|
+
}
|
|
33630
|
+
async function runScreenshots(opts) {
|
|
33631
|
+
const now = opts.now ?? Date.now;
|
|
33632
|
+
const started = now();
|
|
33633
|
+
const budgetUntil = started + opts.budgetMs;
|
|
33634
|
+
const shotTimeout = opts.shotTimeoutMs ?? SHOT_TIMEOUT_MS;
|
|
33635
|
+
await mkdir8(opts.outDir, { recursive: true });
|
|
33636
|
+
const answers = fixtureAnswers(opts.cases);
|
|
33637
|
+
const shelf2 = new Shelf(opts.shelf, opts.fetchImpl);
|
|
33638
|
+
const stub = await startStubServer({
|
|
33639
|
+
appId: opts.appId,
|
|
33640
|
+
files: opts.files,
|
|
33641
|
+
manifest: opts.manifest,
|
|
33642
|
+
...opts.allowedByRole === void 0 ? {} : { allowedByRole: opts.allowedByRole },
|
|
33643
|
+
...opts.sources === void 0 ? {} : { sources: opts.sources },
|
|
33644
|
+
answers,
|
|
33645
|
+
shelf: shelf2
|
|
33646
|
+
});
|
|
33647
|
+
const plan = [];
|
|
33648
|
+
for (const entry of opts.pages) for (const role of opts.roles) plan.push({ entry, role });
|
|
33649
|
+
const shots = new Array(plan.length);
|
|
33650
|
+
let nextId = 0;
|
|
33651
|
+
const one = async (index2) => {
|
|
33652
|
+
const { entry, role } = plan[index2];
|
|
33653
|
+
const file = `${pageName(entry)}.${safeRole(role)}.png`;
|
|
33654
|
+
const base = {
|
|
33655
|
+
page: entry,
|
|
33656
|
+
role,
|
|
33657
|
+
path: null,
|
|
33658
|
+
viewport: { width: opts.width, height: opts.height },
|
|
33659
|
+
size: null,
|
|
33660
|
+
bytes: 0,
|
|
33661
|
+
full: opts.full,
|
|
33662
|
+
ms: 0,
|
|
33663
|
+
handlersStubbed: [],
|
|
33664
|
+
handlersWithoutFixture: [],
|
|
33665
|
+
handlersRefused: []
|
|
33666
|
+
};
|
|
33667
|
+
if (role !== NO_ROLE && !maySeePage(opts.manifest, role, entry)) {
|
|
33668
|
+
shots[index2] = { ...base, skipped: `${role} may not open this page` };
|
|
33669
|
+
return;
|
|
33670
|
+
}
|
|
33671
|
+
const remaining = budgetUntil - now();
|
|
33672
|
+
if (remaining <= 1e3) {
|
|
33673
|
+
shots[index2] = { ...base, skipped: "the screenshot budget ran out" };
|
|
33674
|
+
return;
|
|
33675
|
+
}
|
|
33676
|
+
const t0 = now();
|
|
33677
|
+
const pageUntil = Math.min(t0 + shotTimeout, budgetUntil);
|
|
33678
|
+
const attemptMs = () => Math.max(500, Math.min(ATTEMPT_MS, Math.ceil(shotTimeout / 2), pageUntil - now()));
|
|
33679
|
+
let height = opts.height;
|
|
33680
|
+
if (opts.full) {
|
|
33681
|
+
const mid = nextId++;
|
|
33682
|
+
stub.register(mid, { role, measure: true });
|
|
33683
|
+
const measured = await runChrome({
|
|
33684
|
+
chrome: opts.chrome,
|
|
33685
|
+
url: stub.urlFor(mid, entry),
|
|
33686
|
+
width: opts.width,
|
|
33687
|
+
height: opts.height,
|
|
33688
|
+
timeoutMs: Math.min(MEASURE_MS, attemptMs())
|
|
33689
|
+
});
|
|
33690
|
+
const found = /id="dr-shot-height"[^>]*content="(\d+)"|content="(\d+)"[^>]*id="dr-shot-height"/.exec(
|
|
33691
|
+
measured.stdout
|
|
33692
|
+
);
|
|
33693
|
+
const tall = Number(found?.[1] ?? found?.[2] ?? NaN);
|
|
33694
|
+
if (Number.isFinite(tall)) height = Math.min(FULL_HEIGHT_CAP, Math.max(opts.height, tall));
|
|
33695
|
+
}
|
|
33696
|
+
const out = join17(opts.outDir, file);
|
|
33697
|
+
let id = -1;
|
|
33698
|
+
let result;
|
|
33699
|
+
for (let attempt = 0; attempt < 2 && now() < pageUntil - 500; attempt++) {
|
|
33700
|
+
id = nextId++;
|
|
33701
|
+
stub.register(id, { role });
|
|
33702
|
+
await rm5(out, { force: true });
|
|
33703
|
+
result = await runChrome({
|
|
33704
|
+
chrome: opts.chrome,
|
|
33705
|
+
url: stub.urlFor(id, entry),
|
|
33706
|
+
width: opts.width,
|
|
33707
|
+
height,
|
|
33708
|
+
timeoutMs: attemptMs(),
|
|
33709
|
+
out
|
|
33710
|
+
});
|
|
33711
|
+
if (result.ok) break;
|
|
33712
|
+
}
|
|
33713
|
+
if (result === void 0) {
|
|
33714
|
+
shots[index2] = { ...base, skipped: "the screenshot budget ran out" };
|
|
33715
|
+
return;
|
|
33716
|
+
}
|
|
33717
|
+
const calls = stub.calls(id);
|
|
33718
|
+
const entryOut = {
|
|
33719
|
+
...base,
|
|
33720
|
+
viewport: { width: opts.width, height },
|
|
33721
|
+
ms: now() - t0,
|
|
33722
|
+
handlersStubbed: calls.answered,
|
|
33723
|
+
handlersWithoutFixture: calls.noFixture,
|
|
33724
|
+
handlersRefused: calls.refused
|
|
33725
|
+
};
|
|
33726
|
+
if (!result.ok) {
|
|
33727
|
+
shots[index2] = { ...entryOut, error: result.error ?? "no picture" };
|
|
33728
|
+
return;
|
|
33729
|
+
}
|
|
33730
|
+
const bytes = await readFile12(out);
|
|
33731
|
+
shots[index2] = { ...entryOut, path: file, size: pngSize(bytes), bytes: bytes.length };
|
|
33732
|
+
};
|
|
33733
|
+
try {
|
|
33734
|
+
let cursor = 0;
|
|
33735
|
+
const workers = Array.from({ length: Math.max(1, Math.min(opts.concurrency ?? SHOT_CONCURRENCY, plan.length)) }, async () => {
|
|
33736
|
+
while (cursor < plan.length) {
|
|
33737
|
+
const index2 = cursor++;
|
|
33738
|
+
await one(index2);
|
|
33739
|
+
}
|
|
33740
|
+
});
|
|
33741
|
+
await Promise.all(workers);
|
|
33742
|
+
} finally {
|
|
33743
|
+
await stub.close();
|
|
33744
|
+
}
|
|
33745
|
+
const index = {
|
|
33746
|
+
version: 1,
|
|
33747
|
+
app: opts.appId,
|
|
33748
|
+
pagesFrom: opts.pagesFrom,
|
|
33749
|
+
shelf: opts.shelf.kind === "server" ? { kind: "server", url: opts.shelf.baseUrl } : { kind: "local", path: opts.shelf.root },
|
|
33750
|
+
shelfMisses: [...shelf2.misses].sort(),
|
|
33751
|
+
chrome: opts.chrome,
|
|
33752
|
+
roles: opts.roles,
|
|
33753
|
+
fixtures: Object.fromEntries([...answers].map(([name, answer]) => [name, answer.from])),
|
|
33754
|
+
createdAt: new Date(started).toISOString(),
|
|
33755
|
+
ms: now() - started,
|
|
33756
|
+
shots
|
|
33757
|
+
};
|
|
33758
|
+
await writeFile9(join17(opts.outDir, "index.json"), JSON.stringify(index, null, 2) + "\n");
|
|
33759
|
+
return index;
|
|
33760
|
+
}
|
|
33761
|
+
async function readLocalUi(dir) {
|
|
33762
|
+
const root = join17(dir, "ui");
|
|
33763
|
+
const out = {};
|
|
33764
|
+
const walk5 = async (folder) => {
|
|
33765
|
+
let names;
|
|
33766
|
+
try {
|
|
33767
|
+
names = await readdir3(folder, { withFileTypes: true });
|
|
33768
|
+
} catch {
|
|
33769
|
+
return;
|
|
33770
|
+
}
|
|
33771
|
+
for (const one of names) {
|
|
33772
|
+
const full = join17(folder, one.name);
|
|
33773
|
+
if (one.isDirectory()) await walk5(full);
|
|
33774
|
+
else if (one.isFile()) out[relative3(root, full).split(sep6).join("/")] = await readFile12(full);
|
|
33775
|
+
}
|
|
33776
|
+
};
|
|
33777
|
+
await walk5(root);
|
|
33778
|
+
return out;
|
|
33779
|
+
}
|
|
33780
|
+
function looksLikeShelf(root) {
|
|
33781
|
+
return existsSync3(join17(root, "vendor"));
|
|
33782
|
+
}
|
|
33783
|
+
function renderShotTable(index, outDir) {
|
|
33784
|
+
const rows2 = index.shots.map((shot) => [
|
|
33785
|
+
shot.page,
|
|
33786
|
+
shot.role,
|
|
33787
|
+
shot.path === null ? "-" : shot.size === null ? "?" : `${String(shot.size.width)}x${String(shot.size.height)}`,
|
|
33788
|
+
shot.path === null ? "" : `${(shot.ms / 1e3).toFixed(1)}s`,
|
|
33789
|
+
shot.path ?? (shot.skipped !== void 0 ? `skipped: ${shot.skipped}` : `failed: ${shot.error ?? "?"}`)
|
|
33790
|
+
]);
|
|
33791
|
+
const head = ["page", "role", "size", "time", "file"];
|
|
33792
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows2.map((row) => (row[i] ?? "").length)));
|
|
33793
|
+
const line2 = (cells) => " " + cells.map((cell, i) => i === cells.length - 1 ? cell : cell.padEnd(widths[i] ?? 0)).join(" ") + "\n";
|
|
33794
|
+
const written = index.shots.filter((shot) => shot.path !== null).length;
|
|
33795
|
+
let out = `
|
|
33796
|
+
screenshots \u2192 ${outDir} (${String(written)} of ${String(index.shots.length)} written in ${(index.ms / 1e3).toFixed(1)}s; pages from the ${index.pagesFrom === "pushed" ? "pushed package" : "local ui/ folder"})
|
|
33797
|
+
`;
|
|
33798
|
+
out += line2(head);
|
|
33799
|
+
for (const row of rows2) out += line2(row);
|
|
33800
|
+
const noFixture = [...new Set(index.shots.flatMap((shot) => shot.handlersWithoutFixture))].sort();
|
|
33801
|
+
if (noFixture.length > 0) {
|
|
33802
|
+
out += ` no tests.json fixture (answered []): ${noFixture.join(", ")}
|
|
33803
|
+
`;
|
|
33804
|
+
}
|
|
33805
|
+
if (index.shelfMisses.length > 0) {
|
|
33806
|
+
out += ` not on the shelf (${index.shelf.kind === "server" ? index.shelf.url : index.shelf.path}): ${index.shelfMisses.slice(0, 5).join(", ")}${index.shelfMisses.length > 5 ? ", \u2026" : ""}
|
|
33807
|
+
`;
|
|
33808
|
+
}
|
|
33809
|
+
out += ` index: ${join17(outDir, "index.json")}
|
|
33810
|
+
`;
|
|
33811
|
+
return out;
|
|
33812
|
+
}
|
|
33813
|
+
|
|
33171
33814
|
// src/commands/appspace/_roles.ts
|
|
33172
33815
|
var PREVIEW_ROLE_HEADER = "x-app-space-preview-role";
|
|
33173
33816
|
function asAllowed(raw) {
|
|
@@ -34720,6 +35363,23 @@ function formatFindings(findings) {
|
|
|
34720
35363
|
}
|
|
34721
35364
|
|
|
34722
35365
|
// src/commands/appspace/test.ts
|
|
35366
|
+
function positiveInt(raw, fallback, flag) {
|
|
35367
|
+
const value = raw === void 0 ? fallback : Number.parseInt(raw, 10);
|
|
35368
|
+
if (!Number.isFinite(value) || value <= 0 || raw !== void 0 && !/^\d+$/.test(raw.trim())) {
|
|
35369
|
+
throw new Error(`${flag} must be a positive whole number.`);
|
|
35370
|
+
}
|
|
35371
|
+
return value;
|
|
35372
|
+
}
|
|
35373
|
+
function collectRoles(value, previous = []) {
|
|
35374
|
+
return [...previous, ...value.split(",").map((one) => one.trim()).filter((one) => one !== "")];
|
|
35375
|
+
}
|
|
35376
|
+
async function readLocalManifest(dir) {
|
|
35377
|
+
try {
|
|
35378
|
+
return JSON.parse(await readFile13(join18(dir, "manifest.json"), "utf8"));
|
|
35379
|
+
} catch {
|
|
35380
|
+
return null;
|
|
35381
|
+
}
|
|
35382
|
+
}
|
|
34723
35383
|
var DEFAULT_FIXTURES = "tests.json";
|
|
34724
35384
|
function orderPages(paths) {
|
|
34725
35385
|
const html = paths.filter((path3) => /\.html?$/i.test(path3) && isUiEntry(path3));
|
|
@@ -34766,7 +35426,8 @@ async function readAppFacts(client, appId) {
|
|
|
34766
35426
|
pages: orderPages(files),
|
|
34767
35427
|
bundle,
|
|
34768
35428
|
handlerCode,
|
|
34769
|
-
handlers
|
|
35429
|
+
handlers,
|
|
35430
|
+
...pkg.manifest === void 0 ? {} : { manifest: pkg.manifest }
|
|
34770
35431
|
};
|
|
34771
35432
|
}
|
|
34772
35433
|
var UI_BUDGET_MS = 21e4;
|
|
@@ -34854,10 +35515,10 @@ function evaluateCase(kase, res) {
|
|
|
34854
35515
|
return problems;
|
|
34855
35516
|
}
|
|
34856
35517
|
async function loadFixtures(dir, explicit) {
|
|
34857
|
-
const path3 = explicit ??
|
|
35518
|
+
const path3 = explicit ?? join18(dir, DEFAULT_FIXTURES);
|
|
34858
35519
|
let raw;
|
|
34859
35520
|
try {
|
|
34860
|
-
raw = await
|
|
35521
|
+
raw = await readFile13(path3, "utf8");
|
|
34861
35522
|
} catch {
|
|
34862
35523
|
if (explicit !== void 0) throw new Error(`no fixtures file at ${path3}.`);
|
|
34863
35524
|
return { fixtures: { cases: [], ui: { entry: "index.html" } } };
|
|
@@ -34878,7 +35539,7 @@ async function runHandlerCases(cases, dir, appId, client, roleOf = () => void 0)
|
|
|
34878
35539
|
if (!code.has(kase.handler)) {
|
|
34879
35540
|
code.set(
|
|
34880
35541
|
kase.handler,
|
|
34881
|
-
await
|
|
35542
|
+
await readFile13(join18(dir, "handlers", `${kase.handler}.py`), "utf8").catch(() => null)
|
|
34882
35543
|
);
|
|
34883
35544
|
}
|
|
34884
35545
|
const source = code.get(kase.handler);
|
|
@@ -35133,7 +35794,7 @@ function contractUid(contract, page, component) {
|
|
|
35133
35794
|
}
|
|
35134
35795
|
async function isDashboardFolder2(dir) {
|
|
35135
35796
|
try {
|
|
35136
|
-
const doc = JSON.parse(await
|
|
35797
|
+
const doc = JSON.parse(await readFile13(join18(dir, "contract.json"), "utf8"));
|
|
35137
35798
|
return doc.app?.kind === "dashboard";
|
|
35138
35799
|
} catch {
|
|
35139
35800
|
return false;
|
|
@@ -35334,7 +35995,7 @@ function mergeDomComponents(contract, components, page) {
|
|
|
35334
35995
|
async function writeMergedContract(dir, page, components) {
|
|
35335
35996
|
let contract;
|
|
35336
35997
|
try {
|
|
35337
|
-
contract = JSON.parse(await
|
|
35998
|
+
contract = JSON.parse(await readFile13(contractPath(dir), "utf8"));
|
|
35338
35999
|
} catch {
|
|
35339
36000
|
contract = await extractContract(dir);
|
|
35340
36001
|
}
|
|
@@ -35344,7 +36005,7 @@ async function writeMergedContract(dir, page, components) {
|
|
|
35344
36005
|
async function uiIsStale(dir, entry, stored) {
|
|
35345
36006
|
if (stored === void 0) return false;
|
|
35346
36007
|
try {
|
|
35347
|
-
const local = await
|
|
36008
|
+
const local = await readFile13(join18(dir, "ui", entry));
|
|
35348
36009
|
return !local.equals(Buffer.from(stored, "utf8"));
|
|
35349
36010
|
} catch {
|
|
35350
36011
|
return false;
|
|
@@ -35432,7 +36093,7 @@ function untouchedHandlers(declared, cases, interactions, runs = []) {
|
|
|
35432
36093
|
async function readSeed(dir) {
|
|
35433
36094
|
let parsed;
|
|
35434
36095
|
try {
|
|
35435
|
-
parsed = JSON.parse(await
|
|
36096
|
+
parsed = JSON.parse(await readFile13(join18(dir, "seed.json"), "utf8"));
|
|
35436
36097
|
} catch {
|
|
35437
36098
|
return null;
|
|
35438
36099
|
}
|
|
@@ -35448,7 +36109,7 @@ async function readSeed(dir) {
|
|
|
35448
36109
|
}
|
|
35449
36110
|
async function handlerOwnership(dir, pushed) {
|
|
35450
36111
|
try {
|
|
35451
|
-
const local = JSON.parse(await
|
|
36112
|
+
const local = JSON.parse(await readFile13(join18(dir, "contract.json"), "utf8"));
|
|
35452
36113
|
const handlers2 = local.data?.app?.handlers;
|
|
35453
36114
|
if (typeof handlers2 === "object" && handlers2 !== null) return handlers2;
|
|
35454
36115
|
} catch {
|
|
@@ -35564,6 +36225,20 @@ function registerTestCommand(parent) {
|
|
|
35564
36225
|
).option("--keep-test-rows", "Keep the rows this run wrote, even on a draft.").option(
|
|
35565
36226
|
"--contract",
|
|
35566
36227
|
"Fold the rendered page's data-dr-* components into .appspace/contract.json, so the record says what the app DREW as well as what it declares."
|
|
36228
|
+
).option(
|
|
36229
|
+
"--screenshot",
|
|
36230
|
+
"After the checks, draw every pushed page once per declared role in headless Google Chrome and write <out>/<page>.<role>.png plus index.json. The pages load from a local stub (127.0.0.1) that answers /access as each role and every handler from its tests.json fixture ([] when none): no row is written. Needs Chrome on PATH or CHROME_PATH; nothing is downloaded."
|
|
36231
|
+
).option("--out <dir>", "Where --screenshot writes (default: <dir>/.screenshots).").option(
|
|
36232
|
+
"--role <name>",
|
|
36233
|
+
"With --screenshot: draw only this declared role (repeatable, or comma-separated). Default: every declared role.",
|
|
36234
|
+
collectRoles
|
|
36235
|
+
).option("--width <px>", "With --screenshot: the window width.", "1400").option("--height <px>", "With --screenshot: the window height.", "900").option("--full", "With --screenshot: grow the window to the page\u2019s full height (one extra load per picture).").option(
|
|
36236
|
+
"--shelf <path>",
|
|
36237
|
+
"With --screenshot: serve vendor/ and ui-kit/ from this dr-app-space checkout instead of the server the app is pushed to (for offline use)."
|
|
36238
|
+
).option(
|
|
36239
|
+
"--screenshot-budget <ms>",
|
|
36240
|
+
`With --screenshot: the most the whole screenshot phase may take; each picture is capped at 60s.`,
|
|
36241
|
+
String(SHOT_BUDGET_MS)
|
|
35567
36242
|
)
|
|
35568
36243
|
)
|
|
35569
36244
|
).action(async (dirArg, opts) => {
|
|
@@ -35580,10 +36255,30 @@ function registerTestCommand(parent) {
|
|
|
35580
36255
|
}
|
|
35581
36256
|
const mode = parseInteractMode(opts.interact);
|
|
35582
36257
|
const as = opts.as === void 0 ? void 0 : opts.as.trim();
|
|
36258
|
+
let chrome = null;
|
|
36259
|
+
let shotSize = { width: 0, height: 0, budget: 0 };
|
|
36260
|
+
if (opts.screenshot === true) {
|
|
36261
|
+
chrome = findChrome();
|
|
36262
|
+
if (chrome === null) {
|
|
36263
|
+
process.stderr.write(`error: ${CHROME_MISSING}
|
|
36264
|
+
`);
|
|
36265
|
+
process.exitCode = 2;
|
|
36266
|
+
return;
|
|
36267
|
+
}
|
|
36268
|
+
shotSize = {
|
|
36269
|
+
width: positiveInt(opts.width, 1400, "--width"),
|
|
36270
|
+
height: positiveInt(opts.height, 900, "--height"),
|
|
36271
|
+
budget: positiveInt(opts.screenshotBudget, SHOT_BUDGET_MS, "--screenshot-budget")
|
|
36272
|
+
};
|
|
36273
|
+
if (opts.shelf !== void 0 && !looksLikeShelf(opts.shelf)) {
|
|
36274
|
+
throw new Error(`--shelf ${opts.shelf}: no vendor/ folder there; point it at a dr-app-space checkout.`);
|
|
36275
|
+
}
|
|
36276
|
+
}
|
|
35583
36277
|
const client = await connect(opts);
|
|
35584
36278
|
try {
|
|
35585
36279
|
const view = await readRoleView(client, appId);
|
|
35586
36280
|
if (as !== void 0) requireDeclaredRole(as, view);
|
|
36281
|
+
if (chrome !== null && view.roles.length > 0) shotRoles({ roles: view.roles.map((name) => ({ name })) }, opts.role ?? []);
|
|
35587
36282
|
const roleOf = (handler) => as ?? roleForHandler(handler, view);
|
|
35588
36283
|
const clock = await readServerClock(client, appId);
|
|
35589
36284
|
const outcomes = opts.skipHandlers === true ? [] : await runHandlerCases(fixtures.cases, dir, appId, client, roleOf);
|
|
@@ -35718,6 +36413,33 @@ function registerTestCommand(parent) {
|
|
|
35718
36413
|
skipped.push(...findingsNotChecked(facts !== void 0, pushed));
|
|
35719
36414
|
skipped.push(...leversNotChecked(opts.skipUi === true && !await isDashboardFolder2(dir), runs, pushed));
|
|
35720
36415
|
const parts = facts === void 0 ? null : partsSummary(pushed, facts.bundle, facts.handlerCode);
|
|
36416
|
+
let shots;
|
|
36417
|
+
if (chrome !== null) {
|
|
36418
|
+
const pushedPages = store.problem === void 0 && store.pages.length > 0;
|
|
36419
|
+
const files = pushedPages ? store.bundle : await readLocalUi(dir);
|
|
36420
|
+
const pages = pushedPages ? store.pages : orderPages(Object.keys(files));
|
|
36421
|
+
const manifest = pushedPages && typeof store.manifest === "object" && store.manifest !== null ? store.manifest : await readLocalManifest(dir);
|
|
36422
|
+
const outDir = resolve16(opts.out ?? join18(dir, ".screenshots"));
|
|
36423
|
+
const index = await runScreenshots({
|
|
36424
|
+
appId,
|
|
36425
|
+
pages: choosePages(opts.pages, pages, fixtures.ui.entry).load,
|
|
36426
|
+
files,
|
|
36427
|
+
pagesFrom: pushedPages ? "pushed" : "local",
|
|
36428
|
+
manifest,
|
|
36429
|
+
roles: shotRoles(manifest, opts.role ?? []),
|
|
36430
|
+
allowedByRole: view.allowed,
|
|
36431
|
+
sources: Object.keys(pushed?.data?.sources ?? {}),
|
|
36432
|
+
cases: fixtures.cases,
|
|
36433
|
+
shelf: opts.shelf === void 0 ? { kind: "server", baseUrl: client.baseUrl } : { kind: "local", root: resolve16(opts.shelf) },
|
|
36434
|
+
chrome,
|
|
36435
|
+
outDir,
|
|
36436
|
+
width: shotSize.width,
|
|
36437
|
+
height: shotSize.height,
|
|
36438
|
+
full: opts.full === true,
|
|
36439
|
+
budgetMs: shotSize.budget
|
|
36440
|
+
});
|
|
36441
|
+
shots = { index, outDir };
|
|
36442
|
+
}
|
|
35721
36443
|
const handlerFailures = outcomes.filter((o) => !o.ok).length;
|
|
35722
36444
|
const smokeFailures = runs.reduce(
|
|
35723
36445
|
(total, run) => total + failedCalls(run.report).length + run.report.consoleErrors.length + run.report.pageErrors.length + (run.report.interactions ?? []).filter((one) => one.problems.length > 0).length,
|
|
@@ -35773,6 +36495,8 @@ function registerTestCommand(parent) {
|
|
|
35773
36495
|
parts,
|
|
35774
36496
|
// The test rows the run found, removed, and left (`_testRows.ts`).
|
|
35775
36497
|
rows: rows2,
|
|
36498
|
+
// `--screenshot`: the same document as <out>/index.json, or null.
|
|
36499
|
+
screenshots: shots === void 0 ? null : { ...shots.index, outDir: shots.outDir },
|
|
35776
36500
|
strict: opts.strict === true,
|
|
35777
36501
|
passed: outcomes.length - handlerFailures,
|
|
35778
36502
|
failed,
|
|
@@ -35800,6 +36524,7 @@ ${String(failed)} failure(s).
|
|
|
35800
36524
|
out += rowsLine(rows2);
|
|
35801
36525
|
out += formatSkipped(skipped);
|
|
35802
36526
|
if (facts !== void 0) out += formatPartsSummary(parts);
|
|
36527
|
+
if (shots !== void 0) out += renderShotTable(shots.index, shots.outDir);
|
|
35803
36528
|
out += formatFindings(findings);
|
|
35804
36529
|
process.stdout.write(out);
|
|
35805
36530
|
if (exitCodeFor(failed, findings, opts.strict === true) !== 0) process.exitCode = 1;
|
|
@@ -35810,12 +36535,12 @@ ${String(failed)} failure(s).
|
|
|
35810
36535
|
}
|
|
35811
36536
|
|
|
35812
36537
|
// src/commands/appspace/try.ts
|
|
35813
|
-
import { readFile as
|
|
35814
|
-
import { join as
|
|
36538
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
36539
|
+
import { join as join19 } from "path";
|
|
35815
36540
|
function collect2(value, previous = []) {
|
|
35816
36541
|
return [...previous, value];
|
|
35817
36542
|
}
|
|
35818
|
-
async function readPayloadOption(raw, read = (path3) =>
|
|
36543
|
+
async function readPayloadOption(raw, read = (path3) => readFile14(path3, "utf8")) {
|
|
35819
36544
|
if (raw.startsWith("@")) {
|
|
35820
36545
|
const path3 = raw.slice(1);
|
|
35821
36546
|
if (path3 === "") throw new Error("--payload @ names no file; write --payload @./payload.json.");
|
|
@@ -35890,8 +36615,8 @@ function registerTryCommand(parent) {
|
|
|
35890
36615
|
).action(async (handler, opts) => {
|
|
35891
36616
|
requirePlainName("handler", handler, "read");
|
|
35892
36617
|
const appId = await readManifestId(opts.dir);
|
|
35893
|
-
const code = await
|
|
35894
|
-
|
|
36618
|
+
const code = await readFile14(
|
|
36619
|
+
join19(opts.dir, "handlers", `${handler}.py`),
|
|
35895
36620
|
"utf8"
|
|
35896
36621
|
).catch(() => {
|
|
35897
36622
|
throw new Error(
|
|
@@ -36012,8 +36737,8 @@ function registerPublishCommands(parent) {
|
|
|
36012
36737
|
}
|
|
36013
36738
|
|
|
36014
36739
|
// src/commands/appspace/scaffold.ts
|
|
36015
|
-
import { mkdir as
|
|
36016
|
-
import { dirname as dirname9, join as
|
|
36740
|
+
import { mkdir as mkdir9, readdir as readdir4, stat as stat3, writeFile as writeFile10 } from "fs/promises";
|
|
36741
|
+
import { dirname as dirname9, join as join20 } from "path";
|
|
36017
36742
|
|
|
36018
36743
|
// src/commands/appspace/starters.ts
|
|
36019
36744
|
var STARTER_KINDS = ["dashboard", "app"];
|
|
@@ -36350,7 +37075,7 @@ async function checkTarget(dir, force) {
|
|
|
36350
37075
|
if (force) return;
|
|
36351
37076
|
let entries;
|
|
36352
37077
|
try {
|
|
36353
|
-
entries = await
|
|
37078
|
+
entries = await readdir4(dir);
|
|
36354
37079
|
} catch (err) {
|
|
36355
37080
|
if (err.code === "ENOENT") return;
|
|
36356
37081
|
throw new Error(
|
|
@@ -36370,7 +37095,7 @@ async function existingFiles(dir, relPaths) {
|
|
|
36370
37095
|
const present = [];
|
|
36371
37096
|
for (const rel of relPaths) {
|
|
36372
37097
|
try {
|
|
36373
|
-
await
|
|
37098
|
+
await stat3(join20(dir, rel));
|
|
36374
37099
|
present.push(rel);
|
|
36375
37100
|
} catch {
|
|
36376
37101
|
}
|
|
@@ -36409,13 +37134,13 @@ function registerScaffoldCommand(parent) {
|
|
|
36409
37134
|
const table = ownsData ? (opts.table ?? DEFAULT_TABLE).trim() : void 0;
|
|
36410
37135
|
const record2 = ownsData ? ((opts.record ?? "").trim() || singularize(table ?? DEFAULT_TABLE)).trim() : void 0;
|
|
36411
37136
|
checkNames({ id, table, record: record2 });
|
|
36412
|
-
const dir = (dirArg ?? "").trim() ||
|
|
37137
|
+
const dir = (dirArg ?? "").trim() || join20(".", id);
|
|
36413
37138
|
await checkTarget(dir, opts.force === true);
|
|
36414
37139
|
const contract = starterContract(kind, { id, name, table, record: record2 });
|
|
36415
37140
|
const overwritten = opts.force === true ? await existingFiles(dir, [CONTRACT_FILE]) : [];
|
|
36416
|
-
const target =
|
|
36417
|
-
await
|
|
36418
|
-
await
|
|
37141
|
+
const target = join20(dir, CONTRACT_FILE);
|
|
37142
|
+
await mkdir9(dirname9(target), { recursive: true });
|
|
37143
|
+
await writeFile10(target, `${JSON.stringify(contract, null, 2)}
|
|
36419
37144
|
`);
|
|
36420
37145
|
if (opts.json === true) {
|
|
36421
37146
|
printJson({ id, name, dir, kind, table, record: record2, files: [CONTRACT_FILE], overwritten });
|
|
@@ -36528,40 +37253,40 @@ function registerAppSpaceCommand(root) {
|
|
|
36528
37253
|
}
|
|
36529
37254
|
|
|
36530
37255
|
// src/commands/update.ts
|
|
36531
|
-
import { spawn } from "child_process";
|
|
37256
|
+
import { spawn as spawn2 } from "child_process";
|
|
36532
37257
|
var REGISTRY = "https://datarails.jfrog.io/artifactory/api/npm/dr-cli-virtual";
|
|
36533
37258
|
var PACKAGE = "@datarails-ai/dr-appspace-cli";
|
|
36534
37259
|
function spawnInherit(command, args) {
|
|
36535
|
-
return new Promise((
|
|
36536
|
-
const child =
|
|
37260
|
+
return new Promise((resolve17) => {
|
|
37261
|
+
const child = spawn2(command, args, {
|
|
36537
37262
|
stdio: "inherit",
|
|
36538
37263
|
// Windows resolves `npm` as `npm.cmd`; `shell: true` lets cmd.exe do
|
|
36539
37264
|
// the resolution without us having to special-case the suffix.
|
|
36540
37265
|
shell: true
|
|
36541
37266
|
});
|
|
36542
37267
|
child.on("close", (code) => {
|
|
36543
|
-
|
|
37268
|
+
resolve17(code ?? 1);
|
|
36544
37269
|
});
|
|
36545
37270
|
child.on("error", () => {
|
|
36546
|
-
|
|
37271
|
+
resolve17(1);
|
|
36547
37272
|
});
|
|
36548
37273
|
});
|
|
36549
37274
|
}
|
|
36550
37275
|
function captureVersion() {
|
|
36551
|
-
return new Promise((
|
|
36552
|
-
const child =
|
|
37276
|
+
return new Promise((resolve17) => {
|
|
37277
|
+
const child = spawn2("dr-appspace", ["--version"], { shell: true });
|
|
36553
37278
|
let out = "";
|
|
36554
37279
|
child.stdout.on("data", (chunk) => {
|
|
36555
37280
|
out += chunk.toString();
|
|
36556
37281
|
});
|
|
36557
37282
|
child.on("close", (code) => {
|
|
36558
37283
|
if (code !== 0) {
|
|
36559
|
-
|
|
37284
|
+
resolve17(null);
|
|
36560
37285
|
return;
|
|
36561
37286
|
}
|
|
36562
|
-
|
|
37287
|
+
resolve17(out.trim());
|
|
36563
37288
|
});
|
|
36564
|
-
child.on("error", () =>
|
|
37289
|
+
child.on("error", () => resolve17(null));
|
|
36565
37290
|
});
|
|
36566
37291
|
}
|
|
36567
37292
|
async function runUpdate() {
|