@descix/frqtl-node 0.6.0-p4.1 → 0.6.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/CLAUDE.md +2 -2
- package/README.md +5 -3
- package/dist/dist-manifest.json +58 -54
- package/dist/fat/pkg-node/package.json +2 -10
- package/dist/frqtl/compat/providers/wasm-engine.js +3 -4
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +1 -1
- package/dist/frqtl/objects/FrqtlApp.js +5 -5
- package/dist/frqtl/objects/RecipeContext.js +106 -1
- package/dist/frqtl/objects/constants/WallBehaviors.js +1 -1
- package/dist/frqtl/objects/instruments/capture.js +1 -24
- package/dist/frqtl/objects/instruments/hosted-eyes.js +17 -1
- package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
- package/dist/frqtl/objects/record/Record.js +25 -5
- package/dist/frqtl/objects/schema/engine-scopes.js +2 -2
- package/dist/frqtl/objects/schema/scopes.js +2 -2
- package/dist/frqtl/objects/setup/_worldInit.js +1 -2
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +1 -2
- package/dist/frqtl/parts/CameraControls.js +8 -0
- package/dist/frqtl/parts/DetectorWall.js +1 -1
- package/dist/frqtl/parts/GraphingDetectorWall.js +7 -7
- package/dist/frqtl/parts/LargeObject.js +2 -2
- package/dist/frqtl/parts/LiveChart.js +2 -2
- package/dist/frqtl/parts/LogicGate.js +3 -3
- package/dist/frqtl/parts/OvenBox.js +1 -1
- package/dist/frqtl/parts/PointSource.js +1 -1
- package/dist/frqtl/parts/SlitLightSource.js +1 -1
- package/dist/frqtl/parts/SlitScreen.js +1 -1
- package/dist/frqtl/parts/SphereEmitter.js +12 -12
- package/dist/frqtl/parts/Trace.js +1 -1
- package/dist/frqtl/parts/Tunnel.js +11 -11
- package/dist/frqtl/parts/Wall.js +20 -10
- package/dist/frqtl/scenes/circuit-sat.js +1 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +32 -34
- package/dist/frqtl/scenes/dse-two-emitters.js +33 -34
- package/dist/frqtl/scenes/mirror-route.js +96 -79
- package/dist/shim/bundle/index.js +6 -0
- package/dist/shim/controls/index.js +1 -1
- package/dist/shim/engine/artifact-shell.js +1 -1
- package/dist/shim/engine/router.js +3 -3
- package/dist/shim/host/host-view.js +228 -0
- package/dist/shim/host/index.js +18 -0
- package/dist/shim/index.js +2 -4
- package/dist/shim/notebook/kernel.js +1 -1
- package/dist/shim/notebook/setup-builder.js +2 -2
- package/dist/shim/package.json +1 -0
- package/dist/shim/page-params/index.js +2 -30
- package/dist/shim/project/fork.js +293 -0
- package/dist/shim/project/identity.js +3 -1
- package/dist/shim/project/link.js +43 -3
- package/dist/shim/project/parts-writer.js +62 -5
- package/dist/shim/project/read-text.js +43 -0
- package/dist/shim/project/specifiers.js +104 -15
- package/dist/shim/registry/exhibits.js +15 -34
- package/dist/shim/registry/index.js +2 -2
- package/dist/shim/registry/scene-roster.js +35 -0
- package/dist/shim/registry/scene-source.js +6 -36
- package/dist/shim/registry/walkthrough.js +2 -3
- package/dist/shim/render-state/apply.js +66 -31
- package/dist/shim/render-state/index.js +6 -2
- package/dist/shim/runtime/index.js +4 -4
- package/llms.txt +4 -3
- package/package.json +19 -1
- package/dist/shim/registry/experiments.js +0 -215
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* host-view.js — the ONE declarer of the DeSciX host view for an app DOCUMENT.
|
|
5
|
+
*
|
|
6
|
+
* A DeSciX shell hosts the app beside its chat (SplitView, its default) unless the app asks for
|
|
7
|
+
* another view. This module asks — once, through the app-side SDK (`DeSciXAppSDK.js`, vendored
|
|
8
|
+
* by the publisher beside the page) — and RECORDS on `document.documentElement.dataset.descixView`
|
|
9
|
+
* which host state it met. The mode is a parameter; the policy lives at the call site.
|
|
10
|
+
*
|
|
11
|
+
* Every host state is a row of HOST_VIEW_OUTCOMES. No row throws and no row writes to
|
|
12
|
+
* console.error: the page's job is physics and layout is the host's, so a missing host is a
|
|
13
|
+
* truthful record, never a crash and never a cry-wolf console line. Whether an absence is
|
|
14
|
+
* LEGITIMATE is decided by a gate at the plane that knows (FRAQTL's static URL gate, egpt-frqtl's
|
|
15
|
+
* refresh-site refusal), not here.
|
|
16
|
+
*
|
|
17
|
+
* Two SDK rules shape the order of operations:
|
|
18
|
+
* - The SDK's `view` getter console.errors on a standalone page AND on a shell that does not
|
|
19
|
+
* publish `view`. So `view` is read only after `ready()` resolved `mode:'shell'` AND its
|
|
20
|
+
* `members` list names `view`.
|
|
21
|
+
* - `view.set()` on a host that renders a bare iframe validates and returns the mode while
|
|
22
|
+
* nothing moves. `view.available()` is the only instrument that tells the two apart, so it
|
|
23
|
+
* is consulted here, before `set`, and nowhere else.
|
|
24
|
+
*
|
|
25
|
+
* A caller that names a declared second choice (`fallback`) is answered off the shell's PUBLISHED list,
|
|
26
|
+
* `view.MODES`, read before any `set`: the asked mode when listed, else the fallback when listed (recorded as
|
|
27
|
+
* the `fallback` row, the refusal and the mode set in the detail), else nothing (`unoffered`). A `set` is never
|
|
28
|
+
* retried and never decided by its error: a shell that lists a mode and then refuses it is a contract violation.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The app-side SDK file this declarer consumes. It is PUBLISHER-provided: vendored beside the
|
|
33
|
+
* page by the site that publishes the static bundle, never shipped in the bundle.
|
|
34
|
+
*/
|
|
35
|
+
export const HOST_SDK_SCRIPT = 'DeSciXAppSDK.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The attribute that declares a file host-provided, on the element that consumes it, in the
|
|
39
|
+
* bytes the browser executes. Both the bundle gate (the file must be ABSENT from the bundle) and
|
|
40
|
+
* the publisher (the file must be PRESENT beside the page) read the set from this attribute.
|
|
41
|
+
*/
|
|
42
|
+
export const HOST_PROVIDED_ATTR = 'data-frqtl-host-provided';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The publisher's front page, beside the IDE entry (router.js::STATIC_APP_PAGE). PUBLISHER-provided
|
|
46
|
+
* like HOST_SDK_SCRIPT: the static build declares it once as `<link rel="home" href=HOST_FRONT_PAGE
|
|
47
|
+
* HOST_PROVIDED_ATTR>` (apps/ide/vite.config.js), the bundle gate asserts the bundle does not ship
|
|
48
|
+
* it, and lib/features/ide/home-link.js::homeHrefOf reads the declaration back.
|
|
49
|
+
*/
|
|
50
|
+
export const HOST_FRONT_PAGE = 'index.html';
|
|
51
|
+
|
|
52
|
+
/** The closed table of host states. `setCalls` is how many times `view.set` runs in that row. */
|
|
53
|
+
export const HOST_VIEW_OUTCOMES = Object.freeze([
|
|
54
|
+
{ id: 'pending', setCalls: 0,
|
|
55
|
+
means: 'awaiting DeSciX.ready(); a ready() that never settles stays here' },
|
|
56
|
+
{ id: 'requested', setCalls: 1,
|
|
57
|
+
means: 'shell listening: view.available() true and view.set(mode) returned mode; recorded as requested:<mode>' },
|
|
58
|
+
{ id: 'fallback', setCalls: 1,
|
|
59
|
+
means: 'shell listening; view.MODES lacks the asked mode and offers the caller\'s declared fallback: view.set(fallback) returned it; recorded as fallback:<asked mode>, the refusal and the mode set in dataset.descixViewDetail' },
|
|
60
|
+
{ id: 'unoffered', setCalls: 0,
|
|
61
|
+
means: 'shell listening; view.MODES offers neither the asked mode nor the declared fallback; recorded as unoffered:<asked mode>, the list in dataset.descixViewDetail' },
|
|
62
|
+
{ id: 'not-listening', setCalls: 0,
|
|
63
|
+
means: 'shell publishes view but view.available() is false (bare-iframe host); the host owns the layout' },
|
|
64
|
+
{ id: 'view-unpublished', setCalls: 0,
|
|
65
|
+
means: 'shell resolved, but its members do not include view' },
|
|
66
|
+
{ id: 'standalone', setCalls: 0,
|
|
67
|
+
means: 'ready() resolved standalone: this page is the top window, no shell above it' },
|
|
68
|
+
{ id: 'embedded-no-bridge', setCalls: 0,
|
|
69
|
+
means: 'this page is framed and no ancestor carries a readable bus (the SDK rejects ready() here today)' },
|
|
70
|
+
{ id: 'sdk-absent', setCalls: 0,
|
|
71
|
+
means: 'window.DeSciX is undefined: the publisher-provided DeSciXAppSDK.js is not beside this page' },
|
|
72
|
+
{ id: 'contract-violated', setCalls: null,
|
|
73
|
+
means: 'the SDK or host broke its published shape; the edge is named in dataset.descixViewDetail (CONTRACT_VIOLATIONS)' },
|
|
74
|
+
].map(Object.freeze));
|
|
75
|
+
|
|
76
|
+
/** The closed edges of `contract-violated`, each with its own `view.set` call count. */
|
|
77
|
+
export const CONTRACT_VIOLATIONS = Object.freeze({
|
|
78
|
+
'no-ready': Object.freeze({ setCalls: 0, means: 'window.DeSciX has no ready() function' }),
|
|
79
|
+
'ready-rejected': Object.freeze({ setCalls: 0, means: 'ready() rejected while the SDK reports a mode other than embedded-no-bridge' }),
|
|
80
|
+
'ready-unknown-mode': Object.freeze({ setCalls: 0, means: 'ready() resolved a mode that is not shell, standalone or embedded-no-bridge' }),
|
|
81
|
+
'ready-no-members': Object.freeze({ setCalls: 0, means: 'ready() resolved shell without a members array' }),
|
|
82
|
+
'view-listed-absent': Object.freeze({ setCalls: 0, means: 'members names view but the view getter returned nothing' }),
|
|
83
|
+
'view-no-available': Object.freeze({ setCalls: 0, means: 'view has no available() function' }),
|
|
84
|
+
'view-no-set': Object.freeze({ setCalls: 0, means: 'view has no set() function' }),
|
|
85
|
+
'available-threw': Object.freeze({ setCalls: 0, means: 'view.available() threw' }),
|
|
86
|
+
'view-no-modes': Object.freeze({ setCalls: 0, means: 'a fallback was given but view.MODES is not published' }),
|
|
87
|
+
'set-threw': Object.freeze({ setCalls: 1, means: 'view.set(mode) threw (or view.set(fallback), the mode view.MODES offered)' }),
|
|
88
|
+
'set-returned-other': Object.freeze({ setCalls: 1, means: 'view.set(mode) returned something other than the mode it was asked for' }),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const ROW_IDS = new Set(HOST_VIEW_OUTCOMES.map((r) => r.id));
|
|
92
|
+
|
|
93
|
+
function messageOf(e) {
|
|
94
|
+
return (e && e.message) ? String(e.message) : String(e);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Write a row to the document record. Refuses (throws) a row or edge the tables do not list:
|
|
99
|
+
* that is a defect in THIS module, never a host state.
|
|
100
|
+
*/
|
|
101
|
+
function record(root, row, { suffix = '', detail } = {}) {
|
|
102
|
+
if (!ROW_IDS.has(row)) { throw new Error(`host-view: '${row}' is not a HOST_VIEW_OUTCOMES row`); }
|
|
103
|
+
const value = row + suffix;
|
|
104
|
+
if (root) {
|
|
105
|
+
root.dataset.descixView = value;
|
|
106
|
+
if (detail === undefined) { delete root.dataset.descixViewDetail; } else { root.dataset.descixViewDetail = detail; }
|
|
107
|
+
}
|
|
108
|
+
return detail === undefined ? { row, record: value } : { row, record: value, detail };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function violated(root, edge, specifics) {
|
|
112
|
+
if (!Object.prototype.hasOwnProperty.call(CONTRACT_VIOLATIONS, edge)) {
|
|
113
|
+
throw new Error(`host-view: '${edge}' is not a CONTRACT_VIOLATIONS edge`);
|
|
114
|
+
}
|
|
115
|
+
return record(root, 'contract-violated', { detail: `${edge}: ${specifics}` });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The SDK's own live mode, read without touching any member getter. */
|
|
119
|
+
function liveMode(sdk) {
|
|
120
|
+
try { return sdk.mode; } catch (e) { return undefined; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Ask the host for `mode`, once, and record which host state was met.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} args
|
|
127
|
+
* @param {object|undefined} args.sdk the app-side SDK object (`window.DeSciX`), or undefined
|
|
128
|
+
* @param {string} args.mode the view to ask for: a mode the shell's `view.MODES` offers
|
|
129
|
+
* @param {string} [args.fallback] the caller's declared second choice, asked only when `view.MODES` does not
|
|
130
|
+
* offer `mode` (a non-empty string other than `mode`)
|
|
131
|
+
* @param {{dataset: object}} [args.root] where the record is written; defaults to
|
|
132
|
+
* `document.documentElement` when a document exists
|
|
133
|
+
* @returns {Promise<{row: string, record: string, detail?: string}>}
|
|
134
|
+
*/
|
|
135
|
+
export async function declareHostView({ sdk, mode, fallback, root } = {}) {
|
|
136
|
+
if (typeof mode !== 'string' || mode === '') {
|
|
137
|
+
throw new TypeError(`host-view: declareHostView needs a view mode string (got ${JSON.stringify(mode)})`);
|
|
138
|
+
}
|
|
139
|
+
if (fallback !== undefined && (typeof fallback !== 'string' || fallback === '' || fallback === mode)) {
|
|
140
|
+
throw new TypeError(`host-view: a fallback is a non-empty view mode string other than mode ${JSON.stringify(mode)} (got ${JSON.stringify(fallback)})`);
|
|
141
|
+
}
|
|
142
|
+
const target = root || (typeof document !== 'undefined' ? document.documentElement : null);
|
|
143
|
+
|
|
144
|
+
if (sdk === undefined || sdk === null) { return record(target, 'sdk-absent'); }
|
|
145
|
+
if (typeof sdk.ready !== 'function') {
|
|
146
|
+
return violated(target, 'no-ready', `window.DeSciX is a ${typeof sdk} without ready()`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
record(target, 'pending');
|
|
150
|
+
let ready;
|
|
151
|
+
try {
|
|
152
|
+
ready = await sdk.ready();
|
|
153
|
+
} catch (e) {
|
|
154
|
+
const now = liveMode(sdk);
|
|
155
|
+
if (now === 'embedded-no-bridge') {
|
|
156
|
+
return record(target, 'embedded-no-bridge', { detail: `ready() rejected: ${messageOf(e)}` });
|
|
157
|
+
}
|
|
158
|
+
return violated(target, 'ready-rejected', `SDK mode ${JSON.stringify(now)}; ${messageOf(e)}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const resolved = ready && ready.mode;
|
|
162
|
+
if (resolved === 'standalone') { return record(target, 'standalone'); }
|
|
163
|
+
if (resolved === 'embedded-no-bridge') { return record(target, 'embedded-no-bridge'); }
|
|
164
|
+
if (resolved !== 'shell') {
|
|
165
|
+
return violated(target, 'ready-unknown-mode', `ready() resolved mode ${JSON.stringify(resolved)}`);
|
|
166
|
+
}
|
|
167
|
+
if (!Array.isArray(ready.members)) {
|
|
168
|
+
return violated(target, 'ready-no-members', 'ready() resolved shell without a members array');
|
|
169
|
+
}
|
|
170
|
+
if (!ready.members.includes('view')) {
|
|
171
|
+
return record(target, 'view-unpublished', { detail: `members: ${ready.members.join(',')}` });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const view = sdk.view;
|
|
175
|
+
if (!view) { return violated(target, 'view-listed-absent', 'members names view; the getter returned ' + String(view)); }
|
|
176
|
+
if (typeof view.available !== 'function') { return violated(target, 'view-no-available', 'view.available is ' + typeof view.available); }
|
|
177
|
+
if (typeof view.set !== 'function') { return violated(target, 'view-no-set', 'view.set is ' + typeof view.set); }
|
|
178
|
+
|
|
179
|
+
let listening;
|
|
180
|
+
try {
|
|
181
|
+
listening = view.available();
|
|
182
|
+
} catch (e) {
|
|
183
|
+
return violated(target, 'available-threw', messageOf(e));
|
|
184
|
+
}
|
|
185
|
+
if (!listening) { return record(target, 'not-listening'); }
|
|
186
|
+
|
|
187
|
+
// With a declared fallback the published list decides which ONE mode is asked; without one, the asked mode is.
|
|
188
|
+
let ask = mode;
|
|
189
|
+
if (fallback !== undefined) {
|
|
190
|
+
const modes = view.MODES;
|
|
191
|
+
if (!modes || typeof modes !== 'object') {
|
|
192
|
+
return violated(target, 'view-no-modes', 'fallback given but view.MODES is not published (got ' + String(modes) + ')');
|
|
193
|
+
}
|
|
194
|
+
const offered = Object.values(modes);
|
|
195
|
+
const list = '[' + offered.join(', ') + ']';
|
|
196
|
+
if (!offered.includes(mode)) {
|
|
197
|
+
if (!offered.includes(fallback)) {
|
|
198
|
+
return record(target, 'unoffered', { suffix: ':' + mode,
|
|
199
|
+
detail: `view.MODES ${list} offers neither "${mode}" nor the fallback "${fallback}"` });
|
|
200
|
+
}
|
|
201
|
+
ask = fallback;
|
|
202
|
+
}
|
|
203
|
+
if (ask === fallback) {
|
|
204
|
+
let gotFallback;
|
|
205
|
+
try {
|
|
206
|
+
gotFallback = view.set(fallback);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
return violated(target, 'set-threw', messageOf(e));
|
|
209
|
+
}
|
|
210
|
+
if (gotFallback !== fallback) {
|
|
211
|
+
return violated(target, 'set-returned-other', `asked ${fallback}, set returned ${JSON.stringify(gotFallback)}`);
|
|
212
|
+
}
|
|
213
|
+
return record(target, 'fallback', { suffix: ':' + mode,
|
|
214
|
+
detail: `refused: "${mode}" is not in view.MODES ${list}; set: ${fallback}` });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let got;
|
|
219
|
+
try {
|
|
220
|
+
got = view.set(ask);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
return violated(target, 'set-threw', messageOf(e));
|
|
223
|
+
}
|
|
224
|
+
if (got !== ask) {
|
|
225
|
+
return violated(target, 'set-returned-other', `asked ${ask}, set returned ${JSON.stringify(got)}`);
|
|
226
|
+
}
|
|
227
|
+
return record(target, 'requested', { suffix: ':' + ask });
|
|
228
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* index.js — the published door to the host-view declarer: `@descix/frqtl-web/host-view` and
|
|
5
|
+
* `@descix/frqtl-node/host-view`. Exactly the names below, re-exported from THE declarer (host-view.js); its
|
|
6
|
+
* static-build vocabulary (HOST_PROVIDED_ATTR, HOST_FRONT_PAGE) stays FRAQTL's own.
|
|
7
|
+
*
|
|
8
|
+
* HOST_VIEW_EXPORTS the published names (the parity gate holds both packages' ./host-view to this table)
|
|
9
|
+
*
|
|
10
|
+
* A consuming app calls `declareHostView({ sdk: window.DeSciX, mode, fallback? })` once, under its own policy, and
|
|
11
|
+
* reads the outcome off `document.documentElement.dataset.descixView` (a HOST_VIEW_OUTCOMES row). HOST_SDK_SCRIPT is
|
|
12
|
+
* the publisher-provided SDK file the app vendors beside its page.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The names `./host-view` publishes. */
|
|
16
|
+
export const HOST_VIEW_EXPORTS = Object.freeze(['declareHostView', 'HOST_VIEW_OUTCOMES', 'CONTRACT_VIOLATIONS', 'HOST_SDK_SCRIPT']);
|
|
17
|
+
|
|
18
|
+
export { declareHostView, HOST_VIEW_OUTCOMES, CONTRACT_VIOLATIONS, HOST_SDK_SCRIPT } from './host-view.js';
|
package/dist/shim/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* FAIL-LOUD load error — no window.Common* ambient globals.
|
|
8
8
|
*
|
|
9
9
|
* Usage (under vite with alias 'lib' → repo-root/lib):
|
|
10
|
-
* import { Bus, Runtime,
|
|
10
|
+
* import { Bus, Runtime, sceneGroups, engineRouter } from 'lib/shim';
|
|
11
11
|
*
|
|
12
12
|
* Usage (direct relative path):
|
|
13
13
|
* import { Bus } from '../../lib/shim/index.js';
|
|
@@ -15,9 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
export { Bus } from './bus.js';
|
|
17
17
|
export { Runtime } from './runtime/index.js';
|
|
18
|
-
export {
|
|
19
|
-
isPublicExperiment, publicExperiments,
|
|
20
|
-
internalExperiments } from './registry/experiments.js';
|
|
18
|
+
export { sceneGroups, SHOWCASE_GROUP } from './registry/scene-roster.js';
|
|
21
19
|
export { WALKTHROUGH, walkthroughIs3D, walkthroughModes, walkthroughGroup,
|
|
22
20
|
walkthroughById,
|
|
23
21
|
FIRST_CONTACT_WALKTHROUGH_ID,
|
|
@@ -148,7 +148,7 @@ export class NotebookKernel {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/* ── scope ──────────────────────────────────────────────────────────────
|
|
151
|
-
* THE SCOPE RULE
|
|
151
|
+
* THE SCOPE RULE: a math value's scope IS this kernel's
|
|
152
152
|
* binding scope. On the shipped WASM dist an EGPT value is an arena handle,
|
|
153
153
|
* so if the arena is drained while a binding still names one, that binding is
|
|
154
154
|
* a dead handle and the next cell dies with `arena:stale-handle`. The two
|
|
@@ -288,7 +288,7 @@ export function parseSeed(seed) {
|
|
|
288
288
|
* canvas boots the same scene as every other surface.
|
|
289
289
|
*
|
|
290
290
|
* shape='recipe': the module's DEFAULT export is a `buildExperiment(exp)` RECIPE — a SINGLE
|
|
291
|
-
* RecipeContext arg (the sketch form
|
|
291
|
+
* RecipeContext arg (the sketch form the scene files and the parts writer write). Template:
|
|
292
292
|
* const buildExperiment = (await import('<module>')).default; // FAIL-LOUD if not a function
|
|
293
293
|
* const exp = frqtl.RecipeContext({ seed, deferCanvas: true }); // SIZELESS
|
|
294
294
|
* exp.build(buildExperiment); // runs its co-init call + addObject() — NO start
|
|
@@ -298,7 +298,7 @@ export function parseSeed(seed) {
|
|
|
298
298
|
* recipe takes NO knob bag — a non-empty `opts` on a recipe canvas FAILS LOUD
|
|
299
299
|
* (rather than silently dropping it).
|
|
300
300
|
*
|
|
301
|
-
* WHY the template checks `buildExperiment` but not `exp
|
|
301
|
+
* WHY the template checks `buildExperiment` but not `exp`: the two values
|
|
302
302
|
* have different OWNERS. `buildExperiment` is read off the module namespace by this template
|
|
303
303
|
* itself, so nothing but this template can vouch for it — hence the emitted FAIL-LOUD check.
|
|
304
304
|
* `exp` comes from `frqtl.RecipeContext(...)`, a caps.frqtl member: its owner
|
package/dist/shim/package.json
CHANGED
|
@@ -64,8 +64,7 @@
|
|
|
64
64
|
var KEYS = Object.freeze({
|
|
65
65
|
SEED: 'seed',
|
|
66
66
|
RENDERER: 'renderer',
|
|
67
|
-
ENGINE: 'engine'
|
|
68
|
-
STEPPING: 'stepping'
|
|
67
|
+
ENGINE: 'engine'
|
|
69
68
|
});
|
|
70
69
|
|
|
71
70
|
/**
|
|
@@ -85,7 +84,7 @@ var KEYS = Object.freeze({
|
|
|
85
84
|
var PAGE_PARAMS = Object.freeze([
|
|
86
85
|
Object.freeze({
|
|
87
86
|
key: KEYS.SEED,
|
|
88
|
-
reader: 'lib/frqtl/experiments/
|
|
87
|
+
reader: 'lib/frqtl/experiments/empty/index.html',
|
|
89
88
|
forward: true
|
|
90
89
|
}),
|
|
91
90
|
Object.freeze({
|
|
@@ -93,33 +92,6 @@ var PAGE_PARAMS = Object.freeze([
|
|
|
93
92
|
reader: 'lib/frqtl/experiments/_shared/boot.js',
|
|
94
93
|
forward: true
|
|
95
94
|
}),
|
|
96
|
-
Object.freeze({
|
|
97
|
-
key: KEYS.STEPPING,
|
|
98
|
-
reader: 'lib/frqtl/experiments/single-source-interference/index.html',
|
|
99
|
-
forward: true
|
|
100
|
-
// Circulation §2: `stepping`
|
|
101
|
-
// selects the WAVE-WALK STEPPING SAMPLER for one universe —
|
|
102
|
-
// 'fixed' (deterministic error-accumulator walk, the engine's serde default)
|
|
103
|
-
// vs 'average' (the historic per-axis Bernoulli walk). It is a PER-UNIVERSE
|
|
104
|
-
// PHYSICS option, so it is read at the PAGE plane and ferried RAW into that
|
|
105
|
-
// page's own `universe.init(..., { stepping })` options — deliberately NOT by
|
|
106
|
-
// the boot module, which selects the BACKEND (`?engine=`, forward:false). Putting
|
|
107
|
-
// a physics knob in the backend selection would conflate the two axes.
|
|
108
|
-
//
|
|
109
|
-
// forward:true — unlike `?engine=`, no host owns a competing stepping authority,
|
|
110
|
-
// and a host embedding this page for an A/B (the GYM Compare shape) forwarding
|
|
111
|
-
// its own `?stepping=` is exactly the intended use (the `?renderer=`/`?seed=`
|
|
112
|
-
// precedent). The value is NEVER validated or defaulted page-side: an absent
|
|
113
|
-
// param means the key is omitted from the init bag so the ENGINE's default
|
|
114
|
-
// (the Rust `Stepping` serde enum, `Fixed`) is the ONE authority for it, and a
|
|
115
|
-
// garbage value rides through to `universe.init()`, which throws naming both
|
|
116
|
-
// legal samplers (one validator, zero second interpreters).
|
|
117
|
-
//
|
|
118
|
-
// SCALING (pre-registered, not built): a PAGE_PARAMS entry names ONE reader.
|
|
119
|
-
// When a SECOND page consumes `?stepping=`, HOIST the read into one shared
|
|
120
|
-
// reader module and repoint this entry — never a second entry for the same
|
|
121
|
-
// key, never a copy-pasted inline read.
|
|
122
|
-
}),
|
|
123
95
|
Object.freeze({
|
|
124
96
|
key: KEYS.ENGINE,
|
|
125
97
|
reader: 'lib/frqtl/experiments/_shared/boot.js',
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* fork.js — THE fork transform: a part file's text becomes the text of its renamed copy, `parts/<To>.js`.
|
|
5
|
+
*
|
|
6
|
+
* forkPartText(text, { from, to, libPath, files }) → { text, imports }
|
|
7
|
+
* text the source part file's exact bytes, as text
|
|
8
|
+
* from the class name the file registers (its kind name)
|
|
9
|
+
* to the new class name (the fork's kind name)
|
|
10
|
+
* libPath where the source lives: a package lib-identity path ('frqtl/parts/Wall.js') or, for a fork of a
|
|
11
|
+
* fork, the project path of the held file ('parts/MyWall.js')
|
|
12
|
+
* files the project's files by path ({} when none) — a fork never overwrites one
|
|
13
|
+
* imports [{ from, to }] — each import specifier of the source and the specifier the copy writes
|
|
14
|
+
*
|
|
15
|
+
* The text is parsed once (parts-writer.js parseModule: the vendored acorn's one importer). The copy differs from the source
|
|
16
|
+
* in exactly three kinds of range, and every other byte (comments, docstrings, the schema, every string) is the
|
|
17
|
+
* source's byte:
|
|
18
|
+
* 1. THE CLASS BINDING. `to` replaces the module-scope binding `from`: its declaration's id and every reference
|
|
19
|
+
* that resolves to it (`registerKind(Wall, …)`, `Wall.prototype…`, `instanceof Wall`, `export { Wall }`). A
|
|
20
|
+
* same-named binding in an inner scope, a property key and a string are untouched.
|
|
21
|
+
* 2. THE PALETTE NAME. The registerKind row's `palette.displayName` string becomes `to`, so a palette lists the
|
|
22
|
+
* fork by its own name. Every other property of the row is the source's byte.
|
|
23
|
+
* 3. THE IMPORTS. A package part's relative import is rewritten to the package specifier naming the SAME file
|
|
24
|
+
* (specifiers.js specifierOfLibPath — a part file under frqtl/parts/ → the parts prefix row; frqtl/objects/
|
|
25
|
+
* kinds.js → @descix/frqtl-web/kinds when every imported name is in KINDS_EXPORTS; anything else its own
|
|
26
|
+
* row). A fork of a fork keeps its relative imports (they name project files beside it) and its package
|
|
27
|
+
* specifiers as they are. A fork never copies a non-part module: a second copy of a registry or a schema
|
|
28
|
+
* would split one fact.
|
|
29
|
+
*
|
|
30
|
+
* REFUSED (throws, naming the part): `to` is not an identifier or is a reserved word; `to` equals `from`; `to` is
|
|
31
|
+
* already a registered kind, or `parts/<to>.js` is already a project file; `from` is a kind FORK_REFUSED names
|
|
32
|
+
* (with its reason); the file declares no module-scope `from`; a relative import with no row (naming the import).
|
|
33
|
+
*
|
|
34
|
+
* FORK_REFUSED { <kind>: reason } — the kinds no fork copies, each with the census reason (fork.test.mjs
|
|
35
|
+
* G-CENSUS derives the set from the part files and holds it equal to this table)
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { kindOf } from '../../frqtl/objects/kinds.js';
|
|
39
|
+
import { KINDS_EXPORTS } from '../../frqtl/objects/kinds-entry.js';
|
|
40
|
+
import { specifierOfLibPath } from './specifiers.js';
|
|
41
|
+
import { isIdentifierName, parseModule } from './parts-writer.js';
|
|
42
|
+
|
|
43
|
+
/** The kinds a fork refuses, each with why (the census: a scope row, or the class name used as a string). */
|
|
44
|
+
export const FORK_REFUSED = Object.freeze({
|
|
45
|
+
LargeObject: 'LargeObject is a scope row (schema/scopes.js SCOPE_DESCRIPTORS) and passes \'LargeObject\' as the engine ' +
|
|
46
|
+
'scope name to universe.updateObjectProperty — a copy would be a second scope row',
|
|
47
|
+
PointSource: 'PointSource numbers its default name with declaredCount(ctx.runner, \'PointSource\') — a copy would ' +
|
|
48
|
+
'number itself in its parent\'s sequence (the class name used as a string)'
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** The lib-identity path of the parts directory, and the kinds registry a part imports. */
|
|
52
|
+
const PARTS_DIR = 'frqtl/parts/';
|
|
53
|
+
const KINDS_LIB = 'frqtl/objects/kinds.js';
|
|
54
|
+
|
|
55
|
+
function _fail(msg) {
|
|
56
|
+
throw new Error('[fork] ' + msg + ' (FAIL-LOUD.)');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The children of an AST node (every key holding a node or an array of nodes). */
|
|
60
|
+
function _children(n) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const k of Object.keys(n)) {
|
|
63
|
+
if (k === 'type' || k === 'start' || k === 'end' || k === 'loc' || k === 'range') { continue; }
|
|
64
|
+
const v = n[k];
|
|
65
|
+
if (Array.isArray(v)) { for (const c of v) { if (c && typeof c.type === 'string') { out.push(c); } } }
|
|
66
|
+
else if (v && typeof v.type === 'string') { out.push(v); }
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _patternNames(p, out) {
|
|
72
|
+
if (!p) { return out; }
|
|
73
|
+
if (p.type === 'Identifier') { out.push(p.name); }
|
|
74
|
+
else if (p.type === 'ObjectPattern') { for (const q of p.properties) { _patternNames(q.type === 'RestElement' ? q.argument : q.value, out); } }
|
|
75
|
+
else if (p.type === 'ArrayPattern') { for (const e of p.elements) { _patternNames(e, out); } }
|
|
76
|
+
else if (p.type === 'RestElement') { _patternNames(p.argument, out); }
|
|
77
|
+
else if (p.type === 'AssignmentPattern') { _patternNames(p.left, out); }
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const _FUNCTION = { FunctionDeclaration: 1, FunctionExpression: 1, ArrowFunctionExpression: 1 };
|
|
82
|
+
|
|
83
|
+
/** `var` names declared in a function body (not crossing a nested function). */
|
|
84
|
+
function _varNames(n, out) {
|
|
85
|
+
for (const c of _children(n)) {
|
|
86
|
+
if (_FUNCTION[c.type]) { continue; }
|
|
87
|
+
if (c.type === 'VariableDeclaration' && c.kind === 'var') {
|
|
88
|
+
for (const d of c.declarations) { _patternNames(d.id, out); }
|
|
89
|
+
}
|
|
90
|
+
_varNames(c, out);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Block-scoped names declared directly in a statement list (let/const/class/function). */
|
|
96
|
+
function _lexicalNames(body, out) {
|
|
97
|
+
for (const s of body) {
|
|
98
|
+
const d = (s.type === 'ExportNamedDeclaration' || s.type === 'ExportDefaultDeclaration') ? s.declaration : s;
|
|
99
|
+
if (!d) { continue; }
|
|
100
|
+
if (d.type === 'VariableDeclaration' && d.kind !== 'var') { for (const x of d.declarations) { _patternNames(x.id, out); } }
|
|
101
|
+
else if ((d.type === 'ClassDeclaration' || d.type === 'FunctionDeclaration') && d.id) { out.push(d.id.name); }
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The names a node introduces into the scope it opens (null when it opens none). */
|
|
107
|
+
function _scopeNames(n, isModule) {
|
|
108
|
+
if (_FUNCTION[n.type]) {
|
|
109
|
+
const names = [];
|
|
110
|
+
for (const p of n.params) { _patternNames(p, names); }
|
|
111
|
+
if (n.type === 'FunctionExpression' && n.id) { names.push(n.id.name); }
|
|
112
|
+
if (n.body.type === 'BlockStatement') { _varNames(n.body, names); _lexicalNames(n.body.body, names); }
|
|
113
|
+
return names;
|
|
114
|
+
}
|
|
115
|
+
if (n.type === 'BlockStatement' || n.type === 'StaticBlock') { return isModule ? null : _lexicalNames(n.body, []); }
|
|
116
|
+
if (n.type === 'SwitchStatement') { return _lexicalNames(n.cases.flatMap(function (c) { return c.consequent; }), []); }
|
|
117
|
+
if (n.type === 'ForStatement' || n.type === 'ForInStatement' || n.type === 'ForOfStatement') {
|
|
118
|
+
const init = n.type === 'ForStatement' ? n.init : n.left;
|
|
119
|
+
if (!init || init.type !== 'VariableDeclaration' || init.kind === 'var') { return []; }
|
|
120
|
+
return init.declarations.reduce(function (acc, d) { return _patternNames(d.id, acc); }, []);
|
|
121
|
+
}
|
|
122
|
+
if (n.type === 'CatchClause') { return n.param ? _patternNames(n.param, []) : []; }
|
|
123
|
+
if (n.type === 'ClassExpression' && n.id) { return [n.id.name]; }
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Every Identifier node that resolves to the MODULE-scope binding `name` (declarations and references).
|
|
129
|
+
* Positions that are not references (non-computed property keys and member properties, labels, an import's
|
|
130
|
+
* imported name, an export's exported name when it differs from the local) are skipped.
|
|
131
|
+
*/
|
|
132
|
+
function _moduleBindingIds(ast, name) {
|
|
133
|
+
const hits = [];
|
|
134
|
+
function visit(n, shadowed, parent, key) {
|
|
135
|
+
if (n.type === 'Identifier') {
|
|
136
|
+
if (n.name !== name || shadowed) { return; }
|
|
137
|
+
if (parent) {
|
|
138
|
+
if (parent.type === 'MemberExpression' && key === 'property' && !parent.computed) { return; }
|
|
139
|
+
if ((parent.type === 'Property' || parent.type === 'MethodDefinition' || parent.type === 'PropertyDefinition') &&
|
|
140
|
+
key === 'key' && !parent.computed && !parent.shorthand) { return; }
|
|
141
|
+
if ((parent.type === 'LabeledStatement' || parent.type === 'BreakStatement' || parent.type === 'ContinueStatement') && key === 'label') { return; }
|
|
142
|
+
// An import's imported name and an export's exported name are names, not references: the local
|
|
143
|
+
// is the binding (acorn hands the SAME node to both keys of a shorthand specifier).
|
|
144
|
+
if (parent.type === 'ImportSpecifier' && key === 'imported') { return; }
|
|
145
|
+
if (parent.type === 'ExportSpecifier' && key === 'exported') { return; }
|
|
146
|
+
}
|
|
147
|
+
hits.push(n);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const names = n === ast ? null : _scopeNames(n, false);
|
|
151
|
+
const sh = shadowed || !!(names && names.indexOf(name) !== -1);
|
|
152
|
+
for (const k of Object.keys(n)) {
|
|
153
|
+
if (k === 'type' || k === 'start' || k === 'end' || k === 'loc') { continue; }
|
|
154
|
+
const v = n[k];
|
|
155
|
+
if (Array.isArray(v)) { for (const c of v) { if (c && typeof c.type === 'string') { visit(c, _childShadow(n, k, c, sh, shadowed), n, k); } } }
|
|
156
|
+
else if (v && typeof v.type === 'string') { visit(v, _childShadow(n, k, v, sh, shadowed), n, k); }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// A function DECLARATION's id belongs to the enclosing scope, not the one the function opens.
|
|
160
|
+
function _childShadow(n, k, c, sh, outer) {
|
|
161
|
+
if (n.type === 'FunctionDeclaration' && k === 'id') { return outer; }
|
|
162
|
+
return sh;
|
|
163
|
+
}
|
|
164
|
+
visit(ast, false, null, null);
|
|
165
|
+
return hits;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The module-top binding of `name`: its declaration node, or null. */
|
|
169
|
+
function _topDeclaration(ast, name) {
|
|
170
|
+
for (const s of ast.body) {
|
|
171
|
+
const d = (s.type === 'ExportNamedDeclaration' || s.type === 'ExportDefaultDeclaration') ? s.declaration : s;
|
|
172
|
+
if (!d) { continue; }
|
|
173
|
+
if ((d.type === 'ClassDeclaration' || d.type === 'FunctionDeclaration') && d.id && d.id.name === name) { return d; }
|
|
174
|
+
if (d.type === 'VariableDeclaration' && d.declarations.some(function (x) { return _patternNames(x.id, []).indexOf(name) !== -1; })) { return d; }
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The `palette.displayName` string Literal of the module's `registerKind(<from>, { … })` row, or null. */
|
|
180
|
+
function _paletteName(ast, from) {
|
|
181
|
+
let found = null;
|
|
182
|
+
(function visit(n) {
|
|
183
|
+
if (found) { return; }
|
|
184
|
+
if (n.type === 'CallExpression' && n.callee.type === 'Identifier' && n.callee.name === 'registerKind' &&
|
|
185
|
+
n.arguments[0] && n.arguments[0].type === 'Identifier' && n.arguments[0].name === from &&
|
|
186
|
+
n.arguments[1] && n.arguments[1].type === 'ObjectExpression') {
|
|
187
|
+
const pal = n.arguments[1].properties.find(function (p) { return p.type === 'Property' && !p.computed && p.key.name === 'palette'; });
|
|
188
|
+
if (pal && pal.value.type === 'ObjectExpression') {
|
|
189
|
+
const dn = pal.value.properties.find(function (p) { return p.type === 'Property' && !p.computed && p.key.name === 'displayName'; });
|
|
190
|
+
if (dn && dn.value.type === 'Literal' && typeof dn.value.value === 'string') { found = dn.value; }
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
for (const c of _children(n)) { visit(c); }
|
|
195
|
+
})(ast);
|
|
196
|
+
return found;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The lib-identity (or project) path a relative specifier names from `fromPath`. */
|
|
200
|
+
function _resolve(fromPath, spec) {
|
|
201
|
+
const segs = fromPath.split('/');
|
|
202
|
+
segs.pop();
|
|
203
|
+
for (const s of spec.split('/')) {
|
|
204
|
+
if (s === '.' || s === '') { continue; }
|
|
205
|
+
if (s === '..') { segs.pop(); } else { segs.push(s); }
|
|
206
|
+
}
|
|
207
|
+
return segs.join('/');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function _quoteLike(original, value) {
|
|
211
|
+
const q = original[0] === '"' ? '"' : '\'';
|
|
212
|
+
return q + value.replace(/\\/g, '\\\\').split(q).join('\\' + q) + q;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {string} text — the source part file
|
|
217
|
+
* @param {{from: string, to: string, libPath: string, files: Object<string, string>}} opts
|
|
218
|
+
* @returns {{text: string, imports: Array<{from: string, to: string}>}}
|
|
219
|
+
*/
|
|
220
|
+
export function forkPartText(text, opts) {
|
|
221
|
+
if (typeof text !== 'string' || !text) { _fail('forkPartText(text, opts): text is the part file (got ' + typeof text + ').'); }
|
|
222
|
+
const o = opts || {};
|
|
223
|
+
const from = o.from, to = o.to, libPath = o.libPath, files = o.files;
|
|
224
|
+
if (typeof from !== 'string' || !from) { _fail('forkPartText: `from` is the class name the part registers.'); }
|
|
225
|
+
if (typeof libPath !== 'string' || !libPath) { _fail('forkPartText(' + from + '): `libPath` names the source file.'); }
|
|
226
|
+
if (!files || typeof files !== 'object') { _fail('forkPartText(' + from + '): `files` is the project\'s file map ({} when none).'); }
|
|
227
|
+
if (typeof to !== 'string' || !isIdentifierName(to)) {
|
|
228
|
+
_fail('forkPartText(' + from + '): the new class name ' + JSON.stringify(to) + ' is not an identifier (or is a reserved word) ' +
|
|
229
|
+
'— the class name IS the kind name.');
|
|
230
|
+
}
|
|
231
|
+
if (to === from) { _fail('forkPartText(' + from + '): the new class name equals the part\'s own — a fork is a NEW kind; name it.'); }
|
|
232
|
+
if (kindOf(to)) {
|
|
233
|
+
_fail('forkPartText(' + from + '): "' + to + '" is already a registered kind (' + kindOf(to).from + ') — one name has one owner; ' +
|
|
234
|
+
'choose another name.');
|
|
235
|
+
}
|
|
236
|
+
const target = 'parts/' + to + '.js';
|
|
237
|
+
if (Object.prototype.hasOwnProperty.call(files, target)) {
|
|
238
|
+
_fail('forkPartText(' + from + '): ' + target + ' is already a project file — a fork never overwrites one.');
|
|
239
|
+
}
|
|
240
|
+
if (Object.prototype.hasOwnProperty.call(FORK_REFUSED, from)) {
|
|
241
|
+
_fail('forkPartText(' + from + '): ' + from + ' is not forkable — ' + FORK_REFUSED[from] + '.');
|
|
242
|
+
}
|
|
243
|
+
const pkg = libPath.indexOf(PARTS_DIR) === 0;
|
|
244
|
+
let ast;
|
|
245
|
+
try { ast = parseModule(text); }
|
|
246
|
+
catch (err) { _fail('forkPartText(' + from + '): ' + libPath + ' does not parse (' + err.message + ').'); }
|
|
247
|
+
if (!_topDeclaration(ast, from)) {
|
|
248
|
+
_fail('forkPartText(' + from + '): ' + libPath + ' declares no module-scope ' + from + ' — the file a kind\'s `from` names declares its class.');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const patches = [];
|
|
252
|
+
const imports = [];
|
|
253
|
+
for (const s of ast.body) {
|
|
254
|
+
const src = (s.type === 'ImportDeclaration' || ((s.type === 'ExportNamedDeclaration' || s.type === 'ExportAllDeclaration') && s.source))
|
|
255
|
+
? s.source : null;
|
|
256
|
+
if (!src) { continue; }
|
|
257
|
+
const spec = src.value;
|
|
258
|
+
if (!(spec.startsWith('./') || spec.startsWith('../'))) { imports.push({ from: spec, to: spec }); continue; }
|
|
259
|
+
if (!pkg) { imports.push({ from: spec, to: spec }); continue; } // a fork of a fork: project files beside it
|
|
260
|
+
const lp = _resolve(libPath, spec);
|
|
261
|
+
let out;
|
|
262
|
+
if (lp === KINDS_LIB) {
|
|
263
|
+
const names = (s.specifiers || []).map(function (x) {
|
|
264
|
+
return x.type === 'ImportSpecifier' ? (x.imported.name || x.imported.value) : '*';
|
|
265
|
+
});
|
|
266
|
+
const outside = names.filter(function (n) { return KINDS_EXPORTS.indexOf(n) === -1; });
|
|
267
|
+
if (outside.length) {
|
|
268
|
+
_fail('forkPartText(' + from + '): ' + libPath + ' imports ' + outside.join(', ') + ' from "' + spec + '" — the kinds ' +
|
|
269
|
+
'door (@descix/frqtl-web/kinds) publishes ' + KINDS_EXPORTS.join(', ') + ' only.');
|
|
270
|
+
}
|
|
271
|
+
out = specifierOfLibPath('frqtl/objects/kinds-entry.js');
|
|
272
|
+
} else {
|
|
273
|
+
out = specifierOfLibPath(lp);
|
|
274
|
+
}
|
|
275
|
+
if (!out) {
|
|
276
|
+
_fail('forkPartText(' + from + '): ' + libPath + ' imports "' + spec + '" (' + lp + '), which no specifier row names — a ' +
|
|
277
|
+
'fork imports every non-part module from the package (lib/shim/project/specifiers.js); it never copies one.');
|
|
278
|
+
}
|
|
279
|
+
patches.push({ start: src.start, end: src.end, text: _quoteLike(text.slice(src.start, src.end), out) });
|
|
280
|
+
imports.push({ from: spec, to: out });
|
|
281
|
+
}
|
|
282
|
+
for (const id of _moduleBindingIds(ast, from)) { patches.push({ start: id.start, end: id.end, text: to }); }
|
|
283
|
+
const dn = _paletteName(ast, from);
|
|
284
|
+
if (dn) { patches.push({ start: dn.start, end: dn.end, text: _quoteLike(text.slice(dn.start, dn.end), to) }); }
|
|
285
|
+
|
|
286
|
+
patches.sort(function (a, b) { return b.start - a.start; });
|
|
287
|
+
for (let i = 1; i < patches.length; i++) {
|
|
288
|
+
if (patches[i].end > patches[i - 1].start) { _fail('forkPartText(' + from + '): overlapping rewrites — a bug in the transform.'); }
|
|
289
|
+
}
|
|
290
|
+
let out = text;
|
|
291
|
+
for (const p of patches) { out = out.slice(0, p.start) + p.text + out.slice(p.end); }
|
|
292
|
+
return { text: out, imports: imports };
|
|
293
|
+
}
|