@lampdotdev/lamp 0.35.0 → 0.36.2-nightly.17
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/README.md +51 -11
- package/Resources/GameKit/apple/LampPage.swift +202 -35
- package/Resources/GameKit/apple/LampWebGame.swift +1 -1
- package/Resources/GameKit/page/lamp-live.js +672 -0
- package/Resources/GameKit/web/lamp-game.js +7 -1
- package/Resources/share-rules.json +64 -0
- package/Resources/skills/game/SKILL.md +2 -0
- package/Resources/skills/github-projects/SKILL.md +8 -5
- package/Resources/skills/web-app/SKILL.md +49 -15
- package/Resources/skills/web-or-native/SKILL.md +6 -1
- package/cli/ai.mjs +237 -0
- package/cli/app-ai.mjs +473 -0
- package/cli/browser.mjs +18 -2
- package/cli/calls.mjs +213 -9
- package/cli/dev.mjs +61 -14
- package/cli/lamp.mjs +347 -37
- package/cli/lampapp.mjs +9 -9
- package/cli/lamps/android-host/LampPageGateway.kt +2 -2
- package/cli/lamps/lamp-for-games.js +95 -36
- package/cli/lamps/lamp.js +95 -36
- package/cli/lamps/mac-starter/.lamp/meta.json +1 -1
- package/cli/lamps/mac-starter/Sources/LampSupport.swift +403 -285
- package/cli/lamps/web-starter/lamp.js +95 -36
- package/cli/link.mjs +77 -13
- package/cli/local-ai.mjs +193 -0
- package/cli/openrouter.mjs +133 -0
- package/cli/outside.mjs +65 -2
- package/cli/page.js +160 -8
- package/cli/share.mjs +80 -12
- package/cli/skill/SKILL.md +35 -10
- package/cli/state.mjs +3 -0
- package/cli/update.mjs +117 -12
- package/package.json +1 -1
- package/site/lib/shared-wishes.js +848 -117
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
// Lamp's changes for a web game whose page doesn't run on Lamp's game runtime (lamp-game.js), put in while it's played.
|
|
2
|
+
// The app around a game's page (LampPage.swift) and lamp dev (cli/page.js) give it to the page before any script of
|
|
3
|
+
// the page's own; a web app's or a screen's page goes without it and opens again for each change, as ever.
|
|
4
|
+
//
|
|
5
|
+
// A game that runs on the runtime never uses any of this: lamp-game.js sets `window.__lampLive` as it loads, and
|
|
6
|
+
// what was handed here before it did (a change saved while the page opened) is handed on to it, in order, generations
|
|
7
|
+
// and all.
|
|
8
|
+
//
|
|
9
|
+
// What goes into the running game, with every object it made and its fields where they are:
|
|
10
|
+
// - a stylesheet the page links: the new one replaces it;
|
|
11
|
+
// - a change inside one method of a class a module of the game exports (a getter, setter or static method too), when
|
|
12
|
+
// the new method uses nothing of its module but what the module imports and constants written as a plain number,
|
|
13
|
+
// string, true, false or null: the method is made again from those alone (the module itself never runs again, so
|
|
14
|
+
// nothing at its top level happens twice) and put on the running class, so every object made from it runs the new
|
|
15
|
+
// code from its next call.
|
|
16
|
+
// Anything else waits for Reload, and Lamp is told what and why: the page, a script that isn't a module, a constructor,
|
|
17
|
+
// a field, a new method or class, code outside a class, a method that uses `super`, `import()` or `import.meta` (each read
|
|
18
|
+
// from where the code is), a private member (#name) or anything at its module's top level (its own copy, which the running game holds, would be another), two
|
|
19
|
+
// methods changed at once, and any file the game read or names. A file the game never read and nothing it runs names
|
|
20
|
+
// is no part of the change: the app around the page says with each change every file it served the page (`read`).
|
|
21
|
+
//
|
|
22
|
+
// The game is started before anything goes in: the page has loaded, without an error as it started, and the sources
|
|
23
|
+
// of its modules have been read as they ran. A game that takes longer to be ready says so with
|
|
24
|
+
// `lampLive.waitForReady()` as it starts and `lampLive.ready()` once it is; until then a change waits, and goes in then.
|
|
25
|
+
// A page that stopped with an error as it started has nothing to keep: a change opens it again.
|
|
26
|
+
(() => {
|
|
27
|
+
if (window.__lampLiveHelper) return;
|
|
28
|
+
window.__lampLiveHelper = true;
|
|
29
|
+
|
|
30
|
+
// The browser's own, before the page's code can change them.
|
|
31
|
+
const read = window.fetch.bind(window), later = window.setTimeout.bind(window);
|
|
32
|
+
const tell = (gen, state, why, more) => {
|
|
33
|
+
try {
|
|
34
|
+
const h = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.lampLive;
|
|
35
|
+
if (h) h.postMessage({ gen, state, ...(why ? { why } : {}), ...more });
|
|
36
|
+
} catch (e) {}
|
|
37
|
+
};
|
|
38
|
+
const log = text => { try { console.log(text); } catch (e) {} };
|
|
39
|
+
try { performance.setResourceTimingBufferSize(100000); } catch (e) {}
|
|
40
|
+
|
|
41
|
+
const clean = path => String(path).replace(/^\.?\//, '').split(/[?#]/)[0];
|
|
42
|
+
const pageBase = () => new URL('.', location.href.split(/[?#]/)[0]);
|
|
43
|
+
const pathOf = url => {
|
|
44
|
+
try {
|
|
45
|
+
const u = new URL(url, location.href), base = pageBase();
|
|
46
|
+
if (u.origin !== base.origin || !u.pathname.startsWith(base.pathname)) return null;
|
|
47
|
+
return decodeURIComponent(u.pathname.slice(base.pathname.length));
|
|
48
|
+
} catch (e) { return null; }
|
|
49
|
+
};
|
|
50
|
+
const addressOf = path => new URL(path, pageBase()).href;
|
|
51
|
+
const pagePath = () => pathOf(location.href.split(/[?#]/)[0]) || 'index.html';
|
|
52
|
+
// Lamp's own files beside the page (the runtime, three.js, which is large) aren't read for the game's modules; a change
|
|
53
|
+
// to one the page uses still waits for Reload.
|
|
54
|
+
const lampOwn = path => /^(three|rapier|phaser)\//.test(path) || /^lamp(-[\w-]+)?\.js$/.test(path);
|
|
55
|
+
|
|
56
|
+
// MARK: Is the game started
|
|
57
|
+
|
|
58
|
+
let ready = false, loaded = false, waitAsked = false, readyCalled = false, broken = null;
|
|
59
|
+
window.addEventListener('error', e => {
|
|
60
|
+
if (ready || broken) return;
|
|
61
|
+
// A script of the page's that didn't load, or threw as it ran: the game never started as it should.
|
|
62
|
+
if (e instanceof ErrorEvent) broken = e.message || 'an error';
|
|
63
|
+
else if (e.target && e.target.tagName === 'SCRIPT') broken = `${pathOf(e.target.src) || 'a script'} didn't load`;
|
|
64
|
+
}, true);
|
|
65
|
+
window.addEventListener('unhandledrejection', e => {
|
|
66
|
+
if (!ready && !broken) broken = String(e.reason && e.reason.message || e.reason);
|
|
67
|
+
});
|
|
68
|
+
window.lampLive = Object.freeze({
|
|
69
|
+
waitForReady() { if (!ready) waitAsked = true; },
|
|
70
|
+
ready() { readyCalled = true; settle(); },
|
|
71
|
+
});
|
|
72
|
+
window.addEventListener('load', () => { loaded = true; later(settle, 0); }, { once: true });
|
|
73
|
+
|
|
74
|
+
// MARK: What the page runs, as it started
|
|
75
|
+
|
|
76
|
+
const texts = new Map(); // a module's or stylesheet's address → its source as the page runs it
|
|
77
|
+
const graph = new Set(); // the addresses of the game's modules, from the page's module scripts down
|
|
78
|
+
let pageText = '', sourcesRead = false, sourcesOk = false;
|
|
79
|
+
// Files changed before their sources were read: what the page runs of them can't be told from what's new.
|
|
80
|
+
const unsure = new Set();
|
|
81
|
+
|
|
82
|
+
async function readText(url) {
|
|
83
|
+
const r = await read(url, { cache: 'no-store' });
|
|
84
|
+
if (!r.ok) throw new Error(`${pathOf(url) || url} couldn't be read (${r.status})`);
|
|
85
|
+
return r.text();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let importMap = null;
|
|
89
|
+
function resolve(specifier, from) {
|
|
90
|
+
if (/^\.{0,2}\//.test(specifier)) return new URL(specifier, from).href;
|
|
91
|
+
if (/^[a-z][\w+.-]*:/i.test(specifier)) return specifier;
|
|
92
|
+
if (!importMap) {
|
|
93
|
+
importMap = {};
|
|
94
|
+
for (const s of document.querySelectorAll('script[type="importmap"]')) { try { Object.assign(importMap, JSON.parse(s.textContent).imports || {}); } catch (e) {} }
|
|
95
|
+
}
|
|
96
|
+
if (importMap[specifier]) return new URL(importMap[specifier], location.href).href;
|
|
97
|
+
const prefix = Object.keys(importMap).filter(k => k.endsWith('/') && specifier.startsWith(k)).sort((a, b) => b.length - a.length)[0];
|
|
98
|
+
return prefix ? new URL(importMap[prefix] + specifier.slice(prefix.length), location.href).href : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function readSources() {
|
|
102
|
+
pageText = document.documentElement ? document.documentElement.outerHTML : '';
|
|
103
|
+
const visit = async url => {
|
|
104
|
+
const path = pathOf(url);
|
|
105
|
+
if (graph.has(url) || path == null || lampOwn(path)) return;
|
|
106
|
+
graph.add(url);
|
|
107
|
+
const text = await readText(url);
|
|
108
|
+
texts.set(url, text);
|
|
109
|
+
await Promise.all(importsOf(text).map(i => resolve(i.specifier, url)).filter(Boolean).map(visit));
|
|
110
|
+
};
|
|
111
|
+
const scripts = [...document.querySelectorAll('script[type="module"]')];
|
|
112
|
+
// A module script written in the page: what it imports is the game's too.
|
|
113
|
+
const roots = scripts.flatMap(s => s.getAttribute('src') ? [new URL(s.getAttribute('src'), location.href).href]
|
|
114
|
+
: importsOf(s.textContent).map(i => resolve(i.specifier, location.href)).filter(Boolean));
|
|
115
|
+
const styles = [...document.querySelectorAll('link[rel~="stylesheet"]')].map(l => l.href).filter(h => pathOf(h) != null);
|
|
116
|
+
await Promise.all([...roots.map(visit), ...styles.map(async url => { try { texts.set(url, await readText(url)); } catch (e) {} })]);
|
|
117
|
+
}
|
|
118
|
+
// Read as soon as the page's own scripts are known, while they're the ones it runs.
|
|
119
|
+
const readAtStart = () => readSources().then(() => { sourcesOk = true; }, e => log(`Lamp: the game's code couldn't be read as it started (${e && e.message || e}).`))
|
|
120
|
+
.finally(() => { sourcesRead = true; settle(); });
|
|
121
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', readAtStart, { once: true });
|
|
122
|
+
else later(readAtStart, 0);
|
|
123
|
+
|
|
124
|
+
function settle() {
|
|
125
|
+
if (ready || runtime || !loaded || !sourcesRead || (waitAsked && !readyCalled)) return;
|
|
126
|
+
ready = true;
|
|
127
|
+
run();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// MARK: Changes
|
|
131
|
+
|
|
132
|
+
let runtime = null; // lamp-game.js's own, once it takes over
|
|
133
|
+
const pending = []; // changes handed here and not yet put in, in order
|
|
134
|
+
let held = null; // what waits for Reload: { gen, changed, removed, why }
|
|
135
|
+
let active = null; // the change being put in now, until it's in or held
|
|
136
|
+
let queue = Promise.resolve(), pill = null;
|
|
137
|
+
// Thrown out of a change being put in once the runtime has taken over, which is handed that change instead: from
|
|
138
|
+
// there nothing more of it is put in here, nor said.
|
|
139
|
+
const HANDED = new Error('handed to the game runtime');
|
|
140
|
+
const stillMine = () => { if (runtime) throw HANDED; };
|
|
141
|
+
|
|
142
|
+
const take = change => {
|
|
143
|
+
if (runtime) return runtime(change);
|
|
144
|
+
// The person's Reload goes in at once, whatever waits: nothing queued here is worth keeping over it.
|
|
145
|
+
if (change.now) { reopen(change, 'Reload'); return true; }
|
|
146
|
+
if (!sourcesRead) for (const path of [...(change.changed || []), ...(change.removed || [])]) unsure.add(clean(path));
|
|
147
|
+
pending.push(change);
|
|
148
|
+
// Not started yet: it goes in once the game is, by itself.
|
|
149
|
+
if (!ready && !change.now && !broken) tell(change.gen || 0, 'held', 'the game is still starting', { pause: true });
|
|
150
|
+
run();
|
|
151
|
+
return true;
|
|
152
|
+
};
|
|
153
|
+
take.lampHelper = true;
|
|
154
|
+
// The game runtime, as it loads, takes every change from here on, and those handed here meanwhile.
|
|
155
|
+
Object.defineProperty(window, '__lampLive', {
|
|
156
|
+
configurable: true, enumerable: true,
|
|
157
|
+
get: () => runtime || take,
|
|
158
|
+
set: given => {
|
|
159
|
+
if (given === take || typeof given !== 'function') return;
|
|
160
|
+
runtime = given;
|
|
161
|
+
hidePill();
|
|
162
|
+
// What waits for Reload, then the change being put in (it stops at its next step: `stillMine`), then those
|
|
163
|
+
// not begun, each with its own generation.
|
|
164
|
+
const handed = [];
|
|
165
|
+
if (held) handed.push({ gen: held.gen, changed: [...held.changed], removed: [...held.removed], now: false, pill: held.pill });
|
|
166
|
+
if (active) handed.push(active);
|
|
167
|
+
handed.push(...pending.splice(0));
|
|
168
|
+
held = null;
|
|
169
|
+
active = null;
|
|
170
|
+
for (const change of handed) { try { runtime(change); } catch (e) { log(`Lamp: ${e && e.message || e}`); } }
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
function run() {
|
|
175
|
+
queue = queue.then(async () => {
|
|
176
|
+
while (pending.length && !runtime) {
|
|
177
|
+
const change = pending[0];
|
|
178
|
+
if (broken) return reopen(change, `the game stopped with an error as it started (${broken})`);
|
|
179
|
+
if (!ready) return;
|
|
180
|
+
active = pending.shift();
|
|
181
|
+
await put(active);
|
|
182
|
+
active = null;
|
|
183
|
+
}
|
|
184
|
+
}).catch(e => { if (e !== HANDED) log(`Lamp: ${e && e.message || e}`); });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function reopen(change, why) {
|
|
188
|
+
pending.length = 0;
|
|
189
|
+
tell(change.gen || 0, 'reloading', why);
|
|
190
|
+
location.reload();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function put(change) {
|
|
194
|
+
const gen = change.gen || 0, went = [], waits = [];
|
|
195
|
+
const hold = (path, why) => waits.push({ path, why });
|
|
196
|
+
for (const raw of change.changed || []) {
|
|
197
|
+
const path = clean(raw), url = addressOf(path);
|
|
198
|
+
try {
|
|
199
|
+
if (path === pagePath()) hold(path, `${path} is the page itself`);
|
|
200
|
+
else if (graph.has(url)) {
|
|
201
|
+
const r = await swapModule(url, path);
|
|
202
|
+
stillMine();
|
|
203
|
+
if (typeof r === 'string') hold(path, r);
|
|
204
|
+
else if (r) went.push(r.went);
|
|
205
|
+
} else if (/\.css$/i.test(path) && linksTo(path).length) {
|
|
206
|
+
await swapStyle(path, gen);
|
|
207
|
+
stillMine();
|
|
208
|
+
went.push(path);
|
|
209
|
+
} else if (!unused(path, change.read)) {
|
|
210
|
+
hold(path, /\.m?js$/i.test(path) ? `${path} is code the game runs that isn't one of its modules` : `the game uses ${path}`);
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (e === HANDED) throw e;
|
|
214
|
+
hold(path, `${path} couldn't be put in (${e && e.message || e})`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const raw of change.removed || []) {
|
|
218
|
+
const path = clean(raw);
|
|
219
|
+
if (!unused(path, change.read)) hold(path, `${path}, which the game uses, was taken away`);
|
|
220
|
+
}
|
|
221
|
+
if (went.length) log(`Lamp: put in ${went.join('; ')} while the game ran.`);
|
|
222
|
+
if (waits.length) {
|
|
223
|
+
held = held || { gen, changed: new Set(), removed: new Set(), why: [], pill: false };
|
|
224
|
+
held.gen = gen;
|
|
225
|
+
held.pill = held.pill || !!change.pill;
|
|
226
|
+
for (const w of waits) held.why.push(w.why);
|
|
227
|
+
for (const raw of change.changed || []) held.changed.add(clean(raw));
|
|
228
|
+
for (const raw of change.removed || []) held.removed.add(clean(raw));
|
|
229
|
+
} else if (held) held.gen = gen;
|
|
230
|
+
if (held) {
|
|
231
|
+
const why = [...new Set(held.why)].join('; ') + (went.length ? `. Put in meanwhile: ${went.join('; ')}` : '');
|
|
232
|
+
log(`Lamp: a change waits for Reload: ${why}.`);
|
|
233
|
+
tell(gen, 'held', why);
|
|
234
|
+
if (held.pill) showPill();
|
|
235
|
+
} else tell(gen, 'swapped');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// A file the game has no use for: the page never asked for it (`read`, every file the app around it served the
|
|
239
|
+
// page, which the page can't see for itself), and nothing of the page's names it. Anything else may be in use, and
|
|
240
|
+
// so is everything when the host doesn't say what it served.
|
|
241
|
+
function unused(path, read) {
|
|
242
|
+
if (!sourcesOk || !Array.isArray(read) || read.map(clean).includes(path)) return false;
|
|
243
|
+
try { if (performance.getEntriesByType('resource').some(e => pathOf(e.name) === path)) return false; } catch (e) {}
|
|
244
|
+
const name = path.split('/').pop();
|
|
245
|
+
const named = new RegExp(`(^|[^\\w.-])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^\\w-])`);
|
|
246
|
+
return ![pageText, ...texts.values()].some(text => named.test(text));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function linksTo(path) {
|
|
250
|
+
return [...document.querySelectorAll('link[rel~="stylesheet"]')].filter(l => pathOf(l.href.split(/[?#]/)[0]) === path);
|
|
251
|
+
}
|
|
252
|
+
async function swapStyle(path, gen) {
|
|
253
|
+
await Promise.all(linksTo(path).map(old => new Promise((done, failed) => {
|
|
254
|
+
const next = old.cloneNode();
|
|
255
|
+
next.href = `${addressOf(path)}?lamp=${gen}`;
|
|
256
|
+
next.onload = () => { old.remove(); done(); };
|
|
257
|
+
next.onerror = () => { next.remove(); failed(new Error(`${path} didn't load`)); };
|
|
258
|
+
old.after(next);
|
|
259
|
+
})));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function showPill() {
|
|
263
|
+
if (pill) { pill.style.display = 'block'; return; }
|
|
264
|
+
if (!document.body) return;
|
|
265
|
+
pill = document.createElement('button');
|
|
266
|
+
pill.textContent = 'Reload to apply';
|
|
267
|
+
pill.style.cssText = 'position:fixed;z-index:2147483646;left:50%;transform:translateX(-50%);bottom:calc(14px + env(safe-area-inset-bottom));'
|
|
268
|
+
+ 'padding:8px 16px;border:0;border-radius:18px;background:rgba(20,20,24,.72);color:#fff;font:600 15px -apple-system,system-ui,sans-serif;'
|
|
269
|
+
+ '-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)';
|
|
270
|
+
pill.addEventListener('pointerdown', e => { e.stopPropagation(); e.preventDefault(); reopen({ gen: held ? held.gen : 0 }, 'Reload'); });
|
|
271
|
+
document.body.append(pill);
|
|
272
|
+
}
|
|
273
|
+
function hidePill() { if (pill) pill.style.display = 'none'; }
|
|
274
|
+
|
|
275
|
+
// MARK: A method made again
|
|
276
|
+
|
|
277
|
+
const source = f => Function.prototype.toString.call(f);
|
|
278
|
+
// A method put in here, by its source as its module writes it: its own reads `lampMethod() {…}` (below).
|
|
279
|
+
const written = new WeakMap();
|
|
280
|
+
const sourceOf = f => written.get(f) ?? source(f);
|
|
281
|
+
const isClass = v => typeof v === 'function' && /^class\b/.test(source(v));
|
|
282
|
+
|
|
283
|
+
// Every method, accessor and static method of a class, by where it's kept.
|
|
284
|
+
function methodsOf(C) {
|
|
285
|
+
const out = [];
|
|
286
|
+
for (const [target, skip] of [[C.prototype, ['constructor']], [C, ['length', 'name', 'prototype']]]) {
|
|
287
|
+
for (const key of Reflect.ownKeys(target)) {
|
|
288
|
+
if (skip.includes(key)) continue;
|
|
289
|
+
const d = Object.getOwnPropertyDescriptor(target, key);
|
|
290
|
+
for (const kind of ['value', 'get', 'set']) if (typeof d[kind] === 'function' && !isClass(d[kind])) out.push({ C, target, key, kind, fn: d[kind] });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// A module whose change is inside one method of a class it exports: that method, made again, goes on the running
|
|
297
|
+
// class. Returns what went in, null for no change, or why it waits.
|
|
298
|
+
async function swapModule(url, path) {
|
|
299
|
+
if (unsure.has(path)) return `${path} changed while the game was starting, so what it runs of it isn't known`;
|
|
300
|
+
const old = texts.get(url);
|
|
301
|
+
if (old == null) return `${path} couldn't be read as the game started`;
|
|
302
|
+
const now = await readText(url);
|
|
303
|
+
stillMine();
|
|
304
|
+
if (now === old) return null;
|
|
305
|
+
// The module the game runs: the same one every import of it has, so it doesn't run again.
|
|
306
|
+
const ns = await import(url);
|
|
307
|
+
stillMine();
|
|
308
|
+
let a = 0;
|
|
309
|
+
while (a < old.length && a < now.length && old[a] === now[a]) a++;
|
|
310
|
+
let b = 0;
|
|
311
|
+
while (b < old.length - a && b < now.length - a && old[old.length - 1 - b] === now[now.length - 1 - b]) b++;
|
|
312
|
+
const from = a, to = old.length - b;
|
|
313
|
+
let found = null;
|
|
314
|
+
for (const C of new Set(Object.values(ns).filter(isClass))) {
|
|
315
|
+
for (const m of methodsOf(C)) {
|
|
316
|
+
const text = sourceOf(m.fn), at = old.indexOf(text);
|
|
317
|
+
if (at < 0) continue;
|
|
318
|
+
const head = header(tokens(text));
|
|
319
|
+
if (!head || from < at + head.body + 1 || to > at + text.length - 1) continue;
|
|
320
|
+
if (old.indexOf(text, at + 1) >= 0) return `${path} has more than one method written as ${C.name}.${String(m.key)} is`;
|
|
321
|
+
found = { ...m, at, text };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!found) return `${path} changed outside the body of one method of a class it exports (a constructor, a field, a new method, code outside a class, or more than one method)`;
|
|
325
|
+
const name = `${found.C.name}.${String(found.key)}`;
|
|
326
|
+
const fresh = now.slice(found.at, found.at + found.text.length + now.length - old.length);
|
|
327
|
+
// Read as code, so a color in a string ('#ffffff') isn't taken for one.
|
|
328
|
+
const scan = tokens(fresh), head = header(scan);
|
|
329
|
+
if (!head) return `${name} can't be read as a method`;
|
|
330
|
+
const body = scan.filter(t => t.at > head.body);
|
|
331
|
+
if (body.some(t => t.t === 'priv')) return `${name} uses a private member (#name), which only a fresh start can give a new method`;
|
|
332
|
+
if (scan.some(t => t.v === 'super')) return `${name} uses super`;
|
|
333
|
+
// Both are read from where the code is: the new method's would be read from somewhere other than its module.
|
|
334
|
+
const importAt = scan.findIndex((t, i) => t.v === 'import' && scan[i + 1] && (scan[i + 1].v === '(' || scan[i + 1].v === '.'));
|
|
335
|
+
if (importAt >= 0) return `${name} uses ${scan[importAt + 1].v === '(' ? 'import()' : 'import.meta'}, which a new method would read from somewhere other than ${path}`;
|
|
336
|
+
const top = topLevel(now);
|
|
337
|
+
const { free, declared } = freeNames(scan, head.params);
|
|
338
|
+
const both = [...declared].filter(n => top.declared.has(n) || top.imports.has(n));
|
|
339
|
+
if (both.length) return `${name} declares ${both.join(', ')}, a name its module has too, so which one it means can't be told`;
|
|
340
|
+
const kept = free.filter(n => top.declared.has(n) && !top.constants.has(n));
|
|
341
|
+
if (kept.length) return `${name} uses ${kept.join(', ')} from ${path}'s top level, which the running game has its own copy of`;
|
|
342
|
+
const unknown = free.filter(n => !top.imports.has(n) && !top.constants.has(n) && !(n in globalThis));
|
|
343
|
+
if (unknown.length) return `${name} uses ${unknown.join(', ')}, which Lamp can't tell is safe to give a new method`;
|
|
344
|
+
// The method again, beside the module's imports (the same modules the game runs) and the constants it reads.
|
|
345
|
+
const lines = [];
|
|
346
|
+
for (const i of top.importList) if (free.some(n => i.names.includes(n))) {
|
|
347
|
+
const target = resolve(i.specifier, url);
|
|
348
|
+
if (!target) return `${name} uses ${i.names.join(', ')}, imported from ${i.specifier}, which nothing resolves`;
|
|
349
|
+
lines.push(`${i.clause.length ? `import ${i.clause} from` : 'import'} ${JSON.stringify(target)};`);
|
|
350
|
+
}
|
|
351
|
+
for (const [n, text] of top.constants) if (free.includes(n)) lines.push(text);
|
|
352
|
+
// Under a name of its own: its key, unchanged (the change is inside its body), is never worked out again, so a
|
|
353
|
+
// computed key's expression doesn't run a second time. It keeps the name the running one has.
|
|
354
|
+
const unnamed = fresh.slice(0, head.key) + 'lampMethod' + fresh.slice(head.keyEnd);
|
|
355
|
+
lines.push(`export default ({\n${unnamed}\n});`);
|
|
356
|
+
const blob = URL.createObjectURL(new Blob([lines.join('\n')], { type: 'text/javascript' }));
|
|
357
|
+
let made;
|
|
358
|
+
try { made = (await import(blob)).default; }
|
|
359
|
+
catch (e) { stillMine(); return `the new ${name} doesn't run (${e && e.message || e})`; }
|
|
360
|
+
finally { URL.revokeObjectURL(blob); }
|
|
361
|
+
stillMine();
|
|
362
|
+
const d = Object.getOwnPropertyDescriptor(made, 'lampMethod'), was = Object.getOwnPropertyDescriptor(found.target, found.key);
|
|
363
|
+
if (!d || typeof d[found.kind] !== 'function') return `${name} is another kind of member now`;
|
|
364
|
+
try { Object.defineProperty(d[found.kind], 'name', { value: found.fn.name, configurable: true }); } catch (e) {}
|
|
365
|
+
written.set(d[found.kind], fresh);
|
|
366
|
+
Object.defineProperty(found.target, found.key, { ...was, [found.kind]: d[found.kind] });
|
|
367
|
+
texts.set(url, now);
|
|
368
|
+
return { went: `${path} (${name})` };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// A method as its source reads (`async *[key](…) {`, `get name() {`): where its key starts and ends in the text, the
|
|
372
|
+
// token its parameters open with, and where its body opens. Its modifiers are those words only when a key follows
|
|
373
|
+
// them (`get() {}` is a method named get). Null for anything else.
|
|
374
|
+
function header(scan) {
|
|
375
|
+
let i = 0;
|
|
376
|
+
const keyNext = j => scan[j] && scan[j].v !== '(';
|
|
377
|
+
while (scan[i] && ((scan[i].t === 'id' && ['async', 'get', 'set'].includes(scan[i].v)) || scan[i].v === '*') && keyNext(i + 1)) i++;
|
|
378
|
+
const key = scan[i];
|
|
379
|
+
if (!key) return null;
|
|
380
|
+
let end = i;
|
|
381
|
+
if (key.v === '[') {
|
|
382
|
+
let depth = 0;
|
|
383
|
+
for (; end < scan.length; end++) {
|
|
384
|
+
if (['(', '[', '{'].includes(scan[end].v)) depth++;
|
|
385
|
+
else if ([')', ']', '}'].includes(scan[end].v) && --depth === 0) break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const params = end + 1;
|
|
389
|
+
if (!scan[params] || scan[params].v !== '(') return null;
|
|
390
|
+
let depth = 0;
|
|
391
|
+
for (let j = params; j < scan.length; j++) {
|
|
392
|
+
if (scan[j].v === '(') depth++;
|
|
393
|
+
else if (scan[j].v === ')' && --depth === 0) {
|
|
394
|
+
if (!scan[j + 1] || scan[j + 1].v !== '{') return null;
|
|
395
|
+
return { key: key.at, keyEnd: scan[params].at, params, body: scan[j + 1].at };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// MARK: Reading code
|
|
402
|
+
|
|
403
|
+
// JavaScript as tokens: names, punctuation (with `...` and `?.` as one), and strings, numbers, templates and regular
|
|
404
|
+
// expressions as `lit`, with what a template's `${}` holds read as code. Comments go.
|
|
405
|
+
const KEYWORDS_BEFORE_REGEX = new Set(['return', 'typeof', 'case', 'do', 'else', 'in', 'of', 'new', 'delete', 'void', 'throw', 'yield', 'await', 'instanceof']);
|
|
406
|
+
function tokens(text) {
|
|
407
|
+
const out = [], braces = [];
|
|
408
|
+
let i = 0, nl = false;
|
|
409
|
+
const n = text.length;
|
|
410
|
+
// `nl`: a line ends between this token and the one before, which some words' meaning turns on (`async` before
|
|
411
|
+
// a line break is a name).
|
|
412
|
+
const push = (t, v, at) => { out.push({ t, v, at, nl }); nl = false; };
|
|
413
|
+
const template = () => {
|
|
414
|
+
const at = i;
|
|
415
|
+
while (i < n) {
|
|
416
|
+
const c = text[i];
|
|
417
|
+
if (c === '\\') { i += 2; continue; }
|
|
418
|
+
if (c === '`') { i++; push('lit', '`', at); return; }
|
|
419
|
+
if (c === '$' && text[i + 1] === '{') { i += 2; push('lit', '`', at); braces.push('tpl'); return; }
|
|
420
|
+
i++;
|
|
421
|
+
}
|
|
422
|
+
push('lit', '`', at);
|
|
423
|
+
};
|
|
424
|
+
while (i < n) {
|
|
425
|
+
const c = text[i];
|
|
426
|
+
if (/\s/.test(c)) { if (c === '\n' || c === '\r') nl = true; i++; continue; }
|
|
427
|
+
if (c === '/' && text[i + 1] === '/') { const e = text.indexOf('\n', i); i = e < 0 ? n : e; continue; }
|
|
428
|
+
if (c === '/' && text[i + 1] === '*') {
|
|
429
|
+
const e = text.indexOf('*/', i + 2), end = e < 0 ? n : e + 2;
|
|
430
|
+
if (/[\n\r]/.test(text.slice(i, end))) nl = true;
|
|
431
|
+
i = end;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const at = i;
|
|
435
|
+
if (c === '"' || c === "'") {
|
|
436
|
+
i++;
|
|
437
|
+
while (i < n && text[i] !== c) { if (text[i] === '\\') i++; i++; }
|
|
438
|
+
i++;
|
|
439
|
+
push('lit', 'str', at);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (c === '`') { i++; template(); continue; }
|
|
443
|
+
if (/[A-Za-z_$\u0080-]/.test(c)) {
|
|
444
|
+
let e = i + 1;
|
|
445
|
+
while (e < n && /[\w$\u0080-]/.test(text[e])) e++;
|
|
446
|
+
push('id', text.slice(i, e), at);
|
|
447
|
+
i = e;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (c === '#') {
|
|
451
|
+
let e = i + 1;
|
|
452
|
+
while (e < n && /[\w$]/.test(text[e])) e++;
|
|
453
|
+
push('priv', text.slice(i, e), at);
|
|
454
|
+
i = e;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(text[i + 1]))) {
|
|
458
|
+
let e = i + 1;
|
|
459
|
+
while (e < n && /[\w.]/.test(text[e])) e++;
|
|
460
|
+
push('lit', 'num', at);
|
|
461
|
+
i = e;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (c === '/') {
|
|
465
|
+
const prev = out[out.length - 1];
|
|
466
|
+
const regex = !prev || (prev.t === 'punct' && !/^[)\]}]$/.test(prev.v)) || (prev.t === 'id' && KEYWORDS_BEFORE_REGEX.has(prev.v));
|
|
467
|
+
if (regex) {
|
|
468
|
+
i++;
|
|
469
|
+
let inClass = false;
|
|
470
|
+
while (i < n && (inClass || text[i] !== '/') && text[i] !== '\n') {
|
|
471
|
+
if (text[i] === '\\') i++;
|
|
472
|
+
else if (text[i] === '[') inClass = true;
|
|
473
|
+
else if (text[i] === ']') inClass = false;
|
|
474
|
+
i++;
|
|
475
|
+
}
|
|
476
|
+
i++;
|
|
477
|
+
while (i < n && /[a-z]/i.test(text[i])) i++;
|
|
478
|
+
push('lit', 're', at);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (text.startsWith('...', i)) { push('punct', '...', at); i += 3; continue; }
|
|
483
|
+
if (c === '?' && text[i + 1] === '.' && !/[0-9]/.test(text[i + 2])) { push('punct', '?.', at); i += 2; continue; }
|
|
484
|
+
if (text.startsWith('=>', i)) { push('punct', '=>', at); i += 2; continue; }
|
|
485
|
+
if (c === '{') braces.push('{');
|
|
486
|
+
if (c === '}' && braces.length && braces[braces.length - 1] === 'tpl') { braces.pop(); i++; template(); continue; }
|
|
487
|
+
if (c === '}') braces.pop();
|
|
488
|
+
push('punct', c, at);
|
|
489
|
+
i++;
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// The words that are never a name in a module's code, which is strict: the language's reserved words. A word that's
|
|
495
|
+
// a keyword only somewhere (`async`, `get`, `set`, `of`, `target` in `new.target`) is told by where it stands
|
|
496
|
+
// (`freeNames`), and `undefined`, `NaN` and `Infinity` are names like any other, which a module can declare.
|
|
497
|
+
const RESERVED = new Set(['await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
|
|
498
|
+
'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof',
|
|
499
|
+
'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this',
|
|
500
|
+
'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']);
|
|
501
|
+
|
|
502
|
+
// The names a method reads that it doesn't declare (`free`), and those it declares (`declared`): what it needs from
|
|
503
|
+
// around it. Read from its parameters on, so its own name isn't one. Not scoped: a name declared anywhere in the
|
|
504
|
+
// method counts as declared throughout, which is why one that's also a name of its module's holds the change.
|
|
505
|
+
function freeNames(scan, start) {
|
|
506
|
+
const declared = new Set(), used = new Set();
|
|
507
|
+
// Where each bracket closes, by where it opens.
|
|
508
|
+
const closes = new Map(), open = [];
|
|
509
|
+
scan.forEach((t, i) => {
|
|
510
|
+
if (['(', '[', '{'].includes(t.v)) open.push(i);
|
|
511
|
+
else if ([')', ']', '}'].includes(t.v) && open.length) closes.set(open.pop(), i);
|
|
512
|
+
});
|
|
513
|
+
// `of` is a keyword only in a for loop's head, after what it declares: `for (const v of list)`.
|
|
514
|
+
const ofKeyword = new Set();
|
|
515
|
+
scan.forEach((t, i) => {
|
|
516
|
+
if (t.v !== 'for') return;
|
|
517
|
+
const at = scan[i + 1] && scan[i + 1].v === 'await' ? i + 2 : i + 1;
|
|
518
|
+
if (!scan[at] || scan[at].v !== '(') return;
|
|
519
|
+
let depth = 0;
|
|
520
|
+
for (let j = at; j < (closes.get(at) ?? scan.length); j++) {
|
|
521
|
+
if (['(', '[', '{'].includes(scan[j].v)) depth++;
|
|
522
|
+
else if ([')', ']', '}'].includes(scan[j].v)) depth--;
|
|
523
|
+
else if (depth === 1 && scan[j].v === 'of' && scan[j - 1] && (scan[j - 1].t === 'id' || [']', '}'].includes(scan[j - 1].v))) ofKeyword.add(j);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
const arrowAfter = i => scan[i] && scan[i].v === '(' && scan[(closes.get(i) ?? -2) + 1] && scan[closes.get(i) + 1].v === '=>';
|
|
527
|
+
// A word that says what follows it, rather than naming anything: `async` before a function, an arrow or a
|
|
528
|
+
// method's key on its line; `get` and `set` before an accessor's key in an object or class.
|
|
529
|
+
// A method's definition from its key on: the key (a word, a string or number, `#name`, or `[…]`), its parameters,
|
|
530
|
+
// and the brace its body opens with. `async * 2` and `get[0]` are expressions, with no such thing after them.
|
|
531
|
+
const methodAt = j => {
|
|
532
|
+
const key = scan[j];
|
|
533
|
+
if (!key || !(key.t === 'id' || key.t === 'lit' || key.t === 'priv' || key.v === '[')) return false;
|
|
534
|
+
const params = key.v === '[' ? (closes.get(j) ?? -2) + 1 : j + 1;
|
|
535
|
+
if (!scan[params] || scan[params].v !== '(') return false;
|
|
536
|
+
const after = scan[(closes.get(params) ?? -2) + 1];
|
|
537
|
+
return !!after && after.v === '{';
|
|
538
|
+
};
|
|
539
|
+
const modifier = i => {
|
|
540
|
+
const t = scan[i], next = scan[i + 1], prev = scan[i - 1];
|
|
541
|
+
if (!next || next.nl) return false;
|
|
542
|
+
if (t.v === 'async') return next.v === 'function' || arrowAfter(i + 1) || (next.v === '*' ? methodAt(i + 2) : methodAt(i + 1))
|
|
543
|
+
// `async x => …`: a word after it on its line is an arrow's parameter, as nothing else can be there.
|
|
544
|
+
|| (next.t === 'id' && scan[i + 2] && scan[i + 2].v === '=>');
|
|
545
|
+
if (t.v === 'get' || t.v === 'set') return (!prev || ['{', ',', ';', '}'].includes(prev.v)) && methodAt(i + 1);
|
|
546
|
+
return false;
|
|
547
|
+
};
|
|
548
|
+
// Parameters: of the method, of a function, of an arrow, of a catch.
|
|
549
|
+
const group = open => {
|
|
550
|
+
let depth = 0;
|
|
551
|
+
for (let i = open; i < scan.length; i++) {
|
|
552
|
+
const t = scan[i];
|
|
553
|
+
if (t.v === '(' || t.v === '[' || t.v === '{') depth++;
|
|
554
|
+
else if (t.v === ')' || t.v === ']' || t.v === '}') { if (--depth === 0) return i; }
|
|
555
|
+
else if (t.t === 'id' && depth >= 1) {
|
|
556
|
+
const next = scan[i + 1] && scan[i + 1].v, prev = scan[i - 1] && scan[i - 1].v;
|
|
557
|
+
if (prev !== '.' && prev !== '?.' && prev !== '=' && [',', ')', '=', '}', ']'].includes(next)) declared.add(t.v);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return scan.length;
|
|
561
|
+
};
|
|
562
|
+
for (let i = Math.max(start, 0); i < scan.length; i++) {
|
|
563
|
+
const t = scan[i], prev = scan[i - 1], next = scan[i + 1];
|
|
564
|
+
if (t.v === '(') {
|
|
565
|
+
// A group of parameters: the method's own, or one that's followed by =>, or after function or catch.
|
|
566
|
+
const end = (() => { let d = 0; for (let j = i; j < scan.length; j++) { if (scan[j].v === '(') d++; else if (scan[j].v === ')' && --d === 0) return j; } return scan.length; })();
|
|
567
|
+
const after = scan[end + 1], before = scan[i - 1], beforeThat = scan[i - 2];
|
|
568
|
+
if (i === start || (after && after.v === '=>') || (before && (before.v === 'function' || before.v === 'catch'))
|
|
569
|
+
|| (before && before.t === 'id' && beforeThat && beforeThat.v === 'function')
|
|
570
|
+
|| (after && after.v === '{' && before && before.t === 'id' && !RESERVED.has(before.v) && (!beforeThat || [',', '{', '}', ';'].includes(beforeThat.v)))) group(i);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (t.t !== 'id') continue;
|
|
574
|
+
if (next && next.v === '=>' && (!prev || prev.v !== '.')) { declared.add(t.v); continue; }
|
|
575
|
+
if (prev && (prev.v === 'let' || prev.v === 'const' || prev.v === 'var' || prev.v === 'function' || prev.v === 'class')) { declared.add(t.v); continue; }
|
|
576
|
+
if (prev && (prev.v === '.' || prev.v === '?.')) continue;
|
|
577
|
+
// A key in an object, or a label.
|
|
578
|
+
if (next && next.v === ':' && prev && ['{', ',', ';'].includes(prev.v)) continue;
|
|
579
|
+
if (RESERVED.has(t.v) || ofKeyword.has(i) || modifier(i)) continue;
|
|
580
|
+
// Bound in every function that isn't an arrow, and the method is one.
|
|
581
|
+
if (t.v === 'arguments') continue;
|
|
582
|
+
used.add(t.v);
|
|
583
|
+
}
|
|
584
|
+
// Destructured declarations: `const { a, b: c } = …`, `let [x, y] = …`.
|
|
585
|
+
for (let i = 0; i < scan.length; i++) {
|
|
586
|
+
if (!['let', 'const', 'var'].includes(scan[i].v) || !scan[i + 1] || !['{', '['].includes(scan[i + 1].v)) continue;
|
|
587
|
+
let depth = 0;
|
|
588
|
+
for (let j = i + 1; j < scan.length; j++) {
|
|
589
|
+
const t = scan[j];
|
|
590
|
+
if (t.v === '{' || t.v === '[') depth++;
|
|
591
|
+
else if (t.v === '}' || t.v === ']') { if (--depth === 0) break; }
|
|
592
|
+
else if (t.t === 'id' && scan[j + 1] && [',', '}', ']', '='].includes(scan[j + 1].v)) declared.add(t.v);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return { free: [...used].filter(n => !declared.has(n)), declared };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// What a module declares at its top level: every name (`declared`), its imports with the names they give, and its
|
|
599
|
+
// constants written as a plain value, each with its declaration.
|
|
600
|
+
function topLevel(text) {
|
|
601
|
+
const scan = tokens(text), declared = new Set(), imports = new Set(), constants = new Map(), importList = [];
|
|
602
|
+
let depth = 0;
|
|
603
|
+
for (let i = 0; i < scan.length; i++) {
|
|
604
|
+
const t = scan[i];
|
|
605
|
+
if (t.v === '(' || t.v === '[' || t.v === '{') { depth++; continue; }
|
|
606
|
+
if (t.v === ')' || t.v === ']' || t.v === '}') { depth--; continue; }
|
|
607
|
+
if (depth !== 0 || t.t !== 'id') continue;
|
|
608
|
+
const prev = scan[i - 1];
|
|
609
|
+
if (prev && (prev.v === '.' || prev.v === '?.')) continue;
|
|
610
|
+
if (t.v === 'import' && scan[i + 1] && scan[i + 1].v !== '(' && scan[i + 1].v !== '.') {
|
|
611
|
+
const i0 = i;
|
|
612
|
+
let j = i + 1;
|
|
613
|
+
while (j < scan.length && !(scan[j].t === 'lit' && scan[j].v === 'str')) j++;
|
|
614
|
+
if (j >= scan.length) break;
|
|
615
|
+
const lit = scan[j];
|
|
616
|
+
const end = text.indexOf(text[lit.at], lit.at + 1);
|
|
617
|
+
const specifier = text.slice(lit.at + 1, end);
|
|
618
|
+
const clauseTokens = scan.slice(i0 + 1, j).filter(x => x.v !== 'from');
|
|
619
|
+
const names = [];
|
|
620
|
+
for (let k = 0; k < clauseTokens.length; k++) {
|
|
621
|
+
const c = clauseTokens[k];
|
|
622
|
+
if (c.t !== 'id' || c.v === 'as') continue;
|
|
623
|
+
const after = clauseTokens[k + 1];
|
|
624
|
+
if (after && after.v === 'as') continue;
|
|
625
|
+
names.push(c.v);
|
|
626
|
+
}
|
|
627
|
+
const fromToken = scan.slice(i0 + 1, j).find(x => x.v === 'from');
|
|
628
|
+
const clause = fromToken ? text.slice(scan[i0 + 1].at, fromToken.at).trim() : '';
|
|
629
|
+
for (const name of names) imports.add(name);
|
|
630
|
+
importList.push({ specifier, clause, names });
|
|
631
|
+
i = j;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (t.v === 'function' || t.v === 'class') {
|
|
635
|
+
const name = scan[i + 1] && scan[i + 1].v === '*' ? scan[i + 2] : scan[i + 1];
|
|
636
|
+
if (name && name.t === 'id') declared.add(name.v);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (t.v === 'const' || t.v === 'let' || t.v === 'var') {
|
|
640
|
+
// Every name up to the end of the declaration, erring toward more.
|
|
641
|
+
let d = 0, naming = true;
|
|
642
|
+
for (let j = i + 1; j < scan.length; j++) {
|
|
643
|
+
const s = scan[j];
|
|
644
|
+
if (s.v === '(' || s.v === '[' || s.v === '{') d++;
|
|
645
|
+
else if (s.v === ')' || s.v === ']' || s.v === '}') { if (--d < 0) break; }
|
|
646
|
+
else if (d === 0 && s.v === ';') break;
|
|
647
|
+
else if (d === 0 && s.v === '=') naming = false;
|
|
648
|
+
else if (d === 0 && s.v === ',') naming = true;
|
|
649
|
+
else if (d === 0 && s.t === 'id' && ['const', 'let', 'var', 'function', 'class', 'export', 'import'].includes(s.v)) break;
|
|
650
|
+
else if (naming && s.t === 'id' && scan[j - 1].v !== ':') declared.add(s.v);
|
|
651
|
+
if (d === 0 && !naming && j > i + 3 && text.slice(scan[j - 1].at, s.at).includes('\n') && s.t === 'id' && !['instanceof', 'in', 'of'].includes(s.v)
|
|
652
|
+
&& !['.', '?.', '+', '-', '*', '/', '%', '=', '&', '|', '?', ':', ',', '(', '[', '<', '>', '!', '^', '~'].includes(scan[j - 1].v)) break;
|
|
653
|
+
}
|
|
654
|
+
// A constant that's a plain value: `const SPEED = 3;`, `const NAME = 'x'`, `const OFF = -1`.
|
|
655
|
+
const [name, eq, a, b] = [scan[i + 1], scan[i + 2], scan[i + 3], scan[i + 4]];
|
|
656
|
+
if (t.v === 'const' && name && name.t === 'id' && eq && eq.v === '=' && a) {
|
|
657
|
+
const literal = x => x && ((x.t === 'lit' && (x.v === 'num' || x.v === 'str')) || ['true', 'false', 'null'].includes(x.v));
|
|
658
|
+
const one = literal(a) ? a : a.v === '-' && b && b.t === 'lit' && b.v === 'num' ? b : null;
|
|
659
|
+
const after = one && scan[scan.indexOf(one) + 1];
|
|
660
|
+
const ends = !after || after.v === ';' || (text.slice(one.at, after.at).includes('\n') && !['.', '?.', '+', '-', '*', '/', '%', '?', '[', '(', '`', ',', '&', '|', '<', '>', '=', 'instanceof', 'in'].includes(after.v));
|
|
661
|
+
if (one && ends) {
|
|
662
|
+
const endAt = after ? (after.v === ';' ? after.at + 1 : after.at) : text.length;
|
|
663
|
+
constants.set(name.v, text.slice(t.at, endAt).trim().replace(/;?$/, ';'));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { declared, imports, constants, importList };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function importsOf(text) { return topLevel(text).importList; }
|
|
672
|
+
})();
|