@webjsdev/server 0.8.63 → 0.8.65
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 +7 -2
- 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 +173 -26
- 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/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,348 @@
|
|
|
1
|
+
import { BUILTIN, FRAMEWORK_SERVER_ONLY, extractPackageName } from './scanner.js';
|
|
2
|
+
import { getPackageVersion } from './manifest.js';
|
|
3
|
+
import { normalizeProvider } from './providers.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-memory cache of resolved importmap fragments from api.jspm.io.
|
|
7
|
+
* Two kinds of key share this map:
|
|
8
|
+
* - The UNIFIED key (`<provider>::unified::<sorted installs joined>`)
|
|
9
|
+
* caches the whole-set resolve produced by one `generate` call, the
|
|
10
|
+
* default path (issue #446).
|
|
11
|
+
* - The PER-INSTALL key (`<provider>::<install>`) caches a single
|
|
12
|
+
* install's isolated resolve, used only on the fallback path when the
|
|
13
|
+
* unified call fails because some install is unresolvable.
|
|
14
|
+
* Per-process; cleared by `clearVendorCache` on file-watcher rebuild
|
|
15
|
+
* so new versions get re-resolved.
|
|
16
|
+
*
|
|
17
|
+
* @type {Map<string, Record<string, string>>}
|
|
18
|
+
*/
|
|
19
|
+
const jspmCache = new Map();
|
|
20
|
+
// Set by jspmResolveOne whenever a LIVE resolution attempt fails (network
|
|
21
|
+
// error, timeout, or a non-ok jspm response). resolveVendorImports resets it
|
|
22
|
+
// before a scan and reads it after, so a caller can tell "resolved cleanly"
|
|
23
|
+
// from "served a partial map because the CDN was unreachable" and avoid
|
|
24
|
+
// memoizing the failure as done. Safe under the single-flighted ensureReady
|
|
25
|
+
// (one live resolve at a time); the vendor CLI does not run alongside a server.
|
|
26
|
+
let lastLiveResolveFailed = false;
|
|
27
|
+
|
|
28
|
+
export function isLastLiveResolveFailed() {
|
|
29
|
+
return lastLiveResolveFailed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function setLastLiveResolveFailed(val) {
|
|
33
|
+
lastLiveResolveFailed = val;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const JSPM_GENERATE_ENDPOINT = 'https://api.jspm.io/generate';
|
|
37
|
+
const JSPM_GENERATE_TIMEOUT_MS = 10_000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Make ONE api.jspm.io/generate POST for a list of installs and return a
|
|
41
|
+
* structured result. The single point that talks to the network; both the
|
|
42
|
+
* unified path and the per-install fallback funnel through it.
|
|
43
|
+
*
|
|
44
|
+
* jspm fails the WHOLE batch (401) when ANY one install is unresolvable, so
|
|
45
|
+
* a multi-install POST is all-or-nothing: either the entire coherent graph
|
|
46
|
+
* comes back, or nothing does. `jspmGenerate` uses that property to decide
|
|
47
|
+
* when to fall back to per-install isolation.
|
|
48
|
+
*
|
|
49
|
+
* @param {Array<string>} installs e.g. ['dayjs@1.11.13', '@codemirror/lint@6.9.6']
|
|
50
|
+
* @param {string} provider one of SUPPORTED_PROVIDERS
|
|
51
|
+
* @param {number} [timeoutMs] defaults to the SERVER budget; a CLI caller
|
|
52
|
+
* passes the longer one, since it is a command someone is waiting on rather
|
|
53
|
+
* than a request being held open.
|
|
54
|
+
* @returns {Promise<JspmCallResult>}
|
|
55
|
+
*/
|
|
56
|
+
async function jspmCall(installs, provider, timeoutMs = JSPM_GENERATE_TIMEOUT_MS) {
|
|
57
|
+
const label = installs.length === 1 ? `'${installs[0]}'` : `${installs.length} packages`;
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(JSPM_GENERATE_ENDPOINT, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'content-type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
install: installs,
|
|
66
|
+
// flattenScope:true merges transitive ESM deps into the flat
|
|
67
|
+
// `imports` map instead of a separate `scopes` field. Webjs only
|
|
68
|
+
// consumes `imports`, so without this any package with an
|
|
69
|
+
// unbundled ESM transitive (e.g. react-dom imports `scheduler`,
|
|
70
|
+
// @codemirror/lint imports @codemirror/state) would break in the
|
|
71
|
+
// browser with an unresolved-bare-specifier error. With the
|
|
72
|
+
// WHOLE-set call (issue #446) the flattened transitives are now
|
|
73
|
+
// ALSO mutually consistent: one `@codemirror/view` URL shared by
|
|
74
|
+
// the direct import and lint's transitive need, instead of two
|
|
75
|
+
// skewed versions from independent per-package calls.
|
|
76
|
+
flattenScope: true,
|
|
77
|
+
env: ['browser', 'production', 'module'],
|
|
78
|
+
provider: normalizeProvider(provider),
|
|
79
|
+
}),
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
// jspm.io returns the error reason in the body with a 401 (its
|
|
84
|
+
// quirk: 401 is what it sends for unresolvable installs, not auth
|
|
85
|
+
// failures). Surface it so the user sees WHAT failed and why.
|
|
86
|
+
let detail = '';
|
|
87
|
+
try {
|
|
88
|
+
const body = await response.json();
|
|
89
|
+
if (body && typeof body.error === 'string') detail = `: ${body.error}`;
|
|
90
|
+
} catch { /* non-JSON body */ }
|
|
91
|
+
console.error(
|
|
92
|
+
`[webjs] could not vendor ${label} via ${provider} (status ${response.status})${detail}`,
|
|
93
|
+
);
|
|
94
|
+
// A 5xx/429 is a transient jspm problem worth retrying. A 401/4xx
|
|
95
|
+
// means at least one install is genuinely unresolvable (jspm uses
|
|
96
|
+
// 401 for that): a private / workspace / server-only package (e.g.
|
|
97
|
+
// @webjsdev/server, pg) the browser never fetches
|
|
98
|
+
// anyway. Permanent failures must NOT block readiness.
|
|
99
|
+
const transient = response.status >= 500 || response.status === 429;
|
|
100
|
+
return { ok: false, imports: {}, transient };
|
|
101
|
+
}
|
|
102
|
+
const result = await response.json();
|
|
103
|
+
const imports = (result && result.map && result.map.imports) || {};
|
|
104
|
+
return { ok: true, imports, transient: false };
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const msg = e && e.name === 'AbortError'
|
|
107
|
+
? `timed out after ${timeoutMs}ms`
|
|
108
|
+
: `${e && e.message}`;
|
|
109
|
+
console.error(`[webjs] could not vendor ${label} via ${provider}: ${msg}`);
|
|
110
|
+
return { ok: false, imports: {}, transient: true };
|
|
111
|
+
} finally {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a SINGLE install in isolation, cached per install + provider.
|
|
118
|
+
* This is the FALLBACK path: it only runs when the unified whole-set call
|
|
119
|
+
* fails because some install is unresolvable. Isolating each install means
|
|
120
|
+
* one bad dep (a 401) drops out on its own instead of collapsing the map
|
|
121
|
+
* for its legitimate neighbours. The cross-package coherence the unified
|
|
122
|
+
* call provides is lost for this degraded set, which is acceptable: it is
|
|
123
|
+
* exactly the pre-#446 behaviour, reached only when the app already has an
|
|
124
|
+
* unresolvable dep.
|
|
125
|
+
*
|
|
126
|
+
* Sets `lastLiveResolveFailed` on a TRANSIENT failure (so the caller
|
|
127
|
+
* retries), never on a permanent 401 (tolerated).
|
|
128
|
+
*
|
|
129
|
+
* @param {string} install e.g. 'dayjs@1.11.13' or 'dayjs@1.11.13/plugin/utc'
|
|
130
|
+
* @param {string} [provider] one of SUPPORTED_PROVIDERS; defaults to 'jspm'
|
|
131
|
+
* @returns {Promise<Record<string, string>>}
|
|
132
|
+
*/
|
|
133
|
+
async function jspmResolveOne(install, provider = 'jspm', timeoutMs) {
|
|
134
|
+
const { ok, imports, transient } = await jspmProbeOne(install, provider, timeoutMs);
|
|
135
|
+
// Preserve the public contract: an empty map on any failure, and the
|
|
136
|
+
// module-global retry flag set ONLY on a transient one (a permanent 401
|
|
137
|
+
// for an unresolvable private/server-only dep is tolerated).
|
|
138
|
+
if (!ok && transient) lastLiveResolveFailed = true;
|
|
139
|
+
return imports;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Probe a SINGLE install and return the FULL classification, not just the
|
|
144
|
+
* imports. Unlike `jspmResolveOne` this does NOT collapse a transient
|
|
145
|
+
* failure into the same empty map a permanent one yields, and does NOT
|
|
146
|
+
* touch `lastLiveResolveFailed`: the caller (the 401 fallback in
|
|
147
|
+
* `jspmGenerate`) needs to tell "genuinely unresolvable, safe to drop"
|
|
148
|
+
* (`ok:false, transient:false`) from "a network blip mid-probe, do NOT
|
|
149
|
+
* drop" (`ok:false, transient:true`), and owns the retry flag itself.
|
|
150
|
+
*
|
|
151
|
+
* Cached per install + provider; a successful probe's `{imports}` is the
|
|
152
|
+
* same value `jspmResolveOne` returns, so the two share the cache and the
|
|
153
|
+
* later unified re-run reuses it.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} install
|
|
156
|
+
* @param {string} provider
|
|
157
|
+
* @returns {Promise<JspmCallResult>}
|
|
158
|
+
*/
|
|
159
|
+
function jspmProbeOne(install, provider, timeoutMs) {
|
|
160
|
+
const cacheKey = `${provider}::probe::${install}`;
|
|
161
|
+
const existing = jspmCache.get(cacheKey);
|
|
162
|
+
if (existing) return existing;
|
|
163
|
+
|
|
164
|
+
const promise = (async () => {
|
|
165
|
+
const result = await jspmCall([install], provider, timeoutMs);
|
|
166
|
+
// Do not cache a failure: a transient one must be re-attempted on the
|
|
167
|
+
// next resolve, and a permanent one is cheap to re-confirm and must not
|
|
168
|
+
// pin a stale "unresolvable" verdict across a dependency change.
|
|
169
|
+
if (!result.ok) jspmCache.delete(cacheKey);
|
|
170
|
+
return result;
|
|
171
|
+
})();
|
|
172
|
+
|
|
173
|
+
jspmCache.set(cacheKey, promise);
|
|
174
|
+
return promise;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Last-write-wins merge of per-install import fragments. Subpath installs
|
|
179
|
+
* never collide (their keys include the subpath); a shared base package
|
|
180
|
+
* resolves to the same root URL across fragments, so the merge is stable.
|
|
181
|
+
*
|
|
182
|
+
* @param {Array<Record<string, string>>} fragments
|
|
183
|
+
* @returns {Record<string, string>}
|
|
184
|
+
*/
|
|
185
|
+
function mergePerInstall(fragments) {
|
|
186
|
+
const merged = {};
|
|
187
|
+
for (const fragment of fragments) Object.assign(merged, fragment);
|
|
188
|
+
return merged;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolve a list of `pkg@version` installs to importmap entries.
|
|
193
|
+
*
|
|
194
|
+
* Issue #446: the WHOLE set is resolved in ONE api.jspm.io/generate call
|
|
195
|
+
* (a single `install[]` array) so jspm computes one mutually-consistent
|
|
196
|
+
* dependency graph. A directly-imported package and a transitive that
|
|
197
|
+
* needs a newer version of the same package now agree on one URL, instead
|
|
198
|
+
* of the old per-package-in-isolation merge that pinned the direct dep to
|
|
199
|
+
* its local version while the transitive floated to jspm-latest, producing
|
|
200
|
+
* a missing-export crash in the browser.
|
|
201
|
+
*
|
|
202
|
+
* The per-package-isolation property is PRESERVED as a fallback only: if
|
|
203
|
+
* the unified call fails because some install is unresolvable (a 401 for a
|
|
204
|
+
* private / server-only dep), one bad install must not collapse the map
|
|
205
|
+
* for the rest. So:
|
|
206
|
+
* 1. Try the unified call. On success, return its coherent graph.
|
|
207
|
+
* 2. On a PERMANENT failure (401/4xx), probe each install in isolation
|
|
208
|
+
* to learn which ones resolve, then RE-RUN the unified call over only
|
|
209
|
+
* the resolvable subset so the survivors stay mutually consistent.
|
|
210
|
+
* Only installs whose probe fails PERMANENTLY drop out (genuinely
|
|
211
|
+
* unresolvable, the browser never fetched them anyway); if any probe
|
|
212
|
+
* fails TRANSIENTLY, no one is dropped and the resolve is flagged for
|
|
213
|
+
* retry, so a network blip mid-probe cannot evict a good package. If
|
|
214
|
+
* the re-run itself fails, fall back to the merged per-install
|
|
215
|
+
* fragments so the app is no worse off than pre-#446.
|
|
216
|
+
* 3. On a TRANSIENT failure (network / timeout / 5xx / 429), set the
|
|
217
|
+
* retry flag and serve whatever the per-install probe produced.
|
|
218
|
+
*
|
|
219
|
+
* The unified result is cached per sorted-install-set + provider; the
|
|
220
|
+
* per-install fallback reuses the per-install cache entries.
|
|
221
|
+
*
|
|
222
|
+
* @param {Array<string>} installs e.g. ['dayjs@1.11.13', 'clsx@2.1.1']
|
|
223
|
+
* @param {string} [provider] one of SUPPORTED_PROVIDERS; defaults to 'jspm'
|
|
224
|
+
* @param {number} [timeoutMs] per-call budget. Defaults to the SERVER one,
|
|
225
|
+
* because this runs on a cold first request as well as from the CLI; the two
|
|
226
|
+
* pin commands pass PIN_BUNDLE_TIMEOUT_MS instead. importmap-rails only ever
|
|
227
|
+
* resolves from the CLI, so its flat 60s has no request path to slow down.
|
|
228
|
+
* @returns {Promise<Record<string, string>>}
|
|
229
|
+
*/
|
|
230
|
+
export async function jspmGenerate(installs, provider = 'jspm', timeoutMs) {
|
|
231
|
+
if (installs.length === 0) return {};
|
|
232
|
+
|
|
233
|
+
// A single install has no cross-package graph to reconcile, so the
|
|
234
|
+
// isolated path IS the coherent path; reuse the per-install cache.
|
|
235
|
+
if (installs.length === 1) return jspmResolveOne(installs[0], provider, timeoutMs);
|
|
236
|
+
|
|
237
|
+
// Stable key regardless of scan order so the same dep set hits cache.
|
|
238
|
+
const unifiedKey = `${provider}::unified::${[...installs].sort().join('\n')}`;
|
|
239
|
+
const cached = jspmCache.get(unifiedKey);
|
|
240
|
+
if (cached) return cached;
|
|
241
|
+
|
|
242
|
+
const promise = (async () => {
|
|
243
|
+
const unified = await jspmCall(installs, provider, timeoutMs);
|
|
244
|
+
if (unified.ok) return unified.imports;
|
|
245
|
+
|
|
246
|
+
// The unified call failed. Drop the cached failure so a later retry
|
|
247
|
+
// re-attempts; the per-install fallback owns the retry flag.
|
|
248
|
+
jspmCache.delete(unifiedKey);
|
|
249
|
+
|
|
250
|
+
if (unified.transient) {
|
|
251
|
+
// Network / 5xx: nothing resolved coherently. Fall back to merged
|
|
252
|
+
// per-install fragments (each may still be cached / reachable) so we
|
|
253
|
+
// serve whatever we can, and flag the transient failure for retry.
|
|
254
|
+
lastLiveResolveFailed = true;
|
|
255
|
+
return mergePerInstall(await Promise.all(installs.map(i => jspmResolveOne(i, provider, timeoutMs))));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Permanent failure: at least one install is unresolvable. Probe each
|
|
259
|
+
// in isolation to learn which ones jspm can resolve, then re-run the
|
|
260
|
+
// unified call over only those so the survivors form one consistent
|
|
261
|
+
// graph (restores #446 coherence for the resolvable subset).
|
|
262
|
+
const probes = await Promise.all(installs.map(i => jspmProbeOne(i, provider, timeoutMs)));
|
|
263
|
+
|
|
264
|
+
// A GOOD package whose isolated probe failed TRANSIENTLY (a network blip
|
|
265
|
+
// mid-probe) must NOT be classified as unresolvable and dropped. Only a
|
|
266
|
+
// PERMANENT probe failure (401/404) means the install is genuinely
|
|
267
|
+
// unresolvable. If any probe failed transiently, we cannot safely decide
|
|
268
|
+
// the resolvable set this pass, so flag the whole resolve transient-
|
|
269
|
+
// failed and serve the merged fragments WITHOUT dropping anyone; the next
|
|
270
|
+
// ensureReady retry re-resolves once the blip clears. Conflating the two
|
|
271
|
+
// here is exactly the bug this guard prevents.
|
|
272
|
+
const transientProbe = probes.some(p => !p.ok && p.transient);
|
|
273
|
+
if (transientProbe) {
|
|
274
|
+
lastLiveResolveFailed = true;
|
|
275
|
+
return mergePerInstall(probes.map(p => p.imports));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// From here every failed probe is PERMANENT, so dropping it is safe.
|
|
279
|
+
const resolvable = installs.filter((_, idx) => probes[idx].ok);
|
|
280
|
+
|
|
281
|
+
if (resolvable.length === installs.length) {
|
|
282
|
+
// Every install resolved alone but the batch 401'd: a genuine
|
|
283
|
+
// cross-package CONFLICT jspm could not satisfy as one graph (rare).
|
|
284
|
+
// The coherent graph is unavailable, so serve the merged fragments
|
|
285
|
+
// (pre-#446 behaviour) rather than nothing. NOTE: this degraded path
|
|
286
|
+
// can REINTRODUCE the #446 skew, because last-write-wins on a shared
|
|
287
|
+
// transitive across independent fragments is exactly the merge the
|
|
288
|
+
// unified call exists to avoid. It is a deliberate degrade-not-crash
|
|
289
|
+
// fallback for an unsatisfiable graph: no coherent graph exists, so a
|
|
290
|
+
// possibly-skewed map beats no map. The common conflicting-deps case
|
|
291
|
+
// (one shared transitive needing a newer version, issue #446's repro)
|
|
292
|
+
// IS satisfiable and resolves coherently on the unified path above;
|
|
293
|
+
// only a genuinely unsatisfiable set reaches here.
|
|
294
|
+
return mergePerInstall(probes.map(p => p.imports));
|
|
295
|
+
}
|
|
296
|
+
if (resolvable.length === 0) return {};
|
|
297
|
+
if (resolvable.length === 1) return jspmResolveOne(resolvable[0], provider, timeoutMs);
|
|
298
|
+
|
|
299
|
+
// Re-run unified over the resolvable subset. If even that fails (a
|
|
300
|
+
// conflict among the survivors), fall back to their merged fragments.
|
|
301
|
+
const retry = await jspmCall(resolvable, provider, timeoutMs);
|
|
302
|
+
if (retry.ok) return retry.imports;
|
|
303
|
+
return mergePerInstall(resolvable.map(i => probes[installs.indexOf(i)].imports));
|
|
304
|
+
})();
|
|
305
|
+
|
|
306
|
+
jspmCache.set(unifiedKey, promise);
|
|
307
|
+
return promise;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Build importmap entries for discovered bare imports. For each scanned
|
|
312
|
+
* package, resolve its installed version from node_modules, then ask
|
|
313
|
+
* api.jspm.io/generate for the full importmap fragment.
|
|
314
|
+
*
|
|
315
|
+
* Async because the Generator API call is networked. Called from
|
|
316
|
+
* `resolveVendorImports` on the first request (and after a rebuild),
|
|
317
|
+
* inside `ensureReady`; never at boot, and not on every request.
|
|
318
|
+
*
|
|
319
|
+
* @param {Set<string>} bareImports from scanBareImports()
|
|
320
|
+
* @param {string} appDir
|
|
321
|
+
* @returns {Promise<Record<string, string>>}
|
|
322
|
+
*/
|
|
323
|
+
export async function vendorImportMapEntries(bareImports, appDir) {
|
|
324
|
+
const installs = [];
|
|
325
|
+
for (const spec of bareImports) {
|
|
326
|
+
if (BUILTIN.has(spec)) continue;
|
|
327
|
+
const pkg = extractPackageName(spec);
|
|
328
|
+
if (!pkg || BUILTIN.has(pkg) || FRAMEWORK_SERVER_ONLY.has(pkg)) continue;
|
|
329
|
+
const version = getPackageVersion(pkg, appDir);
|
|
330
|
+
if (!version) continue;
|
|
331
|
+
// Splice the version into the specifier: 'dayjs/plugin/utc' with
|
|
332
|
+
// version 1.11.13 becomes 'dayjs@1.11.13/plugin/utc'. jspm.io's
|
|
333
|
+
// Generator API resolves subpaths individually via the package's
|
|
334
|
+
// `exports` field. Root imports stay as `<pkg>@<version>` with no
|
|
335
|
+
// trailing subpath.
|
|
336
|
+
const subpath = spec.slice(pkg.length);
|
|
337
|
+
installs.push(`${pkg}@${version}${subpath}`);
|
|
338
|
+
}
|
|
339
|
+
return jspmGenerate(installs);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Clear in-memory JSPM generator cache.
|
|
344
|
+
*/
|
|
345
|
+
export function clearJspmCache() {
|
|
346
|
+
jspmCache.clear();
|
|
347
|
+
}
|
|
348
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { join, dirname, sep } from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a package's installed directory on disk, handling both direct
|
|
7
|
+
* installation and npm workspace hoisting.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} pkgName
|
|
10
|
+
* @param {string} appDir
|
|
11
|
+
* @returns {string | null}
|
|
12
|
+
*/
|
|
13
|
+
function resolvePackageDir(pkgName, appDir) {
|
|
14
|
+
let entry;
|
|
15
|
+
try {
|
|
16
|
+
const req = createRequire(join(appDir, 'package.json'));
|
|
17
|
+
entry = req.resolve(pkgName);
|
|
18
|
+
} catch {
|
|
19
|
+
// Resolve ONLY from the app. Falling back to the framework's own
|
|
20
|
+
// resolution would vendor a package the app never installed, which in a
|
|
21
|
+
// monorepo (where everything hoists to the root) is most of them.
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const parts = entry.split(sep);
|
|
26
|
+
const nmIdx = parts.lastIndexOf('node_modules');
|
|
27
|
+
if (nmIdx < 0) {
|
|
28
|
+
let dir = dirname(entry);
|
|
29
|
+
for (let i = 0; i < 8; i++) {
|
|
30
|
+
if (existsSync(join(dir, 'package.json'))) return realpathSync(dir);
|
|
31
|
+
const parent = dirname(dir);
|
|
32
|
+
if (parent === dir) break;
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const segmentsAfterNm = pkgName.startsWith('@') ? 2 : 1;
|
|
38
|
+
const root = parts.slice(0, nmIdx + 1 + segmentsAfterNm).join(sep);
|
|
39
|
+
return realpathSync(root);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read the installed version of a package from `node_modules/<pkg>/
|
|
47
|
+
* package.json`. Handles workspace hoisting and packages that lock
|
|
48
|
+
* down `./package.json` in their exports field.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} pkgName
|
|
51
|
+
* @param {string} appDir
|
|
52
|
+
* @returns {string | null}
|
|
53
|
+
*/
|
|
54
|
+
export function getPackageVersion(pkgName, appDir) {
|
|
55
|
+
const real = resolvePackageDir(pkgName, appDir);
|
|
56
|
+
if (!real) return null;
|
|
57
|
+
try {
|
|
58
|
+
const pkg = JSON.parse(readFileSync(join(real, 'package.json'), 'utf8'));
|
|
59
|
+
return pkg.version || null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read the installed package's declared `dependencies` + `peerDependencies`
|
|
67
|
+
* from its `package.json`, hoist-aware (same resolution as `getPackageVersion`,
|
|
68
|
+
* so a monorepo-hoisted dep resolves from the workspace root). Returns null
|
|
69
|
+
* when the package is not installed / unreadable, which the importmap-coherence
|
|
70
|
+
* check (#450) treats as "could not verify" rather than a conflict.
|
|
71
|
+
*
|
|
72
|
+
* This is the "already-resolved metadata, no network" source the coherence
|
|
73
|
+
* check prefers: the package is on disk because the importmap pinned it, so its
|
|
74
|
+
* manifest is a local read.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} pkgName
|
|
77
|
+
* @param {string} appDir
|
|
78
|
+
* @returns {{ dependencies: Record<string,string>, peerDependencies: Record<string,string> } | null}
|
|
79
|
+
*/
|
|
80
|
+
export function getPackageManifest(pkgName, appDir) {
|
|
81
|
+
const real = resolvePackageDir(pkgName, appDir);
|
|
82
|
+
if (!real) return null;
|
|
83
|
+
try {
|
|
84
|
+
const pkg = JSON.parse(readFileSync(join(real, 'package.json'), 'utf8'));
|
|
85
|
+
return {
|
|
86
|
+
dependencies: pkg.dependencies || {},
|
|
87
|
+
peerDependencies: pkg.peerDependencies || {},
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// JSPM Generator API client
|
|
96
|
+
// ---------------------------------------------------------------------------
|