@webjsdev/server 0.8.91 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/index.d.ts +461 -102
- package/index.js +9 -4
- package/package.json +3 -3
- package/src/action-flash.js +185 -0
- package/src/action-input.js +208 -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 +112 -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/helpers.js +85 -0
- package/src/check/rules-actions.js +37 -1
- package/src/check/rules-components.js +133 -28
- 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 +109 -9
- package/src/check/runner-support.js +53 -171
- package/src/check/runner.js +24 -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 +262 -0
- package/src/core-exports.js +190 -0
- 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/dev-status.js +122 -0
- package/src/dev/esm-hot-hooks.js +373 -0
- package/src/dev/handler.js +632 -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 +191 -133
- package/src/dev/server.js +109 -37
- 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 +322 -78
- 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-styles.js +64 -6
- 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 +150 -69
- 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 +49 -17
- package/src/ssr/head.js +228 -25
- package/src/ssr/preloads.js +31 -45
- package/src/ssr/render.js +243 -55
- package/src/ssr/responses.js +51 -4
- package/src/ssr/shipped-modules.js +52 -16
- package/src/testing.d.ts +31 -2
- package/src/testing.js +85 -17
- 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 +3 -8
- package/src/component-elision.js +0 -1852
- package/src/elision-differential.js +0 -101
- package/src/elision-report.js +0 -193
package/src/dev/watch-tree.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { watch, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { watchRecursive } from './watch-recursive.js';
|
|
4
|
+
import { pollTree, isWatchLimitError } from './watch-poll.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The dev live-reload watcher over one root (#1521).
|
|
@@ -25,58 +26,94 @@ import { watchRecursive } from './watch-recursive.js';
|
|
|
25
26
|
* root watcher's events, so `mkdir lib` mid-session is watched like it was
|
|
26
27
|
* there at boot.
|
|
27
28
|
*
|
|
29
|
+
* When the OS runs out of watches (ENOSPC once the inotify limit is reached,
|
|
30
|
+
* EMFILE for file descriptors, #1689), the directory that failed is polled
|
|
31
|
+
* instead (`watch-poll.js`) and `onFallback` is told once per directory, so its
|
|
32
|
+
* edits keep arriving, a second slower, instead of never. A root watcher that
|
|
33
|
+
* fails that way polls the root's direct entries.
|
|
34
|
+
*
|
|
28
35
|
* @param {string} root absolute directory to watch
|
|
29
36
|
* @param {{
|
|
30
37
|
* ignore: (relPath: string) => boolean,
|
|
31
38
|
* onEvent: (relPath: string) => void,
|
|
32
39
|
* onError: (err: NodeJS.ErrnoException) => void,
|
|
33
|
-
*
|
|
40
|
+
* onFallback?: (name: string, err: NodeJS.ErrnoException) => void,
|
|
41
|
+
* watchFn?: typeof watch,
|
|
42
|
+
* pollMs?: number,
|
|
43
|
+
* }} opts `relPath` is relative to `root`, with OS separators. `onFallback`
|
|
44
|
+
* gets the top-level directory name (`''` for the root). `watchFn` and
|
|
45
|
+
* `pollMs` are test seams.
|
|
34
46
|
* @returns {() => void} closes every watcher
|
|
35
47
|
*/
|
|
36
|
-
export function watchTree(root, { ignore, onEvent, onError }) {
|
|
37
|
-
/** @type {Map<string,
|
|
48
|
+
export function watchTree(root, { ignore, onEvent, onError, onFallback, watchFn = watch, pollMs = 1000 }) {
|
|
49
|
+
/** @type {Map<string, { close: () => void, polled?: boolean }>} */
|
|
38
50
|
const dirs = new Map();
|
|
39
51
|
let closed = false;
|
|
40
52
|
|
|
41
|
-
const guard = (/** @type {import('node:fs').FSWatcher} */ w) => {
|
|
42
|
-
w.on('error', (err) => onError(/** @type {NodeJS.ErrnoException} */ (err)));
|
|
43
|
-
return w;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
53
|
const isDir = (name) => {
|
|
47
54
|
try { return statSync(join(root, name)).isDirectory(); } catch { return false; }
|
|
48
55
|
};
|
|
49
56
|
|
|
57
|
+
/** Replace a top-level directory's watcher with a poller. */
|
|
58
|
+
const pollDir = (name, /** @type {NodeJS.ErrnoException} */ err) => {
|
|
59
|
+
if (closed || dirs.get(name)?.polled) return;
|
|
60
|
+
try { dirs.get(name)?.close(); } catch {}
|
|
61
|
+
const stop = pollTree(join(root, name), {
|
|
62
|
+
ignore: (rel) => ignore(join(name, rel)),
|
|
63
|
+
onEvent: (rel) => onEvent(join(name, rel)),
|
|
64
|
+
intervalMs: pollMs,
|
|
65
|
+
});
|
|
66
|
+
dirs.set(name, { close: stop, polled: true });
|
|
67
|
+
onFallback?.(name, err);
|
|
68
|
+
};
|
|
69
|
+
|
|
50
70
|
const watchDir = (name) => {
|
|
51
71
|
if (closed || dirs.has(name) || ignore(name)) return;
|
|
72
|
+
const failed = (/** @type {NodeJS.ErrnoException} */ err) => {
|
|
73
|
+
if (isWatchLimitError(err)) pollDir(name, err);
|
|
74
|
+
else onError(err);
|
|
75
|
+
};
|
|
52
76
|
try {
|
|
53
77
|
// `watchRecursive` (#1529): on Linux under Node a per-directory walker,
|
|
54
78
|
// since Node 24's recursive watcher goes deaf to a file once it is
|
|
55
79
|
// replaced (`sed -i`, an editor's atomic save).
|
|
56
|
-
const w =
|
|
80
|
+
const w = watchRecursive(join(root, name), (_type, filename) => {
|
|
57
81
|
const rel = filename ? join(name, String(filename)) : name;
|
|
58
82
|
if (!ignore(rel)) onEvent(rel);
|
|
59
|
-
}, { ignore: (rel) => ignore(join(name, rel)) })
|
|
83
|
+
}, { ignore: (rel) => ignore(join(name, rel)), watchFn });
|
|
84
|
+
w.on('error', (err) => failed(/** @type {NodeJS.ErrnoException} */ (err)));
|
|
60
85
|
dirs.set(name, w);
|
|
61
86
|
} catch (err) {
|
|
62
|
-
|
|
87
|
+
failed(/** @type {NodeJS.ErrnoException} */ (err));
|
|
63
88
|
}
|
|
64
89
|
};
|
|
65
90
|
|
|
66
|
-
/**
|
|
91
|
+
/** A top-level entry changed: start or stop following it as a directory. */
|
|
92
|
+
const onTop = (/** @type {string} */ name) => {
|
|
93
|
+
if (ignore(name)) return;
|
|
94
|
+
if (isDir(name)) watchDir(name);
|
|
95
|
+
else if (dirs.has(name)) { try { dirs.get(name)?.close(); } catch {} dirs.delete(name); }
|
|
96
|
+
onEvent(name);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** @type {{ close: () => void } | null} */
|
|
67
100
|
let top = null;
|
|
101
|
+
const pollRoot = (/** @type {NodeJS.ErrnoException} */ err) => {
|
|
102
|
+
if (closed) return;
|
|
103
|
+
try { top?.close(); } catch {}
|
|
104
|
+
top = { close: pollTree(root, { ignore, onEvent: onTop, intervalMs: pollMs, recursive: false }) };
|
|
105
|
+
onFallback?.('', err);
|
|
106
|
+
};
|
|
107
|
+
const rootFailed = (/** @type {NodeJS.ErrnoException} */ err) => {
|
|
108
|
+
if (isWatchLimitError(err)) pollRoot(err);
|
|
109
|
+
else onError(err);
|
|
110
|
+
};
|
|
68
111
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (ignore(name)) return;
|
|
73
|
-
// A top-level entry changed: start or stop following it as a directory.
|
|
74
|
-
if (isDir(name)) watchDir(name);
|
|
75
|
-
else if (dirs.has(name)) { try { dirs.get(name)?.close(); } catch {} dirs.delete(name); }
|
|
76
|
-
onEvent(name);
|
|
77
|
-
}));
|
|
112
|
+
const w = watchFn(root, (_type, filename) => { if (filename) onTop(String(filename)); });
|
|
113
|
+
w.on('error', (err) => rootFailed(/** @type {NodeJS.ErrnoException} */ (err)));
|
|
114
|
+
top = w;
|
|
78
115
|
} catch (err) {
|
|
79
|
-
|
|
116
|
+
rootFailed(/** @type {NodeJS.ErrnoException} */ (err));
|
|
80
117
|
}
|
|
81
118
|
|
|
82
119
|
let entries = [];
|
package/src/dev-classify.js
CHANGED
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
* heuristic ("anything under `app/**` named `page.ts` can morph") is wrong in
|
|
19
19
|
* both directions and neither failure is exotic:
|
|
20
20
|
*
|
|
21
|
-
* - A page
|
|
22
|
-
*
|
|
23
|
-
* browser-bound JS, so it must reload despite being a page.
|
|
21
|
+
* - A page never ships (#1686), but a helper it imports may also be imported by
|
|
22
|
+
* a component, which makes it browser-bound JS: editing it must reload.
|
|
24
23
|
* - A helper under `modules/<feature>/utils/` imported by both a page and a
|
|
25
24
|
* component is a component edit by reachability, and its path says nothing.
|
|
26
25
|
*
|
|
@@ -29,9 +28,9 @@
|
|
|
29
28
|
*/
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* The verdicts, STRONGEST FIRST. The index in this array IS the strength order,
|
|
32
|
+
* which is what `mergeVerdicts` compares and what lets a batch of changes
|
|
33
|
+
* collapse to the strongest verdict it contains rather than the last.
|
|
35
34
|
*
|
|
36
35
|
* - `reload`: a component module, anything transitively reaching one, or any
|
|
37
36
|
* path the server cannot place. The browser does `location.reload()`.
|
|
@@ -40,21 +39,40 @@
|
|
|
40
39
|
* - `page`: a page module and nothing else. The browser re-renders the current
|
|
41
40
|
* URL and morphs the deepest shared boundary, preserving scroll AND the
|
|
42
41
|
* hydrated state of components outside the changed region.
|
|
42
|
+
* - `component`: browser modules the tab re-imports and patches onto the live
|
|
43
|
+
* component classes (#1689), listed in the verdict's `modules`. A tab with no
|
|
44
|
+
* hot-swap support applies it as a full reload.
|
|
45
|
+
* - `css`: a stylesheet and nothing else (#1689). The tab re-requests its
|
|
46
|
+
* stylesheets and swaps them in. A tab with no support reloads.
|
|
43
47
|
*
|
|
44
|
-
* @type {readonly ['reload', 'shell', 'page']}
|
|
48
|
+
* @type {readonly ['reload', 'shell', 'page', 'component', 'css']}
|
|
45
49
|
*/
|
|
46
|
-
export const RELOAD_VERDICTS = /** @type {const} */ (['reload', 'shell', 'page']);
|
|
50
|
+
export const RELOAD_VERDICTS = /** @type {const} */ (['reload', 'shell', 'page', 'component', 'css']);
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
|
-
*
|
|
53
|
+
* A live-reload verdict. `modules` lists the versioned browser URLs a
|
|
54
|
+
* `component` verdict asks the tab to re-import, and `css` says a stylesheet
|
|
55
|
+
* changed too, so the tab refreshes its sheets on top of whatever else it does.
|
|
56
|
+
*
|
|
57
|
+
* @typedef {{
|
|
58
|
+
* v: 'reload' | 'shell' | 'page' | 'component' | 'css',
|
|
59
|
+
* by: string,
|
|
60
|
+
* why: string,
|
|
61
|
+
* modules?: string[],
|
|
62
|
+
* css?: true,
|
|
63
|
+
* }} ReloadVerdict
|
|
50
64
|
*/
|
|
51
65
|
|
|
52
66
|
/**
|
|
53
|
-
*
|
|
54
|
-
* unrecognised
|
|
67
|
+
* Merge two verdicts into the one a batch applies, treating `null` /
|
|
68
|
+
* `undefined` as nothing and an unrecognised name as `reload` (fail safe, never
|
|
69
|
+
* fail open). `v`, `by` and `why` come from the stronger verdict (the first on
|
|
70
|
+
* a tie). `modules` is the sorted union of both and `css` is set when either
|
|
71
|
+
* sets it, because a weaker change in the batch still needs its own work done
|
|
72
|
+
* alongside the stronger one.
|
|
55
73
|
*
|
|
56
74
|
* Used twice for the same reason. The watcher can see several files change
|
|
57
|
-
* inside one
|
|
75
|
+
* inside one debounce window, and the browser relay coalesces a burst of
|
|
58
76
|
* signals into ONE emitted reload (#1397). In both places a batch mixing a page
|
|
59
77
|
* edit and a component edit is a COMPONENT edit, so taking the last verdict
|
|
60
78
|
* would leave the old component class running against fresh markup.
|
|
@@ -63,10 +81,16 @@ export const RELOAD_VERDICTS = /** @type {const} */ (['reload', 'shell', 'page']
|
|
|
63
81
|
* @param {ReloadVerdict | null | undefined} b
|
|
64
82
|
* @returns {ReloadVerdict | null}
|
|
65
83
|
*/
|
|
66
|
-
export function
|
|
84
|
+
export function mergeVerdicts(a, b) {
|
|
67
85
|
if (!a) return b || null;
|
|
68
86
|
if (!b) return a;
|
|
69
|
-
|
|
87
|
+
const top = verdictRank(a.v) <= verdictRank(b.v) ? a : b;
|
|
88
|
+
/** @type {ReloadVerdict} */
|
|
89
|
+
const out = { v: top.v, by: top.by, why: top.why };
|
|
90
|
+
const modules = [...new Set([...(a.modules || []), ...(b.modules || [])])].sort();
|
|
91
|
+
if (modules.length) out.modules = modules;
|
|
92
|
+
if (a.css || b.css) out.css = true;
|
|
93
|
+
return out;
|
|
70
94
|
}
|
|
71
95
|
|
|
72
96
|
/**
|
|
@@ -96,7 +120,7 @@ function verdictRank(v) {
|
|
|
96
120
|
* gap. Reading currency here would therefore turn the feature off for every
|
|
97
121
|
* edit after the first in a burst. Classifying against the previous build's
|
|
98
122
|
* graph is the intended behaviour, and it is conservative in the right
|
|
99
|
-
* direction: a file the stale graph has never seen falls to rung
|
|
123
|
+
* direction: a file the stale graph has never seen falls to rung 7 and
|
|
100
124
|
* reloads.
|
|
101
125
|
* 2. The path is outside `appDir`. The watcher only fires for `appDir` plus the
|
|
102
126
|
* opt-in `webjs.dev.watch` roots (#894), and a file outside `appDir` is
|
|
@@ -104,25 +128,33 @@ function verdictRank(v) {
|
|
|
104
128
|
* change what any layout renders, so it takes the whole-body `shell` swap.
|
|
105
129
|
* 3. The path is in the SHIPPED closure. This is the transitive answer and the
|
|
106
130
|
* one that matters: the browser holds this module, so the page must reload.
|
|
107
|
-
*
|
|
108
|
-
*
|
|
131
|
+
* A CSS module script (`import s from './x.css' with { type: 'css' }`) is in
|
|
132
|
+
* the closure too, so it stops here and never reaches rung 4.
|
|
133
|
+
* 4. The path is a stylesheet (`.css`) the browser does not hold as a module
|
|
134
|
+
* (#1689): a `public/` sheet, an `@import`ed partial, a
|
|
135
|
+
* `webjs.dev.regenerate` input such as Tailwind's. The tab re-requests every
|
|
136
|
+
* same-origin stylesheet and swaps them in place, with no reload and no
|
|
137
|
+
* re-render. Every sheet rather than the changed one, which is what makes a
|
|
138
|
+
* partial and a regenerate output come out right, since neither is the file
|
|
139
|
+
* the page links.
|
|
140
|
+
* 5. The path is a page module. Morphable.
|
|
141
|
+
* 6. The path is somewhere else in the module graph (a layout, a server-only
|
|
109
142
|
* util, a `.server.*` file reachable only from a page). Server-only by
|
|
110
143
|
* elimination, so `shell`.
|
|
111
|
-
*
|
|
112
|
-
* land here. `public/` is not in the module graph at all, so the
|
|
113
|
-
* cannot tell what the file IS or what depends on it, and most of
|
|
114
|
-
* there (an image, a font, a service worker, a hand-written
|
|
115
|
-
* something no amount of re-rendering picks up.
|
|
116
|
-
* the
|
|
117
|
-
*
|
|
118
|
-
* that the file is unknowable.
|
|
144
|
+
* 7. Anything else. A brand-new file, a deleted file, and every other `public/`
|
|
145
|
+
* asset land here. `public/` is not in the module graph at all, so the
|
|
146
|
+
* server cannot tell what the file IS or what depends on it, and most of
|
|
147
|
+
* what lives there (an image, a font, a service worker, a hand-written
|
|
148
|
+
* script) is something no amount of re-rendering picks up. A stylesheet is
|
|
149
|
+
* the one kind it can name from the path alone, which is why rung 4 takes
|
|
150
|
+
* it out first.
|
|
119
151
|
*
|
|
120
152
|
* Note what step 3 sweeps in deliberately. A `'use server'` action file
|
|
121
153
|
* imported by a SHIPPING component is inside the closure (the graph keeps a
|
|
122
154
|
* `.server.*` node and stops at it), so editing it reloads. That is correct
|
|
123
155
|
* rather than merely conservative, because adding an export to that file
|
|
124
156
|
* changes the generated RPC stub the browser holds. The same action imported
|
|
125
|
-
* only by a page falls to step
|
|
157
|
+
* only by a page falls to step 6 and gets `shell`.
|
|
126
158
|
*
|
|
127
159
|
* @param {string} abs absolute path of the changed file
|
|
128
160
|
* @param {object} ctx
|
|
@@ -141,6 +173,7 @@ export function classifyChangedPath(abs, ctx) {
|
|
|
141
173
|
if (!ctx.analysisReady) return { v: 'reload', by, why: 'analysis-cold' };
|
|
142
174
|
if (!isUnder(abs, ctx.appDir, sep)) return { v: 'shell', by, why: 'extra-watch-root' };
|
|
143
175
|
if (ctx.shippedFiles && ctx.shippedFiles.has(abs)) return { v: 'reload', by, why: 'ships-to-browser' };
|
|
176
|
+
if (abs.endsWith('.css')) return { v: 'css', by, why: 'stylesheet', css: true };
|
|
144
177
|
if (ctx.pageFiles && ctx.pageFiles.has(abs)) return { v: 'page', by, why: 'page-module' };
|
|
145
178
|
if (ctx.graphFiles && ctx.graphFiles.has(abs)) return { v: 'shell', by, why: 'server-only-module' };
|
|
146
179
|
return { v: 'reload', by, why: 'unknown-path' };
|
package/src/dev-error.js
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* offending source file for the code frame (a read failure degrades to no
|
|
15
15
|
* frame, never throws).
|
|
16
16
|
*/
|
|
17
|
-
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { findDecorator, decoratorReport, parseMissingExport, isCoreModule, missingCoreNameFix } from './lit-hints.js';
|
|
20
|
+
import { coreSurface, isTypeOnlyAnywhere } from './core-exports.js';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Pull the first useful `file:line:column` out of an error stack. Prefers a
|
|
@@ -40,7 +43,9 @@ export function parseStackLocation(stack, appDir) {
|
|
|
40
43
|
// (the `file:///` URL form, whose path uses forward slashes, still resolves).
|
|
41
44
|
// That matches the framework's POSIX-first dev posture; it degrades, never
|
|
42
45
|
// crashes.
|
|
43
|
-
|
|
46
|
+
// The lookbehind keeps a runtime-internal frame (`node:internal/modules/...`)
|
|
47
|
+
// from reading as the path `/modules/...`.
|
|
48
|
+
const re = /(?<![\w.-])(?:file:\/\/)?(\/[^\s:()?]+)(?:\?[^\s:()]*)?:(\d+):(\d+)/g;
|
|
44
49
|
/** @type {{ file: string, line: number, column: number }[]} */
|
|
45
50
|
const frames = [];
|
|
46
51
|
let m;
|
|
@@ -114,6 +119,15 @@ export function buildDevErrorFrame(error, opts = {}) {
|
|
|
114
119
|
let file = opts.file || null;
|
|
115
120
|
let line = opts.line != null ? opts.line : null;
|
|
116
121
|
let column = opts.column != null ? opts.column : null;
|
|
122
|
+
// A module that fails to PARSE (a decorator is the common case) throws a
|
|
123
|
+
// SyntaxError whose stack OPENS with a bare `file:///abs/x.ts:2` header (a
|
|
124
|
+
// line, no column) and continues with node:internal frames only, which the
|
|
125
|
+
// frame pattern would misread as a path. The header names the source, so it
|
|
126
|
+
// wins.
|
|
127
|
+
if (!file && stack) {
|
|
128
|
+
const head = /^(?:file:\/\/)?(\/[^\s:()?]+)(?:\?[^\s:()]*)?:(\d+)[ \t]*\n/.exec(stack);
|
|
129
|
+
if (head) { file = head[1]; line = Number(head[2]); }
|
|
130
|
+
}
|
|
117
131
|
if (!file && stack) {
|
|
118
132
|
const loc = parseStackLocation(stack, opts.appDir);
|
|
119
133
|
if (loc) { file = loc.file; line = loc.line; column = loc.column; }
|
|
@@ -125,6 +139,20 @@ export function buildDevErrorFrame(error, opts = {}) {
|
|
|
125
139
|
const loc = parseStackLocation(`${err.message}\n${stack || ''}`, opts.appDir);
|
|
126
140
|
if (loc && loc.file === file) { line = loc.line; column = loc.column; }
|
|
127
141
|
}
|
|
142
|
+
// A module that fails to PARSE, caught rather than crashing the process,
|
|
143
|
+
// carries no location at all on Node (the `file:line` arrow header is only
|
|
144
|
+
// printed for an uncaught error), so a decorator three imports down leaves
|
|
145
|
+
// the overlay with nothing to point at. The usual cause of an unexpected
|
|
146
|
+
// token in a WebJs app is a decorator, so look for one in the app source.
|
|
147
|
+
let hint = opts.hint || null;
|
|
148
|
+
if (!hint && !file && err instanceof SyntaxError && opts.appDir) {
|
|
149
|
+
const found = findAppDecorator(opts.appDir);
|
|
150
|
+
if (found) {
|
|
151
|
+
file = found.file; line = found.hit.line; column = found.hit.column;
|
|
152
|
+
const { message, fix } = decoratorReport(found.hit);
|
|
153
|
+
hint = `${message} ${fix}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
128
156
|
const codeFrame = file && line ? readCodeFrame(file, line, column || 0) : null;
|
|
129
157
|
return {
|
|
130
158
|
kind,
|
|
@@ -134,7 +162,80 @@ export function buildDevErrorFrame(error, opts = {}) {
|
|
|
134
162
|
line,
|
|
135
163
|
column,
|
|
136
164
|
codeFrame,
|
|
137
|
-
hint: opts.
|
|
165
|
+
hint: hint || inferHint(err, file, opts.appDir) || null,
|
|
138
166
|
url: typeof opts.url === 'string' && opts.url ? opts.url : null,
|
|
139
167
|
};
|
|
140
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Name the WebJs fix for an error whose own message does not, so the overlay
|
|
172
|
+
* says what to write instead of only what broke.
|
|
173
|
+
*
|
|
174
|
+
* A decorator (`@customElement`, `@property`, ...) is the case it exists for.
|
|
175
|
+
* Node fails the module with a bare `SyntaxError: Invalid or unexpected
|
|
176
|
+
* token`, and Bun, which runs legacy decorators, with a `ReferenceError` for
|
|
177
|
+
* the undefined decorator function. Neither names the fix, so when the failing
|
|
178
|
+
* file carries a decorator the hint is the one `webjs check` prints.
|
|
179
|
+
*
|
|
180
|
+
* The other is a name `@webjsdev/core` does not export (`LitElement`, a
|
|
181
|
+
* decorator function, a type imported as a value). Node says `does not provide
|
|
182
|
+
* an export named X` and Bun `Export named 'X' not found`; the hint is the one
|
|
183
|
+
* the `core-imports-exist` check rule prints for the same import.
|
|
184
|
+
*
|
|
185
|
+
* @param {Error} err
|
|
186
|
+
* @param {string | null} file the located source file, when there is one
|
|
187
|
+
* @param {string} [appDir] resolves the core package the app imports
|
|
188
|
+
* @returns {string | null}
|
|
189
|
+
*/
|
|
190
|
+
export function inferHint(err, file, appDir) {
|
|
191
|
+
const missing = parseMissingExport(err.message);
|
|
192
|
+
if (missing && isCoreModule(missing.module)) {
|
|
193
|
+
const surface = coreSurface(appDir);
|
|
194
|
+
const typeOnly = !!surface && isTypeOnlyAnywhere(surface, missing.name);
|
|
195
|
+
return `\`${missing.name}\` is not a runtime export of @webjsdev/core. ${missingCoreNameFix(missing.name, { typeOnly })}`;
|
|
196
|
+
}
|
|
197
|
+
if ((err instanceof SyntaxError || err instanceof ReferenceError) && file) {
|
|
198
|
+
let src = null;
|
|
199
|
+
try { src = readFileSync(file, 'utf8'); } catch { src = null; }
|
|
200
|
+
const hit = src ? findDecorator(src) : null;
|
|
201
|
+
if (hit) {
|
|
202
|
+
const { message, fix } = decoratorReport(hit);
|
|
203
|
+
return `${message} ${fix}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The first decorator in the app's own source, walking every JS / TS file
|
|
211
|
+
* outside node_modules, `public/`, and dot or underscore folders. Dev-only and
|
|
212
|
+
* reached only for an unlocated SyntaxError, so the synchronous walk costs
|
|
213
|
+
* nothing on the happy path.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} appDir
|
|
216
|
+
* @returns {{ file: string, hit: { name: string, index: number, line: number, column: number } } | null}
|
|
217
|
+
*/
|
|
218
|
+
function findAppDecorator(appDir) {
|
|
219
|
+
/** @type {string[]} */
|
|
220
|
+
const stack = [appDir];
|
|
221
|
+
let budget = 5000;
|
|
222
|
+
while (stack.length && budget-- > 0) {
|
|
223
|
+
const dir = /** @type {string} */ (stack.pop());
|
|
224
|
+
let entries;
|
|
225
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
226
|
+
for (const e of entries) {
|
|
227
|
+
if (e.name === 'node_modules' || e.name.startsWith('.') || e.name.startsWith('_')) continue;
|
|
228
|
+
const abs = join(dir, e.name);
|
|
229
|
+
if (e.isDirectory()) {
|
|
230
|
+
if (dir === appDir && e.name === 'public') continue;
|
|
231
|
+
stack.push(abs);
|
|
232
|
+
} else if (/\.m?[jt]s$/.test(e.name)) {
|
|
233
|
+
let src;
|
|
234
|
+
try { src = readFileSync(abs, 'utf8'); } catch { continue; }
|
|
235
|
+
const hit = findDecorator(src);
|
|
236
|
+
if (hit) return { file: abs, hit };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|