@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,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cascade/image` — the one image API a node body uses, in every host.
|
|
3
|
+
*
|
|
4
|
+
* A node that draws used to have two honest choices and one dishonest one.
|
|
5
|
+
* `new OffscreenCanvas` worked in Studio and under `cascade run` (the CLI
|
|
6
|
+
* installs Skia's canvas as that global), but the bytes in and out went
|
|
7
|
+
* through `cascade/io`'s project paths, and fonts had no portable door at all.
|
|
8
|
+
* So the nodes that needed fonts or files reached for `@napi-rs/canvas` and
|
|
9
|
+
* `node:fs` directly, declared `runsOn: 'server'`, and could never be exported
|
|
10
|
+
* as a browser editor — the commons plotter-social job was blocked on exactly
|
|
11
|
+
* that ("Cascade has no browser image contract").
|
|
12
|
+
*
|
|
13
|
+
* The contract, in full:
|
|
14
|
+
*
|
|
15
|
+
* - **Surface.** `createCanvas(w, h)` is an `OffscreenCanvas`: the browser's
|
|
16
|
+
* own in a page or a worker, Skia's (`@napi-rs/canvas`, installed by the
|
|
17
|
+
* headless host) under `cascade run`/`render`/MCP. `context2d(canvas)` asks
|
|
18
|
+
* for a CPU-backed 2D context (`willReadFrequently`), which is what keeps
|
|
19
|
+
* Chrome on the same software Skia rasteriser the headless host uses, and
|
|
20
|
+
* is the right hint anyway for nodes that read pixels back.
|
|
21
|
+
* - **Pixels in.** `readImage(context, ref)` reads an `ImageRef`, `AssetRef`
|
|
22
|
+
* or project path through the node's declared `assets` capability and
|
|
23
|
+
* decodes it with `decodeImage` (`createImageBitmap` in both hosts).
|
|
24
|
+
* - **Pixels out.** `writeImage(context, canvas, name)` encodes PNG through
|
|
25
|
+
* `encodeImage` and stores it with `assets.write`, returning the `ImageRef`
|
|
26
|
+
* to set on an `image` output. The host decides where it lands
|
|
27
|
+
* (`.cascade-cache/<name>.<hash>.png` headless, an in-memory generated
|
|
28
|
+
* asset in an exported editor), so a node never names a path.
|
|
29
|
+
* - **Fonts.** `registerFont(family, bytes)` makes a face drawable by
|
|
30
|
+
* `ctx.font` in this host: `FontFace` added to the page's or worker's font
|
|
31
|
+
* set in a browser, Skia's `GlobalFonts` headless. Bytes come from the same
|
|
32
|
+
* `assets` capability, so a font is a project file like any other.
|
|
33
|
+
* - **Refusal, not fallback.** No canvas in this host, or no `assets`
|
|
34
|
+
* capability declared, is an error with one sentence saying which, never a
|
|
35
|
+
* silent blank image.
|
|
36
|
+
*
|
|
37
|
+
* A node using it declares `runsOn: 'portable'` and `capabilities: ['assets']`
|
|
38
|
+
* and imports nothing host-specific (`node:*`, `@napi-rs/canvas`, `document`).
|
|
39
|
+
* Then the same body cooks in Studio, in `cascade run`, and in an exported
|
|
40
|
+
* editor's worker.
|
|
41
|
+
*
|
|
42
|
+
* The headless host installs its font door under a global key, like
|
|
43
|
+
* `cascade/io`'s bridge, because the node's copy of this module and the CLI's
|
|
44
|
+
* copy are not always the same module instance.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { canvasPixels, pixelsBitmap, type ImagePixels } from './io.js';
|
|
48
|
+
|
|
49
|
+
export type { ImagePixels } from './io.js';
|
|
50
|
+
|
|
51
|
+
/** An image port value. Mirrors Cascade's core `ImageRef`. */
|
|
52
|
+
export interface ImageRef {
|
|
53
|
+
path: string;
|
|
54
|
+
size: [number, number];
|
|
55
|
+
channels: 'r' | 'a' | 'rg' | 'rgb' | 'rgba';
|
|
56
|
+
depth: 'u8' | 'u16' | 'f16' | 'f32';
|
|
57
|
+
space: 'srgb' | 'linear';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Anything naming stored bytes: an `ImageRef`, an `AssetRef`, or a project path. */
|
|
61
|
+
export type ImageSourceRef = string | { readonly path: string };
|
|
62
|
+
|
|
63
|
+
/** The part of the `assets` capability these helpers use. */
|
|
64
|
+
export interface ImageAssets {
|
|
65
|
+
read(ref: { path: string }, options: { signal: unknown }): Promise<Uint8Array>;
|
|
66
|
+
write(
|
|
67
|
+
data: Uint8Array,
|
|
68
|
+
metadata: { mediaType?: string; suggestedName?: string },
|
|
69
|
+
options: { signal: unknown },
|
|
70
|
+
): Promise<{ path: string; mediaType?: string }>;
|
|
71
|
+
/** Optional: store an intermediate from its pixels (see core `AssetCapability.keepImage`). */
|
|
72
|
+
keepImage?(
|
|
73
|
+
pixels: ImagePixels,
|
|
74
|
+
metadata: { suggestedName?: string },
|
|
75
|
+
options: { signal: unknown },
|
|
76
|
+
): Promise<{ path: string; mediaType?: string }>;
|
|
77
|
+
/** Optional: the pixels behind a stored image while the host holds them in memory. */
|
|
78
|
+
imagePixels?(ref: { path: string }): ImagePixels | undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The slice of a definition-v1 `NodeExecutionContext` these helpers need, so
|
|
83
|
+
* a node passes its own `context`. Typed loosely on purpose: a node that did
|
|
84
|
+
* not declare `assets` still type-checks here and gets a sentence at run time
|
|
85
|
+
* naming the missing declaration rather than a type error about generics.
|
|
86
|
+
*/
|
|
87
|
+
export interface ImageNodeContext {
|
|
88
|
+
readonly capabilities: object;
|
|
89
|
+
readonly signal?: unknown;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type ImageType = 'image/png' | 'image/webp' | 'image/jpeg';
|
|
93
|
+
|
|
94
|
+
/** What a host gives `registerFont`. A browser realm needs none. */
|
|
95
|
+
export interface ImageHost {
|
|
96
|
+
/** For a `--verbose` line and for errors. */
|
|
97
|
+
readonly renderer: string;
|
|
98
|
+
registerFont(family: string, bytes: Uint8Array, descriptors: FontDescriptors): void | Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface FontDescriptors {
|
|
102
|
+
/** CSS weight, `'400'`, `'bold'`. Headless reads the weight from the file. */
|
|
103
|
+
weight?: string;
|
|
104
|
+
/** `'normal'` or `'italic'`. Headless reads the style from the file. */
|
|
105
|
+
style?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const HOST_KEY = '__cascadeImageHost';
|
|
109
|
+
|
|
110
|
+
const NO_CANVAS =
|
|
111
|
+
'cascade/image: this host has no canvas. In a browser this needs OffscreenCanvas; headless, Cascade installs ' +
|
|
112
|
+
'@napi-rs/canvas before a run (install it with "npm install @napi-rs/canvas" if it is missing).';
|
|
113
|
+
|
|
114
|
+
/** Install the headless host's font door. Returns the disposer. */
|
|
115
|
+
export function installImageHost(host: ImageHost | null): () => void {
|
|
116
|
+
const globals = globalThis as Record<string, unknown>;
|
|
117
|
+
const previous = globals[HOST_KEY];
|
|
118
|
+
globals[HOST_KEY] = host ?? undefined;
|
|
119
|
+
return () => {
|
|
120
|
+
globals[HOST_KEY] = previous;
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function installedHost(): ImageHost | null {
|
|
125
|
+
const host = (globalThis as Record<string, unknown>)[HOST_KEY] as ImageHost | undefined;
|
|
126
|
+
return host && typeof host.registerFont === 'function' ? host : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type CanvasConstructor = new (width: number, height: number) => OffscreenCanvas;
|
|
130
|
+
|
|
131
|
+
function canvasConstructor(): CanvasConstructor | undefined {
|
|
132
|
+
return (globalThis as { OffscreenCanvas?: CanvasConstructor }).OffscreenCanvas;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Whether this host can draw. Never throws. */
|
|
136
|
+
export function canvasAvailable(): boolean {
|
|
137
|
+
return typeof canvasConstructor() === 'function';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A drawing surface of whole pixels, at least 1x1. */
|
|
141
|
+
export function createCanvas(width: number, height: number): OffscreenCanvas {
|
|
142
|
+
const Canvas = canvasConstructor();
|
|
143
|
+
if (!Canvas) throw new Error(NO_CANVAS);
|
|
144
|
+
return new Canvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The canvas's 2D context, CPU-backed. `willReadFrequently` keeps a browser
|
|
149
|
+
* off the GPU rasteriser, so a node draws with the same software Skia in Chrome
|
|
150
|
+
* as headless, and `getImageData` does not stall on a readback.
|
|
151
|
+
*/
|
|
152
|
+
export function context2d(canvas: OffscreenCanvas): OffscreenCanvasRenderingContext2D {
|
|
153
|
+
const context = canvas.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D | null;
|
|
154
|
+
if (!context) throw new Error('cascade/image: the canvas has no 2D context');
|
|
155
|
+
return context;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** `createCanvas` and `context2d` in one: `const { canvas, context } = surface(w, h)`. */
|
|
159
|
+
export function surface(width: number, height: number): {
|
|
160
|
+
canvas: OffscreenCanvas;
|
|
161
|
+
context: OffscreenCanvasRenderingContext2D;
|
|
162
|
+
} {
|
|
163
|
+
const canvas = createCanvas(width, height);
|
|
164
|
+
return { canvas, context: context2d(canvas) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Encoded bytes (PNG, JPEG, WebP) to something `drawImage` accepts. */
|
|
168
|
+
export async function decodeImage(bytes: Uint8Array | ArrayBuffer | Blob): Promise<ImageBitmap> {
|
|
169
|
+
const create = (globalThis as { createImageBitmap?: (source: Blob) => Promise<ImageBitmap> }).createImageBitmap;
|
|
170
|
+
if (typeof create !== 'function') throw new Error(NO_CANVAS);
|
|
171
|
+
const blob = bytes instanceof Blob ? bytes : new Blob([bytes as BlobPart]);
|
|
172
|
+
const image = await create(blob);
|
|
173
|
+
if (!image?.width || !image?.height) throw new Error('cascade/image: could not decode the image data');
|
|
174
|
+
return image;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** A canvas's pixels as encoded bytes, PNG unless asked otherwise. */
|
|
178
|
+
export async function encodeImage(
|
|
179
|
+
canvas: OffscreenCanvas,
|
|
180
|
+
options: { type?: ImageType; quality?: number } = {},
|
|
181
|
+
): Promise<Uint8Array> {
|
|
182
|
+
const type = options.type ?? 'image/png';
|
|
183
|
+
if (typeof canvas.convertToBlob !== 'function') throw new Error('cascade/image: this canvas cannot encode (no convertToBlob)');
|
|
184
|
+
const blob = await canvas.convertToBlob({ type, ...(options.quality === undefined ? {} : { quality: options.quality }) });
|
|
185
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// One record per realm, so a font registered once is not added again on every
|
|
189
|
+
// cook. Keyed by family and content: new bytes under a family name register.
|
|
190
|
+
// The headless realm is keyed by its renderer name rather than the host
|
|
191
|
+
// object, because a long-lived process (MCP) installs a fresh host per run
|
|
192
|
+
// while Skia's font registry lives for the whole process.
|
|
193
|
+
const registered = new Map<unknown, Set<string>>();
|
|
194
|
+
|
|
195
|
+
function fingerprint(bytes: Uint8Array): string {
|
|
196
|
+
let hash = 0x811c9dc5;
|
|
197
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
198
|
+
hash ^= bytes[index]!;
|
|
199
|
+
hash = Math.imul(hash, 0x01000193);
|
|
200
|
+
}
|
|
201
|
+
return `${bytes.length}:${(hash >>> 0).toString(16)}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface BrowserFontSet { add(face: unknown): unknown }
|
|
205
|
+
type FontFaceConstructor = new (family: string, source: ArrayBuffer | Uint8Array, descriptors?: FontDescriptors) => {
|
|
206
|
+
load(): Promise<unknown>;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Make `family` drawable by `ctx.font` here. Idempotent per family and bytes.
|
|
211
|
+
* A browser realm (page or worker) uses `FontFace`; the headless host
|
|
212
|
+
* registers with Skia. A host with neither refuses.
|
|
213
|
+
*/
|
|
214
|
+
export async function registerFont(
|
|
215
|
+
family: string,
|
|
216
|
+
bytes: Uint8Array | ArrayBuffer,
|
|
217
|
+
descriptors: FontDescriptors = {},
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
if (!family.trim()) throw new Error('cascade/image: registerFont needs a family name');
|
|
220
|
+
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
221
|
+
if (!data.byteLength) throw new Error(`cascade/image: the font for ${family} is empty`);
|
|
222
|
+
const host = installedHost();
|
|
223
|
+
const globals = globalThis as { FontFace?: FontFaceConstructor; fonts?: BrowserFontSet; document?: { fonts?: BrowserFontSet } };
|
|
224
|
+
const fonts = host ? null : globals.fonts ?? globals.document?.fonts;
|
|
225
|
+
const realm: unknown = host ? `headless:${host.renderer}` : fonts ?? globalThis;
|
|
226
|
+
const key = `${family}\u0000${descriptors.weight ?? ''}\u0000${descriptors.style ?? ''}\u0000${fingerprint(data)}`;
|
|
227
|
+
const seen = registered.get(realm) ?? new Set<string>();
|
|
228
|
+
if (seen.has(key)) return;
|
|
229
|
+
if (host) {
|
|
230
|
+
await host.registerFont(family, data, descriptors);
|
|
231
|
+
} else if (typeof globals.FontFace === 'function' && fonts) {
|
|
232
|
+
// A copy: FontFace may detach or keep the buffer it is given.
|
|
233
|
+
const face = new globals.FontFace(family, data.slice().buffer, descriptors);
|
|
234
|
+
fonts.add(await face.load());
|
|
235
|
+
} else {
|
|
236
|
+
throw new Error('cascade/image: this host cannot register fonts (no FontFace and no headless canvas host)');
|
|
237
|
+
}
|
|
238
|
+
seen.add(key);
|
|
239
|
+
registered.set(realm, seen);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function assetsOf(context: ImageNodeContext): ImageAssets {
|
|
243
|
+
const assets = (context.capabilities as { assets?: ImageAssets }).assets;
|
|
244
|
+
if (!assets || typeof assets.read !== 'function' || typeof assets.write !== 'function') {
|
|
245
|
+
throw new Error("cascade/image: this node reads or writes images, so its definition must declare capabilities: ['assets']");
|
|
246
|
+
}
|
|
247
|
+
return assets;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function refOf(ref: ImageSourceRef): { path: string } {
|
|
251
|
+
const path = typeof ref === 'string' ? ref : ref?.path;
|
|
252
|
+
if (typeof path !== 'string' || !path) throw new Error('cascade/image: no image to read (an empty path or an unset image input)');
|
|
253
|
+
return { path };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const NEVER = { aborted: false, addEventListener() {}, removeEventListener() {} };
|
|
257
|
+
|
|
258
|
+
/** A stored file's bytes through the node's `assets` capability: an image, a font, anything. */
|
|
259
|
+
export async function readBytes(context: ImageNodeContext, ref: ImageSourceRef): Promise<Uint8Array> {
|
|
260
|
+
return assetsOf(context).read(refOf(ref), { signal: context.signal ?? NEVER });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Read and decode an image input, an asset, or a project file. */
|
|
264
|
+
export async function readImage(context: ImageNodeContext, ref: ImageSourceRef): Promise<ImageBitmap> {
|
|
265
|
+
// An image the host still holds in memory skips the read and the decode.
|
|
266
|
+
const pixels = assetsOf(context).imagePixels?.(refOf(ref));
|
|
267
|
+
if (pixels) return pixelsBitmap(pixels);
|
|
268
|
+
return decodeImage(await readBytes(context, ref));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Encode a canvas and store it; set the result on an `image` output. `name`
|
|
273
|
+
* is a suggestion (`context.nodeId + '.png'` keeps instances apart); the host
|
|
274
|
+
* picks the stored path.
|
|
275
|
+
*/
|
|
276
|
+
export async function writeImage(
|
|
277
|
+
context: ImageNodeContext,
|
|
278
|
+
canvas: OffscreenCanvas,
|
|
279
|
+
name: string,
|
|
280
|
+
options: { type?: ImageType; quality?: number; space?: 'srgb' | 'linear' } = {},
|
|
281
|
+
): Promise<ImageRef> {
|
|
282
|
+
const type = options.type ?? 'image/png';
|
|
283
|
+
const assets = assetsOf(context);
|
|
284
|
+
const signal = context.signal ?? NEVER;
|
|
285
|
+
// A PNG intermediate goes to the host as pixels when it can take them: a
|
|
286
|
+
// fast lossless write, or no encode at all until something needs the file.
|
|
287
|
+
const pixels = type === 'image/png' && assets.keepImage ? canvasPixels(canvas) : null;
|
|
288
|
+
const stored = pixels && assets.keepImage
|
|
289
|
+
? await assets.keepImage(pixels, { suggestedName: name }, { signal })
|
|
290
|
+
: await assets.write(
|
|
291
|
+
await encodeImage(canvas, { type, ...(options.quality === undefined ? {} : { quality: options.quality }) }),
|
|
292
|
+
{ mediaType: type, suggestedName: name },
|
|
293
|
+
{ signal },
|
|
294
|
+
);
|
|
295
|
+
return {
|
|
296
|
+
path: stored.path,
|
|
297
|
+
size: [canvas.width, canvas.height],
|
|
298
|
+
channels: 'rgba',
|
|
299
|
+
depth: 'u8',
|
|
300
|
+
space: options.space ?? 'srgb',
|
|
301
|
+
};
|
|
302
|
+
}
|
package/dist/cli/runtime/io.ts
CHANGED
|
@@ -51,6 +51,25 @@ export interface IoBridge {
|
|
|
51
51
|
write(path: string, data: Uint8Array): Promise<string>;
|
|
52
52
|
/** Host-owned display URL, valid until the host releases its stored asset. */
|
|
53
53
|
url?(path: string, options?: MediaOptions): string;
|
|
54
|
+
/**
|
|
55
|
+
* Store an intermediate image from its pixels and return the stored path,
|
|
56
|
+
* as `write` would. Optional. The headless host has it: it writes a fast
|
|
57
|
+
* lossless PNG, and in a run that keeps pixels it holds them in memory and
|
|
58
|
+
* writes behind (`pixels` below). Without it `saveImage` encodes and writes.
|
|
59
|
+
*/
|
|
60
|
+
keep?(path: string, pixels: ImagePixels): Promise<string>;
|
|
61
|
+
/** The pixels behind a path `keep` returned, while the host holds them. */
|
|
62
|
+
pixels?(path: string): ImagePixels | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* An image's pixels in memory. Mirrors Cascade's core `ImagePixels`: 8-bit
|
|
67
|
+
* RGBA, sRGB, unpremultiplied, top row first — `ImageData`'s layout.
|
|
68
|
+
*/
|
|
69
|
+
export interface ImagePixels {
|
|
70
|
+
readonly width: number;
|
|
71
|
+
readonly height: number;
|
|
72
|
+
readonly data: Uint8ClampedArray;
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
const IO_BRIDGE_KEY = '__cascadeIoBridge';
|
|
@@ -96,6 +115,8 @@ export async function loadBytes(path: string, options: MediaOptions = { raw: tru
|
|
|
96
115
|
* to hand it. Pass a width explicitly to work at a smaller size on purpose.
|
|
97
116
|
*/
|
|
98
117
|
export async function loadImage(path: string, options: MediaOptions = { raw: true }): Promise<HTMLImageElement> {
|
|
118
|
+
const held = heldBitmap(path);
|
|
119
|
+
if (held) return held as unknown as HTMLImageElement;
|
|
99
120
|
const image = new Image();
|
|
100
121
|
const bridge = localIo();
|
|
101
122
|
let temporaryUrl: string | undefined;
|
|
@@ -123,8 +144,41 @@ export async function loadImage(path: string, options: MediaOptions = { raw: tru
|
|
|
123
144
|
}
|
|
124
145
|
}
|
|
125
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Pixels the host still holds in memory for `path` (a path `keep` returned),
|
|
149
|
+
* as something `drawImage` takes, or null to read the file. Bypasses the read
|
|
150
|
+
* and the decode; the image is the same either way (`ImagePixels` is what a
|
|
151
|
+
* PNG decodes to).
|
|
152
|
+
*/
|
|
153
|
+
function heldBitmap(path: string): ImageBitmap | null {
|
|
154
|
+
if (/^(https?:|data:|blob:)/.test(path)) return null;
|
|
155
|
+
const pixels = localIo()?.pixels?.(path.replace(/^\.?\//, ''));
|
|
156
|
+
return pixels ? pixelsBitmap(pixels) : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* `ImagePixels` as a drawable: a canvas holding them, with an `ImageBitmap`'s
|
|
161
|
+
* `width`, `height` and `close()` (and an image's `naturalWidth`/`naturalHeight`).
|
|
162
|
+
*/
|
|
163
|
+
export function pixelsBitmap(pixels: ImagePixels): ImageBitmap {
|
|
164
|
+
const Canvas = (globalThis as { OffscreenCanvas?: new (w: number, h: number) => OffscreenCanvas }).OffscreenCanvas;
|
|
165
|
+
if (!Canvas) throw new Error('cascade/io: this host has no OffscreenCanvas to hold the image');
|
|
166
|
+
const canvas = new Canvas(pixels.width, pixels.height);
|
|
167
|
+
const context = canvas.getContext('2d') as OffscreenCanvasRenderingContext2D | null;
|
|
168
|
+
if (!context) throw new Error('cascade/io: the canvas has no 2D context');
|
|
169
|
+
const data = new ImageData(pixels.data as Uint8ClampedArray<ArrayBuffer>, pixels.width, pixels.height);
|
|
170
|
+
context.putImageData(data, 0, 0);
|
|
171
|
+
const drawable = canvas as unknown as Record<string, unknown>;
|
|
172
|
+
drawable.close = () => {};
|
|
173
|
+
drawable.naturalWidth = pixels.width;
|
|
174
|
+
drawable.naturalHeight = pixels.height;
|
|
175
|
+
return canvas as unknown as ImageBitmap;
|
|
176
|
+
}
|
|
177
|
+
|
|
126
178
|
/** Same, as an ImageBitmap — the right input for `texImage2D`. */
|
|
127
179
|
export async function loadBitmap(path: string, options: MediaOptions = { raw: true }): Promise<ImageBitmap> {
|
|
180
|
+
const held = heldBitmap(path);
|
|
181
|
+
if (held) return held;
|
|
128
182
|
const bridge = localIo();
|
|
129
183
|
if (bridge?.url) return createImageBitmap(await loadImage(path, options));
|
|
130
184
|
if (bridge) return createImageBitmap(new Blob([await loadBytes(path, options)]));
|
|
@@ -177,6 +231,15 @@ export async function saveImage(
|
|
|
177
231
|
const type = `image/${format}`;
|
|
178
232
|
let blob: Blob;
|
|
179
233
|
|
|
234
|
+
// A host that keeps intermediates gets the pixels, not an encode: the
|
|
235
|
+
// headless host writes a fast PNG (or holds them in memory), where the
|
|
236
|
+
// canvas's own encoder would spend six times as long.
|
|
237
|
+
const bridge = localIo();
|
|
238
|
+
if (format === 'png' && bridge?.keep) {
|
|
239
|
+
const pixels = canvasPixels(source);
|
|
240
|
+
if (pixels) return bridge.keep(path.replace(/^\.?\//, ''), pixels);
|
|
241
|
+
}
|
|
242
|
+
|
|
180
243
|
if (source instanceof Blob) {
|
|
181
244
|
blob = source;
|
|
182
245
|
} else if (typeof (source as OffscreenCanvas).convertToBlob === 'function') {
|
|
@@ -200,6 +263,21 @@ export async function saveImage(
|
|
|
200
263
|
return saveBytes(blob, path);
|
|
201
264
|
}
|
|
202
265
|
|
|
266
|
+
/**
|
|
267
|
+
* A canvas's pixels as `ImagePixels`, or null for anything that is not a 2D
|
|
268
|
+
* canvas (a Blob, an encoder-only source, a WebGL canvas, whose `getContext('2d')`
|
|
269
|
+
* is null).
|
|
270
|
+
*/
|
|
271
|
+
export function canvasPixels(source: unknown): ImagePixels | null {
|
|
272
|
+
const canvas = source as { width?: unknown; height?: unknown; getContext?: (kind: '2d') => CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null };
|
|
273
|
+
if (typeof canvas?.getContext !== 'function' || typeof canvas.width !== 'number' || typeof canvas.height !== 'number') return null;
|
|
274
|
+
if (!canvas.width || !canvas.height) return null;
|
|
275
|
+
const context = canvas.getContext('2d');
|
|
276
|
+
if (!context || typeof context.getImageData !== 'function') return null;
|
|
277
|
+
const image = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
278
|
+
return { width: image.width, height: image.height, data: image.data };
|
|
279
|
+
}
|
|
280
|
+
|
|
203
281
|
/** Write any bytes into the project cache and return the path. */
|
|
204
282
|
export async function saveBytes(data: Blob | ArrayBuffer | Uint8Array, path: string): Promise<string> {
|
|
205
283
|
const clean = path.replace(/^\.?\//, '');
|
|
@@ -115,7 +115,10 @@ async function overHttp<T>(stage: string, args: Record<string, unknown>, signal?
|
|
|
115
115
|
const postStage = async () => fetch('/api/exec', {
|
|
116
116
|
method: 'POST',
|
|
117
117
|
headers: { 'Content-Type': 'application/json', 'X-Cascade-Exec-Capability': await capability() },
|
|
118
|
-
|
|
118
|
+
// `cook`: a node's cook is a pure function of its inputs, so the server's
|
|
119
|
+
// default stage cache may serve it (see StageCachePolicy). A panel posts
|
|
120
|
+
// through its own api.runStage, without this, and is never cached unasked.
|
|
121
|
+
body: JSON.stringify({ stage, args, caller: 'cook' }),
|
|
119
122
|
signal,
|
|
120
123
|
});
|
|
121
124
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deployVercel,
|
|
3
|
+
exportEditor,
|
|
4
|
+
scanForLeaks,
|
|
5
|
+
vercelConfig
|
|
6
|
+
} from "./chunk-3RXL5GHB.js";
|
|
7
|
+
import "./chunk-OM6BEME6.js";
|
|
8
|
+
import "./chunk-MP653A5O.js";
|
|
9
|
+
import "./chunk-MI5JLQYA.js";
|
|
10
|
+
import "./chunk-GRIN2DDP.js";
|
|
11
|
+
import "./chunk-X4WKAOQP.js";
|
|
12
|
+
import "./chunk-DEQSEFFH.js";
|
|
13
|
+
import "./chunk-RXDIMXN5.js";
|
|
14
|
+
import "./chunk-QQPA2ZHH.js";
|
|
15
|
+
import "./chunk-KH4MUBKF.js";
|
|
16
|
+
import "./chunk-HSNOBIVT.js";
|
|
17
|
+
import "./chunk-L6B6FUQP.js";
|
|
18
|
+
import "./chunk-HFHWYL55.js";
|
|
19
|
+
import "./chunk-KBGUO27Q.js";
|
|
20
|
+
import "./chunk-ICH7N5PN.js";
|
|
21
|
+
import "./chunk-CO22XHNG.js";
|
|
22
|
+
import "./chunk-LTXWBXOM.js";
|
|
23
|
+
import "./chunk-BZ63XQ3I.js";
|
|
24
|
+
import "./chunk-YH637PDL.js";
|
|
25
|
+
import "./chunk-X566FWBV.js";
|
|
26
|
+
import "./chunk-WD62JIP6.js";
|
|
27
|
+
import "./chunk-5ORUQ6ES.js";
|
|
28
|
+
import "./chunk-2JEU3MGB.js";
|
|
29
|
+
import "./chunk-NJFMDXHR.js";
|
|
30
|
+
import "./chunk-J7VK7MNF.js";
|
|
31
|
+
import "./chunk-FWMSGKEH.js";
|
|
32
|
+
import "./chunk-JVOCK5W4.js";
|
|
33
|
+
|
|
34
|
+
// src/cli/share.ts
|
|
35
|
+
import fs from "node:fs/promises";
|
|
36
|
+
import os from "node:os";
|
|
37
|
+
import path from "node:path";
|
|
38
|
+
var SHARE_USAGE = "cascade share <graph-file | site-directory> [--name <slug>] [--scope <team>] [--protected] [--json]";
|
|
39
|
+
function shareSlug(text) {
|
|
40
|
+
const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^cascade-/, "").slice(0, 80).replace(/-+$/, "");
|
|
41
|
+
if (!slug) throw new Error(`"${text}" makes no usable name; pass --name <slug>`);
|
|
42
|
+
return slug;
|
|
43
|
+
}
|
|
44
|
+
async function share(target, options = {}) {
|
|
45
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
46
|
+
const source = path.resolve(target);
|
|
47
|
+
const stat = await fs.stat(source).catch(() => void 0);
|
|
48
|
+
if (!stat) throw new Error(`${target} does not exist`);
|
|
49
|
+
const isGraph = stat.isFile() && source.endsWith(".cascade");
|
|
50
|
+
if (!isGraph && !stat.isDirectory()) throw new Error(`cascade share takes a .cascade graph or a directory with an index.html (got ${target})`);
|
|
51
|
+
const scratch = await fs.mkdtemp(path.join(os.tmpdir(), "cascade-share-"));
|
|
52
|
+
try {
|
|
53
|
+
const site = path.join(scratch, "site");
|
|
54
|
+
let name;
|
|
55
|
+
let projectRoot;
|
|
56
|
+
if (isGraph) {
|
|
57
|
+
const { plan } = await exportEditor(source, { out: site, log });
|
|
58
|
+
name = shareSlug(options.name ?? plan.editor.id);
|
|
59
|
+
projectRoot = path.dirname(source);
|
|
60
|
+
} else {
|
|
61
|
+
if (!(await fs.stat(path.join(source, "index.html")).catch(() => void 0))?.isFile())
|
|
62
|
+
throw new Error(`${target} has no index.html to share`);
|
|
63
|
+
await copySite(source, site);
|
|
64
|
+
if (!await fs.stat(path.join(site, "vercel.json")).catch(() => void 0))
|
|
65
|
+
await fs.writeFile(path.join(site, "vercel.json"), `${JSON.stringify(vercelConfig(), null, 2)}
|
|
66
|
+
`);
|
|
67
|
+
projectRoot = await findProjectRoot(source);
|
|
68
|
+
await scanForLeaks(site, projectRoot ?? source);
|
|
69
|
+
name = shareSlug(options.name ?? path.basename(source));
|
|
70
|
+
}
|
|
71
|
+
const project = `cascade-${name}`;
|
|
72
|
+
const deploy = options.deploy ?? deployVercel;
|
|
73
|
+
const deployed = await deploy(site, { project, scope: options.scope, public: options.protected !== true, log });
|
|
74
|
+
const record = {
|
|
75
|
+
source: projectRoot ? path.relative(projectRoot, source).split(path.sep).join("/") || "." : path.basename(source),
|
|
76
|
+
name,
|
|
77
|
+
project,
|
|
78
|
+
url: deployed.url,
|
|
79
|
+
deployment: deployed.deployment,
|
|
80
|
+
sharedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
81
|
+
};
|
|
82
|
+
const shares = projectRoot ? await recordShare(projectRoot, record) : void 0;
|
|
83
|
+
return { ...record, ...shares ? { shares } : {} };
|
|
84
|
+
} finally {
|
|
85
|
+
await fs.rm(scratch, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function recordShare(projectRoot, record) {
|
|
89
|
+
const file = path.join(projectRoot, "shares.json");
|
|
90
|
+
const existing = await readShares(projectRoot);
|
|
91
|
+
const shares = [...existing.shares.filter((item) => item.source !== record.source), record].sort((a, b) => a.source.localeCompare(b.source));
|
|
92
|
+
const next = { format: "cascade.shares", version: 1, shares };
|
|
93
|
+
await fs.writeFile(file, `${JSON.stringify(next, null, 2)}
|
|
94
|
+
`);
|
|
95
|
+
return file;
|
|
96
|
+
}
|
|
97
|
+
async function readShares(projectRoot) {
|
|
98
|
+
const text = await fs.readFile(path.join(projectRoot, "shares.json"), "utf8").catch(() => void 0);
|
|
99
|
+
if (!text) return { format: "cascade.shares", version: 1, shares: [] };
|
|
100
|
+
const parsed = JSON.parse(text);
|
|
101
|
+
if (parsed.format !== "cascade.shares" || !Array.isArray(parsed.shares)) throw new Error(`${path.join(projectRoot, "shares.json")} is not a cascade.shares file`);
|
|
102
|
+
return { format: "cascade.shares", version: 1, shares: parsed.shares };
|
|
103
|
+
}
|
|
104
|
+
async function findProjectRoot(start) {
|
|
105
|
+
for (let directory = start; ; directory = path.dirname(directory)) {
|
|
106
|
+
if (await fs.stat(path.join(directory, "cascade.json")).catch(() => void 0)) return directory;
|
|
107
|
+
if (path.dirname(directory) === directory) return void 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function copySite(from, to) {
|
|
111
|
+
await fs.cp(from, to, {
|
|
112
|
+
recursive: true,
|
|
113
|
+
dereference: true,
|
|
114
|
+
filter: (file) => {
|
|
115
|
+
const relative = path.relative(from, file);
|
|
116
|
+
return !relative || relative.split(path.sep).every((part) => !part.startsWith(".") && part !== "licensed");
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function runShareCommand(args) {
|
|
121
|
+
const rest = args.slice(1);
|
|
122
|
+
const json = rest.includes("--json");
|
|
123
|
+
let target;
|
|
124
|
+
const values = {};
|
|
125
|
+
let isProtected = false;
|
|
126
|
+
for (let index = 0; index < rest.length; index++) {
|
|
127
|
+
const arg = rest[index];
|
|
128
|
+
if (arg === "--json") continue;
|
|
129
|
+
if (arg === "--protected") {
|
|
130
|
+
isProtected = true;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (arg === "--name" || arg === "--scope") {
|
|
134
|
+
const value = rest[++index];
|
|
135
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} needs a value`);
|
|
136
|
+
values[arg.slice(2)] = value;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (arg.startsWith("--")) throw new Error(`Unknown option ${arg}
|
|
140
|
+
Usage: ${SHARE_USAGE}`);
|
|
141
|
+
if (target) throw new Error(`cascade share takes one graph or directory
|
|
142
|
+
Usage: ${SHARE_USAGE}`);
|
|
143
|
+
target = arg;
|
|
144
|
+
}
|
|
145
|
+
if (!target) throw new Error(`Usage: ${SHARE_USAGE}`);
|
|
146
|
+
const result = await share(target, {
|
|
147
|
+
...values.name ? { name: values.name } : {},
|
|
148
|
+
...values.scope ? { scope: values.scope } : {},
|
|
149
|
+
protected: isProtected,
|
|
150
|
+
log: json ? () => {
|
|
151
|
+
} : (line) => console.log(line)
|
|
152
|
+
});
|
|
153
|
+
if (json) console.log(JSON.stringify(result));
|
|
154
|
+
else console.log(`
|
|
155
|
+
Shared: ${result.url}${result.shares ? `
|
|
156
|
+
Recorded in ${result.shares}` : ""}`);
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
SHARE_USAGE,
|
|
160
|
+
readShares,
|
|
161
|
+
recordShare,
|
|
162
|
+
runShareCommand,
|
|
163
|
+
share,
|
|
164
|
+
shareSlug
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=share-MHF6TNRK.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callStage,
|
|
3
|
+
cascadeVersion,
|
|
4
|
+
formatStageTrace,
|
|
5
|
+
stageCacheKey,
|
|
6
|
+
stageSources
|
|
7
|
+
} from "./chunk-5ORUQ6ES.js";
|
|
8
|
+
export {
|
|
9
|
+
callStage,
|
|
10
|
+
cascadeVersion,
|
|
11
|
+
formatStageTrace,
|
|
12
|
+
stageCacheKey,
|
|
13
|
+
stageSources
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=stageCache-TY2BFZJR.js.map
|