Skip to content
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

Support for --jsxFactory option #12135

Merged
merged 8 commits into from
Nov 10, 2016
Merged
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 @@ -11337,10 +11356,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 @@ -19596,7 +19615,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 @@ -2381,6 +2381,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 @@ -2897,6 +2901,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 @@ -1628,7 +1628,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 @@ -1651,10 +1678,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 @@ -438,6 +438,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 @@ -589,6 +593,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 @@ -2473,6 +2473,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 @@ -3081,6 +3082,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