@tsrx/core 0.0.4 → 0.0.5
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 +1 -1
- package/src/index.js +23 -1
- package/src/parse/index.js +64 -1
- package/src/parse/style.js +1 -1
- package/src/plugin.js +4 -3
- package/src/source-map-utils.js +1 -1
- package/src/transform/lazy.js +664 -0
- package/src/transform/scoping.js +180 -0
- package/src/transform/segments.js +218 -52
- package/types/index.d.ts +1 -1
- package/types/parse.d.ts +6 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -132,7 +132,29 @@ export { escape } from './utils/escaping.js';
|
|
|
132
132
|
|
|
133
133
|
// Transform
|
|
134
134
|
export { render_stylesheets as renderStylesheets } from './transform/stylesheet.js';
|
|
135
|
-
export {
|
|
135
|
+
export {
|
|
136
|
+
prepare_stylesheet_for_render as prepareStylesheetForRender,
|
|
137
|
+
is_style_element as isStyleElement,
|
|
138
|
+
is_composite_element as isCompositeElement,
|
|
139
|
+
annotate_with_hash as annotateWithHash,
|
|
140
|
+
annotate_component_with_hash as annotateComponentWithHash,
|
|
141
|
+
add_hash_class as addHashClass,
|
|
142
|
+
} from './transform/scoping.js';
|
|
143
|
+
export {
|
|
144
|
+
convert_source_map_to_mappings as convertSourceMapToMappings,
|
|
145
|
+
create_volar_mappings_result as createVolarMappingsResult,
|
|
146
|
+
dedupe_mappings as dedupeMappings,
|
|
147
|
+
serialize_mapping_value as serializeMappingValue,
|
|
148
|
+
} from './transform/segments.js';
|
|
149
|
+
export {
|
|
150
|
+
create_lazy_context as createLazyContext,
|
|
151
|
+
collect_lazy_bindings as collectLazyBindings,
|
|
152
|
+
collect_lazy_bindings_from_component as collectLazyBindingsFromComponent,
|
|
153
|
+
collect_lazy_bindings_from_statements as collectLazyBindingsFromStatements,
|
|
154
|
+
preallocate_lazy_ids as preallocateLazyIds,
|
|
155
|
+
apply_lazy_transforms as applyLazyTransforms,
|
|
156
|
+
replace_lazy_params as replaceLazyParams,
|
|
157
|
+
} from './transform/lazy.js';
|
|
136
158
|
|
|
137
159
|
// Analyze
|
|
138
160
|
export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
|
package/src/parse/index.js
CHANGED
|
@@ -110,6 +110,68 @@ export function isWhitespaceTextNode(node) {
|
|
|
110
110
|
return false;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* @type {AcornPlugin}
|
|
115
|
+
*/
|
|
116
|
+
function elementTemplateClosingTagPlugin(Base) {
|
|
117
|
+
const jsxTagStart = /** @type {any} */ (Base).acornTypeScript?.tokTypes?.jsxTagStart;
|
|
118
|
+
if (!jsxTagStart) return Base;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {any} parser
|
|
122
|
+
*/
|
|
123
|
+
function inElementTemplateBodyDirect(parser) {
|
|
124
|
+
const stack = parser.context;
|
|
125
|
+
const top = stack[stack.length - 1];
|
|
126
|
+
const below = stack[stack.length - 2];
|
|
127
|
+
return top && top.token === '{' && below && below.token === '<tag>...</tag>';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {any} parser
|
|
132
|
+
*/
|
|
133
|
+
function inElementTemplateBodyAnywhere(parser) {
|
|
134
|
+
const stack = parser.context;
|
|
135
|
+
for (let i = 1; i < stack.length; i++) {
|
|
136
|
+
if (
|
|
137
|
+
stack[i] &&
|
|
138
|
+
stack[i].token === '{' &&
|
|
139
|
+
stack[i - 1] &&
|
|
140
|
+
stack[i - 1].token === '<tag>...</tag>'
|
|
141
|
+
) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return class extends Base {
|
|
149
|
+
/** @param {number} code */
|
|
150
|
+
// @ts-ignore — extending acorn's Parser with internal hooks
|
|
151
|
+
getTokenFromCode(code) {
|
|
152
|
+
if (code === 60 /* '<' */ && !(/** @type {any} */ (this).inType)) {
|
|
153
|
+
const self = /** @type {any} */ (this);
|
|
154
|
+
const nextChar =
|
|
155
|
+
self.pos + 1 < self.input.length ? self.input.charCodeAt(self.pos + 1) : -1;
|
|
156
|
+
if (nextChar === 47 /* '/' */ && inElementTemplateBodyDirect(self)) {
|
|
157
|
+
++self.pos;
|
|
158
|
+
return self.finishToken(jsxTagStart);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// @ts-ignore — super dispatches to next layer in the plugin chain
|
|
162
|
+
return super.getTokenFromCode(code);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// @ts-ignore — extending acorn's Parser with internal hooks
|
|
166
|
+
canInsertSemicolon() {
|
|
167
|
+
const self = /** @type {any} */ (this);
|
|
168
|
+
if (self.type === jsxTagStart && inElementTemplateBodyAnywhere(self)) return true;
|
|
169
|
+
// @ts-ignore
|
|
170
|
+
return super.canInsertSemicolon();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
113
175
|
/**
|
|
114
176
|
* Create a parser by composing Acorn with TypeScript/JSX support and optional framework plugins.
|
|
115
177
|
*
|
|
@@ -125,6 +187,7 @@ export function createParser(...plugins) {
|
|
|
125
187
|
acorn.Parser.extend(
|
|
126
188
|
tsPlugin({ jsx: true }),
|
|
127
189
|
...plugins.map((p) => /** @type {AcornPlugin} */ (/** @type {unknown} */ (p))),
|
|
190
|
+
elementTemplateClosingTagPlugin,
|
|
128
191
|
)
|
|
129
192
|
)
|
|
130
193
|
);
|
|
@@ -151,7 +214,7 @@ export function createParser(...plugins) {
|
|
|
151
214
|
allowReturnOutsideFunction: true,
|
|
152
215
|
locations: true,
|
|
153
216
|
onComment,
|
|
154
|
-
|
|
217
|
+
tsrxOptions: {
|
|
155
218
|
filename,
|
|
156
219
|
errors: options?.errors ?? [],
|
|
157
220
|
loose: options?.loose || false,
|
package/src/parse/style.js
CHANGED
package/src/plugin.js
CHANGED
|
@@ -51,9 +51,10 @@ export function TSRXPlugin(config) {
|
|
|
51
51
|
*/
|
|
52
52
|
constructor(options, input) {
|
|
53
53
|
super(options, input);
|
|
54
|
-
|
|
55
|
-
this.#
|
|
56
|
-
this.#
|
|
54
|
+
const tsrx_options = options?.tsrxOptions ?? options?.rippleOptions;
|
|
55
|
+
this.#loose = tsrx_options?.loose === true;
|
|
56
|
+
this.#errors = tsrx_options?.errors;
|
|
57
|
+
this.#filename = tsrx_options?.filename || null;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
package/src/source-map-utils.js
CHANGED
|
@@ -286,7 +286,7 @@ export function build_line_offsets(text) {
|
|
|
286
286
|
* @param {number} [gen_max_len]
|
|
287
287
|
* @returns {CodeMapping | Error}
|
|
288
288
|
*/
|
|
289
|
-
function maybe_get_mapping_from_node(
|
|
289
|
+
export function maybe_get_mapping_from_node(
|
|
290
290
|
node,
|
|
291
291
|
src_to_gen_map,
|
|
292
292
|
gen_line_offsets,
|
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lazy destructuring transform — framework-agnostic.
|
|
5
|
+
*
|
|
6
|
+
* Shared between `@tsrx/react` and `@tsrx/solid`. Walks an AST and rewrites
|
|
7
|
+
* references to names introduced by `&{ ... }` / `&[ ... ]` destructuring
|
|
8
|
+
* patterns into member-expression accesses on a generated source identifier.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* 1. Create a context with `createLazyContext()` (or provide any object with
|
|
12
|
+
* a `lazy_next_id: number` field).
|
|
13
|
+
* 2. Run `preallocateLazyIds(root, context)` once over the full program to
|
|
14
|
+
* assign stable `metadata.lazy_id` values to every lazy pattern.
|
|
15
|
+
* 3. For each function/component scope, collect bindings with
|
|
16
|
+
* `collectLazyBindingsFromComponent(params, body, context)` and pass the
|
|
17
|
+
* resulting map into `applyLazyTransforms(body, map)`.
|
|
18
|
+
* 4. If a component declares lazy params, pass its params through
|
|
19
|
+
* `replaceLazyParams(params)` before emitting.
|
|
20
|
+
*
|
|
21
|
+
* The transform is purely AST-to-AST and has no framework-specific knowledge.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {{ lazy_next_id: number }} LazyContext
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {{ source_name: string, read: () => any }} LazyBinding
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a fresh lazy-id allocation context.
|
|
34
|
+
*
|
|
35
|
+
* @returns {LazyContext}
|
|
36
|
+
*/
|
|
37
|
+
export function create_lazy_context() {
|
|
38
|
+
return { lazy_next_id: 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {LazyContext} context
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
function generate_lazy_id(context) {
|
|
46
|
+
return `__lazy${context.lazy_next_id++}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} name
|
|
51
|
+
* @returns {any}
|
|
52
|
+
*/
|
|
53
|
+
function create_generated_identifier(name) {
|
|
54
|
+
return /** @type {any} */ ({ type: 'Identifier', name, metadata: { path: [] } });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collect lazy bindings from a destructuring pattern.
|
|
59
|
+
*
|
|
60
|
+
* For `&{ name, age }` on source `S`, maps `name` → `S.name`, `age` → `S.age`.
|
|
61
|
+
* For `&[a, b]` on source `S`, maps `a` → `S[0]`, `b` → `S[1]`. Handles nested
|
|
62
|
+
* `AssignmentPattern` (default values); skips `RestElement`.
|
|
63
|
+
*
|
|
64
|
+
* @param {any} pattern
|
|
65
|
+
* @param {string} source_name
|
|
66
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
67
|
+
*/
|
|
68
|
+
export function collect_lazy_bindings(pattern, source_name, lazy_bindings) {
|
|
69
|
+
if (pattern.type === 'ObjectPattern') {
|
|
70
|
+
for (const prop of pattern.properties || []) {
|
|
71
|
+
if (prop.type === 'RestElement') continue;
|
|
72
|
+
const value = prop.value;
|
|
73
|
+
const actual = value.type === 'AssignmentPattern' ? value.left : value;
|
|
74
|
+
if (actual.type === 'Identifier') {
|
|
75
|
+
const key = prop.key;
|
|
76
|
+
const computed = prop.computed || key.type !== 'Identifier';
|
|
77
|
+
lazy_bindings.set(actual.name, {
|
|
78
|
+
source_name,
|
|
79
|
+
read: () => ({
|
|
80
|
+
type: 'MemberExpression',
|
|
81
|
+
object: create_generated_identifier(source_name),
|
|
82
|
+
property: computed
|
|
83
|
+
? { ...key }
|
|
84
|
+
: { type: 'Identifier', name: key.name, metadata: { path: [] } },
|
|
85
|
+
computed,
|
|
86
|
+
optional: false,
|
|
87
|
+
metadata: { path: [] },
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else if (pattern.type === 'ArrayPattern') {
|
|
93
|
+
for (let i = 0; i < (pattern.elements || []).length; i++) {
|
|
94
|
+
const element = pattern.elements[i];
|
|
95
|
+
if (!element) continue;
|
|
96
|
+
if (element.type === 'RestElement') continue;
|
|
97
|
+
const actual = element.type === 'AssignmentPattern' ? element.left : element;
|
|
98
|
+
if (actual.type === 'Identifier') {
|
|
99
|
+
const index = i;
|
|
100
|
+
lazy_bindings.set(actual.name, {
|
|
101
|
+
source_name,
|
|
102
|
+
read: () => ({
|
|
103
|
+
type: 'MemberExpression',
|
|
104
|
+
object: create_generated_identifier(source_name),
|
|
105
|
+
property: { type: 'Literal', value: index, raw: String(index), metadata: { path: [] } },
|
|
106
|
+
computed: true,
|
|
107
|
+
optional: false,
|
|
108
|
+
metadata: { path: [] },
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Collect lazy bindings from a component's params and top-level body declarations.
|
|
118
|
+
* Mutates each lazy pattern's `metadata.lazy_id` in place (idempotent if already set).
|
|
119
|
+
*
|
|
120
|
+
* @param {any[]} params
|
|
121
|
+
* @param {any[]} body
|
|
122
|
+
* @param {LazyContext} context
|
|
123
|
+
* @returns {Map<string, LazyBinding>}
|
|
124
|
+
*/
|
|
125
|
+
export function collect_lazy_bindings_from_component(params, body, context) {
|
|
126
|
+
/** @type {Map<string, LazyBinding>} */
|
|
127
|
+
const lazy_bindings = new Map();
|
|
128
|
+
|
|
129
|
+
for (const param of params) {
|
|
130
|
+
const pattern = param.type === 'AssignmentPattern' ? param.left : param;
|
|
131
|
+
if ((pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') && pattern.lazy) {
|
|
132
|
+
const lazy_name = pattern.metadata?.lazy_id || generate_lazy_id(context);
|
|
133
|
+
if (!pattern.metadata?.lazy_id) {
|
|
134
|
+
pattern.metadata = { ...pattern.metadata, lazy_id: lazy_name };
|
|
135
|
+
}
|
|
136
|
+
collect_lazy_bindings(pattern, lazy_name, lazy_bindings);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// VariableDeclaration lazy patterns already have their `lazy_id` assigned
|
|
141
|
+
// by `preallocate_lazy_ids` (run once over the whole program by the target
|
|
142
|
+
// transforms), so `collect_lazy_bindings_from_statements` handles them
|
|
143
|
+
// alongside the expression-statement assignment form.
|
|
144
|
+
collect_lazy_bindings_from_statements(body, lazy_bindings);
|
|
145
|
+
|
|
146
|
+
return lazy_bindings;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Collect lazy bindings from statements at the top level of a block. Reads
|
|
151
|
+
* already-allocated `lazy_id` values from pattern metadata. Handles both
|
|
152
|
+
* `let &[x] = ...` variable declarations and statement-level `&[x] = expr;`
|
|
153
|
+
* assignment expressions.
|
|
154
|
+
*
|
|
155
|
+
* @param {any[]} statements
|
|
156
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
157
|
+
*/
|
|
158
|
+
export function collect_lazy_bindings_from_statements(statements, lazy_bindings) {
|
|
159
|
+
for (const stmt of statements || []) {
|
|
160
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
161
|
+
for (const declarator of stmt.declarations || []) {
|
|
162
|
+
const pattern = declarator.id;
|
|
163
|
+
if (
|
|
164
|
+
(pattern?.type === 'ObjectPattern' || pattern?.type === 'ArrayPattern') &&
|
|
165
|
+
pattern.lazy &&
|
|
166
|
+
pattern.metadata?.lazy_id &&
|
|
167
|
+
!lazy_bindings_contains(lazy_bindings, pattern)
|
|
168
|
+
) {
|
|
169
|
+
collect_lazy_bindings(pattern, pattern.metadata.lazy_id, lazy_bindings);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else if (
|
|
173
|
+
stmt.type === 'ExpressionStatement' &&
|
|
174
|
+
stmt.expression?.type === 'AssignmentExpression' &&
|
|
175
|
+
stmt.expression.operator === '=' &&
|
|
176
|
+
(stmt.expression.left?.type === 'ObjectPattern' ||
|
|
177
|
+
stmt.expression.left?.type === 'ArrayPattern') &&
|
|
178
|
+
stmt.expression.left.lazy &&
|
|
179
|
+
stmt.expression.left.metadata?.lazy_id
|
|
180
|
+
) {
|
|
181
|
+
collect_lazy_bindings(
|
|
182
|
+
stmt.expression.left,
|
|
183
|
+
stmt.expression.left.metadata.lazy_id,
|
|
184
|
+
lazy_bindings,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
192
|
+
* @param {any} pattern
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
function lazy_bindings_contains(lazy_bindings, pattern) {
|
|
196
|
+
if (pattern.type === 'ObjectPattern') {
|
|
197
|
+
for (const prop of pattern.properties || []) {
|
|
198
|
+
if (prop.type === 'RestElement') continue;
|
|
199
|
+
const value = prop.value;
|
|
200
|
+
const actual = value?.type === 'AssignmentPattern' ? value.left : value;
|
|
201
|
+
if (actual?.type === 'Identifier' && lazy_bindings.has(actual.name)) return true;
|
|
202
|
+
}
|
|
203
|
+
} else if (pattern.type === 'ArrayPattern') {
|
|
204
|
+
for (const element of pattern.elements || []) {
|
|
205
|
+
if (!element || element.type === 'RestElement') continue;
|
|
206
|
+
const actual = element.type === 'AssignmentPattern' ? element.left : element;
|
|
207
|
+
if (actual?.type === 'Identifier' && lazy_bindings.has(actual.name)) return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Walk the AST and pre-allocate `lazy_id` metadata on every lazy destructuring
|
|
215
|
+
* pattern: function/component params, variable declarator ids, and statement-level
|
|
216
|
+
* assignment LHS. Idempotent: skips patterns that already have a `lazy_id`.
|
|
217
|
+
*
|
|
218
|
+
* @param {any} root
|
|
219
|
+
* @param {LazyContext} context
|
|
220
|
+
*/
|
|
221
|
+
export function preallocate_lazy_ids(root, context) {
|
|
222
|
+
/** @param {any} pattern */
|
|
223
|
+
const assign_id = (pattern) => {
|
|
224
|
+
if (
|
|
225
|
+
(pattern?.type === 'ObjectPattern' || pattern?.type === 'ArrayPattern') &&
|
|
226
|
+
pattern.lazy &&
|
|
227
|
+
!pattern.metadata?.lazy_id
|
|
228
|
+
) {
|
|
229
|
+
pattern.metadata = {
|
|
230
|
+
...pattern.metadata,
|
|
231
|
+
lazy_id: generate_lazy_id(context),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** @param {any} node */
|
|
237
|
+
const visit = (node) => {
|
|
238
|
+
if (!node || typeof node !== 'object') return;
|
|
239
|
+
if (Array.isArray(node)) {
|
|
240
|
+
for (const child of node) visit(child);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (
|
|
245
|
+
node.type === 'FunctionDeclaration' ||
|
|
246
|
+
node.type === 'FunctionExpression' ||
|
|
247
|
+
node.type === 'ArrowFunctionExpression' ||
|
|
248
|
+
node.type === 'Component'
|
|
249
|
+
) {
|
|
250
|
+
for (const param of node.params || []) {
|
|
251
|
+
assign_id(param?.type === 'AssignmentPattern' ? param.left : param);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (node.type === 'VariableDeclarator') {
|
|
256
|
+
assign_id(node.id);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
node.type === 'ExpressionStatement' &&
|
|
261
|
+
node.expression?.type === 'AssignmentExpression' &&
|
|
262
|
+
node.expression.operator === '='
|
|
263
|
+
) {
|
|
264
|
+
assign_id(node.expression.left);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const key of Object.keys(node)) {
|
|
268
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
269
|
+
visit(node[key]);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
visit(root);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Recursively rewrite lazy-binding references in `node`.
|
|
278
|
+
*
|
|
279
|
+
* @param {any} node
|
|
280
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
281
|
+
* @returns {any}
|
|
282
|
+
*/
|
|
283
|
+
export function apply_lazy_transforms(node, lazy_bindings) {
|
|
284
|
+
if (!node || typeof node !== 'object') return node;
|
|
285
|
+
if (Array.isArray(node)) return node.map((child) => apply_lazy_transforms(child, lazy_bindings));
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
node.type === 'FunctionDeclaration' ||
|
|
289
|
+
node.type === 'FunctionExpression' ||
|
|
290
|
+
node.type === 'ArrowFunctionExpression'
|
|
291
|
+
) {
|
|
292
|
+
// Default parameter values are evaluated in the outer scope — transform them first.
|
|
293
|
+
let params_changed = false;
|
|
294
|
+
const new_params = (node.params || []).map((/** @type {any} */ param) => {
|
|
295
|
+
const transformed = transform_param_defaults(param, lazy_bindings);
|
|
296
|
+
if (transformed !== param) params_changed = true;
|
|
297
|
+
return transformed;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/** @type {Set<string>} */
|
|
301
|
+
const shadowed = new Set();
|
|
302
|
+
for (const param of node.params || []) {
|
|
303
|
+
collect_shadowed_names(param, lazy_bindings, shadowed);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const outer_minus_shadow =
|
|
307
|
+
shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
|
|
308
|
+
|
|
309
|
+
/** @type {Map<string, LazyBinding>} */
|
|
310
|
+
const own_bindings = new Map();
|
|
311
|
+
let had_lazy_param = false;
|
|
312
|
+
for (const param of node.params || []) {
|
|
313
|
+
const pattern = param?.type === 'AssignmentPattern' ? param.left : param;
|
|
314
|
+
if (
|
|
315
|
+
(pattern?.type === 'ObjectPattern' || pattern?.type === 'ArrayPattern') &&
|
|
316
|
+
pattern.lazy &&
|
|
317
|
+
pattern.metadata?.lazy_id
|
|
318
|
+
) {
|
|
319
|
+
had_lazy_param = true;
|
|
320
|
+
collect_lazy_bindings(pattern, pattern.metadata.lazy_id, own_bindings);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Own bindings override any outer binding with the same name.
|
|
325
|
+
const inner_bindings =
|
|
326
|
+
own_bindings.size > 0
|
|
327
|
+
? new Map([...outer_minus_shadow, ...own_bindings])
|
|
328
|
+
: outer_minus_shadow;
|
|
329
|
+
|
|
330
|
+
if (inner_bindings.size === 0 && !params_changed && !had_lazy_param) return node;
|
|
331
|
+
|
|
332
|
+
const new_body =
|
|
333
|
+
inner_bindings.size > 0 ? apply_lazy_transforms(node.body, inner_bindings) : node.body;
|
|
334
|
+
|
|
335
|
+
const final_params_src = params_changed ? new_params : node.params;
|
|
336
|
+
const final_params = had_lazy_param ? replace_lazy_params(final_params_src) : final_params_src;
|
|
337
|
+
|
|
338
|
+
if (new_body !== node.body || final_params !== node.params) {
|
|
339
|
+
return { ...node, params: final_params, body: new_body };
|
|
340
|
+
}
|
|
341
|
+
return node;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (node.type === 'BlockStatement' || node.type === 'Program') {
|
|
345
|
+
const block_bindings = collect_block_shadowed_names(node.body, lazy_bindings);
|
|
346
|
+
const after_shadow =
|
|
347
|
+
block_bindings.size > 0 ? remove_shadowed(lazy_bindings, block_bindings) : lazy_bindings;
|
|
348
|
+
|
|
349
|
+
/** @type {Map<string, LazyBinding>} */
|
|
350
|
+
const block_lazy = new Map();
|
|
351
|
+
collect_lazy_bindings_from_statements(node.body, block_lazy);
|
|
352
|
+
|
|
353
|
+
const effective_bindings =
|
|
354
|
+
block_lazy.size > 0 ? new Map([...after_shadow, ...block_lazy]) : after_shadow;
|
|
355
|
+
|
|
356
|
+
let changed = false;
|
|
357
|
+
const new_body = node.body.map((/** @type {any} */ stmt) => {
|
|
358
|
+
const transformed = apply_lazy_transforms(stmt, effective_bindings);
|
|
359
|
+
if (transformed !== stmt) changed = true;
|
|
360
|
+
return transformed;
|
|
361
|
+
});
|
|
362
|
+
return changed ? { ...node, body: new_body } : node;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (node.type === 'CatchClause') {
|
|
366
|
+
/** @type {Set<string>} */
|
|
367
|
+
const shadowed = new Set();
|
|
368
|
+
if (node.param) collect_shadowed_names(node.param, lazy_bindings, shadowed);
|
|
369
|
+
const effective_bindings =
|
|
370
|
+
shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
|
|
371
|
+
const new_body = apply_lazy_transforms(node.body, effective_bindings);
|
|
372
|
+
if (new_body !== node.body) return { ...node, body: new_body };
|
|
373
|
+
return node;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (node.type === 'ForStatement') {
|
|
377
|
+
/** @type {Set<string>} */
|
|
378
|
+
const shadowed = new Set();
|
|
379
|
+
if (node.init?.type === 'VariableDeclaration') {
|
|
380
|
+
for (const decl of node.init.declarations) {
|
|
381
|
+
if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const effective_bindings =
|
|
385
|
+
shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
|
|
386
|
+
let changed = false;
|
|
387
|
+
const new_init = apply_lazy_transforms(node.init, effective_bindings);
|
|
388
|
+
if (new_init !== node.init) changed = true;
|
|
389
|
+
const new_test = apply_lazy_transforms(node.test, effective_bindings);
|
|
390
|
+
if (new_test !== node.test) changed = true;
|
|
391
|
+
const new_update = apply_lazy_transforms(node.update, effective_bindings);
|
|
392
|
+
if (new_update !== node.update) changed = true;
|
|
393
|
+
const new_body = apply_lazy_transforms(node.body, effective_bindings);
|
|
394
|
+
if (new_body !== node.body) changed = true;
|
|
395
|
+
return changed
|
|
396
|
+
? { ...node, init: new_init, test: new_test, update: new_update, body: new_body }
|
|
397
|
+
: node;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (node.type === 'ForOfStatement' || node.type === 'ForInStatement') {
|
|
401
|
+
/** @type {Set<string>} */
|
|
402
|
+
const shadowed = new Set();
|
|
403
|
+
if (node.left?.type === 'VariableDeclaration') {
|
|
404
|
+
for (const decl of node.left.declarations) {
|
|
405
|
+
if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const effective_bindings =
|
|
409
|
+
shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
|
|
410
|
+
// `node.left` is a binding site, not an expression context: a declaration
|
|
411
|
+
// like `const x` or `const [a, b]` has no outer references to rewrite,
|
|
412
|
+
// and recursing here would hit the VariableDeclarator handler and
|
|
413
|
+
// rewrite a lazy declarator id that `preallocate_lazy_ids` already
|
|
414
|
+
// tagged — double-processing the loop variable. Leave `node.left`
|
|
415
|
+
// untouched; the body and right-hand side are the only scopes with
|
|
416
|
+
// live references.
|
|
417
|
+
let changed = false;
|
|
418
|
+
// The right-hand side is evaluated in the outer scope (before the loop
|
|
419
|
+
// variable is bound), so use the unshadowed bindings there.
|
|
420
|
+
const new_right = apply_lazy_transforms(node.right, lazy_bindings);
|
|
421
|
+
if (new_right !== node.right) changed = true;
|
|
422
|
+
const new_body = apply_lazy_transforms(node.body, effective_bindings);
|
|
423
|
+
if (new_body !== node.body) changed = true;
|
|
424
|
+
return changed ? { ...node, right: new_right, body: new_body } : node;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (node.type === 'SwitchStatement') {
|
|
428
|
+
let changed = false;
|
|
429
|
+
const new_discriminant = apply_lazy_transforms(node.discriminant, lazy_bindings);
|
|
430
|
+
if (new_discriminant !== node.discriminant) changed = true;
|
|
431
|
+
const new_cases = node.cases.map((/** @type {any} */ switch_case) => {
|
|
432
|
+
const case_bindings = collect_block_shadowed_names(switch_case.consequent, lazy_bindings);
|
|
433
|
+
const effective_bindings =
|
|
434
|
+
case_bindings.size > 0 ? remove_shadowed(lazy_bindings, case_bindings) : lazy_bindings;
|
|
435
|
+
let case_changed = false;
|
|
436
|
+
const new_test = switch_case.test
|
|
437
|
+
? apply_lazy_transforms(switch_case.test, lazy_bindings)
|
|
438
|
+
: null;
|
|
439
|
+
if (new_test !== switch_case.test) case_changed = true;
|
|
440
|
+
const new_consequent = switch_case.consequent.map((/** @type {any} */ stmt) => {
|
|
441
|
+
const transformed = apply_lazy_transforms(stmt, effective_bindings);
|
|
442
|
+
if (transformed !== stmt) case_changed = true;
|
|
443
|
+
return transformed;
|
|
444
|
+
});
|
|
445
|
+
if (case_changed) {
|
|
446
|
+
changed = true;
|
|
447
|
+
return { ...switch_case, test: new_test, consequent: new_consequent };
|
|
448
|
+
}
|
|
449
|
+
return switch_case;
|
|
450
|
+
});
|
|
451
|
+
return changed ? { ...node, discriminant: new_discriminant, cases: new_cases } : node;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Standalone lazy destructuring assignment: `&[data] = track(0);` becomes
|
|
455
|
+
// `const __lazy0 = track(0);`. Individual name bindings are already in scope
|
|
456
|
+
// via the enclosing BlockStatement handler.
|
|
457
|
+
if (
|
|
458
|
+
node.type === 'ExpressionStatement' &&
|
|
459
|
+
node.expression?.type === 'AssignmentExpression' &&
|
|
460
|
+
node.expression.operator === '=' &&
|
|
461
|
+
(node.expression.left?.type === 'ObjectPattern' ||
|
|
462
|
+
node.expression.left?.type === 'ArrayPattern') &&
|
|
463
|
+
node.expression.left.lazy &&
|
|
464
|
+
node.expression.left.metadata?.lazy_id
|
|
465
|
+
) {
|
|
466
|
+
const pattern = node.expression.left;
|
|
467
|
+
const lazy_id = create_generated_identifier(pattern.metadata.lazy_id);
|
|
468
|
+
if (pattern.typeAnnotation) lazy_id.typeAnnotation = pattern.typeAnnotation;
|
|
469
|
+
const init = apply_lazy_transforms(node.expression.right, lazy_bindings);
|
|
470
|
+
return /** @type {any} */ ({
|
|
471
|
+
type: 'VariableDeclaration',
|
|
472
|
+
kind: 'const',
|
|
473
|
+
declarations: [
|
|
474
|
+
{
|
|
475
|
+
type: 'VariableDeclarator',
|
|
476
|
+
id: lazy_id,
|
|
477
|
+
init,
|
|
478
|
+
metadata: { path: [] },
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
metadata: { path: [] },
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// AssignmentExpression / UpdateExpression whose target is a lazy identifier.
|
|
486
|
+
if (
|
|
487
|
+
node.type === 'AssignmentExpression' &&
|
|
488
|
+
node.left?.type === 'Identifier' &&
|
|
489
|
+
lazy_bindings.has(node.left.name)
|
|
490
|
+
) {
|
|
491
|
+
const binding = /** @type {LazyBinding} */ (lazy_bindings.get(node.left.name));
|
|
492
|
+
return {
|
|
493
|
+
...node,
|
|
494
|
+
left: binding.read(),
|
|
495
|
+
right: apply_lazy_transforms(node.right, lazy_bindings),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (
|
|
500
|
+
node.type === 'UpdateExpression' &&
|
|
501
|
+
node.argument?.type === 'Identifier' &&
|
|
502
|
+
lazy_bindings.has(node.argument.name)
|
|
503
|
+
) {
|
|
504
|
+
const binding = /** @type {LazyBinding} */ (lazy_bindings.get(node.argument.name));
|
|
505
|
+
return { ...node, argument: binding.read() };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Replace lazy variable declaration patterns with generated identifiers.
|
|
509
|
+
if (node.type === 'VariableDeclarator' && node.id?.metadata?.lazy_id) {
|
|
510
|
+
const lazy_id = create_generated_identifier(node.id.metadata.lazy_id);
|
|
511
|
+
if (node.id.typeAnnotation) lazy_id.typeAnnotation = node.id.typeAnnotation;
|
|
512
|
+
return {
|
|
513
|
+
...node,
|
|
514
|
+
id: lazy_id,
|
|
515
|
+
init: apply_lazy_transforms(node.init, lazy_bindings),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Shorthand object properties `{ name }` → `{ name: __lazy0.name }`.
|
|
520
|
+
if (node.type === 'Property' && node.shorthand && node.value?.type === 'Identifier') {
|
|
521
|
+
const binding = lazy_bindings.get(node.value.name);
|
|
522
|
+
if (binding) {
|
|
523
|
+
return { ...node, shorthand: false, value: binding.read() };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Bare identifier reference.
|
|
528
|
+
if (node.type === 'Identifier' && lazy_bindings.has(node.name)) {
|
|
529
|
+
const binding = /** @type {LazyBinding} */ (lazy_bindings.get(node.name));
|
|
530
|
+
return binding.read();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// JSXIdentifier is a label (component/element name), never a reference.
|
|
534
|
+
if (node.type === 'JSXIdentifier') return node;
|
|
535
|
+
|
|
536
|
+
let changed = false;
|
|
537
|
+
/** @type {any} */
|
|
538
|
+
const clone = { ...node };
|
|
539
|
+
for (const key of Object.keys(node)) {
|
|
540
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
541
|
+
|
|
542
|
+
// Skip non-computed, non-shorthand property keys (they are labels).
|
|
543
|
+
if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) continue;
|
|
544
|
+
// Skip non-computed member expression property access.
|
|
545
|
+
if (key === 'property' && node.type === 'MemberExpression' && !node.computed) continue;
|
|
546
|
+
// Skip JSXMemberExpression property (label, not reference).
|
|
547
|
+
if (key === 'property' && node.type === 'JSXMemberExpression') continue;
|
|
548
|
+
// Skip JSXAttribute names (labels).
|
|
549
|
+
if (key === 'name' && node.type === 'JSXAttribute') continue;
|
|
550
|
+
// Skip VariableDeclarator id (already handled above).
|
|
551
|
+
if (key === 'id' && node.type === 'VariableDeclarator') continue;
|
|
552
|
+
|
|
553
|
+
const new_value = apply_lazy_transforms(node[key], lazy_bindings);
|
|
554
|
+
if (new_value !== node[key]) {
|
|
555
|
+
clone[key] = new_value;
|
|
556
|
+
changed = true;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return changed ? clone : node;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* @param {any} param
|
|
564
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
565
|
+
*/
|
|
566
|
+
function transform_param_defaults(param, lazy_bindings) {
|
|
567
|
+
if (param?.type === 'AssignmentPattern') {
|
|
568
|
+
const new_right = apply_lazy_transforms(param.right, lazy_bindings);
|
|
569
|
+
if (new_right !== param.right) return { ...param, right: new_right };
|
|
570
|
+
}
|
|
571
|
+
return param;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* @param {any} pattern
|
|
576
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
577
|
+
* @param {Set<string>} shadowed
|
|
578
|
+
*/
|
|
579
|
+
function collect_shadowed_names(pattern, lazy_bindings, shadowed) {
|
|
580
|
+
if (!pattern || typeof pattern !== 'object') return;
|
|
581
|
+
if (pattern.type === 'Identifier' && lazy_bindings.has(pattern.name)) {
|
|
582
|
+
shadowed.add(pattern.name);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (pattern.type === 'AssignmentPattern') {
|
|
586
|
+
collect_shadowed_names(pattern.left, lazy_bindings, shadowed);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (pattern.type === 'RestElement') {
|
|
590
|
+
collect_shadowed_names(pattern.argument, lazy_bindings, shadowed);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (pattern.type === 'ObjectPattern') {
|
|
594
|
+
for (const prop of pattern.properties || []) {
|
|
595
|
+
if (prop.type === 'RestElement') {
|
|
596
|
+
collect_shadowed_names(prop.argument, lazy_bindings, shadowed);
|
|
597
|
+
} else {
|
|
598
|
+
collect_shadowed_names(prop.value, lazy_bindings, shadowed);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (pattern.type === 'ArrayPattern') {
|
|
604
|
+
for (const element of pattern.elements || []) {
|
|
605
|
+
if (element) collect_shadowed_names(element, lazy_bindings, shadowed);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* @param {any[]} statements
|
|
612
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
613
|
+
* @returns {Set<string>}
|
|
614
|
+
*/
|
|
615
|
+
function collect_block_shadowed_names(statements, lazy_bindings) {
|
|
616
|
+
/** @type {Set<string>} */
|
|
617
|
+
const shadowed = new Set();
|
|
618
|
+
for (const stmt of statements) {
|
|
619
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
620
|
+
for (const decl of stmt.declarations) {
|
|
621
|
+
if (decl.id?.metadata?.lazy_id) continue;
|
|
622
|
+
if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
|
|
623
|
+
}
|
|
624
|
+
} else if (stmt.type === 'FunctionDeclaration' && stmt.id) {
|
|
625
|
+
if (lazy_bindings.has(stmt.id.name)) shadowed.add(stmt.id.name);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return shadowed;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
633
|
+
* @param {Set<string>} shadowed
|
|
634
|
+
* @returns {Map<string, LazyBinding>}
|
|
635
|
+
*/
|
|
636
|
+
function remove_shadowed(lazy_bindings, shadowed) {
|
|
637
|
+
const result = new Map(lazy_bindings);
|
|
638
|
+
for (const name of shadowed) result.delete(name);
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Replace any lazy `&{}` / `&[]` patterns in a parameter list with their
|
|
644
|
+
* generated lazy identifiers. Leaves non-lazy params untouched.
|
|
645
|
+
*
|
|
646
|
+
* @param {any[]} params
|
|
647
|
+
* @returns {any[]}
|
|
648
|
+
*/
|
|
649
|
+
export function replace_lazy_params(params) {
|
|
650
|
+
return params.map((param) => {
|
|
651
|
+
const pattern = param.type === 'AssignmentPattern' ? param.left : param;
|
|
652
|
+
if (
|
|
653
|
+
(pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') &&
|
|
654
|
+
pattern.lazy &&
|
|
655
|
+
pattern.metadata?.lazy_id
|
|
656
|
+
) {
|
|
657
|
+
const lazy_id = create_generated_identifier(pattern.metadata.lazy_id);
|
|
658
|
+
if (pattern.typeAnnotation) lazy_id.typeAnnotation = pattern.typeAnnotation;
|
|
659
|
+
if (param.type === 'AssignmentPattern') return { ...param, left: lazy_id };
|
|
660
|
+
return lazy_id;
|
|
661
|
+
}
|
|
662
|
+
return param;
|
|
663
|
+
});
|
|
664
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic CSS scoping utilities shared between the `@tsrx/react`
|
|
3
|
+
* and `@tsrx/solid` transforms. These walk the template AST and annotate
|
|
4
|
+
* `Element` nodes with a hash class so scope-qualified selectors
|
|
5
|
+
* (e.g. `.foo.hash`) match after rendering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { walk } from 'zimmerframe';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mark every selector inside the stylesheet as "used" so `renderStylesheets`
|
|
12
|
+
* does not comment it out. We skip selector-pruning because component
|
|
13
|
+
* boundaries can be dynamic — any selector authored inside the component's
|
|
14
|
+
* `<style>` block is considered intentional.
|
|
15
|
+
*
|
|
16
|
+
* @param {any} stylesheet
|
|
17
|
+
* @returns {any}
|
|
18
|
+
*/
|
|
19
|
+
export function prepare_stylesheet_for_render(stylesheet) {
|
|
20
|
+
walk(stylesheet, null, {
|
|
21
|
+
_(node, { next }) {
|
|
22
|
+
if (node && node.metadata && typeof node.metadata === 'object') {
|
|
23
|
+
node.metadata.used = true;
|
|
24
|
+
if (node.type === 'RelativeSelector' && !node.metadata.is_global) {
|
|
25
|
+
node.metadata.scoped = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return next();
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
return stylesheet;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {any} node
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
export function is_style_element(node) {
|
|
39
|
+
return (
|
|
40
|
+
node &&
|
|
41
|
+
node.type === 'Element' &&
|
|
42
|
+
node.id &&
|
|
43
|
+
node.id.type === 'Identifier' &&
|
|
44
|
+
node.id.name === 'style'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {any} node
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
export function is_composite_element(node) {
|
|
53
|
+
if (!node || node.type !== 'Element' || !node.id) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (node.id.type === 'Identifier') {
|
|
58
|
+
return /^[A-Z]/.test(node.id.name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return node.id.type === 'MemberExpression';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recursively walk `Element` nodes within a component body and add the hash
|
|
66
|
+
* class name so scope-qualified selectors (e.g. `.foo.hash`) match.
|
|
67
|
+
*
|
|
68
|
+
* @param {any} node
|
|
69
|
+
* @param {string} hash
|
|
70
|
+
* @returns {any}
|
|
71
|
+
*/
|
|
72
|
+
export function annotate_with_hash(node, hash) {
|
|
73
|
+
if (!node || typeof node !== 'object') return node;
|
|
74
|
+
if (
|
|
75
|
+
node.type === 'Component' ||
|
|
76
|
+
node.type === 'FunctionDeclaration' ||
|
|
77
|
+
node.type === 'FunctionExpression' ||
|
|
78
|
+
node.type === 'ArrowFunctionExpression'
|
|
79
|
+
) {
|
|
80
|
+
return node;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (node.type === 'Element') {
|
|
84
|
+
if (!is_style_element(node) && !is_composite_element(node)) {
|
|
85
|
+
add_hash_class(node, hash);
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(node.children)) {
|
|
88
|
+
node.children = node.children
|
|
89
|
+
.filter((/** @type {any} */ child) => !is_style_element(child))
|
|
90
|
+
.map((/** @type {any} */ child) => annotate_with_hash(child, hash));
|
|
91
|
+
}
|
|
92
|
+
return node;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const key of Object.keys(node)) {
|
|
96
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const value = node[key];
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
node[key] = value.map((/** @type {any} */ child) => annotate_with_hash(child, hash));
|
|
103
|
+
} else if (value && typeof value === 'object') {
|
|
104
|
+
node[key] = annotate_with_hash(value, hash);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return node;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {any} component
|
|
113
|
+
* @param {string} hash
|
|
114
|
+
* @returns {void}
|
|
115
|
+
*/
|
|
116
|
+
export function annotate_component_with_hash(component, hash) {
|
|
117
|
+
/** @type {any[]} */
|
|
118
|
+
const body = component.body;
|
|
119
|
+
component.body = body
|
|
120
|
+
.filter((/** @type {any} */ child) => !is_style_element(child))
|
|
121
|
+
.map((/** @type {any} */ child) => annotate_with_hash(child, hash));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Ensure the element carries a `class` attribute containing the scoping hash.
|
|
126
|
+
*
|
|
127
|
+
* @param {any} element
|
|
128
|
+
* @param {string} hash
|
|
129
|
+
* @returns {void}
|
|
130
|
+
*/
|
|
131
|
+
export function add_hash_class(element, hash) {
|
|
132
|
+
const attrs = element.attributes || (element.attributes = []);
|
|
133
|
+
const existing = attrs.find(
|
|
134
|
+
(/** @type {any} */ a) =>
|
|
135
|
+
a.type === 'Attribute' &&
|
|
136
|
+
a.name &&
|
|
137
|
+
a.name.type === 'Identifier' &&
|
|
138
|
+
(a.name.name === 'class' || a.name.name === 'className'),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (!existing) {
|
|
142
|
+
attrs.push({
|
|
143
|
+
type: 'Attribute',
|
|
144
|
+
name: { type: 'Identifier', name: 'class' },
|
|
145
|
+
value: { type: 'Literal', value: hash, raw: JSON.stringify(hash) },
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const value = existing.value;
|
|
151
|
+
if (!value) {
|
|
152
|
+
existing.value = { type: 'Literal', value: hash, raw: JSON.stringify(hash) };
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (value.type === 'Literal' && typeof value.value === 'string') {
|
|
157
|
+
const merged = `${value.value} ${hash}`;
|
|
158
|
+
existing.value = { type: 'Literal', value: merged, raw: JSON.stringify(merged) };
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Dynamic expression. Concatenate at runtime via template literal.
|
|
163
|
+
const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
|
|
164
|
+
existing.value = {
|
|
165
|
+
type: 'TemplateLiteral',
|
|
166
|
+
expressions: [expression],
|
|
167
|
+
quasis: [
|
|
168
|
+
{
|
|
169
|
+
type: 'TemplateElement',
|
|
170
|
+
value: { raw: '', cooked: '' },
|
|
171
|
+
tail: false,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'TemplateElement',
|
|
175
|
+
value: { raw: ` ${hash}`, cooked: ` ${hash}` },
|
|
176
|
+
tail: true,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
CodeMapping,
|
|
10
10
|
VolarMappingsResult,
|
|
11
11
|
PostProcessingChanges,
|
|
12
|
+
LineOffsets,
|
|
12
13
|
} from '../../types/index';
|
|
13
14
|
@import { CodeMapping as VolarCodeMapping } from '@volar/language-core';
|
|
14
15
|
*/
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
mapping_data_verify_complete,
|
|
52
53
|
build_line_offsets,
|
|
53
54
|
get_mapping_from_node,
|
|
55
|
+
maybe_get_mapping_from_node,
|
|
54
56
|
} from '../source-map-utils.js';
|
|
55
57
|
|
|
56
58
|
const LABEL_TO_COMPONENT_REPLACE_REGEX = /(function|\((property|method)\))/;
|
|
@@ -668,9 +670,21 @@ export function convert_source_map_to_mappings(
|
|
|
668
670
|
return;
|
|
669
671
|
} else if (node.type === 'JSXExpressionContainer') {
|
|
670
672
|
if (node.loc) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
+
// Use maybe_get_mapping_from_node because a transform may set the
|
|
674
|
+
// container's loc to the source range of the original `{...}`
|
|
675
|
+
// construct (e.g. a Ripple TSRXExpression or Text node), while
|
|
676
|
+
// esrap only emits a segment for the inner expression. In that
|
|
677
|
+
// case the container's start/end won't resolve — skip rather
|
|
678
|
+
// than hard-failing, and rely on the inner expression's mapping.
|
|
679
|
+
const mapping = maybe_get_mapping_from_node(
|
|
680
|
+
node,
|
|
681
|
+
src_to_gen_map,
|
|
682
|
+
gen_line_offsets,
|
|
683
|
+
mapping_data_verify_only,
|
|
673
684
|
);
|
|
685
|
+
if (!(mapping instanceof Error)) {
|
|
686
|
+
mappings.push(mapping);
|
|
687
|
+
}
|
|
674
688
|
}
|
|
675
689
|
// Visit the expression inside {}
|
|
676
690
|
if (node.expression) {
|
|
@@ -726,24 +740,38 @@ export function convert_source_map_to_mappings(
|
|
|
726
740
|
}
|
|
727
741
|
}
|
|
728
742
|
|
|
729
|
-
if (closing || opening.selfClosing) {
|
|
730
|
-
// Add the whole closing tag or the self-closing
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
743
|
+
if ((closing?.loc || opening.loc) && (closing || opening.selfClosing)) {
|
|
744
|
+
// Add the whole closing tag or the self-closing.
|
|
745
|
+
// For self-closing elements, use maybe_get_mapping_from_node because
|
|
746
|
+
// attribute transforms (e.g. class→className, {ref fn}→ref={fn}) can shift
|
|
747
|
+
// the position of `/>` in the generated output, making the source map
|
|
748
|
+
// entry for the opening element's end position unresolvable.
|
|
749
|
+
const target_node = closing ? closing : opening;
|
|
750
|
+
const mapping = closing
|
|
751
|
+
? get_mapping_from_node(
|
|
752
|
+
target_node,
|
|
753
|
+
src_to_gen_map,
|
|
754
|
+
gen_line_offsets,
|
|
755
|
+
mapping_data_verify_only,
|
|
756
|
+
)
|
|
757
|
+
: maybe_get_mapping_from_node(
|
|
758
|
+
target_node,
|
|
759
|
+
src_to_gen_map,
|
|
760
|
+
gen_line_offsets,
|
|
761
|
+
mapping_data_verify_only,
|
|
762
|
+
);
|
|
737
763
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
764
|
+
if (!(mapping instanceof Error)) {
|
|
765
|
+
// The generated code includes a semicolon after the closing or self-closed tag
|
|
766
|
+
// We're extending the mapping to include the semicolon
|
|
767
|
+
// because the diagnostics errors can include the whole element
|
|
768
|
+
// and we need to account for the semicolon as it's a part of the diagnostic
|
|
769
|
+
// At the same time, we could've instead applied this logic to the whole `node` element
|
|
770
|
+
// but since we already map the opening - start, we just need the proper end
|
|
771
|
+
// and it was causing some issues with mappings
|
|
772
|
+
mapping.generatedLengths = [mapping.generatedLengths[0] + 1];
|
|
773
|
+
mappings.push(mapping);
|
|
774
|
+
}
|
|
747
775
|
}
|
|
748
776
|
|
|
749
777
|
if (closing) {
|
|
@@ -769,37 +797,52 @@ export function convert_source_map_to_mappings(
|
|
|
769
797
|
let start = node_fn.start;
|
|
770
798
|
const async_keyword = 'async';
|
|
771
799
|
|
|
772
|
-
if (node_fn.
|
|
773
|
-
|
|
800
|
+
if (is_component && node_fn.id?.loc) {
|
|
801
|
+
const mapping = get_mapping_from_node(node_fn.id, src_to_gen_map, gen_line_offsets);
|
|
802
|
+
const generated_id_start = mapping.generatedOffsets[0];
|
|
803
|
+
const generated_keyword_start = find_component_keyword_offset(
|
|
804
|
+
generated_code,
|
|
805
|
+
generated_id_start,
|
|
806
|
+
);
|
|
807
|
+
mapping.sourceOffsets = [start];
|
|
808
|
+
mapping.lengths = [source_func_keyword.length];
|
|
809
|
+
mapping.generatedOffsets = [generated_keyword_start];
|
|
810
|
+
mapping.generatedLengths = ['function'.length];
|
|
811
|
+
mapping.data.customData.hover = replace_label_to_component;
|
|
812
|
+
mappings.push(mapping);
|
|
813
|
+
} else {
|
|
814
|
+
if (node_fn.async) {
|
|
815
|
+
// We explicitly mapped async and function in esrap
|
|
816
|
+
tokens.push({
|
|
817
|
+
source: async_keyword,
|
|
818
|
+
generated: async_keyword,
|
|
819
|
+
loc: {
|
|
820
|
+
start: { line: node_fn.loc.start.line, column: start_col },
|
|
821
|
+
end: {
|
|
822
|
+
line: node_fn.loc.start.line,
|
|
823
|
+
column: start_col + async_keyword.length,
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
metadata: {},
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
start_col += async_keyword.length + 1; // +1 for space
|
|
830
|
+
start += async_keyword.length + 1;
|
|
831
|
+
}
|
|
832
|
+
|
|
774
833
|
tokens.push({
|
|
775
|
-
source:
|
|
776
|
-
generated:
|
|
834
|
+
source: source_func_keyword,
|
|
835
|
+
generated: 'function',
|
|
777
836
|
loc: {
|
|
778
837
|
start: { line: node_fn.loc.start.line, column: start_col },
|
|
779
838
|
end: {
|
|
780
839
|
line: node_fn.loc.start.line,
|
|
781
|
-
column: start_col +
|
|
840
|
+
column: start_col + source_func_keyword.length,
|
|
782
841
|
},
|
|
783
842
|
},
|
|
784
|
-
metadata: {},
|
|
843
|
+
metadata: is_component ? { hover: replace_label_to_component } : {},
|
|
785
844
|
});
|
|
786
|
-
|
|
787
|
-
start_col += async_keyword.length + 1; // +1 for space
|
|
788
|
-
start += async_keyword.length + 1;
|
|
789
845
|
}
|
|
790
|
-
|
|
791
|
-
tokens.push({
|
|
792
|
-
source: source_func_keyword,
|
|
793
|
-
generated: 'function',
|
|
794
|
-
loc: {
|
|
795
|
-
start: { line: node_fn.loc.start.line, column: start_col },
|
|
796
|
-
end: {
|
|
797
|
-
line: node_fn.loc.start.line,
|
|
798
|
-
column: start_col + source_func_keyword.length,
|
|
799
|
-
},
|
|
800
|
-
},
|
|
801
|
-
metadata: is_component ? { hover: replace_label_to_component } : {},
|
|
802
|
-
});
|
|
803
846
|
}
|
|
804
847
|
|
|
805
848
|
// Visit in source order: id, params, body
|
|
@@ -982,9 +1025,11 @@ export function convert_source_map_to_mappings(
|
|
|
982
1025
|
visit(node.body);
|
|
983
1026
|
}
|
|
984
1027
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1028
|
+
if (node.loc) {
|
|
1029
|
+
mappings.push(
|
|
1030
|
+
get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
988
1033
|
|
|
989
1034
|
return;
|
|
990
1035
|
} else if (node.type === 'WhileStatement' || node.type === 'DoWhileStatement') {
|
|
@@ -1321,9 +1366,11 @@ export function convert_source_map_to_mappings(
|
|
|
1321
1366
|
}
|
|
1322
1367
|
}
|
|
1323
1368
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1369
|
+
if (node.loc) {
|
|
1370
|
+
mappings.push(
|
|
1371
|
+
get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1327
1374
|
|
|
1328
1375
|
return;
|
|
1329
1376
|
} else if (node.type === 'SwitchCase') {
|
|
@@ -2030,11 +2077,16 @@ export function convert_source_map_to_mappings(
|
|
|
2030
2077
|
);
|
|
2031
2078
|
const source_length = source_text.length;
|
|
2032
2079
|
const gen_length = gen_text.length;
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2080
|
+
let gen_line_col;
|
|
2081
|
+
try {
|
|
2082
|
+
gen_line_col = get_generated_position(
|
|
2083
|
+
token.loc.start.line,
|
|
2084
|
+
token.loc.start.column,
|
|
2085
|
+
src_to_gen_map,
|
|
2086
|
+
);
|
|
2087
|
+
} catch {
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2038
2090
|
const gen_start = loc_to_offset(gen_line_col.line, gen_line_col.column, gen_line_offsets);
|
|
2039
2091
|
|
|
2040
2092
|
/** @type {CustomMappingData} */
|
|
@@ -2138,3 +2190,117 @@ export function convert_source_map_to_mappings(
|
|
|
2138
2190
|
cssMappings,
|
|
2139
2191
|
};
|
|
2140
2192
|
}
|
|
2193
|
+
|
|
2194
|
+
/**
|
|
2195
|
+
* @param {string} generated_code
|
|
2196
|
+
* @param {number} generated_id_start
|
|
2197
|
+
* @returns {number}
|
|
2198
|
+
*/
|
|
2199
|
+
function find_component_keyword_offset(generated_code, generated_id_start) {
|
|
2200
|
+
const function_keyword_index = generated_code.lastIndexOf('function', generated_id_start);
|
|
2201
|
+
|
|
2202
|
+
if (function_keyword_index === -1) {
|
|
2203
|
+
return generated_id_start;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
return function_keyword_index;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
/**
|
|
2210
|
+
* Build a `VolarMappingsResult` from generated code plus source-map metadata.
|
|
2211
|
+
*
|
|
2212
|
+
* Framework packages are responsible for producing the generated AST/code/map.
|
|
2213
|
+
* Core owns the generic mapping conversion and result envelope so the editor
|
|
2214
|
+
* integration is not coupled to any specific framework package.
|
|
2215
|
+
*
|
|
2216
|
+
* @param {{
|
|
2217
|
+
* ast: AST.Program,
|
|
2218
|
+
* ast_from_source: AST.Program,
|
|
2219
|
+
* source: string,
|
|
2220
|
+
* generated_code: string,
|
|
2221
|
+
* source_map: RawSourceMap,
|
|
2222
|
+
* errors?: import('../../types/index').CompileError[],
|
|
2223
|
+
* post_processing_changes?: PostProcessingChanges,
|
|
2224
|
+
* line_offsets?: LineOffsets,
|
|
2225
|
+
* }} params
|
|
2226
|
+
* @returns {VolarMappingsResult}
|
|
2227
|
+
*/
|
|
2228
|
+
export function create_volar_mappings_result({
|
|
2229
|
+
ast,
|
|
2230
|
+
ast_from_source,
|
|
2231
|
+
source,
|
|
2232
|
+
generated_code,
|
|
2233
|
+
source_map,
|
|
2234
|
+
errors = [],
|
|
2235
|
+
post_processing_changes,
|
|
2236
|
+
line_offsets,
|
|
2237
|
+
}) {
|
|
2238
|
+
return {
|
|
2239
|
+
...convert_source_map_to_mappings(
|
|
2240
|
+
ast,
|
|
2241
|
+
ast_from_source,
|
|
2242
|
+
source,
|
|
2243
|
+
generated_code,
|
|
2244
|
+
source_map,
|
|
2245
|
+
/** @type {PostProcessingChanges} */ (post_processing_changes),
|
|
2246
|
+
line_offsets ?? build_line_offsets(generated_code),
|
|
2247
|
+
),
|
|
2248
|
+
errors,
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
/**
|
|
2253
|
+
* Remove byte-for-byte duplicate mappings. Framework compilers that extract
|
|
2254
|
+
* shared helpers or replay JSX can emit identical mapping entries for the
|
|
2255
|
+
* same source and generated span; Volar merges duplicates into a single
|
|
2256
|
+
* hover/navigation result, so deduping upstream avoids a stutter.
|
|
2257
|
+
*
|
|
2258
|
+
* @param {CodeMapping[]} mappings
|
|
2259
|
+
* @returns {CodeMapping[]}
|
|
2260
|
+
*/
|
|
2261
|
+
export function dedupe_mappings(mappings) {
|
|
2262
|
+
const deduped = [];
|
|
2263
|
+
const seen = new Set();
|
|
2264
|
+
|
|
2265
|
+
for (const mapping of mappings) {
|
|
2266
|
+
const key = JSON.stringify(serialize_mapping_value(mapping));
|
|
2267
|
+
|
|
2268
|
+
if (seen.has(key)) {
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
seen.add(key);
|
|
2273
|
+
deduped.push(mapping);
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
return deduped;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
/**
|
|
2280
|
+
* Serialize a mapping (or any nested value) into a stable JSON-friendly
|
|
2281
|
+
* shape so {@link dedupe_mappings} can compare two entries by content.
|
|
2282
|
+
* Object keys are sorted and functions are reduced to their source so
|
|
2283
|
+
* structurally-identical entries produce the same string.
|
|
2284
|
+
*
|
|
2285
|
+
* @param {unknown} value
|
|
2286
|
+
* @returns {unknown}
|
|
2287
|
+
*/
|
|
2288
|
+
export function serialize_mapping_value(value) {
|
|
2289
|
+
if (typeof value === 'function') {
|
|
2290
|
+
return value.toString();
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
if (Array.isArray(value)) {
|
|
2294
|
+
return value.map(serialize_mapping_value);
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
if (value && typeof value === 'object') {
|
|
2298
|
+
return Object.fromEntries(
|
|
2299
|
+
Object.entries(value)
|
|
2300
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
2301
|
+
.map(([key, nested_value]) => [key, serialize_mapping_value(nested_value)]),
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
return value;
|
|
2306
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -1387,7 +1387,7 @@ export interface TransformClientState extends BaseState {
|
|
|
1387
1387
|
applyParentCssScope?: AST.CSS.StyleSheet['hash'];
|
|
1388
1388
|
skip_children_traversal: boolean;
|
|
1389
1389
|
return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
|
|
1390
|
-
|
|
1390
|
+
is_tsrx_element?: boolean;
|
|
1391
1391
|
}
|
|
1392
1392
|
|
|
1393
1393
|
/** Override zimmerframe types and provide our own */
|
package/types/parse.d.ts
CHANGED
|
@@ -182,7 +182,12 @@ export namespace Parse {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
export interface Options extends Omit<acorn.Options, 'onComment' | 'ecmaVersion'> {
|
|
185
|
-
|
|
185
|
+
tsrxOptions?: {
|
|
186
|
+
loose: boolean;
|
|
187
|
+
errors: CoreCompiler.CompileError[];
|
|
188
|
+
filename: string | undefined;
|
|
189
|
+
};
|
|
190
|
+
rippleOptions?: {
|
|
186
191
|
loose: boolean;
|
|
187
192
|
errors: CoreCompiler.CompileError[];
|
|
188
193
|
filename: string | undefined;
|