@webjsdev/server 0.9.0 → 0.10.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/README.md +4 -1
- package/index.d.ts +793 -125
- package/index.js +8 -4
- package/package.json +3 -3
- package/src/action-flash.js +185 -0
- package/src/action-params.js +156 -0
- package/src/action-route.js +8 -9
- package/src/action-seed.js +44 -5
- package/src/actions.js +97 -15
- package/src/api.js +67 -10
- package/src/app-layout.js +133 -0
- package/src/asset-hash.js +82 -49
- package/src/auth.js +612 -108
- package/src/browser-entries.js +2 -17
- package/src/build-epoch.js +1 -1
- package/src/cache-tags.js +5 -1
- package/src/check/rules-actions.js +37 -1
- package/src/check/rules-components.js +114 -40
- package/src/check/rules-imports.js +175 -4
- package/src/check/rules-route-modules.js +200 -0
- package/src/check/rules-routes.js +55 -0
- package/src/check/rules-routing.js +143 -95
- package/src/check/rules-typescript.js +24 -2
- package/src/check/rules.js +104 -9
- package/src/check/runner-support.js +53 -171
- package/src/check/runner.js +22 -6
- package/src/client-signals.js +171 -0
- package/src/component-scanner.js +22 -16
- package/src/context.js +152 -16
- package/src/cookie-store.js +269 -0
- package/src/core-exports.js +190 -0
- package/src/csrf.js +21 -3
- package/src/dev/bun-app-source.js +95 -5
- package/src/dev/config.js +7 -51
- package/src/dev/dev-service-worker.js +48 -0
- package/src/dev/esm-hot-hooks.js +373 -0
- package/src/dev/handler.js +502 -155
- package/src/dev/helpers.js +127 -96
- package/src/dev/hot-host.js +33 -6
- package/src/dev/poke-state.js +110 -0
- package/src/dev/serve.js +177 -129
- package/src/dev/server.js +92 -33
- package/src/dev/specifier-versions.js +125 -0
- package/src/dev/watch-ignore.js +49 -4
- package/src/dev/watch-poll.js +78 -0
- package/src/dev/watch-tree.js +59 -22
- package/src/dev-classify.js +59 -26
- package/src/dev-error.js +104 -3
- package/src/dev-hot-classify.js +291 -0
- package/src/dev-hot-client.js +245 -0
- package/src/dev-import.js +22 -5
- package/src/dev-overlay.js +26 -0
- package/src/dev-ready-probe.js +69 -0
- package/src/dev-regenerate.js +2 -2
- package/src/dev-reload-hold.js +49 -20
- package/src/dev-reload-reason.js +61 -0
- package/src/dev-reload-worker.js +62 -13
- package/src/dev-secret.js +73 -0
- package/src/dev-source-locations-hook.js +4 -1
- package/src/dev-source-locations.js +33 -2
- package/src/dev.js +0 -1
- package/src/env-files.js +124 -0
- package/src/file-storage.js +2 -2
- package/src/foreign-imports.js +228 -0
- package/src/form-action-identity.js +52 -25
- package/src/form-dispatch.js +142 -66
- package/src/html-cache.js +35 -4
- package/src/importmap.js +2 -2
- package/src/instrumentation.js +59 -3
- package/src/js-scan.js +5 -4
- package/src/jwe.js +155 -0
- package/src/lazy-components.js +166 -0
- package/src/listener-core.js +4 -2
- package/src/lit-hints.js +172 -0
- package/src/middleware-load.js +248 -0
- package/src/module-graph.js +73 -47
- package/src/module-side-effect.js +284 -0
- package/src/public-files.js +73 -0
- package/src/redirect-location.js +19 -0
- package/src/redirects.js +9 -1
- package/src/route-frontier.js +151 -0
- package/src/route-report.js +130 -0
- package/src/router.js +120 -13
- package/src/session.js +32 -10
- package/src/ssr/client-router-flag.js +1 -1
- package/src/ssr/document.js +37 -13
- package/src/ssr/head.js +78 -23
- package/src/ssr/preloads.js +31 -45
- package/src/ssr/render.js +241 -55
- package/src/ssr/responses.js +51 -4
- package/src/ssr/shipped-modules.js +22 -16
- package/src/testing.d.ts +71 -2
- package/src/testing.js +103 -21
- package/src/thenable-params.js +26 -0
- package/src/vendor/jspm.js +12 -1
- package/src/vendor/pins.js +9 -8
- package/src/vendor/scanner.js +14 -9
- package/src/vendor.js +5 -3
- package/webjs-config.schema.json +1 -6
- package/src/component-elision.js +0 -1828
- package/src/elision-differential.js +0 -101
- package/src/elision-report.js +0 -193
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev component hot swap, BROWSER half (#1689). A browser-safe,
|
|
3
|
+
* import-free module that `reloadClientJs` (`dev/helpers.js`) inlines into
|
|
4
|
+
* `/__webjs/reload.js` after an `export`-keyword strip, like `dev-styles.js`,
|
|
5
|
+
* so the browser test drives the exact code that ships. Production never loads
|
|
6
|
+
* it, so `@webjsdev/core`'s `hot.js` reads find nothing there.
|
|
7
|
+
*
|
|
8
|
+
* `installHotClient(globalThis)` publishes `{ strings, replace, applyModules }`
|
|
9
|
+
* on `globalThis[Symbol.for('webjs.hot')]`, which core reads:
|
|
10
|
+
*
|
|
11
|
+
* - `strings(arr, kind)` interns template strings by their text, so the
|
|
12
|
+
* re-evaluated module's unchanged templates keep the identity the reconciler
|
|
13
|
+
* compares (`prev.strings === tr.strings`) and update in place, keeping
|
|
14
|
+
* their DOM (focus, an input's value, a nested component), while a template
|
|
15
|
+
* whose text changed rebuilds.
|
|
16
|
+
* - `replace(tag, Old, New)` is what core's `customElements.define` wrapper
|
|
17
|
+
* calls on a second define of a known tag. The native registry keeps the
|
|
18
|
+
* class and the lifecycle callbacks it read at define time, so the new
|
|
19
|
+
* class's members are copied onto the REGISTERED one, level by level up to
|
|
20
|
+
* the first class both chains share, and every live instance re-renders.
|
|
21
|
+
* Reactive values live per instance in `__propValues` and instance signals in
|
|
22
|
+
* fields, so both survive. A change the native registry would ignore throws
|
|
23
|
+
* before anything is patched, and the reload client reloads with the reason.
|
|
24
|
+
* - `applyModules(urls)` imports the versioned component modules a
|
|
25
|
+
* `component` verdict lists, in order, and reports the tags it swapped.
|
|
26
|
+
*
|
|
27
|
+
* The reload script runs before the page's boot script (`ssr/head.js`), so the
|
|
28
|
+
* interner sees every template from the first evaluation on.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Callbacks the native registry captures at define time. */
|
|
32
|
+
const HOT_NATIVE_CALLBACKS = [
|
|
33
|
+
'connectedCallback', 'disconnectedCallback', 'adoptedCallback', 'attributeChangedCallback',
|
|
34
|
+
'connectedMoveCallback', 'formAssociatedCallback', 'formResetCallback', 'formDisabledCallback',
|
|
35
|
+
'formStateRestoreCallback',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** Statics that are part of a class's identity or compared by a guard, never copied. */
|
|
39
|
+
const HOT_FIXED_STATICS = ['prototype', 'name', 'length', 'properties', 'elementProperties', 'elementStyles', 'observedAttributes', 'finalized'];
|
|
40
|
+
|
|
41
|
+
/** @param {any} C */
|
|
42
|
+
function hotChain(C) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (let c = C; c && c !== Function.prototype; c = Object.getPrototypeOf(c)) out.push(c);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {unknown} v */
|
|
49
|
+
function hotText(v) {
|
|
50
|
+
if (v === undefined) return '';
|
|
51
|
+
if (typeof v === 'function') return String(v);
|
|
52
|
+
if (v && typeof v === 'object') {
|
|
53
|
+
const o = /** @type {Record<string, unknown>} */ (v);
|
|
54
|
+
return '{' + Object.keys(o).sort().map((k) => k + ':' + hotText(o[k])).join(',') + '}';
|
|
55
|
+
}
|
|
56
|
+
try { return JSON.stringify(v); } catch (_) { return String(v); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The declared reactive property shape of a class, as one comparable string,
|
|
61
|
+
* plus the names (for the patch to skip their prototype accessors).
|
|
62
|
+
* @param {any} C
|
|
63
|
+
*/
|
|
64
|
+
function hotShape(C) {
|
|
65
|
+
/** @type {Record<string, unknown>} */
|
|
66
|
+
const all = {};
|
|
67
|
+
for (const c of hotChain(C).reverse()) {
|
|
68
|
+
if (Object.prototype.hasOwnProperty.call(c, 'properties') && c.properties && typeof c.properties === 'object') Object.assign(all, c.properties);
|
|
69
|
+
}
|
|
70
|
+
if (C.elementProperties instanceof Map) for (const [k, v] of C.elementProperties) all[k] = v;
|
|
71
|
+
const names = Object.keys(all).sort();
|
|
72
|
+
const text = names.map((name) => {
|
|
73
|
+
const raw = all[name];
|
|
74
|
+
const d = /** @type {any} */ (raw && typeof raw === 'object' ? raw : { type: raw });
|
|
75
|
+
return [name, d.type && d.type.name, hotText(d.attribute), !!d.reflect, !!d.state, !!d.noAccessor,
|
|
76
|
+
hotText(d.converter), hotText(d.hasChanged), hotText(d.default)].join('|');
|
|
77
|
+
}).join('\n');
|
|
78
|
+
return { names, text };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @param {any} C */
|
|
82
|
+
function hotObserved(C) {
|
|
83
|
+
try { return [...(C.observedAttributes || [])].map(String).sort().join(','); } catch (_) { return '?'; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @param {unknown} s @returns {CSSStyleSheet | null} */
|
|
87
|
+
function hotSheet(s) {
|
|
88
|
+
if (typeof CSSStyleSheet !== 'undefined' && s instanceof CSSStyleSheet) return s;
|
|
89
|
+
const o = /** @type {any} */ (s);
|
|
90
|
+
if (o && typeof CSSStyleSheet !== 'undefined' && o.styleSheet instanceof CSSStyleSheet) return o.styleSheet;
|
|
91
|
+
if (o && typeof o.text === 'string' && typeof CSSStyleSheet !== 'undefined') {
|
|
92
|
+
const sheet = new CSSStyleSheet();
|
|
93
|
+
sheet.replaceSync(o.text);
|
|
94
|
+
return sheet;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Every element in `root` (and in each open shadow root below it) that is an
|
|
101
|
+
* instance of `C`, including instances of a subclass.
|
|
102
|
+
* @param {Document | ShadowRoot | Element} root
|
|
103
|
+
* @param {Function} C
|
|
104
|
+
* @param {Element[]} [out]
|
|
105
|
+
*/
|
|
106
|
+
function hotInstances(root, C, out = []) {
|
|
107
|
+
for (const el of root.querySelectorAll('*')) {
|
|
108
|
+
if (el instanceof C) out.push(el);
|
|
109
|
+
if (el.shadowRoot) hotInstances(el.shadowRoot, C, out);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The reason `New` cannot be patched onto the registered `Old`, or null.
|
|
116
|
+
* @param {any} Old
|
|
117
|
+
* @param {any} New
|
|
118
|
+
* @returns {{ why: string, depth: number }}
|
|
119
|
+
*/
|
|
120
|
+
function hotCheck(Old, New) {
|
|
121
|
+
const oldChain = hotChain(Old);
|
|
122
|
+
const newChain = hotChain(New);
|
|
123
|
+
const depth = newChain.findIndex((c) => oldChain.includes(c));
|
|
124
|
+
if (depth === -1 || oldChain.indexOf(newChain[depth]) !== depth) return { why: 'class-chain-changed', depth };
|
|
125
|
+
if (hotShape(Old).text !== hotShape(New).text) return { why: 'reactive-properties-changed', depth };
|
|
126
|
+
if (hotObserved(Old) !== hotObserved(New)) return { why: 'observed-attributes-changed', depth };
|
|
127
|
+
if (Old.shadow !== New.shadow) return { why: 'static-shadow-changed', depth };
|
|
128
|
+
if (Old.formAssociated !== New.formAssociated) return { why: 'form-associated-changed', depth };
|
|
129
|
+
for (let i = 0; i < depth; i++) {
|
|
130
|
+
for (const cb of HOT_NATIVE_CALLBACKS) {
|
|
131
|
+
const o = Object.getOwnPropertyDescriptor(oldChain[i].prototype, cb);
|
|
132
|
+
const n = Object.getOwnPropertyDescriptor(newChain[i].prototype, cb);
|
|
133
|
+
if (!!o !== !!n || (o && n && String(o.value) !== String(n.value))) return { why: cb + '-changed', depth };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { why: '', depth };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Copy `from`'s own members onto `to`, then drop the ones `from` no longer
|
|
141
|
+
* has. `skip` names members that stay as they are.
|
|
142
|
+
* @param {any} to
|
|
143
|
+
* @param {any} from
|
|
144
|
+
* @param {(key: string | symbol) => boolean} skip
|
|
145
|
+
* @param {(key: string | symbol) => boolean} keep members never deleted
|
|
146
|
+
*/
|
|
147
|
+
function hotCopy(to, from, skip, keep) {
|
|
148
|
+
for (const key of Reflect.ownKeys(from)) {
|
|
149
|
+
if (skip(key)) continue;
|
|
150
|
+
const d = Object.getOwnPropertyDescriptor(from, key);
|
|
151
|
+
if (d) Object.defineProperty(to, key, d);
|
|
152
|
+
}
|
|
153
|
+
for (const key of Reflect.ownKeys(to)) {
|
|
154
|
+
if (skip(key) || keep(key) || Object.prototype.hasOwnProperty.call(from, key)) continue;
|
|
155
|
+
delete to[key];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Install the dev hot client on `g` (normally `globalThis`). Idempotent.
|
|
161
|
+
* @param {any} g
|
|
162
|
+
*/
|
|
163
|
+
export function installHotClient(g) {
|
|
164
|
+
const KEY = Symbol.for('webjs.hot');
|
|
165
|
+
if (g[KEY]) return g[KEY];
|
|
166
|
+
/** @type {WeakMap<object, any>} */
|
|
167
|
+
const canonical = new WeakMap();
|
|
168
|
+
/** @type {Map<string, any>} */
|
|
169
|
+
const byText = new Map();
|
|
170
|
+
/** @type {string[] | null} */
|
|
171
|
+
let swapped = null;
|
|
172
|
+
|
|
173
|
+
/** @param {any} arr @param {number} kind */
|
|
174
|
+
function strings(arr, kind) {
|
|
175
|
+
const hit = canonical.get(arr);
|
|
176
|
+
if (hit) return hit;
|
|
177
|
+
const key = kind + '\u0000' + Array.prototype.join.call(arr, '\u0001');
|
|
178
|
+
let c = byText.get(key);
|
|
179
|
+
if (!c) byText.set(key, (c = arr));
|
|
180
|
+
canonical.set(arr, c);
|
|
181
|
+
return c;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @param {string} tag @param {any} Old @param {any} New */
|
|
185
|
+
function replace(tag, Old, New) {
|
|
186
|
+
const { why, depth } = hotCheck(Old, New);
|
|
187
|
+
if (why) throw new Error('[webjs] hot swap of <' + tag + '> needs a reload: ' + why);
|
|
188
|
+
const props = new Set(hotShape(Old).names);
|
|
189
|
+
const oldChain = hotChain(Old);
|
|
190
|
+
const newChain = hotChain(New);
|
|
191
|
+
for (let i = 0; i < depth; i++) {
|
|
192
|
+
hotCopy(oldChain[i].prototype, newChain[i].prototype,
|
|
193
|
+
(k) => k === 'constructor' || props.has(/** @type {string} */ (k)) || HOT_NATIVE_CALLBACKS.includes(/** @type {string} */ (k)),
|
|
194
|
+
() => false);
|
|
195
|
+
hotCopy(oldChain[i], newChain[i],
|
|
196
|
+
(k) => HOT_FIXED_STATICS.includes(/** @type {string} */ (k)),
|
|
197
|
+
// Framework state cached on a class (a symbol, a `_`-prefixed name)
|
|
198
|
+
// is not the author's, so it is never deleted.
|
|
199
|
+
(k) => typeof k === 'symbol' || k.startsWith('_'));
|
|
200
|
+
}
|
|
201
|
+
// A class that memoizes its finalized styles (Lit's `elementStyles`) keeps
|
|
202
|
+
// the old ones in a cache; an own value shadows it with the new styles.
|
|
203
|
+
if (typeof Old.finalizeStyles === 'function') {
|
|
204
|
+
Object.defineProperty(Old, 'elementStyles', { value: Old.finalizeStyles(Old.styles), configurable: true, writable: true });
|
|
205
|
+
}
|
|
206
|
+
const doc = g.document;
|
|
207
|
+
if (doc) {
|
|
208
|
+
for (const el of hotInstances(doc, Old)) {
|
|
209
|
+
const C = /** @type {any} */ (el).constructor;
|
|
210
|
+
if (el.shadowRoot && C.shadow === true && 'adoptedStyleSheets' in el.shadowRoot) {
|
|
211
|
+
const styles = typeof C.finalizeStyles === 'function' ? C.elementStyles : C.styles;
|
|
212
|
+
const list = Array.isArray(styles) ? styles.flat(Infinity) : styles ? [styles] : [];
|
|
213
|
+
el.shadowRoot.adoptedStyleSheets = list.map(hotSheet).filter(Boolean);
|
|
214
|
+
}
|
|
215
|
+
const inst = /** @type {any} */ (el);
|
|
216
|
+
if (typeof inst.requestUpdate === 'function') inst.requestUpdate();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (swapped) swapped.push(tag);
|
|
220
|
+
return Old;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Import the versioned component modules a `component` verdict lists, in
|
|
225
|
+
* order. A module whose define throws (a guard refused the swap) rejects its
|
|
226
|
+
* import, which ends the apply with that reason.
|
|
227
|
+
* @param {string[]} urls
|
|
228
|
+
* @returns {Promise<{ ok: true, tags: string[] } | { ok: false, why: string }>}
|
|
229
|
+
*/
|
|
230
|
+
function applyModules(urls) {
|
|
231
|
+
/** @type {string[]} */
|
|
232
|
+
const tags = [];
|
|
233
|
+
swapped = tags;
|
|
234
|
+
let p = Promise.resolve();
|
|
235
|
+
for (const u of urls) p = p.then(() => import(u));
|
|
236
|
+
return p.then(
|
|
237
|
+
() => { swapped = null; return { ok: /** @type {true} */ (true), tags }; },
|
|
238
|
+
(e) => { swapped = null; return { ok: /** @type {false} */ (false), why: String((e && e.message) || e) }; },
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const api = { strings, replace, applyModules };
|
|
243
|
+
g[KEY] = api;
|
|
244
|
+
return api;
|
|
245
|
+
}
|
package/src/dev-import.js
CHANGED
|
@@ -22,17 +22,29 @@
|
|
|
22
22
|
* a plain path, so on Bun it falls back to the file URL (no fresh import; the
|
|
23
23
|
* dev supervisor's restart still picks the edit up).
|
|
24
24
|
*
|
|
25
|
+
* The content query covers the ENTRY only: a module the entry imports is not
|
|
26
|
+
* re-imported through here. On Node the dev resolve hook (`dev/esm-hot-hooks.js`,
|
|
27
|
+
* #1689) versions everything below it, adding `webjs-v=<n>` to an edited module
|
|
28
|
+
* and its importers on top of this query, which is also what carries a
|
|
29
|
+
* dependency's edit to an entry whose own bytes did not change. Bun has no
|
|
30
|
+
* resolve hook, so there the dev source plugins stamp the versions below the
|
|
31
|
+
* entry (`dev/bun-app-source.js`) and this specifier adds the entry's own
|
|
32
|
+
* `webjs-v=<n>` after the content query.
|
|
33
|
+
*
|
|
25
34
|
* Outside dev the specifier is the plain file URL, exactly as before.
|
|
26
35
|
*/
|
|
27
36
|
import { pathToFileURL } from 'node:url';
|
|
28
37
|
import { readFileSync } from 'node:fs';
|
|
29
38
|
import { createHash } from 'node:crypto';
|
|
39
|
+
import { explainImportError } from './foreign-imports.js';
|
|
40
|
+
import { moduleVersion } from './dev/esm-hot-hooks.js';
|
|
30
41
|
|
|
31
42
|
/**
|
|
32
43
|
* @param {string} file absolute file path
|
|
33
44
|
* @param {boolean} dev whether to cache-bust
|
|
34
|
-
* @param {{ bun?: boolean, version?: (file: string) => string }} [opts]
|
|
35
|
-
* seams: the runtime (default: detected)
|
|
45
|
+
* @param {{ bun?: boolean, version?: (file: string) => string, moduleVersion?: (file: string) => number }} [opts]
|
|
46
|
+
* test seams: the runtime (default: detected), the content version, and the
|
|
47
|
+
* Bun module version (#1689)
|
|
36
48
|
* @returns {string}
|
|
37
49
|
*/
|
|
38
50
|
export function devImportSpecifier(file, dev, opts = {}) {
|
|
@@ -40,7 +52,10 @@ export function devImportSpecifier(file, dev, opts = {}) {
|
|
|
40
52
|
if (!dev) return url;
|
|
41
53
|
const bun = opts.bun ?? Boolean(typeof process !== 'undefined' && process.versions && process.versions.bun);
|
|
42
54
|
const bust = `?t=${(opts.version ?? contentVersion)(file)}`;
|
|
43
|
-
if (bun && !/[?#]/.test(file))
|
|
55
|
+
if (bun && !/[?#]/.test(file)) {
|
|
56
|
+
const v = (opts.moduleVersion ?? moduleVersion)(file);
|
|
57
|
+
return file + bust + (v ? `&webjs-v=${v}` : '');
|
|
58
|
+
}
|
|
44
59
|
return url + bust;
|
|
45
60
|
}
|
|
46
61
|
|
|
@@ -113,7 +128,9 @@ export async function devImport(file, dev) {
|
|
|
113
128
|
// promise instead. A rejection is evicted so the next call re-imports (the
|
|
114
129
|
// retry semantic a cold import had), and `clearProdImportCache` drops the
|
|
115
130
|
// whole map on an embedded host's `rebuild()`.
|
|
116
|
-
|
|
131
|
+
// A load failure caused by a foreign import (`next/*`, `react`, `lit`)
|
|
132
|
+
// is rewritten to name the WebJs replacement (foreign-imports.js).
|
|
133
|
+
const p = import(base).catch((e) => { throw explainImportError(e, file); });
|
|
117
134
|
PROD_MODULES.set(file, p);
|
|
118
135
|
p.catch(() => { if (PROD_MODULES.get(file) === p) PROD_MODULES.delete(file); });
|
|
119
136
|
return p;
|
|
@@ -123,6 +140,6 @@ export async function devImport(file, dev) {
|
|
|
123
140
|
return await import(spec);
|
|
124
141
|
} catch (e) {
|
|
125
142
|
RETRY_KEYS.set(base, `${base}-r${++retrySeq}`);
|
|
126
|
-
throw e;
|
|
143
|
+
throw explainImportError(e, file);
|
|
127
144
|
}
|
|
128
145
|
}
|
package/src/dev-overlay.js
CHANGED
|
@@ -88,6 +88,32 @@ export function dismissDevOverlay() {
|
|
|
88
88
|
__wjPendingSeq = -1;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Take the overlay down after a component hot swap only when the swap settled
|
|
93
|
+
* what it describes (#1689). A component swap re-renders nothing on the
|
|
94
|
+
* server, so nothing would re-raise a frame it wrongly dismissed: a `render`
|
|
95
|
+
* error on this page, or a `ts-strip` failure in some other module, would
|
|
96
|
+
* vanish while it still stands. So the overlay goes only for a `rebuild`
|
|
97
|
+
* frame (the rebuild that produced this swap succeeded) or a frame whose
|
|
98
|
+
* `file` is one the swap re-imported or was edited for, `paths` holding those
|
|
99
|
+
* as app-root paths (`/components/x.ts`).
|
|
100
|
+
*
|
|
101
|
+
* @param {string[]} paths
|
|
102
|
+
* @returns {boolean} whether it dismissed
|
|
103
|
+
*/
|
|
104
|
+
export function dismissDevOverlayCoveredBy(paths) {
|
|
105
|
+
const f = __wjFrame;
|
|
106
|
+
if (!f) return false;
|
|
107
|
+
let hit = f.kind === 'rebuild';
|
|
108
|
+
const file = String(f.file || '').replace(/\\/g, '/');
|
|
109
|
+
for (let i = 0; !hit && file && i < paths.length; i++) {
|
|
110
|
+
const p = paths[i];
|
|
111
|
+
hit = !!p && file.length >= p.length && file.slice(file.length - p.length) === p;
|
|
112
|
+
}
|
|
113
|
+
if (hit) dismissDevOverlay();
|
|
114
|
+
return hit;
|
|
115
|
+
}
|
|
116
|
+
|
|
91
117
|
/** Append a styled text row to `parent`. */
|
|
92
118
|
function __wjRow(parent, css, text) {
|
|
93
119
|
const d = document.createElement('div');
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev reload client's readiness probe, the BROWSER half (#893, #1689).
|
|
3
|
+
* Browser-safe and import-free: `reloadClientJs` (`dev/helpers.js`) inlines it
|
|
4
|
+
* after an `export` strip, so the unit test drives the exact code that ships.
|
|
5
|
+
*
|
|
6
|
+
* Every reload signal waits for `/__webjs/version` to answer before it is
|
|
7
|
+
* applied, so a tab never reloads INTO a server that is still restarting and
|
|
8
|
+
* paints a half-rendered page. The probe used to give up after 100 tries (about
|
|
9
|
+
* 10 seconds) and reload anyway, which turned any restart slower than that (a
|
|
10
|
+
* cold `dev.before` step, a laptop waking up) into the browser's own error page
|
|
11
|
+
* and a dead tab. It now waits for as long as it takes, Next's posture: 100ms
|
|
12
|
+
* between tries for the first 2 seconds, then 1s for 10 seconds, then every 5s,
|
|
13
|
+
* and it says once that it is waiting. It never reloads by itself.
|
|
14
|
+
*
|
|
15
|
+
* One loop runs at a time. A reload signal that arrives while a probe is
|
|
16
|
+
* already waiting joins it instead of starting a second one, so a burst of
|
|
17
|
+
* edits during a restart does not multiply the requests.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {(url: string, init: object) => Promise<{ ok: boolean }>} fetchFn
|
|
22
|
+
* @param {string} url the readiness endpoint
|
|
23
|
+
* @param {{ setTimeout: (fn: () => void, ms: number) => unknown }} [timers]
|
|
24
|
+
* @param {(msg: string) => void} [log]
|
|
25
|
+
* @returns {(cb: () => void) => void} `whenReady(cb)` runs `cb` once the server answers
|
|
26
|
+
*/
|
|
27
|
+
export function createReadyProbe(fetchFn, url, timers, log) {
|
|
28
|
+
const t = timers || { setTimeout: function (fn, ms) { return setTimeout(fn, ms); } };
|
|
29
|
+
const say = log || function (msg) { console.info(msg); };
|
|
30
|
+
/** @type {Array<() => void> | null} */
|
|
31
|
+
let waiting = null;
|
|
32
|
+
|
|
33
|
+
/** @param {number} failures */
|
|
34
|
+
function delayAfter(failures) {
|
|
35
|
+
if (failures <= 20) return 100;
|
|
36
|
+
if (failures <= 30) return 1000;
|
|
37
|
+
return 5000;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function settle() {
|
|
41
|
+
const cbs = waiting || [];
|
|
42
|
+
waiting = null;
|
|
43
|
+
for (const cb of cbs) {
|
|
44
|
+
try { cb(); } catch (e) { console.error('[webjs] dev reload handler threw', e); }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {number} failures */
|
|
49
|
+
function attempt(failures) {
|
|
50
|
+
let p;
|
|
51
|
+
try { p = fetchFn(url, { cache: 'no-store' }); } catch (_) { p = Promise.reject(_); }
|
|
52
|
+
Promise.resolve(p).then(function (r) {
|
|
53
|
+
if (r && r.ok) settle(); else again(failures + 1);
|
|
54
|
+
}, function () { again(failures + 1); });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {number} failures */
|
|
58
|
+
function again(failures) {
|
|
59
|
+
// Once per loop, when the fast tries run out (about 2 seconds in).
|
|
60
|
+
if (failures === 20) say('[webjs] waiting for the dev server...');
|
|
61
|
+
t.setTimeout(function () { attempt(failures); }, delayAfter(failures));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return function whenReady(cb) {
|
|
65
|
+
if (waiting) { waiting.push(cb); return; }
|
|
66
|
+
waiting = [cb];
|
|
67
|
+
attempt(0);
|
|
68
|
+
};
|
|
69
|
+
}
|
package/src/dev-regenerate.js
CHANGED
|
@@ -63,8 +63,8 @@ export const REGENERATE_MANIFEST = '.webjs/regenerate.json';
|
|
|
63
63
|
* Read + normalize the `webjs.dev.regenerate` rules from an app's
|
|
64
64
|
* `package.json`. Defensive: a missing/malformed block yields `[]` (a plain app
|
|
65
65
|
* runs unchanged). Each rule's `output` is normalized to an appDir-relative path
|
|
66
|
-
* with no leading slash
|
|
67
|
-
*
|
|
66
|
+
* with no leading slash (`public/tailwind.css`), the file a request for the
|
|
67
|
+
* root url `/tailwind.css` maps to under `public/`.
|
|
68
68
|
*
|
|
69
69
|
* @param {string} appDir
|
|
70
70
|
* @param {(p: string) => Promise<string>} [read] injectable reader for tests
|
package/src/dev-reload-hold.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* intermediate save, and releases it when the build is done. The embed bridge
|
|
7
7
|
* (`dev-embed-client.js`) owns the switch, the `__webjsEmbedHold` page global,
|
|
8
8
|
* set from the host's `{ type: 'hold', enabled }` command. The reload client
|
|
9
|
-
* routes every reload signal through this module: while held it
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* routes every reload signal through this module: while held it merges every
|
|
10
|
+
* verdict it sees, and on release it applies that one reload, or nothing when
|
|
11
|
+
* nothing changed.
|
|
12
12
|
*
|
|
13
13
|
* Import-free and inlined verbatim (`export` stripped) into the served reload
|
|
14
14
|
* client by `reloadClientJs` in `dev/helpers.js`, the same shared-source rule as
|
|
@@ -19,42 +19,71 @@
|
|
|
19
19
|
* Verdict strength, strongest first. Restated rather than imported from
|
|
20
20
|
* `dev-reload-worker.js` because this file is inlined on its own.
|
|
21
21
|
*/
|
|
22
|
-
var HOLD_STRENGTH = ['reload', 'shell', 'page'];
|
|
22
|
+
var HOLD_STRENGTH = ['reload', 'shell', 'page', 'component', 'css'];
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Read one held signal into the verdict object shape. A bare string (a relay
|
|
26
|
+
* older than #1689 posts the name alone) becomes `{ v }`, and anything that
|
|
27
|
+
* is not a known verdict is a full `reload`, the fail-safe every other reload
|
|
28
|
+
* path uses.
|
|
27
29
|
*
|
|
28
|
-
* @param {
|
|
29
|
-
* @
|
|
30
|
-
* @returns {string}
|
|
30
|
+
* @param {unknown} raw
|
|
31
|
+
* @returns {{ v: string, by: string, why: string, modules: string[], css: boolean }}
|
|
31
32
|
*/
|
|
32
|
-
|
|
33
|
-
var
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
33
|
+
function readHeldVerdict(raw) {
|
|
34
|
+
var o = typeof raw === 'string' ? { v: raw } : raw;
|
|
35
|
+
if (!o || typeof o !== 'object' || HOLD_STRENGTH.indexOf(/** @type {any} */ (o).v) === -1) {
|
|
36
|
+
return { v: 'reload', by: '', why: 'unreadable-frame', modules: [], css: false };
|
|
37
|
+
}
|
|
38
|
+
var a = /** @type {any} */ (o);
|
|
39
|
+
return {
|
|
40
|
+
v: a.v,
|
|
41
|
+
by: typeof a.by === 'string' ? a.by : '',
|
|
42
|
+
why: typeof a.why === 'string' ? a.why : '',
|
|
43
|
+
modules: Array.isArray(a.modules) ? a.modules.slice() : [],
|
|
44
|
+
css: a.css === true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Merge an incoming verdict into the one held so far: the stronger `v` wins
|
|
50
|
+
* with its `by` / `why` (the held one on a tie), `modules` unions and `css`
|
|
51
|
+
* ORs, the same rule as the server's `mergeVerdicts`.
|
|
52
|
+
*
|
|
53
|
+
* @param {{ v: string, by: string, why: string, modules: string[], css: boolean } | null} a held so far (null = none)
|
|
54
|
+
* @param {unknown} b the incoming verdict, an object or a legacy string
|
|
55
|
+
* @returns {{ v: string, by: string, why: string, modules: string[], css: boolean }}
|
|
56
|
+
*/
|
|
57
|
+
export function mergeHeldVerdict(a, b) {
|
|
58
|
+
var next = readHeldVerdict(b);
|
|
59
|
+
if (a === null) return next;
|
|
60
|
+
var top = HOLD_STRENGTH.indexOf(a.v) <= HOLD_STRENGTH.indexOf(next.v) ? a : next;
|
|
61
|
+
var modules = a.modules.slice();
|
|
62
|
+
for (var i = 0; i < next.modules.length; i++) {
|
|
63
|
+
if (modules.indexOf(next.modules[i]) === -1) modules.push(next.modules[i]);
|
|
64
|
+
}
|
|
65
|
+
modules.sort();
|
|
66
|
+
return { v: top.v, by: top.by, why: top.why, modules: modules, css: a.css || next.css };
|
|
39
67
|
}
|
|
40
68
|
|
|
41
69
|
/**
|
|
42
70
|
* Build the hold gate for one page.
|
|
43
71
|
*
|
|
44
|
-
* @param {(verdict: string
|
|
72
|
+
* @param {(verdict: { v: string, by: string, why: string, modules: string[], css: boolean }) => void} apply
|
|
73
|
+
* applies a reload (the client's own apply)
|
|
45
74
|
* @param {() => boolean} isHeld whether the host currently holds reloads
|
|
46
|
-
* @returns {{ offer: (verdict: unknown) => boolean, release: () => void, pending: () => string | null }}
|
|
75
|
+
* @returns {{ offer: (verdict: unknown) => boolean, release: () => void, pending: () => ({ v: string, by: string, why: string, modules: string[], css: boolean } | null) }}
|
|
47
76
|
* `offer` returns true when it kept the signal (held), false when the caller
|
|
48
77
|
* should apply it now; `release` applies the one pending reload once the hold
|
|
49
78
|
* is off.
|
|
50
79
|
*/
|
|
51
80
|
export function createReloadHold(apply, isHeld) {
|
|
52
|
-
/** @type {string | null} */
|
|
81
|
+
/** @type {{ v: string, by: string, why: string, modules: string[], css: boolean } | null} */
|
|
53
82
|
var held = null;
|
|
54
83
|
return {
|
|
55
84
|
offer: function (verdict) {
|
|
56
85
|
if (!isHeld()) return false;
|
|
57
|
-
held =
|
|
86
|
+
held = mergeHeldVerdict(held, verdict);
|
|
58
87
|
return true;
|
|
59
88
|
},
|
|
60
89
|
release: function () {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why the dev server reloaded the page, printed AFTER the reload (#1689), the
|
|
3
|
+
* BROWSER half. Browser-safe and import-free: `reloadClientJs` (`dev/helpers.js`)
|
|
4
|
+
* inlines it after an `export` strip, so the unit test drives the exact code
|
|
5
|
+
* that ships.
|
|
6
|
+
*
|
|
7
|
+
* A reload replaces the document, and with it the console of the page that
|
|
8
|
+
* decided to reload, so a reason logged before `location.reload()` is gone by
|
|
9
|
+
* the time anyone looks (Next prints its `REACT_REFRESH_FULL_RELOAD` reason for
|
|
10
|
+
* the same reason). `reloadWithReason` stores `{ why, by }` in
|
|
11
|
+
* `sessionStorage`, which survives a same-tab reload, and the reloaded page's
|
|
12
|
+
* client prints it once with `printPendingReason`:
|
|
13
|
+
* `[webjs] full reload: <why> (<by>)`. Where storage is unavailable (a
|
|
14
|
+
* sandboxed frame, storage disabled) the reason is logged before the reload
|
|
15
|
+
* instead, which is the best that can be done.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const RELOAD_REASON_KEY = 'webjs:dev-reload-reason';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{ why?: string, by?: string }} r
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
function reloadReasonLine(r) {
|
|
25
|
+
return '[webjs] full reload: ' + (r.why || 'reload') + (r.by ? ' (' + r.by + ')' : '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Remember why, then reload.
|
|
30
|
+
* @param {{ v?: string, why?: string, by?: string }} verdict
|
|
31
|
+
* @param {{ reload: () => void }} loc
|
|
32
|
+
* @param {Storage | null | undefined} store
|
|
33
|
+
* @param {(msg: string) => void} log
|
|
34
|
+
*/
|
|
35
|
+
export function reloadWithReason(verdict, loc, store, log) {
|
|
36
|
+
const r = { why: (verdict && (verdict.why || verdict.v)) || 'reload', by: (verdict && verdict.by) || '' };
|
|
37
|
+
let kept = false;
|
|
38
|
+
try {
|
|
39
|
+
if (store) { store.setItem(RELOAD_REASON_KEY, JSON.stringify(r)); kept = true; }
|
|
40
|
+
} catch (_) { /* storage disabled or full: log it now instead */ }
|
|
41
|
+
if (!kept) log(reloadReasonLine(r));
|
|
42
|
+
loc.reload();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Print the reason a previous page stored before it reloaded, once.
|
|
47
|
+
* @param {Storage | null | undefined} store
|
|
48
|
+
* @param {(msg: string) => void} log
|
|
49
|
+
*/
|
|
50
|
+
export function printPendingReason(store, log) {
|
|
51
|
+
let raw = null;
|
|
52
|
+
try {
|
|
53
|
+
if (!store) return;
|
|
54
|
+
raw = store.getItem(RELOAD_REASON_KEY);
|
|
55
|
+
if (raw !== null) store.removeItem(RELOAD_REASON_KEY);
|
|
56
|
+
} catch (_) { return; }
|
|
57
|
+
if (!raw) return;
|
|
58
|
+
let r;
|
|
59
|
+
try { r = JSON.parse(raw); } catch (_) { return; }
|
|
60
|
+
if (r && typeof r === 'object') log(reloadReasonLine(r));
|
|
61
|
+
}
|