@tsrx/core 0.1.64 → 0.1.66
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/README.md +26 -1
- package/package.json +9 -5
- package/src/analyze/css-analyze.js +44 -6
- package/src/analyze/index.js +14 -1
- package/src/analyze/style-analyze.js +467 -0
- package/src/analyze/validation.js +57 -0
- package/src/diagnostics.js +22 -0
- package/src/index.js +17 -0
- package/src/parse/style.js +97 -7
- package/src/plugin.js +145 -47
- package/src/scope.js +1 -1
- package/src/transform/jsx/index.js +94 -417
- package/src/transform/jsx/style-scopes.js +842 -0
- package/src/transform/scoping.js +129 -79
- package/src/transform/segments.js +16 -4
- package/src/transform/style-ref.js +74 -13
- package/src/transform/stylesheet.js +2 -1
- package/src/utils/is-reference.js +59 -0
- package/tests/fixtures/scoped-styles/README.md +71 -0
- package/tests/fixtures/scoped-styles/apply-forms.expected.json +18 -0
- package/tests/fixtures/scoped-styles/apply-forms.tsrx +69 -0
- package/tests/fixtures/scoped-styles/assigned-positions.expected.json +29 -0
- package/tests/fixtures/scoped-styles/assigned-positions.tsrx +90 -0
- package/tests/fixtures/scoped-styles/class-opt-in.expected.json +13 -0
- package/tests/fixtures/scoped-styles/class-opt-in.tsrx +39 -0
- package/tests/fixtures/scoped-styles/control-flow-else-if.expected.json +11 -0
- package/tests/fixtures/scoped-styles/control-flow-else-if.tsrx +26 -0
- package/tests/fixtures/scoped-styles/control-flow.expected.json +17 -0
- package/tests/fixtures/scoped-styles/control-flow.tsrx +102 -0
- package/tests/fixtures/scoped-styles/cross-module-apply.expected.json +14 -0
- package/tests/fixtures/scoped-styles/cross-module-apply.tsrx +48 -0
- package/tests/fixtures/scoped-styles/element-rooted-templates.expected.json +12 -0
- package/tests/fixtures/scoped-styles/element-rooted-templates.tsrx +33 -0
- package/tests/fixtures/scoped-styles/precedence.expected.json +11 -0
- package/tests/fixtures/scoped-styles/precedence.tsrx +45 -0
- package/tests/fixtures/scoped-styles/rfc-opening-example/panel.expected.json +12 -0
- package/tests/fixtures/scoped-styles/rfc-opening-example/panel.tsrx +46 -0
- package/tests/fixtures/scoped-styles/rfc-opening-example/theme.expected.json +9 -0
- package/tests/fixtures/scoped-styles/rfc-opening-example/theme.tsrx +24 -0
- package/tests/fixtures/scoped-styles/search-panel.expected.json +11 -0
- package/tests/fixtures/scoped-styles/search-panel.tsrx +48 -0
- package/tests/fixtures/scoped-styles/sibling-scope.expected.json +11 -0
- package/tests/fixtures/scoped-styles/sibling-scope.tsrx +44 -0
- package/tests/fixtures/scoped-styles/sibling-scopes.expected.json +12 -0
- package/tests/fixtures/scoped-styles/sibling-scopes.tsrx +51 -0
- package/tests/fixtures/scoped-styles/theme-composition.expected.json +14 -0
- package/tests/fixtures/scoped-styles/theme-composition.tsrx +45 -0
- package/tests/fixtures/scoped-styles/theme-diamond.expected.json +10 -0
- package/tests/fixtures/scoped-styles/theme-diamond.tsrx +18 -0
- package/tests/shared/scoped-styles-fixtures.js +67 -0
- package/tests/utils/fixtures/style-syntax.js +519 -0
- package/types/index.d.ts +85 -0
|
@@ -32,6 +32,48 @@ export const TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR =
|
|
|
32
32
|
"This TSRX template output is unused. Return it, assign it to a value that is rendered, or make it part of the rendered output of a function '@{...}' body.";
|
|
33
33
|
export const TSRX_UNSUPPORTED_LAZY_ASSIGNMENT_POSITION_ERROR =
|
|
34
34
|
'Lazy destructuring assignments require a directly lazy target as a standalone statement inside a program, block, TSRX code block, or switch case.';
|
|
35
|
+
export const TSRX_STYLE_APPLY_VALUE_ERROR =
|
|
36
|
+
"The 'apply' attribute of a <style> block requires an expression value: apply={theme} or apply={[a, b]}.";
|
|
37
|
+
export const TSRX_STYLE_APPLY_DUPLICATE_ERROR =
|
|
38
|
+
"A <style> block accepts a single 'apply' attribute; pass several themes as an array: apply={[a, b]}.";
|
|
39
|
+
export const TSRX_STYLE_APPLY_UNSUPPORTED_HOST_ERROR =
|
|
40
|
+
"The 'apply' attribute is only supported on scoped <style> blocks, not on <head> styles or resource styles.";
|
|
41
|
+
export const TSRX_STYLE_RESERVED_CLASS_KEY_ERROR =
|
|
42
|
+
"'$class' is reserved on assigned <style> blocks for the block's scope hash; rename the '.$class' selector.";
|
|
43
|
+
export const TSRX_STYLE_STANDALONE_AT_MODULE_SCOPE_ERROR =
|
|
44
|
+
'A standalone <style> block is only allowed inside a template scope. At module scope assign it: const theme = <style>…</style>.';
|
|
45
|
+
export const TSRX_STYLE_STANDALONE_NEEDS_FRAGMENT_ERROR =
|
|
46
|
+
'A standalone <style> block must be a child of an element or a fragment. Wrap it with the output it styles in a fragment: <><style>…</style><div>…</div></>.';
|
|
47
|
+
export const TSRX_STYLE_STANDALONE_OUTSIDE_TEMPLATE_ERROR =
|
|
48
|
+
'A standalone <style> block with CSS text is TSRX template syntax and needs an enclosing @{ … } body or an @if/@for/@switch/@try body. In plain TSX give <style> an expression child instead: <style>{css}</style>. To declare a reusable block here, assign it: const theme = <style>…</style>.';
|
|
49
|
+
export const TSRX_CSS_GLOBAL_NESTED_IN_PSEUDOCLASS_ERROR =
|
|
50
|
+
'A :global selector cannot be inside a pseudoclass.';
|
|
51
|
+
export const TSRX_CSS_GLOBAL_MIDDLE_PLACEMENT_ERROR =
|
|
52
|
+
':global(...) can be at the start or end of a selector sequence, but not in the middle.';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} name
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function tsrx_style_apply_target_error(name) {
|
|
59
|
+
return `'${name}' is not a style block. An 'apply' target must be a variable, import, or member holding an assigned <style> block.`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} name
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function tsrx_style_apply_before_declaration_error(name) {
|
|
67
|
+
return `'${name}' is applied before its declaration. Declare the style block before the block that applies it.`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} name
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
export function tsrx_style_unknown_attribute_error(name) {
|
|
75
|
+
return `Unknown <style> attribute '${name}'. Scoped style blocks accept 'ref' and 'apply'.`;
|
|
76
|
+
}
|
|
35
77
|
|
|
36
78
|
const invalid_nestings = {
|
|
37
79
|
// <p> cannot contain block-level elements
|
|
@@ -434,3 +476,18 @@ export function validate_nesting(element, context, errors) {
|
|
|
434
476
|
}
|
|
435
477
|
}
|
|
436
478
|
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Report a style diagnostic through the shared error channel so editors get
|
|
482
|
+
* positions and `@tsrx-ignore` applies.
|
|
483
|
+
*
|
|
484
|
+
* @param {string} message
|
|
485
|
+
* @param {string} code
|
|
486
|
+
* @param {AST.Node} node
|
|
487
|
+
* @param {string | null} filename
|
|
488
|
+
* @param {CompileError[] | undefined} errors
|
|
489
|
+
* @param {AST.CommentWithLocation[] | undefined} comments
|
|
490
|
+
*/
|
|
491
|
+
export function validate_style(message, code, node, filename, errors, comments) {
|
|
492
|
+
error(message, filename, node, errors, comments, code);
|
|
493
|
+
}
|
package/src/diagnostics.js
CHANGED
|
@@ -6,4 +6,26 @@ export const DIAGNOSTIC_CODES = {
|
|
|
6
6
|
TEMPLATE_RETURN_STATEMENT: 'tsrx-template-return-statement',
|
|
7
7
|
FORGOTTEN_STATEMENT_CONTAINER: 'tsrx-forgotten-statement-container',
|
|
8
8
|
UNSUPPORTED_LAZY_ASSIGNMENT_POSITION: 'tsrx-unsupported-lazy-assignment-position',
|
|
9
|
+
/** `<style apply>` carries no expression value. */
|
|
10
|
+
STYLE_APPLY_VALUE: 'tsrx-style-apply-value',
|
|
11
|
+
/** An `apply` entry is not an identifier, member, or array of those, or does not resolve to a style block. */
|
|
12
|
+
STYLE_APPLY_TARGET: 'tsrx-style-apply-target',
|
|
13
|
+
/** An `apply` target is declared after the applying block in source order. */
|
|
14
|
+
STYLE_APPLY_BEFORE_DECLARATION: 'tsrx-style-apply-before-declaration',
|
|
15
|
+
/** Two `apply` attributes on one `<style>` block. */
|
|
16
|
+
STYLE_APPLY_DUPLICATE: 'tsrx-style-apply-duplicate',
|
|
17
|
+
/** `apply` on a `<head>` style or a resource (`href`) style. */
|
|
18
|
+
STYLE_APPLY_UNSUPPORTED_HOST: 'tsrx-style-apply-unsupported-host',
|
|
19
|
+
/** An assigned style block authors a `.$class` class selector. */
|
|
20
|
+
STYLE_RESERVED_CLASS_KEY: 'tsrx-style-reserved-class-key',
|
|
21
|
+
/** A standalone `<style>` block at module scope. */
|
|
22
|
+
STYLE_STANDALONE_AT_MODULE_SCOPE: 'tsrx-style-standalone-at-module-scope',
|
|
23
|
+
/** A standalone `<style>` block with CSS text outside any `@{ … }` or control-flow body. */
|
|
24
|
+
STYLE_STANDALONE_OUTSIDE_TEMPLATE: 'tsrx-style-standalone-outside-template',
|
|
25
|
+
/** A standalone `<style>` block in a statement slot: the lone output of a `@{ … }` or control-flow body, or a statement. */
|
|
26
|
+
STYLE_STANDALONE_NEEDS_FRAGMENT: 'tsrx-style-standalone-needs-fragment',
|
|
27
|
+
/** A `<style>` attribute other than `ref` and `apply`. */
|
|
28
|
+
STYLE_UNKNOWN_ATTRIBUTE: 'tsrx-style-unknown-attribute',
|
|
29
|
+
/** `:global` used where the scoping rules do not allow it. */
|
|
30
|
+
CSS_GLOBAL_PLACEMENT: 'tsrx-css-global-placement',
|
|
9
31
|
};
|
package/src/index.js
CHANGED
|
@@ -186,6 +186,7 @@ export {
|
|
|
186
186
|
collect_style_ref_attributes as collectStyleRefAttributes,
|
|
187
187
|
create_style_class_map as createStyleClassMap,
|
|
188
188
|
create_style_class_map_from_stylesheet as createStyleClassMapFromStylesheet,
|
|
189
|
+
build_style_class_map as buildStyleClassMap,
|
|
189
190
|
create_style_ref_setup_statements as createStyleRefSetupStatements,
|
|
190
191
|
get_style_element_stylesheet as getStyleElementStylesheet,
|
|
191
192
|
} from './transform/style-ref.js';
|
|
@@ -279,4 +280,20 @@ export {
|
|
|
279
280
|
validate_forgotten_statement_container as validateForgottenStatementContainer,
|
|
280
281
|
validate_nesting as validateNesting,
|
|
281
282
|
is_template_value_position as isTemplateValuePosition,
|
|
283
|
+
TSRX_STYLE_APPLY_VALUE_ERROR,
|
|
284
|
+
TSRX_STYLE_APPLY_DUPLICATE_ERROR,
|
|
285
|
+
TSRX_STYLE_APPLY_UNSUPPORTED_HOST_ERROR,
|
|
286
|
+
TSRX_STYLE_RESERVED_CLASS_KEY_ERROR,
|
|
287
|
+
TSRX_STYLE_STANDALONE_AT_MODULE_SCOPE_ERROR,
|
|
288
|
+
TSRX_STYLE_STANDALONE_OUTSIDE_TEMPLATE_ERROR,
|
|
289
|
+
TSRX_STYLE_STANDALONE_NEEDS_FRAGMENT_ERROR,
|
|
290
|
+
TSRX_CSS_GLOBAL_NESTED_IN_PSEUDOCLASS_ERROR,
|
|
291
|
+
TSRX_CSS_GLOBAL_MIDDLE_PLACEMENT_ERROR,
|
|
292
|
+
tsrx_style_apply_target_error as tsrxStyleApplyTargetError,
|
|
293
|
+
tsrx_style_apply_before_declaration_error as tsrxStyleApplyBeforeDeclarationError,
|
|
294
|
+
tsrx_style_unknown_attribute_error as tsrxStyleUnknownAttributeError,
|
|
282
295
|
} from './analyze/validation.js';
|
|
296
|
+
export {
|
|
297
|
+
analyze_styles as analyzeStyles,
|
|
298
|
+
is_standalone_style_position as isStandaloneStylePosition,
|
|
299
|
+
} from './analyze/style-analyze.js';
|
package/src/parse/style.js
CHANGED
|
@@ -113,7 +113,15 @@ class Parser {
|
|
|
113
113
|
/**
|
|
114
114
|
* @template {string} T
|
|
115
115
|
* @param {string} content
|
|
116
|
-
* @param {{
|
|
116
|
+
* @param {{
|
|
117
|
+
* filename: NonEmptyString<T>,
|
|
118
|
+
* line: number,
|
|
119
|
+
* column: number,
|
|
120
|
+
* body?: { start: number, line: number, column: number },
|
|
121
|
+
* }} location position of the `<style>` tag in the source file, and optionally
|
|
122
|
+
* `body`, the file offset and 1-based line / 0-based column of the style body's
|
|
123
|
+
* first character. With `body`, the sheet records `sourceStart` and a
|
|
124
|
+
* file-relative `loc`, and diagnostics on its nodes carry file positions.
|
|
117
125
|
* @param {{ loose?: boolean }} options
|
|
118
126
|
* @returns {AST.CSS.StyleSheet}
|
|
119
127
|
*/
|
|
@@ -126,14 +134,82 @@ export function parse_style(content, location, options) {
|
|
|
126
134
|
// pre-image-resistant hash to avoid leaking file structure into the bundle.
|
|
127
135
|
const hash_source = `${location.filename}:${location.line}:${location.column}:${content}`;
|
|
128
136
|
|
|
129
|
-
|
|
137
|
+
/** @type {AST.CSS.StyleSheet} */
|
|
138
|
+
const sheet = {
|
|
130
139
|
source: content,
|
|
131
140
|
hash: `tsrx-${strong_hash(hash_source)}`,
|
|
132
141
|
type: 'StyleSheet',
|
|
133
142
|
children: read_body(parser),
|
|
134
143
|
start: 0,
|
|
135
144
|
end: content.length,
|
|
145
|
+
filename: location.filename,
|
|
136
146
|
};
|
|
147
|
+
|
|
148
|
+
if (location.body) {
|
|
149
|
+
sheet.sourceStart = location.body.start;
|
|
150
|
+
sheet.loc = {
|
|
151
|
+
start: { line: location.body.line, column: location.body.column },
|
|
152
|
+
end: css_position(sheet, content.length, location.body),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return sheet;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Map a style-body offset to a file line / column, given the file position of
|
|
161
|
+
* the body's first character.
|
|
162
|
+
*
|
|
163
|
+
* @param {AST.CSS.StyleSheet} sheet
|
|
164
|
+
* @param {number} offset
|
|
165
|
+
* @param {{ line: number, column: number }} origin
|
|
166
|
+
* @returns {{ line: number, column: number }}
|
|
167
|
+
*/
|
|
168
|
+
function css_position(sheet, offset, origin) {
|
|
169
|
+
const source = sheet.source;
|
|
170
|
+
let line = origin.line;
|
|
171
|
+
let line_start = -1;
|
|
172
|
+
for (let i = 0; i < offset; i++) {
|
|
173
|
+
const ch = source.charCodeAt(i);
|
|
174
|
+
if (ch === 13 && source.charCodeAt(i + 1) === 10) {
|
|
175
|
+
i++;
|
|
176
|
+
} else if (ch !== 10 && ch !== 13 && ch !== 0x2028 && ch !== 0x2029) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
line++;
|
|
180
|
+
line_start = i;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
line,
|
|
184
|
+
column: line_start === -1 ? origin.column + offset : offset - line_start - 1,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The file-relative `start` / `end` / `loc` of a CSS node in a sheet parsed
|
|
190
|
+
* with a `body` origin; the node itself when the sheet has none. CSS node
|
|
191
|
+
* offsets are relative to the style body, so this is what a diagnostic on a
|
|
192
|
+
* CSS node has to be anchored on.
|
|
193
|
+
*
|
|
194
|
+
* @param {AST.CSS.StyleSheet | null | undefined} sheet
|
|
195
|
+
* @param {AST.CSS.Node} node
|
|
196
|
+
* @returns {AST.Node | AST.NodeWithLocation}
|
|
197
|
+
*/
|
|
198
|
+
export function css_node_source_position(sheet, node) {
|
|
199
|
+
if (sheet?.sourceStart === undefined || !sheet.loc) {
|
|
200
|
+
return /** @type {AST.Node} */ (/** @type {unknown} */ (node));
|
|
201
|
+
}
|
|
202
|
+
const origin = sheet.loc.start;
|
|
203
|
+
return /** @type {AST.NodeWithLocation} */ (
|
|
204
|
+
/** @type {unknown} */ ({
|
|
205
|
+
start: node.start + sheet.sourceStart,
|
|
206
|
+
end: node.end + sheet.sourceStart,
|
|
207
|
+
loc: {
|
|
208
|
+
start: css_position(sheet, node.start, origin),
|
|
209
|
+
end: css_position(sheet, node.end, origin),
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
);
|
|
137
213
|
}
|
|
138
214
|
|
|
139
215
|
/** @param {Parser} parser */
|
|
@@ -163,12 +239,26 @@ function read_body(parser) {
|
|
|
163
239
|
const children = [];
|
|
164
240
|
|
|
165
241
|
while (parser.index < parser.template.length) {
|
|
166
|
-
|
|
242
|
+
try {
|
|
243
|
+
allow_comment_or_whitespace(parser);
|
|
167
244
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
245
|
+
if (parser.index >= parser.template.length) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (parser.match('@')) {
|
|
250
|
+
children.push(read_at_rule(parser));
|
|
251
|
+
} else {
|
|
252
|
+
children.push(read_rule(parser));
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (!parser.loose) {
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
// Partial CSS while typing (`.foo {`, `/*`), or whatever follows an
|
|
259
|
+
// unclosed module-scope `<style>`, must not take the whole file down in
|
|
260
|
+
// loose mode: keep the rules parsed so far and drop the rest.
|
|
261
|
+
break;
|
|
172
262
|
}
|
|
173
263
|
}
|
|
174
264
|
|
package/src/plugin.js
CHANGED
|
@@ -2245,26 +2245,66 @@ export function TSRXPlugin(config) {
|
|
|
2245
2245
|
* synthesize the closing element, and restore the tokenizer state past it.
|
|
2246
2246
|
* Shared by `<style>` and `<script>`.
|
|
2247
2247
|
*
|
|
2248
|
+
* Without a closing tag the element is unclosed and the rest of the input
|
|
2249
|
+
* is its body, except that in loose mode inside a template the body stops
|
|
2250
|
+
* at the next tag start (`<x`, `</x>`, `</>`): while `<style>` is being
|
|
2251
|
+
* typed, partial CSS reaches the loose CSS parser, and the tokenizer
|
|
2252
|
+
* resumes at the following sibling or closing tag so the rest of the file
|
|
2253
|
+
* keeps its mappings. Outside a template only JS follows, with no marker to
|
|
2254
|
+
* resume at, so the body runs to the end of the input.
|
|
2255
|
+
*
|
|
2248
2256
|
* @param {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} open
|
|
2249
2257
|
* @param {AST.JSXStyleElement | AST.TSRXJSXElement} node
|
|
2250
2258
|
* @param {'style' | 'script'} tagName
|
|
2259
|
+
* @param {number} [contextDepth] tokenizer context depth to restore before
|
|
2260
|
+
* reading the token after the closing tag when the element sits outside
|
|
2261
|
+
* a template (see parseElement)
|
|
2251
2262
|
* @returns {string} The raw body text
|
|
2252
2263
|
*/
|
|
2253
|
-
#parseRawTextElement(open, node, tagName) {
|
|
2264
|
+
#parseRawTextElement(open, node, tagName, contextDepth) {
|
|
2254
2265
|
const closeTag = `</${tagName}>`;
|
|
2255
2266
|
const contentStart = open.end;
|
|
2256
2267
|
const input = this.input.slice(contentStart);
|
|
2257
|
-
const
|
|
2258
|
-
const
|
|
2268
|
+
const parent = this.#path.at(-2);
|
|
2269
|
+
const insideTemplate = this.#isNativeTemplateNode(parent);
|
|
2270
|
+
let relativeCloseStart = input.indexOf(closeTag);
|
|
2271
|
+
const unclosed = relativeCloseStart === -1;
|
|
2259
2272
|
|
|
2273
|
+
if (unclosed) {
|
|
2274
|
+
this.#report_broken_markup_error(
|
|
2275
|
+
open.end,
|
|
2276
|
+
`Unclosed tag '<${tagName}>'. Expected '${closeTag}' before end of template.`,
|
|
2277
|
+
);
|
|
2278
|
+
node.unclosed = true;
|
|
2279
|
+
relativeCloseStart = input.length;
|
|
2280
|
+
if (!this.#loose) {
|
|
2281
|
+
const newLines = input.match(regex_newline_characters)?.length;
|
|
2282
|
+
if (newLines) {
|
|
2283
|
+
this.curLine = open.loc.end.line + newLines;
|
|
2284
|
+
this.lineStart = contentStart + input.lastIndexOf('\n') + 1;
|
|
2285
|
+
}
|
|
2286
|
+
return input;
|
|
2287
|
+
}
|
|
2288
|
+
if (insideTemplate) {
|
|
2289
|
+
// A `<` inside CSS text (`content: "<"`, `<!--`) is not a tag start.
|
|
2290
|
+
let lt = input.indexOf('<');
|
|
2291
|
+
while (lt !== -1 && !can_start_tag_after_lt(input, lt)) {
|
|
2292
|
+
lt = input.indexOf('<', lt + 1);
|
|
2293
|
+
}
|
|
2294
|
+
if (lt !== -1) relativeCloseStart = lt;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
const content = input.slice(0, relativeCloseStart);
|
|
2260
2299
|
const newLines = content.match(regex_newline_characters)?.length;
|
|
2261
2300
|
if (newLines) {
|
|
2262
2301
|
this.curLine = open.loc.end.line + newLines;
|
|
2263
2302
|
this.lineStart = contentStart + content.lastIndexOf('\n') + 1;
|
|
2264
2303
|
}
|
|
2265
2304
|
|
|
2266
|
-
|
|
2267
|
-
|
|
2305
|
+
const closingStart = contentStart + content.length;
|
|
2306
|
+
let closingEnd = closingStart;
|
|
2307
|
+
if (!unclosed) {
|
|
2268
2308
|
const closingLineInfo = get_line_info(this, closingStart);
|
|
2269
2309
|
const closingStartLoc = new acorn.Position(closingLineInfo.line, closingLineInfo.column);
|
|
2270
2310
|
const nameStart = closingStart + 2;
|
|
@@ -2284,7 +2324,7 @@ export function TSRXPlugin(config) {
|
|
|
2284
2324
|
nameEnd,
|
|
2285
2325
|
new acorn.Position(nameEndInfo.line, nameEndInfo.column),
|
|
2286
2326
|
);
|
|
2287
|
-
|
|
2327
|
+
closingEnd = closingStart + closeTag.length;
|
|
2288
2328
|
const closingEndInfo = get_line_info(this, closingEnd);
|
|
2289
2329
|
const closingElement =
|
|
2290
2330
|
/** @type {ESTreeJSX.TSRXJSXClosingElement & AST.NodeWithLocation} */ (
|
|
@@ -2298,65 +2338,84 @@ export function TSRXPlugin(config) {
|
|
|
2298
2338
|
new acorn.Position(closingEndInfo.line, closingEndInfo.column),
|
|
2299
2339
|
);
|
|
2300
2340
|
node.closingElement = closingElement;
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
if (this.curContext() === tstc.tc_expr && !insideTemplate) {
|
|
2344
|
+
this.context.pop();
|
|
2345
|
+
}
|
|
2346
|
+
this.exprAllowed = false;
|
|
2347
|
+
this.pos = closingEnd;
|
|
2348
|
+
const closingEndInfo = get_line_info(this, closingEnd);
|
|
2349
|
+
this.curLine = closingEndInfo.line;
|
|
2350
|
+
this.lineStart = closingEnd - closingEndInfo.column;
|
|
2351
|
+
if (insideTemplate && relativeCloseStart === 0) {
|
|
2352
|
+
// Acorn has already tokenized the adjacent tag start (this element's
|
|
2353
|
+
// closing tag, or, when unclosed, the next sibling or parent close);
|
|
2354
|
+
// the element resumes there manually, so drop the stale tag context.
|
|
2355
|
+
if (this.curContext() === tstc.tc_oTag) {
|
|
2304
2356
|
this.context.pop();
|
|
2305
2357
|
}
|
|
2306
|
-
this.
|
|
2307
|
-
|
|
2308
|
-
this.curLine = closingEndInfo.line;
|
|
2309
|
-
this.lineStart = closingEnd - closingEndInfo.column;
|
|
2310
|
-
if (insideTemplate && relativeCloseStart === 0) {
|
|
2311
|
-
// Acorn has already tokenized the adjacent closing tag; TSRX
|
|
2312
|
-
// synthesizes that close manually, so drop the stale tag context.
|
|
2313
|
-
if (this.curContext() === tstc.tc_oTag) {
|
|
2314
|
-
this.context.pop();
|
|
2315
|
-
}
|
|
2316
|
-
if (this.curContext() === tstc.tc_expr) {
|
|
2317
|
-
this.context.pop();
|
|
2318
|
-
}
|
|
2358
|
+
if (this.curContext() === tstc.tc_expr) {
|
|
2359
|
+
this.context.pop();
|
|
2319
2360
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2361
|
+
}
|
|
2362
|
+
if (!insideTemplate && this.#path.at(-1) === node) {
|
|
2363
|
+
// Outside a template (a `@{ … }` body, a `@case` body, a statement),
|
|
2364
|
+
// the element must leave the tokenizer context exactly where it
|
|
2365
|
+
// began — like a balanced element does after its closing tag — so
|
|
2366
|
+
// the following `}`, `@case`, or sibling tokenizes as code, not as
|
|
2367
|
+
// JSX text of a children context this element's `<` opened.
|
|
2368
|
+
if (contextDepth !== undefined && this.context.length > contextDepth) {
|
|
2369
|
+
this.context.length = contextDepth;
|
|
2370
|
+
}
|
|
2371
|
+
this.#path.pop();
|
|
2372
|
+
try {
|
|
2328
2373
|
this.next();
|
|
2374
|
+
} finally {
|
|
2375
|
+
this.#path.push(node);
|
|
2329
2376
|
}
|
|
2330
2377
|
} else {
|
|
2331
|
-
this
|
|
2332
|
-
open.end,
|
|
2333
|
-
`Unclosed tag '<${tagName}>'. Expected '${closeTag}' before end of template.`,
|
|
2334
|
-
);
|
|
2335
|
-
node.unclosed = true;
|
|
2378
|
+
this.next();
|
|
2336
2379
|
}
|
|
2337
2380
|
|
|
2338
2381
|
return content;
|
|
2339
2382
|
}
|
|
2340
2383
|
|
|
2384
|
+
/**
|
|
2385
|
+
* Whether the first non-whitespace character after an opening tag is
|
|
2386
|
+
* `{`: the element's children start with an expression container.
|
|
2387
|
+
*
|
|
2388
|
+
* @param {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} open
|
|
2389
|
+
* @returns {boolean}
|
|
2390
|
+
*/
|
|
2391
|
+
#hasExpressionChildStart(open) {
|
|
2392
|
+
if (open.selfClosing) return false;
|
|
2393
|
+
const index = skip_whitespace_from(this.input, open.end);
|
|
2394
|
+
return this.input.charCodeAt(index) === CharCode.openBrace;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2341
2397
|
/**
|
|
2342
2398
|
* @param {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} open
|
|
2343
2399
|
* @param {AST.JSXStyleElement} node
|
|
2344
2400
|
* @param {boolean} insideHead
|
|
2401
|
+
* @param {number} [contextDepth] see #parseRawTextElement
|
|
2345
2402
|
*/
|
|
2346
|
-
#parseStyleElement(open, node, insideHead) {
|
|
2403
|
+
#parseStyleElement(open, node, insideHead, contextDepth) {
|
|
2347
2404
|
const filename = this.#filename;
|
|
2348
2405
|
if (!filename) {
|
|
2349
2406
|
throw new Error(
|
|
2350
2407
|
'<style> elements require a filename: pass one to parse so style scope hashes are unique per file.',
|
|
2351
2408
|
);
|
|
2352
2409
|
}
|
|
2353
|
-
const content = this.#parseRawTextElement(open, node, 'style');
|
|
2410
|
+
const content = this.#parseRawTextElement(open, node, 'style', contextDepth);
|
|
2411
|
+
const bodyLoc = get_line_info(this, open.end);
|
|
2354
2412
|
const parsedCss = parse_style(
|
|
2355
2413
|
content,
|
|
2356
2414
|
{
|
|
2357
2415
|
filename,
|
|
2358
2416
|
line: open.loc.start.line,
|
|
2359
2417
|
column: open.loc.start.column,
|
|
2418
|
+
body: { start: open.end, line: bodyLoc.line, column: bodyLoc.column },
|
|
2360
2419
|
},
|
|
2361
2420
|
{ loose: this.#loose },
|
|
2362
2421
|
);
|
|
@@ -2384,9 +2443,10 @@ export function TSRXPlugin(config) {
|
|
|
2384
2443
|
*
|
|
2385
2444
|
* @param {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} open
|
|
2386
2445
|
* @param {AST.TSRXJSXElement} node
|
|
2446
|
+
* @param {number} [contextDepth] see #parseRawTextElement
|
|
2387
2447
|
*/
|
|
2388
|
-
#parseScriptElement(open, node) {
|
|
2389
|
-
const content = this.#parseRawTextElement(open, node, 'script');
|
|
2448
|
+
#parseScriptElement(open, node, contextDepth) {
|
|
2449
|
+
const content = this.#parseRawTextElement(open, node, 'script', contextDepth);
|
|
2390
2450
|
node.content = content;
|
|
2391
2451
|
node.children = [];
|
|
2392
2452
|
|
|
@@ -4740,7 +4800,10 @@ export function TSRXPlugin(config) {
|
|
|
4740
4800
|
}
|
|
4741
4801
|
const tag_name = open.name ? this.getElementName(open.name) : null;
|
|
4742
4802
|
const is_dynamic = this.#isDynamicJSXElementName(open.name);
|
|
4743
|
-
|
|
4803
|
+
// `<style>` holds raw CSS text (a JSXStyleElement) unless its first
|
|
4804
|
+
// child is an expression container: `<style>{css}</style>` is the
|
|
4805
|
+
// ordinary TSX element, parsed like any other host element.
|
|
4806
|
+
const is_style = tag_name === 'style' && !this.#hasExpressionChildStart(open);
|
|
4744
4807
|
const is_script = tag_name === 'script';
|
|
4745
4808
|
const inside_head = this.#path.findLast((n) => this.#isNativeElementNamed(n, 'head'));
|
|
4746
4809
|
|
|
@@ -4792,12 +4855,28 @@ export function TSRXPlugin(config) {
|
|
|
4792
4855
|
this.#path.push(node);
|
|
4793
4856
|
|
|
4794
4857
|
if (!is_fragment && open.selfClosing) {
|
|
4858
|
+
if (is_style) {
|
|
4859
|
+
// `<style apply={theme} />` has no CSS body and no scope hash of
|
|
4860
|
+
// its own; it only contributes its `apply` targets to the scope.
|
|
4861
|
+
const style = /** @type {AST.JSXStyleElement} */ (node);
|
|
4862
|
+
style.css = '';
|
|
4863
|
+
style.children = [];
|
|
4864
|
+
}
|
|
4795
4865
|
this.#path.pop();
|
|
4796
4866
|
} else if (is_style) {
|
|
4797
|
-
this.#parseStyleElement(
|
|
4867
|
+
this.#parseStyleElement(
|
|
4868
|
+
open,
|
|
4869
|
+
/** @type {AST.JSXStyleElement} */ (node),
|
|
4870
|
+
!!inside_head,
|
|
4871
|
+
pre_element_context_depth,
|
|
4872
|
+
);
|
|
4798
4873
|
this.#path.pop();
|
|
4799
4874
|
} else if (is_script) {
|
|
4800
|
-
this.#parseScriptElement(
|
|
4875
|
+
this.#parseScriptElement(
|
|
4876
|
+
open,
|
|
4877
|
+
/** @type {AST.TSRXJSXElement} */ (node),
|
|
4878
|
+
pre_element_context_depth,
|
|
4879
|
+
);
|
|
4801
4880
|
this.#path.pop();
|
|
4802
4881
|
} else {
|
|
4803
4882
|
this.#parseNativeTemplateBody(node, /** @type {AST.Node[]} */ (node.children), {
|
|
@@ -4842,11 +4921,30 @@ export function TSRXPlugin(config) {
|
|
|
4842
4921
|
}
|
|
4843
4922
|
}
|
|
4844
4923
|
|
|
4845
|
-
if (
|
|
4846
|
-
const
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4924
|
+
if (is_style || is_script) {
|
|
4925
|
+
const raw_text = /** @type {AST.JSXStyleElement} */ (node);
|
|
4926
|
+
if (raw_text.closingElement) {
|
|
4927
|
+
const closing = /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
|
|
4928
|
+
raw_text.closingElement
|
|
4929
|
+
);
|
|
4930
|
+
return this.finishNodeAt(node, node.type, closing.end, closing.loc.end);
|
|
4931
|
+
}
|
|
4932
|
+
if (raw_text.unclosed && this.#loose) {
|
|
4933
|
+
// Loose recovery captured a body without a closing tag: the element
|
|
4934
|
+
// ends where its body ends, not at whatever token was read next.
|
|
4935
|
+
const script = /** @type {AST.TSRXJSXElement} */ (/** @type {unknown} */ (node));
|
|
4936
|
+
const body = (is_style ? raw_text.css : script.content) ?? '';
|
|
4937
|
+
const body_end =
|
|
4938
|
+
/** @type {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} */ (open).end +
|
|
4939
|
+
body.length;
|
|
4940
|
+
const body_end_info = get_line_info(this, body_end);
|
|
4941
|
+
return this.finishNodeAt(
|
|
4942
|
+
node,
|
|
4943
|
+
node.type,
|
|
4944
|
+
body_end,
|
|
4945
|
+
new acorn.Position(body_end_info.line, body_end_info.column),
|
|
4946
|
+
);
|
|
4947
|
+
}
|
|
4850
4948
|
}
|
|
4851
4949
|
|
|
4852
4950
|
return this.finishNode(node, node.type);
|
package/src/scope.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
@import * as AST from 'estree';
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import is_reference from 'is-reference';
|
|
14
|
+
import { is_reference } from './utils/is-reference.js';
|
|
15
15
|
import { extract_identifiers, object, unwrap_pattern } from './utils/ast.js';
|
|
16
16
|
import { walk } from 'zimmerframe';
|
|
17
17
|
import { is_reserved } from './utils.js';
|