octane 0.2.3 → 0.2.4
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/dist/cjs/css.cjs +9 -0
- package/dist/cjs/dom-tables.cjs +6 -1
- package/dist/cjs/runtime.cjs +122 -27
- package/dist/cjs/runtime.server.cjs +258 -63
- package/dist/cjs/server/index.cjs +4 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/compiler/bundler.js +7 -0
- package/dist/compiler/compile.js +258 -528
- package/dist/compiler/hydrate-boundaries.js +165 -15
- package/dist/compiler/parser.node.js +34 -8
- package/dist/compiler/strong-mode.js +836 -3
- package/dist/compiler/style-scopes.js +1134 -0
- package/dist/compiler/volar.LICENSES.txt +1 -1
- package/dist/compiler/volar.js +4543 -2839
- package/dist/css.d.ts +7 -0
- package/dist/css.js +8 -0
- package/dist/dom-tables.js +6 -1
- package/dist/runtime.d.ts +19 -3
- package/dist/runtime.js +129 -28
- package/dist/runtime.server.d.ts +28 -1
- package/dist/runtime.server.js +263 -64
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +4 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -30,7 +30,14 @@ function inheritGeneratedOrigin(root, origin) {
|
|
|
30
30
|
for (const item of value) visit(item);
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
// Adopted parser nodes (including StyleSheet subtrees) may be frozen
|
|
34
|
+
// and already carry CSS-relative positions; only stamp generated nodes.
|
|
35
|
+
if (
|
|
36
|
+
typeof value.type === 'string' &&
|
|
37
|
+
value.loc == null &&
|
|
38
|
+
origin?.loc != null &&
|
|
39
|
+
!Object.isFrozen(value)
|
|
40
|
+
) {
|
|
34
41
|
value.start = origin.start;
|
|
35
42
|
value.end = origin.end;
|
|
36
43
|
value.loc = origin.loc;
|
|
@@ -440,6 +447,161 @@ function isHookCall(node, hookNames) {
|
|
|
440
447
|
);
|
|
441
448
|
}
|
|
442
449
|
|
|
450
|
+
function collectSubtreeNodes(value, output) {
|
|
451
|
+
if (!value || typeof value !== 'object') return;
|
|
452
|
+
if (Array.isArray(value)) {
|
|
453
|
+
for (const child of value) collectSubtreeNodes(child, output);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (output.has(value)) return;
|
|
457
|
+
output.add(value);
|
|
458
|
+
for (const [key, child] of Object.entries(value)) {
|
|
459
|
+
if (!SKIP_KEYS.has(key)) collectSubtreeNodes(child, output);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function subtreeContainsNode(root, target) {
|
|
464
|
+
if (root === target) return true;
|
|
465
|
+
const seen = new WeakSet();
|
|
466
|
+
const visit = (value) => {
|
|
467
|
+
if (!value || typeof value !== 'object') return false;
|
|
468
|
+
if (Array.isArray(value)) {
|
|
469
|
+
for (const child of value) {
|
|
470
|
+
if (visit(child)) return true;
|
|
471
|
+
}
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
if (seen.has(value)) return false;
|
|
475
|
+
seen.add(value);
|
|
476
|
+
if (value === target) return true;
|
|
477
|
+
for (const [key, child] of Object.entries(value)) {
|
|
478
|
+
if (!SKIP_KEYS.has(key) && visit(child)) return true;
|
|
479
|
+
}
|
|
480
|
+
return false;
|
|
481
|
+
};
|
|
482
|
+
return visit(root);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function collectOwnStyleBlocks(nodes) {
|
|
486
|
+
const blocks = [];
|
|
487
|
+
for (const node of nodes ?? []) {
|
|
488
|
+
if (node?.type === 'JSXStyleElement' && !isFloatStyleResource(node)) blocks.push(node);
|
|
489
|
+
}
|
|
490
|
+
return blocks;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function isFloatStyleResource(node) {
|
|
494
|
+
let hasHref = false;
|
|
495
|
+
let hasPrecedence = false;
|
|
496
|
+
for (const attribute of node.openingElement?.attributes ?? []) {
|
|
497
|
+
if (attribute?.type !== 'JSXAttribute' && attribute?.type !== 'Attribute') continue;
|
|
498
|
+
const name = jsxAttributeName(attribute);
|
|
499
|
+
if (name === 'href') hasHref = true;
|
|
500
|
+
else if (name === 'precedence') hasPrecedence = true;
|
|
501
|
+
}
|
|
502
|
+
return hasHref && hasPrecedence;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function isStampableHost(node) {
|
|
506
|
+
if (node?.type !== 'JSXElement' && node?.type !== 'Element') return false;
|
|
507
|
+
if (node.metadata?.dynamicElement) return true;
|
|
508
|
+
const name = node.openingElement?.name ?? node.name;
|
|
509
|
+
if (!name) return false;
|
|
510
|
+
if (name.type === 'JSXIdentifier' || name.type === 'Identifier') {
|
|
511
|
+
return name.name !== 'style' && !/^[A-Z]/.test(name.name);
|
|
512
|
+
}
|
|
513
|
+
// Namespaced hosts (`svg:rect`) are stamped by the style-scope pass;
|
|
514
|
+
// member expressions are composites and are not.
|
|
515
|
+
return name.type === 'JSXNamespacedName' || name.type === 'NamespacedName';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function walkStampableHosts(node, visitHost) {
|
|
519
|
+
if (!node || typeof node !== 'object') return;
|
|
520
|
+
if (Array.isArray(node)) {
|
|
521
|
+
for (const child of node) walkStampableHosts(child, visitHost);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (isFunction(node)) return;
|
|
525
|
+
if (isStampableHost(node)) visitHost(node);
|
|
526
|
+
for (const [key, child] of Object.entries(node)) {
|
|
527
|
+
if (!SKIP_KEYS.has(key)) walkStampableHosts(child, visitHost);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* A children list that holds standalone `<style>` blocks is one lexical style
|
|
533
|
+
* scope (plan S8.5 / RFC sibling scopes). Extraction is safe when that whole
|
|
534
|
+
* scope — its blocks and the host elements they stamp — sits inside the split
|
|
535
|
+
* boundary, because both compiles keep the authored-position hash. A scope
|
|
536
|
+
* that has blocks or stamped hosts on both sides of the boundary would emit
|
|
537
|
+
* disagreeing class lists; that is still `OCTANE_HYDRATE_SPLIT_STYLE`. Styles
|
|
538
|
+
* nested in functions never joined the enclosing scopes and are not checked.
|
|
539
|
+
*/
|
|
540
|
+
function styleScopeStraddlesBoundary(list, own, inside) {
|
|
541
|
+
let hasInside = false;
|
|
542
|
+
let hasOutside = false;
|
|
543
|
+
const mark = (node) => {
|
|
544
|
+
if (inside.has(node)) hasInside = true;
|
|
545
|
+
else hasOutside = true;
|
|
546
|
+
};
|
|
547
|
+
for (const block of own) mark(block);
|
|
548
|
+
for (const item of list) {
|
|
549
|
+
if (own.includes(item)) continue;
|
|
550
|
+
walkStampableHosts(item, mark);
|
|
551
|
+
}
|
|
552
|
+
return hasInside && hasOutside;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function assertHydrateStyleScopes(boundary, filename) {
|
|
556
|
+
const inside = new WeakSet();
|
|
557
|
+
collectSubtreeNodes(boundary.node.children, inside);
|
|
558
|
+
const seenLists = new WeakSet();
|
|
559
|
+
const seen = new WeakSet();
|
|
560
|
+
const visit = (node, inFunction) => {
|
|
561
|
+
if (!node || typeof node !== 'object') return;
|
|
562
|
+
if (Array.isArray(node)) {
|
|
563
|
+
for (const child of node) visit(child, inFunction);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (seen.has(node)) return;
|
|
567
|
+
seen.add(node);
|
|
568
|
+
if (TRANSPARENT_TS_EXPRESSIONS.has(node.type)) {
|
|
569
|
+
visit(node.expression, inFunction);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (!inFunction && isFunction(node)) {
|
|
573
|
+
const nested = !subtreeContainsNode(node, boundary.node);
|
|
574
|
+
for (const [key, child] of Object.entries(node)) {
|
|
575
|
+
if (!SKIP_KEYS.has(key)) visit(child, nested);
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (
|
|
580
|
+
!inFunction &&
|
|
581
|
+
(node.type === 'JSXElement' || node.type === 'JSXFragment' || node.type === 'Element') &&
|
|
582
|
+
Array.isArray(node.children)
|
|
583
|
+
) {
|
|
584
|
+
const own = collectOwnStyleBlocks(node.children);
|
|
585
|
+
if (own.length > 0 && !seenLists.has(node.children)) {
|
|
586
|
+
seenLists.add(node.children);
|
|
587
|
+
if (styleScopeStraddlesBoundary(node.children, own, inside)) {
|
|
588
|
+
const reported = own.find((block) => inside.has(block)) ?? own[0];
|
|
589
|
+
throw extractionError(
|
|
590
|
+
'OCTANE_HYDRATE_SPLIT_STYLE',
|
|
591
|
+
filename,
|
|
592
|
+
reported,
|
|
593
|
+
'a scoped <style> cannot straddle a split Hydrate boundary — its style scope has blocks or stamped elements on both sides. Keep the whole scope inside the boundary, move it entirely outside, extract a child component, or set `split={false}`',
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
for (const [key, child] of Object.entries(node)) {
|
|
599
|
+
if (!SKIP_KEYS.has(key)) visit(child, inFunction);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
visit(boundary.ast ?? boundary.node, false);
|
|
603
|
+
}
|
|
604
|
+
|
|
443
605
|
function validateBoundary(boundary, filename, hookNames) {
|
|
444
606
|
for (const child of boundary.node.children ?? []) {
|
|
445
607
|
if (child?.type === 'JSXExpressionContainer' && isFunction(child.expression)) {
|
|
@@ -489,20 +651,6 @@ function validateBoundary(boundary, filename, hookNames) {
|
|
|
489
651
|
'direct hook calls cannot move into a split child. Call the hook in a child component or set `split={false}`',
|
|
490
652
|
);
|
|
491
653
|
}
|
|
492
|
-
// `directHooks` also marks the owning component's direct render scope: a
|
|
493
|
-
// scoped <style> there belongs to that component's single style scope,
|
|
494
|
-
// which extraction would tear in half (the server annotates the whole
|
|
495
|
-
// component with one scope hash; the split chunk would compile the sheet
|
|
496
|
-
// under another). Styles nested inside functions never joined the
|
|
497
|
-
// component scope, so they move freely.
|
|
498
|
-
if (directHooks && node.type === 'JSXStyleElement') {
|
|
499
|
-
throw extractionError(
|
|
500
|
-
'OCTANE_HYDRATE_SPLIT_STYLE',
|
|
501
|
-
filename,
|
|
502
|
-
node,
|
|
503
|
-
'a scoped <style> cannot move into a split child — its rules belong to the owning component’s style scope. Move the <style> outside the boundary, into a child component, or set `split={false}`',
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
654
|
if (isFunction(node)) {
|
|
507
655
|
// A nested function keeps hook calls in its own invocation. Ordinary
|
|
508
656
|
// functions also bind their own receiver; arrows still capture `this`
|
|
@@ -530,6 +678,7 @@ function validateBoundary(boundary, filename, hookNames) {
|
|
|
530
678
|
}
|
|
531
679
|
};
|
|
532
680
|
visit(boundary.node.children);
|
|
681
|
+
assertHydrateStyleScopes(boundary, filename);
|
|
533
682
|
}
|
|
534
683
|
|
|
535
684
|
/** Resolve Hydrate boundaries and assign source-order paths under their nearest boundary. */
|
|
@@ -1607,6 +1756,7 @@ export function prepareHydrateBoundaries(source, filename, boundaryPath = null,
|
|
|
1607
1756
|
for (const boundary of analysis.boundaries) {
|
|
1608
1757
|
boundary.filename = filename;
|
|
1609
1758
|
boundary.hookNames = analysis.imports.hookNames;
|
|
1759
|
+
boundary.ast = analysis.ast;
|
|
1610
1760
|
}
|
|
1611
1761
|
const request = sameSourceRequest(filename);
|
|
1612
1762
|
const moduleMovePlan = createModuleMovePlanAst(analysis, request);
|
|
@@ -40,9 +40,41 @@ function publishOutput(options, selected) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* `<style>` with an expression child — `<style>{css}</style>`, an ordinary
|
|
45
|
+
* element rather than a scoped block. The compat facade reads every `<style>`
|
|
46
|
+
* element's raw text as CSS after its own error translation, so this shape
|
|
47
|
+
* surfaces as a bare `Error` from that CSS reader instead of a `SyntaxError`.
|
|
48
|
+
*/
|
|
49
|
+
const STYLE_EXPRESSION_CHILD = /<style\b[^>]*>\s*\{/;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A native rejection the JavaScript parser may still accept: a `SyntaxError`
|
|
53
|
+
* the facade translated, or the bare `Error` its CSS reader raises for a
|
|
54
|
+
* `<style>` expression child. Operational failures keep their own name or
|
|
55
|
+
* code and stay visible, and so does any other bare `Error`: without the
|
|
56
|
+
* `<style …>{` shape in the source there is nothing the CSS reader could have
|
|
57
|
+
* rejected.
|
|
58
|
+
* @param {unknown} error
|
|
59
|
+
* @param {string} source
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
function isNativeSyntaxRejection(error, source) {
|
|
63
|
+
if (!(error instanceof Error)) return false;
|
|
64
|
+
if (error.name === 'ParserOperationalError') return false;
|
|
65
|
+
if ('code' in error && typeof error.code === 'string' && error.code.startsWith('ERR_TSRX_')) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (error instanceof SyntaxError) return true;
|
|
69
|
+
return (
|
|
70
|
+
error.name === 'Error' && error.constructor === Error && STYLE_EXPRESSION_CHILD.test(source)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
/**
|
|
44
75
|
* Keep native parsing first while accepting authored syntax already supported
|
|
45
|
-
* by the JavaScript parser, such as literal less-than text in TSRX children
|
|
76
|
+
* by the JavaScript parser, such as literal less-than text in TSRX children
|
|
77
|
+
* or a `<style>` element with an expression child.
|
|
46
78
|
* @param {string} source
|
|
47
79
|
* @param {string} [filename]
|
|
48
80
|
* @param {ParseOptions} [options]
|
|
@@ -56,13 +88,7 @@ export function parseModule(source, filename = 'module.tsrx', options) {
|
|
|
56
88
|
program = parseNativeModule(source, resolvedFilename, nativeOptions);
|
|
57
89
|
} catch (nativeError) {
|
|
58
90
|
// Operational failures must stay visible, even if given a syntax subclass.
|
|
59
|
-
if (
|
|
60
|
-
!(nativeError instanceof SyntaxError) ||
|
|
61
|
-
nativeError.name === 'ParserOperationalError' ||
|
|
62
|
-
('code' in nativeError &&
|
|
63
|
-
typeof nativeError.code === 'string' &&
|
|
64
|
-
nativeError.code.startsWith('ERR_TSRX_'))
|
|
65
|
-
) {
|
|
91
|
+
if (!isNativeSyntaxRejection(nativeError, source)) {
|
|
66
92
|
publishOutput(options, nativeOptions);
|
|
67
93
|
throw nativeError;
|
|
68
94
|
}
|