@tsrx/core 0.1.45 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/package.json +1 -1
- package/src/analyze/index.js +177 -0
- package/src/analyze/validation.js +19 -0
- package/src/index.js +10 -0
- package/src/plugin.js +3 -104
- package/src/transform/jsx/index.js +4 -30
- package/src/transform/style-ref.js +1 -14
- package/src/utils/ast.js +90 -1
- package/types/index.d.ts +32 -0
package/README.md
CHANGED
|
@@ -27,9 +27,10 @@ pnpm add @tsrx/core
|
|
|
27
27
|
## Usage
|
|
28
28
|
|
|
29
29
|
```js
|
|
30
|
-
import { parseModule } from '@tsrx/core';
|
|
30
|
+
import { analyzeTsrx, parseModule } from '@tsrx/core';
|
|
31
31
|
|
|
32
32
|
const ast = parseModule(source, 'App.tsrx');
|
|
33
|
+
const analysis = analyzeTsrx(ast, 'App.tsrx');
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
The parser produces an ESTree-compatible AST, augmented with the TSRX node types
|
|
@@ -54,6 +55,10 @@ here and keeps package docs focused on the core parser API.
|
|
|
54
55
|
|
|
55
56
|
- **`parseModule(source, filename, options?)`** — parse a TSRX module into an
|
|
56
57
|
ESTree AST.
|
|
58
|
+
- **`analyzeTsrx(ast, filename, options?)`** — run target-neutral semantic
|
|
59
|
+
validation before framework analysis or transformation. Pass `collect: true`,
|
|
60
|
+
`typeOnly: true`, or `to_ts: true` to collect non-fatal diagnostics for
|
|
61
|
+
editor/type-only output.
|
|
57
62
|
- **Scope analysis** — `createScopes`, `Scope`, `ScopeRoot`, binding tracking
|
|
58
63
|
(`import`, `prop`, `let`, `const`, `function`, `for_pattern`, …).
|
|
59
64
|
- **AST utilities** — pattern walkers, identifier extraction, builders, location
|
package/package.json
CHANGED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@import * as AST from 'estree';
|
|
3
|
+
@import { TSRXAnalysisOptions, TSRXAnalysisResult, TSRXAnalysisState } from '../../types/index';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { walk } from 'zimmerframe';
|
|
7
|
+
import {
|
|
8
|
+
is_code_block_function_body,
|
|
9
|
+
is_statement_position,
|
|
10
|
+
is_tsrx_render_output_node,
|
|
11
|
+
} from '../utils/ast.js';
|
|
12
|
+
import { validate_forgotten_statement_container } from './validation.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Wrappers that preserve an expression's value and therefore do not turn a
|
|
16
|
+
* template into a used value on their own. Looking through them keeps strict
|
|
17
|
+
* builds and `preserveParens` type-only builds on the same diagnostic path.
|
|
18
|
+
*
|
|
19
|
+
* @param {AST.Node} parent
|
|
20
|
+
* @param {AST.Node} child
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function is_transparent_expression_wrapper(parent, child) {
|
|
24
|
+
return (
|
|
25
|
+
(parent.type === 'ParenthesizedExpression' ||
|
|
26
|
+
parent.type === 'TSAsExpression' ||
|
|
27
|
+
parent.type === 'TSSatisfiesExpression' ||
|
|
28
|
+
parent.type === 'TSNonNullExpression' ||
|
|
29
|
+
parent.type === 'TSInstantiationExpression' ||
|
|
30
|
+
parent.type === 'TSTypeAssertion' ||
|
|
31
|
+
parent.type === 'ChainExpression') &&
|
|
32
|
+
/** @type {{ expression?: AST.Node }} */ (parent).expression === child
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A template is unused only when it is itself the statement being executed.
|
|
38
|
+
* Templates nested in assignments, returns, arguments, operands, or other
|
|
39
|
+
* value-producing expressions may be consumed later and are valid.
|
|
40
|
+
*
|
|
41
|
+
* @param {AST.Node} node
|
|
42
|
+
* @param {AST.Node[]} path
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
function is_free_floating_template(node, path) {
|
|
46
|
+
let child = node;
|
|
47
|
+
|
|
48
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
49
|
+
const parent = path[i];
|
|
50
|
+
|
|
51
|
+
if (is_transparent_expression_wrapper(parent, child)) {
|
|
52
|
+
child = parent;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (parent.type === 'ExpressionStatement' && parent.expression === child) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (is_statement_position(parent, child)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {AST.Function} node
|
|
72
|
+
* @param {{ next: (state?: TSRXAnalysisState) => unknown, state: TSRXAnalysisState }} context
|
|
73
|
+
*/
|
|
74
|
+
function visit_function(node, { next, state }) {
|
|
75
|
+
next({
|
|
76
|
+
...state,
|
|
77
|
+
function: node,
|
|
78
|
+
function_body_is_code_block: is_code_block_function_body(node.body, node),
|
|
79
|
+
inside_template_output: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {AST.Node} node
|
|
85
|
+
* @param {{ next: (state?: TSRXAnalysisState) => unknown, path: AST.Node[], state: TSRXAnalysisState }} context
|
|
86
|
+
*/
|
|
87
|
+
function visit_render_output(node, { next, path, state }) {
|
|
88
|
+
if (!is_tsrx_render_output_node(node)) {
|
|
89
|
+
next();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
state.function &&
|
|
95
|
+
!(state.function_body_is_code_block && state.function.body === node) &&
|
|
96
|
+
!state.inside_template_output &&
|
|
97
|
+
is_free_floating_template(node, path)
|
|
98
|
+
) {
|
|
99
|
+
validate_forgotten_statement_container(
|
|
100
|
+
node,
|
|
101
|
+
state.filename,
|
|
102
|
+
state.collect ? state.errors : undefined,
|
|
103
|
+
state.comments,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A JSXCodeBlock contains ordinary setup statements in `body` as well as
|
|
108
|
+
// the retained output in `render`. Reset the template context while walking
|
|
109
|
+
// both fields so free-floating output in setup is still diagnosed. The
|
|
110
|
+
// render node itself is retained by the code block, and establishes template
|
|
111
|
+
// context for its own descendants when this visitor reaches it.
|
|
112
|
+
next({ ...state, inside_template_output: node.type !== 'JSXCodeBlock' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {AST.ClassDeclaration | AST.ClassExpression} _node
|
|
117
|
+
* @param {{ next: (state?: TSRXAnalysisState) => unknown, state: TSRXAnalysisState }} context
|
|
118
|
+
*/
|
|
119
|
+
function visit_class(_node, { next, state }) {
|
|
120
|
+
next({
|
|
121
|
+
...state,
|
|
122
|
+
function: null,
|
|
123
|
+
function_body_is_code_block: false,
|
|
124
|
+
inside_template_output: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const visitors = {
|
|
129
|
+
FunctionDeclaration: visit_function,
|
|
130
|
+
FunctionExpression: visit_function,
|
|
131
|
+
ArrowFunctionExpression: visit_function,
|
|
132
|
+
|
|
133
|
+
// A class body is not part of the surrounding function's execution context.
|
|
134
|
+
// Method/function nodes establish their own context when reached.
|
|
135
|
+
ClassDeclaration: visit_class,
|
|
136
|
+
ClassExpression: visit_class,
|
|
137
|
+
|
|
138
|
+
JSXElement: visit_render_output,
|
|
139
|
+
JSXFragment: visit_render_output,
|
|
140
|
+
JSXStyleElement: visit_render_output,
|
|
141
|
+
JSXCodeBlock: visit_render_output,
|
|
142
|
+
JSXIfExpression: visit_render_output,
|
|
143
|
+
JSXForExpression: visit_render_output,
|
|
144
|
+
JSXSwitchExpression: visit_render_output,
|
|
145
|
+
JSXTryExpression: visit_render_output,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Run target-neutral semantic validation over a parsed TSRX module. Parsing
|
|
150
|
+
* remains syntax-only; every target invokes this pass before target analysis or
|
|
151
|
+
* transformation. Type-only/Volar callers collect diagnostics and continue.
|
|
152
|
+
*
|
|
153
|
+
* @param {AST.Program} ast
|
|
154
|
+
* @param {string | null | undefined} filename
|
|
155
|
+
* @param {TSRXAnalysisOptions} [options]
|
|
156
|
+
* @returns {TSRXAnalysisResult}
|
|
157
|
+
*/
|
|
158
|
+
export function analyze_tsrx(ast, filename, options = {}) {
|
|
159
|
+
const errors = options.errors ?? [];
|
|
160
|
+
const comments = options.comments ?? [];
|
|
161
|
+
const collect = !!(options.collect || options.loose || options.typeOnly || options.to_ts);
|
|
162
|
+
|
|
163
|
+
/** @type {TSRXAnalysisState} */
|
|
164
|
+
const state = {
|
|
165
|
+
filename: filename ?? null,
|
|
166
|
+
collect,
|
|
167
|
+
errors,
|
|
168
|
+
comments,
|
|
169
|
+
function: null,
|
|
170
|
+
function_body_is_code_block: false,
|
|
171
|
+
inside_template_output: false,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
walk(ast, state, visitors);
|
|
175
|
+
|
|
176
|
+
return { ast, errors, comments };
|
|
177
|
+
}
|
|
@@ -28,6 +28,8 @@ export const TSRX_WHILE_STATEMENT_ERROR =
|
|
|
28
28
|
'While loops are not supported in TSRX templates. Move the while loop into a function.';
|
|
29
29
|
export const TSRX_DO_WHILE_STATEMENT_ERROR =
|
|
30
30
|
'Do...while loops are not supported in TSRX templates. Move the do...while loop into a function.';
|
|
31
|
+
export const TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR =
|
|
32
|
+
"This TSRX template output is unused. Return it, assign it to a value that is rendered, or make it part of the rendered output of a function '@{...}' body.";
|
|
31
33
|
|
|
32
34
|
const invalid_nestings = {
|
|
33
35
|
// <p> cannot contain block-level elements
|
|
@@ -201,6 +203,23 @@ export function validate_tsrx_return_statement(node, filename, errors, comments)
|
|
|
201
203
|
);
|
|
202
204
|
}
|
|
203
205
|
|
|
206
|
+
/**
|
|
207
|
+
* @param {AST.Node} node
|
|
208
|
+
* @param {string | null | undefined} filename
|
|
209
|
+
* @param {CompileError[]} [errors]
|
|
210
|
+
* @param {AST.CommentWithLocation[]} [comments]
|
|
211
|
+
*/
|
|
212
|
+
export function validate_forgotten_statement_container(node, filename, errors, comments) {
|
|
213
|
+
error(
|
|
214
|
+
TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR,
|
|
215
|
+
filename ?? null,
|
|
216
|
+
node,
|
|
217
|
+
errors,
|
|
218
|
+
comments,
|
|
219
|
+
DIAGNOSTIC_CODES.FORGOTTEN_STATEMENT_CONTAINER,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
204
223
|
/**
|
|
205
224
|
* @param {AST.ReturnStatement} node
|
|
206
225
|
* @param {string | null | undefined} filename
|
package/src/index.js
CHANGED
|
@@ -87,10 +87,18 @@ export {
|
|
|
87
87
|
build_assignment_value as buildAssignmentValue,
|
|
88
88
|
is_class_node as isClassNode,
|
|
89
89
|
is_function_node as isFunctionNode,
|
|
90
|
+
is_function_or_class_node as isFunctionOrClassNode,
|
|
90
91
|
is_function_or_component_node as isFunctionOrComponentNode,
|
|
91
92
|
is_inside_component as isInsideComponent,
|
|
93
|
+
is_template_directive as isTemplateDirective,
|
|
94
|
+
is_tsrx_render_output_node as isTsrxRenderOutputNode,
|
|
95
|
+
is_code_block_function_body as isCodeBlockFunctionBody,
|
|
96
|
+
is_statement_position as isStatementPosition,
|
|
92
97
|
} from './utils/ast.js';
|
|
93
98
|
|
|
99
|
+
// Shared TSRX semantic analysis
|
|
100
|
+
export { analyze_tsrx as analyzeTsrx } from './analyze/index.js';
|
|
101
|
+
|
|
94
102
|
// Builders (namespace re-export — members mirror AST node kinds)
|
|
95
103
|
export * as builders from './utils/builders.js';
|
|
96
104
|
|
|
@@ -244,6 +252,7 @@ export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
|
|
|
244
252
|
export { prune_css as pruneCss } from './analyze/prune.js';
|
|
245
253
|
export {
|
|
246
254
|
TSRX_DO_WHILE_STATEMENT_ERROR,
|
|
255
|
+
TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR,
|
|
247
256
|
TSRX_FOR_IN_STATEMENT_ERROR,
|
|
248
257
|
TSRX_FOR_STATEMENT_ERROR,
|
|
249
258
|
TSRX_IF_BREAK_ERROR,
|
|
@@ -264,6 +273,7 @@ export {
|
|
|
264
273
|
validate_tsrx_loop_return_statement as validateTsrxLoopReturnStatement,
|
|
265
274
|
validate_tsrx_return_statement as validateTsrxReturnStatement,
|
|
266
275
|
validate_tsrx_unsupported_loop_statement as validateTsrxUnsupportedLoopStatement,
|
|
276
|
+
validate_forgotten_statement_container as validateForgottenStatementContainer,
|
|
267
277
|
validate_nesting as validateNesting,
|
|
268
278
|
is_template_value_position as isTemplateValuePosition,
|
|
269
279
|
} from './analyze/validation.js';
|
package/src/plugin.js
CHANGED
|
@@ -11,8 +11,7 @@ import { regex_newline_characters } from './utils/patterns.js';
|
|
|
11
11
|
import { error } from './errors.js';
|
|
12
12
|
import { DIAGNOSTIC_CODES } from './diagnostics.js';
|
|
13
13
|
import { TSRX_RETURN_STATEMENT_ERROR } from './analyze/validation.js';
|
|
14
|
-
|
|
15
|
-
"This function body contains TSRX template output, but it is a normal JavaScript block. Add '@' before the opening brace to use a TSRX statement container.";
|
|
14
|
+
import { is_tsrx_render_output_node } from './utils/ast.js';
|
|
16
15
|
|
|
17
16
|
const CharCode = Object.freeze({
|
|
18
17
|
tab: 9,
|
|
@@ -1162,104 +1161,6 @@ export function TSRXPlugin(config) {
|
|
|
1162
1161
|
return this.#isCodeBlockStart(index);
|
|
1163
1162
|
}
|
|
1164
1163
|
|
|
1165
|
-
/**
|
|
1166
|
-
* @param {AST.Node | null | undefined} node
|
|
1167
|
-
*/
|
|
1168
|
-
#isRenderOutputNode(node) {
|
|
1169
|
-
if (!node) return false;
|
|
1170
|
-
switch (node.type) {
|
|
1171
|
-
case 'JSXElement':
|
|
1172
|
-
case 'JSXFragment':
|
|
1173
|
-
case 'JSXStyleElement':
|
|
1174
|
-
case 'JSXCodeBlock':
|
|
1175
|
-
case 'JSXIfExpression':
|
|
1176
|
-
case 'JSXForExpression':
|
|
1177
|
-
case 'JSXSwitchExpression':
|
|
1178
|
-
case 'JSXTryExpression':
|
|
1179
|
-
return true;
|
|
1180
|
-
}
|
|
1181
|
-
return false;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
/**
|
|
1185
|
-
* @param {AST.Node | null | undefined} node
|
|
1186
|
-
*/
|
|
1187
|
-
#isForgottenStatementContainerOutputNode(node) {
|
|
1188
|
-
return this.#isRenderOutputNode(node) && node?.type !== 'JSXCodeBlock';
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
/**
|
|
1192
|
-
* @param {AST.Node | null | undefined} node
|
|
1193
|
-
*/
|
|
1194
|
-
#isIgnoredForgottenStatementContainerStatement(node) {
|
|
1195
|
-
return !node || node.type === 'EmptyStatement';
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
/**
|
|
1199
|
-
* A normal function body that directly contains a bare JSX/control-flow node
|
|
1200
|
-
* almost always means the author wrote `{ ... <div /> }` but intended
|
|
1201
|
-
* `@{ ... <div /> }`. Only report when adding `@` would produce a valid
|
|
1202
|
-
* statement container: setup statements first, followed by one final render
|
|
1203
|
-
* output. Report only direct body children so ordinary nested callbacks/branches
|
|
1204
|
-
* are diagnosed by their own function body, not their parent.
|
|
1205
|
-
* @param {AST.Node} node
|
|
1206
|
-
*/
|
|
1207
|
-
#reportForgottenStatementContainerBody(node) {
|
|
1208
|
-
if (!this.#collect) {
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
const body = /** @type {{ body?: AST.Node }} */ (node).body;
|
|
1213
|
-
if (body?.type !== 'BlockStatement') {
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const statements = /** @type {AST.BlockStatement} */ (body).body || [];
|
|
1218
|
-
const has_return_type = Boolean(/** @type {{ returnType?: AST.Node }} */ (node).returnType);
|
|
1219
|
-
if (!has_return_type) {
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
let target = null;
|
|
1224
|
-
let target_index = -1;
|
|
1225
|
-
for (let index = 0; index < statements.length; index++) {
|
|
1226
|
-
const statement = statements[index];
|
|
1227
|
-
const output =
|
|
1228
|
-
this.#isForgottenStatementContainerOutputNode(statement) ||
|
|
1229
|
-
(statement.type === 'ExpressionStatement' &&
|
|
1230
|
-
this.#isForgottenStatementContainerOutputNode(statement.expression))
|
|
1231
|
-
? statement
|
|
1232
|
-
: null;
|
|
1233
|
-
|
|
1234
|
-
if (!output) {
|
|
1235
|
-
continue;
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
if (target_index !== -1) {
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
target_index = index;
|
|
1242
|
-
target = output;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
if (!target) {
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
for (const statement of statements.slice(target_index + 1)) {
|
|
1250
|
-
if (!this.#isIgnoredForgottenStatementContainerStatement(statement)) {
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
this.#report_recoverable_error_range(
|
|
1256
|
-
/** @type {number} */ (target.start),
|
|
1257
|
-
/** @type {number} */ (target.end),
|
|
1258
|
-
FORGOTTEN_STATEMENT_CONTAINER_ERROR,
|
|
1259
|
-
DIAGNOSTIC_CODES.FORGOTTEN_STATEMENT_CONTAINER,
|
|
1260
|
-
);
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
1164
|
/**
|
|
1264
1165
|
* Inside a code block (`@{ … }` or a directive's `{ }`), decides whether the
|
|
1265
1166
|
* next thing is the single bare render node (`<tag …>`, `<>…</>`, or an
|
|
@@ -1500,7 +1401,7 @@ export function TSRXPlugin(config) {
|
|
|
1500
1401
|
}
|
|
1501
1402
|
|
|
1502
1403
|
const last = flat[flat.length - 1];
|
|
1503
|
-
if (
|
|
1404
|
+
if (is_tsrx_render_output_node(last)) {
|
|
1504
1405
|
node.render = last;
|
|
1505
1406
|
node.body = /** @type {AST.Statement[]} */ (flat.slice(0, -1));
|
|
1506
1407
|
} else {
|
|
@@ -3455,9 +3356,7 @@ export function TSRXPlugin(config) {
|
|
|
3455
3356
|
this.exitScope();
|
|
3456
3357
|
return node;
|
|
3457
3358
|
}
|
|
3458
|
-
|
|
3459
|
-
this.#reportForgottenStatementContainerBody(parsed);
|
|
3460
|
-
return parsed;
|
|
3359
|
+
return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
|
|
3461
3360
|
} finally {
|
|
3462
3361
|
this.#functionBodyDepth--;
|
|
3463
3362
|
}
|
|
@@ -61,6 +61,10 @@ import {
|
|
|
61
61
|
} from '../jsx-interleave.js';
|
|
62
62
|
import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
|
|
63
63
|
import { lower_server_module_for_types } from './server-module.js';
|
|
64
|
+
import {
|
|
65
|
+
is_function_or_class_node as is_function_or_class_boundary,
|
|
66
|
+
is_template_directive as is_jsx_control_flow_expression,
|
|
67
|
+
} from '../../utils/ast.js';
|
|
64
68
|
|
|
65
69
|
const TEMPLATE_FRAGMENT_ERROR =
|
|
66
70
|
'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only in expression position.';
|
|
@@ -114,20 +118,6 @@ function report_jsx_fragment_in_tsrx_error(node, transform_context) {
|
|
|
114
118
|
* @typedef {{ source_name: string, read: () => any }} LazyBinding
|
|
115
119
|
*/
|
|
116
120
|
|
|
117
|
-
/**
|
|
118
|
-
* @param {any} node
|
|
119
|
-
* @returns {boolean}
|
|
120
|
-
*/
|
|
121
|
-
function is_function_or_class_boundary(node) {
|
|
122
|
-
return (
|
|
123
|
-
node?.type === 'FunctionDeclaration' ||
|
|
124
|
-
node?.type === 'FunctionExpression' ||
|
|
125
|
-
node?.type === 'ArrowFunctionExpression' ||
|
|
126
|
-
node?.type === 'ClassDeclaration' ||
|
|
127
|
-
node?.type === 'ClassExpression'
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
121
|
/**
|
|
132
122
|
* @param {any} node
|
|
133
123
|
* @param {boolean} [inside_function]
|
|
@@ -264,22 +254,6 @@ function expand_child_code_blocks(node, seen = new Set()) {
|
|
|
264
254
|
return out;
|
|
265
255
|
}
|
|
266
256
|
|
|
267
|
-
/**
|
|
268
|
-
* A `@`-prefixed JSX control-flow expression (`@if`/`@for`/`@switch`/`@try`).
|
|
269
|
-
* These are the only control-flow nodes that can appear in expression position;
|
|
270
|
-
* the plain statement forms (`IfStatement`, `SwitchStatement`, …) never do.
|
|
271
|
-
* @param {any} node
|
|
272
|
-
* @returns {boolean}
|
|
273
|
-
*/
|
|
274
|
-
function is_jsx_control_flow_expression(node) {
|
|
275
|
-
return (
|
|
276
|
-
node?.type === 'JSXIfExpression' ||
|
|
277
|
-
node?.type === 'JSXForExpression' ||
|
|
278
|
-
node?.type === 'JSXSwitchExpression' ||
|
|
279
|
-
node?.type === 'JSXTryExpression'
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
257
|
/**
|
|
284
258
|
* Wrap a render-output node in a native TSRX fragment so it flows through the
|
|
285
259
|
* same single-child render path as a `<> … </>` output. This is a compiler
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @import * as AST from 'estree' */
|
|
2
2
|
|
|
3
3
|
import * as b from '../utils/builders.js';
|
|
4
|
+
import { is_function_or_class_node as is_function_or_class_boundary } from '../utils/ast.js';
|
|
4
5
|
import { clone_expression_node, clone_identifier } from './jsx/ast-builders.js';
|
|
5
6
|
|
|
6
7
|
const regex_backslash_and_following_character = /\\(.)/g;
|
|
@@ -236,20 +237,6 @@ function is_style_element(node) {
|
|
|
236
237
|
return !!node && node.type === 'JSXStyleElement';
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
/**
|
|
240
|
-
* @param {any} node
|
|
241
|
-
* @returns {boolean}
|
|
242
|
-
*/
|
|
243
|
-
function is_function_or_class_boundary(node) {
|
|
244
|
-
return (
|
|
245
|
-
node?.type === 'FunctionDeclaration' ||
|
|
246
|
-
node?.type === 'FunctionExpression' ||
|
|
247
|
-
node?.type === 'ArrowFunctionExpression' ||
|
|
248
|
-
node?.type === 'ClassDeclaration' ||
|
|
249
|
-
node?.type === 'ClassExpression'
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
240
|
/**
|
|
254
241
|
* @param {any} css
|
|
255
242
|
* @returns {Map<string, any>}
|
package/src/utils/ast.js
CHANGED
|
@@ -22,7 +22,7 @@ import * as b from './builders.js';
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* @param {AST.Node} node
|
|
25
|
-
* @returns {
|
|
25
|
+
* @returns {node is AST.Function}
|
|
26
26
|
*/
|
|
27
27
|
export function is_function_node(node) {
|
|
28
28
|
return (
|
|
@@ -32,6 +32,14 @@ export function is_function_node(node) {
|
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @param {AST.Node | null | undefined} node
|
|
37
|
+
* @returns {node is AST.Function | AST.ClassDeclaration | AST.ClassExpression}
|
|
38
|
+
*/
|
|
39
|
+
export function is_function_or_class_node(node) {
|
|
40
|
+
return !!node && (is_function_node(node) || is_class_node(node));
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
/**
|
|
36
44
|
* @param {AST.Node} node
|
|
37
45
|
* @returns {boolean}
|
|
@@ -48,6 +56,87 @@ export function is_class_node(node) {
|
|
|
48
56
|
return node.type === 'ClassExpression' || node.type === 'ClassDeclaration';
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
/**
|
|
60
|
+
* A parsed `@if`/`@for`/`@switch`/`@try` control-flow directive.
|
|
61
|
+
*
|
|
62
|
+
* @param {AST.Node | null | undefined} node
|
|
63
|
+
* @returns {node is AST.JSXTemplateDirective}
|
|
64
|
+
*/
|
|
65
|
+
export function is_template_directive(node) {
|
|
66
|
+
return (
|
|
67
|
+
node?.type === 'JSXIfExpression' ||
|
|
68
|
+
node?.type === 'JSXForExpression' ||
|
|
69
|
+
node?.type === 'JSXSwitchExpression' ||
|
|
70
|
+
node?.type === 'JSXTryExpression'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Any source AST node that can be the rendered output of a TSRX template or
|
|
76
|
+
* statement container.
|
|
77
|
+
*
|
|
78
|
+
* @param {AST.Node | null | undefined} node
|
|
79
|
+
* @returns {node is AST.TSRXRenderOutput}
|
|
80
|
+
*/
|
|
81
|
+
export function is_tsrx_render_output_node(node) {
|
|
82
|
+
return !!(
|
|
83
|
+
node &&
|
|
84
|
+
(node.type === 'JSXElement' ||
|
|
85
|
+
node.type === 'JSXFragment' ||
|
|
86
|
+
node.type === 'JSXStyleElement' ||
|
|
87
|
+
node.type === 'JSXCodeBlock' ||
|
|
88
|
+
is_template_directive(node))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {AST.Node | null | undefined} node
|
|
94
|
+
* @param {AST.Node | null | undefined} parent
|
|
95
|
+
* @returns {node is AST.JSXCodeBlock}
|
|
96
|
+
*/
|
|
97
|
+
export function is_code_block_function_body(node, parent) {
|
|
98
|
+
return !!(
|
|
99
|
+
node &&
|
|
100
|
+
node.type === 'JSXCodeBlock' &&
|
|
101
|
+
parent &&
|
|
102
|
+
is_function_node(parent) &&
|
|
103
|
+
parent.body === node
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns whether `child` directly occupies a statement slot of `parent`.
|
|
109
|
+
* Parser-native TSRX output nodes can appear in these slots without an
|
|
110
|
+
* `ExpressionStatement` wrapper, notably as braceless control-flow bodies.
|
|
111
|
+
*
|
|
112
|
+
* @param {AST.Node} parent
|
|
113
|
+
* @param {AST.Node} child
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
export function is_statement_position(parent, child) {
|
|
117
|
+
switch (parent.type) {
|
|
118
|
+
case 'Program':
|
|
119
|
+
case 'BlockStatement':
|
|
120
|
+
return parent.body.includes(/** @type {AST.Statement} */ (child));
|
|
121
|
+
case 'SwitchCase':
|
|
122
|
+
return parent.consequent.includes(/** @type {AST.Statement} */ (child));
|
|
123
|
+
case 'JSXCodeBlock':
|
|
124
|
+
return parent.body.includes(/** @type {AST.Statement} */ (child));
|
|
125
|
+
case 'IfStatement':
|
|
126
|
+
return parent.consequent === child || parent.alternate === child;
|
|
127
|
+
case 'ForStatement':
|
|
128
|
+
case 'ForInStatement':
|
|
129
|
+
case 'ForOfStatement':
|
|
130
|
+
case 'WhileStatement':
|
|
131
|
+
case 'DoWhileStatement':
|
|
132
|
+
case 'LabeledStatement':
|
|
133
|
+
case 'WithStatement':
|
|
134
|
+
return parent.body === child;
|
|
135
|
+
default:
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
51
140
|
/**
|
|
52
141
|
* Returns the closest native TSRX function in an ancestry path. By default,
|
|
53
142
|
* function and class boundaries stop the search so callers only match direct
|
package/types/index.d.ts
CHANGED
|
@@ -445,6 +445,14 @@ declare module 'estree' {
|
|
|
445
445
|
| JSXSwitchExpression
|
|
446
446
|
| JSXTryExpression;
|
|
447
447
|
|
|
448
|
+
/** Any node that can be the rendered output of a TSRX template or statement container. */
|
|
449
|
+
type TSRXRenderOutput =
|
|
450
|
+
| ESTreeJSX.JSXElement
|
|
451
|
+
| ESTreeJSX.JSXFragment
|
|
452
|
+
| JSXStyleElement
|
|
453
|
+
| JSXCodeBlock
|
|
454
|
+
| JSXTemplateDirective;
|
|
455
|
+
|
|
448
456
|
interface ParenthesizedExpression extends AST.BaseNode {
|
|
449
457
|
type: 'ParenthesizedExpression';
|
|
450
458
|
expression: AST.Expression;
|
|
@@ -1269,6 +1277,30 @@ export interface AnalyzeOptions extends ParseOptions, Pick<CompileOptions, 'mode
|
|
|
1269
1277
|
to_ts?: boolean;
|
|
1270
1278
|
}
|
|
1271
1279
|
|
|
1280
|
+
/** Options for the target-neutral TSRX semantic analysis pass. */
|
|
1281
|
+
export interface TSRXAnalysisOptions extends ParseOptions {
|
|
1282
|
+
typeOnly?: boolean;
|
|
1283
|
+
to_ts?: boolean;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/** Traversal state used by the target-neutral TSRX semantic analysis pass. */
|
|
1287
|
+
export interface TSRXAnalysisState {
|
|
1288
|
+
filename: string | null;
|
|
1289
|
+
collect: boolean;
|
|
1290
|
+
errors: CompileError[];
|
|
1291
|
+
comments: AST.CommentWithLocation[];
|
|
1292
|
+
function: AST.Function | null;
|
|
1293
|
+
function_body_is_code_block: boolean;
|
|
1294
|
+
inside_template_output: boolean;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/** Result of target-neutral TSRX semantic analysis. */
|
|
1298
|
+
export interface TSRXAnalysisResult {
|
|
1299
|
+
ast: AST.Program;
|
|
1300
|
+
errors: CompileError[];
|
|
1301
|
+
comments: AST.CommentWithLocation[];
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1272
1304
|
/**
|
|
1273
1305
|
* Result of parsing operation
|
|
1274
1306
|
*/
|