-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace espree and doctrine with typescript in docgen. #21238
Conversation
Hey, @sainthkh thanks for working on this, and I'm excited to help with this. I want to set expectations as to when would I be able to, though: I think I can come to this by next week. |
@nosolosw Thanks. I can wait. There're some PRs of mine that took 3+ months to merge. It doesn't matter to me. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an amazing effort we'll really benefit from. Thanks!
I've done an initial scan and left some notes and questions.
@@ -210,8 +210,7 @@ function LinkControl( { | |||
// is still unassigned after calling `onChange`. | |||
const hadFocusLoss = | |||
isEndingEditWithFocus.current && | |||
wrapperNode.current && | |||
! wrapperNode.current.contains( document.activeElement ); | |||
! wrapperNode.current?.contains( document.activeElement ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this change required for some reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line of code is why everything started. After #19831, everyone thought that we could use optional chaining in Gutenberg.
But we couldn't because espree
4.x in docgen cannot parse it. @aduth thought we can simply do it by updating espree
to the latest version (#19952). Unfortunately, espree doesn't support leadingComment
properties after 5.0. So, we had to change the parser. (Older version of docgen extract JSDoc from leadingComment
and parsed it with doctrine
. And doctrine
is deprecated and unused.)
As TypeScript can parse JS and JSDoc, it was chosen.
This line means that we can use optional chaining without any error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to back it out at this point, but I'd generally suggest to find ways to limit the scope of a pull request as much as possible, especially if it's already a +3400/-8400 proposal. It's definitely related as the original motivation of the changes, but I think it could have been fine to do as a separate follow-up pull request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although it's not a topic of this PR, it should be tested.
That's why I reverted it and added new test new-syntax
under test/fixtures
.
Later, when new syntax doesn't work or want to check if a new syntax works, we can use this file.
packages/docgen/src/compile.js
Outdated
module.exports = function( filePath ) { | ||
const options = { | ||
allowJs: true, | ||
target: ts.ScriptTarget.ES2020, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repeat:
Should we use an evergreen target like ESNext?
target: ts.ScriptTarget.ES2020, | |
target: ts.ScriptTarget.ESNext, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't important because this option is for the transpiled result of the code. It's not for how TypeScript treats new syntaxes. In other words, even when we set this to ES3
, TypeScript can compile optional chaining and other ES2015-2020 features. But in this case, the result of the transpilation can be run on the environments that only support ES3 (maybe notorious IE9?).
We don't need transpiled JavaScript in docgen. All we need is the generated AST. So, it doesn't matter.
But to avoid confusion, I removed target option from compile.js
and changed ES2020
to ESNext
in other files. (8d6f136)
const code = raw | ||
// typescript interprets @wordpress in @example code as a JSDoc tag. | ||
// So, it should be replaced for the time being. | ||
.replace( /@wordpress/g, '__WORDPRESS_IMPORT__' ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This replaces @wordpress
globally in the source, not just comments?
So, it should be replaced for the time being.
Are we tracking some condition when this could be removed?
Do you think we could hit issues where e.g. import escapeHtml from '@wordpress/escape-html'
becomes import escapeHtml from 'WORDPRESS_IMPORT/escape-html'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. I commented when they're restored. (5ca2084)
2. That can be a problem when we have written code that exports imported items directly from other packages like this:
// packages/some-package/src/index.js
import escapeHtml from '@wordpress/escape-html';
export { escapeHtml }
In this case, the parser tries to open packages/escape-html/src/index.js
and find jsdoc of escapeHtml
. Then, it can fail because __WORDPRESS_IMPORT__
doesn't exist. I guess it's more of a design flaw than a feature. Don't you think so?
Maybe, but for those rare cases, we need to show human-readable reason to the users than cannot find module '__WORDPRESS_IMPORT__'
3. But your comment made me curious about this case:
/**
* @param {import('@wordpress/element').WPSyntheticEvent} p what if @wordpress?
*/
I found that it shows import( '__WORDPRESS_IMPORT__/element' )
. I fixed it. (68d1198)
const getLocalName = ( s ) => { | ||
switch ( s.kind ) { | ||
case SyntaxKind.ClassDeclaration: | ||
case SyntaxKind.FunctionDeclaration: | ||
default: | ||
return s.name ? s.name.text : '*default*'; | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the switch
do anything relevant here?
const getLocalName = ( s ) => { | |
switch ( s.kind ) { | |
case SyntaxKind.ClassDeclaration: | |
case SyntaxKind.FunctionDeclaration: | |
default: | |
return s.name ? s.name.text : '*default*'; | |
} | |
}; | |
const getLocalName = ( s ) => s.name ? s.name.text : '*default*'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I was migrating getLocalName(), I copied the original function first to think what can be changed.
In the original, switch
makes perfect sense. But in the new one, we don't need it. So, I removed it entirely. (8e9839c)
* - export default function myDeclaration() {} | ||
* - export class MyDeclaration {} | ||
* - export function myDeclaration() {} | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some cases that don't seem to be covered in this list, are these handled properly?
export { default as foo, createContext as cc } from 'react'
const bar = 'baz'
const myDefault = 'd'
export { bar as quux, myDefault as default }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added them here because I want to be clear which condition handles which AST case. I thought your examples could be deduced from export { a, b } from 'c'
and export { myDeclaration };
, but it seems that it wasn't. I added them. (39bc2ac)
And they're handled properly and tested with unit tests.
if ( rawValue[ 0 ] === '{' && rawValue[ rawValue.length - 1 ] === '}' ) { | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty conditional block here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I first thought about parsing objects and arrays. But I realized that to add them to docs, I need to return them back to strings. And it's unnecessary. So, I gave it up.
But I forgot to remove the code for that attempt. (85a6aee)
if ( rawValue === 'null' ) return null; | ||
if ( rawValue === 'true' ) return true; | ||
if ( rawValue === 'false' ) return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't realize we allowed this, I'd expect linter to enforce braces on these conditional blocks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason, curly
rule doesn't work. When I add it to root .eslintrc.js
, it works. But somehow, this rule is ignored although it's set to [ 'error', 'all' ] in eslint-plugin/configs/es5.js
.
Fixed them anyway. (2a37323)
We need to open an issue for this, right?
packages/docgen/src/index.js
Outdated
// if ( result === undefined ) { | ||
// process.stdout.write( | ||
// '\nFile was processed, but contained no ES6 module exports:' | ||
// ); | ||
// process.stdout.write( `\n${ sourceFilePath }` ); | ||
// process.stdout.write( '\n\n' ); | ||
// process.exit( 0 ); | ||
// } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove or restore this commented out section?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revived it. (b7118f1)
c7bf7b1
to
7762075
Compare
@sirreal Thanks for all the reviews. I fixed them all. |
7762075
to
4ff5416
Compare
Update docs. Update docs. Update docs. Update doc. Update docs. Update docs. Update docs. Update doc. Update doc. Update doc. Update doc. Update doc.
dd74e79
to
d150875
Compare
1. When I was writing this, I also thought about the length of this PR. In my experience, when PR is too long, they're abandoned because it's too hard to follow the changes. 2. But the problem is that this PR is based on the entirely new AST. As TypeScript should incorporate its own type syntaxes, they had to create their own AST, so TypeScript AST is quite different from other ASTs. (When you compare the ASTs of these 3 different parses (espree, babel, recast), they're mostly same.) Because of that, almost all of docgen code is re-written. That's why it's hard to break this PR into a few smaller PRs. One step is tightly related to the next step. 3. That made me also think how to make this PR easier to review. First thing I did was to revert I'm also thinking about writing about the code flow and other helpful notes or even setting up small goals to focus on small areas of code. But I also think this can bias the code review. I'm a bit hesitant about setting goals for review, or should I? I also thought about removing changes in the list of changes above. It does make us discuss less by postponing some issues later in other new issues. But it doesn't make me remove some codes to make it easier to review. |
I landed #21405 👍 |
Hi @sainthkh, thanks for your patience! The first thing I want to say is that I really appreciate all the love and work you've put into this, bravo! 👏 This is promising, although I was wondering what do you think of these:
|
1. When I first decided to use 2. I am against using There is a package called It's a simple parser. It becomes both our advantage and disadvantage.
Maybe, we can try @babel/plugin-transfrom-typescript to parse the types, but I don't know it works. It needs experiment. 3. As I don't mind changing to Babel, the second question is automatically solved. |
@sainthkh thanks for the additional context! I was thinking about your comment and the next steps:
It feels to me that way we're fixing the most pressing issues (using new syntax) and we buy some time to look into the TypeScript integration. However, to be honest, I don't know what kind of TypeScript issues we may run into in step 2 as I'm still leveling up in that area (although I've looked at the logs and it doesn't seem to me that we have now any blockers). I'd welcome a sanity check, though @sainthkh @aduth @sirreal do you think this is sound? |
Good enough. I'll test |
I was reading Gutenberg code to implement #21807. It made me react a bit slow. Now, I finished replacing I experimented if babel can generate type ast. And it can. Here's the proof.. All we need to do is add We can parse types with babel by generating code like Todo list
|
Will we still want this pull request in its current form, given that we've merged #21853? I understand there are still tasks remaining, but it's unclear to me if we'll pursue those as separate independent pull requests, or if you'd prefer to try to reconcile the changes proposed in this branch to support them. |
Noting that I remarked some comments on initial experimenting to use |
|
@sainthkh, any plans to reuse more code from this PR or can it be closed? |
Description
(hello: string) => string
.How has this been tested?
Screenshots
N/A
Types of changes
Bug fix
Changes
Object
->object
. TypeScript treatsObject
asany
. And it's the firstdon't
of the do's and don'ts list. It might need a lot of changes in the code.object
means{}
thing.Object
means this global object. That's why it is not recommended to useObject
.Array<X>
,Array.<X>
,X[]
->X[]
. Type generator doesn't respect how it is written. Actually, there is no way to find which is written in which way. I thought[]
is a visual way of telling "it's an array"[]
for primitives andArray<>
for user types that start with a capital letter likeArray<ReactNode>
or allArray<>
.string | undefined
overstring=
number | nullable over
?number`- _status_
string: Notice status. (Default:
info)
check here(optional)
after name likeboolean (optional)
. I'm not sure about this and didn't implement it.Check code here
Later works
In this PR, I focused on migrating and tried not to add new features as much as possible. Because it's big (changes in 120 files with +3,454 −8,408) and it can make code hard to review and get merged.
The list below is the things that should be added for better developer experience.
eslint-plugin-jsdoc
configs likevalid-types
. We can remove/* eslint-disable */
s for perfectly ok types like{[setting:string]:any}
.object
->Object
' rule.docgen
cannot handle docgen types like below:wrong type name
,Object
.[p={x:[1, 2]}]
."abc\"def\"ghi"
.awaited
type for TS3.9parent
element of the TypeScript ast.Note
Before this PR is merged, it should be rebased and docs should be regenerated. If not, it can break other PRs.
Checklist: