Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Minor refactors in prep for new JSX runtime #742

Merged
merged 1 commit into from
Sep 2, 2022
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
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