Skip to content

Commit e3756fa

Browse files
committed
fix: zachw/cfr-default-exports
1 parent 8a7053e commit e3756fa

File tree

9 files changed

+165
-19
lines changed

9 files changed

+165
-19
lines changed

packages/data-context/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@urql/core": "2.4.4",
2323
"@urql/exchange-execute": "1.1.0",
2424
"@urql/exchange-graphcache": "4.3.6",
25+
"ast-types": "^0.14.2",
2526
"chokidar": "3.5.1",
2627
"common-path-prefix": "3.0.0",
2728
"cross-fetch": "^3.1.4",

packages/data-context/src/actions/CodegenActions.ts

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import templates from '../codegen/templates'
77
import type { CodeGenType } from '../gen/graphcache-config.gen'
88
import { WizardFrontendFramework, WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
99
import { parse as parseReactComponent, resolver as reactDocgenResolvers } from 'react-docgen'
10+
import { visit } from 'ast-types'
1011

1112
interface ReactComponentDescriptor {
1213
displayName: string
14+
exportName: string
15+
isDefault: boolean
1316
}
1417

1518
export class CodegenActions {
@@ -19,12 +22,20 @@ export class CodegenActions {
1922
try {
2023
const src = await this.ctx.fs.readFile(filePath, 'utf8')
2124

22-
const result = parseReactComponent(src, reactDocgenResolvers.findAllExportedComponentDefinitions)
25+
const linker: Linker = {}
26+
let result = parseReactComponent(src, findAllWithLink(linker))
27+
2328
// types appear to be incorrect in [email protected]
2429
// TODO: update when 6.0.0 stable is out for fixed types.
2530
const defs = (Array.isArray(result) ? result : [result]) as ReactComponentDescriptor[]
2631

27-
return defs
32+
const mappedDefs = defs.map((meta) => {
33+
const displayName = meta.displayName || ''
34+
35+
return { ...meta, displayName, ...linker[displayName] }
36+
})
37+
38+
return mappedDefs
2839
} catch (err) {
2940
this.ctx.debug(err)
3041

@@ -139,3 +150,70 @@ export class CodegenActions {
139150
return WIZARD_FRAMEWORKS.find((framework) => framework.configFramework === config?.component?.devServer.framework)
140151
}
141152
}
153+
154+
type Linker = Record<string, { exportName: string, isDefault: boolean }>
155+
function findAllWithLink (linker: Linker) {
156+
return (ast: any, parser: any, importer: any) => {
157+
visit(ast, {
158+
// export const Foo, export { Foo, Bar }, export function FooBar () { ... }
159+
visitExportNamedDeclaration: (path) => {
160+
const declaration = path.node.declaration as any
161+
162+
if (declaration) { // export const Foo
163+
if (declaration.id) {
164+
linker[declaration.id.name] = { exportName: declaration.id.name, isDefault: false }
165+
} else { // export const Foo, Bar
166+
(path.node.declaration as any).declarations.forEach((node: any) => {
167+
const id = node.name ?? node.id?.name
168+
169+
if (id) {
170+
linker[id] = { exportName: id, isDefault: false }
171+
}
172+
})
173+
}
174+
} else { // export { Foo, Bar }
175+
path.node.specifiers?.forEach((node) => {
176+
if (!node.local?.name) {
177+
return
178+
}
179+
180+
if (node.exported?.name === 'default') { // export { Foo as default }
181+
linker[node.local.name] = {
182+
exportName: node.local.name,
183+
isDefault: true,
184+
}
185+
} else {
186+
linker[node.local.name] = {
187+
exportName: node.exported.name,
188+
isDefault: false,
189+
}
190+
}
191+
})
192+
}
193+
194+
return false
195+
},
196+
// export default Foo
197+
visitExportDefaultDeclaration: (path) => {
198+
const declaration: any = path.node.declaration
199+
const id: string = declaration.name || declaration.id?.name
200+
201+
if (id) { // export default Foo
202+
linker[id] = {
203+
exportName: id,
204+
isDefault: true,
205+
}
206+
} else { // export default () => {}
207+
linker[''] = {
208+
exportName: 'Component',
209+
isDefault: true,
210+
}
211+
}
212+
213+
return false
214+
},
215+
})
216+
217+
return reactDocgenResolvers.findAllExportedComponentDefinitions(ast, parser, importer)
218+
}
219+
}

packages/data-context/test/unit/actions/CodegenActions.spec.ts

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,59 +24,98 @@ describe('CodegenActions', () => {
2424
const reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-class.jsx`)
2525

2626
expect(reactComponents).to.have.length(1)
27-
expect(reactComponents[0].displayName).to.equal('Counter')
27+
expect(reactComponents[0].exportName).to.equal('Counter')
28+
expect(reactComponents[0].isDefault).to.equal(false)
2829
})
2930

3031
it('returns React components from file with functional component', async () => {
3132
const reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-functional.jsx`)
3233

3334
expect(reactComponents).to.have.length(1)
34-
expect(reactComponents[0].displayName).to.equal('Counter')
35+
expect(reactComponents[0].exportName).to.equal('Counter')
36+
expect(reactComponents[0].isDefault).to.equal(false)
3537
})
3638

3739
it('returns only exported React components from file with functional components', async () => {
3840
const reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-multiple-components.jsx`)
3941

4042
expect(reactComponents).to.have.length(2)
41-
expect(reactComponents[0].displayName).to.equal('CounterContainer')
42-
expect(reactComponents[1].displayName).to.equal('CounterView')
43-
})
44-
45-
it('returns both named and default exports', async () => {
46-
const reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-mixed-multiple-components.tsx`)
43+
expect(reactComponents[0].exportName).to.equal('CounterContainer')
44+
expect(reactComponents[0].isDefault).to.equal(false)
4745

48-
expect(reactComponents).to.have.length(2)
49-
expect(reactComponents[0].displayName).to.equal('CounterContainer')
50-
expect(reactComponents[1].displayName).to.equal('CounterView')
46+
expect(reactComponents[1].exportName).to.equal('CounterView')
47+
expect(reactComponents[1].isDefault).to.equal(false)
5148
})
5249

5350
it('returns React components from a tsx file', async () => {
5451
const reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter.tsx`)
5552

5653
expect(reactComponents).to.have.length(1)
57-
expect(reactComponents[0].displayName).to.equal('Counter')
54+
expect(reactComponents[0].exportName).to.equal('Counter')
55+
expect(reactComponents[0].isDefault).to.equal(false)
5856
})
5957

6058
it('returns React components that are exported by default', async () => {
61-
const reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-default.tsx`)
59+
let reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-default.tsx`)
60+
61+
expect(reactComponents).to.have.length(1)
62+
expect(reactComponents[0].exportName).to.equal('CounterDefault')
63+
expect(reactComponents[0].isDefault).to.equal(true)
6264

65+
reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/default-anonymous.jsx`)
6366
expect(reactComponents).to.have.length(1)
64-
expect(reactComponents[0].displayName).to.equal('CounterDefault')
67+
expect(reactComponents[0].exportName).to.equal('Component')
68+
expect(reactComponents[0].isDefault).to.equal(true)
69+
70+
reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/default-function.jsx`)
71+
expect(reactComponents).to.have.length(1)
72+
expect(reactComponents[0].exportName).to.equal('HelloWorld')
73+
expect(reactComponents[0].isDefault).to.equal(true)
74+
75+
reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/default-class.jsx`)
76+
expect(reactComponents).to.have.length(1)
77+
expect(reactComponents[0].exportName).to.equal('HelloWorld')
78+
expect(reactComponents[0].isDefault).to.equal(true)
79+
80+
reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/default-specifier.jsx`)
81+
expect(reactComponents).to.have.length(1)
82+
expect(reactComponents[0].exportName).to.equal('HelloWorld')
83+
expect(reactComponents[0].isDefault).to.equal(true)
6584
})
6685

6786
it('returns React components defined with arrow functions', async () => {
6887
const reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-arrow-function.jsx`)
6988

7089
expect(reactComponents).to.have.length(1)
71-
expect(reactComponents[0].displayName).to.equal('Counter')
90+
expect(reactComponents[0].exportName).to.equal('Counter')
91+
expect(reactComponents[0].isDefault).to.equal(false)
7292
})
7393

7494
it('returns React components from a file with multiple separate export statements', async () => {
7595
const reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-separate-exports.jsx`)
7696

7797
expect(reactComponents).to.have.length(2)
78-
expect(reactComponents[0].displayName).to.equal('CounterView')
79-
expect(reactComponents[1].displayName).to.equal('CounterContainer')
98+
expect(reactComponents[0].exportName).to.equal('CounterView')
99+
expect(reactComponents[0].isDefault).to.equal(false)
100+
expect(reactComponents[1].exportName).to.equal('CounterContainer')
101+
expect(reactComponents[1].isDefault).to.equal(true)
102+
})
103+
104+
it('returns React components that are exported and aliased', async () => {
105+
const reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/export-alias.jsx`)
106+
107+
expect(reactComponents).to.have.length(1)
108+
expect(reactComponents[0].exportName).to.equal('HelloWorld')
109+
expect(reactComponents[0].isDefault).to.equal(false)
110+
})
111+
112+
// TODO: "react-docgen" will resolve HOCs but our export detection does not. Can fall back to displayName here
113+
it.skip('handles higher-order-components', async () => {
114+
const reactComponents = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-hoc.jsx`)
115+
116+
expect(reactComponents).to.have.length(1)
117+
expect(reactComponents[0].exportName).to.equal('Counter')
118+
expect(reactComponents[0].isDefault).to.equal(true)
80119
})
81120

82121
it('does not throw while parsing empty file', async () => {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react'
2+
3+
function Counter () {
4+
const [count, setCount] = React.useState(0)
5+
6+
return <p onClick={() => setCount(count + 1)}>count: {count}</p>
7+
}
8+
9+
const connect = (component) => component
10+
11+
export default connect(Counter)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default () => <div>Hello World</div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react'
2+
3+
export default class HelloWorld extends React.Component {
4+
render () {
5+
return <div>HelloWorld</div>
6+
}
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function HelloWorld () {
2+
return <div>Hello World</div>
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const HelloWorld = () => <div>Hello World</div>
2+
3+
export { HelloWorld as default }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const hw = () => <div>HelloWorld</div>
2+
3+
export { hw as HelloWorld }

0 commit comments

Comments
 (0)