Skip to content

Commit 0389edb

Browse files
huntiefacebook-github-bot
authored andcommitted
Reattach default-export doc comments
Summary: **Context** Strict TypeScript API readiness: High quality inline docs should reach users via TypeScript in their IDEs. **This diff** Prior iterations of our Flow → TS translation stack dropped doc comments for default-exported values, and/or identifiers which change shape after type transformation (e.g. Flow `component` syntax), meaning many root APIs (`View`, `ScrollView`, `Pressable`, and others) showed no documentation on hover. This diff extends the existing `reattachDocComments` transform to handle doc comment repositioning (suitable for the TS lang server) from a greater set of source positions: - the exported declaration - a `.displayName` assignment - a HOC-wrapped inner component - a renamed wrapper's public-named component - a `declare const` / `declare export default typeof X` stub **Impact** (With the source code JSDoc improvements earlier in this stack.) | Before (legacy types) | After (Strict API) | | -- | | {F1991869487} | {F1991869472} | | ⚠️ No inline docs for many symbols | ✅ New, detailed inline docs reach the TS server 🎉 | Changelog: [General][Fixed] - Preserve doc comments on root API symbols in the generated TypeScript types Differential Revision: D109316361
1 parent 701de4e commit 0389edb

2 files changed

Lines changed: 369 additions & 31 deletions

File tree

scripts/js-api/build-types/transforms/flow/__tests__/reattachDocComments-test.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ async function translate(code: string): Promise<string> {
1919
return print(result.ast, result.mutatedCode, prettierOptions);
2020
}
2121

22+
/**
23+
* Assert that `marker` appears exactly once (moved, not duplicated) and is the
24+
* leading comment of the `export default` declaration.
25+
*/
26+
function expectDocOnDefaultExport(result: string, marker: string): void {
27+
expect(result.split(marker).length - 1).toBe(1);
28+
expect(result).toMatch(
29+
new RegExp(`/\\*\\*[\\s\\S]*?${marker}[\\s\\S]*?\\*/\\s*export default`),
30+
);
31+
}
32+
2233
describe('reattachDocComments', () => {
2334
test('should move component doc block', async () => {
2435
const code = `
@@ -53,6 +64,7 @@ describe('reattachDocComments', () => {
5364
"
5465
`);
5566
});
67+
5668
test('should move variable doc block', async () => {
5769
const code = `
5870
const bar = 'bar';
@@ -83,4 +95,156 @@ describe('reattachDocComments', () => {
8395
"
8496
`);
8597
});
98+
99+
test('should move doc block from a `component` declaration', async () => {
100+
const code = `
101+
import * as React from 'react';
102+
103+
/**
104+
* Foo documentation
105+
*/
106+
component Foo(...props: FooProps) {
107+
return null;
108+
}
109+
110+
Foo.displayName = 'Foo';
111+
112+
export default Foo;
113+
`;
114+
const result = await translate(code);
115+
expectDocOnDefaultExport(result, 'Foo documentation');
116+
});
117+
118+
test('should move doc block from a memo-wrapped component via its display name', async () => {
119+
const code = `
120+
import * as React from 'react';
121+
122+
/**
123+
* Foo documentation
124+
*/
125+
function Foo(props: FooProps) {
126+
return null;
127+
}
128+
129+
const MemoedFoo = memo(Foo);
130+
MemoedFoo.displayName = 'Foo';
131+
132+
export default MemoedFoo;
133+
`;
134+
const result = await translate(code);
135+
expectDocOnDefaultExport(result, 'Foo documentation');
136+
});
137+
138+
test('should move doc block from the public (display) named declaration', async () => {
139+
const code = `
140+
import * as React from 'react';
141+
142+
/**
143+
* Foo documentation
144+
*/
145+
class Foo extends React.Component<FooProps> {}
146+
147+
const FooWrapper = (props: FooProps) => null;
148+
FooWrapper.displayName = 'Foo';
149+
150+
export default FooWrapper;
151+
`;
152+
const result = await translate(code);
153+
expectDocOnDefaultExport(result, 'Foo documentation');
154+
});
155+
156+
test('should reattach through an `as` cast in the default export', async () => {
157+
const code = `
158+
import * as React from 'react';
159+
160+
/**
161+
* Foo documentation
162+
*/
163+
const Foo = (props: FooProps) => null;
164+
165+
export default Foo as FooType;
166+
`;
167+
const result = await translate(code);
168+
expectDocOnDefaultExport(result, 'Foo documentation');
169+
});
170+
171+
test('should leave the doc block on a directly class-exported declaration', async () => {
172+
const code = `
173+
import * as React from 'react';
174+
175+
/**
176+
* Foo documentation
177+
*/
178+
class Foo extends React.Component<FooProps> {}
179+
180+
export default Foo;
181+
`;
182+
const result = await translate(code);
183+
// TypeScript resolves the doc on the class declaration; it must stay there
184+
// and must not be moved onto the `export default` statement.
185+
expect(result.split('Foo documentation').length - 1).toBe(1);
186+
expect(result).toMatch(/\*\/\s*class Foo/);
187+
expect(result).not.toMatch(/\*\/\s*export default/);
188+
});
189+
190+
test('should move doc block from a `declare const` (.js.flow stub)', async () => {
191+
const code = `
192+
'use strict';
193+
194+
/**
195+
* Foo documentation
196+
*/
197+
declare const Foo: {bar(): void};
198+
199+
export default Foo;
200+
`;
201+
const result = await translate(code);
202+
expectDocOnDefaultExport(result, 'Foo documentation');
203+
});
204+
205+
test('should reattach for `declare export default typeof Foo`', async () => {
206+
const code = `
207+
'use strict';
208+
209+
/**
210+
* Foo documentation
211+
*/
212+
declare const Foo: {bar(): void};
213+
214+
declare export default typeof Foo;
215+
`;
216+
const result = await translate(code);
217+
expect(result.split('Foo documentation').length - 1).toBe(1);
218+
expect(result).toMatch(
219+
/\/\*\*[\s\S]*?Foo documentation[\s\S]*?\*\/\s*declare export default/,
220+
);
221+
});
222+
223+
test('should not duplicate a doc block already on the default export', async () => {
224+
const code = `
225+
const Foo = (props: FooProps) => null;
226+
227+
/**
228+
* Foo documentation
229+
*/
230+
export default Foo;
231+
`;
232+
const result = await translate(code);
233+
expectDocOnDefaultExport(result, 'Foo documentation');
234+
});
235+
236+
test('should ignore build directive comments', async () => {
237+
const code = `
238+
import * as React from 'react';
239+
240+
/** @build-types emit-as-interface */
241+
const Foo = (props: FooProps) => null;
242+
243+
export default Foo;
244+
`;
245+
const result = await translate(code);
246+
// The directive must not be moved onto (or left as a leading comment of)
247+
// the default export.
248+
expect(result).not.toMatch(/\*\/\s*export default/);
249+
});
86250
});

0 commit comments

Comments
 (0)