@webjsdev/server 0.8.17 → 0.8.18
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/asset-hash.js +155 -1
- package/src/check.js +37 -7
- package/src/dev.js +11 -2
- package/src/js-scan.js +13 -3
package/package.json
CHANGED
package/src/asset-hash.js
CHANGED
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
|
|
40
40
|
import { createHash } from 'node:crypto';
|
|
41
41
|
import { readFileSync } from 'node:fs';
|
|
42
|
-
import { join, resolve, sep } from 'node:path';
|
|
42
|
+
import { extname, join, resolve, sep } from 'node:path';
|
|
43
|
+
import { redactStringsAndTemplates } from './js-scan.js';
|
|
44
|
+
import { resolveImport } from './module-graph.js';
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
47
|
* Length of the emitted hex hash. 12 hex chars (48 bits) is plenty to
|
|
@@ -264,3 +266,155 @@ export function withAssetHash(url, basePath = '') {
|
|
|
264
266
|
const sepChar = url.includes('?') ? '&' : '?';
|
|
265
267
|
return `${url}${sepChar}v=${hash}`;
|
|
266
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Static `import` specifier with positional indices. Mirrors module-graph.js's
|
|
272
|
+
* `IMPORT_RE` (side-effect / default / namespace / named imports), but captures
|
|
273
|
+
* the quote (group 1) and specifier (group 2) so the `/d` flag yields the
|
|
274
|
+
* specifier's start/end for an in-place splice. Excludes dynamic `import(...)`
|
|
275
|
+
* (no whitespace after `import`) and `import.meta` (no quote), exactly like the
|
|
276
|
+
* graph scanner, so the rewrite set is the static-import set the modulepreload
|
|
277
|
+
* hints cover.
|
|
278
|
+
*
|
|
279
|
+
* @type {RegExp}
|
|
280
|
+
*/
|
|
281
|
+
const IMPORT_SPEC_RE = /\bimport\s+(?:(?:[\w*{}\s,]+)\s+from\s+)?(['"])([^'"]+)\1/dg;
|
|
282
|
+
|
|
283
|
+
/** Match `export … from '…'` re-exports (mirrors module-graph's EXPORT_FROM_RE). @type {RegExp} */
|
|
284
|
+
const EXPORT_SPEC_RE = /\bexport\b[^'";]+?\sfrom\s+(['"])([^'"]+)\1/dg;
|
|
285
|
+
|
|
286
|
+
/** A server-file specifier: served as a stub at a bare URL, never preloaded. @type {RegExp} */
|
|
287
|
+
const SERVER_FILE_RE = /\.server\.(?:js|ts|mjs|mts)$/;
|
|
288
|
+
|
|
289
|
+
/** JS-ish specifier extensions that map to a `.ts`/`.mts` sibling on disk. @type {Set<string>} */
|
|
290
|
+
const JS_SPEC_EXTS = new Set(['.js', '.mjs', '.jsx', '.cjs']);
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Normalize a relative import specifier's extension to the RESOLVED file's, so
|
|
294
|
+
* the URL the browser fetches equals the modulepreload href (which `ssr.js`
|
|
295
|
+
* derives from the resolved absolute path via `toUrlPath`, after `resolveImport`
|
|
296
|
+
* rewrites a `.js` specifier to its `.ts` sibling and resolves an extensionless
|
|
297
|
+
* one). Without this, an `import './x.js'` whose file is `x.ts` would be served
|
|
298
|
+
* `./x.js?v=H` (fetched as `/x.js?v=H`) while the preload is `/x.ts?v=H`: same
|
|
299
|
+
* hash, different path, so the preload is wasted and the file double-fetched.
|
|
300
|
+
*
|
|
301
|
+
* Conservative: only swaps a known JS-ish extension or appends to a genuinely
|
|
302
|
+
* extensionless basename; a directory specifier (`.` / `..` / trailing-slash)
|
|
303
|
+
* or an unrecognized extension is left untouched.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} spec the author's relative specifier (no query)
|
|
306
|
+
* @param {string} abs the resolved absolute file path
|
|
307
|
+
* @returns {string}
|
|
308
|
+
*/
|
|
309
|
+
function normalizeSpecToResolved(spec, abs) {
|
|
310
|
+
const absExt = extname(abs);
|
|
311
|
+
if (!absExt) return spec;
|
|
312
|
+
const specExt = extname(spec);
|
|
313
|
+
if (specExt === absExt) return spec;
|
|
314
|
+
const base = spec.slice(spec.lastIndexOf('/') + 1);
|
|
315
|
+
if (base === '' || base === '.' || base === '..') return spec; // directory import
|
|
316
|
+
if (specExt) return JS_SPEC_EXTS.has(specExt) ? spec.slice(0, -specExt.length) + absExt : spec;
|
|
317
|
+
return spec + absExt; // extensionless
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Append `?v=<content-hash>` to every SAME-ORIGIN relative / root-absolute
|
|
322
|
+
* static-import specifier in a served module's source, so the URL the browser
|
|
323
|
+
* actually fetches matches the `?v=`-versioned `modulepreload` hint and boot
|
|
324
|
+
* specifier the framework emits for that file.
|
|
325
|
+
*
|
|
326
|
+
* The bug this fixes (#369): the boot script imports the entry modules with
|
|
327
|
+
* `?v=<hash>` and the head emits `<link rel=modulepreload href=".../x.ts?v=hash">`
|
|
328
|
+
* for every transitive module, but a layout/page imports its components with a
|
|
329
|
+
* BARE relative specifier (`import '../components/x.ts'`). The browser resolves
|
|
330
|
+
* a relative specifier against the importer's URL and a `?v` query is NOT
|
|
331
|
+
* inherited across that resolution, so it fetches the UN-versioned URL: a
|
|
332
|
+
* different cache key from the preload. Result: the preload is wasted, the
|
|
333
|
+
* module is downloaded a second time, and the served copy gets the 1h fallback
|
|
334
|
+
* cache header instead of `immutable`. Rewriting the specifier in the served
|
|
335
|
+
* source to carry the same `?v=<hash>` collapses both to one cache key (one
|
|
336
|
+
* fetch, preload used, immutable cached).
|
|
337
|
+
*
|
|
338
|
+
* The appended hash is `assetHashFor(resolvedTarget)`, the exact value
|
|
339
|
+
* `withAssetHash` computes for the modulepreload href of the same file (same
|
|
340
|
+
* absolute path -> same hash, elision-fingerprint fold included), so the two
|
|
341
|
+
* URLs are byte-identical by construction.
|
|
342
|
+
*
|
|
343
|
+
* The fetched URL is made to equal the preload href even when the specifier's
|
|
344
|
+
* extension differs from the file on disk: `normalizeSpecToResolved` rewrites a
|
|
345
|
+
* `.js`/extensionless specifier to the resolved `.ts` sibling (the form the
|
|
346
|
+
* preload, derived from the resolved path, uses), then `?v` is appended.
|
|
347
|
+
*
|
|
348
|
+
* Scope, mirroring the modulepreload set so the rewrite never diverges from it:
|
|
349
|
+
* - Only `.`-relative specifiers (`./`, `../`). The browser resolves these
|
|
350
|
+
* against the importer's own URL, which is already base-path-prefixed, so
|
|
351
|
+
* the rewrite is base-path-correct by construction. A `/`-root-absolute
|
|
352
|
+
* specifier is deliberately NOT versioned: it would miss the `webjs.basePath`
|
|
353
|
+
* prefix under a sub-path deploy (a pre-existing author-import limitation),
|
|
354
|
+
* and apps use relative imports. A BARE specifier (`@webjsdev/core`) is
|
|
355
|
+
* importmap-resolved and versioned at its importmap TARGET; rewriting it here
|
|
356
|
+
* would break the importmap key.
|
|
357
|
+
* - Only targets that resolve to a readable file UNDER `_appDir` (the servable
|
|
358
|
+
* same-origin root). A `.server.*` target is excluded: it serves as a stub
|
|
359
|
+
* at a bare URL and is never in the preload set, and its served bytes are
|
|
360
|
+
* generated (not the file bytes the hash covers).
|
|
361
|
+
* - A specifier already carrying a query is left as-is.
|
|
362
|
+
*
|
|
363
|
+
* Matching runs over a redaction mask with `blankStrings` on (NO literal body
|
|
364
|
+
* survives), so an `import`/`export … from` printed inside an `html\`\`` template
|
|
365
|
+
* OR a plain string literal is never rewritten (a plain-string body is NOT
|
|
366
|
+
* blanked by the default mask, so the stricter mask is required here to avoid
|
|
367
|
+
* splicing `?v` into the served string's value).
|
|
368
|
+
*
|
|
369
|
+
* A pure pass-through when fingerprinting is disabled (dev, or roots unset), so
|
|
370
|
+
* dev output stays byte-identical. Runs AFTER `elideImportsFromSource` in the
|
|
371
|
+
* serve path, so an elided side-effect import (already replaced by a comment)
|
|
372
|
+
* is not matched.
|
|
373
|
+
*
|
|
374
|
+
* @param {string} source the served module source (post-elision, type-stripped if TS)
|
|
375
|
+
* @param {string} importerAbs absolute path of the module being served
|
|
376
|
+
* @returns {string}
|
|
377
|
+
*/
|
|
378
|
+
export function versionModuleImports(source, importerAbs) {
|
|
379
|
+
if (!_enabled || !_appDir) return source;
|
|
380
|
+
if (typeof source !== 'string') return source;
|
|
381
|
+
// Cheap bail before any regex/redaction work for a module with no imports.
|
|
382
|
+
if (!source.includes('import') && !source.includes('export')) return source;
|
|
383
|
+
|
|
384
|
+
const masked = redactStringsAndTemplates(source, true);
|
|
385
|
+
/** @type {Array<{ start: number, end: number, text: string }>} */
|
|
386
|
+
const edits = [];
|
|
387
|
+
for (const re of [IMPORT_SPEC_RE, EXPORT_SPEC_RE]) {
|
|
388
|
+
re.lastIndex = 0;
|
|
389
|
+
for (const m of source.matchAll(re)) {
|
|
390
|
+
// The keyword must sit in code position; if blanked in the mask it lives
|
|
391
|
+
// inside a string / template / comment and is not a real import edge.
|
|
392
|
+
if (masked[/** @type {number} */ (m.index)] === ' ') continue;
|
|
393
|
+
const spec = m[2];
|
|
394
|
+
// Only `./` `../` relative specifiers (base-path-correct, the app pattern).
|
|
395
|
+
// Bare -> importmap target; `/`-absolute -> pre-existing basePath gap.
|
|
396
|
+
if (spec[0] !== '.') continue;
|
|
397
|
+
if (spec.includes('?')) continue;
|
|
398
|
+
const abs = resolveImport(spec, importerAbs, _appDir);
|
|
399
|
+
if (!abs || SERVER_FILE_RE.test(abs)) continue;
|
|
400
|
+
if (!abs.startsWith(_appDir + sep)) continue;
|
|
401
|
+
const hash = assetHashFor(abs);
|
|
402
|
+
if (!hash) continue;
|
|
403
|
+
// `/d` flag: indices[2] is [start, end] of the specifier CONTENT (no
|
|
404
|
+
// quotes). Replace it with the resolved-extension form + `?v` so the
|
|
405
|
+
// fetched URL is byte-identical to the preload href.
|
|
406
|
+
const [start, end] = /** @type {[number, number]} */ (m.indices[2]);
|
|
407
|
+
edits.push({ start, end, text: `${normalizeSpecToResolved(spec, abs)}?v=${hash}` });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (edits.length === 0) return source;
|
|
411
|
+
|
|
412
|
+
edits.sort((a, b) => a.start - b.start);
|
|
413
|
+
let out = '';
|
|
414
|
+
let last = 0;
|
|
415
|
+
for (const e of edits) {
|
|
416
|
+
out += source.slice(last, e.start) + e.text;
|
|
417
|
+
last = e.end;
|
|
418
|
+
}
|
|
419
|
+
return out + source.slice(last);
|
|
420
|
+
}
|
package/src/check.js
CHANGED
|
@@ -91,13 +91,18 @@ export const RULES = [
|
|
|
91
91
|
{
|
|
92
92
|
name: 'gitignore-vendor-not-ignored',
|
|
93
93
|
description:
|
|
94
|
-
'Verifies the `.gitignore` exception for `.webjs/vendor/` is structurally correct via `git check-ignore`. The intended pattern is
|
|
94
|
+
'Verifies the `.gitignore` exception for `.webjs/vendor/` is structurally correct via `git check-ignore`. The intended pattern is `**/.webjs/*` (NOT `.webjs/`) plus `!**/.webjs/vendor/` plus `!**/.webjs/vendor/**`. The `**/` prefix matches `.webjs/` at any depth so a nested / monorepo app does not leak its generated `.webjs/routes.d.ts`; the older root-anchored `.webjs/*` also passes this rule (the probe is run from the app root). The common-looking pattern `.webjs/` excludes the directory itself, after which git cannot re-include children (gitignore semantics: a parent exclusion blocks child negations). Without this rule, an AI agent or human editor would silently break `webjs vendor pin` by simplifying the pattern; the failure is invisible until production. Rule fires when the working directory is a git repo and a `.gitignore` exists; skipped when neither is true.',
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
name: 'no-browser-globals-in-render',
|
|
98
98
|
description:
|
|
99
99
|
'Flags genuinely browser-only APIs used in a WebComponent constructor, willUpdate, or render() method. The SSR pipeline instantiates the component, runs willUpdate plus controllers\' hostUpdate, reflects properties, and calls render() to produce HTML, on a server element shim that backs the attribute methods but has no real DOM. So a browser global (document, window, localStorage, sessionStorage, navigator, location, matchMedia, screen, history) or an unshimmed HTMLElement member on `this` (attachShadow, shadowRoot, classList, querySelector, querySelectorAll, getBoundingClientRect, focus, blur, scrollIntoView) touched there throws at SSR time (the isomorphic footgun). The attribute methods (getAttribute/setAttribute/hasAttribute/removeAttribute/toggleAttribute), the event methods (addEventListener/removeEventListener/dispatchEvent), and attachInternals are shim-backed and run server-side, so they are NOT flagged. The flagged APIs belong in connectedCallback() or a lifecycle hook (firstUpdated/updated), which SSR never calls; seed first-paint defaults in the constructor (or derive them in willUpdate) only from server-known inputs (attributes, props). Conservative: only the constructor, willUpdate, and render bodies are scanned, and only direct references, so helper indirection is not flagged (the runtime SSR error covers that case).',
|
|
100
100
|
},
|
|
101
|
+
{
|
|
102
|
+
name: 'no-scaffold-placeholder',
|
|
103
|
+
description:
|
|
104
|
+
'The one sentinel-based check, and the deliberate exception to the "objectively broken code" framing of the other rules: it flags scaffold example content that has not been replaced, so a delivered app contains only what the user intended and never leftover scaffold code. `webjs create` marks its example/demo files (the example homepage in app/page.ts, the example app chrome in app/layout.ts) with a one-line marker comment carrying the literal token `webjs-scaffold-placeholder`. The rule scans raw source (the marker lives in a comment, which the redacted scan view would hide) and fails for every file that still carries the token. A freshly scaffolded app fails this rule BY DESIGN until its placeholders are addressed: replace the example content, or deliberately keep it, and in either case delete the marker line. No finished app legitimately ships a literal remove-me marker, so this still qualifies as a check rather than a convention. The marker is acknowledge-and-remove, so keeping a piece is a one-line deletion rather than a forced rewrite.',
|
|
105
|
+
},
|
|
101
106
|
];
|
|
102
107
|
|
|
103
108
|
/** Set of all known rule names for fast lookup. */
|
|
@@ -465,6 +470,27 @@ export async function checkConventions(appDir) {
|
|
|
465
470
|
}
|
|
466
471
|
}
|
|
467
472
|
|
|
473
|
+
// --- Rule: no-scaffold-placeholder ---
|
|
474
|
+
// `webjs create` marks its example/demo files (app/page.ts, app/layout.ts)
|
|
475
|
+
// with a one-line marker comment so a delivered app cannot silently ship
|
|
476
|
+
// leftover scaffold content. Scan RAW `content`: the marker lives in a
|
|
477
|
+
// comment, which the `scan` view redacts to whitespace. One violation per
|
|
478
|
+
// still-marked file. The token is assembled here so this source does not
|
|
479
|
+
// itself carry the contiguous literal.
|
|
480
|
+
{
|
|
481
|
+
const MARKER = 'webjs-scaffold-' + 'placeholder';
|
|
482
|
+
for (const { rel, content } of files) {
|
|
483
|
+
if (!content.includes(MARKER)) continue;
|
|
484
|
+
violations.push({
|
|
485
|
+
rule: 'no-scaffold-placeholder',
|
|
486
|
+
file: rel,
|
|
487
|
+
message:
|
|
488
|
+
'Scaffold placeholder marker still present. This file is unmodified example content from `webjs create`, and the delivered app should contain only what the user intended, not leftover scaffold code.',
|
|
489
|
+
fix: `Replace the example content (or deliberately keep it), then delete the marker comment line carrying the ${MARKER} token.`,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
468
494
|
// --- Rule: no-server-env-in-components ---
|
|
469
495
|
// Catches `process.env.X` reads in component files where X is not a
|
|
470
496
|
// WEBJS_PUBLIC_* var and not NODE_ENV. The SSR shim only exposes those
|
|
@@ -728,9 +754,12 @@ export async function checkConventions(appDir) {
|
|
|
728
754
|
// --- Rule: gitignore-vendor-not-ignored ---
|
|
729
755
|
// The .gitignore pattern for .webjs/vendor/ is subtle: `.webjs/`
|
|
730
756
|
// alone excludes the directory entirely and git can't re-include
|
|
731
|
-
// children of an excluded parent. The correct pattern is
|
|
732
|
-
// plus
|
|
733
|
-
//
|
|
757
|
+
// children of an excluded parent. The correct pattern is `**/.webjs/*`
|
|
758
|
+
// plus `!**/.webjs/vendor/` plus `!**/.webjs/vendor/**` (the `**/`
|
|
759
|
+
// prefix ignores `.webjs/` at any depth so a nested app does not leak
|
|
760
|
+
// its generated routes.d.ts; the older root-anchored `.webjs/*` also
|
|
761
|
+
// passes, since this probe runs from the app root). AI agents and
|
|
762
|
+
// human reviewers frequently "simplify" this back to `.webjs/`,
|
|
734
763
|
// silently breaking `webjs vendor pin`.
|
|
735
764
|
//
|
|
736
765
|
// This rule verifies the actual gitignore behavior by spawning
|
|
@@ -778,9 +807,10 @@ export async function checkConventions(appDir) {
|
|
|
778
807
|
`${probe} is gitignored, but \`webjs vendor pin\` writes files under .webjs/vendor/ and they MUST be committed for production deploys to use the pin (instead of calling api.jspm.io on every cold start). The most common cause: a \`.webjs/\` line in .gitignore that excludes the parent directory before the \`!.webjs/vendor/\` exception can take effect (git semantics: a parent exclusion blocks child negations). A second possible cause is a broader rule (e.g. \`*.js\` at root) that hides bundle files added by \`webjs vendor pin --download\`.`,
|
|
779
808
|
fix:
|
|
780
809
|
'Replace `.webjs/` in your .gitignore with this three-line pattern:\n' +
|
|
781
|
-
'
|
|
782
|
-
'
|
|
783
|
-
'
|
|
810
|
+
' **/.webjs/*\n' +
|
|
811
|
+
' !**/.webjs/vendor/\n' +
|
|
812
|
+
' !**/.webjs/vendor/**\n' +
|
|
813
|
+
'The `**/` prefix ignores `.webjs/` at any depth (so a nested / monorepo app does not leak its generated `.webjs/routes.d.ts`) while still re-including the committed vendor pin. ' +
|
|
784
814
|
'Verify with `git check-ignore -q .webjs/vendor/importmap.json` (exit 1 means correctly un-ignored).',
|
|
785
815
|
});
|
|
786
816
|
}
|
package/src/dev.js
CHANGED
|
@@ -115,7 +115,7 @@ function shouldAccessLog(pathname) {
|
|
|
115
115
|
}
|
|
116
116
|
import { setVendorEntries, setCoreInstall, publishBuildId, setBasePath, basePath } from './importmap.js';
|
|
117
117
|
import { readBasePath, stripBasePath, withBasePath } from './base-path.js';
|
|
118
|
-
import { setAssetRoots, clearAssetHashCache, setElisionFingerprint, withAssetHash, assetHashFor } from './asset-hash.js';
|
|
118
|
+
import { setAssetRoots, clearAssetHashCache, setElisionFingerprint, withAssetHash, assetHashFor, versionModuleImports } from './asset-hash.js';
|
|
119
119
|
import { urlFromRequest } from './forwarded.js';
|
|
120
120
|
import { compileHeaderRules, applySecurityHeaders, webRequestIsHttps } from './headers.js';
|
|
121
121
|
import {
|
|
@@ -2142,9 +2142,13 @@ async function jsModuleResponse(abs, dev, elideOpts, immutable) {
|
|
|
2142
2142
|
let source;
|
|
2143
2143
|
try { source = await readFile(abs, 'utf8'); }
|
|
2144
2144
|
catch { return new Response('Not found', { status: 404 }); }
|
|
2145
|
-
|
|
2145
|
+
let code = elideImportsFromSource(
|
|
2146
2146
|
source, abs, elideOpts.moduleGraph, elideOpts.elidableComponents, resolveImport, elideOpts.appDir,
|
|
2147
2147
|
);
|
|
2148
|
+
// Version same-origin relative import specifiers so the URL the browser
|
|
2149
|
+
// fetches matches the `?v=`-versioned modulepreload + boot specifier (#369).
|
|
2150
|
+
// A no-op in dev (fingerprinting disabled).
|
|
2151
|
+
code = versionModuleImports(code, abs);
|
|
2148
2152
|
// Buffered (string) body, so opt into the conditional-GET funnel for the
|
|
2149
2153
|
// weak ETag + 304 (see fileResponse).
|
|
2150
2154
|
const headers = {
|
|
@@ -2267,6 +2271,11 @@ async function tsResponse(abs, dev, elideOpts, cache, immutable, reportDevError)
|
|
|
2267
2271
|
code, abs, elideOpts.moduleGraph, elideOpts.elidableComponents, resolveImport, elideOpts.appDir,
|
|
2268
2272
|
);
|
|
2269
2273
|
}
|
|
2274
|
+
// Version same-origin relative import specifiers so the URL the browser
|
|
2275
|
+
// fetches matches the `?v=`-versioned modulepreload + boot specifier (#369).
|
|
2276
|
+
// A no-op in dev (fingerprinting disabled). Cached with the elision result:
|
|
2277
|
+
// PROD files are static within a deploy, so the baked `?v` stays correct.
|
|
2278
|
+
code = versionModuleImports(code, abs);
|
|
2270
2279
|
// Evict oldest entry if cache is full (simple FIFO: Map preserves insertion order).
|
|
2271
2280
|
if (cache.size >= TS_CACHE_MAX) {
|
|
2272
2281
|
const oldest = cache.keys().next().value;
|
package/src/js-scan.js
CHANGED
|
@@ -67,10 +67,20 @@ const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
|
67
67
|
* nesting, so a nested `` html`...${html`...`}...` `` is delimited correctly
|
|
68
68
|
* (the inner backtick is not mistaken for the outer close).
|
|
69
69
|
*
|
|
70
|
+
* `blankStrings` (default false, so existing callers are byte-identical)
|
|
71
|
+
* additionally blanks PLAIN string bodies and disables the verbatim-template
|
|
72
|
+
* fast path, yielding a mask in which NO literal body survives. Callers that
|
|
73
|
+
* only check whether a keyword sits in code position (not inside any literal),
|
|
74
|
+
* e.g. the import-versioning pass, pass true so an `import '…'` written inside
|
|
75
|
+
* a plain string is correctly masked out (the default mask keeps plain-string
|
|
76
|
+
* bodies verbatim so `register('tag')` stays readable, which would otherwise
|
|
77
|
+
* leave such a string looking like a real import).
|
|
78
|
+
*
|
|
70
79
|
* @param {string} src
|
|
80
|
+
* @param {boolean} [blankStrings=false] also blank plain-string + verbatim-template bodies
|
|
71
81
|
* @returns {string}
|
|
72
82
|
*/
|
|
73
|
-
export function redactStringsAndTemplates(src) {
|
|
83
|
+
export function redactStringsAndTemplates(src, blankStrings = false) {
|
|
74
84
|
const n = src.length;
|
|
75
85
|
let out = '';
|
|
76
86
|
let i = 0;
|
|
@@ -157,7 +167,7 @@ export function redactStringsAndTemplates(src) {
|
|
|
157
167
|
if (ch === '\n') hasNewline = true;
|
|
158
168
|
k++;
|
|
159
169
|
}
|
|
160
|
-
const verbatim = !forceBlank && !tagged && closed && !hasNewline && !hasInterp;
|
|
170
|
+
const verbatim = !forceBlank && !blankStrings && !tagged && closed && !hasNewline && !hasInterp;
|
|
161
171
|
out += '`'; i++;
|
|
162
172
|
if (verbatim) {
|
|
163
173
|
while (i < n) {
|
|
@@ -197,7 +207,7 @@ export function redactStringsAndTemplates(src) {
|
|
|
197
207
|
if (c === '/' && next === '/') { scanLineComment(); continue; }
|
|
198
208
|
if (c === '/' && next === '*') { scanBlockComment(); continue; }
|
|
199
209
|
if (c === '/' && isRegex()) { scanRegex(); continue; }
|
|
200
|
-
if (c === "'" || c === '"') { scanString(c, blank); continue; }
|
|
210
|
+
if (c === "'" || c === '"') { scanString(c, blank || blankStrings); continue; }
|
|
201
211
|
if (c === '`') { scanTemplate(blank); continue; }
|
|
202
212
|
if (c === '{') { brace++; lastSig = '{'; lastWord = ''; lastWasIncDec = false; out += blank ? ' ' : c; i++; continue; }
|
|
203
213
|
if (c === '}') { brace--; lastSig = '}'; lastWord = ''; lastWasIncDec = false; out += blank ? ' ' : c; i++; continue; }
|