@tsrx/core 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json 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.17",
6
+ "version": "0.1.18",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -1095,7 +1095,7 @@ export function prune_css(css, element, styleClasses, topScopedClasses) {
1095
1095
  // A class is standalone only when the entire effective selector chain (after resolving
1096
1096
  // nesting and stripping :global) is a single RelativeSelector with a single ClassSelector.
1097
1097
  // This prevents classes from compound selectors like `.wrapper .nested` or selectors
1098
- // inside :global() from being exported through style refs.
1098
+ // inside :global() from being exported through style expression maps.
1099
1099
  if (selectors.length === 1) {
1100
1100
  const sole_selector = selectors[0];
1101
1101
  if (
package/src/index.js CHANGED
@@ -170,7 +170,9 @@ export {
170
170
  export {
171
171
  collect_style_ref_attributes as collectStyleRefAttributes,
172
172
  create_style_class_map as createStyleClassMap,
173
+ create_style_class_map_from_stylesheet as createStyleClassMapFromStylesheet,
173
174
  create_style_ref_setup_statements as createStyleRefSetupStatements,
175
+ get_style_element_stylesheet as getStyleElementStylesheet,
174
176
  } from './transform/style-ref.js';
175
177
  export {
176
178
  clone_expression_node,
@@ -45,7 +45,9 @@ import { prepare_stylesheet_for_render, annotate_with_hash, is_style_element } f
45
45
  import {
46
46
  collect_style_ref_attributes,
47
47
  create_style_class_map,
48
+ create_style_class_map_from_stylesheet,
48
49
  create_style_ref_setup_statements,
50
+ get_style_element_stylesheet,
49
51
  } from '../style-ref.js';
50
52
  import { is_function_or_component_node } from '../../utils/ast.js';
51
53
  import {
@@ -251,7 +253,16 @@ export function createJsxTransform(platform) {
251
253
  );
252
254
  },
253
255
 
254
- Element(node, { next, state }) {
256
+ Element(node, { next, path, state }) {
257
+ if (is_style_element(node) && is_style_expression_position(path)) {
258
+ const stylesheet = get_style_element_stylesheet(node);
259
+ if (stylesheet) {
260
+ analyze_css(stylesheet);
261
+ state.stylesheets.push(stylesheet);
262
+ return /** @type {any} */ (create_style_class_map_from_stylesheet(stylesheet));
263
+ }
264
+ }
265
+
255
266
  // Capture raw children BEFORE the walker transforms them so a
256
267
  // platform hook (e.g. Solid's textContent optimization) can
257
268
  // inspect the original Text / TSRXExpression nodes rather than
@@ -1420,7 +1431,8 @@ function prepare_tsrx_fragment_styles(node, transform_context) {
1420
1431
  annotate_tsrx_with_hash(
1421
1432
  node,
1422
1433
  css.hash,
1423
- transform_context.platform.jsx.rewriteClassAttr ? 'className' : 'class',
1434
+ transform_context.platform.jsx.classAttrName ??
1435
+ (transform_context.platform.jsx.rewriteClassAttr ? 'className' : 'class'),
1424
1436
  transform_context.typeOnly,
1425
1437
  );
1426
1438
  return { css, style_refs };
@@ -1502,11 +1514,33 @@ function collect_style_elements(node, styles) {
1502
1514
  return;
1503
1515
  }
1504
1516
 
1505
- for (const key of Object.keys(node)) {
1506
- if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
1507
- continue;
1517
+ if (node.type === 'Element') {
1518
+ collect_style_elements(node.children || [], styles);
1519
+ return;
1520
+ }
1521
+
1522
+ if (node.type === 'BlockStatement') {
1523
+ collect_style_elements(node.body || [], styles);
1524
+ return;
1525
+ }
1526
+
1527
+ if (node.type === 'IfStatement') {
1528
+ collect_style_elements(node.consequent, styles);
1529
+ collect_style_elements(node.alternate, styles);
1530
+ return;
1531
+ }
1532
+
1533
+ if (node.type === 'SwitchStatement') {
1534
+ for (const switch_case of node.cases || []) {
1535
+ collect_style_elements(switch_case.consequent || [], styles);
1508
1536
  }
1509
- collect_style_elements(node[key], styles);
1537
+ return;
1538
+ }
1539
+
1540
+ if (node.type === 'TryStatement') {
1541
+ collect_style_elements(node.block, styles);
1542
+ collect_style_elements(node.handler?.body, styles);
1543
+ collect_style_elements(node.finalizer, styles);
1510
1544
  }
1511
1545
  }
1512
1546
 
@@ -1548,24 +1582,55 @@ function strip_style_elements(node) {
1548
1582
  return node;
1549
1583
  }
1550
1584
 
1551
- for (const key of Object.keys(node)) {
1552
- if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
1553
- continue;
1554
- }
1555
- const value = node[key];
1556
- if (Array.isArray(value)) {
1557
- node[key] = strip_style_elements(value);
1558
- } else if (value && typeof value === 'object') {
1559
- const stripped = strip_style_elements(value);
1560
- if (stripped) {
1561
- node[key] = stripped;
1562
- }
1585
+ if (node.type === 'Element') {
1586
+ node.children = strip_style_elements(node.children || []);
1587
+ return node;
1588
+ }
1589
+
1590
+ if (node.type === 'BlockStatement') {
1591
+ node.body = strip_style_elements(node.body || []);
1592
+ return node;
1593
+ }
1594
+
1595
+ if (node.type === 'IfStatement') {
1596
+ node.consequent = strip_style_elements(node.consequent);
1597
+ if (node.alternate) node.alternate = strip_style_elements(node.alternate);
1598
+ return node;
1599
+ }
1600
+
1601
+ if (node.type === 'SwitchStatement') {
1602
+ for (const switch_case of node.cases || []) {
1603
+ switch_case.consequent = strip_style_elements(switch_case.consequent || []);
1563
1604
  }
1605
+ return node;
1606
+ }
1607
+
1608
+ if (node.type === 'TryStatement') {
1609
+ node.block = strip_style_elements(node.block);
1610
+ if (node.handler?.body) node.handler.body = strip_style_elements(node.handler.body);
1611
+ if (node.finalizer) node.finalizer = strip_style_elements(node.finalizer);
1564
1612
  }
1565
1613
 
1566
1614
  return node;
1567
1615
  }
1568
1616
 
1617
+ /**
1618
+ * @param {any[]} path
1619
+ * @returns {boolean}
1620
+ */
1621
+ function is_style_expression_position(path) {
1622
+ const parent = path.at(-1);
1623
+ return !(
1624
+ parent?.type === 'Element' ||
1625
+ parent?.type === 'Tsrx' ||
1626
+ parent?.type === 'Tsx' ||
1627
+ parent?.type === 'TsxCompat' ||
1628
+ parent?.type === 'BlockStatement' ||
1629
+ parent?.type === 'Program' ||
1630
+ parent?.type === 'SwitchCase'
1631
+ );
1632
+ }
1633
+
1569
1634
  /**
1570
1635
  * @param {any} node
1571
1636
  * @param {TransformContext} transform_context
@@ -5605,8 +5670,8 @@ export function to_jsx_attribute(attr, transform_context) {
5605
5670
  attr,
5606
5671
  );
5607
5672
  }
5608
- // Platforms that expect React-style DOM attrs (React) rewrite `class` to
5609
- // `className`; Preact and Solid accept `class` natively and keep it.
5673
+ // Keep this legacy hook for targets that need React-style DOM attrs. The
5674
+ // current first-party targets preserve authored `class`.
5610
5675
  let attr_name = attr.name;
5611
5676
  if (
5612
5677
  transform_context.platform.jsx.rewriteClassAttr &&
@@ -119,7 +119,7 @@ export function annotate_with_hash(
119
119
  return node;
120
120
  }
121
121
  if (!is_style_element(node) && !is_composite_element(node)) {
122
- add_hash_class(node, hash);
122
+ add_hash_class(node, hash, jsx_class_attr_name);
123
123
  }
124
124
  if (Array.isArray(node.children)) {
125
125
  node.children = node.children
@@ -184,13 +184,14 @@ export function annotate_component_with_hash(
184
184
  }
185
185
 
186
186
  /**
187
- * Ensure the element carries a `class` attribute containing the scoping hash.
187
+ * Ensure the element carries a class attribute containing the scoping hash.
188
188
  *
189
189
  * @param {any} element
190
190
  * @param {string} hash
191
+ * @param {'class' | 'className'} [class_attr_name='class']
191
192
  * @returns {void}
192
193
  */
193
- export function add_hash_class(element, hash) {
194
+ export function add_hash_class(element, hash, class_attr_name = 'class') {
194
195
  const attrs = element.attributes || (element.attributes = []);
195
196
  const existing = attrs.find(
196
197
  (/** @type {any} */ a) =>
@@ -203,7 +204,7 @@ export function add_hash_class(element, hash) {
203
204
  if (!existing) {
204
205
  attrs.push({
205
206
  type: 'Attribute',
206
- name: b.id('class'),
207
+ name: b.id(class_attr_name),
207
208
  value: { type: 'Literal', value: hash, raw: JSON.stringify(hash) },
208
209
  });
209
210
  return;
@@ -249,18 +250,6 @@ function add_hash_class_to_jsx_element(element, hash, jsx_class_attr_name) {
249
250
  return;
250
251
  }
251
252
 
252
- if (existing.name.name !== jsx_class_attr_name) {
253
- existing.name = {
254
- type: 'JSXIdentifier',
255
- name: jsx_class_attr_name,
256
- metadata: {
257
- ...(existing.name.metadata || { path: [] }),
258
- source_name: existing.name.name,
259
- source_length: existing.name.name.length,
260
- },
261
- };
262
- }
263
-
264
253
  const value = existing.value;
265
254
  if (!value) {
266
255
  existing.value = { type: 'Literal', value: hash, raw: JSON.stringify(hash) };
@@ -3,6 +3,8 @@
3
3
  import * as b from '../utils/builders.js';
4
4
  import { clone_expression_node, clone_identifier } from './jsx/ast-builders.js';
5
5
 
6
+ const regex_backslash_and_following_character = /\\(.)/g;
7
+
6
8
  /**
7
9
  * @typedef {{
8
10
  * allowMutableRefTarget?: boolean;
@@ -19,7 +21,7 @@ import { clone_expression_node, clone_identifier } from './jsx/ast-builders.js';
19
21
  export function create_style_class_map(component, css) {
20
22
  const hash = css?.hash ?? null;
21
23
  const top_scoped_classes = /** @type {Map<string, any>} */ (
22
- component?.metadata?.topScopedClasses ?? new Map()
24
+ component?.metadata?.topScopedClasses ?? collect_style_class_map_entries(css)
23
25
  );
24
26
  const class_names = [...top_scoped_classes.keys()].sort();
25
27
 
@@ -30,6 +32,28 @@ export function create_style_class_map(component, css) {
30
32
  );
31
33
  }
32
34
 
35
+ /**
36
+ * @param {any} css
37
+ * @returns {AST.ObjectExpression}
38
+ */
39
+ export function create_style_class_map_from_stylesheet(css) {
40
+ return create_style_class_map(
41
+ { metadata: { topScopedClasses: collect_style_class_map_entries(css) } },
42
+ css,
43
+ );
44
+ }
45
+
46
+ /**
47
+ * @param {any} style_element
48
+ * @returns {any | null}
49
+ */
50
+ export function get_style_element_stylesheet(style_element) {
51
+ return (
52
+ style_element?.children?.find?.((/** @type {any} */ child) => child.type === 'StyleSheet') ??
53
+ null
54
+ );
55
+ }
56
+
33
57
  /**
34
58
  * @param {any} node
35
59
  * @param {any[]} [refs]
@@ -233,3 +257,70 @@ function is_function_or_class_boundary(node) {
233
257
  node?.type === 'ClassExpression'
234
258
  );
235
259
  }
260
+
261
+ /**
262
+ * @param {any} css
263
+ * @returns {Map<string, any>}
264
+ */
265
+ function collect_style_class_map_entries(css) {
266
+ const entries = new Map();
267
+ collect_rule_class_map_entries(css, entries);
268
+ return entries;
269
+ }
270
+
271
+ /**
272
+ * @param {any} node
273
+ * @param {Map<string, any>} entries
274
+ * @returns {void}
275
+ */
276
+ function collect_rule_class_map_entries(node, entries) {
277
+ if (!node || typeof node !== 'object') return;
278
+
279
+ if (Array.isArray(node)) {
280
+ for (const child of node) collect_rule_class_map_entries(child, entries);
281
+ return;
282
+ }
283
+
284
+ if (node.type === 'ComplexSelector') {
285
+ const class_selector = get_standalone_class_selector(node);
286
+ if (class_selector) {
287
+ const name = class_selector.name.replace(regex_backslash_and_following_character, '$1');
288
+ if (!entries.has(name)) {
289
+ entries.set(name, {
290
+ start: class_selector.start,
291
+ end: class_selector.end,
292
+ selector: class_selector,
293
+ });
294
+ }
295
+ }
296
+ }
297
+
298
+ if (is_function_or_class_boundary(node)) {
299
+ return;
300
+ }
301
+
302
+ for (const key of Object.keys(node)) {
303
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
304
+ continue;
305
+ }
306
+ collect_rule_class_map_entries(node[key], entries);
307
+ }
308
+ }
309
+
310
+ /**
311
+ * @param {any} complex_selector
312
+ * @returns {any | null}
313
+ */
314
+ function get_standalone_class_selector(complex_selector) {
315
+ if (complex_selector?.children?.length !== 1) return null;
316
+ const relative_selector = complex_selector.children[0];
317
+ if (
318
+ relative_selector?.metadata?.is_global ||
319
+ relative_selector?.metadata?.is_global_like ||
320
+ relative_selector?.selectors?.length !== 1
321
+ ) {
322
+ return null;
323
+ }
324
+ const selector = relative_selector.selectors[0];
325
+ return selector?.type === 'ClassSelector' ? selector : null;
326
+ }
package/types/index.d.ts CHANGED
@@ -26,6 +26,7 @@ export { createJsxTransform };
26
26
 
27
27
  export function collectStyleRefAttributes(node: any, refs?: any[]): any[];
28
28
  export function createStyleClassMap(component: any, css: any): AST.ObjectExpression;
29
+ export function createStyleClassMapFromStylesheet(css: any): AST.ObjectExpression;
29
30
  export function createStyleRefSetupStatements(
30
31
  refAttributes: any[],
31
32
  styleMap: AST.Expression,
@@ -35,6 +36,7 @@ export function createStyleRefSetupStatements(
35
36
  visitExpression?: (expression: AST.Expression) => AST.Expression;
36
37
  },
37
38
  ): AST.Statement[];
39
+ export function getStyleElementStylesheet(styleElement: any): any | null;
38
40
 
39
41
  /**
40
42
  * Compile error interface
@@ -354,10 +354,15 @@ export interface JsxPlatform {
354
354
 
355
355
  jsx: {
356
356
  /**
357
- * Rewrite Ripple's `class` attribute to React's `className`. React: true.
358
- * Preact and Solid accept `class` natively, so: false.
357
+ * Rewrite Ripple's `class` attribute to `className` for legacy targets
358
+ * that require it. First-party targets keep authored `class`.
359
359
  */
360
360
  rewriteClassAttr: boolean;
361
+ /**
362
+ * Attribute name to use when TSRX injects scoped CSS classes. This does
363
+ * not rewrite authored attributes.
364
+ */
365
+ classAttrName?: 'class' | 'className';
361
366
  /**
362
367
  * Accepted values of `kind` in `<tsx:kind>` compat blocks. React accepts
363
368
  * only `'react'`. Preact accepts both `'preact'` and `'react'`.