Skip to content

Commit c458576

Browse files
authored
Merge pull request #12135 from Microsoft/jsxFactory
Support for --jsxFactory option
2 parents be5e5fb + dd7f00f commit c458576

File tree

48 files changed

+2961
-16
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2961
-16
lines changed

src/compiler/checker.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,9 @@ namespace ts {
335335
});
336336

337337
let jsxElementType: Type;
338+
let _jsxNamespace: string;
339+
let _jsxFactoryEntity: EntityName;
340+
338341
/** Things we lazy load from the JSX namespace */
339342
const jsxTypes = createMap<Type>();
340343
const JsxNames = {
@@ -372,6 +375,22 @@ namespace ts {
372375

373376
return checker;
374377

378+
function getJsxNamespace(): string {
379+
if (_jsxNamespace === undefined) {
380+
_jsxNamespace = "React";
381+
if (compilerOptions.jsxFactory) {
382+
_jsxFactoryEntity = parseIsolatedEntityName(compilerOptions.jsxFactory, languageVersion);
383+
if (_jsxFactoryEntity) {
384+
_jsxNamespace = getFirstIdentifier(_jsxFactoryEntity).text;
385+
}
386+
}
387+
else if (compilerOptions.reactNamespace) {
388+
_jsxNamespace = compilerOptions.reactNamespace;
389+
}
390+
}
391+
return _jsxNamespace;
392+
}
393+
375394
function getEmitResolver(sourceFile: SourceFile, cancellationToken: CancellationToken) {
376395
// Ensure we have all the type information in place for this file so that all the
377396
// emitter questions of this resolver will return the right information.
@@ -11468,10 +11487,10 @@ namespace ts {
1146811487
function checkJsxOpeningLikeElement(node: JsxOpeningLikeElement) {
1146911488
checkGrammarJsxElement(node);
1147011489
checkJsxPreconditions(node);
11471-
// The reactNamespace symbol should be marked as 'used' so we don't incorrectly elide its import. And if there
11472-
// is no reactNamespace symbol in scope when targeting React emit, we should issue an error.
11490+
// The reactNamespace/jsxFactory's root symbol should be marked as 'used' so we don't incorrectly elide its import.
11491+
// And if there is no reactNamespace/jsxFactory's symbol in scope when targeting React emit, we should issue an error.
1147311492
const reactRefErr = compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined;
11474-
const reactNamespace = compilerOptions.reactNamespace ? compilerOptions.reactNamespace : "React";
11493+
const reactNamespace = getJsxNamespace();
1147511494
const reactSym = resolveName(node.tagName, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace);
1147611495
if (reactSym) {
1147711496
// Mark local symbol as referenced here because it might not have been marked
@@ -19738,7 +19757,8 @@ namespace ts {
1973819757
getTypeReferenceDirectivesForEntityName,
1973919758
getTypeReferenceDirectivesForSymbol,
1974019759
isLiteralConstDeclaration,
19741-
writeLiteralConstValue
19760+
writeLiteralConstValue,
19761+
getJsxFactoryEntity: () => _jsxFactoryEntity
1974219762
};
1974319763

1974419764
// defined here to avoid outer scope pollution

src/compiler/commandLineParser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ namespace ts {
7777
type: "string",
7878
description: Diagnostics.Specify_the_object_invoked_for_createElement_and_spread_when_targeting_react_JSX_emit
7979
},
80+
{
81+
name: "jsxFactory",
82+
type: "string",
83+
description: Diagnostics.Specify_the_JSX_factory_function_to_use_when_targeting_react_JSX_emit_e_g_React_createElement_or_h
84+
},
8085
{
8186
name: "listFiles",
8287
type: "boolean",

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2389,6 +2389,10 @@
23892389
"category": "Error",
23902390
"code": 5066
23912391
},
2392+
"Invalid value for 'jsxFactory'. '{0}' is not a valid identifier or qualified-name.": {
2393+
"category": "Error",
2394+
"code": 5067
2395+
},
23922396
"Concatenate and emit output to single file.": {
23932397
"category": "Message",
23942398
"code": 6001
@@ -2905,6 +2909,10 @@
29052909
"category": "Message",
29062910
"code": 6145
29072911
},
2912+
"Specify the JSX factory function to use when targeting 'react' JSX emit, e.g. 'React.createElement' or 'h'.": {
2913+
"category": "Message",
2914+
"code": 6146
2915+
},
29082916
"Variable '{0}' implicitly has an '{1}' type.": {
29092917
"category": "Error",
29102918
"code": 7005

src/compiler/factory.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,7 +1641,34 @@ namespace ts {
16411641
return react;
16421642
}
16431643

1644-
export function createReactCreateElement(reactNamespace: string, tagName: Expression, props: Expression, children: Expression[], parentElement: JsxOpeningLikeElement, location: TextRange): LeftHandSideExpression {
1644+
function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement): Expression {
1645+
if (isQualifiedName(jsxFactory)) {
1646+
return createPropertyAccess(
1647+
createJsxFactoryExpressionFromEntityName(
1648+
jsxFactory.left,
1649+
parent
1650+
),
1651+
setEmitFlags(
1652+
getMutableClone(jsxFactory.right),
1653+
EmitFlags.NoSourceMap
1654+
)
1655+
);
1656+
}
1657+
else {
1658+
return createReactNamespace(jsxFactory.text, parent);
1659+
}
1660+
}
1661+
1662+
function createJsxFactoryExpression(jsxFactoryEntity: EntityName, reactNamespace: string, parent: JsxOpeningLikeElement): Expression {
1663+
return jsxFactoryEntity ?
1664+
createJsxFactoryExpressionFromEntityName(jsxFactoryEntity, parent) :
1665+
createPropertyAccess(
1666+
createReactNamespace(reactNamespace, parent),
1667+
"createElement"
1668+
);
1669+
}
1670+
1671+
export function createExpressionForJsxElement(jsxFactoryEntity: EntityName, reactNamespace: string, tagName: Expression, props: Expression, children: Expression[], parentElement: JsxOpeningLikeElement, location: TextRange): LeftHandSideExpression {
16451672
const argumentsList = [tagName];
16461673
if (props) {
16471674
argumentsList.push(props);
@@ -1664,10 +1691,7 @@ namespace ts {
16641691
}
16651692

16661693
return createCall(
1667-
createPropertyAccess(
1668-
createReactNamespace(reactNamespace, parentElement),
1669-
"createElement"
1670-
),
1694+
createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement),
16711695
/*typeArguments*/ undefined,
16721696
argumentsList,
16731697
location

src/compiler/parser.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,10 @@ namespace ts {
440440
return result;
441441
}
442442

443+
export function parseIsolatedEntityName(text: string, languageVersion: ScriptTarget): EntityName {
444+
return Parser.parseIsolatedEntityName(text, languageVersion);
445+
}
446+
443447
export function isExternalModule(file: SourceFile): boolean {
444448
return file.externalModuleIndicator !== undefined;
445449
}
@@ -591,6 +595,16 @@ namespace ts {
591595
return result;
592596
}
593597

598+
export function parseIsolatedEntityName(content: string, languageVersion: ScriptTarget): EntityName {
599+
initializeState(content, languageVersion, /*syntaxCursor*/ undefined, ScriptKind.JS);
600+
// Prime the scanner.
601+
nextToken();
602+
const entityName = parseEntityName(/*allowReservedWords*/ true);
603+
const isInvalid = token() === SyntaxKind.EndOfFileToken && !parseDiagnostics.length;
604+
clearState();
605+
return isInvalid ? entityName : undefined;
606+
}
607+
594608
function getLanguageVariant(scriptKind: ScriptKind) {
595609
// .tsx and .jsx files are treated as jsx language variant.
596610
return scriptKind === ScriptKind.TSX || scriptKind === ScriptKind.JSX || scriptKind === ScriptKind.JS ? LanguageVariant.JSX : LanguageVariant.Standard;

src/compiler/program.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1670,7 +1670,15 @@ namespace ts {
16701670
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "emitDecoratorMetadata", "experimentalDecorators"));
16711671
}
16721672

1673-
if (options.reactNamespace && !isIdentifierText(options.reactNamespace, languageVersion)) {
1673+
if (options.jsxFactory) {
1674+
if (options.reactNamespace) {
1675+
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_0_cannot_be_specified_with_option_1, "reactNamespace", "jsxFactory"));
1676+
}
1677+
if (!parseIsolatedEntityName(options.jsxFactory, languageVersion)) {
1678+
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Invalid_value_for_jsxFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFactory));
1679+
}
1680+
}
1681+
else if (options.reactNamespace && !isIdentifierText(options.reactNamespace, languageVersion)) {
16741682
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Invalid_value_for_reactNamespace_0_is_not_a_valid_identifier, options.reactNamespace));
16751683
}
16761684

src/compiler/transformers/jsx.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ namespace ts {
113113
|| createAssignHelper(currentSourceFile.externalHelpersModuleName, segments);
114114
}
115115

116-
const element = createReactCreateElement(
116+
const element = createExpressionForJsxElement(
117+
context.getEmitResolver().getJsxFactoryEntity(),
117118
compilerOptions.reactNamespace,
118119
tagName,
119120
objectProperties,

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2485,6 +2485,7 @@ namespace ts {
24852485
getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): string[];
24862486
isLiteralConstDeclaration(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration): boolean;
24872487
writeLiteralConstValue(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration, writer: SymbolWriter): void;
2488+
getJsxFactoryEntity(): EntityName;
24882489
}
24892490

24902491
export const enum SymbolFlags {
@@ -3095,6 +3096,7 @@ namespace ts {
30953096
project?: string;
30963097
/* @internal */ pretty?: DiagnosticStyle;
30973098
reactNamespace?: string;
3099+
jsxFactory?: string;
30983100
removeComments?: boolean;
30993101
rootDir?: string;
31003102
rootDirs?: string[];

src/harness/harness.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1768,7 +1768,7 @@ namespace Harness {
17681768
}
17691769

17701770
// Regex for parsing options in the format "@Alpha: Value of any sort"
1771-
const optionRegex = /^[\/]{2}\s*@(\w+)\s*:\s*(\S*)/gm; // multiple matches on multiple lines
1771+
const optionRegex = /^[\/]{2}\s*@(\w+)\s*:\s*([^\r\n]*)/gm; // multiple matches on multiple lines
17721772

17731773
function extractCompilerSettings(content: string): CompilerSettings {
17741774
const opts: CompilerSettings = {};
@@ -1777,7 +1777,7 @@ namespace Harness {
17771777
/* tslint:disable:no-null-keyword */
17781778
while ((match = optionRegex.exec(content)) !== null) {
17791779
/* tslint:enable:no-null-keyword */
1780-
opts[match[1]] = match[2];
1780+
opts[match[1]] = match[2].trim();
17811781
}
17821782

17831783
return opts;
@@ -1805,7 +1805,7 @@ namespace Harness {
18051805
// Comment line, check for global/file @options and record them
18061806
optionRegex.lastIndex = 0;
18071807
const metaDataName = testMetaData[1].toLowerCase();
1808-
currentFileOptions[testMetaData[1]] = testMetaData[2];
1808+
currentFileOptions[testMetaData[1]] = testMetaData[2].trim();
18091809
if (metaDataName !== "filename") {
18101810
continue;
18111811
}
@@ -1825,12 +1825,12 @@ namespace Harness {
18251825
// Reset local data
18261826
currentFileContent = undefined;
18271827
currentFileOptions = {};
1828-
currentFileName = testMetaData[2];
1828+
currentFileName = testMetaData[2].trim();
18291829
refs = [];
18301830
}
18311831
else {
18321832
// First metadata marker in the file
1833-
currentFileName = testMetaData[2];
1833+
currentFileName = testMetaData[2].trim();
18341834
}
18351835
}
18361836
else {

src/harness/unittests/transpile.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,10 @@ var x = 0;`, {
385385
options: { compilerOptions: { reactNamespace: "react" }, fileName: "input.js", reportDiagnostics: true }
386386
});
387387

388+
transpilesCorrectly("Supports setting 'jsxFactory'", "x;", {
389+
options: { compilerOptions: { jsxFactory: "createElement" }, fileName: "input.js", reportDiagnostics: true }
390+
});
391+
388392
transpilesCorrectly("Supports setting 'removeComments'", "x;", {
389393
options: { compilerOptions: { removeComments: true }, fileName: "input.js", reportDiagnostics: true }
390394
});

0 commit comments

Comments
 (0)