Skip to content

Support JSX fragments with jsxFragmentFactory compiler option and @jsxFrag pragma #38720

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

Merged
merged 1 commit into from
Jun 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 72 additions & 23 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -954,16 +954,32 @@ namespace ts {
if (location) {
const file = getSourceFileOfNode(location);
if (file) {
if (file.localJsxNamespace) {
return file.localJsxNamespace;
if (isJsxOpeningFragment(location)) {
if (file.localJsxFragmentNamespace) {
return file.localJsxFragmentNamespace;
}
const jsxFragmentPragma = file.pragmas.get("jsxfrag");
if (jsxFragmentPragma) {
const chosenPragma = isArray(jsxFragmentPragma) ? jsxFragmentPragma[0] : jsxFragmentPragma;
file.localJsxFragmentFactory = parseIsolatedEntityName(chosenPragma.arguments.factory, languageVersion);
visitNode(file.localJsxFragmentFactory, markAsSynthetic);
if (file.localJsxFragmentFactory) {
return file.localJsxFragmentNamespace = getFirstIdentifier(file.localJsxFragmentFactory).escapedText;
}
}
}
const jsxPragma = file.pragmas.get("jsx");
if (jsxPragma) {
const chosenpragma = isArray(jsxPragma) ? jsxPragma[0] : jsxPragma;
file.localJsxFactory = parseIsolatedEntityName(chosenpragma.arguments.factory, languageVersion);
visitNode(file.localJsxFactory, markAsSynthetic);
if (file.localJsxFactory) {
return file.localJsxNamespace = getFirstIdentifier(file.localJsxFactory).escapedText;
else {
if (file.localJsxNamespace) {
return file.localJsxNamespace;
}
const jsxPragma = file.pragmas.get("jsx");
if (jsxPragma) {
const chosenPragma = isArray(jsxPragma) ? jsxPragma[0] : jsxPragma;
file.localJsxFactory = parseIsolatedEntityName(chosenPragma.arguments.factory, languageVersion);
visitNode(file.localJsxFactory, markAsSynthetic);
if (file.localJsxFactory) {
return file.localJsxNamespace = getFirstIdentifier(file.localJsxFactory).escapedText;
}
}
}
}
Expand Down Expand Up @@ -23736,10 +23752,14 @@ namespace ts {
function checkJsxFragment(node: JsxFragment): Type {
checkJsxOpeningLikeElementOrOpeningFragment(node.openingFragment);

if (compilerOptions.jsx === JsxEmit.React && (compilerOptions.jsxFactory || getSourceFileOfNode(node).pragmas.has("jsx"))) {
// by default, jsx:'react' will use jsxFactory = React.createElement and jsxFragmentFactory = React.Fragment
// if jsxFactory compiler option is provided, ensure jsxFragmentFactory compiler option or @jsxFrag pragma is provided too
const nodeSourceFile = getSourceFileOfNode(node);
if (compilerOptions.jsx === JsxEmit.React && (compilerOptions.jsxFactory || nodeSourceFile.pragmas.has("jsx"))
&& !compilerOptions.jsxFragmentFactory && !nodeSourceFile.pragmas.has("jsxfrag")) {
error(node, compilerOptions.jsxFactory
? Diagnostics.JSX_fragment_is_not_supported_when_using_jsxFactory
: Diagnostics.JSX_fragment_is_not_supported_when_using_an_inline_JSX_factory_pragma);
? Diagnostics.The_jsxFragmentFactory_compiler_option_must_be_provided_to_use_JSX_fragments_with_the_jsxFactory_compiler_option
: Diagnostics.An_jsxFrag_pragma_is_required_when_using_an_jsx_pragma_with_JSX_fragments);
}

checkJsxChildren(node);
Expand Down Expand Up @@ -24197,21 +24217,28 @@ namespace ts {
if (isNodeOpeningLikeElement) {
checkGrammarJsxElement(<JsxOpeningLikeElement>node);
}

checkJsxPreconditions(node);
// 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 = diagnostics && compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined;
const reactNamespace = getJsxNamespace(node);
const reactLocation = isNodeOpeningLikeElement ? (<JsxOpeningLikeElement>node).tagName : node;
const reactSym = resolveName(reactLocation, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace, /*isUse*/ true);
if (reactSym) {
const jsxFactoryRefErr = diagnostics && compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined;
const jsxFactoryNamespace = getJsxNamespace(node);
const jsxFactoryLocation = isNodeOpeningLikeElement ? (<JsxOpeningLikeElement>node).tagName : node;

// allow null as jsxFragmentFactory
let jsxFactorySym: Symbol | undefined;
if (!(isJsxOpeningFragment(node) && jsxFactoryNamespace === "null")) {
jsxFactorySym = resolveName(jsxFactoryLocation, jsxFactoryNamespace, SymbolFlags.Value, jsxFactoryRefErr, jsxFactoryNamespace, /*isUse*/ true);
}

if (jsxFactorySym) {
// Mark local symbol as referenced here because it might not have been marked
// if jsx emit was not react as there wont be error being emitted
reactSym.isReferenced = SymbolFlags.All;
// if jsx emit was not jsxFactory as there wont be error being emitted
jsxFactorySym.isReferenced = SymbolFlags.All;

// If react symbol is alias, mark it as referenced
if (reactSym.flags & SymbolFlags.Alias && !getTypeOnlyAliasDeclaration(reactSym)) {
markAliasSymbolAsReferenced(reactSym);
// If react/jsxFactory symbol is alias, mark it as refereced
if (jsxFactorySym.flags & SymbolFlags.Alias && !getTypeOnlyAliasDeclaration(jsxFactorySym)) {
markAliasSymbolAsReferenced(jsxFactorySym);
}
}

Expand Down Expand Up @@ -36728,10 +36755,31 @@ namespace ts {
return literalTypeToNode(<FreshableType>type, node, tracker);
}

function getJsxFactoryEntity(location: Node) {
function getJsxFactoryEntity(location: Node): EntityName | undefined {
return location ? (getJsxNamespace(location), (getSourceFileOfNode(location).localJsxFactory || _jsxFactoryEntity)) : _jsxFactoryEntity;
}

function getJsxFragmentFactoryEntity(location: Node): EntityName | undefined {
if (location) {
const file = getSourceFileOfNode(location);
if (file) {
if (file.localJsxFragmentFactory) {
return file.localJsxFragmentFactory;
}
const jsxFragPragmas = file.pragmas.get("jsxfrag");
const jsxFragPragma = isArray(jsxFragPragmas) ? jsxFragPragmas[0] : jsxFragPragmas;
if (jsxFragPragma) {
file.localJsxFragmentFactory = parseIsolatedEntityName(jsxFragPragma.arguments.factory, languageVersion);
return file.localJsxFragmentFactory;
}
}
}

if (compilerOptions.jsxFragmentFactory) {
return parseIsolatedEntityName(compilerOptions.jsxFragmentFactory, languageVersion);
}
}

function createResolver(): EmitResolver {
// this variable and functions that use it are deliberately moved here from the outer scope
// to avoid scope pollution
Expand Down Expand Up @@ -36806,6 +36854,7 @@ namespace ts {
return !!(symbol && getCheckFlags(symbol) & CheckFlags.Late);
},
getJsxFactoryEntity,
getJsxFragmentFactoryEntity,
getAllAccessorDeclarations(accessor: AccessorDeclaration): AllAccessorDeclarations {
accessor = getParseTreeNode(accessor, isGetOrSetAccessorDeclaration)!; // TODO: GH#18217
const otherKind = accessor.kind === SyntaxKind.SetAccessor ? SyntaxKind.GetAccessor : SyntaxKind.SetAccessor;
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,12 @@ namespace ts {
category: Diagnostics.Advanced_Options,
description: Diagnostics.Specify_the_JSX_factory_function_to_use_when_targeting_react_JSX_emit_e_g_React_createElement_or_h
},
{
name: "jsxFragmentFactory",
type: "string",
category: Diagnostics.Advanced_Options,
description: Diagnostics.Specify_the_JSX_fragment_factory_function_to_use_when_targeting_react_JSX_emit_with_jsxFactory_compiler_option_is_specified_e_g_Fragment
},
{
name: "resolveJsonModule",
type: "boolean",
Expand Down
12 changes: 10 additions & 2 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5027,11 +5027,11 @@
"category": "Error",
"code": 17015
},
"JSX fragment is not supported when using --jsxFactory": {
"The 'jsxFragmentFactory' compiler option must be provided to use JSX fragments with the 'jsxFactory' compiler option.": {
"category": "Error",
"code": 17016
},
"JSX fragment is not supported when using an inline JSX factory pragma": {
"An @jsxFrag pragma is required when using an @jsx pragma with JSX fragments.": {
Copy link
Member

Choose a reason for hiding this comment

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

Hopefully this doesn’t turn into the GIF pronunciation debate, but I don’t pronounce the @ character so I think these articles should be “a,” not “an” 🙃

Copy link
Member

Choose a reason for hiding this comment

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

Pretty glad we moved away from "working @microsoft" to "working at @microsoft"

Copy link
Member

Choose a reason for hiding this comment

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

... I pronounce the @. 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When @weswigham first mentioned this as PR comment, I didn't fully agree with him, but when I read it over many times I was internally conflicted which way is the right way. In the end I chose to appease @weswigham 🤷

"category": "Error",
"code": 17017
},
Expand Down Expand Up @@ -5837,5 +5837,13 @@
"Only numeric enums can have computed members, but this expression has type '{0}'. If you do not need exhaustiveness checks, consider using an object literal instead.": {
"category": "Error",
"code": 18033
},
"Specify the JSX fragment factory function to use when targeting 'react' JSX emit with 'jsxFactory' compiler option is specified, e.g. 'Fragment'.": {
"category": "Message",
"code": 18034
},
"Invalid value for 'jsxFragmentFactory'. '{0}' is not a valid identifier or qualified-name.": {
"category": "Error",
"code": 18035
}
}
1 change: 1 addition & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ namespace ts {
getTypeReferenceDirectivesForSymbol: notImplemented,
isLiteralConstDeclaration: notImplemented,
getJsxFactoryEntity: notImplemented,
getJsxFragmentFactoryEntity: notImplemented,
getAllAccessorDeclarations: notImplemented,
getSymbolOfExternalModuleSpecifier: notImplemented,
isBindingCapturedByNode: notImplemented,
Expand Down
22 changes: 13 additions & 9 deletions src/compiler/factory/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ namespace ts {
);
}

function createJsxFragmentFactoryExpression(factory: NodeFactory, jsxFragmentFactoryEntity: EntityName | undefined, reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression {
return jsxFragmentFactoryEntity ?
createJsxFactoryExpressionFromEntityName(factory, jsxFragmentFactoryEntity, parent) :
factory.createPropertyAccessExpression(
createReactNamespace(reactNamespace, parent),
"Fragment"
);
}

export function createExpressionForJsxElement(factory: NodeFactory, jsxFactoryEntity: EntityName | undefined, reactNamespace: string, tagName: Expression, props: Expression | undefined, children: readonly Expression[] | undefined, parentElement: JsxOpeningLikeElement, location: TextRange): LeftHandSideExpression {
const argumentsList = [tagName];
if (props) {
Expand Down Expand Up @@ -87,14 +96,9 @@ namespace ts {
);
}

export function createExpressionForJsxFragment(factory: NodeFactory, jsxFactoryEntity: EntityName | undefined, reactNamespace: string, children: readonly Expression[], parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression {
const tagName = factory.createPropertyAccessExpression(
createReactNamespace(reactNamespace, parentElement),
"Fragment"
);

const argumentsList = [<Expression>tagName];
argumentsList.push(factory.createNull());
export function createExpressionForJsxFragment(factory: NodeFactory, jsxFactoryEntity: EntityName | undefined, jsxFragmentFactoryEntity: EntityName | undefined, reactNamespace: string, children: readonly Expression[], parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression {
const tagName = createJsxFragmentFactoryExpression(factory, jsxFragmentFactoryEntity, reactNamespace, parentElement);
const argumentsList = [tagName, factory.createNull()];

if (children && children.length > 0) {
if (children.length > 1) {
Expand Down Expand Up @@ -820,4 +824,4 @@ namespace ts {
export function isStaticModifier(node: Modifier): node is StaticKeyword {
return node.kind === SyntaxKind.StaticKeyword;
}
}
}
4 changes: 3 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8568,7 +8568,9 @@ namespace ts {
});
break;
}
case "jsx": return; // Accessed directly
case "jsx":
case "jsxfrag":
return; // Accessed directly
default: Debug.fail("Unhandled pragma kind"); // Can this be made into an assertNever in the future?
}
});
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3196,6 +3196,15 @@ namespace ts {
createOptionValueDiagnostic("reactNamespace", Diagnostics.Invalid_value_for_reactNamespace_0_is_not_a_valid_identifier, options.reactNamespace);
}

if (options.jsxFragmentFactory) {
if (!options.jsxFactory) {
createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "jsxFragmentFactory", "jsxFactory");
}
if (!parseIsolatedEntityName(options.jsxFragmentFactory, languageVersion)) {
createOptionValueDiagnostic("jsxFragmentFactory", Diagnostics.Invalid_value_for_jsxFragmentFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFragmentFactory);
}
}

// If the emit is enabled make sure that every output file is unique and not overwriting any of the input files
if (!options.noEmit && !options.suppressOutputPathCheck) {
const emitHost = getEmitHost();
Expand Down
1 change: 1 addition & 0 deletions src/compiler/transformers/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ namespace ts {
const element = createExpressionForJsxFragment(
factory,
context.getEmitResolver().getJsxFactoryEntity(currentSourceFile),
context.getEmitResolver().getJsxFragmentFactoryEntity(currentSourceFile),
compilerOptions.reactNamespace!, // TODO: GH#18217
mapDefined(children, transformJsxChildToExpression),
node,
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3454,7 +3454,9 @@ namespace ts {
/* @internal */ version: string;
/* @internal */ pragmas: ReadonlyPragmaMap;
/* @internal */ localJsxNamespace?: __String;
/* @internal */ localJsxFragmentNamespace?: __String;
/* @internal */ localJsxFactory?: EntityName;
/* @internal */ localJsxFragmentFactory?: EntityName;

/* @internal */ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit;
}
Expand Down Expand Up @@ -4463,6 +4465,7 @@ namespace ts {
getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): string[] | undefined;
isLiteralConstDeclaration(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration): boolean;
getJsxFactoryEntity(location?: Node): EntityName | undefined;
getJsxFragmentFactoryEntity(location?: Node): EntityName | undefined;
getAllAccessorDeclarations(declaration: AccessorDeclaration): AllAccessorDeclarations;
getSymbolOfExternalModuleSpecifier(node: StringLiteralLike): Symbol | undefined;
isBindingCapturedByNode(node: Node, decl: VariableDeclaration | BindingElement): boolean;
Expand Down Expand Up @@ -5681,6 +5684,7 @@ namespace ts {
/* @internal */ pretty?: boolean;
reactNamespace?: string;
jsxFactory?: string;
jsxFragmentFactory?: string;
composite?: boolean;
incremental?: boolean;
tsBuildInfoFile?: string;
Expand Down Expand Up @@ -7928,6 +7932,10 @@ namespace ts {
args: [{ name: "factory" }],
kind: PragmaKindFlags.MultiLine
},
"jsxfrag": {
args: [{ name: "factory" }],
kind: PragmaKindFlags.MultiLine
},
} as const;

/* @internal */
Expand Down
4 changes: 4 additions & 0 deletions src/testRunner/unittests/services/transpile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ var x = 0;`, {
options: { compilerOptions: { jsxFactory: "createElement" }, fileName: "input.js", reportDiagnostics: true }
});

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

transpilesCorrectly("Supports setting 'removeComments'", "x;", {
options: { compilerOptions: { removeComments: true }, fileName: "input.js", reportDiagnostics: true }
});
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2770,6 +2770,7 @@ declare namespace ts {
project?: string;
reactNamespace?: string;
jsxFactory?: string;
jsxFragmentFactory?: string;
composite?: boolean;
incremental?: boolean;
tsBuildInfoFile?: string;
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2770,6 +2770,7 @@ declare namespace ts {
project?: string;
reactNamespace?: string;
jsxFactory?: string;
jsxFragmentFactory?: string;
composite?: boolean;
incremental?: boolean;
tsBuildInfoFile?: string;
Expand Down
46 changes: 46 additions & 0 deletions tests/baselines/reference/inlineJsxAndJsxFragPragma.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//// [tests/cases/conformance/jsx/inline/inlineJsxAndJsxFragPragma.tsx] ////

//// [renderer.d.ts]
declare global {
namespace JSX {
interface IntrinsicElements {
[e: string]: any;
}
}
}
export function h(): void;
export function jsx(): void;
export function Fragment(): void;

//// [preacty.tsx]
/**
* @jsx h
* @jsxFrag Fragment
*/
import {h, Fragment} from "./renderer";
<><div></div></>

//// [snabbdomy.tsx]
/* @jsx jsx */
/* @jsxfrag null */
import {jsx} from "./renderer";
<><span></span></>

//// [preacty.js]
"use strict";
exports.__esModule = true;
/**
* @jsx h
* @jsxFrag Fragment
*/
var renderer_1 = require("./renderer");
renderer_1.h(renderer_1.Fragment, null,
renderer_1.h("div", null));
//// [snabbdomy.js]
"use strict";
exports.__esModule = true;
/* @jsx jsx */
/* @jsxfrag null */
var renderer_1 = require("./renderer");
renderer_1.jsx(null, null,
renderer_1.jsx("span", null));
Loading