@field/cascade 0.10.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 +20 -3
- package/CHANGELOG.md +187 -0
- package/README.md +1 -2
- package/dist/assets/{CodeEditor-DSZBpzdy.js → CodeEditor-4O7FQ__R.js} +4 -4
- package/dist/assets/{CodePanel-sY6sZa7B.js → CodePanel-BU-autmq.js} +2 -2
- package/dist/assets/{CustomNodeDialog-BRKnqZhp.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
- package/dist/assets/{OpenGraphDialog-ZsgL-iwr.js → OpenGraphDialog-BwbRlC0X.js} +1 -1
- package/dist/assets/{ProjectSettings-Cw17nNe3.js → ProjectSettings-DLOXLxcA.js} +1 -1
- package/dist/assets/{SettingsDialog-p25h3tj0.js → SettingsDialog-Bz8JxISH.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-DzFLUotB.js → VersionHistoryDialog-D5Z0nJ5-.js} +1 -1
- package/dist/assets/{core-BICrEt0y.js → core-Yqi0Ni61.js} +1 -1
- package/dist/assets/{cssMode-D7TXw57I.js → cssMode-CxUzc4vp.js} +1 -1
- package/dist/assets/{definitions-DUww2Rt-.js → definitions-GreEMXLV.js} +1 -1
- package/dist/assets/{editor.api-DvkCO3ko.js → editor.api-BNFb25CX.js} +1 -1
- package/dist/assets/{freemarker2-BA2lxsF1.js → freemarker2-0ZDnWxeM.js} +1 -1
- package/dist/assets/geo-u0n_jOsD.js +1 -0
- package/dist/assets/{handlebars-BcDiyPd8.js → handlebars-Cy18MoaD.js} +1 -1
- package/dist/assets/{html-C7O4TyhX.js → html-B-2Dx7nq.js} +1 -1
- package/dist/assets/{htmlMode-WaDGF3Mj.js → htmlMode-DUAc9B_d.js} +1 -1
- package/dist/assets/{image-DR4oeyJB.js → image-cAa-Nu9O.js} +1 -1
- package/dist/assets/{index-DyvDC6pS.js → index-5ZtDAuy2.js} +42 -41
- package/dist/assets/{javascript-CpIEK7tz.js → javascript-DeQsDqP5.js} +1 -1
- package/dist/assets/{jsonMode-B2FltW0f.js → jsonMode-BiFwJVSk.js} +1 -1
- package/dist/assets/{liquid-CN6FFHZE.js → liquid-BLoHH68a.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-QhV9XtLp.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
- package/dist/assets/math-DioIQOPJ.js +1 -0
- package/dist/assets/{mdx-DE89pafV.js → mdx-CFXQVNfs.js} +1 -1
- package/dist/assets/pop-DC_TQG9t.js +1 -0
- package/dist/assets/{python-ByTllTB9.js → python-Cf7S7p2E.js} +1 -1
- package/dist/assets/{razor-USYlRRCE.js → razor-CMHBcG-g.js} +1 -1
- package/dist/assets/{tsMode-BXpN8iUz.js → tsMode-BEih0GDj.js} +1 -1
- package/dist/assets/{typescript-DInBjGQQ.js → typescript-COEKYD_a.js} +1 -1
- package/dist/assets/{workers-C7Ye5B7D.js → workers-BNhAhMNn.js} +1 -1
- package/dist/assets/{xml-U0Uocqyo.js → xml-CPiMpnPl.js} +1 -1
- package/dist/assets/{yaml-CBw_PVYf.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 +100 -30756
- 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 +4 -1
- 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/documentFormat.d.ts +18 -0
- package/dist/contracts/documentFormat.js +195 -0
- package/dist/contracts/editor.d.ts +17 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/image.d.ts +141 -0
- package/dist/image.js +127 -0
- package/dist/index.html +1 -1
- package/dist/player/editor.js +32 -7
- 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/stage.js +4 -1
- package/doc/CLI_REFERENCE.md +73 -2
- package/doc/EDITOR_EXPORT.md +11 -2
- package/doc/MCP.md +17 -1
- package/doc/NODE_REFERENCE.md +142 -0
- package/doc/PROJECT_AUTHORING.md +127 -7
- package/package.json +16 -4
- package/dist/assets/ExportDialog-C7dVGua4.js +0 -358
- package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
- package/dist/assets/geo-C2RHOVHC.js +0 -1
- package/dist/assets/math-DaDEgWKt.js +0 -1
- package/dist/assets/pop-pLH7L0PW.js +0 -1
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalFuturePath,
|
|
3
|
+
createBrowserRuntimeHost,
|
|
4
|
+
exists,
|
|
5
|
+
inside,
|
|
6
|
+
playerFile,
|
|
7
|
+
staticRuntimePlugin
|
|
8
|
+
} from "./chunk-3JTJHF7V.js";
|
|
9
|
+
import {
|
|
10
|
+
PREFLIGHT_WARNING_CODES,
|
|
11
|
+
cookProjectGraph,
|
|
12
|
+
createRuntime,
|
|
13
|
+
installProjectIo,
|
|
14
|
+
moduleId,
|
|
15
|
+
projectModuleFile,
|
|
16
|
+
validateNodeModuleArchitecture
|
|
17
|
+
} from "./chunk-EUIAC3U3.js";
|
|
18
|
+
import {
|
|
19
|
+
builtinNodeRegistration
|
|
20
|
+
} from "./chunk-36P5K3SD.js";
|
|
21
|
+
import {
|
|
22
|
+
ProjectRoot,
|
|
23
|
+
extractNodeDefinition
|
|
24
|
+
} from "./chunk-RQZ4SX5R.js";
|
|
25
|
+
import {
|
|
26
|
+
installHeadlessCanvas
|
|
27
|
+
} from "./chunk-WIH4TEIS.js";
|
|
28
|
+
|
|
29
|
+
// src/cli/editorCommands.ts
|
|
30
|
+
import fs2 from "node:fs/promises";
|
|
31
|
+
import path2 from "node:path";
|
|
32
|
+
|
|
33
|
+
// packages/runtime/src/editor/index.ts
|
|
34
|
+
var SCENE_FORMAT = "cascade.scene";
|
|
35
|
+
var platform = globalThis;
|
|
36
|
+
function connectionEnds(connection) {
|
|
37
|
+
if (Array.isArray(connection)) {
|
|
38
|
+
const [source, target] = connection;
|
|
39
|
+
return { source: source[0], sourcePort: source[2], target: target[0], targetPort: target[2] };
|
|
40
|
+
}
|
|
41
|
+
const object = connection;
|
|
42
|
+
return {
|
|
43
|
+
source: object.source.nodeId,
|
|
44
|
+
sourcePort: object.source.outputName,
|
|
45
|
+
target: object.target.nodeId,
|
|
46
|
+
targetPort: object.target.inputName
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function requireEditor(document) {
|
|
50
|
+
const editor = document.editor;
|
|
51
|
+
if (!editor || typeof editor !== "object" || typeof editor.id !== "string" || !editor.id)
|
|
52
|
+
throw new Error('This graph declares no editor. Add an "editor" section with an id (see doc/EDITOR_EXPORT.md).');
|
|
53
|
+
return editor;
|
|
54
|
+
}
|
|
55
|
+
function validateEditor(document) {
|
|
56
|
+
const out = [];
|
|
57
|
+
const error = (code, message) => out.push({ severity: "error", code, message });
|
|
58
|
+
const warn = (code, message) => out.push({ severity: "warning", code, message });
|
|
59
|
+
const editor = document.editor;
|
|
60
|
+
if (!editor) return [{ severity: "error", code: "editor/missing", message: "The graph has no editor section" }];
|
|
61
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(editor.id ?? ""))
|
|
62
|
+
error("editor/id", `editor.id must be a plain identifier, got ${JSON.stringify(editor.id)}`);
|
|
63
|
+
const nodes = new Map(document.nodes.map((node) => [node.id, node]));
|
|
64
|
+
const ids = /* @__PURE__ */ new Set();
|
|
65
|
+
for (const parameter of editor.parameters ?? []) {
|
|
66
|
+
if (ids.has(parameter.id)) error("editor/duplicate-parameter", `Parameter id ${parameter.id} is used twice`);
|
|
67
|
+
ids.add(parameter.id);
|
|
68
|
+
if (!nodes.has(parameter.node))
|
|
69
|
+
error("editor/parameter-node", `Parameter ${parameter.id} targets missing node ${parameter.node}`);
|
|
70
|
+
}
|
|
71
|
+
for (const parameter of editor.parameters ?? [])
|
|
72
|
+
for (const condition of conditionsOf(parameter))
|
|
73
|
+
if (!ids.has(condition.param))
|
|
74
|
+
warn("editor/visible-when", `Parameter ${parameter.id} is shown when unknown parameter ${condition.param} matches`);
|
|
75
|
+
const outputIds = /* @__PURE__ */ new Set();
|
|
76
|
+
let views = 0;
|
|
77
|
+
for (const output of editor.outputs ?? []) {
|
|
78
|
+
if (outputIds.has(output.id)) error("editor/duplicate-output", `Output id ${output.id} is used twice`);
|
|
79
|
+
outputIds.add(output.id);
|
|
80
|
+
if (!nodes.has(output.node)) error("editor/output-node", `Output ${output.id} reads missing node ${output.node}`);
|
|
81
|
+
if (output.view) views++;
|
|
82
|
+
for (const id of output.parameters ?? [])
|
|
83
|
+
if (!ids.has(id)) error("editor/output-parameter", `Output ${output.id} shows unknown parameter ${id}`);
|
|
84
|
+
}
|
|
85
|
+
if (!(editor.outputs ?? []).length) error("editor/no-outputs", "The editor declares no outputs");
|
|
86
|
+
if (views > 1) warn("editor/views", "More than one output sets view; the first is shown");
|
|
87
|
+
for (const preset of editor.presets ?? [])
|
|
88
|
+
for (const key of Object.keys(preset.values ?? {}))
|
|
89
|
+
if (!ids.has(key)) warn("editor/preset-parameter", `Preset ${preset.id} sets unknown parameter ${key}`);
|
|
90
|
+
for (const id of editor.peek ?? [])
|
|
91
|
+
if (!ids.has(id)) warn("editor/peek-parameter", `peek names unknown parameter ${id}`);
|
|
92
|
+
for (const id of Object.keys(editor.draft ?? {}))
|
|
93
|
+
if (!ids.has(id)) warn("editor/draft-parameter", `draft sets unknown parameter ${id}`);
|
|
94
|
+
for (const frozen of editor.freeze ?? [])
|
|
95
|
+
if (!nodes.has(frozen.node)) error("editor/freeze-node", `Frozen source ${frozen.node} is not in the graph`);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function selectEditorSubgraph(document) {
|
|
99
|
+
const editor = requireEditor(document);
|
|
100
|
+
const frozen = new Set((editor.freeze ?? []).map((item) => item.node));
|
|
101
|
+
const children = /* @__PURE__ */ new Map();
|
|
102
|
+
const parents = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const node of document.nodes) {
|
|
104
|
+
if (!node.parent) continue;
|
|
105
|
+
parents.set(node.id, node.parent);
|
|
106
|
+
children.set(node.parent, [...children.get(node.parent) ?? [], node.id]);
|
|
107
|
+
}
|
|
108
|
+
const upstream = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const connection of document.connections ?? []) {
|
|
110
|
+
const { source, target } = connectionEnds(connection);
|
|
111
|
+
upstream.set(target, [...upstream.get(target) ?? [], source]);
|
|
112
|
+
}
|
|
113
|
+
const known = new Set(document.nodes.map((node) => node.id));
|
|
114
|
+
const selected = /* @__PURE__ */ new Set();
|
|
115
|
+
const queue = [
|
|
116
|
+
...(editor.outputs ?? []).map((output) => output.node),
|
|
117
|
+
...(editor.parameters ?? []).map((parameter) => parameter.node)
|
|
118
|
+
].filter((id) => known.has(id));
|
|
119
|
+
while (queue.length) {
|
|
120
|
+
const id = queue.pop();
|
|
121
|
+
if (selected.has(id)) continue;
|
|
122
|
+
selected.add(id);
|
|
123
|
+
const parent = parents.get(id);
|
|
124
|
+
if (parent) queue.push(parent);
|
|
125
|
+
if (frozen.has(id)) continue;
|
|
126
|
+
queue.push(...children.get(id) ?? [], ...upstream.get(id) ?? []);
|
|
127
|
+
}
|
|
128
|
+
return selected;
|
|
129
|
+
}
|
|
130
|
+
function editorGraphVersion(document) {
|
|
131
|
+
const editor = requireEditor(document);
|
|
132
|
+
const selected = selectEditorSubgraph(document);
|
|
133
|
+
const frozen = new Set((editor.freeze ?? []).map((item) => item.node));
|
|
134
|
+
const nodes = document.nodes.filter((node) => selected.has(node.id)).map((node) => [node.id, frozen.has(node.id) ? "frozen" : moduleOf(node), node.parent ?? ""]).sort((a, b) => String(a[0]).localeCompare(String(b[0])));
|
|
135
|
+
const connections = (document.connections ?? []).map(connectionEnds).filter((end) => selected.has(end.source) && selected.has(end.target) && !frozen.has(end.target)).map((end) => `${end.source}.${frozen.has(end.source) ? "" : end.sourcePort ?? ""}>${end.target}.${end.targetPort ?? ""}`).sort();
|
|
136
|
+
const parameters = (editor.parameters ?? []).map((parameter) => [parameter.id, parameter.node, parameter.prop, controlOf(parameter)]).sort((a, b) => String(a[0]).localeCompare(String(b[0])));
|
|
137
|
+
return `h${hash(JSON.stringify([editor.id, parameters, nodes, connections]))}`;
|
|
138
|
+
}
|
|
139
|
+
function publishedValues(document) {
|
|
140
|
+
const editor = requireEditor(document);
|
|
141
|
+
const nodes = new Map(document.nodes.map((node) => [node.id, node]));
|
|
142
|
+
const values = {};
|
|
143
|
+
for (const parameter of editor.parameters ?? []) {
|
|
144
|
+
const stored = propsOf(nodes.get(parameter.node))[parameter.prop];
|
|
145
|
+
values[parameter.id] = isBinding(stored) ? stored.value : stored;
|
|
146
|
+
}
|
|
147
|
+
return values;
|
|
148
|
+
}
|
|
149
|
+
function parseScene(input) {
|
|
150
|
+
const value = typeof input === "string" ? JSON.parse(input) : input;
|
|
151
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("A scene must be a JSON object");
|
|
152
|
+
const scene = value;
|
|
153
|
+
if (scene.format !== SCENE_FORMAT) throw new Error(`Not a Cascade scene (format ${JSON.stringify(scene.format)})`);
|
|
154
|
+
if (scene.version !== 1) throw new Error(`Unsupported scene version ${JSON.stringify(scene.version)}`);
|
|
155
|
+
if (!scene.graph || typeof scene.graph.id !== "string") throw new Error("A scene must name its graph");
|
|
156
|
+
if (!scene.values || typeof scene.values !== "object" || Array.isArray(scene.values))
|
|
157
|
+
throw new Error("A scene must carry a values object");
|
|
158
|
+
for (const [id, file] of Object.entries(scene.files ?? {})) {
|
|
159
|
+
if (!file || typeof file.name !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number")
|
|
160
|
+
throw new Error(`Scene file ${id} needs a name, a size and a sha256`);
|
|
161
|
+
if (file.data !== void 0 && typeof file.data !== "string") throw new Error(`Scene file ${id} data must be base64`);
|
|
162
|
+
}
|
|
163
|
+
return scene;
|
|
164
|
+
}
|
|
165
|
+
function resolveScene(document, sceneInput) {
|
|
166
|
+
const scene = parseScene(sceneInput);
|
|
167
|
+
const editor = requireEditor(document);
|
|
168
|
+
const warnings = [];
|
|
169
|
+
if (scene.graph.id !== editor.id)
|
|
170
|
+
warnings.push(`Scene was made for "${scene.graph.id}", not "${editor.id}"; matching parameters are applied`);
|
|
171
|
+
else if (scene.graph.version && scene.graph.version !== editorGraphVersion(document))
|
|
172
|
+
warnings.push("Scene was saved against another version of this graph; matching parameters are applied");
|
|
173
|
+
const parameters = new Map((editor.parameters ?? []).map((parameter) => [parameter.id, parameter]));
|
|
174
|
+
for (const id of Object.keys(scene.values))
|
|
175
|
+
if (!parameters.has(id)) warnings.push(`Unknown parameter "${id}" ignored`);
|
|
176
|
+
for (const id of Object.keys(scene.files ?? {}))
|
|
177
|
+
if (!parameters.has(id)) warnings.push(`Unknown file parameter "${id}" ignored`);
|
|
178
|
+
const current = publishedValues(document);
|
|
179
|
+
const values = {};
|
|
180
|
+
const files = {};
|
|
181
|
+
for (const parameter of parameters.values()) {
|
|
182
|
+
if (controlOf(parameter, current[parameter.id]) === "file") {
|
|
183
|
+
const file = scene.files?.[parameter.id];
|
|
184
|
+
if (file) files[parameter.id] = file;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!(parameter.id in scene.values)) continue;
|
|
188
|
+
const coerced = coerceValue(parameter, scene.values[parameter.id], current[parameter.id]);
|
|
189
|
+
if (coerced.warning) warnings.push(coerced.warning);
|
|
190
|
+
if (coerced.value !== void 0) values[parameter.id] = coerced.value;
|
|
191
|
+
}
|
|
192
|
+
return { values, files, warnings };
|
|
193
|
+
}
|
|
194
|
+
function applyScene(document, sceneInput, options = {}) {
|
|
195
|
+
const editor = requireEditor(document);
|
|
196
|
+
const resolved = resolveScene(document, sceneInput);
|
|
197
|
+
const warnings = [...resolved.warnings];
|
|
198
|
+
const updates = { ...resolved.values };
|
|
199
|
+
for (const parameter of editor.parameters ?? []) {
|
|
200
|
+
const file = resolved.files[parameter.id];
|
|
201
|
+
if (!file) continue;
|
|
202
|
+
const value = options.resolveFile?.(parameter, file);
|
|
203
|
+
if (value === void 0) warnings.push(`${labelOf(parameter)}: "${file.name}" is not available here; the graph's file is kept`);
|
|
204
|
+
else updates[parameter.id] = value;
|
|
205
|
+
}
|
|
206
|
+
const result = setPublishedValues(document, updates);
|
|
207
|
+
return { document: result, values: publishedValues(result), warnings };
|
|
208
|
+
}
|
|
209
|
+
function setPublishedValues(document, updates) {
|
|
210
|
+
const editor = requireEditor(document);
|
|
211
|
+
const writes = /* @__PURE__ */ new Map();
|
|
212
|
+
for (const parameter of editor.parameters ?? []) {
|
|
213
|
+
if (!(parameter.id in updates) || updates[parameter.id] === void 0) continue;
|
|
214
|
+
const props = writes.get(parameter.node) ?? {};
|
|
215
|
+
props[parameter.prop] = updates[parameter.id];
|
|
216
|
+
writes.set(parameter.node, props);
|
|
217
|
+
}
|
|
218
|
+
const nodes = document.nodes.map((node) => {
|
|
219
|
+
const props = writes.get(node.id);
|
|
220
|
+
if (!props) return node;
|
|
221
|
+
const merged = { ...propsOf(node) };
|
|
222
|
+
for (const [name, value] of Object.entries(props)) {
|
|
223
|
+
const stored = merged[name];
|
|
224
|
+
merged[name] = isBinding(stored) ? { ...stored, value } : value;
|
|
225
|
+
}
|
|
226
|
+
return { ...node, props: merged };
|
|
227
|
+
});
|
|
228
|
+
return { ...document, nodes };
|
|
229
|
+
}
|
|
230
|
+
function conditionsOf(parameter) {
|
|
231
|
+
const condition = parameter.visibleWhen;
|
|
232
|
+
if (!condition) return [];
|
|
233
|
+
return Array.isArray(condition) ? condition : [condition];
|
|
234
|
+
}
|
|
235
|
+
function coerceValue(parameter, value, current) {
|
|
236
|
+
const label = labelOf(parameter);
|
|
237
|
+
const reject = (what) => ({ warning: `${label}: ${what}; kept the graph's value` });
|
|
238
|
+
if (value === void 0) return {};
|
|
239
|
+
const control = controlOf(parameter, current);
|
|
240
|
+
switch (control) {
|
|
241
|
+
case "number":
|
|
242
|
+
case "slider":
|
|
243
|
+
return typeof value === "number" && Number.isFinite(value) ? { value } : reject("expected a number");
|
|
244
|
+
case "int":
|
|
245
|
+
case "seed":
|
|
246
|
+
return typeof value === "number" && Number.isFinite(value) ? { value: Math.round(value) } : reject("expected a whole number");
|
|
247
|
+
case "bool":
|
|
248
|
+
return typeof value === "boolean" ? { value } : reject("expected true or false");
|
|
249
|
+
case "text":
|
|
250
|
+
case "textarea":
|
|
251
|
+
return typeof value === "string" ? { value } : reject("expected text");
|
|
252
|
+
case "select": {
|
|
253
|
+
const options = (parameter.options ?? []).map(
|
|
254
|
+
(option) => option !== null && typeof option === "object" && "value" in option ? option.value : option
|
|
255
|
+
);
|
|
256
|
+
return options.some((option) => Object.is(option, value)) ? { value } : reject(`${JSON.stringify(value)} is not one of its options`);
|
|
257
|
+
}
|
|
258
|
+
case "color":
|
|
259
|
+
return typeof value === "string" || Array.isArray(value) && value.every((part) => typeof part === "number") ? { value } : reject("expected a colour");
|
|
260
|
+
case "vec2":
|
|
261
|
+
return Array.isArray(value) && value.length === 2 && value.every((part) => typeof part === "number" && Number.isFinite(part)) ? { value } : reject("expected two numbers");
|
|
262
|
+
default:
|
|
263
|
+
return { value };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function controlOf(parameter, current, type) {
|
|
267
|
+
if (parameter.control) return parameter.control;
|
|
268
|
+
if (parameter.options?.length) return "select";
|
|
269
|
+
const byType = {
|
|
270
|
+
float: parameter.min !== void 0 && parameter.max !== void 0 ? "slider" : "number",
|
|
271
|
+
int: "int",
|
|
272
|
+
bool: "bool",
|
|
273
|
+
string: "text",
|
|
274
|
+
color: "color",
|
|
275
|
+
vec2: "vec2",
|
|
276
|
+
vec2i: "vec2",
|
|
277
|
+
image: "file",
|
|
278
|
+
asset: "file"
|
|
279
|
+
};
|
|
280
|
+
if (type && byType[type]) return byType[type];
|
|
281
|
+
if (typeof current === "boolean") return "bool";
|
|
282
|
+
if (typeof current === "string") return "text";
|
|
283
|
+
if (Array.isArray(current) && current.length === 2) return "vec2";
|
|
284
|
+
if (current && typeof current === "object" && "path" in current) return "file";
|
|
285
|
+
return typeof current === "number" ? "number" : "any";
|
|
286
|
+
}
|
|
287
|
+
function fromBase64(text) {
|
|
288
|
+
const binary = platform.atob(text);
|
|
289
|
+
const bytes = new Uint8Array(binary.length);
|
|
290
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
291
|
+
return bytes;
|
|
292
|
+
}
|
|
293
|
+
function labelOf(parameter) {
|
|
294
|
+
return parameter.label ?? parameter.id;
|
|
295
|
+
}
|
|
296
|
+
function moduleOf(node) {
|
|
297
|
+
return node.module ?? node.type ?? "";
|
|
298
|
+
}
|
|
299
|
+
function propsOf(node) {
|
|
300
|
+
const props = node?.props;
|
|
301
|
+
return props && typeof props === "object" ? props : {};
|
|
302
|
+
}
|
|
303
|
+
function isBinding(value) {
|
|
304
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || !("value" in value)) return false;
|
|
305
|
+
return Object.keys(value).every((key) => key === "value" || key === "expression" || key === "channel");
|
|
306
|
+
}
|
|
307
|
+
function hash(text) {
|
|
308
|
+
let h = 0xcbf29ce484222325n;
|
|
309
|
+
const prime = 0x100000001b3n;
|
|
310
|
+
for (let index = 0; index < text.length; index++) {
|
|
311
|
+
h ^= BigInt(text.charCodeAt(index));
|
|
312
|
+
h = h * prime & 0xffffffffffffffffn;
|
|
313
|
+
}
|
|
314
|
+
return h.toString(16).padStart(16, "0");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/cli/editorExport.ts
|
|
318
|
+
import fs from "node:fs/promises";
|
|
319
|
+
import os from "node:os";
|
|
320
|
+
import path from "node:path";
|
|
321
|
+
import { execFile } from "node:child_process";
|
|
322
|
+
import { createRequire } from "node:module";
|
|
323
|
+
import { promisify } from "node:util";
|
|
324
|
+
import { build } from "esbuild";
|
|
325
|
+
import ts from "typescript";
|
|
326
|
+
var run = promisify(execFile);
|
|
327
|
+
var PERSONAL = /(note|transcript|calendar|diary|journal|whoop|weight|sleep|hrv|heart|health|body|location|gps|email|phone|address|observation|caption|person|people)/i;
|
|
328
|
+
var COORDINATE = /^(lat|lon|lng|latitude|longitude)$/i;
|
|
329
|
+
var LIVE_CAPABILITIES = /* @__PURE__ */ new Set(["files", "python", "shell"]);
|
|
330
|
+
async function exportEditor(file, options = {}) {
|
|
331
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
332
|
+
const prepared = await prepareExport(file);
|
|
333
|
+
if (options.plan) return { plan: prepared.plan };
|
|
334
|
+
if (!options.out) throw new Error("cascade export requires --out <directory> (or --plan to review without building)");
|
|
335
|
+
const directory = await buildSite(prepared, options.out);
|
|
336
|
+
const plan = JSON.parse(await fs.readFile(path.join(directory, "export-manifest.json"), "utf8"));
|
|
337
|
+
let url;
|
|
338
|
+
if (options.deploy === "vercel") url = await deployVercel(directory, options.project ?? prepared.plan.editor.id, options.scope, log, options.public === true);
|
|
339
|
+
return { plan, directory, ...url ? { url } : {} };
|
|
340
|
+
}
|
|
341
|
+
async function prepareExport(file) {
|
|
342
|
+
const graphFile = path.resolve(file);
|
|
343
|
+
const project = new ProjectRoot(path.dirname(graphFile));
|
|
344
|
+
const source = JSON.parse(await fs.readFile(graphFile, "utf8"));
|
|
345
|
+
if (!Array.isArray(source.nodes)) throw new Error("Graph document must contain a nodes array");
|
|
346
|
+
const editor = requireEditor(source);
|
|
347
|
+
const problems = validateEditor(source).filter((item) => item.severity === "error").map((item) => item.message);
|
|
348
|
+
const warnings = validateEditor(source).filter((item) => item.severity === "warning").map((item) => item.message);
|
|
349
|
+
if (problems.length) throw new Error(`The editor declaration has errors:
|
|
350
|
+
${problems.map((line) => ` ${line}`).join("\n")}`);
|
|
351
|
+
const selected = selectEditorSubgraph(source);
|
|
352
|
+
const frozenNodes = new Map((editor.freeze ?? []).map((item) => [item.node, item]));
|
|
353
|
+
const nodes = source.nodes.filter((node) => selected.has(node.id));
|
|
354
|
+
const modules = [];
|
|
355
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
356
|
+
const notExportable = [];
|
|
357
|
+
const live = [];
|
|
358
|
+
const seen = /* @__PURE__ */ new Set();
|
|
359
|
+
for (const node of nodes) {
|
|
360
|
+
if (frozenNodes.has(node.id)) continue;
|
|
361
|
+
const id = moduleId(node);
|
|
362
|
+
const builtin = builtinNodeRegistration(id);
|
|
363
|
+
if (builtin) {
|
|
364
|
+
definitions.set(node.id, builtin.definition);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const resolved = await projectModuleFile(project.root, node, id);
|
|
368
|
+
if (!resolved.file) {
|
|
369
|
+
notExportable.push(`${node.id} (${id}: no project module found)`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const sourceFile = project.resolve(path.relative(project.root, resolved.file));
|
|
373
|
+
const code = await fs.readFile(sourceFile, "utf8");
|
|
374
|
+
const extracted = extractNodeDefinition(code, sourceFile, ts);
|
|
375
|
+
if (!extracted.ok) {
|
|
376
|
+
notExportable.push(`${node.id} (${id}: not a definition-v1 module)`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const definition = extracted.definition;
|
|
380
|
+
definitions.set(node.id, definition);
|
|
381
|
+
const capabilities = definition.capabilities ?? [];
|
|
382
|
+
if (capabilities.includes("python") || capabilities.includes("shell")) {
|
|
383
|
+
notExportable.push(`${node.id} (${id}: Python/shell node)`);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (definition.runsOn === "server" || definition.source === "live" || capabilities.some((name) => LIVE_CAPABILITIES.has(name))) {
|
|
387
|
+
live.push(`${node.id} (${id}${definition.source === "live" ? ", live source" : definition.runsOn === "server" ? ", server node" : ", reads files"})`);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const architecture = validateNodeModuleArchitecture(code, sourceFile, ts);
|
|
391
|
+
if (architecture.length) {
|
|
392
|
+
notExportable.push(...architecture.map((item) => `${node.id} (${id}: ${item.code}: ${item.message})`));
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (!seen.has(id)) {
|
|
396
|
+
seen.add(id);
|
|
397
|
+
modules.push({ id, file: sourceFile, definition });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const refusals = [];
|
|
401
|
+
if (notExportable.length) refusals.push(`Not exportable (only browser-runnable nodes ship; port these to TypeScript or freeze their output):
|
|
402
|
+
${notExportable.map((line) => ` ${line}`).join("\n")}`);
|
|
403
|
+
if (live.length) refusals.push(`Live data sources are still wired in; freeze them with \`cascade freeze\` or take them out of the published path:
|
|
404
|
+
${live.map((line) => ` ${line}`).join("\n")}`);
|
|
405
|
+
if (refusals.length) throw new Error(refusals.join("\n"));
|
|
406
|
+
const assets = /* @__PURE__ */ new Map();
|
|
407
|
+
const frozenPlan = [];
|
|
408
|
+
const personal = [];
|
|
409
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
410
|
+
for (const frozen of editor.freeze ?? []) {
|
|
411
|
+
if (!selected.has(frozen.node)) continue;
|
|
412
|
+
const snapshotPath = checkedAssetPath(frozen.snapshot, `snapshot for ${frozen.node}`);
|
|
413
|
+
const snapshot = JSON.parse(await fs.readFile(project.resolve(snapshotPath), "utf8"));
|
|
414
|
+
if (snapshot.format !== "cascade.frozen" || snapshot.version !== 1)
|
|
415
|
+
throw new Error(`${frozen.snapshot} is not a cascade freeze snapshot`);
|
|
416
|
+
const fields = snapshot.fields.map((field) => {
|
|
417
|
+
const flagged = (snapshot.personal ?? []).includes(field) || field.split(/[.[\]]/).some((part) => PERSONAL.test(part) || COORDINATE.test(part));
|
|
418
|
+
if (flagged) personal.push(`${frozen.node}: ${field}`);
|
|
419
|
+
return { field, personal: flagged };
|
|
420
|
+
});
|
|
421
|
+
frozenPlan.push({ node: frozen.node, snapshot: snapshotPath, fields });
|
|
422
|
+
for (const name of snapshot.files ?? []) await addAsset(project, assets, checkedAssetPath(name, `file of snapshot ${frozen.node}`), "frozen", snapshotPath);
|
|
423
|
+
const original = source.nodes.find((node) => node.id === frozen.node);
|
|
424
|
+
replacements.set(frozen.node, {
|
|
425
|
+
id: original.id,
|
|
426
|
+
module: "cascade.core.Frozen",
|
|
427
|
+
...original.parent ? { parent: original.parent } : {},
|
|
428
|
+
props: { value: snapshot.value }
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const shippedNodes = nodes.map((node) => replacements.get(node.id) ?? stripNode(node));
|
|
432
|
+
const connections = [];
|
|
433
|
+
for (const connection of source.connections ?? []) {
|
|
434
|
+
const ends = connectionEnds(connection);
|
|
435
|
+
if (!selected.has(ends.source) || !selected.has(ends.target) || frozenNodes.has(ends.target)) continue;
|
|
436
|
+
if (!frozenNodes.has(ends.source)) {
|
|
437
|
+
connections.push(connection);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
connections.push([[ends.source, 0, "value"], Array.isArray(connection) ? connection[1] : [ends.target, 0, ends.targetPort]]);
|
|
441
|
+
}
|
|
442
|
+
const shippedEditor = {
|
|
443
|
+
id: editor.id,
|
|
444
|
+
...editor.title ? { title: editor.title } : {},
|
|
445
|
+
...editor.description ? { description: editor.description } : {},
|
|
446
|
+
...editor.groups ? { groups: editor.groups } : {},
|
|
447
|
+
parameters: editor.parameters ?? [],
|
|
448
|
+
outputs: editor.outputs ?? [],
|
|
449
|
+
...editor.presets ? { presets: editor.presets } : {},
|
|
450
|
+
...editor.freeze ? { freeze: editor.freeze.filter((item) => selected.has(item.node)).map((item) => ({ node: item.node, output: item.output, snapshot: "" })) } : {},
|
|
451
|
+
...editor.panel ? { panel: { module: "", ...editor.panel.placement ? { placement: editor.panel.placement } : {} } } : {},
|
|
452
|
+
...editor.inspect === false ? { inspect: false } : {},
|
|
453
|
+
...editor.peek ? { peek: editor.peek } : {},
|
|
454
|
+
...editor.draft ? { draft: editor.draft } : {},
|
|
455
|
+
...editor.fonts?.length ? { fonts: editor.fonts } : {}
|
|
456
|
+
};
|
|
457
|
+
const document = {
|
|
458
|
+
version: source.version,
|
|
459
|
+
...typeof source.metadata?.name === "string" ? { metadata: { name: source.metadata.name } } : {},
|
|
460
|
+
nodes: shippedNodes,
|
|
461
|
+
connections,
|
|
462
|
+
editor: shippedEditor
|
|
463
|
+
};
|
|
464
|
+
const graphVersion = editorGraphVersion(document);
|
|
465
|
+
if (graphVersion !== editorGraphVersion(source))
|
|
466
|
+
throw new Error("Internal: the exported graph version differs from the source graph version");
|
|
467
|
+
for (const asset of editor.assets ?? []) await addAsset(project, assets, checkedAssetPath(asset, "editor.assets"), "asset", "editor.assets");
|
|
468
|
+
for (const font of editor.fonts ?? []) await addAsset(project, assets, checkedFont(font), "asset", "editor.fonts");
|
|
469
|
+
for (const literal of await literalAssets(document, modules)) await addAsset(project, assets, checkedAssetPath(literal.path, literal.where), "asset", literal.where);
|
|
470
|
+
for (const asset of assets.values()) await refuseIgnored(project.root, asset.name);
|
|
471
|
+
const planNodes = shippedNodes.map((node) => ({
|
|
472
|
+
id: node.id,
|
|
473
|
+
module: String(node.module ?? node.type ?? ""),
|
|
474
|
+
kind: replacements.has(node.id) ? "frozen" : builtinNodeRegistration(moduleId(node)) ? "builtin" : "project",
|
|
475
|
+
props: Object.keys(node.props ?? {})
|
|
476
|
+
}));
|
|
477
|
+
const files = [];
|
|
478
|
+
for (const asset of assets.values())
|
|
479
|
+
files.push({ path: `assets/${asset.name}`, size: (await fs.stat(asset.file)).size, from: asset.from === "editor.assets" ? asset.name : `${asset.name} (${asset.from})`, kind: asset.kind });
|
|
480
|
+
const panel = editor.panel?.module ? project.resolve(checkedAssetPath(editor.panel.module, "editor.panel.module")) : void 0;
|
|
481
|
+
const plan = {
|
|
482
|
+
format: "cascade.export-plan",
|
|
483
|
+
version: 1,
|
|
484
|
+
editor: { id: editor.id, ...editor.title ? { title: editor.title } : {}, graphVersion },
|
|
485
|
+
graph: path.basename(graphFile),
|
|
486
|
+
nodes: planNodes,
|
|
487
|
+
excluded: source.nodes.filter((node) => !selected.has(node.id)).map((node) => node.id),
|
|
488
|
+
parameters: (editor.parameters ?? []).map((item) => `${item.id} \u2192 ${item.node}.${item.prop}`),
|
|
489
|
+
outputs: (editor.outputs ?? []).map((item) => `${item.id} \u2192 ${item.node}.${item.port} (${item.format})`),
|
|
490
|
+
frozen: frozenPlan,
|
|
491
|
+
files,
|
|
492
|
+
personal,
|
|
493
|
+
warnings
|
|
494
|
+
};
|
|
495
|
+
return { plan, project, document, modules, assets: [...assets.values()].map(({ name, file: file2 }) => ({ name, file: file2 })), ...panel ? { panel } : {} };
|
|
496
|
+
}
|
|
497
|
+
function formatPlan(plan) {
|
|
498
|
+
const lines = [];
|
|
499
|
+
lines.push(`Editor ${plan.editor.title ?? plan.editor.id} (${plan.editor.id}, ${plan.editor.graphVersion}) from ${plan.graph}`);
|
|
500
|
+
lines.push("", `Nodes shipped (${plan.nodes.length}):`);
|
|
501
|
+
for (const node of plan.nodes) lines.push(` ${node.id} ${node.module}${node.kind === "frozen" ? " [frozen snapshot]" : ""}${node.props.length ? ` props: ${node.props.join(", ")}` : ""}`);
|
|
502
|
+
lines.push("", `Not shipped (${plan.excluded.length}): ${plan.excluded.join(", ") || "none"}`);
|
|
503
|
+
lines.push("", `Published parameters (${plan.parameters.length}):`, ...plan.parameters.map((line) => ` ${line}`));
|
|
504
|
+
lines.push("", "Outputs:", ...plan.outputs.map((line) => ` ${line}`));
|
|
505
|
+
if (plan.frozen.length) {
|
|
506
|
+
lines.push("", "Frozen sources:");
|
|
507
|
+
for (const frozen of plan.frozen) {
|
|
508
|
+
lines.push(` ${frozen.node} from ${frozen.snapshot}`);
|
|
509
|
+
for (const field of frozen.fields) lines.push(` ${field.field}${field.personal ? " PERSONAL" : ""}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
lines.push("", "Files:");
|
|
513
|
+
for (const file of plan.files) lines.push(` ${file.path} ${formatSize(file.size)} ${file.kind} from ${file.from}`);
|
|
514
|
+
if (plan.personal.length) lines.push("", "PERSONAL \u2014 check before sharing:", ...plan.personal.map((line) => ` ${line}`));
|
|
515
|
+
if (plan.warnings.length) lines.push("", "Warnings:", ...plan.warnings.map((line) => ` ${line}`));
|
|
516
|
+
return lines.join("\n");
|
|
517
|
+
}
|
|
518
|
+
async function buildSite(prepared, outOption) {
|
|
519
|
+
const { project, document, modules, assets, panel } = prepared;
|
|
520
|
+
const out = path.resolve(process.cwd(), outOption);
|
|
521
|
+
const canonicalOut = await canonicalFuturePath(out);
|
|
522
|
+
for (const sourceDirectory of ["assets", "nodes", "lib"]) {
|
|
523
|
+
const sourceRoot = await canonicalFuturePath(path.join(project.root, sourceDirectory));
|
|
524
|
+
if (inside(sourceRoot, canonicalOut)) throw new Error(`Export output cannot be inside the project's ${sourceDirectory} directory`);
|
|
525
|
+
}
|
|
526
|
+
if (inside(canonicalOut, await fs.realpath(project.root))) throw new Error("Export output cannot contain the project");
|
|
527
|
+
if (await exists(out)) {
|
|
528
|
+
if (!await exists(path.join(out, "export-manifest.json")))
|
|
529
|
+
throw new Error(`${out} exists and is not a previous export. Choose a new directory.`);
|
|
530
|
+
}
|
|
531
|
+
const plugins = [await staticRuntimePlugin(project.root), sveltePlugin(project.root)];
|
|
532
|
+
const editorEntry = await playerFile("editor");
|
|
533
|
+
const imports = modules.map((item, index) => `import { execute as execute${index} } from ${JSON.stringify(item.file)};`).join("\n");
|
|
534
|
+
const registrations = modules.map((item, index) => `{ kind:'definition-v1', moduleId:${JSON.stringify(item.id)}, definition:${JSON.stringify(item.definition)}, loadExecute:async()=>execute${index} }`).join(",");
|
|
535
|
+
const assetSource = assets.map((item) => `${JSON.stringify(item.name)}:new URL(${JSON.stringify(`./assets/${item.name.split("/").map(encodeURIComponent).join("/")}`)},import.meta.url).href`).join(",");
|
|
536
|
+
const panelImport = panel ? `import * as panel from ${JSON.stringify(panel)};` : "";
|
|
537
|
+
const engineEntry = await playerFile("editorEngine");
|
|
538
|
+
const fonts = fontSources(document.editor?.fonts ?? []);
|
|
539
|
+
const entry = `import { startEditor } from ${JSON.stringify(editorEntry)};
|
|
540
|
+
${panelImport}
|
|
541
|
+
${fonts.loader}
|
|
542
|
+
window.cascadeEditorReady = startEditor({document:${JSON.stringify(document)},worker:new URL('./editor-worker.js',import.meta.url),assets:{${assetSource}}${panel ? ",panel" : ""}});`;
|
|
543
|
+
const workerEntry = `import { serveEditorEngine } from ${JSON.stringify(engineEntry)};
|
|
544
|
+
${imports}
|
|
545
|
+
${fonts.loader}
|
|
546
|
+
serveEditorEngine([${registrations}]);`;
|
|
547
|
+
const bundle = (contents, sourcefile) => build({
|
|
548
|
+
bundle: true,
|
|
549
|
+
platform: "browser",
|
|
550
|
+
format: "esm",
|
|
551
|
+
target: "es2022",
|
|
552
|
+
write: false,
|
|
553
|
+
minify: true,
|
|
554
|
+
legalComments: "none",
|
|
555
|
+
logLevel: "silent",
|
|
556
|
+
plugins,
|
|
557
|
+
stdin: { contents, resolveDir: project.root, sourcefile }
|
|
558
|
+
});
|
|
559
|
+
const [result, workerResult] = await Promise.all([bundle(entry, "editor-entry.js"), bundle(workerEntry, "editor-worker-entry.js")]);
|
|
560
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
561
|
+
const staging = await fs.mkdtemp(path.join(path.dirname(out), ".cascade-export-"));
|
|
562
|
+
try {
|
|
563
|
+
const title = escapeHtml(document.editor?.title ?? document.editor?.id ?? "Cascade");
|
|
564
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,maximum-scale=1"><meta name="robots" content="noindex,nofollow"><meta name="referrer" content="no-referrer"><link rel="icon" href="data:,"><title>${title}</title><style>${fonts.css}html,body{margin:0;height:100%;background:#f4f4f2}@media (prefers-color-scheme:dark){html,body{background:#151515}}</style></head><body><script type="module" src="./editor.js"></script></body></html>`;
|
|
565
|
+
const files = [];
|
|
566
|
+
const write = async (name, data, kind, from) => {
|
|
567
|
+
await fs.mkdir(path.dirname(path.join(staging, name)), { recursive: true });
|
|
568
|
+
await fs.writeFile(path.join(staging, name), data);
|
|
569
|
+
files.push({ path: name, size: typeof data === "string" ? Buffer.byteLength(data) : data.byteLength, from, kind });
|
|
570
|
+
};
|
|
571
|
+
await write("index.html", html, "page", "generated");
|
|
572
|
+
await write("editor.js", result.outputFiles[0].contents, "code", "cascade editor shell");
|
|
573
|
+
await write("editor-worker.js", workerResult.outputFiles[0].contents, "code", "cascade runtime + project nodes");
|
|
574
|
+
for (const asset of assets) {
|
|
575
|
+
const target = path.join(staging, "assets", asset.name);
|
|
576
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
577
|
+
await fs.copyFile(asset.file, target);
|
|
578
|
+
}
|
|
579
|
+
await write("vercel.json", `${JSON.stringify(vercelConfig(), null, 2)}
|
|
580
|
+
`, "config", "generated");
|
|
581
|
+
const plan = { ...prepared.plan, files: [...files, ...prepared.plan.files] };
|
|
582
|
+
await fs.writeFile(path.join(staging, "export-manifest.json"), `${JSON.stringify(plan, null, 2)}
|
|
583
|
+
`);
|
|
584
|
+
await scanForLeaks(staging, project.root);
|
|
585
|
+
await fs.rm(out, { recursive: true, force: true });
|
|
586
|
+
await fs.rename(staging, out);
|
|
587
|
+
await fs.chmod(out, 493);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
await fs.rm(staging, { recursive: true, force: true });
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
function vercelConfig() {
|
|
595
|
+
return {
|
|
596
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
597
|
+
framework: null,
|
|
598
|
+
buildCommand: "",
|
|
599
|
+
installCommand: "",
|
|
600
|
+
outputDirectory: ".",
|
|
601
|
+
headers: [{
|
|
602
|
+
source: "/(.*)",
|
|
603
|
+
headers: [
|
|
604
|
+
{ key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data: blob:; worker-src 'self' blob:; connect-src 'self' blob: data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'" },
|
|
605
|
+
{ key: "Referrer-Policy", value: "no-referrer" },
|
|
606
|
+
{ key: "X-Robots-Tag", value: "noindex, nofollow" },
|
|
607
|
+
{ key: "X-Content-Type-Options", value: "nosniff" }
|
|
608
|
+
]
|
|
609
|
+
}]
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
var LEAK_PATTERNS = [
|
|
613
|
+
["libSQL/Turso URL", /libsql:\/\/|\.turso\.io/i],
|
|
614
|
+
["local path", /\/Users\/[A-Za-z]|\/home\/[a-z][a-z0-9_-]*\/|[A-Z]:\\\\Users\\\\|file:\/\//],
|
|
615
|
+
["home-relative path", /["'`\s(]~\/[A-Za-z._-]/],
|
|
616
|
+
["Observatory endpoint", /observatory\.field\.io|observatory-api|\/api\/observatory/i],
|
|
617
|
+
["bearer token", /Bearer\s+[A-Za-z0-9._~+/-]{16,}/],
|
|
618
|
+
["API key", /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{20,}|\bghp_[A-Za-z0-9]{20,}|\bgithub_pat_[A-Za-z0-9_]{20,}|\bxox[abprs]-[A-Za-z0-9-]{10,}|\bAKIA[0-9A-Z]{16}\b|\bAIza[0-9A-Za-z_-]{30,}/],
|
|
619
|
+
["JWT", /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\./],
|
|
620
|
+
["private key", /-----BEGIN [A-Z ]*PRIVATE KEY-----/],
|
|
621
|
+
["env file reference", /(?:^|[^A-Za-z0-9_])\.env(?:\.local|\.production|\.development)?["'`]/]
|
|
622
|
+
];
|
|
623
|
+
async function scanForLeaks(directory, projectRoot) {
|
|
624
|
+
const secrets = await secretValues(projectRoot);
|
|
625
|
+
const findings = [];
|
|
626
|
+
for (const file of await filesUnder(directory)) {
|
|
627
|
+
const name = path.relative(directory, file);
|
|
628
|
+
if (/(^|\/)\.env/.test(name) || /(^|\/)\.vercel\//.test(name)) {
|
|
629
|
+
findings.push(`${name}: environment or link file in the site`);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (/(^|\/)licensed\//.test(name)) {
|
|
633
|
+
findings.push(`${name}: licensed file in the site`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (!/\.(js|mjs|html|json|svg|txt|css|md|map|xml)$/i.test(name)) continue;
|
|
637
|
+
const text = await fs.readFile(file, "utf8");
|
|
638
|
+
for (const [label, pattern] of LEAK_PATTERNS) {
|
|
639
|
+
const match = pattern.exec(text);
|
|
640
|
+
if (match) findings.push(`${name}: ${label} near "${excerpt(text, match.index)}"`);
|
|
641
|
+
}
|
|
642
|
+
for (const [key, value] of secrets) if (text.includes(value)) findings.push(`${name}: the value of ${key}`);
|
|
643
|
+
}
|
|
644
|
+
if (findings.length) throw new Error(`The export was stopped: it would publish sensitive content.
|
|
645
|
+
${findings.map((line) => ` ${line}`).join("\n")}`);
|
|
646
|
+
}
|
|
647
|
+
async function secretValues(projectRoot) {
|
|
648
|
+
const secret = /(KEY|TOKEN|SECRET|PASSWORD|PASSWD|AUTH|CREDENTIAL|DATABASE_URL|DB_URL|_URL|_DSN)$/i;
|
|
649
|
+
const values = /* @__PURE__ */ new Map();
|
|
650
|
+
for (const [key, value] of Object.entries(process.env)) if (value && value.length >= 12 && secret.test(key)) values.set(key, value);
|
|
651
|
+
for (const entry of await fs.readdir(projectRoot).catch(() => [])) {
|
|
652
|
+
if (!entry.startsWith(".env")) continue;
|
|
653
|
+
const text = await fs.readFile(path.join(projectRoot, entry), "utf8").catch(() => "");
|
|
654
|
+
for (const line of text.split("\n")) {
|
|
655
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*["']?([^"'\n]*)["']?\s*$/.exec(line);
|
|
656
|
+
if (match && match[2].length >= 8) values.set(`${match[1]} (${entry})`, match[2]);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
values.set("your home directory", os.homedir());
|
|
660
|
+
return [...values];
|
|
661
|
+
}
|
|
662
|
+
async function deployVercel(directory, projectName, scope, log, intendedPublic) {
|
|
663
|
+
const team = scope ?? "field-io";
|
|
664
|
+
const scratch = await fs.mkdtemp(path.join(os.tmpdir(), "cascade-vercel-"));
|
|
665
|
+
try {
|
|
666
|
+
await run("vercel", ["link", "--yes", "--project", projectName, "--scope", team], { cwd: scratch });
|
|
667
|
+
const link = await fs.readFile(path.join(scratch, ".vercel", "project.json"), "utf8");
|
|
668
|
+
await fs.mkdir(path.join(directory, ".vercel"), { recursive: true });
|
|
669
|
+
await fs.writeFile(path.join(directory, ".vercel", "project.json"), link);
|
|
670
|
+
} finally {
|
|
671
|
+
await fs.rm(scratch, { recursive: true, force: true });
|
|
672
|
+
}
|
|
673
|
+
log(`Deploying ${directory} to Vercel project ${team}/${projectName}\u2026`);
|
|
674
|
+
const { stdout } = await run("vercel", ["deploy", "--prod", "--yes", "--scope", team], { cwd: directory, maxBuffer: 16 * 1024 * 1024 });
|
|
675
|
+
const url = stdout.trim().split("\n").reverse().find((line) => /^https:\/\//.test(line.trim()))?.trim();
|
|
676
|
+
if (!url) throw new Error(`Vercel did not report a deployment URL:
|
|
677
|
+
${stdout}`);
|
|
678
|
+
const response = await fetch(`https://${projectName}.vercel.app/`, { redirect: "manual" });
|
|
679
|
+
const verdict = publicVerdict(response.status, intendedPublic);
|
|
680
|
+
if (verdict === "public-as-intended") log(`Public, as intended (--public): https://${projectName}.vercel.app`);
|
|
681
|
+
else if (verdict === "public-by-accident")
|
|
682
|
+
throw new Error(`Deployed, but https://${projectName}.vercel.app is publicly readable. Turn on Vercel Authentication (Settings \u2192 Deployment Protection) for ${team}/${projectName}.`);
|
|
683
|
+
return url;
|
|
684
|
+
}
|
|
685
|
+
function publicVerdict(status, intendedPublic) {
|
|
686
|
+
if (status !== 200) return "protected";
|
|
687
|
+
return intendedPublic ? "public-as-intended" : "public-by-accident";
|
|
688
|
+
}
|
|
689
|
+
function sveltePlugin(projectRoot) {
|
|
690
|
+
return {
|
|
691
|
+
name: "cascade-editor-svelte",
|
|
692
|
+
setup(builder) {
|
|
693
|
+
builder.onLoad({ filter: /\.svelte$/ }, async (args) => {
|
|
694
|
+
let compiler;
|
|
695
|
+
try {
|
|
696
|
+
compiler = createRequire(path.join(projectRoot, "package.json"))("svelte/compiler");
|
|
697
|
+
} catch {
|
|
698
|
+
return { errors: [{ text: `${args.path}: install svelte in the project to use a Svelte panel` }] };
|
|
699
|
+
}
|
|
700
|
+
const source = await fs.readFile(args.path, "utf8");
|
|
701
|
+
const output = compiler.compile(source, { filename: args.path, generate: "client", css: "injected" });
|
|
702
|
+
return { contents: output.js.code, loader: "js", resolveDir: path.dirname(args.path) };
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
async function literalAssets(document, modules) {
|
|
708
|
+
const registrations = modules.map((item) => ({
|
|
709
|
+
kind: "definition-v1",
|
|
710
|
+
moduleId: item.id,
|
|
711
|
+
definition: item.definition,
|
|
712
|
+
loadExecute: async () => {
|
|
713
|
+
throw new Error("Exports never execute project code");
|
|
714
|
+
}
|
|
715
|
+
}));
|
|
716
|
+
const runtime = createRuntime({ host: createBrowserRuntimeHost({
|
|
717
|
+
modules: { resolve: async (id) => registrations.find((item) => item.moduleId === id) ?? null },
|
|
718
|
+
assets: {},
|
|
719
|
+
gpu: {}
|
|
720
|
+
}), nodes: registrations });
|
|
721
|
+
const found = [];
|
|
722
|
+
try {
|
|
723
|
+
const graph = await runtime.load(document);
|
|
724
|
+
const blocking = graph.preflight().filter((item) => !PREFLIGHT_WARNING_CODES.has(item.code));
|
|
725
|
+
if (blocking.length) throw new Error(`This graph cannot run in the browser:
|
|
726
|
+
${blocking.map((item) => ` ${item.path ?? ""} ${item.code}: ${item.message}`).join("\n")}`);
|
|
727
|
+
for (const node of graph.inspect().nodes)
|
|
728
|
+
for (const [name, port] of [...Object.entries(node.inputs), ...Object.entries(node.props)]) {
|
|
729
|
+
if (port.type !== "image" && port.type !== "asset") continue;
|
|
730
|
+
const value = port.value;
|
|
731
|
+
const asset = typeof value === "string" ? value : value && typeof value === "object" && "path" in value ? value.path : void 0;
|
|
732
|
+
if (typeof asset === "string" && asset && !/^(https?:|data:|blob:)/.test(asset)) found.push({ path: asset, where: `${node.id}.${name}` });
|
|
733
|
+
}
|
|
734
|
+
} finally {
|
|
735
|
+
await runtime.dispose();
|
|
736
|
+
}
|
|
737
|
+
return found;
|
|
738
|
+
}
|
|
739
|
+
var FONT_FILE = /\.(woff2?|ttf|otf)$/i;
|
|
740
|
+
function checkedFont(font) {
|
|
741
|
+
const where = `editor.fonts ${JSON.stringify(font?.family ?? "")}`;
|
|
742
|
+
if (!font || typeof font.family !== "string" || !font.family.trim()) throw new Error(`${where}: needs a family name`);
|
|
743
|
+
if (font.license !== "OFL-1.1")
|
|
744
|
+
throw new Error(`${where}: only OFL-1.1 fonts are exported (got ${JSON.stringify(font.license)}); a licensed font never leaves the project`);
|
|
745
|
+
if (typeof font.file !== "string" || !FONT_FILE.test(font.file)) throw new Error(`${where}: file must be a .woff2, .woff, .ttf or .otf`);
|
|
746
|
+
return checkedAssetPath(font.file, where);
|
|
747
|
+
}
|
|
748
|
+
function fontSources(fonts) {
|
|
749
|
+
const url = (file) => `./assets/${file.replace(/^\.\//, "").split("/").map(encodeURIComponent).join("/")}`;
|
|
750
|
+
const css = fonts.map((font) => `@font-face{font-family:${JSON.stringify(font.family)};src:url(${JSON.stringify(url(font.file))});font-weight:${font.weight ?? "normal"};font-style:${font.style ?? "normal"};font-display:swap}`).join("");
|
|
751
|
+
const list = JSON.stringify(fonts.map((font) => ({ family: font.family, url: url(font.file), weight: String(font.weight ?? "normal"), style: font.style ?? "normal" })));
|
|
752
|
+
const loader = fonts.length ? `await (async()=>{const s=globalThis;if(typeof FontFace==='undefined'||!s.fonts)return;await Promise.all(${list}.map(async(f)=>{try{const face=new FontFace(f.family,'url('+new URL(f.url,import.meta.url).href+')',{weight:f.weight,style:f.style});s.fonts.add(await face.load())}catch(e){console.warn('Font '+f.family+' did not load',e)}}))})();` : "";
|
|
753
|
+
return { css, loader };
|
|
754
|
+
}
|
|
755
|
+
function checkedAssetPath(asset, where) {
|
|
756
|
+
const name = String(asset).replace(/^\.\//, "");
|
|
757
|
+
if (!name || name.includes("\\") || path.isAbsolute(name) || name.split("/").some((part) => !part || part === ".." || part.startsWith(".")))
|
|
758
|
+
throw new Error(`${where}: ${asset} must be a visible project-relative file`);
|
|
759
|
+
if (name.split("/").includes("licensed"))
|
|
760
|
+
throw new Error(`${where}: ${asset} is under a licensed/ directory and is never exported`);
|
|
761
|
+
return name;
|
|
762
|
+
}
|
|
763
|
+
async function addAsset(project, assets, name, kind, from) {
|
|
764
|
+
if (assets.has(name)) return;
|
|
765
|
+
const file = project.resolve(name);
|
|
766
|
+
const real = await fs.realpath(file);
|
|
767
|
+
if (real.split(path.sep).includes("licensed")) throw new Error(`${name} resolves into a licensed/ directory and is never exported`);
|
|
768
|
+
if (!(await fs.stat(real)).isFile()) throw new Error(`${name} is not a file`);
|
|
769
|
+
assets.set(name, { name, file: real, kind, from });
|
|
770
|
+
}
|
|
771
|
+
async function refuseIgnored(root, name) {
|
|
772
|
+
try {
|
|
773
|
+
await run("git", ["check-ignore", "-q", "--", name], { cwd: root });
|
|
774
|
+
} catch (error) {
|
|
775
|
+
const code = error.code;
|
|
776
|
+
if (code === 1 || code === 128 || code === "ENOENT") return;
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
779
|
+
throw new Error(`${name} is gitignored, so it is not exported. Commit it, or select another file.`);
|
|
780
|
+
}
|
|
781
|
+
function stripNode(node) {
|
|
782
|
+
return {
|
|
783
|
+
id: node.id,
|
|
784
|
+
...node.module ? { module: node.module } : {},
|
|
785
|
+
...node.type ? { type: node.type } : {},
|
|
786
|
+
...node.parent ? { parent: node.parent } : {},
|
|
787
|
+
...node.inputs ? { inputs: node.inputs } : {},
|
|
788
|
+
...node.props ? { props: node.props } : {}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
async function filesUnder(directory) {
|
|
792
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
793
|
+
const nested = await Promise.all(entries.map(async (entry) => {
|
|
794
|
+
const file = path.join(directory, entry.name);
|
|
795
|
+
return entry.isDirectory() ? filesUnder(file) : [file];
|
|
796
|
+
}));
|
|
797
|
+
return nested.flat();
|
|
798
|
+
}
|
|
799
|
+
function excerpt(text, index) {
|
|
800
|
+
return text.slice(Math.max(0, index - 20), index + 40).replace(/\s+/g, " ");
|
|
801
|
+
}
|
|
802
|
+
function escapeHtml(text) {
|
|
803
|
+
return text.replace(/[&<>"]/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[char]);
|
|
804
|
+
}
|
|
805
|
+
function formatSize(bytes) {
|
|
806
|
+
return bytes >= 1048576 ? `${(bytes / 1048576).toFixed(1)} MB` : bytes >= 1024 ? `${Math.round(bytes / 1024)} KB` : `${bytes} B`;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/cli/editorCommands.ts
|
|
810
|
+
var EDITOR_COMMANDS = ["export", "freeze", "publish", "scene"];
|
|
811
|
+
var EDITOR_HELP = `
|
|
812
|
+
Shareable editors (see doc/EDITOR_EXPORT.md):
|
|
813
|
+
cascade export <graph> --plan Print what would ship, build nothing
|
|
814
|
+
cascade export <graph> --out <dir> [--deploy vercel [--project <name>] [--scope <team>] [--public]]
|
|
815
|
+
cascade publish <graph> <node>.<prop> [--id <id>] [--label <text>] [--group <name>]
|
|
816
|
+
[--control <kind>] [--min n] [--max n] [--step n] [--help <text>] [--remove]
|
|
817
|
+
cascade freeze <graph> <node> --fields <a,b.c> [--output <port>] [--personal <a,..>]
|
|
818
|
+
cascade scene <graph> <scene.json> --out <new-graph.cascade>
|
|
819
|
+
cascade run <graph> --scene <scene.json> [--write <node>.<port>=<file>]...
|
|
820
|
+
`;
|
|
821
|
+
async function runEditorCommand(args) {
|
|
822
|
+
const [command, graph] = args;
|
|
823
|
+
if (!graph || graph.startsWith("-")) throw new Error(`cascade ${command} requires a graph file`);
|
|
824
|
+
const flags = parseFlags(args.slice(2));
|
|
825
|
+
switch (command) {
|
|
826
|
+
case "export":
|
|
827
|
+
return exportCommand(graph, flags);
|
|
828
|
+
case "publish":
|
|
829
|
+
return publishCommand(graph, flags);
|
|
830
|
+
case "freeze":
|
|
831
|
+
return freezeCommand(graph, flags);
|
|
832
|
+
case "scene":
|
|
833
|
+
return sceneCommand(graph, flags);
|
|
834
|
+
case "run":
|
|
835
|
+
return runWithScene(graph, flags);
|
|
836
|
+
default:
|
|
837
|
+
throw new Error(`Unknown command ${command}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
var SWITCHES = /* @__PURE__ */ new Set(["--plan", "--remove", "--json", "--public"]);
|
|
841
|
+
function parseFlags(args) {
|
|
842
|
+
const positional = [];
|
|
843
|
+
const values = /* @__PURE__ */ new Map();
|
|
844
|
+
const switches = /* @__PURE__ */ new Set();
|
|
845
|
+
for (let index = 0; index < args.length; index++) {
|
|
846
|
+
const arg = args[index];
|
|
847
|
+
if (!arg.startsWith("--")) {
|
|
848
|
+
positional.push(arg);
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (SWITCHES.has(arg)) {
|
|
852
|
+
switches.add(arg);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const value = args[index + 1];
|
|
856
|
+
if (value === void 0 || value.startsWith("--") && !/^--?\d/.test(value)) throw new Error(`${arg} needs a value`);
|
|
857
|
+
values.set(arg, [...values.get(arg) ?? [], value]);
|
|
858
|
+
index++;
|
|
859
|
+
}
|
|
860
|
+
return { positional, values, switches };
|
|
861
|
+
}
|
|
862
|
+
var one = (flags, name) => {
|
|
863
|
+
const list = flags.values.get(name);
|
|
864
|
+
return list?.[list.length - 1];
|
|
865
|
+
};
|
|
866
|
+
async function exportCommand(graph, flags) {
|
|
867
|
+
const deploy = one(flags, "--deploy");
|
|
868
|
+
if (deploy && deploy !== "vercel") throw new Error("--deploy supports vercel only");
|
|
869
|
+
const result = await exportEditor(graph, {
|
|
870
|
+
plan: flags.switches.has("--plan"),
|
|
871
|
+
out: one(flags, "--out"),
|
|
872
|
+
...deploy ? { deploy: "vercel" } : {},
|
|
873
|
+
project: one(flags, "--project"),
|
|
874
|
+
scope: one(flags, "--scope"),
|
|
875
|
+
...flags.switches.has("--public") ? { public: true } : {}
|
|
876
|
+
});
|
|
877
|
+
if (flags.switches.has("--json")) console.log(JSON.stringify(result.plan, null, 2));
|
|
878
|
+
else console.log(formatPlan(result.plan));
|
|
879
|
+
if (result.directory) console.log(`
|
|
880
|
+
Editor exported: ${result.directory}
|
|
881
|
+
Manifest: ${path2.join(result.directory, "export-manifest.json")}`);
|
|
882
|
+
if (result.url) console.log(`Deployed: ${result.url}`);
|
|
883
|
+
}
|
|
884
|
+
async function readGraph(file) {
|
|
885
|
+
return JSON.parse(await fs2.readFile(file, "utf8"));
|
|
886
|
+
}
|
|
887
|
+
async function writeEditor(file, document, editor) {
|
|
888
|
+
const next = { ...document, editor };
|
|
889
|
+
await fs2.writeFile(file, `${JSON.stringify(next, null, 2)}
|
|
890
|
+
`);
|
|
891
|
+
}
|
|
892
|
+
async function publishCommand(graph, flags) {
|
|
893
|
+
const target = flags.positional[0];
|
|
894
|
+
const match = target ? /^([^.]+)\.(.+)$/.exec(target) : null;
|
|
895
|
+
if (!match) throw new Error("cascade publish needs <node>.<prop>");
|
|
896
|
+
const [, node, prop] = match;
|
|
897
|
+
const document = await readGraph(graph);
|
|
898
|
+
if (!document.nodes.some((item) => item.id === node)) throw new Error(`No node ${node} in ${graph}`);
|
|
899
|
+
const editor = document.editor ?? { id: path2.basename(graph).replace(/\.cascade$/, "") };
|
|
900
|
+
const parameters = [...editor.parameters ?? []];
|
|
901
|
+
const id = one(flags, "--id") ?? prop;
|
|
902
|
+
const existing = parameters.findIndex((item) => item.node === node && item.prop === prop);
|
|
903
|
+
if (flags.switches.has("--remove")) {
|
|
904
|
+
if (existing < 0) throw new Error(`${node}.${prop} is not published`);
|
|
905
|
+
parameters.splice(existing, 1);
|
|
906
|
+
await writeEditor(graph, document, { ...editor, parameters });
|
|
907
|
+
console.log(`Unpublished ${node}.${prop}`);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const number = (name) => {
|
|
911
|
+
const value = one(flags, name);
|
|
912
|
+
if (value === void 0) return void 0;
|
|
913
|
+
const parsed = Number(value);
|
|
914
|
+
if (!Number.isFinite(parsed)) throw new Error(`${name} must be a number`);
|
|
915
|
+
return parsed;
|
|
916
|
+
};
|
|
917
|
+
const previous = existing >= 0 ? parameters[existing] : void 0;
|
|
918
|
+
const parameter = {
|
|
919
|
+
...previous,
|
|
920
|
+
id: previous?.id ?? id,
|
|
921
|
+
node,
|
|
922
|
+
prop,
|
|
923
|
+
...one(flags, "--label") ? { label: one(flags, "--label") } : {},
|
|
924
|
+
...one(flags, "--group") ? { group: one(flags, "--group") } : {},
|
|
925
|
+
...one(flags, "--control") ? { control: one(flags, "--control") } : {},
|
|
926
|
+
...number("--min") !== void 0 ? { min: number("--min") } : {},
|
|
927
|
+
...number("--max") !== void 0 ? { max: number("--max") } : {},
|
|
928
|
+
...number("--step") !== void 0 ? { step: number("--step") } : {},
|
|
929
|
+
...number("--order") !== void 0 ? { order: number("--order") } : {},
|
|
930
|
+
...one(flags, "--help") ? { help: one(flags, "--help") } : {},
|
|
931
|
+
...one(flags, "--unit") ? { unit: one(flags, "--unit") } : {}
|
|
932
|
+
};
|
|
933
|
+
if (existing >= 0) parameters[existing] = parameter;
|
|
934
|
+
else {
|
|
935
|
+
if (parameters.some((item) => item.id === parameter.id)) throw new Error(`Parameter id ${parameter.id} is taken; pass --id`);
|
|
936
|
+
parameters.push(parameter);
|
|
937
|
+
}
|
|
938
|
+
await writeEditor(graph, document, { ...editor, parameters });
|
|
939
|
+
console.log(`${existing >= 0 ? "Updated" : "Published"} ${node}.${prop} as "${parameter.id}"`);
|
|
940
|
+
}
|
|
941
|
+
async function freezeCommand(graph, flags) {
|
|
942
|
+
const node = flags.positional[0];
|
|
943
|
+
if (!node) throw new Error("cascade freeze needs a node id");
|
|
944
|
+
const fields = (one(flags, "--fields") ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
945
|
+
if (!fields.length) throw new Error("cascade freeze needs --fields: name exactly the values to keep");
|
|
946
|
+
const personal = (one(flags, "--personal") ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
947
|
+
const document = await readGraph(graph);
|
|
948
|
+
if (!document.nodes.some((item) => item.id === node)) throw new Error(`No node ${node} in ${graph}`);
|
|
949
|
+
const graphFile = path2.resolve(graph);
|
|
950
|
+
const project = new ProjectRoot(path2.dirname(graphFile));
|
|
951
|
+
await installHeadlessCanvas().catch(() => void 0);
|
|
952
|
+
const removeIo = installProjectIo(project);
|
|
953
|
+
const { outputs } = await cookProjectGraph(graphFile, document, node).finally(removeIo);
|
|
954
|
+
const produced = outputs.get(node);
|
|
955
|
+
if (!produced) throw new Error(`${node} produced nothing`);
|
|
956
|
+
const output = one(flags, "--output") ?? [...produced.keys()][0];
|
|
957
|
+
if (!produced.has(output)) throw new Error(`${node} has no output ${output}; it has ${[...produced.keys()].join(", ")}`);
|
|
958
|
+
const full = toJson(produced.get(output));
|
|
959
|
+
const directory = path2.join("frozen", node);
|
|
960
|
+
await fs2.mkdir(project.resolve(directory), { recursive: true });
|
|
961
|
+
const files = [];
|
|
962
|
+
const pick = async (value) => {
|
|
963
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.path === "string") {
|
|
964
|
+
const from = value.path;
|
|
965
|
+
const source = project.resolve(from.replace(/^\.\//, ""));
|
|
966
|
+
const name = `${directory}/${path2.basename(from)}`;
|
|
967
|
+
await fs2.copyFile(source, project.resolve(name));
|
|
968
|
+
files.push(name);
|
|
969
|
+
return { ...value, path: name };
|
|
970
|
+
}
|
|
971
|
+
if (Array.isArray(value)) return Promise.all(value.map(pick));
|
|
972
|
+
if (value && typeof value === "object")
|
|
973
|
+
return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [key, await pick(item)])));
|
|
974
|
+
return value;
|
|
975
|
+
};
|
|
976
|
+
const chosen = {};
|
|
977
|
+
for (const field of fields) {
|
|
978
|
+
const value = getPath(full, field);
|
|
979
|
+
if (value === void 0) throw new Error(`${node}.${output} has no field ${field}`);
|
|
980
|
+
setPath(chosen, field, await pick(value));
|
|
981
|
+
}
|
|
982
|
+
if (fields.includes("path") && typeof chosen.path === "string" && full && typeof full === "object" && !Array.isArray(full)) {
|
|
983
|
+
const { path: copied } = await pick({ path: chosen.path });
|
|
984
|
+
chosen.path = copied;
|
|
985
|
+
}
|
|
986
|
+
const snapshot = {
|
|
987
|
+
format: "cascade.frozen",
|
|
988
|
+
version: 1,
|
|
989
|
+
node,
|
|
990
|
+
output,
|
|
991
|
+
fields,
|
|
992
|
+
...personal.length ? { personal } : {},
|
|
993
|
+
value: chosen,
|
|
994
|
+
...files.length ? { files } : {},
|
|
995
|
+
frozenAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
996
|
+
};
|
|
997
|
+
const snapshotFile = `${directory}.json`;
|
|
998
|
+
await fs2.writeFile(project.resolve(snapshotFile), `${JSON.stringify(snapshot, null, 2)}
|
|
999
|
+
`);
|
|
1000
|
+
const editor = document.editor ?? { id: path2.basename(graph).replace(/\.cascade$/, "") };
|
|
1001
|
+
const freeze = (editor.freeze ?? []).filter((item) => item.node !== node);
|
|
1002
|
+
freeze.push({ node, output, snapshot: snapshotFile });
|
|
1003
|
+
await writeEditor(graph, document, { ...editor, freeze });
|
|
1004
|
+
console.log(`Froze ${node}.${output}: ${fields.join(", ")} \u2192 ${snapshotFile}${files.length ? ` (+${files.length} file${files.length > 1 ? "s" : ""})` : ""}`);
|
|
1005
|
+
}
|
|
1006
|
+
async function sceneCommand(graph, flags) {
|
|
1007
|
+
const sceneFile = flags.positional[0];
|
|
1008
|
+
const out = one(flags, "--out");
|
|
1009
|
+
if (!sceneFile || !out) throw new Error("cascade scene needs <scene.json> --out <new-graph.cascade>");
|
|
1010
|
+
if (path2.resolve(out) === path2.resolve(graph)) throw new Error("--out must be a new file; the source graph is never overwritten");
|
|
1011
|
+
const { document, warnings } = await sceneDocument(graph, sceneFile);
|
|
1012
|
+
await fs2.writeFile(out, `${JSON.stringify(document, null, 2)}
|
|
1013
|
+
`);
|
|
1014
|
+
for (const warning of warnings) console.warn(`Warning: ${warning}`);
|
|
1015
|
+
console.log(`Scene applied: ${out}`);
|
|
1016
|
+
}
|
|
1017
|
+
async function sceneDocument(graph, sceneFile) {
|
|
1018
|
+
const document = await readGraph(graph);
|
|
1019
|
+
const scene = parseScene(await fs2.readFile(sceneFile, "utf8"));
|
|
1020
|
+
const project = new ProjectRoot(path2.dirname(path2.resolve(graph)));
|
|
1021
|
+
const located = /* @__PURE__ */ new Map();
|
|
1022
|
+
for (const [id, file] of Object.entries(scene.files ?? {})) {
|
|
1023
|
+
const found = await locateSceneFile(project, file);
|
|
1024
|
+
if (found) located.set(id, { path: found, ...file.mediaType ? { mediaType: file.mediaType } : {} });
|
|
1025
|
+
}
|
|
1026
|
+
return applyScene(document, scene, { resolveFile: (parameter) => located.get(parameter.id) });
|
|
1027
|
+
}
|
|
1028
|
+
async function locateSceneFile(project, file) {
|
|
1029
|
+
const { createHash } = await import("node:crypto");
|
|
1030
|
+
const safe = path2.basename(file.name).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1031
|
+
if (file.data) {
|
|
1032
|
+
const bytes = fromBase64(file.data);
|
|
1033
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
1034
|
+
if (digest !== file.sha256) throw new Error(`Scene file ${file.name} does not match its hash`);
|
|
1035
|
+
const name = `assets/scenes/${digest.slice(0, 12)}-${safe}`;
|
|
1036
|
+
await fs2.mkdir(path2.dirname(project.resolve(name)), { recursive: true });
|
|
1037
|
+
await fs2.writeFile(project.resolve(name), bytes);
|
|
1038
|
+
return name;
|
|
1039
|
+
}
|
|
1040
|
+
for (const candidate of await filesNamed(project.root, path2.basename(file.name))) {
|
|
1041
|
+
const digest = createHash("sha256").update(await fs2.readFile(candidate)).digest("hex");
|
|
1042
|
+
if (digest === file.sha256) return path2.relative(project.root, candidate).split(path2.sep).join("/");
|
|
1043
|
+
}
|
|
1044
|
+
return void 0;
|
|
1045
|
+
}
|
|
1046
|
+
async function filesNamed(root, name) {
|
|
1047
|
+
const found = [];
|
|
1048
|
+
const walk = async (directory, depth) => {
|
|
1049
|
+
if (depth > 6) return;
|
|
1050
|
+
for (const entry of await fs2.readdir(directory, { withFileTypes: true }).catch(() => [])) {
|
|
1051
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".cascade-cache") continue;
|
|
1052
|
+
const file = path2.join(directory, entry.name);
|
|
1053
|
+
if (entry.isDirectory()) await walk(file, depth + 1);
|
|
1054
|
+
else if (entry.name === name) found.push(file);
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
await walk(path2.join(root, "assets"), 0);
|
|
1058
|
+
await walk(path2.join(root, "licensed"), 0);
|
|
1059
|
+
return found;
|
|
1060
|
+
}
|
|
1061
|
+
async function runWithScene(graph, flags) {
|
|
1062
|
+
const graphFile = path2.resolve(graph);
|
|
1063
|
+
let document = await readGraph(graph);
|
|
1064
|
+
const scene = one(flags, "--scene");
|
|
1065
|
+
if (scene) {
|
|
1066
|
+
const applied = await sceneDocument(graph, scene);
|
|
1067
|
+
document = applied.document;
|
|
1068
|
+
for (const warning of applied.warnings) console.warn(`Warning: ${warning}`);
|
|
1069
|
+
}
|
|
1070
|
+
await installHeadlessCanvas().catch(() => void 0);
|
|
1071
|
+
const project = new ProjectRoot(path2.dirname(graphFile));
|
|
1072
|
+
const removeIo = installProjectIo(project);
|
|
1073
|
+
const { outputs } = await cookProjectGraph(graphFile, document).finally(removeIo);
|
|
1074
|
+
for (const spec of flags.values.get("--write") ?? []) {
|
|
1075
|
+
const match = /^([^.=]+)\.([^=]+)=(.+)$/.exec(spec);
|
|
1076
|
+
if (!match) throw new Error(`--write expects <node>.<port>=<file>, got ${spec}`);
|
|
1077
|
+
const [, node, port, file] = match;
|
|
1078
|
+
const value = outputs.get(node)?.get(port);
|
|
1079
|
+
if (value === void 0) throw new Error(`${node}.${port} produced nothing`);
|
|
1080
|
+
if (typeof value === "string") await fs2.writeFile(file, value);
|
|
1081
|
+
else if (value && typeof value === "object" && typeof value.path === "string")
|
|
1082
|
+
await fs2.copyFile(project.resolve(value.path.replace(/^\.\//, "")), file);
|
|
1083
|
+
else await fs2.writeFile(file, JSON.stringify(toJson(value)));
|
|
1084
|
+
console.log(`Wrote ${node}.${port} \u2192 ${file}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
function toJson(value) {
|
|
1088
|
+
return JSON.parse(JSON.stringify(value, (_key, item) => ArrayBuffer.isView(item) ? Array.from(item) : item) ?? "null");
|
|
1089
|
+
}
|
|
1090
|
+
function getPath(value, field) {
|
|
1091
|
+
let current = value;
|
|
1092
|
+
for (const part of field.split(".").flatMap((item) => item.split(/\[(\d+)\]/).filter(Boolean))) {
|
|
1093
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
1094
|
+
current = current[part];
|
|
1095
|
+
}
|
|
1096
|
+
return current;
|
|
1097
|
+
}
|
|
1098
|
+
function setPath(target, field, value) {
|
|
1099
|
+
const parts = field.split(".").flatMap((item) => item.split(/\[(\d+)\]/).filter(Boolean));
|
|
1100
|
+
let current = target;
|
|
1101
|
+
parts.forEach((part, index) => {
|
|
1102
|
+
const last = index === parts.length - 1;
|
|
1103
|
+
const container = current;
|
|
1104
|
+
if (last) {
|
|
1105
|
+
container[part] = value;
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (container[part] === void 0) container[part] = /^\d+$/.test(parts[index + 1]) ? [] : {};
|
|
1109
|
+
current = container[part];
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
export {
|
|
1114
|
+
EDITOR_COMMANDS,
|
|
1115
|
+
EDITOR_HELP,
|
|
1116
|
+
runEditorCommand,
|
|
1117
|
+
sceneDocument
|
|
1118
|
+
};
|
|
1119
|
+
//# sourceMappingURL=chunk-WKURDVYP.js.map
|