@webjsdev/server 0.8.31 → 0.8.33
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/check.js +112 -66
- package/src/component-elision.js +259 -52
- package/src/component-scanner.js +15 -10
- package/src/dev.js +56 -4
- package/src/js-scan.js +292 -10
- package/src/ssr.js +38 -4
- package/src/vendor.js +4 -1
- package/webjs-config.schema.json +5 -0
package/src/js-scan.js
CHANGED
|
@@ -81,9 +81,38 @@ const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
|
81
81
|
* @returns {string}
|
|
82
82
|
*/
|
|
83
83
|
export function redactStringsAndTemplates(src, blankStrings = false) {
|
|
84
|
+
return scanLiterals(src, { placeholder: false, blankStrings }).out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Core string / template / comment lexer shared by `redactStringsAndTemplates`
|
|
89
|
+
* (#179) and `redactToPlaceholders` (#634). ONE walker owns the
|
|
90
|
+
* regex-versus-division, tagged-template, and `${...}`-nesting disambiguation,
|
|
91
|
+
* so the two output modes can never drift apart.
|
|
92
|
+
*
|
|
93
|
+
* Two output modes, selected by `placeholder`:
|
|
94
|
+
* - MASK (placeholder=false): position-preserving mask (same length, newlines
|
|
95
|
+
* kept). Comment and regex bodies blank to spaces; template bodies blank,
|
|
96
|
+
* recursing `${...}` holes as blanked code; plain-string bodies stay VERBATIM
|
|
97
|
+
* so a caller can read `register('tag')`, UNLESS `blankStrings` is set (then
|
|
98
|
+
* string bodies blank too and the verbatim-template fast path is disabled).
|
|
99
|
+
* `literals` is empty.
|
|
100
|
+
* - PLACEHOLDER (placeholder=true): each string / template-text body is
|
|
101
|
+
* replaced by a `__STR_<idx>__` token (delimiters kept) and pushed to
|
|
102
|
+
* `literals`, while `${...}` holes are scanned as REAL code. Lets a caller
|
|
103
|
+
* tell a real top-level `register(...)` / `import` from one shown inside a
|
|
104
|
+
* code-sample literal (resolve the token via `literals[idx]`). In this mode
|
|
105
|
+
* code is never blanked, so the mask emit-formulas below reduce to verbatim.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} src
|
|
108
|
+
* @param {{ placeholder?: boolean, blankStrings?: boolean }} [opts]
|
|
109
|
+
* @returns {{ out: string, literals: string[] }}
|
|
110
|
+
*/
|
|
111
|
+
function scanLiterals(src, { placeholder = false, blankStrings = false } = {}) {
|
|
84
112
|
const n = src.length;
|
|
85
113
|
let out = '';
|
|
86
114
|
let i = 0;
|
|
115
|
+
const literals = [];
|
|
87
116
|
// Previous significant token in code position, tracked as we walk (more
|
|
88
117
|
// robust than scanning `out`, whose tail is blanked spaces inside a hole).
|
|
89
118
|
// `lastSig` is the last non-whitespace source char; `lastWord` is the last
|
|
@@ -112,7 +141,7 @@ export function redactStringsAndTemplates(src, blankStrings = false) {
|
|
|
112
141
|
if (/[\w$]/.test(lastSig)) return !lastWordIsProp && REGEX_PRECEDING_KEYWORDS.has(lastWord);
|
|
113
142
|
return true;
|
|
114
143
|
};
|
|
115
|
-
// A template is tagged when the previous token is a value.
|
|
144
|
+
// A template is tagged when the previous token is a value (mask mode only).
|
|
116
145
|
const isTagged = () => /[\w$)\]'"`]/.test(lastSig);
|
|
117
146
|
|
|
118
147
|
const scanLineComment = () => {
|
|
@@ -140,9 +169,25 @@ export function redactStringsAndTemplates(src, blankStrings = false) {
|
|
|
140
169
|
}
|
|
141
170
|
markValue();
|
|
142
171
|
};
|
|
143
|
-
// Strings:
|
|
172
|
+
// Strings. PLACEHOLDER: collect the body into `literals` and emit
|
|
173
|
+
// `q__STR_idx__q`. MASK: KEEP the body verbatim (so tag-name-has-hyphen can
|
|
144
174
|
// read register('foo')); blank it when inside an already-blanked hole.
|
|
145
175
|
const scanString = (q, blank) => {
|
|
176
|
+
if (placeholder) {
|
|
177
|
+
i++;
|
|
178
|
+
let body = '';
|
|
179
|
+
while (i < n) {
|
|
180
|
+
if (src[i] === '\\' && i + 1 < n) { body += src[i] + src[i + 1]; i += 2; continue; }
|
|
181
|
+
if (src[i] === q) { i++; break; }
|
|
182
|
+
if (src[i] === '\n') { i++; break; }
|
|
183
|
+
body += src[i]; i++;
|
|
184
|
+
}
|
|
185
|
+
const idx = literals.length;
|
|
186
|
+
literals.push(body);
|
|
187
|
+
out += q + `__STR_${idx}__` + q;
|
|
188
|
+
markValue();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
146
191
|
out += q; i++;
|
|
147
192
|
while (i < n) {
|
|
148
193
|
if (src[i] === '\\' && i + 1 < n) { out += blank ? ' ' : src[i] + src[i + 1]; i += 2; continue; }
|
|
@@ -152,9 +197,47 @@ export function redactStringsAndTemplates(src, blankStrings = false) {
|
|
|
152
197
|
}
|
|
153
198
|
markValue();
|
|
154
199
|
};
|
|
155
|
-
// Template literal.
|
|
156
|
-
// (
|
|
200
|
+
// Template literal. PLACEHOLDER: each text segment becomes a `__STR_idx__`
|
|
201
|
+
// token (collected) and each `${...}` hole is scanned as REAL code. MASK:
|
|
202
|
+
// verbatim fast path for a simple closed single-line untagged literal, else
|
|
203
|
+
// blank the text and recurse holes as blanked code (`forceBlank` is set when
|
|
204
|
+
// already inside a blanked hole, so everything nested blanks regardless).
|
|
157
205
|
const scanTemplate = (forceBlank) => {
|
|
206
|
+
if (placeholder) {
|
|
207
|
+
out += '`'; i++;
|
|
208
|
+
while (i < n) {
|
|
209
|
+
const c = src[i];
|
|
210
|
+
if (c === '\\' && i + 1 < n) {
|
|
211
|
+
let body = '\\' + src[i + 1];
|
|
212
|
+
i += 2;
|
|
213
|
+
while (i < n && src[i] !== '`' && !(src[i] === '$' && src[i + 1] === '{')) {
|
|
214
|
+
if (src[i] === '\\' && i + 1 < n) { body += src[i] + src[i + 1]; i += 2; continue; }
|
|
215
|
+
body += src[i]; i++;
|
|
216
|
+
}
|
|
217
|
+
const idx = literals.length;
|
|
218
|
+
literals.push(body);
|
|
219
|
+
out += `__STR_${idx}__`;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (c === '`') { out += '`'; i++; break; }
|
|
223
|
+
if (c === '$' && src[i + 1] === '{') {
|
|
224
|
+
out += '${'; i += 2;
|
|
225
|
+
scanCode(true, false);
|
|
226
|
+
if (i < n && src[i] === '}') { out += '}'; i++; }
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
let body = '';
|
|
230
|
+
while (i < n && src[i] !== '`' && !(src[i] === '$' && src[i + 1] === '{')) {
|
|
231
|
+
if (src[i] === '\\' && i + 1 < n) { body += src[i] + src[i + 1]; i += 2; continue; }
|
|
232
|
+
body += src[i]; i++;
|
|
233
|
+
}
|
|
234
|
+
const idx = literals.length;
|
|
235
|
+
literals.push(body);
|
|
236
|
+
out += `__STR_${idx}__`;
|
|
237
|
+
}
|
|
238
|
+
markValue();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
158
241
|
const tagged = isTagged();
|
|
159
242
|
let hasInterp = false, hasNewline = false, closed = false, depth = 0, k = i + 1;
|
|
160
243
|
while (k < n) {
|
|
@@ -198,7 +281,9 @@ export function redactStringsAndTemplates(src, blankStrings = false) {
|
|
|
198
281
|
|
|
199
282
|
// Scan code. `stopHole`: return at the `}` that closes the enclosing template
|
|
200
283
|
// hole (the caller emits it). `blank`: emit spaces for code (inside a blanked
|
|
201
|
-
// hole
|
|
284
|
+
// hole; mask mode only, never set in placeholder mode so the formulas below
|
|
285
|
+
// reduce to verbatim). Literals are always lexed so braces/quotes inside them
|
|
286
|
+
// never count.
|
|
202
287
|
function scanCode(stopHole, blank) {
|
|
203
288
|
let brace = 0;
|
|
204
289
|
while (i < n) {
|
|
@@ -228,7 +313,7 @@ export function redactStringsAndTemplates(src, blankStrings = false) {
|
|
|
228
313
|
}
|
|
229
314
|
|
|
230
315
|
scanCode(false, false);
|
|
231
|
-
return out;
|
|
316
|
+
return { out, literals };
|
|
232
317
|
}
|
|
233
318
|
|
|
234
319
|
/**
|
|
@@ -347,16 +432,194 @@ export function maskComments(src) {
|
|
|
347
432
|
*/
|
|
348
433
|
export function extractWebComponentClassBodies(content) {
|
|
349
434
|
const bodies = [];
|
|
350
|
-
const re = /class\s+\w+\s+extends\s+WebComponent
|
|
435
|
+
const re = /class\s+\w+\s+extends\s+WebComponent/g;
|
|
351
436
|
let m;
|
|
352
437
|
while ((m = re.exec(content)) !== null) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (
|
|
438
|
+
let i = m.index + m[0].length;
|
|
439
|
+
while (i < content.length && /\s/.test(content[i])) i++;
|
|
440
|
+
if (i >= content.length) continue;
|
|
441
|
+
|
|
442
|
+
let factoryArg = '';
|
|
443
|
+
const factoryProps = new Set();
|
|
444
|
+
if (content[i] === '(') {
|
|
445
|
+
const closeParen = matchClosingParenthesis(content, i + 1);
|
|
446
|
+
if (closeParen === -1) continue;
|
|
447
|
+
factoryArg = content.slice(i + 1, closeParen);
|
|
448
|
+
|
|
449
|
+
const objStart = factoryArg.indexOf('{');
|
|
450
|
+
if (objStart !== -1) {
|
|
451
|
+
const objEnd = matchClosingBrace(factoryArg, objStart + 1);
|
|
452
|
+
if (objEnd !== -1) {
|
|
453
|
+
const objContent = factoryArg.slice(objStart + 1, objEnd);
|
|
454
|
+
parsePropsFromObjectLiteral(objContent, factoryProps);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
i = closeParen + 1;
|
|
458
|
+
while (i < content.length && /\s/.test(content[i])) i++;
|
|
459
|
+
}
|
|
460
|
+
if (content[i] === '{') {
|
|
461
|
+
const bodyStart = i + 1;
|
|
462
|
+
const end = matchClosingBrace(content, bodyStart);
|
|
463
|
+
if (end !== -1) {
|
|
464
|
+
bodies.push({
|
|
465
|
+
body: content.slice(bodyStart, end),
|
|
466
|
+
factoryProps,
|
|
467
|
+
factoryArg
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
356
471
|
}
|
|
357
472
|
return bodies;
|
|
358
473
|
}
|
|
359
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Extract properties from an object literal block.
|
|
477
|
+
*
|
|
478
|
+
* @param {string} obj
|
|
479
|
+
* @param {Set<string>} names
|
|
480
|
+
*/
|
|
481
|
+
export function parsePropsFromObjectLiteral(obj, names) {
|
|
482
|
+
let i = 0;
|
|
483
|
+
while (i < obj.length) {
|
|
484
|
+
while (i < obj.length && /[\s,]/.test(obj[i])) i++;
|
|
485
|
+
if (i >= obj.length) break;
|
|
486
|
+
let key = '';
|
|
487
|
+
if (obj[i] === '"' || obj[i] === "'") {
|
|
488
|
+
const quote = obj[i++];
|
|
489
|
+
while (i < obj.length && obj[i] !== quote) { key += obj[i++]; }
|
|
490
|
+
i++; // closing quote
|
|
491
|
+
} else {
|
|
492
|
+
while (i < obj.length && /[A-Za-z0-9_$]/.test(obj[i])) key += obj[i++];
|
|
493
|
+
}
|
|
494
|
+
while (i < obj.length && /\s/.test(obj[i])) i++;
|
|
495
|
+
if (obj[i] !== ':') break;
|
|
496
|
+
i++;
|
|
497
|
+
while (i < obj.length && /\s/.test(obj[i])) i++;
|
|
498
|
+
if (obj[i] === '{') {
|
|
499
|
+
const valEnd = matchClosingBrace(obj, i + 1);
|
|
500
|
+
if (valEnd === -1) break;
|
|
501
|
+
i = valEnd + 1;
|
|
502
|
+
} else {
|
|
503
|
+
while (i < obj.length && obj[i] !== ',' && obj[i] !== '}') i++;
|
|
504
|
+
}
|
|
505
|
+
if (key) names.add(key);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Like {@link parsePropsFromObjectLiteral} but captures each entry's raw
|
|
511
|
+
* VALUE text alongside its key, so a rule can inspect the declaration
|
|
512
|
+
* (`prop<CommentFormatted[]>(Object)`) rather than just the name.
|
|
513
|
+
*
|
|
514
|
+
* The value is read with balanced depth across `()`, `{}`, `[]`, `<>`,
|
|
515
|
+
* and string/template literals, so an entry is split only on a
|
|
516
|
+
* TOP-LEVEL comma. That keeps a value's own commas intact, whether they
|
|
517
|
+
* sit inside an options object (`prop(Object, { default: () => [] })`),
|
|
518
|
+
* an array literal, or a nested generic (`prop<Map<string, number>>(…)`).
|
|
519
|
+
* Angle-bracket depth is decremented only when already open, so a stray
|
|
520
|
+
* `=>` arrow or `>=` does not unbalance a value with no generic.
|
|
521
|
+
*
|
|
522
|
+
* @param {string} obj object-literal body (no enclosing braces)
|
|
523
|
+
* @returns {{ key: string, value: string }[]}
|
|
524
|
+
*/
|
|
525
|
+
export function parsePropEntries(obj) {
|
|
526
|
+
/** @type {{ key: string, value: string }[]} */
|
|
527
|
+
const entries = [];
|
|
528
|
+
const n = obj.length;
|
|
529
|
+
let i = 0;
|
|
530
|
+
while (i < n) {
|
|
531
|
+
while (i < n && /[\s,]/.test(obj[i])) i++;
|
|
532
|
+
if (i >= n) break;
|
|
533
|
+
let key = '';
|
|
534
|
+
if (obj[i] === '"' || obj[i] === "'") {
|
|
535
|
+
const quote = obj[i++];
|
|
536
|
+
while (i < n && obj[i] !== quote) { key += obj[i++]; }
|
|
537
|
+
i++; // closing quote
|
|
538
|
+
} else {
|
|
539
|
+
while (i < n && /[A-Za-z0-9_$]/.test(obj[i])) key += obj[i++];
|
|
540
|
+
}
|
|
541
|
+
while (i < n && /\s/.test(obj[i])) i++;
|
|
542
|
+
if (obj[i] !== ':') break;
|
|
543
|
+
i++;
|
|
544
|
+
while (i < n && /\s/.test(obj[i])) i++;
|
|
545
|
+
let round = 0, curly = 0, square = 0, angle = 0;
|
|
546
|
+
let value = '';
|
|
547
|
+
while (i < n) {
|
|
548
|
+
const c = obj[i];
|
|
549
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
550
|
+
const quote = c;
|
|
551
|
+
value += c; i++;
|
|
552
|
+
while (i < n && obj[i] !== quote) {
|
|
553
|
+
if (obj[i] === '\\') { value += obj[i] + (obj[i + 1] || ''); i += 2; continue; }
|
|
554
|
+
value += obj[i++];
|
|
555
|
+
}
|
|
556
|
+
if (i < n) value += obj[i++];
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (c === ',' && round === 0 && curly === 0 && square === 0 && angle === 0) break;
|
|
560
|
+
if (c === '}' && curly === 0) break; // end of the enclosing object literal
|
|
561
|
+
if (c === '(') round++;
|
|
562
|
+
else if (c === ')') { if (round > 0) round--; }
|
|
563
|
+
else if (c === '{') curly++;
|
|
564
|
+
else if (c === '}') curly--;
|
|
565
|
+
else if (c === '[') square++;
|
|
566
|
+
else if (c === ']') { if (square > 0) square--; }
|
|
567
|
+
else if (c === '<') angle++;
|
|
568
|
+
else if (c === '>') { if (angle > 0) angle--; }
|
|
569
|
+
value += obj[i++];
|
|
570
|
+
}
|
|
571
|
+
if (key) entries.push({ key, value: value.trim() });
|
|
572
|
+
}
|
|
573
|
+
return entries;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Walk forward from `start` (just after an opening `(`) and return the
|
|
579
|
+
* index of the matching `)`. Tracks string/template-literal state so
|
|
580
|
+
* `)` inside strings or templates does not decrement depth.
|
|
581
|
+
* Returns -1 if no balanced parenthesis is found.
|
|
582
|
+
*
|
|
583
|
+
* @param {string} s
|
|
584
|
+
* @param {number} start
|
|
585
|
+
*/
|
|
586
|
+
export function matchClosingParenthesis(s, start) {
|
|
587
|
+
let depth = 1;
|
|
588
|
+
let i = start;
|
|
589
|
+
let str = ''; // '', "'", '"', or backtick
|
|
590
|
+
while (i < s.length) {
|
|
591
|
+
const c = s[i];
|
|
592
|
+
if (str) {
|
|
593
|
+
if (c === '\\') { i += 2; continue; }
|
|
594
|
+
if (c === str) str = '';
|
|
595
|
+
else if (str === '`' && c === '$' && s[i + 1] === '{') {
|
|
596
|
+
const closeBrace = matchClosingBrace(s, i + 2);
|
|
597
|
+
if (closeBrace === -1) return -1;
|
|
598
|
+
i = closeBrace + 1;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
i++;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (c === "'" || c === '"' || c === '`') { str = c; i++; continue; }
|
|
605
|
+
if (c === '/' && s[i + 1] === '/') { // line comment
|
|
606
|
+
while (i < s.length && s[i] !== '\n') i++;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (c === '/' && s[i + 1] === '*') { // block comment
|
|
610
|
+
i += 2;
|
|
611
|
+
while (i < s.length && !(s[i] === '*' && s[i + 1] === '/')) i++;
|
|
612
|
+
i += 2;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (c === '(') depth++;
|
|
616
|
+
else if (c === ')') { depth--; if (depth === 0) return i; }
|
|
617
|
+
i++;
|
|
618
|
+
}
|
|
619
|
+
return -1;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
|
|
360
623
|
/**
|
|
361
624
|
* Walk forward from `start` (just after an opening `{`) and return the
|
|
362
625
|
* index of the matching `}`. Tracks string/template-literal state so
|
|
@@ -401,3 +664,22 @@ export function matchClosingBrace(s, start) {
|
|
|
401
664
|
}
|
|
402
665
|
return -1;
|
|
403
666
|
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Return `src` with the BODY of every comment replaced by spaces, and the
|
|
670
|
+
* body of single-quoted, double-quoted, and template-literal segments
|
|
671
|
+
* replaced by placeholders like `__STR_index__`. Quote delimiters and template
|
|
672
|
+
* backticks are preserved so code structure is unchanged. Template holes
|
|
673
|
+
* `${...}` are left intact (and scanned recursively as code).
|
|
674
|
+
*
|
|
675
|
+
* Returns `{ redacted: string, literals: string[] }` where `literals` holds
|
|
676
|
+
* the original unquoted literal contents.
|
|
677
|
+
*
|
|
678
|
+
* @param {string} src
|
|
679
|
+
* @returns {{ redacted: string, literals: string[] }}
|
|
680
|
+
*/
|
|
681
|
+
export function redactToPlaceholders(src) {
|
|
682
|
+
const { out, literals } = scanLiterals(src, { placeholder: true });
|
|
683
|
+
return { redacted: out, literals };
|
|
684
|
+
}
|
|
685
|
+
|
package/src/ssr.js
CHANGED
|
@@ -148,10 +148,31 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
148
148
|
// zero application JS. The analysis is conservative (anything that
|
|
149
149
|
// touches the client router, a signal, an event, an npm import, or a
|
|
150
150
|
// shipping component keeps shipping).
|
|
151
|
+
//
|
|
152
|
+
// Import-only route modules (#605) go one step further: a page / layout
|
|
153
|
+
// whose only client relevance is importing shipping components is itself
|
|
154
|
+
// dead weight on the client (it never hydrates), so it is dropped and its
|
|
155
|
+
// component modules are emitted directly in its place. The component set is
|
|
156
|
+
// the STATIC closure the analyser computed (what loading the module would
|
|
157
|
+
// have registered), so a component imported but only conditionally rendered
|
|
158
|
+
// still registers. Dedup so a component shared across the page and a layout
|
|
159
|
+
// (or two layouts) is emitted once.
|
|
151
160
|
const inert = opts.inertRouteModules;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
const importOnly = opts.importOnlyRouteModules;
|
|
162
|
+
const moduleUrls = [];
|
|
163
|
+
{
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
const push = (abs) => {
|
|
166
|
+
const u = toUrlPath(abs, opts.appDir);
|
|
167
|
+
if (!seen.has(u)) { seen.add(u); moduleUrls.push(u); }
|
|
168
|
+
};
|
|
169
|
+
for (const f of [route.file, ...route.layouts]) {
|
|
170
|
+
if (inert && inert.has(f)) continue;
|
|
171
|
+
const emit = importOnly && importOnly.get(f);
|
|
172
|
+
if (emit) emit.forEach(push);
|
|
173
|
+
else push(f);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
155
176
|
// Emit <link rel="modulepreload"> for every custom element that
|
|
156
177
|
// actually rendered PLUS their transitive dependencies (from the
|
|
157
178
|
// module graph). URLs are deduplicated so the browser never sees
|
|
@@ -847,6 +868,19 @@ export function publicEnvShim(opts) {
|
|
|
847
868
|
+ `</script>`;
|
|
848
869
|
}
|
|
849
870
|
|
|
871
|
+
// Client-router opt-out (#629). Default ON (the framework's automatic-nav
|
|
872
|
+
// thesis): the router auto-enables in the browser when `@webjsdev/core` loads.
|
|
873
|
+
// `webjs.clientRouter: false` flips this off app-wide; `dev.js` reads the
|
|
874
|
+
// config at boot / each rebuild and calls `setClientRouterEnabled`, and
|
|
875
|
+
// `wrapHead` then emits a `window.__WEBJS_CLIENT_ROUTER__=false` flag BEFORE
|
|
876
|
+
// the deferred boot module so the bundle's module-end auto-enable skips. A
|
|
877
|
+
// module-level switch (mirrors setBasePath / setElisionFingerprint) so no opt
|
|
878
|
+
// has to thread through every render path; default true keeps every existing
|
|
879
|
+
// app and test byte-identical.
|
|
880
|
+
let _clientRouterEnabled = true;
|
|
881
|
+
export function setClientRouterEnabled(enabled) { _clientRouterEnabled = enabled !== false; }
|
|
882
|
+
function clientRouterEnabled() { return _clientRouterEnabled; }
|
|
883
|
+
|
|
850
884
|
function wrapHead(opts) {
|
|
851
885
|
// CSP nonce: if provided, all inline <script> tags get nonce="…" so they
|
|
852
886
|
// pass strict Content-Security-Policy headers. The nonce is extracted from
|
|
@@ -1355,7 +1389,7 @@ function wrapHead(opts) {
|
|
|
1355
1389
|
${opts.nonce ? `<meta name="csp-nonce" content="${escapeAttr(opts.nonce)}">` : ''}
|
|
1356
1390
|
${metaTags.join('\n')}
|
|
1357
1391
|
<title>${escapeHtml(title)}</title>
|
|
1358
|
-
${publicEnvShim({ dev: opts.dev, nonce: opts.nonce })}
|
|
1392
|
+
${publicEnvShim({ dev: opts.dev, nonce: opts.nonce })}${clientRouterEnabled() ? '' : `\n<script${n}>window.__WEBJS_CLIENT_ROUTER__=false;</script>`}
|
|
1359
1393
|
${importMapTag({ nonce: opts.nonce })}
|
|
1360
1394
|
${linkTags.join('\n')}
|
|
1361
1395
|
${scriptTags.length ? scriptTags.join('\n') + '\n' : ''}${boot}
|
package/src/vendor.js
CHANGED
|
@@ -100,12 +100,15 @@ export async function scanBareImports(dir, skipFiles) {
|
|
|
100
100
|
* `'@tanstack/query'` → `'@tanstack/query'`
|
|
101
101
|
* `'@tanstack/query/x'` → `'@tanstack/query'`
|
|
102
102
|
* `'./foo'`, `'../bar'`, `'/baz'` → `null` (relative/absolute)
|
|
103
|
+
* `'#components/x.ts'`, `'#lib/db'` → `null` (`#` path alias, #555: resolves to
|
|
104
|
+
* a LOCAL file via `package.json` "imports", never an npm package, so it must
|
|
105
|
+
* not be sent to the vendor resolver, #623)
|
|
103
106
|
*
|
|
104
107
|
* @param {string} spec
|
|
105
108
|
* @returns {string | null}
|
|
106
109
|
*/
|
|
107
110
|
export function extractPackageName(spec) {
|
|
108
|
-
if (!spec || spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('__')) return null;
|
|
111
|
+
if (!spec || spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('__') || spec.startsWith('#')) return null;
|
|
109
112
|
if (/^[a-z]+:/.test(spec)) return null;
|
|
110
113
|
if (spec.startsWith('@')) {
|
|
111
114
|
const parts = spec.split('/');
|
package/webjs-config.schema.json
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"type": "boolean",
|
|
18
18
|
"default": true
|
|
19
19
|
},
|
|
20
|
+
"clientRouter": {
|
|
21
|
+
"description": "Client-router switch (#629). Default true: the client router auto-enables in the browser whenever @webjsdev/core loads, so any page with a component gets SPA-style navigation with no import. Set to false to opt the whole app out (pure MPA, full-page navigation). Read by readClientRouterEnabled in dev.js.",
|
|
22
|
+
"type": "boolean",
|
|
23
|
+
"default": true
|
|
24
|
+
},
|
|
20
25
|
"headers": {
|
|
21
26
|
"description": "Per-path response-header rules, shaped like Next's. Each rule pairs a URLPattern source against header directives. Read by compileHeaderRules in headers.js.",
|
|
22
27
|
"type": "array",
|