@webjsdev/server 0.8.67 → 0.8.69
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/package.json +1 -1
- package/src/auth.js +87 -26
- package/src/dev/config.js +22 -0
- package/src/dev/handler.js +39 -1
- package/src/dev/serve.js +24 -1
- package/src/dev-embed-client.js +331 -0
- package/src/dev-embed.js +147 -0
- package/src/dev-overlay.js +8 -0
- package/src/dev-source-locations-hook.js +139 -0
- package/src/dev-source-locations.js +453 -0
- package/src/ssr/head.js +2 -1
- package/src/webjs-config-validate.js +3 -3
- package/webjs-config.schema.json +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/server",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.69",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The server for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Provides the file-based router, SSR, server actions, route handlers, middleware, and live reload on Node 24+ or Bun.",
|
|
6
6
|
"main": "index.js",
|
package/src/auth.js
CHANGED
|
@@ -15,6 +15,10 @@ const enc = new TextEncoder();
|
|
|
15
15
|
const dec = new TextDecoder();
|
|
16
16
|
const AUTH_COOKIE = 'webjs.auth';
|
|
17
17
|
const STATE_COOKIE = 'webjs.auth.state';
|
|
18
|
+
// Carries the post-sign-in target across the OAuth round trip (#1495). Signed
|
|
19
|
+
// with the auth secret and short-lived (the same 10 minutes as the state
|
|
20
|
+
// cookie), so the callback only ever lands on a path this app chose.
|
|
21
|
+
const REDIRECT_COOKIE = 'webjs.auth.redirect';
|
|
18
22
|
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60; // 30 days (seconds)
|
|
19
23
|
|
|
20
24
|
// -- Web Crypto helpers -----------------------------------------------------
|
|
@@ -65,6 +69,27 @@ function randomId() {
|
|
|
65
69
|
return b64url(bytes.buffer);
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Accept a post-sign-in target only when it is a same-origin local path: one
|
|
74
|
+
* leading `/`, the next character neither `/` nor `\` (both make a
|
|
75
|
+
* protocol-relative URL a browser follows off-site), and no control
|
|
76
|
+
* characters or whitespace a browser might strip before resolving. Anything
|
|
77
|
+
* else returns `null` and the caller falls back to its default. A bad value is
|
|
78
|
+
* DROPPED, never repaired, since a repaired open redirect is still one.
|
|
79
|
+
*
|
|
80
|
+
* @param {unknown} value
|
|
81
|
+
* @returns {string | null}
|
|
82
|
+
*/
|
|
83
|
+
function safeRedirectPath(value) {
|
|
84
|
+
if (typeof value !== 'string') return null;
|
|
85
|
+
if (value.length === 0 || value.length > 2048) return null;
|
|
86
|
+
if (value[0] !== '/') return null;
|
|
87
|
+
if (value[1] === '/' || value[1] === '\\') return null;
|
|
88
|
+
// eslint-disable-next-line no-control-regex
|
|
89
|
+
if (/[\u0000-\u001f\u007f\s\\]/.test(value)) return null;
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
68
93
|
// -- Cookie helpers ---------------------------------------------------------
|
|
69
94
|
|
|
70
95
|
/** @param {string} header @returns {Record<string,string>} */
|
|
@@ -290,11 +315,16 @@ export function createAuth(config) {
|
|
|
290
315
|
if (ok === false) return new Response(null, { status: 302, headers: { location: pages.error || '/?error=AccessDenied' } });
|
|
291
316
|
}
|
|
292
317
|
const cookie = await writeSession(user);
|
|
293
|
-
|
|
318
|
+
// `opts.redirectTo` is the app's own server-side choice and is trusted.
|
|
319
|
+
// `data.redirectTo` usually arrives from a submitted form, so it goes
|
|
320
|
+
// through the same local-path check the OAuth flow uses (#1495).
|
|
321
|
+
const redirectTo = opts.redirectTo || safeRedirectPath(data && data.redirectTo) || '/';
|
|
294
322
|
return new Response(null, { status: 302, headers: { location: /** @type {string} */ (redirectTo), 'set-cookie': cookie } });
|
|
295
323
|
}
|
|
296
324
|
|
|
297
|
-
if (provider.type === 'oauth')
|
|
325
|
+
if (provider.type === 'oauth') {
|
|
326
|
+
return oauthRedirect(provider, { ...opts, redirectTo: opts.redirectTo || (data && data.redirectTo) });
|
|
327
|
+
}
|
|
298
328
|
return new Response('Unsupported provider type', { status: 400 });
|
|
299
329
|
}
|
|
300
330
|
|
|
@@ -332,9 +362,30 @@ export function createAuth(config) {
|
|
|
332
362
|
const hdrs = new Headers();
|
|
333
363
|
hdrs.set('location', url.toString());
|
|
334
364
|
hdrs.append('set-cookie', setCookie(STATE_COOKIE, await sign(state, secret), 600_000, secure()));
|
|
365
|
+
// The post-sign-in target rides in its own signed cookie (#1495). With no
|
|
366
|
+
// valid target, any leftover cookie from an abandoned earlier attempt is
|
|
367
|
+
// cleared so it cannot steer this sign-in somewhere stale.
|
|
368
|
+
const target = safeRedirectPath(opts.redirectTo);
|
|
369
|
+
hdrs.append('set-cookie', target
|
|
370
|
+
? setCookie(REDIRECT_COOKIE, await sign(target, secret), 600_000, secure())
|
|
371
|
+
: clearCookie(REDIRECT_COOKIE));
|
|
335
372
|
return new Response(null, { status: 302, headers: hdrs });
|
|
336
373
|
}
|
|
337
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Read the signed post-sign-in target the redirect leg stored, re-checking it
|
|
377
|
+
* as a local path (defence in depth: the signature proves this app wrote it,
|
|
378
|
+
* the check proves it is still a safe target). Null when absent or invalid.
|
|
379
|
+
*
|
|
380
|
+
* @param {Request} req
|
|
381
|
+
* @returns {Promise<string | null>}
|
|
382
|
+
*/
|
|
383
|
+
async function readRedirectCookie(req) {
|
|
384
|
+
const raw = parseCookies(req.headers.get('cookie') || '')[REDIRECT_COOKIE];
|
|
385
|
+
if (!raw) return null;
|
|
386
|
+
return safeRedirectPath(await unsign(raw, secret));
|
|
387
|
+
}
|
|
388
|
+
|
|
338
389
|
async function oauthCallback(req, provider) {
|
|
339
390
|
const url = new URL(req.url);
|
|
340
391
|
const code = url.searchParams.get('code');
|
|
@@ -376,13 +427,20 @@ export function createAuth(config) {
|
|
|
376
427
|
|
|
377
428
|
if (cb.signIn) {
|
|
378
429
|
const ok = await cb.signIn({ user, account: { provider: provider.id, accessToken } });
|
|
379
|
-
if (ok === false)
|
|
430
|
+
if (ok === false) {
|
|
431
|
+
const denied = new Headers();
|
|
432
|
+
denied.set('location', pages.error || '/?error=AccessDenied');
|
|
433
|
+
denied.append('set-cookie', clearCookie(STATE_COOKIE));
|
|
434
|
+
denied.append('set-cookie', clearCookie(REDIRECT_COOKIE));
|
|
435
|
+
return new Response(null, { status: 302, headers: denied });
|
|
436
|
+
}
|
|
380
437
|
}
|
|
381
438
|
|
|
382
439
|
const hdrs = new Headers();
|
|
383
|
-
hdrs.set('location', '/');
|
|
440
|
+
hdrs.set('location', (await readRedirectCookie(req)) || '/');
|
|
384
441
|
hdrs.append('set-cookie', await writeSession(user));
|
|
385
442
|
hdrs.append('set-cookie', clearCookie(STATE_COOKIE));
|
|
443
|
+
hdrs.append('set-cookie', clearCookie(REDIRECT_COOKIE));
|
|
386
444
|
return new Response(null, { status: 302, headers: hdrs });
|
|
387
445
|
}
|
|
388
446
|
|
|
@@ -401,7 +459,7 @@ export function createAuth(config) {
|
|
|
401
459
|
if (seg[0] === 'signin' && seg[1]) {
|
|
402
460
|
const p = providers.get(seg[1]);
|
|
403
461
|
if (!p || p.type !== 'oauth') return new Response('Unknown OAuth provider', { status: 404 });
|
|
404
|
-
return oauthRedirect(p, { req });
|
|
462
|
+
return oauthRedirect(p, { req, redirectTo: new URL(req.url).searchParams.get('redirectTo') });
|
|
405
463
|
}
|
|
406
464
|
if (seg[0] === 'callback' && seg[1]) {
|
|
407
465
|
const p = providers.get(seg[1]);
|
|
@@ -423,28 +481,31 @@ export function createAuth(config) {
|
|
|
423
481
|
if (seg[0] === 'signin' && seg[1]) {
|
|
424
482
|
const provider = providers.get(seg[1]);
|
|
425
483
|
if (!provider) return new Response('Unknown provider', { status: 404 });
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
484
|
+
// Bounded body read (issue #237): the sign-in endpoint is public and
|
|
485
|
+
// unauthenticated, so cap its body to defend against memory exhaustion.
|
|
486
|
+
// Credentials (and an OAuth form's `redirectTo`, #1495) are small
|
|
487
|
+
// fixed-shape JSON / form data, so the JSON / RPC limit applies. An
|
|
488
|
+
// over-limit body is a 413 before any parse, and is never buffered
|
|
489
|
+
// whole (see body-limit.js).
|
|
490
|
+
const limits = getBodyLimits();
|
|
491
|
+
const limit = limits ? limits.json : DEFAULT_MAX_BODY_BYTES;
|
|
492
|
+
/** @type {Record<string, unknown>} */
|
|
493
|
+
let body = {};
|
|
494
|
+
const ct = req.headers.get('content-type') || '';
|
|
495
|
+
if (ct.includes('json')) {
|
|
496
|
+
const { tooLarge, text } = await readTextBounded(req, limit);
|
|
497
|
+
if (tooLarge) return payloadTooLarge();
|
|
498
|
+
body = text ? JSON.parse(text) : {};
|
|
499
|
+
} else if (ct.includes('form')) {
|
|
500
|
+
const { tooLarge, formData } = await readFormDataBounded(req, limit);
|
|
501
|
+
if (tooLarge) return payloadTooLarge();
|
|
502
|
+
for (const [k, v] of formData.entries()) body[k] = v;
|
|
503
|
+
}
|
|
504
|
+
if (provider.type === 'credentials') return signInFn('credentials', body, { req });
|
|
505
|
+
if (provider.type === 'oauth') {
|
|
506
|
+
const redirectTo = body.redirectTo ?? new URL(req.url).searchParams.get('redirectTo');
|
|
507
|
+
return oauthRedirect(provider, { req, redirectTo });
|
|
446
508
|
}
|
|
447
|
-
if (provider.type === 'oauth') return oauthRedirect(provider, { req });
|
|
448
509
|
}
|
|
449
510
|
if (seg[0] === 'signout') return signOutFn({ req });
|
|
450
511
|
return new Response('Not found', { status: 404 });
|
package/src/dev/config.js
CHANGED
|
@@ -317,6 +317,28 @@ export async function readDevWatchPathsFromApp(appDir) {
|
|
|
317
317
|
return out;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* The dev tooling switches in `webjs.dev` (#1504): `embedOrigins` (the embed
|
|
322
|
+
* bridge, #1498) and `sourceLocations` (#1499). Raw values; the env overrides
|
|
323
|
+
* and validation live with each feature (`resolveEmbedOrigins`,
|
|
324
|
+
* `sourceLocationsRequested`). A missing or unreadable package.json is both
|
|
325
|
+
* off.
|
|
326
|
+
*
|
|
327
|
+
* @param {string} appDir
|
|
328
|
+
* @returns {Promise<{ embedOrigins: unknown, sourceLocations: unknown }>}
|
|
329
|
+
*/
|
|
330
|
+
export async function readDevToolingFromApp(appDir) {
|
|
331
|
+
let dev;
|
|
332
|
+
try {
|
|
333
|
+
const pkg = JSON.parse(await readFile(join(appDir, 'package.json'), 'utf8'));
|
|
334
|
+
dev = pkg && pkg.webjs && pkg.webjs.dev;
|
|
335
|
+
} catch {
|
|
336
|
+
dev = undefined;
|
|
337
|
+
}
|
|
338
|
+
if (!dev || typeof dev !== 'object') return { embedOrigins: undefined, sourceLocations: undefined };
|
|
339
|
+
return { embedOrigins: dev.embedOrigins, sourceLocations: dev.sourceLocations };
|
|
340
|
+
}
|
|
341
|
+
|
|
320
342
|
/**
|
|
321
343
|
* Resolve the node:http server timeouts (issue #237) from the app's
|
|
322
344
|
* package.json `webjs.requestTimeoutMs` / `webjs.headersTimeoutMs` /
|
package/src/dev/handler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { join, resolve, sep } from 'node:path';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
buildActionIndex,
|
|
17
17
|
} from '../actions.js';
|
|
18
18
|
import { registerActionHooks } from '../action-seed.js';
|
|
19
|
+
import { sourceLocationsRequested } from '../dev-source-locations.js';
|
|
20
|
+
import { registerSourceLocationHook } from '../dev-source-locations-hook.js';
|
|
19
21
|
import { readRegenerateRules, isRegenerateOutputPath } from '../dev-regenerate.js';
|
|
20
22
|
import { classifyChangedPath } from '../dev-classify.js';
|
|
21
23
|
import { ensureStripper } from '../ts-strip.js';
|
|
@@ -39,6 +41,7 @@ import { setAssetUrlProvider, setFormActionResolver } from '@webjsdev/core';
|
|
|
39
41
|
import { resolveActionIdentity } from '../form-action-identity.js';
|
|
40
42
|
import { setAssetRoots, clearAssetHashCache, setElisionFingerprint, withAssetHash, assetHashFor, resolveAssetUrl } from '../asset-hash.js';
|
|
41
43
|
import { applySecurityHeaders, webRequestIsHttps } from '../headers.js';
|
|
44
|
+
import { resolveEmbedOrigins, setEmbedOrigins, allowEmbedFraming } from '../dev-embed.js';
|
|
42
45
|
import {
|
|
43
46
|
applyRedirects,
|
|
44
47
|
applyTrailingSlash,
|
|
@@ -59,6 +62,7 @@ import {
|
|
|
59
62
|
readAllowedOriginsFromApp,
|
|
60
63
|
readCspConfigFromApp,
|
|
61
64
|
readBodyLimitsFromApp,
|
|
65
|
+
readDevToolingFromApp,
|
|
62
66
|
} from './config.js';
|
|
63
67
|
import {
|
|
64
68
|
exists, kebab, resolveRequestId, shouldAccessLog, loadAppEnv, collectRouteModules,
|
|
@@ -213,6 +217,16 @@ export async function createRequestHandler(opts) {
|
|
|
213
217
|
// analysis pass re-reads it each rebuild so a dev toggle takes effect live.
|
|
214
218
|
setClientRouterEnabled(await readClientRouterEnabled(appDir));
|
|
215
219
|
|
|
220
|
+
// Dev embed bridge (#1498): `WEBJS_EMBED_ORIGINS` (or `webjs.dev.embedOrigins`) lets the listed parent
|
|
221
|
+
// origins frame this dev server and receive the bridge's messages. Read once
|
|
222
|
+
// here, and only in dev, so a production handler records no origins and
|
|
223
|
+
// neither injects the script nor relaxes a framing header.
|
|
224
|
+
// `webjs.dev.embedOrigins` in package.json is the default (#1504); a set
|
|
225
|
+
// env var replaces it for one run.
|
|
226
|
+
const devTooling = dev ? await readDevToolingFromApp(appDir) : { embedOrigins: undefined, sourceLocations: undefined };
|
|
227
|
+
const embedOriginsValue = dev ? resolveEmbedOrigins(process.env.WEBJS_EMBED_ORIGINS, devTooling.embedOrigins) : [];
|
|
228
|
+
setEmbedOrigins(embedOriginsValue);
|
|
229
|
+
|
|
216
230
|
const coreDir = locateCoreDir(appDir);
|
|
217
231
|
// Switch the importmap between dist/ bundles and src/ per-file
|
|
218
232
|
// URLs depending on whether the resolved @webjsdev/core install
|
|
@@ -278,6 +292,21 @@ export async function createRequestHandler(opts) {
|
|
|
278
292
|
// on seeding would mean `webjs.seed: false` silently broke every no-JS form.
|
|
279
293
|
await registerActionHooks({ seed: await readSeedEnabled(appDir), dev });
|
|
280
294
|
|
|
295
|
+
// Dev source locations (#1499): `WEBJS_SOURCE_LOCATIONS=1` (or
|
|
296
|
+
// `webjs.dev.sourceLocations: true`) under `webjs dev`
|
|
297
|
+
// stamps `data-webjs-src="<file>:<line>"` on the element opening tags of the
|
|
298
|
+
// app's `html` templates. Read once (the SSR load hook is process-global and
|
|
299
|
+
// cannot be uninstalled), never in prod. The browser half reads
|
|
300
|
+
// `state.sourceLocations` in serve.js; the SSR half is this load hook, which
|
|
301
|
+
// must be in place before any app module is imported.
|
|
302
|
+
// `webjs.dev.sourceLocations` is the default (#1504); the env var wins.
|
|
303
|
+
const sourceLocations = dev && sourceLocationsRequested(process.env, devTooling.sourceLocations);
|
|
304
|
+
if (sourceLocations) {
|
|
305
|
+
registerSourceLocationHook(appDir);
|
|
306
|
+
// Module URLs are realpaths, so a symlinked app dir registers both forms.
|
|
307
|
+
try { const real = realpathSync(appDir); if (real !== appDir) registerSourceLocationHook(real); } catch { /* keep appDir only */ }
|
|
308
|
+
}
|
|
309
|
+
|
|
281
310
|
// When an app commits a vendor pin (.webjs/vendor/importmap.json) it carries a
|
|
282
311
|
// deterministic vendor map that is cheap to read (one file, no analysis, no
|
|
283
312
|
// network). Resolve it AT BOOT and publish the build id immediately so the
|
|
@@ -430,6 +459,9 @@ export async function createRequestHandler(opts) {
|
|
|
430
459
|
// settings (a multi-tenant embedder, or the differential elision test)
|
|
431
460
|
// must not share it, or the second would serve the first's elided source.
|
|
432
461
|
tsCache: new Map(),
|
|
462
|
+
// Dev source locations (#1499), fixed for the handler's lifetime, so the
|
|
463
|
+
// per-handler tsCache above never mixes annotated and plain bytes.
|
|
464
|
+
sourceLocations,
|
|
433
465
|
// The most recent unresolved dev error frame (#264), or null. Pushed to the
|
|
434
466
|
// SSE channel for the live overlay and replayed to a freshly-connected tab.
|
|
435
467
|
// Dev-only (never populated when !dev); cleared on a successful rebuild.
|
|
@@ -996,6 +1028,12 @@ export async function createRequestHandler(opts) {
|
|
|
996
1028
|
}
|
|
997
1029
|
}
|
|
998
1030
|
|
|
1031
|
+
// Dev embed bridge (#1498): let the allowed origins frame the page. Runs
|
|
1032
|
+
// after every header source above (defaults, webjs.headers, middleware,
|
|
1033
|
+
// the CSP), so it overrides them all, which is the point: the variable
|
|
1034
|
+
// is the developer saying "this dev server is framed by these origins".
|
|
1035
|
+
if (embedOriginsValue.length) allowEmbedFraming(merged.headers, embedOriginsValue);
|
|
1036
|
+
|
|
999
1037
|
// Server HTML cache write (#241): if the SSR marked this response as a
|
|
1000
1038
|
// cache candidate (an opted-in `revalidate` page), store the FINAL body
|
|
1001
1039
|
// now, after segment middleware has added any per-user Set-Cookie (which
|
package/src/dev/serve.js
CHANGED
|
@@ -28,6 +28,7 @@ import { stripTypeScript } from '../ts-strip.js';
|
|
|
28
28
|
import { serveDownloadedBundle } from '../vendor.js';
|
|
29
29
|
import { resolveImport } from '../module-graph.js';
|
|
30
30
|
import { elideImportsFromSource } from '../component-elision.js';
|
|
31
|
+
import { annotateSourceLocations, isSourceLocationCandidate, sourceLocationFile } from '../dev-source-locations.js';
|
|
31
32
|
import { basePath } from '../importmap.js';
|
|
32
33
|
import { withBasePath } from '../base-path.js';
|
|
33
34
|
import { versionModuleImports } from '../asset-hash.js';
|
|
@@ -322,6 +323,9 @@ export async function handleCore(req, ctx) {
|
|
|
322
323
|
moduleGraph: state.moduleGraph,
|
|
323
324
|
elidableComponents: state.elidableComponents,
|
|
324
325
|
appDir,
|
|
326
|
+
// Dev source locations (#1499): annotate the served module's `html`
|
|
327
|
+
// templates exactly as the SSR load hook annotates the server's copy.
|
|
328
|
+
sourceLocations: state.sourceLocations === true,
|
|
325
329
|
};
|
|
326
330
|
// A `?v=<hash>` app-module request is content-addressed -> immutable
|
|
327
331
|
// (#243); an un-fingerprinted request keeps the 1h fallback.
|
|
@@ -691,7 +695,7 @@ export async function jsModuleResponse(abs, dev, elideOpts, immutable) {
|
|
|
691
695
|
try { source = await readFile(abs, 'utf8'); }
|
|
692
696
|
catch { return new Response('Not found', { status: 404 }); }
|
|
693
697
|
let code = elideImportsFromSource(
|
|
694
|
-
source, abs, elideOpts.moduleGraph, elideOpts.elidableComponents, resolveImport, elideOpts.appDir,
|
|
698
|
+
annotateServedModule(source, abs, elideOpts), abs, elideOpts.moduleGraph, elideOpts.elidableComponents, resolveImport, elideOpts.appDir,
|
|
695
699
|
);
|
|
696
700
|
// Version same-origin relative import specifiers so the URL the browser
|
|
697
701
|
// fetches matches the `?v=`-versioned modulepreload + boot specifier (#369).
|
|
@@ -707,6 +711,24 @@ export async function jsModuleResponse(abs, dev, elideOpts, immutable) {
|
|
|
707
711
|
return new Response(code, { status: 200, headers });
|
|
708
712
|
}
|
|
709
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Dev source locations (#1499): stamp `data-webjs-src` on the element opening
|
|
716
|
+
* tags of a served app module's `html` templates, the same transform the SSR
|
|
717
|
+
* load hook applies to the server's copy, so the two renders agree. A no-op
|
|
718
|
+
* unless the handler was built with `WEBJS_SOURCE_LOCATIONS=1` under dev, and
|
|
719
|
+
* for any module outside the app or under `node_modules`. Runs on the stripped
|
|
720
|
+
* code, which keeps every line where the source had it.
|
|
721
|
+
*
|
|
722
|
+
* @param {string} code
|
|
723
|
+
* @param {string} abs
|
|
724
|
+
* @param {{ appDir: string, sourceLocations?: boolean } | undefined} opts
|
|
725
|
+
* @returns {string}
|
|
726
|
+
*/
|
|
727
|
+
function annotateServedModule(code, abs, opts) {
|
|
728
|
+
if (!opts || !opts.sourceLocations || !isSourceLocationCandidate(abs, opts.appDir)) return code;
|
|
729
|
+
return annotateSourceLocations(code, sourceLocationFile(abs, opts.appDir));
|
|
730
|
+
}
|
|
731
|
+
|
|
710
732
|
/**
|
|
711
733
|
* Strip TypeScript types from `source` via Node's built-in
|
|
712
734
|
* `module.stripTypeScriptTypes`. Position-preserving whitespace
|
|
@@ -815,6 +837,7 @@ export async function tsResponse(abs, dev, elideOpts, cache, immutable, reportDe
|
|
|
815
837
|
throw err;
|
|
816
838
|
}
|
|
817
839
|
if (elideOpts) {
|
|
840
|
+
code = annotateServedModule(code, abs, elideOpts);
|
|
818
841
|
code = elideImportsFromSource(
|
|
819
842
|
code, abs, elideOpts.moduleGraph, elideOpts.elidableComponents, resolveImport, elideOpts.appDir,
|
|
820
843
|
);
|