@descix/frqtl-node 0.6.0-p0.2 → 0.6.0-p4.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 +8 -4
- package/dist/dist-manifest.json +70 -62
- package/dist/eyes/index.js +5 -2
- 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 +136 -30
- package/dist/frqtl/objects/ObjectFactory.js +4 -5
- package/dist/frqtl/objects/RecipeContext.js +617 -9
- package/dist/frqtl/objects/instruments/hosted-eyes.js +268 -57
- package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
- package/dist/frqtl/objects/record/Record.js +326 -0
- package/dist/frqtl/objects/record/open-record.js +22 -0
- 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/FrqtlVision.js +21 -6
- 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/index.js +112 -96
- package/dist/shim/bundle/indexHtml.js +66 -39
- package/dist/shim/engine/router.js +2 -2
- package/dist/shim/index.js +1 -1
- package/dist/shim/notebook/format.js +1 -1
- package/dist/shim/notebook/store.js +2 -3
- package/dist/shim/page-params/index.js +2 -2
- package/dist/shim/project/index.js +20 -5
- package/dist/shim/project/link.js +182 -37
- package/dist/shim/project/manifest.js +151 -0
- package/dist/shim/project/parts-module.js +67 -0
- package/dist/shim/project/parts-writer.js +457 -0
- package/dist/shim/project/record-format.js +345 -0
- package/dist/shim/project/specifiers.js +41 -13
- package/dist/shim/registry/index.js +1 -1
- package/dist/shim/registry/scene-source.js +44 -30
- package/dist/shim/registry/walkthrough.js +13 -12
- package/dist/shim/render-state/apply.js +11 -14
- package/dist/shim/storage/codec-chains.js +88 -0
- package/dist/shim/storage/folders.js +205 -0
- package/dist/shim/storage/index.js +64 -8
- package/dist/shim/storage/make-namespace-store.js +5 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/dist/shim/bundle/frqnb.js +0 -112
- package/dist/shim/bundle/manifest.js +0 -95
- package/dist/shim/storage/modules.js +0 -249
- package/dist/shim/storage/projects.js +0 -150
|
@@ -0,0 +1,457 @@
|
|
|
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?, length? } what a key declares when it is a literal
|
|
32
|
+
* (read-back); an array expression's element count otherwise
|
|
33
|
+
*
|
|
34
|
+
* serializeValue(value, key) → text THE one value serializer: numbers, strings (single-quoted),
|
|
35
|
+
* booleans, null, arrays inline, plain objects. Anything else THROWS
|
|
36
|
+
* naming the key.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { parse } from '../../frqtl/assets/vendor/acorn.mjs';
|
|
40
|
+
import { importSpecifierOf } from './parts-module.js';
|
|
41
|
+
|
|
42
|
+
/** The export the writer patches. */
|
|
43
|
+
export const PARTS_EXPORT = 'parts';
|
|
44
|
+
|
|
45
|
+
const _IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
46
|
+
const _RESERVED = new Set(('break case catch class const continue debugger default delete do else export extends ' +
|
|
47
|
+
'false finally for function if import in instanceof new null return super switch this throw true try typeof ' +
|
|
48
|
+
'var void while with yield let static enum await implements package protected interface private public').split(' '));
|
|
49
|
+
|
|
50
|
+
function _keyText(k) {
|
|
51
|
+
return (_IDENT.test(k) && !_RESERVED.has(k)) ? k : _quote(k);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _quote(s) {
|
|
55
|
+
return '\'' + s.replace(/\\/g, '\\\\').replace(/'/g, '\\\'').replace(/\n/g, '\\n').replace(/\r/g, '\\r')
|
|
56
|
+
.replace(/\t/g, '\\t').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029') + '\'';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {*} v
|
|
61
|
+
* @param {string} key — named in a refusal
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
export function serializeValue(v, key) {
|
|
65
|
+
if (v === null) { return 'null'; }
|
|
66
|
+
const t = typeof v;
|
|
67
|
+
if (t === 'number') {
|
|
68
|
+
if (!Number.isFinite(v)) { throw new Error('"' + key + '" is ' + String(v) + ' — the writer writes finite numbers only.'); }
|
|
69
|
+
return Object.is(v, -0) ? '0' : String(v);
|
|
70
|
+
}
|
|
71
|
+
if (t === 'string') { return _quote(v); }
|
|
72
|
+
if (t === 'boolean') { return v ? 'true' : 'false'; }
|
|
73
|
+
if (Array.isArray(v)) {
|
|
74
|
+
return '[' + v.map(function (x, i) { return serializeValue(x, key + '[' + i + ']'); }).join(', ') + ']';
|
|
75
|
+
}
|
|
76
|
+
if (t === 'object') {
|
|
77
|
+
const proto = Object.getPrototypeOf(v);
|
|
78
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
79
|
+
throw new Error('"' + key + '" is a ' + ((v.constructor && v.constructor.name) || 'class') +
|
|
80
|
+
' instance — the writer writes numbers, strings, booleans, null, arrays and plain objects.');
|
|
81
|
+
}
|
|
82
|
+
const ks = Object.keys(v);
|
|
83
|
+
if (!ks.length) { return '{}'; }
|
|
84
|
+
return '{ ' + ks.map(function (k) { return _keyText(k) + ': ' + serializeValue(v[k], key + '.' + k); }).join(', ') + ' }';
|
|
85
|
+
}
|
|
86
|
+
throw new Error('"' + key + '" is ' + (t === 'undefined' ? 'undefined' : 'a ' + t) +
|
|
87
|
+
' — the writer writes numbers, strings, booleans, null, arrays and plain objects.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A node whose text IS its value: a literal (not a regex), a negated number, or arrays/objects of those. */
|
|
91
|
+
function _isLiteral(n) {
|
|
92
|
+
if (!n) { return false; }
|
|
93
|
+
if (n.type === 'Literal') { return !n.regex && typeof n.value !== 'bigint'; }
|
|
94
|
+
if (n.type === 'UnaryExpression' && n.operator === '-') { return n.argument.type === 'Literal' && typeof n.argument.value === 'number'; }
|
|
95
|
+
if (n.type === 'ArrayExpression') { return n.elements.every(function (e) { return e && _isLiteral(e); }); }
|
|
96
|
+
if (n.type === 'ObjectExpression') {
|
|
97
|
+
return n.properties.every(function (p) {
|
|
98
|
+
return p.type === 'Property' && !p.computed && p.kind === 'init' && !p.method && !p.shorthand && _isLiteral(p.value);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The plain (non-computed) key of a Property node, else null. */
|
|
105
|
+
function _plainKey(p) {
|
|
106
|
+
if (p.type !== 'Property' || p.computed) { return null; }
|
|
107
|
+
if (p.key.type === 'Identifier') { return p.key.name; }
|
|
108
|
+
if (p.key.type === 'Literal' && (typeof p.key.value === 'string' || typeof p.key.value === 'number')) { return String(p.key.value); }
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The `parts` ObjectExpression: `export const parts = { … }`, else null. */
|
|
113
|
+
function _partsLiteral(ast) {
|
|
114
|
+
for (const s of ast.body) {
|
|
115
|
+
if (s.type !== 'ExportNamedDeclaration' || !s.declaration || s.declaration.type !== 'VariableDeclaration') { continue; }
|
|
116
|
+
for (const d of s.declaration.declarations) {
|
|
117
|
+
if (d.id.type === 'Identifier' && d.id.name === PARTS_EXPORT) {
|
|
118
|
+
return (d.init && d.init.type === 'ObjectExpression') ? d.init : null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Module-top bindings: name → { kind: 'import', source, imported } | { kind: 'decl' }. */
|
|
126
|
+
function _topBindings(ast) {
|
|
127
|
+
const out = new Map();
|
|
128
|
+
for (const s of ast.body) {
|
|
129
|
+
if (s.type === 'ImportDeclaration') {
|
|
130
|
+
for (const sp of s.specifiers) {
|
|
131
|
+
out.set(sp.local.name, { kind: 'import', source: s.source.value,
|
|
132
|
+
imported: sp.type === 'ImportSpecifier' ? (sp.imported.name || sp.imported.value) : null });
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const decl = (s.type === 'ExportNamedDeclaration' || s.type === 'ExportDefaultDeclaration') ? s.declaration : s;
|
|
137
|
+
if (!decl) { continue; }
|
|
138
|
+
if (decl.type === 'VariableDeclaration') {
|
|
139
|
+
for (const d of decl.declarations) { _patternNames(d.id).forEach(function (n) { out.set(n, { kind: 'decl' }); }); }
|
|
140
|
+
} else if ((decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') && decl.id) {
|
|
141
|
+
out.set(decl.id.name, { kind: 'decl' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _patternNames(p) {
|
|
148
|
+
if (!p) { return []; }
|
|
149
|
+
if (p.type === 'Identifier') { return [p.name]; }
|
|
150
|
+
if (p.type === 'ObjectPattern') { return p.properties.flatMap(function (q) { return _patternNames(q.type === 'RestElement' ? q.argument : q.value); }); }
|
|
151
|
+
if (p.type === 'ArrayPattern') { return p.elements.flatMap(_patternNames); }
|
|
152
|
+
if (p.type === 'RestElement') { return _patternNames(p.argument); }
|
|
153
|
+
if (p.type === 'AssignmentPattern') { return _patternNames(p.left); }
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The whitespace that starts the line holding offset `at`. */
|
|
158
|
+
function _indentAt(text, at) {
|
|
159
|
+
const ls = text.lastIndexOf('\n', at - 1) + 1;
|
|
160
|
+
const m = /^[ \t]*/.exec(text.slice(ls));
|
|
161
|
+
return m[0];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Is `obj` (an ObjectExpression) written across lines? */
|
|
165
|
+
function _multiline(text, obj) {
|
|
166
|
+
return text.slice(obj.start, obj.end).indexOf('\n') !== -1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The insertion that appends `prop` (a `key: value` text) as the LAST property of ObjectExpression `obj`.
|
|
171
|
+
* Matches the literal's own layout: one line → `, k: v` / `{ k: v }`; multi-line → a new line at the last
|
|
172
|
+
* property's indent, keeping a trailing comma the literal already writes.
|
|
173
|
+
*/
|
|
174
|
+
function _appendProperty(text, obj, prop) {
|
|
175
|
+
const close = obj.end - 1; // the '}'
|
|
176
|
+
const last = obj.properties[obj.properties.length - 1];
|
|
177
|
+
if (!last) {
|
|
178
|
+
const inner = text.slice(obj.start + 1, close);
|
|
179
|
+
if (inner.indexOf('\n') !== -1) {
|
|
180
|
+
const ind = _indentAt(text, close);
|
|
181
|
+
return { start: obj.start + 1, end: close, text: '\n' + ind + ' ' + prop + ',\n' + ind };
|
|
182
|
+
}
|
|
183
|
+
return { start: obj.start + 1, end: close, text: ' ' + prop + ' ' };
|
|
184
|
+
}
|
|
185
|
+
const between = text.slice(last.end, close);
|
|
186
|
+
const comma = between.indexOf(',');
|
|
187
|
+
if (_multiline(text, obj)) {
|
|
188
|
+
const ind = _indentAt(text, last.start);
|
|
189
|
+
if (comma !== -1) { return { start: last.end + comma + 1, end: last.end + comma + 1, text: '\n' + ind + prop + ',' }; }
|
|
190
|
+
return { start: last.end, end: last.end, text: ',\n' + ind + prop };
|
|
191
|
+
}
|
|
192
|
+
if (comma !== -1) { return { start: last.end + comma + 1, end: last.end + comma + 1, text: ' ' + prop + ',' }; }
|
|
193
|
+
return { start: last.end, end: last.end, text: ', ' + prop };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The removal of Property `p` from ObjectExpression `obj`, its comma (and, when it owns whole lines, those lines) included. */
|
|
197
|
+
function _removeProperty(text, obj, p) {
|
|
198
|
+
const i = obj.properties.indexOf(p);
|
|
199
|
+
const next = obj.properties[i + 1];
|
|
200
|
+
const stop = next ? next.start : obj.end - 1;
|
|
201
|
+
const after = text.slice(p.end, stop);
|
|
202
|
+
const comma = after.indexOf(',');
|
|
203
|
+
let start = p.start;
|
|
204
|
+
let end = comma !== -1 ? p.end + comma + 1 : p.end;
|
|
205
|
+
if (comma === -1 && i > 0) {
|
|
206
|
+
// the last property with no trailing comma: the comma before it goes
|
|
207
|
+
const prev = obj.properties[i - 1];
|
|
208
|
+
const pc = text.slice(prev.end, p.start).indexOf(',');
|
|
209
|
+
if (pc !== -1) { start = prev.end + pc; }
|
|
210
|
+
}
|
|
211
|
+
// An entry that owns its lines (only whitespace before it on its first line, only whitespace after its
|
|
212
|
+
// comma on its last) leaves no blank line behind.
|
|
213
|
+
const ls = text.lastIndexOf('\n', start - 1) + 1;
|
|
214
|
+
const le = text.indexOf('\n', end);
|
|
215
|
+
if (/^[ \t]*$/.test(text.slice(ls, start)) && le !== -1 && /^[ \t]*$/.test(text.slice(end, le))) {
|
|
216
|
+
start = ls; end = le + 1;
|
|
217
|
+
}
|
|
218
|
+
return { start: start, end: end, text: '' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {string} text — the sketch file's text
|
|
223
|
+
* @param {object[]} edits — see the file header
|
|
224
|
+
* @param {{entry?: string}} [opts] — the sketch's project path (default 'sketch.js'), for a fork's relative import
|
|
225
|
+
* @returns {{text: string, changes: object[], refused: object[]}}
|
|
226
|
+
*/
|
|
227
|
+
export function patchParts(text, edits, opts) {
|
|
228
|
+
const entry = (opts && opts.entry) || 'sketch.js';
|
|
229
|
+
if (typeof text !== 'string') {
|
|
230
|
+
throw new TypeError('[parts-writer] patchParts(text, edits): text is the sketch file (got ' + typeof text + '). (FAIL-LOUD.)');
|
|
231
|
+
}
|
|
232
|
+
if (!Array.isArray(edits)) {
|
|
233
|
+
throw new TypeError('[parts-writer] patchParts(text, edits): edits is an array of { op: set | add | remove }. (FAIL-LOUD.)');
|
|
234
|
+
}
|
|
235
|
+
const changes = [];
|
|
236
|
+
const refused = [];
|
|
237
|
+
if (!edits.length) { return { text: text, changes: changes, refused: refused }; }
|
|
238
|
+
function refuse(e, reason, key) {
|
|
239
|
+
const r = { op: e.op, name: e.name, reason: reason };
|
|
240
|
+
if (key !== undefined) { r.key = key; }
|
|
241
|
+
refused.push(r);
|
|
242
|
+
}
|
|
243
|
+
let ast;
|
|
244
|
+
try { ast = parse(text, { ecmaVersion: 'latest', sourceType: 'module' }); }
|
|
245
|
+
catch (err) {
|
|
246
|
+
edits.forEach(function (e) { refuse(e, 'the sketch does not parse (' + err.message + ') — fix it, then Run.'); });
|
|
247
|
+
return { text: text, changes: changes, refused: refused };
|
|
248
|
+
}
|
|
249
|
+
const parts = _partsLiteral(ast);
|
|
250
|
+
if (!parts) {
|
|
251
|
+
edits.forEach(function (e) {
|
|
252
|
+
refuse(e, 'this sketch has no `export const ' + PARTS_EXPORT + ' = { … }` object literal — the writer writes ' +
|
|
253
|
+
'only into that literal; declare the parts there (exp.declare(' + PARTS_EXPORT + ')) to make them editable.');
|
|
254
|
+
});
|
|
255
|
+
return { text: text, changes: changes, refused: refused };
|
|
256
|
+
}
|
|
257
|
+
const byName = new Map();
|
|
258
|
+
let opaque = false; // a spread or a computed key: a name may come from it
|
|
259
|
+
for (const p of parts.properties) {
|
|
260
|
+
const k = _plainKey(p);
|
|
261
|
+
if (k === null) { opaque = true; continue; }
|
|
262
|
+
byName.set(k, p); // a duplicated key: the last one wins, as in the language
|
|
263
|
+
}
|
|
264
|
+
const bindings = _topBindings(ast);
|
|
265
|
+
const patches = [];
|
|
266
|
+
const importLines = []; // { Kind, line }
|
|
267
|
+
const entryOf = function (e) {
|
|
268
|
+
const p = byName.get(e.name);
|
|
269
|
+
if (!p) {
|
|
270
|
+
refuse(e, opaque
|
|
271
|
+
? 'no plain key "' + e.name + '" in the parts literal, which carries a spread or a computed key — a name that ' +
|
|
272
|
+
'comes from either is not writable here; write the entry as a plain key.'
|
|
273
|
+
: 'the parts literal has no entry "' + e.name + '".');
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
return p;
|
|
277
|
+
};
|
|
278
|
+
const adds = [];
|
|
279
|
+
for (const e of edits) {
|
|
280
|
+
if (!e || typeof e !== 'object' || typeof e.name !== 'string' || !e.name) {
|
|
281
|
+
throw new TypeError('[parts-writer] an edit is { op, name, … } with a non-empty name (got ' + JSON.stringify(e) + '). (FAIL-LOUD.)');
|
|
282
|
+
}
|
|
283
|
+
if (e.op === 'set') {
|
|
284
|
+
const p = entryOf(e);
|
|
285
|
+
if (!p) { continue; }
|
|
286
|
+
const v = p.value;
|
|
287
|
+
if (v.type !== 'ArrayExpression' || v.elements.length !== 2 || !v.elements[0] || v.elements[0].type !== 'Identifier' ||
|
|
288
|
+
!v.elements[1] || v.elements[1].type !== 'ObjectExpression') {
|
|
289
|
+
refuse(e, 'the entry "' + e.name + '" is not written as [Kind, { …props }] — the writer patches only that form.');
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const obj = v.elements[1];
|
|
293
|
+
const own = new Map();
|
|
294
|
+
for (const q of obj.properties) { const k = _plainKey(q); if (k !== null) { own.set(k, q); } }
|
|
295
|
+
const appended = [];
|
|
296
|
+
for (const key of Object.keys(e.props || {})) {
|
|
297
|
+
let lit;
|
|
298
|
+
try { lit = serializeValue(e.props[key], key); }
|
|
299
|
+
catch (err) { refuse(e, err.message, key); continue; }
|
|
300
|
+
const q = own.get(key);
|
|
301
|
+
if (q && !q.shorthand) {
|
|
302
|
+
const old = text.slice(q.value.start, q.value.end);
|
|
303
|
+
if (old === lit) { continue; }
|
|
304
|
+
patches.push({ start: q.value.start, end: q.value.end, text: lit });
|
|
305
|
+
const c = { op: 'set', name: e.name, key: key, from: old, to: lit };
|
|
306
|
+
if (!_isLiteral(q.value)) { c.literalized = true; }
|
|
307
|
+
changes.push(c);
|
|
308
|
+
} else if (q && q.shorthand) {
|
|
309
|
+
patches.push({ start: q.start, end: q.end, text: _keyText(key) + ': ' + lit });
|
|
310
|
+
changes.push({ op: 'set', name: e.name, key: key, from: key, to: lit, literalized: true });
|
|
311
|
+
} else {
|
|
312
|
+
appended.push(_keyText(key) + ': ' + lit);
|
|
313
|
+
changes.push({ op: 'set', name: e.name, key: key, from: null, to: lit });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (appended.length) {
|
|
317
|
+
const ins = _appendProperty(text, obj, appended.join(_multiline(text, obj) ? ',\n' + _indentAt(text, (obj.properties[obj.properties.length - 1] || obj).start) : ', '));
|
|
318
|
+
patches.push(ins);
|
|
319
|
+
}
|
|
320
|
+
} else if (e.op === 'remove') {
|
|
321
|
+
const p = entryOf(e);
|
|
322
|
+
if (!p) { continue; }
|
|
323
|
+
patches.push(_removeProperty(text, parts, p));
|
|
324
|
+
changes.push({ op: 'remove', name: e.name, from: text.slice(p.start, p.end), to: null });
|
|
325
|
+
} else if (e.op === 'add') {
|
|
326
|
+
if (byName.has(e.name) || adds.some(function (a) { return a.name === e.name; })) {
|
|
327
|
+
refuse(e, 'the parts literal already has an entry "' + e.name + '" — a declared name is one key.');
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (typeof e.Kind !== 'string' || !_IDENT.test(e.Kind)) {
|
|
331
|
+
throw new TypeError('[parts-writer] add: Kind is the kind\'s class name (got ' + JSON.stringify(e.Kind) + '). (FAIL-LOUD.)');
|
|
332
|
+
}
|
|
333
|
+
if (e.module === null || e.module === undefined) {
|
|
334
|
+
refuse(e, 'the kind ' + e.Kind + ' has no importable module (an internal kind, not a part of ' +
|
|
335
|
+
'@descix/frqtl-web/parts nor a project file), so a sketch cannot import it — declare it in code.');
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const spec = importSpecifierOf(e.module, entry);
|
|
339
|
+
const b = bindings.get(e.Kind);
|
|
340
|
+
if (b && !(b.kind === 'import' && b.source === spec && b.imported === e.Kind)) {
|
|
341
|
+
refuse(e, 'the identifier ' + e.Kind + ' is already bound in this sketch to ' +
|
|
342
|
+
(b.kind === 'import' ? '"' + (b.imported || '*') + '" from \'' + b.source + '\'' : 'its own declaration') +
|
|
343
|
+
' — the writer cannot import the kind ' + e.Kind + ' under that name.');
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
let lit;
|
|
347
|
+
try { lit = serializeValue(e.props || {}, e.name); }
|
|
348
|
+
catch (err) { refuse(e, err.message); continue; }
|
|
349
|
+
if (!b && !importLines.some(function (l) { return l.Kind === e.Kind; })) {
|
|
350
|
+
importLines.push({ Kind: e.Kind, line: 'import { ' + e.Kind + ' } from ' + _quote(spec) + ';' });
|
|
351
|
+
}
|
|
352
|
+
adds.push({ name: e.name, text: _keyText(e.name) + ': [' + e.Kind + ', ' + lit + ']' });
|
|
353
|
+
changes.push({ op: 'add', name: e.name, from: null, to: _keyText(e.name) + ': [' + e.Kind + ', ' + lit + ']',
|
|
354
|
+
import: (!b ? 'import { ' + e.Kind + ' } from ' + _quote(spec) + ';' : null) });
|
|
355
|
+
} else {
|
|
356
|
+
throw new TypeError('[parts-writer] unknown edit op ' + JSON.stringify(e.op) + ' — set | add | remove. (FAIL-LOUD.)');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (adds.length) {
|
|
360
|
+
const joined = adds.map(function (a) { return a.text; });
|
|
361
|
+
if (_multiline(text, parts) || parts.properties.length === 0) {
|
|
362
|
+
if (parts.properties.length === 0) {
|
|
363
|
+
const ind = _indentAt(text, parts.start);
|
|
364
|
+
patches.push({ start: parts.start + 1, end: parts.end - 1,
|
|
365
|
+
text: '\n' + joined.map(function (t) { return ind + ' ' + t + ','; }).join('\n') + '\n' + ind });
|
|
366
|
+
} else {
|
|
367
|
+
const ind = _indentAt(text, parts.properties[parts.properties.length - 1].start);
|
|
368
|
+
const last = parts.properties[parts.properties.length - 1];
|
|
369
|
+
const comma = text.slice(last.end, parts.end - 1).indexOf(',');
|
|
370
|
+
if (comma !== -1) {
|
|
371
|
+
patches.push({ start: last.end + comma + 1, end: last.end + comma + 1,
|
|
372
|
+
text: joined.map(function (t) { return '\n' + ind + t + ','; }).join('') });
|
|
373
|
+
} else {
|
|
374
|
+
patches.push({ start: last.end, end: last.end, text: joined.map(function (t) { return ',\n' + ind + t; }).join('') });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
patches.push(_appendProperty(text, parts, joined.join(', ')));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (importLines.length) {
|
|
382
|
+
const imports = ast.body.filter(function (s) { return s.type === 'ImportDeclaration'; });
|
|
383
|
+
let at;
|
|
384
|
+
if (imports.length) {
|
|
385
|
+
const lastImp = imports[imports.length - 1];
|
|
386
|
+
const nl = text.indexOf('\n', lastImp.end);
|
|
387
|
+
at = nl === -1 ? text.length : nl + 1;
|
|
388
|
+
const block = importLines.map(function (l) { return l.line + '\n'; }).join('');
|
|
389
|
+
patches.push({ start: at, end: at, text: nl === -1 ? '\n' + block : block });
|
|
390
|
+
} else {
|
|
391
|
+
const first = ast.body[0];
|
|
392
|
+
at = first ? text.lastIndexOf('\n', first.start - 1) + 1 : text.length;
|
|
393
|
+
patches.push({ start: at, end: at, text: importLines.map(function (l) { return l.line + '\n'; }).join('') + '\n' });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
patches.sort(function (a, b) { return b.start - a.start || b.end - a.end; });
|
|
397
|
+
for (let i = 1; i < patches.length; i++) {
|
|
398
|
+
if (patches[i].end > patches[i - 1].start) {
|
|
399
|
+
throw new Error('[parts-writer] two edits touch overlapping text — one edit per property. (FAIL-LOUD.)');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
let out = text;
|
|
403
|
+
for (const p of patches) { out = out.slice(0, p.start) + p.text + out.slice(p.end); }
|
|
404
|
+
return { text: out, changes: changes, refused: refused };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The props ObjectExpression of entry `name` in the parsed `parts` literal; THROWS naming what is missing. */
|
|
408
|
+
function _entryProps(text, name, who) {
|
|
409
|
+
const ast = parse(text, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
410
|
+
const parts = _partsLiteral(ast);
|
|
411
|
+
if (!parts) {
|
|
412
|
+
throw new Error('[parts-writer] ' + who + ': the sketch has no `export const ' + PARTS_EXPORT + ' = { … }` object literal. (FAIL-LOUD.)');
|
|
413
|
+
}
|
|
414
|
+
let p = null;
|
|
415
|
+
for (const q of parts.properties) { if (_plainKey(q) === name) { p = q; } }
|
|
416
|
+
const v = p && p.value;
|
|
417
|
+
if (!v || v.type !== 'ArrayExpression' || v.elements.length !== 2 || !v.elements[1] || v.elements[1].type !== 'ObjectExpression') {
|
|
418
|
+
throw new Error('[parts-writer] ' + who + ': the parts literal has no entry "' + name + '" written as [Kind, { …props }]. (FAIL-LOUD.)');
|
|
419
|
+
}
|
|
420
|
+
return v.elements[1];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function _literalValue(n) {
|
|
424
|
+
if (n.type === 'Literal') { return n.value; }
|
|
425
|
+
if (n.type === 'UnaryExpression') { return -n.argument.value; }
|
|
426
|
+
if (n.type === 'ArrayExpression') { return n.elements.map(_literalValue); }
|
|
427
|
+
const o = {};
|
|
428
|
+
for (const p of n.properties) { o[_plainKey(p)] = _literalValue(p.value); }
|
|
429
|
+
return o;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* entryKeys(text, name) → string[] — the plain prop keys entry `name` spells in the parsed `parts` literal, in
|
|
434
|
+
* source order (a spread or a computed key is not a plain key). The writer asks it how an entry spells a value
|
|
435
|
+
* (e.g. `rect` alone, or `rect` beside `z` / `depth`), never a regex over the text. THROWS on no such entry.
|
|
436
|
+
*/
|
|
437
|
+
export function entryKeys(text, name) {
|
|
438
|
+
const out = [];
|
|
439
|
+
for (const q of _entryProps(text, name, 'entryKeys').properties) { const k = _plainKey(q); if (k !== null) { out.push(k); } }
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* entryLiteral(text, name, key) → { literal: true, value } | { literal: false, length? } — what entry `name`'s
|
|
445
|
+
* `key` declares when its value is a literal (the read-back after a write: the text a patch produced, parsed
|
|
446
|
+
* again). A non-literal ARRAY expression answers its element count as `length` (the arity it spells); any
|
|
447
|
+
* other expression answers no length. THROWS on no such entry or no such key.
|
|
448
|
+
*/
|
|
449
|
+
export function entryLiteral(text, name, key) {
|
|
450
|
+
for (const q of _entryProps(text, name, 'entryLiteral').properties) {
|
|
451
|
+
if (_plainKey(q) !== key) { continue; }
|
|
452
|
+
if (_isLiteral(q.value)) { return { literal: true, value: _literalValue(q.value) }; }
|
|
453
|
+
// An array expression still spells its arity (`center: [srcX, cy]` is two numbers).
|
|
454
|
+
return q.value.type === 'ArrayExpression' ? { literal: false, length: q.value.elements.length } : { literal: false };
|
|
455
|
+
}
|
|
456
|
+
throw new Error('[parts-writer] entryLiteral: the entry "' + name + '" spells no key "' + key + '". (FAIL-LOUD.)');
|
|
457
|
+
}
|