@descix/frqtl-node 0.6.0-p0.2 → 0.6.0-p1.1
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/CLAUDE.md +2 -2
- package/README.md +7 -3
- package/dist/dist-manifest.json +54 -48
- package/dist/frqtl/assets/vendor/acorn.LICENSE +21 -0
- package/dist/frqtl/assets/vendor/acorn.PINNED.json +9 -0
- package/dist/frqtl/assets/vendor/acorn.mjs +6266 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +41 -0
- package/dist/frqtl/experiments/_shared/boot-failure.js +60 -0
- package/dist/frqtl/objects/FrqtlApp.js +109 -30
- package/dist/frqtl/objects/ObjectFactory.js +4 -5
- package/dist/frqtl/objects/RecipeContext.js +451 -9
- package/dist/frqtl/objects/instruments/hosted-eyes.js +68 -28
- package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
- package/dist/frqtl/objects/schema/index.js +72 -0
- package/dist/frqtl/parts/DetectorWall.js +1 -1
- package/dist/frqtl/parts/FrqtlControlsPanel.js +4 -2
- package/dist/frqtl/parts/Wall.js +2 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +18 -14
- package/dist/frqtl/scenes/absorbing-wall.js +18 -14
- package/dist/frqtl/scenes/atomic-model.js +3 -0
- package/dist/frqtl/scenes/big-bang.js +3 -0
- package/dist/frqtl/scenes/blackbody.js +14 -11
- package/dist/frqtl/scenes/circuit-sat.js +7 -4
- package/dist/frqtl/scenes/collision-matrix-3d.js +3 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +19 -15
- package/dist/frqtl/scenes/counting-wall.js +19 -15
- package/dist/frqtl/scenes/double-slit.js +3 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +64 -58
- package/dist/frqtl/scenes/dse-one-slit.js +59 -55
- package/dist/frqtl/scenes/dse-photoelectric.js +28 -24
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +124 -99
- package/dist/frqtl/scenes/dse-two-emitters.js +64 -58
- package/dist/frqtl/scenes/dse-two-slits.js +69 -65
- package/dist/frqtl/scenes/empty.js +3 -0
- package/dist/frqtl/scenes/laser-buildup.js +23 -16
- package/dist/frqtl/scenes/mirror-3d.js +31 -27
- package/dist/frqtl/scenes/mirror-route.js +4 -1
- package/dist/frqtl/scenes/mirror.js +24 -20
- package/dist/frqtl/scenes/omni-3d.js +7 -4
- package/dist/frqtl/scenes/omni-source.js +7 -4
- package/dist/frqtl/scenes/self-interference.js +13 -10
- package/dist/frqtl/scenes/smoking-gun.js +15 -11
- package/dist/frqtl/scenes/straight-walk.js +7 -4
- package/dist/frqtl/scenes/sun.js +11 -8
- package/dist/frqtl/scenes/two-source.js +22 -17
- package/dist/frqtl/scenes/wave.js +10 -7
- package/dist/shim/bundle/manifest.js +1 -1
- package/dist/shim/engine/router.js +2 -2
- package/dist/shim/page-params/index.js +2 -2
- package/dist/shim/project/index.js +7 -5
- package/dist/shim/project/link.js +12 -5
- package/dist/shim/project/parts-module.js +67 -0
- package/dist/shim/project/parts-writer.js +453 -0
- package/dist/shim/project/specifiers.js +41 -13
- package/dist/shim/registry/walkthrough.js +9 -9
- package/dist/shim/render-state/apply.js +11 -14
- package/llms.txt +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* parts-writer.js — THE text patcher of a sketch's `export const parts = {…}` literal.
|
|
5
|
+
*
|
|
6
|
+
* patchParts(text, edits[, { entry }]) → { text, changes, refused }
|
|
7
|
+
*
|
|
8
|
+
* The sketch is parsed once (acorn, vendored: lib/frqtl/assets/vendor/acorn.mjs) into a syntax tree with
|
|
9
|
+
* source ranges. Only the ranges an edit touches are replaced; every other byte of the file — untouched
|
|
10
|
+
* entries, untouched properties, expressions, spreads, comments, the author's code — is the input's byte.
|
|
11
|
+
*
|
|
12
|
+
* edits[i] = { op: 'set', name, props } within entry `name`'s props object, replace ONLY the
|
|
13
|
+
* value text of each named property; a property the
|
|
14
|
+
* literal lacks is appended last in that entry
|
|
15
|
+
* | { op: 'add', name, Kind, module, props } a new entry as the LAST key, plus at most one
|
|
16
|
+
* import line for Kind (after the last import statement;
|
|
17
|
+
* `module` from parts-module.js moduleOfKind)
|
|
18
|
+
* | { op: 'remove', name } the entry's property node, its comma included
|
|
19
|
+
*
|
|
20
|
+
* changes[i] = { op, name, key?, from?, to, literalized? } from/to are source TEXT; `literalized: true`
|
|
21
|
+
* when the old value was an expression (its text is `from`), now its literal value
|
|
22
|
+
* refused[i] = { op, name, key?, reason } every edit not applied, with why. Never silent.
|
|
23
|
+
*
|
|
24
|
+
* REFUSED (named, into `refused`): a sketch with no `export const parts = { … }` object literal (every edit);
|
|
25
|
+
* a touched entry whose value is not `[Identifier, { … }]`; a touched name the literal does not hold as a
|
|
26
|
+
* plain key while it carries a spread or a computed key (the name may come from either); an added Kind whose
|
|
27
|
+
* identifier is already bound in the file to something else; an added Kind with `module === null`; a value
|
|
28
|
+
* the serializer cannot write (a function, NaN/Infinity, undefined, a class instance), naming its key.
|
|
29
|
+
*
|
|
30
|
+
* entryKeys(text, name) → string[] the plain prop keys entry `name` spells (how an entry spells a value)
|
|
31
|
+
* entryLiteral(text, name, key) → { literal, value? } what a key declares when it is a literal (read-back)
|
|
32
|
+
*
|
|
33
|
+
* serializeValue(value, key) → text THE one value serializer: numbers, strings (single-quoted),
|
|
34
|
+
* booleans, null, arrays inline, plain objects. Anything else THROWS
|
|
35
|
+
* naming the key.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { parse } from '../../frqtl/assets/vendor/acorn.mjs';
|
|
39
|
+
import { importSpecifierOf } from './parts-module.js';
|
|
40
|
+
|
|
41
|
+
/** The export the writer patches. */
|
|
42
|
+
export const PARTS_EXPORT = 'parts';
|
|
43
|
+
|
|
44
|
+
const _IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
45
|
+
const _RESERVED = new Set(('break case catch class const continue debugger default delete do else export extends ' +
|
|
46
|
+
'false finally for function if import in instanceof new null return super switch this throw true try typeof ' +
|
|
47
|
+
'var void while with yield let static enum await implements package protected interface private public').split(' '));
|
|
48
|
+
|
|
49
|
+
function _keyText(k) {
|
|
50
|
+
return (_IDENT.test(k) && !_RESERVED.has(k)) ? k : _quote(k);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _quote(s) {
|
|
54
|
+
return '\'' + s.replace(/\\/g, '\\\\').replace(/'/g, '\\\'').replace(/\n/g, '\\n').replace(/\r/g, '\\r')
|
|
55
|
+
.replace(/\t/g, '\\t').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029') + '\'';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {*} v
|
|
60
|
+
* @param {string} key — named in a refusal
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function serializeValue(v, key) {
|
|
64
|
+
if (v === null) { return 'null'; }
|
|
65
|
+
const t = typeof v;
|
|
66
|
+
if (t === 'number') {
|
|
67
|
+
if (!Number.isFinite(v)) { throw new Error('"' + key + '" is ' + String(v) + ' — the writer writes finite numbers only.'); }
|
|
68
|
+
return Object.is(v, -0) ? '0' : String(v);
|
|
69
|
+
}
|
|
70
|
+
if (t === 'string') { return _quote(v); }
|
|
71
|
+
if (t === 'boolean') { return v ? 'true' : 'false'; }
|
|
72
|
+
if (Array.isArray(v)) {
|
|
73
|
+
return '[' + v.map(function (x, i) { return serializeValue(x, key + '[' + i + ']'); }).join(', ') + ']';
|
|
74
|
+
}
|
|
75
|
+
if (t === 'object') {
|
|
76
|
+
const proto = Object.getPrototypeOf(v);
|
|
77
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
78
|
+
throw new Error('"' + key + '" is a ' + ((v.constructor && v.constructor.name) || 'class') +
|
|
79
|
+
' instance — the writer writes numbers, strings, booleans, null, arrays and plain objects.');
|
|
80
|
+
}
|
|
81
|
+
const ks = Object.keys(v);
|
|
82
|
+
if (!ks.length) { return '{}'; }
|
|
83
|
+
return '{ ' + ks.map(function (k) { return _keyText(k) + ': ' + serializeValue(v[k], key + '.' + k); }).join(', ') + ' }';
|
|
84
|
+
}
|
|
85
|
+
throw new Error('"' + key + '" is ' + (t === 'undefined' ? 'undefined' : 'a ' + t) +
|
|
86
|
+
' — the writer writes numbers, strings, booleans, null, arrays and plain objects.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A node whose text IS its value: a literal (not a regex), a negated number, or arrays/objects of those. */
|
|
90
|
+
function _isLiteral(n) {
|
|
91
|
+
if (!n) { return false; }
|
|
92
|
+
if (n.type === 'Literal') { return !n.regex && typeof n.value !== 'bigint'; }
|
|
93
|
+
if (n.type === 'UnaryExpression' && n.operator === '-') { return n.argument.type === 'Literal' && typeof n.argument.value === 'number'; }
|
|
94
|
+
if (n.type === 'ArrayExpression') { return n.elements.every(function (e) { return e && _isLiteral(e); }); }
|
|
95
|
+
if (n.type === 'ObjectExpression') {
|
|
96
|
+
return n.properties.every(function (p) {
|
|
97
|
+
return p.type === 'Property' && !p.computed && p.kind === 'init' && !p.method && !p.shorthand && _isLiteral(p.value);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The plain (non-computed) key of a Property node, else null. */
|
|
104
|
+
function _plainKey(p) {
|
|
105
|
+
if (p.type !== 'Property' || p.computed) { return null; }
|
|
106
|
+
if (p.key.type === 'Identifier') { return p.key.name; }
|
|
107
|
+
if (p.key.type === 'Literal' && (typeof p.key.value === 'string' || typeof p.key.value === 'number')) { return String(p.key.value); }
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The `parts` ObjectExpression: `export const parts = { … }`, else null. */
|
|
112
|
+
function _partsLiteral(ast) {
|
|
113
|
+
for (const s of ast.body) {
|
|
114
|
+
if (s.type !== 'ExportNamedDeclaration' || !s.declaration || s.declaration.type !== 'VariableDeclaration') { continue; }
|
|
115
|
+
for (const d of s.declaration.declarations) {
|
|
116
|
+
if (d.id.type === 'Identifier' && d.id.name === PARTS_EXPORT) {
|
|
117
|
+
return (d.init && d.init.type === 'ObjectExpression') ? d.init : null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Module-top bindings: name → { kind: 'import', source, imported } | { kind: 'decl' }. */
|
|
125
|
+
function _topBindings(ast) {
|
|
126
|
+
const out = new Map();
|
|
127
|
+
for (const s of ast.body) {
|
|
128
|
+
if (s.type === 'ImportDeclaration') {
|
|
129
|
+
for (const sp of s.specifiers) {
|
|
130
|
+
out.set(sp.local.name, { kind: 'import', source: s.source.value,
|
|
131
|
+
imported: sp.type === 'ImportSpecifier' ? (sp.imported.name || sp.imported.value) : null });
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const decl = (s.type === 'ExportNamedDeclaration' || s.type === 'ExportDefaultDeclaration') ? s.declaration : s;
|
|
136
|
+
if (!decl) { continue; }
|
|
137
|
+
if (decl.type === 'VariableDeclaration') {
|
|
138
|
+
for (const d of decl.declarations) { _patternNames(d.id).forEach(function (n) { out.set(n, { kind: 'decl' }); }); }
|
|
139
|
+
} else if ((decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') && decl.id) {
|
|
140
|
+
out.set(decl.id.name, { kind: 'decl' });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _patternNames(p) {
|
|
147
|
+
if (!p) { return []; }
|
|
148
|
+
if (p.type === 'Identifier') { return [p.name]; }
|
|
149
|
+
if (p.type === 'ObjectPattern') { return p.properties.flatMap(function (q) { return _patternNames(q.type === 'RestElement' ? q.argument : q.value); }); }
|
|
150
|
+
if (p.type === 'ArrayPattern') { return p.elements.flatMap(_patternNames); }
|
|
151
|
+
if (p.type === 'RestElement') { return _patternNames(p.argument); }
|
|
152
|
+
if (p.type === 'AssignmentPattern') { return _patternNames(p.left); }
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The whitespace that starts the line holding offset `at`. */
|
|
157
|
+
function _indentAt(text, at) {
|
|
158
|
+
const ls = text.lastIndexOf('\n', at - 1) + 1;
|
|
159
|
+
const m = /^[ \t]*/.exec(text.slice(ls));
|
|
160
|
+
return m[0];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Is `obj` (an ObjectExpression) written across lines? */
|
|
164
|
+
function _multiline(text, obj) {
|
|
165
|
+
return text.slice(obj.start, obj.end).indexOf('\n') !== -1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The insertion that appends `prop` (a `key: value` text) as the LAST property of ObjectExpression `obj`.
|
|
170
|
+
* Matches the literal's own layout: one line → `, k: v` / `{ k: v }`; multi-line → a new line at the last
|
|
171
|
+
* property's indent, keeping a trailing comma the literal already writes.
|
|
172
|
+
*/
|
|
173
|
+
function _appendProperty(text, obj, prop) {
|
|
174
|
+
const close = obj.end - 1; // the '}'
|
|
175
|
+
const last = obj.properties[obj.properties.length - 1];
|
|
176
|
+
if (!last) {
|
|
177
|
+
const inner = text.slice(obj.start + 1, close);
|
|
178
|
+
if (inner.indexOf('\n') !== -1) {
|
|
179
|
+
const ind = _indentAt(text, close);
|
|
180
|
+
return { start: obj.start + 1, end: close, text: '\n' + ind + ' ' + prop + ',\n' + ind };
|
|
181
|
+
}
|
|
182
|
+
return { start: obj.start + 1, end: close, text: ' ' + prop + ' ' };
|
|
183
|
+
}
|
|
184
|
+
const between = text.slice(last.end, close);
|
|
185
|
+
const comma = between.indexOf(',');
|
|
186
|
+
if (_multiline(text, obj)) {
|
|
187
|
+
const ind = _indentAt(text, last.start);
|
|
188
|
+
if (comma !== -1) { return { start: last.end + comma + 1, end: last.end + comma + 1, text: '\n' + ind + prop + ',' }; }
|
|
189
|
+
return { start: last.end, end: last.end, text: ',\n' + ind + prop };
|
|
190
|
+
}
|
|
191
|
+
if (comma !== -1) { return { start: last.end + comma + 1, end: last.end + comma + 1, text: ' ' + prop + ',' }; }
|
|
192
|
+
return { start: last.end, end: last.end, text: ', ' + prop };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The removal of Property `p` from ObjectExpression `obj`, its comma (and, when it owns whole lines, those lines) included. */
|
|
196
|
+
function _removeProperty(text, obj, p) {
|
|
197
|
+
const i = obj.properties.indexOf(p);
|
|
198
|
+
const next = obj.properties[i + 1];
|
|
199
|
+
const stop = next ? next.start : obj.end - 1;
|
|
200
|
+
const after = text.slice(p.end, stop);
|
|
201
|
+
const comma = after.indexOf(',');
|
|
202
|
+
let start = p.start;
|
|
203
|
+
let end = comma !== -1 ? p.end + comma + 1 : p.end;
|
|
204
|
+
if (comma === -1 && i > 0) {
|
|
205
|
+
// the last property with no trailing comma: the comma before it goes
|
|
206
|
+
const prev = obj.properties[i - 1];
|
|
207
|
+
const pc = text.slice(prev.end, p.start).indexOf(',');
|
|
208
|
+
if (pc !== -1) { start = prev.end + pc; }
|
|
209
|
+
}
|
|
210
|
+
// An entry that owns its lines (only whitespace before it on its first line, only whitespace after its
|
|
211
|
+
// comma on its last) leaves no blank line behind.
|
|
212
|
+
const ls = text.lastIndexOf('\n', start - 1) + 1;
|
|
213
|
+
const le = text.indexOf('\n', end);
|
|
214
|
+
if (/^[ \t]*$/.test(text.slice(ls, start)) && le !== -1 && /^[ \t]*$/.test(text.slice(end, le))) {
|
|
215
|
+
start = ls; end = le + 1;
|
|
216
|
+
}
|
|
217
|
+
return { start: start, end: end, text: '' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {string} text — the sketch file's text
|
|
222
|
+
* @param {object[]} edits — see the file header
|
|
223
|
+
* @param {{entry?: string}} [opts] — the sketch's project path (default 'sketch.js'), for a fork's relative import
|
|
224
|
+
* @returns {{text: string, changes: object[], refused: object[]}}
|
|
225
|
+
*/
|
|
226
|
+
export function patchParts(text, edits, opts) {
|
|
227
|
+
const entry = (opts && opts.entry) || 'sketch.js';
|
|
228
|
+
if (typeof text !== 'string') {
|
|
229
|
+
throw new TypeError('[parts-writer] patchParts(text, edits): text is the sketch file (got ' + typeof text + '). (FAIL-LOUD.)');
|
|
230
|
+
}
|
|
231
|
+
if (!Array.isArray(edits)) {
|
|
232
|
+
throw new TypeError('[parts-writer] patchParts(text, edits): edits is an array of { op: set | add | remove }. (FAIL-LOUD.)');
|
|
233
|
+
}
|
|
234
|
+
const changes = [];
|
|
235
|
+
const refused = [];
|
|
236
|
+
if (!edits.length) { return { text: text, changes: changes, refused: refused }; }
|
|
237
|
+
function refuse(e, reason, key) {
|
|
238
|
+
const r = { op: e.op, name: e.name, reason: reason };
|
|
239
|
+
if (key !== undefined) { r.key = key; }
|
|
240
|
+
refused.push(r);
|
|
241
|
+
}
|
|
242
|
+
let ast;
|
|
243
|
+
try { ast = parse(text, { ecmaVersion: 'latest', sourceType: 'module' }); }
|
|
244
|
+
catch (err) {
|
|
245
|
+
edits.forEach(function (e) { refuse(e, 'the sketch does not parse (' + err.message + ') — fix it, then Run.'); });
|
|
246
|
+
return { text: text, changes: changes, refused: refused };
|
|
247
|
+
}
|
|
248
|
+
const parts = _partsLiteral(ast);
|
|
249
|
+
if (!parts) {
|
|
250
|
+
edits.forEach(function (e) {
|
|
251
|
+
refuse(e, 'this sketch has no `export const ' + PARTS_EXPORT + ' = { … }` object literal — the writer writes ' +
|
|
252
|
+
'only into that literal; declare the parts there (exp.declare(' + PARTS_EXPORT + ')) to make them editable.');
|
|
253
|
+
});
|
|
254
|
+
return { text: text, changes: changes, refused: refused };
|
|
255
|
+
}
|
|
256
|
+
const byName = new Map();
|
|
257
|
+
let opaque = false; // a spread or a computed key: a name may come from it
|
|
258
|
+
for (const p of parts.properties) {
|
|
259
|
+
const k = _plainKey(p);
|
|
260
|
+
if (k === null) { opaque = true; continue; }
|
|
261
|
+
byName.set(k, p); // a duplicated key: the last one wins, as in the language
|
|
262
|
+
}
|
|
263
|
+
const bindings = _topBindings(ast);
|
|
264
|
+
const patches = [];
|
|
265
|
+
const importLines = []; // { Kind, line }
|
|
266
|
+
const entryOf = function (e) {
|
|
267
|
+
const p = byName.get(e.name);
|
|
268
|
+
if (!p) {
|
|
269
|
+
refuse(e, opaque
|
|
270
|
+
? 'no plain key "' + e.name + '" in the parts literal, which carries a spread or a computed key — a name that ' +
|
|
271
|
+
'comes from either is not writable here; write the entry as a plain key.'
|
|
272
|
+
: 'the parts literal has no entry "' + e.name + '".');
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return p;
|
|
276
|
+
};
|
|
277
|
+
const adds = [];
|
|
278
|
+
for (const e of edits) {
|
|
279
|
+
if (!e || typeof e !== 'object' || typeof e.name !== 'string' || !e.name) {
|
|
280
|
+
throw new TypeError('[parts-writer] an edit is { op, name, … } with a non-empty name (got ' + JSON.stringify(e) + '). (FAIL-LOUD.)');
|
|
281
|
+
}
|
|
282
|
+
if (e.op === 'set') {
|
|
283
|
+
const p = entryOf(e);
|
|
284
|
+
if (!p) { continue; }
|
|
285
|
+
const v = p.value;
|
|
286
|
+
if (v.type !== 'ArrayExpression' || v.elements.length !== 2 || !v.elements[0] || v.elements[0].type !== 'Identifier' ||
|
|
287
|
+
!v.elements[1] || v.elements[1].type !== 'ObjectExpression') {
|
|
288
|
+
refuse(e, 'the entry "' + e.name + '" is not written as [Kind, { …props }] — the writer patches only that form.');
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const obj = v.elements[1];
|
|
292
|
+
const own = new Map();
|
|
293
|
+
for (const q of obj.properties) { const k = _plainKey(q); if (k !== null) { own.set(k, q); } }
|
|
294
|
+
const appended = [];
|
|
295
|
+
for (const key of Object.keys(e.props || {})) {
|
|
296
|
+
let lit;
|
|
297
|
+
try { lit = serializeValue(e.props[key], key); }
|
|
298
|
+
catch (err) { refuse(e, err.message, key); continue; }
|
|
299
|
+
const q = own.get(key);
|
|
300
|
+
if (q && !q.shorthand) {
|
|
301
|
+
const old = text.slice(q.value.start, q.value.end);
|
|
302
|
+
if (old === lit) { continue; }
|
|
303
|
+
patches.push({ start: q.value.start, end: q.value.end, text: lit });
|
|
304
|
+
const c = { op: 'set', name: e.name, key: key, from: old, to: lit };
|
|
305
|
+
if (!_isLiteral(q.value)) { c.literalized = true; }
|
|
306
|
+
changes.push(c);
|
|
307
|
+
} else if (q && q.shorthand) {
|
|
308
|
+
patches.push({ start: q.start, end: q.end, text: _keyText(key) + ': ' + lit });
|
|
309
|
+
changes.push({ op: 'set', name: e.name, key: key, from: key, to: lit, literalized: true });
|
|
310
|
+
} else {
|
|
311
|
+
appended.push(_keyText(key) + ': ' + lit);
|
|
312
|
+
changes.push({ op: 'set', name: e.name, key: key, from: null, to: lit });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (appended.length) {
|
|
316
|
+
const ins = _appendProperty(text, obj, appended.join(_multiline(text, obj) ? ',\n' + _indentAt(text, (obj.properties[obj.properties.length - 1] || obj).start) : ', '));
|
|
317
|
+
patches.push(ins);
|
|
318
|
+
}
|
|
319
|
+
} else if (e.op === 'remove') {
|
|
320
|
+
const p = entryOf(e);
|
|
321
|
+
if (!p) { continue; }
|
|
322
|
+
patches.push(_removeProperty(text, parts, p));
|
|
323
|
+
changes.push({ op: 'remove', name: e.name, from: text.slice(p.start, p.end), to: null });
|
|
324
|
+
} else if (e.op === 'add') {
|
|
325
|
+
if (byName.has(e.name) || adds.some(function (a) { return a.name === e.name; })) {
|
|
326
|
+
refuse(e, 'the parts literal already has an entry "' + e.name + '" — a declared name is one key.');
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (typeof e.Kind !== 'string' || !_IDENT.test(e.Kind)) {
|
|
330
|
+
throw new TypeError('[parts-writer] add: Kind is the kind\'s class name (got ' + JSON.stringify(e.Kind) + '). (FAIL-LOUD.)');
|
|
331
|
+
}
|
|
332
|
+
if (e.module === null || e.module === undefined) {
|
|
333
|
+
refuse(e, 'the kind ' + e.Kind + ' has no importable module (an internal kind, not a part of ' +
|
|
334
|
+
'@descix/frqtl-web/parts nor a project file), so a sketch cannot import it — declare it in code.');
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const spec = importSpecifierOf(e.module, entry);
|
|
338
|
+
const b = bindings.get(e.Kind);
|
|
339
|
+
if (b && !(b.kind === 'import' && b.source === spec && b.imported === e.Kind)) {
|
|
340
|
+
refuse(e, 'the identifier ' + e.Kind + ' is already bound in this sketch to ' +
|
|
341
|
+
(b.kind === 'import' ? '"' + (b.imported || '*') + '" from \'' + b.source + '\'' : 'its own declaration') +
|
|
342
|
+
' — the writer cannot import the kind ' + e.Kind + ' under that name.');
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
let lit;
|
|
346
|
+
try { lit = serializeValue(e.props || {}, e.name); }
|
|
347
|
+
catch (err) { refuse(e, err.message); continue; }
|
|
348
|
+
if (!b && !importLines.some(function (l) { return l.Kind === e.Kind; })) {
|
|
349
|
+
importLines.push({ Kind: e.Kind, line: 'import { ' + e.Kind + ' } from ' + _quote(spec) + ';' });
|
|
350
|
+
}
|
|
351
|
+
adds.push({ name: e.name, text: _keyText(e.name) + ': [' + e.Kind + ', ' + lit + ']' });
|
|
352
|
+
changes.push({ op: 'add', name: e.name, from: null, to: _keyText(e.name) + ': [' + e.Kind + ', ' + lit + ']',
|
|
353
|
+
import: (!b ? 'import { ' + e.Kind + ' } from ' + _quote(spec) + ';' : null) });
|
|
354
|
+
} else {
|
|
355
|
+
throw new TypeError('[parts-writer] unknown edit op ' + JSON.stringify(e.op) + ' — set | add | remove. (FAIL-LOUD.)');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (adds.length) {
|
|
359
|
+
const joined = adds.map(function (a) { return a.text; });
|
|
360
|
+
if (_multiline(text, parts) || parts.properties.length === 0) {
|
|
361
|
+
if (parts.properties.length === 0) {
|
|
362
|
+
const ind = _indentAt(text, parts.start);
|
|
363
|
+
patches.push({ start: parts.start + 1, end: parts.end - 1,
|
|
364
|
+
text: '\n' + joined.map(function (t) { return ind + ' ' + t + ','; }).join('\n') + '\n' + ind });
|
|
365
|
+
} else {
|
|
366
|
+
const ind = _indentAt(text, parts.properties[parts.properties.length - 1].start);
|
|
367
|
+
const last = parts.properties[parts.properties.length - 1];
|
|
368
|
+
const comma = text.slice(last.end, parts.end - 1).indexOf(',');
|
|
369
|
+
if (comma !== -1) {
|
|
370
|
+
patches.push({ start: last.end + comma + 1, end: last.end + comma + 1,
|
|
371
|
+
text: joined.map(function (t) { return '\n' + ind + t + ','; }).join('') });
|
|
372
|
+
} else {
|
|
373
|
+
patches.push({ start: last.end, end: last.end, text: joined.map(function (t) { return ',\n' + ind + t; }).join('') });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
patches.push(_appendProperty(text, parts, joined.join(', ')));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (importLines.length) {
|
|
381
|
+
const imports = ast.body.filter(function (s) { return s.type === 'ImportDeclaration'; });
|
|
382
|
+
let at;
|
|
383
|
+
if (imports.length) {
|
|
384
|
+
const lastImp = imports[imports.length - 1];
|
|
385
|
+
const nl = text.indexOf('\n', lastImp.end);
|
|
386
|
+
at = nl === -1 ? text.length : nl + 1;
|
|
387
|
+
const block = importLines.map(function (l) { return l.line + '\n'; }).join('');
|
|
388
|
+
patches.push({ start: at, end: at, text: nl === -1 ? '\n' + block : block });
|
|
389
|
+
} else {
|
|
390
|
+
const first = ast.body[0];
|
|
391
|
+
at = first ? text.lastIndexOf('\n', first.start - 1) + 1 : text.length;
|
|
392
|
+
patches.push({ start: at, end: at, text: importLines.map(function (l) { return l.line + '\n'; }).join('') + '\n' });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
patches.sort(function (a, b) { return b.start - a.start || b.end - a.end; });
|
|
396
|
+
for (let i = 1; i < patches.length; i++) {
|
|
397
|
+
if (patches[i].end > patches[i - 1].start) {
|
|
398
|
+
throw new Error('[parts-writer] two edits touch overlapping text — one edit per property. (FAIL-LOUD.)');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
let out = text;
|
|
402
|
+
for (const p of patches) { out = out.slice(0, p.start) + p.text + out.slice(p.end); }
|
|
403
|
+
return { text: out, changes: changes, refused: refused };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** The props ObjectExpression of entry `name` in the parsed `parts` literal; THROWS naming what is missing. */
|
|
407
|
+
function _entryProps(text, name, who) {
|
|
408
|
+
const ast = parse(text, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
409
|
+
const parts = _partsLiteral(ast);
|
|
410
|
+
if (!parts) {
|
|
411
|
+
throw new Error('[parts-writer] ' + who + ': the sketch has no `export const ' + PARTS_EXPORT + ' = { … }` object literal. (FAIL-LOUD.)');
|
|
412
|
+
}
|
|
413
|
+
let p = null;
|
|
414
|
+
for (const q of parts.properties) { if (_plainKey(q) === name) { p = q; } }
|
|
415
|
+
const v = p && p.value;
|
|
416
|
+
if (!v || v.type !== 'ArrayExpression' || v.elements.length !== 2 || !v.elements[1] || v.elements[1].type !== 'ObjectExpression') {
|
|
417
|
+
throw new Error('[parts-writer] ' + who + ': the parts literal has no entry "' + name + '" written as [Kind, { …props }]. (FAIL-LOUD.)');
|
|
418
|
+
}
|
|
419
|
+
return v.elements[1];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function _literalValue(n) {
|
|
423
|
+
if (n.type === 'Literal') { return n.value; }
|
|
424
|
+
if (n.type === 'UnaryExpression') { return -n.argument.value; }
|
|
425
|
+
if (n.type === 'ArrayExpression') { return n.elements.map(_literalValue); }
|
|
426
|
+
const o = {};
|
|
427
|
+
for (const p of n.properties) { o[_plainKey(p)] = _literalValue(p.value); }
|
|
428
|
+
return o;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* entryKeys(text, name) → string[] — the plain prop keys entry `name` spells in the parsed `parts` literal, in
|
|
433
|
+
* source order (a spread or a computed key is not a plain key). The writer asks it how an entry spells a value
|
|
434
|
+
* (e.g. `rect` alone, or `rect` beside `z` / `depth`), never a regex over the text. THROWS on no such entry.
|
|
435
|
+
*/
|
|
436
|
+
export function entryKeys(text, name) {
|
|
437
|
+
const out = [];
|
|
438
|
+
for (const q of _entryProps(text, name, 'entryKeys').properties) { const k = _plainKey(q); if (k !== null) { out.push(k); } }
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* entryLiteral(text, name, key) → { literal: true, value } | { literal: false } — what entry `name`'s `key`
|
|
444
|
+
* declares when its value is a literal (the read-back after a write: the text a patch produced, parsed again).
|
|
445
|
+
* THROWS on no such entry or no such key.
|
|
446
|
+
*/
|
|
447
|
+
export function entryLiteral(text, name, key) {
|
|
448
|
+
for (const q of _entryProps(text, name, 'entryLiteral').properties) {
|
|
449
|
+
if (_plainKey(q) !== key) { continue; }
|
|
450
|
+
return _isLiteral(q.value) ? { literal: true, value: _literalValue(q.value) } : { literal: false };
|
|
451
|
+
}
|
|
452
|
+
throw new Error('[parts-writer] entryLiteral: the entry "' + name + '" spells no key "' + key + '". (FAIL-LOUD.)');
|
|
453
|
+
}
|
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
* is a PREFIX row (one part = one file under it)
|
|
8
8
|
* libPathOf(specifier) → the lib-identity path it names; THROWS on an unknown specifier
|
|
9
9
|
* importMapFor(libRootUrl) → { imports } — the rows as an import map, every libPath appended to
|
|
10
|
-
* the URL
|
|
11
|
-
*
|
|
12
|
-
* a
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* the URL of the lib-identity root AS SEEN FROM THE PAGE: an absolute
|
|
11
|
+
* URL or one starting with '/', './' or '../' (any other root THROWS —
|
|
12
|
+
* an import map ignores such a value as a bare specifier); prefix rows
|
|
13
|
+
* stay prefixes. No filesystem computation, so a vendored dist root
|
|
14
|
+
* takes the same call.
|
|
15
|
+
* FRQTL_WEB_HOST_SPECIFIERS rows { specifier, libPath, why } a HOST PAGE imports and a sketch never does:
|
|
16
|
+
* ./boot (FRQTL_WEB_BOOT, the scene doors), ./registry and ./project. libPathOf
|
|
17
|
+
* refuses every one of them, so a sketch cannot boot a page.
|
|
18
|
+
* pageImportMapFor(libRootUrl) → importMapFor(libRootUrl) plus every FRQTL_WEB_HOST_SPECIFIERS row: the map
|
|
19
|
+
* of a page that boots a scene through the doors and runs sketches beside it.
|
|
18
20
|
*
|
|
19
21
|
* Inside lib/ the specifier is a SELF-REFERENCE — the package naming itself, the way node resolves a
|
|
20
22
|
* package importing itself by name — not a lib→sdk edge. It is legal ONLY in sketch content
|
|
@@ -47,7 +49,17 @@ export const FRQTL_WEB_SPECIFIERS = Object.freeze([
|
|
|
47
49
|
]);
|
|
48
50
|
|
|
49
51
|
/** The scene doors a host page imports — the package's `./boot` export; never a sketch specifier. */
|
|
50
|
-
export const FRQTL_WEB_BOOT = Object.freeze({ specifier: '@descix/frqtl-web/boot', libPath: 'frqtl/experiments/_shared/doors.js'
|
|
52
|
+
export const FRQTL_WEB_BOOT = Object.freeze({ specifier: '@descix/frqtl-web/boot', libPath: 'frqtl/experiments/_shared/doors.js',
|
|
53
|
+
why: 'the scene doors (bootScene, bootProject)' });
|
|
54
|
+
|
|
55
|
+
/** What a host page imports beside its sketches. Every row is a public exports entry of @descix/frqtl-web. */
|
|
56
|
+
export const FRQTL_WEB_HOST_SPECIFIERS = Object.freeze([
|
|
57
|
+
FRQTL_WEB_BOOT,
|
|
58
|
+
Object.freeze({ specifier: '@descix/frqtl-web/registry', libPath: 'shim/registry/index.js',
|
|
59
|
+
why: 'the walk-through rungs, the showcase roster, the experiment registry, loadSceneSource' }),
|
|
60
|
+
Object.freeze({ specifier: '@descix/frqtl-web/project', libPath: 'shim/project/index.js',
|
|
61
|
+
why: 'the specifier table, the link site, sketchProject and the project identity' })
|
|
62
|
+
]);
|
|
51
63
|
|
|
52
64
|
/** The package a self-reference names. */
|
|
53
65
|
export const FRQTL_WEB_PACKAGE = '@descix/frqtl-web';
|
|
@@ -80,9 +92,15 @@ export function libPathOf(specifier) {
|
|
|
80
92
|
'. (FAIL-LOUD.)');
|
|
81
93
|
}
|
|
82
94
|
|
|
95
|
+
/** An import map resolves an address only if it is an absolute URL or starts with '/', './' or '../'. */
|
|
96
|
+
function _isMapAddress(url) {
|
|
97
|
+
if (/^(\/|\.\/|\.\.\/)/.test(url)) { return true; }
|
|
98
|
+
try { new URL(url); return true; } catch (e) { return false; }
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
/**
|
|
84
|
-
* @param {string} libRootUrl — the lib-identity root as seen from the page, ending in
|
|
85
|
-
* (e.g. '../../', '/pub/abc123/', 'vendor/frqtl-web/0.6.0/dist/')
|
|
102
|
+
* @param {string} libRootUrl — the lib-identity root as seen from the page, ending in `/`: an absolute
|
|
103
|
+
* URL or one starting with '/', './' or '../' (e.g. '../../', '/pub/abc123/', './vendor/frqtl-web/0.6.0/dist/')
|
|
86
104
|
* @returns {{imports: Object<string, string>}}
|
|
87
105
|
*/
|
|
88
106
|
export function importMapFor(libRootUrl) {
|
|
@@ -90,6 +108,12 @@ export function importMapFor(libRootUrl) {
|
|
|
90
108
|
throw new TypeError('[specifiers] importMapFor(libRootUrl): the lib-identity root URL as seen from the page, ' +
|
|
91
109
|
'ending in "/" (got ' + JSON.stringify(libRootUrl) + '). (FAIL-LOUD.)');
|
|
92
110
|
}
|
|
111
|
+
if (!_isMapAddress(libRootUrl)) {
|
|
112
|
+
throw new TypeError('[specifiers] importMapFor(libRootUrl): ' + JSON.stringify(libRootUrl) + ' is neither an ' +
|
|
113
|
+
'absolute URL nor a path starting with "/", "./" or "../", so a browser reads every row built on it as a ' +
|
|
114
|
+
'bare specifier and ignores it. Write it as ' + JSON.stringify('./' + libRootUrl) + ' (relative to the page) ' +
|
|
115
|
+
'or ' + JSON.stringify('/' + libRootUrl) + ' (from the origin root). (FAIL-LOUD.)');
|
|
116
|
+
}
|
|
93
117
|
const imports = {};
|
|
94
118
|
for (const row of FRQTL_WEB_SPECIFIERS) { imports[row.specifier] = libRootUrl + row.libPath; }
|
|
95
119
|
return { imports: imports };
|
|
@@ -97,10 +121,14 @@ export function importMapFor(libRootUrl) {
|
|
|
97
121
|
|
|
98
122
|
/**
|
|
99
123
|
* @param {string} libRootUrl — as importMapFor's
|
|
100
|
-
* @returns {{imports: Object<string, string>}} importMapFor(libRootUrl) plus
|
|
124
|
+
* @returns {{imports: Object<string, string>}} importMapFor(libRootUrl) plus every FRQTL_WEB_HOST_SPECIFIERS row
|
|
125
|
+
*
|
|
126
|
+
* The map carries no dist-manifest.json row: the manifest is a fact of a PACKAGE dist, and this map is also
|
|
127
|
+
* rendered for lib roots that are not one (the static site's graph has none). A host reads it from the root
|
|
128
|
+
* it already holds: `new URL('dist-manifest.json', root)`.
|
|
101
129
|
*/
|
|
102
130
|
export function pageImportMapFor(libRootUrl) {
|
|
103
131
|
const map = importMapFor(libRootUrl);
|
|
104
|
-
map.imports[
|
|
132
|
+
for (const row of FRQTL_WEB_HOST_SPECIFIERS) { map.imports[row.specifier] = libRootUrl + row.libPath; }
|
|
105
133
|
return map;
|
|
106
134
|
}
|
|
@@ -46,17 +46,17 @@
|
|
|
46
46
|
* is the config-mirror anti-pattern this contract exists to kill. Every mode delta
|
|
47
47
|
* must be expressible as DECLARED descriptor fields (emitter `is2D`, Wall/Tunnel
|
|
48
48
|
* `z`/`depth`, ordinary Walls for the z-collimators, camera on the renderState) so
|
|
49
|
-
* a built scene
|
|
49
|
+
* a built scene's declared parts hold concrete, mode-PINNED state and survive Restart.
|
|
50
50
|
*
|
|
51
|
-
* ── The recipe form (
|
|
51
|
+
* ── The recipe form (a parts block, written back by the parts writer) ─────────
|
|
52
52
|
* Rungs are OBJECT recipes: each body states the WORLD first, in its co-init call
|
|
53
|
-
* (`exp.createCanvas(
|
|
54
|
-
* `exp.
|
|
55
|
-
* world, the sketch does, once, before any part.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
53
|
+
* (`exp.createCanvas(…)` / `exp.createScene(…)`), then declares its parts —
|
|
54
|
+
* `const p = exp.declare(parts)` over the module's `export const parts = { name: [Kind, props] }`
|
|
55
|
+
* block — a PART never inits the world, the sketch does, once, before any part. Every
|
|
56
|
+
* declared part is a real authored object, so a loaded rung is DRAGGABLE / INSPECTABLE,
|
|
57
|
+
* and a STUDIO hand (grab/drag, Inspector, knob, visibility) is written into the SAME
|
|
58
|
+
* sketch text by the parts writer, `await exp.toSketch()`: it patches the touched
|
|
59
|
+
* entries of the parts block in place and leaves every other line of the file as authored.
|
|
60
60
|
*
|
|
61
61
|
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
62
62
|
* Zero new physics (canonical-bench SU1) — every rung composes already-goldened
|
|
@@ -31,13 +31,14 @@
|
|
|
31
31
|
* `runner.updateObjectProperty(lo, 'visible', v)`, backdrop:v. An ABSENT
|
|
32
32
|
* `backdropVisible` (null after normalizing) is not written — the scene's own
|
|
33
33
|
* `addBackdrop(…, {visible})` rules — and reports backdrop:null, as does no backdrop LO.
|
|
34
|
-
*
|
|
34
|
+
* Page controls are not state: a sketch declares them in its own code.
|
|
35
35
|
*
|
|
36
36
|
* backdropOf(runner) — THE answer to "which LO is the backdrop" for a host: the
|
|
37
37
|
* census LO marked `backdrop === true`, or null. STUDIO's
|
|
38
38
|
* save captures its visibility through it, so capture and
|
|
39
39
|
* replay read one fact.
|
|
40
|
-
* normalizeSavedRenderState(raw) — tolerant normalizer for a LOADED bag
|
|
40
|
+
* normalizeSavedRenderState(raw) — tolerant normalizer for a LOADED bag; THROWS only on a bag
|
|
41
|
+
* that carries page controls (they are author code now).
|
|
41
42
|
* pickCameraReplay(saved) — pure decision: named preset over raw orbit.
|
|
42
43
|
* partitionRenderRows(rows, declared, known) — apply / retain / throw, before anything is written.
|
|
43
44
|
* renderRowsFromBag(bag) — a { key: value } render-option bag as ordered rows.
|
|
@@ -83,12 +84,18 @@ function pickCameraReplay(saved) {
|
|
|
83
84
|
* seed: string|null,
|
|
84
85
|
* backdropVisible: boolean|null,
|
|
85
86
|
* camera: {orbit:object|null, preset:string|null}|null,
|
|
86
|
-
* render: {key:string, value:*}[]
|
|
87
|
-
* pageControls: {id:string, value:*, bind:{object:string,prop:string}|null}[]
|
|
87
|
+
* render: {key:string, value:*}[]
|
|
88
88
|
* }}
|
|
89
|
+
* @throws {Error} a bag whose `pageControls` holds a row — page controls are declared in the sketch's own
|
|
90
|
+
* code, so a saved bag never carries them; migrate the project (move the controls into its sketch).
|
|
89
91
|
*/
|
|
90
92
|
function normalizeSavedRenderState(raw) {
|
|
91
93
|
var r = _isPlainObject(raw) ? raw : {};
|
|
94
|
+
if (Array.isArray(r.pageControls) && r.pageControls.length > 0) {
|
|
95
|
+
throw new Error('[render-state] this saved project carries page controls (renderState.pageControls: ' +
|
|
96
|
+
r.pageControls.map(function (row) { return _isPlainObject(row) ? String(row.id) : '?'; }).join(', ') +
|
|
97
|
+
') — page controls are declared in the sketch\'s own code; move them into the sketch and save again. (FAIL-LOUD.)');
|
|
98
|
+
}
|
|
92
99
|
var camera = null;
|
|
93
100
|
if (_isPlainObject(r.camera)) {
|
|
94
101
|
camera = {
|
|
@@ -105,16 +112,6 @@ function normalizeSavedRenderState(raw) {
|
|
|
105
112
|
camera: camera,
|
|
106
113
|
render: Array.isArray(r.render) ? r.render.filter(function (row) {
|
|
107
114
|
return _isPlainObject(row) && typeof row.key === 'string' && row.key.length > 0;
|
|
108
|
-
}) : [],
|
|
109
|
-
pageControls: Array.isArray(r.pageControls) ? r.pageControls.filter(function (row) {
|
|
110
|
-
return _isPlainObject(row) && typeof row.id === 'string' && row.id.length > 0;
|
|
111
|
-
}).map(function (row) {
|
|
112
|
-
return {
|
|
113
|
-
id: row.id,
|
|
114
|
-
value: row.value,
|
|
115
|
-
bind: (_isPlainObject(row.bind) && typeof row.bind.object === 'string' &&
|
|
116
|
-
typeof row.bind.prop === 'string') ? { object: row.bind.object, prop: row.bind.prop } : null
|
|
117
|
-
};
|
|
118
115
|
}) : []
|
|
119
116
|
};
|
|
120
117
|
}
|
package/llms.txt
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @descix/frqtl-node
|
|
2
2
|
|
|
3
3
|
> The FRQTL physics engine (compiled Rust → WASM) for node: the same modules as @descix/frqtl-web, plus
|
|
4
|
-
> `bootNode` and the hosted eyes. Pre-release 0.6.0-
|
|
4
|
+
> `bootNode` and the hosted eyes. Pre-release 0.6.0-p1.1 on npm dist-tag `next`. Node ≥ 22.12.
|
|
5
5
|
|
|
6
6
|
- Install: `npm install @descix/frqtl-node@next`
|
|
7
7
|
- `import { bootNode, Wall } from '@descix/frqtl-node'` (require() works too); `await bootNode()` → the engine handle.
|
|
8
8
|
- `./eyes`: `openHostedEyes({ sceneRecord, sceneSource: { kind: 'project', files, entry }, pageUrl })`.
|
|
9
9
|
- `./project`: `linkProject` links a scene file against this package's own copy.
|
|
10
10
|
- `./register`: `node --import @descix/frqtl-node/register scene.mjs` resolves a scene's `@descix/frqtl-web/...` imports to this package.
|
|
11
|
-
- Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.6.0-
|
|
11
|
+
- Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.6.0-p1.1/CLAUDE.md
|
|
12
12
|
- Acceptance is visual; no CLI ships. No account, credits or tokens are needed.
|