@field/cascade 0.10.0 → 0.12.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 +25 -5
- package/ARCHITECTURE.md +5 -4
- package/CHANGELOG.md +382 -0
- package/README.md +1 -2
- package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
- package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
- package/dist/assets/{CodeEditor-DSZBpzdy.js → CodeEditor-C-xYG-Wn.js} +120 -120
- package/dist/assets/CodePanel-CtXd50m6.js +2 -0
- package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
- package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
- package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
- package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
- package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
- package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
- package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
- package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
- package/dist/assets/{SettingsDialog-p25h3tj0.js → SettingsDialog-CBX3Wa87.js} +1 -1
- package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
- package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
- package/dist/assets/{VersionHistoryDialog-DzFLUotB.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
- package/dist/assets/assert-BO2_erPD.js +1 -0
- package/dist/assets/assert-hvykGCnP.js +1 -0
- package/dist/assets/builder-BmBKcDix.js +1 -0
- package/dist/assets/camera-DFF7YA3F.js +1 -0
- package/dist/assets/checks-DAvyO9Gf.js +1 -0
- package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
- package/dist/assets/{core-BICrEt0y.js → core-DB-MkGjj.js} +3 -4
- package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
- package/dist/assets/{cssMode-D7TXw57I.js → cssMode-DPVckSOc.js} +1 -1
- package/dist/assets/definitions-B227e7f_.js +1 -0
- package/dist/assets/definitions-D0WOUJd7.js +1 -0
- package/dist/assets/{editor.api-DvkCO3ko.js → editor.api-Dkutg_Z1.js} +1 -1
- package/dist/assets/expressions-X-Z-GrgW.js +5 -0
- package/dist/assets/file-DAL6IfjV.js +1 -0
- package/dist/assets/filters-D3k1LRMa.js +1 -0
- package/dist/assets/{freemarker2-BA2lxsF1.js → freemarker2-CLiLKk3B.js} +1 -1
- package/dist/assets/generators-BhDxRAFC.js +1 -0
- package/dist/assets/geo-BsMImJIJ.js +1 -0
- package/dist/assets/geo-C4YngDob.js +2 -0
- package/dist/assets/{handlebars-BcDiyPd8.js → handlebars-CcD3aMeL.js} +1 -1
- package/dist/assets/{html-C7O4TyhX.js → html-DvZWGfqb.js} +1 -1
- package/dist/assets/{htmlMode-WaDGF3Mj.js → htmlMode-DUD3VaoN.js} +1 -1
- package/dist/assets/image-0SfH-snh.js +1 -0
- package/dist/assets/image-Ck-RdGU4.js +2 -0
- package/dist/assets/index-BWnrmIB9.js +137 -0
- package/dist/assets/index-DIaxEONI.css +1 -0
- package/dist/assets/io-BGmt2g0D.js +1 -0
- package/dist/assets/{javascript-CpIEK7tz.js → javascript-Cap2ahgh.js} +1 -1
- package/dist/assets/{jsonMode-B2FltW0f.js → jsonMode-B0uLjp9C.js} +1 -1
- package/dist/assets/{liquid-CN6FFHZE.js → liquid-CnxwhfLn.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-QhV9XtLp.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
- package/dist/assets/math-GeULS-sk.js +1 -0
- package/dist/assets/{mdx-DE89pafV.js → mdx-BV3jbrVa.js} +1 -1
- package/dist/assets/pop-KMF_qloi.js +1 -0
- package/dist/assets/preload-helper-Czpn1I53.js +1 -0
- package/dist/assets/{python-ByTllTB9.js → python-CGyFk4XK.js} +1 -1
- package/dist/assets/{razor-USYlRRCE.js → razor-CQfzX3-N.js} +1 -1
- package/dist/assets/remap-5SjXQU-q.js +1 -0
- package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
- package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
- package/dist/assets/text-ZpX7qNkx.js +2 -0
- package/dist/assets/time-P_OiBhcf.js +1 -0
- package/dist/assets/{tsMode-BXpN8iUz.js → tsMode-BOeXzO9H.js} +1 -1
- package/dist/assets/{typescript-DInBjGQQ.js → typescript-J1SnE_Tu.js} +1 -1
- package/dist/assets/{workers-C7Ye5B7D.js → workers-CYwwTiVF.js} +1 -1
- package/dist/assets/{xml-U0Uocqyo.js → xml-DnPRNzK9.js} +1 -1
- package/dist/assets/{yaml-CBw_PVYf.js → yaml-BmmYBNa9.js} +1 -1
- package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
- package/dist/cli/Graph-OB6EWSGL.js +2975 -0
- package/dist/cli/assert-GROGQNCQ.js +25 -0
- package/dist/cli/checks-3WB44XBL.js +189 -0
- package/dist/cli/chunk-2JEU3MGB.js +1125 -0
- package/dist/cli/chunk-2KXB47FP.js +115 -0
- package/dist/cli/chunk-3RXL5GHB.js +816 -0
- package/dist/cli/chunk-53LGFPWX.js +138 -0
- package/dist/cli/chunk-5ORUQ6ES.js +267 -0
- package/dist/cli/chunk-73S4JZXK.js +336 -0
- package/dist/cli/chunk-7QE2ZNSN.js +121 -0
- package/dist/cli/chunk-BP7UKF36.js +376 -0
- package/dist/cli/chunk-BPODRH52.js +201 -0
- package/dist/cli/chunk-BZ63XQ3I.js +1301 -0
- package/dist/cli/chunk-CKS2FQWR.js +89 -0
- package/dist/cli/chunk-CO22XHNG.js +593 -0
- package/dist/cli/chunk-CTPLRSWF.js +2197 -0
- package/dist/cli/chunk-DEQSEFFH.js +153 -0
- package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
- package/dist/cli/chunk-EZRWJ4G2.js +104 -0
- package/dist/cli/chunk-FWMSGKEH.js +71 -0
- package/dist/cli/chunk-G23A2BZZ.js +705 -0
- package/dist/cli/chunk-GRIN2DDP.js +1465 -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-JVOCK5W4.js +83 -0
- package/dist/cli/chunk-KBGUO27Q.js +15 -0
- package/dist/cli/chunk-KH4MUBKF.js +1551 -0
- package/dist/cli/chunk-L6B6FUQP.js +330 -0
- package/dist/cli/chunk-LTXWBXOM.js +438 -0
- package/dist/cli/chunk-MI5JLQYA.js +220 -0
- package/dist/cli/chunk-MP653A5O.js +3766 -0
- package/dist/cli/chunk-NJFMDXHR.js +320 -0
- package/dist/cli/chunk-OM6BEME6.js +267 -0
- package/dist/cli/chunk-OWWBYLHC.js +136 -0
- package/dist/cli/chunk-QQPA2ZHH.js +273 -0
- package/dist/cli/chunk-RUU3YWXU.js +99 -0
- package/dist/cli/chunk-RXDIMXN5.js +98 -0
- package/dist/cli/chunk-ST6LROUG.js +267 -0
- package/dist/cli/chunk-TJW67QSI.js +225 -0
- package/dist/cli/chunk-WD62JIP6.js +118 -0
- package/dist/cli/chunk-WGKPJZDY.js +364 -0
- package/dist/cli/chunk-X4WKAOQP.js +634 -0
- package/dist/cli/chunk-X566FWBV.js +156 -0
- package/dist/cli/chunk-YH637PDL.js +317 -0
- package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
- package/dist/cli/chunk-ZSCUZE6I.js +112 -0
- package/dist/cli/cliCommands-JJN5Z3ZC.js +3470 -0
- package/dist/cli/commands-PHYFYA3F.js +1374 -0
- package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
- package/dist/cli/definitions-BPOPW52L.js +32 -0
- package/dist/cli/doctorCommand-ORWQRNYP.js +106 -0
- package/dist/cli/edit-S5KEQ5RA.js +28 -0
- package/dist/cli/editorCommands-NFR32JOY.js +41 -0
- package/dist/cli/encode-ICPEPMH5.js +17 -0
- package/dist/cli/file-3HTQHPYQ.js +41 -0
- package/dist/cli/filters-6ODFWYK4.js +379 -0
- package/dist/cli/frames-LSAUBZ7P.js +16 -0
- package/dist/cli/generators-YU7GB4MD.js +210 -0
- package/dist/cli/geo-6ZPGKZRU.js +43 -0
- package/dist/cli/graphCommands-EOUOBOIJ.js +184 -0
- package/dist/cli/graphFile-YTGJB5F5.js +292 -0
- package/dist/cli/headlessCanvas-ENRHC7GS.js +16 -0
- package/dist/cli/helpCommand-PULH6ZYB.js +157 -0
- package/dist/cli/image-TXELZZK5.js +55 -0
- package/dist/cli/index.js +100 -30756
- package/dist/cli/math-RSYRQ3C6.js +16 -0
- package/dist/cli/mcp-JVZFQOEX.js +164 -0
- package/dist/cli/parity-X6DEH2DV.js +334 -0
- package/dist/cli/pop-B7SIIYKN.js +39 -0
- package/dist/cli/project-VYGIA7JX.js +12 -0
- package/dist/cli/projectDefinition-F3D7UIHJ.js +27 -0
- package/dist/cli/projectRuntime-2Q4JKEH3.js +55 -0
- package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
- package/dist/cli/render-PT4MWI6U.js +438 -0
- package/dist/cli/runCommand-HNPQVZMS.js +198 -0
- package/dist/cli/runner-5XPXVW7J.js +43 -0
- package/dist/cli/runtime/image.ts +302 -0
- package/dist/cli/runtime/io.ts +78 -0
- package/dist/cli/runtime/stage.ts +4 -1
- package/dist/cli/share-MHF6TNRK.js +166 -0
- package/dist/cli/stageCache-TY2BFZJR.js +15 -0
- package/dist/cli/stageRunner-KY2EJP7X.js +10 -0
- package/dist/cli/text-MB3EKR4H.js +126 -0
- package/dist/cli/verbs-PI2JH5FI.js +19 -0
- package/dist/cli/webBuild-WPPMGGW7.js +44 -0
- package/dist/contracts/capabilities.d.ts +18 -1
- package/dist/contracts/color.d.ts +8 -0
- package/dist/contracts/color.js +23 -0
- package/dist/contracts/definition.d.ts +10 -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/contracts/schema.d.ts +1 -1
- package/dist/contracts/schema.js +1 -0
- package/dist/contracts/validate.js +11 -1
- package/dist/contracts/values.d.ts +24 -0
- package/dist/image.d.ts +156 -0
- package/dist/image.js +161 -0
- package/dist/index.html +8 -2
- package/dist/io.d.ts +29 -0
- package/dist/io.js +40 -0
- package/dist/player/app.js +3 -2
- package/dist/player/editor.js +35 -9
- package/dist/player/editorEngine.js +3 -2
- package/dist/runtime/builtins/assert/checks.d.ts +18 -0
- package/dist/runtime/builtins/assert/checks.js +207 -0
- package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
- package/dist/runtime/builtins/assert/definitions.js +54 -0
- package/dist/runtime/builtins/assert/index.d.ts +152 -0
- package/dist/runtime/builtins/assert/index.js +36 -0
- package/dist/runtime/builtins/image/contactSheet.d.ts +64 -0
- package/dist/runtime/builtins/image/contactSheet.js +150 -0
- package/dist/runtime/builtins/image/definitions.d.ts +950 -0
- package/dist/runtime/builtins/image/definitions.js +343 -0
- package/dist/runtime/builtins/image/file.d.ts +8 -0
- package/dist/runtime/builtins/image/file.js +37 -0
- package/dist/runtime/builtins/image/filters.d.ts +8 -0
- package/dist/runtime/builtins/image/filters.js +394 -0
- package/dist/runtime/builtins/image/generators.d.ts +7 -0
- package/dist/runtime/builtins/image/generators.js +144 -0
- package/dist/runtime/builtins/image/index.d.ts +952 -0
- package/dist/runtime/builtins/image/index.js +44 -0
- package/dist/runtime/builtins/image/io.d.ts +100 -0
- package/dist/runtime/builtins/image/io.js +142 -0
- package/dist/runtime/builtins/image/planar.d.ts +55 -0
- package/dist/runtime/builtins/image/planar.js +274 -0
- package/dist/runtime/builtins/image/simplex.d.ts +22 -0
- package/dist/runtime/builtins/image/simplex.js +92 -0
- package/dist/runtime/builtins/image/text.d.ts +4 -0
- package/dist/runtime/builtins/image/text.js +141 -0
- package/dist/runtime/builtins/index.d.ts +3 -0
- package/dist/runtime/builtins/index.js +7 -0
- package/dist/runtime/gpu.d.ts +25 -0
- package/dist/runtime/gpu.js +40 -0
- package/dist/runtime/runtime.d.ts +2 -0
- package/dist/runtime/runtime.js +268 -44
- package/dist/runtime/types.d.ts +30 -0
- package/dist/stage.js +4 -1
- package/doc/CLI_REFERENCE.md +91 -2
- package/doc/EDITOR_EXPORT.md +37 -2
- package/doc/MCP.md +18 -1
- package/doc/NODE_REFERENCE.md +424 -10
- package/doc/PROJECT_AUTHORING.md +222 -7
- package/package.json +19 -4
- package/dist/assets/CodePanel-sY6sZa7B.js +0 -2
- package/dist/assets/CustomNodeDialog-BRKnqZhp.js +0 -1
- package/dist/assets/ExportDialog-C7dVGua4.js +0 -358
- package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
- package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +0 -1
- package/dist/assets/ProjectSettings-Cw17nNe3.js +0 -2
- package/dist/assets/definitions-DUww2Rt-.js +0 -1
- package/dist/assets/geo-C2RHOVHC.js +0 -1
- package/dist/assets/image-DR4oeyJB.js +0 -2062
- package/dist/assets/index-7vae6YFC.css +0 -1
- package/dist/assets/index-DyvDC6pS.js +0 -219
- package/dist/assets/math-DaDEgWKt.js +0 -1
- package/dist/assets/pop-pLH7L0PW.js +0 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
declaredStagePython
|
|
3
|
+
} from "./chunk-WD62JIP6.js";
|
|
4
|
+
|
|
5
|
+
// server/src/stageRunner.ts
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
var TIMEOUT_MS = 10 * 60 * 1e3;
|
|
11
|
+
var MAX_BUFFER = 64 * 1024 * 1024;
|
|
12
|
+
function resolveProjectExecutable(project, configuredPath) {
|
|
13
|
+
if (path.isAbsolute(configuredPath)) {
|
|
14
|
+
throw new Error("cascade.json exec.stages.python must be python, python3, or a project-relative executable");
|
|
15
|
+
}
|
|
16
|
+
const executable = path.resolve(project.root, configuredPath);
|
|
17
|
+
const relative = path.relative(project.root, executable);
|
|
18
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
19
|
+
throw new Error("cascade.json exec.stages.python must be python, python3, or a project-relative executable");
|
|
20
|
+
}
|
|
21
|
+
return executable;
|
|
22
|
+
}
|
|
23
|
+
function readStageConfig(project) {
|
|
24
|
+
let raw;
|
|
25
|
+
try {
|
|
26
|
+
raw = JSON.parse(fs.readFileSync(project.resolve("cascade.json"), "utf8"));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code === "ENOENT") {
|
|
29
|
+
throw new Error("Project stages are not configured; add exec.stages to cascade.json");
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Cannot read cascade.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
}
|
|
33
|
+
const stages = raw?.exec?.stages;
|
|
34
|
+
const value = stages && typeof stages === "object" && !Array.isArray(stages) ? stages : null;
|
|
35
|
+
const entrypoint = value && typeof value.entrypoint === "string" ? value.entrypoint : "";
|
|
36
|
+
if (!entrypoint) {
|
|
37
|
+
throw new Error("Project stages are not configured; add exec.stages.entrypoint to cascade.json");
|
|
38
|
+
}
|
|
39
|
+
project.resolve(entrypoint);
|
|
40
|
+
const worker = typeof value?.worker === "string" && value.worker ? value.worker : void 0;
|
|
41
|
+
if (value?.worker !== void 0 && !worker) {
|
|
42
|
+
throw new Error("cascade.json exec.stages.worker must be a project-relative path");
|
|
43
|
+
}
|
|
44
|
+
if (worker) project.resolve(worker);
|
|
45
|
+
const configuredPython = value?.python;
|
|
46
|
+
let python = "python3";
|
|
47
|
+
let explicit = false;
|
|
48
|
+
if (configuredPython !== void 0) {
|
|
49
|
+
if (typeof configuredPython !== "string" || !configuredPython.trim()) {
|
|
50
|
+
throw new Error("cascade.json exec.stages.python must be python, python3, or a project-relative executable");
|
|
51
|
+
}
|
|
52
|
+
const executable = configuredPython.trim();
|
|
53
|
+
explicit = executable !== "python" && executable !== "python3";
|
|
54
|
+
python = explicit ? resolveProjectExecutable(project, executable) : executable;
|
|
55
|
+
}
|
|
56
|
+
if (!explicit) python = declaredStagePython(project.root) ?? python;
|
|
57
|
+
const sources = value?.sources;
|
|
58
|
+
if (sources !== void 0 && (!Array.isArray(sources) || sources.some((item) => typeof item !== "string" || !item))) {
|
|
59
|
+
throw new Error("cascade.json exec.stages.sources must be a list of project-relative paths");
|
|
60
|
+
}
|
|
61
|
+
const workers = value?.workers;
|
|
62
|
+
if (workers !== void 0 && (typeof workers !== "number" || !Number.isInteger(workers) || workers < 1)) {
|
|
63
|
+
throw new Error("cascade.json exec.stages.workers must be a whole number of at least 1");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
python,
|
|
67
|
+
entrypoint,
|
|
68
|
+
...worker ? { worker } : {},
|
|
69
|
+
...sources ? { sources } : {},
|
|
70
|
+
...workers !== void 0 ? { workers } : {},
|
|
71
|
+
cache: readCachePolicy(value?.cache)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function readCachePolicy(value) {
|
|
75
|
+
const strings = (list, name) => {
|
|
76
|
+
if (list === void 0) return [];
|
|
77
|
+
if (!Array.isArray(list) || list.some((item) => typeof item !== "string")) {
|
|
78
|
+
throw new Error(`cascade.json exec.stages.cache.${name} must be a list of names`);
|
|
79
|
+
}
|
|
80
|
+
return list;
|
|
81
|
+
};
|
|
82
|
+
if (value === void 0) return { enabled: true, scope: "cook", exclude: [], outputs: [] };
|
|
83
|
+
if (value === false) return { enabled: false, scope: "cook", exclude: [], outputs: [] };
|
|
84
|
+
if (value === true) return { enabled: true, scope: "all", exclude: [], outputs: [] };
|
|
85
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
86
|
+
const record = value;
|
|
87
|
+
if (record.scope !== void 0 && record.scope !== "cook" && record.scope !== "all") {
|
|
88
|
+
throw new Error('cascade.json exec.stages.cache.scope must be "cook" or "all"');
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
enabled: record.enabled !== false,
|
|
92
|
+
scope: record.scope === "all" ? "all" : "cook",
|
|
93
|
+
exclude: strings(record.exclude, "exclude"),
|
|
94
|
+
outputs: strings(record.outputs, "outputs")
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
throw new Error("cascade.json exec.stages.cache must be true, false or { exclude, outputs, scope }");
|
|
98
|
+
}
|
|
99
|
+
async function runProjectStage(project, stage, args, options = {}) {
|
|
100
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(stage)) {
|
|
101
|
+
throw new Error("stage contains unsupported characters");
|
|
102
|
+
}
|
|
103
|
+
const { python, entrypoint } = readStageConfig(project);
|
|
104
|
+
const script = project.resolve(entrypoint);
|
|
105
|
+
const payload = JSON.stringify(args);
|
|
106
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
107
|
+
const child = execFile(
|
|
108
|
+
python,
|
|
109
|
+
[script, "--stage", stage, "--args", "-"],
|
|
110
|
+
{
|
|
111
|
+
cwd: project.root,
|
|
112
|
+
timeout: options.timeout ?? TIMEOUT_MS,
|
|
113
|
+
maxBuffer: MAX_BUFFER,
|
|
114
|
+
env: {
|
|
115
|
+
...options.env ?? process.env,
|
|
116
|
+
...options.cascade ? { CASCADE_STAGE_ENVELOPE: JSON.stringify(options.cascade) } : {},
|
|
117
|
+
// Python checks a .pyc against the source's size and whole-second
|
|
118
|
+
// mtime, so a quick same-length edit would run the old bytecode.
|
|
119
|
+
...options.codeHash ? { PYTHONPYCACHEPREFIX: path.join(os.tmpdir(), "cascade-pycache", options.codeHash) } : {}
|
|
120
|
+
},
|
|
121
|
+
signal: options.signal
|
|
122
|
+
},
|
|
123
|
+
(error, out, err) => {
|
|
124
|
+
if (error) {
|
|
125
|
+
const output = err?.trim() || out.trim().split("\n").pop() || error.message;
|
|
126
|
+
return reject(new Error(`stage "${stage}" failed: ${output}`));
|
|
127
|
+
}
|
|
128
|
+
if (err?.trim()) options.onOutput?.("stderr", err.trim());
|
|
129
|
+
resolve(out);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
child.stdin?.end(payload);
|
|
133
|
+
});
|
|
134
|
+
if (options.onOutput) {
|
|
135
|
+
const lines = stdout.trim().split("\n");
|
|
136
|
+
const chatter = lines.slice(0, -1).join("\n").trim();
|
|
137
|
+
if (chatter) options.onOutput("stdout", chatter);
|
|
138
|
+
}
|
|
139
|
+
const lastLine = stdout.trim().split("\n").pop() || "{}";
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(lastLine);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new Error(`stage "${stage}" returned no JSON: ${lastLine.slice(0, 200)}`);
|
|
145
|
+
}
|
|
146
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
147
|
+
throw new Error(`stage "${stage}" failed: ${String(parsed.error)}`);
|
|
148
|
+
}
|
|
149
|
+
return parsed;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
readStageConfig,
|
|
154
|
+
runProjectStage
|
|
155
|
+
};
|
|
156
|
+
//# sourceMappingURL=chunk-X566FWBV.js.map
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// server/src/pythonWorker.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "readline";
|
|
6
|
+
var REQUEST_TIMEOUT_MS = 12e4;
|
|
7
|
+
var CANCEL_GRACE_MS = 3e3;
|
|
8
|
+
var StageCancelledError = class extends Error {
|
|
9
|
+
constructor(stage) {
|
|
10
|
+
super(`stage "${stage}" was cancelled`);
|
|
11
|
+
this.cancelled = true;
|
|
12
|
+
this.name = "StageCancelledError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var liveWorkers = /* @__PURE__ */ new Set();
|
|
16
|
+
function pythonWorkerStatuses() {
|
|
17
|
+
return [...liveWorkers].map((worker) => worker.status);
|
|
18
|
+
}
|
|
19
|
+
function shutdownAllPythonWorkers() {
|
|
20
|
+
for (const worker of [...liveWorkers]) worker.shutdown();
|
|
21
|
+
}
|
|
22
|
+
var PythonWorker = class {
|
|
23
|
+
constructor(project, entrypoint, python = "python3", env = process.env, options = {}) {
|
|
24
|
+
this.proc = null;
|
|
25
|
+
this.nextId = 1;
|
|
26
|
+
this.queue = [];
|
|
27
|
+
/** The request the worker is running, or one it was told to abandon and has
|
|
28
|
+
* not yet answered. Nothing else is written until it answers or exits. */
|
|
29
|
+
this.inFlight = null;
|
|
30
|
+
/** Set once the in-flight request has been cancelled: its answer is
|
|
31
|
+
* discarded whatever it says. */
|
|
32
|
+
this.inFlightCancelled = false;
|
|
33
|
+
this.killTimer = null;
|
|
34
|
+
/** The code hash the running process started with. */
|
|
35
|
+
this.spawnedCodeHash = null;
|
|
36
|
+
/** Set by a restart, cleared by the answer that follows it. */
|
|
37
|
+
this.restartPending = false;
|
|
38
|
+
/** How many times the worker restarted because its code changed. */
|
|
39
|
+
this.restarts = 0;
|
|
40
|
+
this.project = project;
|
|
41
|
+
this.entrypoint = entrypoint;
|
|
42
|
+
this.python = python;
|
|
43
|
+
this.env = env;
|
|
44
|
+
this.cancelGraceMs = options.cancelGraceMs ?? CANCEL_GRACE_MS;
|
|
45
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
46
|
+
this.sources = options.sources ?? null;
|
|
47
|
+
this.onStderr = options.onStderr ?? ((text) => console.error("[python-worker]", text));
|
|
48
|
+
liveWorkers.add(this);
|
|
49
|
+
}
|
|
50
|
+
get status() {
|
|
51
|
+
const pid = this.pid;
|
|
52
|
+
return {
|
|
53
|
+
entrypoint: this.entrypoint,
|
|
54
|
+
pid,
|
|
55
|
+
state: pid === null ? "stopped" : this.inFlight ? "busy" : "idle",
|
|
56
|
+
queued: this.queue.length,
|
|
57
|
+
codeHash: this.spawnedCodeHash,
|
|
58
|
+
restarts: this.restarts
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Requests sent or waiting: what a pool balances on. */
|
|
62
|
+
get pending() {
|
|
63
|
+
return (this.inFlight ? 1 : 0) + this.queue.length;
|
|
64
|
+
}
|
|
65
|
+
/** The code hash the running worker started with; null before the first
|
|
66
|
+
* request or when sources are not watched. */
|
|
67
|
+
get codeHash() {
|
|
68
|
+
return this.spawnedCodeHash;
|
|
69
|
+
}
|
|
70
|
+
/** The worker's process id, or null when none is running. For tests and
|
|
71
|
+
* diagnostics: a cancelled stage must not leave an orphan behind. */
|
|
72
|
+
get pid() {
|
|
73
|
+
return this.proc?.pid ?? null;
|
|
74
|
+
}
|
|
75
|
+
ensureStarted() {
|
|
76
|
+
if (this.proc && this.proc.exitCode === null && this.proc.signalCode === null) return this.proc;
|
|
77
|
+
this.spawnedCodeHash = this.sources?.codeHash() ?? null;
|
|
78
|
+
const scriptPath = this.project.resolve(this.entrypoint);
|
|
79
|
+
const proc = spawn(this.python, [scriptPath], { cwd: this.project.root, env: this.spawnEnv() });
|
|
80
|
+
readline.createInterface({ input: proc.stdout }).on("line", (line) => this.handleLine(proc, line));
|
|
81
|
+
proc.stderr.on("data", (chunk) => this.onStderr(chunk.toString().trimEnd()));
|
|
82
|
+
proc.stdin.on("error", () => {
|
|
83
|
+
});
|
|
84
|
+
proc.on("error", (error) => {
|
|
85
|
+
if (this.proc !== proc) return;
|
|
86
|
+
this.proc = null;
|
|
87
|
+
const current = this.inFlight;
|
|
88
|
+
this.inFlight = null;
|
|
89
|
+
this.inFlightCancelled = false;
|
|
90
|
+
if (current) this.settle(current, new Error(`python worker could not start (${this.python}): ${error.message}`));
|
|
91
|
+
else this.onStderr(`python worker could not start (${this.python}): ${error.message}`);
|
|
92
|
+
});
|
|
93
|
+
proc.on("exit", (code, signal) => {
|
|
94
|
+
if (this.proc !== proc) return;
|
|
95
|
+
this.proc = null;
|
|
96
|
+
this.clearKillTimer();
|
|
97
|
+
const current = this.inFlight;
|
|
98
|
+
const cancelled = this.inFlightCancelled;
|
|
99
|
+
this.inFlight = null;
|
|
100
|
+
this.inFlightCancelled = false;
|
|
101
|
+
if (current && !cancelled) {
|
|
102
|
+
console.warn(`[python-worker] exited (${signal ?? `code ${code}`}) \u2014 will respawn on next call`);
|
|
103
|
+
this.settle(current, new Error(`python worker exited (${signal ?? `code ${code}`}) before responding`));
|
|
104
|
+
}
|
|
105
|
+
this.pump();
|
|
106
|
+
});
|
|
107
|
+
this.proc = proc;
|
|
108
|
+
return proc;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The environment for a new process. With sources watched, Python's bytecode
|
|
112
|
+
* cache is moved to a folder named by the code hash.
|
|
113
|
+
*
|
|
114
|
+
* Without that a restart is not enough: Python validates a `.pyc` by the
|
|
115
|
+
* source's size and its mtime in whole seconds, so an edit that keeps the
|
|
116
|
+
* length (`VALUE = 1` to `VALUE = 2`) within the same second is served from
|
|
117
|
+
* the old bytecode by the fresh process. Found by the respawn test.
|
|
118
|
+
*/
|
|
119
|
+
spawnEnv() {
|
|
120
|
+
if (!this.spawnedCodeHash) return this.env;
|
|
121
|
+
const prefix = path.join(os.tmpdir(), "cascade-pycache", this.spawnedCodeHash);
|
|
122
|
+
return { ...this.env, PYTHONPYCACHEPREFIX: prefix };
|
|
123
|
+
}
|
|
124
|
+
handleLine(proc, line) {
|
|
125
|
+
if (this.proc !== proc) return;
|
|
126
|
+
let msg;
|
|
127
|
+
try {
|
|
128
|
+
msg = JSON.parse(line);
|
|
129
|
+
} catch {
|
|
130
|
+
console.error("[python-worker] non-JSON line from worker:", line);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const current = this.inFlight;
|
|
134
|
+
if (!current || msg.id !== current.id) return;
|
|
135
|
+
const cancelled = this.inFlightCancelled;
|
|
136
|
+
this.inFlight = null;
|
|
137
|
+
this.inFlightCancelled = false;
|
|
138
|
+
this.clearKillTimer();
|
|
139
|
+
if (!cancelled) {
|
|
140
|
+
const restarted = this.restartPending;
|
|
141
|
+
this.restartPending = false;
|
|
142
|
+
if (msg.ok) {
|
|
143
|
+
this.settle(current, null, {
|
|
144
|
+
result: msg.result,
|
|
145
|
+
codeHash: this.spawnedCodeHash,
|
|
146
|
+
restarted,
|
|
147
|
+
...typeof msg.cache === "string" ? { projectCache: msg.cache } : {}
|
|
148
|
+
});
|
|
149
|
+
} else this.settle(current, new Error(msg.error || "python worker reported an error"));
|
|
150
|
+
}
|
|
151
|
+
this.pump();
|
|
152
|
+
}
|
|
153
|
+
settle(request, error, answer) {
|
|
154
|
+
if (request.timer) clearTimeout(request.timer);
|
|
155
|
+
request.timer = null;
|
|
156
|
+
if (error) request.reject(error);
|
|
157
|
+
else request.resolve(answer);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Stop a running worker whose code changed since it started. Only ever
|
|
161
|
+
* called with nothing in flight, so no request is lost: the exit handler
|
|
162
|
+
* finds no current request, and the next `ensureStarted` spawns afresh.
|
|
163
|
+
*/
|
|
164
|
+
restartIfCodeChanged() {
|
|
165
|
+
if (!this.sources || !this.proc) return;
|
|
166
|
+
const now = this.sources.codeHash();
|
|
167
|
+
if (now === this.spawnedCodeHash) return;
|
|
168
|
+
console.warn(`[python-worker] stage code changed (${this.spawnedCodeHash} -> ${now}), restarting`);
|
|
169
|
+
const stale = this.proc;
|
|
170
|
+
this.proc = null;
|
|
171
|
+
stale.kill();
|
|
172
|
+
this.restarts += 1;
|
|
173
|
+
this.restartPending = true;
|
|
174
|
+
}
|
|
175
|
+
clearKillTimer() {
|
|
176
|
+
if (this.killTimer) clearTimeout(this.killTimer);
|
|
177
|
+
this.killTimer = null;
|
|
178
|
+
}
|
|
179
|
+
/** Send the next queued request if the worker is free. */
|
|
180
|
+
pump() {
|
|
181
|
+
if (this.inFlight) return;
|
|
182
|
+
const next = this.queue.shift();
|
|
183
|
+
if (!next) return;
|
|
184
|
+
this.restartIfCodeChanged();
|
|
185
|
+
const proc = this.ensureStarted();
|
|
186
|
+
this.inFlight = next;
|
|
187
|
+
this.inFlightCancelled = false;
|
|
188
|
+
proc.stdin.write(next.line);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Start the process now rather than on the first call, so its interpreter
|
|
192
|
+
* start and imports (4.7 s for cloud-plots) overlap other work. A render
|
|
193
|
+
* worker warms while the probe frames run. A no-op when one is running.
|
|
194
|
+
*/
|
|
195
|
+
warm() {
|
|
196
|
+
if (this.inFlight || this.queue.length) return;
|
|
197
|
+
this.restartIfCodeChanged();
|
|
198
|
+
this.ensureStarted();
|
|
199
|
+
}
|
|
200
|
+
/** Run a stage and return its result. */
|
|
201
|
+
async call(stage, args, options = {}) {
|
|
202
|
+
return (await this.callWithMeta(stage, args, options)).result;
|
|
203
|
+
}
|
|
204
|
+
/** Run a stage and return its result with the code hash that produced it. */
|
|
205
|
+
callWithMeta(stage, args, options = {}) {
|
|
206
|
+
const { signal, cascade } = options;
|
|
207
|
+
if (signal?.aborted) return Promise.reject(new StageCancelledError(stage));
|
|
208
|
+
const id = this.nextId++;
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const onAbort = () => this.cancel(id);
|
|
211
|
+
const request = {
|
|
212
|
+
id,
|
|
213
|
+
stage,
|
|
214
|
+
line: JSON.stringify(cascade ? { id, stage, args, cascade } : { id, stage, args }) + "\n",
|
|
215
|
+
resolve: (r) => {
|
|
216
|
+
signal?.removeEventListener("abort", onAbort);
|
|
217
|
+
resolve(r);
|
|
218
|
+
},
|
|
219
|
+
reject: (e) => {
|
|
220
|
+
signal?.removeEventListener("abort", onAbort);
|
|
221
|
+
reject(e);
|
|
222
|
+
},
|
|
223
|
+
timer: null
|
|
224
|
+
};
|
|
225
|
+
request.timer = setTimeout(() => {
|
|
226
|
+
this.cancel(id, new Error(`python worker timed out after ${this.requestTimeoutMs}ms on stage "${stage}"`));
|
|
227
|
+
}, this.requestTimeoutMs);
|
|
228
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
229
|
+
this.queue.push(request);
|
|
230
|
+
this.pump();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Withdraw a request. A queued one is dropped without ever reaching Python;
|
|
235
|
+
* the one in flight is interrupted, and its answer — whenever it comes — is
|
|
236
|
+
* discarded, so a superseded result is never delivered.
|
|
237
|
+
*/
|
|
238
|
+
cancel(id, reason) {
|
|
239
|
+
const queued = this.queue.findIndex((request) => request.id === id);
|
|
240
|
+
if (queued >= 0) {
|
|
241
|
+
const [request] = this.queue.splice(queued, 1);
|
|
242
|
+
this.settle(request, reason ?? new StageCancelledError(request.stage));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const current = this.inFlight;
|
|
246
|
+
if (!current || current.id !== id || this.inFlightCancelled) return;
|
|
247
|
+
this.inFlightCancelled = true;
|
|
248
|
+
this.settle(current, reason ?? new StageCancelledError(current.stage));
|
|
249
|
+
const proc = this.proc;
|
|
250
|
+
if (!proc) return;
|
|
251
|
+
proc.kill("SIGINT");
|
|
252
|
+
this.clearKillTimer();
|
|
253
|
+
this.killTimer = setTimeout(() => {
|
|
254
|
+
this.killTimer = null;
|
|
255
|
+
if (this.proc === proc && this.inFlight === current) proc.kill("SIGKILL");
|
|
256
|
+
}, this.cancelGraceMs);
|
|
257
|
+
}
|
|
258
|
+
shutdown() {
|
|
259
|
+
liveWorkers.delete(this);
|
|
260
|
+
this.clearKillTimer();
|
|
261
|
+
for (const request of this.queue.splice(0)) this.settle(request, new Error("python worker shut down"));
|
|
262
|
+
const current = this.inFlight;
|
|
263
|
+
this.inFlight = null;
|
|
264
|
+
this.inFlightCancelled = false;
|
|
265
|
+
if (current) this.settle(current, new Error("python worker shut down"));
|
|
266
|
+
const proc = this.proc;
|
|
267
|
+
this.proc = null;
|
|
268
|
+
proc?.kill();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
function defaultStageWorkerCount(cores = os.availableParallelism?.() ?? os.cpus().length, memoryBytes = os.totalmem()) {
|
|
272
|
+
const byCores = Math.floor(cores / 4);
|
|
273
|
+
const byMemory = Math.floor(memoryBytes / (8 * 1024 ** 3));
|
|
274
|
+
return Math.max(2, Math.min(4, byCores, byMemory));
|
|
275
|
+
}
|
|
276
|
+
var PythonWorkerPool = class {
|
|
277
|
+
constructor(size, create) {
|
|
278
|
+
this.size = size;
|
|
279
|
+
this.create = create;
|
|
280
|
+
this.workers = [];
|
|
281
|
+
}
|
|
282
|
+
/** The workers started so far. */
|
|
283
|
+
get members() {
|
|
284
|
+
return this.workers;
|
|
285
|
+
}
|
|
286
|
+
/** Start the first worker now (see `PythonWorker.warm`). */
|
|
287
|
+
warm() {
|
|
288
|
+
if (!this.workers.length) this.workers.push(this.create());
|
|
289
|
+
this.workers[0].warm();
|
|
290
|
+
}
|
|
291
|
+
callWithMeta(stage, args, options = {}) {
|
|
292
|
+
return this.pick().callWithMeta(stage, args, options);
|
|
293
|
+
}
|
|
294
|
+
pick() {
|
|
295
|
+
const idle = this.workers.find((worker) => worker.pending === 0);
|
|
296
|
+
if (idle) return idle;
|
|
297
|
+
if (this.workers.length < Math.max(1, this.size)) {
|
|
298
|
+
const worker = this.create();
|
|
299
|
+
this.workers.push(worker);
|
|
300
|
+
return worker;
|
|
301
|
+
}
|
|
302
|
+
return this.workers.reduce((best, worker) => worker.pending < best.pending ? worker : best);
|
|
303
|
+
}
|
|
304
|
+
shutdown() {
|
|
305
|
+
for (const worker of this.workers.splice(0)) worker.shutdown();
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export {
|
|
310
|
+
StageCancelledError,
|
|
311
|
+
pythonWorkerStatuses,
|
|
312
|
+
shutdownAllPythonWorkers,
|
|
313
|
+
PythonWorker,
|
|
314
|
+
defaultStageWorkerCount,
|
|
315
|
+
PythonWorkerPool
|
|
316
|
+
};
|
|
317
|
+
//# sourceMappingURL=chunk-YH637PDL.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/cli/runEnv.ts
|
|
2
|
+
function parseEnvArgs(args) {
|
|
3
|
+
const pairs = [];
|
|
4
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
5
|
+
if (args[index] !== "--env") continue;
|
|
6
|
+
const pair = args[index + 1];
|
|
7
|
+
if (pair === void 0) throw new Error("--env takes NAME=value");
|
|
8
|
+
pairs.push(pair);
|
|
9
|
+
}
|
|
10
|
+
if (!pairs.length) return void 0;
|
|
11
|
+
const env = {};
|
|
12
|
+
for (const pair of pairs) {
|
|
13
|
+
const at = pair.indexOf("=");
|
|
14
|
+
const name = at > 0 ? pair.slice(0, at) : "";
|
|
15
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`--env takes NAME=value (got "${pair}")`);
|
|
16
|
+
env[name] = pair.slice(at + 1);
|
|
17
|
+
}
|
|
18
|
+
return env;
|
|
19
|
+
}
|
|
20
|
+
function applyEnv(env) {
|
|
21
|
+
if (!env) return () => {
|
|
22
|
+
};
|
|
23
|
+
const previous = Object.keys(env).map((name) => [name, process.env[name]]);
|
|
24
|
+
for (const [name, value] of Object.entries(env)) process.env[name] = value;
|
|
25
|
+
return () => {
|
|
26
|
+
for (const [name, value] of previous) {
|
|
27
|
+
if (value === void 0) delete process.env[name];
|
|
28
|
+
else process.env[name] = value;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
parseEnvArgs,
|
|
35
|
+
applyEnv
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=chunk-ZL4JTG5Y.js.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// src/nodes/definition/studioImages.ts
|
|
2
|
+
var STUDIO_IMAGE_ROOT = ".cascade-studio/";
|
|
3
|
+
var PER_NAME = 3;
|
|
4
|
+
var BUDGET_BYTES = 512 * 1024 * 1024;
|
|
5
|
+
var kept = /* @__PURE__ */ new Map();
|
|
6
|
+
var bytes = 0;
|
|
7
|
+
var sequence = 0;
|
|
8
|
+
function clean(path) {
|
|
9
|
+
return path.replace(/^\.\//, "");
|
|
10
|
+
}
|
|
11
|
+
function drop(path) {
|
|
12
|
+
const entry = kept.get(path);
|
|
13
|
+
if (!entry) return;
|
|
14
|
+
kept.delete(path);
|
|
15
|
+
bytes -= entry.pixels.data.byteLength;
|
|
16
|
+
}
|
|
17
|
+
function keepStudioImage(pixels, name = "image.png") {
|
|
18
|
+
const safe = name.replace(/[^A-Za-z0-9._-]/g, "_") || "image.png";
|
|
19
|
+
const path = `${STUDIO_IMAGE_ROOT}${++sequence}-${safe}`;
|
|
20
|
+
kept.set(path, { pixels, name: safe });
|
|
21
|
+
bytes += pixels.data.byteLength;
|
|
22
|
+
const siblings = [...kept].filter(([, entry]) => entry.name === safe).map(([key]) => key);
|
|
23
|
+
for (const old of siblings.slice(0, Math.max(0, siblings.length - PER_NAME))) drop(old);
|
|
24
|
+
for (const old of kept.keys()) {
|
|
25
|
+
if (bytes <= BUDGET_BYTES || old === path) break;
|
|
26
|
+
drop(old);
|
|
27
|
+
}
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
function isStudioImage(path) {
|
|
31
|
+
return kept.has(clean(path));
|
|
32
|
+
}
|
|
33
|
+
function studioImagePixels(path) {
|
|
34
|
+
return kept.get(clean(path))?.pixels;
|
|
35
|
+
}
|
|
36
|
+
function draw(pixels) {
|
|
37
|
+
const globals = globalThis;
|
|
38
|
+
if (!globals.ImageData) throw new Error("Studio cannot draw a kept image: no ImageData here");
|
|
39
|
+
let canvas;
|
|
40
|
+
if (globals.document) {
|
|
41
|
+
canvas = globals.document.createElement("canvas");
|
|
42
|
+
canvas.width = pixels.width;
|
|
43
|
+
canvas.height = pixels.height;
|
|
44
|
+
} else if (globals.OffscreenCanvas) {
|
|
45
|
+
canvas = new globals.OffscreenCanvas(pixels.width, pixels.height);
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error("Studio cannot draw a kept image: no canvas here");
|
|
48
|
+
}
|
|
49
|
+
canvas.getContext("2d").putImageData(new globals.ImageData(pixels.data, pixels.width, pixels.height), 0, 0);
|
|
50
|
+
return canvas;
|
|
51
|
+
}
|
|
52
|
+
async function studioImageBytes(path) {
|
|
53
|
+
const entry = kept.get(clean(path));
|
|
54
|
+
if (!entry) return void 0;
|
|
55
|
+
const canvas = draw(entry.pixels);
|
|
56
|
+
const blob = canvas.convertToBlob ? await canvas.convertToBlob({ type: "image/png" }) : await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
|
|
57
|
+
if (!blob) throw new Error(`Studio could not encode ${path}`);
|
|
58
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
59
|
+
}
|
|
60
|
+
function studioImageUrl(path) {
|
|
61
|
+
const entry = kept.get(clean(path));
|
|
62
|
+
if (!entry) return void 0;
|
|
63
|
+
entry.url ??= draw(entry.pixels).toDataURL("image/png");
|
|
64
|
+
return entry.url;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/nodes/definition/browserCapabilities.ts
|
|
68
|
+
function assetUrl(path) {
|
|
69
|
+
const clean2 = path.replace(/^\.\//, "");
|
|
70
|
+
return `/api/media/${clean2.split("/").map(encodeURIComponent).join("/")}?raw=1`;
|
|
71
|
+
}
|
|
72
|
+
var browserAssetCapability = {
|
|
73
|
+
async read(ref, { signal }) {
|
|
74
|
+
const kept2 = await studioImageBytes(ref.path);
|
|
75
|
+
if (kept2) return kept2;
|
|
76
|
+
const response = await fetch(assetUrl(ref.path), { signal });
|
|
77
|
+
if (!response.ok) throw new Error(`Could not read asset ${ref.path} (${response.status})`);
|
|
78
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
79
|
+
},
|
|
80
|
+
async write(data, metadata, { signal }) {
|
|
81
|
+
const name = metadata.suggestedName?.trim() || "asset.bin";
|
|
82
|
+
const form = new FormData();
|
|
83
|
+
form.append("file", new Blob([new Uint8Array(data)], { type: metadata.mediaType }), name);
|
|
84
|
+
const response = await fetch("/api/assets", {
|
|
85
|
+
method: "POST",
|
|
86
|
+
body: form,
|
|
87
|
+
signal
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) throw new Error(`Could not write asset ${name} (${response.status})`);
|
|
90
|
+
const result = await response.json();
|
|
91
|
+
if (!result.path) throw new Error(`Asset upload returned no path for ${name}`);
|
|
92
|
+
return { path: result.path, ...metadata.mediaType ? { mediaType: metadata.mediaType } : {} };
|
|
93
|
+
},
|
|
94
|
+
imagePixels(ref) {
|
|
95
|
+
return studioImagePixels(ref.path);
|
|
96
|
+
},
|
|
97
|
+
async resolveUrl(ref) {
|
|
98
|
+
return { value: isStudioImage(ref.path) ? studioImageUrl(ref.path) : assetUrl(ref.path), release: () => {
|
|
99
|
+
} };
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var studioImageAssetCapability = {
|
|
103
|
+
...browserAssetCapability,
|
|
104
|
+
async keepImage(pixels, metadata) {
|
|
105
|
+
return { path: keepStudioImage(pixels, metadata.suggestedName), mediaType: "image/png" };
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
browserAssetCapability
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=chunk-ZSCUZE6I.js.map
|