@field/cascade 0.8.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.
- package/AGENTS.md +36 -3
- package/CHANGELOG.md +401 -0
- package/DESIGN.md +14 -0
- package/README.md +41 -26
- package/dist/assets/CodeEditor-4O7FQ__R.js +623 -0
- package/dist/assets/CodePanel-BU-autmq.js +2 -0
- package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
- package/dist/assets/OpenGraphDialog-BwbRlC0X.js +1 -0
- package/dist/assets/ProjectSettings-DLOXLxcA.js +2 -0
- package/dist/assets/SettingsDialog-Bz8JxISH.js +1 -0
- package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-D5Z0nJ5-.js} +2 -2
- package/dist/assets/{core-BInLG4gw.js → core-Yqi0Ni61.js} +8 -8
- package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-CxUzc4vp.js} +1 -1
- package/dist/assets/definitions-GreEMXLV.js +1 -0
- package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-BNFb25CX.js} +1 -1
- package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-0ZDnWxeM.js} +1 -1
- package/dist/assets/geo-u0n_jOsD.js +1 -0
- package/dist/assets/{handlebars-BCSchLT2.js → handlebars-Cy18MoaD.js} +1 -1
- package/dist/assets/{html-CVpSv_k9.js → html-B-2Dx7nq.js} +1 -1
- package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-DUAc9B_d.js} +1 -1
- package/dist/assets/{image-BLn--oSF.js → image-cAa-Nu9O.js} +3 -3
- package/dist/assets/index-5ZtDAuy2.js +220 -0
- package/dist/assets/index-7vae6YFC.css +1 -0
- package/dist/assets/index-client-DqYmKstU.js +1 -0
- package/dist/assets/{javascript-BpX0Gvcf.js → javascript-DeQsDqP5.js} +1 -1
- package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-BiFwJVSk.js} +1 -1
- package/dist/assets/legacy-CTEeXeqF.js +2 -0
- package/dist/assets/{liquid-efUz51Wf.js → liquid-BLoHH68a.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
- package/dist/assets/math-DioIQOPJ.js +1 -0
- package/dist/assets/{mdx-DP_sjGVy.js → mdx-CFXQVNfs.js} +1 -1
- package/dist/assets/pop-DC_TQG9t.js +1 -0
- package/dist/assets/{python-aAuP3LUi.js → python-Cf7S7p2E.js} +1 -1
- package/dist/assets/{razor-7M6sMmC5.js → razor-CMHBcG-g.js} +1 -1
- package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
- package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BEih0GDj.js} +1 -1
- package/dist/assets/{typescript-ddhYBGyT.js → typescript-COEKYD_a.js} +1 -1
- package/dist/assets/{workers-X5la8DDT.js → workers-BNhAhMNn.js} +1 -1
- package/dist/assets/{xml-BzbXa3Ps.js → xml-CPiMpnPl.js} +1 -1
- package/dist/assets/{yaml-CXs-VzxL.js → yaml-Bgp1ITLR.js} +1 -1
- package/dist/cli/Graph-ZJJTRQTM.js +2975 -0
- package/dist/cli/chunk-2KXB47FP.js +115 -0
- package/dist/cli/chunk-35D343DC.js +1447 -0
- package/dist/cli/chunk-36P5K3SD.js +631 -0
- package/dist/cli/chunk-3JTJHF7V.js +267 -0
- package/dist/cli/chunk-4UDFYF5G.js +132 -0
- package/dist/cli/chunk-53NMBJAO.js +35 -0
- package/dist/cli/chunk-A4OAMOZH.js +310 -0
- package/dist/cli/chunk-BIJO5OFS.js +71 -0
- package/dist/cli/chunk-BP7UKF36.js +376 -0
- package/dist/cli/chunk-CKS2FQWR.js +89 -0
- package/dist/cli/chunk-CO22XHNG.js +593 -0
- package/dist/cli/chunk-D6BUOHUG.js +634 -0
- package/dist/cli/chunk-DEQSEFFH.js +153 -0
- package/dist/cli/chunk-DUIUFVU2.js +241 -0
- package/dist/cli/chunk-EUIAC3U3.js +3006 -0
- package/dist/cli/chunk-FCDXJ6EK.js +2208 -0
- package/dist/cli/chunk-GFS6W3C5.js +308 -0
- package/dist/cli/chunk-HFHWYL55.js +139 -0
- package/dist/cli/chunk-HSNOBIVT.js +263 -0
- package/dist/cli/chunk-ICH7N5PN.js +26 -0
- package/dist/cli/chunk-J7VK7MNF.js +418 -0
- package/dist/cli/chunk-KH4MUBKF.js +1551 -0
- package/dist/cli/chunk-MIIEFUGG.js +623 -0
- package/dist/cli/chunk-OWWBYLHC.js +136 -0
- package/dist/cli/chunk-POMMNKZZ.js +672 -0
- package/dist/cli/chunk-QBBKRNGY.js +90 -0
- package/dist/cli/chunk-QQPA2ZHH.js +273 -0
- package/dist/cli/chunk-RLCAJBVR.js +224 -0
- package/dist/cli/chunk-RQZ4SX5R.js +1125 -0
- package/dist/cli/chunk-RUU3YWXU.js +99 -0
- package/dist/cli/chunk-RXDIMXN5.js +98 -0
- package/dist/cli/chunk-SGJAET2L.js +197 -0
- package/dist/cli/chunk-VVGSLVSF.js +151 -0
- package/dist/cli/chunk-WD62JIP6.js +118 -0
- package/dist/cli/chunk-WIH4TEIS.js +104 -0
- package/dist/cli/chunk-WKURDVYP.js +1119 -0
- package/dist/cli/chunk-WPUDGEFY.js +20 -0
- package/dist/cli/chunk-XWDSKG7N.js +1301 -0
- package/dist/cli/chunk-YAT5Y25U.js +121 -0
- package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
- package/dist/cli/cliCommands-UQSFGEHF.js +3464 -0
- package/dist/cli/commands-Q23M26QM.js +1292 -0
- package/dist/cli/definitions-HF2FTHYU.js +32 -0
- package/dist/cli/doctorCommand-B7BPQMN6.js +76 -0
- package/dist/cli/edit-JHCPTQFT.js +28 -0
- package/dist/cli/editorCommands-UIF5YLUZ.js +33 -0
- package/dist/cli/encode-HAUZTYPT.js +17 -0
- package/dist/cli/frames-LSAUBZ7P.js +16 -0
- package/dist/cli/geo-4RNK3QVN.js +43 -0
- package/dist/cli/graphCommands-6BMAKGQE.js +184 -0
- package/dist/cli/graphFile-VPSQ6OF7.js +286 -0
- package/dist/cli/headlessCanvas-5XLDZT7M.js +15 -0
- package/dist/cli/helpCommand-3VRE3KVO.js +155 -0
- package/dist/cli/index.js +106 -14207
- package/dist/cli/math-ZRMLKU2D.js +16 -0
- package/dist/cli/mcp-ZLEEMYGF.js +164 -0
- package/dist/cli/parity-KINARCP2.js +328 -0
- package/dist/cli/pop-USDFXB7H.js +39 -0
- package/dist/cli/project-K3QIPGBB.js +12 -0
- package/dist/cli/projectDefinition-DMTZN2NE.js +27 -0
- package/dist/cli/projectRuntime-Z5PJOH47.js +48 -0
- package/dist/cli/registerStandardNodes-S2VLMF6J.js +3081 -0
- package/dist/cli/render-YURAPPUI.js +436 -0
- package/dist/cli/runCommand-PI764LEG.js +198 -0
- package/dist/cli/runner-RA6UKEPD.js +37 -0
- package/dist/cli/runtime/image.ts +277 -0
- package/dist/cli/runtime/stage.ts +74 -11
- package/dist/cli/stageRunner-KUNIMQKJ.js +10 -0
- package/dist/cli/verbs-RCE3K4VQ.js +19 -0
- package/dist/cli/webBuild-GT4PZT5D.js +37 -0
- package/dist/contracts/bundle.d.ts +75 -0
- package/dist/contracts/bundle.js +112 -0
- package/dist/contracts/definition.d.ts +67 -4
- package/dist/contracts/document.d.ts +29 -0
- package/dist/contracts/documentFormat.d.ts +18 -0
- package/dist/contracts/documentFormat.js +195 -0
- package/dist/contracts/editor.d.ts +189 -0
- package/dist/contracts/editor.js +2 -0
- package/dist/contracts/index.d.ts +3 -0
- package/dist/contracts/index.js +3 -0
- package/dist/contracts/validate.js +59 -3
- package/dist/contracts/values.d.ts +3 -3
- package/dist/contracts/values.js +1 -0
- package/dist/image.d.ts +141 -0
- package/dist/image.js +127 -0
- package/dist/index.html +4 -4
- package/dist/player/app.js +17 -5
- package/dist/player/editor.js +1919 -0
- package/dist/player/editorEngine.js +477 -0
- package/dist/runtime/builtins/core/bundle.d.ts +70 -0
- package/dist/runtime/builtins/core/bundle.js +81 -0
- package/dist/runtime/builtins/core/index.d.ts +3 -2
- package/dist/runtime/builtins/core/index.js +6 -2
- package/dist/runtime/builtins/core/routing.d.ts +27 -0
- package/dist/runtime/builtins/core/routing.js +28 -0
- package/dist/runtime/builtins/headless.d.ts +19 -0
- package/dist/runtime/builtins/headless.js +25 -0
- package/dist/runtime/builtins/image/contactSheet.d.ts +226 -0
- package/dist/runtime/builtins/image/contactSheet.js +199 -0
- package/dist/runtime/builtins/image/index.d.ts +9 -0
- package/dist/runtime/builtins/image/index.js +9 -0
- package/dist/runtime/edit/document.d.ts +94 -0
- package/dist/runtime/edit/document.js +194 -0
- package/dist/runtime/edit/graphEditor.d.ts +223 -0
- package/dist/runtime/edit/graphEditor.js +893 -0
- package/dist/runtime/edit/index.d.ts +12 -0
- package/dist/runtime/edit/index.js +6 -0
- package/dist/runtime/edit/layout.d.ts +50 -0
- package/dist/runtime/edit/layout.js +126 -0
- package/dist/runtime/edit/operations.d.ts +65 -0
- package/dist/runtime/edit/operations.js +57 -0
- package/dist/runtime/edit/parameters.d.ts +75 -0
- package/dist/runtime/edit/parameters.js +284 -0
- package/dist/runtime/edit/read.d.ts +35 -0
- package/dist/runtime/edit/read.js +65 -0
- package/dist/runtime/editor/index.d.ts +104 -0
- package/dist/runtime/editor/index.js +439 -0
- package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
- package/dist/runtime/expressions/bundlePaths.js +55 -0
- package/dist/runtime/expressions/index.d.ts +2 -0
- package/dist/runtime/expressions/index.js +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/params/animator.d.ts +5 -0
- package/dist/runtime/params/animator.js +16 -0
- package/dist/runtime/runtime.js +238 -12
- package/dist/runtime/seed.d.ts +29 -0
- package/dist/runtime/seed.js +52 -0
- package/dist/runtime/types.d.ts +12 -0
- package/dist/stage.js +43 -9
- package/dist/studio/panel.d.ts +2 -0
- package/doc/CLI_REFERENCE.md +171 -0
- package/doc/EDITOR_EXPORT.md +235 -0
- package/doc/HEADLESS_GPU.md +27 -0
- package/doc/MCP.md +69 -0
- package/doc/NODE_REFERENCE.md +208 -0
- package/doc/PROJECT_AUTHORING.md +258 -1
- package/package.json +29 -8
- package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
- package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
- package/dist/assets/ExportDialog-CjTbbTZJ.js +0 -358
- package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
- package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
- package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
- package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
- package/dist/assets/definitions-DZpOjWKR.js +0 -1
- package/dist/assets/geo-D2VPjAdt.js +0 -1
- package/dist/assets/index-CMiBdEuT.js +0 -212
- package/dist/assets/index-DMIuml9a.css +0 -1
- package/dist/assets/index-client-BI2H82FJ.js +0 -1
- package/dist/assets/legacy-ZA7textG.js +0 -2
- package/dist/assets/math-B6hpxPJO.js +0 -1
- package/dist/assets/pop-CYUIjT6x.js +0 -1
- package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cascade/image` — the one image API a node body uses, in every host.
|
|
3
|
+
*
|
|
4
|
+
* A node that draws used to have two honest choices and one dishonest one.
|
|
5
|
+
* `new OffscreenCanvas` worked in Studio and under `cascade run` (the CLI
|
|
6
|
+
* installs Skia's canvas as that global), but the bytes in and out went
|
|
7
|
+
* through `cascade/io`'s project paths, and fonts had no portable door at all.
|
|
8
|
+
* So the nodes that needed fonts or files reached for `@napi-rs/canvas` and
|
|
9
|
+
* `node:fs` directly, declared `runsOn: 'server'`, and could never be exported
|
|
10
|
+
* as a browser editor — the commons plotter-social job was blocked on exactly
|
|
11
|
+
* that ("Cascade has no browser image contract").
|
|
12
|
+
*
|
|
13
|
+
* The contract, in full:
|
|
14
|
+
*
|
|
15
|
+
* - **Surface.** `createCanvas(w, h)` is an `OffscreenCanvas`: the browser's
|
|
16
|
+
* own in a page or a worker, Skia's (`@napi-rs/canvas`, installed by the
|
|
17
|
+
* headless host) under `cascade run`/`render`/MCP. `context2d(canvas)` asks
|
|
18
|
+
* for a CPU-backed 2D context (`willReadFrequently`), which is what keeps
|
|
19
|
+
* Chrome on the same software Skia rasteriser the headless host uses, and
|
|
20
|
+
* is the right hint anyway for nodes that read pixels back.
|
|
21
|
+
* - **Pixels in.** `readImage(context, ref)` reads an `ImageRef`, `AssetRef`
|
|
22
|
+
* or project path through the node's declared `assets` capability and
|
|
23
|
+
* decodes it with `decodeImage` (`createImageBitmap` in both hosts).
|
|
24
|
+
* - **Pixels out.** `writeImage(context, canvas, name)` encodes PNG through
|
|
25
|
+
* `encodeImage` and stores it with `assets.write`, returning the `ImageRef`
|
|
26
|
+
* to set on an `image` output. The host decides where it lands
|
|
27
|
+
* (`.cascade-cache/<name>.<hash>.png` headless, an in-memory generated
|
|
28
|
+
* asset in an exported editor), so a node never names a path.
|
|
29
|
+
* - **Fonts.** `registerFont(family, bytes)` makes a face drawable by
|
|
30
|
+
* `ctx.font` in this host: `FontFace` added to the page's or worker's font
|
|
31
|
+
* set in a browser, Skia's `GlobalFonts` headless. Bytes come from the same
|
|
32
|
+
* `assets` capability, so a font is a project file like any other.
|
|
33
|
+
* - **Refusal, not fallback.** No canvas in this host, or no `assets`
|
|
34
|
+
* capability declared, is an error with one sentence saying which, never a
|
|
35
|
+
* silent blank image.
|
|
36
|
+
*
|
|
37
|
+
* A node using it declares `runsOn: 'portable'` and `capabilities: ['assets']`
|
|
38
|
+
* and imports nothing host-specific (`node:*`, `@napi-rs/canvas`, `document`).
|
|
39
|
+
* Then the same body cooks in Studio, in `cascade run`, and in an exported
|
|
40
|
+
* editor's worker.
|
|
41
|
+
*
|
|
42
|
+
* The headless host installs its font door under a global key, like
|
|
43
|
+
* `cascade/io`'s bridge, because the node's copy of this module and the CLI's
|
|
44
|
+
* copy are not always the same module instance.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/** An image port value. Mirrors Cascade's core `ImageRef`. */
|
|
48
|
+
export interface ImageRef {
|
|
49
|
+
path: string;
|
|
50
|
+
size: [number, number];
|
|
51
|
+
channels: 'r' | 'a' | 'rg' | 'rgb' | 'rgba';
|
|
52
|
+
depth: 'u8' | 'u16' | 'f16' | 'f32';
|
|
53
|
+
space: 'srgb' | 'linear';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Anything naming stored bytes: an `ImageRef`, an `AssetRef`, or a project path. */
|
|
57
|
+
export type ImageSourceRef = string | { readonly path: string };
|
|
58
|
+
|
|
59
|
+
/** The part of the `assets` capability these helpers use. */
|
|
60
|
+
export interface ImageAssets {
|
|
61
|
+
read(ref: { path: string }, options: { signal: unknown }): Promise<Uint8Array>;
|
|
62
|
+
write(
|
|
63
|
+
data: Uint8Array,
|
|
64
|
+
metadata: { mediaType?: string; suggestedName?: string },
|
|
65
|
+
options: { signal: unknown },
|
|
66
|
+
): Promise<{ path: string; mediaType?: string }>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The slice of a definition-v1 `NodeExecutionContext` these helpers need, so
|
|
71
|
+
* a node passes its own `context`. Typed loosely on purpose: a node that did
|
|
72
|
+
* not declare `assets` still type-checks here and gets a sentence at run time
|
|
73
|
+
* naming the missing declaration rather than a type error about generics.
|
|
74
|
+
*/
|
|
75
|
+
export interface ImageNodeContext {
|
|
76
|
+
readonly capabilities: object;
|
|
77
|
+
readonly signal?: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type ImageType = 'image/png' | 'image/webp' | 'image/jpeg';
|
|
81
|
+
|
|
82
|
+
/** What a host gives `registerFont`. A browser realm needs none. */
|
|
83
|
+
export interface ImageHost {
|
|
84
|
+
/** For a `--verbose` line and for errors. */
|
|
85
|
+
readonly renderer: string;
|
|
86
|
+
registerFont(family: string, bytes: Uint8Array, descriptors: FontDescriptors): void | Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface FontDescriptors {
|
|
90
|
+
/** CSS weight, `'400'`, `'bold'`. Headless reads the weight from the file. */
|
|
91
|
+
weight?: string;
|
|
92
|
+
/** `'normal'` or `'italic'`. Headless reads the style from the file. */
|
|
93
|
+
style?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const HOST_KEY = '__cascadeImageHost';
|
|
97
|
+
|
|
98
|
+
const NO_CANVAS =
|
|
99
|
+
'cascade/image: this host has no canvas. In a browser this needs OffscreenCanvas; headless, Cascade installs ' +
|
|
100
|
+
'@napi-rs/canvas before a run (install it with "npm install @napi-rs/canvas" if it is missing).';
|
|
101
|
+
|
|
102
|
+
/** Install the headless host's font door. Returns the disposer. */
|
|
103
|
+
export function installImageHost(host: ImageHost | null): () => void {
|
|
104
|
+
const globals = globalThis as Record<string, unknown>;
|
|
105
|
+
const previous = globals[HOST_KEY];
|
|
106
|
+
globals[HOST_KEY] = host ?? undefined;
|
|
107
|
+
return () => {
|
|
108
|
+
globals[HOST_KEY] = previous;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function installedHost(): ImageHost | null {
|
|
113
|
+
const host = (globalThis as Record<string, unknown>)[HOST_KEY] as ImageHost | undefined;
|
|
114
|
+
return host && typeof host.registerFont === 'function' ? host : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type CanvasConstructor = new (width: number, height: number) => OffscreenCanvas;
|
|
118
|
+
|
|
119
|
+
function canvasConstructor(): CanvasConstructor | undefined {
|
|
120
|
+
return (globalThis as { OffscreenCanvas?: CanvasConstructor }).OffscreenCanvas;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Whether this host can draw. Never throws. */
|
|
124
|
+
export function canvasAvailable(): boolean {
|
|
125
|
+
return typeof canvasConstructor() === 'function';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A drawing surface of whole pixels, at least 1x1. */
|
|
129
|
+
export function createCanvas(width: number, height: number): OffscreenCanvas {
|
|
130
|
+
const Canvas = canvasConstructor();
|
|
131
|
+
if (!Canvas) throw new Error(NO_CANVAS);
|
|
132
|
+
return new Canvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The canvas's 2D context, CPU-backed. `willReadFrequently` keeps a browser
|
|
137
|
+
* off the GPU rasteriser, so a node draws with the same software Skia in Chrome
|
|
138
|
+
* as headless, and `getImageData` does not stall on a readback.
|
|
139
|
+
*/
|
|
140
|
+
export function context2d(canvas: OffscreenCanvas): OffscreenCanvasRenderingContext2D {
|
|
141
|
+
const context = canvas.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D | null;
|
|
142
|
+
if (!context) throw new Error('cascade/image: the canvas has no 2D context');
|
|
143
|
+
return context;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** `createCanvas` and `context2d` in one: `const { canvas, context } = surface(w, h)`. */
|
|
147
|
+
export function surface(width: number, height: number): {
|
|
148
|
+
canvas: OffscreenCanvas;
|
|
149
|
+
context: OffscreenCanvasRenderingContext2D;
|
|
150
|
+
} {
|
|
151
|
+
const canvas = createCanvas(width, height);
|
|
152
|
+
return { canvas, context: context2d(canvas) };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Encoded bytes (PNG, JPEG, WebP) to something `drawImage` accepts. */
|
|
156
|
+
export async function decodeImage(bytes: Uint8Array | ArrayBuffer | Blob): Promise<ImageBitmap> {
|
|
157
|
+
const create = (globalThis as { createImageBitmap?: (source: Blob) => Promise<ImageBitmap> }).createImageBitmap;
|
|
158
|
+
if (typeof create !== 'function') throw new Error(NO_CANVAS);
|
|
159
|
+
const blob = bytes instanceof Blob ? bytes : new Blob([bytes as BlobPart]);
|
|
160
|
+
const image = await create(blob);
|
|
161
|
+
if (!image?.width || !image?.height) throw new Error('cascade/image: could not decode the image data');
|
|
162
|
+
return image;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A canvas's pixels as encoded bytes, PNG unless asked otherwise. */
|
|
166
|
+
export async function encodeImage(
|
|
167
|
+
canvas: OffscreenCanvas,
|
|
168
|
+
options: { type?: ImageType; quality?: number } = {},
|
|
169
|
+
): Promise<Uint8Array> {
|
|
170
|
+
const type = options.type ?? 'image/png';
|
|
171
|
+
if (typeof canvas.convertToBlob !== 'function') throw new Error('cascade/image: this canvas cannot encode (no convertToBlob)');
|
|
172
|
+
const blob = await canvas.convertToBlob({ type, ...(options.quality === undefined ? {} : { quality: options.quality }) });
|
|
173
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// One record per realm, so a font registered once is not added again on every
|
|
177
|
+
// cook. Keyed by family and content: new bytes under a family name register.
|
|
178
|
+
// The headless realm is keyed by its renderer name rather than the host
|
|
179
|
+
// object, because a long-lived process (MCP) installs a fresh host per run
|
|
180
|
+
// while Skia's font registry lives for the whole process.
|
|
181
|
+
const registered = new Map<unknown, Set<string>>();
|
|
182
|
+
|
|
183
|
+
function fingerprint(bytes: Uint8Array): string {
|
|
184
|
+
let hash = 0x811c9dc5;
|
|
185
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
186
|
+
hash ^= bytes[index]!;
|
|
187
|
+
hash = Math.imul(hash, 0x01000193);
|
|
188
|
+
}
|
|
189
|
+
return `${bytes.length}:${(hash >>> 0).toString(16)}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface BrowserFontSet { add(face: unknown): unknown }
|
|
193
|
+
type FontFaceConstructor = new (family: string, source: ArrayBuffer | Uint8Array, descriptors?: FontDescriptors) => {
|
|
194
|
+
load(): Promise<unknown>;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Make `family` drawable by `ctx.font` here. Idempotent per family and bytes.
|
|
199
|
+
* A browser realm (page or worker) uses `FontFace`; the headless host
|
|
200
|
+
* registers with Skia. A host with neither refuses.
|
|
201
|
+
*/
|
|
202
|
+
export async function registerFont(
|
|
203
|
+
family: string,
|
|
204
|
+
bytes: Uint8Array | ArrayBuffer,
|
|
205
|
+
descriptors: FontDescriptors = {},
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
if (!family.trim()) throw new Error('cascade/image: registerFont needs a family name');
|
|
208
|
+
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
209
|
+
if (!data.byteLength) throw new Error(`cascade/image: the font for ${family} is empty`);
|
|
210
|
+
const host = installedHost();
|
|
211
|
+
const globals = globalThis as { FontFace?: FontFaceConstructor; fonts?: BrowserFontSet; document?: { fonts?: BrowserFontSet } };
|
|
212
|
+
const fonts = host ? null : globals.fonts ?? globals.document?.fonts;
|
|
213
|
+
const realm: unknown = host ? `headless:${host.renderer}` : fonts ?? globalThis;
|
|
214
|
+
const key = `${family}\u0000${descriptors.weight ?? ''}\u0000${descriptors.style ?? ''}\u0000${fingerprint(data)}`;
|
|
215
|
+
const seen = registered.get(realm) ?? new Set<string>();
|
|
216
|
+
if (seen.has(key)) return;
|
|
217
|
+
if (host) {
|
|
218
|
+
await host.registerFont(family, data, descriptors);
|
|
219
|
+
} else if (typeof globals.FontFace === 'function' && fonts) {
|
|
220
|
+
// A copy: FontFace may detach or keep the buffer it is given.
|
|
221
|
+
const face = new globals.FontFace(family, data.slice().buffer, descriptors);
|
|
222
|
+
fonts.add(await face.load());
|
|
223
|
+
} else {
|
|
224
|
+
throw new Error('cascade/image: this host cannot register fonts (no FontFace and no headless canvas host)');
|
|
225
|
+
}
|
|
226
|
+
seen.add(key);
|
|
227
|
+
registered.set(realm, seen);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function assetsOf(context: ImageNodeContext): ImageAssets {
|
|
231
|
+
const assets = (context.capabilities as { assets?: ImageAssets }).assets;
|
|
232
|
+
if (!assets || typeof assets.read !== 'function' || typeof assets.write !== 'function') {
|
|
233
|
+
throw new Error("cascade/image: this node reads or writes images, so its definition must declare capabilities: ['assets']");
|
|
234
|
+
}
|
|
235
|
+
return assets;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function refOf(ref: ImageSourceRef): { path: string } {
|
|
239
|
+
const path = typeof ref === 'string' ? ref : ref?.path;
|
|
240
|
+
if (typeof path !== 'string' || !path) throw new Error('cascade/image: no image to read (an empty path or an unset image input)');
|
|
241
|
+
return { path };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const NEVER = { aborted: false, addEventListener() {}, removeEventListener() {} };
|
|
245
|
+
|
|
246
|
+
/** A stored file's bytes through the node's `assets` capability: an image, a font, anything. */
|
|
247
|
+
export async function readBytes(context: ImageNodeContext, ref: ImageSourceRef): Promise<Uint8Array> {
|
|
248
|
+
return assetsOf(context).read(refOf(ref), { signal: context.signal ?? NEVER });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Read and decode an image input, an asset, or a project file. */
|
|
252
|
+
export async function readImage(context: ImageNodeContext, ref: ImageSourceRef): Promise<ImageBitmap> {
|
|
253
|
+
return decodeImage(await readBytes(context, ref));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Encode a canvas and store it; set the result on an `image` output. `name`
|
|
258
|
+
* is a suggestion (`context.nodeId + '.png'` keeps instances apart); the host
|
|
259
|
+
* picks the stored path.
|
|
260
|
+
*/
|
|
261
|
+
export async function writeImage(
|
|
262
|
+
context: ImageNodeContext,
|
|
263
|
+
canvas: OffscreenCanvas,
|
|
264
|
+
name: string,
|
|
265
|
+
options: { type?: ImageType; quality?: number; space?: 'srgb' | 'linear' } = {},
|
|
266
|
+
): Promise<ImageRef> {
|
|
267
|
+
const type = options.type ?? 'image/png';
|
|
268
|
+
const bytes = await encodeImage(canvas, { type, ...(options.quality === undefined ? {} : { quality: options.quality }) });
|
|
269
|
+
const stored = await assetsOf(context).write(bytes, { mediaType: type, suggestedName: name }, { signal: context.signal ?? NEVER });
|
|
270
|
+
return {
|
|
271
|
+
path: stored.path,
|
|
272
|
+
size: [canvas.width, canvas.height],
|
|
273
|
+
channels: 'rgba',
|
|
274
|
+
depth: 'u8',
|
|
275
|
+
space: options.space ?? 'srgb',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
@@ -17,12 +17,52 @@
|
|
|
17
17
|
* carries a project-relative path, never pixels.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
export interface StageCallOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Abandons the stage. In Studio the request is withdrawn — dropped if it is
|
|
23
|
+
* still queued for the worker, interrupted if it is running — and the call
|
|
24
|
+
* rejects at once, so a superseded cook stops costing anything the moment it
|
|
25
|
+
* is superseded rather than when the stage happens to finish.
|
|
26
|
+
*
|
|
27
|
+
* Optional: when it is absent the signal of the cook in flight is used (see
|
|
28
|
+
* `COOK_SIGNAL_KEY`), so a node that never heard of this still cancels.
|
|
29
|
+
*/
|
|
30
|
+
readonly signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
export interface StageBridge {
|
|
21
|
-
(stage: string, args: Record<string, unknown
|
|
34
|
+
(stage: string, args: Record<string, unknown>, options?: StageCallOptions): Promise<unknown>;
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
const BRIDGE_KEY = '__cascadeStageBridge';
|
|
25
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Where the scheduler publishes the signal of the interactive cook in flight.
|
|
41
|
+
*
|
|
42
|
+
* On `globalThis` for the same reason as the bridge: every project module is
|
|
43
|
+
* compiled with its own copy of this file, so module state would not be shared
|
|
44
|
+
* with the scheduler that sets it. The interactive lane runs one node at a
|
|
45
|
+
* time, which is what makes a single ambient value correct: a stage started
|
|
46
|
+
* while node N cooks belongs to node N's cook. Read at call time, never later.
|
|
47
|
+
*/
|
|
48
|
+
export const COOK_SIGNAL_KEY = '__cascadeCookSignal';
|
|
49
|
+
|
|
50
|
+
function ambientCookSignal(): AbortSignal | undefined {
|
|
51
|
+
const read = (globalThis as Record<string, unknown>)[COOK_SIGNAL_KEY];
|
|
52
|
+
if (typeof read !== 'function') return undefined;
|
|
53
|
+
const signal = (read as () => AbortSignal | null | undefined)();
|
|
54
|
+
return signal ?? undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The rejection a withdrawn stage produces, recognisable without importing a
|
|
58
|
+
* class across module copies. */
|
|
59
|
+
function cancelledError(stage: string, signal: AbortSignal): Error {
|
|
60
|
+
const reason = signal.reason instanceof Error ? signal.reason.message : 'superseded';
|
|
61
|
+
const error = new Error(`stage "${stage}" was cancelled: ${reason}`);
|
|
62
|
+
error.name = 'AbortError';
|
|
63
|
+
return error;
|
|
64
|
+
}
|
|
65
|
+
|
|
26
66
|
/**
|
|
27
67
|
* Install the local transport. Called by a host that can reach the filesystem —
|
|
28
68
|
* the CLI does this before running a graph. Returns a disposer so a host can
|
|
@@ -71,20 +111,30 @@ async function capability(): Promise<string> {
|
|
|
71
111
|
return token;
|
|
72
112
|
}
|
|
73
113
|
|
|
74
|
-
async function overHttp<T>(stage: string, args: Record<string, unknown
|
|
114
|
+
async function overHttp<T>(stage: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<T> {
|
|
75
115
|
const postStage = async () => fetch('/api/exec', {
|
|
76
116
|
method: 'POST',
|
|
77
117
|
headers: { 'Content-Type': 'application/json', 'X-Cascade-Exec-Capability': await capability() },
|
|
78
|
-
|
|
118
|
+
// `cook`: a node's cook is a pure function of its inputs, so the server's
|
|
119
|
+
// default stage cache may serve it (see StageCachePolicy). A panel posts
|
|
120
|
+
// through its own api.runStage, without this, and is never cached unasked.
|
|
121
|
+
body: JSON.stringify({ stage, args, caller: 'cook' }),
|
|
122
|
+
signal,
|
|
79
123
|
});
|
|
80
124
|
|
|
81
|
-
let response
|
|
82
|
-
|
|
83
|
-
|
|
125
|
+
let response: Response;
|
|
126
|
+
let body: { ok?: boolean; stdout?: string; stderr?: string; error?: string };
|
|
127
|
+
try {
|
|
84
128
|
response = await postStage();
|
|
129
|
+
if (response.status === 403) {
|
|
130
|
+
capabilityPromise = undefined;
|
|
131
|
+
response = await postStage();
|
|
132
|
+
}
|
|
133
|
+
body = await response.json() as typeof body;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (signal?.aborted) throw cancelledError(stage, signal);
|
|
136
|
+
throw error;
|
|
85
137
|
}
|
|
86
|
-
|
|
87
|
-
const body = await response.json() as { ok?: boolean; stdout?: string; stderr?: string; error?: string };
|
|
88
138
|
if (!response.ok || !body.ok) {
|
|
89
139
|
throw new Error(`stage "${stage}" failed: ${body.stderr || body.error || 'unknown error'}`);
|
|
90
140
|
}
|
|
@@ -113,16 +163,29 @@ export async function stageAvailable(): Promise<boolean> {
|
|
|
113
163
|
* rather than a URL parse error from a fetch that was never going to work —
|
|
114
164
|
* which is what this whole file exists to stop happening.
|
|
115
165
|
*/
|
|
116
|
-
export async function runStage<T = unknown>(
|
|
166
|
+
export async function runStage<T = unknown>(
|
|
167
|
+
stage: string,
|
|
168
|
+
args: Record<string, unknown> = {},
|
|
169
|
+
options: StageCallOptions = {},
|
|
170
|
+
): Promise<T> {
|
|
171
|
+
const signal = options.signal ?? ambientCookSignal();
|
|
172
|
+
if (signal?.aborted) throw cancelledError(stage, signal);
|
|
117
173
|
const bridge = localBridge();
|
|
118
|
-
if (bridge)
|
|
174
|
+
if (bridge) {
|
|
175
|
+
try {
|
|
176
|
+
return await bridge(stage, args, signal ? { signal } : undefined) as T;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (signal?.aborted) throw cancelledError(stage, signal);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
119
182
|
if (typeof document === 'undefined') {
|
|
120
183
|
throw new Error(
|
|
121
184
|
`Cannot run stage "${stage}": no host bridge is installed and there is no page to post from. ` +
|
|
122
185
|
'A headless host must call installStageBridge() from cascade/stage before executing the graph.',
|
|
123
186
|
);
|
|
124
187
|
}
|
|
125
|
-
return overHttp<T>(stage, args);
|
|
188
|
+
return overHttp<T>(stage, args, signal);
|
|
126
189
|
}
|
|
127
190
|
|
|
128
191
|
/** A scratch path for a node's output, scoped by node id so re-running a graph
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VERBS,
|
|
3
|
+
graphCommandsHelp,
|
|
4
|
+
graphOptions,
|
|
5
|
+
isGraphCommand,
|
|
6
|
+
mainHelp,
|
|
7
|
+
mcpTools,
|
|
8
|
+
verbFor
|
|
9
|
+
} from "./chunk-POMMNKZZ.js";
|
|
10
|
+
export {
|
|
11
|
+
VERBS,
|
|
12
|
+
graphCommandsHelp,
|
|
13
|
+
graphOptions,
|
|
14
|
+
isGraphCommand,
|
|
15
|
+
mainHelp,
|
|
16
|
+
mcpTools,
|
|
17
|
+
verbFor
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=verbs-RCE3K4VQ.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPlayer,
|
|
3
|
+
canonicalFuturePath,
|
|
4
|
+
exists,
|
|
5
|
+
inside,
|
|
6
|
+
installRoot,
|
|
7
|
+
playerFile,
|
|
8
|
+
staticRuntimePlugin
|
|
9
|
+
} from "./chunk-3JTJHF7V.js";
|
|
10
|
+
import "./chunk-EUIAC3U3.js";
|
|
11
|
+
import "./chunk-36P5K3SD.js";
|
|
12
|
+
import "./chunk-D6BUOHUG.js";
|
|
13
|
+
import "./chunk-RXDIMXN5.js";
|
|
14
|
+
import "./chunk-QQPA2ZHH.js";
|
|
15
|
+
import "./chunk-KH4MUBKF.js";
|
|
16
|
+
import "./chunk-35D343DC.js";
|
|
17
|
+
import "./chunk-DEQSEFFH.js";
|
|
18
|
+
import "./chunk-HSNOBIVT.js";
|
|
19
|
+
import "./chunk-ICH7N5PN.js";
|
|
20
|
+
import "./chunk-GFS6W3C5.js";
|
|
21
|
+
import "./chunk-HFHWYL55.js";
|
|
22
|
+
import "./chunk-CO22XHNG.js";
|
|
23
|
+
import "./chunk-RQZ4SX5R.js";
|
|
24
|
+
import "./chunk-A4OAMOZH.js";
|
|
25
|
+
import "./chunk-J7VK7MNF.js";
|
|
26
|
+
import "./chunk-XWDSKG7N.js";
|
|
27
|
+
import "./chunk-BIJO5OFS.js";
|
|
28
|
+
export {
|
|
29
|
+
buildPlayer,
|
|
30
|
+
canonicalFuturePath,
|
|
31
|
+
exists,
|
|
32
|
+
inside,
|
|
33
|
+
installRoot,
|
|
34
|
+
playerFile,
|
|
35
|
+
staticRuntimePlugin
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=webBuild-GT4PZT5D.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundles: one port carrying a set of named fields.
|
|
3
|
+
*
|
|
4
|
+
* Marcus, 2026-10-10, looking at a graph where one node fanned out a dozen
|
|
5
|
+
* outputs wired everywhere: *"Instead of having these nodes with many different
|
|
6
|
+
* outputs (more than 3 or 4 outputs), can we just export some kind of JSON
|
|
7
|
+
* object, a sort of multi-object, instead of outputting 12 different outputs?"*
|
|
8
|
+
*
|
|
9
|
+
* A bundle value is a plain object of fields. Each field is any ordinary
|
|
10
|
+
* Cascade value; an image or a cache entry travels as its reference (an
|
|
11
|
+
* `ImageRef`, an `AssetRef`), never as pixels, so a bundle is as cheap to pass
|
|
12
|
+
* as the fields it names.
|
|
13
|
+
*
|
|
14
|
+
* What makes it more than `object`:
|
|
15
|
+
*
|
|
16
|
+
* - a producer declares its fields (`fields: { dpi: "float", ... }`), so a
|
|
17
|
+
* checker can name an unknown field and suggest the right one;
|
|
18
|
+
* - an optional `schema` name (`format`, `signals`, `marks`) keeps two
|
|
19
|
+
* unrelated bundles from being wired together;
|
|
20
|
+
* - a consumer may declare the fields it reads (`fields: ["dpi"]`). It then
|
|
21
|
+
* sees only those, and Studio re-cooks it only when one of them changed;
|
|
22
|
+
* - expressions read a wired bundle's fields: `ch("format.dpi")`.
|
|
23
|
+
*
|
|
24
|
+
* Everything here is pure and dependency-free so the runtime, Studio and the
|
|
25
|
+
* CLI checker share one reading of the rules.
|
|
26
|
+
*/
|
|
27
|
+
import type { CascadeType } from "./values.js";
|
|
28
|
+
/** A declared field: its type, or its type with words for whoever reads it. */
|
|
29
|
+
export type BundleFieldDefinition = CascadeType | Readonly<{
|
|
30
|
+
type: CascadeType;
|
|
31
|
+
description?: string;
|
|
32
|
+
}>;
|
|
33
|
+
/** A producer's declaration: field name to type. */
|
|
34
|
+
export type BundleFieldDefinitions = Readonly<Record<string, BundleFieldDefinition>>;
|
|
35
|
+
export type Bundle = Readonly<Record<string, unknown>>;
|
|
36
|
+
/** The type a field declaration names. */
|
|
37
|
+
export declare function bundleFieldType(definition: BundleFieldDefinition | undefined): string;
|
|
38
|
+
/** The description a field declaration carries, if any. */
|
|
39
|
+
export declare function bundleFieldDescription(definition: BundleFieldDefinition | undefined): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Whether two bundle ports may be wired. A port without a schema is generic and
|
|
42
|
+
* takes any bundle; two named schemas must agree.
|
|
43
|
+
*/
|
|
44
|
+
export declare function bundleSchemasCompatible(output: string | undefined, input: string | undefined): boolean;
|
|
45
|
+
/** True for a value a bundle port can carry: a plain, non-array object. */
|
|
46
|
+
export declare function isBundleValue(value: unknown): value is Bundle;
|
|
47
|
+
/**
|
|
48
|
+
* A field path as an expression writes it: `format.dpi` is field `dpi` of the
|
|
49
|
+
* port `format`. Only the first dot splits, so a field name never needs
|
|
50
|
+
* escaping. Null when there is no dot to split on.
|
|
51
|
+
*/
|
|
52
|
+
export declare function splitBundlePath(name: string): {
|
|
53
|
+
port: string;
|
|
54
|
+
field: string;
|
|
55
|
+
} | null;
|
|
56
|
+
/**
|
|
57
|
+
* A bundle narrowed to the fields a consumer declared. A field the producer
|
|
58
|
+
* did not set is absent rather than `undefined`, the same as on the full value.
|
|
59
|
+
*/
|
|
60
|
+
export declare function selectBundleFields(value: unknown, fields: readonly string[]): Bundle | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* The closest candidate to a misspelt name, for "did you mean". Edit distance
|
|
63
|
+
* with a threshold that scales with the length, so `dpu` finds `dpi` and
|
|
64
|
+
* `colour` does not find `count`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function suggestName(name: string, candidates: Iterable<string>): string | undefined;
|
|
67
|
+
/** "unknown field X — did you mean Y? (fields: a, b, c)" */
|
|
68
|
+
export declare function unknownFieldMessage(field: string, known: readonly string[], where: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* What is wrong with a value a producer committed to a bundle port, or null.
|
|
71
|
+
* With declared fields, every key must be declared; a declared field may be
|
|
72
|
+
* absent. Field types are not checked here; the runtime does that per type.
|
|
73
|
+
*/
|
|
74
|
+
export declare function bundleValueProblem(value: unknown, fields: BundleFieldDefinitions | undefined): string | null;
|
|
75
|
+
//# sourceMappingURL=bundle.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** The type a field declaration names. */
|
|
2
|
+
export function bundleFieldType(definition) {
|
|
3
|
+
if (!definition)
|
|
4
|
+
return "any";
|
|
5
|
+
return typeof definition === "string" ? definition : definition.type;
|
|
6
|
+
}
|
|
7
|
+
/** The description a field declaration carries, if any. */
|
|
8
|
+
export function bundleFieldDescription(definition) {
|
|
9
|
+
return typeof definition === "object" && definition ? definition.description : undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Whether two bundle ports may be wired. A port without a schema is generic and
|
|
13
|
+
* takes any bundle; two named schemas must agree.
|
|
14
|
+
*/
|
|
15
|
+
export function bundleSchemasCompatible(output, input) {
|
|
16
|
+
return !output || !input || output === input;
|
|
17
|
+
}
|
|
18
|
+
/** True for a value a bundle port can carry: a plain, non-array object. */
|
|
19
|
+
export function isBundleValue(value) {
|
|
20
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
21
|
+
return false;
|
|
22
|
+
const prototype = Object.getPrototypeOf(value);
|
|
23
|
+
return prototype === Object.prototype || prototype === null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A field path as an expression writes it: `format.dpi` is field `dpi` of the
|
|
27
|
+
* port `format`. Only the first dot splits, so a field name never needs
|
|
28
|
+
* escaping. Null when there is no dot to split on.
|
|
29
|
+
*/
|
|
30
|
+
export function splitBundlePath(name) {
|
|
31
|
+
const dot = name.indexOf(".");
|
|
32
|
+
if (dot <= 0 || dot === name.length - 1)
|
|
33
|
+
return null;
|
|
34
|
+
return { port: name.slice(0, dot), field: name.slice(dot + 1) };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A bundle narrowed to the fields a consumer declared. A field the producer
|
|
38
|
+
* did not set is absent rather than `undefined`, the same as on the full value.
|
|
39
|
+
*/
|
|
40
|
+
export function selectBundleFields(value, fields) {
|
|
41
|
+
if (!isBundleValue(value))
|
|
42
|
+
return value;
|
|
43
|
+
const selected = {};
|
|
44
|
+
for (const field of fields)
|
|
45
|
+
if (Object.prototype.hasOwnProperty.call(value, field))
|
|
46
|
+
selected[field] = value[field];
|
|
47
|
+
return Object.freeze(selected);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The closest candidate to a misspelt name, for "did you mean". Edit distance
|
|
51
|
+
* with a threshold that scales with the length, so `dpu` finds `dpi` and
|
|
52
|
+
* `colour` does not find `count`.
|
|
53
|
+
*/
|
|
54
|
+
export function suggestName(name, candidates) {
|
|
55
|
+
let best;
|
|
56
|
+
let bestDistance = Infinity;
|
|
57
|
+
const lower = name.toLowerCase();
|
|
58
|
+
for (const candidate of candidates) {
|
|
59
|
+
const distance = editDistance(lower, candidate.toLowerCase());
|
|
60
|
+
if (distance < bestDistance) {
|
|
61
|
+
best = candidate;
|
|
62
|
+
bestDistance = distance;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (best === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
const limit = Math.max(1, Math.floor(Math.max(name.length, best.length) / 3));
|
|
68
|
+
return bestDistance <= limit ? best : undefined;
|
|
69
|
+
}
|
|
70
|
+
/** "unknown field X — did you mean Y? (fields: a, b, c)" */
|
|
71
|
+
export function unknownFieldMessage(field, known, where) {
|
|
72
|
+
const suggestion = suggestName(field, known);
|
|
73
|
+
return `${where} reads unknown bundle field "${field}"`
|
|
74
|
+
+ (suggestion ? ` — did you mean "${suggestion}"?` : "")
|
|
75
|
+
+ (known.length ? ` (fields: ${known.join(", ")})` : " (the bundle declares no fields)");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* What is wrong with a value a producer committed to a bundle port, or null.
|
|
79
|
+
* With declared fields, every key must be declared; a declared field may be
|
|
80
|
+
* absent. Field types are not checked here; the runtime does that per type.
|
|
81
|
+
*/
|
|
82
|
+
export function bundleValueProblem(value, fields) {
|
|
83
|
+
if (value === undefined || value === null)
|
|
84
|
+
return null;
|
|
85
|
+
if (!isBundleValue(value))
|
|
86
|
+
return "a bundle must be a plain object of fields";
|
|
87
|
+
if (!fields)
|
|
88
|
+
return null;
|
|
89
|
+
const known = Object.keys(fields);
|
|
90
|
+
for (const key of Object.keys(value))
|
|
91
|
+
if (!Object.prototype.hasOwnProperty.call(fields, key)) {
|
|
92
|
+
const suggestion = suggestName(key, known);
|
|
93
|
+
return `sets undeclared bundle field "${key}"`
|
|
94
|
+
+ (suggestion ? ` — did you mean "${suggestion}"?` : "")
|
|
95
|
+
+ ` (declared: ${known.join(", ") || "none"})`;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function editDistance(a, b) {
|
|
100
|
+
const previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
101
|
+
for (let i = 1; i <= a.length; i++) {
|
|
102
|
+
let diagonal = previous[0];
|
|
103
|
+
previous[0] = i;
|
|
104
|
+
for (let j = 1; j <= b.length; j++) {
|
|
105
|
+
const above = previous[j];
|
|
106
|
+
previous[j] = Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
107
|
+
diagonal = above;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return previous[b.length];
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=bundle.js.map
|