diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 065fc617857c1..79f5fc6fdce7a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22400,10 +22400,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 emit with React.createElement and React.Fragment + // if a custom jsxFactory is used, ensure jsxFragFactory or jsxFrag pragma is defined too + const nodeSourceFile = getSourceFileOfNode(node); + if (compilerOptions.jsx === JsxEmit.React && (compilerOptions.jsxFactory || nodeSourceFile.pragmas.has("jsx")) + && !compilerOptions.jsxFragFactory && !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.JSX_fragment_needs_corresponding_jsxFragFactory_compiler_option_when_using_jsxFactory_compiler_option + : Diagnostics.JSX_fragment_needs_corresponding_jsxFrag_prama_when_using_jsx_pragma); } checkJsxChildren(node); @@ -34951,6 +34955,26 @@ namespace ts { return !!(symbol && getCheckFlags(symbol) & CheckFlags.Late); }, getJsxFactoryEntity: location => location ? (getJsxNamespace(location), (getSourceFileOfNode(location).localJsxFactory || _jsxFactoryEntity)) : _jsxFactoryEntity, + getJsxFragFactoryEntity: (location): EntityName | undefined => { + if (location) { + const file = getSourceFileOfNode(location); + if (file) { + if (file.localJsxFragFactory) { + return file.localJsxFragFactory; + } + const jsxFragPragmas = file.pragmas.get("jsxFrag"); + const jsxFragPragma = Array.isArray(jsxFragPragmas) ? jsxFragPragmas[0] : jsxFragPragmas; + if (jsxFragPragma) { + file.localJsxFragFactory = parseIsolatedEntityName(jsxFragPragma.arguments.factory, languageVersion); + return file.localJsxFragFactory; + } + } + } + + if (compilerOptions.jsxFragFactory) { + return parseIsolatedEntityName(compilerOptions.jsxFragFactory, languageVersion); + } + }, getAllAccessorDeclarations(accessor: AccessorDeclaration): AllAccessorDeclarations { accessor = getParseTreeNode(accessor, isGetOrSetAccessorDeclaration)!; // TODO: GH#18217 const otherKind = accessor.kind === SyntaxKind.SetAccessor ? SyntaxKind.GetAccessor : SyntaxKind.SetAccessor; diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 8fd84130cbc47..7c8b4ba7081e2 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -759,13 +759,18 @@ 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: "jsxFragFactory", + 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_specified_e_g_Preact_Fragment + }, { name: "resolveJsonModule", type: "boolean", category: Diagnostics.Advanced_Options, description: Diagnostics.Include_modules_imported_with_json_extension }, - { name: "out", type: "string", diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index e9c54b7c19921..fc48529d9ad98 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4847,11 +4847,11 @@ "category": "Error", "code": 17015 }, - "JSX fragment is not supported when using --jsxFactory": { + "JSX fragment needs corresponding `jsxFragFactory` compiler option when using `jsxFactory` compiler option.": { "category": "Error", "code": 17016 }, - "JSX fragment is not supported when using an inline JSX factory pragma": { + "JSX fragment needs corresponding @jsxFrag prama when using @jsx pragma.": { "category": "Error", "code": 17017 }, @@ -5505,5 +5505,13 @@ "An optional chain cannot contain private identifiers.": { "category": "Error", "code": 18030 + }, + "Invalid value for 'jsxFragFactory'. '{0}' is not a valid identifier or qualified-name.": { + "category": "Error", + "code": 18031 + }, + "Specify the JSX fragment factory function to use when targeting 'react' JSX emit with `jsxFactory` compiler option specified, e.g. 'Preact.Fragment'.": { + "category": "Message", + "code": 18032 } } diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index d576083087102..fb29863857268 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -656,6 +656,7 @@ namespace ts { getTypeReferenceDirectivesForSymbol: notImplemented, isLiteralConstDeclaration: notImplemented, getJsxFactoryEntity: notImplemented, + getJsxFragFactoryEntity: notImplemented, getAllAccessorDeclarations: notImplemented, getSymbolOfExternalModuleSpecifier: notImplemented, isBindingCapturedByNode: notImplemented, diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 64613525939cd..83de2a8e58416 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -135,6 +135,15 @@ namespace ts { ); } + function createJsxFragFactoryExpression(jsxFragFactoryEntity: EntityName | undefined, reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression { + return jsxFragFactoryEntity ? + createJsxFactoryExpressionFromEntityName(jsxFragFactoryEntity, parent) : + createPropertyAccess( + createReactNamespace(reactNamespace, parent), + "Fragment" + ); + } + export function createExpressionForJsxElement(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, tagName: Expression, props: Expression, children: readonly Expression[], parentElement: JsxOpeningLikeElement, location: TextRange): LeftHandSideExpression { const argumentsList = [tagName]; if (props) { @@ -167,14 +176,9 @@ namespace ts { ); } - export function createExpressionForJsxFragment(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, children: readonly Expression[], parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression { - const tagName = createPropertyAccess( - createReactNamespace(reactNamespace, parentElement), - "Fragment" - ); - - const argumentsList = [tagName]; - argumentsList.push(createNull()); + export function createExpressionForJsxFragment(jsxFactoryEntity: EntityName | undefined, jsxFragFactoryEntity: EntityName | undefined, reactNamespace: string, children: readonly Expression[], parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression { + const tagName = createJsxFragFactoryExpression(jsxFragFactoryEntity, reactNamespace, parentElement); + const argumentsList = [tagName, createNull()]; if (children && children.length > 0) { if (children.length > 1) { diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 0ea7d1c2889f7..ebc3b6de9f437 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -3165,6 +3165,16 @@ namespace ts { createOptionValueDiagnostic("jsxFactory", Diagnostics.Invalid_value_for_jsxFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFactory); } } + + if (options.jsxFragFactory) { + if (!options.jsxFactory) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "jsxFragFactory", "jsxFactory"); + } + if (!parseIsolatedEntityName(options.jsxFragFactory, languageVersion)) { + createOptionValueDiagnostic("jsxFragFactory", Diagnostics.Invalid_value_for_jsxFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFragFactory); + } + } + else if (options.reactNamespace && !isIdentifierText(options.reactNamespace, languageVersion)) { createOptionValueDiagnostic("reactNamespace", Diagnostics.Invalid_value_for_reactNamespace_0_is_not_a_valid_identifier, options.reactNamespace); } diff --git a/src/compiler/transformers/jsx.ts b/src/compiler/transformers/jsx.ts index 308bfc18664e4..8da0147332106 100644 --- a/src/compiler/transformers/jsx.ts +++ b/src/compiler/transformers/jsx.ts @@ -136,6 +136,7 @@ namespace ts { function visitJsxOpeningFragment(node: JsxOpeningFragment, children: readonly JsxChild[], isChild: boolean, location: TextRange) { const element = createExpressionForJsxFragment( context.getEmitResolver().getJsxFactoryEntity(currentSourceFile), + context.getEmitResolver().getJsxFragFactoryEntity(currentSourceFile), compilerOptions.reactNamespace!, // TODO: GH#18217 mapDefined(children, transformJsxChildToExpression), node, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 12aae7b465cc8..bf16abe94bfbd 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2980,6 +2980,7 @@ namespace ts { /* @internal */ pragmas: ReadonlyPragmaMap; /* @internal */ localJsxNamespace?: __String; /* @internal */ localJsxFactory?: EntityName; + /* @internal */ localJsxFragFactory?: EntityName; /*@internal*/ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit; } @@ -3927,6 +3928,7 @@ namespace ts { getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): string[] | undefined; isLiteralConstDeclaration(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration): boolean; getJsxFactoryEntity(location?: Node): EntityName | undefined; + getJsxFragFactoryEntity(location?: Node): EntityName | undefined; getAllAccessorDeclarations(declaration: AccessorDeclaration): AllAccessorDeclarations; getSymbolOfExternalModuleSpecifier(node: StringLiteralLike): Symbol | undefined; isBindingCapturedByNode(node: Node, decl: VariableDeclaration | BindingElement): boolean; @@ -5088,6 +5090,7 @@ namespace ts { /* @internal */ pretty?: boolean; reactNamespace?: string; jsxFactory?: string; + jsxFragFactory?: string; composite?: boolean; incremental?: boolean; tsBuildInfoFile?: string; @@ -6520,6 +6523,10 @@ namespace ts { args: [{ name: "factory" }], kind: PragmaKindFlags.MultiLine }, + "jsxFrag": { + args: [{ name: "factory" }], + kind: PragmaKindFlags.MultiLine + }, } as const; /* @internal */ diff --git a/src/testRunner/unittests/services/transpile.ts b/src/testRunner/unittests/services/transpile.ts index ed4a943357322..528839af692d3 100644 --- a/src/testRunner/unittests/services/transpile.ts +++ b/src/testRunner/unittests/services/transpile.ts @@ -363,6 +363,10 @@ var x = 0;`, { options: { compilerOptions: { jsxFactory: "createElement" }, fileName: "input.js", reportDiagnostics: true } }); + transpilesCorrectly("Supports setting 'jsxFragFactory'", "x;", { + options: { compilerOptions: { jsxFragFactory: "x.frag" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'removeComments'", "x;", { options: { compilerOptions: { removeComments: true }, fileName: "input.js", reportDiagnostics: true } }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 279905f477905..b46e96a8d9448 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2688,6 +2688,7 @@ declare namespace ts { project?: string; reactNamespace?: string; jsxFactory?: string; + jsxFragFactory?: string; composite?: boolean; incremental?: boolean; tsBuildInfoFile?: string; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 320f17193d775..1310fa52ca94b 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2688,6 +2688,7 @@ declare namespace ts { project?: string; reactNamespace?: string; jsxFactory?: string; + jsxFragFactory?: string; composite?: boolean; incremental?: boolean; tsBuildInfoFile?: string; diff --git a/tests/baselines/reference/inlineJsxFactoryWithFragmentIsError.errors.txt b/tests/baselines/reference/inlineJsxFactoryWithFragmentIsError.errors.txt index b70ddf5f9b9aa..0080a3883bf9c 100644 --- a/tests/baselines/reference/inlineJsxFactoryWithFragmentIsError.errors.txt +++ b/tests/baselines/reference/inlineJsxFactoryWithFragmentIsError.errors.txt @@ -1,5 +1,5 @@ -tests/cases/conformance/jsx/inline/index.tsx(3,1): error TS17017: JSX fragment is not supported when using an inline JSX factory pragma -tests/cases/conformance/jsx/inline/reacty.tsx(3,1): error TS17017: JSX fragment is not supported when using an inline JSX factory pragma +tests/cases/conformance/jsx/inline/index.tsx(3,1): error TS17017: JSX fragment needs corresponding @jsxFrag prama when using @jsx pragma. +tests/cases/conformance/jsx/inline/reacty.tsx(3,1): error TS17017: JSX fragment needs corresponding @jsxFrag prama when using @jsx pragma. ==== tests/cases/conformance/jsx/inline/renderer.d.ts (0 errors) ==== @@ -17,10 +17,10 @@ tests/cases/conformance/jsx/inline/reacty.tsx(3,1): error TS17017: JSX fragment import * as React from "./renderer"; <> ~~~~~~~~~~~~ -!!! error TS17017: JSX fragment is not supported when using an inline JSX factory pragma +!!! error TS17017: JSX fragment needs corresponding @jsxFrag prama when using @jsx pragma. ==== tests/cases/conformance/jsx/inline/index.tsx (1 errors) ==== /** @jsx dom */ import { dom } from "./renderer"; <> ~~~~~~~~~~~~ -!!! error TS17017: JSX fragment is not supported when using an inline JSX factory pragma \ No newline at end of file +!!! error TS17017: JSX fragment needs corresponding @jsxFrag prama when using @jsx pragma. \ No newline at end of file diff --git a/tests/baselines/reference/jsxFactoryAndFragment.errors.txt b/tests/baselines/reference/jsxFactoryAndFragment.errors.txt index bec2ea5cc0fb6..d05c34af41691 100644 --- a/tests/baselines/reference/jsxFactoryAndFragment.errors.txt +++ b/tests/baselines/reference/jsxFactoryAndFragment.errors.txt @@ -1,6 +1,6 @@ -tests/cases/compiler/jsxFactoryAndFragment.tsx(3,1): error TS17016: JSX fragment is not supported when using --jsxFactory -tests/cases/compiler/jsxFactoryAndFragment.tsx(4,1): error TS17016: JSX fragment is not supported when using --jsxFactory -tests/cases/compiler/jsxFactoryAndFragment.tsx(4,17): error TS17016: JSX fragment is not supported when using --jsxFactory +tests/cases/compiler/jsxFactoryAndFragment.tsx(3,1): error TS17016: JSX fragment needs corresponding `jsxFragFactory` compiler option when using `jsxFactory` compiler option. +tests/cases/compiler/jsxFactoryAndFragment.tsx(4,1): error TS17016: JSX fragment needs corresponding `jsxFragFactory` compiler option when using `jsxFactory` compiler option. +tests/cases/compiler/jsxFactoryAndFragment.tsx(4,17): error TS17016: JSX fragment needs corresponding `jsxFragFactory` compiler option when using `jsxFactory` compiler option. ==== tests/cases/compiler/jsxFactoryAndFragment.tsx (3 errors) ==== @@ -8,9 +8,9 @@ tests/cases/compiler/jsxFactoryAndFragment.tsx(4,17): error TS17016: JSX fragmen <>; ~~~~~ -!!! error TS17016: JSX fragment is not supported when using --jsxFactory +!!! error TS17016: JSX fragment needs corresponding `jsxFragFactory` compiler option when using `jsxFactory` compiler option. <>1<>2.12.2; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -!!! error TS17016: JSX fragment is not supported when using --jsxFactory +!!! error TS17016: JSX fragment needs corresponding `jsxFragFactory` compiler option when using `jsxFactory` compiler option. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -!!! error TS17016: JSX fragment is not supported when using --jsxFactory \ No newline at end of file +!!! error TS17016: JSX fragment needs corresponding `jsxFragFactory` compiler option when using `jsxFactory` compiler option. \ No newline at end of file diff --git a/tests/baselines/reference/showConfig/Shows tsconfig for single option/jsxFragFactory/tsconfig.json b/tests/baselines/reference/showConfig/Shows tsconfig for single option/jsxFragFactory/tsconfig.json new file mode 100644 index 0000000000000..feaf4ac1607e9 --- /dev/null +++ b/tests/baselines/reference/showConfig/Shows tsconfig for single option/jsxFragFactory/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "jsxFragFactory": "someString" + } +} diff --git a/tests/baselines/reference/transpile/Supports setting jsxFragFactory.errors.txt b/tests/baselines/reference/transpile/Supports setting jsxFragFactory.errors.txt new file mode 100644 index 0000000000000..f377c6934e3e8 --- /dev/null +++ b/tests/baselines/reference/transpile/Supports setting jsxFragFactory.errors.txt @@ -0,0 +1,6 @@ +error TS5052: Option 'jsxFragFactory' cannot be specified without specifying option 'jsxFactory'. + + +!!! error TS5052: Option 'jsxFragFactory' cannot be specified without specifying option 'jsxFactory'. +==== input.js (0 errors) ==== + x; \ No newline at end of file diff --git a/tests/baselines/reference/transpile/Supports setting jsxFragFactory.js b/tests/baselines/reference/transpile/Supports setting jsxFragFactory.js new file mode 100644 index 0000000000000..8394371f9081a --- /dev/null +++ b/tests/baselines/reference/transpile/Supports setting jsxFragFactory.js @@ -0,0 +1,2 @@ +x; +//# sourceMappingURL=input.js.map \ No newline at end of file diff --git a/tests/baselines/reference/transpile/Supports setting jsxFragFactory.oldTranspile.errors.txt b/tests/baselines/reference/transpile/Supports setting jsxFragFactory.oldTranspile.errors.txt new file mode 100644 index 0000000000000..f377c6934e3e8 --- /dev/null +++ b/tests/baselines/reference/transpile/Supports setting jsxFragFactory.oldTranspile.errors.txt @@ -0,0 +1,6 @@ +error TS5052: Option 'jsxFragFactory' cannot be specified without specifying option 'jsxFactory'. + + +!!! error TS5052: Option 'jsxFragFactory' cannot be specified without specifying option 'jsxFactory'. +==== input.js (0 errors) ==== + x; \ No newline at end of file diff --git a/tests/baselines/reference/transpile/Supports setting jsxFragFactory.oldTranspile.js b/tests/baselines/reference/transpile/Supports setting jsxFragFactory.oldTranspile.js new file mode 100644 index 0000000000000..8394371f9081a --- /dev/null +++ b/tests/baselines/reference/transpile/Supports setting jsxFragFactory.oldTranspile.js @@ -0,0 +1,2 @@ +x; +//# sourceMappingURL=input.js.map \ No newline at end of file