@webjsdev/server 0.8.62 → 0.8.64
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/index.d.ts +32 -3
- package/package.json +2 -2
- package/src/browser-entries.js +64 -0
- package/src/check/helpers.js +365 -0
- package/src/check/rules-actions.js +174 -0
- package/src/check/rules-components.js +275 -0
- package/src/check/rules-imports.js +166 -0
- package/src/check/rules-registry.js +128 -0
- package/src/check/rules-routing.js +224 -0
- package/src/check/rules-typescript.js +177 -0
- package/src/check/rules.js +127 -0
- package/src/check/runner-support.js +405 -0
- package/src/check/runner.js +113 -0
- package/src/check.js +4 -1774
- package/src/component-elision.js +1 -1
- package/src/dev/config.js +338 -0
- package/src/dev/handler.js +1372 -0
- package/src/dev/helpers.js +542 -0
- package/src/dev/serve.js +840 -0
- package/src/dev/server.js +322 -0
- package/src/dev-classify.js +174 -0
- package/src/dev-reload-worker.js +133 -7
- package/src/dev-styles.js +97 -0
- package/src/dev.js +22 -3077
- package/src/form-dispatch.js +12 -3
- package/src/importmap.js +6 -3
- package/src/listener-core.js +16 -2
- package/src/module-graph.js +112 -7
- package/src/rate-limit.js +50 -5
- package/src/router.js +11 -1
- package/src/ssr/client-router-flag.js +42 -0
- package/src/ssr/document.js +361 -0
- package/src/ssr/env-shim.js +51 -0
- package/src/ssr/escape.js +29 -0
- package/src/ssr/head.js +857 -0
- package/src/ssr/preloads.js +195 -0
- package/src/ssr/render.js +1481 -0
- package/src/ssr/responses.js +201 -0
- package/src/ssr.js +28 -2445
- package/src/vendor/audit.js +400 -0
- package/src/vendor/integrity.js +245 -0
- package/src/vendor/jspm.js +348 -0
- package/src/vendor/manifest.js +96 -0
- package/src/vendor/pins.js +892 -0
- package/src/vendor/providers.js +35 -0
- package/src/vendor/resolver.js +225 -0
- package/src/vendor/scanner.js +132 -0
- package/src/vendor.js +29 -2371
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider names accepted by `webjs vendor pin --from <provider>`.
|
|
3
|
+
* Default `jspm` resolves to jspm.io. Same set Rails's importmap-rails
|
|
4
|
+
* accepts (`packager.rb:normalize_provider`).
|
|
5
|
+
*
|
|
6
|
+
* jspm.io's Generator API itself supports multiple providers via the
|
|
7
|
+
* `provider` field in the request body. We surface the same choice as
|
|
8
|
+
* a CLI flag.
|
|
9
|
+
*
|
|
10
|
+
* @type {Set<string>}
|
|
11
|
+
*/
|
|
12
|
+
export const SUPPORTED_PROVIDERS = new Set(['jspm', 'jsdelivr', 'unpkg', 'skypack']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normalize the user-facing provider name to what the jspm.io API
|
|
16
|
+
* expects in its `provider` field. Mirrors importmap-rails's
|
|
17
|
+
* `normalize_provider`: `jspm` is shorthand for `jspm.io`; the rest
|
|
18
|
+
* pass through verbatim.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} name
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeProvider(name) {
|
|
24
|
+
return name === 'jspm' ? 'jspm.io' : name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Outcome of one api.jspm.io/generate POST.
|
|
29
|
+
* @typedef {Object} JspmCallResult
|
|
30
|
+
* @property {boolean} ok true when jspm returned a 2xx with a usable map
|
|
31
|
+
* @property {Record<string, string>} imports the resolved imports (empty on failure)
|
|
32
|
+
* @property {boolean} transient true when the failure is worth retrying
|
|
33
|
+
* (network / timeout / 5xx / 429), false for a permanent 4xx
|
|
34
|
+
* (jspm uses 401 for "this install is unresolvable")
|
|
35
|
+
*/
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { sha384Integrity } from './integrity.js';
|
|
4
|
+
import { vendorImportMapEntries, isLastLiveResolveFailed, setLastLiveResolveFailed } from './jspm.js';
|
|
5
|
+
import { readPinFile, pinDir } from './pins.js';
|
|
6
|
+
import { BUFFERED_MARKER } from '../conditional-get.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Per-process cache of SHA-384 integrity hashes for live-resolved vendor
|
|
10
|
+
* URLs, keyed by the FINAL cross-origin URL. A vendor bundle at a given
|
|
11
|
+
* versioned URL is immutable, so once hashed it never needs re-fetching
|
|
12
|
+
* within the process: a re-resolve (e.g. after a file-watcher rebuild
|
|
13
|
+
* that did not change the dep) reuses the hash instead of re-downloading.
|
|
14
|
+
* Cleared by `clearVendorCache` alongside the jspm fragment cache so a
|
|
15
|
+
* version bump re-hashes. This is NOT a persistent cache (that is the pin
|
|
16
|
+
* file's job); it only avoids redundant fetches in one running process.
|
|
17
|
+
*
|
|
18
|
+
* @type {Map<string, string>}
|
|
19
|
+
*/
|
|
20
|
+
const liveIntegrityCache = new Map();
|
|
21
|
+
|
|
22
|
+
export function clearLiveIntegrityCache() {
|
|
23
|
+
liveIntegrityCache.clear();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Bounds a single bundle GET during the SERVER's warmup live-integrity pass
|
|
27
|
+
// (`fetchLiveIntegrity`). Short on purpose: that pass gates readiness, so a
|
|
28
|
+
// stalled CDN must not hold the first request, and it is additionally capped
|
|
29
|
+
// by INTEGRITY_TOTAL_BUDGET_MS across all URLs.
|
|
30
|
+
const INTEGRITY_FETCH_TIMEOUT_MS = 10_000;
|
|
31
|
+
// Cap concurrent bundle fetches so a large dep set does not open dozens of
|
|
32
|
+
// sockets at once during warmup. Matches the bounded posture of the rest of
|
|
33
|
+
// vendor.js (the jspm resolve is per-package but the network is the shared
|
|
34
|
+
// constraint).
|
|
35
|
+
const INTEGRITY_FETCH_CONCURRENCY = 6;
|
|
36
|
+
// Total wall-clock budget for the whole live-integrity hashing phase. It runs
|
|
37
|
+
// inside the readiness-gating warmup, so even a CDN that serves the importmap
|
|
38
|
+
// then hangs on every bundle GET must not stall the first request for the sum
|
|
39
|
+
// of per-fetch timeouts. Once the budget passes, the remaining URLs are left
|
|
40
|
+
// without integrity (the same fail-open fallback as a fetch failure) instead of
|
|
41
|
+
// waiting out a 10s timeout each. A healthy CDN finishes in well under this.
|
|
42
|
+
const INTEGRITY_TOTAL_BUDGET_MS = 15_000;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch a single cross-origin URL with a bounded timeout and return its
|
|
46
|
+
* SHA-384 SRI hash, or null on any failure (network, timeout, non-ok).
|
|
47
|
+
* Fail-OPEN by design: a CDN hiccup must never break warmup, so a failure
|
|
48
|
+
* is a skipped hash (the URL serves without `integrity`, the pre-#235
|
|
49
|
+
* behavior for that one URL), not a thrown error.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} url
|
|
52
|
+
* @returns {Promise<string | null>}
|
|
53
|
+
*/
|
|
54
|
+
async function fetchLiveIntegrity(url) {
|
|
55
|
+
const cached = liveIntegrityCache.get(url);
|
|
56
|
+
if (cached) return cached;
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timer = setTimeout(() => controller.abort(), INTEGRITY_FETCH_TIMEOUT_MS);
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
61
|
+
if (!response.ok) return null;
|
|
62
|
+
// Hash the raw response bytes (arrayBuffer -> Uint8Array), the same
|
|
63
|
+
// primitive the browser's SRI implementation hashes. Decoding to a
|
|
64
|
+
// string first would risk encoding round-trip drift. See the matching
|
|
65
|
+
// comment in downloadBundle / fetchIntegrity.
|
|
66
|
+
const buf = new Uint8Array(await response.arrayBuffer());
|
|
67
|
+
const sri = await sha384Integrity(buf);
|
|
68
|
+
liveIntegrityCache.set(url, sri);
|
|
69
|
+
return sri;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compute SRI integrity for the CROSS-ORIGIN targets of a live-resolved
|
|
79
|
+
* import map. Same-origin targets (the `@webjsdev/core/*` runtime under
|
|
80
|
+
* `/__webjs/core/...` and any local `/__webjs/vendor/...` bundle) are
|
|
81
|
+
* skipped: they are served by the framework and already trusted, and SRI
|
|
82
|
+
* is a cross-origin defense.
|
|
83
|
+
*
|
|
84
|
+
* The returned map is keyed by the FINAL URL (the import-map target
|
|
85
|
+
* value), matching `vendorIntegrityFor(url)`'s lookup key so ssr.js emits
|
|
86
|
+
* the `integrity` sibling for free.
|
|
87
|
+
*
|
|
88
|
+
* Bounded and fail-open: cross-origin bundles are fetched in parallel with
|
|
89
|
+
* a small concurrency cap and a per-fetch timeout, and a failed fetch is
|
|
90
|
+
* skipped (no integrity for that one URL) rather than breaking the resolve.
|
|
91
|
+
* A single one-time `console.warn` reports the count of URLs that could not
|
|
92
|
+
* be hashed (no per-URL spam).
|
|
93
|
+
*
|
|
94
|
+
* @param {Record<string, string>} imports specifier -> final URL
|
|
95
|
+
* @returns {Promise<Record<string, string>>} integrity keyed by final URL
|
|
96
|
+
*/
|
|
97
|
+
async function computeLiveIntegrity(imports) {
|
|
98
|
+
// De-duplicate by URL: two specifiers can resolve to the same bundle URL
|
|
99
|
+
// (a bare import and one of its subpaths), so hash each URL once.
|
|
100
|
+
const urls = [...new Set(Object.values(imports))].filter((u) => /^https:\/\//.test(u));
|
|
101
|
+
/** @type {Record<string, string>} */
|
|
102
|
+
const integrity = {};
|
|
103
|
+
if (urls.length === 0) return integrity;
|
|
104
|
+
|
|
105
|
+
const failed = [];
|
|
106
|
+
let next = 0;
|
|
107
|
+
const deadline = Date.now() + INTEGRITY_TOTAL_BUDGET_MS;
|
|
108
|
+
async function worker() {
|
|
109
|
+
// Stop claiming new URLs once the total budget passes; a URL already
|
|
110
|
+
// in flight still settles under its own per-fetch timeout.
|
|
111
|
+
while (next < urls.length && Date.now() < deadline) {
|
|
112
|
+
const url = urls[next++];
|
|
113
|
+
const sri = await fetchLiveIntegrity(url);
|
|
114
|
+
if (sri) integrity[url] = sri;
|
|
115
|
+
else failed.push(url);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const workerCount = Math.min(INTEGRITY_FETCH_CONCURRENCY, urls.length);
|
|
119
|
+
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
120
|
+
// Any URL never claimed because the budget passed also fails open (served
|
|
121
|
+
// without integrity), the same outcome as a fetch failure.
|
|
122
|
+
while (next < urls.length) failed.push(urls[next++]);
|
|
123
|
+
|
|
124
|
+
if (failed.length) {
|
|
125
|
+
// One-time, count-based warning. The app still boots and the imports
|
|
126
|
+
// still work; only these URLs lack SRI (served as before #235). Run
|
|
127
|
+
// `webjs vendor pin` to lock in integrity, or retry once the CDN is
|
|
128
|
+
// healthy. Naming one example URL aids diagnosis without per-URL spam.
|
|
129
|
+
console.warn(
|
|
130
|
+
`[webjs] could not compute SRI for ${failed.length} live-resolved ` +
|
|
131
|
+
`vendor URL(s) (e.g. ${failed[0]}); they will load WITHOUT integrity. ` +
|
|
132
|
+
`This is a fail-open fallback for a CDN fetch failure or the warmup ` +
|
|
133
|
+
`time budget; the app still works. Run \`webjs vendor pin\` to lock in ` +
|
|
134
|
+
`SRI hashes.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return integrity;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function resolveVendorImports(appDir, getBareImports) {
|
|
141
|
+
const file = await readPinFile(appDir);
|
|
142
|
+
// A committed pin file IS the import map. The whole-app bare-import scan is
|
|
143
|
+
// discarded in that case, so it must never run (runtime-first boot: no
|
|
144
|
+
// static analysis when pinned). The scan is supplied as a thunk and invoked
|
|
145
|
+
// solely here, only when there is no pin file.
|
|
146
|
+
if (file) {
|
|
147
|
+
// A pin file is a deterministic disk read: always "ok" (no live CDN call
|
|
148
|
+
// that could partially fail). This is the recommended prod posture. The
|
|
149
|
+
// pin's own integrity is used verbatim; the live-hash path below is NOT
|
|
150
|
+
// taken for a pinned app.
|
|
151
|
+
return { imports: file.imports, integrity: file.integrity || {}, ok: true };
|
|
152
|
+
}
|
|
153
|
+
setLastLiveResolveFailed(false);
|
|
154
|
+
const bareImports = await getBareImports();
|
|
155
|
+
const imports = await vendorImportMapEntries(bareImports, appDir);
|
|
156
|
+
// Fill the SRI gap for live-resolved (unpinned) apps (#235): hash each
|
|
157
|
+
// cross-origin bundle and key the integrity by its final URL, the same
|
|
158
|
+
// shape the pin path uses and `vendorIntegrityFor` looks up. Bounded +
|
|
159
|
+
// fail-open, so a CDN fetch failure degrades to a missing hash for that
|
|
160
|
+
// URL (a warning), never a broken resolve. This runs only AFTER a live
|
|
161
|
+
// resolve produced URLs; if the resolve itself failed there is nothing to
|
|
162
|
+
// hash.
|
|
163
|
+
const integrity = await computeLiveIntegrity(imports);
|
|
164
|
+
// ok=false means at least one install could not be resolved (CDN unreachable
|
|
165
|
+
// / timeout / non-ok), so `imports` is partial. The caller must not memoize
|
|
166
|
+
// this as done; it should retry once the CDN recovers.
|
|
167
|
+
return { imports, integrity, ok: !isLastLiveResolveFailed() };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Serve a downloaded vendor bundle from `.webjs/vendor/<filename>`.
|
|
172
|
+
* Called by dev.js when the importmap contains `/__webjs/vendor/`
|
|
173
|
+
* paths (i.e. user ran `webjs vendor pin --download`).
|
|
174
|
+
*
|
|
175
|
+
* @param {string} filename e.g. `'dayjs@1.11.13.js'`
|
|
176
|
+
* @param {string} appDir
|
|
177
|
+
* @param {boolean} dev
|
|
178
|
+
* @returns {Promise<Response>}
|
|
179
|
+
*/
|
|
180
|
+
export async function serveDownloadedBundle(filename, appDir, dev) {
|
|
181
|
+
// Strict allowlist. Vendor filenames are framework-generated:
|
|
182
|
+
// `<pkg>@<version>.js` or `<pkg>@<version>__<subpath>.js` plus the
|
|
183
|
+
// `@scope__name` form for scoped packages. The legal charset is
|
|
184
|
+
// alphanumeric plus `@`, `.`, `_`, `-`, `+` (`+` covers semver
|
|
185
|
+
// build metadata like `1.0.0+build.42`). Reject anything else
|
|
186
|
+
// (slashes / backslashes / dots-dots / null bytes / Unicode
|
|
187
|
+
// separators / glob chars) without echoing the input.
|
|
188
|
+
if (!/^[A-Za-z0-9@._+-]+\.js$/.test(filename) || filename.includes('..')) {
|
|
189
|
+
return new Response(`/* invalid vendor filename */`, {
|
|
190
|
+
status: 400,
|
|
191
|
+
headers: { 'content-type': 'application/javascript; charset=utf-8' },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
// Read as raw bytes (no encoding arg). downloadBundle writes the
|
|
196
|
+
// file from the response arrayBuffer (the same primitive the
|
|
197
|
+
// browser's SRI implementation hashes), so the bytes on disk are
|
|
198
|
+
// byte-identical to what jspm.io originally served. Reading with
|
|
199
|
+
// utf8 here would decode-then-re-encode and risk dropping the SRI
|
|
200
|
+
// match if any byte didn't round-trip exactly (e.g. invalid
|
|
201
|
+
// surrogate replacement). Keep the I/O binary end-to-end.
|
|
202
|
+
const body = await readFile(join(pinDir(appDir), filename));
|
|
203
|
+
// Buffered (bytes) body, so opt into the conditional-GET funnel, which
|
|
204
|
+
// hashes the bytes into a weak ETag (for downstream caches that strip the
|
|
205
|
+
// `immutable` directive) and honors If-None-Match -> 304. A WEAK validator
|
|
206
|
+
// is correct here because compression may re-encode the bytes per request
|
|
207
|
+
// (RFC 7232 2.3.3); the funnel is the single source for that. See
|
|
208
|
+
// conditional-get.js.
|
|
209
|
+
return new Response(body, {
|
|
210
|
+
headers: {
|
|
211
|
+
'content-type': 'application/javascript; charset=utf-8',
|
|
212
|
+
'cache-control': dev ? 'no-cache' : 'public, max-age=31536000, immutable',
|
|
213
|
+
[BUFFERED_MARKER]: '1',
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
} catch {
|
|
217
|
+
// Don't echo `filename` (already validated by the regex above so
|
|
218
|
+
// safe to echo, but keep the body fixed for grep-ability and to
|
|
219
|
+
// discourage anyone copying this pattern with untrusted input).
|
|
220
|
+
return new Response(`/* vendor bundle not found. Run webjs vendor pin --download to (re-)download. */`, {
|
|
221
|
+
status: 404,
|
|
222
|
+
headers: { 'content-type': 'application/javascript; charset=utf-8' },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { buildModuleGraph, reachableBareSpecifiers } from '../module-graph.js';
|
|
2
|
+
import { browserEntryFiles } from '../browser-entries.js';
|
|
3
|
+
import { scanComponents } from '../component-scanner.js';
|
|
4
|
+
import { buildRouteTable } from '../router.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Set of package names whose importmap entries are populated by the
|
|
8
|
+
* framework, not by the vendor scanner. The scanner skips these to
|
|
9
|
+
* keep `@webjsdev/core` (and any future framework-internal package)
|
|
10
|
+
* off the jspm.io path: their bytes are served by the dev server's
|
|
11
|
+
* dedicated `/__webjs/core/*` route, and `buildCoreEntries()` in
|
|
12
|
+
* `importmap.js` derives one importmap line per exported subpath
|
|
13
|
+
* directly from the package's own `exports` field.
|
|
14
|
+
*
|
|
15
|
+
* The `'@webjsdev/core/'` prefix entry is here so that `extractPackageName`
|
|
16
|
+
* returning the bare name is enough to recognise core-subpath imports
|
|
17
|
+
* (`@webjsdev/core/directives`, `@webjsdev/core/task`, …) and skip
|
|
18
|
+
* them; the prefix form catches anything whose extractPackageName
|
|
19
|
+
* returns null but whose specifier starts with the prefix. Same
|
|
20
|
+
* mechanism, no special casing per subpath.
|
|
21
|
+
*/
|
|
22
|
+
export const BUILTIN = new Set(['@webjsdev/core', '@webjsdev/core/']);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Server-only framework packages that must NEVER be vendored to the browser.
|
|
26
|
+
* Unlike `@webjsdev/core` (browser-bound, served locally via `/__webjs/core/*`),
|
|
27
|
+
* these are pure server packages: the CLI, the SSR runtime, the MCP server. A
|
|
28
|
+
* stray browser-graph scan
|
|
29
|
+
* that surfaces one of them must not push it onto the jspm path (#713). Matched
|
|
30
|
+
* on the extracted package name, so subpaths (`@webjsdev/cli/bin/webjs.js`) are
|
|
31
|
+
* covered. `@webjsdev/ui` is intentionally absent: its components ARE
|
|
32
|
+
* browser-bound, so it stays vendorable.
|
|
33
|
+
*/
|
|
34
|
+
export const FRAMEWORK_SERVER_ONLY = new Set(['@webjsdev/cli', '@webjsdev/server', '@webjsdev/mcp']);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract the package name from a bare specifier.
|
|
38
|
+
* `'dayjs'` → `'dayjs'`
|
|
39
|
+
* `'dayjs/locale/en'` → `'dayjs'`
|
|
40
|
+
* `'@tanstack/query'` → `'@tanstack/query'`
|
|
41
|
+
* `'@tanstack/query/x'` → `'@tanstack/query'`
|
|
42
|
+
* `'./foo'`, `'../bar'`, `'/baz'` → `null` (relative/absolute)
|
|
43
|
+
* `'#components/x.ts'`, `'#lib/db'` → `null` (`#` path alias, #555: resolves to
|
|
44
|
+
* a LOCAL file via `package.json` "imports", never an npm package, so it must
|
|
45
|
+
* not be sent to the vendor resolver, #623)
|
|
46
|
+
*
|
|
47
|
+
* @param {string} spec
|
|
48
|
+
* @returns {string | null}
|
|
49
|
+
*/
|
|
50
|
+
export function extractPackageName(spec) {
|
|
51
|
+
if (!spec || spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('__') || spec.startsWith('#')) return null;
|
|
52
|
+
if (/^[a-z]+:/.test(spec)) return null;
|
|
53
|
+
if (spec.startsWith('@')) {
|
|
54
|
+
const parts = spec.split('/');
|
|
55
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
56
|
+
}
|
|
57
|
+
return spec.split('/')[0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Bare npm specifiers that could reach the browser, filtered of the framework
|
|
62
|
+
* packages that must never be vendored.
|
|
63
|
+
*
|
|
64
|
+
* The scan is ROOTED IN THE MODULE GRAPH, not in a filesystem walk. Only files
|
|
65
|
+
* reachable from a browser-bound entry (page / layout / error / loading /
|
|
66
|
+
* not-found / forbidden / unauthorized / instrumentation-client / any
|
|
67
|
+
* component) contribute, which is the same authorization gate the dev server
|
|
68
|
+
* uses to decide what it will serve at all. A file nothing imports contributes
|
|
69
|
+
* nothing, so a build script under `scripts/`, a tooling config, a test helper
|
|
70
|
+
* and an unreferenced module all drop out by reachability rather than by name
|
|
71
|
+
* (there is no exclusion LIST any more, which is the point: the old one was
|
|
72
|
+
* open-ended and went stale).
|
|
73
|
+
*
|
|
74
|
+
* Excluded on top of unreachability:
|
|
75
|
+
* - `.server.{js,ts,mjs,mts}` files. They are REACHED (the browser fetches an
|
|
76
|
+
* RPC or throw-at-load stub at that URL) but their source never ships, so
|
|
77
|
+
* their bare imports must not enter the importmap.
|
|
78
|
+
* - `@webjsdev/core` and its subpaths (BUILTIN, served locally).
|
|
79
|
+
* - `@webjsdev/cli` / `@webjsdev/server` / `@webjsdev/mcp` (#713).
|
|
80
|
+
* - `import type` statements and any `import` written inside a comment,
|
|
81
|
+
* string, template literal or regex body. Both come free from the module
|
|
82
|
+
* graph's blanked-mask scanner (#753 / #805); this function no longer has
|
|
83
|
+
* a scanner of its own.
|
|
84
|
+
*
|
|
85
|
+
* This is the OUT-OF-PROCESS entry point (`pinAll`, `webjs doctor`). It builds
|
|
86
|
+
* its own graph, route table and component scan, roughly 70-200ms on the
|
|
87
|
+
* in-repo apps, and applies NO elision pruning, so the result is a SUPERSET of
|
|
88
|
+
* what the running server serves. That superset relation is what lets
|
|
89
|
+
* `prunePinToReachable` intersect a committed pin down to the runtime answer
|
|
90
|
+
* (#197). The dev server calls `reachedBareImports` instead, with the graph it
|
|
91
|
+
* already has and its elision skip set.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} appDir
|
|
94
|
+
* @returns {Promise<Set<string>>}
|
|
95
|
+
*/
|
|
96
|
+
export async function scanBareImports(appDir) {
|
|
97
|
+
let graph, components, routeTable;
|
|
98
|
+
try {
|
|
99
|
+
graph = await buildModuleGraph(appDir);
|
|
100
|
+
components = await scanComponents(appDir);
|
|
101
|
+
routeTable = await buildRouteTable(appDir);
|
|
102
|
+
} catch {
|
|
103
|
+
// An app the analysis cannot process yields no vendor specifiers rather
|
|
104
|
+
// than a throw, matching how check.js and elision-report.js degrade. The
|
|
105
|
+
// dev server surfaces the real problem.
|
|
106
|
+
return new Set();
|
|
107
|
+
}
|
|
108
|
+
return reachedBareImports(graph, [...browserEntryFiles(routeTable, components)], appDir);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The in-process form: the caller already has a module graph, the entry set,
|
|
113
|
+
* and (on the runtime path) the elision skip set. Used by the dev server so a
|
|
114
|
+
* vendor resolve does not rebuild analysis it just built.
|
|
115
|
+
*
|
|
116
|
+
* @param {import('../module-graph.js').ModuleGraph} graph
|
|
117
|
+
* @param {string[]} entryFiles
|
|
118
|
+
* @param {string} appDir
|
|
119
|
+
* @param {Set<string>} [skip] elided / inert / import-only modules
|
|
120
|
+
* @returns {Set<string>}
|
|
121
|
+
*/
|
|
122
|
+
export function reachedBareImports(graph, entryFiles, appDir, skip) {
|
|
123
|
+
const found = reachableBareSpecifiers(graph, entryFiles, appDir, skip);
|
|
124
|
+
for (const b of BUILTIN) found.delete(b);
|
|
125
|
+
// Drop core subpaths (served locally via `/__webjs/core/*`) and server-only
|
|
126
|
+
// framework packages (#713) so neither reaches the importmap / jspm path.
|
|
127
|
+
for (const spec of found) {
|
|
128
|
+
const p = extractPackageName(spec);
|
|
129
|
+
if (p && (BUILTIN.has(p) || FRAMEWORK_SERVER_ONLY.has(p))) found.delete(spec);
|
|
130
|
+
}
|
|
131
|
+
return found;
|
|
132
|
+
}
|