@litsx/babel-preset-litsx 0.2.1 → 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.
@@ -44,6 +44,15 @@ import {
44
44
 
45
45
  let t;
46
46
 
47
+ function isCapitalizedComponentName(name) {
48
+ if (typeof name !== "string" || name.length === 0) {
49
+ return false;
50
+ }
51
+
52
+ const first = name[0];
53
+ return first === first.toUpperCase() && first !== first.toLowerCase();
54
+ }
55
+
47
56
  export function createTransformFunctionToClassPlugin(defaultPluginOptions = {}) {
48
57
  return function transformFunctionToClassPlugin(_api, pluginOptions = {}) {
49
58
  ensureTypescriptModule();
@@ -76,6 +85,7 @@ export function createTransformFunctionToClassPlugin(defaultPluginOptions = {})
76
85
  this.__litsxNeedsStaticHoistsMixin = false;
77
86
  this.__litsxNeedsLightDomMixin = false;
78
87
  this.__litsxNeedsCallbackRef = false;
88
+ this.__litsxNeedsRendererCallImport = false;
79
89
  this.__litsxWarnings = [];
80
90
  this.__litsxResolvedPluginOptions = resolvedPluginOptions;
81
91
  this.__litsxTypeResolver = fileLikelyNeedsTypeResolver(this)
@@ -142,15 +152,21 @@ export function createTransformFunctionToClassPlugin(defaultPluginOptions = {})
142
152
  return;
143
153
  }
144
154
 
145
- if (initPath && initPath.isArrowFunctionExpression() && !isInsideFunctionOrClass(varPath)) {
155
+ if (
156
+ initPath &&
157
+ initPath.isArrowFunctionExpression() &&
158
+ !isInsideFunctionOrClass(varPath) &&
159
+ t.isIdentifier(varPath.node.id) &&
160
+ isCapitalizedComponentName(varPath.node.id.name)
161
+ ) {
146
162
  const programPath = varPath.findParent((p) => p.isProgram());
147
- const elementCandidates = collectElementCandidates(initPath, programPath);
148
163
  const classNode = transformFunction(
149
164
  initPath,
150
165
  programPath,
151
166
  varPath.node.id.name,
152
167
  {
153
168
  ...resolvedPluginOptions,
169
+ state: this,
154
170
  typeResolver: getTypeResolverForFunction(initPath, this),
155
171
  warn: (warning) => {
156
172
  this.__litsxWarnings.push(warning);
@@ -160,12 +176,6 @@ export function createTransformFunctionToClassPlugin(defaultPluginOptions = {})
160
176
 
161
177
  if (!classNode) return;
162
178
 
163
- if (elementCandidates.size) {
164
- classNode._litsxElementCandidates &&= new Set(classNode._litsxElementCandidates);
165
- const elementSet = classNode._litsxElementCandidates ||= new Set();
166
- elementCandidates.forEach((candidate) => elementSet.add(candidate));
167
- }
168
-
169
179
  const declarationPath = varPath.parentPath;
170
180
  if (!declarationPath.isVariableDeclaration()) return;
171
181
 
@@ -176,15 +186,21 @@ export function createTransformFunctionToClassPlugin(defaultPluginOptions = {})
176
186
  }
177
187
  },
178
188
  FunctionDeclaration(funcPath) {
179
- if (!isInsideFunctionOrClass(funcPath)) {
189
+ if (
190
+ !funcPath.parentPath?.isExportNamedDeclaration?.() &&
191
+ !funcPath.parentPath?.isExportDefaultDeclaration?.() &&
192
+ !isInsideFunctionOrClass(funcPath) &&
193
+ funcPath.node.id &&
194
+ isCapitalizedComponentName(funcPath.node.id.name)
195
+ ) {
180
196
  const programPath = funcPath.findParent((p) => p.isProgram());
181
- const elementCandidates = collectElementCandidates(funcPath, programPath);
182
197
  const classNode = transformFunction(
183
198
  funcPath,
184
199
  programPath,
185
200
  undefined,
186
201
  {
187
202
  ...resolvedPluginOptions,
203
+ state: this,
188
204
  typeResolver: getTypeResolverForFunction(funcPath, this),
189
205
  warn: (warning) => {
190
206
  this.__litsxWarnings.push(warning);
@@ -197,11 +213,6 @@ export function createTransformFunctionToClassPlugin(defaultPluginOptions = {})
197
213
  if (funcPath.node.id) {
198
214
  funcPath.scope.removeBinding(funcPath.node.id.name);
199
215
  }
200
- if (elementCandidates.size) {
201
- classNode._litsxElementCandidates &&= new Set(classNode._litsxElementCandidates);
202
- const elementSet = classNode._litsxElementCandidates ||= new Set();
203
- elementCandidates.forEach((candidate) => elementSet.add(candidate));
204
- }
205
216
  funcPath.replaceWith(classNode);
206
217
  funcPath.requeue();
207
218
  updateTransformState(this, classNode);
@@ -213,6 +224,7 @@ export function createTransformFunctionToClassPlugin(defaultPluginOptions = {})
213
224
  }
214
225
 
215
226
  export default createTransformFunctionToClassPlugin();
227
+ export { isCapitalizedComponentName };
216
228
 
217
229
  function getOrCreateModuleStaticHoistSymbol(programPath, hoistName) {
218
230
  let symbolMap = programPath.getData("__litsxStaticHoistSymbols");
@@ -353,6 +365,7 @@ function getTypeResolverForFunction(functionPath, state) {
353
365
 
354
366
  function transformFunction(functionPath, programPath, className, options = {}) {
355
367
  const { node } = functionPath;
368
+ const elementCandidates = collectElementCandidates(functionPath, programPath, options);
356
369
  const forwardRefOptions = options.forwardRef || null;
357
370
  let resolvedName = className;
358
371
  if (!resolvedName && node && node.id && t.isIdentifier(node.id)) {
@@ -384,7 +397,7 @@ function transformFunction(functionPath, programPath, className, options = {}) {
384
397
  ReturnStatement(returnPath) {
385
398
  if (t.isJSXElement(returnPath.node.argument)) {
386
399
  returnStatement = returnPath.node;
387
- transformJSXExpressions(returnPath, bindings);
400
+ transformJSXExpressions(returnPath, bindings, options.state ?? null);
388
401
  }
389
402
  },
390
403
  });
@@ -466,7 +479,7 @@ function transformFunction(functionPath, programPath, className, options = {}) {
466
479
  createHandlerClassMember,
467
480
  });
468
481
 
469
- return createComponentClass({
482
+ const classNode = createComponentClass({
470
483
  className,
471
484
  classMembers,
472
485
  hoistMembers,
@@ -477,6 +490,14 @@ function transformFunction(functionPath, programPath, className, options = {}) {
477
490
  needsUnsafeCss,
478
491
  needsCallbackRef,
479
492
  });
493
+
494
+ if (classNode && elementCandidates.size) {
495
+ classNode._litsxElementCandidates &&= new Set(classNode._litsxElementCandidates);
496
+ const elementSet = classNode._litsxElementCandidates ||= new Set();
497
+ elementCandidates.forEach((candidate) => elementSet.add(candidate));
498
+ }
499
+
500
+ return classNode;
480
501
  }
481
502
 
482
503
  function ensureClassIdentifier(classNode, fallbackName) {
@@ -514,36 +535,163 @@ function createNestedInitializerStatement(pattern, root, defaultValue, t) {
514
535
  ]);
515
536
  }
516
537
 
517
- function collectElementCandidates(functionPath, programPath) {
538
+ function collectElementCandidates(functionPath, programPath, options = {}) {
518
539
  const candidates = new Set();
519
540
  if (!programPath) return candidates;
541
+ programPath.scope.crawl();
542
+ const compatPascalNames =
543
+ programPath.getData("__litsxCompatPascalNames") || new Set();
520
544
 
521
- const importNames = new Set();
545
+ const availableNames = new Set();
546
+ const helperPaths = new Map();
522
547
  programPath.get("body").forEach((nodePath) => {
523
- if (!nodePath.isImportDeclaration()) return;
524
- nodePath.node.specifiers.forEach((specifier) => {
525
- if (specifier.local) {
526
- importNames.add(specifier.local.name);
548
+ if (nodePath.isImportDeclaration()) {
549
+ nodePath.node.specifiers.forEach((specifier) => {
550
+ if (specifier.local?.name) {
551
+ availableNames.add(specifier.local.name);
552
+ }
553
+ });
554
+ return;
555
+ }
556
+
557
+ if (nodePath.isClassDeclaration() && nodePath.node.id?.name) {
558
+ availableNames.add(nodePath.node.id.name);
559
+ return;
560
+ }
561
+
562
+ if (
563
+ (nodePath.isExportNamedDeclaration() || nodePath.isExportDefaultDeclaration()) &&
564
+ nodePath.get("declaration")?.isClassDeclaration?.() &&
565
+ nodePath.node.declaration?.id?.name
566
+ ) {
567
+ availableNames.add(nodePath.node.declaration.id.name);
568
+ return;
569
+ }
570
+
571
+ if (nodePath.isFunctionDeclaration() && nodePath.node.id?.name) {
572
+ availableNames.add(nodePath.node.id.name);
573
+ helperPaths.set(nodePath.node.id.name, nodePath);
574
+ return;
575
+ }
576
+
577
+ if (!nodePath.isVariableDeclaration()) return;
578
+ nodePath.get("declarations").forEach((declaratorPath) => {
579
+ const declarator = declaratorPath.node;
580
+ if (!t.isIdentifier(declarator.id)) {
581
+ return;
582
+ }
583
+
584
+ availableNames.add(declarator.id.name);
585
+
586
+ const initPath = declaratorPath.get("init");
587
+ if (
588
+ initPath?.isArrowFunctionExpression?.() ||
589
+ initPath?.isFunctionExpression?.()
590
+ ) {
591
+ helperPaths.set(declarator.id.name, initPath);
527
592
  }
528
593
  });
529
594
  });
530
595
 
531
- functionPath.traverse({
532
- JSXOpeningElement(path) {
533
- if (!path.node.name || path.node.name.type !== "JSXIdentifier") return;
534
- const originalName = path.node.name.name;
535
- if (!importNames.has(originalName)) return;
596
+ const helperCandidateCache = new Map();
536
597
 
537
- path.node.__scopedOriginal = originalName;
538
- candidates.add(originalName);
539
- },
540
- JSXClosingElement(path) {
541
- if (!path.node.name || path.node.name.type !== "JSXIdentifier") return;
542
- const originalName = path.node.name.name;
543
- if (!importNames.has(originalName)) return;
544
- path.node.__scopedOriginal = originalName;
545
- },
546
- });
598
+ function isCapitalizedName(name) {
599
+ if (typeof name !== "string" || name.length === 0) {
600
+ return false;
601
+ }
602
+
603
+ const first = name[0];
604
+ return first === first.toUpperCase() && first !== first.toLowerCase();
605
+ }
606
+
607
+ function toKebab(name) {
608
+ return name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
609
+ }
610
+
611
+ function isProgramLevelBinding(binding) {
612
+ return binding?.scope?.path?.isProgram?.() === true;
613
+ }
614
+
615
+ function maybeRewriteComponentName(nameNode, pathForErrors = null) {
616
+ if (!nameNode || nameNode.type !== "JSXIdentifier") return null;
617
+ const originalName = nameNode.__scopedOriginal || nameNode.name;
618
+ if (!isCapitalizedName(originalName)) return null;
619
+ const binding = pathForErrors?.scope?.getBinding?.(originalName) || null;
620
+ if (!binding) {
621
+ if (availableNames.has(originalName)) {
622
+ return originalName;
623
+ }
624
+ if (compatPascalNames.has(originalName)) {
625
+ return null;
626
+ }
627
+ if (options?.allowUnknownPascalCase === true) {
628
+ return null;
629
+ }
630
+ throw (pathForErrors?.buildCodeFrameError?.(
631
+ `Unknown LitSX component "${originalName}". Add an import or declare it in this module before using it in JSX.`
632
+ ) || new Error(
633
+ `Unknown LitSX component "${originalName}". Add an import or declare it in this module before using it in JSX.`
634
+ ));
635
+ }
636
+
637
+ if (!isProgramLevelBinding(binding)) {
638
+ return null;
639
+ }
640
+
641
+ return originalName;
642
+ }
643
+
644
+ function scanFunction(path, seen = new Set()) {
645
+ if (!path?.node) {
646
+ return new Set();
647
+ }
648
+
649
+ if (helperCandidateCache.has(path.node)) {
650
+ return new Set(helperCandidateCache.get(path.node));
651
+ }
652
+
653
+ if (seen.has(path.node)) {
654
+ return new Set();
655
+ }
656
+
657
+ const nextSeen = new Set(seen);
658
+ nextSeen.add(path.node);
659
+ const localCandidates = new Set();
660
+ const referencedHelpers = new Set();
661
+
662
+ path.traverse({
663
+ JSXOpeningElement(jsxPath) {
664
+ const candidate = maybeRewriteComponentName(jsxPath.node.name, jsxPath);
665
+ if (candidate) {
666
+ localCandidates.add(candidate);
667
+ }
668
+ },
669
+ JSXClosingElement(jsxPath) {
670
+ maybeRewriteComponentName(jsxPath.node.name, jsxPath);
671
+ },
672
+ Identifier(identifierPath) {
673
+ if (!identifierPath.isReferencedIdentifier()) {
674
+ return;
675
+ }
676
+
677
+ if (!helperPaths.has(identifierPath.node.name)) {
678
+ return;
679
+ }
680
+
681
+ referencedHelpers.add(identifierPath.node.name);
682
+ },
683
+ });
684
+
685
+ referencedHelpers.forEach((helperName) => {
686
+ const helperCandidates = scanFunction(helperPaths.get(helperName), nextSeen);
687
+ helperCandidates.forEach((candidate) => localCandidates.add(candidate));
688
+ });
689
+
690
+ helperCandidateCache.set(path.node, new Set(localCandidates));
691
+ return localCandidates;
692
+ }
693
+
694
+ scanFunction(functionPath).forEach((candidate) => candidates.add(candidate));
547
695
 
548
696
  return candidates;
549
697
  }
@@ -8,11 +8,82 @@ function createThisMemberExpression(propName) {
8
8
  return t.memberExpression(t.thisExpression(), t.identifier(propName));
9
9
  }
10
10
 
11
- export function transformJSXExpressions(jsxPath, bindings) {
11
+ function getBoundPropName(bindingInfo) {
12
+ if (typeof bindingInfo === "string") {
13
+ return bindingInfo;
14
+ }
15
+
16
+ if (bindingInfo && typeof bindingInfo === "object") {
17
+ return bindingInfo.bindKey ?? null;
18
+ }
19
+
20
+ return null;
21
+ }
22
+
23
+ function isPropBackedCallee(node, localNames) {
24
+ if (t.isIdentifier(node)) {
25
+ return localNames.includes(node.name);
26
+ }
27
+
28
+ if (
29
+ t.isMemberExpression(node) &&
30
+ !node.computed &&
31
+ t.isIdentifier(node.object)
32
+ ) {
33
+ return localNames.includes(node.object.name) && node.object.name === "props";
34
+ }
35
+
36
+ return false;
37
+ }
38
+
39
+ function getPropBackedCalleeReplacement(node, bindings) {
40
+ if (t.isIdentifier(node)) {
41
+ const propName = getBoundPropName(bindings.get(node.name));
42
+ return propName ? createThisMemberExpression(propName) : node;
43
+ }
44
+
45
+ if (
46
+ t.isMemberExpression(node) &&
47
+ !node.computed &&
48
+ t.isIdentifier(node.object)
49
+ ) {
50
+ const propName = getBoundPropName(bindings.get(node.object.name));
51
+ if (!propName) {
52
+ return node;
53
+ }
54
+
55
+ return t.memberExpression(
56
+ createThisMemberExpression(propName),
57
+ t.cloneNode(node.property),
58
+ false
59
+ );
60
+ }
61
+
62
+ return node;
63
+ }
64
+
65
+ export function transformJSXExpressions(jsxPath, bindings, state = null) {
12
66
  const localNames = Array.from(bindings.keys());
13
67
 
14
68
  jsxPath.traverse({
15
69
  JSXExpressionContainer(expressionPath) {
70
+ if (t.isCallExpression(expressionPath.node.expression)) {
71
+ const { callee, arguments: args } = expressionPath.node.expression;
72
+ if (isPropBackedCallee(callee, localNames)) {
73
+ if (state) {
74
+ state.__litsxNeedsRendererCallImport = true;
75
+ }
76
+ expressionPath.node.expression = t.callExpression(
77
+ t.identifier("renderRendererCall"),
78
+ [
79
+ getPropBackedCalleeReplacement(callee, bindings),
80
+ ...args,
81
+ ]
82
+ );
83
+ return;
84
+ }
85
+ }
86
+
16
87
  if (t.isIdentifier(expressionPath.node.expression)) {
17
88
  const name = expressionPath.node.expression.name;
18
89
  if (localNames.includes(name)) {
@@ -22,6 +22,15 @@ function createLitsxInfrastructureImport(importedName) {
22
22
  );
23
23
  }
24
24
 
25
+ function createLitsxInternalRuntimeImport(importedName) {
26
+ return t.importDeclaration(
27
+ [
28
+ t.importSpecifier(t.identifier(importedName), t.identifier(importedName)),
29
+ ],
30
+ t.stringLiteral("@litsx/litsx/internal/runtime-render-context")
31
+ );
32
+ }
33
+
25
34
  function createLitsxImport(importedName) {
26
35
  return t.importDeclaration(
27
36
  [
@@ -225,5 +234,28 @@ export function finalizeProgram(programPath, state) {
225
234
  }
226
235
  }
227
236
 
237
+ if (state.__litsxNeedsRendererCallImport) {
238
+ const bodyPathsWithInternalRuntime = programPath.get("body");
239
+ const internalRuntimeImports = bodyPathsWithInternalRuntime.filter(
240
+ (n) =>
241
+ n.isImportDeclaration() &&
242
+ n.node.source.value === "@litsx/litsx/internal/runtime-render-context"
243
+ );
244
+
245
+ let internalRuntimeImported = false;
246
+ internalRuntimeImports.some((importPath) => {
247
+ if (ensureNamedImport(importPath, "renderRendererCall")) {
248
+ internalRuntimeImported = true;
249
+ return true;
250
+ }
251
+
252
+ return false;
253
+ });
254
+
255
+ if (!internalRuntimeImported) {
256
+ programPath.unshiftContainer("body", createLitsxInternalRuntimeImport("renderRendererCall"));
257
+ }
258
+ }
259
+
228
260
  pruneUnusedLitsxStaticImports(programPath);
229
261
  }