@pygmalionjs/pygmalion 0.8.1 → 0.10.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/README.md +34 -0
- package/binding-source.d.ts +25 -0
- package/dist-lib/{CameraLayer-B2D29VXQ.js → CameraLayer-BUOlJcUa.js} +17 -17
- package/dist-lib/pygmalion.js +24524 -12555
- package/dist-lib/{runtime-BAy8W1mJ.js → runtime-BOfDlGPR.js} +12438 -11118
- package/dist-lib/style.css +1 -1
- package/dist-lib/testing.js +19 -19
- package/dist-lib/types/binding/catalog.d.ts +10 -0
- package/dist-lib/types/binding/contracts.d.ts +202 -0
- package/dist-lib/types/binding/documentCommands.d.ts +38 -0
- package/dist-lib/types/binding/duplicateMetadata.d.ts +8 -0
- package/dist-lib/types/binding/index.d.ts +7 -0
- package/dist-lib/types/binding/preview.d.ts +37 -0
- package/dist-lib/types/binding/source.d.ts +109 -0
- package/dist-lib/types/binding/useDataProjection.d.ts +9 -0
- package/dist-lib/types/binding/validate.d.ts +8 -0
- package/dist-lib/types/core/runtime.d.ts +6 -0
- package/dist-lib/types/document/appearance.d.ts +112 -0
- package/dist-lib/types/document/assets.d.ts +204 -0
- package/dist-lib/types/document/components.d.ts +169 -0
- package/dist-lib/types/document/contracts.d.ts +171 -0
- package/dist-lib/types/document/engine.d.ts +20 -0
- package/dist-lib/types/document/exportSettings.d.ts +45 -0
- package/dist-lib/types/document/geometry.d.ts +11 -0
- package/dist-lib/types/document/layout.d.ts +53 -0
- package/dist-lib/types/document/prototype.d.ts +424 -0
- package/dist-lib/types/document/prototypeReferences.d.ts +3 -0
- package/dist-lib/types/document/richText.d.ts +185 -0
- package/dist-lib/types/document/styles.d.ts +216 -0
- package/dist-lib/types/document/svgImport.d.ts +17 -0
- package/dist-lib/types/document/vector.d.ts +222 -0
- package/dist-lib/types/editor/designCompiler.d.ts +7 -0
- package/dist-lib/types/editor/designImport.d.ts +7 -0
- package/dist-lib/types/editor/inspect.d.ts +8 -0
- package/dist-lib/types/editor/revisionCatalogInstall.d.ts +1 -1
- package/dist-lib/types/editor/screenInteractions.d.ts +3 -3
- package/dist-lib/types/editor/store.d.ts +1 -1
- package/dist-lib/types/host/project.d.ts +44 -0
- package/dist-lib/types/host/registryConnections.d.ts +11 -0
- package/dist-lib/types/host/sourceFilePlan.d.ts +15 -0
- package/dist-lib/types/host/sourceWriteback.d.ts +50 -0
- package/dist-lib/types/lib.d.ts +103 -3
- package/dist-lib/types/token-library/bindings.d.ts +62 -0
- package/dist-lib/types/token-library/contracts.d.ts +117 -0
- package/dist-lib/types/token-library/engine.d.ts +13 -0
- package/dist-lib/types/token-library/valueResolver.d.ts +19 -0
- package/dist-lib/types/workspace/WorkspaceShell.d.ts +9 -0
- package/dist-lib/types/workspace/application/ApplicationWorkspace.d.ts +3 -0
- package/dist-lib/types/workspace/configuration.d.ts +47 -0
- package/dist-lib/types/workspace/contracts.d.ts +64 -0
- package/dist-lib/types/workspace/controller.d.ts +35 -0
- package/dist-lib/types/workspace/documentSessions.d.ts +42 -0
- package/dist-lib/types/workspace/edit/AppearancePanel.d.ts +23 -0
- package/dist-lib/types/workspace/edit/ArrangeLayers.d.ts +10 -0
- package/dist-lib/types/workspace/edit/AssetsPanel.d.ts +16 -0
- package/dist-lib/types/workspace/edit/CanvasGuides.d.ts +19 -0
- package/dist-lib/types/workspace/edit/ComponentPanel.d.ts +11 -0
- package/dist-lib/types/workspace/edit/DataPanel.d.ts +16 -0
- package/dist-lib/types/workspace/edit/DesignWorkspace.d.ts +10 -0
- package/dist-lib/types/workspace/edit/LayerTree.d.ts +14 -0
- package/dist-lib/types/workspace/edit/LayoutPanel.d.ts +13 -0
- package/dist-lib/types/workspace/edit/PrototypePanel.d.ts +12 -0
- package/dist-lib/types/workspace/edit/ScopeTransferDialog.d.ts +40 -0
- package/dist-lib/types/workspace/edit/SelectionHandles.d.ts +52 -0
- package/dist-lib/types/workspace/edit/StylesPanel.d.ts +8 -0
- package/dist-lib/types/workspace/edit/TextEditor.d.ts +50 -0
- package/dist-lib/types/workspace/edit/VectorEditor.d.ts +89 -0
- package/dist-lib/types/workspace/edit/VectorToolbar.d.ts +5 -0
- package/dist-lib/types/workspace/edit/canvasGuideModel.d.ts +102 -0
- package/dist-lib/types/workspace/edit/clipboard.d.ts +35 -0
- package/dist-lib/types/workspace/edit/duplicateMetadata.d.ts +2 -0
- package/dist-lib/types/workspace/edit/geometryHandles.d.ts +224 -0
- package/dist-lib/types/workspace/edit/layerArrangement.d.ts +39 -0
- package/dist-lib/types/workspace/edit/layerMoves.d.ts +44 -0
- package/dist-lib/types/workspace/edit/projection.d.ts +12 -0
- package/dist-lib/types/workspace/edit/scopeTransfer.d.ts +96 -0
- package/dist-lib/types/workspace/edit/textStyles.d.ts +75 -0
- package/dist-lib/types/workspace/edit/vectorTools.d.ts +136 -0
- package/dist-lib/types/workspace/legacyModeAdapter.d.ts +13 -0
- package/dist-lib/types/workspace/source/ResponsiveLayoutPanel.d.ts +20 -0
- package/dist-lib/types/workspace/source/SourceReview.d.ts +11 -0
- package/dist-lib/types/workspace/source/appearanceSource.d.ts +19 -0
- package/dist-lib/types/workspace/source/compiler.d.ts +5 -0
- package/dist-lib/types/workspace/source/contracts.d.ts +156 -0
- package/dist-lib/types/workspace/source/controller.d.ts +50 -0
- package/dist-lib/types/workspace/source/documentRevision.d.ts +2 -0
- package/dist-lib/types/workspace/source/draftPreviewClient.d.ts +13 -0
- package/dist-lib/types/workspace/source/draftRecipe.d.ts +30 -0
- package/dist-lib/types/workspace/source/flowContracts.d.ts +72 -0
- package/dist-lib/types/workspace/source/prototypeSource.d.ts +4 -0
- package/dist-lib/types/workspace/source/responsive.d.ts +13 -0
- package/dist-lib/types/workspace/source/responsiveContracts.d.ts +78 -0
- package/dist-lib/types/workspace/source/structure.d.ts +21 -0
- package/dist-lib/types/workspace/source/textSource.d.ts +28 -0
- package/dist-lib/types/workspace/tokens/TokenLibraryContext.d.ts +11 -0
- package/dist-lib/types/workspace/tokens/TokenLibraryWorkspace.d.ts +14 -0
- package/dist-lib/types/workspace/tokens/contracts.d.ts +74 -0
- package/dist-lib/types/workspace/tokens/controller.d.ts +44 -0
- package/dist-lib/types/workspace/view/PrototypePlayer.d.ts +17 -0
- package/dist-lib/types/workspace/view/StoryboardWorkspace.d.ts +3 -0
- package/dist-lib/types/workspace/view/prototypePlayback.d.ts +46 -0
- package/draft-preview.d.ts +54 -0
- package/host-runtime.d.ts +73 -0
- package/inspect.d.ts +2 -0
- package/jsx-source-reconciler.d.ts +35 -0
- package/node/design-session.mjs +3 -0
- package/node/host-runtime.mjs +4 -0
- package/node/inspect-plugin.mjs +3 -1
- package/node/inspect-writeback.mjs +18 -2
- package/node/jsx-source-reconciler.mjs +155 -0
- package/node/preview-artifact-store.mjs +10 -7
- package/node/revision-catalog-resolver.mjs +159 -0
- package/node/source-archive-runtime.mjs +63 -0
- package/node/source-archive.mjs +88 -0
- package/node/source-bindings.mjs +65 -0
- package/node/source-file-lock.mjs +15 -0
- package/node/source-file-plan.mjs +162 -0
- package/node/source-file-proofs.mjs +44 -0
- package/node/source-module.mjs +17 -0
- package/node/source-responsive-codec.d.mts +17 -0
- package/node/source-responsive-codec.mjs +133 -0
- package/node/source-responsive.mjs +189 -0
- package/node/source-service-inputs.mjs +81 -0
- package/node/source-service-plugin.mjs +57 -0
- package/node/source-service.mjs +4 -0
- package/node/source-structure-reconcile.mjs +186 -0
- package/node/source-structure-wrapper-codec.d.mts +20 -0
- package/node/source-structure-wrapper-codec.mjs +205 -0
- package/node/source-structure.mjs +404 -0
- package/node/token-source-references.mjs +136 -0
- package/node/token-source-service.mjs +151 -0
- package/node/workspace-draft-entry.mjs +52 -0
- package/node/workspace-draft-preview-plugin.mjs +70 -0
- package/node/workspace-draft-preview.mjs +232 -0
- package/node/workspace-draft-process.mjs +129 -0
- package/node/workspace-draft-runtime.mjs +67 -0
- package/node/workspace-source-identity.d.mts +1 -0
- package/node/workspace-source-identity.mjs +21 -0
- package/node/workspace-source-plan.mjs +195 -0
- package/node/workspace-source-session.mjs +187 -0
- package/package.json +90 -6
- package/source-files.d.ts +35 -0
- package/source-module.d.ts +16 -0
- package/source-responsive.d.ts +10 -0
- package/source-service.d.ts +92 -0
- package/source-structure.d.ts +155 -0
- package/workspace-draft-entry.d.ts +15 -0
- package/workspace-source-plan.d.ts +44 -0
- package/docs/coverage-contracts.md +0 -444
- package/docs/screen-state-contract.md +0 -252
|
@@ -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 @@
|
|
|
1
|
+
export declare function workspaceSourceFingerprint(value: unknown): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Full JSON identity with sorted object keys; array order and every value are retained. */
|
|
2
|
+
export function workspaceSourceFingerprint(value) {
|
|
3
|
+
const ancestors = new Set();
|
|
4
|
+
const encode = (item) => {
|
|
5
|
+
if (item === null || typeof item === 'string' || typeof item === 'boolean') return JSON.stringify(item);
|
|
6
|
+
if (typeof item === 'number' && Number.isFinite(item)) return Object.is(item, -0) ? '-0' : String(item);
|
|
7
|
+
if (!item || typeof item !== 'object' || ancestors.has(item)
|
|
8
|
+
|| (!Array.isArray(item) && ![Object.prototype, null].includes(Object.getPrototypeOf(item)))) throw new Error('Source fingerprints require complete, finite JSON values.');
|
|
9
|
+
ancestors.add(item);
|
|
10
|
+
try {
|
|
11
|
+
if (Object.getOwnPropertySymbols(item).length) throw new Error('Source fingerprints cannot omit symbol properties.');
|
|
12
|
+
if (Array.isArray(item)) {
|
|
13
|
+
if (Object.keys(item).length !== item.length || Array.from({ length: item.length }, (_, index) => index).some((index) => !Object.hasOwn(item, index))) throw new Error('Source fingerprints require dense JSON arrays.');
|
|
14
|
+
return `[${item.map(encode).join(',')}]`;
|
|
15
|
+
}
|
|
16
|
+
return `{${Object.keys(item).sort().map((key) => `${JSON.stringify(key)}:${encode(item[key])}`).join(',')}}`;
|
|
17
|
+
} finally { ancestors.delete(item); }
|
|
18
|
+
};
|
|
19
|
+
return encode(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { captureSourceFilePlan } from './source-file-plan.mjs';
|
|
3
|
+
import { workspaceSourceFingerprint as fingerprint } from './workspace-source-identity.mjs';
|
|
4
|
+
|
|
5
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
6
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
8
|
+
const same = (left, right) => fingerprint(left) === fingerprint(right);
|
|
9
|
+
function freeze(value) {
|
|
10
|
+
if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value); }
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
const capture = value => freeze(JSON.parse(fingerprint(value)));
|
|
14
|
+
function limit(value, maximum, label) {
|
|
15
|
+
const selected = value ?? maximum;
|
|
16
|
+
if (!Number.isSafeInteger(selected) || selected < 1 || selected > maximum) throw new Error(`${label} must be a positive integer no greater than ${maximum}.`);
|
|
17
|
+
return selected;
|
|
18
|
+
}
|
|
19
|
+
function targetIdentity(target) {
|
|
20
|
+
if (!record(target) || !text(target.screenId) || !text(target.sourceRevision)
|
|
21
|
+
|| Object.keys(target).some(key => !['screenId', 'sourceRevision'].includes(key))) throw new Error('A source target requires an exact screen and source revision.');
|
|
22
|
+
}
|
|
23
|
+
function documentIdentity(document, target) {
|
|
24
|
+
if (!record(document) || document.schemaVersion !== 1 || !text(document.id)
|
|
25
|
+
|| document.sourceRevision !== target.sourceRevision || !text(document.rootId)
|
|
26
|
+
|| !record(document.nodes) || !record(document.nodes[document.rootId]) || !record(document.metadata)
|
|
27
|
+
|| (document.metadata.screenId !== undefined && document.metadata.screenId !== target.screenId)) {
|
|
28
|
+
throw new Error('The complete design document must belong to the selected source target.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function baselineIdentity(baseline, target) {
|
|
32
|
+
if (!record(baseline)) throw new Error('A source baseline requires a document and mapping.');
|
|
33
|
+
documentIdentity(baseline.document, target);
|
|
34
|
+
const mapping = baseline.mapping;
|
|
35
|
+
if (!record(mapping) || mapping.version !== 1 || !same(mapping.target, target)
|
|
36
|
+
|| mapping.documentId !== baseline.document.id || mapping.baselineFingerprint !== fingerprint(baseline.document)
|
|
37
|
+
|| !Array.isArray(mapping.rules) || !Array.isArray(mapping.files) || !mapping.files.length || mapping.files.length > 3000) {
|
|
38
|
+
throw new Error('The complete source mapping must identify its baseline, target, and file proofs.');
|
|
39
|
+
}
|
|
40
|
+
const files = new Set();
|
|
41
|
+
for (const proof of mapping.files) {
|
|
42
|
+
if (!record(proof) || !text(proof.file) || /[\\\x00-\x1f\x7f?#]/.test(proof.file)
|
|
43
|
+
|| proof.file.startsWith('/') || /^[a-z]:/i.test(proof.file)
|
|
44
|
+
|| proof.file.split('/').some(part => !part || ['.', '..', '.git'].includes(part))
|
|
45
|
+
|| (proof.sha256 !== null && (typeof proof.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(proof.sha256))) || files.has(proof.file)) {
|
|
46
|
+
throw new Error('Source mapping proofs require unique relative files and exact SHA-256 hashes or explicit absence.');
|
|
47
|
+
}
|
|
48
|
+
files.add(proof.file);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function requestIdentity(target, baseline, document, plan) {
|
|
52
|
+
const parts = { version: 1, target, documentId: document.id, beforeFingerprint: fingerprint(baseline.document),
|
|
53
|
+
afterFingerprint: fingerprint(document), mappingFingerprint: fingerprint(baseline.mapping), payloadFingerprint: fingerprint(plan) };
|
|
54
|
+
return { ...parts, fingerprint: fingerprint(parts) };
|
|
55
|
+
}
|
|
56
|
+
function completePreview(result, plan) {
|
|
57
|
+
const files = new Set(plan.changes.map(change => change.file));
|
|
58
|
+
const complete = entries => Array.isArray(entries) && entries.length === files.size
|
|
59
|
+
&& new Set(entries).size === files.size && entries.every(file => files.has(file));
|
|
60
|
+
if (!record(result) || !Array.isArray(result.files) || !complete(result.files.map(entry => entry?.file))
|
|
61
|
+
|| !complete(result.affectedFiles) || result.files.some(entry => !text(entry.diff))) {
|
|
62
|
+
throw new Error('Review must include each changed file exactly once with a nonempty diff and affected-file entry.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Authorize only plans compiled from a fresh host baseline and completely reviewed for one target.
|
|
68
|
+
* This service performs no source I/O. Host callbacks own compilation semantics and current file proofs.
|
|
69
|
+
*/
|
|
70
|
+
export function createWorkspaceSourcePlanService(options) {
|
|
71
|
+
const callbacks = Object.fromEntries(['loadBaseline', 'compilePlan', 'previewPlan', 'applyPlan', 'assertTarget'].map(name => [name, options?.[name]]));
|
|
72
|
+
if (Object.values(callbacks).some(callback => typeof callback !== 'function')) throw new Error('A source plan service requires baseline, compiler, preview, apply, and target callbacks.');
|
|
73
|
+
const ttl = limit(options.receiptTtlMs, 30 * 60_000, 'Receipt lifetime');
|
|
74
|
+
const capacity = limit(options.maxReceipts, 8, 'Receipt capacity');
|
|
75
|
+
const now = options.now ?? Date.now;
|
|
76
|
+
if (typeof now !== 'function') throw new Error('The source receipt clock must be callable.');
|
|
77
|
+
const clock = () => { const value = now(); if (!Number.isFinite(value)) throw new Error('The source receipt clock must return a finite timestamp.'); return value; };
|
|
78
|
+
const receipts = new Map(); const operations = new Set();
|
|
79
|
+
let disposed = false; let busy = null; let revision = 0;
|
|
80
|
+
const keyOf = (target, document) => hash(fingerprint({ target, document }));
|
|
81
|
+
const expired = (receipt, time) => time < receipt.createdAt || time - receipt.createdAt >= ttl;
|
|
82
|
+
|
|
83
|
+
const run = async (exclusive, externalSignal, operation) => {
|
|
84
|
+
if (disposed) throw new Error('The source plan service has been disposed.');
|
|
85
|
+
if (exclusive && busy) throw new Error('Another source plan operation is in progress.');
|
|
86
|
+
externalSignal?.throwIfAborted();
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const current = { controller };
|
|
89
|
+
const failureCleanups = [];
|
|
90
|
+
const abort = () => controller.abort(externalSignal.reason);
|
|
91
|
+
const check = () => {
|
|
92
|
+
controller.signal.throwIfAborted();
|
|
93
|
+
if (disposed) throw new Error('The source plan service has been disposed.');
|
|
94
|
+
};
|
|
95
|
+
operations.add(current); if (exclusive) busy = current;
|
|
96
|
+
try {
|
|
97
|
+
externalSignal?.addEventListener('abort', abort, { once: true });
|
|
98
|
+
const result = await operation({ signal: controller.signal, check, onFailure: cleanup => failureCleanups.push(cleanup) });
|
|
99
|
+
check(); return result;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
for (const cleanup of failureCleanups) cleanup();
|
|
102
|
+
throw error;
|
|
103
|
+
} finally {
|
|
104
|
+
externalSignal?.removeEventListener('abort', abort); operations.delete(current);
|
|
105
|
+
if (busy === current) busy = null;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const assertTarget = async (target, operation) => {
|
|
109
|
+
operation.check(); targetIdentity(target);
|
|
110
|
+
await callbacks.assertTarget(target, operation.signal); operation.check();
|
|
111
|
+
};
|
|
112
|
+
const currentReceipt = (key, receipt, operation) => {
|
|
113
|
+
operation.check();
|
|
114
|
+
if (!receipt || receipts.get(key) !== receipt || expired(receipt, clock())) {
|
|
115
|
+
receipts.delete(key);
|
|
116
|
+
throw new Error('The compiled source receipt is missing, expired, or consumed. Compile and review again.');
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const validate = async (input, operation, requireReview) => {
|
|
120
|
+
const request = capture(input);
|
|
121
|
+
if (!record(request) || request.sourceKind !== 'file-plan') throw new Error('A compiled file-plan request is required.');
|
|
122
|
+
targetIdentity(request.target); documentIdentity(request.document, request.target);
|
|
123
|
+
await assertTarget(request.target, operation);
|
|
124
|
+
const key = keyOf(request.target, request.document); const receipt = receipts.get(key);
|
|
125
|
+
currentReceipt(key, receipt, operation);
|
|
126
|
+
const identity = requestIdentity(receipt.target, receipt.baseline, receipt.document, receipt.plan);
|
|
127
|
+
if (!same(request.payload, receipt.plan) || !same(request.identity, identity)) throw new Error('The request must match the complete compiled document, source mapping, target, and file plan.');
|
|
128
|
+
if (requireReview && receipt.reviewed !== identity.fingerprint) throw new Error('Review the complete source diff for this target before applying or building.');
|
|
129
|
+
return { request, key, receipt };
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
load(input, signal) {
|
|
133
|
+
return run(false, signal, async operation => {
|
|
134
|
+
const target = capture(input); await assertTarget(target, operation);
|
|
135
|
+
const baseline = capture(await callbacks.loadBaseline(target, operation.signal));
|
|
136
|
+
operation.check(); baselineIdentity(baseline, target); operation.check(); return baseline;
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
compile(input, signal) {
|
|
140
|
+
return run(true, signal, async operation => {
|
|
141
|
+
const { target, baseline, document } = capture(input);
|
|
142
|
+
targetIdentity(target); baselineIdentity(baseline, target); documentIdentity(document, target);
|
|
143
|
+
if (document.id !== baseline.document.id || document.rootId !== baseline.document.rootId) throw new Error('The edited document must retain the source baseline identity.');
|
|
144
|
+
if (same(document, baseline.document)) throw new Error('There are no document changes to compile.');
|
|
145
|
+
const key = keyOf(target, document); receipts.delete(key);
|
|
146
|
+
await assertTarget(target, operation);
|
|
147
|
+
const actual = capture(await callbacks.loadBaseline(target, operation.signal));
|
|
148
|
+
operation.check(); baselineIdentity(actual, target);
|
|
149
|
+
if (!same(baseline, actual)) throw new Error('The supplied baseline differs from the current host source baseline.');
|
|
150
|
+
const compiled = await callbacks.compilePlan(capture({ target, baseline: actual, document }), operation.signal);
|
|
151
|
+
operation.check();
|
|
152
|
+
const plan = capture(captureSourceFilePlan(compiled, { sourceRevision: target.sourceRevision, allowedFiles: actual.mapping.files.map(proof => proof.file) }));
|
|
153
|
+
if (!same(plan.proofs, actual.mapping.files)) throw new Error('The compiled plan must retain every source mapping proof exactly.');
|
|
154
|
+
if (plan.changes.some(change => change.beforeSha256 === hash(change.content))) throw new Error('Every compiled source change must produce different content.');
|
|
155
|
+
const createdAt = clock(); operation.check();
|
|
156
|
+
for (const [oldKey, receipt] of receipts) if (expired(receipt, createdAt)) receipts.delete(oldKey);
|
|
157
|
+
const receipt = { target, baseline: actual, document, plan, createdAt, reviewed: null };
|
|
158
|
+
receipts.set(key, receipt);
|
|
159
|
+
operation.onFailure(() => { if (receipts.get(key) === receipt) receipts.delete(key); });
|
|
160
|
+
while (receipts.size > capacity) receipts.delete(receipts.keys().next().value);
|
|
161
|
+
return plan;
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
preview(input, signal) {
|
|
165
|
+
return run(true, signal, async operation => {
|
|
166
|
+
const { request, key, receipt } = await validate(input, operation, false);
|
|
167
|
+
receipt.reviewed = null;
|
|
168
|
+
const result = capture(await callbacks.previewPlan(receipt.plan, request.target, operation.signal));
|
|
169
|
+
currentReceipt(key, receipt, operation); completePreview(result, receipt.plan);
|
|
170
|
+
receipt.reviewed = request.identity.fingerprint;
|
|
171
|
+
operation.onFailure(() => { receipt.reviewed = null; });
|
|
172
|
+
return capture({ ...result, revision: ++revision });
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
validatePreviewRequest(input, signal) {
|
|
176
|
+
return run(true, signal, async operation => (await validate(input, operation, true)).request);
|
|
177
|
+
},
|
|
178
|
+
apply(input, signal) {
|
|
179
|
+
return run(true, signal, async operation => {
|
|
180
|
+
const { request, receipt } = await validate(input, operation, true);
|
|
181
|
+
// Once a writer starts, every cached review requires recompilation, including failed or aborted writes.
|
|
182
|
+
receipts.clear(); operation.check();
|
|
183
|
+
const result = capture(await callbacks.applyPlan(receipt.plan, request.target, operation.signal));
|
|
184
|
+
operation.check();
|
|
185
|
+
if (!record(result) || !Number.isSafeInteger(result.applied) || result.applied < 1) throw new Error('The source writer did not record the reviewed changes. Compile and review again.');
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
dispose() {
|
|
190
|
+
if (disposed) return;
|
|
191
|
+
disposed = true; receipts.clear();
|
|
192
|
+
for (const operation of operations) operation.controller.abort(new Error('The source plan service has been disposed.'));
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -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.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Code-backed DOM design sandbox and visual QA editor",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -48,12 +48,54 @@
|
|
|
48
48
|
"types": "./dist-lib/types/testing.d.ts",
|
|
49
49
|
"default": "./dist-lib/testing.js"
|
|
50
50
|
},
|
|
51
|
-
"./style.css": "./dist-lib/style.css"
|
|
51
|
+
"./style.css": "./dist-lib/style.css",
|
|
52
|
+
"./binding-source": {
|
|
53
|
+
"types": "./binding-source.d.ts",
|
|
54
|
+
"default": "./node/source-bindings.mjs"
|
|
55
|
+
},
|
|
56
|
+
"./draft-preview": {
|
|
57
|
+
"types": "./draft-preview.d.ts",
|
|
58
|
+
"default": "./node/workspace-draft-preview.mjs"
|
|
59
|
+
},
|
|
60
|
+
"./source-files": {
|
|
61
|
+
"types": "./source-files.d.ts",
|
|
62
|
+
"default": "./node/source-file-plan.mjs"
|
|
63
|
+
},
|
|
64
|
+
"./source-structure": {
|
|
65
|
+
"types": "./source-structure.d.ts",
|
|
66
|
+
"default": "./node/source-structure.mjs"
|
|
67
|
+
},
|
|
68
|
+
"./source-plan": {
|
|
69
|
+
"types": "./workspace-source-plan.d.ts",
|
|
70
|
+
"default": "./node/workspace-source-plan.mjs"
|
|
71
|
+
},
|
|
72
|
+
"./source-responsive": {
|
|
73
|
+
"types": "./source-responsive.d.ts",
|
|
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"
|
|
95
|
+
}
|
|
52
96
|
},
|
|
53
97
|
"files": [
|
|
54
98
|
"dist-lib",
|
|
55
|
-
"docs/coverage-contracts.md",
|
|
56
|
-
"docs/screen-state-contract.md",
|
|
57
99
|
"node/contract.mjs",
|
|
58
100
|
"node/contract.d.mts",
|
|
59
101
|
"node/component-branches.mjs",
|
|
@@ -79,6 +121,12 @@
|
|
|
79
121
|
"node/source-diff.mjs",
|
|
80
122
|
"node/source-graph.mjs",
|
|
81
123
|
"node/source-operations.mjs",
|
|
124
|
+
"node/source-file-proofs.mjs",
|
|
125
|
+
"node/workspace-source-identity.mjs",
|
|
126
|
+
"node/workspace-source-identity.d.mts",
|
|
127
|
+
"node/workspace-draft-preview-plugin.mjs",
|
|
128
|
+
"node/workspace-draft-preview.mjs",
|
|
129
|
+
"draft-preview.d.ts",
|
|
82
130
|
"node/storyboard-canonical.mjs",
|
|
83
131
|
"node/storyboard-capture-scheduler.mjs",
|
|
84
132
|
"node/storyboard-capture-sessions.mjs",
|
|
@@ -92,13 +140,49 @@
|
|
|
92
140
|
"qa.d.ts",
|
|
93
141
|
"storyboard.d.ts",
|
|
94
142
|
"runtime.d.ts",
|
|
95
|
-
"vite.d.ts"
|
|
143
|
+
"vite.d.ts",
|
|
144
|
+
"node/source-bindings.mjs",
|
|
145
|
+
"binding-source.d.ts",
|
|
146
|
+
"node/source-file-plan.mjs",
|
|
147
|
+
"node/source-file-lock.mjs",
|
|
148
|
+
"source-files.d.ts",
|
|
149
|
+
"node/source-archive.mjs",
|
|
150
|
+
"node/token-source-references.mjs",
|
|
151
|
+
"node/source-structure.mjs",
|
|
152
|
+
"source-structure.d.ts",
|
|
153
|
+
"node/workspace-source-plan.mjs",
|
|
154
|
+
"workspace-source-plan.d.ts",
|
|
155
|
+
"node/source-structure-reconcile.mjs",
|
|
156
|
+
"node/source-structure-wrapper-codec.mjs",
|
|
157
|
+
"node/source-structure-wrapper-codec.d.mts",
|
|
158
|
+
"node/source-responsive.mjs",
|
|
159
|
+
"source-responsive.d.ts",
|
|
160
|
+
"node/source-responsive-codec.mjs",
|
|
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"
|
|
96
180
|
],
|
|
97
181
|
"scripts": {
|
|
98
182
|
"dev": "vite",
|
|
99
183
|
"build": "tsc && vite build",
|
|
100
184
|
"build:lib": "vite build --config vite.lib.config.ts && npm run build:types",
|
|
101
|
-
"test": "node --test node
|
|
185
|
+
"test": "node --test 'node/**/*.test.mjs' 'src/**/*.test.mjs'",
|
|
102
186
|
"lint": "eslint .",
|
|
103
187
|
"lint:language": "node scripts/lint-language.mjs",
|
|
104
188
|
"typecheck": "tsc --noEmit",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SourceFilePlan } from './dist-lib/types/host/sourceFilePlan.js';
|
|
2
|
+
import type { InspectPreviewResult } from './dist-lib/types/editor/host.js';
|
|
3
|
+
|
|
4
|
+
export interface SourceFilePlanOptions {
|
|
5
|
+
sourceRevision: string;
|
|
6
|
+
allowedFiles: readonly string[];
|
|
7
|
+
excludedSourcePrefixes?: readonly string[];
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
/** Capture and validate a serializable plan without reading or changing source files. */
|
|
11
|
+
export declare function captureSourceFilePlan(plan: SourceFilePlan, options: SourceFilePlanOptions): SourceFilePlan;
|
|
12
|
+
export declare function previewSourceFilePlan(root: string, plan: SourceFilePlan, options: SourceFilePlanOptions): Promise<Pick<InspectPreviewResult, 'files' | 'affectedFiles'>>;
|
|
13
|
+
export declare function applySourceFilePlan(root: string, plan: SourceFilePlan, options: SourceFilePlanOptions): Promise<{ applied: number; changed: number; files: string[] }>;
|
|
14
|
+
|
|
15
|
+
export interface TokenSourceReferenceEdit { start: number; end: number; before: string; after: string }
|
|
16
|
+
export interface TokenSourceReferenceDiagnostic { code: string; message: string; file: string; start?: number; name?: string }
|
|
17
|
+
/** Diagnostics block the complete rewrite; offsets address UTF-16 source positions. */
|
|
18
|
+
export declare function rewriteTokenSourceReferences(input: {
|
|
19
|
+
file: string;
|
|
20
|
+
source: string;
|
|
21
|
+
renames: readonly { from: string; to: string | null }[];
|
|
22
|
+
}): {
|
|
23
|
+
source: string;
|
|
24
|
+
edits: TokenSourceReferenceEdit[];
|
|
25
|
+
diagnostics: TokenSourceReferenceDiagnostic[];
|
|
26
|
+
references: { name: string; start: number; end: number; kind: 'static' | 'finite' }[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build/validation workers must settle before the callback returns and its archive is removed.
|
|
31
|
+
* Cancellation waits for callback settlement and cleanup, then rejects even if the callback returned.
|
|
32
|
+
*/
|
|
33
|
+
export declare function withSourceArchive<T>(options: {
|
|
34
|
+
projectRoot: string; sourceRevision: string; appDirectory?: string; signal?: AbortSignal;
|
|
35
|
+
}, operation: (context: Readonly<{ appRoot: string; sourceRevision: string; signal: AbortSignal }>) => Promise<T>): Promise<T>;
|