@pygmalionjs/pygmalion 0.9.0 → 0.11.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.
Files changed (37) hide show
  1. package/README.md +34 -0
  2. package/dist-lib/{CameraLayer-BdOko8XE.js → CameraLayer-BZ8HgrUG.js} +1 -1
  3. package/dist-lib/pygmalion.js +10813 -10529
  4. package/dist-lib/{runtime-Dxb-IaLN.js → runtime-Bxn8116i.js} +1825 -1798
  5. package/dist-lib/testing.js +15 -15
  6. package/dist-lib/types/binding/duplicateMetadata.d.ts +8 -0
  7. package/dist-lib/types/binding/index.d.ts +1 -0
  8. package/dist-lib/types/document/contracts.d.ts +10 -0
  9. package/dist-lib/types/document/prototype.d.ts +26 -5
  10. package/dist-lib/types/document/prototypeOccurrence.d.ts +18 -0
  11. package/dist-lib/types/lib.d.ts +6 -2
  12. package/dist-lib/types/token-library/valueResolver.d.ts +19 -0
  13. package/dist-lib/types/workspace/configuration.d.ts +8 -0
  14. package/dist-lib/types/workspace/edit/projection.d.ts +1 -1
  15. package/dist-lib/types/workspace/edit/prototypeProjection.d.ts +7 -0
  16. package/dist-lib/types/workspace/view/PrototypePlayer.d.ts +4 -3
  17. package/dist-lib/types/workspace/view/prototypePlayback.d.ts +2 -2
  18. package/host-runtime.d.ts +73 -0
  19. package/jsx-source-reconciler.d.ts +35 -0
  20. package/node/host-runtime.mjs +4 -0
  21. package/node/jsx-source-reconciler.mjs +155 -0
  22. package/node/revision-catalog-resolver.mjs +159 -0
  23. package/node/source-archive-runtime.mjs +63 -0
  24. package/node/source-module.mjs +17 -0
  25. package/node/source-service-inputs.mjs +81 -0
  26. package/node/source-service-plugin.mjs +57 -0
  27. package/node/source-service.mjs +4 -0
  28. package/node/token-source-service.mjs +151 -0
  29. package/node/workspace-draft-entry.mjs +52 -0
  30. package/node/workspace-draft-preview.mjs +3 -2
  31. package/node/workspace-draft-process.mjs +129 -0
  32. package/node/workspace-draft-runtime.mjs +67 -0
  33. package/node/workspace-source-session.mjs +187 -0
  34. package/package.json +40 -2
  35. package/source-module.d.ts +16 -0
  36. package/source-service.d.ts +92 -0
  37. package/workspace-draft-entry.d.ts +15 -0
@@ -0,0 +1,129 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { promisify } from 'node:util';
5
+
6
+ const execute = promisify(execFile);
7
+
8
+ function signalGroup(pid, signal) {
9
+ try {
10
+ process.kill(-pid, signal);
11
+ return true;
12
+ } catch (error) {
13
+ if (error.code === 'ESRCH') return false;
14
+ throw error;
15
+ }
16
+ }
17
+
18
+ async function hasRunningGroup(pid) {
19
+ if (!signalGroup(pid, 0)) return false;
20
+ let stdout;
21
+ try {
22
+ ({ stdout } = await execute('ps', ['-ax', '-o', 'pgid=,stat='], { timeout: 5_000 }));
23
+ } catch {
24
+ if (process.platform === 'linux') {
25
+ try {
26
+ for (const entry of await readdir('/proc')) {
27
+ if (!/^\d+$/.test(entry)) continue;
28
+ const stat = await readFile(`/proc/${entry}/stat`, 'utf8').catch(error => {
29
+ if (error.code === 'ENOENT' || error.code === 'ESRCH') return '';
30
+ throw error;
31
+ });
32
+ // Parenthesized command names may contain spaces and closing parentheses.
33
+ const [state, , group] = stat.slice(stat.lastIndexOf(')') + 1).trim().split(/\s+/);
34
+ if (Number(group) === pid && state !== 'Z') return true;
35
+ }
36
+ return false;
37
+ } catch { /* Keep the process group alive until its state can be determined. */ }
38
+ }
39
+ // An unavailable inspector cannot authorize cleanup while descendants remain.
40
+ return signalGroup(pid, 0);
41
+ }
42
+ return stdout.split('\n').some(line => {
43
+ const [group, status] = line.trim().split(/\s+/);
44
+ // Reaping belongs to the operating system; zombies cannot write source files.
45
+ return Number(group) === pid && status && !status.startsWith('Z');
46
+ });
47
+ }
48
+
49
+ async function terminateGroup(pid, graceMs) {
50
+ if (!pid || !signalGroup(pid, 'SIGTERM')) return;
51
+ const deadline = Date.now() + graceMs;
52
+ while (await hasRunningGroup(pid)) {
53
+ if (Date.now() >= deadline) signalGroup(pid, 'SIGKILL');
54
+ await delay(25);
55
+ }
56
+ }
57
+
58
+ /** Resolve or reject only after the command and its process group stop writing. */
59
+ export async function executeWorkspaceDraftProcess(command, args, {
60
+ signal, timeout = 240_000, terminationGraceMs = 1_000, maxBuffer = 8 * 1024 * 1024, ...options
61
+ } = {}) {
62
+ signal?.throwIfAborted();
63
+ if (process.platform === 'win32') {
64
+ throw new Error('Draft process group cleanup supports macOS and Linux.');
65
+ }
66
+ if (!Number.isFinite(timeout) || timeout < 0 || timeout > 2 ** 31 - 1
67
+ || !Number.isFinite(terminationGraceMs) || terminationGraceMs < 0
68
+ || !Number.isSafeInteger(maxBuffer) || maxBuffer < 0) {
69
+ throw new TypeError('Process timeout, termination grace, and output limits must be finite non-negative values.');
70
+ }
71
+ let child;
72
+ let termination;
73
+ let cancellation;
74
+ let cancelled = false;
75
+ let timeoutHandle;
76
+ const terminate = () => {
77
+ termination ??= terminateGroup(child?.pid, terminationGraceMs);
78
+ // Observe termination failure while the process close event is still pending.
79
+ void termination.catch(() => {});
80
+ return termination;
81
+ };
82
+ const cancel = reason => {
83
+ if (!cancelled) { cancelled = true; cancellation = reason; }
84
+ void terminate();
85
+ };
86
+ const onAbort = () => cancel(signal.reason ?? new DOMException('Draft execution cancelled.', 'AbortError'));
87
+ const closed = new Promise(resolve => {
88
+ // spawn owns the detached process group; execFile does not forward detached.
89
+ child = spawn(command, args, { ...options, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
90
+ const stdout = []; const stderr = [];
91
+ let stdoutSize = 0; let stderrSize = 0; let spawnError;
92
+ const append = (chunks, chunk, size) => {
93
+ if (size > maxBuffer) {
94
+ const error = Object.assign(new Error('Draft process output exceeded its byte limit.'), {
95
+ code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER',
96
+ });
97
+ cancel(error);
98
+ } else chunks.push(chunk);
99
+ };
100
+ child.stdout.on('data', chunk => { stdoutSize += chunk.length; append(stdout, chunk, stdoutSize); });
101
+ child.stderr.on('data', chunk => { stderrSize += chunk.length; append(stderr, chunk, stderrSize); });
102
+ child.once('error', error => { spawnError = error; });
103
+ child.once('close', (code, exitSignal) => {
104
+ const out = Buffer.concat(stdout).toString(); const err = Buffer.concat(stderr).toString();
105
+ const error = spawnError ?? (code === 0 ? undefined : Object.assign(
106
+ new Error(`Draft process failed (${exitSignal ?? code}).${err ? `\n${err}` : ''}`),
107
+ { code, signal: exitSignal, stdout: out, stderr: err },
108
+ ));
109
+ resolve({ error, stdout: out, stderr: err });
110
+ });
111
+ // Successful or failed leaders may leave descendants behind as well.
112
+ child.once('exit', terminate);
113
+ });
114
+ signal?.addEventListener('abort', onAbort, { once: true });
115
+ if (signal?.aborted) onAbort();
116
+ if (timeout > 0) {
117
+ timeoutHandle = setTimeout(() => cancel(new DOMException('Draft execution timed out.', 'TimeoutError')), timeout);
118
+ }
119
+ try {
120
+ const result = await closed;
121
+ await terminate();
122
+ if (cancelled) throw cancellation;
123
+ if (result.error) throw result.error;
124
+ return { stdout: result.stdout, stderr: result.stderr };
125
+ } finally {
126
+ clearTimeout(timeoutHandle);
127
+ signal?.removeEventListener('abort', onAbort);
128
+ }
129
+ }
@@ -0,0 +1,67 @@
1
+ import { readFile, realpath, stat } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ export const WORKSPACE_DRAFT_TEMP_PREFIX = 'pygmalion-draft-preview-';
7
+ export const WORKSPACE_DRAFT_OUTPUT_DIRECTORY = 'output';
8
+
9
+ function relative(value) {
10
+ return typeof value === 'string' && value.length > 0 && !path.isAbsolute(value)
11
+ && !/^[a-z]:/i.test(value) && !/[\\\x00-\x1f\x7f?#]/.test(value)
12
+ && value.split('/').every(part => part && part !== '.' && part !== '..' && part !== '.git');
13
+ }
14
+
15
+ /** Own CLI cancellation listeners without exiting ahead of an active child process. */
16
+ export function createWorkspaceDraftLifecycle() {
17
+ const controller = new AbortController();
18
+ const listeners = new Map();
19
+ for (const [name, exitCode] of [['SIGTERM', 143], ['SIGINT', 130]]) {
20
+ const listener = () => {
21
+ if (controller.signal.aborted) return;
22
+ process.exitCode = exitCode;
23
+ controller.abort(new DOMException('Draft execution cancelled.', 'AbortError'));
24
+ };
25
+ listeners.set(name, listener);
26
+ process.on(name, listener);
27
+ }
28
+ return Object.freeze({
29
+ signal: controller.signal,
30
+ dispose() { for (const [name, listener] of listeners) process.removeListener(name, listener); listeners.clear(); },
31
+ });
32
+ }
33
+
34
+ /** Verify that source and output belong to one SDK-created temporary layout. */
35
+ export async function assertWorkspaceDraftDirectories({ appRoot, outputDirectory, appDirectory = '.' }) {
36
+ if (appDirectory !== '.' && !relative(appDirectory)) throw new TypeError('The draft application directory must be project-relative.');
37
+ const isolatedRoot = await realpath(appRoot);
38
+ const outputRoot = await realpath(outputDirectory);
39
+ const temporaryRoot = path.dirname(outputRoot);
40
+ const systemTemporary = await realpath(os.tmpdir());
41
+ if (path.dirname(temporaryRoot) !== systemTemporary || !path.basename(temporaryRoot).startsWith(WORKSPACE_DRAFT_TEMP_PREFIX)
42
+ || isolatedRoot !== path.join(temporaryRoot, 'source', appDirectory)
43
+ || path.basename(outputRoot) !== WORKSPACE_DRAFT_OUTPUT_DIRECTORY
44
+ || !(await stat(isolatedRoot)).isDirectory() || !(await stat(outputRoot)).isDirectory()) {
45
+ throw new Error('Draft compilation requires source and output directories from one isolated SDK preview.');
46
+ }
47
+ return Object.freeze({ appRoot: isolatedRoot, outputDirectory: outputRoot, temporaryRoot });
48
+ }
49
+
50
+ /** Import the explicit root ESM entry from this install, without falling back to the caller's dependencies. */
51
+ export async function importIsolatedPackage({ appRoot, name }) {
52
+ if (typeof name !== 'string' || !/^(?:@[a-zA-Z0-9][\w.-]*\/)?[a-zA-Z0-9][\w.-]*$/.test(name)) {
53
+ throw new TypeError('An isolated package requires a complete package name.');
54
+ }
55
+ const directory = await realpath(path.join(appRoot, 'node_modules', name));
56
+ const metadata = JSON.parse(await readFile(path.join(directory, 'package.json'), 'utf8'));
57
+ const entry = metadata.exports?.['.']?.import;
58
+ if (typeof entry !== 'string' || !entry.startsWith('./') || !relative(entry.slice(2))) {
59
+ throw new Error(`The isolated package must declare a relative root import export: ${name}`);
60
+ }
61
+ const resolved = await realpath(path.join(directory, entry));
62
+ const local = path.relative(directory, resolved);
63
+ if (!relative(local) || !(await stat(resolved)).isFile()) {
64
+ throw new Error(`The isolated package entry must remain inside its package directory: ${name}`);
65
+ }
66
+ return import(pathToFileURL(resolved).href);
67
+ }
@@ -0,0 +1,187 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { realpath } from 'node:fs/promises';
3
+ import { workspaceSourceFingerprint as fingerprint } from './workspace-source-identity.mjs';
4
+ import { applySourceFilePlan, previewSourceFilePlan } from './source-file-plan.mjs';
5
+ import { createWorkspaceSourcePlanService } from './workspace-source-plan.mjs';
6
+ import { readSourceInputs } from './source-service-inputs.mjs';
7
+
8
+ const hash = value => createHash('sha256').update(value).digest('hex');
9
+ const same = (left, right) => fingerprint(left) === fingerprint(right);
10
+ function capture(value) {
11
+ const freeze = item => { if (item && typeof item === 'object') { Object.values(item).forEach(freeze); Object.freeze(item); } return item; };
12
+ return freeze(JSON.parse(fingerprint(value)));
13
+ }
14
+ const baselineKey = (target, baseline) => fingerprint([target, fingerprint(baseline.document), fingerprint(baseline.mapping)]);
15
+
16
+ /** Retain server proofs across external edits while keeping projection and compilation host-defined. */
17
+ export function createWorkspaceSourceSession({
18
+ appRoot, allowedFiles, overlayFiles = allowedFiles, optionalFiles = [], inputFiles,
19
+ loadCodec, assertTarget: validateTarget, archive, currentRevision, projectBaseline,
20
+ compilePlan, previewPlan = previewSourceFilePlan, applyPlan = applySourceFilePlan,
21
+ reconcile, projectReconcileInputs, applyMessage = 'The reviewed source changes were applied.',
22
+ }) {
23
+ if (![inputFiles, loadCodec, validateTarget, archive, currentRevision, projectBaseline, compilePlan, reconcile, projectReconcileInputs].every(callback => typeof callback === 'function')) {
24
+ throw new Error('A source session requires explicit projection, compilation, reconciliation, revision, and archive callbacks.');
25
+ }
26
+ const readInputs = (root, files, signal) => readSourceInputs(root, files, { optionalFiles, signal });
27
+ const baselines = new Map();
28
+ const working = new Map();
29
+ const operations = new Set();
30
+ let disposed = false; let busy; let archiveService;
31
+ const check = signal => { signal.throwIfAborted(); if (disposed) throw new Error('The source service has been disposed.'); };
32
+ const retain = (map, key, value) => {
33
+ map.delete(key); map.set(key, { ...value, createdAt: Date.now() });
34
+ while (map.size > 8) { const oldest = map.keys().next().value; map.get(oldest).service?.dispose(); map.delete(oldest); }
35
+ };
36
+ const lookup = (map, key) => {
37
+ const value = map.get(key);
38
+ const now = Date.now();
39
+ if (value && (now < value.createdAt || now - value.createdAt >= 30 * 60_000)) { value.service?.dispose(); map.delete(key); return undefined; }
40
+ return value;
41
+ };
42
+ const run = async (signal, operation) => {
43
+ if (disposed) throw new Error('The source service has been disposed.');
44
+ signal?.throwIfAborted();
45
+
46
+ while (busy?.controller.signal.aborted) {
47
+ await busy.settled; signal?.throwIfAborted();
48
+ if (disposed) throw new Error('The source service has been disposed.');
49
+ }
50
+ if (busy) throw new Error('Another source review is in progress.');
51
+ const controller = new AbortController();
52
+ let finish;
53
+ const settled = new Promise(resolve => { finish = resolve; });
54
+ const slot = { controller, settled }; busy = slot; operations.add(controller);
55
+ const abort = () => controller.abort(signal.reason);
56
+ signal?.addEventListener('abort', abort, { once: true });
57
+ try { const result = await operation(controller.signal); check(controller.signal); return result; }
58
+ finally { signal?.removeEventListener('abort', abort); operations.delete(controller); if (busy === slot) busy = undefined; finish(); }
59
+ };
60
+ const assertTarget = async (target, signal) => {
61
+ check(signal);
62
+ if (!target || typeof target.screenId !== 'string' || !/^[a-f0-9]{40}$/.test(target.sourceRevision ?? '')) throw new Error('An exact source target is required.');
63
+ await validateTarget(target, signal); check(signal);
64
+ };
65
+ const assertHead = async (target, signal) => {
66
+ check(signal);
67
+ if (await currentRevision() !== target.sourceRevision) throw new Error('The working source revision differs from the selected revision.');
68
+ check(signal);
69
+ };
70
+ const fromInputs = async (codec, target, sourceInputs, projectedStructure, signal) => {
71
+ const result = await projectBaseline({ codec, target, sourceInputs, projectedStructure }, signal);
72
+ check(signal); return capture(result);
73
+ };
74
+ const currentInputs = async (context, signal) => {
75
+ await assertHead(context.target, signal);
76
+ const actual = await readInputs(appRoot, Object.keys(context.inputs), signal);
77
+ await assertHead(context.target, signal);
78
+ if (!same(actual, context.inputs)) throw new Error('Working inputs changed after reconciliation. Review the source again.');
79
+ };
80
+
81
+ const overlay = async (context, root, signal) => {
82
+ if (await realpath(root) === await realpath(appRoot)) throw new Error('Reconciled inputs require an isolated source root.');
83
+ const original = await readInputs(root, Object.keys(context.archiveInputs), signal);
84
+ if (!same(original, context.archiveInputs)) throw new Error('The isolated archive differs from the retained source proof.');
85
+ const changes = Object.keys(original).filter(file => original[file] !== context.inputs[file]).map(file => {
86
+ if (context.inputs[file] === null) throw new Error('Reconciliation cannot delete a source input.');
87
+ return { file, beforeSha256: original[file] === null ? null : hash(original[file]), content: context.inputs[file] };
88
+ });
89
+ if (changes.length) await applySourceFilePlan(root, { version: 1, sourceRevision: context.target.sourceRevision,
90
+ proofs: Object.entries(original).map(([file, source]) => ({ file, sha256: source === null ? null : hash(source) })), changes,
91
+ }, { sourceRevision: context.target.sourceRevision, allowedFiles: overlayFiles, signal });
92
+ check(signal);
93
+ if (!same(await readInputs(root, Object.keys(context.inputs), signal), context.inputs)) throw new Error('The isolated inputs differ from the reconciled baseline.');
94
+ };
95
+ const isolated = (target, context, signal, operation) => archive(target.sourceRevision, signal, async ({ appRoot: root }) => {
96
+ if (context) await overlay(context, root, signal);
97
+ check(signal); return operation(root);
98
+ });
99
+ const clearOtherReviews = active => {
100
+ if (active !== archiveService) { archiveService.dispose(); archiveService = makeService(); }
101
+ for (const [key, context] of working) if (context.service !== active) { context.service.dispose(); working.delete(key); }
102
+ };
103
+ function makeService(context) {
104
+ const service = createWorkspaceSourcePlanService({
105
+ async assertTarget(target, signal) { await assertTarget(target, signal); if (context) await currentInputs(context, signal); },
106
+ async loadBaseline(target, signal) {
107
+ if (context) return context.baseline;
108
+ const codec = await loadCodec(); check(signal);
109
+ return isolated(target, null, signal, async root => {
110
+ const inputs = capture(await readInputs(root, inputFiles(codec), signal));
111
+ const baseline = await fromInputs(codec, target, inputs, undefined, signal);
112
+ retain(baselines, baselineKey(target, baseline), { target: capture(target), baseline, inputs, codec });
113
+ return baseline;
114
+ });
115
+ },
116
+ compilePlan({ target, baseline, document }, signal) {
117
+ return isolated(target, context, signal, root => compilePlan({ appRoot: root, sourceRevision: target.sourceRevision,
118
+ before: baseline.document, after: document, mapping: baseline.mapping, signal, ...(context ? { reconciledIdentity: context.identity } : {}) }));
119
+ },
120
+ previewPlan(plan, target, signal) {
121
+ return isolated(target, context, signal, root => previewPlan(root, plan, { sourceRevision: target.sourceRevision, allowedFiles, signal }));
122
+ },
123
+ async applyPlan(plan, target, signal) {
124
+ await assertHead(target, signal); clearOtherReviews(service);
125
+ const result = await applyPlan(appRoot, plan, { sourceRevision: target.sourceRevision, allowedFiles, signal });
126
+ return { applied: result.applied, message: applyMessage };
127
+ },
128
+ });
129
+ return service;
130
+ }
131
+ archiveService = makeService();
132
+ const forBaseline = input => lookup(working, baselineKey(input.target, input.baseline));
133
+ const forRequest = request => lookup(working, fingerprint([request.target, request.identity?.beforeFingerprint ?? '', request.identity?.mappingFingerprint ?? '']));
134
+ return Object.freeze({
135
+ load(input, signal) { const target = capture(input); return run(signal, current => archiveService.load(target, current)); },
136
+ compile(input, signal) { const request = capture(input); return run(signal, current => (forBaseline(request)?.service ?? archiveService).compile(request, current)); },
137
+ preview(input, signal) { const request = capture(input); return run(signal, current => (forRequest(request)?.service ?? archiveService).preview(request, current)); },
138
+ apply(input, signal) { const request = capture(input); return run(signal, current => (forRequest(request)?.service ?? archiveService).apply(request, current)); },
139
+ validatePreviewRequest(input, signal) { const request = capture(input); return run(signal, current => (forRequest(request)?.service ?? archiveService).validatePreviewRequest(request, current)); },
140
+ preparePreviewSource(input, root, signal) {
141
+ const request = capture(input);
142
+ return run(signal, async current => {
143
+ const context = forRequest(request);
144
+ await (context?.service ?? archiveService).validatePreviewRequest(request, current);
145
+ if (context) await overlay(context, root, current);
146
+ });
147
+ },
148
+ reconcile(input, signal) {
149
+ if (!input || typeof input !== 'object' || Object.keys(input).some(key => !['target', 'baseline', 'document'].includes(key))) {
150
+ throw new Error('Reconciliation accepts only a target, baseline, and document.');
151
+ }
152
+ const request = capture(input);
153
+ return run(signal, async current => {
154
+ await assertTarget(request.target, current); await assertHead(request.target, current);
155
+ const key = baselineKey(request.target, request.baseline);
156
+ const origin = lookup(baselines, key) ?? lookup(working, key);
157
+ if (!origin) throw new Error('The retained source baseline is unavailable. Load it again.');
158
+ const externalInputs = capture(await readInputs(appRoot, Object.keys(origin.inputs), current));
159
+ await assertHead(request.target, current);
160
+ const loadBaseline = ({ target, sourceInputs, projectedStructure }, operationSignal) => fromInputs(origin.codec, target, sourceInputs, projectedStructure, operationSignal);
161
+ const result = capture(await reconcile({ ...request, baselineInputs: origin.inputs, externalInputs, loadBaseline,
162
+ ...(origin.identity ? { baselineIdentity: origin.identity } : {}) }, current));
163
+ check(current);
164
+ if (result.status !== 'ready') return result;
165
+ const projected = projectReconcileInputs({ baselineInputs: origin.inputs, externalInputs,
166
+ ...(origin.identity ? { baselineIdentity: origin.identity } : {}) });
167
+ const baseline = await fromInputs(origin.codec, request.target, externalInputs, projected.projectedStructure, current);
168
+ if (!same(result.baseline, baseline) || !same(result.document, request.document)) throw new Error('The reconciled result differs from the verified baseline or complete document.');
169
+ const nextKey = baselineKey(request.target, baseline);
170
+ if (nextKey !== key) {
171
+ const context = { target: request.target, baseline, inputs: externalInputs, archiveInputs: origin.archiveInputs ?? origin.inputs, codec: origin.codec, identity: projected.identity };
172
+ await currentInputs(context, current);
173
+ context.service = makeService(context);
174
+ lookup(working, nextKey)?.service.dispose(); retain(working, nextKey, context);
175
+ }
176
+ return result;
177
+ });
178
+ },
179
+ dispose() {
180
+ if (disposed) return;
181
+ disposed = true;
182
+ for (const controller of operations) controller.abort(new Error('The source service has been disposed.'));
183
+ archiveService.dispose(); for (const context of working.values()) context.service.dispose();
184
+ working.clear(); baselines.clear();
185
+ },
186
+ });
187
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pygmalionjs/pygmalion",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Code-backed DOM design sandbox and visual QA editor",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -72,6 +72,26 @@
72
72
  "./source-responsive": {
73
73
  "types": "./source-responsive.d.ts",
74
74
  "default": "./node/source-responsive.mjs"
75
+ },
76
+ "./host-runtime": {
77
+ "types": "./host-runtime.d.ts",
78
+ "default": "./node/host-runtime.mjs"
79
+ },
80
+ "./source-service": {
81
+ "types": "./source-service.d.ts",
82
+ "default": "./node/source-service.mjs"
83
+ },
84
+ "./draft-entry": {
85
+ "types": "./workspace-draft-entry.d.ts",
86
+ "default": "./node/workspace-draft-entry.mjs"
87
+ },
88
+ "./source-module": {
89
+ "types": "./source-module.d.ts",
90
+ "default": "./node/source-module.mjs"
91
+ },
92
+ "./source-reconciler": {
93
+ "types": "./jsx-source-reconciler.d.ts",
94
+ "default": "./node/jsx-source-reconciler.mjs"
75
95
  }
76
96
  },
77
97
  "files": [
@@ -138,7 +158,25 @@
138
158
  "node/source-responsive.mjs",
139
159
  "source-responsive.d.ts",
140
160
  "node/source-responsive-codec.mjs",
141
- "node/source-responsive-codec.d.mts"
161
+ "node/source-responsive-codec.d.mts",
162
+ "host-runtime.d.ts",
163
+ "node/host-runtime.mjs",
164
+ "source-service.d.ts",
165
+ "node/source-service.mjs",
166
+ "workspace-draft-entry.d.ts",
167
+ "node/workspace-draft-entry.mjs",
168
+ "source-module.d.ts",
169
+ "node/source-module.mjs",
170
+ "node/workspace-draft-process.mjs",
171
+ "node/revision-catalog-resolver.mjs",
172
+ "node/source-archive-runtime.mjs",
173
+ "node/source-service-inputs.mjs",
174
+ "node/source-service-plugin.mjs",
175
+ "node/workspace-source-session.mjs",
176
+ "node/token-source-service.mjs",
177
+ "jsx-source-reconciler.d.ts",
178
+ "node/jsx-source-reconciler.mjs",
179
+ "node/workspace-draft-runtime.mjs"
142
180
  ],
143
181
  "scripts": {
144
182
  "dev": "vite",
@@ -0,0 +1,16 @@
1
+ export interface SourceModuleBuildResult {
2
+ outputFiles?: readonly { text: string }[];
3
+ }
4
+
5
+ export interface BundledSourceModuleOptions {
6
+ /** A trusted bundler supplied by the consuming build tool. */
7
+ build(options: Record<string, unknown>): Promise<SourceModuleBuildResult>;
8
+ /** Application aliases, plugins, dependency paths, loaders, and entry declaration. */
9
+ buildOptions: Record<string, unknown>;
10
+ /** Absolute logical filename used by CommonJS resolution; no output file is written. */
11
+ filename: string;
12
+ require?: (id: string) => unknown;
13
+ }
14
+
15
+ /** Executes trusted source code in the current process. This is not an execution sandbox. */
16
+ export declare function evaluateBundledSourceModule<T = unknown>(options: BundledSourceModuleOptions): Promise<T>;
@@ -0,0 +1,92 @@
1
+ import type { Plugin, ViteDevServer } from 'vite';
2
+ import type { DesignDocument } from './dist-lib/types/document/contracts.js';
3
+ import type { SourceFilePlan } from './dist-lib/types/host/sourceFilePlan.js';
4
+ import type { InspectApplyResult, InspectPreviewResult } from './dist-lib/types/editor/host.js';
5
+ import type { WorkspaceTarget } from './dist-lib/types/workspace/contracts.js';
6
+ import type { WorkspaceFileSourceRequest, WorkspaceSourceBaseline, WorkspaceSourceReconcileInput, WorkspaceSourceReconcileResult } from './dist-lib/types/workspace/source/flowContracts.js';
7
+ import type { WorkspaceTokenLibraryBaseline, WorkspaceTokenSourceRequest } from './dist-lib/types/workspace/tokens/contracts.js';
8
+ import type { TokenLibraryDocument } from './dist-lib/types/token-library/contracts.js';
9
+ import type { SourceFilePlanOptions } from './source-files.js';
10
+ import type { WorkspaceSourcePlanService } from './workspace-source-plan.js';
11
+
12
+ export type SourceInputs = Readonly<Record<string, string | null>>;
13
+ export type SourceArchive = <T>(sourceRevision: string, signal: AbortSignal,
14
+ operation: (context: { appRoot: string; sourceRevision?: string; signal?: AbortSignal }) => Promise<T>) => Promise<T>;
15
+ export interface SourceInputOptions { optionalFiles?: readonly string[]; signal?: AbortSignal; maxBytes?: number }
16
+ export declare function assertSourceInputParent(root: string, file: string): Promise<void>;
17
+ export declare function readSourceInputs(appRoot: string, files: readonly string[], options?: SourceInputOptions): Promise<Record<string, string | null>>;
18
+ export declare function readProvenSourceFiles(appRoot: string, proofs: SourceFilePlan['proofs'], options?: SourceInputOptions): Promise<Map<string, { file: string; sha256: string | null; source: string | null }>>;
19
+ export declare function withSourceFiles<T>(options: { files: Map<string, string>; signal?: AbortSignal }, operation: (root: string) => Promise<T>): Promise<T>;
20
+
21
+ interface SourceWriters {
22
+ previewPlan?(root: string, plan: SourceFilePlan, options: SourceFilePlanOptions): Promise<Pick<InspectPreviewResult, 'files' | 'affectedFiles'>>;
23
+ applyPlan?(root: string, plan: SourceFilePlan, options: SourceFilePlanOptions): Promise<{ applied: number }>;
24
+ }
25
+ export interface WorkspaceSourceSessionOptions<Codec, Projection = unknown, Identity = unknown> extends SourceWriters {
26
+ appRoot: string;
27
+ allowedFiles: readonly string[];
28
+ overlayFiles?: readonly string[];
29
+ optionalFiles?: readonly string[];
30
+ inputFiles(codec: Codec): readonly string[];
31
+ loadCodec(): Promise<Codec>;
32
+ assertTarget(target: WorkspaceTarget, signal: AbortSignal): void | Promise<void>;
33
+ archive: SourceArchive;
34
+ currentRevision(): Promise<string>;
35
+ projectBaseline(input: { codec: Codec; target: WorkspaceTarget; sourceInputs: SourceInputs; projectedStructure?: Projection }, signal: AbortSignal): Promise<WorkspaceSourceBaseline>;
36
+ compilePlan(input: { appRoot: string; sourceRevision: string; before: DesignDocument; after: DesignDocument; mapping: WorkspaceSourceBaseline['mapping']; signal: AbortSignal; reconciledIdentity?: Identity }): Promise<SourceFilePlan>;
37
+ reconcile(input: WorkspaceSourceReconcileInput & {
38
+ baselineInputs: SourceInputs; externalInputs: SourceInputs; baselineIdentity?: Identity;
39
+ loadBaseline(input: { target: WorkspaceTarget; sourceInputs: SourceInputs; projectedStructure?: Projection }, signal: AbortSignal): Promise<WorkspaceSourceBaseline>;
40
+ }, signal: AbortSignal): Promise<WorkspaceSourceReconcileResult>;
41
+ projectReconcileInputs(input: { baselineInputs: SourceInputs; externalInputs: SourceInputs; baselineIdentity?: Identity }): { projectedStructure: Projection; identity: Identity };
42
+ applyMessage?: string;
43
+ }
44
+ export interface WorkspaceSourceSession extends WorkspaceSourcePlanService {
45
+ reconcile(input: WorkspaceSourceReconcileInput, signal?: AbortSignal): Promise<WorkspaceSourceReconcileResult>;
46
+ /** Validates the retained review before overlaying reconciled bytes into an isolated root. */
47
+ preparePreviewSource(input: WorkspaceFileSourceRequest, root: string, signal?: AbortSignal): Promise<void>;
48
+ }
49
+ export declare function createWorkspaceSourceSession<Codec, Projection = unknown, Identity = unknown>(options: WorkspaceSourceSessionOptions<Codec, Projection, Identity>): WorkspaceSourceSession;
50
+
51
+ export interface LoadedTokenSource<SourceFiles = unknown, Ledger = unknown> extends WorkspaceTokenLibraryBaseline {
52
+ sourceFiles: SourceFiles;
53
+ ledger: Ledger;
54
+ consumerFiles: readonly string[];
55
+ }
56
+ export interface TokenSourceServiceOptions<Codec, SourceFiles = unknown, Ledger = unknown> extends SourceWriters {
57
+ appRoot: string;
58
+ loadCodec(): Promise<Codec>;
59
+ assertRevision(sourceRevision: string, signal?: AbortSignal): void | Promise<void>;
60
+ assertTarget(target: WorkspaceTarget, signal: AbortSignal): void | Promise<void>;
61
+ archive: SourceArchive;
62
+ loadSource(input: { appRoot: string; sourceRevision: string; codec: Codec; signal: AbortSignal }): Promise<LoadedTokenSource<SourceFiles, Ledger>>;
63
+ compilePlan(input: { appRoot: string; sourceRevision: string; before: TokenLibraryDocument; after: TokenLibraryDocument; codec: Codec; signal: AbortSignal; dependenciesReady: true }): Promise<SourceFilePlan>;
64
+ listConsumers(appRoot: string, signal: AbortSignal): Promise<readonly string[]>;
65
+ currentRevision(): Promise<string>;
66
+ regenerate(signal: AbortSignal): Promise<unknown>;
67
+ install(root: string, signal: AbortSignal): Promise<unknown>;
68
+ applyMessage?: string;
69
+ regenerationFailureMessage?(error: unknown): string;
70
+ }
71
+ export interface TokenSourceService<SourceFiles = unknown, Ledger = unknown> {
72
+ load(sourceRevision: string, signal?: AbortSignal): Promise<WorkspaceTokenLibraryBaseline & { sourceFiles: SourceFiles; ledger: Ledger }>;
73
+ compile(input: { baseline: WorkspaceTokenLibraryBaseline; document: TokenLibraryDocument }, signal?: AbortSignal): Promise<SourceFilePlan>;
74
+ preview(request: WorkspaceTokenSourceRequest, signal?: AbortSignal): Promise<InspectPreviewResult>;
75
+ validatePreviewRequest(request: WorkspaceTokenSourceRequest, signal?: AbortSignal): Promise<WorkspaceTokenSourceRequest>;
76
+ apply(request: WorkspaceTokenSourceRequest, signal?: AbortSignal): Promise<InspectApplyResult>;
77
+ dispose(): void;
78
+ }
79
+ export declare function createTokenSourceService<Codec, SourceFiles = unknown, Ledger = unknown>(options: TokenSourceServiceOptions<Codec, SourceFiles, Ledger>): TokenSourceService<SourceFiles, Ledger>;
80
+
81
+ export declare function sourceServicePlugin<Service extends { dispose(): void | Promise<void> }>(options: {
82
+ name?: string;
83
+ endpoint: string;
84
+ operations: readonly (keyof Service & string)[];
85
+ createService(server: ViteDevServer): Service;
86
+ transformInput?(operation: keyof Service & string, input: unknown): unknown;
87
+ maxBytes?: number;
88
+ }): {
89
+ plugin: Plugin;
90
+ validatePreviewRequest: Service extends { validatePreviewRequest: infer Method } ? Method : never;
91
+ preparePreviewSource: Service extends { preparePreviewSource: infer Method } ? Method : never;
92
+ };
@@ -0,0 +1,15 @@
1
+ import type { WorkspaceDraftRecipe } from './dist-lib/types/workspace/source/draftRecipe.js';
2
+
3
+ /** Trusted build-time declarations for the mounted application's browser entry. */
4
+ export interface WorkspaceDraftEntryOptions {
5
+ documentRevision: string;
6
+ recipe: WorkspaceDraftRecipe;
7
+ /** Module resolving the SDK recipe runner. Defaults to the public package. */
8
+ sdkModule?: string;
9
+ /** An application module exporting its desired-state adapter. */
10
+ desiredStateAdapter?: { module: string; exportName: string };
11
+ failureMessage?: string;
12
+ }
13
+
14
+ /** Returns ESM source for a virtual build entry; the application supplies its own bundler and boot setup. */
15
+ export declare function createWorkspaceDraftEntry(options: WorkspaceDraftEntryOptions): string;