@pygmalionjs/pygmalion 0.6.18 → 0.6.20

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.
@@ -1,4 +1,4 @@
1
- import { F as s, N as a, e as r, s as i, a as t } from "./FrozenRoutePreview-CZSFEQgN.js";
1
+ import { F as s, N as a, e as r, s as i, a as t } from "./FrozenRoutePreview-B6UY1aLt.js";
2
2
  export {
3
3
  s as FrozenRoutePreviewView,
4
4
  a as NodeModel,
@@ -1,3 +1,4 @@
1
+ import { type InteractiveSessionSurface } from '../editor/interactiveSessionSurface';
1
2
  /**
2
3
  * True while a flow session still owes this screen its snapshot. Frames use
3
4
  * it exactly like the artifact-hydration gate: activation waits for the
@@ -24,11 +25,18 @@ export declare function useViewportSessionPending(page: {
24
25
  * itself — the session does that work in one instance the warm pool keeps.
25
26
  */
26
27
  export declare function useInteractiveSessionPending(page: {
27
- importPageId?: string;
28
28
  id: string;
29
29
  interactiveStateId?: string;
30
30
  interactiveOptionId?: string;
31
- }): boolean;
31
+ interactiveOptionIds?: readonly string[];
32
+ }, targetCacheKey?: string): boolean;
33
+ /** The runner-owned live document currently reproducing this state. */
34
+ export declare function useInteractiveSessionSurface(page: {
35
+ id: string;
36
+ interactiveStateId?: string;
37
+ interactiveOptionId?: string;
38
+ interactiveOptionIds?: readonly string[];
39
+ }): InteractiveSessionSurface | null;
32
40
  /**
33
41
  * True while this frame holds one of the host's live screen slots, meaning it
34
42
  * renders the running app rather than its frozen capture.
@@ -67,6 +67,8 @@ export interface DesignScreenInteractionRunOptions {
67
67
  * The capture default is 600ms. A step's explicit `settleMs` always wins.
68
68
  */
69
69
  defaultActionSettleMs?: number;
70
+ /** Called after boot presets succeed and immediately before actions begin. */
71
+ onBeforeInteractions?: () => void;
70
72
  }
71
73
  /** A JSON-compatible value that conveys host-specific meaning without Pygmalion interpreting it. */
72
74
  export type DesignSerializable = string | number | boolean | null | readonly DesignSerializable[] | {
@@ -1,5 +1,9 @@
1
1
  import { type DesignImportController, type DesignScreenCaptureSpec } from './designImport';
2
+ import type { ResolvedFlowPath } from './flowSessions';
3
+ import type { ScreenFlowPath } from './screenFlows';
2
4
  import { type PageModel } from './store';
5
+ /** Applies the active frame's own boot condition to a path-specific session. */
6
+ export declare function resolveActivePagePath(resolved: ResolvedFlowPath, path: ScreenFlowPath, page: PageModel): ResolvedFlowPath;
3
7
  /** Diagnostic counters for the headless debug surface. */
4
8
  export declare const __flowSchedulerDebug: {
5
9
  calls: number;
@@ -69,7 +73,13 @@ type InteractivePreparationController = Pick<DesignImportController, 'prepareScr
69
73
  * The runner already waits for a trailing paint and stable geometry before it
70
74
  * serializes, so repeating the capture stability pass only delays the panel.
71
75
  */
72
- export declare function prepareInteractiveSessionState(iframe: HTMLIFrameElement, spec: DesignScreenCaptureSpec, controller?: InteractivePreparationController | null): Promise<import("./designImport").DesignScreenCaptureReport>;
76
+ export declare function prepareInteractiveSessionState(iframe: HTMLIFrameElement, spec: DesignScreenCaptureSpec, controller?: InteractivePreparationController | null, onBeforeInteractions?: () => void): Promise<import("./designImport").DesignScreenCaptureReport>;
77
+ export declare function interactiveSessionScreenId(page: {
78
+ id: string;
79
+ interactiveStateId?: string;
80
+ interactiveOptionId?: string;
81
+ interactiveOptionIds?: readonly string[];
82
+ }): string;
73
83
  /**
74
84
  * Materializes one screen in its declared interactive state through the
75
85
  * screen's flow path. The prefix walks as pass-through; the target publishes
@@ -78,14 +88,14 @@ export declare function prepareInteractiveSessionState(iframe: HTMLIFrameElement
78
88
  * Returns false when no flow claims the screen — those frames keep the live
79
89
  * boot fallback.
80
90
  */
81
- export declare function materializePageInteractiveState(page: PageModel, previewRevision: string): boolean;
91
+ export declare function materializePageInteractiveState(page: PageModel, previewRevision: string, targetCacheKey?: string): boolean;
82
92
  /** True while an interactive-state session still owes this page its snapshot. */
83
93
  export declare function interactiveSessionWillDeliver(page: {
84
- importPageId?: string;
85
94
  id: string;
86
95
  interactiveStateId?: string;
87
96
  interactiveOptionId?: string;
88
- }): boolean;
97
+ interactiveOptionIds?: readonly string[];
98
+ }, targetCacheKey?: string): boolean;
89
99
  /** Subscribe to interactive-state delivery changes. */
90
100
  export declare function subscribeInteractiveSessions(listener: () => void): () => void;
91
101
  /** Diagnostic counters for the interactive-state path. */
@@ -87,7 +87,7 @@ export interface FlowSessionRunnerOptions {
87
87
  preset?: DesignScreenPreset;
88
88
  interactions?: readonly DesignScreenInteraction[];
89
89
  assertions?: readonly DesignScreenAssertion[];
90
- }) => Promise<DesignScreenCaptureReport>;
90
+ }, waypoint: ResolvedFlowWaypoint) => Promise<DesignScreenCaptureReport>;
91
91
  /** Extra settle before serializing a waypoint (fonts, trailing paints). */
92
92
  settle?: (iframe: HTMLIFrameElement, ms: number) => Promise<void>;
93
93
  /** Layout-stability wait before serializing. Defaults to waitForGeometryStability. */
@@ -95,6 +95,8 @@ export interface FlowSessionRunnerOptions {
95
95
  serialize?: (iframe: HTMLIFrameElement, fallbackUrl: string) => string | null;
96
96
  /** Writes one waypoint snapshot into the route preview store. */
97
97
  publish?: (cacheKey: string, snapshot: string) => boolean;
98
+ /** Releases a live document after its delivery finishes or is abandoned. */
99
+ onWaypointFinish?: (iframe: HTMLIFrameElement, waypoint: ResolvedFlowWaypoint, outcome: 'completed' | 'failed' | 'cancelled') => void;
98
100
  onError?: (pathId: string, error: unknown) => void;
99
101
  }
100
102
  /**
@@ -144,6 +146,11 @@ export declare function createFlowSessionRunner(options?: FlowSessionRunnerOptio
144
146
  setConcurrency(next: number): void;
145
147
  /** Session state of one screen, or null when no session claims it. */
146
148
  screenState(screenId: string): FlowScreenState | null;
149
+ /**
150
+ * Forgets a terminal delivery whose external snapshot was evicted. A
151
+ * running or queued screen keeps its claim and cannot be invalidated.
152
+ */
153
+ forgetScreen(screenId: string): boolean;
147
154
  /** True while the session still owes this screen its snapshot. */
148
155
  willDeliver(screenId: string): boolean;
149
156
  subscribe(listener: () => void): () => void;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The live document currently reproducing a right-panel screen state.
3
+ *
4
+ * State preservation and state presentation are intentionally separate. The
5
+ * flow runner keeps this iframe alive until its exact frozen snapshot is
6
+ * published, while the active frame may mirror the running document
7
+ * immediately so capture stability never becomes interaction latency.
8
+ */
9
+ export interface InteractiveSessionSurface {
10
+ iframe: HTMLIFrameElement;
11
+ screenId: string;
12
+ }
13
+ /** Exposes a runner-owned iframe without transferring its ownership. */
14
+ export declare function exposeInteractiveSessionSurface(screenId: string, iframe: HTMLIFrameElement): void;
15
+ /** Releases a surface only when the caller still owns the registered iframe. */
16
+ export declare function releaseInteractiveSessionSurface(screenId: string, iframe: HTMLIFrameElement): void;
17
+ export declare function getInteractiveSessionSurface(screenId: string): InteractiveSessionSurface | null;
18
+ export declare function subscribeInteractiveSessionSurface(screenId: string, listener: () => void): () => void;
package/inspect.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { InspectApplyPayload } from './dist-lib/types/lib';
2
+
3
+ export interface PygmalionInspectWritebackOptions {
4
+ sourceDirectory?: string;
5
+ normalizeValue?: (property: string, value: string) => string;
6
+ }
7
+
8
+ export interface PygmalionInspectPreviewResult {
9
+ files: { file: string; diff: string }[];
10
+ affectedFiles: string[];
11
+ }
12
+
13
+ export interface PygmalionInspectWriteResult {
14
+ applied: number;
15
+ changed: number;
16
+ files: string[];
17
+ }
18
+
19
+ export declare function previewInspectChanges(
20
+ root: string,
21
+ input: InspectApplyPayload,
22
+ options?: PygmalionInspectWritebackOptions,
23
+ ): Promise<PygmalionInspectPreviewResult>;
24
+
25
+ export declare function writeInspectChanges(
26
+ root: string,
27
+ input: InspectApplyPayload,
28
+ options?: PygmalionInspectWritebackOptions,
29
+ ): Promise<PygmalionInspectWriteResult>;
30
+
31
+ export declare function findAffectedSourceFiles(
32
+ root: string,
33
+ componentFiles: string[],
34
+ options?: { sourceDirectory?: string },
35
+ ): Promise<string[]>;
@@ -385,7 +385,7 @@ export function pygmalionInspectPlugin({
385
385
  magic.append(`
386
386
 
387
387
  ;(() => {
388
- if (!import.meta.env.DEV) return;
388
+ if (typeof window === 'undefined') return;
389
389
  (globalThis.__PYG_CSS_MODULES__ ||= []).push(${items});
390
390
  })();`);
391
391
  }
@@ -0,0 +1,163 @@
1
+ import fsp from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { writeStyleEdit, writeTextEdit } from './inspect-plugin.mjs';
5
+ import { createUnifiedDiff } from './source-diff.mjs';
6
+ import { writeSourceOperations } from './source-operations.mjs';
7
+
8
+ const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts']);
9
+
10
+ function normalizeSourceDirectory(value = 'src') {
11
+ return value.replace(/^\.?\//, '').replace(/\/$/, '') || '.';
12
+ }
13
+
14
+ function validateRelativeSourcePath(root, value, sourceDirectory, extensions) {
15
+ if (
16
+ typeof value !== 'string' ||
17
+ path.isAbsolute(value) ||
18
+ value.includes('\0') ||
19
+ value.split(/[\\/]/).includes('..') ||
20
+ !extensions.has(path.extname(value).toLowerCase())
21
+ ) {
22
+ throw new Error(`Source path cannot be modified: ${String(value)}`);
23
+ }
24
+ const normalized = value.split(path.sep).join('/');
25
+ if (
26
+ sourceDirectory !== '.' &&
27
+ normalized !== sourceDirectory &&
28
+ !normalized.startsWith(`${sourceDirectory}/`)
29
+ ) {
30
+ throw new Error(`Only ${sourceDirectory} source files can be modified`);
31
+ }
32
+ const absolutePath = path.resolve(root, normalized);
33
+ const relativePath = path.relative(root, absolutePath);
34
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
35
+ throw new Error('Source path escapes app root');
36
+ }
37
+ return {
38
+ absolutePath,
39
+ relativePath: relativePath.split(path.sep).join('/'),
40
+ };
41
+ }
42
+
43
+ function normalizeInspectChanges(input) {
44
+ const styles = Array.isArray(input?.styles) ? input.styles : [];
45
+ const texts = Array.isArray(input?.texts) ? input.texts : [];
46
+ const operations = Array.isArray(input?.operations) ? input.operations : [];
47
+ const operationCount = styles.length + texts.length + operations.length;
48
+ if (operationCount === 0 || operationCount > 100) {
49
+ throw new Error('Inspect changes must contain between 1 and 100 operations');
50
+ }
51
+ return { styles, texts, operations, operationCount };
52
+ }
53
+
54
+ function collectInspectFiles(root, changes, sourceDirectory) {
55
+ const files = new Map();
56
+ for (const style of changes.styles) {
57
+ const file = validateRelativeSourcePath(
58
+ root,
59
+ style?.file,
60
+ sourceDirectory,
61
+ new Set(['.scss']),
62
+ );
63
+ files.set(file.relativePath, file.absolutePath);
64
+ }
65
+ for (const text of changes.texts) {
66
+ const file = validateRelativeSourcePath(
67
+ root,
68
+ text?.componentFile,
69
+ sourceDirectory,
70
+ SOURCE_EXTENSIONS,
71
+ );
72
+ files.set(file.relativePath, file.absolutePath);
73
+ }
74
+ for (const operation of changes.operations) {
75
+ const file = validateRelativeSourcePath(
76
+ root,
77
+ operation?.identity?.componentFile,
78
+ sourceDirectory,
79
+ SOURCE_EXTENSIONS,
80
+ );
81
+ files.set(file.relativePath, file.absolutePath);
82
+ }
83
+ return files;
84
+ }
85
+
86
+ async function applyInspectChanges(root, changes, options) {
87
+ const results = [];
88
+ for (const style of changes.styles) {
89
+ results.push(await writeStyleEdit(root, style, options));
90
+ }
91
+ const sourceResult = changes.operations.length
92
+ ? await writeSourceOperations(root, changes.operations, options)
93
+ : null;
94
+ for (const text of changes.texts) {
95
+ results.push(await writeTextEdit(root, text, options));
96
+ }
97
+ return {
98
+ applied: results.length + (sourceResult?.operations ?? 0),
99
+ changed: results.filter((result) => result.changed).length + (sourceResult?.changed ?? 0),
100
+ };
101
+ }
102
+
103
+ export async function previewInspectChanges(root, input, options = {}) {
104
+ const appRoot = path.resolve(root);
105
+ const sourceDirectory = normalizeSourceDirectory(options.sourceDirectory);
106
+ const changes = normalizeInspectChanges(input);
107
+ const files = collectInspectFiles(appRoot, changes, sourceDirectory);
108
+ const before = new Map();
109
+ for (const [relativePath, absolutePath] of files) {
110
+ before.set(relativePath, await fsp.readFile(absolutePath, 'utf8'));
111
+ }
112
+
113
+ const temporaryRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'pygmalion-inspect-'));
114
+ try {
115
+ for (const [relativePath, source] of before) {
116
+ const target = path.join(temporaryRoot, relativePath);
117
+ await fsp.mkdir(path.dirname(target), { recursive: true });
118
+ await fsp.writeFile(target, source);
119
+ }
120
+ await applyInspectChanges(temporaryRoot, changes, {
121
+ ...options,
122
+ sourceDirectory,
123
+ });
124
+ const diffs = [];
125
+ for (const [relativePath, source] of before) {
126
+ const next = await fsp.readFile(path.join(temporaryRoot, relativePath), 'utf8');
127
+ const diff = createUnifiedDiff(source, next, relativePath);
128
+ if (diff) diffs.push({ file: relativePath, diff });
129
+ }
130
+ return {
131
+ files: diffs,
132
+ affectedFiles: [...files.keys()].sort(),
133
+ };
134
+ } finally {
135
+ await fsp.rm(temporaryRoot, { recursive: true, force: true });
136
+ }
137
+ }
138
+
139
+ export async function writeInspectChanges(root, input, options = {}) {
140
+ const appRoot = path.resolve(root);
141
+ const sourceDirectory = normalizeSourceDirectory(options.sourceDirectory);
142
+ const changes = normalizeInspectChanges(input);
143
+ const files = collectInspectFiles(appRoot, changes, sourceDirectory);
144
+ const backups = new Map();
145
+ for (const [relativePath, absolutePath] of files) {
146
+ backups.set(relativePath, await fsp.readFile(absolutePath, 'utf8'));
147
+ }
148
+
149
+ try {
150
+ const result = await applyInspectChanges(appRoot, changes, {
151
+ ...options,
152
+ sourceDirectory,
153
+ });
154
+ return { ...result, files: [...files.keys()].sort() };
155
+ } catch (error) {
156
+ await Promise.all(
157
+ [...backups].map(([relativePath, source]) =>
158
+ fsp.writeFile(files.get(relativePath), source),
159
+ ),
160
+ );
161
+ throw error;
162
+ }
163
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ previewInspectChanges,
3
+ writeInspectChanges,
4
+ } from './inspect-writeback.mjs';
5
+ export { findAffectedSourceFiles } from './source-graph.mjs';
package/node/vite.mjs CHANGED
@@ -84,6 +84,10 @@ import {
84
84
  validateQaCaptureRequest,
85
85
  } from './qa-capture-plugin.mjs';
86
86
  import { resolveGitSourceState } from './source-revision.mjs';
87
+ import {
88
+ previewInspectChanges,
89
+ writeInspectChanges,
90
+ } from './inspect-writeback.mjs';
87
91
 
88
92
  function requiredPath(value, name, base) {
89
93
  if (typeof value !== 'string' || value.trim() === '') {
@@ -414,6 +418,8 @@ export {
414
418
  writeSourceOperations,
415
419
  writeStyleEdit,
416
420
  writeTextEdit,
421
+ previewInspectChanges,
422
+ writeInspectChanges,
417
423
  convertRoutePreviewArtifactV1ToV2,
418
424
  convertRoutePreviewArtifactV2ToV1,
419
425
  createRoutePreviewArtifactV2,
@@ -0,0 +1,59 @@
1
+ const path = require('node:path');
2
+
3
+ const inspectPlugins = new Map();
4
+
5
+ function normalizeSourceDirectory(value) {
6
+ if (typeof value !== 'string') return 'src';
7
+ return value.replace(/^\.?\//, '').replace(/\/$/, '') || '.';
8
+ }
9
+
10
+ function isWithinSourceDirectory(relativePath, sourceDirectory) {
11
+ return (
12
+ sourceDirectory === '.' ||
13
+ relativePath === sourceDirectory ||
14
+ relativePath.startsWith(`${sourceDirectory}/`)
15
+ );
16
+ }
17
+
18
+ function inspectPlugin(root, sourceDirectory) {
19
+ const key = `${root}\0${sourceDirectory}`;
20
+ let promise = inspectPlugins.get(key);
21
+ if (!promise) {
22
+ promise = import('./inspect-plugin.mjs').then(({ pygmalionInspectPlugin }) =>
23
+ pygmalionInspectPlugin({ root, sourceDirectory }),
24
+ );
25
+ inspectPlugins.set(key, promise);
26
+ }
27
+ return promise;
28
+ }
29
+
30
+ module.exports = function pygmalionWebpackLoader(source, inputMap) {
31
+ const callback = this.async();
32
+ const options = typeof this.getOptions === 'function' ? this.getOptions() : {};
33
+ const root = path.resolve(options.root || this.rootContext || process.cwd());
34
+ const sourceDirectory = normalizeSourceDirectory(options.sourceDirectory);
35
+ const componentFile = path
36
+ .relative(root, this.resourcePath)
37
+ .split(path.sep)
38
+ .join('/');
39
+
40
+ if (
41
+ componentFile.startsWith('../') ||
42
+ path.isAbsolute(componentFile) ||
43
+ !isWithinSourceDirectory(componentFile, sourceDirectory)
44
+ ) {
45
+ callback(null, source, inputMap);
46
+ return;
47
+ }
48
+
49
+ inspectPlugin(root, sourceDirectory)
50
+ .then((plugin) => {
51
+ const transformed = plugin.transform(String(source), this.resourcePath);
52
+ if (!transformed) {
53
+ callback(null, source, inputMap);
54
+ return;
55
+ }
56
+ callback(null, transformed.code, transformed.map);
57
+ })
58
+ .catch((error) => callback(error));
59
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pygmalionjs/pygmalion",
3
- "version": "0.6.18",
3
+ "version": "0.6.20",
4
4
  "description": "Code-backed DOM design sandbox and visual QA editor",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -23,7 +23,12 @@
23
23
  "types": "./storyboard.d.ts",
24
24
  "default": "./node/storyboard.mjs"
25
25
  },
26
+ "./inspect": {
27
+ "types": "./inspect.d.ts",
28
+ "default": "./node/inspect.mjs"
29
+ },
26
30
  "./dev-view": "./node/dev-view.vite.mjs",
31
+ "./webpack-loader": "./node/webpack-loader.cjs",
27
32
  "./testing": {
28
33
  "types": "./dist-lib/types/testing.d.ts",
29
34
  "default": "./dist-lib/testing.js"
@@ -40,6 +45,8 @@
40
45
  "node/dev-mirror.mjs",
41
46
  "node/dev-view.vite.mjs",
42
47
  "node/inspect-plugin.mjs",
48
+ "node/inspect.mjs",
49
+ "node/inspect-writeback.mjs",
43
50
  "node/preview-artifact-plugin.mjs",
44
51
  "node/preview-artifact-store.mjs",
45
52
  "node/preview-capture-worker.mjs",
@@ -59,6 +66,8 @@
59
66
  "node/storyboard-environment.mjs",
60
67
  "node/storyboard.mjs",
61
68
  "node/vite.mjs",
69
+ "node/webpack-loader.cjs",
70
+ "inspect.d.ts",
62
71
  "qa.d.ts",
63
72
  "storyboard.d.ts",
64
73
  "vite.d.ts"
package/vite.d.ts CHANGED
@@ -264,6 +264,34 @@ export declare function writeTextEdit(
264
264
  options?: { sourceDirectory?: string },
265
265
  ): Promise<unknown>;
266
266
 
267
+ export interface PygmalionInspectWritebackOptions {
268
+ sourceDirectory?: string;
269
+ normalizeValue?: (property: string, value: string) => string;
270
+ }
271
+
272
+ export interface PygmalionInspectPreviewResult {
273
+ files: { file: string; diff: string }[];
274
+ affectedFiles: string[];
275
+ }
276
+
277
+ export interface PygmalionInspectWriteResult {
278
+ applied: number;
279
+ changed: number;
280
+ files: string[];
281
+ }
282
+
283
+ export declare function previewInspectChanges(
284
+ root: string,
285
+ input: import('./types').InspectApplyPayload,
286
+ options?: PygmalionInspectWritebackOptions,
287
+ ): Promise<PygmalionInspectPreviewResult>;
288
+
289
+ export declare function writeInspectChanges(
290
+ root: string,
291
+ input: import('./types').InspectApplyPayload,
292
+ options?: PygmalionInspectWritebackOptions,
293
+ ): Promise<PygmalionInspectWriteResult>;
294
+
267
295
  export interface PygmalionSourceOperationApplyResult {
268
296
  code: string;
269
297
  changed: boolean;