@pygmalionjs/pygmalion 0.5.15 → 0.5.16
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-BR5CBAD3.js → FrozenRoutePreview-CZOpvN6z.js} +1545 -1515
- package/dist-lib/pygmalion.js +5034 -4955
- package/dist-lib/testing.js +1 -1
- package/dist-lib/types/editor/host.d.ts +19 -0
- package/dist-lib/types/lib.d.ts +20 -0
- package/node/dev-mirror.mjs +63 -17
- package/package.json +1 -1
package/dist-lib/testing.js
CHANGED
|
@@ -87,6 +87,25 @@ export interface DesignChangeResult {
|
|
|
87
87
|
export declare function setAppOrigin(origin: string): void;
|
|
88
88
|
export declare function getAppOrigin(): string;
|
|
89
89
|
export declare function subscribeAppOrigin(listener: () => void): () => void;
|
|
90
|
+
export declare function setArtifactOrigin(origin: string): void;
|
|
91
|
+
/**
|
|
92
|
+
* Origin a captured snapshot resolves its relative assets against.
|
|
93
|
+
*
|
|
94
|
+
* This is not the same question as "where does the live screen run". A capture is
|
|
95
|
+
* already rendered — it needs somewhere to fetch the images and fonts its markup
|
|
96
|
+
* still points at — while a live frame needs the checkout of a specific revision.
|
|
97
|
+
* Answering both with one value tied the catalog to a checkout that takes minutes
|
|
98
|
+
* to build, so opening the editor showed a spinner instead of the frames sitting
|
|
99
|
+
* on disk.
|
|
100
|
+
*
|
|
101
|
+
* The order is most-accurate-first: an origin the host declared, then the live
|
|
102
|
+
* one once a runtime exists, then the editor's own — which serves the same
|
|
103
|
+
* application and lets the catalog paint immediately. Assets it cannot resolve
|
|
104
|
+
* are the ones the working tree no longer has; the live origin replaces them the
|
|
105
|
+
* moment it comes up, because these readers re-run when it does.
|
|
106
|
+
*/
|
|
107
|
+
export declare function getArtifactOrigin(): string;
|
|
108
|
+
export declare function subscribeArtifactOrigin(listener: () => void): () => void;
|
|
90
109
|
/** Baseline code version of the preview (dev SHA, etc.). When a change is made, a new static screen cache of the same URL is also created. */
|
|
91
110
|
export declare function setPreviewRevision(revision: string): void;
|
|
92
111
|
export declare function getPreviewRevision(): string;
|
package/dist-lib/types/lib.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export { createDesignComponentCatalog, createDesignComponentVariantMatrix, creat
|
|
|
30
30
|
export type { DesignCatalogBuild, DesignCatalogFrame, DesignCatalogItem, DesignCatalogKind, DesignCatalogPageDescriptor, DesignComponentCatalogOptions, DesignComponentVariantMatrix, DesignGalleryCatalogOptions, PygmalionCatalogDocumentProps, PygmalionCatalogMatrixProps, PygmalionCatalogSectionProps, } from './editor/catalog';
|
|
31
31
|
export { setTokens, applyTokenOverride, resetTokenOverrides, } from './editor/tokens';
|
|
32
32
|
export { setAppOrigin as setPygmalionAppOrigin } from './editor/host';
|
|
33
|
+
export { setArtifactOrigin as setPygmalionArtifactOrigin } from './editor/host';
|
|
33
34
|
export { setPreviewRevision as setPygmalionPreviewRevision } from './editor/host';
|
|
34
35
|
export { clearRoutePreviewSnapshots as clearPygmalionPreviewCache, recommendedRoutePreviewConcurrency as getRecommendedPygmalionPreviewConcurrency, setRoutePreviewConcurrency as setPygmalionPreviewConcurrency, } from './editor/routePreview';
|
|
35
36
|
export type { RoutePreviewArtifactBundle } from './editor/routePreview';
|
|
@@ -100,6 +101,25 @@ export declare const __debug: {
|
|
|
100
101
|
} | null;
|
|
101
102
|
hasShadowArtifact: boolean;
|
|
102
103
|
hasSnapshot: boolean;
|
|
104
|
+
keys: {
|
|
105
|
+
which: "active" | "inactive";
|
|
106
|
+
sameAsDebugKey: boolean;
|
|
107
|
+
hasShadowArtifact: boolean;
|
|
108
|
+
hasSnapshot: boolean;
|
|
109
|
+
}[];
|
|
110
|
+
isActivePage: boolean;
|
|
111
|
+
snapshotShape: {
|
|
112
|
+
length: number;
|
|
113
|
+
bodyLength: number;
|
|
114
|
+
elements: number;
|
|
115
|
+
basePlaceholderLeft: boolean;
|
|
116
|
+
baseHref: string | null;
|
|
117
|
+
head: string;
|
|
118
|
+
} | null;
|
|
119
|
+
previewIdentityReady: boolean;
|
|
120
|
+
seedRevision: string;
|
|
121
|
+
hostRevision: string;
|
|
122
|
+
staticSurface: import("./editor/routePreviewStatus").StaticRoutePreviewSurface;
|
|
103
123
|
tier: import("./editor/frameLod").FrameLifecycleTier;
|
|
104
124
|
magnified: boolean;
|
|
105
125
|
promotion: {
|
package/node/dev-mirror.mjs
CHANGED
|
@@ -29,6 +29,9 @@ const LEASE_HEARTBEAT_MS = 2_000;
|
|
|
29
29
|
// costs a checkout and a dependency install, so the reaper errs toward keeping.
|
|
30
30
|
const DEFAULT_MIRROR_GRACE_MS = 6 * 60 * 60 * 1_000;
|
|
31
31
|
const LEASE_WAIT_TIMEOUT_MS = 180_000;
|
|
32
|
+
// A serving editor holds this for its whole session. It marks the checkout as in
|
|
33
|
+
// use for the reaper; it must never stand in the way of a sync.
|
|
34
|
+
const SERVE_LEASE_LABEL = 'serve';
|
|
32
35
|
|
|
33
36
|
export function resolveDevMirrorInventoryOutputRoot(inventory, mirrorAppRoot) {
|
|
34
37
|
return path.resolve(inventory?.outputRoot ?? mirrorAppRoot);
|
|
@@ -469,10 +472,25 @@ export async function acquireDevMirrorLease({ repoRoot, mirrorRoot, label = 'cap
|
|
|
469
472
|
};
|
|
470
473
|
}
|
|
471
474
|
|
|
472
|
-
|
|
473
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Leases that a sync must wait out.
|
|
477
|
+
*
|
|
478
|
+
* A reader lease is short: a capture holds one while it walks the checkout, and
|
|
479
|
+
* moving the files under it would corrupt what it is reading. A serve lease is
|
|
480
|
+
* the opposite — it lasts as long as an editor is open, and it exists so a
|
|
481
|
+
* reaper can tell a live checkout from an abandoned one. Treating the two the
|
|
482
|
+
* same wedged the second editor on a ref: it waited three minutes for a lease
|
|
483
|
+
* that, by design, never clears, and then reported the mirror as held.
|
|
484
|
+
*/
|
|
485
|
+
export function leasesBlockingSync(leases) {
|
|
486
|
+
return leases.filter((lease) => lease.label !== SERVE_LEASE_LABEL);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function waitForLeasesToClear(repoRoot, mirrorRoot, timeoutMs) {
|
|
490
|
+
const limit = Number.isFinite(timeoutMs) ? timeoutMs : LEASE_WAIT_TIMEOUT_MS;
|
|
491
|
+
const deadline = Date.now() + limit;
|
|
474
492
|
while (true) {
|
|
475
|
-
const held = await freshLeases(repoRoot, mirrorRoot);
|
|
493
|
+
const held = leasesBlockingSync(await freshLeases(repoRoot, mirrorRoot));
|
|
476
494
|
if (held.length === 0) return;
|
|
477
495
|
if (Date.now() >= deadline) {
|
|
478
496
|
const labels = [...new Set(held.map((lease) => lease.label))].join(', ');
|
|
@@ -844,7 +862,7 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
844
862
|
const lease = await acquireDevMirrorLease({
|
|
845
863
|
repoRoot,
|
|
846
864
|
mirrorRoot: activeRoot,
|
|
847
|
-
label:
|
|
865
|
+
label: SERVE_LEASE_LABEL,
|
|
848
866
|
}).catch(() => null);
|
|
849
867
|
if (lease) serveLease = { mirrorRoot: activeRoot, lease };
|
|
850
868
|
};
|
|
@@ -854,6 +872,9 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
854
872
|
await held?.lease.release().catch(() => undefined);
|
|
855
873
|
};
|
|
856
874
|
|
|
875
|
+
// Whether anything has asked for the checkout yet. The runtime is built once,
|
|
876
|
+
// on first demand, and every later demand joins the same work.
|
|
877
|
+
let runtimeRequested = false;
|
|
857
878
|
let previewChild = null;
|
|
858
879
|
let previewPort = null;
|
|
859
880
|
let syncPromise = null;
|
|
@@ -1117,8 +1138,9 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
1117
1138
|
let warning = null;
|
|
1118
1139
|
try {
|
|
1119
1140
|
return await withSharedSyncLock(async () => {
|
|
1120
|
-
// A capture reading this checkout must finish before it moves.
|
|
1121
|
-
|
|
1141
|
+
// A capture reading this checkout must finish before it moves. A serving
|
|
1142
|
+
// editor is not that kind of holder — see leasesBlockingSync.
|
|
1143
|
+
await waitForLeasesToClear(repoRoot, mirrorRoot, options.leaseWaitTimeoutMs);
|
|
1122
1144
|
const mirrorState = await syncDevMirrorWorktree({
|
|
1123
1145
|
repoRoot,
|
|
1124
1146
|
mirrorRoot,
|
|
@@ -1195,14 +1217,27 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
1195
1217
|
process.once('exit', () => {
|
|
1196
1218
|
previewChild?.kill('SIGTERM');
|
|
1197
1219
|
});
|
|
1198
|
-
//
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1220
|
+
// Nothing is prepared until something asks for it.
|
|
1221
|
+
//
|
|
1222
|
+
// Opening the editor used to build the checkout: fetch, worktree, install,
|
|
1223
|
+
// inventory — minutes before the first pixel, on every start, even when all
|
|
1224
|
+
// anyone did was look at the catalog. The captured frames are already on
|
|
1225
|
+
// disk and belong to the consuming app, so browsing needs none of it.
|
|
1226
|
+
//
|
|
1227
|
+
// What genuinely needs the checkout asks for it: a refresh or a revision
|
|
1228
|
+
// switch (below), and any request for the preview origin — the live frame,
|
|
1229
|
+
// its assets, a capture. Those requests kick the work off and answer
|
|
1230
|
+
// "preparing" until it lands, which is what they already did while a
|
|
1231
|
+
// start-time sync was in flight.
|
|
1232
|
+
const startRuntime = () => {
|
|
1233
|
+
if (runtimeRequested) return;
|
|
1234
|
+
runtimeRequested = true;
|
|
1235
|
+
void syncMirror()
|
|
1236
|
+
.then(() =>
|
|
1237
|
+
reapDevMirrorWorktrees({ repoRoot, mirrorBaseRoot, keep: [mirrorRoot] }),
|
|
1238
|
+
)
|
|
1239
|
+
.then((result) => {
|
|
1240
|
+
const removed = result?.removed ?? [];
|
|
1206
1241
|
if (removed.length > 0) {
|
|
1207
1242
|
console.log(
|
|
1208
1243
|
`[pygmalion] released ${removed.length} unused dev screen checkout(s): ${removed
|
|
@@ -1211,8 +1246,8 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
1211
1246
|
);
|
|
1212
1247
|
}
|
|
1213
1248
|
})
|
|
1214
|
-
.catch(() => undefined)
|
|
1215
|
-
|
|
1249
|
+
.catch(() => undefined);
|
|
1250
|
+
};
|
|
1216
1251
|
|
|
1217
1252
|
server.middlewares.use(async (req, res, next) => {
|
|
1218
1253
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
@@ -1249,6 +1284,10 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
1249
1284
|
}
|
|
1250
1285
|
let nextStatus;
|
|
1251
1286
|
try {
|
|
1287
|
+
// An explicit refresh is a demand like any other — mark the runtime as
|
|
1288
|
+
// started so a later preview request joins this work instead of
|
|
1289
|
+
// scheduling a second one.
|
|
1290
|
+
runtimeRequested = true;
|
|
1252
1291
|
nextStatus = await syncMirror(requestedRef);
|
|
1253
1292
|
} catch (error) {
|
|
1254
1293
|
json(res, 400, {
|
|
@@ -1265,7 +1304,14 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
1265
1304
|
// no longer matches the revision this origin is named after.
|
|
1266
1305
|
const current = await verifiedStatus();
|
|
1267
1306
|
if (current.state !== 'ready' || previewPort == null) {
|
|
1268
|
-
|
|
1307
|
+
// This is the first thing that actually needs the checkout, so it is
|
|
1308
|
+
// the thing that starts it.
|
|
1309
|
+
startRuntime();
|
|
1310
|
+
json(res, 503, {
|
|
1311
|
+
ok: false,
|
|
1312
|
+
error: current.error ?? 'Preparing the dev screen',
|
|
1313
|
+
preparing: true,
|
|
1314
|
+
});
|
|
1269
1315
|
return;
|
|
1270
1316
|
}
|
|
1271
1317
|
proxyRequest(req, res, previewPort, prefix);
|