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

+24-4
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

+5
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

+8
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

+29-5
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

+14
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

+9-1
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

+2-1
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

+2
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

+5-5
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

+4
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
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
error TS5053: Option 'reactNamespace' cannot be specified with option 'jsxFactory'.
2+
3+
4+
!!! error TS5053: Option 'reactNamespace' cannot be specified with option 'jsxFactory'.
5+
==== tests/cases/compiler/Element.ts (0 errors) ====
6+
7+
declare namespace JSX {
8+
interface Element {
9+
name: string;
10+
isIntrinsic: boolean;
11+
isCustomElement: boolean;
12+
toString(renderId?: number): string;
13+
bindDOM(renderId?: number): number;
14+
resetComponent(): void;
15+
instantiateComponents(renderId?: number): number;
16+
props: any;
17+
}
18+
}
19+
export namespace Element {
20+
export function isElement(el: any): el is JSX.Element {
21+
return el.markAsChildOfRootElement !== undefined;
22+
}
23+
24+
export function createElement(args: any[]) {
25+
26+
return {
27+
}
28+
}
29+
}
30+
31+
export let createElement = Element.createElement;
32+
33+
function toCamelCase(text: string): string {
34+
return text[0].toLowerCase() + text.substring(1);
35+
}
36+
37+
==== tests/cases/compiler/test.tsx (0 errors) ====
38+
import { Element} from './Element';
39+
40+
let c: {
41+
a?: {
42+
b: string
43+
}
44+
};
45+
46+
class A {
47+
view() {
48+
return [
49+
<meta content="helloworld"></meta>,
50+
<meta content={c.a!.b}></meta>
51+
];
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//// [tests/cases/compiler/jsxFactoryAndReactNamespace.ts] ////
2+
3+
//// [Element.ts]
4+
5+
declare namespace JSX {
6+
interface Element {
7+
name: string;
8+
isIntrinsic: boolean;
9+
isCustomElement: boolean;
10+
toString(renderId?: number): string;
11+
bindDOM(renderId?: number): number;
12+
resetComponent(): void;
13+
instantiateComponents(renderId?: number): number;
14+
props: any;
15+
}
16+
}
17+
export namespace Element {
18+
export function isElement(el: any): el is JSX.Element {
19+
return el.markAsChildOfRootElement !== undefined;
20+
}
21+
22+
export function createElement(args: any[]) {
23+
24+
return {
25+
}
26+
}
27+
}
28+
29+
export let createElement = Element.createElement;
30+
31+
function toCamelCase(text: string): string {
32+
return text[0].toLowerCase() + text.substring(1);
33+
}
34+
35+
//// [test.tsx]
36+
import { Element} from './Element';
37+
38+
let c: {
39+
a?: {
40+
b: string
41+
}
42+
};
43+
44+
class A {
45+
view() {
46+
return [
47+
<meta content="helloworld"></meta>,
48+
<meta content={c.a!.b}></meta>
49+
];
50+
}
51+
}
52+
53+
//// [Element.js]
54+
"use strict";
55+
var Element;
56+
(function (Element) {
57+
function isElement(el) {
58+
return el.markAsChildOfRootElement !== undefined;
59+
}
60+
Element.isElement = isElement;
61+
function createElement(args) {
62+
return {};
63+
}
64+
Element.createElement = createElement;
65+
})(Element = exports.Element || (exports.Element = {}));
66+
exports.createElement = Element.createElement;
67+
function toCamelCase(text) {
68+
return text[0].toLowerCase() + text.substring(1);
69+
}
70+
//// [test.js]
71+
"use strict";
72+
const Element_1 = require("./Element");
73+
let c;
74+
class A {
75+
view() {
76+
return [
77+
Element_1.Element.createElement("meta", { content: "helloworld" }),
78+
Element_1.Element.createElement("meta", { content: c.a.b })
79+
];
80+
}
81+
}

0 commit comments

Comments
 (0)