-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Add jsxFactory compiler option. #11267
Conversation
Hi @bradleyayers, I'm your friendly neighborhood Microsoft Pull Request Bot (You can call me MSBOT). Thanks for your contribution! TTYL, MSBOT; |
@@ -2346,6 +2346,10 @@ namespace ts { | |||
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Invalid_value_for_reactNamespace_0_is_not_a_valid_identifier, options.reactNamespace)); | |||
} | |||
|
|||
if (options.jsxFactory && options.reactNamespace) { |
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.
should add another check as well that jsxFactory
can not be used without jsx: react
@@ -1618,17 +1618,23 @@ namespace ts { | |||
); | |||
} | |||
|
|||
function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement) { | |||
export function createJsxFactory(jsxFactory: string) { | |||
// No explicit validation of this parameter is required. Users are |
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.
do not think you can do this. in Program:verifyCompilerOptions, you should check that isIdentifierText(options.jsxFactory, languageVersion)
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 went with this approach for simplicity, based on the belief this was acceptable behaviour as per @RyanCavanaugh's comment:
No explicit validation of this parameter occurs; users are assumed to have provided a correct string
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.
To avoid going around in circles I'll wait for @RyanCavanaugh to chime in.
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.
Maybe we should just restrict this to dotted names for simplicity's sake. Thoughts @mhegazy ?
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.
Maybe we should just restrict this to dotted names for simplicity's sake. Thoughts @mhegazy ?
I agree. let's add a check that this is a QualifiedName in Program:verifyCompilerOptions
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 need some help with deciding what strategy is best for this.
To validate a dotted name as a QualifiedName
, there’s a few ways I can see to do it:
- basic regex or string manipulation (e.g.
.split(‘.’)
) - creating a mini parser using scanner, iterating over the tokens and asserting it’s a sequence of
Identifier (Dot, Identifier)*
- using the parser (
createSourceFile
) to parse the stringa.b.c
and using the resulting source file to both assert the structure, and to extract thea
identifier to mark it as used - …?
Balancing simplicity with correctness is the challenge here, I'm not sure what the preferred bias is.
return createIdentifier(jsxFactory); | ||
} | ||
|
||
export function createReactCreateElement(reactNamespace: string, parentElement: JsxOpeningLikeElement) { |
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.
make reactNamespace
optional, or string|undefined
.
const jsxFactoryRefErr = compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined; | ||
let jsxFactoryNamespace = "React"; | ||
if (compilerOptions.jsxFactory) { | ||
jsxFactoryNamespace = compilerOptions.jsxFactory.split(".")[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.
if we say jsxFactory
is an identifier, no need to do this 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 was using treating jsxFactory
as an Identifier
in factory.ts
as a convenience in the a.b.c
case, but really in that scenario it should be a PropertyAccessExpression
.
In that case I only want a
out of a.b.c
so that I can set referenced = true
on that symbol alone.
@@ -0,0 +1,12 @@ | |||
//@filename: file.tsx | |||
//@jsx: react | |||
//@jsxFactory: a.b.c |
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.
@RyanCavanaugh are dotted names allowed?
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.
well.. i suppose if you pass React.createElement
it should just work.
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.
OK, so we need to verify it is a qualified name. we do not want unexpected characters in there.
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.
Done.
const jsxFactoryRefErr = compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined; | ||
let jsxFactoryNamespace = "React"; | ||
if (compilerOptions.jsxFactory) { | ||
jsxFactoryNamespace = compilerOptions.jsxFactory.split(".")[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.
This could also be an expression like foo["bar"]
, should be able to split on .
/ [
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 think a qualified name is sufficient here. we could relax that in the future.
@@ -1618,17 +1618,38 @@ namespace ts { | |||
); | |||
} | |||
|
|||
function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement) { | |||
export function createEntityName(entityName: string) { |
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'm skeptical the implementation of this is ideal, open to suggestions.
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.
@ahejlsberg thoughts?
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.
Perhaps a parseIsolatedEntityName
could set a flag in the parser to have it treat the source as synthetic.
@@ -2719,6 +2719,7 @@ namespace ts { | |||
inlineSources?: boolean; | |||
isolatedModules?: boolean; | |||
jsx?: JsxEmit; | |||
jsxFactory?: string; |
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.
Instead of parsing the jsxFactory
to an EntityName
multiple times through-out the program, would it be better to have this be EntityName
and parse it once up-front?
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'd probably be cleanest to do this once in checker.ts and then only again if needed in emitter.ts.
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 somewhat similar to what we do for externalHelpersModuleName
on SourceFile
.
while (entityName.kind === SyntaxKind.QualifiedName) { | ||
entityName = (<QualifiedName>entityName).left; | ||
} | ||
Debug.assertNode(entityName, node => node.kind === SyntaxKind.Identifier); |
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.
There is already an isIdentifier
node test you can pass to Debug.assertNode
.
@@ -584,6 +591,20 @@ namespace ts { | |||
return result; | |||
} | |||
|
|||
export function parseIsolatedEntityName(content: string, allowReservedWords = 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.
Is allowReservedWords
ever used by a caller? If not, I would just remove it.
@@ -463,6 +463,13 @@ namespace ts { | |||
} | |||
|
|||
/* @internal */ | |||
export function parseIsolatedEntityName(content: string) { | |||
const result = Parser.parseIsolatedEntityName(content); | |||
Parser.fixupParentReferences(result.entityName); |
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.
Parent references aren't used in the emit transforms with respect to synthesized nodes. If this is only used in emit, then fixing up the parent references is unneeded.
@@ -2719,6 +2719,7 @@ namespace ts { | |||
inlineSources?: boolean; | |||
isolatedModules?: boolean; | |||
jsx?: JsxEmit; | |||
jsxFactory?: string; |
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 somewhat similar to what we do for externalHelpersModuleName
on SourceFile
.
@@ -1618,17 +1618,38 @@ namespace ts { | |||
); | |||
} | |||
|
|||
function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement) { | |||
export function createEntityName(entityName: string) { |
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.
Perhaps a parseIsolatedEntityName
could set a flag in the parser to have it treat the source as synthetic.
const entityName = parseEntityName(allowReservedWords); | ||
parseExpected(SyntaxKind.EndOfFileToken); | ||
|
||
const diagnostics = parseDiagnostics; |
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.
Are you using these diagnostics, other than to check that they exist in program.ts:1546? You could return undefined
rather than allocating the object literal for every call.
const element = createReactCreateElement( | ||
compilerOptions.reactNamespace, | ||
const jsxFactory = compilerOptions.jsxFactory | ||
? createJsxFactory(compilerOptions.jsxFactory) |
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.
Since this is a fully-synthesized entity name, consider caching it on SourceFile
, similar to externalHelpersModuleName
rather than reparsing it each time it is hit. You could also create it once at the top of transformJsx
.
// it isn't in scope when targeting React emit, we should issue an error. | ||
const jsxFactoryRefErr = compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined; | ||
let jsxFactoryNamespace = "React"; | ||
if (compilerOptions.jsxFactory) { |
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 could be done once during initializeTypeChecker
rather than on every call to checkJsxOpeningLikeElement
.
return createExpressionFromEntityName(entityName); | ||
} | ||
|
||
export function createReactCreateElement(reactNamespace: string | undefined, parentElement: JsxOpeningLikeElement) { | ||
// To ensure the emit resolver can properly resolve the namespace, we need to |
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 really understand this comment. What does "resolving the namespace" by the emit resolver mean, and why does it need to? My guess would have been to ensure that the React
symbol is treated as "used" (so that the import is not elided), but this was already done manually in checker.ts:10804
, so I must be missing something.
poke. poke-poke. :) |
Poke. I'd like this too. |
This should be fixed by #12135 |
Fixes #9582