looply-cli 0.5.12 → 0.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.src-hash +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +15 -0
- package/dist/bin.js.map +1 -1
- package/dist/branch-delivery-wiring.d.ts +3 -1
- package/dist/branch-delivery-wiring.d.ts.map +1 -1
- package/dist/branch-delivery-wiring.js +47 -2
- package/dist/branch-delivery-wiring.js.map +1 -1
- package/dist/config.d.ts +19 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +29 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon-cmd.d.ts.map +1 -1
- package/dist/daemon-cmd.js +5 -0
- package/dist/daemon-cmd.js.map +1 -1
- package/dist/delivery-rules.d.ts +11 -0
- package/dist/delivery-rules.d.ts.map +1 -1
- package/dist/delivery-rules.js +34 -1
- package/dist/delivery-rules.js.map +1 -1
- package/dist/github-api.d.ts +2 -0
- package/dist/github-api.d.ts.map +1 -1
- package/dist/github-api.js +3 -0
- package/dist/github-api.js.map +1 -1
- package/dist/goal-gap.d.ts +9 -0
- package/dist/goal-gap.d.ts.map +1 -1
- package/dist/goal-gap.js +9 -0
- package/dist/goal-gap.js.map +1 -1
- package/dist/heroku-api.d.ts +87 -0
- package/dist/heroku-api.d.ts.map +1 -0
- package/dist/heroku-api.js +178 -0
- package/dist/heroku-api.js.map +1 -0
- package/dist/mcp-main.js +3 -0
- package/dist/mcp-main.js.map +1 -1
- package/dist/mcp-server.d.ts +5 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +14 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/model-transport.d.ts.map +1 -1
- package/dist/model-transport.js +4 -0
- package/dist/model-transport.js.map +1 -1
- package/dist/preview-cmd.d.ts +51 -0
- package/dist/preview-cmd.d.ts.map +1 -0
- package/dist/preview-cmd.js +194 -0
- package/dist/preview-cmd.js.map +1 -0
- package/dist/preview-environments.d.ts +173 -0
- package/dist/preview-environments.d.ts.map +1 -0
- package/dist/preview-environments.js +482 -0
- package/dist/preview-environments.js.map +1 -0
- package/dist/preview-github.d.ts +63 -0
- package/dist/preview-github.d.ts.map +1 -0
- package/dist/preview-github.js +130 -0
- package/dist/preview-github.js.map +1 -0
- package/dist/preview-settings-cache.d.ts +33 -0
- package/dist/preview-settings-cache.d.ts.map +1 -0
- package/dist/preview-settings-cache.js +108 -0
- package/dist/preview-settings-cache.js.map +1 -0
- package/dist/preview-wiring.d.ts +35 -0
- package/dist/preview-wiring.d.ts.map +1 -0
- package/dist/preview-wiring.js +81 -0
- package/dist/preview-wiring.js.map +1 -0
- package/dist/run-cmd.d.ts.map +1 -1
- package/dist/run-cmd.js +18 -1
- package/dist/run-cmd.js.map +1 -1
- package/dist/screenshot-tool.d.ts +45 -0
- package/dist/screenshot-tool.d.ts.map +1 -0
- package/dist/screenshot-tool.js +49 -0
- package/dist/screenshot-tool.js.map +1 -0
- package/dist/server-client.d.ts +28 -0
- package/dist/server-client.d.ts.map +1 -1
- package/dist/server-client.js +28 -0
- package/dist/server-client.js.map +1 -1
- package/dist/visual-proof-browser.d.ts +184 -0
- package/dist/visual-proof-browser.d.ts.map +1 -0
- package/dist/visual-proof-browser.js +267 -0
- package/dist/visual-proof-browser.js.map +1 -0
- package/dist/visual-proof-cmd.d.ts +24 -0
- package/dist/visual-proof-cmd.d.ts.map +1 -0
- package/dist/visual-proof-cmd.js +144 -0
- package/dist/visual-proof-cmd.js.map +1 -0
- package/dist/visual-proof-config.d.ts +151 -0
- package/dist/visual-proof-config.d.ts.map +1 -0
- package/dist/visual-proof-config.js +381 -0
- package/dist/visual-proof-config.js.map +1 -0
- package/dist/visual-proof-publish.d.ts +77 -0
- package/dist/visual-proof-publish.d.ts.map +1 -0
- package/dist/visual-proof-publish.js +211 -0
- package/dist/visual-proof-publish.js.map +1 -0
- package/dist/visual-proof-run.d.ts +73 -0
- package/dist/visual-proof-run.d.ts.map +1 -0
- package/dist/visual-proof-run.js +153 -0
- package/dist/visual-proof-run.js.map +1 -0
- package/dist/visual-proof-scope.d.ts +41 -0
- package/dist/visual-proof-scope.d.ts.map +1 -0
- package/dist/visual-proof-scope.js +205 -0
- package/dist/visual-proof-scope.js.map +1 -0
- package/dist/visual-proof-serve.d.ts +11 -0
- package/dist/visual-proof-serve.d.ts.map +1 -0
- package/dist/visual-proof-serve.js +100 -0
- package/dist/visual-proof-serve.js.map +1 -0
- package/dist/visual-proof-store.d.ts +76 -0
- package/dist/visual-proof-store.d.ts.map +1 -0
- package/dist/visual-proof-store.js +117 -0
- package/dist/visual-proof-store.js.map +1 -0
- package/dist/visual-proof.d.ts +87 -0
- package/dist/visual-proof.d.ts.map +1 -0
- package/dist/visual-proof.js +343 -0
- package/dist/visual-proof.js.map +1 -0
- package/package.json +5 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isUiFile = isUiFile;
|
|
4
|
+
exports.touchedApps = touchedApps;
|
|
5
|
+
exports.readRouter = readRouter;
|
|
6
|
+
exports.pickRoutes = pickRoutes;
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const glob_1 = require("./glob");
|
|
9
|
+
/**
|
|
10
|
+
* WHICH APPS A CHANGE TOUCHED, AND WHICH OF THEIR SCREENS TO SHOW.
|
|
11
|
+
*
|
|
12
|
+
* Pure: the changed paths, the ticket's words and the router's source text go
|
|
13
|
+
* in; a short ordered list of screens comes out. In order of how sure it is:
|
|
14
|
+
*
|
|
15
|
+
* 1. a configured route whose `when` matches a changed file;
|
|
16
|
+
* 2. a route whose page component IS a changed file, read from the app's router
|
|
17
|
+
* (`<Route path="orders" element={<Orders />} />` and the import naming it);
|
|
18
|
+
* 3. a configured or routed screen whose name the ticket says ("the sidebar on
|
|
19
|
+
* the dashboard" → dashboard);
|
|
20
|
+
* 4. the configured routes with no `when`, which a project lists as the screens
|
|
21
|
+
* that show its shell — a sidebar or header change shows on all of them.
|
|
22
|
+
*
|
|
23
|
+
* Capped at `max_routes`. An app with no screen found shows its root.
|
|
24
|
+
*/
|
|
25
|
+
/** File types that change what a person sees. A change touching none of them shows nothing new. */
|
|
26
|
+
const UI_FILE = /\.(tsx|jsx|vue|svelte|astro|html?|css|scss|sass|less|styl|svg|png|jpe?g|gif|webp|ico|woff2?|ttf|json|ts|js|mjs|cjs)$/i;
|
|
27
|
+
/** Files that never change a screen, even inside an app folder. */
|
|
28
|
+
const NOT_UI = /(^|\/)(__tests__|__mocks__|test|tests|e2e|spec)\/|\.(test|spec|stories)\.[a-z]+$|(^|\/)(package-lock\.json|tsconfig[^/]*\.json|vitest\.config\.[a-z]+|jest\.config\.[a-z]+|README[^/]*)$/i;
|
|
29
|
+
function isUiFile(path) {
|
|
30
|
+
return UI_FILE.test(path) && !NOT_UI.test(path);
|
|
31
|
+
}
|
|
32
|
+
/** The apps a set of changed paths touches with UI files. */
|
|
33
|
+
function touchedApps(cfg, changedPaths) {
|
|
34
|
+
if (!cfg.enabled)
|
|
35
|
+
return [];
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const app of cfg.apps) {
|
|
38
|
+
const when = app.when.length > 0 ? app.when : ['**'];
|
|
39
|
+
const changed = changedPaths.filter((p) => isUiFile(p) && when.some((g) => (0, glob_1.matchesGlob)(p, g)));
|
|
40
|
+
if (changed.length > 0)
|
|
41
|
+
out.push({ app, changed });
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The screens a React Router file declares, best effort. Reads `<Route path=… element={<X`
|
|
47
|
+
* (nested layout routes contribute their path), and resolves `X` through a static
|
|
48
|
+
* `import X from '…'`, `import { X } from '…'`, or `const X = lazy(() => import('…'))`
|
|
49
|
+
* (any wrapper whose argument is `() => import('…')`). Parameterised paths are left out:
|
|
50
|
+
* nobody can screenshot `orders/:id` without an id.
|
|
51
|
+
*/
|
|
52
|
+
function readRouter(source, routerPath) {
|
|
53
|
+
const imports = new Map();
|
|
54
|
+
const dir = node_path_1.posix.dirname(routerPath.replace(/\\/g, '/'));
|
|
55
|
+
const resolve = (spec) => (spec.startsWith('.') ? node_path_1.posix.normalize(node_path_1.posix.join(dir, spec)).replace(/\.(tsx?|jsx?)$/, '') : null);
|
|
56
|
+
for (const m of source.matchAll(/import\s+([A-Za-z_$][\w$]*)\s*(?:,\s*\{[^}]*\})?\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
57
|
+
const r = resolve(m[2]);
|
|
58
|
+
if (r !== null)
|
|
59
|
+
imports.set(m[1], r);
|
|
60
|
+
}
|
|
61
|
+
for (const m of source.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
62
|
+
const r = resolve(m[2]);
|
|
63
|
+
if (r === null)
|
|
64
|
+
continue;
|
|
65
|
+
for (const part of m[1].split(',')) {
|
|
66
|
+
const name = part.trim().split(/\s+as\s+/).pop()?.trim();
|
|
67
|
+
if (name)
|
|
68
|
+
imports.set(name, r);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const m of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[\w$.]*\s*\(\s*\(\s*\)\s*=>\s*import\(\s*['"]([^'"]+)['"]\s*\)/g)) {
|
|
72
|
+
const r = resolve(m[2]);
|
|
73
|
+
if (r !== null)
|
|
74
|
+
imports.set(m[1], r);
|
|
75
|
+
}
|
|
76
|
+
const pages = [];
|
|
77
|
+
const stack = [];
|
|
78
|
+
for (const t of routeTags(source)) {
|
|
79
|
+
if (t.close) {
|
|
80
|
+
stack.pop();
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const { attrs, selfClosing } = t;
|
|
84
|
+
const p = /\bpath\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*['"]([^'"]*)['"]\s*\})/.exec(attrs);
|
|
85
|
+
const raw = p ? (p[1] ?? p[2] ?? p[3] ?? '') : '';
|
|
86
|
+
const own = raw.replace(/^\/+|\/+$/g, '');
|
|
87
|
+
const full = (raw.startsWith('/') ? own : [...stack, own].filter((s) => s !== '').join('/')).replace(/\/\*$/, '').replace(/^\*$/, '');
|
|
88
|
+
// Every component the element renders — `<RequireAuth><AppShell /></RequireAuth>` is AppShell's
|
|
89
|
+
// route as much as RequireAuth's — and the first one imported from this app names its module.
|
|
90
|
+
const el = /\belement\s*=\s*\{([\s\S]*)\}/.exec(attrs);
|
|
91
|
+
const names = el === null ? [] : [...el[1].matchAll(/<\s*([A-Z][\w$]*)/g)].map((x) => x[1]);
|
|
92
|
+
const shown = names.find((n) => imports.has(n)) ?? names[0];
|
|
93
|
+
if (shown !== undefined && !full.includes(':') && !full.includes('*')) {
|
|
94
|
+
pages.push({ path: full, component: shown, module: imports.get(shown) ?? null, layout: !selfClosing });
|
|
95
|
+
}
|
|
96
|
+
if (!selfClosing)
|
|
97
|
+
stack.push(raw.startsWith('/') ? own : [...stack, own].filter((s) => s !== '').join('/'));
|
|
98
|
+
}
|
|
99
|
+
return pages;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `<Route …>` / `<Route … />` / `</Route>` in source order. The attributes are read with
|
|
103
|
+
* brace depth tracked, because `element={<Orders />}` itself contains `/>`.
|
|
104
|
+
*/
|
|
105
|
+
function routeTags(source) {
|
|
106
|
+
const out = [];
|
|
107
|
+
const re = /<\/Route\s*>|<Route\b/g;
|
|
108
|
+
let m;
|
|
109
|
+
while ((m = re.exec(source)) !== null) {
|
|
110
|
+
if (m[0].startsWith('</')) {
|
|
111
|
+
out.push({ close: true });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
let depth = 0;
|
|
115
|
+
let quote = null;
|
|
116
|
+
let i = m.index + m[0].length;
|
|
117
|
+
for (; i < source.length; i++) {
|
|
118
|
+
const c = source[i];
|
|
119
|
+
if (quote !== null) {
|
|
120
|
+
if (c === quote)
|
|
121
|
+
quote = null;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (c === '"' || c === "'" || c === '`')
|
|
125
|
+
quote = c;
|
|
126
|
+
else if (c === '{')
|
|
127
|
+
depth++;
|
|
128
|
+
else if (c === '}')
|
|
129
|
+
depth--;
|
|
130
|
+
else if (c === '>' && depth === 0)
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
const attrs = source.slice(m.index + m[0].length, i);
|
|
134
|
+
out.push({ close: false, attrs, selfClosing: /\/\s*$/.test(attrs) });
|
|
135
|
+
re.lastIndex = i + 1;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
const words = (s) => s
|
|
140
|
+
.toLowerCase()
|
|
141
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
142
|
+
.split(/[^a-z0-9]+/)
|
|
143
|
+
.filter((w) => w.length >= 3);
|
|
144
|
+
/** Does the ticket's wording name this screen? Every word of the screen's name or last path segment. */
|
|
145
|
+
function named(goalWords, route) {
|
|
146
|
+
const last = route.path.split('/').filter(Boolean).pop() ?? '';
|
|
147
|
+
for (const cand of [route.name, last]) {
|
|
148
|
+
const ws = words(cand);
|
|
149
|
+
if (ws.length > 0 && ws.every((w) => goalWords.has(w) || goalWords.has(w.replace(/s$/, '')) || goalWords.has(`${w}s`)))
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const stem = (p) => p.replace(/\\/g, '/').replace(/\.(tsx?|jsx?|vue|svelte)$/i, '').replace(/\/index$/, '');
|
|
155
|
+
/** The screens to capture for one touched app. */
|
|
156
|
+
function pickRoutes(input) {
|
|
157
|
+
const { app, changed } = input;
|
|
158
|
+
const chosen = [];
|
|
159
|
+
const why = [];
|
|
160
|
+
const add = (r, because) => {
|
|
161
|
+
if (chosen.length >= input.maxRoutes || chosen.some((c) => c.path === r.path))
|
|
162
|
+
return;
|
|
163
|
+
chosen.push(r);
|
|
164
|
+
why.push(`${r.name || r.path || 'home'}: ${because}`);
|
|
165
|
+
};
|
|
166
|
+
for (const r of app.routes) {
|
|
167
|
+
const hit = r.when.length > 0 ? changed.find((p) => r.when.some((g) => (0, glob_1.matchesGlob)(p, g))) : undefined;
|
|
168
|
+
if (hit !== undefined)
|
|
169
|
+
add(r, `configured for ${hit}`);
|
|
170
|
+
}
|
|
171
|
+
const routed = input.routerSource !== null && app.router !== null ? readRouter(input.routerSource, app.router) : [];
|
|
172
|
+
const changedStems = changed.map(stem);
|
|
173
|
+
const screens = routed.filter((p) => !p.layout);
|
|
174
|
+
for (const page of routed) {
|
|
175
|
+
const mod = page.module;
|
|
176
|
+
if (mod === null)
|
|
177
|
+
continue;
|
|
178
|
+
// The page module itself, a folder of that name, or a sibling like `Orders.parts.tsx`.
|
|
179
|
+
const hit = changedStems.find((c) => c === mod || c.startsWith(`${mod}/`) || ((0, node_path_1.dirname)(mod) !== '.' && c.startsWith(`${mod}.`)));
|
|
180
|
+
if (hit === undefined)
|
|
181
|
+
continue;
|
|
182
|
+
if (!page.layout) {
|
|
183
|
+
add({ path: page.path, name: page.component, when: [] }, `its page ${page.component} changed`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// A shell changed: it is on every screen under it. The project's own screens first, then the first ones routed.
|
|
187
|
+
const under = (r) => page.path === '' || r.path === page.path || r.path.startsWith(`${page.path}/`);
|
|
188
|
+
const listed = app.routes.filter((r) => r.when.length === 0 && under(r));
|
|
189
|
+
const fallback = screens.filter((x) => x.path !== '' && under(x)).slice(0, 2).map((x) => ({ path: x.path, name: x.component, when: [] }));
|
|
190
|
+
for (const r of listed.length > 0 ? listed : fallback)
|
|
191
|
+
add(r, `${page.component}, shown on every screen under /${page.path}, changed`);
|
|
192
|
+
}
|
|
193
|
+
const goalWords = new Set(words(input.goalText));
|
|
194
|
+
for (const r of [...app.routes, ...screens.map((p) => ({ path: p.path, name: p.component, when: [] }))]) {
|
|
195
|
+
if (named(goalWords, r))
|
|
196
|
+
add(r, 'the ticket names it');
|
|
197
|
+
}
|
|
198
|
+
for (const r of app.routes)
|
|
199
|
+
if (r.when.length === 0)
|
|
200
|
+
add(r, 'a configured screen');
|
|
201
|
+
if (chosen.length === 0)
|
|
202
|
+
add({ path: '', name: 'home', when: [] }, 'the app’s first screen (nothing more specific was found)');
|
|
203
|
+
return { app, changed, routes: chosen, why };
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=visual-proof-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visual-proof-scope.js","sourceRoot":"","sources":["../src/visual-proof-scope.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface StaticSite {
|
|
2
|
+
/** `http://127.0.0.1:<port>` */
|
|
3
|
+
readonly origin: string;
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare function serveStatic(input: {
|
|
7
|
+
root: string;
|
|
8
|
+
mount: string;
|
|
9
|
+
spa: boolean;
|
|
10
|
+
}): Promise<StaticSite>;
|
|
11
|
+
//# sourceMappingURL=visual-proof-serve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visual-proof-serve.d.ts","sourceRoot":"","sources":["../src/visual-proof-serve.ts"],"names":[],"mappings":"AAwCA,MAAM,WAAW,UAAU;IACzB,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAoD3G"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serveStatic = serveStatic;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_http_1 = require("node:http");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
/**
|
|
8
|
+
* LOOPLY'S OWN STATIC SERVER FOR A BUILT APP.
|
|
9
|
+
*
|
|
10
|
+
* The app was built in the sandbox; its output is plain files. Serving them is
|
|
11
|
+
* Looply's job, not the project's: no project script runs on the host to do it,
|
|
12
|
+
* so nothing the change wrote can reach the network or the disk from here.
|
|
13
|
+
*
|
|
14
|
+
* - binds 127.0.0.1 on a free port, nothing else;
|
|
15
|
+
* - serves only files under `root` (resolved through links), read-only, GET/HEAD;
|
|
16
|
+
* - the app lives under `mount` (`/admin/`); an unknown path under it gets
|
|
17
|
+
* `index.html` when `spa` (client-side routing), else 404;
|
|
18
|
+
* - everything else is 404 — API calls never reach here, the browser answers them.
|
|
19
|
+
*/
|
|
20
|
+
const TYPES = {
|
|
21
|
+
'.html': 'text/html; charset=utf-8',
|
|
22
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
23
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
24
|
+
'.css': 'text/css; charset=utf-8',
|
|
25
|
+
'.json': 'application/json; charset=utf-8',
|
|
26
|
+
'.webmanifest': 'application/manifest+json',
|
|
27
|
+
'.svg': 'image/svg+xml',
|
|
28
|
+
'.png': 'image/png',
|
|
29
|
+
'.jpg': 'image/jpeg',
|
|
30
|
+
'.jpeg': 'image/jpeg',
|
|
31
|
+
'.gif': 'image/gif',
|
|
32
|
+
'.webp': 'image/webp',
|
|
33
|
+
'.ico': 'image/x-icon',
|
|
34
|
+
'.woff': 'font/woff',
|
|
35
|
+
'.woff2': 'font/woff2',
|
|
36
|
+
'.ttf': 'font/ttf',
|
|
37
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
38
|
+
'.map': 'application/json',
|
|
39
|
+
};
|
|
40
|
+
async function serveStatic(input) {
|
|
41
|
+
if (!(0, node_fs_1.existsSync)(input.root) || !(0, node_fs_1.statSync)(input.root).isDirectory())
|
|
42
|
+
throw new Error(`the built app is not at ${input.root}`);
|
|
43
|
+
const root = (0, node_fs_1.realpathSync)(input.root);
|
|
44
|
+
const mount = input.mount.endsWith('/') ? input.mount : `${input.mount}/`;
|
|
45
|
+
const index = (0, node_path_1.join)(root, 'index.html');
|
|
46
|
+
const server = (0, node_http_1.createServer)((req, res) => {
|
|
47
|
+
const send = (code, body = '') => {
|
|
48
|
+
res.writeHead(code, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
|
|
49
|
+
res.end(body);
|
|
50
|
+
};
|
|
51
|
+
if (req.method !== 'GET' && req.method !== 'HEAD')
|
|
52
|
+
return send(405);
|
|
53
|
+
let path;
|
|
54
|
+
try {
|
|
55
|
+
path = decodeURIComponent(new URL(req.url ?? '/', 'http://x').pathname);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return send(400);
|
|
59
|
+
}
|
|
60
|
+
if (path === mount.slice(0, -1))
|
|
61
|
+
path = mount;
|
|
62
|
+
if (!path.startsWith(mount))
|
|
63
|
+
return send(404);
|
|
64
|
+
const rel = path.slice(mount.length);
|
|
65
|
+
let file = (0, node_path_1.resolve)(root, rel === '' ? 'index.html' : rel);
|
|
66
|
+
if (file !== root && !file.startsWith(root + node_path_1.sep))
|
|
67
|
+
return send(404);
|
|
68
|
+
let ok = (0, node_fs_1.existsSync)(file) && (0, node_fs_1.statSync)(file).isFile();
|
|
69
|
+
if (ok) {
|
|
70
|
+
// A link inside the output must not lead out of it.
|
|
71
|
+
const real = (0, node_fs_1.realpathSync)(file);
|
|
72
|
+
if (real !== root && !real.startsWith(root + node_path_1.sep))
|
|
73
|
+
return send(404);
|
|
74
|
+
file = real;
|
|
75
|
+
}
|
|
76
|
+
else if (input.spa && (0, node_path_1.extname)(rel) === '' && (0, node_fs_1.existsSync)(index)) {
|
|
77
|
+
file = index;
|
|
78
|
+
ok = true;
|
|
79
|
+
}
|
|
80
|
+
if (!ok)
|
|
81
|
+
return send(404);
|
|
82
|
+
res.writeHead(200, { 'content-type': TYPES[(0, node_path_1.extname)(file).toLowerCase()] ?? 'application/octet-stream', 'cache-control': 'no-store' });
|
|
83
|
+
if (req.method === 'HEAD')
|
|
84
|
+
return res.end();
|
|
85
|
+
(0, node_fs_1.createReadStream)(file).pipe(res);
|
|
86
|
+
});
|
|
87
|
+
await new Promise((ok, fail) => {
|
|
88
|
+
server.once('error', fail);
|
|
89
|
+
server.listen(0, '127.0.0.1', () => ok());
|
|
90
|
+
});
|
|
91
|
+
const port = server.address().port;
|
|
92
|
+
return {
|
|
93
|
+
origin: `http://127.0.0.1:${port}`,
|
|
94
|
+
close: () => new Promise((ok) => {
|
|
95
|
+
server.closeAllConnections?.();
|
|
96
|
+
server.close(() => ok());
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=visual-proof-serve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visual-proof-serve.js","sourceRoot":"","sources":["../src/visual-proof-serve.ts"],"names":[],"mappings":";;AA8CA,kCAoDC;AAlGD,qCAA+E;AAC/E,yCAAsD;AAEtD,yCAAwD;AAExD;;;;;;;;;;;;GAYG;AAEH,MAAM,KAAK,GAA2B;IACpC,OAAO,EAAE,0BAA0B;IACnC,KAAK,EAAE,gCAAgC;IACvC,MAAM,EAAE,gCAAgC;IACxC,MAAM,EAAE,yBAAyB;IACjC,OAAO,EAAE,iCAAiC;IAC1C,cAAc,EAAE,2BAA2B;IAC3C,MAAM,EAAE,eAAe;IACvB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;IACnB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,cAAc;IACtB,OAAO,EAAE,WAAW;IACpB,QAAQ,EAAE,YAAY;IACtB,MAAM,EAAE,UAAU;IAClB,MAAM,EAAE,2BAA2B;IACnC,MAAM,EAAE,kBAAkB;CAC3B,CAAC;AAQK,KAAK,UAAU,WAAW,CAAC,KAAoD;IACpF,IAAI,CAAC,IAAA,oBAAU,EAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAA,kBAAQ,EAAC,KAAK,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7H,MAAM,IAAI,GAAG,IAAA,sBAAY,EAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC;IAC1E,MAAM,KAAK,GAAG,IAAA,gBAAI,EAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAEvC,MAAM,MAAM,GAAW,IAAA,wBAAY,EAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC/C,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,IAAI,GAAG,EAAE,EAAQ,EAAE;YAC7C,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,cAAc,EAAE,2BAA2B,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC,CAAC;YAClG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChB,CAAC,CAAC;QACF,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACpE,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,kBAAkB,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC1E,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QACD,IAAI,IAAI,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAAE,IAAI,GAAG,KAAK,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,IAAI,GAAG,IAAA,mBAAO,EAAC,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC1D,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,eAAG,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACpE,IAAI,EAAE,GAAG,IAAA,oBAAU,EAAC,IAAI,CAAC,IAAI,IAAA,kBAAQ,EAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;QACrD,IAAI,EAAE,EAAE,CAAC;YACP,oDAAoD;YACpD,MAAM,IAAI,GAAG,IAAA,sBAAY,EAAC,IAAI,CAAC,CAAC;YAChC,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,eAAG,CAAC;gBAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;YACpE,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;aAAM,IAAI,KAAK,CAAC,GAAG,IAAI,IAAA,mBAAO,EAAC,GAAG,CAAC,KAAK,EAAE,IAAI,IAAA,oBAAU,EAAC,KAAK,CAAC,EAAE,CAAC;YACjE,IAAI,GAAG,KAAK,CAAC;YACb,EAAE,GAAG,IAAI,CAAC;QACZ,CAAC;QACD,IAAI,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QAC1B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,KAAK,CAAC,IAAA,mBAAO,EAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,0BAA0B,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC,CAAC;QACtI,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;YAAE,OAAO,GAAG,CAAC,GAAG,EAAE,CAAC;QAC5C,IAAA,0BAAgB,EAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,OAAO,CAAO,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE;QACnC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC3B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,GAAI,MAAM,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAC;IACpD,OAAO;QACL,MAAM,EAAE,oBAAoB,IAAI,EAAE;QAClC,KAAK,EAAE,GAAG,EAAE,CACV,IAAI,OAAO,CAAO,CAAC,EAAE,EAAE,EAAE;YACvB,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3B,CAAC,CAAC;KACL,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE SCREENSHOTS ARE KEPT, AND HOW MUCH.
|
|
3
|
+
*
|
|
4
|
+
* Under the machine's workspaces folder (the one AutoRyn or `machine-settings.json`
|
|
5
|
+
* names — `D:\looply-home\workspaces` on the owner's PC), never the home drive:
|
|
6
|
+
*
|
|
7
|
+
* <workspaces>/visual-proof/<project key>/<goal or "adhoc">/<head sha 12>/
|
|
8
|
+
* manifest.json frames/…jpg sheets/…jpg
|
|
9
|
+
*
|
|
10
|
+
* The whole `visual-proof` folder is held under a cap (`visual_proof.max_mb`,
|
|
11
|
+
* default 2 GB). After each capture the oldest captures are deleted until it
|
|
12
|
+
* fits — never the one just made.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PROOF_DIR = "visual-proof";
|
|
15
|
+
export declare const MANIFEST = "manifest.json";
|
|
16
|
+
export type Side = 'base' | 'head';
|
|
17
|
+
export interface Shot {
|
|
18
|
+
readonly file: string;
|
|
19
|
+
readonly app: string;
|
|
20
|
+
readonly route: string;
|
|
21
|
+
readonly routeName: string;
|
|
22
|
+
readonly side: Side;
|
|
23
|
+
readonly viewport: string;
|
|
24
|
+
readonly theme: 'light' | 'dark';
|
|
25
|
+
readonly dir: 'ltr' | 'rtl';
|
|
26
|
+
readonly ok: boolean;
|
|
27
|
+
readonly because?: string;
|
|
28
|
+
/** Taken, but something the project said to wait for never appeared. */
|
|
29
|
+
readonly warning?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface Sheet {
|
|
32
|
+
readonly file: string;
|
|
33
|
+
readonly app: string;
|
|
34
|
+
readonly route: string;
|
|
35
|
+
readonly label: string;
|
|
36
|
+
readonly bytes: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ProofManifest {
|
|
39
|
+
readonly schema: 1;
|
|
40
|
+
readonly goalId: string | null;
|
|
41
|
+
readonly project: string;
|
|
42
|
+
readonly baseSha: string;
|
|
43
|
+
readonly headSha: string;
|
|
44
|
+
readonly createdAt: string;
|
|
45
|
+
readonly apps: ReadonlyArray<{
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly routes: ReadonlyArray<{
|
|
48
|
+
path: string;
|
|
49
|
+
name: string;
|
|
50
|
+
}>;
|
|
51
|
+
readonly why: readonly string[];
|
|
52
|
+
readonly built: {
|
|
53
|
+
readonly base: string;
|
|
54
|
+
readonly head: string;
|
|
55
|
+
};
|
|
56
|
+
}>;
|
|
57
|
+
readonly shots: readonly Shot[];
|
|
58
|
+
readonly sheets: readonly Sheet[];
|
|
59
|
+
/** What could not be shown, in words. */
|
|
60
|
+
readonly notes: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
export declare function projectKey(projectDir: string): string;
|
|
63
|
+
export declare function captureDir(base: string, project: string, goalId: string | null, headSha: string): string;
|
|
64
|
+
export declare function writeManifest(dir: string, m: ProofManifest): void;
|
|
65
|
+
export declare function readManifest(dir: string): ProofManifest | null;
|
|
66
|
+
/** The newest capture for a goal (any head), or for exactly `headSha` when given. */
|
|
67
|
+
export declare function findCapture(base: string, project: string, goalId: string, headSha?: string): {
|
|
68
|
+
dir: string;
|
|
69
|
+
manifest: ProofManifest;
|
|
70
|
+
} | null;
|
|
71
|
+
/**
|
|
72
|
+
* Delete the oldest captures until the whole store fits in `maxMb`. Returns what was removed.
|
|
73
|
+
* A capture is a folder holding a manifest; `keep` is never removed.
|
|
74
|
+
*/
|
|
75
|
+
export declare function enforceCap(base: string, maxMb: number, keep: string | null): string[];
|
|
76
|
+
//# sourceMappingURL=visual-proof-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visual-proof-store.d.ts","sourceRoot":"","sources":["../src/visual-proof-store.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,SAAS,iBAAiB,CAAC;AACxC,eAAO,MAAM,QAAQ,kBAAkB,CAAC;AAExC,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnC,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,KAAK,GAAG,KAAK,CAAC;IAC5B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;QAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAC/D,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;QAChC,QAAQ,CAAC,KAAK,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;KAClE,CAAC,CAAC;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAGxG;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,aAAa,GAAG,IAAI,CAKjE;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAO9D;AAED,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAA;CAAE,GAAG,IAAI,CAc5I;AAYD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAwBrF"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MANIFEST = exports.PROOF_DIR = void 0;
|
|
4
|
+
exports.projectKey = projectKey;
|
|
5
|
+
exports.captureDir = captureDir;
|
|
6
|
+
exports.writeManifest = writeManifest;
|
|
7
|
+
exports.readManifest = readManifest;
|
|
8
|
+
exports.findCapture = findCapture;
|
|
9
|
+
exports.enforceCap = enforceCap;
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
/**
|
|
14
|
+
* WHERE SCREENSHOTS ARE KEPT, AND HOW MUCH.
|
|
15
|
+
*
|
|
16
|
+
* Under the machine's workspaces folder (the one AutoRyn or `machine-settings.json`
|
|
17
|
+
* names — `D:\looply-home\workspaces` on the owner's PC), never the home drive:
|
|
18
|
+
*
|
|
19
|
+
* <workspaces>/visual-proof/<project key>/<goal or "adhoc">/<head sha 12>/
|
|
20
|
+
* manifest.json frames/…jpg sheets/…jpg
|
|
21
|
+
*
|
|
22
|
+
* The whole `visual-proof` folder is held under a cap (`visual_proof.max_mb`,
|
|
23
|
+
* default 2 GB). After each capture the oldest captures are deleted until it
|
|
24
|
+
* fits — never the one just made.
|
|
25
|
+
*/
|
|
26
|
+
exports.PROOF_DIR = 'visual-proof';
|
|
27
|
+
exports.MANIFEST = 'manifest.json';
|
|
28
|
+
function projectKey(projectDir) {
|
|
29
|
+
return (0, node_crypto_1.createHash)('sha256').update(projectDir.replace(/\\/g, '/').toLowerCase()).digest('hex').slice(0, 16);
|
|
30
|
+
}
|
|
31
|
+
function captureDir(base, project, goalId, headSha) {
|
|
32
|
+
const goal = goalId !== null && /^[A-Za-z0-9_-]{1,80}$/.test(goalId) ? goalId : 'adhoc';
|
|
33
|
+
return (0, node_path_1.join)(base, exports.PROOF_DIR, project, goal, headSha.slice(0, 12));
|
|
34
|
+
}
|
|
35
|
+
function writeManifest(dir, m) {
|
|
36
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
37
|
+
const tmp = (0, node_path_1.join)(dir, `${exports.MANIFEST}.${process.pid}.tmp`);
|
|
38
|
+
(0, node_fs_1.writeFileSync)(tmp, `${JSON.stringify(m, null, 2)}\n`, 'utf8');
|
|
39
|
+
(0, node_fs_1.renameSync)(tmp, (0, node_path_1.join)(dir, exports.MANIFEST));
|
|
40
|
+
}
|
|
41
|
+
function readManifest(dir) {
|
|
42
|
+
try {
|
|
43
|
+
const m = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, exports.MANIFEST), 'utf8'));
|
|
44
|
+
return m.schema === 1 && Array.isArray(m.shots) ? m : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** The newest capture for a goal (any head), or for exactly `headSha` when given. */
|
|
51
|
+
function findCapture(base, project, goalId, headSha) {
|
|
52
|
+
const goalDir = (0, node_path_1.join)(base, exports.PROOF_DIR, project, goalId);
|
|
53
|
+
if (!(0, node_fs_1.existsSync)(goalDir))
|
|
54
|
+
return null;
|
|
55
|
+
const dirs = (0, node_fs_1.readdirSync)(goalDir)
|
|
56
|
+
.filter((d) => headSha === undefined || d === headSha.slice(0, 12))
|
|
57
|
+
.map((d) => (0, node_path_1.join)(goalDir, d));
|
|
58
|
+
let best = null;
|
|
59
|
+
for (const dir of dirs) {
|
|
60
|
+
const manifest = readManifest(dir);
|
|
61
|
+
if (manifest === null)
|
|
62
|
+
continue;
|
|
63
|
+
const at = Date.parse(manifest.createdAt) || 0;
|
|
64
|
+
if (best === null || at > best.at)
|
|
65
|
+
best = { dir, manifest, at };
|
|
66
|
+
}
|
|
67
|
+
return best === null ? null : { dir: best.dir, manifest: best.manifest };
|
|
68
|
+
}
|
|
69
|
+
function sizeOf(path) {
|
|
70
|
+
let total = 0;
|
|
71
|
+
const st = (0, node_fs_1.statSync)(path, { throwIfNoEntry: false });
|
|
72
|
+
if (st === undefined)
|
|
73
|
+
return 0;
|
|
74
|
+
if (st.isFile())
|
|
75
|
+
return st.size;
|
|
76
|
+
if (!st.isDirectory())
|
|
77
|
+
return 0;
|
|
78
|
+
for (const e of (0, node_fs_1.readdirSync)(path))
|
|
79
|
+
total += sizeOf((0, node_path_1.join)(path, e));
|
|
80
|
+
return total;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Delete the oldest captures until the whole store fits in `maxMb`. Returns what was removed.
|
|
84
|
+
* A capture is a folder holding a manifest; `keep` is never removed.
|
|
85
|
+
*/
|
|
86
|
+
function enforceCap(base, maxMb, keep) {
|
|
87
|
+
const root = (0, node_path_1.join)(base, exports.PROOF_DIR);
|
|
88
|
+
if (!(0, node_fs_1.existsSync)(root))
|
|
89
|
+
return [];
|
|
90
|
+
const captures = [];
|
|
91
|
+
const walk = (dir, depth) => {
|
|
92
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, exports.MANIFEST))) {
|
|
93
|
+
captures.push({ dir, bytes: sizeOf(dir), at: (0, node_fs_1.statSync)((0, node_path_1.join)(dir, exports.MANIFEST)).mtimeMs });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (depth > 3)
|
|
97
|
+
return;
|
|
98
|
+
for (const e of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true }))
|
|
99
|
+
if (e.isDirectory())
|
|
100
|
+
walk((0, node_path_1.join)(dir, e.name), depth + 1);
|
|
101
|
+
};
|
|
102
|
+
walk(root, 0);
|
|
103
|
+
let total = captures.reduce((n, c) => n + c.bytes, 0);
|
|
104
|
+
const limit = maxMb * 1024 * 1024;
|
|
105
|
+
const removed = [];
|
|
106
|
+
for (const c of captures.sort((a, b) => a.at - b.at)) {
|
|
107
|
+
if (total <= limit)
|
|
108
|
+
break;
|
|
109
|
+
if (keep !== null && c.dir === keep)
|
|
110
|
+
continue;
|
|
111
|
+
(0, node_fs_1.rmSync)(c.dir, { recursive: true, force: true });
|
|
112
|
+
total -= c.bytes;
|
|
113
|
+
removed.push(c.dir);
|
|
114
|
+
}
|
|
115
|
+
return removed;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=visual-proof-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visual-proof-store.js","sourceRoot":"","sources":["../src/visual-proof-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { CommandRunner } from './gate';
|
|
2
|
+
import { type GitExec } from './git-driver';
|
|
3
|
+
import { type CaptureEngine } from './visual-proof-browser';
|
|
4
|
+
import { type ProofApp, type Viewport, type VisualProofConfig } from './visual-proof-config';
|
|
5
|
+
import { type ProofManifest } from './visual-proof-store';
|
|
6
|
+
/**
|
|
7
|
+
* BEFORE AND AFTER SCREENSHOTS OF A UI CHANGE.
|
|
8
|
+
*
|
|
9
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
* Given a checkout, a base commit and a head commit:
|
|
11
|
+
*
|
|
12
|
+
* 1. the changed files decide which configured apps were touched (none: no proof,
|
|
13
|
+
* and nothing is built — a change to the API is not a UI change);
|
|
14
|
+
* 2. for each, the screens to show (`visual-proof-scope.ts`);
|
|
15
|
+
* 3. each side is built IN THE SANDBOX, offline — in a throwaway checkout of
|
|
16
|
+
* that commit with the project's `node_modules` carried in, exactly as a
|
|
17
|
+
* machine checkout is provisioned — and its built folder is kept, by commit,
|
|
18
|
+
* so the next goal on the same base does not build it again;
|
|
19
|
+
* 4. Looply serves each side's built folder itself and headless Chromium,
|
|
20
|
+
* with no network, captures every screen at every width, light and dark, and
|
|
21
|
+
* right-to-left when the app says how;
|
|
22
|
+
* 5. one contact sheet per screen and width puts BEFORE beside AFTER;
|
|
23
|
+
* 6. a manifest records what was shown and, in words, what could not be.
|
|
24
|
+
*
|
|
25
|
+
* A failure is never a gate. Nothing here decides whether work is kept: a proof
|
|
26
|
+
* that could not be made says why, and the goal goes on exactly as before.
|
|
27
|
+
*/
|
|
28
|
+
export interface ProofInput {
|
|
29
|
+
/** Where git runs and the head commit lives: the run's checkout or the project. */
|
|
30
|
+
readonly checkout: string;
|
|
31
|
+
/** The project's own folder, where fixtures and node_modules are found when the checkout lacks them. */
|
|
32
|
+
readonly projectDir: string;
|
|
33
|
+
readonly projectName: string;
|
|
34
|
+
readonly goalId: string | null;
|
|
35
|
+
readonly goalText: string;
|
|
36
|
+
/** A ref or sha; the proof compares against its merge base with `head`. */
|
|
37
|
+
readonly base: string;
|
|
38
|
+
readonly head: string;
|
|
39
|
+
readonly config: VisualProofConfig;
|
|
40
|
+
/** Folders carried into a throwaway checkout (`run.carry_into_checkouts`). */
|
|
41
|
+
readonly carry: readonly string[];
|
|
42
|
+
/** The machine's workspaces folder; screenshots go under it. */
|
|
43
|
+
readonly storeBase: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ProofDeps {
|
|
46
|
+
readonly git: GitExec;
|
|
47
|
+
/** The sandboxed command runner for a tree (production: `makeUntrustedGateRunner`). */
|
|
48
|
+
buildRunner(treeDir: string): CommandRunner;
|
|
49
|
+
/** The browser (production: `launchEngine`). */
|
|
50
|
+
engine?(roots: readonly string[]): Promise<CaptureEngine>;
|
|
51
|
+
now(): number;
|
|
52
|
+
log(line: string): void;
|
|
53
|
+
}
|
|
54
|
+
export type ProofOutcome = {
|
|
55
|
+
readonly kind: 'not-ui';
|
|
56
|
+
readonly because: string;
|
|
57
|
+
} | {
|
|
58
|
+
readonly kind: 'captured';
|
|
59
|
+
readonly dir: string;
|
|
60
|
+
readonly manifest: ProofManifest;
|
|
61
|
+
} | {
|
|
62
|
+
readonly kind: 'failed';
|
|
63
|
+
readonly because: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* One key per REPOSITORY, whichever checkout asks: its shared git directory. A run in a
|
|
67
|
+
* machine-made checkout and the daemon in the project's own folder find the same captures.
|
|
68
|
+
*/
|
|
69
|
+
export declare function repoKey(exec: GitExec, fallbackDir: string): Promise<string>;
|
|
70
|
+
/**
|
|
71
|
+
* The variants one screen is shown in: every width in every theme, then right-to-left
|
|
72
|
+
* (light) at every width when the app says how to switch it.
|
|
73
|
+
*/
|
|
74
|
+
export declare function variantsFor(cfg: VisualProofConfig, app: ProofApp): Array<{
|
|
75
|
+
viewport: Viewport;
|
|
76
|
+
theme: 'light' | 'dark';
|
|
77
|
+
dir: 'ltr' | 'rtl';
|
|
78
|
+
}>;
|
|
79
|
+
export declare function storageFor(app: ProofApp, login: Readonly<Record<string, string>>, theme: 'light' | 'dark', dir: 'ltr' | 'rtl'): Record<string, string>;
|
|
80
|
+
export declare function captureVisualProof(input: ProofInput, deps: ProofDeps): Promise<ProofOutcome>;
|
|
81
|
+
/**
|
|
82
|
+
* The carried folders one app's build needs: its own `carry` when it names one; else, for a
|
|
83
|
+
* build that starts `cd <dir> &&`, only the carried folders under that directory (the admin's
|
|
84
|
+
* build needs `admin/node_modules`, not the API's); else all of them.
|
|
85
|
+
*/
|
|
86
|
+
export declare function carryFor(app: ProofApp, carry: readonly string[]): string[];
|
|
87
|
+
//# sourceMappingURL=visual-proof.d.ts.map
|