[builtin] (html)')
+ expect(output).toContain(' [builtin] (html)')
+
+ expect(jsonTree).toMatchObject({
+ entries: [
+ expect.objectContaining({
+ referenceName: 'FeaturePage',
+ node: expect.objectContaining({
+ name: 'FeaturePage',
+ symbolKind: 'component',
+ }),
+ }),
+ ],
+ roots: [
+ expect.objectContaining({
+ name: 'FeaturePage',
+ usages: expect.arrayContaining([
+ expect.objectContaining({
+ node: expect.objectContaining({
+ name: 'Section',
+ symbolKind: 'component',
+ usages: expect.arrayContaining([
+ expect.objectContaining({
+ node: expect.objectContaining({
+ name: 'section',
+ symbolKind: 'builtin',
+ }),
+ }),
+ ]),
+ }),
+ }),
+ expect.objectContaining({
+ node: expect.objectContaining({
+ name: 'Container',
+ symbolKind: 'component',
+ usages: expect.arrayContaining([
+ expect.objectContaining({
+ node: expect.objectContaining({
+ name: 'div',
+ symbolKind: 'builtin',
+ }),
+ }),
+ ]),
+ }),
+ }),
+ ]),
+ }),
+ ],
+ })
+ })
+
it('prints multiple React entry locations when the entry file renders more than one root', () => {
const graph = analyzeReactUsage('src/multi-entry.tsx', {
cwd: fixtureDirectory,
diff --git a/src/analyzers/react/bindings.ts b/src/analyzers/react/bindings.ts
index 41cd176..87f41cc 100644
--- a/src/analyzers/react/bindings.ts
+++ b/src/analyzers/react/bindings.ts
@@ -117,6 +117,15 @@ function getImportBinding(
}
}
+ if (specifier.type === 'ImportNamespaceSpecifier') {
+ return {
+ localName: specifier.local.name,
+ importedName: '*',
+ sourceSpecifier,
+ ...(sourcePath === undefined ? {} : { sourcePath }),
+ }
+ }
+
return undefined
}
diff --git a/src/analyzers/react/entries.ts b/src/analyzers/react/entries.ts
index fdfd47d..c45802e 100644
--- a/src/analyzers/react/entries.ts
+++ b/src/analyzers/react/entries.ts
@@ -9,6 +9,7 @@ import {
getComponentReferenceName,
getCreateElementComponentReferenceName,
getHookReferenceName,
+ getMemberExpressionComponentReferenceName,
isNode,
} from './walk.js'
@@ -73,7 +74,9 @@ function collectNodeEntryUsages(
let nextHasComponentAncestor = hasComponentAncestor
if (node.type === 'JSXElement') {
- const referenceName = getComponentReferenceName(node)
+ const referenceName =
+ getComponentReferenceName(node) ??
+ getMemberExpressionComponentReferenceName(node)
if (referenceName !== undefined) {
if (!hasComponentAncestor) {
addPendingReactUsageEntry(
diff --git a/src/analyzers/react/references.ts b/src/analyzers/react/references.ts
index 817333c..2b29707 100644
--- a/src/analyzers/react/references.ts
+++ b/src/analyzers/react/references.ts
@@ -15,6 +15,17 @@ export function resolveReactReference(
return getBuiltinNodeId(name)
}
+ const dotIndex = name.indexOf('.')
+ if (dotIndex !== -1) {
+ return resolveNamespaceMemberReference(
+ fileAnalysis,
+ fileAnalyses,
+ name.slice(0, dotIndex),
+ name.slice(dotIndex + 1),
+ kind,
+ )
+ }
+
const localSymbol = fileAnalysis.allSymbolsByName.get(name)
if (localSymbol !== undefined && localSymbol.kind === kind) {
return localSymbol.id
@@ -50,6 +61,36 @@ export function resolveReactReference(
return targetId
}
+function resolveNamespaceMemberReference(
+ fileAnalysis: FileAnalysis,
+ fileAnalyses: ReadonlyMap,
+ namespaceName: string,
+ propertyName: string,
+ kind: ReactSymbolKind,
+): string | undefined {
+ const importBinding = fileAnalysis.importsByLocalName.get(namespaceName)
+ if (
+ importBinding === undefined ||
+ importBinding.importedName !== '*' ||
+ importBinding.sourcePath === undefined
+ ) {
+ return undefined
+ }
+
+ const sourceFileAnalysis = fileAnalyses.get(importBinding.sourcePath)
+ if (sourceFileAnalysis === undefined) {
+ return undefined
+ }
+
+ return resolveExportedSymbol(
+ sourceFileAnalysis,
+ propertyName,
+ kind,
+ fileAnalyses,
+ new Set(),
+ )
+}
+
function resolveExportedSymbol(
fileAnalysis: FileAnalysis,
exportName: string,
diff --git a/src/analyzers/react/usage.ts b/src/analyzers/react/usage.ts
index 969fbba..f7b33bb 100644
--- a/src/analyzers/react/usage.ts
+++ b/src/analyzers/react/usage.ts
@@ -4,6 +4,7 @@ import {
getComponentReferenceName,
getCreateElementComponentReferenceName,
getHookReferenceName,
+ getMemberExpressionComponentReferenceName,
getStyledBuiltinReferenceName,
getStyledComponentReferenceName,
walkReactUsageTree,
@@ -18,6 +19,11 @@ export function analyzeSymbolUsages(
const name = getComponentReferenceName(node)
if (name !== undefined) {
symbol.componentReferences.add(name)
+ } else {
+ const memberName = getMemberExpressionComponentReferenceName(node)
+ if (memberName !== undefined) {
+ symbol.componentReferences.add(memberName)
+ }
}
if (includeBuiltins) {
diff --git a/src/analyzers/react/walk.ts b/src/analyzers/react/walk.ts
index 10da610..6f2ae03 100644
--- a/src/analyzers/react/walk.ts
+++ b/src/analyzers/react/walk.ts
@@ -124,6 +124,28 @@ export function getComponentReferenceName(
return name !== undefined && isComponentName(name) ? name : undefined
}
+export function getMemberExpressionComponentReferenceName(
+ node: JSXElement,
+): string | undefined {
+ const name = node.openingElement.name
+ if (name.type !== 'JSXMemberExpression') {
+ return undefined
+ }
+
+ if (name.object.type !== 'JSXIdentifier') {
+ return undefined
+ }
+
+ const objectName = name.object.name
+ const propertyName = name.property.name
+
+ if (isComponentName(objectName)) {
+ return `${objectName}.${propertyName}`
+ }
+
+ return undefined
+}
+
export function getBuiltinReferenceName(node: JSXElement): string | undefined {
const name = getJsxName(node.openingElement.name)
return name !== undefined && isIntrinsicElementName(name) ? name : undefined
diff --git a/test/fixtures/react-mode/src/components/FeatureSection.styled.tsx b/test/fixtures/react-mode/src/components/FeatureSection.styled.tsx
new file mode 100644
index 0000000..0753a82
--- /dev/null
+++ b/test/fixtures/react-mode/src/components/FeatureSection.styled.tsx
@@ -0,0 +1,9 @@
+import styled from 'styled-components'
+
+export const Section = styled.section`
+ display: flex;
+`
+
+export const Container = styled.div`
+ padding: 1rem;
+`
diff --git a/test/fixtures/react-mode/src/namespace-styled-entry.tsx b/test/fixtures/react-mode/src/namespace-styled-entry.tsx
new file mode 100644
index 0000000..9d7567e
--- /dev/null
+++ b/test/fixtures/react-mode/src/namespace-styled-entry.tsx
@@ -0,0 +1,9 @@
+import * as Styled from './components/FeatureSection.styled'
+
+export function FeaturePage() {
+ return (
+
+ Hello
+
+ )
+}