@samitouri / QOS-React / commits / 0c72eed413

Pass environment options through babel plugin

Updates the babel plugin so that environment options — including custom hook definitions — can be passed in through the plugin: * Renames `CompilerFlags` => `PluginOptions` since they are specific to the babel plugin, and are no longer just flags. * Moves the definition of `useFreeze()` out of the builtin hook list and instead passes it when our unit tests configure the plugin.

Joe Savona committed Feb 16, 2023 at 14:24 UTC 0c72eed413d0c4a484d5f8bc122c5d2d231952b3
6 files changed +70 -56
compiler/forget/src/Babel/BabelPlugin.ts
+4 -4
@@ -12,11 +12,11 @@ import generate from "@babel/generator";
12 import jsx from "@babel/plugin-syntax-jsx";
13 import * as t from "@babel/types";
14 import prettier from "prettier";
15 -import { CompilerFlags, parseCompilerFlags } from "../CompilerFlags";
15 import { compile } from "../CompilerPipeline";
16 +import { parsePluginOptions, PluginOptions } from "./PluginOptions";
17
18 type BabelPluginPass = {
19 - opts: CompilerFlags;
19 + opts: PluginOptions;
20 };
21
22 /**
@@ -47,7 +47,7 @@ export default function ReactForgetBabelPlugin(
47 if (fn.scope.getProgramParent() !== fn.scope.parent) {
48 return;
49 }
50 - const ast = compile(fn, null);
50 + const ast = compile(fn, pass.opts.environment);
51
52 // We are generating a new FunctionDeclaration node, so we must skip over it or this
53 // traversal will loop infinitely.
@@ -73,7 +73,7 @@ export default function ReactForgetBabelPlugin(
73 // prior to B, if A does not have a Program visitor and B does, B will run first. We always
74 // want Forget to run true to source as possible.
75 Program(path, pass) {
76 - const flags = parseCompilerFlags(pass.opts);
76 + const flags = parsePluginOptions(pass.opts);
77 path.traverse(visitor, {
78 ...pass,
79 opts: { ...pass.opts, ...flags },
compiler/forget/src/Babel/PluginOptions.ts new
+45
@@ -0,0 +1,45 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import { EnvironmentOptions } from "../HIR/Environment";
9 +
10 +export type PluginOptions = {
11 + /**
12 + * Enable to make Forget only compile functions containing the 'use forget' directive.
13 + */
14 + enableOnlyOnUseForgetDirective: boolean;
15 +
16 + environment: EnvironmentOptions | null;
17 +};
18 +
19 +export const defaultOptions: PluginOptions = {
20 + enableOnlyOnUseForgetDirective: false,
21 + environment: null,
22 +} as const;
23 +
24 +export function parsePluginOptions(obj: unknown): PluginOptions {
25 + if (obj == null || typeof obj !== "object") {
26 + return defaultOptions;
27 + }
28 + const invalidOptions: Array<string> = [];
29 + let parsedOptions: Partial<PluginOptions> = Object.create(null);
30 + for (const [key, value] of Object.entries(obj)) {
31 + if (isCompilerFlag(key)) {
32 + parsedOptions[key] = value;
33 + } else {
34 + invalidOptions.push(key);
35 + }
36 + }
37 + if (invalidOptions.length > 0) {
38 + console.error(`Unexpected React Forget compiler flags: ${invalidOptions}`);
39 + }
40 + return { ...defaultOptions, ...parsedOptions };
41 +}
42 +
43 +function isCompilerFlag(s: string): s is keyof typeof defaultOptions {
44 + return Object.prototype.hasOwnProperty.call(defaultOptions, s);
45 +}
compiler/forget/src/Babel/RunReactForgetBabelPlugin.ts
+4 -2
@@ -4,6 +4,7 @@ import * as parser from "@babel/parser";
4 import invariant from "invariant";
5 import prettier from "prettier";
6 import ReactForgetBabelPlugin from "./BabelPlugin";
7 +import { PluginOptions } from "./PluginOptions";
8
9 type ReactForgetBabelPluginResult = {
10 ast: BabelCore.BabelFileResult["ast"];
@@ -14,7 +15,8 @@ type ReactForgetBabelPluginResult = {
15 export default function runReactForgetBabelPlugin(
16 text: string,
17 file: string,
17 - language: "flow" | "typescript"
18 + language: "flow" | "typescript",
19 + options: PluginOptions | null
20 ): ReactForgetBabelPluginResult {
21 const ast = parser.parse(text, {
22 sourceFilename: file,
@@ -25,7 +27,7 @@ export default function runReactForgetBabelPlugin(
27 filename: file,
28 highlightCode: false,
29 retainLines: true,
28 - plugins: [ReactForgetBabelPlugin],
30 + plugins: [[ReactForgetBabelPlugin, options]],
31 sourceType: "module",
32 });
33 invariant(
compiler/forget/src/CompilerFlags.ts deleted
-40
@@ -1,40 +0,0 @@
1 -/**
2 - * Copyright (c) Facebook, Inc. and its affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -export type CompilerFlags = {
9 - /**
10 - * Enable to make Forget only compile functions containing the 'use forget' directive.
11 - */
12 - enableOnlyOnUseForgetDirective: boolean;
13 -};
14 -
15 -export const defaultFlags: CompilerFlags = {
16 - enableOnlyOnUseForgetDirective: false,
17 -} as const;
18 -
19 -export function parseCompilerFlags(obj: unknown): CompilerFlags {
20 - if (obj == null || typeof obj !== "object") {
21 - return defaultFlags;
22 - }
23 - const invalidFlags: Array<string> = [];
24 - let parsedFlags: Partial<CompilerFlags> = Object.create(null);
25 - for (const [key, value] of Object.entries(obj)) {
26 - if (isCompilerFlag(key)) {
27 - parsedFlags[key] = value;
28 - } else {
29 - invalidFlags.push(key);
30 - }
31 - }
32 - if (invalidFlags.length > 0) {
33 - console.error(`Unexpected React Forget compiler flags: ${invalidFlags}`);
34 - }
35 - return { ...defaultFlags, ...parsedFlags };
36 -}
37 -
38 -function isCompilerFlag(s: string): s is keyof typeof defaultFlags {
39 - return Object.prototype.hasOwnProperty.call(defaultFlags, s);
40 -}
compiler/forget/src/HIR/Hooks.ts
-9
@@ -26,15 +26,6 @@ export const BUILTIN_HOOKS: Map<string, Hook> = new Map([
26 valueKind: ValueKind.Mutable,
27 },
28 ],
29 - [
30 - "useFreeze",
31 - {
32 - kind: "Ref",
33 - name: "useFreeze",
34 - effectKind: Effect.Freeze,
35 - valueKind: ValueKind.Frozen,
36 - },
37 - ],
29 [
30 "useMemo",
31 {
compiler/forget/src/__tests__/hir-test.ts
+17 -1
@@ -10,6 +10,7 @@
10 import { wasmFolder } from "@hpcc-js/wasm";
11 import path from "path";
12 import runReactForgetBabelPlugin from "../Babel/RunReactForgetBabelPlugin";
13 +import { Effect, ValueKind } from "../HIR";
14 import { toggleLogging } from "../Utils/logger";
15 import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
16
@@ -60,7 +61,22 @@ describe("React Forget (HIR version)", () => {
61 }
62 try {
63 items.push({
63 - js: runReactForgetBabelPlugin(input, file, language).code,
64 + js: runReactForgetBabelPlugin(input, file, language, {
65 + enableOnlyOnUseForgetDirective: false,
66 + environment: {
67 + customHooks: new Map([
68 + [
69 + "useFreeze",
70 + {
71 + name: "useFreeze",
72 + kind: "Custom",
73 + valueKind: ValueKind.Frozen,
74 + effectKind: Effect.Freeze,
75 + },
76 + ],
77 + ]),
78 + },
79 + }).code,
80 });
81 } catch (e) {
82 error = e;