From e9bc44fe561f64434d8da3212ae7cb96c9c4bd62 Mon Sep 17 00:00:00 2001 From: Alan Pierce Date: Fri, 2 Sep 2022 15:15:47 -0700 Subject: [PATCH] Minor refactors in prep for new JSX runtime (#742) This PR has a few details pulled from #740 to make it easier to read the substantial changes in that PR: * Reorder the options interface to better group related options together. * Reorder the methods in JSXTransformer to be in (roughly) top-down topological order rather than the more scattered order that it was previously. * Change website advanced options to dynamically only show the ones relevant based on what is selected. --- src/Options-gen-types.ts | 8 +- src/Options.ts | 61 +++++----- src/transformers/JSXTransformer.ts | 174 ++++++++++++++--------------- website/src/Constants.ts | 8 +- website/src/SucraseOptionsBox.tsx | 85 ++++++++------ 5 files changed, 176 insertions(+), 160 deletions(-) diff --git a/src/Options-gen-types.ts b/src/Options-gen-types.ts index 584394ce..203885bb 100644 --- a/src/Options-gen-types.ts +++ b/src/Options-gen-types.ts @@ -19,16 +19,16 @@ export const SourceMapOptions = t.iface([], { export const Options = t.iface([], { transforms: t.array("Transform"), + disableESTransforms: t.opt("boolean"), + production: t.opt("boolean"), jsxPragma: t.opt("string"), jsxFragmentPragma: t.opt("string"), + preserveDynamicImport: t.opt("boolean"), + injectCreateRequireForImportRequire: t.opt("boolean"), enableLegacyTypeScriptModuleInterop: t.opt("boolean"), enableLegacyBabel5ModuleInterop: t.opt("boolean"), sourceMapOptions: t.opt("SourceMapOptions"), filePath: t.opt("string"), - production: t.opt("boolean"), - disableESTransforms: t.opt("boolean"), - preserveDynamicImport: t.opt("boolean"), - injectCreateRequireForImportRequire: t.opt("boolean"), }); const exportedTypeSuite: t.ITypeSuite = { diff --git a/src/Options.ts b/src/Options.ts index f56a2c26..2eef3da2 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -15,45 +15,31 @@ export interface SourceMapOptions { } export interface Options { - transforms: Array; - /** - * If specified, function name to use in place of React.createClass when compiling JSX. - */ - jsxPragma?: string; /** - * If specified, function name to use in place of React.Fragment when compiling JSX. + * Unordered array of transform names describing both the allowed syntax + * (where applicable) and the transformation behavior. */ - jsxFragmentPragma?: string; - /** - * If true, replicate the import behavior of TypeScript's esModuleInterop: false. - */ - enableLegacyTypeScriptModuleInterop?: boolean; - /** - * If true, replicate the import behavior Babel 5 and babel-plugin-add-module-exports. - */ - enableLegacyBabel5ModuleInterop?: boolean; + transforms: Array; /** - * If specified, we also return a RawSourceMap object alongside the code. Currently, source maps - * simply map each line to the original line without any mappings within lines, since Sucrase - * preserves line numbers. filePath must be specified if this option is enabled. + * Opts out of all ES syntax transformations: optional chaining, nullish + * coalescing, class fields, numeric separators, optional catch binding. */ - sourceMapOptions?: SourceMapOptions; + disableESTransforms?: boolean; /** - * File path to use in error messages, React display names, and source maps. + * Compile code for production use. Currently only applies to the JSX transform. */ - filePath?: string; + production?: boolean; /** - * If specified, omit any development-specific code in the output. + * If specified, function name to use in place of React.createClass when compiling JSX. */ - production?: boolean; + jsxPragma?: string; /** - * Opts out ES syntax transformations, like optional chaining, nullish coalescing, numeric - * separators, etc. + * If specified, function name to use in place of React.Fragment when compiling JSX. */ - disableESTransforms?: boolean; + jsxFragmentPragma?: string; /** - * If specified, the imports transform does not attempt to change dynamic import() - * expressions into require() calls. + * If specified, the imports transform does not attempt to change dynamic + * import() expressions into require() calls. */ preserveDynamicImport?: boolean; /** @@ -67,6 +53,25 @@ export interface Options { * same code to target ESM and CJS. */ injectCreateRequireForImportRequire?: boolean; + /** + * If true, replicate the import behavior of TypeScript's esModuleInterop: false. + */ + enableLegacyTypeScriptModuleInterop?: boolean; + /** + * If true, replicate the import behavior Babel 5 and babel-plugin-add-module-exports. + */ + enableLegacyBabel5ModuleInterop?: boolean; + /** + * If specified, we also return a RawSourceMap object alongside the code. + * Currently, source maps simply map each line to the original line without + * any mappings within lines, since Sucrase preserves line numbers. filePath + * must be specified if this option is enabled. + */ + sourceMapOptions?: SourceMapOptions; + /** + * File path to use in error messages, React display names, and source maps. + */ + filePath?: string; } export function validateOptions(options: Options): void { diff --git a/src/transformers/JSXTransformer.ts b/src/transformers/JSXTransformer.ts index cc0a1014..15a7e1eb 100644 --- a/src/transformers/JSXTransformer.ts +++ b/src/transformers/JSXTransformer.ts @@ -42,26 +42,83 @@ export default class JSXTransformer extends Transformer { } } - /** - * Lazily calculate line numbers to avoid unneeded work. We assume this is always called in - * increasing order by index. - */ - getLineNumberForIndex(index: number): number { - const code = this.tokens.code; - while (this.lastIndex < index && this.lastIndex < code.length) { - if (code[this.lastIndex] === "\n") { - this.lastLineNumber++; + processJSXTag(): void { + const {jsxPragmaInfo} = this; + const resolvedPragmaBaseName = this.importProcessor + ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.base) || jsxPragmaInfo.base + : jsxPragmaInfo.base; + const firstTokenStart = this.tokens.currentToken().start; + // First tag is always jsxTagStart. + this.tokens.replaceToken(`${resolvedPragmaBaseName}${jsxPragmaInfo.suffix}(`); + + if (this.tokens.matches1(tt.jsxTagEnd)) { + // Fragment syntax. + const resolvedFragmentPragmaBaseName = this.importProcessor + ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.fragmentBase) || + jsxPragmaInfo.fragmentBase + : jsxPragmaInfo.fragmentBase; + this.tokens.replaceToken( + `${resolvedFragmentPragmaBaseName}${jsxPragmaInfo.fragmentSuffix}, null`, + ); + // Tag with children. + this.processChildren(); + while (!this.tokens.matches1(tt.jsxTagEnd)) { + this.tokens.replaceToken(""); + } + this.tokens.replaceToken(")"); + } else { + // Normal open tag or self-closing tag. + this.processTagIntro(); + this.processProps(firstTokenStart); + + if (this.tokens.matches2(tt.slash, tt.jsxTagEnd)) { + // Self-closing tag. + this.tokens.replaceToken(""); + this.tokens.replaceToken(")"); + } else if (this.tokens.matches1(tt.jsxTagEnd)) { + this.tokens.replaceToken(""); + // Tag with children. + this.processChildren(); + while (!this.tokens.matches1(tt.jsxTagEnd)) { + this.tokens.replaceToken(""); + } + this.tokens.replaceToken(")"); + } else { + throw new Error("Expected either /> or > at the end of the tag."); } - this.lastIndex++; } - return this.lastLineNumber; } - getFilenameVarName(): string { - if (!this.filenameVarName) { - this.filenameVarName = this.nameManager.claimFreeName("_jsxFileName"); + /** + * Process the first part of a tag, before any props. + */ + processTagIntro(): void { + // Walk forward until we see one of these patterns: + // jsxName to start the first prop, preceded by another jsxName to end the tag name. + // jsxName to start the first prop, preceded by greaterThan to end the type argument. + // [open brace] to start the first prop. + // [jsxTagEnd] to end the open-tag. + // [slash, jsxTagEnd] to end the self-closing tag. + let introEnd = this.tokens.currentIndex() + 1; + while ( + this.tokens.tokens[introEnd].isType || + (!this.tokens.matches2AtIndex(introEnd - 1, tt.jsxName, tt.jsxName) && + !this.tokens.matches2AtIndex(introEnd - 1, tt.greaterThan, tt.jsxName) && + !this.tokens.matches1AtIndex(introEnd, tt.braceL) && + !this.tokens.matches1AtIndex(introEnd, tt.jsxTagEnd) && + !this.tokens.matches2AtIndex(introEnd, tt.slash, tt.jsxTagEnd)) + ) { + introEnd++; + } + if (introEnd === this.tokens.currentIndex() + 1) { + const tagName = this.tokens.identifierName(); + if (startsWithLowerCase(tagName)) { + this.tokens.replaceToken(`'${tagName}'`); + } + } + while (this.tokens.currentIndex() < introEnd) { + this.rootTransformer.processToken(); } - return this.filenameVarName; } processProps(firstTokenStart: number): void { @@ -128,35 +185,25 @@ export default class JSXTransformer extends Transformer { } /** - * Process the first part of a tag, before any props. + * Lazily calculate line numbers to avoid unneeded work. We assume this is always called in + * increasing order by index. */ - processTagIntro(): void { - // Walk forward until we see one of these patterns: - // jsxName to start the first prop, preceded by another jsxName to end the tag name. - // jsxName to start the first prop, preceded by greaterThan to end the type argument. - // [open brace] to start the first prop. - // [jsxTagEnd] to end the open-tag. - // [slash, jsxTagEnd] to end the self-closing tag. - let introEnd = this.tokens.currentIndex() + 1; - while ( - this.tokens.tokens[introEnd].isType || - (!this.tokens.matches2AtIndex(introEnd - 1, tt.jsxName, tt.jsxName) && - !this.tokens.matches2AtIndex(introEnd - 1, tt.greaterThan, tt.jsxName) && - !this.tokens.matches1AtIndex(introEnd, tt.braceL) && - !this.tokens.matches1AtIndex(introEnd, tt.jsxTagEnd) && - !this.tokens.matches2AtIndex(introEnd, tt.slash, tt.jsxTagEnd)) - ) { - introEnd++; - } - if (introEnd === this.tokens.currentIndex() + 1) { - const tagName = this.tokens.identifierName(); - if (startsWithLowerCase(tagName)) { - this.tokens.replaceToken(`'${tagName}'`); + getLineNumberForIndex(index: number): number { + const code = this.tokens.code; + while (this.lastIndex < index && this.lastIndex < code.length) { + if (code[this.lastIndex] === "\n") { + this.lastLineNumber++; } + this.lastIndex++; } - while (this.tokens.currentIndex() < introEnd) { - this.rootTransformer.processToken(); + return this.lastLineNumber; + } + + getFilenameVarName(): string { + if (!this.filenameVarName) { + this.filenameVarName = this.nameManager.claimFreeName("_jsxFileName"); } + return this.filenameVarName; } processChildren(): void { @@ -200,53 +247,6 @@ export default class JSXTransformer extends Transformer { this.tokens.replaceToken(`, ${literalCode}${replacementCode}`); } } - - processJSXTag(): void { - const {jsxPragmaInfo} = this; - const resolvedPragmaBaseName = this.importProcessor - ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.base) || jsxPragmaInfo.base - : jsxPragmaInfo.base; - const firstTokenStart = this.tokens.currentToken().start; - // First tag is always jsxTagStart. - this.tokens.replaceToken(`${resolvedPragmaBaseName}${jsxPragmaInfo.suffix}(`); - - if (this.tokens.matches1(tt.jsxTagEnd)) { - // Fragment syntax. - const resolvedFragmentPragmaBaseName = this.importProcessor - ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.fragmentBase) || - jsxPragmaInfo.fragmentBase - : jsxPragmaInfo.fragmentBase; - this.tokens.replaceToken( - `${resolvedFragmentPragmaBaseName}${jsxPragmaInfo.fragmentSuffix}, null`, - ); - // Tag with children. - this.processChildren(); - while (!this.tokens.matches1(tt.jsxTagEnd)) { - this.tokens.replaceToken(""); - } - this.tokens.replaceToken(")"); - } else { - // Normal open tag or self-closing tag. - this.processTagIntro(); - this.processProps(firstTokenStart); - - if (this.tokens.matches2(tt.slash, tt.jsxTagEnd)) { - // Self-closing tag. - this.tokens.replaceToken(""); - this.tokens.replaceToken(")"); - } else if (this.tokens.matches1(tt.jsxTagEnd)) { - this.tokens.replaceToken(""); - // Tag with children. - this.processChildren(); - while (!this.tokens.matches1(tt.jsxTagEnd)) { - this.tokens.replaceToken(""); - } - this.tokens.replaceToken(")"); - } else { - throw new Error("Expected either /> or > at the end of the tag."); - } - } - } } /** diff --git a/website/src/Constants.ts b/website/src/Constants.ts index 94f8b0e5..60717b37 100644 --- a/website/src/Constants.ts +++ b/website/src/Constants.ts @@ -53,14 +53,14 @@ export type HydratedOptions = Omit, "filePath" | "sourceMapOpt */ export const DEFAULT_OPTIONS: HydratedOptions = { transforms: ["jsx", "typescript", "imports"], + disableESTransforms: false, + production: false, jsxPragma: "React.createElement", jsxFragmentPragma: "React.Fragment", - enableLegacyTypeScriptModuleInterop: false, - enableLegacyBabel5ModuleInterop: false, - production: false, - disableESTransforms: false, preserveDynamicImport: false, injectCreateRequireForImportRequire: false, + enableLegacyTypeScriptModuleInterop: false, + enableLegacyBabel5ModuleInterop: false, }; export interface DisplayOptions { diff --git a/website/src/SucraseOptionsBox.tsx b/website/src/SucraseOptionsBox.tsx index ea7b7eae..7a633427 100644 --- a/website/src/SucraseOptionsBox.tsx +++ b/website/src/SucraseOptionsBox.tsx @@ -134,45 +134,56 @@ export default function SucraseOptionsBox({ options={options} onUpdateOptions={onUpdateOptions} /> - - - + {options.transforms.includes("jsx") && ( + <> + + + + + )}
- - - - + {options.transforms.includes("imports") && ( + <> + + + + + )} + {!options.transforms.includes("imports") && + options.transforms.includes("typescript") && ( + + )}
)}