@field/cascade 0.12.0 → 0.13.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 +1 -1
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/dist/cli/{chunk-OM6BEME6.js → chunk-2L34ERS2.js} +3 -3
- package/dist/cli/{chunk-3RXL5GHB.js → chunk-JEW5MPGI.js} +4 -4
- package/dist/cli/{chunk-ST6LROUG.js → chunk-OBZAP3GG.js} +4 -4
- package/dist/cli/{chunk-73S4JZXK.js → chunk-QAGATJGE.js} +5 -5
- package/dist/cli/{chunk-MP653A5O.js → chunk-QKL7CCRS.js} +84 -10
- package/dist/cli/{chunk-2JEU3MGB.js → chunk-W5PJ5RMP.js} +155 -83
- package/dist/cli/{chunk-EU4ZFZ7Y.js → chunk-W7OUAT7J.js} +2 -2
- package/dist/cli/{cliCommands-JJN5Z3ZC.js → cliCommands-XVFRD7CF.js} +3 -3
- package/dist/cli/{commands-PHYFYA3F.js → commands-M2XLTPTZ.js} +7 -7
- package/dist/cli/{doctorCommand-ORWQRNYP.js → doctorCommand-RAEI7GVY.js} +2 -2
- package/dist/cli/{editorCommands-NFR32JOY.js → editorCommands-5BBG4S6V.js} +6 -6
- package/dist/cli/{graphCommands-EOUOBOIJ.js → graphCommands-MULQ7VD2.js} +2 -2
- package/dist/cli/{graphFile-YTGJB5F5.js → graphFile-CITF36ZM.js} +3 -3
- package/dist/cli/{helpCommand-PULH6ZYB.js → helpCommand-X2QDANM3.js} +2 -2
- package/dist/cli/index.js +13 -13
- package/dist/cli/{mcp-JVZFQOEX.js → mcp-YRIGRLYQ.js} +3 -3
- package/dist/cli/{parity-X6DEH2DV.js → parity-3N3HTKWX.js} +4 -4
- package/dist/cli/{project-VYGIA7JX.js → project-SJF5ZF3F.js} +2 -2
- package/dist/cli/{projectRuntime-2Q4JKEH3.js → projectRuntime-NY4XMDFH.js} +3 -3
- package/dist/cli/{render-PT4MWI6U.js → render-Y23W6MY6.js} +10 -5
- package/dist/cli/{runCommand-HNPQVZMS.js → runCommand-XOG7AVC5.js} +3 -3
- package/dist/cli/{runner-5XPXVW7J.js → runner-DNJO5WO2.js} +5 -5
- package/dist/cli/{share-MHF6TNRK.js → share-B7HTCOJX.js} +5 -5
- package/dist/cli/{webBuild-WPPMGGW7.js → webBuild-2K4COCGY.js} +4 -4
- package/dist/contracts/capabilities.d.ts +21 -0
- package/dist/player/app.js +8 -5
- package/dist/player/embed.js +2 -0
- package/dist/player/types.d.ts +6 -0
- package/dist/runtime/definition/architecture.js +89 -0
- package/doc/PROJECT_AUTHORING.md +60 -1
- package/doc/WEB_PLAYER.md +9 -1
- package/package.json +1 -1
|
@@ -18,6 +18,15 @@ export function validateNodeModuleArchitecture(source, fileName, ts) {
|
|
|
18
18
|
const diagnostics = [];
|
|
19
19
|
const exportsDefinition = file.statements.some((statement) => ts.isVariableStatement(statement) && isDefinition(statement));
|
|
20
20
|
for (const statement of file.statements) {
|
|
21
|
+
if (ts.isVariableStatement(statement) &&
|
|
22
|
+
!isDefinition(statement) &&
|
|
23
|
+
!supersededByDefinition(statement) &&
|
|
24
|
+
isPureConstant(statement)) {
|
|
25
|
+
// A helper: `const TAU = Math.PI * 2`, `const PALETTE = [...] as const`,
|
|
26
|
+
// `const lerp = (a, b, t) => a + (b - a) * t`. Immutable, so it cannot
|
|
27
|
+
// carry anything from one cook to the next.
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
21
30
|
if (ts.isVariableStatement(statement) && !isDefinition(statement)) {
|
|
22
31
|
// `icon` and `runsOn` get their own sentence, because the general
|
|
23
32
|
// message sends you looking for the wrong thing. A dynamic node declares
|
|
@@ -97,6 +106,86 @@ export function validateNodeModuleArchitecture(source, fileName, ts) {
|
|
|
97
106
|
? declaration.name.text
|
|
98
107
|
: null;
|
|
99
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether a module-level declaration is an immutable helper rather than
|
|
111
|
+
* state: `const` only, and every initializer pure (see `isPure`).
|
|
112
|
+
*/
|
|
113
|
+
function isPureConstant(statement) {
|
|
114
|
+
if (!(statement.declarationList.flags & ts.NodeFlags.Const))
|
|
115
|
+
return false;
|
|
116
|
+
return statement.declarationList.declarations.every((declaration) => ts.isIdentifier(declaration.name) &&
|
|
117
|
+
declaration.initializer !== undefined &&
|
|
118
|
+
isPure(declaration.initializer, false));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* An expression that creates no mutable value: literals, arithmetic and
|
|
122
|
+
* logic over them, references and property reads (`Math.PI`), functions
|
|
123
|
+
* (they close over nothing mutable, since nothing at module level is), and
|
|
124
|
+
* array/object literals only when frozen — `as const` or `Object.freeze(…)`.
|
|
125
|
+
* Calls, `new`, regular expressions (`lastIndex` is state) and a bare
|
|
126
|
+
* array or object are not.
|
|
127
|
+
*/
|
|
128
|
+
function isPure(node, frozen) {
|
|
129
|
+
if (ts.isNumericLiteral(node) || ts.isStringLiteral(node) || ts.isBigIntLiteral(node) ||
|
|
130
|
+
ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
131
|
+
node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
132
|
+
node.kind === ts.SyntaxKind.NullKeyword)
|
|
133
|
+
return true;
|
|
134
|
+
if (ts.isIdentifier(node))
|
|
135
|
+
return true;
|
|
136
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
|
|
137
|
+
return true;
|
|
138
|
+
if (ts.isParenthesizedExpression(node))
|
|
139
|
+
return isPure(node.expression, frozen);
|
|
140
|
+
if (ts.isAsExpression(node))
|
|
141
|
+
return isPure(node.expression, frozen || isConstAssertion(node.type));
|
|
142
|
+
if (ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node) || ts.isTypeAssertionExpression(node))
|
|
143
|
+
return isPure(node.expression, frozen);
|
|
144
|
+
if (ts.isPrefixUnaryExpression(node))
|
|
145
|
+
return node.operator !== ts.SyntaxKind.PlusPlusToken &&
|
|
146
|
+
node.operator !== ts.SyntaxKind.MinusMinusToken &&
|
|
147
|
+
isPure(node.operand, false);
|
|
148
|
+
if (ts.isBinaryExpression(node))
|
|
149
|
+
return !isAssignment(node.operatorToken.kind) &&
|
|
150
|
+
isPure(node.left, false) && isPure(node.right, false);
|
|
151
|
+
if (ts.isConditionalExpression(node))
|
|
152
|
+
return isPure(node.condition, false) && isPure(node.whenTrue, frozen) && isPure(node.whenFalse, frozen);
|
|
153
|
+
if (ts.isTemplateExpression(node))
|
|
154
|
+
return node.templateSpans.every((span) => isPure(span.expression, false));
|
|
155
|
+
if (ts.isPropertyAccessExpression(node))
|
|
156
|
+
return isPure(node.expression, false);
|
|
157
|
+
if (ts.isElementAccessExpression(node))
|
|
158
|
+
return isPure(node.expression, false) && isPure(node.argumentExpression, false);
|
|
159
|
+
if (ts.isArrayLiteralExpression(node))
|
|
160
|
+
return frozen && node.elements.every((element) => !ts.isSpreadElement(element) && isPure(element, true));
|
|
161
|
+
if (ts.isObjectLiteralExpression(node))
|
|
162
|
+
return frozen && node.properties.every((property) => ts.isPropertyAssignment(property)
|
|
163
|
+
? !ts.isComputedPropertyName(property.name) && isPure(property.initializer, true)
|
|
164
|
+
: ts.isShorthandPropertyAssignment(property));
|
|
165
|
+
if (ts.isCallExpression(node) &&
|
|
166
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
167
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
168
|
+
node.expression.expression.text === "Object" &&
|
|
169
|
+
node.expression.name.text === "freeze" &&
|
|
170
|
+
node.arguments.length === 1) {
|
|
171
|
+
const [argument] = node.arguments;
|
|
172
|
+
// Object.freeze is shallow: a nested array or object needs its own freeze.
|
|
173
|
+
return ts.isArrayLiteralExpression(argument)
|
|
174
|
+
? argument.elements.every((element) => !ts.isSpreadElement(element) && isPure(element, false))
|
|
175
|
+
: ts.isObjectLiteralExpression(argument)
|
|
176
|
+
? argument.properties.every((property) => ts.isPropertyAssignment(property) &&
|
|
177
|
+
!ts.isComputedPropertyName(property.name) &&
|
|
178
|
+
isPure(property.initializer, false))
|
|
179
|
+
: false;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
function isConstAssertion(type) {
|
|
184
|
+
return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === "const";
|
|
185
|
+
}
|
|
186
|
+
function isAssignment(kind) {
|
|
187
|
+
return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
|
|
188
|
+
}
|
|
100
189
|
function isDefinition(statement) {
|
|
101
190
|
return Boolean(statement.declarationList.flags & ts.NodeFlags.Const) &&
|
|
102
191
|
Boolean(statement.modifiers?.some((item) => item.kind === ts.SyntaxKind.ExportKeyword)) &&
|
package/doc/PROJECT_AUTHORING.md
CHANGED
|
@@ -196,6 +196,65 @@ The initiating node ID is captured as `api.sourceNodeId`; `setParam` uses the sa
|
|
|
196
196
|
}
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
+
### Reading another project's data
|
|
200
|
+
|
|
201
|
+
File reads stay inside the project: a `..` path or a symlink out of it is
|
|
202
|
+
refused. To read another folder, declare it as a read-only root:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"reads": ["../other-sketch/data"]
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Each entry is a folder relative to the project (absolute and `~` paths are
|
|
211
|
+
refused). The `files` capability's `read`, `list` and `stat`, `cascade/io`'s
|
|
212
|
+
loaders and Studio's media route may then resolve into it, whether the path
|
|
213
|
+
reaches it with `..` (`../other-sketch/data/points.csv`) or through a symlink in
|
|
214
|
+
the project (`data -> ../other-sketch/data`). What counts is where the file
|
|
215
|
+
really is, so a symlink inside the read root that points elsewhere is still
|
|
216
|
+
refused. Writes into a read root are always refused. The declaration is the
|
|
217
|
+
project's, in `cascade.json`, never the graph's; Studio's Project Settings
|
|
218
|
+
cannot change it.
|
|
219
|
+
|
|
220
|
+
### Named exports and a keyed cache
|
|
221
|
+
|
|
222
|
+
A node with the `files` capability (`runsOn: 'server'`) writes under the name
|
|
223
|
+
it gives, never renamed by a content hash, but only into `.cascade-cache/`. To
|
|
224
|
+
write a stably named export, such as a ladder's chosen variant, declare the
|
|
225
|
+
folder:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"writes": ["renders"]
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
await context.capabilities.files.write('renders/cloud-final.png', bytes, { signal: context.signal });
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Each entry is a folder inside the project; `..`, absolute paths, the project
|
|
238
|
+
root, `nodes`, `.git` and `.cascade` are refused, and a symlink out of a write
|
|
239
|
+
folder is refused like any other. `cascade/io`'s `write` and `saveImage` keep
|
|
240
|
+
content-hashed names in `.cascade-cache/`: they are intermediates between
|
|
241
|
+
nodes, and the hash is what stops a fixed name handing on the wrong pixels.
|
|
242
|
+
|
|
243
|
+
For a cache a node must find again on the next run, by a key it derives from
|
|
244
|
+
everything the bytes depend on, use `files.cache`:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const { files } = context.capabilities;
|
|
248
|
+
const key = `stage/${hashOf(inputs, props)}`;
|
|
249
|
+
const hit = await files.cache.get(key, { signal: context.signal }); // Uint8Array | null
|
|
250
|
+
if (!hit) await files.cache.put(key, computed, { signal: context.signal });
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Entries live in `.cascade-cache/keyed/<key>`; keys are letters, digits, `.`,
|
|
254
|
+
`_` and `-` in `/`-separated parts, each starting with a letter or digit.
|
|
255
|
+
Writes are atomic, so parallel render workers can share one cache. The cache
|
|
256
|
+
rule above still holds: it may only memoize work from wired inputs and props.
|
|
257
|
+
|
|
199
258
|
Read settings without giving nodes write access:
|
|
200
259
|
|
|
201
260
|
```ts
|
|
@@ -241,7 +300,7 @@ Clean graph design is clean code design:
|
|
|
241
300
|
- **One responsibility per node**, named for what it does (`layout_rings`, not `main`).
|
|
242
301
|
- **Data flows through typed ports**, never through module-level state or files one node writes and another reads behind the graph's back.
|
|
243
302
|
- **What varies is a parameter**; seeds, counts and colours are on the node, not constants in the code.
|
|
244
|
-
- **Shared helpers go in `lib/` or the kit** (`KIT.md`); the node is the unit you compose and reuse.
|
|
303
|
+
- **Shared helpers go in `lib/` or the kit** (`KIT.md`); the node is the unit you compose and reuse. A helper only this node uses may stay in its file: function declarations and immutable `const`s (`const TAU = Math.PI * 2`, `const PALETTE = [...] as const`, `const lerp = (a, b, t) => …`) pass `cascade check`. Anything that can change between cooks (`let`, `new Map()`, a bare array or object, a regex, a call) is module state and is refused (`architecture/module-state`).
|
|
245
304
|
|
|
246
305
|
Split a node when it does two of load, generate, style, render and export, or when its `execute` grows past about 150 lines. `cascade check` warns on both (`shape/long-execute`, `shape/mixed-jobs`); the warnings are advice and never fail the check. Drawing with noise and colour is one job's worth of calls, so it only flags a module doing four of the five, or loading and exporting in the same node.
|
|
247
306
|
|
package/doc/WEB_PLAYER.md
CHANGED
|
@@ -54,6 +54,12 @@ player.play();
|
|
|
54
54
|
await player.dispose();
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
For a neutral preview page, `chrome: false` hides the player's dark frame, Play
|
|
58
|
+
button and frame field, leaving the output alone on a transparent page; the
|
|
59
|
+
controller still plays, seeks and reports errors. The plain-iframe form is
|
|
60
|
+
`player.html?chrome=0`. The other options have URL forms too: `frame`, `fps`,
|
|
61
|
+
`autoplay=1`, `outputNode` with `outputPort`.
|
|
62
|
+
|
|
57
63
|
The generic `mountPlayer` export from `cascade/player` accepts the same options
|
|
58
64
|
plus `url`, pointing to a built `player.html`. Each mount creates an iframe, so
|
|
59
65
|
two sketches can use the same node IDs and cache filenames without sharing data.
|
|
@@ -70,7 +76,9 @@ serialized and playback coalesces requests instead of building a catch-up queue.
|
|
|
70
76
|
|
|
71
77
|
- `play()`, `pause()`, `seek(frame)`.
|
|
72
78
|
- `setInput(nodeId, name, value)` and `setProp(nodeId, name, value)`, which await
|
|
73
|
-
the resulting cook.
|
|
79
|
+
the resulting cook. Every cook re-runs only the nodes whose inputs or props
|
|
80
|
+
changed and everything downstream of them; an unchanged upstream node keeps
|
|
81
|
+
its outputs (`run({ reuse: "strict" })`).
|
|
74
82
|
- `selectOutput(nodeId, port)` and `getOutput(nodeId, port)`.
|
|
75
83
|
- `downloadOutput()`, returning the selected output as a `Blob`.
|
|
76
84
|
- `resize(width, height)` for the display, without changing authored resolution.
|