Skip to content

Commit

Permalink
Merge pull request #12135 from Microsoft/jsxFactory
Browse files Browse the repository at this point in the history
Support for --jsxFactory option
  • Loading branch information
sheetalkamat authored Nov 10, 2016
2 parents be5e5fb + dd7f00f commit c458576
Show file tree
Hide file tree
Showing 48 changed files with 2,961 additions and 16 deletions.
28 changes: 24 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,9 @@ namespace ts {
});

let jsxElementType: Type;
let _jsxNamespace: string;
let _jsxFactoryEntity: EntityName;

/** Things we lazy load from the JSX namespace */
const jsxTypes = createMap<Type>();
const JsxNames = {
Expand Down Expand Up @@ -372,6 +375,22 @@ namespace ts {

return checker;

function getJsxNamespace(): string {
if (_jsxNamespace === undefined) {
_jsxNamespace = "React";
if (compilerOptions.jsxFactory) {
_jsxFactoryEntity = parseIsolatedEntityName(compilerOptions.jsxFactory, languageVersion);
if (_jsxFactoryEntity) {
_jsxNamespace = getFirstIdentifier(_jsxFactoryEntity).text;
}
}
else if (compilerOptions.reactNamespace) {
_jsxNamespace = compilerOptions.reactNamespace;
}
}
return _jsxNamespace;
}

function getEmitResolver(sourceFile: SourceFile, cancellationToken: CancellationToken) {
// Ensure we have all the type information in place for this file so that all the
// emitter questions of this resolver will return the right information.
Expand Down Expand Up @@ -11468,10 +11487,10 @@ namespace ts {
function checkJsxOpeningLikeElement(node: JsxOpeningLikeElement) {
checkGrammarJsxElement(node);
checkJsxPreconditions(node);
// The reactNamespace symbol should be marked as 'used' so we don't incorrectly elide its import. And if there
// is no reactNamespace symbol in scope when targeting React emit, we should issue an error.
// The reactNamespace/jsxFactory's root symbol should be marked as 'used' so we don't incorrectly elide its import.
// And if there is no reactNamespace/jsxFactory's symbol in scope when targeting React emit, we should issue an error.
const reactRefErr = compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined;
const reactNamespace = compilerOptions.reactNamespace ? compilerOptions.reactNamespace : "React";
const reactNamespace = getJsxNamespace();
const reactSym = resolveName(node.tagName, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace);
if (reactSym) {
// Mark local symbol as referenced here because it might not have been marked
Expand Down Expand Up @@ -19738,7 +19757,8 @@ namespace ts {
getTypeReferenceDirectivesForEntityName,
getTypeReferenceDirectivesForSymbol,
isLiteralConstDeclaration,
writeLiteralConstValue
writeLiteralConstValue,
getJsxFactoryEntity: () => _jsxFactoryEntity
};

// defined here to avoid outer scope pollution
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ namespace ts {
type: "string",
description: Diagnostics.Specify_the_object_invoked_for_createElement_and_spread_when_targeting_react_JSX_emit
},
{
name: "jsxFactory",
type: "string",
description: Diagnostics.Specify_the_JSX_factory_function_to_use_when_targeting_react_JSX_emit_e_g_React_createElement_or_h
},
{
name: "listFiles",
type: "boolean",
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2389,6 +2389,10 @@
"category": "Error",
"code": 5066
},
"Invalid value for 'jsxFactory'. '{0}' is not a valid identifier or qualified-name.": {
"category": "Error",
"code": 5067
},
"Concatenate and emit output to single file.": {
"category": "Message",
"code": 6001
Expand Down Expand Up @@ -2905,6 +2909,10 @@
"category": "Message",
"code": 6145
},
"Specify the JSX factory function to use when targeting 'react' JSX emit, e.g. 'React.createElement' or 'h'.": {
"category": "Message",
"code": 6146
},
"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
"code": 7005
Expand Down
34 changes: 29 additions & 5 deletions src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1641,7 +1641,34 @@ namespace ts {
return react;
}

export function createReactCreateElement(reactNamespace: string, tagName: Expression, props: Expression, children: Expression[], parentElement: JsxOpeningLikeElement, location: TextRange): LeftHandSideExpression {
function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement): Expression {
if (isQualifiedName(jsxFactory)) {
return createPropertyAccess(
createJsxFactoryExpressionFromEntityName(
jsxFactory.left,
parent
),
setEmitFlags(
getMutableClone(jsxFactory.right),
EmitFlags.NoSourceMap
)
);
}
else {
return createReactNamespace(jsxFactory.text, parent);
}
}

function createJsxFactoryExpression(jsxFactoryEntity: EntityName, reactNamespace: string, parent: JsxOpeningLikeElement): Expression {
return jsxFactoryEntity ?
createJsxFactoryExpressionFromEntityName(jsxFactoryEntity, parent) :
createPropertyAccess(
createReactNamespace(reactNamespace, parent),
"createElement"
);
}

export function createExpressionForJsxElement(jsxFactoryEntity: EntityName, reactNamespace: string, tagName: Expression, props: Expression, children: Expression[], parentElement: JsxOpeningLikeElement, location: TextRange): LeftHandSideExpression {
const argumentsList = [tagName];
if (props) {
argumentsList.push(props);
Expand All @@ -1664,10 +1691,7 @@ namespace ts {
}

return createCall(
createPropertyAccess(
createReactNamespace(reactNamespace, parentElement),
"createElement"
),
createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement),
/*typeArguments*/ undefined,
argumentsList,
location
Expand Down
14 changes: 14 additions & 0 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,10 @@ namespace ts {
return result;
}

export function parseIsolatedEntityName(text: string, languageVersion: ScriptTarget): EntityName {
return Parser.parseIsolatedEntityName(text, languageVersion);
}

export function isExternalModule(file: SourceFile): boolean {
return file.externalModuleIndicator !== undefined;
}
Expand Down Expand Up @@ -591,6 +595,16 @@ namespace ts {
return result;
}

export function parseIsolatedEntityName(content: string, languageVersion: ScriptTarget): EntityName {
initializeState(content, languageVersion, /*syntaxCursor*/ undefined, ScriptKind.JS);
// Prime the scanner.
nextToken();
const entityName = parseEntityName(/*allowReservedWords*/ true);
const isInvalid = token() === SyntaxKind.EndOfFileToken && !parseDiagnostics.length;
clearState();
return isInvalid ? entityName : undefined;
}

function getLanguageVariant(scriptKind: ScriptKind) {
// .tsx and .jsx files are treated as jsx language variant.
return scriptKind === ScriptKind.TSX || scriptKind === ScriptKind.JSX || scriptKind === ScriptKind.JS ? LanguageVariant.JSX : LanguageVariant.Standard;
Expand Down
10 changes: 9 additions & 1 deletion src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1670,7 +1670,15 @@ namespace ts {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "emitDecoratorMetadata", "experimentalDecorators"));
}

if (options.reactNamespace && !isIdentifierText(options.reactNamespace, languageVersion)) {
if (options.jsxFactory) {
if (options.reactNamespace) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_0_cannot_be_specified_with_option_1, "reactNamespace", "jsxFactory"));
}
if (!parseIsolatedEntityName(options.jsxFactory, languageVersion)) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Invalid_value_for_jsxFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFactory));
}
}
else if (options.reactNamespace && !isIdentifierText(options.reactNamespace, languageVersion)) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Invalid_value_for_reactNamespace_0_is_not_a_valid_identifier, options.reactNamespace));
}

Expand Down
3 changes: 2 additions & 1 deletion src/compiler/transformers/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ namespace ts {
|| createAssignHelper(currentSourceFile.externalHelpersModuleName, segments);
}

const element = createReactCreateElement(
const element = createExpressionForJsxElement(
context.getEmitResolver().getJsxFactoryEntity(),
compilerOptions.reactNamespace,
tagName,
objectProperties,
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2485,6 +2485,7 @@ namespace ts {
getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): string[];
isLiteralConstDeclaration(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration): boolean;
writeLiteralConstValue(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration, writer: SymbolWriter): void;
getJsxFactoryEntity(): EntityName;
}

export const enum SymbolFlags {
Expand Down Expand Up @@ -3095,6 +3096,7 @@ namespace ts {
project?: string;
/* @internal */ pretty?: DiagnosticStyle;
reactNamespace?: string;
jsxFactory?: string;
removeComments?: boolean;
rootDir?: string;
rootDirs?: string[];
Expand Down
10 changes: 5 additions & 5 deletions src/harness/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1768,7 +1768,7 @@ namespace Harness {
}

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

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

return opts;
Expand Down Expand Up @@ -1805,7 +1805,7 @@ namespace Harness {
// Comment line, check for global/file @options and record them
optionRegex.lastIndex = 0;
const metaDataName = testMetaData[1].toLowerCase();
currentFileOptions[testMetaData[1]] = testMetaData[2];
currentFileOptions[testMetaData[1]] = testMetaData[2].trim();
if (metaDataName !== "filename") {
continue;
}
Expand All @@ -1825,12 +1825,12 @@ namespace Harness {
// Reset local data
currentFileContent = undefined;
currentFileOptions = {};
currentFileName = testMetaData[2];
currentFileName = testMetaData[2].trim();
refs = [];
}
else {
// First metadata marker in the file
currentFileName = testMetaData[2];
currentFileName = testMetaData[2].trim();
}
}
else {
Expand Down
4 changes: 4 additions & 0 deletions src/harness/unittests/transpile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,10 @@ var x = 0;`, {
options: { compilerOptions: { reactNamespace: "react" }, fileName: "input.js", reportDiagnostics: true }
});

transpilesCorrectly("Supports setting 'jsxFactory'", "x;", {
options: { compilerOptions: { jsxFactory: "createElement" }, fileName: "input.js", reportDiagnostics: true }
});

transpilesCorrectly("Supports setting 'removeComments'", "x;", {
options: { compilerOptions: { removeComments: true }, fileName: "input.js", reportDiagnostics: true }
});
Expand Down
53 changes: 53 additions & 0 deletions tests/baselines/reference/jsxFactoryAndReactNamespace.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
error TS5053: Option 'reactNamespace' cannot be specified with option 'jsxFactory'.


!!! error TS5053: Option 'reactNamespace' cannot be specified with option 'jsxFactory'.
==== tests/cases/compiler/Element.ts (0 errors) ====

declare namespace JSX {
interface Element {
name: string;
isIntrinsic: boolean;
isCustomElement: boolean;
toString(renderId?: number): string;
bindDOM(renderId?: number): number;
resetComponent(): void;
instantiateComponents(renderId?: number): number;
props: any;
}
}
export namespace Element {
export function isElement(el: any): el is JSX.Element {
return el.markAsChildOfRootElement !== undefined;
}

export function createElement(args: any[]) {

return {
}
}
}

export let createElement = Element.createElement;

function toCamelCase(text: string): string {
return text[0].toLowerCase() + text.substring(1);
}

==== tests/cases/compiler/test.tsx (0 errors) ====
import { Element} from './Element';

let c: {
a?: {
b: string
}
};

class A {
view() {
return [
<meta content="helloworld"></meta>,
<meta content={c.a!.b}></meta>
];
}
}
81 changes: 81 additions & 0 deletions tests/baselines/reference/jsxFactoryAndReactNamespace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//// [tests/cases/compiler/jsxFactoryAndReactNamespace.ts] ////

//// [Element.ts]

declare namespace JSX {
interface Element {
name: string;
isIntrinsic: boolean;
isCustomElement: boolean;
toString(renderId?: number): string;
bindDOM(renderId?: number): number;
resetComponent(): void;
instantiateComponents(renderId?: number): number;
props: any;
}
}
export namespace Element {
export function isElement(el: any): el is JSX.Element {
return el.markAsChildOfRootElement !== undefined;
}

export function createElement(args: any[]) {

return {
}
}
}

export let createElement = Element.createElement;

function toCamelCase(text: string): string {
return text[0].toLowerCase() + text.substring(1);
}

//// [test.tsx]
import { Element} from './Element';

let c: {
a?: {
b: string
}
};

class A {
view() {
return [
<meta content="helloworld"></meta>,
<meta content={c.a!.b}></meta>
];
}
}

//// [Element.js]
"use strict";
var Element;
(function (Element) {
function isElement(el) {
return el.markAsChildOfRootElement !== undefined;
}
Element.isElement = isElement;
function createElement(args) {
return {};
}
Element.createElement = createElement;
})(Element = exports.Element || (exports.Element = {}));
exports.createElement = Element.createElement;
function toCamelCase(text) {
return text[0].toLowerCase() + text.substring(1);
}
//// [test.js]
"use strict";
const Element_1 = require("./Element");
let c;
class A {
view() {
return [
Element_1.Element.createElement("meta", { content: "helloworld" }),
Element_1.Element.createElement("meta", { content: c.a.b })
];
}
}
Loading

0 comments on commit c458576

Please sign in to comment.