mithril-lynx 0.0.7 → 0.0.8
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 +24 -7
- package/package.json +2 -1
- package/plugin.d.ts +14 -0
- package/plugin.js +241 -74
- package/src/dev-reload-client.js +0 -122
package/README.md
CHANGED
|
@@ -300,10 +300,27 @@ Tests run against `@lynx-js/testing-environment`'s jsdom-backed PAPI polyfill. T
|
|
|
300
300
|
|
|
301
301
|
**Not provided**: Fast Refresh and a devtools/inspector bundle (project plan, Phase 9 / subsystem 12) are explicitly out of scope — they're deep, compiler-driven DX features in upstream ReactLynx with no zero-compiler equivalent worth building.
|
|
302
302
|
|
|
303
|
-
In development, `pluginMithrilLynx()`
|
|
304
|
-
|
|
305
|
-
bundle
|
|
306
|
-
|
|
307
|
-
is
|
|
308
|
-
|
|
309
|
-
|
|
303
|
+
In development, `pluginMithrilLynx()` reloads the running page after every
|
|
304
|
+
successful rebuild instead of attempting module HMR — Mithril view code runs in
|
|
305
|
+
the main-thread/Lepus bundle, and rspack's hot runtime can only patch modules on
|
|
306
|
+
the background thread, so HMR could never reach it. (No synthetic background
|
|
307
|
+
entry is created for this: the reload runs from the dev-server process, not from
|
|
308
|
+
a chunk inside the app.)
|
|
309
|
+
|
|
310
|
+
The reload goes through the Lynx DevTool connector and sends CDP `Page.reload`
|
|
311
|
+
to the session currently serving your bundle. That reloads the existing page
|
|
312
|
+
**in place**, so nothing is pushed onto the viewer's back stack. Two consequences
|
|
313
|
+
worth knowing:
|
|
314
|
+
|
|
315
|
+
- **Live reload reaches the device over adb, not over the LAN.** The DevTool
|
|
316
|
+
connector talks to the adb server (it sets up an adb reverse tunnel to the
|
|
317
|
+
device's DebugRouter). `npm run dev` and the QR-scan flow are unaffected and
|
|
318
|
+
still work over Wi-Fi — only the automatic reload needs adb. With no adb
|
|
319
|
+
connection the rebuild logs a warning and you reload by hand. Wireless
|
|
320
|
+
debugging (`adb connect <ip>:5555`) satisfies this with no cable, and
|
|
321
|
+
`ANDROID_SERIAL` picks the device when several are attached.
|
|
322
|
+
- It is a **dev-server convenience, not a portable SDK API**: the connector is
|
|
323
|
+
imported lazily by a dev rebuild only, and no reload code is bundled into the
|
|
324
|
+
app in any build.
|
|
325
|
+
|
|
326
|
+
Turn it off with `pluginMithrilLynx({ liveReload: false })`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mithril-lynx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Mithril.js rendered through Lynx's Element PAPI \u2014 a contract-complete port of mithril/render/render.js@2.3.8 to the Lynx main thread, packaged as a reusable framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"test": "rstest run"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
|
+
"@lynx-js/devtool-connector": "^0.13.2",
|
|
90
91
|
"@lynx-js/runtime-wrapper-webpack-plugin": "^0.2.4",
|
|
91
92
|
"@lynx-js/template-webpack-plugin": "^0.16.0"
|
|
92
93
|
},
|
package/plugin.d.ts
CHANGED
|
@@ -6,6 +6,20 @@ import type { RsbuildPlugin } from "@lynx-js/rspeedy";
|
|
|
6
6
|
export interface PluginMithrilLynxOptions {
|
|
7
7
|
/** Lynx engine target SDK version. Defaults to "3.5". */
|
|
8
8
|
targetSdkVersion?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Opt back into module-level HMR. Defaults to false, which the plugin forces
|
|
11
|
+
* because rspack's hot runtime can only patch modules on the background
|
|
12
|
+
* thread, while Mithril view code always runs on the main thread — so HMR
|
|
13
|
+
* adds hot-update error noise without ever being able to update anything.
|
|
14
|
+
*/
|
|
15
|
+
hmr?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Reload the running page after each successful dev rebuild. Defaults to
|
|
18
|
+
* true. Reloading goes through the Lynx DevTool connector, which reaches the
|
|
19
|
+
* device over adb, so a device connected only over Wi-Fi must be reloaded
|
|
20
|
+
* manually.
|
|
21
|
+
*/
|
|
22
|
+
liveReload?: boolean;
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
/**
|
package/plugin.js
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { createRequire } from "node:module";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
21
|
|
|
23
22
|
import { RuntimeWrapperWebpackPlugin } from "@lynx-js/runtime-wrapper-webpack-plugin";
|
|
24
23
|
import { LynxEncodePlugin, LynxTemplatePlugin } from "@lynx-js/template-webpack-plugin";
|
|
@@ -28,53 +27,200 @@ const PLUGIN_NAME = "mithril-lynx-template-webpack";
|
|
|
28
27
|
const BACKGROUND_CANDIDATES = ["background.ts", "background.js"];
|
|
29
28
|
const STYLE_CANDIDATES = ["style.css"];
|
|
30
29
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Live reload (dev only)
|
|
32
|
+
//
|
|
33
|
+
// How a rebuild reaches the device, and why it works this way:
|
|
34
|
+
//
|
|
35
|
+
// - Real module HMR cannot help here at all. Mithril view code lives in the
|
|
36
|
+
// main-thread/Lepus chunk, and rspack's hot runtime can only patch modules in
|
|
37
|
+
// the registry it runs from — the background/JS thread. That is an
|
|
38
|
+
// architectural mismatch, not a bug to fix.
|
|
39
|
+
// - A CDP command the bundle sends to *itself*
|
|
40
|
+
// (NativeModules.LynxDevToolSetModule.invokeCdp) is a silent no-op: there is
|
|
41
|
+
// no external DevTool session behind it.
|
|
42
|
+
// - ExplorerModule.openSchema(url) — what 0.0.7 shipped — works, but it is a
|
|
43
|
+
// real navigation: Lynx Go starts a NEW LynxViewShellActivity every time and
|
|
44
|
+
// never finishes the one it replaces, so Back then steps through one frozen
|
|
45
|
+
// snapshot per reload.
|
|
46
|
+
// - Page.reload from an *external* DevTool session reloads the existing page in
|
|
47
|
+
// place (the DevTools reference notes the session URL is unchanged after it),
|
|
48
|
+
// so nothing new is pushed onto the back stack.
|
|
49
|
+
//
|
|
50
|
+
// So this runs from the Node dev-server process rather than from a chunk
|
|
51
|
+
// bundled into the app, and no synthetic background entry is needed.
|
|
52
|
+
//
|
|
53
|
+
// The trade-off: the DevTool connector reaches the device through adb, so live
|
|
54
|
+
// reload needs the device connected over adb. openSchema could work over Wi-Fi
|
|
55
|
+
// alone, but only by corrupting the back stack.
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/** Lynx Go's own shell page is a Lynx session too — never a reload target. */
|
|
59
|
+
const VIEWER_SHELL_BUNDLE = "homepage.lynx.bundle";
|
|
60
|
+
|
|
61
|
+
let connectorPromise;
|
|
62
|
+
let devtoolTransport;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lazily loads the DevTool connector. Kept lazy so `@lynx-js/devtool-connector`
|
|
66
|
+
* is only ever loaded by a dev rebuild, never by a production build.
|
|
67
|
+
*/
|
|
68
|
+
function getDevtoolConnector() {
|
|
69
|
+
if (!connectorPromise) {
|
|
70
|
+
connectorPromise = Promise.all([
|
|
71
|
+
import("@lynx-js/devtool-connector"),
|
|
72
|
+
import("@lynx-js/devtool-connector/transport"),
|
|
73
|
+
])
|
|
74
|
+
.then(([{ Connector }, { AndroidTransport }]) => {
|
|
75
|
+
devtoolTransport = new AndroidTransport();
|
|
76
|
+
return new Connector([devtoolTransport]);
|
|
77
|
+
})
|
|
78
|
+
.catch((error) => {
|
|
79
|
+
// Don't cache a rejection: the usual cause is the package not
|
|
80
|
+
// being installed yet, and the dev server outlives an
|
|
81
|
+
// `npm install`.
|
|
82
|
+
connectorPromise = undefined;
|
|
83
|
+
throw error;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return connectorPromise;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Last path segment of a URL, ignoring any query string or fragment. */
|
|
90
|
+
function bundleBasename(url) {
|
|
91
|
+
if (typeof url !== "string") return "";
|
|
92
|
+
const withoutQuery = url.split("?")[0].split("#")[0];
|
|
93
|
+
const segments = withoutQuery.split("/");
|
|
94
|
+
return segments[segments.length - 1] || withoutQuery;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isViewerShell(url) {
|
|
98
|
+
return bundleBasename(url) === VIEWER_SHELL_BUNDLE;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Adds a unique query parameter to a bundle URL.
|
|
103
|
+
*
|
|
104
|
+
* `Page.reload` on its own is not enough to pick up a rebuild: measured
|
|
105
|
+
* on-device, a reload without this re-fetched and re-ran the PREVIOUS bundle
|
|
106
|
+
* (the loaded template stayed byte-for-byte the old one, and the old text
|
|
107
|
+
* stayed on screen) even with `ignoreCache: true`. Both the HTTP layer and
|
|
108
|
+
* Lynx's own bytecode cache are keyed by URL, so changing the URL is what
|
|
109
|
+
* actually invalidates them. The session's own URL is unaffected — the DevTools
|
|
110
|
+
* reference notes it does not change after a reload, and that was confirmed
|
|
111
|
+
* here too.
|
|
112
|
+
*
|
|
113
|
+
* Returns undefined for anything that isn't an http(s) URL, in which case the
|
|
114
|
+
* caller lets Page.reload use the URL it already has (it rejects anything else).
|
|
115
|
+
*/
|
|
116
|
+
export function cacheBustedUrl(url, now = Date.now()) {
|
|
117
|
+
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) return undefined;
|
|
118
|
+
const [base, query = ""] = url.split("?");
|
|
119
|
+
const params = new URLSearchParams(query);
|
|
120
|
+
params.set("t", String(now));
|
|
121
|
+
return `${base}?${params.toString()}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function matchesHint(url, hints) {
|
|
125
|
+
if (typeof url !== "string" || hints.length === 0) return false;
|
|
126
|
+
return hints.some((hint) => url.includes(hint));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Chooses which client/session to reload: `targets` is `[{ client, sessions }]`,
|
|
131
|
+
* returns `{ clientId, sessionId, url }` or null.
|
|
132
|
+
*
|
|
133
|
+
* Pure and exported so it can be tested without a device attached.
|
|
134
|
+
*/
|
|
135
|
+
export function pickReloadTarget(targets, { bundleHints = [] } = {}) {
|
|
136
|
+
const candidates = [];
|
|
137
|
+
for (const { client, sessions } of targets) {
|
|
138
|
+
for (const session of sessions ?? []) {
|
|
139
|
+
if (session?.type !== "lynx") continue;
|
|
140
|
+
if (isViewerShell(session.url)) continue;
|
|
141
|
+
candidates.push({ clientId: client.id, session });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (candidates.length === 0) return null;
|
|
145
|
+
|
|
146
|
+
// Prefer the session actually serving one of this app's bundles over
|
|
147
|
+
// "whatever was opened most recently": with a second Lynx app, or a second
|
|
148
|
+
// attached device, the newest session need not be ours.
|
|
149
|
+
const preferred = candidates.filter((candidate) => matchesHint(candidate.session.url, bundleHints));
|
|
150
|
+
const pool = preferred.length > 0 ? preferred : candidates;
|
|
151
|
+
|
|
152
|
+
const latest = pool.reduce((a, b) => (b.session.session_id > a.session.session_id ? b : a));
|
|
153
|
+
return {
|
|
154
|
+
clientId: latest.clientId,
|
|
155
|
+
sessionId: latest.session.session_id,
|
|
156
|
+
url: latest.session.url,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
161
|
+
|
|
162
|
+
async function listClientSessions(connector) {
|
|
163
|
+
const clients = await connector.listClients();
|
|
164
|
+
const targets = [];
|
|
165
|
+
for (const client of clients) {
|
|
166
|
+
try {
|
|
167
|
+
targets.push({ client, sessions: await connector.sendListSessionMessage(client.id) });
|
|
168
|
+
} catch {
|
|
169
|
+
// This client doesn't support session listing, or isn't ready yet.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return targets;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Finds a session worth reloading, retrying briefly: on the very first rebuild
|
|
177
|
+
* the DevTool client may not have registered with the device yet.
|
|
178
|
+
*/
|
|
179
|
+
async function findReloadTarget(bundleHints, { attempts = 3, delayMs = 400 } = {}) {
|
|
180
|
+
const connector = await getDevtoolConnector();
|
|
181
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
182
|
+
const target = pickReloadTarget(await listClientSessions(connector), { bundleHints });
|
|
183
|
+
if (target) return { connector, target };
|
|
184
|
+
if (attempt < attempts - 1) await sleep(delayMs);
|
|
185
|
+
}
|
|
186
|
+
return { connector, target: null };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Reloads the running page in place. Returns true when a session was reloaded,
|
|
191
|
+
* false when none was found (the caller logs that).
|
|
192
|
+
*/
|
|
193
|
+
async function reloadViaDevtool(bundleHints) {
|
|
194
|
+
const { connector, target } = await findReloadTarget(bundleHints);
|
|
195
|
+
if (!target) return false;
|
|
196
|
+
|
|
197
|
+
const params = { ignoreCache: true };
|
|
198
|
+
// Without a changed URL the device replays its cached copy of the previous
|
|
199
|
+
// bundle — see cacheBustedUrl(). The session URL itself does not change.
|
|
200
|
+
const url = cacheBustedUrl(target.url);
|
|
201
|
+
if (url != null) params.url = url;
|
|
202
|
+
|
|
203
|
+
await connector.sendCDPMessage(target.clientId, target.sessionId, "Page.reload", params);
|
|
204
|
+
console.info(
|
|
205
|
+
`[mithril-lynx] Reloaded ${target.url || "(url unknown)"} (session ${target.sessionId}${url != null ? "" : ", no cache-busting: non-http url"}).`,
|
|
64
206
|
);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Releases the adb connection when the dev server goes away. */
|
|
211
|
+
async function closeDevtoolTransport() {
|
|
212
|
+
const transport = devtoolTransport;
|
|
213
|
+
devtoolTransport = undefined;
|
|
214
|
+
connectorPromise = undefined;
|
|
215
|
+
await transport?.close?.();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Turns a connector failure into an actionable one-liner. */
|
|
219
|
+
function describeReloadFailure(error) {
|
|
220
|
+
if (error?.code === "ERR_MODULE_NOT_FOUND" || /devtool-connector/.test(error?.message ?? "")) {
|
|
221
|
+
return "Live reload is off: @lynx-js/devtool-connector is not installed. Run `npm install @lynx-js/devtool-connector`, then restart the dev server.";
|
|
222
|
+
}
|
|
223
|
+
return `Live reload unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
78
224
|
}
|
|
79
225
|
|
|
80
226
|
function findSibling(dir, candidates) {
|
|
@@ -110,6 +256,12 @@ function packageRootOf(resolvedFile) {
|
|
|
110
256
|
export function pluginMithrilLynx(options = {}) {
|
|
111
257
|
const targetSdkVersion = options.targetSdkVersion ?? "3.5";
|
|
112
258
|
const hmr = options.hmr ?? false;
|
|
259
|
+
const liveReload = options.liveReload ?? true;
|
|
260
|
+
|
|
261
|
+
// Filled in by modifyBundlerChain below with "<entry>.bundle" for every
|
|
262
|
+
// configured entry, so a reload prefers the session actually serving this
|
|
263
|
+
// app over whichever Lynx session happens to be newest.
|
|
264
|
+
const bundleHints = new Set();
|
|
113
265
|
|
|
114
266
|
return {
|
|
115
267
|
name: PLUGIN_NAME,
|
|
@@ -123,10 +275,10 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
123
275
|
// HMR's eval'd *.hot-update.js chunks also aren't runtime-wrapped
|
|
124
276
|
// the way the real background.js bundle is, and fail native-side
|
|
125
277
|
// with "ReferenceError: exports is not defined" if hot is left on.
|
|
126
|
-
// Force dev.hmr off (unless the app explicitly set it)
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
278
|
+
// Force dev.hmr off (unless the app explicitly set it) -- real HMR
|
|
279
|
+
// can't reach this framework's app code regardless of how reload
|
|
280
|
+
// itself is triggered (see reloadViaDevtool() above), so leaving it
|
|
281
|
+
// on only adds that error noise for no benefit.
|
|
130
282
|
api.modifyRsbuildConfig({
|
|
131
283
|
// Not a plain default: Rsbuild has already stamped dev.hmr:true onto
|
|
132
284
|
// the config by the time ANY hook sees it (even api.getRsbuildConfig
|
|
@@ -139,7 +291,35 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
139
291
|
order: "post",
|
|
140
292
|
});
|
|
141
293
|
|
|
142
|
-
|
|
294
|
+
// Live reload itself: on every successful dev rebuild (skipping the
|
|
295
|
+
// first, which is the initial build rather than an edit), find the
|
|
296
|
+
// running Lynx session and CDP-reload it in place. See the block
|
|
297
|
+
// above reloadViaDevtool() for why this runs from here (the Node
|
|
298
|
+
// dev-server process) instead of from a chunk bundled into the app.
|
|
299
|
+
if (liveReload) {
|
|
300
|
+
api.onAfterDevCompile(async ({ isFirstCompile, stats }) => {
|
|
301
|
+
if (isFirstCompile || stats.hasErrors()) return;
|
|
302
|
+
let reloaded = false;
|
|
303
|
+
try {
|
|
304
|
+
reloaded = await reloadViaDevtool([...bundleHints]);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.warn(`[mithril-lynx] ${describeReloadFailure(error)}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!reloaded) {
|
|
310
|
+
console.warn(
|
|
311
|
+
"[mithril-lynx] Live reload unavailable: no Lynx session found for this app. " +
|
|
312
|
+
"Is the device connected over adb with the page open in Lynx Go? Reload manually.",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// The transport owns adb port-forwards; don't let them outlive
|
|
318
|
+
// the dev server.
|
|
319
|
+
api.onCloseDevServer?.(closeDevtoolTransport);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
api.modifyBundlerChain((chain) => {
|
|
143
323
|
// mithril-lynx's own src/lynx-mithril-shim.js deep-imports mithril's
|
|
144
324
|
// internal render/cachedAttrsIsStaticMap.js (and its emptyAttrs
|
|
145
325
|
// singleton). If the app's own `require("mithril")` resolves to a
|
|
@@ -211,30 +391,12 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
211
391
|
const bgAsset = `.rspeedy/${name}/background.js`;
|
|
212
392
|
const mtAsset = `.rspeedy/${name}/main-thread.js`;
|
|
213
393
|
const hasBackground = bgSource != null;
|
|
214
|
-
// In dev, always materialize a background chunk — even for an app
|
|
215
|
-
// with no background.ts of its own — so live reload's client has
|
|
216
|
-
// somewhere to run. In production, keep the original behavior
|
|
217
|
-
// exactly (no background chunk at all when the app doesn't use one).
|
|
218
|
-
const includeBackground = hasBackground || isDev;
|
|
219
394
|
|
|
220
395
|
// Each entry always has main-thread code and may opt into a
|
|
221
396
|
// background thread by adding a sibling background.ts file.
|
|
222
|
-
if (
|
|
223
|
-
// Imported by its resolved absolute path (plus the query string
|
|
224
|
-
// the client reads its config from) rather than through the bare
|
|
225
|
-
// "mithril-lynx/dev-reload-client" specifier + an alias: the
|
|
226
|
-
// package-wide "mithril-lynx" prefix alias set above matches that
|
|
227
|
-
// specifier first regardless of registration order (webpack/
|
|
228
|
-
// rspack's resolve.alias picks the first matching entry, not the
|
|
229
|
-
// most specific one), which broke the import entirely.
|
|
230
|
-
const bgImports = isDev
|
|
231
|
-
? [
|
|
232
|
-
`${DEV_RELOAD_CLIENT_PATH}?${createDevReloadClientQuery(api, environment, name)}`,
|
|
233
|
-
...(hasBackground ? [bgSource] : []),
|
|
234
|
-
]
|
|
235
|
-
: bgSource;
|
|
397
|
+
if (hasBackground) {
|
|
236
398
|
chain.entry(bgEntry).add({
|
|
237
|
-
import:
|
|
399
|
+
import: bgSource,
|
|
238
400
|
filename: bgAsset,
|
|
239
401
|
});
|
|
240
402
|
}
|
|
@@ -249,14 +411,19 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
249
411
|
...LynxTemplatePlugin.defaultOptions,
|
|
250
412
|
filename: `${name}.bundle`,
|
|
251
413
|
intermediate: `.rspeedy/${name}`,
|
|
252
|
-
chunks:
|
|
414
|
+
chunks: hasBackground ? [bgEntry, mtEntry] : [mtEntry],
|
|
253
415
|
dsl: "react_nodiff",
|
|
254
416
|
targetSdkVersion,
|
|
255
417
|
cssPlugins: [],
|
|
256
418
|
},
|
|
257
419
|
]);
|
|
258
420
|
|
|
259
|
-
|
|
421
|
+
// The bundle this entry produces is always "<name>.bundle"
|
|
422
|
+
// (the filename above), and a loaded session's URL ends with
|
|
423
|
+
// it — that is what lets a reload pick out this app's session.
|
|
424
|
+
bundleHints.add(`${name}.bundle`);
|
|
425
|
+
|
|
426
|
+
if (hasBackground) {
|
|
260
427
|
// Background chunks run in the JavaScript thread and need the
|
|
261
428
|
// Lynx runtime wrapper; main-thread chunks are encoded as lepus.
|
|
262
429
|
chain.plugin(`runtime-wrapper-${name}`).use(
|
package/src/dev-reload-client.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// Development-only client for Lynx Go.
|
|
2
|
-
//
|
|
3
|
-
// Mithril view code runs in the main-thread/Lepus bundle, while WebSocket and
|
|
4
|
-
// Native Module access only exist in the background JS bundle. Consequently
|
|
5
|
-
// normal module HMR cannot patch the view code. On a successful rebuild this
|
|
6
|
-
// client asks Lynx Go's ExplorerModule to load the same bundle URL again.
|
|
7
|
-
|
|
8
|
-
function parseResourceQuery(query) {
|
|
9
|
-
const values = {};
|
|
10
|
-
if (typeof query !== "string" || !query.startsWith("?")) return values;
|
|
11
|
-
for (const pair of query.slice(1).split("&")) {
|
|
12
|
-
const index = pair.indexOf("=");
|
|
13
|
-
const key = index === -1 ? pair : pair.slice(0, index);
|
|
14
|
-
const value = index === -1 ? "" : pair.slice(index + 1);
|
|
15
|
-
values[key] = decodeURIComponent(value);
|
|
16
|
-
}
|
|
17
|
-
return values;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function socketURL(options) {
|
|
21
|
-
const hostname = options.hostname || "";
|
|
22
|
-
const port = options.port ? `:${options.port}` : "";
|
|
23
|
-
const pathname = options.pathname || "/rsbuild-hmr";
|
|
24
|
-
const token = options.token ? `?token=${encodeURIComponent(options.token)}` : "";
|
|
25
|
-
return `${options.protocol || "ws"}://${hostname}${port}${pathname}${token}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const options = parseResourceQuery(__resourceQuery);
|
|
29
|
-
const bundleUrl = options["bundle-url"];
|
|
30
|
-
let currentHash;
|
|
31
|
-
let initialBuild = true;
|
|
32
|
-
let socket;
|
|
33
|
-
// ExplorerModule.openSchema(url) makes Lynx Go fully reload — including
|
|
34
|
-
// re-executing this very module from scratch. That would naturally replace
|
|
35
|
-
// this stale instance's socket with a fresh one, except the OLD connection
|
|
36
|
-
// is otherwise left open: confirmed on-device that every earlier reload's
|
|
37
|
-
// socket stays alive and keeps receiving server broadcasts, so a build a few
|
|
38
|
-
// reloads in fires openSchema several times at once (one call per still-open
|
|
39
|
-
// stale socket), and the resulting navigations race and clobber each other —
|
|
40
|
-
// only the first reload after a fresh QR/URL load (a single open socket)
|
|
41
|
-
// reliably lands. Closing this socket ourselves right before reloading, and
|
|
42
|
-
// suppressing the reconnect-on-close it would otherwise trigger, keeps
|
|
43
|
-
// exactly one socket alive at a time.
|
|
44
|
-
let reloading = false;
|
|
45
|
-
|
|
46
|
-
console.info(
|
|
47
|
-
"[mithril-lynx] ExplorerModule.openSchema:",
|
|
48
|
-
typeof NativeModules !== "undefined" && typeof NativeModules.ExplorerModule?.openSchema,
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
function reload() {
|
|
52
|
-
const openSchema =
|
|
53
|
-
typeof NativeModules !== "undefined" && NativeModules.ExplorerModule?.openSchema;
|
|
54
|
-
if (typeof openSchema !== "function") {
|
|
55
|
-
console.warn("[mithril-lynx] Live reload unavailable: ExplorerModule.openSchema was not found.");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (!bundleUrl) {
|
|
59
|
-
console.warn("[mithril-lynx] Live reload unavailable: the bundle URL was not configured.");
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
console.info("[mithril-lynx] Reloading updated bundle.");
|
|
63
|
-
reloading = true;
|
|
64
|
-
socket?.close();
|
|
65
|
-
openSchema.call(NativeModules.ExplorerModule, bundleUrl);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function handleMessage(rawMessage) {
|
|
69
|
-
let message;
|
|
70
|
-
try {
|
|
71
|
-
message = JSON.parse(rawMessage);
|
|
72
|
-
} catch {
|
|
73
|
-
console.warn("[mithril-lynx] Ignoring an invalid dev-server message.");
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
switch (message.type) {
|
|
78
|
-
case "hash":
|
|
79
|
-
currentHash = message.data;
|
|
80
|
-
break;
|
|
81
|
-
case "ok":
|
|
82
|
-
// The server sends hash + ok immediately after connecting. Reloading
|
|
83
|
-
// then would loop forever, so only act after a later successful build.
|
|
84
|
-
if (initialBuild) initialBuild = false;
|
|
85
|
-
else if (currentHash) reload();
|
|
86
|
-
break;
|
|
87
|
-
case "still-ok":
|
|
88
|
-
console.info("[mithril-lynx] Nothing changed.");
|
|
89
|
-
break;
|
|
90
|
-
case "warnings":
|
|
91
|
-
console.warn("[mithril-lynx] Build completed with warnings.", message.data);
|
|
92
|
-
if (!message.params?.preventReloading && !initialBuild) reload();
|
|
93
|
-
break;
|
|
94
|
-
case "errors":
|
|
95
|
-
console.warn("[mithril-lynx] Build failed; waiting for the next successful build.", message.data);
|
|
96
|
-
break;
|
|
97
|
-
case "invalid":
|
|
98
|
-
console.info("[mithril-lynx] App updated. Recompiling...");
|
|
99
|
-
break;
|
|
100
|
-
case "error":
|
|
101
|
-
console.error("[mithril-lynx] Dev server error:", message.data);
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function connect(retries = 0) {
|
|
107
|
-
socket = new WebSocket(socketURL(options));
|
|
108
|
-
socket.onmessage = (event) => handleMessage(event.data);
|
|
109
|
-
socket.onerror = (error) => console.error("[mithril-lynx] Dev server connection error:", error);
|
|
110
|
-
socket.onclose = () => {
|
|
111
|
-
if (reloading) return; // Deliberately closed by reload() — a fresh socket is on its way already.
|
|
112
|
-
if (retries >= 10) {
|
|
113
|
-
console.error("[mithril-lynx] Unable to reconnect to the dev server.");
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const delay = 1000 * 2 ** retries + Math.random() * 100;
|
|
117
|
-
console.info("[mithril-lynx] Dev server disconnected; reconnecting...");
|
|
118
|
-
setTimeout(() => connect(retries + 1), delay);
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
connect();
|