@tsrx/core 0.1.49 → 0.1.51

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.
@@ -1,29 +1,46 @@
1
1
  /** @import * as AST from 'estree' */
2
+ /** @import * as ESTreeJSX from 'estree-jsx' */
3
+ /** @import { ClassMapCollectionState, StyleRefOptions, TopScopedClasses, Visitors } from '../../types/index' */
2
4
 
5
+ import { walk } from 'zimmerframe';
3
6
  import * as b from '../utils/builders.js';
4
- import { is_function_or_class_node as is_function_or_class_boundary } from '../utils/ast.js';
7
+ import {
8
+ child_nodes,
9
+ is_function_or_class_node as is_function_or_class_boundary,
10
+ is_style_element,
11
+ } from '../utils/ast.js';
5
12
  import { clone_ast_node, clone_identifier } from './jsx/ast-builders.js';
6
13
 
7
14
  const regex_backslash_and_following_character = /\\(.)/g;
8
15
 
9
16
  /**
10
- * @typedef {{
11
- * allowMutableRefTarget?: boolean;
12
- * createTempIdentifier?: () => AST.Identifier;
13
- * visitExpression?: (expression: AST.Expression) => AST.Expression;
14
- * }} StyleRefOptions
17
+ * @param {AST.Node} component
18
+ * @param {AST.CSS.StyleSheet} css
19
+ * @returns {AST.ObjectExpression}
15
20
  */
21
+ export function create_style_class_map(component, css) {
22
+ return build_style_class_map(
23
+ component.metadata?.topScopedClasses ?? collect_style_class_map_entries(css),
24
+ css.hash,
25
+ );
26
+ }
16
27
 
17
28
  /**
18
- * @param {any} component
19
- * @param {any} css
29
+ * @param {AST.CSS.StyleSheet} css
20
30
  * @returns {AST.ObjectExpression}
21
31
  */
22
- export function create_style_class_map(component, css) {
23
- const hash = css?.hash ?? null;
24
- const top_scoped_classes = /** @type {Map<string, any>} */ (
25
- component?.metadata?.topScopedClasses ?? collect_style_class_map_entries(css)
26
- );
32
+ export function create_style_class_map_from_stylesheet(css) {
33
+ return build_style_class_map(collect_style_class_map_entries(css), css.hash);
34
+ }
35
+
36
+ /**
37
+ * `{ foo: 'hash foo', … }` for every class the style expression exposes.
38
+ *
39
+ * @param {TopScopedClasses} top_scoped_classes
40
+ * @param {string | null} hash
41
+ * @returns {AST.ObjectExpression}
42
+ */
43
+ function build_style_class_map(top_scoped_classes, hash) {
27
44
  const class_names = [...top_scoped_classes.keys()].sort();
28
45
 
29
46
  return b.object(
@@ -34,42 +51,28 @@ export function create_style_class_map(component, css) {
34
51
  }
35
52
 
36
53
  /**
37
- * @param {any} css
38
- * @returns {AST.ObjectExpression}
39
- */
40
- export function create_style_class_map_from_stylesheet(css) {
41
- return create_style_class_map(
42
- { metadata: { topScopedClasses: collect_style_class_map_entries(css) } },
43
- css,
44
- );
45
- }
46
-
47
- /**
48
- * @param {any} style_element
49
- * @returns {any | null}
54
+ * @param {AST.JSXStyleElement} style_element
55
+ * @returns {AST.CSS.StyleSheet | null}
50
56
  */
51
57
  export function get_style_element_stylesheet(style_element) {
52
- return (
53
- style_element?.children?.find?.((/** @type {any} */ child) => child.type === 'StyleSheet') ??
54
- null
55
- );
58
+ return style_element.children?.find((child) => child.type === 'StyleSheet') ?? null;
56
59
  }
57
60
 
58
61
  /**
59
- * @param {any} node
60
- * @param {any[]} [refs]
61
- * @returns {any[]}
62
+ * @param {AST.Node | AST.Node[]} node
63
+ * @param {ESTreeJSX.JSXAttribute[]} [refs]
64
+ * @returns {ESTreeJSX.JSXAttribute[]}
62
65
  */
63
66
  export function collect_style_ref_attributes(node, refs = []) {
64
- if (!node || typeof node !== 'object') return refs;
65
-
66
67
  if (Array.isArray(node)) {
67
68
  for (const child of node) collect_style_ref_attributes(child, refs);
68
69
  return refs;
69
70
  }
70
71
 
72
+ if (!node || typeof node !== 'object') return refs;
73
+
71
74
  if (is_style_element(node)) {
72
- for (const attr of node.openingElement?.attributes || []) {
75
+ for (const attr of node.openingElement.attributes) {
73
76
  if (is_ref_attribute(attr) && attr.value) {
74
77
  refs.push(attr);
75
78
  }
@@ -81,18 +84,15 @@ export function collect_style_ref_attributes(node, refs = []) {
81
84
  return refs;
82
85
  }
83
86
 
84
- for (const key of Object.keys(node)) {
85
- if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
86
- continue;
87
- }
88
- collect_style_ref_attributes(node[key], refs);
87
+ for (const child of child_nodes(node, 'css')) {
88
+ collect_style_ref_attributes(child, refs);
89
89
  }
90
90
 
91
91
  return refs;
92
92
  }
93
93
 
94
94
  /**
95
- * @param {any[]} ref_attributes
95
+ * @param {ESTreeJSX.JSXAttribute[]} ref_attributes
96
96
  * @param {AST.Expression} style_map
97
97
  * @param {StyleRefOptions} [options]
98
98
  * @returns {AST.Statement[]}
@@ -203,7 +203,7 @@ function visit_expression(expression, options) {
203
203
  }
204
204
 
205
205
  /**
206
- * @param {any} attr
206
+ * @param {ESTreeJSX.JSXAttribute} attr
207
207
  * @returns {AST.Expression | null}
208
208
  */
209
209
  function get_ref_attribute_expression(attr) {
@@ -216,28 +216,21 @@ function get_ref_attribute_expression(attr) {
216
216
  }
217
217
 
218
218
  /**
219
- * @param {any} attr
220
- * @returns {boolean}
219
+ * @param {ESTreeJSX.JSXAttributeNode} attr
220
+ * @returns {attr is ESTreeJSX.JSXAttribute}
221
221
  */
222
222
  function is_ref_attribute(attr) {
223
223
  return (
224
- attr?.type === 'JSXAttribute' && attr.name?.type === 'JSXIdentifier' && attr.name.name === 'ref'
224
+ attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === 'ref'
225
225
  );
226
226
  }
227
227
 
228
228
  /**
229
- * @param {any} node
230
- * @returns {boolean}
231
- */
232
- function is_style_element(node) {
233
- return !!node && node.type === 'JSXStyleElement';
234
- }
235
-
236
- /**
237
- * @param {any} css
238
- * @returns {Map<string, any>}
229
+ * @param {AST.CSS.StyleSheet} css
230
+ * @returns {TopScopedClasses}
239
231
  */
240
232
  function collect_style_class_map_entries(css) {
233
+ /** @type {TopScopedClasses} */
241
234
  const entries = new Map();
242
235
  collect_rule_class_map_entries(css, entries);
243
236
  return entries;
@@ -249,7 +242,7 @@ function collect_style_class_map_entries(css) {
249
242
  * `create_style_class_map_from_stylesheet`, so marking and the generated map
250
243
  * always agree; calling both is harmless.
251
244
  *
252
- * @param {any} css
245
+ * @param {AST.CSS.StyleSheet} css
253
246
  * @returns {void}
254
247
  */
255
248
  export function mark_class_map_selectors(css) {
@@ -257,66 +250,59 @@ export function mark_class_map_selectors(css) {
257
250
  }
258
251
 
259
252
  /**
260
- * @param {any} node
261
- * @param {Map<string, any>} entries
262
- * @param {any} [enclosing_selector] the nearest prelude-level selector; classes
263
- * found inside another selector (e.g. in `:global(...)` args) mark it as the
264
- * selector that carries their class map entry
253
+ * The state threaded through the class-map collection walk: the nearest
254
+ * prelude-level selector. Classes found inside another selector (e.g. in
255
+ * `:global(...)` args) mark it as the selector that carries their class map
256
+ * entry.
257
+ *
258
+ * @param {AST.CSS.StyleSheet} css
259
+ * @param {TopScopedClasses} entries
265
260
  * @returns {void}
266
261
  */
267
- function collect_rule_class_map_entries(node, entries, enclosing_selector = null) {
268
- if (!node || typeof node !== 'object') return;
269
-
270
- if (Array.isArray(node)) {
271
- for (const child of node) collect_rule_class_map_entries(child, entries, enclosing_selector);
272
- return;
273
- }
274
-
275
- if (node.type === 'ComplexSelector') {
276
- enclosing_selector ??= node;
277
- const class_selector = get_standalone_class_selector(node);
278
- if (class_selector) {
279
- // Mark the prelude-level selector for every occurrence (not just the
280
- // deduped first) so the render preparation of style expressions keeps
281
- // exactly the selectors whose classes the map exposes.
282
- (enclosing_selector.metadata ??= {}).class_map_selector = true;
283
- const name = class_selector.name.replace(regex_backslash_and_following_character, '$1');
284
- if (!entries.has(name)) {
285
- entries.set(name, {
286
- start: class_selector.start,
287
- end: class_selector.end,
288
- selector: class_selector,
289
- });
290
- }
291
- }
292
- }
293
-
294
- if (is_function_or_class_boundary(node)) {
295
- return;
296
- }
297
-
298
- for (const key of Object.keys(node)) {
299
- if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
300
- continue;
301
- }
302
- collect_rule_class_map_entries(node[key], entries, enclosing_selector);
303
- }
262
+ function collect_rule_class_map_entries(css, entries) {
263
+ walk(
264
+ /** @type {AST.CSS.Node} */ (css),
265
+ /** @type {ClassMapCollectionState} */ ({ enclosing_selector: null }),
266
+ /** @type {Visitors<AST.CSS.Node, ClassMapCollectionState>} */ ({
267
+ ComplexSelector(node, context) {
268
+ const enclosing_selector = context.state.enclosing_selector ?? node;
269
+ const class_selector = get_standalone_class_selector(node);
270
+
271
+ if (class_selector) {
272
+ // Mark the prelude-level selector for every occurrence (not just the
273
+ // deduped first) so the render preparation of style expressions keeps
274
+ // exactly the selectors whose classes the map exposes.
275
+ enclosing_selector.metadata.class_map_selector = true;
276
+ const name = class_selector.name.replace(regex_backslash_and_following_character, '$1');
277
+ if (!entries.has(name)) {
278
+ entries.set(name, {
279
+ start: class_selector.start,
280
+ end: class_selector.end,
281
+ selector: class_selector,
282
+ });
283
+ }
284
+ }
285
+
286
+ context.next({ enclosing_selector });
287
+ },
288
+ }),
289
+ );
304
290
  }
305
291
 
306
292
  /**
307
- * @param {any} complex_selector
308
- * @returns {any | null}
293
+ * @param {AST.CSS.ComplexSelector} complex_selector
294
+ * @returns {AST.CSS.ClassSelector | null}
309
295
  */
310
296
  function get_standalone_class_selector(complex_selector) {
311
- if (complex_selector?.children?.length !== 1) return null;
297
+ if (complex_selector.children.length !== 1) return null;
312
298
  const relative_selector = complex_selector.children[0];
313
299
  if (
314
- relative_selector?.metadata?.is_global ||
315
- relative_selector?.metadata?.is_global_like ||
316
- relative_selector?.selectors?.length !== 1
300
+ relative_selector.metadata.is_global ||
301
+ relative_selector.metadata.is_global_like ||
302
+ relative_selector.selectors.length !== 1
317
303
  ) {
318
304
  return null;
319
305
  }
320
306
  const selector = relative_selector.selectors[0];
321
- return selector?.type === 'ClassSelector' ? selector : null;
307
+ return selector.type === 'ClassSelector' ? selector : null;
322
308
  }
@@ -1,22 +1,6 @@
1
1
  /**
2
2
  @import * as AST from 'estree';
3
- @import { Visitors } from '../../types/index';
4
- */
5
-
6
- /**
7
- @typedef {{
8
- code: MagicString;
9
- hash: string;
10
- minify: boolean;
11
- selector: string;
12
- keyframes: Record<string, {
13
- indexes: number[];
14
- local: boolean | undefined;
15
- }>;
16
- specificity: {
17
- bumped: boolean
18
- }
19
- }} State
3
+ @import { StylesheetRenderState, Visitors } from '../../types/index';
20
4
  */
21
5
 
22
6
  import MagicString from 'magic-string';
@@ -39,7 +23,7 @@ function remove_css_prefix(name) {
39
23
  /**
40
24
  * Walk backwards until we find a non-whitespace character
41
25
  * @param {number} end
42
- * @param {State} state
26
+ * @param {StylesheetRenderState} state
43
27
  */
44
28
  function remove_preceding_whitespace(end, state) {
45
29
  let start = end;
@@ -121,7 +105,7 @@ function has_global_in_middle(rule) {
121
105
  /**
122
106
  * @param {AST.CSS.PseudoClassSelector} selector
123
107
  * @param {AST.CSS.Combinator | null} combinator
124
- * @param {State} state
108
+ * @param {StylesheetRenderState} state
125
109
  */
126
110
  function remove_global_pseudo_class(selector, combinator, state) {
127
111
  if (selector.args === null) {
@@ -166,7 +150,7 @@ function escape_comment_close(node, code) {
166
150
  }
167
151
 
168
152
  /**
169
- * @param {State} state
153
+ * @param {StylesheetRenderState} state
170
154
  * @param {number} index
171
155
  */
172
156
  function append_hash(state, index) {
@@ -207,7 +191,7 @@ function is_empty(rule, is_in_global_block) {
207
191
  return true;
208
192
  }
209
193
 
210
- /** @type {Visitors<AST.CSS.Node, State>} */
194
+ /** @type {Visitors<AST.CSS.Node, StylesheetRenderState>} */
211
195
  const visitors = {
212
196
  _: (node, context) => {
213
197
  context.state.code.addSourcemapLocation(node.start);
package/src/utils/ast.js CHANGED
@@ -1,22 +1,6 @@
1
1
  /** @import * as AST from 'estree' */
2
2
 
3
- /**
4
- * Represents the path of a destructured assignment from either a declaration
5
- * or assignment expression. For example, given `const { foo: { bar: baz } } = quux`,
6
- * the path of `baz` is `foo.bar`
7
- * @typedef {{
8
- * node: AST.Identifier | AST.MemberExpression;
9
- * is_rest: boolean;
10
- * has_default_value: boolean;
11
- * expression: (object: AST.Identifier | AST.CallExpression) => AST.Expression;
12
- * update_expression: (object: AST.Identifier) => AST.Expression;
13
- * }} DestructuredAssignment
14
- * - `node`: The node the destructuring path ends in. Can be a member expression only for assignment expressions
15
- * - `is_rest`: `true` if this is a `...rest` destructuring
16
- * - `has_default_value`: `true` if this has a fallback value like `const { foo = 'bar' } = ..`
17
- * - `expression`: The value of the current path. Will be a call expression if a rest element or default is involved — e.g. `const { foo: { bar: baz = 42 }, ...rest } = quux` — since we can't represent `baz` or `rest` purely as a path. Will be an await expression in case of an async default value (`const { foo = await bar } = ...`)
18
- * - `update_expression`: Like `expression` but without default values.
19
- */
3
+ /** @import { DestructuredAssignment } from '../../types/index' */
20
4
 
21
5
  import * as b from './builders.js';
22
6
 
@@ -57,6 +41,19 @@ export function child_nodes(node, skip_key) {
57
41
  return children;
58
42
  }
59
43
 
44
+ /**
45
+ * The children a node carries, as nodes. Node types differ in whether they
46
+ * have a `children` slot at all (`JSXCodeBlock` does not) and in what it may
47
+ * hold, so this reads it uniformly instead of forcing every caller to narrow.
48
+ *
49
+ * @param {AST.Node} node
50
+ * @returns {AST.Node[]}
51
+ */
52
+ export function node_children(node) {
53
+ const children = /** @type {AST.TraversableAstNode} */ (node).children;
54
+ return Array.isArray(children) ? children.filter(is_ast_node) : [];
55
+ }
56
+
60
57
  /**
61
58
  * @template {object} T
62
59
  * @param {T | null | undefined} node
@@ -68,6 +65,14 @@ export function has_location(node) {
68
65
  return location.loc != null && location.start !== undefined && location.end !== undefined;
69
66
  }
70
67
 
68
+ /**
69
+ * @param {AST.Node | null | undefined} node
70
+ * @returns {node is AST.JSXStyleElement}
71
+ */
72
+ export function is_style_element(node) {
73
+ return !!node && node.type === 'JSXStyleElement';
74
+ }
75
+
71
76
  /**
72
77
  * @param {AST.Node} node
73
78
  * @returns {node is AST.Function}
@@ -90,7 +95,7 @@ export function is_function_or_class_node(node) {
90
95
 
91
96
  /**
92
97
  * @param {AST.Node} node
93
- * @returns {boolean}
98
+ * @returns {node is AST.Function}
94
99
  */
95
100
  export function is_function_or_component_node(node) {
96
101
  return is_function_node(node);
@@ -199,8 +204,8 @@ export function get_component_from_path(path, includes_functions = false) {
199
204
  const node = path[i];
200
205
 
201
206
  if (is_function_node(node)) {
202
- if (/** @type {any} */ (node).metadata?.native_tsrx_function) {
203
- return /** @type {AST.Function} */ (node);
207
+ if (node.metadata?.native_tsrx_function) {
208
+ return node;
204
209
  }
205
210
  if (!includes_functions) {
206
211
  return;
@@ -604,6 +604,47 @@ export function ts_type_literal(members, loc_info) {
604
604
  return set_location(node, loc_info);
605
605
  }
606
606
 
607
+ /**
608
+ * `<left>.<right>` — the qualified entity name of a type reference or of an
609
+ * import-equals module reference.
610
+ *
611
+ * @param {AST.EntityName} left
612
+ * @param {AST.Identifier} right
613
+ * @param {AST.NodeWithLocation} [loc_info]
614
+ * @returns {AST.TSQualifiedName}
615
+ */
616
+ export function ts_qualified_name(left, right, loc_info) {
617
+ const node = /** @type {AST.TSQualifiedName} */ ({
618
+ type: 'TSQualifiedName',
619
+ left,
620
+ right,
621
+ metadata: { path: [] },
622
+ });
623
+
624
+ return set_location(node, loc_info);
625
+ }
626
+
627
+ /**
628
+ * `import <id> = <module_reference>;` — the alias form that keeps every meaning
629
+ * (value, type, namespace) of the referenced binding.
630
+ *
631
+ * @param {AST.Identifier} id
632
+ * @param {AST.EntityName | AST.TSExternalModuleReference} module_reference
633
+ * @param {AST.NodeWithLocation} [loc_info]
634
+ * @returns {AST.TSStatement<AST.TSImportEqualsDeclaration>}
635
+ */
636
+ export function ts_import_equals(id, module_reference, loc_info) {
637
+ const node = /** @type {AST.TSStatement<AST.TSImportEqualsDeclaration>} */ ({
638
+ type: 'TSImportEqualsDeclaration',
639
+ id,
640
+ moduleReference: module_reference,
641
+ importKind: 'value',
642
+ metadata: { path: [] },
643
+ });
644
+
645
+ return set_location(node, loc_info);
646
+ }
647
+
607
648
  /**
608
649
  * `@types/estree`'s `Statement` union has no TS declarations in it, so the
609
650
  * result is typed as both: a `type X = …` alias occupies a statement slot
@@ -612,10 +653,10 @@ export function ts_type_literal(members, loc_info) {
612
653
  * @param {AST.Identifier} id
613
654
  * @param {AST.Node} type_annotation
614
655
  * @param {AST.NodeWithLocation} [loc_info]
615
- * @returns {AST.TSTypeAliasDeclaration & AST.Statement}
656
+ * @returns {AST.TSStatement<AST.TSTypeAliasDeclaration>}
616
657
  */
617
658
  export function ts_type_alias(id, type_annotation, loc_info) {
618
- const node = /** @type {AST.TSTypeAliasDeclaration & AST.Statement} */ ({
659
+ const node = /** @type {AST.TSStatement<AST.TSTypeAliasDeclaration>} */ ({
619
660
  type: 'TSTypeAliasDeclaration',
620
661
  id,
621
662
  typeParameters: undefined,
@@ -685,6 +726,29 @@ export function prop(kind, key, value, computed = false, shorthand = false) {
685
726
  };
686
727
  }
687
728
 
729
+ /**
730
+ * A property of an object *pattern* — its value is a binding target, not an
731
+ * expression, so it cannot be built with {@link prop}.
732
+ *
733
+ * @param {AST.Expression} key
734
+ * @param {AST.Pattern} value
735
+ * @param {boolean} computed
736
+ * @param {boolean} shorthand
737
+ * @returns {AST.AssignmentProperty}
738
+ */
739
+ export function assignment_prop(key, value, computed = false, shorthand = false) {
740
+ return {
741
+ type: 'Property',
742
+ kind: 'init',
743
+ key,
744
+ value,
745
+ method: false,
746
+ shorthand,
747
+ computed,
748
+ metadata: { path: [] },
749
+ };
750
+ }
751
+
688
752
  /**
689
753
  * @param {AST.Expression | AST.PrivateIdentifier} key
690
754
  * @param {AST.Expression | null | undefined} value
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Rolldown plugins that make `.tsrx` modules visible to Vite's dependency
3
+ * scanner.
4
+ *
5
+ * The scanner runs through Rolldown without the main plugin pipeline, so on its
6
+ * own it cannot read `.tsrx` modules. Any npm dependency imported only from
7
+ * `.tsrx` files is then discovered at request time rather than at startup,
8
+ * which forces a re-optimize and a full page reload. Registering one of these
9
+ * under `optimizeDeps.rolldownOptions.plugins` teaches the scan pass to compile
10
+ * `.tsrx` modules so their imports are crawled up front.
11
+ *
12
+ * Which factory to use depends on how the host plugin exposes `.tsrx` modules:
13
+ *
14
+ * - {@link createDepScanTransformPlugin} for plugins that transform `.tsrx`
15
+ * ids directly. Those plugins must also list the extension in
16
+ * `optimizeDeps.extensions`, because the scanner externalizes anything that
17
+ * is not a known JS type before a plugin gets a chance to run.
18
+ * - {@link createDepScanLoadPlugin} for plugins that rewrite `.tsrx` ids to a
19
+ * virtual `<path>.tsx` form. The scanner accepts those on extension alone, so
20
+ * no `optimizeDeps.extensions` entry is needed, but it never calls Vite
21
+ * `load()` hooks for virtual ids.
22
+ *
23
+ * Both swallow compile failures. Vite responds to a scan error by skipping
24
+ * pre-bundling for the whole project, so a single malformed file would
25
+ * otherwise cost every other dependency its pre-bundle. Handing back an empty
26
+ * module keeps the rest of the graph crawlable and leaves the error to the host
27
+ * plugin's own transform, which reports it at request time where it can be
28
+ * surfaced properly.
29
+ *
30
+ * @import { DepScanCompile, DepScanLoadPlugin, DepScanTransformPlugin } from '../../types/vite/dep-scan.js'
31
+ */
32
+
33
+ import { readFile } from 'node:fs/promises';
34
+
35
+ /**
36
+ * Render `imports` as a side-effect import prelude. Used for runtime modules
37
+ * that the host plugin's own output depends on but that the scan pass would
38
+ * otherwise miss — a JSX runtime, for instance, which the scanner's own JSX
39
+ * transform may resolve to a different specifier than the host plugin does.
40
+ *
41
+ * @param {string[] | undefined} imports
42
+ * @returns {string}
43
+ */
44
+ function render_prelude(imports) {
45
+ if (imports === undefined || imports.length === 0) return '';
46
+
47
+ return imports.map((source) => `import ${JSON.stringify(source)};`).join('\n') + '\n';
48
+ }
49
+
50
+ /**
51
+ * Scan plugin for host plugins that transform `.tsrx` ids directly.
52
+ *
53
+ * @param {{
54
+ * name: string,
55
+ * filter: RegExp,
56
+ * compile: DepScanCompile,
57
+ * imports?: string[],
58
+ * moduleType?: string,
59
+ * }} options
60
+ * @returns {DepScanTransformPlugin}
61
+ */
62
+ export function createDepScanTransformPlugin({
63
+ name,
64
+ filter,
65
+ compile,
66
+ imports,
67
+ moduleType = 'tsx',
68
+ }) {
69
+ const prelude = render_prelude(imports);
70
+
71
+ return {
72
+ name,
73
+ transform: {
74
+ filter: { id: filter },
75
+ async handler(/** @type {string} */ code, /** @type {string} */ id) {
76
+ try {
77
+ const { code: compiled } = await compile(code, id);
78
+ return { code: prelude + compiled, moduleType };
79
+ } catch {
80
+ return { code: '', moduleType };
81
+ }
82
+ },
83
+ },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Scan plugin for host plugins that rewrite `.tsrx` ids to a virtual
89
+ * `<path>.tsx` form, which the scanner resolves but never loads.
90
+ *
91
+ * @param {{
92
+ * name: string,
93
+ * isVirtual: (id: string) => boolean,
94
+ * toRealPath: (id: string) => string,
95
+ * compile: DepScanCompile,
96
+ * imports?: string[],
97
+ * moduleType?: string,
98
+ * }} options
99
+ * @returns {DepScanLoadPlugin}
100
+ */
101
+ export function createDepScanLoadPlugin({
102
+ name,
103
+ isVirtual,
104
+ toRealPath,
105
+ compile,
106
+ imports,
107
+ moduleType = 'tsx',
108
+ }) {
109
+ const prelude = render_prelude(imports);
110
+
111
+ return {
112
+ name,
113
+ async load(/** @type {string} */ id) {
114
+ // Both callbacks are written against plain paths, so drop any query
115
+ // suffix once here rather than leaving each caller to remember.
116
+ const path = id.split('?')[0];
117
+
118
+ if (!isVirtual(path)) return null;
119
+
120
+ const real_path = toRealPath(path);
121
+
122
+ try {
123
+ // The read is inside the try along with the compile: a virtual id
124
+ // pointing at a file that has since moved is just as survivable as
125
+ // one that fails to parse, and the host plugin's own `load()` will
126
+ // report either at request time.
127
+ const source = await readFile(real_path, 'utf-8');
128
+ const { code } = await compile(source, real_path);
129
+ return { code: prelude + code, moduleType };
130
+ } catch {
131
+ return { code: '', moduleType };
132
+ }
133
+ },
134
+ };
135
+ }