@datarails-ai/dr-appspace-cli 0.12.32 → 0.12.34
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 -67
- 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.34",
|
|
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:43:18.858Z";
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -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) {
|
|
@@ -33588,16 +34231,15 @@ var SEE = {
|
|
|
33588
34231
|
};
|
|
33589
34232
|
var NO_COMPONENTS = "This page did not load dr-components; compile it again with the current CLI.";
|
|
33590
34233
|
var NO_PART = (type) => `${type} is not a part this page's dr-components draws.`;
|
|
33591
|
-
var EMBEDDED = /<script\b[^>]*\bid\s*=\s*["']dr-contract["'][^>]*>([\s\S]*?)<\/script>/i;
|
|
33592
34234
|
function pushedContract(bundle, pages) {
|
|
33593
34235
|
const order2 = [...pages, ...Object.keys(bundle).filter((path3) => !pages.includes(path3))];
|
|
33594
34236
|
for (const path3 of order2) {
|
|
33595
34237
|
const html = bundle[path3];
|
|
33596
34238
|
if (typeof html !== "string") continue;
|
|
33597
|
-
const found =
|
|
34239
|
+
const found = contractBlockText(html);
|
|
33598
34240
|
if (found === null) continue;
|
|
33599
34241
|
try {
|
|
33600
|
-
const parsed = JSON.parse(found
|
|
34242
|
+
const parsed = JSON.parse(found);
|
|
33601
34243
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
33602
34244
|
return parsed;
|
|
33603
34245
|
}
|
|
@@ -34720,6 +35362,23 @@ function formatFindings(findings) {
|
|
|
34720
35362
|
}
|
|
34721
35363
|
|
|
34722
35364
|
// src/commands/appspace/test.ts
|
|
35365
|
+
function positiveInt(raw, fallback, flag) {
|
|
35366
|
+
const value = raw === void 0 ? fallback : Number.parseInt(raw, 10);
|
|
35367
|
+
if (!Number.isFinite(value) || value <= 0 || raw !== void 0 && !/^\d+$/.test(raw.trim())) {
|
|
35368
|
+
throw new Error(`${flag} must be a positive whole number.`);
|
|
35369
|
+
}
|
|
35370
|
+
return value;
|
|
35371
|
+
}
|
|
35372
|
+
function collectRoles(value, previous = []) {
|
|
35373
|
+
return [...previous, ...value.split(",").map((one) => one.trim()).filter((one) => one !== "")];
|
|
35374
|
+
}
|
|
35375
|
+
async function readLocalManifest(dir) {
|
|
35376
|
+
try {
|
|
35377
|
+
return JSON.parse(await readFile13(join18(dir, "manifest.json"), "utf8"));
|
|
35378
|
+
} catch {
|
|
35379
|
+
return null;
|
|
35380
|
+
}
|
|
35381
|
+
}
|
|
34723
35382
|
var DEFAULT_FIXTURES = "tests.json";
|
|
34724
35383
|
function orderPages(paths) {
|
|
34725
35384
|
const html = paths.filter((path3) => /\.html?$/i.test(path3) && isUiEntry(path3));
|
|
@@ -34766,7 +35425,8 @@ async function readAppFacts(client, appId) {
|
|
|
34766
35425
|
pages: orderPages(files),
|
|
34767
35426
|
bundle,
|
|
34768
35427
|
handlerCode,
|
|
34769
|
-
handlers
|
|
35428
|
+
handlers,
|
|
35429
|
+
...pkg.manifest === void 0 ? {} : { manifest: pkg.manifest }
|
|
34770
35430
|
};
|
|
34771
35431
|
}
|
|
34772
35432
|
var UI_BUDGET_MS = 21e4;
|
|
@@ -34854,10 +35514,10 @@ function evaluateCase(kase, res) {
|
|
|
34854
35514
|
return problems;
|
|
34855
35515
|
}
|
|
34856
35516
|
async function loadFixtures(dir, explicit) {
|
|
34857
|
-
const path3 = explicit ??
|
|
35517
|
+
const path3 = explicit ?? join18(dir, DEFAULT_FIXTURES);
|
|
34858
35518
|
let raw;
|
|
34859
35519
|
try {
|
|
34860
|
-
raw = await
|
|
35520
|
+
raw = await readFile13(path3, "utf8");
|
|
34861
35521
|
} catch {
|
|
34862
35522
|
if (explicit !== void 0) throw new Error(`no fixtures file at ${path3}.`);
|
|
34863
35523
|
return { fixtures: { cases: [], ui: { entry: "index.html" } } };
|
|
@@ -34878,7 +35538,7 @@ async function runHandlerCases(cases, dir, appId, client, roleOf = () => void 0)
|
|
|
34878
35538
|
if (!code.has(kase.handler)) {
|
|
34879
35539
|
code.set(
|
|
34880
35540
|
kase.handler,
|
|
34881
|
-
await
|
|
35541
|
+
await readFile13(join18(dir, "handlers", `${kase.handler}.py`), "utf8").catch(() => null)
|
|
34882
35542
|
);
|
|
34883
35543
|
}
|
|
34884
35544
|
const source = code.get(kase.handler);
|
|
@@ -35133,7 +35793,7 @@ function contractUid(contract, page, component) {
|
|
|
35133
35793
|
}
|
|
35134
35794
|
async function isDashboardFolder2(dir) {
|
|
35135
35795
|
try {
|
|
35136
|
-
const doc = JSON.parse(await
|
|
35796
|
+
const doc = JSON.parse(await readFile13(join18(dir, "contract.json"), "utf8"));
|
|
35137
35797
|
return doc.app?.kind === "dashboard";
|
|
35138
35798
|
} catch {
|
|
35139
35799
|
return false;
|
|
@@ -35334,7 +35994,7 @@ function mergeDomComponents(contract, components, page) {
|
|
|
35334
35994
|
async function writeMergedContract(dir, page, components) {
|
|
35335
35995
|
let contract;
|
|
35336
35996
|
try {
|
|
35337
|
-
contract = JSON.parse(await
|
|
35997
|
+
contract = JSON.parse(await readFile13(contractPath(dir), "utf8"));
|
|
35338
35998
|
} catch {
|
|
35339
35999
|
contract = await extractContract(dir);
|
|
35340
36000
|
}
|
|
@@ -35344,7 +36004,7 @@ async function writeMergedContract(dir, page, components) {
|
|
|
35344
36004
|
async function uiIsStale(dir, entry, stored) {
|
|
35345
36005
|
if (stored === void 0) return false;
|
|
35346
36006
|
try {
|
|
35347
|
-
const local = await
|
|
36007
|
+
const local = await readFile13(join18(dir, "ui", entry));
|
|
35348
36008
|
return !local.equals(Buffer.from(stored, "utf8"));
|
|
35349
36009
|
} catch {
|
|
35350
36010
|
return false;
|
|
@@ -35432,7 +36092,7 @@ function untouchedHandlers(declared, cases, interactions, runs = []) {
|
|
|
35432
36092
|
async function readSeed(dir) {
|
|
35433
36093
|
let parsed;
|
|
35434
36094
|
try {
|
|
35435
|
-
parsed = JSON.parse(await
|
|
36095
|
+
parsed = JSON.parse(await readFile13(join18(dir, "seed.json"), "utf8"));
|
|
35436
36096
|
} catch {
|
|
35437
36097
|
return null;
|
|
35438
36098
|
}
|
|
@@ -35448,7 +36108,7 @@ async function readSeed(dir) {
|
|
|
35448
36108
|
}
|
|
35449
36109
|
async function handlerOwnership(dir, pushed) {
|
|
35450
36110
|
try {
|
|
35451
|
-
const local = JSON.parse(await
|
|
36111
|
+
const local = JSON.parse(await readFile13(join18(dir, "contract.json"), "utf8"));
|
|
35452
36112
|
const handlers2 = local.data?.app?.handlers;
|
|
35453
36113
|
if (typeof handlers2 === "object" && handlers2 !== null) return handlers2;
|
|
35454
36114
|
} catch {
|
|
@@ -35564,6 +36224,20 @@ function registerTestCommand(parent) {
|
|
|
35564
36224
|
).option("--keep-test-rows", "Keep the rows this run wrote, even on a draft.").option(
|
|
35565
36225
|
"--contract",
|
|
35566
36226
|
"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."
|
|
36227
|
+
).option(
|
|
36228
|
+
"--screenshot",
|
|
36229
|
+
"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."
|
|
36230
|
+
).option("--out <dir>", "Where --screenshot writes (default: <dir>/.screenshots).").option(
|
|
36231
|
+
"--role <name>",
|
|
36232
|
+
"With --screenshot: draw only this declared role (repeatable, or comma-separated). Default: every declared role.",
|
|
36233
|
+
collectRoles
|
|
36234
|
+
).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(
|
|
36235
|
+
"--shelf <path>",
|
|
36236
|
+
"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)."
|
|
36237
|
+
).option(
|
|
36238
|
+
"--screenshot-budget <ms>",
|
|
36239
|
+
`With --screenshot: the most the whole screenshot phase may take; each picture is capped at 60s.`,
|
|
36240
|
+
String(SHOT_BUDGET_MS)
|
|
35567
36241
|
)
|
|
35568
36242
|
)
|
|
35569
36243
|
).action(async (dirArg, opts) => {
|
|
@@ -35580,10 +36254,30 @@ function registerTestCommand(parent) {
|
|
|
35580
36254
|
}
|
|
35581
36255
|
const mode = parseInteractMode(opts.interact);
|
|
35582
36256
|
const as = opts.as === void 0 ? void 0 : opts.as.trim();
|
|
36257
|
+
let chrome = null;
|
|
36258
|
+
let shotSize = { width: 0, height: 0, budget: 0 };
|
|
36259
|
+
if (opts.screenshot === true) {
|
|
36260
|
+
chrome = findChrome();
|
|
36261
|
+
if (chrome === null) {
|
|
36262
|
+
process.stderr.write(`error: ${CHROME_MISSING}
|
|
36263
|
+
`);
|
|
36264
|
+
process.exitCode = 2;
|
|
36265
|
+
return;
|
|
36266
|
+
}
|
|
36267
|
+
shotSize = {
|
|
36268
|
+
width: positiveInt(opts.width, 1400, "--width"),
|
|
36269
|
+
height: positiveInt(opts.height, 900, "--height"),
|
|
36270
|
+
budget: positiveInt(opts.screenshotBudget, SHOT_BUDGET_MS, "--screenshot-budget")
|
|
36271
|
+
};
|
|
36272
|
+
if (opts.shelf !== void 0 && !looksLikeShelf(opts.shelf)) {
|
|
36273
|
+
throw new Error(`--shelf ${opts.shelf}: no vendor/ folder there; point it at a dr-app-space checkout.`);
|
|
36274
|
+
}
|
|
36275
|
+
}
|
|
35583
36276
|
const client = await connect(opts);
|
|
35584
36277
|
try {
|
|
35585
36278
|
const view = await readRoleView(client, appId);
|
|
35586
36279
|
if (as !== void 0) requireDeclaredRole(as, view);
|
|
36280
|
+
if (chrome !== null && view.roles.length > 0) shotRoles({ roles: view.roles.map((name) => ({ name })) }, opts.role ?? []);
|
|
35587
36281
|
const roleOf = (handler) => as ?? roleForHandler(handler, view);
|
|
35588
36282
|
const clock = await readServerClock(client, appId);
|
|
35589
36283
|
const outcomes = opts.skipHandlers === true ? [] : await runHandlerCases(fixtures.cases, dir, appId, client, roleOf);
|
|
@@ -35718,6 +36412,33 @@ function registerTestCommand(parent) {
|
|
|
35718
36412
|
skipped.push(...findingsNotChecked(facts !== void 0, pushed));
|
|
35719
36413
|
skipped.push(...leversNotChecked(opts.skipUi === true && !await isDashboardFolder2(dir), runs, pushed));
|
|
35720
36414
|
const parts = facts === void 0 ? null : partsSummary(pushed, facts.bundle, facts.handlerCode);
|
|
36415
|
+
let shots;
|
|
36416
|
+
if (chrome !== null) {
|
|
36417
|
+
const pushedPages = store.problem === void 0 && store.pages.length > 0;
|
|
36418
|
+
const files = pushedPages ? store.bundle : await readLocalUi(dir);
|
|
36419
|
+
const pages = pushedPages ? store.pages : orderPages(Object.keys(files));
|
|
36420
|
+
const manifest = pushedPages && typeof store.manifest === "object" && store.manifest !== null ? store.manifest : await readLocalManifest(dir);
|
|
36421
|
+
const outDir = resolve16(opts.out ?? join18(dir, ".screenshots"));
|
|
36422
|
+
const index = await runScreenshots({
|
|
36423
|
+
appId,
|
|
36424
|
+
pages: choosePages(opts.pages, pages, fixtures.ui.entry).load,
|
|
36425
|
+
files,
|
|
36426
|
+
pagesFrom: pushedPages ? "pushed" : "local",
|
|
36427
|
+
manifest,
|
|
36428
|
+
roles: shotRoles(manifest, opts.role ?? []),
|
|
36429
|
+
allowedByRole: view.allowed,
|
|
36430
|
+
sources: Object.keys(pushed?.data?.sources ?? {}),
|
|
36431
|
+
cases: fixtures.cases,
|
|
36432
|
+
shelf: opts.shelf === void 0 ? { kind: "server", baseUrl: client.baseUrl } : { kind: "local", root: resolve16(opts.shelf) },
|
|
36433
|
+
chrome,
|
|
36434
|
+
outDir,
|
|
36435
|
+
width: shotSize.width,
|
|
36436
|
+
height: shotSize.height,
|
|
36437
|
+
full: opts.full === true,
|
|
36438
|
+
budgetMs: shotSize.budget
|
|
36439
|
+
});
|
|
36440
|
+
shots = { index, outDir };
|
|
36441
|
+
}
|
|
35721
36442
|
const handlerFailures = outcomes.filter((o) => !o.ok).length;
|
|
35722
36443
|
const smokeFailures = runs.reduce(
|
|
35723
36444
|
(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 +36494,8 @@ function registerTestCommand(parent) {
|
|
|
35773
36494
|
parts,
|
|
35774
36495
|
// The test rows the run found, removed, and left (`_testRows.ts`).
|
|
35775
36496
|
rows: rows2,
|
|
36497
|
+
// `--screenshot`: the same document as <out>/index.json, or null.
|
|
36498
|
+
screenshots: shots === void 0 ? null : { ...shots.index, outDir: shots.outDir },
|
|
35776
36499
|
strict: opts.strict === true,
|
|
35777
36500
|
passed: outcomes.length - handlerFailures,
|
|
35778
36501
|
failed,
|
|
@@ -35800,6 +36523,7 @@ ${String(failed)} failure(s).
|
|
|
35800
36523
|
out += rowsLine(rows2);
|
|
35801
36524
|
out += formatSkipped(skipped);
|
|
35802
36525
|
if (facts !== void 0) out += formatPartsSummary(parts);
|
|
36526
|
+
if (shots !== void 0) out += renderShotTable(shots.index, shots.outDir);
|
|
35803
36527
|
out += formatFindings(findings);
|
|
35804
36528
|
process.stdout.write(out);
|
|
35805
36529
|
if (exitCodeFor(failed, findings, opts.strict === true) !== 0) process.exitCode = 1;
|
|
@@ -35810,12 +36534,12 @@ ${String(failed)} failure(s).
|
|
|
35810
36534
|
}
|
|
35811
36535
|
|
|
35812
36536
|
// src/commands/appspace/try.ts
|
|
35813
|
-
import { readFile as
|
|
35814
|
-
import { join as
|
|
36537
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
36538
|
+
import { join as join19 } from "path";
|
|
35815
36539
|
function collect2(value, previous = []) {
|
|
35816
36540
|
return [...previous, value];
|
|
35817
36541
|
}
|
|
35818
|
-
async function readPayloadOption(raw, read = (path3) =>
|
|
36542
|
+
async function readPayloadOption(raw, read = (path3) => readFile14(path3, "utf8")) {
|
|
35819
36543
|
if (raw.startsWith("@")) {
|
|
35820
36544
|
const path3 = raw.slice(1);
|
|
35821
36545
|
if (path3 === "") throw new Error("--payload @ names no file; write --payload @./payload.json.");
|
|
@@ -35890,8 +36614,8 @@ function registerTryCommand(parent) {
|
|
|
35890
36614
|
).action(async (handler, opts) => {
|
|
35891
36615
|
requirePlainName("handler", handler, "read");
|
|
35892
36616
|
const appId = await readManifestId(opts.dir);
|
|
35893
|
-
const code = await
|
|
35894
|
-
|
|
36617
|
+
const code = await readFile14(
|
|
36618
|
+
join19(opts.dir, "handlers", `${handler}.py`),
|
|
35895
36619
|
"utf8"
|
|
35896
36620
|
).catch(() => {
|
|
35897
36621
|
throw new Error(
|
|
@@ -36012,8 +36736,8 @@ function registerPublishCommands(parent) {
|
|
|
36012
36736
|
}
|
|
36013
36737
|
|
|
36014
36738
|
// src/commands/appspace/scaffold.ts
|
|
36015
|
-
import { mkdir as
|
|
36016
|
-
import { dirname as dirname9, join as
|
|
36739
|
+
import { mkdir as mkdir9, readdir as readdir4, stat as stat3, writeFile as writeFile10 } from "fs/promises";
|
|
36740
|
+
import { dirname as dirname9, join as join20 } from "path";
|
|
36017
36741
|
|
|
36018
36742
|
// src/commands/appspace/starters.ts
|
|
36019
36743
|
var STARTER_KINDS = ["dashboard", "app"];
|
|
@@ -36350,7 +37074,7 @@ async function checkTarget(dir, force) {
|
|
|
36350
37074
|
if (force) return;
|
|
36351
37075
|
let entries;
|
|
36352
37076
|
try {
|
|
36353
|
-
entries = await
|
|
37077
|
+
entries = await readdir4(dir);
|
|
36354
37078
|
} catch (err) {
|
|
36355
37079
|
if (err.code === "ENOENT") return;
|
|
36356
37080
|
throw new Error(
|
|
@@ -36370,7 +37094,7 @@ async function existingFiles(dir, relPaths) {
|
|
|
36370
37094
|
const present = [];
|
|
36371
37095
|
for (const rel of relPaths) {
|
|
36372
37096
|
try {
|
|
36373
|
-
await
|
|
37097
|
+
await stat3(join20(dir, rel));
|
|
36374
37098
|
present.push(rel);
|
|
36375
37099
|
} catch {
|
|
36376
37100
|
}
|
|
@@ -36409,13 +37133,13 @@ function registerScaffoldCommand(parent) {
|
|
|
36409
37133
|
const table = ownsData ? (opts.table ?? DEFAULT_TABLE).trim() : void 0;
|
|
36410
37134
|
const record2 = ownsData ? ((opts.record ?? "").trim() || singularize(table ?? DEFAULT_TABLE)).trim() : void 0;
|
|
36411
37135
|
checkNames({ id, table, record: record2 });
|
|
36412
|
-
const dir = (dirArg ?? "").trim() ||
|
|
37136
|
+
const dir = (dirArg ?? "").trim() || join20(".", id);
|
|
36413
37137
|
await checkTarget(dir, opts.force === true);
|
|
36414
37138
|
const contract = starterContract(kind, { id, name, table, record: record2 });
|
|
36415
37139
|
const overwritten = opts.force === true ? await existingFiles(dir, [CONTRACT_FILE]) : [];
|
|
36416
|
-
const target =
|
|
36417
|
-
await
|
|
36418
|
-
await
|
|
37140
|
+
const target = join20(dir, CONTRACT_FILE);
|
|
37141
|
+
await mkdir9(dirname9(target), { recursive: true });
|
|
37142
|
+
await writeFile10(target, `${JSON.stringify(contract, null, 2)}
|
|
36419
37143
|
`);
|
|
36420
37144
|
if (opts.json === true) {
|
|
36421
37145
|
printJson({ id, name, dir, kind, table, record: record2, files: [CONTRACT_FILE], overwritten });
|
|
@@ -36528,40 +37252,40 @@ function registerAppSpaceCommand(root) {
|
|
|
36528
37252
|
}
|
|
36529
37253
|
|
|
36530
37254
|
// src/commands/update.ts
|
|
36531
|
-
import { spawn } from "child_process";
|
|
37255
|
+
import { spawn as spawn2 } from "child_process";
|
|
36532
37256
|
var REGISTRY = "https://datarails.jfrog.io/artifactory/api/npm/dr-cli-virtual";
|
|
36533
37257
|
var PACKAGE = "@datarails-ai/dr-appspace-cli";
|
|
36534
37258
|
function spawnInherit(command, args) {
|
|
36535
|
-
return new Promise((
|
|
36536
|
-
const child =
|
|
37259
|
+
return new Promise((resolve17) => {
|
|
37260
|
+
const child = spawn2(command, args, {
|
|
36537
37261
|
stdio: "inherit",
|
|
36538
37262
|
// Windows resolves `npm` as `npm.cmd`; `shell: true` lets cmd.exe do
|
|
36539
37263
|
// the resolution without us having to special-case the suffix.
|
|
36540
37264
|
shell: true
|
|
36541
37265
|
});
|
|
36542
37266
|
child.on("close", (code) => {
|
|
36543
|
-
|
|
37267
|
+
resolve17(code ?? 1);
|
|
36544
37268
|
});
|
|
36545
37269
|
child.on("error", () => {
|
|
36546
|
-
|
|
37270
|
+
resolve17(1);
|
|
36547
37271
|
});
|
|
36548
37272
|
});
|
|
36549
37273
|
}
|
|
36550
37274
|
function captureVersion() {
|
|
36551
|
-
return new Promise((
|
|
36552
|
-
const child =
|
|
37275
|
+
return new Promise((resolve17) => {
|
|
37276
|
+
const child = spawn2("dr-appspace", ["--version"], { shell: true });
|
|
36553
37277
|
let out = "";
|
|
36554
37278
|
child.stdout.on("data", (chunk) => {
|
|
36555
37279
|
out += chunk.toString();
|
|
36556
37280
|
});
|
|
36557
37281
|
child.on("close", (code) => {
|
|
36558
37282
|
if (code !== 0) {
|
|
36559
|
-
|
|
37283
|
+
resolve17(null);
|
|
36560
37284
|
return;
|
|
36561
37285
|
}
|
|
36562
|
-
|
|
37286
|
+
resolve17(out.trim());
|
|
36563
37287
|
});
|
|
36564
|
-
child.on("error", () =>
|
|
37288
|
+
child.on("error", () => resolve17(null));
|
|
36565
37289
|
});
|
|
36566
37290
|
}
|
|
36567
37291
|
async function runUpdate() {
|