@pygmalionjs/pygmalion 0.2.44 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/FrozenRoutePreview-efexOSqR.js +9416 -0
- package/dist-lib/pygmalion.js +17853 -1374
- package/dist-lib/style.css +1 -1
- package/dist-lib/testing.js +1 -1
- package/node/component-branches.mjs +108 -0
- package/node/dev-view.vite.mjs +45 -1
- package/node/preview-artifact-plugin.mjs +20 -1
- package/node/preview-artifact-store.mjs +483 -29
- package/node/preview-capture-worker.mjs +529 -0
- package/node/route-dependency-digest.mjs +151 -0
- package/node/route-preview-artifact-v3.mjs +10 -5
- package/node/source-graph.mjs +258 -0
- package/node/storyboard-capture-runtime.mjs +213 -5
- package/node/storyboard-environment.mjs +97 -10
- package/node/vite.mjs +53 -2
- package/package.json +5 -2
- package/types.d.ts +495 -0
- package/vite.d.ts +68 -0
- package/dist-lib/App-CCaXx_KA.js +0 -20441
|
@@ -2,7 +2,11 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import ts from 'typescript';
|
|
5
|
-
import { buildSourceGraph } from './source-graph.mjs';
|
|
5
|
+
import { buildSourceGraph, findSourceUsage } from './source-graph.mjs';
|
|
6
|
+
import {
|
|
7
|
+
componentBranchesMiddleware,
|
|
8
|
+
PYGMALION_COMPONENT_BRANCHES_CONTROL,
|
|
9
|
+
} from './component-branches.mjs';
|
|
6
10
|
import {
|
|
7
11
|
canonicalizeStoryboardExecutions,
|
|
8
12
|
PYGMALION_STORYBOARD_CANONICAL_CONTROL,
|
|
@@ -10,6 +14,9 @@ import {
|
|
|
10
14
|
|
|
11
15
|
export const PYGMALION_STORYBOARD_ENVIRONMENT_CONTROL =
|
|
12
16
|
'/__pygmalion-storyboard/environment';
|
|
17
|
+
export const PYGMALION_SOURCE_USAGE_CONTROL =
|
|
18
|
+
'/__pygmalion-storyboard/source-usage';
|
|
19
|
+
const MAX_SOURCE_USAGE_REQUEST = 100;
|
|
13
20
|
export const PYGMALION_STORYBOARD_ENVIRONMENT_QUERY =
|
|
14
21
|
'__pygmalion_environment';
|
|
15
22
|
|
|
@@ -1285,12 +1292,29 @@ export function storyboardEnvironmentBootstrapSource() {
|
|
|
1285
1292
|
value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
1286
1293
|
const environment = plainRecord(decode(encoded));
|
|
1287
1294
|
if (!environment) return;
|
|
1295
|
+
// The Node capture runtime installs the host screen preset with an init
|
|
1296
|
+
// script before any page script runs; in-editor session walks reach the
|
|
1297
|
+
// same global through the boot URL so fixtures that read it at module or
|
|
1298
|
+
// mount time (auth role overrides, static mic levels) behave identically.
|
|
1299
|
+
const screenPreset = plainRecord(environment.screenPreset);
|
|
1300
|
+
if (screenPreset) {
|
|
1301
|
+
Object.defineProperty(globalThis, '__PYGMALION_SCREEN_PRESET__', {
|
|
1302
|
+
configurable: true,
|
|
1303
|
+
value: screenPreset,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
// 'sandbox' virtualizes the whole store per document: reads of undeclared
|
|
1307
|
+
// keys see a pristine profile and writes never reach the shared origin
|
|
1308
|
+
// storage. Hidden preview instances run with it so a walk that persists UI
|
|
1309
|
+
// state (a collapsed sidebar, a dismissed notice) cannot poison sibling
|
|
1310
|
+
// instances or the designer's own tab. Declared entries seed the sandbox.
|
|
1311
|
+
const sandboxed = environment.storageIsolation === 'sandbox';
|
|
1288
1312
|
const storageOverlays = new Map();
|
|
1289
1313
|
const addStorageOverlay = (storage, entries) => {
|
|
1290
1314
|
const record = plainRecord(entries);
|
|
1291
|
-
if (!storage || !record) return;
|
|
1315
|
+
if (!storage || (!record && !sandboxed)) return;
|
|
1292
1316
|
const overlay = {};
|
|
1293
|
-
for (const [key, value] of Object.entries(record)) {
|
|
1317
|
+
for (const [key, value] of Object.entries(record ?? {})) {
|
|
1294
1318
|
if (
|
|
1295
1319
|
typeof key === 'string' &&
|
|
1296
1320
|
key.length <= 512 &&
|
|
@@ -1315,9 +1339,11 @@ export function storyboardEnvironmentBootstrapSource() {
|
|
|
1315
1339
|
value(key) {
|
|
1316
1340
|
const normalizedKey = String(key);
|
|
1317
1341
|
const overlay = storageOverlays.get(this);
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1342
|
+
if (overlay && Object.hasOwn(overlay, normalizedKey)) {
|
|
1343
|
+
return overlay[normalizedKey];
|
|
1344
|
+
}
|
|
1345
|
+
if (overlay && sandboxed) return null;
|
|
1346
|
+
return originalGetItem.call(this, normalizedKey);
|
|
1321
1347
|
},
|
|
1322
1348
|
},
|
|
1323
1349
|
setItem: {
|
|
@@ -1325,7 +1351,7 @@ export function storyboardEnvironmentBootstrapSource() {
|
|
|
1325
1351
|
value(key, value) {
|
|
1326
1352
|
const normalizedKey = String(key);
|
|
1327
1353
|
const overlay = storageOverlays.get(this);
|
|
1328
|
-
if (overlay && Object.hasOwn(overlay, normalizedKey)) {
|
|
1354
|
+
if (overlay && (sandboxed || Object.hasOwn(overlay, normalizedKey))) {
|
|
1329
1355
|
overlay[normalizedKey] = String(value);
|
|
1330
1356
|
return;
|
|
1331
1357
|
}
|
|
@@ -1337,7 +1363,7 @@ export function storyboardEnvironmentBootstrapSource() {
|
|
|
1337
1363
|
value(key) {
|
|
1338
1364
|
const normalizedKey = String(key);
|
|
1339
1365
|
const overlay = storageOverlays.get(this);
|
|
1340
|
-
if (overlay && Object.hasOwn(overlay, normalizedKey)) {
|
|
1366
|
+
if (overlay && (sandboxed || Object.hasOwn(overlay, normalizedKey))) {
|
|
1341
1367
|
overlay[normalizedKey] = null;
|
|
1342
1368
|
return;
|
|
1343
1369
|
}
|
|
@@ -1351,6 +1377,7 @@ export function storyboardEnvironmentBootstrapSource() {
|
|
|
1351
1377
|
if (overlay) {
|
|
1352
1378
|
for (const key of Object.keys(overlay)) overlay[key] = null;
|
|
1353
1379
|
}
|
|
1380
|
+
if (sandboxed) return;
|
|
1354
1381
|
return originalClear.call(this);
|
|
1355
1382
|
},
|
|
1356
1383
|
},
|
|
@@ -1441,17 +1468,25 @@ export function storyboardEnvironmentBootstrapSource() {
|
|
|
1441
1468
|
})();`;
|
|
1442
1469
|
}
|
|
1443
1470
|
|
|
1471
|
+
export const PYGMALION_STORYBOARD_RUNTIME_ATTRIBUTE =
|
|
1472
|
+
'data-pygmalion-storyboard-runtime';
|
|
1473
|
+
|
|
1444
1474
|
export function pygmalionStoryboardRuntimePlugin() {
|
|
1445
1475
|
return {
|
|
1446
1476
|
name: 'pygmalion-storyboard-runtime',
|
|
1447
1477
|
enforce: 'pre',
|
|
1448
1478
|
transformIndexHtml: {
|
|
1449
1479
|
order: 'pre',
|
|
1450
|
-
handler() {
|
|
1480
|
+
handler(html) {
|
|
1481
|
+
// Composed stacks include this plugin twice (the storyboard plugin
|
|
1482
|
+
// embeds it and hosts also add it directly). Vite applies each hook's
|
|
1483
|
+
// tags before the next hook runs, so a copy already in the document
|
|
1484
|
+
// means another instance injected first — one bootstrap is enough.
|
|
1485
|
+
if (html?.includes(PYGMALION_STORYBOARD_RUNTIME_ATTRIBUTE)) return;
|
|
1451
1486
|
return [
|
|
1452
1487
|
{
|
|
1453
1488
|
tag: 'script',
|
|
1454
|
-
attrs: {
|
|
1489
|
+
attrs: { [PYGMALION_STORYBOARD_RUNTIME_ATTRIBUTE]: '' },
|
|
1455
1490
|
children: storyboardEnvironmentBootstrapSource(),
|
|
1456
1491
|
injectTo: 'head-prepend',
|
|
1457
1492
|
},
|
|
@@ -1484,6 +1519,47 @@ async function readExecutionResults(request) {
|
|
|
1484
1519
|
return parsed.executions;
|
|
1485
1520
|
}
|
|
1486
1521
|
|
|
1522
|
+
/**
|
|
1523
|
+
* Serves symbol-level usage for the files a catalog sheet traces to.
|
|
1524
|
+
*
|
|
1525
|
+
* A batch, not one request per file: the scan is a whole-project pass, so
|
|
1526
|
+
* asking 23 times both wastes it and leaves a long tail of entries whose
|
|
1527
|
+
* answer has not arrived — which reads as "unused" exactly when it is least
|
|
1528
|
+
* true.
|
|
1529
|
+
*/
|
|
1530
|
+
export function sourceUsageMiddleware(root, { sourceDirectory = 'src' } = {}) {
|
|
1531
|
+
const projectRoot = path.resolve(root);
|
|
1532
|
+
return async (request, response, next) => {
|
|
1533
|
+
if (request.method !== 'GET') {
|
|
1534
|
+
next();
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
try {
|
|
1538
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
1539
|
+
const files = (url.searchParams.get('files') ?? '')
|
|
1540
|
+
.split(',')
|
|
1541
|
+
.map((file) => file.trim())
|
|
1542
|
+
.filter(Boolean);
|
|
1543
|
+
if (files.length === 0) {
|
|
1544
|
+
sendJson(response, 400, { error: 'Expected a files parameter.' });
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (files.length > MAX_SOURCE_USAGE_REQUEST) {
|
|
1548
|
+
sendJson(response, 413, {
|
|
1549
|
+
error: `At most ${MAX_SOURCE_USAGE_REQUEST} files can be requested at once.`,
|
|
1550
|
+
});
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
const usage = await findSourceUsage(projectRoot, files, { sourceDirectory });
|
|
1554
|
+
sendJson(response, 200, { usage });
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
sendJson(response, 400, {
|
|
1557
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1487
1563
|
function sendJson(response, statusCode, body) {
|
|
1488
1564
|
response.statusCode = statusCode;
|
|
1489
1565
|
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
@@ -1527,6 +1603,17 @@ export function pygmalionStoryboardPlugin(options) {
|
|
|
1527
1603
|
});
|
|
1528
1604
|
}
|
|
1529
1605
|
});
|
|
1606
|
+
// Component branch discovery — the editor's "discovered branches" list
|
|
1607
|
+
// and the host's dimension-declaration gate read the same manifest.
|
|
1608
|
+
server.middlewares.use(
|
|
1609
|
+
PYGMALION_COMPONENT_BRANCHES_CONTROL,
|
|
1610
|
+
componentBranchesMiddleware(root),
|
|
1611
|
+
);
|
|
1612
|
+
// Symbol-level usage — what the catalog storyboard means by "used by".
|
|
1613
|
+
server.middlewares.use(
|
|
1614
|
+
PYGMALION_SOURCE_USAGE_CONTROL,
|
|
1615
|
+
sourceUsageMiddleware(root, { sourceDirectory }),
|
|
1616
|
+
);
|
|
1530
1617
|
server.middlewares.use(
|
|
1531
1618
|
canonicalEndpoint,
|
|
1532
1619
|
async (request, response, next) => {
|
package/node/vite.mjs
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
PYGMALION_PREVIEW_ARTIFACT_ENDPOINT,
|
|
70
70
|
pygmalionPreviewArtifactPlugin,
|
|
71
71
|
} from './preview-artifact-plugin.mjs';
|
|
72
|
+
import { createPreviewCaptureWorkerChannel } from './preview-capture-worker.mjs';
|
|
72
73
|
import {
|
|
73
74
|
DEFAULT_QA_CAPTURE_MAX_FRAMES,
|
|
74
75
|
PYGMALION_QA_ARTIFACT_PREFIX,
|
|
@@ -207,6 +208,12 @@ export function createPygmalionVitePlugins(config) {
|
|
|
207
208
|
|
|
208
209
|
const project = resolvePygmalionProject(config);
|
|
209
210
|
const plugins = [
|
|
211
|
+
// The environment bootstrap must run in the editor host too, not only in
|
|
212
|
+
// the capture preview server: in-editor hidden instances (flow sessions,
|
|
213
|
+
// sweeps, live boots) carry declarative environments and the storage
|
|
214
|
+
// sandbox in their boot URL. Without the query parameter the bootstrap
|
|
215
|
+
// is a no-op, so ordinary app tabs are unaffected.
|
|
216
|
+
pygmalionStoryboardRuntimePlugin(),
|
|
210
217
|
pygmalionStoryboardPlugin({
|
|
211
218
|
root: project.appRoot,
|
|
212
219
|
sourceDirectory: project.sourceDirectory,
|
|
@@ -217,17 +224,55 @@ export function createPygmalionVitePlugins(config) {
|
|
|
217
224
|
// it lazily because a capture starts long after the plugin list is assembled.
|
|
218
225
|
let devMirror = null;
|
|
219
226
|
|
|
220
|
-
|
|
227
|
+
// A host either hands over a generator function or names a warm capture
|
|
228
|
+
// worker script; the function wins when both are present so existing
|
|
229
|
+
// configurations keep their exact behavior.
|
|
230
|
+
let generateArtifact = project.preview.generateArtifact;
|
|
231
|
+
let captureWorkerChannel = null;
|
|
232
|
+
if (!generateArtifact && project.preview.captureWorker) {
|
|
233
|
+
const captureWorker = optionalObject(project.preview.captureWorker);
|
|
234
|
+
if (
|
|
235
|
+
typeof captureWorker.script !== 'string' ||
|
|
236
|
+
captureWorker.script.trim() === ''
|
|
237
|
+
) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
'Pygmalion preview.captureWorker.script must be a non-empty string',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
captureWorkerChannel = createPreviewCaptureWorkerChannel({
|
|
243
|
+
script: path.resolve(project.appRoot, captureWorker.script),
|
|
244
|
+
args: captureWorker.args,
|
|
245
|
+
env: captureWorker.env,
|
|
246
|
+
cwd: project.appRoot,
|
|
247
|
+
idleMs: captureWorker.idleMs,
|
|
248
|
+
jobTimeoutMs: captureWorker.jobTimeoutMs,
|
|
249
|
+
});
|
|
250
|
+
generateArtifact = captureWorkerChannel.generateArtifact;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (project.preview.artifactFile || generateArtifact) {
|
|
221
254
|
plugins.push(
|
|
222
255
|
pygmalionPreviewArtifactPlugin({
|
|
223
256
|
root: project.appRoot,
|
|
224
257
|
artifactFile: project.preview.artifactFile,
|
|
225
258
|
endpoint: project.preview.artifactEndpoint,
|
|
226
|
-
generateArtifact
|
|
259
|
+
generateArtifact,
|
|
227
260
|
acquireLease: (label) => devMirror?.acquireLease(label) ?? null,
|
|
228
261
|
}),
|
|
229
262
|
);
|
|
230
263
|
}
|
|
264
|
+
if (captureWorkerChannel) {
|
|
265
|
+
const channel = captureWorkerChannel;
|
|
266
|
+
plugins.push({
|
|
267
|
+
name: 'pygmalion-preview-capture-worker',
|
|
268
|
+
apply: 'serve',
|
|
269
|
+
configureServer(server) {
|
|
270
|
+
server.httpServer?.once('close', () => {
|
|
271
|
+
void channel.dispose();
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
231
276
|
|
|
232
277
|
if (
|
|
233
278
|
project.qa !== false &&
|
|
@@ -329,6 +374,11 @@ export function createPygmalionVitePlugins(config) {
|
|
|
329
374
|
return plugins;
|
|
330
375
|
}
|
|
331
376
|
|
|
377
|
+
export {
|
|
378
|
+
PYGMALION_COMPONENT_BRANCHES_CONTROL,
|
|
379
|
+
scanComponentBranches,
|
|
380
|
+
scanComponentBranchesFile,
|
|
381
|
+
} from './component-branches.mjs';
|
|
332
382
|
export {
|
|
333
383
|
PYGMALION_DEV_CONTROL,
|
|
334
384
|
PYGMALION_DEV_PREFIX,
|
|
@@ -380,6 +430,7 @@ export {
|
|
|
380
430
|
DEFAULT_PYGMALION_PREVIEW_ARTIFACT_FILE,
|
|
381
431
|
PYGMALION_PREVIEW_ARTIFACT_ENDPOINT,
|
|
382
432
|
pygmalionPreviewArtifactPlugin,
|
|
433
|
+
createPreviewCaptureWorkerChannel,
|
|
383
434
|
DEFAULT_QA_CAPTURE_MAX_FRAMES,
|
|
384
435
|
PYGMALION_QA_ARTIFACT_PREFIX,
|
|
385
436
|
PYGMALION_QA_CAPTURE_ENDPOINT,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pygmalionjs/pygmalion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Code-backed DOM design sandbox and visual QA editor",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,12 +32,14 @@
|
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"dist-lib",
|
|
35
|
+
"node/component-branches.mjs",
|
|
35
36
|
"node/design-session.mjs",
|
|
36
37
|
"node/dev-mirror.mjs",
|
|
37
38
|
"node/dev-view.vite.mjs",
|
|
38
39
|
"node/inspect-plugin.mjs",
|
|
39
40
|
"node/preview-artifact-plugin.mjs",
|
|
40
41
|
"node/preview-artifact-store.mjs",
|
|
42
|
+
"node/preview-capture-worker.mjs",
|
|
41
43
|
"node/preview-vite-cache.mjs",
|
|
42
44
|
"node/qa-capture-plugin.mjs",
|
|
43
45
|
"node/route-preview-artifact-v2.mjs",
|
|
@@ -64,7 +66,7 @@
|
|
|
64
66
|
"dev": "vite",
|
|
65
67
|
"build": "tsc && vite build",
|
|
66
68
|
"build:lib": "vite build --config vite.lib.config.ts",
|
|
67
|
-
"test": "node --test node/*.test.mjs src/**/*.test.mjs",
|
|
69
|
+
"test": "node --test node/*.test.mjs src/*.test.mjs src/**/*.test.mjs",
|
|
68
70
|
"lint:language": "node scripts/lint-language.mjs",
|
|
69
71
|
"typecheck": "tsc --noEmit",
|
|
70
72
|
"prepublishOnly": "npm run lint:language && npm test && npm run typecheck && npm run build:lib",
|
|
@@ -93,6 +95,7 @@
|
|
|
93
95
|
"@types/react": "^19.0.0",
|
|
94
96
|
"@types/react-dom": "^19.0.0",
|
|
95
97
|
"@vitejs/plugin-react": "^4.3.0",
|
|
98
|
+
"happy-dom": "^20.11.1",
|
|
96
99
|
"react": "^19.0.0",
|
|
97
100
|
"react-dom": "^19.0.0",
|
|
98
101
|
"tailwindcss": "^4.0.0",
|