@litsx/babel-preset-litsx 0.2.0 → 0.3.0

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.
@@ -0,0 +1,360 @@
1
+ import jsxSyntaxPlugin from "@babel/plugin-syntax-jsx";
2
+ import { decodeVirtualAttributeName } from "@litsx/jsx-authoring";
3
+
4
+ let t;
5
+
6
+ function createHostReferenceExpression() {
7
+ return t.conditionalExpression(
8
+ t.binaryExpression(
9
+ "===",
10
+ t.unaryExpression("typeof", t.thisExpression(), true),
11
+ t.stringLiteral("undefined")
12
+ ),
13
+ t.nullLiteral(),
14
+ t.thisExpression()
15
+ );
16
+ }
17
+
18
+ function stringifyJsxName(nameNode) {
19
+ if (t.isJSXIdentifier(nameNode)) {
20
+ return nameNode.name;
21
+ }
22
+
23
+ if (t.isJSXMemberExpression(nameNode)) {
24
+ return `${stringifyJsxName(nameNode.object)}.${nameNode.property.name}`;
25
+ }
26
+
27
+ if (t.isJSXNamespacedName(nameNode)) {
28
+ return `${nameNode.namespace.name}:${nameNode.name.name}`;
29
+ }
30
+
31
+ return "unknown";
32
+ }
33
+
34
+ function getTag(node) {
35
+ const name = stringifyJsxName(node.name);
36
+ const isCapitalized =
37
+ name.charAt(0) === name.charAt(0).toUpperCase() &&
38
+ name.charAt(0) !== name.charAt(0).toLowerCase();
39
+ const isComponent =
40
+ node.name.type !== "JSXIdentifier" || isCapitalized || name.includes("-");
41
+ return { name, isComponent };
42
+ }
43
+
44
+ function unwrapExpression(node) {
45
+ let current = node;
46
+
47
+ while (current) {
48
+ if (t.isParenthesizedExpression?.(current)) {
49
+ current = current.expression;
50
+ continue;
51
+ }
52
+
53
+ if (
54
+ t.isTSAsExpression?.(current) ||
55
+ t.isTSSatisfiesExpression?.(current) ||
56
+ t.isTypeCastExpression?.(current) ||
57
+ t.isTSNonNullExpression?.(current)
58
+ ) {
59
+ current = current.expression;
60
+ continue;
61
+ }
62
+
63
+ break;
64
+ }
65
+
66
+ return current;
67
+ }
68
+
69
+ function getFunctionNodeFromBinding(binding) {
70
+ if (!binding?.path) {
71
+ return null;
72
+ }
73
+
74
+ if (binding.path.isFunctionDeclaration()) {
75
+ return binding.path.node;
76
+ }
77
+
78
+ if (binding.path.isVariableDeclarator()) {
79
+ const init = unwrapExpression(binding.path.node.init);
80
+ if (t.isArrowFunctionExpression(init) || t.isFunctionExpression(init)) {
81
+ return init;
82
+ }
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ function mergeBooleanResults(results) {
89
+ return results.some(Boolean);
90
+ }
91
+
92
+ function jsxTreeNeedsRendererContext(node, scope, seenBindings = new Set()) {
93
+ if (!node) {
94
+ return false;
95
+ }
96
+
97
+ if (t.isJSXFragment(node)) {
98
+ return mergeBooleanResults(
99
+ node.children.map((child) => jsxChildNeedsRendererContext(child, scope, seenBindings))
100
+ );
101
+ }
102
+
103
+ if (!t.isJSXElement(node)) {
104
+ return false;
105
+ }
106
+
107
+ const { isComponent } = getTag(node.openingElement);
108
+ if (isComponent) {
109
+ return true;
110
+ }
111
+
112
+ const childNeedsContext = mergeBooleanResults(
113
+ node.children.map((child) => jsxChildNeedsRendererContext(child, scope, seenBindings))
114
+ );
115
+
116
+ if (childNeedsContext) {
117
+ return true;
118
+ }
119
+
120
+ return mergeBooleanResults(
121
+ node.openingElement.attributes.map((attr) => {
122
+ if (!t.isJSXAttribute(attr) || !t.isJSXExpressionContainer(attr.value)) {
123
+ return false;
124
+ }
125
+ return expressionNeedsRendererContext(attr.value.expression, scope, seenBindings);
126
+ })
127
+ );
128
+ }
129
+
130
+ function jsxChildNeedsRendererContext(child, scope, seenBindings) {
131
+ if (t.isJSXElement(child) || t.isJSXFragment(child)) {
132
+ return jsxTreeNeedsRendererContext(child, scope, seenBindings);
133
+ }
134
+
135
+ if (t.isJSXExpressionContainer(child)) {
136
+ return expressionNeedsRendererContext(child.expression, scope, seenBindings);
137
+ }
138
+
139
+ return false;
140
+ }
141
+
142
+ function functionBodyNeedsRendererContext(body, scope, seenBindings = new Set()) {
143
+ if (!body) {
144
+ return false;
145
+ }
146
+
147
+ if (t.isBlockStatement(body)) {
148
+ return mergeBooleanResults(
149
+ body.body.map((statement) => statementNeedsRendererContext(statement, scope, seenBindings))
150
+ );
151
+ }
152
+
153
+ return expressionNeedsRendererContext(body, scope, seenBindings);
154
+ }
155
+
156
+ function statementNeedsRendererContext(statement, scope, seenBindings) {
157
+ if (t.isReturnStatement(statement)) {
158
+ return expressionNeedsRendererContext(statement.argument, scope, seenBindings);
159
+ }
160
+
161
+ if (t.isIfStatement(statement)) {
162
+ return mergeBooleanResults([
163
+ statementNeedsRendererContext(statement.consequent, scope, seenBindings),
164
+ statement.alternate
165
+ ? statementNeedsRendererContext(statement.alternate, scope, seenBindings)
166
+ : false,
167
+ ]);
168
+ }
169
+
170
+ if (t.isBlockStatement(statement)) {
171
+ return functionBodyNeedsRendererContext(statement, scope, seenBindings);
172
+ }
173
+
174
+ return false;
175
+ }
176
+
177
+ function callExpressionNeedsRendererContext(node, scope, seenBindings) {
178
+ const callee = unwrapExpression(node.callee);
179
+ if (!t.isIdentifier(callee)) {
180
+ return false;
181
+ }
182
+
183
+ const binding = scope.getBinding(callee.name);
184
+ const functionNode = getFunctionNodeFromBinding(binding);
185
+ if (!functionNode) {
186
+ return false;
187
+ }
188
+
189
+ if (seenBindings.has(binding)) {
190
+ return false;
191
+ }
192
+
193
+ const nextSeenBindings = new Set(seenBindings);
194
+ nextSeenBindings.add(binding);
195
+ return functionBodyNeedsRendererContext(functionNode.body, binding.path.scope, nextSeenBindings);
196
+ }
197
+
198
+ function expressionNeedsRendererContext(node, scope, seenBindings = new Set()) {
199
+ const expression = unwrapExpression(node);
200
+ if (!expression) {
201
+ return false;
202
+ }
203
+
204
+ if (t.isJSXElement(expression) || t.isJSXFragment(expression)) {
205
+ return jsxTreeNeedsRendererContext(expression, scope, seenBindings);
206
+ }
207
+
208
+ if (t.isConditionalExpression(expression)) {
209
+ return mergeBooleanResults([
210
+ expressionNeedsRendererContext(expression.consequent, scope, seenBindings),
211
+ expressionNeedsRendererContext(expression.alternate, scope, seenBindings),
212
+ ]);
213
+ }
214
+
215
+ if (t.isLogicalExpression(expression)) {
216
+ return mergeBooleanResults([
217
+ expressionNeedsRendererContext(expression.left, scope, seenBindings),
218
+ expressionNeedsRendererContext(expression.right, scope, seenBindings),
219
+ ]);
220
+ }
221
+
222
+ if (t.isSequenceExpression(expression)) {
223
+ return mergeBooleanResults(
224
+ expression.expressions.map((part) => expressionNeedsRendererContext(part, scope, seenBindings))
225
+ );
226
+ }
227
+
228
+ if (t.isArrayExpression(expression)) {
229
+ return mergeBooleanResults(
230
+ expression.elements.filter(Boolean).map((part) => expressionNeedsRendererContext(part, scope, seenBindings))
231
+ );
232
+ }
233
+
234
+ if (t.isCallExpression(expression)) {
235
+ return callExpressionNeedsRendererContext(expression, scope, seenBindings);
236
+ }
237
+
238
+ return false;
239
+ }
240
+
241
+ function isBindableFunctionReference(expressionPath) {
242
+ const expression = unwrapExpression(expressionPath.node);
243
+ if (
244
+ t.isArrowFunctionExpression(expression) ||
245
+ t.isFunctionExpression(expression)
246
+ ) {
247
+ return functionBodyNeedsRendererContext(expression.body, expressionPath.scope);
248
+ }
249
+
250
+ if (t.isIdentifier(expression)) {
251
+ const binding = expressionPath.scope.getBinding(expression.name);
252
+ const functionNode = getFunctionNodeFromBinding(binding);
253
+ if (!functionNode) {
254
+ return false;
255
+ }
256
+ return functionBodyNeedsRendererContext(functionNode.body, binding.path.scope, new Set([binding]));
257
+ }
258
+
259
+ return false;
260
+ }
261
+
262
+ function shouldBindRendererContext(attributePath, rawName, expressionPath) {
263
+ if (typeof rawName !== "string" || rawName[0] !== ".") {
264
+ return false;
265
+ }
266
+
267
+ const openingElement = attributePath.parentPath;
268
+ if (!openingElement?.isJSXOpeningElement()) {
269
+ return false;
270
+ }
271
+
272
+ const { isComponent } = getTag(openingElement.node);
273
+ if (!isComponent) {
274
+ return false;
275
+ }
276
+
277
+ return isBindableFunctionReference(expressionPath);
278
+ }
279
+
280
+ function ensureRendererBindingImport(programPath) {
281
+ const bodyPaths = programPath.get("body");
282
+ const runtimeImports = bodyPaths.filter(
283
+ (path) =>
284
+ path.isImportDeclaration() &&
285
+ path.node.source.value === "@litsx/litsx/internal/runtime-render-context"
286
+ );
287
+
288
+ const importSpecifier = t.importSpecifier(
289
+ t.identifier("bindRendererContext"),
290
+ t.identifier("bindRendererContext")
291
+ );
292
+
293
+ for (const importPath of runtimeImports) {
294
+ const { specifiers } = importPath.node;
295
+ const hasImport = specifiers.some(
296
+ (specifier) =>
297
+ t.isImportSpecifier(specifier) &&
298
+ t.isIdentifier(specifier.imported, { name: "bindRendererContext" })
299
+ );
300
+
301
+ if (hasImport) {
302
+ return;
303
+ }
304
+
305
+ specifiers.push(importSpecifier);
306
+ return;
307
+ }
308
+
309
+ programPath.unshiftContainer("body", t.importDeclaration(
310
+ [importSpecifier],
311
+ t.stringLiteral("@litsx/litsx/internal/runtime-render-context")
312
+ ));
313
+ }
314
+
315
+ export default function transformLitsxRendererProps(api) {
316
+ api.assertVersion?.(7);
317
+ t = api.types;
318
+
319
+ return {
320
+ name: "transform-litsx-renderer-props",
321
+ inherits: jsxSyntaxPlugin.default || jsxSyntaxPlugin,
322
+ visitor: {
323
+ Program: {
324
+ enter(_, state) {
325
+ state.__litsxNeedsRendererBindingImport = false;
326
+ },
327
+ exit(programPath, state) {
328
+ if (state.__litsxNeedsRendererBindingImport) {
329
+ ensureRendererBindingImport(programPath);
330
+ }
331
+ },
332
+ },
333
+ JSXAttribute(path, state) {
334
+ const { node } = path;
335
+ if (node.value?.type !== "JSXExpressionContainer") {
336
+ return;
337
+ }
338
+
339
+ const rawName = decodeVirtualAttributeName(node.name.name) ?? node.name.name;
340
+ const expressionPath = path.get("value.expression");
341
+ if (!expressionPath?.node) {
342
+ return;
343
+ }
344
+
345
+ if (!shouldBindRendererContext(path, rawName, expressionPath)) {
346
+ return;
347
+ }
348
+
349
+ state.__litsxNeedsRendererBindingImport = true;
350
+ node.value.expression = t.callExpression(
351
+ t.identifier("bindRendererContext"),
352
+ [
353
+ createHostReferenceExpression(),
354
+ expressionPath.node,
355
+ ]
356
+ );
357
+ },
358
+ },
359
+ };
360
+ }
@@ -1,5 +1,14 @@
1
1
  let t;
2
2
 
3
+ function isCapitalizedComponentName(name) {
4
+ if (typeof name !== "string" || name.length === 0) {
5
+ return false;
6
+ }
7
+
8
+ const first = name[0];
9
+ return first === first.toUpperCase() && first !== first.toLowerCase();
10
+ }
11
+
3
12
  export function setWrapperUtilsBabelTypes(nextTypes) {
4
13
  t = nextTypes;
5
14
  }
@@ -67,6 +76,7 @@ export function maybeTransformWrappedVariableDeclarator({
67
76
  {
68
77
  ...resolvedPluginOptions,
69
78
  ...wrapperMeta.options,
79
+ state,
70
80
  typeResolver: state?.__litsxTypeResolver,
71
81
  warn: (warning) => {
72
82
  state?.__litsxWarnings?.push(warning);
@@ -129,12 +139,17 @@ export function handlePotentialComponentExport({
129
139
  exportName = declaration.declarations[0].id.name;
130
140
  }
131
141
 
142
+ if (exportName && !isCapitalizedComponentName(exportName)) {
143
+ return false;
144
+ }
145
+
132
146
  const classNode = transformFunction(
133
147
  declarationPath,
134
148
  exportPath.findParent((p) => p.isProgram()),
135
149
  exportName,
136
150
  {
137
151
  ...state?.__litsxResolvedPluginOptions,
152
+ state,
138
153
  typeResolver,
139
154
  warn: (warning) => {
140
155
  state?.__litsxWarnings?.push(warning);
@@ -170,6 +185,10 @@ export function handlePotentialComponentExport({
170
185
  : undefined;
171
186
  const programPath = exportPath.findParent((p) => p.isProgram());
172
187
 
188
+ if (exportName && !isCapitalizedComponentName(exportName)) {
189
+ return false;
190
+ }
191
+
173
192
  if (initPath.isCallExpression()) {
174
193
  const wrapperMeta = getWrapperMetadata(initPath);
175
194
  if (!wrapperMeta) return false;
@@ -184,6 +203,7 @@ export function handlePotentialComponentExport({
184
203
  {
185
204
  ...state?.__litsxResolvedPluginOptions,
186
205
  ...wrapperMeta.options,
206
+ state,
187
207
  typeResolver,
188
208
  warn: (warning) => {
189
209
  state?.__litsxWarnings?.push(warning);
@@ -196,7 +216,11 @@ export function handlePotentialComponentExport({
196
216
  exportPath.scope.removeBinding(exportName);
197
217
  }
198
218
 
199
- exportPath.replaceWith(t.exportNamedDeclaration(classNode, []));
219
+ exportPath.replaceWith(
220
+ isDefault
221
+ ? t.exportDefaultDeclaration(classNode)
222
+ : t.exportNamedDeclaration(classNode, [])
223
+ );
200
224
  exportPath.requeue();
201
225
  pruneWrapperImports(wrapperMeta);
202
226
  updateTransformState?.(state, classNode);
@@ -217,6 +241,10 @@ export function handlePotentialComponentExport({
217
241
  ? wrapperMeta.functionPath.node.id.name
218
242
  : undefined;
219
243
 
244
+ if (!inferredName || !isCapitalizedComponentName(inferredName)) {
245
+ return false;
246
+ }
247
+
220
248
  const classNode = transformFunction(
221
249
  wrapperMeta.functionPath,
222
250
  programPath,
@@ -224,6 +252,7 @@ export function handlePotentialComponentExport({
224
252
  {
225
253
  ...state?.__litsxResolvedPluginOptions,
226
254
  ...wrapperMeta.options,
255
+ state,
227
256
  typeResolver,
228
257
  warn: (warning) => {
229
258
  state?.__litsxWarnings?.push(warning);
package/src/pipeline.js CHANGED
@@ -3,6 +3,7 @@ import transformLitsxScopedElements from "@litsx/babel-plugin-transform-litsx-sc
3
3
  import transformLitsxDomRefs from "./internal/transform-litsx-dom-refs.js";
4
4
  import transformLitsxHooks from "./internal/transform-litsx-hooks.js";
5
5
  import transformLitsxComponents from "./internal/transform-litsx-components.js";
6
+ import transformLitsxRendererProps from "./internal/transform-litsx-renderer-props.js";
6
7
 
7
8
  const NATIVE_TRANSFORM_OPTION_KEYS = [
8
9
  "defaultDomMode",
@@ -55,6 +56,7 @@ function shouldIncludeFeaturePlugin(sourceFeatures, key) {
55
56
 
56
57
  export function createLitsxPresetPlugins(options = {}, sourceFeatures = null) {
57
58
  const plugins = [
59
+ [transformLitsxRendererProps, options.transformLitsxRendererProps || {}],
58
60
  [transformLitsxComponents, normalizeTransformLitsxOptions(options)],
59
61
  ];
60
62