mithril-lynx 0.0.9 → 2.0.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/.omo/plans/m-request-fetch-lynx.md +306 -0
- package/.omo/plans/m-route-en-memoria.md +397 -0
- package/.omo/plans/mithril-lynx-v2-desde-cero.md +548 -0
- package/FETCH_INVESTIGATION.md +307 -0
- package/README.md +32 -302
- package/REQUEST.md +71 -0
- package/ROUTE.md +71 -0
- package/package.json +24 -80
- package/plugin.d.ts +2 -31
- package/plugin.js +107 -436
- package/rstest.config.ts +27 -0
- package/src/apply-patch.js +179 -0
- package/src/backends/virtual-backend.js +80 -0
- package/src/background.d.ts +11 -0
- package/src/background.js +79 -0
- package/src/channel.js +41 -0
- package/src/commit.js +67 -0
- package/src/dev-reload-client.js +168 -184
- package/src/dev-transport-noop.js +10 -0
- package/src/fake-dom.js +374 -0
- package/src/main-thread.d.ts +1 -0
- package/src/main-thread.js +68 -0
- package/src/mount-redraw.js +67 -0
- package/src/patch-protocol.js +40 -0
- package/src/reload/version.js +28 -0
- package/src/request.d.ts +37 -0
- package/src/request.js +181 -0
- package/src/route.d.ts +33 -0
- package/src/route.js +207 -0
- package/test/end-to-end.test.ts +86 -0
- package/test/reload-version.test.ts +17 -0
- package/test/request.test.ts +182 -0
- package/test/route-hot-reload.test.ts +40 -0
- package/test/route.test.ts +152 -0
- package/test/setup.ts +25 -0
- package/test/structural-reload.test.ts +95 -0
- package/CONTRACT.md +0 -151
- package/LICENSE +0 -21
- package/background.d.ts +0 -54
- package/background.js +0 -169
- package/element.d.ts +0 -34
- package/element.js +0 -83
- package/gesture.d.ts +0 -40
- package/gesture.js +0 -117
- package/internal/constants.js +0 -26
- package/internal/virtual-node.js +0 -388
- package/list.d.ts +0 -31
- package/list.js +0 -185
- package/main-thread.d.ts +0 -43
- package/main-thread.js +0 -165
- package/navigation.d.ts +0 -35
- package/navigation.js +0 -76
- package/renderer/background.d.ts +0 -21
- package/renderer/background.js +0 -84
- package/renderer/main-thread.d.ts +0 -12
- package/renderer/main-thread.js +0 -175
- package/src/lynx-mithril-shim.d.ts +0 -16
- package/src/lynx-mithril-shim.js +0 -1505
- package/src/worklet-runtime.js +0 -82
- package/testing.d.ts +0 -10
- package/testing.js +0 -91
package/plugin.js
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
// plugin.js
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Rspeedy/Rsbuild plugin wiring the two-bundle build (main-thread/Lepus +
|
|
4
|
+
// background/JS) for mithril-lynx apps. Adapted from the previous
|
|
5
|
+
// mithril-lynx's plugin.js — this file is build TOOLING, not the
|
|
6
|
+
// redraw/reload mechanism that motivated the rewrite (see
|
|
7
|
+
// mithril-lynx-v2-desde-cero.md §2: the old bugs lived in the
|
|
8
|
+
// shim/commit/reload layer, never here), so it is reused with fixes
|
|
9
|
+
// rather than rewritten from nothing. Two real changes from the old one:
|
|
6
10
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
11
|
+
// 1. (F0.2 fix, the actual point of this file's existence in the plan)
|
|
12
|
+
// `RuntimeWrapperWebpackPlugin`'s `test` regex now also matches
|
|
13
|
+
// `.hot-update.js` chunks. The old regex (`${name}/background\.js$`)
|
|
14
|
+
// matched the initial background ASSET path (nested under
|
|
15
|
+
// `.rspeedy/<name>/`, with a slash) but never the flat, double-
|
|
16
|
+
// underscore-named hot-update chunk (`<name>__background.<hash>.hot-
|
|
17
|
+
// update.js`) — confirmed with a plain regex test against both real
|
|
18
|
+
// filenames, not a guess. That gap is the entire reason the old
|
|
19
|
+
// version needed a runtime monkey-patch of `lynx.requireModuleAsync`
|
|
20
|
+
// in its dev-reload client; this one's client has no such patch (see
|
|
21
|
+
// src/dev-reload-client.js).
|
|
12
22
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
23
|
+
// 2. Only ONE rendering mode exists (plan §2 non-goals: no main-thread-
|
|
24
|
+
// owned/data-channel modes) — so there is no mode-detection logic here,
|
|
25
|
+
// `dev.hmr` is unconditionally on in dev, and every entry always gets a
|
|
26
|
+
// background chunk.
|
|
17
27
|
|
|
18
28
|
import fs from "node:fs";
|
|
19
29
|
import path from "node:path";
|
|
@@ -24,94 +34,33 @@ import { RuntimeWrapperWebpackPlugin } from "@lynx-js/runtime-wrapper-webpack-pl
|
|
|
24
34
|
import { LynxEncodePlugin, LynxTemplatePlugin } from "@lynx-js/template-webpack-plugin";
|
|
25
35
|
|
|
26
36
|
const PLUGIN_NAME = "mithril-lynx-template-webpack";
|
|
27
|
-
|
|
28
|
-
const BACKGROUND_CANDIDATES = ["background.ts", "background.js"];
|
|
29
37
|
const STYLE_CANDIDATES = ["style.css"];
|
|
30
38
|
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Live reload (dev only)
|
|
33
|
-
//
|
|
34
|
-
// Two strategies, both triggered by a successful rebuild, picked by the
|
|
35
|
-
// `liveReload` option:
|
|
36
|
-
//
|
|
37
|
-
// - `true` (default): the "in-bundle" client. A small WebSocket client
|
|
38
|
-
// (src/dev-reload-client.js) is injected into a synthetic background chunk
|
|
39
|
-
// and runs on the device; on an `ok` message it sends a CDP `Page.reload`
|
|
40
|
-
// through `NativeModules.LynxDevToolSetModule.invokeCdp` — the same
|
|
41
|
-
// mechanism Rspeedy's own live-reload client uses
|
|
42
|
-
// (@lynx-js/webpack-dev-transport/lib/client/reloadApp.js) — with a
|
|
43
|
-
// cache-busted URL. Page.reload reloads the CURRENT page in place without
|
|
44
|
-
// starting a new Activity (verified in 0.0.8). This works over Wi-Fi alone
|
|
45
|
-
// (the WebSocket is the only network hop): no adb, no cable. Requires the
|
|
46
|
-
// viewer SDK to expose the DevTool CDP bridge native module (Lynx Go does).
|
|
47
|
-
//
|
|
48
|
-
// - `"devtool"`: the 0.0.8 strategy. Runs from the Node dev-server process
|
|
49
|
-
// via @lynx-js/devtool-connector (adb on Android) and sends CDP
|
|
50
|
-
// `Page.reload` to the session serving this app's bundle. Also reloads in
|
|
51
|
-
// place (no back-stack corruption), but needs the device on adb — so it is
|
|
52
|
-
// the fallback for viewers/SDKs where the in-bundle CDP bridge is missing.
|
|
53
|
-
//
|
|
54
|
-
// Why not real module HMR: mithril view code lives in the main-thread/Lepus
|
|
55
|
-
// chunk, and rspack's hot runtime can only patch modules in the registry it
|
|
56
|
-
// runs from — the background/JS thread. That is an architectural mismatch,
|
|
57
|
-
// not a bug to fix. dev.hmr is forced off regardless of strategy.
|
|
58
|
-
//
|
|
59
|
-
// Why not lynx.reload() (0.0.9's original mechanism): on the device's engine
|
|
60
|
-
// (reported 3.5) it re-executes the main-thread bundle against the live
|
|
61
|
-
// session but does not re-inject the runtime helpers
|
|
62
|
-
// removeComponents/updatePage, so the render crashes with two TypeErrors and
|
|
63
|
-
// the background chunk never re-runs (socket dies; later builds ignored). Kept
|
|
64
|
-
// only as an in-client fallback for viewers whose reload path is intact.
|
|
65
|
-
//
|
|
66
|
-
// Why not ExplorerModule.openSchema(url) (0.0.7's mechanism): it works over
|
|
67
|
-
// Wi-Fi, but it is a real navigation — Lynx Go starts a NEW
|
|
68
|
-
// LynxViewShellActivity every time and never finishes the one it replaces, so
|
|
69
|
-
// Back steps through one frozen snapshot per reload. Kept only as an
|
|
70
|
-
// in-client fallback for viewers lacking `lynx.reload`.
|
|
71
|
-
//
|
|
72
|
-
// Why not lynx.fetchBundle + lynx.loadScript: in lynx-core these are the
|
|
73
|
-
// lazy-bundle path (LazyBundleLoader / LoadCustomSectionScript) — they load a
|
|
74
|
-
// customSection of a SEPARATE lazily-fetched bundle, not the page's own
|
|
75
|
-
// main-thread/Lepus template, so they cannot re-render the current page.
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
// The in-bundle client only runs where a full JS engine with WebSocket + the
|
|
79
|
-
// `lynx` global exists: the background/JS thread. The main-thread/Lepus VM
|
|
80
|
-
// cannot use it. An app without background.ts therefore gets a synthetic
|
|
81
|
-
// background entry in development; see `includeBackground` below. Imported by
|
|
82
|
-
// its resolved absolute path (plus the query string the client reads its
|
|
83
|
-
// config from) rather than through the bare "mithril-lynx/dev-reload-client"
|
|
84
|
-
// specifier + an alias: the package-wide "mithril-lynx" prefix alias set
|
|
85
|
-
// below matches that specifier first regardless of registration order
|
|
86
|
-
// (webpack/rspack's resolve.alias picks the first matching entry, not the
|
|
87
|
-
// most specific one), which broke the import entirely in 0.0.7.
|
|
88
39
|
const DEV_RELOAD_CLIENT_PATH = path.join(
|
|
89
40
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
90
41
|
"src",
|
|
91
42
|
"dev-reload-client.js",
|
|
92
43
|
);
|
|
93
44
|
|
|
45
|
+
const DEV_TRANSPORT_NOOP_PATH = path.join(
|
|
46
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
47
|
+
"src",
|
|
48
|
+
"dev-transport-noop.js",
|
|
49
|
+
);
|
|
50
|
+
|
|
94
51
|
/**
|
|
95
52
|
* Builds the query string the in-bundle dev-reload client reads its config
|
|
96
|
-
* from
|
|
97
|
-
*
|
|
98
|
-
* `@lynx-js/rsbuild-plugin` bakes into `@lynx-js/webpack-dev-transport/client`.
|
|
53
|
+
* from — unchanged from v1's version (build-tooling glue, not part of the
|
|
54
|
+
* bug this rewrite is about).
|
|
99
55
|
*/
|
|
100
56
|
function createDevReloadClientQuery(api, environment, entryName) {
|
|
101
57
|
const config = environment.config ?? {};
|
|
102
58
|
const dev = config.dev ?? {};
|
|
103
59
|
const server = config.server ?? {};
|
|
104
60
|
const devServer = api.context.devServer ?? {};
|
|
105
|
-
// Rsbuild resolves this to the LAN address it advertises when server.host
|
|
106
|
-
// is 0.0.0.0, which is the address a physical Lynx Go device can use.
|
|
107
61
|
const hostname = dev.client?.host || devServer.hostname || server.host || "";
|
|
108
62
|
const port = devServer.port ?? server.port ?? "";
|
|
109
63
|
const protocol = devServer.https ? "https" : "http";
|
|
110
|
-
// At this point in the pipeline Rsbuild hasn't started the dev server yet,
|
|
111
|
-
// so `dev.assetPrefix` (when it's the default, host-derived one) still
|
|
112
|
-
// contains the literal "<port>" placeholder it's only resolved to a real
|
|
113
|
-
// port number later. `hostname`/`port` above are already the real values,
|
|
114
|
-
// so resolve the placeholder the same way rather than trusting assetPrefix.
|
|
115
64
|
const assetPrefix = (typeof dev.assetPrefix === "string" ? dev.assetPrefix : "/").replaceAll(
|
|
116
65
|
"<port>",
|
|
117
66
|
String(port),
|
|
@@ -134,180 +83,6 @@ function createDevReloadClientQuery(api, environment, entryName) {
|
|
|
134
83
|
return params.toString();
|
|
135
84
|
}
|
|
136
85
|
|
|
137
|
-
// --- Devtool-connector strategy (liveReload: "devtool") --------------------
|
|
138
|
-
// Everything below through closeDevtoolTransport()/describeReloadFailure() is
|
|
139
|
-
// the 0.0.8 adb/CDP path, used only when the in-bundle client can't run (e.g.
|
|
140
|
-
// a viewer whose SDK predates the DevTool CDP bridge native module). It is
|
|
141
|
-
// otherwise dormant.
|
|
142
|
-
|
|
143
|
-
/** Lynx Go's own shell page is a Lynx session too — never a reload target. */
|
|
144
|
-
const VIEWER_SHELL_BUNDLE = "homepage.lynx.bundle";
|
|
145
|
-
|
|
146
|
-
let connectorPromise;
|
|
147
|
-
let devtoolTransport;
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Lazily loads the DevTool connector. Kept lazy so `@lynx-js/devtool-connector`
|
|
151
|
-
* is only ever loaded by a dev rebuild, never by a production build.
|
|
152
|
-
*/
|
|
153
|
-
function getDevtoolConnector() {
|
|
154
|
-
if (!connectorPromise) {
|
|
155
|
-
connectorPromise = Promise.all([
|
|
156
|
-
import("@lynx-js/devtool-connector"),
|
|
157
|
-
import("@lynx-js/devtool-connector/transport"),
|
|
158
|
-
])
|
|
159
|
-
.then(([{ Connector }, { AndroidTransport }]) => {
|
|
160
|
-
devtoolTransport = new AndroidTransport();
|
|
161
|
-
return new Connector([devtoolTransport]);
|
|
162
|
-
})
|
|
163
|
-
.catch((error) => {
|
|
164
|
-
// Don't cache a rejection: the usual cause is the package not
|
|
165
|
-
// being installed yet, and the dev server outlives an
|
|
166
|
-
// `npm install`.
|
|
167
|
-
connectorPromise = undefined;
|
|
168
|
-
throw error;
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
return connectorPromise;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Last path segment of a URL, ignoring any query string or fragment. */
|
|
175
|
-
function bundleBasename(url) {
|
|
176
|
-
if (typeof url !== "string") return "";
|
|
177
|
-
const withoutQuery = url.split("?")[0].split("#")[0];
|
|
178
|
-
const segments = withoutQuery.split("/");
|
|
179
|
-
return segments[segments.length - 1] || withoutQuery;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function isViewerShell(url) {
|
|
183
|
-
return bundleBasename(url) === VIEWER_SHELL_BUNDLE;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Adds a unique query parameter to a bundle URL.
|
|
188
|
-
*
|
|
189
|
-
* `Page.reload` on its own is not enough to pick up a rebuild: measured
|
|
190
|
-
* on-device, a reload without this re-fetched and re-ran the PREVIOUS bundle
|
|
191
|
-
* (the loaded template stayed byte-for-byte the old one, and the old text
|
|
192
|
-
* stayed on screen) even with `ignoreCache: true`. Both the HTTP layer and
|
|
193
|
-
* Lynx's own bytecode cache are keyed by URL, so changing the URL is what
|
|
194
|
-
* actually invalidates them. The session's own URL is unaffected — the DevTools
|
|
195
|
-
* reference notes it does not change after a reload, and that was confirmed
|
|
196
|
-
* here too.
|
|
197
|
-
*
|
|
198
|
-
* Returns undefined for anything that isn't an http(s) URL, in which case the
|
|
199
|
-
* caller lets Page.reload use the URL it already has (it rejects anything else).
|
|
200
|
-
*/
|
|
201
|
-
export function cacheBustedUrl(url, now = Date.now()) {
|
|
202
|
-
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) return undefined;
|
|
203
|
-
const [base, query = ""] = url.split("?");
|
|
204
|
-
const params = new URLSearchParams(query);
|
|
205
|
-
params.set("t", String(now));
|
|
206
|
-
return `${base}?${params.toString()}`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function matchesHint(url, hints) {
|
|
210
|
-
if (typeof url !== "string" || hints.length === 0) return false;
|
|
211
|
-
return hints.some((hint) => url.includes(hint));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Chooses which client/session to reload: `targets` is `[{ client, sessions }]`,
|
|
216
|
-
* returns `{ clientId, sessionId, url }` or null.
|
|
217
|
-
*
|
|
218
|
-
* Pure and exported so it can be tested without a device attached.
|
|
219
|
-
*/
|
|
220
|
-
export function pickReloadTarget(targets, { bundleHints = [] } = {}) {
|
|
221
|
-
const candidates = [];
|
|
222
|
-
for (const { client, sessions } of targets) {
|
|
223
|
-
for (const session of sessions ?? []) {
|
|
224
|
-
if (session?.type !== "lynx") continue;
|
|
225
|
-
if (isViewerShell(session.url)) continue;
|
|
226
|
-
candidates.push({ clientId: client.id, session });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (candidates.length === 0) return null;
|
|
230
|
-
|
|
231
|
-
// Prefer the session actually serving one of this app's bundles over
|
|
232
|
-
// "whatever was opened most recently": with a second Lynx app, or a second
|
|
233
|
-
// attached device, the newest session need not be ours.
|
|
234
|
-
const preferred = candidates.filter((candidate) => matchesHint(candidate.session.url, bundleHints));
|
|
235
|
-
const pool = preferred.length > 0 ? preferred : candidates;
|
|
236
|
-
|
|
237
|
-
const latest = pool.reduce((a, b) => (b.session.session_id > a.session.session_id ? b : a));
|
|
238
|
-
return {
|
|
239
|
-
clientId: latest.clientId,
|
|
240
|
-
sessionId: latest.session.session_id,
|
|
241
|
-
url: latest.session.url,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
246
|
-
|
|
247
|
-
async function listClientSessions(connector) {
|
|
248
|
-
const clients = await connector.listClients();
|
|
249
|
-
const targets = [];
|
|
250
|
-
for (const client of clients) {
|
|
251
|
-
try {
|
|
252
|
-
targets.push({ client, sessions: await connector.sendListSessionMessage(client.id) });
|
|
253
|
-
} catch {
|
|
254
|
-
// This client doesn't support session listing, or isn't ready yet.
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return targets;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Finds a session worth reloading, retrying briefly: on the very first rebuild
|
|
262
|
-
* the DevTool client may not have registered with the device yet.
|
|
263
|
-
*/
|
|
264
|
-
async function findReloadTarget(bundleHints, { attempts = 3, delayMs = 400 } = {}) {
|
|
265
|
-
const connector = await getDevtoolConnector();
|
|
266
|
-
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
267
|
-
const target = pickReloadTarget(await listClientSessions(connector), { bundleHints });
|
|
268
|
-
if (target) return { connector, target };
|
|
269
|
-
if (attempt < attempts - 1) await sleep(delayMs);
|
|
270
|
-
}
|
|
271
|
-
return { connector, target: null };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Reloads the running page in place. Returns true when a session was reloaded,
|
|
276
|
-
* false when none was found (the caller logs that).
|
|
277
|
-
*/
|
|
278
|
-
async function reloadViaDevtool(bundleHints) {
|
|
279
|
-
const { connector, target } = await findReloadTarget(bundleHints);
|
|
280
|
-
if (!target) return false;
|
|
281
|
-
|
|
282
|
-
const params = { ignoreCache: true };
|
|
283
|
-
// Without a changed URL the device replays its cached copy of the previous
|
|
284
|
-
// bundle — see cacheBustedUrl(). The session URL itself does not change.
|
|
285
|
-
const url = cacheBustedUrl(target.url);
|
|
286
|
-
if (url != null) params.url = url;
|
|
287
|
-
|
|
288
|
-
await connector.sendCDPMessage(target.clientId, target.sessionId, "Page.reload", params);
|
|
289
|
-
console.info(
|
|
290
|
-
`[mithril-lynx] Reloaded ${target.url || "(url unknown)"} (session ${target.sessionId}${url != null ? "" : ", no cache-busting: non-http url"}).`,
|
|
291
|
-
);
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** Releases the adb connection when the dev server goes away. */
|
|
296
|
-
async function closeDevtoolTransport() {
|
|
297
|
-
const transport = devtoolTransport;
|
|
298
|
-
devtoolTransport = undefined;
|
|
299
|
-
connectorPromise = undefined;
|
|
300
|
-
await transport?.close?.();
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Turns a connector failure into an actionable one-liner. */
|
|
304
|
-
function describeReloadFailure(error) {
|
|
305
|
-
if (error?.code === "ERR_MODULE_NOT_FOUND" || /devtool-connector/.test(error?.message ?? "")) {
|
|
306
|
-
return "Live reload is off: @lynx-js/devtool-connector is not installed. Run `npm install @lynx-js/devtool-connector`, then restart the dev server.";
|
|
307
|
-
}
|
|
308
|
-
return `Live reload unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
86
|
function findSibling(dir, candidates) {
|
|
312
87
|
for (const name of candidates) {
|
|
313
88
|
const candidate = path.join(dir, name);
|
|
@@ -316,18 +91,12 @@ function findSibling(dir, candidates) {
|
|
|
316
91
|
return null;
|
|
317
92
|
}
|
|
318
93
|
|
|
319
|
-
|
|
320
|
-
* Walks up from a resolved file to the root of the package that owns it,
|
|
321
|
-
* verified by name rather than assumed from the layout — this package's
|
|
322
|
-
* exports map deliberately doesn't expose ./package.json, so the usual
|
|
323
|
-
* require.resolve("<pkg>/package.json") trick isn't available here.
|
|
324
|
-
*/
|
|
325
|
-
function packageRootOf(resolvedFile) {
|
|
94
|
+
function packageRootOf(resolvedFile, expectedName) {
|
|
326
95
|
let dir = path.dirname(resolvedFile);
|
|
327
96
|
for (let i = 0; i < 10; i++) {
|
|
328
97
|
try {
|
|
329
98
|
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
330
|
-
if (pkg.name ===
|
|
99
|
+
if (pkg.name === expectedName) return dir;
|
|
331
100
|
} catch {
|
|
332
101
|
// keep walking
|
|
333
102
|
}
|
|
@@ -340,129 +109,57 @@ function packageRootOf(resolvedFile) {
|
|
|
340
109
|
|
|
341
110
|
export function pluginMithrilLynx(options = {}) {
|
|
342
111
|
const targetSdkVersion = options.targetSdkVersion ?? "3.5";
|
|
343
|
-
const hmr = options.hmr ?? false;
|
|
344
|
-
// `true` (default): in-bundle client over Wi-Fi via CDP Page.reload (see
|
|
345
|
-
// src/dev-reload-client.js) — no adb. `"devtool"`: 0.0.8's adb/CDP
|
|
346
|
-
// Page.reload path (fallback for viewers without the in-bundle CDP bridge).
|
|
347
|
-
// `false`: off.
|
|
348
112
|
const liveReload = options.liveReload ?? true;
|
|
349
|
-
const useDevtoolReload = liveReload === "devtool";
|
|
350
|
-
|
|
351
|
-
// Filled in by modifyBundlerChain below with "<entry>.bundle" for every
|
|
352
|
-
// configured entry, so a reload prefers the session actually serving this
|
|
353
|
-
// app over whichever Lynx session happens to be newest (devtool mode only).
|
|
354
|
-
const bundleHints = new Set();
|
|
355
113
|
|
|
356
114
|
return {
|
|
357
115
|
name: PLUGIN_NAME,
|
|
358
116
|
setup(api) {
|
|
359
|
-
// Keep the template plugin discoverable by Rspeedy's Lynx internals.
|
|
360
117
|
api.expose(Symbol.for("LynxTemplatePlugin"), { LynxTemplatePlugin });
|
|
361
118
|
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
// HMR's eval'd *.hot-update.js chunks also aren't runtime-wrapped
|
|
366
|
-
// the way the real background.js bundle is, and fail native-side
|
|
367
|
-
// with "ReferenceError: exports is not defined" if hot is left on.
|
|
368
|
-
// Force dev.hmr off (unless the app explicitly set it) -- real HMR
|
|
369
|
-
// can't reach this framework's app code regardless of whether the
|
|
370
|
-
// reload strategy is in-bundle (CDP Page.reload) or devtool (Node
|
|
371
|
-
// process, CDP Page.reload), so leaving it on only adds error noise
|
|
372
|
-
// for no benefit.
|
|
119
|
+
// One mode only -> dev.hmr is unconditionally on in dev (the old
|
|
120
|
+
// version had to detect renderer-mode-vs-not here; there is no
|
|
121
|
+
// "not" to detect anymore).
|
|
373
122
|
api.modifyRsbuildConfig({
|
|
374
|
-
|
|
375
|
-
// the config by the time ANY hook sees it (even api.getRsbuildConfig
|
|
376
|
-
// ("original")), so there's no reliable way to tell "the app asked for
|
|
377
|
-
// hot module replacement" apart from "Rsbuild defaulted it" -- this
|
|
378
|
-
// always wins, with an explicit opt-out via pluginMithrilLynx({ hmr })
|
|
379
|
-
// for anyone who's fixed up their own app-level accept() story and the
|
|
380
|
-
// RuntimeWrapperWebpackPlugin gap noted below.
|
|
381
|
-
handler: (config, { mergeRsbuildConfig }) => mergeRsbuildConfig(config, { dev: { hmr } }),
|
|
123
|
+
handler: (config, { mergeRsbuildConfig }) => mergeRsbuildConfig(config, { dev: { hmr: true } }),
|
|
382
124
|
order: "post",
|
|
383
125
|
});
|
|
384
126
|
|
|
385
|
-
//
|
|
386
|
-
// (
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
} catch (error) {
|
|
397
|
-
console.warn(`[mithril-lynx] ${describeReloadFailure(error)}`);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (!reloaded) {
|
|
401
|
-
console.warn(
|
|
402
|
-
"[mithril-lynx] Live reload unavailable: no Lynx session found for this app. " +
|
|
403
|
-
"Is the device connected over adb with the page open in Lynx Go? Reload manually.",
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
// The transport owns adb port-forwards; don't let them outlive
|
|
409
|
-
// the dev server.
|
|
410
|
-
api.onCloseDevServer?.(closeDevtoolTransport);
|
|
411
|
-
}
|
|
127
|
+
// dev.hmr:true makes Rsbuild/Rspeedy inject its own HMR transport
|
|
128
|
+
// client (a second WebSocket to /rsbuild-hmr) alongside the
|
|
129
|
+
// in-bundle client above — re-alias it to a no-op so `module.hot`
|
|
130
|
+
// stays live without a competing channel (same fix v1 F2 made).
|
|
131
|
+
api.modifyBundlerChain({
|
|
132
|
+
handler: (chain, { isDev }) => {
|
|
133
|
+
if (!isDev) return;
|
|
134
|
+
chain.resolve.alias.set("@lynx-js/webpack-dev-transport/client", DEV_TRANSPORT_NOOP_PATH);
|
|
135
|
+
},
|
|
136
|
+
order: "post",
|
|
137
|
+
});
|
|
412
138
|
|
|
413
139
|
api.modifyBundlerChain((chain, { isDev, environment }) => {
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
// shadows Node's normal directory-walk resolution once linked — the
|
|
421
|
-
// two copies' emptyAttrs singletons differ. The shim then can't
|
|
422
|
-
// recognize the app's legitimately-reused empty-attrs object as
|
|
423
|
-
// such, and Mithril's own updateAttrs() misfires its "Don't reuse
|
|
424
|
-
// attrs object" warning on every plain `m(tag, null, ...)` element,
|
|
425
|
-
// every redraw. Force a single resolution by aliasing "mithril" to
|
|
426
|
-
// whatever copy the app itself resolves from its own project root.
|
|
427
|
-
try {
|
|
428
|
-
const appRequire = createRequire(path.join(process.cwd(), "package.json"));
|
|
429
|
-
// Resolve the PACKAGE DIRECTORY (not mithril's own main entry
|
|
430
|
-
// file) — a prefix alias needs "mithril/render/x" to rewrite to
|
|
431
|
-
// "<dir>/render/x", which only works aliased to a directory.
|
|
432
|
-
const mithrilDir = path.dirname(appRequire.resolve("mithril/package.json"));
|
|
433
|
-
chain.resolve.alias.set("mithril", mithrilDir);
|
|
434
|
-
} catch {
|
|
435
|
-
// App has no local "mithril" resolvable from its own root —
|
|
436
|
-
// leave resolution as-is rather than guessing.
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Same class of problem, worse symptom: mithril-lynx itself keeps
|
|
440
|
-
// per-app state in module-level variables — the shim's rootWrapper/
|
|
441
|
-
// redraw/runRender, main-thread.js's latestData and its cross-thread
|
|
442
|
-
// handler maps, background.js's mirror of those. Two physical copies
|
|
443
|
-
// means two disconnected renderers: the app renders through one, and
|
|
444
|
-
// any LIBRARY that depends on mithril-lynx (a component library, say,
|
|
445
|
-
// resolving its own nested copy once linked) calls shim.redraw() on
|
|
446
|
-
// the other — whose `redraw` is still null. That's a silent no-op:
|
|
447
|
-
// no error, nothing logged, components simply never update. Confirmed
|
|
448
|
-
// on real hardware 2026-09-11 while building mithril-lynx-ui, where
|
|
449
|
-
// it read as "the animation just doesn't run".
|
|
140
|
+
// Force a single resolved copy of "mithril-runtime" — a
|
|
141
|
+
// `file:`-linked local package can otherwise resolve a
|
|
142
|
+
// second physical copy with its own module-level state
|
|
143
|
+
// (this exact class of bug bit the previous mithril-lynx:
|
|
144
|
+
// mithril's own emptyAttrs singleton, and that framework's
|
|
145
|
+
// own per-app render state).
|
|
450
146
|
try {
|
|
451
147
|
const appRequire = createRequire(path.join(process.cwd(), "package.json"));
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
// The bare specifier has to point at the entry FILE: aliasing it
|
|
455
|
-
// to the directory would bypass this package's own exports map
|
|
456
|
-
// (which has no "main" to fall back on) and fail to resolve.
|
|
457
|
-
// The prefix alias then keeps subpaths — "mithril-lynx/main-thread"
|
|
458
|
-
// and friends, which hold state of their own — on that same copy.
|
|
459
|
-
chain.resolve.alias.set("mithril-lynx$", path.join(selfDir, "src", "lynx-mithril-shim.js"));
|
|
460
|
-
chain.resolve.alias.set("mithril-lynx", selfDir);
|
|
461
|
-
}
|
|
148
|
+
const mithrilDir = path.dirname(appRequire.resolve("mithril-runtime/package.json"));
|
|
149
|
+
chain.resolve.alias.set("mithril-runtime", mithrilDir);
|
|
462
150
|
} catch {
|
|
463
|
-
// App
|
|
464
|
-
// consumed some other way) — leave resolution alone.
|
|
151
|
+
// App has no local "mithril-runtime" resolvable from its own root.
|
|
465
152
|
}
|
|
153
|
+
// Note: the old mithril-lynx also force-aliased its OWN
|
|
154
|
+
// package name here (a second copy would mean two
|
|
155
|
+
// disconnected renderers with separate module-level state).
|
|
156
|
+
// This framework's per-app state lives inside closures
|
|
157
|
+
// created by `renderApp()`/`setupRenderer()` calls, not
|
|
158
|
+
// module-level variables — the same class of bug can't
|
|
159
|
+
// reappear the same way, so this dedup isn't reproduced
|
|
160
|
+
// here. Revisit if a multi-copy scenario (npm link, a
|
|
161
|
+
// component library nesting its own copy) turns up the same
|
|
162
|
+
// symptom in practice.
|
|
466
163
|
|
|
467
164
|
const rawEntries = Object.entries(chain.entryPoints.entries() ?? {});
|
|
468
165
|
chain.entryPoints.clear();
|
|
@@ -474,45 +171,25 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
474
171
|
if (typeof mtSource !== "string") continue;
|
|
475
172
|
|
|
476
173
|
const dir = path.dirname(mtSource);
|
|
477
|
-
const bgSource = findSibling(dir,
|
|
174
|
+
const bgSource = findSibling(dir, ["background.ts", "background.js"]);
|
|
478
175
|
const cssSource = findSibling(dir, STYLE_CANDIDATES);
|
|
176
|
+
if (bgSource == null) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`[mithril-lynx] entry "${name}": no sibling background.ts/background.js found next to ${mtSource}. ` +
|
|
179
|
+
"mithril-lynx has exactly one rendering mode and it always needs a background entry — see the plan's §2 non-goals.",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
479
182
|
|
|
480
183
|
const bgEntry = `${name}__background`;
|
|
481
184
|
const mtEntry = `${name}__main-thread`;
|
|
482
185
|
const bgAsset = `.rspeedy/${name}/background.js`;
|
|
483
186
|
const mtAsset = `.rspeedy/${name}/main-thread.js`;
|
|
484
|
-
const hasBackground = bgSource != null;
|
|
485
|
-
// In dev with the in-bundle reload client, always materialize a
|
|
486
|
-
// background chunk — even for an app with no background.ts of its
|
|
487
|
-
// own — so the dev-reload client has somewhere to run. In
|
|
488
|
-
// production, or when the reload is driven from the Node process
|
|
489
|
-
// (devtool mode), keep the original behavior exactly (no
|
|
490
|
-
// background chunk at all when the app doesn't use one).
|
|
491
|
-
const includeBackground = hasBackground || (isDev && liveReload === true);
|
|
492
187
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
// The dev-reload client is imported by its resolved absolute path
|
|
497
|
-
// (plus the query string it reads its config from) rather than
|
|
498
|
-
// through the bare "mithril-lynx/dev-reload-client" specifier + an
|
|
499
|
-
// alias: the package-wide "mithril-lynx" prefix alias set above
|
|
500
|
-
// matches that specifier first regardless of registration order
|
|
501
|
-
// (webpack/rspack's resolve.alias picks the first matching entry,
|
|
502
|
-
// not the most specific one), which broke the import entirely in
|
|
503
|
-
// 0.0.7.
|
|
504
|
-
const bgImports = isDev && liveReload === true
|
|
505
|
-
? [
|
|
506
|
-
`${DEV_RELOAD_CLIENT_PATH}?${createDevReloadClientQuery(api, environment, name)}`,
|
|
507
|
-
...(hasBackground ? [bgSource] : []),
|
|
508
|
-
]
|
|
509
|
-
: bgSource;
|
|
510
|
-
chain.entry(bgEntry).add({
|
|
511
|
-
import: bgImports,
|
|
512
|
-
filename: bgAsset,
|
|
513
|
-
});
|
|
514
|
-
}
|
|
188
|
+
const bgImports = isDev && liveReload
|
|
189
|
+
? [`${DEV_RELOAD_CLIENT_PATH}?${createDevReloadClientQuery(api, environment, name)}`, bgSource]
|
|
190
|
+
: bgSource;
|
|
515
191
|
|
|
192
|
+
chain.entry(bgEntry).add({ import: bgImports, filename: bgAsset });
|
|
516
193
|
chain.entry(mtEntry).add({
|
|
517
194
|
import: cssSource != null ? [mtSource, cssSource] : [mtSource],
|
|
518
195
|
filename: mtAsset,
|
|
@@ -523,32 +200,41 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
523
200
|
...LynxTemplatePlugin.defaultOptions,
|
|
524
201
|
filename: `${name}.bundle`,
|
|
525
202
|
intermediate: `.rspeedy/${name}`,
|
|
526
|
-
chunks:
|
|
203
|
+
chunks: [bgEntry, mtEntry],
|
|
527
204
|
dsl: "react_nodiff",
|
|
528
205
|
targetSdkVersion,
|
|
529
206
|
cssPlugins: [],
|
|
530
207
|
},
|
|
531
208
|
]);
|
|
532
209
|
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
210
|
+
// --- F0.2 fix: the actual change this plugin exists to make ---
|
|
211
|
+
// v1: `test: new RegExp(`${name}/background\\.js$`)` matches
|
|
212
|
+
// the initial asset (`.rspeedy/<name>/background.js`, a
|
|
213
|
+
// nested PATH using a slash) but never a hot-update chunk
|
|
214
|
+
// (`<bgEntry>.<hash>.hot-update.js` — a FLAT file named
|
|
215
|
+
// after the webpack chunk NAME, with a double underscore,
|
|
216
|
+
// no slash). Verified against both real filename shapes
|
|
217
|
+
// with a plain regex test, not a live build, before
|
|
218
|
+
// writing this — see the plan's §8 F0.2 entry. Requiring
|
|
219
|
+
// `${name}[_/]` scopes the match to just this entry's own
|
|
220
|
+
// chunks (so a multi-entry app's OTHER pages' background
|
|
221
|
+
// chunks aren't double-wrapped by this instance) while
|
|
222
|
+
// still excluding the main-thread/lepus asset, which
|
|
223
|
+
// never contains "background".
|
|
224
|
+
chain.plugin(`runtime-wrapper-${name}`).use(
|
|
225
|
+
RuntimeWrapperWebpackPlugin,
|
|
226
|
+
[
|
|
227
|
+
{
|
|
228
|
+
targetSdkVersion,
|
|
229
|
+
test: new RegExp(`(^|/)${name}[_/].*background.*\\.js$`),
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
console.info(
|
|
235
|
+
`[mithril-lynx:build] entry="${name}" hmr=true liveReload=${liveReload} ` +
|
|
236
|
+
`bgEntry=${bgEntry} mtEntry=${mtEntry} targetSdk=${targetSdkVersion}`,
|
|
237
|
+
);
|
|
552
238
|
}
|
|
553
239
|
|
|
554
240
|
chain.plugin("encode").use(LynxEncodePlugin, []);
|
|
@@ -556,11 +242,6 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
556
242
|
chain.plugin("before-encode").use({
|
|
557
243
|
apply(compiler) {
|
|
558
244
|
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
|
559
|
-
// The default grouping only routes a chunk to lepus (main thread)
|
|
560
|
-
// when its asset carries `lynx:main-thread`. These hand-built
|
|
561
|
-
// entries don't, so main-thread JS lands in `manifest` and lepus
|
|
562
|
-
// stays empty. Re-map it here: background JS to manifest, the
|
|
563
|
-
// main-thread chunk to lepus. CSS is already grouped correctly.
|
|
564
245
|
const hooks = LynxTemplatePlugin.getLynxTemplatePluginHooks(compilation);
|
|
565
246
|
hooks.beforeEncode.tap(PLUGIN_NAME, (args) => {
|
|
566
247
|
const pageName = args.intermediate ? path.basename(args.intermediate) : "";
|
|
@@ -571,24 +252,14 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
571
252
|
|
|
572
253
|
const backgroundAsset = compilation.getAsset(bgAsset);
|
|
573
254
|
const mainThreadAsset = compilation.getAsset(mtAsset);
|
|
574
|
-
|
|
575
255
|
if (!mainThreadAsset) return args;
|
|
576
256
|
|
|
577
257
|
args.encodeData.compilerOptions.targetSdkVersion = targetSdkVersion;
|
|
578
258
|
args.encodeData.compilerOptions.enableEventRefactor = true;
|
|
579
|
-
|
|
580
|
-
// Route tap/gesture events through the refactored main-thread
|
|
581
|
-
// path so `__AddEventListener` handlers fire. This page-config
|
|
582
|
-
// flag was dropped from `@lynx-js/config-rsbuild-plugin` 0.2.0's
|
|
583
|
-
// schema, so set it on the page config directly.
|
|
584
259
|
args.encodeData.sourceContent.config.enableEventHandleRefactor = true;
|
|
585
260
|
|
|
586
261
|
args.encodeData.manifest = backgroundAsset
|
|
587
|
-
? {
|
|
588
|
-
[backgroundAsset.name]: backgroundAsset.source
|
|
589
|
-
.source()
|
|
590
|
-
.toString(),
|
|
591
|
-
}
|
|
262
|
+
? { [backgroundAsset.name]: backgroundAsset.source.source().toString() }
|
|
592
263
|
: {};
|
|
593
264
|
args.encodeData.lepusCode = {
|
|
594
265
|
root: mainThreadAsset,
|