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
71 changes: 71 additions & 0 deletions e2e/react.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,77 @@ describe('analyzeReactUsage', () => {
})
})

it('resolves builtin elements from namespace-imported styled-components', () => {
const graph = analyzeReactUsage('src/namespace-styled-entry.tsx', {
cwd: fixtureDirectory,
includeBuiltins: true,
})

const output = printReactUsageTree(graph, {
color: false,
})
const jsonTree = graphToSerializableReactTree(graph)

expect(output).toContain(
'<FeaturePage /> [component] (src/namespace-styled-entry.tsx)',
)
expect(output).toContain(
'<Section /> as Styled.Section [component] (src/components/FeatureSection.styled.tsx)',
)
expect(output).toContain(
'<Container /> as Styled.Container [component] (src/components/FeatureSection.styled.tsx)',
)
expect(output).toContain('<section> [builtin] (html)')
expect(output).toContain('<div> [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,
Expand Down
9 changes: 9 additions & 0 deletions src/analyzers/react/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ function getImportBinding(
}
}

if (specifier.type === 'ImportNamespaceSpecifier') {
return {
localName: specifier.local.name,
importedName: '*',
sourceSpecifier,
...(sourcePath === undefined ? {} : { sourcePath }),
}
}

return undefined
}

Expand Down
5 changes: 4 additions & 1 deletion src/analyzers/react/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getComponentReferenceName,
getCreateElementComponentReferenceName,
getHookReferenceName,
getMemberExpressionComponentReferenceName,
isNode,
} from './walk.js'

Expand Down Expand Up @@ -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(
Expand Down
41 changes: 41 additions & 0 deletions src/analyzers/react/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -50,6 +61,36 @@ export function resolveReactReference(
return targetId
}

function resolveNamespaceMemberReference(
fileAnalysis: FileAnalysis,
fileAnalyses: ReadonlyMap<string, FileAnalysis>,
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<string>(),
)
}

function resolveExportedSymbol(
fileAnalysis: FileAnalysis,
exportName: string,
Expand Down
6 changes: 6 additions & 0 deletions src/analyzers/react/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getComponentReferenceName,
getCreateElementComponentReferenceName,
getHookReferenceName,
getMemberExpressionComponentReferenceName,
getStyledBuiltinReferenceName,
getStyledComponentReferenceName,
walkReactUsageTree,
Expand All @@ -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) {
Expand Down
22 changes: 22 additions & 0 deletions src/analyzers/react/walk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import styled from 'styled-components'

export const Section = styled.section`
display: flex;
`

export const Container = styled.div`
padding: 1rem;
`
9 changes: 9 additions & 0 deletions test/fixtures/react-mode/src/namespace-styled-entry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Styled from './components/FeatureSection.styled'

export function FeaturePage() {
return (
<Styled.Section>
<Styled.Container>Hello</Styled.Container>
</Styled.Section>
)
}
Loading