Skip to content

Commit

Permalink
Minor refactors in prep for new JSX runtime
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alangpierce committed Sep 2, 2022
1 parent e9a1281 commit c70e0ef
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 160 deletions.
8 changes: 4 additions & 4 deletions src/Options-gen-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
61 changes: 33 additions & 28 deletions src/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,31 @@ export interface SourceMapOptions {
}

export interface Options {
transforms: Array<Transform>;
/**
* 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<Transform>;
/**
* 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;
/**
Expand All @@ -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 {
Expand Down
174 changes: 87 additions & 87 deletions src/transformers/JSXTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.");
}
}
}
}

/**
Expand Down
8 changes: 4 additions & 4 deletions website/src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ export type HydratedOptions = Omit<Required<Options>, "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 {
Expand Down
Loading

0 comments on commit c70e0ef

Please # to comment.