[babel] Make gating option a pair of module and project

There are some internal restrictions in Metro that only allow us to specify one 
gating module as an injected dependency. To allow multiple projects, this PR 
updates the Babel plugin to take a gating options config specifiying a project 
name. The project name is used as a suffix for the generated import; for 
example: 

```js 

const options = { 

// ... 

gating: { 

module: "ReactForgetFeatureFlag", 

importSpecifierName: "isForgetEnabled_Secret", 

}; 

// generates 

import {isForgetEnabled_Secret} from "ReactForgetFeatureFlag"; // a module that 
exports multiple flags 

// ... 

```
This commit is contained in:
Lauren Tan
2023-04-03 12:33:23 -04:00
parent 1aec5ef523
commit b19555573f
21 changed files with 137 additions and 100 deletions
+54 -44
View File
@@ -12,14 +12,16 @@ import jsx from "@babel/plugin-syntax-jsx";
import * as t from "@babel/types";
import invariant from "invariant";
import { compile } from "../CompilerPipeline";
import { parsePluginOptions, PluginOptions } from "./PluginOptions";
import {
GatingOptions,
parsePluginOptions,
PluginOptions,
} from "./PluginOptions";
type BabelPluginPass = {
opts: PluginOptions;
};
const testId = t.identifier("isForgetEnabled");
function hasUseForgetDirective(directives: t.Directive[]): boolean {
for (const directive of directives) {
if (directive.value.value === "use forget") {
@@ -46,7 +48,7 @@ export default function ReactForgetBabelPlugin(
hasForgetCompiledCode = true;
const compiled = compile(fn, pass.opts.environment);
if (pass.opts.gatingModule) {
if (pass.opts.gating != null) {
// Rename existing function
invariant(fn.node.id, "FunctionDeclaration must have a name");
const original = fn.node.id;
@@ -59,7 +61,14 @@ export default function ReactForgetBabelPlugin(
compiledFn.skip();
// Build and append gating test
compiledFn.insertAfter(buildGatingTest(fn, compiled.id, original));
compiledFn.insertAfter(
buildGatingTest({
originalFnDecl: fn,
compiledIdent: compiled.id,
originalIdent: original,
gating: pass.opts.gating,
})
);
} else {
fn.replaceWith(compiled);
}
@@ -108,10 +117,10 @@ export default function ReactForgetBabelPlugin(
opts: { ...pass.opts, ...options },
});
if (options.gatingModule && hasForgetCompiledCode) {
if (options.gating != null && hasForgetCompiledCode) {
path.unshiftContainer(
"body",
buildImportForGatingModule(options.gatingModule)
buildImportForGatingModule(options.gating)
);
}
} catch (err) {
@@ -194,62 +203,63 @@ function buildBlockStatement(
return body.node;
}
function buildGatingTest(
uncompiled: BabelCore.NodePath<t.FunctionDeclaration>,
compiled: t.Identifier,
original: t.Identifier
): t.Node | t.Node[] {
const test = buildTest({
uncompiled: uncompiled.node.id!,
compiled,
original,
});
type GatingTestOptions = {
originalFnDecl: BabelCore.NodePath<t.FunctionDeclaration>;
compiledIdent: t.Identifier;
originalIdent: t.Identifier;
gating: GatingOptions;
};
function buildGatingTest({
originalFnDecl,
compiledIdent,
originalIdent,
gating,
}: GatingTestOptions): t.Node | t.Node[] {
const testVarDecl = t.variableDeclaration("const", [
t.variableDeclarator(
originalIdent,
t.conditionalExpression(
t.callExpression(buildSpecifierIdent(gating), []),
compiledIdent,
originalFnDecl.node.id!
)
),
]);
// Re-export new declaration
const parent = uncompiled.parentPath;
const parent = originalFnDecl.parentPath;
if (t.isExportDefaultDeclaration(parent)) {
// Re-add uncompiled function
parent.replaceWith(uncompiled)[0].skip();
parent.replaceWith(originalFnDecl)[0].skip();
// Add test and synthesize new export
return [test, t.exportDefaultDeclaration(original)];
return [testVarDecl, t.exportDefaultDeclaration(originalIdent)];
} else if (t.isExportNamedDeclaration(parent)) {
// Re-add uncompiled function
parent.replaceWith(uncompiled)[0].skip();
parent.replaceWith(originalFnDecl)[0].skip();
// Add and export test
return t.exportNamedDeclaration(test);
return t.exportNamedDeclaration(testVarDecl);
}
// Just add the test, no need for re-export
return test;
return testVarDecl;
}
function addSuffix(id: t.Identifier, suffix: string): t.Identifier {
return t.identifier(`${id.name}${suffix}`);
}
function buildTest(ids: {
uncompiled: t.Identifier;
compiled: t.Identifier;
original: t.Identifier;
}): t.VariableDeclaration {
return t.variableDeclaration("const", [
t.variableDeclarator(
ids.original,
t.conditionalExpression(
t.callExpression(testId, []),
ids.compiled,
ids.uncompiled
)
),
]);
}
function buildImportForGatingModule(gatingModule: string): t.ImportDeclaration {
const importDefaultSpecifier = t.importDefaultSpecifier(testId);
function buildImportForGatingModule(
gating: GatingOptions
): t.ImportDeclaration {
const specifierIdent = buildSpecifierIdent(gating);
return t.importDeclaration(
[importDefaultSpecifier],
t.stringLiteral(gatingModule)
[t.importSpecifier(specifierIdent, specifierIdent)],
t.stringLiteral(gating.source)
);
}
function buildSpecifierIdent(gating: GatingOptions): t.Identifier {
return t.identifier(gating.importSpecifierName);
}
+22 -8
View File
@@ -7,6 +7,17 @@
import { EnvironmentConfig } from "../HIR/Environment";
export type GatingOptions = {
/**
* Source for the imported module that exports the `importSpecifierName` functions
*/
source: string;
/**
* Unique name for the feature flag test condition, eg `isForgetEnabled_ProjectName`
*/
importSpecifierName: string;
};
export type PluginOptions = {
/**
* Enable to make Forget only compile functions containing the 'use forget' directive.
@@ -18,23 +29,26 @@ export type PluginOptions = {
logger: Logger | null;
/**
* Specifying a `gatingModule`, makes Forget compile and emit a separate
* version of the function gated by importing an `isForgetEnabled` from the
* specified `gatingModule`.
* Specifying a `gating` config, makes Forget compile and emit a separate
* version of the function gated by importing the `gating.importSpecifierName` from the
* specified `gating.source`.
*
* For example:
* gatingModule: 'ReactForgetFeatureFlag'
* gating: {
* source: 'ReactForgetFeatureFlag',
* importSpecifierName: 'isForgetEnabled_Pokes',
* }
*
* produces:
* import isForgetEnabled from ReactForgetFeatureFlag
* import {isForgetEnabled_Pokes} from 'ReactForgetFeatureFlag';
*
* Foo_forget() {}
*
* Foo_uncompiled() {}
*
* var Foo = isForgetEnabled ? Foo_forget : Foo_uncompiled;
* var Foo = isForgetEnabled_Pokes() ? Foo_forget : Foo_uncompiled;
*/
gatingModule: string | null;
gating: GatingOptions | null;
};
export type Logger = {
@@ -45,7 +59,7 @@ export const defaultOptions: PluginOptions = {
enableOnlyOnUseForgetDirective: false,
environment: null,
logger: null,
gatingModule: null,
gating: null,
} as const;
export function parsePluginOptions(obj: unknown): PluginOptions {
@@ -56,7 +56,7 @@ describe("React Forget", () => {
]),
},
logger: null,
gatingModule: options.gatingModule,
gating: options.gating,
}).code,
});
} catch (e) {
@@ -52,7 +52,7 @@ describe("React Forget (Disable memoization of JSX elements)", () => {
]),
},
logger: null,
gatingModule: options.gatingModule,
gating: options.gating,
}).code,
});
} catch (e) {
@@ -2,7 +2,7 @@
## Input
```javascript
// @gatingModule
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
export default ErrorView;
@@ -12,7 +12,7 @@ export default ErrorView;
## Code
```javascript
import isForgetEnabled from "ReactForgetFeatureFlag"; // @gatingModule
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating
function ErrorView_uncompiled(error, _retry) {
return <MessageBox error={error}></MessageBox>;
}
@@ -29,7 +29,9 @@ function ErrorView_forget(error, _retry) {
}
return t0;
}
const ErrorView = isForgetEnabled() ? ErrorView_forget : ErrorView_uncompiled;
const ErrorView = isForgetEnabled_Fixtures()
? ErrorView_forget
: ErrorView_uncompiled;
export default ErrorView;
@@ -1,4 +1,4 @@
// @gatingModule
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
export default ErrorView;
@@ -2,7 +2,7 @@
## Input
```javascript
// @gatingModule
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
export default Renderer = (props) => (
@@ -1,4 +1,4 @@
// @gatingModule
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
export default Renderer = (props) => (
@@ -2,7 +2,7 @@
## Input
```javascript
// @gatingModule @forgetDirective
// @gating @forgetDirective
export default function Bar(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -22,7 +22,7 @@ function Foo(props) {
## Code
```javascript
import isForgetEnabled from "ReactForgetFeatureFlag"; // @gatingModule @forgetDirective
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @forgetDirective
function Bar_uncompiled(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -40,7 +40,7 @@ function Bar_forget(props) {
}
return t0;
}
const Bar = isForgetEnabled() ? Bar_forget : Bar_uncompiled;
const Bar = isForgetEnabled_Fixtures() ? Bar_forget : Bar_uncompiled;
export default Bar;
function NoForget(props) {
@@ -64,7 +64,7 @@ function Foo_forget(props) {
}
return t0;
}
const Foo = isForgetEnabled() ? Foo_forget : Foo_uncompiled;
const Foo = isForgetEnabled_Fixtures() ? Foo_forget : Foo_uncompiled;
```
@@ -1,4 +1,4 @@
// @gatingModule @forgetDirective
// @gating @forgetDirective
export default function Bar(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -2,7 +2,7 @@
## Input
```javascript
// @gatingModule @forgetDirective
// @gating @forgetDirective
export default function Bar(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -22,7 +22,7 @@ export function Foo(props) {
## Code
```javascript
import isForgetEnabled from "ReactForgetFeatureFlag"; // @gatingModule @forgetDirective
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @forgetDirective
function Bar_uncompiled(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -40,7 +40,7 @@ function Bar_forget(props) {
}
return t0;
}
const Bar = isForgetEnabled() ? Bar_forget : Bar_uncompiled;
const Bar = isForgetEnabled_Fixtures() ? Bar_forget : Bar_uncompiled;
export default Bar;
function NoForget(props) {
@@ -64,7 +64,7 @@ function Foo_forget(props) {
}
return t0;
}
export const Foo = isForgetEnabled() ? Foo_forget : Foo_uncompiled;
export const Foo = isForgetEnabled_Fixtures() ? Foo_forget : Foo_uncompiled;
```
@@ -1,4 +1,4 @@
// @gatingModule @forgetDirective
// @gating @forgetDirective
export default function Bar(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -2,7 +2,7 @@
## Input
```javascript
// @gatingModule @forgetDirective
// @gating @forgetDirective
export function Bar(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -22,7 +22,7 @@ export function Foo(props) {
## Code
```javascript
import isForgetEnabled from "ReactForgetFeatureFlag"; // @gatingModule @forgetDirective
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @forgetDirective
function Bar_uncompiled(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -40,7 +40,7 @@ function Bar_forget(props) {
}
return t0;
}
export const Bar = isForgetEnabled() ? Bar_forget : Bar_uncompiled;
export const Bar = isForgetEnabled_Fixtures() ? Bar_forget : Bar_uncompiled;
export function NoForget(props) {
return <Bar>{props.noForget}</Bar>;
@@ -63,7 +63,7 @@ function Foo_forget(props) {
}
return t0;
}
export const Foo = isForgetEnabled() ? Foo_forget : Foo_uncompiled;
export const Foo = isForgetEnabled_Fixtures() ? Foo_forget : Foo_uncompiled;
```
@@ -1,4 +1,4 @@
// @gatingModule @forgetDirective
// @gating @forgetDirective
export function Bar(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -2,7 +2,7 @@
## Input
```javascript
// @gatingModule @forgetDirective
// @gating @forgetDirective
function Bar(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -22,7 +22,7 @@ function Foo(props) {
## Code
```javascript
import isForgetEnabled from "ReactForgetFeatureFlag"; // @gatingModule @forgetDirective
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @forgetDirective
function Bar_uncompiled(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -40,7 +40,7 @@ function Bar_forget(props) {
}
return t0;
}
const Bar = isForgetEnabled() ? Bar_forget : Bar_uncompiled;
const Bar = isForgetEnabled_Fixtures() ? Bar_forget : Bar_uncompiled;
function NoForget(props) {
return <Bar>{props.noForget}</Bar>;
@@ -63,7 +63,7 @@ function Foo_forget(props) {
}
return t0;
}
const Foo = isForgetEnabled() ? Foo_forget : Foo_uncompiled;
const Foo = isForgetEnabled_Fixtures() ? Foo_forget : Foo_uncompiled;
```
@@ -1,4 +1,4 @@
// @gatingModule @forgetDirective
// @gating @forgetDirective
function Bar(props) {
"use forget";
return <div>{props.bar}</div>;
@@ -2,7 +2,7 @@
## Input
```javascript
// @gatingModule
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
export const Renderer = (props) => (
@@ -17,7 +17,7 @@ export const Renderer = (props) => (
## Code
```javascript
import isForgetEnabled from "ReactForgetFeatureFlag"; // @gatingModule
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating
function ErrorView_uncompiled(error, _retry) {
return <MessageBox error={error}></MessageBox>;
}
@@ -34,7 +34,9 @@ function ErrorView_forget(error, _retry) {
}
return t0;
}
const ErrorView = isForgetEnabled() ? ErrorView_forget : ErrorView_uncompiled;
const ErrorView = isForgetEnabled_Fixtures()
? ErrorView_forget
: ErrorView_uncompiled;
function Renderer_uncompiled(props) {
return (
<Foo>
@@ -73,7 +75,7 @@ function Renderer_forget(props) {
}
return t2;
}
export const Renderer = isForgetEnabled()
export const Renderer = isForgetEnabled_Fixtures()
? Renderer_forget
: Renderer_uncompiled;
@@ -1,4 +1,4 @@
// @gatingModule
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
export const Renderer = (props) => (
@@ -2,7 +2,7 @@
## Input
```javascript
// @gatingModule
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
const Renderer = (props) => (
@@ -19,7 +19,7 @@ export default Renderer;
## Code
```javascript
import isForgetEnabled from "ReactForgetFeatureFlag"; // @gatingModule
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating
function ErrorView_uncompiled(error, _retry) {
return <MessageBox error={error}></MessageBox>;
}
@@ -36,7 +36,9 @@ function ErrorView_forget(error, _retry) {
}
return t0;
}
const ErrorView = isForgetEnabled() ? ErrorView_forget : ErrorView_uncompiled;
const ErrorView = isForgetEnabled_Fixtures()
? ErrorView_forget
: ErrorView_uncompiled;
function Renderer_uncompiled(props) {
return (
<Foo>
@@ -75,7 +77,9 @@ function Renderer_forget(props) {
}
return t2;
}
const Renderer = isForgetEnabled() ? Renderer_forget : Renderer_uncompiled;
const Renderer = isForgetEnabled_Fixtures()
? Renderer_forget
: Renderer_uncompiled;
export default Renderer;
```
@@ -1,4 +1,4 @@
// @gatingModule
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
const Renderer = (props) => (
@@ -11,6 +11,7 @@
import fs from "fs";
import path from "path";
import { GatingOptions, PluginOptions } from "../../Babel/PluginOptions";
const EXPECT_SUFFIX = ".expect.md";
@@ -40,17 +41,16 @@ expect.extend({
},
});
type FixtureTestOptions = {
debug: boolean;
language: "flow" | "typescript";
};
export default function generateTestsFromFixtures(
fixturesPath: string,
transform: (
input: string,
file: any,
options: {
debug: boolean;
enableOnlyOnUseForgetDirective: boolean;
gatingModule: string | null;
language: "flow" | "typescript";
}
options: FixtureTestOptions & PluginOptions
) => string
) {
let files: Array<string>;
@@ -90,7 +90,7 @@ export default function generateTestsFromFixtures(
let input: string | null = null;
let debug = false;
let enableOnlyOnUseForgetDirective = false;
let gatingModule: string | null = null;
let gating: GatingOptions | null = null;
if (inputFile != null) {
input = fs.readFileSync(inputFile, "utf8");
@@ -105,8 +105,11 @@ export default function generateTestsFromFixtures(
if (lines[0]!.indexOf("@forgetDirective") !== -1) {
enableOnlyOnUseForgetDirective = true;
}
if (lines[0]!.indexOf("@gatingModule") !== -1) {
gatingModule = "ReactForgetFeatureFlag";
if (lines[0]!.indexOf("@gating") !== -1) {
gating = {
source: "ReactForgetFeatureFlag",
importSpecifierName: "isForgetEnabled_Fixtures",
};
}
}
@@ -114,9 +117,11 @@ export default function generateTestsFromFixtures(
let receivedOutput;
if (input !== null) {
receivedOutput = transform(input, basename, {
environment: null,
logger: null,
debug,
enableOnlyOnUseForgetDirective,
gatingModule,
gating,
language: parseLanguage(input),
});
} else {