@tsrx/core 0.1.27 → 0.1.29
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 +4 -1
- package/src/analyze/prune.js +6 -4
- package/src/parse/index.js +3 -2
- package/src/parse/parse-module.js +3 -1
- package/src/parse/style.js +12 -3
- package/src/plugin.js +43 -10
- package/src/transform/jsx/helpers.js +7 -8
- package/src/transform/jsx/index.js +150 -33
- package/src/transform/scoping.js +64 -6
- package/src/transform/style-ref.js +24 -3
- package/src/utils/builders.js +38 -0
- package/{src → types}/helpers.d.ts +2 -0
- package/types/index.d.ts +20 -2
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Core compiler infrastructure for TSRX syntax",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.29",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"./types/acorn": {
|
|
28
28
|
"types": "./types/acorn.d.ts"
|
|
29
29
|
},
|
|
30
|
+
"./types/helpers": {
|
|
31
|
+
"types": "./types/helpers.d.ts"
|
|
32
|
+
},
|
|
30
33
|
"./runtime/ref": {
|
|
31
34
|
"types": "./types/runtime/ref.d.ts",
|
|
32
35
|
"default": "./src/runtime/ref.js"
|
package/src/analyze/prune.js
CHANGED
|
@@ -756,11 +756,13 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
|
|
|
756
756
|
if (attribute.type !== 'JSXAttribute') continue;
|
|
757
757
|
|
|
758
758
|
const lowerCaseName = name.toLowerCase();
|
|
759
|
+
const accepted_names = [lowerCaseName, `$${lowerCaseName}`];
|
|
760
|
+
if (lowerCaseName === 'class') {
|
|
761
|
+
// React-style targets author the class attribute as `className`.
|
|
762
|
+
accepted_names.push('classname');
|
|
763
|
+
}
|
|
759
764
|
const attributeName = get_attribute_name(attribute);
|
|
760
|
-
if (
|
|
761
|
-
!attributeName ||
|
|
762
|
-
![lowerCaseName, `$${lowerCaseName}`].includes(attributeName.toLowerCase())
|
|
763
|
-
) {
|
|
765
|
+
if (!attributeName || !accepted_names.includes(attributeName.toLowerCase())) {
|
|
764
766
|
continue;
|
|
765
767
|
}
|
|
766
768
|
|
package/src/parse/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
@import * as AST from 'estree'
|
|
3
3
|
@import * as ESTreeJSX from 'estree-jsx'
|
|
4
4
|
@import { Parse } from '../../types/parse'
|
|
5
|
+
@import { NonEmptyString } from '../../types/helpers'
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import * as acorn from 'acorn';
|
|
@@ -189,7 +190,7 @@ function elementTemplateClosingTagPlugin(Base) {
|
|
|
189
190
|
* extend the base parser with framework-specific syntax.
|
|
190
191
|
*
|
|
191
192
|
* @param {...(AcornPlugin | Function)} plugins - Framework parser plugins to compose
|
|
192
|
-
* @returns {(source: string, filename
|
|
193
|
+
* @returns {<T extends string>(source: string, filename: NonEmptyString<T>, options?: any) => AST.Program} A parse function
|
|
193
194
|
*/
|
|
194
195
|
export function createParser(...plugins) {
|
|
195
196
|
const parser = /** @type {Parse.ParserConstructor} */ (
|
|
@@ -204,7 +205,7 @@ export function createParser(...plugins) {
|
|
|
204
205
|
|
|
205
206
|
/**
|
|
206
207
|
* @param {string} source
|
|
207
|
-
* @param {string}
|
|
208
|
+
* @param {string} filename
|
|
208
209
|
* @param {any} [options]
|
|
209
210
|
* @returns {AST.Program}
|
|
210
211
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @import * as AST from 'estree' */
|
|
2
2
|
/** @import { ParseOptions } from '../../types/index' */
|
|
3
|
+
/** @import { NonEmptyString } from '../../types/helpers' */
|
|
3
4
|
|
|
4
5
|
import { createParser } from './index.js';
|
|
5
6
|
import { TSRXPlugin } from '../plugin.js';
|
|
@@ -8,8 +9,9 @@ const parse = createParser(TSRXPlugin());
|
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Parse source code to an ESTree AST using the TSRX parser.
|
|
12
|
+
* @template {string} T
|
|
11
13
|
* @param {string} source
|
|
12
|
-
* @param {
|
|
14
|
+
* @param {NonEmptyString<T>} filename
|
|
13
15
|
* @param {ParseOptions} [options]
|
|
14
16
|
* @returns {AST.Program}
|
|
15
17
|
*/
|
package/src/parse/style.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @import * as AST from 'estree' */
|
|
2
|
+
/** @import { NonEmptyString } from '../../types/helpers' */
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import { strong_hash } from '../utils/hashing.js';
|
|
4
5
|
|
|
5
6
|
const REGEX_MATCHER = /^[~^$*|]?=/;
|
|
6
7
|
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/;
|
|
@@ -110,16 +111,24 @@ class Parser {
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
/**
|
|
114
|
+
* @template {string} T
|
|
113
115
|
* @param {string} content
|
|
116
|
+
* @param {{ filename: NonEmptyString<T>, line: number, column: number }} location position of the `<style>` tag in the source file
|
|
114
117
|
* @param {{ loose?: boolean }} options
|
|
115
118
|
* @returns {AST.CSS.StyleSheet}
|
|
116
119
|
*/
|
|
117
|
-
export function parse_style(content, options) {
|
|
120
|
+
export function parse_style(content, location, options) {
|
|
118
121
|
const parser = new Parser(content, options.loose || false);
|
|
119
122
|
|
|
123
|
+
// The filename and the position of the `<style>` tag keep the hash unique
|
|
124
|
+
// when identical content appears in multiple style blocks within a file or
|
|
125
|
+
// across files. The filename may be an absolute path, so this must be a
|
|
126
|
+
// pre-image-resistant hash to avoid leaking file structure into the bundle.
|
|
127
|
+
const hash_source = `${location.filename}:${location.line}:${location.column}:${content}`;
|
|
128
|
+
|
|
120
129
|
return {
|
|
121
130
|
source: content,
|
|
122
|
-
hash: `tsrx-${
|
|
131
|
+
hash: `tsrx-${strong_hash(hash_source)}`,
|
|
123
132
|
type: 'StyleSheet',
|
|
124
133
|
children: read_body(parser),
|
|
125
134
|
start: 0,
|
package/src/plugin.js
CHANGED
|
@@ -547,7 +547,7 @@ export function TSRXPlugin(config) {
|
|
|
547
547
|
let index = start;
|
|
548
548
|
let value = '';
|
|
549
549
|
while (index < this.input.length) {
|
|
550
|
-
if (this.#isTemplateLineCommentStart(index)) {
|
|
550
|
+
if (this.#isTemplateLineCommentStart(index, start)) {
|
|
551
551
|
const comment_start = index;
|
|
552
552
|
const comment_start_loc = acorn.getLineInfo(this.input, comment_start);
|
|
553
553
|
index += 2;
|
|
@@ -600,6 +600,7 @@ export function TSRXPlugin(config) {
|
|
|
600
600
|
ch === CharCode.lessThan ||
|
|
601
601
|
ch === CharCode.openBrace ||
|
|
602
602
|
ch === CharCode.closeBrace ||
|
|
603
|
+
this.#isCodeBlockStart(index) ||
|
|
603
604
|
this.#isJSXControlFlowDirectiveAt(index)
|
|
604
605
|
) {
|
|
605
606
|
break;
|
|
@@ -961,14 +962,29 @@ export function TSRXPlugin(config) {
|
|
|
961
962
|
}
|
|
962
963
|
|
|
963
964
|
/**
|
|
965
|
+
* A `//` is a comment only when nothing but whitespace precedes it on its
|
|
966
|
+
* line, or — given `run_start`, the position where the current text run
|
|
967
|
+
* began (right after a sibling element, code block, or expression
|
|
968
|
+
* container) — since that boundary. Once real text has begun, `//` is
|
|
969
|
+
* literal so inline text like `https://…` stays text.
|
|
964
970
|
* @param {number} index
|
|
971
|
+
* @param {number} [run_start]
|
|
965
972
|
*/
|
|
966
|
-
#isTemplateLineCommentStart(index) {
|
|
967
|
-
|
|
968
|
-
this.input.charCodeAt(index)
|
|
969
|
-
this.input.charCodeAt(index + 1)
|
|
970
|
-
|
|
971
|
-
|
|
973
|
+
#isTemplateLineCommentStart(index, run_start = -1) {
|
|
974
|
+
if (
|
|
975
|
+
this.input.charCodeAt(index) !== CharCode.slash ||
|
|
976
|
+
this.input.charCodeAt(index + 1) !== CharCode.slash
|
|
977
|
+
) {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
if (this.#isLineStartPosition(index)) return true;
|
|
981
|
+
if (run_start < 0) return false;
|
|
982
|
+
for (let i = index - 1; i >= run_start; i--) {
|
|
983
|
+
const ch = this.input.charCodeAt(i);
|
|
984
|
+
if (ch === CharCode.lineFeed || ch === CharCode.carriageReturn) return false;
|
|
985
|
+
if (ch !== CharCode.space && ch !== CharCode.tab) return false;
|
|
986
|
+
}
|
|
987
|
+
return true;
|
|
972
988
|
}
|
|
973
989
|
|
|
974
990
|
/**
|
|
@@ -996,7 +1012,7 @@ export function TSRXPlugin(config) {
|
|
|
996
1012
|
ch === CharCode.openBrace ||
|
|
997
1013
|
ch === CharCode.closeBrace ||
|
|
998
1014
|
this.#isJSXControlFlowDirectiveAt(index) ||
|
|
999
|
-
this.#isTemplateLineCommentStart(index) ||
|
|
1015
|
+
this.#isTemplateLineCommentStart(index, start) ||
|
|
1000
1016
|
this.#isTemplateBlockCommentStart(index)
|
|
1001
1017
|
) {
|
|
1002
1018
|
break;
|
|
@@ -1979,11 +1995,25 @@ export function TSRXPlugin(config) {
|
|
|
1979
1995
|
* @param {boolean} insideHead
|
|
1980
1996
|
*/
|
|
1981
1997
|
#parseStyleElement(open, node, insideHead) {
|
|
1998
|
+
const filename = this.#filename;
|
|
1999
|
+
if (!filename) {
|
|
2000
|
+
throw new Error(
|
|
2001
|
+
'<style> elements require a filename: pass one to parse so style scope hashes are unique per file.',
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
1982
2004
|
const contentStart = open.end;
|
|
1983
2005
|
const input = this.input.slice(contentStart);
|
|
1984
2006
|
const relativeCloseStart = input.indexOf('</style>');
|
|
1985
2007
|
const content = relativeCloseStart === -1 ? input : input.slice(0, relativeCloseStart);
|
|
1986
|
-
const parsedCss = parse_style(
|
|
2008
|
+
const parsedCss = parse_style(
|
|
2009
|
+
content,
|
|
2010
|
+
{
|
|
2011
|
+
filename,
|
|
2012
|
+
line: open.loc.start.line,
|
|
2013
|
+
column: open.loc.start.column,
|
|
2014
|
+
},
|
|
2015
|
+
{ loose: this.#loose },
|
|
2016
|
+
);
|
|
1987
2017
|
|
|
1988
2018
|
if (!insideHead) {
|
|
1989
2019
|
node.metadata.styleScopeHash = parsedCss.hash;
|
|
@@ -2714,7 +2744,10 @@ export function TSRXPlugin(config) {
|
|
|
2714
2744
|
this.pos++;
|
|
2715
2745
|
return this.finishToken(tt.arrow);
|
|
2716
2746
|
}
|
|
2717
|
-
if (code === CharCode.lessThan) {
|
|
2747
|
+
if (code === CharCode.lessThan && this.type !== tstt.jsxText) {
|
|
2748
|
+
// After a JSX text token a `<` can only open a tag; without this guard
|
|
2749
|
+
// text ending in an identifier character (`hello<div>`) would read as
|
|
2750
|
+
// the start of a type argument list (`hello<T>`).
|
|
2718
2751
|
const next = this.input.charCodeAt(this.pos + 1);
|
|
2719
2752
|
if (
|
|
2720
2753
|
next !== CharCode.slash &&
|
|
@@ -4,20 +4,19 @@
|
|
|
4
4
|
import tsx from 'esrap/languages/tsx';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Zimmerframe provides `path` as the ancestor chain. A native template node
|
|
8
|
-
*
|
|
9
|
-
* renders as a standalone expression (e.g. a return value).
|
|
7
|
+
* Zimmerframe provides `path` as the ancestor chain. A native template node in
|
|
8
|
+
* the children list of any JSX element/fragment renders as a JSX child;
|
|
9
|
+
* anywhere else it renders as a standalone expression (e.g. a return value).
|
|
10
|
+
* The parent may be a parsed native template node or a synthetic fragment the
|
|
11
|
+
* transform built around render children — either way a bare expression in a
|
|
12
|
+
* child slot would print as JSX text.
|
|
10
13
|
*
|
|
11
14
|
* @param {any[]} path
|
|
12
15
|
* @returns {boolean}
|
|
13
16
|
*/
|
|
14
17
|
export function in_jsx_child_context(path) {
|
|
15
18
|
const parent = path[path.length - 1];
|
|
16
|
-
return (
|
|
17
|
-
!!parent &&
|
|
18
|
-
(parent.type === 'JSXElement' || parent.type === 'JSXFragment') &&
|
|
19
|
-
parent.metadata?.native_tsrx
|
|
20
|
-
);
|
|
19
|
+
return !!parent && (parent.type === 'JSXElement' || parent.type === 'JSXFragment');
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/**
|
|
@@ -146,10 +146,57 @@ function mark_nested_function_return_jsx(node, inside_function = false, seen = n
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
149
|
+
* Lower a `@{ … }` code block that appears as an element/fragment child,
|
|
150
|
+
* paying only for what the block uses while keeping each block its own
|
|
151
|
+
* lexical scope:
|
|
152
|
+
*
|
|
153
|
+
* - no setup code: the scope is unobservable, so the render output merges
|
|
154
|
+
* directly into the children list (template-only chains collapse to the
|
|
155
|
+
* innermost output, empty chains to nothing);
|
|
156
|
+
* - code-only: a plain `{ … }` statement block — statements run in source
|
|
157
|
+
* order, scoped, and render nothing (the render pipeline already handles
|
|
158
|
+
* statements interleaved with JSX children);
|
|
159
|
+
* - setup code + render output: kept as a `JSXCodeBlock` (with any nested
|
|
160
|
+
* chain simplified) for the context-aware lowering into a scoped IIFE
|
|
161
|
+
* (`transform_jsx_code_block` / `build_render_statements`).
|
|
162
|
+
*
|
|
163
|
+
* Always returns zero or one node.
|
|
164
|
+
* @param {any} block
|
|
165
|
+
* @returns {any[]}
|
|
166
|
+
*/
|
|
167
|
+
function lower_code_block_child(block) {
|
|
168
|
+
const body = block.body || [];
|
|
169
|
+
const render = block.render ?? null;
|
|
170
|
+
|
|
171
|
+
if (body.length === 0) {
|
|
172
|
+
if (render == null) return [];
|
|
173
|
+
if (render.type === 'JSXCodeBlock') return lower_code_block_child(render);
|
|
174
|
+
return [render];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (render?.type === 'JSXCodeBlock') {
|
|
178
|
+
const inner = lower_code_block_child(render);
|
|
179
|
+
if (inner.length === 0) {
|
|
180
|
+
return [b.block(body, block)];
|
|
181
|
+
}
|
|
182
|
+
if (inner[0].type === 'BlockStatement') {
|
|
183
|
+
return [b.block([...body, inner[0]], block)];
|
|
184
|
+
}
|
|
185
|
+
// The chain still renders — simplify the render to the lowered inner
|
|
186
|
+
// node and leave the block for the context-aware lowering.
|
|
187
|
+
return [{ ...block, render: inner[0] }];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (render == null) {
|
|
191
|
+
return [b.block(body, block)];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return [block];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Lower `@{ … }` code blocks that appear as element/fragment children (see
|
|
199
|
+
* `lower_code_block_child`). This is the element-scoped equivalent of
|
|
153
200
|
* `transform_function`'s body lowering — function and arrow bodies are never
|
|
154
201
|
* element children, so they are untouched here.
|
|
155
202
|
* @param {any} node
|
|
@@ -170,9 +217,7 @@ function expand_child_code_blocks(node, seen = new Set()) {
|
|
|
170
217
|
node.children.some((/** @type {any} */ c) => c?.type === 'JSXCodeBlock')
|
|
171
218
|
) {
|
|
172
219
|
node.children = node.children.flatMap((/** @type {any} */ child) =>
|
|
173
|
-
child?.type === 'JSXCodeBlock'
|
|
174
|
-
? [...child.body, ...(child.render != null ? [child.render] : [])]
|
|
175
|
-
: [child],
|
|
220
|
+
child?.type === 'JSXCodeBlock' ? lower_code_block_child(child) : [child],
|
|
176
221
|
);
|
|
177
222
|
}
|
|
178
223
|
|
|
@@ -396,15 +441,6 @@ export function createJsxTransform(platform) {
|
|
|
396
441
|
return next() ?? node;
|
|
397
442
|
}
|
|
398
443
|
|
|
399
|
-
if (is_style_element(node) && is_style_expression_position(path)) {
|
|
400
|
-
const stylesheet = get_style_element_stylesheet(node);
|
|
401
|
-
if (stylesheet) {
|
|
402
|
-
analyze_css(stylesheet);
|
|
403
|
-
state.stylesheets.push(stylesheet);
|
|
404
|
-
return /** @type {any} */ (create_style_expression_value(node, stylesheet, state));
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
444
|
// Capture raw children BEFORE the walker transforms them so platform
|
|
409
445
|
// hooks can inspect the original JSX child shape.
|
|
410
446
|
const raw_children = /** @type {any} */ (node.children || []).map(
|
|
@@ -442,16 +478,14 @@ export function createJsxTransform(platform) {
|
|
|
442
478
|
const stylesheet = get_style_element_stylesheet(node);
|
|
443
479
|
if (stylesheet) {
|
|
444
480
|
analyze_css(stylesheet);
|
|
445
|
-
state.stylesheets.push(stylesheet);
|
|
481
|
+
state.stylesheets.push(prepare_stylesheet_for_render(stylesheet, true));
|
|
446
482
|
return /** @type {any} */ (create_style_expression_value(node, stylesheet, state));
|
|
447
483
|
}
|
|
448
484
|
}
|
|
449
|
-
return
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
[],
|
|
454
|
-
)
|
|
485
|
+
return b.jsx_element(
|
|
486
|
+
/** @type {ESTreeJSX.JSXElement} */ ({ ...node, type: 'JSXElement', children: [] }),
|
|
487
|
+
node.openingElement?.attributes ?? [],
|
|
488
|
+
[],
|
|
455
489
|
);
|
|
456
490
|
},
|
|
457
491
|
|
|
@@ -516,9 +550,7 @@ export function createJsxTransform(platform) {
|
|
|
516
550
|
sourceMapContent: source,
|
|
517
551
|
});
|
|
518
552
|
|
|
519
|
-
const { css, cssHash } = render_css_result(
|
|
520
|
-
/** @type {any} */ (stylesheets.map(prepare_stylesheet_for_render)),
|
|
521
|
-
);
|
|
553
|
+
const { css, cssHash } = render_css_result(/** @type {any} */ (stylesheets));
|
|
522
554
|
|
|
523
555
|
return { ast: final_program, code: result.code, map: result.map, css, cssHash };
|
|
524
556
|
}
|
|
@@ -801,6 +833,79 @@ function build_component_statements(body_nodes, transform_context) {
|
|
|
801
833
|
return build_render_statements(body_nodes, false, transform_context);
|
|
802
834
|
}
|
|
803
835
|
|
|
836
|
+
/**
|
|
837
|
+
* Statements for one `@{ … }` scope level: the setup statements followed by
|
|
838
|
+
* the lowered chain continuation. A nested level that declares anything is
|
|
839
|
+
* kept in a nested plain `{ … }` block, so a whole chain shares a single
|
|
840
|
+
* closure while still scoping each level; the generated `return` exits that
|
|
841
|
+
* closure.
|
|
842
|
+
* @param {any} block
|
|
843
|
+
* @param {TransformContext} transform_context
|
|
844
|
+
* @returns {{ statements: any[], has_render: boolean }}
|
|
845
|
+
*/
|
|
846
|
+
function code_block_scope_statements(block, transform_context) {
|
|
847
|
+
const statements = [...(block.body || [])];
|
|
848
|
+
const render = block.render ?? null;
|
|
849
|
+
|
|
850
|
+
if (render == null) {
|
|
851
|
+
return { statements, has_render: false };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (render.type === 'JSXCodeBlock') {
|
|
855
|
+
const inner = code_block_scope_statements(render, transform_context);
|
|
856
|
+
if (inner.statements.length > 0) {
|
|
857
|
+
if ((render.body || []).length > 0) {
|
|
858
|
+
statements.push(b.block(inner.statements, render));
|
|
859
|
+
} else {
|
|
860
|
+
statements.push(...inner.statements);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return { statements, has_render: inner.has_render };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
statements: [...statements, ...build_render_statements([render], true, transform_context)],
|
|
868
|
+
has_render: true,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Lower a `@{ … }` code block that appears in a component/IIFE statement
|
|
874
|
+
* stream, keeping each block its own lexical scope:
|
|
875
|
+
*
|
|
876
|
+
* - no setup code: the scope is unobservable, so the render output (if any)
|
|
877
|
+
* merges directly into the stream;
|
|
878
|
+
* - code-only: a plain `{ … }` statement block;
|
|
879
|
+
* - setup code + render output: a scoped IIFE expression child whose value is
|
|
880
|
+
* the render output, with nested chains folded into the one closure.
|
|
881
|
+
*
|
|
882
|
+
* Always returns zero or one node.
|
|
883
|
+
* @param {any} block
|
|
884
|
+
* @param {TransformContext} transform_context
|
|
885
|
+
* @returns {any[]}
|
|
886
|
+
*/
|
|
887
|
+
function lower_code_block_stream_node(block, transform_context) {
|
|
888
|
+
const body = block.body || [];
|
|
889
|
+
const render = block.render ?? null;
|
|
890
|
+
|
|
891
|
+
if (body.length === 0) {
|
|
892
|
+
if (render == null) return [];
|
|
893
|
+
if (render.type === 'JSXCodeBlock') {
|
|
894
|
+
return lower_code_block_stream_node(render, transform_context);
|
|
895
|
+
}
|
|
896
|
+
return [render];
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const { statements, has_render } = code_block_scope_statements(block, transform_context);
|
|
900
|
+
|
|
901
|
+
if (!has_render) {
|
|
902
|
+
return [b.block(statements, block)];
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const iife = b.call(b.arrow([], b.block(statements, block)));
|
|
906
|
+
return [to_jsx_expression_container(iife, block)];
|
|
907
|
+
}
|
|
908
|
+
|
|
804
909
|
/**
|
|
805
910
|
* @param {any[]} body_nodes
|
|
806
911
|
* @param {boolean} return_null_when_empty
|
|
@@ -809,9 +914,7 @@ function build_component_statements(body_nodes, transform_context) {
|
|
|
809
914
|
*/
|
|
810
915
|
function build_render_statements(body_nodes, return_null_when_empty, transform_context) {
|
|
811
916
|
body_nodes = body_nodes.flatMap((node) =>
|
|
812
|
-
node?.type === 'JSXCodeBlock'
|
|
813
|
-
? [...node.body, ...(node.render != null ? [node.render] : [])]
|
|
814
|
-
: [node],
|
|
917
|
+
node?.type === 'JSXCodeBlock' ? lower_code_block_stream_node(node, transform_context) : [node],
|
|
815
918
|
);
|
|
816
919
|
|
|
817
920
|
const statements = [];
|
|
@@ -1262,10 +1365,10 @@ function transform_return_statement(node, { next, visit, state, path }) {
|
|
|
1262
1365
|
|
|
1263
1366
|
/**
|
|
1264
1367
|
* @param {any} node
|
|
1265
|
-
* @param {{ state: TransformContext, path: AST.Node[] }} context
|
|
1368
|
+
* @param {{ state: TransformContext, path: AST.Node[], visit: (node: any, state?: TransformContext) => any }} context
|
|
1266
1369
|
* @returns {any}
|
|
1267
1370
|
*/
|
|
1268
|
-
function transform_jsx_code_block(node, { state, path }) {
|
|
1371
|
+
function transform_jsx_code_block(node, { state, path, visit }) {
|
|
1269
1372
|
const body_nodes = get_jsx_code_block_body_nodes(node, state);
|
|
1270
1373
|
const parent = /** @type {any} */ (path.at(-1));
|
|
1271
1374
|
|
|
@@ -1282,10 +1385,22 @@ function transform_jsx_code_block(node, { state, path }) {
|
|
|
1282
1385
|
}
|
|
1283
1386
|
|
|
1284
1387
|
const expression = b.call(
|
|
1285
|
-
b.arrow(
|
|
1388
|
+
b.arrow(
|
|
1389
|
+
[],
|
|
1390
|
+
b.block(
|
|
1391
|
+
mark_native_pretransformed_jsx(build_render_statements(body_nodes, true, state)),
|
|
1392
|
+
node,
|
|
1393
|
+
),
|
|
1394
|
+
),
|
|
1286
1395
|
);
|
|
1287
1396
|
|
|
1288
|
-
|
|
1397
|
+
// Setup statements were carried over verbatim, so re-visit the lowered
|
|
1398
|
+
// scope: TSRX-only nodes they contain (style elements, nested `@{ … }`
|
|
1399
|
+
// blocks) still need their own lowering before printing.
|
|
1400
|
+
const result = in_jsx_child_context(path)
|
|
1401
|
+
? to_jsx_expression_container(expression, node)
|
|
1402
|
+
: expression;
|
|
1403
|
+
return visit(result, state);
|
|
1289
1404
|
}
|
|
1290
1405
|
|
|
1291
1406
|
/**
|
|
@@ -1806,6 +1921,8 @@ function prepare_tsrx_fragment_styles(node, transform_context) {
|
|
|
1806
1921
|
if (!css) return null;
|
|
1807
1922
|
|
|
1808
1923
|
const style_refs = collect_style_ref_attributes(node);
|
|
1924
|
+
// `prune_css` inside marks the matching selectors as used/scoped; selectors
|
|
1925
|
+
// that match no element render commented out, like the Ripple target.
|
|
1809
1926
|
apply_css_definition_metadata(node, css, transform_context, style_refs.length > 0);
|
|
1810
1927
|
transform_context.stylesheets.push(css);
|
|
1811
1928
|
const fragment = annotate_tsrx_with_hash(
|
package/src/transform/scoping.js
CHANGED
|
@@ -7,20 +7,47 @@
|
|
|
7
7
|
|
|
8
8
|
import { walk } from 'zimmerframe';
|
|
9
9
|
import * as b from '../utils/builders.js';
|
|
10
|
+
import { mark_class_map_selectors } from './style-ref.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Mark
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `<style>` block is
|
|
13
|
+
* Mark selectors inside the stylesheet as "used" so `renderStylesheets` does
|
|
14
|
+
* not comment them out.
|
|
15
|
+
*
|
|
16
|
+
* For a free-standing `<style>` block every selector is marked: we skip
|
|
17
|
+
* selector-pruning because component boundaries can be dynamic — any selector
|
|
18
|
+
* authored inside the component's `<style>` block is considered intentional.
|
|
19
|
+
*
|
|
20
|
+
* When the `<style>` block is assigned to a variable (`is_style_expression`),
|
|
21
|
+
* the only selectors reachable through the generated class map are standalone
|
|
22
|
+
* class selectors — scoped (`.x`) or global-wrapped (`:global(.x)`). Anything
|
|
23
|
+
* else at the top level — element selectors, compound selectors, descendant
|
|
24
|
+
* chains, global tag selectors — never ends up in the class map and is marked
|
|
25
|
+
* unused for `renderStylesheets` to comment out. Selectors of nested rules ride
|
|
26
|
+
* along with their parent: they apply where the parent's class matched, and the
|
|
27
|
+
* whole rule is pruned when the parent itself is unreachable.
|
|
16
28
|
*
|
|
17
29
|
* @param {any} stylesheet
|
|
30
|
+
* @param {boolean} [is_style_expression]
|
|
18
31
|
* @returns {any}
|
|
19
32
|
*/
|
|
20
|
-
export function prepare_stylesheet_for_render(stylesheet) {
|
|
33
|
+
export function prepare_stylesheet_for_render(stylesheet, is_style_expression = false) {
|
|
34
|
+
if (is_style_expression) {
|
|
35
|
+
mark_class_map_selectors(stylesheet);
|
|
36
|
+
}
|
|
21
37
|
walk(stylesheet, null, {
|
|
22
|
-
_(node, { next }) {
|
|
38
|
+
_(node, { next, path }) {
|
|
23
39
|
if (node && node.metadata && typeof node.metadata === 'object') {
|
|
40
|
+
if (
|
|
41
|
+
is_style_expression &&
|
|
42
|
+
node.type === 'ComplexSelector' &&
|
|
43
|
+
is_unreachable_via_class_map(node, path)
|
|
44
|
+
) {
|
|
45
|
+
// Not in the generated class map. The analyzer pre-marks global
|
|
46
|
+
// selectors as used, so reset, and leave the subtree untouched —
|
|
47
|
+
// no `scoped` marks that would splice the hash into pruned output.
|
|
48
|
+
node.metadata.used = false;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
24
51
|
node.metadata.used = true;
|
|
25
52
|
if (node.type === 'RelativeSelector' && !node.metadata.is_global) {
|
|
26
53
|
node.metadata.scoped = true;
|
|
@@ -32,6 +59,37 @@ export function prepare_stylesheet_for_render(stylesheet) {
|
|
|
32
59
|
return stylesheet;
|
|
33
60
|
}
|
|
34
61
|
|
|
62
|
+
/**
|
|
63
|
+
* True when a selector of a style expression should be pruned because nothing
|
|
64
|
+
* reachable through the generated class map can match it. The class map
|
|
65
|
+
* collection in `style-ref.js` is the single decider of what the map exposes:
|
|
66
|
+
* it marks the carrying prelude-level selectors with `class_map_selector`.
|
|
67
|
+
* The remaining cases are structural, not class-shaped: selectors of nested
|
|
68
|
+
* rules ride along with their parent (the whole rule is pruned when the parent
|
|
69
|
+
* is unreachable), selectors inside another selector's arguments belong to
|
|
70
|
+
* their enclosing prelude-level selector, and a bare `:global` block prelude
|
|
71
|
+
* is kept because its contents render unscoped as authored and cannot be
|
|
72
|
+
* pruned selector-by-selector.
|
|
73
|
+
*
|
|
74
|
+
* @param {any} complex_selector
|
|
75
|
+
* @param {any[]} path
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
function is_unreachable_via_class_map(complex_selector, path) {
|
|
79
|
+
if (complex_selector.metadata.class_map_selector) return false;
|
|
80
|
+
if (complex_selector.metadata.rule?.metadata?.parent_rule != null) return false;
|
|
81
|
+
if (path.some((parent) => parent.type === 'ComplexSelector')) return false;
|
|
82
|
+
|
|
83
|
+
if (complex_selector.children?.length === 1) {
|
|
84
|
+
const first = complex_selector.children[0]?.selectors?.[0];
|
|
85
|
+
if (first?.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
35
93
|
/**
|
|
36
94
|
* @param {any} node
|
|
37
95
|
* @returns {boolean}
|
|
@@ -260,22 +260,43 @@ function collect_style_class_map_entries(css) {
|
|
|
260
260
|
return entries;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Stamp `class_map_selector` on the prelude-level selectors whose classes the
|
|
265
|
+
* class map exposes, without building the map. Runs the same collection as
|
|
266
|
+
* `create_style_class_map_from_stylesheet`, so marking and the generated map
|
|
267
|
+
* always agree; calling both is harmless.
|
|
268
|
+
*
|
|
269
|
+
* @param {any} css
|
|
270
|
+
* @returns {void}
|
|
271
|
+
*/
|
|
272
|
+
export function mark_class_map_selectors(css) {
|
|
273
|
+
collect_rule_class_map_entries(css, new Map());
|
|
274
|
+
}
|
|
275
|
+
|
|
263
276
|
/**
|
|
264
277
|
* @param {any} node
|
|
265
278
|
* @param {Map<string, any>} entries
|
|
279
|
+
* @param {any} [enclosing_selector] the nearest prelude-level selector; classes
|
|
280
|
+
* found inside another selector (e.g. in `:global(...)` args) mark it as the
|
|
281
|
+
* selector that carries their class map entry
|
|
266
282
|
* @returns {void}
|
|
267
283
|
*/
|
|
268
|
-
function collect_rule_class_map_entries(node, entries) {
|
|
284
|
+
function collect_rule_class_map_entries(node, entries, enclosing_selector = null) {
|
|
269
285
|
if (!node || typeof node !== 'object') return;
|
|
270
286
|
|
|
271
287
|
if (Array.isArray(node)) {
|
|
272
|
-
for (const child of node) collect_rule_class_map_entries(child, entries);
|
|
288
|
+
for (const child of node) collect_rule_class_map_entries(child, entries, enclosing_selector);
|
|
273
289
|
return;
|
|
274
290
|
}
|
|
275
291
|
|
|
276
292
|
if (node.type === 'ComplexSelector') {
|
|
293
|
+
enclosing_selector ??= node;
|
|
277
294
|
const class_selector = get_standalone_class_selector(node);
|
|
278
295
|
if (class_selector) {
|
|
296
|
+
// Mark the prelude-level selector for every occurrence (not just the
|
|
297
|
+
// deduped first) so the render preparation of style expressions keeps
|
|
298
|
+
// exactly the selectors whose classes the map exposes.
|
|
299
|
+
(enclosing_selector.metadata ??= {}).class_map_selector = true;
|
|
279
300
|
const name = class_selector.name.replace(regex_backslash_and_following_character, '$1');
|
|
280
301
|
if (!entries.has(name)) {
|
|
281
302
|
entries.set(name, {
|
|
@@ -295,7 +316,7 @@ function collect_rule_class_map_entries(node, entries) {
|
|
|
295
316
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
296
317
|
continue;
|
|
297
318
|
}
|
|
298
|
-
collect_rule_class_map_entries(node[key], entries);
|
|
319
|
+
collect_rule_class_map_entries(node[key], entries, enclosing_selector);
|
|
299
320
|
}
|
|
300
321
|
}
|
|
301
322
|
|
package/src/utils/builders.js
CHANGED
|
@@ -1249,6 +1249,44 @@ export function jsx_fragment(children = [], attributes = []) {
|
|
|
1249
1249
|
};
|
|
1250
1250
|
}
|
|
1251
1251
|
|
|
1252
|
+
/**
|
|
1253
|
+
* Ripple's internal fragment template node (the normalized form of a
|
|
1254
|
+
* `JSXFragment`).
|
|
1255
|
+
* @param {AST.Node[]} [children]
|
|
1256
|
+
* @param {AST.NodeWithLocation} [loc_info]
|
|
1257
|
+
* @returns {AST.TsrxFragment}
|
|
1258
|
+
*/
|
|
1259
|
+
export function tsrx_fragment(children = [], loc_info) {
|
|
1260
|
+
const node = /** @type {AST.TsrxFragment} */ (
|
|
1261
|
+
/** @type {unknown} */ ({
|
|
1262
|
+
type: 'TsrxFragment',
|
|
1263
|
+
children,
|
|
1264
|
+
attributes: [],
|
|
1265
|
+
selfClosing: false,
|
|
1266
|
+
metadata: { path: [] },
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
return set_location(node, loc_info);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Ripple's internal expression template child (the normalized form of a
|
|
1275
|
+
* `JSXExpressionContainer` child).
|
|
1276
|
+
* @param {AST.Expression} expression
|
|
1277
|
+
* @param {AST.NodeWithLocation} [loc_info]
|
|
1278
|
+
* @returns {AST.TSRXExpression}
|
|
1279
|
+
*/
|
|
1280
|
+
export function tsrx_expression(expression, loc_info) {
|
|
1281
|
+
const node = /** @type {AST.TSRXExpression} */ ({
|
|
1282
|
+
type: 'TSRXExpression',
|
|
1283
|
+
expression,
|
|
1284
|
+
metadata: { path: [] },
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
return set_location(node, loc_info);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1252
1290
|
/**
|
|
1253
1291
|
* @param {AST.Expression | ESTreeJSX.JSXEmptyExpression} expression
|
|
1254
1292
|
* @param {AST.NodeWithLocation} [loc_info]
|
package/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { TSESTree } from '@typescript-eslint/types';
|
|
|
4
4
|
import type { Parse } from './parse.js';
|
|
5
5
|
import type * as ESRap from 'esrap';
|
|
6
6
|
import type { Position } from 'acorn';
|
|
7
|
-
import type { RequireAllOrNone } from '
|
|
7
|
+
import type { RequireAllOrNone } from './helpers';
|
|
8
8
|
import type {
|
|
9
9
|
JsxPlatform,
|
|
10
10
|
JsxPlatformHooks,
|
|
@@ -170,6 +170,17 @@ declare module 'estree' {
|
|
|
170
170
|
interface SimpleCallExpression {
|
|
171
171
|
metadata: BaseNodeMetaData & {
|
|
172
172
|
hash?: string;
|
|
173
|
+
/**
|
|
174
|
+
* A generated `(() => @{ … })()` inline-component IIFE for a code
|
|
175
|
+
* block; collapsible once the block's statements lower into the
|
|
176
|
+
* component callback.
|
|
177
|
+
*/
|
|
178
|
+
tsrx_code_block_component?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* A generated zero-argument scope IIFE for a `@{ … }` code-block
|
|
181
|
+
* chain level; runs synchronously inside its `with_scope` wrapper.
|
|
182
|
+
*/
|
|
183
|
+
tsrx_code_block_scope?: boolean;
|
|
173
184
|
};
|
|
174
185
|
}
|
|
175
186
|
|
|
@@ -323,7 +334,14 @@ declare module 'estree' {
|
|
|
323
334
|
closingElement?: ESTreeJSX.JSXClosingFragment | null;
|
|
324
335
|
selfClosing?: boolean;
|
|
325
336
|
attributes?: Array<Attribute | SpreadAttribute>;
|
|
326
|
-
metadata: BaseNodeMetaData
|
|
337
|
+
metadata: BaseNodeMetaData & {
|
|
338
|
+
/**
|
|
339
|
+
* A synthetic wrapper for a nested code-block render chain
|
|
340
|
+
* (`@{ @{ … } }`), so render-slot consumers see a template node;
|
|
341
|
+
* template-children lowering unwraps it.
|
|
342
|
+
*/
|
|
343
|
+
tsrx_code_block_chain?: boolean;
|
|
344
|
+
};
|
|
327
345
|
start: number;
|
|
328
346
|
end: number;
|
|
329
347
|
}
|