@tsrx/core 0.0.26 → 0.0.28

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.
@@ -439,7 +439,6 @@ export function convert_source_map_to_mappings(
439
439
  }
440
440
 
441
441
  /**
442
- * @typedef {AST.MethodDefinition & {value: {metadata: {is_component: true}}}} MethodIsComponent
443
442
  * @typedef {AST.Property & {value: AST.FunctionExpression, method: true} & {value: {metadata: {is_component: true}}}} PropertyIsComponent
444
443
  */
445
444
 
@@ -447,7 +446,7 @@ export function convert_source_map_to_mappings(
447
446
  * Maps `component` to the identifier's location
448
447
  * e.g. const obj = { component something() { } }
449
448
  * since there is no function keyword in source maps
450
- * @param {MethodIsComponent | PropertyIsComponent} node
449
+ * @param {PropertyIsComponent} node
451
450
  * @returns {void}
452
451
  */
453
452
  function set_component_mapping_to_name(node) {
@@ -563,16 +562,17 @@ export function convert_source_map_to_mappings(
563
562
  } else if (node.type === 'JSXIdentifier') {
564
563
  // JSXIdentifiers can also be capitalized (for dynamic components)
565
564
  if (node.loc && node.name) {
566
- if (node.metadata?.is_capitalized) {
567
- tokens.push({
568
- source: node.metadata.source_name,
569
- generated: node.name,
570
- loc: node.loc,
571
- metadata: {},
572
- });
573
- } else {
574
- tokens.push({ source: node.name, generated: node.name, loc: node.loc, metadata: {} });
565
+ /** @type {Token} */
566
+ const token = {
567
+ source: node.metadata?.is_capitalized ? node.metadata.source_name : node.name,
568
+ generated: node.name,
569
+ loc: node.loc,
570
+ metadata: {},
571
+ };
572
+ if (node.metadata?.disable_verification) {
573
+ token.mappingData = { ...mapping_data, verification: false };
575
574
  }
575
+ tokens.push(token);
576
576
  }
577
577
  return; // Leaf node, don't traverse further
578
578
  } else if (node.type === 'Literal') {
@@ -867,6 +867,15 @@ export function convert_source_map_to_mappings(
867
867
  // but since we already map the opening - start, we just need the proper end
868
868
  // and it was causing some issues with mappings
869
869
  mapping.generatedLengths = [mapping.generatedLengths[0] + 1];
870
+ if (!closing && opening.selfClosing) {
871
+ const generated_close_length = '/>;'.length;
872
+ mapping.sourceOffsets = [/** @type {AST.NodeWithLocation} */ (opening).end - 2];
873
+ mapping.lengths = ['/>'.length];
874
+ mapping.generatedOffsets = [
875
+ mapping.generatedOffsets[0] + mapping.generatedLengths[0] - generated_close_length,
876
+ ];
877
+ mapping.generatedLengths = [generated_close_length];
878
+ }
870
879
  mappings.push(mapping);
871
880
  }
872
881
 
@@ -1530,15 +1539,8 @@ export function convert_source_map_to_mappings(
1530
1539
  set_bracket_computed_mapping(node, mappings);
1531
1540
  }
1532
1541
 
1533
- if (node.value.metadata.is_component) {
1534
- set_component_mapping_to_name(/** @type {MethodIsComponent} */ (node));
1535
- }
1536
-
1537
1542
  if (node.key.type === 'Literal') {
1538
- handle_literal(
1539
- node.key,
1540
- /** @type {AST.FunctionExpression} */ (node.value).metadata.is_component,
1541
- );
1543
+ handle_literal(node.key);
1542
1544
  } else {
1543
1545
  visit(node.key);
1544
1546
  }
@@ -543,3 +543,22 @@ export function render_stylesheets(stylesheets, minify = false) {
543
543
 
544
544
  return css;
545
545
  }
546
+
547
+ /**
548
+ * Render the `{ css, cssHash }` slice of a `CompileResult` from a list of
549
+ * stylesheets. Returns `{ css: '', cssHash: null }` when the list is empty
550
+ * so consumers can pass the result straight into a flat compile result.
551
+ *
552
+ * @param {AST.CSS.StyleSheet[]} stylesheets
553
+ * @param {boolean} [minify]
554
+ * @returns {{ css: string, cssHash: string | null }}
555
+ */
556
+ export function render_css_result(stylesheets, minify = false) {
557
+ if (stylesheets.length === 0) {
558
+ return { css: '', cssHash: null };
559
+ }
560
+ return {
561
+ css: render_stylesheets(stylesheets, minify),
562
+ cssHash: stylesheets.map((s) => s.hash).join(' '),
563
+ };
564
+ }
package/types/index.d.ts CHANGED
@@ -213,6 +213,7 @@ declare module 'estree' {
213
213
  Text: TextNode;
214
214
  Attribute: Attribute;
215
215
  RefAttribute: RefAttribute;
216
+ RefExpression: RefExpression;
216
217
  SpreadAttribute: SpreadAttribute;
217
218
  ParenthesizedExpression: ParenthesizedExpression;
218
219
  ScriptContent: ScriptContent;
@@ -220,6 +221,7 @@ declare module 'estree' {
220
221
 
221
222
  interface ExpressionMap {
222
223
  Style: Style;
224
+ RefExpression: RefExpression;
223
225
  Text: TextNode;
224
226
  JSXEmptyExpression: ESTreeJSX.JSXEmptyExpression;
225
227
  ParenthesizedExpression: ParenthesizedExpression;
@@ -437,6 +439,12 @@ declare module 'estree' {
437
439
  loc?: AST.SourceLocation;
438
440
  }
439
441
 
442
+ interface RefExpression extends AST.BaseNode {
443
+ type: 'RefExpression';
444
+ argument: AST.Expression;
445
+ loc?: AST.SourceLocation;
446
+ }
447
+
440
448
  interface SpreadAttribute extends AST.BaseNode {
441
449
  type: 'SpreadAttribute';
442
450
  argument: AST.Expression;
@@ -463,10 +471,6 @@ declare module 'estree' {
463
471
  body: (Program['body'][number] | Component | FunctionExpression)[];
464
472
  }
465
473
 
466
- interface TSRXMethodDefinition extends Omit<AST.MethodDefinition, 'value'> {
467
- value: AST.MethodDefinition['value'] | Component;
468
- }
469
-
470
474
  interface TSRXProperty extends Omit<AST.Property, 'value'> {
471
475
  value: AST.Property['value'] | Component;
472
476
  }
@@ -1575,15 +1579,17 @@ export interface VolarMappingsResult {
1575
1579
  * Result of compilation operation
1576
1580
  */
1577
1581
  export interface CompileResult {
1578
- /** The transformed AST */
1579
- ast: AST.Program;
1580
- /** The generated JavaScript code with source map */
1581
- js: {
1582
- code: string;
1583
- map: import('source-map').RawSourceMap;
1584
- };
1585
- /** The generated CSS */
1582
+ /** The generated JavaScript code */
1583
+ code: string;
1584
+ /** Source map for the generated code */
1585
+ map: import('source-map').RawSourceMap;
1586
+ /** Rendered CSS for the module, or `''` when the module emits no styles. */
1586
1587
  css: string;
1588
+ /**
1589
+ * Space-separated scope hashes for the rendered CSS, or `null` when the
1590
+ * module emits no styles.
1591
+ */
1592
+ cssHash: string | null;
1587
1593
  /**
1588
1594
  * Non-fatal errors collected during compilation. Populated only when the
1589
1595
  * caller passes `collect: true` or `loose: true`; empty otherwise.
@@ -1599,6 +1605,46 @@ export interface VolarCompileOptions extends Omit<ParseOptions, 'errors' | 'comm
1599
1605
  dev?: boolean;
1600
1606
  }
1601
1607
 
1608
+ /**
1609
+ * Common base options accepted by every TSRX target's `compile` entry point.
1610
+ * Targets that need extra knobs (e.g. ripple's `mode`/`dev`/`hmr`, preact's
1611
+ * `suspenseSource`) intersect their own option type with this base when
1612
+ * declaring their `compile` export.
1613
+ */
1614
+ export interface BaseCompileOptions {
1615
+ collect?: boolean;
1616
+ loose?: boolean;
1617
+ }
1618
+
1619
+ /**
1620
+ * Shared `compile` signature for every TSRX target package. Per-target
1621
+ * `compile` declarations should be `CompileFn<TOptions, TResult>` so any
1622
+ * drift in the shared contract becomes a typecheck error in every package.
1623
+ *
1624
+ * @template TOptions Per-target options accepted as the third argument.
1625
+ * Defaults to {@link BaseCompileOptions}.
1626
+ * @template TResult Per-target result type. Must extend {@link CompileResult};
1627
+ * targets may add fields (e.g. ripple's deprecated `js` back-compat field)
1628
+ * via intersection.
1629
+ */
1630
+ export type CompileFn<
1631
+ TOptions = BaseCompileOptions,
1632
+ TResult extends CompileResult = CompileResult,
1633
+ > = (source: string, filename?: string, options?: TOptions) => TResult;
1634
+
1635
+ /**
1636
+ * Shared `compile_to_volar_mappings` signature for every TSRX target package.
1637
+ *
1638
+ * @template TOptions Per-target options accepted as the third argument.
1639
+ * Defaults to {@link ParseOptions}; targets may intersect their own option
1640
+ * type to add e.g. `suspenseSource`.
1641
+ */
1642
+ export type VolarCompileFn<TOptions = ParseOptions> = (
1643
+ source: string,
1644
+ filename?: string,
1645
+ options?: TOptions,
1646
+ ) => VolarMappingsResult;
1647
+
1602
1648
  /**
1603
1649
  * Source map transformation types
1604
1650
  */
@@ -14,7 +14,14 @@ export interface JsxTransformResult {
14
14
  * downstream Vite / Rollup plugins to chain source maps.
15
15
  */
16
16
  map: RawSourceMap;
17
- css: { code: string; hash: string } | null;
17
+ /** Rendered CSS for the module, or `''` when the module emits no styles. */
18
+ css: string;
19
+ /**
20
+ * Space-separated scope hashes for the rendered CSS, or `null` when the
21
+ * module emits no styles. When multiple `<style>` blocks contribute, the
22
+ * hashes appear in source order.
23
+ */
24
+ cssHash: string | null;
18
25
  }
19
26
 
20
27
  /**
@@ -29,7 +36,10 @@ export interface JsxTransformContext {
29
36
  needs_error_boundary: boolean;
30
37
  needs_suspense: boolean;
31
38
  needs_merge_refs: boolean;
39
+ needs_ref_prop: boolean;
40
+ needs_normalize_spread_props: boolean;
32
41
  needs_fragment: boolean;
42
+ module_scoped_hook_components: boolean;
33
43
  helper_state: {
34
44
  base_name: string;
35
45
  next_id: number;
@@ -48,6 +58,8 @@ export interface JsxTransformContext {
48
58
  errors: CompileError[] | undefined;
49
59
  /** Module-level comments used to honor `@tsrx-ignore` / `@tsrx-expect-error`. */
50
60
  comments: AST.CommentWithLocation[] | undefined;
61
+ /** True when emitting a type-only virtual TSX module; preserves lazy destructuring patterns. */
62
+ typeOnly: boolean;
51
63
  }
52
64
 
53
65
  /**
@@ -80,6 +92,20 @@ export interface JsxTransformOptions {
80
92
  * `@tsrx-expect-error` line comments.
81
93
  */
82
94
  comments?: AST.CommentWithLocation[];
95
+ /**
96
+ * Override whether hook-isolation helper components are emitted directly at
97
+ * module scope. React runtime compilation enables this, while editor tooling
98
+ * can disable it to preserve lexical `typeof` helper prop types.
99
+ */
100
+ moduleScopedHookComponents?: boolean;
101
+ /**
102
+ * Emit a type-only virtual TSX module — output is fed to TypeScript for
103
+ * editor diagnostics / completions and never executed. Skips the lazy
104
+ * destructuring rewrite (`&{ a, b }` → `__lazy0: { a: any; b: any }`) so
105
+ * destructuring patterns survive and TypeScript can flow real types to the
106
+ * bindings.
107
+ */
108
+ typeOnly?: boolean;
83
109
  }
84
110
 
85
111
  /**
@@ -135,6 +161,13 @@ export interface JsxPlatformHooks {
135
161
  * state behaves like normal component state.
136
162
  */
137
163
  wrapHelperComponent?: (helperFn: any, helperId: any, ctx: any, sourceNode: any) => any;
164
+ /**
165
+ * Emit hook-isolation helper components as unique module-scope declarations
166
+ * instead of lazily creating and caching them from the parent component body.
167
+ * React enables this so generated branches stay compatible with the React
168
+ * Compiler's Rules of Hooks validation.
169
+ */
170
+ moduleScopedHookComponents?: boolean;
138
171
  /**
139
172
  * Inject module-level imports after the main walk. Default: import
140
173
  * `Suspense` from `platform.imports.suspense` and `TsrxErrorBoundary`
@@ -162,7 +195,7 @@ export interface JsxPlatformHooks {
162
195
  * Optionally replace the default React-style `.map(...)` lowering for a
163
196
  * `for...of` body after the shared transform has already produced its render
164
197
  * statements and applied any explicit or implicit keys. Vue uses this to hand
165
- * the loop to the downstream Vapor JSX compiler as a native `v-for` template.
198
+ * the loop to the downstream Vapor JSX compiler as a typed `VaporFor` component.
166
199
  */
167
200
  renderForOf?: (node: any, loopParams: any[], bodyStatements: any[], ctx: any) => any | null;
168
201
  /**
@@ -259,9 +292,14 @@ export interface JsxPlatform {
259
292
  * Module to import `mergeRefs` from when an element has more than one
260
293
  * `ref` attribute and the platform uses the `'merge-refs'` strategy.
261
294
  * Required when `jsx.multiRefStrategy === 'merge-refs'`; ignored
262
- * otherwise. React: `'@tsrx/react/merge-refs'`. Preact: `'@tsrx/preact/merge-refs'`.
295
+ * otherwise. React: `'@tsrx/react/ref'`. Preact: `'@tsrx/preact/ref'`.
263
296
  */
264
297
  mergeRefs?: string;
298
+ /**
299
+ * Module to import named-ref-prop helpers from when compiling
300
+ * `prop={ref expr}` or normalizing host spreads containing named refs.
301
+ */
302
+ refProp?: string;
265
303
  };
266
304
 
267
305
  jsx: {
@@ -289,6 +327,12 @@ export interface JsxPlatform {
289
327
  * unchanged. The platform's runtime is responsible.
290
328
  */
291
329
  multiRefStrategy?: 'merge-refs' | 'array';
330
+ /**
331
+ * Some JSX runtimes do not apply a `ref` that arrives through a props
332
+ * spread. In that case, host spread normalization also emits an
333
+ * explicit `ref={normalized.ref}` attribute.
334
+ */
335
+ hostSpreadRefStrategy?: 'explicit-ref-attr';
292
336
  };
293
337
 
294
338
  validation: {
@@ -0,0 +1,32 @@
1
+ export type MergeableRefCallback<T> = {
2
+ bivarianceHack(node: T | null): void | (() => void);
3
+ }['bivarianceHack'];
4
+ export type MergeableRefObject<T> = { current: T | null };
5
+ export type MergeableVueRef<T> = { value: T | null };
6
+ export type RefProp<T = unknown> = (node: T | null) => void | (() => void);
7
+
8
+ export type MergeableRef<T> =
9
+ | MergeableRefCallback<T>
10
+ | MergeableRefObject<T>
11
+ | MergeableVueRef<T>
12
+ | null
13
+ | undefined;
14
+
15
+ export function mergeRefs<T = any>(...refs: Array<MergeableRef<T>>): (node: T | null) => () => void;
16
+ export function isRefProp(value: unknown): boolean;
17
+ export function create_ref_prop<T>(
18
+ get_ref_value: () => T,
19
+ set_ref_value?: (value: T) => void,
20
+ ): RefProp<T>;
21
+ export function apply_ref_value<T>(
22
+ ref_value: unknown,
23
+ node: T | null,
24
+ set_ref_value?: (value: T) => void,
25
+ ): void | (() => void);
26
+ export function merge_ref_props<T = any>(
27
+ ...refs: unknown[]
28
+ ): undefined | ((node: T | null) => void | (() => void));
29
+ export function normalize_spread_props<T extends Record<PropertyKey, any> | null | undefined>(
30
+ props: T,
31
+ ...outer_refs: unknown[]
32
+ ): T | Record<PropertyKey, any>;
@@ -1,61 +0,0 @@
1
- /**
2
- * Merge multiple refs (function refs and ref objects) into a single
3
- * callback ref. Used by the tsrx-react, tsrx-preact, and tsrx-vue
4
- * compilers when an element has more than one `ref` attribute, since
5
- * those runtimes treat duplicate `ref` props as a regular duplicate-prop
6
- * collision (last wins) rather than running both. Solid does not use this
7
- * helper — its native runtime accepts an array of refs and the compiler
8
- * emits an array literal directly.
9
- *
10
- * The returned callback ref handles four cases per input:
11
- * - `null` / `undefined`: skipped.
12
- * - function ref: invoked with the node. If it returns a function, that
13
- * return value is treated as a React 19 cleanup. Otherwise we record a
14
- * cleanup that calls the ref with `null` so the legacy unmount contract
15
- * still fires.
16
- * - React-style ref object (`{ current }`): assigned on mount, cleared
17
- * on unmount.
18
- * - Vue-style ref object (`{ value }`, e.g. `ref()` / `useTemplateRef()`):
19
- * assigned on mount, cleared on unmount.
20
- *
21
- * The merged ref always returns a cleanup. Under React 19 the cleanup
22
- * runs on unmount and the inner refs are not separately re-invoked with
23
- * `null`. Under older React, Preact, and Vue the cleanup return value
24
- * is ignored, so the runtime instead invokes the merged ref a second
25
- * time with `null` — which re-runs the loop body, calling each inner
26
- * with `null` and clearing each ref object. Either way every inner ref
27
- * sees a balanced mount/unmount.
28
- *
29
- * @param {...((node: any) => void | (() => void)) | { current: any } | { value: any } | null | undefined} refs
30
- * @returns {(node: any) => (() => void)}
31
- */
32
- export function mergeRefs(...refs) {
33
- return (node) => {
34
- /** @type {Array<() => void>} */
35
- const cleanups = [];
36
- for (const ref of refs) {
37
- if (ref == null) continue;
38
- if (typeof ref === 'function') {
39
- const result = ref(node);
40
- if (typeof result === 'function') {
41
- cleanups.push(result);
42
- } else {
43
- cleanups.push(() => ref(null));
44
- }
45
- } else if ('current' in ref) {
46
- ref.current = node;
47
- cleanups.push(() => {
48
- ref.current = null;
49
- });
50
- } else if ('value' in ref) {
51
- ref.value = node;
52
- cleanups.push(() => {
53
- ref.value = null;
54
- });
55
- }
56
- }
57
- return () => {
58
- for (const cleanup of cleanups) cleanup();
59
- };
60
- };
61
- }
@@ -1,12 +0,0 @@
1
- export type MergeableRefCallback<T> = (node: T | null) => void | (() => void);
2
- export type MergeableRefObject<T> = { current: T | null };
3
- export type MergeableVueRef<T> = { value: T | null };
4
-
5
- export type MergeableRef<T> =
6
- | MergeableRefCallback<T>
7
- | MergeableRefObject<T>
8
- | MergeableVueRef<T>
9
- | null
10
- | undefined;
11
-
12
- export function mergeRefs<T = any>(...refs: Array<MergeableRef<T>>): (node: T | null) => () => void;