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
23 changes: 20 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ namespace ts {
});

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

return checker;

function getJsxNamespace(): string {
if (_jsxNamespace === undefined) {
_jsxNamespace = "React";
if (compilerOptions.jsxFactory) {
const jsxEntity = host.getJsxFactoryEntity();
if (jsxEntity) {
_jsxNamespace = getFirstIdentifier(jsxEntity).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 +11354,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
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
18 changes: 16 additions & 2 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@ namespace ts {
// Map storing if there is emit blocking diagnostics for given input
const hasEmitBlockingDiagnostics = createFileMap<boolean>(getCanonicalFileName);

// ReactNamespace and jsxFactory information
let jsxFactoryEntity: EntityName;

let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModuleFull[];
if (host.resolveModuleNames) {
resolveModuleNamesWorker = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile).map(resolved => {
Expand Down Expand Up @@ -421,7 +424,8 @@ namespace ts {
getFileProcessingDiagnostics: () => fileProcessingDiagnostics,
getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives,
isSourceFileFromExternalLibrary,
dropDiagnosticsProducingTypeChecker
dropDiagnosticsProducingTypeChecker,
getJsxFactoryEntity: () => jsxFactoryEntity
};

verifyCompilerOptions();
Expand Down Expand Up @@ -727,6 +731,7 @@ namespace ts {
writeFile: writeFileCallback || (
(fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)),
isEmitBlocked,
getJsxFactoryEntity: program.getJsxFactoryEntity,
};
}

Expand Down Expand Up @@ -1670,7 +1675,16 @@ 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"));
}
jsxFactoryEntity = parseIsolatedEntityName(options.jsxFactory, languageVersion);
if (!jsxFactoryEntity) {
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.getEmitHost().getJsxFactoryEntity(),
compilerOptions.reactNamespace,
tagName,
objectProperties,
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2177,6 +2177,7 @@ namespace ts {
getTypeChecker(): TypeChecker;

/* @internal */ getCommonSourceDirectory(): string;
/* @internal */ getJsxFactoryEntity(): EntityName;

// For testing purposes only. Should not be used by any other consumers (including the
// language service).
Expand Down Expand Up @@ -2250,6 +2251,7 @@ namespace ts {
/* @internal */
export interface TypeCheckerHost {
getCompilerOptions(): CompilerOptions;
getJsxFactoryEntity(): EntityName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels odd. i would rather we parsed the jsxFactory string one more time in the checker instead.


getSourceFiles(): SourceFile[];
getSourceFile(fileName: string): SourceFile;
Expand Down Expand Up @@ -3081,6 +3083,7 @@ namespace ts {
project?: string;
/* @internal */ pretty?: DiagnosticStyle;
reactNamespace?: string;
jsxFactory?: string;
removeComments?: boolean;
rootDir?: string;
rootDirs?: string[];
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ namespace ts {
/* @internal */
isSourceFileFromExternalLibrary(file: SourceFile): boolean;

/* @internal */
getJsxFactoryEntity(): EntityName;
getCommonSourceDirectory(): string;
getCanonicalFileName(fileName: string): string;
getNewLine(): 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>
];
}
}
Loading