Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/compiler/app-core/app-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ const getModuleImports = (moduleMap: ModuleMap, filePath: string, importedModule
moduleFile.localImports.forEach((localImport) => {
getModuleImports(moduleMap, localImport, importedModules);
});

// Follow functional component dependencies resolved via typeChecker.
moduleFile.functionalComponentDeps?.forEach((depPath) => {
getModuleImports(moduleMap, depPath, importedModules);
});
}
return importedModules;
};
Expand Down
1 change: 1 addition & 0 deletions src/compiler/build/compiler-ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const getModuleLegacy = (compilerCtx: d.CompilerCtx, sourceFilePath: stri
isCollectionDependency: false,
isLegacy: false,
localImports: [],
functionalComponentDeps: [],
originalCollectionComponentPath: null,
originalImports: [],
potentialCmpRefs: [],
Expand Down
72 changes: 65 additions & 7 deletions src/compiler/transformers/static-to-meta/call-expression.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { normalizePath } from '@utils';
import ts from 'typescript';

import type * as d from '../../../declarations';
import { H } from '../core-runtime-apis';
import { gatherVdomMeta } from './vdom';

export const parseCallExpression = (m: d.Module | d.ComponentCompilerMeta, node: ts.CallExpression) => {
export const parseCallExpression = (
m: d.Module | d.ComponentCompilerMeta,
node: ts.CallExpression,
typeChecker?: ts.TypeChecker,
) => {
if (node.arguments != null && node.arguments.length > 0) {
if (ts.isIdentifier(node.expression)) {
// h('tag')
visitCallExpressionArgs(m, node.expression, node.arguments);
visitCallExpressionArgs(m, node.expression, node.arguments, typeChecker);
} else if (ts.isPropertyAccessExpression(node.expression)) {
// document.createElement('tag')
const n = node.expression.name;
if (ts.isIdentifier(n) && n) {
visitCallExpressionArgs(m, n, node.arguments);
visitCallExpressionArgs(m, n, node.arguments, typeChecker);
}
}
}
Expand All @@ -23,11 +28,12 @@ const visitCallExpressionArgs = (
m: d.Module | d.ComponentCompilerMeta,
callExpressionName: ts.Identifier,
args: ts.NodeArray<ts.Expression>,
typeChecker?: ts.TypeChecker,
) => {
const fnName = callExpressionName.escapedText as string;

if (fnName === 'h' || fnName === H || fnName === 'createElement') {
visitCallExpressionArg(m, args[0]);
visitCallExpressionArg(m, args[0], typeChecker);

if (fnName === 'h' || fnName === H) {
gatherVdomMeta(m, args);
Expand All @@ -42,14 +48,14 @@ const visitCallExpressionArgs = (
) {
// Handle jsx-runtime calls (jsx, jsxs, jsxDEV)
// These have the same signature as h() for metadata purposes
visitCallExpressionArg(m, args[0]);
visitCallExpressionArg(m, args[0], typeChecker);
gatherVdomMeta(m, args);
// TypeScript's jsx transform passes key as the 3rd argument
if (args.length > 2 && args[2]) {
m.hasVdomKey = true;
}
} else if (args.length > 1 && fnName === 'createElementNS') {
visitCallExpressionArg(m, args[1]);
visitCallExpressionArg(m, args[1], typeChecker);
} else if (fnName === 'require' && args.length > 0 && (m as d.Module).originalImports) {
const arg = args[0];
if (ts.isStringLiteral(arg)) {
Expand All @@ -60,7 +66,11 @@ const visitCallExpressionArgs = (
}
};

const visitCallExpressionArg = (m: d.Module | d.ComponentCompilerMeta, arg: ts.Expression) => {
const visitCallExpressionArg = (
m: d.Module | d.ComponentCompilerMeta,
arg: ts.Expression,
typeChecker?: ts.TypeChecker,
) => {
if (ts.isStringLiteral(arg)) {
let tag = arg.text;

Expand All @@ -72,5 +82,53 @@ const visitCallExpressionArg = (m: d.Module | d.ComponentCompilerMeta, arg: ts.E
m.potentialCmpRefs.push(tag);
}
}
} else if (typeChecker && ts.isIdentifier(arg)) {
// Handle functional component references like <MyIcon /> which compiles to h(MyIcon, ...)
// Use typeChecker to resolve the identifier to its source file
resolveFunctionalComponentDep(m, arg, typeChecker);
}
};

/**
* Resolves a functional component identifier to its source file and tracks it
* as a dependency so that build-conditionals can be properly propagated.
*
* @param m the module or component metadata to track the dependency on
* @param identifier the identifier node representing the functional component
* @param typeChecker the TypeScript type checker for symbol resolution
*/
const resolveFunctionalComponentDep = (
m: d.Module | d.ComponentCompilerMeta,
identifier: ts.Identifier,
typeChecker: ts.TypeChecker,
) => {
try {
const symbol = typeChecker.getSymbolAtLocation(identifier);
if (!symbol) return;

// Follow aliases (imports) to get the actual symbol
const aliasedSymbol = typeChecker.getAliasedSymbol(symbol);
const targetSymbol = aliasedSymbol || symbol;

// Get the declaration to find the source file
const declarations = targetSymbol.declarations;
if (!declarations || declarations.length === 0) return;

const declaration = declarations[0];
const sourceFile = declaration.getSourceFile();
if (!sourceFile) return;

const sourceFilePath = normalizePath(sourceFile.fileName);

// Track this as a functional component dependency
// We store it on the module (not component) since that's where localImports lives
const moduleFile = m as d.Module;
if (moduleFile.functionalComponentDeps) {
if (!moduleFile.functionalComponentDeps.includes(sourceFilePath)) {
moduleFile.functionalComponentDeps.push(sourceFilePath);
}
}
} catch (_e) {
// Symbol resolution can fail in some edge cases - silently ignore
}
};
2 changes: 1 addition & 1 deletion src/compiler/transformers/static-to-meta/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export const parseStaticComponentMeta = (
validateComponentMembers(node, buildCtx);

if (ts.isCallExpression(node)) {
parseCallExpression(cmp, node);
parseCallExpression(cmp, node, typeChecker);
} else if (ts.isStringLiteral(node)) {
parseStringLiteral(cmp, node);
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/transformers/static-to-meta/parse-static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const updateModule = (
parseModuleImport(config, compilerCtx, buildCtx, moduleFile, srcDirPath, node, true);
return;
} else if (ts.isCallExpression(node)) {
parseCallExpression(moduleFile, node);
parseCallExpression(moduleFile, node, typeChecker);
} else if (ts.isStringLiteral(node)) {
parseStringLiteral(moduleFile, node);
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/transformers/static-to-meta/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const convertStaticToMeta = (
} else if (ts.isImportDeclaration(node)) {
parseModuleImport(config, compilerCtx, buildCtx, moduleFile, dirPath, node, !transformOpts.isolatedModules);
} else if (ts.isCallExpression(node)) {
parseCallExpression(moduleFile, node);
parseCallExpression(moduleFile, node, typeChecker);
} else if (ts.isStringLiteral(node)) {
parseStringLiteral(moduleFile, node);
}
Expand Down
Loading
Loading