@samitouri / QOS-React-2 / commits / 915a0b8389

[be] Consolidate sprout -> snap

--- No changes to snap or sprout's functionality. Tweaks to consolidate sprout into snap while keeping its simple interface and most developer patterns. - to keep `filter` mode fast, we do not run sprout in filter mode - sprout is run in non-filter mode for both test and update ~~Small qol improvement: `--watch` will start you in `filter` mode~~ ### Cost of this change `performance.now()` is quite noisy due to background processes and ThreadPool logic (especially with asymmetric task distribution), so I used `process.cpuUsage` which reports time spent in user-space. This was much less noisy (1-4% standard dev / mean) Running all tests becomes slower by ~50%. Initial runs are slower because they load in Forget's `require` chains. - 23.9s previous initial run - 34.6s current initial run - 11.5s previous subsequent runs - 15.4s current subsequent runs Running filtered tests remains very fast (~100ms on the average case) --- Additional modes or commands could be added as needed (e.g. run tests in filter mode, with sprout output)

Mofei Zhang committed Feb 27, 2024 at 06:54 UTC 915a0b8389cc26eabdd32183030aa210f6c83042
33 files changed +919 -1286
compiler/packages/babel-plugin-react-forget/package.json
+1 -4
@@ -9,14 +9,11 @@
9 ],
10 "scripts": {
11 "build": "rimraf dist && rollup --config --bundleConfigAsCjs",
12 - "test": "concurrently -g -n snap,sprout \"yarn snap:ci\" \"yarn sprout:ci\"",
12 + "test": "yarn snap:ci",
13 "jest": "tsc && ts-node \"$(yarn --silent which jest)\"",
14 "snap": "node ../snap/dist/main.js",
15 "snap:build": "yarn workspace snap run build",
16 "snap:ci": "yarn snap:build && yarn snap",
17 - "sprout": "node ../sprout/dist/main.js",
18 - "sprout:build": "yarn workspace sprout run build",
19 - "sprout:ci": "yarn sprout:build && yarn sprout",
17 "ts:analyze-trace": "scripts/ts-analyze-trace.sh",
18 "prettier": "node ./scripts/prettier.js write-changed",
19 "prettier:all": "node ./scripts/prettier.js write",
compiler/packages/babel-plugin-react-forget/src/Babel/RunReactForgetBabelPlugin.ts
+1
@@ -13,6 +13,7 @@ import invariant from "invariant";
13 import type { PluginOptions } from "../Entrypoint";
14 import ReactForgetBabelPlugin from "./BabelPlugin";
15
16 +export const DEFAULT_PLUGINS = ["babel-plugin-fbt", "babel-plugin-fbt-runtime"];
17 export function runReactForgetBabelPlugin(
18 text: string,
19 file: string,
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/early-return-no-declarations-reassignments-dependencies.expect.md
+1 -11
@@ -117,14 +117,4 @@ export const FIXTURE_ENTRYPOINT = {
117 };
118
119 ```
120 -
121 -### Eval output
122 -(kind: ok) [42]
123 -[42]
124 -[3.14]
125 -[3.14]
126 -[42]
127 -[3.14]
128 -[42]
129 -[3.14]
130 -logs: ['fallthrough','fallthrough','fallthrough','fallthrough','fallthrough','fallthrough','fallthrough','fallthrough']
\ No newline at end of file
120 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hook-declaration-basic.flow.expect.md
+1 -1
@@ -13,7 +13,7 @@ export default hook useFoo(bar: number) {
13
14 ```javascript
15 import { unstable_useMemoCache as useMemoCache } from "react";
16 -function useFoo(bar) {
16 +export default function useFoo(bar) {
17 const $ = useMemoCache(2);
18 let t0;
19 if ($[0] !== bar) {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/allow-locals-named-like-hooks.expect.md
+1 -1
@@ -78,7 +78,7 @@ export const FIXTURE_ENTRYPOINT = {
78 (kind: exception) Stringify is not defined
79 logs: ['The above error occurred in the <WrapperTestComponent> component:\n' +
80 '\n' +
81 - ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:54:26)\n' +
81 + ' at WrapperTestComponent (<project_root>/packages/snap/dist/sprout/evaluator.js:54:26)\n' +
82 '\n' +
83 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
84 'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.']
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-throw.expect.md
+1 -1
@@ -37,7 +37,7 @@ export const FIXTURE_ENTRYPOINT = {
37 (kind: exception) undefined
38 logs: ['The above error occurred in the <WrapperTestComponent> component:\n' +
39 '\n' +
40 - ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:54:26)\n' +
40 + ' at WrapperTestComponent (<project_root>/packages/snap/dist/sprout/evaluator.js:54:26)\n' +
41 '\n' +
42 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
43 'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.']
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/tsconfig.json
+1 -1
@@ -15,7 +15,7 @@
15 "jsx": "preserve",
16 "paths": {
17 // Editor integration for sprout shared runtime files
18 - "shared-runtime": ["../../../../sprout/src/shared-runtime.ts"]
18 + "shared-runtime": ["../../../../snap/src/sprout/shared-runtime.ts"]
19 },
20 "verbatimModuleSyntax": true,
21 "module": "ESNext",
compiler/packages/fixture-test-utils/package.json deleted
-25
@@ -1,25 +0,0 @@
1 -{
2 - "name": "fixture-test-utils",
3 - "version": "1.0.0",
4 - "main": "dist/index.js",
5 - "license": "MIT",
6 - "scripts": {
7 - "build": "rimraf dist && tsc",
8 - "test": "echo 'no tests'",
9 - "prettier": "prettier --write src"
10 - },
11 - "dependencies": {
12 - "@parcel/watcher": "^2.1.0",
13 - "chalk": "4",
14 - "prettier": "2.8.8",
15 - "readline": "^1.3.0",
16 - "typescript": "^5.1.0",
17 - "yargs": "^17.7.1"
18 - },
19 - "devDependencies": {
20 - "@types/node": "^18.7.18",
21 - "@typescript-eslint/eslint-plugin": "^5.51.0",
22 - "@typescript-eslint/parser": "^5.51.0",
23 - "rimraf": "^3.0.2"
24 - }
25 -}
compiler/packages/fixture-test-utils/src/compiler-utils.ts deleted
-180
@@ -1,180 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and 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 assert from "assert";
9 -import type { runReactForgetBabelPlugin as RunReactForgetBabelPlugin } from "babel-plugin-react-forget/src/Babel/RunReactForgetBabelPlugin";
10 -import {
11 - CompilationMode,
12 - PanicThresholdOptions,
13 -} from "babel-plugin-react-forget/src/Entrypoint";
14 -import type { Effect, ValueKind } from "babel-plugin-react-forget/src/HIR";
15 -import type { parseConfigPragma as ParseConfigPragma } from "babel-plugin-react-forget/src/HIR/Environment";
16 -import prettier from "prettier";
17 -
18 -export function parseLanguage(source: string): "flow" | "typescript" {
19 - return source.indexOf("@flow") !== -1 ? "flow" : "typescript";
20 -}
21 -
22 -export function transformFixtureInput(
23 - input: string,
24 - basename: string,
25 - pluginFn: typeof RunReactForgetBabelPlugin,
26 - parseConfigPragmaFn: typeof ParseConfigPragma,
27 - includeAst: boolean = false
28 -) {
29 - // Extract the first line to quickly check for custom test directives
30 - const firstLine = input.substring(0, input.indexOf("\n"));
31 -
32 - let language = parseLanguage(firstLine);
33 - let gating = null;
34 - let enableEmitInstrumentForget = null;
35 - let enableEmitFreeze = null;
36 - let enableEmitHookGuards = null;
37 - let compilationMode: CompilationMode = "all";
38 - let enableUseMemoCachePolyfill = false;
39 - let panicThreshold: PanicThresholdOptions = "ALL_ERRORS";
40 - let hookPattern: string | null = null;
41 -
42 - if (firstLine.indexOf("@compilationMode(annotation)") !== -1) {
43 - assert(
44 - compilationMode === "all",
45 - "Cannot set @compilationMode(..) more than once"
46 - );
47 - compilationMode = "annotation";
48 - }
49 - if (firstLine.indexOf("@compilationMode(infer)") !== -1) {
50 - assert(
51 - compilationMode === "all",
52 - "Cannot set @compilationMode(..) more than once"
53 - );
54 - compilationMode = "infer";
55 - }
56 -
57 - if (firstLine.includes("@gating")) {
58 - gating = {
59 - source: "ReactForgetFeatureFlag",
60 - importSpecifierName: "isForgetEnabled_Fixtures",
61 - };
62 - }
63 - if (firstLine.includes("@instrumentForget")) {
64 - enableEmitInstrumentForget = {
65 - source: "react-forget-runtime",
66 - importSpecifierName: "useRenderCounter",
67 - };
68 - }
69 - if (firstLine.includes("@enableEmitFreeze")) {
70 - enableEmitFreeze = {
71 - source: "react-forget-runtime",
72 - importSpecifierName: "makeReadOnly",
73 - };
74 - }
75 - if (firstLine.includes("@enableEmitHookGuards")) {
76 - enableEmitHookGuards = {
77 - source: "react-forget-runtime",
78 - importSpecifierName: "$dispatcherGuard",
79 - };
80 - }
81 - if (firstLine.includes("@enableUseMemoCachePolyfill")) {
82 - enableUseMemoCachePolyfill = true;
83 - }
84 - if (firstLine.includes("@panicThreshold(NONE)")) {
85 - panicThreshold = "NONE";
86 - }
87 -
88 - let eslintSuppressionRules: Array<string> | null = null;
89 - const eslintSuppressionMatch = /@eslintSuppressionRules\(([^)]+)\)/.exec(
90 - firstLine
91 - );
92 - if (eslintSuppressionMatch != null) {
93 - eslintSuppressionRules = eslintSuppressionMatch[1].split("|");
94 - }
95 -
96 - let flowSuppressions: boolean = false;
97 - if (firstLine.includes("@enableFlowSuppressions")) {
98 - flowSuppressions = true;
99 - }
100 -
101 -
102 - const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine);
103 - if (
104 - hookPatternMatch &&
105 - hookPatternMatch.length > 1 &&
106 - hookPatternMatch[1].trim().length > 0
107 - ) {
108 - hookPattern = hookPatternMatch[1].trim();
109 - } else if (firstLine.includes("@hookPattern")) {
110 - throw new Error(
111 - 'Invalid @hookPattern:"..." pragma, must contain the prefix between balanced double quotes eg @hookPattern:"pattern"'
112 - );
113 - }
114 -
115 - const config = parseConfigPragmaFn(firstLine);
116 - const result = pluginFn(
117 - input,
118 - basename,
119 - language,
120 - {
121 - environment: {
122 - ...config,
123 - customHooks: new Map([
124 - [
125 - "useFreeze",
126 - {
127 - valueKind: "frozen" as ValueKind,
128 - effectKind: "freeze" as Effect,
129 - transitiveMixedData: false,
130 - noAlias: false,
131 - },
132 - ],
133 - [
134 - "useFragment",
135 - {
136 - valueKind: "frozen" as ValueKind,
137 - effectKind: "freeze" as Effect,
138 - transitiveMixedData: true,
139 - noAlias: true,
140 - },
141 - ],
142 - [
143 - "useNoAlias",
144 - {
145 - valueKind: "mutable" as ValueKind,
146 - effectKind: "read" as Effect,
147 - transitiveMixedData: false,
148 - noAlias: true,
149 - },
150 - ],
151 - ]),
152 - enableEmitFreeze,
153 - enableEmitInstrumentForget,
154 - enableEmitHookGuards,
155 - assertValidMutableRanges: true,
156 - hookPattern,
157 - },
158 - compilationMode,
159 - logger: null,
160 - gating,
161 - panicThreshold,
162 - noEmit: false,
163 - enableUseMemoCachePolyfill,
164 - eslintSuppressionRules,
165 - flowSuppressions,
166 - },
167 - includeAst
168 - );
169 -
170 - return {
171 - ...result,
172 - code:
173 - result.code != null
174 - ? prettier.format(result.code, {
175 - semi: true,
176 - parser: language === "typescript" ? "babel-ts" : "flow",
177 - })
178 - : result.code,
179 - };
180 -}
compiler/packages/fixture-test-utils/src/index.ts deleted
-11
@@ -1,11 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and 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 * from "./constants";
9 -export * from "./fixture-utils";
10 -export * from "./compiler-utils";
11 -export * from "./output-utils";
compiler/packages/fixture-test-utils/tsconfig.json deleted
-24
@@ -1,24 +0,0 @@
1 -{
2 - "extends": "@tsconfig/node18-strictest/tsconfig.json",
3 - "compilerOptions": {
4 - "declaration": true,
5 - "rootDir": "src",
6 - "outDir": "dist",
7 - // https://github.com/microsoft/TypeScript/issues/30925
8 - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
9 - "jsx": "react-jsxdev",
10 -
11 - // weaken strictness from preset
12 - "importsNotUsedAsValues": "remove",
13 - "noUncheckedIndexedAccess": false,
14 - "noUnusedParameters": false,
15 - "useUnknownInCatchVariables": false,
16 - "target": "ES2015",
17 - // ideally turn off only during dev, or on a per-file basis
18 - "noUnusedLocals": false,
19 - "sourceMap": true,
20 - "composite": true
21 - },
22 - "exclude": ["node_modules"],
23 - "include": ["src/**/*.ts"]
24 -}
compiler/packages/snap/package.json
+24 -3
@@ -11,7 +11,7 @@
11 "scripts": {
12 "build": "rimraf dist && tsc --build",
13 "test": "echo 'no tests'",
14 - "lint": "yarn eslint src"
14 + "prettier": "prettier --write 'src/**/*.ts'"
15 },
16 "repository": {
17 "type": "git",
@@ -19,24 +19,45 @@
19 },
20 "dependencies": {
21 "@babel/code-frame": "^7.22.5",
22 + "@babel/generator": "7.2.0",
23 + "@babel/plugin-syntax-jsx": "^7.18.6",
24 + "@babel/preset-flow": "^7.7.4",
25 + "@babel/preset-typescript": "^7.18.6",
26 "@parcel/watcher": "^2.1.0",
27 + "@testing-library/react": "^13.4.0",
28 + "babel-plugin-react-forget": "*",
29 + "babel-plugin-syntax-hermes-parser": "^0.15.1",
30 + "hermes-parser": "^0.19.1",
31 "chalk": "4",
24 - "fixture-test-utils": "*",
32 + "fbt": "^1.0.0",
33 + "jsdom": "^22.1.0",
34 + "prettier": "2.8.8",
35 + "react": "^0.0.0-experimental-493f72b0a-20230727",
36 + "react-dom": "^0.0.0-experimental-493f72b0a-20230727",
37 "readline": "^1.3.0",
38 "typescript": "^5.1.0",
39 "yargs": "^17.7.1"
40 },
41 "devDependencies": {
42 "@types/babel__code-frame": "^7.0.6",
43 + "@babel/core": "^7.19.1",
44 + "@babel/parser": "^7.19.1",
45 + "@babel/plugin-syntax-typescript": "^7.18.6",
46 + "@babel/plugin-transform-modules-commonjs": "^7.18.6",
47 + "@babel/preset-react": "^7.18.6",
48 + "@babel/traverse": "^7.19.1",
49 + "@types/fbt": "^1.0.4",
50 "@types/node": "^18.7.18",
51 "@typescript-eslint/eslint-plugin": "^5.51.0",
52 "@typescript-eslint/parser": "^5.51.0",
53 + "prettier": "2.8.8",
54 "rimraf": "^3.0.2"
55 },
56 "resolutions": {
57 "./**/@babel/parser": "7.7.4",
58 "./**/@babel/types": "7.7.4",
59 "@babel/core": "7.2.0",
40 - "@babel/traverse": "7.1.6"
60 + "@babel/traverse": "7.1.6",
61 + "@babel/preset-flow": "7.22.5"
62 }
63 }
compiler/packages/snap/src/SproutTodoFilter.ts renamed
compiler/packages/snap/src/compiler.ts new
+380
@@ -0,0 +1,380 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and 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 type * as BabelCore from "@babel/core";
9 +import { transformFromAstSync } from "@babel/core";
10 +
11 +import * as BabelParser from "@babel/parser";
12 +import { NodePath } from "@babel/traverse";
13 +import * as t from "@babel/types";
14 +import assert from "assert";
15 +import type {
16 + CompilationMode,
17 + PanicThresholdOptions,
18 + PluginOptions,
19 +} from "babel-plugin-react-forget/src/Entrypoint";
20 +import type { Effect, ValueKind } from "babel-plugin-react-forget/src/HIR";
21 +import type { parseConfigPragma as ParseConfigPragma } from "babel-plugin-react-forget/src/HIR/Environment";
22 +import * as HermesParser from "hermes-parser";
23 +import invariant from "invariant";
24 +import path from "path";
25 +import prettier from "prettier";
26 +import SproutTodoFilter from "./SproutTodoFilter";
27 +import { isExpectError } from "./fixture-utils";
28 +export function parseLanguage(source: string): "flow" | "typescript" {
29 + return source.indexOf("@flow") !== -1 ? "flow" : "typescript";
30 +}
31 +
32 +function makePluginOptions(
33 + firstLine: string,
34 + parseConfigPragmaFn: typeof ParseConfigPragma
35 +): PluginOptions {
36 + let gating = null;
37 + let enableEmitInstrumentForget = null;
38 + let enableEmitFreeze = null;
39 + let enableEmitHookGuards = null;
40 + let compilationMode: CompilationMode = "all";
41 + let enableUseMemoCachePolyfill = false;
42 + let panicThreshold: PanicThresholdOptions = "ALL_ERRORS";
43 + let hookPattern: string | null = null;
44 +
45 + if (firstLine.indexOf("@compilationMode(annotation)") !== -1) {
46 + assert(
47 + compilationMode === "all",
48 + "Cannot set @compilationMode(..) more than once"
49 + );
50 + compilationMode = "annotation";
51 + }
52 + if (firstLine.indexOf("@compilationMode(infer)") !== -1) {
53 + assert(
54 + compilationMode === "all",
55 + "Cannot set @compilationMode(..) more than once"
56 + );
57 + compilationMode = "infer";
58 + }
59 +
60 + if (firstLine.includes("@gating")) {
61 + gating = {
62 + source: "ReactForgetFeatureFlag",
63 + importSpecifierName: "isForgetEnabled_Fixtures",
64 + };
65 + }
66 + if (firstLine.includes("@instrumentForget")) {
67 + enableEmitInstrumentForget = {
68 + source: "react-forget-runtime",
69 + importSpecifierName: "useRenderCounter",
70 + };
71 + }
72 + if (firstLine.includes("@enableEmitFreeze")) {
73 + enableEmitFreeze = {
74 + source: "react-forget-runtime",
75 + importSpecifierName: "makeReadOnly",
76 + };
77 + }
78 + if (firstLine.includes("@enableEmitHookGuards")) {
79 + enableEmitHookGuards = {
80 + source: "react-forget-runtime",
81 + importSpecifierName: "$dispatcherGuard",
82 + };
83 + }
84 + if (firstLine.includes("@enableUseMemoCachePolyfill")) {
85 + enableUseMemoCachePolyfill = true;
86 + }
87 + if (firstLine.includes("@panicThreshold(NONE)")) {
88 + panicThreshold = "NONE";
89 + }
90 +
91 + let eslintSuppressionRules: Array<string> | null = null;
92 + const eslintSuppressionMatch = /@eslintSuppressionRules\(([^)]+)\)/.exec(
93 + firstLine
94 + );
95 + if (eslintSuppressionMatch != null) {
96 + eslintSuppressionRules = eslintSuppressionMatch[1].split("|");
97 + }
98 +
99 + let flowSuppressions: boolean = false;
100 + if (firstLine.includes("@enableFlowSuppressions")) {
101 + flowSuppressions = true;
102 + }
103 +
104 + const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine);
105 + if (
106 + hookPatternMatch &&
107 + hookPatternMatch.length > 1 &&
108 + hookPatternMatch[1].trim().length > 0
109 + ) {
110 + hookPattern = hookPatternMatch[1].trim();
111 + } else if (firstLine.includes("@hookPattern")) {
112 + throw new Error(
113 + 'Invalid @hookPattern:"..." pragma, must contain the prefix between balanced double quotes eg @hookPattern:"pattern"'
114 + );
115 + }
116 +
117 + const config = parseConfigPragmaFn(firstLine);
118 + return {
119 + environment: {
120 + ...config,
121 + customHooks: new Map([
122 + [
123 + "useFreeze",
124 + {
125 + valueKind: "frozen" as ValueKind,
126 + effectKind: "freeze" as Effect,
127 + transitiveMixedData: false,
128 + noAlias: false,
129 + },
130 + ],
131 + [
132 + "useFragment",
133 + {
134 + valueKind: "frozen" as ValueKind,
135 + effectKind: "freeze" as Effect,
136 + transitiveMixedData: true,
137 + noAlias: true,
138 + },
139 + ],
140 + [
141 + "useNoAlias",
142 + {
143 + valueKind: "mutable" as ValueKind,
144 + effectKind: "read" as Effect,
145 + transitiveMixedData: false,
146 + noAlias: true,
147 + },
148 + ],
149 + ]),
150 + enableEmitFreeze,
151 + enableEmitInstrumentForget,
152 + enableEmitHookGuards,
153 + assertValidMutableRanges: true,
154 + hookPattern,
155 + },
156 + compilationMode,
157 + logger: null,
158 + gating,
159 + panicThreshold,
160 + noEmit: false,
161 + enableUseMemoCachePolyfill,
162 + eslintSuppressionRules,
163 + flowSuppressions,
164 + };
165 +}
166 +
167 +export function parseInput(
168 + input: string,
169 + filename: string,
170 + language: "flow" | "typescript"
171 +): BabelCore.types.File {
172 + // Extract the first line to quickly check for custom test directives
173 + if (language === "flow") {
174 + return HermesParser.parse(input, {
175 + babel: true,
176 + flow: "all",
177 + sourceFilename: filename,
178 + sourceType: "module",
179 + enableExperimentalComponentSyntax: true,
180 + });
181 + } else {
182 + return BabelParser.parse(input, {
183 + sourceFilename: filename,
184 + plugins: ["typescript", "jsx"],
185 + sourceType: "module",
186 + });
187 + }
188 +}
189 +
190 +function getEvaluatorPresets(
191 + language: "typescript" | "flow"
192 +): Array<BabelCore.PluginItem> {
193 + const presets: Array<BabelCore.PluginItem> = [
194 + {
195 + plugins: ["babel-plugin-fbt", "babel-plugin-fbt-runtime"],
196 + },
197 + ];
198 + presets.push(
199 + language === "typescript"
200 + ? [
201 + "@babel/preset-typescript",
202 + {
203 + /**
204 + * onlyRemoveTypeImports needs to be set as fbt imports
205 + * would otherwise be removed by this pass.
206 + * https://github.com/facebook/fbt/issues/49
207 + * https://github.com/facebook/sfbt/issues/72
208 + * https://dev.to/retyui/how-to-add-support-typescript-for-fbt-an-internationalization-framework-3lo0
209 + */
210 + onlyRemoveTypeImports: true,
211 + },
212 + ]
213 + : "@babel/preset-flow"
214 + );
215 +
216 + presets.push({
217 + plugins: ["@babel/plugin-syntax-jsx"],
218 + });
219 + presets.push(
220 + ["@babel/preset-react", { throwIfNamespace: false }],
221 + {
222 + plugins: ["@babel/plugin-transform-modules-commonjs"],
223 + },
224 + {
225 + plugins: [
226 + function BabelPluginRewriteRequirePath() {
227 + return {
228 + visitor: {
229 + CallExpression(path: NodePath<t.CallExpression>) {
230 + const { callee } = path.node;
231 + if (callee.type === "Identifier" && callee.name === "require") {
232 + const arg = path.node.arguments[0];
233 + if (arg.type === "StringLiteral") {
234 + // rewrite to use relative import as eval happens in
235 + // sprout/evaluator.ts
236 + if (arg.value === "shared-runtime") {
237 + arg.value = "./shared-runtime";
238 + } else if (arg.value === "ReactForgetFeatureFlag") {
239 + arg.value = "./ReactForgetFeatureFlag";
240 + }
241 + }
242 + }
243 + },
244 + },
245 + };
246 + },
247 + ],
248 + }
249 + );
250 + return presets;
251 +}
252 +function format(inputCode: string, language: "typescript" | "flow"): string {
253 + return prettier.format(inputCode, {
254 + semi: true,
255 + parser: language === "typescript" ? "babel-ts" : "flow",
256 + });
257 +}
258 +const TypescriptEvaluatorPresets = getEvaluatorPresets("typescript");
259 +const FlowEvaluatorPresets = getEvaluatorPresets("flow");
260 +
261 +export type TransformResult = {
262 + forgetOutput: string;
263 + evaluatorCode: {
264 + original: string;
265 + forget: string;
266 + } | null;
267 +};
268 +
269 +export function transformFixtureInput(
270 + input: string,
271 + fixturePath: string,
272 + parseConfigPragmaFn: typeof ParseConfigPragma,
273 + plugin: BabelCore.PluginObj,
274 + includeEvaluator: boolean
275 +): { kind: "ok"; value: TransformResult } | { kind: "err"; msg: string } {
276 + // Extract the first line to quickly check for custom test directives
277 + const firstLine = input.substring(0, input.indexOf("\n"));
278 +
279 + const language = parseLanguage(firstLine);
280 + // Preserve file extension as it determines typescript's babel transform
281 + // mode (e.g. stripping types, parsing rules for brackets)
282 + const filename =
283 + path.basename(fixturePath) + (language === "typescript" ? ".ts" : "");
284 + const inputAst = parseInput(input, filename, language);
285 +
286 + const presets =
287 + language === "typescript"
288 + ? TypescriptEvaluatorPresets
289 + : FlowEvaluatorPresets;
290 +
291 + /**
292 + * Get Forget compiled code
293 + */
294 + const forgetResult = transformFromAstSync(inputAst, input, {
295 + filename,
296 + highlightCode: false,
297 + retainLines: true,
298 + plugins: [
299 + [plugin, makePluginOptions(firstLine, parseConfigPragmaFn)],
300 + "babel-plugin-fbt",
301 + "babel-plugin-fbt-runtime",
302 + ],
303 + sourceType: "module",
304 + ast: includeEvaluator,
305 + cloneInputAst: includeEvaluator,
306 + });
307 + invariant(
308 + forgetResult?.code != null,
309 + "Expected BabelPluginReactForget to codegen successfully."
310 + );
311 + const forgetOutput = forgetResult.code;
312 + let evaluatorCode = null;
313 +
314 + if (
315 + includeEvaluator &&
316 + !SproutTodoFilter.has(fixturePath) &&
317 + !isExpectError(filename)
318 + ) {
319 + let forgetEval: string;
320 + try {
321 + invariant(
322 + forgetResult?.ast != null,
323 + "Expected BabelPluginReactForget ast."
324 + );
325 + const result = transformFromAstSync(forgetResult.ast, forgetOutput, {
326 + presets,
327 + filename,
328 + });
329 + if (result?.code == null) {
330 + return {
331 + kind: "err",
332 + msg: "Unexpected error in forget transform pipeline - no code emitted",
333 + };
334 + } else {
335 + forgetEval = result.code;
336 + }
337 + } catch (e) {
338 + return {
339 + kind: "err",
340 + msg: "Unexpected error in Forget transform pipeline: " + e.message,
341 + };
342 + }
343 +
344 + /**
345 + * Get evaluator code for source (no Forget)
346 + */
347 + let originalEval: string;
348 + try {
349 + const result = transformFromAstSync(inputAst, input, {
350 + presets,
351 + filename,
352 + });
353 +
354 + if (result?.code == null) {
355 + return {
356 + kind: "err",
357 + msg: "Unexpected error in non-forget transform pipeline - no code emitted",
358 + };
359 + } else {
360 + originalEval = result.code;
361 + }
362 + } catch (e) {
363 + return {
364 + kind: "err",
365 + msg: "Unexpected error in non-forget transform pipeline: " + e.message,
366 + };
367 + }
368 + evaluatorCode = {
369 + forget: forgetEval,
370 + original: originalEval,
371 + };
372 + }
373 + return {
374 + kind: "ok",
375 + value: {
376 + forgetOutput: format(forgetOutput, language),
377 + evaluatorCode,
378 + },
379 + };
380 +}
compiler/packages/snap/src/constants.ts renamed
+1 -1
@@ -15,7 +15,7 @@ export const COMPILER_PATH = path.join(
15 process.cwd(),
16 "dist",
17 "Babel",
18 - "RunReactForgetBabelPlugin.js"
18 + "BabelPlugin.js"
19 );
20 export const LOGGER_PATH = path.join(
21 process.cwd(),
compiler/packages/snap/src/fixture-utils.ts renamed
+6 -2
@@ -97,19 +97,21 @@ export async function readTestFilter(): Promise<TestFilter | null> {
97 export function getBasename(fixture: TestFixture): string {
98 return stripExtension(path.basename(fixture.inputPath), INPUT_EXTENSIONS);
99 }
100 -export function isExpectError(fixture: TestFixture): boolean {
101 - const basename = getBasename(fixture);
100 +export function isExpectError(fixture: TestFixture | string): boolean {
101 + const basename = typeof fixture === "string" ? fixture : getBasename(fixture);
102 return basename.startsWith("error.") || basename.startsWith("todo.error");
103 }
104
105 export type TestFixture =
106 | {
107 + fixturePath: string;
108 input: string | null;
109 inputPath: string;
110 snapshot: string | null;
111 snapshotPath: string;
112 }
113 | {
114 + fixturePath: string;
115 input: null;
116 inputPath: string;
117 snapshot: string;
@@ -181,6 +183,7 @@ export async function getFixtures(
183 for (const [partialPath, { value, filepath }] of inputs) {
184 const output = outputs.get(partialPath) ?? null;
185 fixtures.set(partialPath, {
186 + fixturePath: partialPath,
187 input: value,
188 inputPath: filepath,
189 snapshot: output,
@@ -191,6 +194,7 @@ export async function getFixtures(
194 for (const [partialPath, output] of outputs) {
195 if (!fixtures.has(partialPath)) {
196 fixtures.set(partialPath, {
197 + fixturePath: partialPath,
198 input: null,
199 inputPath: "none",
200 snapshot: output,
compiler/packages/snap/src/reporter.ts renamed
+11 -29
@@ -16,10 +16,12 @@ function wrapWithTripleBackticks(s: string, ext: string | null = null): string {
16 ${s}
17 \`\`\``;
18 }
19 +const SPROUT_SEPARATOR = "\n### Eval output\n";
20
21 export function writeOutputToString(
22 input: string,
22 - output: string | null,
23 + compilerOutput: string | null,
24 + evaluatorOutput: string | null,
25 errorMessage: string | null
26 ) {
27 // leading newline intentional
@@ -29,11 +31,11 @@ export function writeOutputToString(
31 ${wrapWithTripleBackticks(input, "javascript")}
32 `; // trailing newline + space internional
33
32 - if (output != null) {
34 + if (compilerOutput != null) {
35 result += `
36 ## Code
37
36 -${output == null ? "[ none ]" : wrapWithTripleBackticks(output, "javascript")}
38 +${wrapWithTripleBackticks(compilerOutput, "javascript")}
39 `;
40 } else {
41 result += "\n";
@@ -46,7 +48,11 @@ ${output == null ? "[ none ]" : wrapWithTripleBackticks(output, "javascript")}
48 ${wrapWithTripleBackticks(errorMessage.replace(/^\/.*?:\s/, ""))}
49 \n`;
50 }
49 - return result + ` `;
51 + result += ` `;
52 + if (evaluatorOutput != null) {
53 + result += SPROUT_SEPARATOR + evaluatorOutput;
54 + }
55 + return result;
56 }
57
58 export type TestResult = {
@@ -56,31 +62,7 @@ export type TestResult = {
62 unexpectedError: string | null;
63 };
64 export type TestResults = Map<string, TestResult>;
59 -export enum UpdateSnapshotKind {
60 - Snap,
61 - Sprout,
62 -}
63 -const SPROUT_SEPARATOR = "\n### Eval output\n";
64 -export function getUpdatedSnapshot(
65 - currentSnapshot: string | null,
66 - data: string,
67 - kind: UpdateSnapshotKind
68 -): string {
69 - let currentData = currentSnapshot?.split(SPROUT_SEPARATOR) ?? [];
70 - invariant(
71 - currentData.length <= 2,
72 - "Found duplicate sprout snapshots in fixture!"
73 - );
74 - if (kind === UpdateSnapshotKind.Snap) {
75 - const sproutData = currentData[1] ?? null;
76 - const sproutSnapshot =
77 - sproutData != null ? SPROUT_SEPARATOR + sproutData : "";
78 - return data + sproutSnapshot;
79 - } else {
80 - const snapSnapshot = currentData[0] ?? "";
81 - return snapSnapshot + SPROUT_SEPARATOR + data;
82 - }
83 -}
65 +
66 /**
67 * Update the fixtures directory given the compilation results
68 */
compiler/packages/snap/src/runner-watch.ts new
+224
@@ -0,0 +1,224 @@
1 +import watcher from "@parcel/watcher";
2 +import path from "path";
3 +import ts from "typescript";
4 +import { FILTER_FILENAME, FIXTURES_PATH } from "./constants";
5 +import { TestFilter, readTestFilter } from "./fixture-utils";
6 +
7 +export function watchSrc(
8 + onStart: () => void,
9 + onComplete: (isSuccess: boolean) => void
10 +): ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> {
11 + const configPath = ts.findConfigFile(
12 + /*searchPath*/ "./",
13 + ts.sys.fileExists,
14 + "tsconfig.json"
15 + );
16 + if (!configPath) {
17 + throw new Error("Could not find a valid 'tsconfig.json'.");
18 + }
19 + const createProgram = ts.createSemanticDiagnosticsBuilderProgram;
20 + const host = ts.createWatchCompilerHost(
21 + configPath,
22 + {},
23 + ts.sys,
24 + createProgram,
25 + () => {}, // we manually report errors in afterProgramCreate
26 + () => {} // we manually report watch status
27 + );
28 +
29 + const origCreateProgram = host.createProgram;
30 + host.createProgram = (rootNames, options, host, oldProgram) => {
31 + onStart();
32 + return origCreateProgram(rootNames, options, host, oldProgram);
33 + };
34 + const origPostProgramCreate = host.afterProgramCreate;
35 + host.afterProgramCreate = (program) => {
36 + origPostProgramCreate!(program);
37 +
38 + // syntactic diagnostics refer to javascript syntax
39 + const errors = program
40 + .getSyntacticDiagnostics()
41 + .filter((diag) => diag.category === ts.DiagnosticCategory.Error);
42 + // semantic diagnostics refer to typescript semantics
43 + errors.push(
44 + ...program
45 + .getSemanticDiagnostics()
46 + .filter((diag) => diag.category === ts.DiagnosticCategory.Error)
47 + );
48 +
49 + if (errors.length > 0) {
50 + for (const diagnostic of errors) {
51 + let fileLoc: string;
52 + if (diagnostic.file) {
53 + // https://github.com/microsoft/TypeScript/blob/ddd5084659c423f4003d2176e12d879b6a5bcf30/src/compiler/program.ts#L663-L674
54 + const { line, character } = ts.getLineAndCharacterOfPosition(
55 + diagnostic.file,
56 + diagnostic.start!
57 + );
58 + const fileName = path.relative(
59 + ts.sys.getCurrentDirectory(),
60 + diagnostic.file.fileName
61 + );
62 + fileLoc = `${fileName}:${line + 1}:${character + 1} - `;
63 + } else {
64 + fileLoc = "";
65 + }
66 + console.error(
67 + `${fileLoc}error TS${diagnostic.code}:`,
68 + ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")
69 + );
70 + }
71 + console.error(
72 + `Compilation failed (${errors.length} ${
73 + errors.length > 1 ? "errors" : "error"
74 + }).\n`
75 + );
76 + }
77 +
78 + const isSuccess = errors.length === 0;
79 + onComplete(isSuccess);
80 + };
81 +
82 + // `createWatchProgram` creates an initial program, watches files, and updates
83 + // the program over time.
84 + return ts.createWatchProgram(host);
85 +}
86 +
87 +/**
88 + * Watch mode helpers
89 + */
90 +export enum RunnerAction {
91 + Test = "Test",
92 + Update = "Update",
93 +}
94 +
95 +type RunnerMode = {
96 + action: RunnerAction;
97 + filter: boolean;
98 +};
99 +
100 +export type RunnerState = {
101 + // Monotonically increasing integer to describe the 'version' of the compiler.
102 + // This is passed to `compile()` when compiling, so that the worker knows when
103 + // to reset its module cache (compared to using its cached compiler version)
104 + compilerVersion: number;
105 + isCompilerBuildValid: boolean;
106 + // timestamp of the last update
107 + lastUpdate: number;
108 + mode: RunnerMode;
109 + filter: TestFilter | null;
110 +};
111 +
112 +function subscribeFixtures(
113 + state: RunnerState,
114 + onChange: (state: RunnerState) => void
115 +) {
116 + // Watch the fixtures directory for changes
117 + /* const fileSubscription = */
118 + watcher.subscribe(FIXTURES_PATH, async (err, _events) => {
119 + if (err) {
120 + console.error(err);
121 + process.exit(1);
122 + }
123 + // Try to ignore changes that occurred as a result of our explicitly updating
124 + // fixtures in update().
125 + // Currently keeps a timestamp of last known changes, and ignore events that occurred
126 + // around that timestamp.
127 + const isRealUpdate = performance.now() - state.lastUpdate > 5000;
128 + if (isRealUpdate) {
129 + // Fixtures changed, re-run tests
130 + onChange(state);
131 + }
132 + });
133 +}
134 +
135 +function subscribeFilterFile(
136 + state: RunnerState,
137 + onChange: (state: RunnerState) => void
138 +) {
139 + const filterSubscription = watcher.subscribe(
140 + process.cwd(),
141 + async (err, events) => {
142 + if (err) {
143 + console.error(err);
144 + process.exit(1);
145 + } else if (
146 + events.findIndex((event) => event.path.includes(FILTER_FILENAME)) !== -1
147 + ) {
148 + state.filter = await readTestFilter();
149 + if (state.mode.filter) {
150 + state.mode.action = RunnerAction.Test;
151 + onChange(state);
152 + }
153 + }
154 + }
155 + );
156 +}
157 +
158 +function subscribeTsc(
159 + state: RunnerState,
160 + onChange: (state: RunnerState) => void
161 +) {
162 + // Run TS in incremental watch mode
163 + watchSrc(
164 + function onStart() {
165 + // Notify the user when compilation starts but don't clear the screen yet
166 + console.log("\nCompiling...");
167 + },
168 + (isSuccess) => {
169 + // Bump the compiler version after a build finishes
170 + // and re-run tests
171 + if (isSuccess) {
172 + state.compilerVersion++;
173 + }
174 + state.isCompilerBuildValid = isSuccess;
175 + if (state.filter) {
176 + state.mode.action = RunnerAction.Test;
177 + } else {
178 + state.mode.action = RunnerAction.Test;
179 + }
180 + onChange(state);
181 + }
182 + );
183 +}
184 +
185 +function subscribeKeyEvents(
186 + state: RunnerState,
187 + onChange: (state: RunnerState) => void
188 +) {
189 + process.stdin.on("keypress", (str, key) => {
190 + if (key.name === "u") {
191 + // u => update fixtures
192 + state.mode.action = RunnerAction.Update;
193 + } else if (key.name === "q") {
194 + process.exit(0);
195 + } else if (key.name === "f") {
196 + state.mode.filter = !state.mode.filter;
197 + } else {
198 + // any other key re-runs tests
199 + state.mode.action = RunnerAction.Test;
200 + }
201 + onChange(state);
202 + });
203 +}
204 +
205 +export async function makeWatchRunner(
206 + onChange: (state: RunnerState) => void,
207 + filterMode: boolean
208 +): Promise<void> {
209 + const state = {
210 + compilerVersion: 0,
211 + isCompilerBuildValid: false,
212 + lastUpdate: -1,
213 + mode: {
214 + action: RunnerAction.Test,
215 + filter: filterMode,
216 + },
217 + filter: await readTestFilter(),
218 + };
219 +
220 + subscribeTsc(state, onChange);
221 + subscribeFixtures(state, onChange);
222 + subscribeKeyEvents(state, onChange);
223 + subscribeFilterFile(state, onChange);
224 +}
compiler/packages/snap/src/runner-worker.ts renamed
+99 -51
@@ -6,18 +6,17 @@
6 */
7
8 import { codeFrameColumns } from "@babel/code-frame";
9 -import type { runReactForgetBabelPlugin as RunReactForgetBabelPlugin } from "babel-plugin-react-forget/src/Babel/RunReactForgetBabelPlugin";
9 +import type { PluginObj } from "@babel/core";
10 import type { parseConfigPragma as ParseConfigPragma } from "babel-plugin-react-forget/src/HIR/Environment";
11 +import { TransformResult, transformFixtureInput } from "./compiler";
12 import {
12 - TestFixture,
13 - TestResult,
14 - UpdateSnapshotKind,
15 - getBasename,
16 - getUpdatedSnapshot,
17 - isExpectError,
18 - transformFixtureInput,
19 - writeOutputToString,
20 -} from "fixture-test-utils";
13 + COMPILER_PATH,
14 + LOGGER_PATH,
15 + PARSE_CONFIG_PRAGMA_PATH,
16 +} from "./constants";
17 +import { TestFixture, getBasename, isExpectError } from "./fixture-utils";
18 +import { TestResult, writeOutputToString } from "./reporter";
19 +import { runSprout } from "./sprout";
20
21 const originalConsoleError = console.error;
22
@@ -34,15 +33,16 @@ export function clearRequireCache() {
33 });
34 }
35
37 -export async function compile(
38 - compilerPath: string,
39 - loggerPath: string,
40 - parseConfigPragmaPath: string,
41 - fixture: TestFixture,
36 +function compile(
37 + input: string,
38 + fixturePath: string,
39 compilerVersion: number,
43 - implicitDebugMode: boolean,
44 - isOnlyFixture: boolean
45 -): Promise<TestResult> {
40 + shouldLog: boolean,
41 + includeEvaluator: boolean
42 +): {
43 + error: string | null;
44 + compileResult: TransformResult | null;
45 +} {
46 const seenConsoleErrors: Array<string> = [];
47 console.error = (...messages: Array<string>) => {
48 seenConsoleErrors.push(...messages);
@@ -51,47 +51,38 @@ export async function compile(
51 clearRequireCache();
52 }
53 version = compilerVersion;
54 - const { input, snapshot: expected, snapshotPath: outputPath } = fixture;
55 - const basename = getBasename(fixture);
56 - const expectError = isExpectError(fixture);
57 -
58 - // Input will be null if the input file did not exist, in which case the output file
59 - // is stale
60 - if (input === null) {
61 - return {
62 - outputPath,
63 - actual: null,
64 - expected,
65 - unexpectedError: null,
66 - };
67 - }
54
69 - let code: string | null = null;
55 + let compileResult: TransformResult | null = null;
56 let error: string | null = null;
57 try {
58 // NOTE: we intentionally require lazily here so that we can clear the require cache
59 // and load fresh versions of the compiler when `compilerVersion` changes.
74 - const { runReactForgetBabelPlugin } = require(compilerPath) as {
75 - runReactForgetBabelPlugin: typeof RunReactForgetBabelPlugin;
60 + const { default: ReactForgetBabelPlugin } = require(COMPILER_PATH) as {
61 + default: PluginObj;
62 };
77 - const { toggleLogging } = require(loggerPath);
78 - const { parseConfigPragma } = require(parseConfigPragmaPath) as {
63 + const { toggleLogging } = require(LOGGER_PATH);
64 + const { parseConfigPragma } = require(PARSE_CONFIG_PRAGMA_PATH) as {
65 parseConfigPragma: typeof ParseConfigPragma;
66 };
67
68 // only try logging if we filtered out all but one fixture,
69 // since console log order is non-deterministic
84 - const shouldLogPragma = input.split("\n")[0].includes("@debug");
85 - toggleLogging(isOnlyFixture && (shouldLogPragma || implicitDebugMode));
86 - code =
87 - transformFixtureInput(
88 - input,
89 - basename,
90 - runReactForgetBabelPlugin,
91 - parseConfigPragma
92 - ).code ?? null;
70 + toggleLogging(shouldLog);
71 + const result = transformFixtureInput(
72 + input,
73 + fixturePath,
74 + parseConfigPragma,
75 + ReactForgetBabelPlugin,
76 + includeEvaluator
77 + );
78 +
79 + if (result.kind === "err") {
80 + error = result.msg;
81 + } else {
82 + compileResult = result.value;
83 + }
84 } catch (e) {
94 - if (isOnlyFixture && !expectError) {
85 + if (shouldLog) {
86 console.error(e.stack);
87 }
88 error = e.message.replace(/\u001b[^m]*m/g, "");
@@ -128,6 +119,41 @@ export async function compile(
119 error = `ConsoleError: ${consoleError}`;
120 }
121 }
122 + console.error = originalConsoleError;
123 +
124 + return {
125 + error,
126 + compileResult,
127 + };
128 +}
129 +
130 +export async function transformFixture(
131 + fixture: TestFixture,
132 + compilerVersion: number,
133 + shouldLog: boolean,
134 + includeEvaluator: boolean
135 +): Promise<TestResult> {
136 + const { input, snapshot: expected, snapshotPath: outputPath } = fixture;
137 + const basename = getBasename(fixture);
138 + const expectError = isExpectError(fixture);
139 +
140 + // Input will be null if the input file did not exist, in which case the output file
141 + // is stale
142 + if (input === null) {
143 + return {
144 + outputPath,
145 + actual: null,
146 + expected,
147 + unexpectedError: null,
148 + };
149 + }
150 + const { compileResult, error } = compile(
151 + input,
152 + fixture.fixturePath,
153 + compilerVersion,
154 + shouldLog,
155 + includeEvaluator
156 + );
157
158 let unexpectedError: string | null = null;
159 if (expectError) {
@@ -137,16 +163,38 @@ export async function compile(
163 } else {
164 if (error !== null) {
165 unexpectedError = `Expected fixture '${basename}' to succeed but it failed with error:\n\n${error}`;
140 - } else if (code == null || code.length === 0) {
166 + } else if (compileResult == null) {
167 unexpectedError = `Expected output for fixture '${basename}'.`;
168 }
169 }
170
145 - console.error = originalConsoleError;
146 - const output = writeOutputToString(input, code, error);
171 + const snapOutput: string | null = compileResult?.forgetOutput ?? null;
172 + let sproutOutput: string | null = null;
173 + if (compileResult?.evaluatorCode != null) {
174 + const sproutResult = runSprout(
175 + compileResult.evaluatorCode.original,
176 + compileResult.evaluatorCode.forget
177 + );
178 + if (sproutResult.kind === "invalid") {
179 + unexpectedError ??= "";
180 + unexpectedError += `\n\n${sproutResult.value}`;
181 + } else {
182 + sproutOutput = sproutResult.value;
183 + }
184 + } else if (!includeEvaluator && expected != null) {
185 + sproutOutput = expected.split("\n### Eval output\n")[1];
186 + }
187 +
188 + const actualOutput = writeOutputToString(
189 + input,
190 + snapOutput,
191 + sproutOutput,
192 + error
193 + );
194 +
195 return {
196 outputPath,
149 - actual: getUpdatedSnapshot(expected, output, UpdateSnapshotKind.Snap),
197 + actual: actualOutput,
198 expected,
199 unexpectedError,
200 };
compiler/packages/snap/src/runner.ts
+83 -328
@@ -5,49 +5,27 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import watcher from "@parcel/watcher";
9 -import {
10 - COMPILER_PATH,
11 - FILTER_FILENAME,
12 - FILTER_PATH,
13 - FIXTURES_PATH,
14 - LOGGER_PATH,
15 - PARSE_CONFIG_PRAGMA_PATH,
16 - TestFilter,
17 - TestResult,
18 - TestResults,
19 - getFixtures,
20 - readTestFilter,
21 - report,
22 - update,
23 -} from "fixture-test-utils";
8 import { Worker } from "jest-worker";
25 -import path from "path";
9 import process from "process";
10 import * as readline from "readline";
11 import ts from "typescript";
12 import yargs from "yargs";
13 import { hideBin } from "yargs/helpers";
31 -import * as compiler from "./compiler-worker";
14 +import { FILTER_PATH } from "./constants";
15 +import { TestFilter, getFixtures, readTestFilter } from "./fixture-utils";
16 +import { TestResult, TestResults, report, update } from "./reporter";
17 +import {
18 + RunnerAction,
19 + RunnerState,
20 + makeWatchRunner,
21 + watchSrc,
22 +} from "./runner-watch";
23 +import * as runnerWorker from "./runner-worker";
24
33 -const WORKER_PATH = require.resolve("./compiler-worker.js");
25 +const WORKER_PATH = require.resolve("./runner-worker.js");
26
27 readline.emitKeypressEvents(process.stdin);
28
37 -process.stdin.on("keypress", function (chunk, key) {
38 - if (key && key.name === "c" && key.ctrl) {
39 - cleanup(-1);
40 - }
41 -});
42 -process.on("SIGINT", function () {
43 - // Parent process may send SIGINT
44 - cleanup(-1);
45 -});
46 -
47 -process.on("SIGTERM", function () {
48 - cleanup(-1);
49 -});
50 -
29 type RunnerOptions = {
30 sync: boolean;
31 workerThreads: boolean;
@@ -85,31 +63,11 @@ const opts: RunnerOptions = yargs
63 .strict()
64 .parseSync(hideBin(process.argv));
65
88 -/**
89 - * Cleanup / handle interrupts
90 - */
91 -const cleanupTasks: Array<() => void> = new Array();
92 -function pushCleanupTask(fn: () => void) {
93 - cleanupTasks.push(fn);
94 -}
95 -function cleanup(code: number) {
96 - for (const task of cleanupTasks) {
97 - task();
98 - }
99 - process.exit(code);
100 -}
101 -function clearConsole() {
102 - // console.clear() only works when stdout is connected to a TTY device.
103 - // we're currently piping stdout (see main.ts), so let's do a 'hack'
104 - console.log("\u001Bc");
105 -}
106 -
66 /**
67 * Do a test run and return the test results
68 */
110 -async function run(
111 - worker: Worker & typeof compiler,
112 - opts: RunnerOptions,
69 +async function runFixtures(
70 + worker: Worker & typeof runnerWorker,
71 filter: TestFilter | null,
72 compilerVersion: number
73 ): Promise<TestResults> {
@@ -126,14 +84,11 @@ async function run(
84 for (const [fixtureName, fixture] of fixtures) {
85 work.push(
86 worker
129 - .compile(
130 - COMPILER_PATH,
131 - LOGGER_PATH,
132 - PARSE_CONFIG_PRAGMA_PATH,
87 + .transformFixture(
88 fixture,
89 compilerVersion,
135 - filter?.debug ?? false,
136 - isOnlyFixture
90 + (filter?.debug ?? false) && isOnlyFixture,
91 + true
92 )
93 .then((result) => [fixtureName, result])
94 );
@@ -143,14 +98,11 @@ async function run(
98 } else {
99 entries = [];
100 for (const [fixtureName, fixture] of fixtures) {
146 - let output = await compiler.compile(
147 - COMPILER_PATH,
148 - LOGGER_PATH,
149 - PARSE_CONFIG_PRAGMA_PATH,
101 + let output = await runnerWorker.transformFixture(
102 fixture,
103 compilerVersion,
152 - filter?.debug ?? false,
153 - isOnlyFixture
104 + (filter?.debug ?? false) && isOnlyFixture,
105 + true
106 );
107 entries.push([fixtureName, output]);
108 }
@@ -159,289 +111,92 @@ async function run(
111 return new Map(entries);
112 }
113
162 -function watchSrc(
163 - onStart: () => void,
164 - onComplete: (isSuccess: boolean) => void
165 -): ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> {
166 - const configPath = ts.findConfigFile(
167 - /*searchPath*/ "./",
168 - ts.sys.fileExists,
169 - "tsconfig.json"
170 - );
171 - if (!configPath) {
172 - throw new Error("Could not find a valid 'tsconfig.json'.");
173 - }
174 - const createProgram = ts.createSemanticDiagnosticsBuilderProgram;
175 - const host = ts.createWatchCompilerHost(
176 - configPath,
177 - {},
178 - ts.sys,
179 - createProgram,
180 - () => {}, // we manually report errors in afterProgramCreate
181 - () => {} // we manually report watch status
182 - );
183 -
184 - const origCreateProgram = host.createProgram;
185 - host.createProgram = (rootNames, options, host, oldProgram) => {
186 - onStart();
187 - return origCreateProgram(rootNames, options, host, oldProgram);
188 - };
189 - const origPostProgramCreate = host.afterProgramCreate;
190 - host.afterProgramCreate = (program) => {
191 - origPostProgramCreate!(program);
192 -
193 - // syntactic diagnostics refer to javascript syntax
194 - const errors = program
195 - .getSyntacticDiagnostics()
196 - .filter((diag) => diag.category === ts.DiagnosticCategory.Error);
197 - // semantic diagnostics refer to typescript semantics
198 - errors.push(
199 - ...program
200 - .getSemanticDiagnostics()
201 - .filter((diag) => diag.category === ts.DiagnosticCategory.Error)
114 +// Callback to re-run tests after some change
115 +async function onChange(
116 + worker: Worker & typeof runnerWorker,
117 + state: RunnerState
118 +) {
119 + const { compilerVersion, isCompilerBuildValid, mode, filter } = state;
120 + if (isCompilerBuildValid) {
121 + const start = performance.now();
122 +
123 + // console.clear() only works when stdout is connected to a TTY device.
124 + // we're currently piping stdout (see main.ts), so let's do a 'hack'
125 + console.log("\u001Bc");
126 +
127 + // we don't clear console after this point, since
128 + // it may contain debug console logging
129 + const results = await runFixtures(
130 + worker,
131 + mode.filter ? filter : null,
132 + compilerVersion
133 );
203 -
204 - if (errors.length > 0) {
205 - for (const diagnostic of errors) {
206 - let fileLoc: string;
207 - if (diagnostic.file) {
208 - // https://github.com/microsoft/TypeScript/blob/ddd5084659c423f4003d2176e12d879b6a5bcf30/src/compiler/program.ts#L663-L674
209 - const { line, character } = ts.getLineAndCharacterOfPosition(
210 - diagnostic.file,
211 - diagnostic.start!
212 - );
213 - const fileName = path.relative(
214 - ts.sys.getCurrentDirectory(),
215 - diagnostic.file.fileName
216 - );
217 - fileLoc = `${fileName}:${line + 1}:${character + 1} - `;
218 - } else {
219 - fileLoc = "";
220 - }
221 - console.error(
222 - `${fileLoc}error TS${diagnostic.code}:`,
223 - ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")
224 - );
225 - }
226 - console.error(
227 - `Compilation failed (${errors.length} ${
228 - errors.length > 1 ? "errors" : "error"
229 - }).\n`
230 - );
134 + const end = performance.now();
135 + if (mode.action === RunnerAction.Update) {
136 + update(results);
137 + state.lastUpdate = end;
138 + } else {
139 + report(results);
140 }
232 -
233 - const isSuccess = errors.length === 0;
234 - onComplete(isSuccess);
235 - };
236 -
237 - // `createWatchProgram` creates an initial program, watches files, and updates
238 - // the program over time.
239 - return ts.createWatchProgram(host);
240 -}
241 -
242 -enum Mode {
243 - Test = "Test",
244 - Update = "Update",
141 + console.log(`Completed in ${Math.floor(end - start)} ms`);
142 + } else {
143 + console.error(
144 + `${mode}: Found errors in Forget source code, skipping test fixtures.`
145 + );
146 + }
147 + console.log(
148 + "\n" +
149 + (mode.filter
150 + ? `Current mode = FILTER, filter test fixtures by "${FILTER_PATH}".`
151 + : "Current mode = NORMAL, run all test fixtures.") +
152 + "\nWaiting for input or file changes...\n" +
153 + "u - update all fixtures\n" +
154 + `f - toggle (turn ${mode.filter ? "off" : "on"}) filter mode\n` +
155 + "q - quit\n" +
156 + "[any] - rerun tests\n"
157 + );
158 }
159
160 /**
161 * Runs the compiler in watch or single-execution mode
162 */
163 export async function main(opts: RunnerOptions): Promise<void> {
251 - const worker: Worker & typeof compiler = new Worker(WORKER_PATH, {
164 + const worker: Worker & typeof runnerWorker = new Worker(WORKER_PATH, {
165 enableWorkerThreads: opts.workerThreads,
166 }) as any;
167 worker.getStderr().pipe(process.stderr);
168 worker.getStdout().pipe(process.stdout);
256 - pushCleanupTask(() => {
257 - worker.end();
258 - });
169
170 if (opts.watch) {
261 - // Monotonically increasing integer to describe the 'version' of the compiler.
262 - // This is passed to `compile()` (from compiler-worker) when compiling, so
263 - // that the worker knows when it has to reset its module cache and when its
264 - // safe to use a cached compiler version
265 - let compilerVersion = 0;
266 - let isCompilerValid = false;
267 - let lastUpdate = -1;
268 - let filterMode: boolean = opts.filter;
269 - let testFilter: TestFilter | null;
270 -
271 - function isRealUpdate(): boolean {
272 - // Try to ignore changes that occurred as a result of our explicitly updating
273 - // fixtures in update().
274 - // Currently keeps a timestamp of last known changes, and ignore events that occurred
275 - // around that timestamp.
276 - return performance.now() - lastUpdate > 5000;
277 - }
278 -
279 - function onStart() {
280 - // Notify the user when compilation starts but don't clear the screen yet
281 - console.log("\nCompiling...");
282 - }
283 -
284 - // Callback to re-run tests after some change
285 - async function onChange({ mode }: { mode: Mode }) {
286 - if (isCompilerValid) {
287 - const start = performance.now();
288 - clearConsole();
289 - console.log("Running tests...");
290 - // we don't clear console after this point, since
291 - // it may contain debug console logging
292 - const results = await run(
293 - worker,
294 - opts,
295 - filterMode ? await readTestFilter() : null,
296 - compilerVersion
297 - );
298 - if (mode === Mode.Update) {
299 - update(results);
300 - } else {
301 - report(results);
302 - }
303 - const end = performance.now();
304 - if (mode === Mode.Update) {
305 - lastUpdate = end;
306 - }
307 - console.log(`Completed in ${Math.floor(end - start)} ms`);
308 - } else {
309 - console.error(
310 - `${mode}: Found errors in Forget source code, skipping test fixtures.`
311 - );
312 - }
313 - console.log(
314 - "\n" +
315 - (filterMode
316 - ? `Current mode = FILTER, filter test fixtures by "${FILTER_PATH}".`
317 - : "Current mode = NORMAL, run all test fixtures.") +
318 - "\nWaiting for input or file changes...\n" +
319 - "u - update all fixtures\n" +
320 - `f - toggle (turn ${filterMode ? "off" : "on"}) filter mode\n` +
321 - "q - quit\n" +
322 - "[any] - rerun tests\n"
323 - );
324 - }
325 -
326 - // Run TS in incremental watch mode
327 - const tsWatch = watchSrc(onStart, (isSuccess) => {
328 - // Bump the compiler version after a build finishes
329 - // and re-run tests
330 - if (isSuccess) {
331 - compilerVersion++;
332 - }
333 - isCompilerValid = isSuccess;
334 - onChange({ mode: Mode.Test });
335 - });
336 - pushCleanupTask(() => {
337 - tsWatch.close();
338 - });
339 -
340 - // Watch the fixtures directory for changes
341 - const fileSubscription = watcher.subscribe(
342 - FIXTURES_PATH,
343 - async (err, _events) => {
344 - if (err) {
345 - console.error(err);
346 - process.exit(1);
347 - }
348 - if (isRealUpdate()) {
349 - // Fixtures changed, re-run tests
350 - onChange({ mode: Mode.Test });
351 - }
352 - }
353 - );
354 -
355 - pushCleanupTask(() => {
356 - fileSubscription
357 - .then((subscription) => {
358 - subscription.unsubscribe();
359 - })
360 - .catch((err) => {
361 - console.log("error cleaning up file subscription", err);
362 - });
363 - });
364 -
365 - const filterSubscription = watcher.subscribe(
366 - process.cwd(),
367 - async (err, events) => {
368 - if (err) {
369 - console.error(err);
370 - process.exit(1);
371 - } else if (
372 - events.findIndex((event) => event.path.includes(FILTER_FILENAME)) !==
373 - -1
374 - ) {
375 - if (filterMode) {
376 - testFilter = await readTestFilter();
377 - onChange({ mode: Mode.Test });
378 - }
379 - }
380 - }
381 - );
382 - pushCleanupTask(() => {
383 - filterSubscription
384 - .then((subscription) => {
385 - subscription.unsubscribe();
386 - })
387 - .catch((err) => {
388 - console.log("error cleaning up filter subscription", err);
389 - });
390 - });
391 -
392 - // Basic key event handling
393 - process.stdin.on("keypress", (str, key) => {
394 - if (key.name === "u") {
395 - // u => update fixtures
396 - onChange({ mode: Mode.Update });
397 - } else if (key.name === "q") {
398 - process.exit(0);
399 - } else if (key.name === "f") {
400 - filterMode = !filterMode;
401 - onChange({ mode: Mode.Test });
402 - } else {
403 - // any other key re-runs tests
404 - onChange({ mode: Mode.Test });
405 - }
406 - });
171 + makeWatchRunner((state) => onChange(worker, state), opts.filter);
172 } else {
173 // Non-watch mode. For simplicity we re-use the same watchSrc() function.
174 // After the first build completes run tests and exit
410 - let tsWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> | null =
411 - null;
412 - tsWatch = watchSrc(
413 - () => {},
414 - async (compileSuccess: boolean) => {
415 - let isSuccess = compileSuccess;
416 - if (compileSuccess) {
417 - const testFilter = opts.filter ? await readTestFilter() : null;
418 - const results = await run(worker, opts, testFilter, 0);
419 - if (opts.update) {
420 - update(results);
175 + const tsWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> =
176 + watchSrc(
177 + () => {},
178 + async (compileSuccess: boolean) => {
179 + let isSuccess = compileSuccess;
180 + if (compileSuccess) {
181 + const testFilter = opts.filter ? await readTestFilter() : null;
182 + const results = await runFixtures(worker, testFilter, 0);
183 + if (opts.update) {
184 + update(results);
185 + } else {
186 + const testSuccess = report(results);
187 + isSuccess &&= testSuccess;
188 + }
189 } else {
422 - const testSuccess = report(results);
423 - isSuccess &&= testSuccess;
190 + console.error(
191 + "Found errors in Forget source code, skipping test fixtures."
192 + );
193 }
425 - } else {
426 - console.error(
427 - "Found errors in Forget source code, skipping test fixtures."
428 - );
429 - }
430 - if (tsWatch != null) {
194 tsWatch.close();
432 - tsWatch = null;
195 + await worker.end();
196 + process.exit(isSuccess ? 0 : 1);
197 }
434 - await worker.end();
435 - process.exit(isSuccess ? 0 : 1);
436 - }
437 - );
438 - pushCleanupTask(() => {
439 - tsWatch?.close();
440 - tsWatch = null;
441 - });
198 + );
199 }
200 }
201
445 -// I couldn't figure out the right combination of settings to allow using `await` at the top-level,
446 -// but it's easy enough to use the promise API just here
202 main(opts).catch((error) => console.error(error));
compiler/packages/snap/src/sprout/README.md renamed
+1 -31
@@ -7,37 +7,7 @@ We hope to add fuzzing capabilities to Sprout, synthesizing sets of program inpu
7 Sprout is now enabled for all fixtures! If Sprout cannot execute your fixture due to some technical limitations, add your fixture to [`SproutTodoFilter.ts`](./src/SproutTodoFilter.ts) with a comment explaining why.
8
9 ### Sprout CLI
10 -Sprout can be run from `packages/babel-plugin-react-forget`. When adding new fixtures to Sprout, please check that fixture outputs look reasonable with `yarn sprout --filter --verbose`.
11 -
12 -```sh
13 -# rebuild when sprout or babel-plugin-react-forget changes
14 -$ yarn sprout:build
15 -
16 -# evaluate all test fixtures not listed in SproutTodoFilter
17 -$ yarn sprout
18 -
19 -# show all sprout options
20 -$ yarn sprout --help
21 -Options:
22 - --sync Run compiler in main thread.
23 - [boolean] [default: false]
24 - --filter Evaluate fixtures in filter mode.
25 - [boolean] [default: false]
26 - --verbose Print all fixture outputs and logs.
27 - [boolean] [default: false]
28 -```
29 -
30 -Sprout can be run in filter mode with `yarn sprout --filter`. Just like Snap, Sprout expects `babel-plugin-react-forget/testfilter.txt` be formatted as such.
31 -- *first line:* `// @only` or `// @skip`
32 -- *all other lines:* a test fixture name, i.e. the relative path from the compiler fixtures dir, without a `.js` or `.expect.md` extension.
33 -
34 -Example:
35 -```c
36 -// @only
37 -console-readonly
38 -constant-propagate-global-phis
39 -dce-loop
40 -```
10 +Sprout is now run as a part of snap, except when in filter mode.
11
12 ### Adding fixtures to Sprout
13
compiler/packages/snap/src/sprout/ReactForgetFeatureFlag.ts renamed
compiler/packages/snap/src/sprout/evaluator.ts renamed
+1 -1
@@ -6,11 +6,11 @@
6 */
7
8 import { render } from "@testing-library/react";
9 -import { PROJECT_ROOT } from "fixture-test-utils";
9 import { JSDOM } from "jsdom";
10 import util from "util";
11 import { z } from "zod";
12 import { fromZodError } from "zod-validation-error";
13 +import { PROJECT_ROOT } from "../constants";
14 import { initFbt, toJSON } from "./shared-runtime";
15 const React = require("react");
16
compiler/packages/snap/src/sprout/index.ts new
+57
@@ -0,0 +1,57 @@
1 +import { EvaluatorResult, doEval } from "./evaluator";
2 +
3 +export type SproutResult =
4 + | { kind: "success"; value: string }
5 + | { kind: "invalid"; value: string };
6 +
7 +function stringify(result: EvaluatorResult): string {
8 + return `(kind: ${result.kind}) ${result.value}${
9 + result.logs.length > 0 ? `\nlogs: [${result.logs.toString()}]` : ""
10 + }`;
11 +}
12 +function makeError(description: string, value: string): SproutResult {
13 + return {
14 + kind: "invalid",
15 + value: description + "\n" + value,
16 + };
17 +}
18 +function logsEqual(a: Array<string>, b: Array<string>) {
19 + if (a.length !== b.length) {
20 + return false;
21 + }
22 + return a.every((val, idx) => val === b[idx]);
23 +}
24 +export function runSprout(
25 + originalCode: string,
26 + forgetCode: string
27 +): SproutResult {
28 + const nonForgetResult = doEval(originalCode);
29 + const forgetResult = doEval(forgetCode);
30 +
31 + if (forgetResult.kind === "UnexpectedError") {
32 + return makeError("Unexpected error in Forget runner", forgetResult.value);
33 + } else if (nonForgetResult.kind === "UnexpectedError") {
34 + return makeError(
35 + "Unexpected error in non-forget runner",
36 + nonForgetResult.value
37 + );
38 + } else if (
39 + forgetResult.kind !== nonForgetResult.kind ||
40 + forgetResult.value !== nonForgetResult.value ||
41 + !logsEqual(forgetResult.logs, nonForgetResult.logs)
42 + ) {
43 + return makeError(
44 + "Found differences in evaluator results",
45 + `Non-forget (expected):
46 +${stringify(nonForgetResult)}
47 +Forget:
48 +${stringify(forgetResult)}
49 +`
50 + );
51 + } else {
52 + return {
53 + kind: "success",
54 + value: stringify(forgetResult),
55 + };
56 + }
57 +}
compiler/packages/snap/src/sprout/shared-runtime.ts renamed
compiler/packages/snap/src/types.d.ts new
+13
@@ -0,0 +1,13 @@
1 +// v0.17.1
2 +declare module "hermes-parser" {
3 + type HermesParserOptions = {
4 + allowReturnOutsideFunction?: boolean;
5 + babel?: boolean;
6 + flow?: "all" | "detect";
7 + enableExperimentalComponentSyntax?: boolean;
8 + sourceFilename?: string;
9 + sourceType?: "module" | "script" | "unambiguous";
10 + tokens?: boolean;
11 + };
12 + export function parse(code: string, options: Partial<HermesParserOptions>);
13 +}
compiler/packages/snap/tsconfig.json
-3
@@ -18,9 +18,6 @@
18 "exclude": ["node_modules"],
19 "include": ["src/**/*.ts"],
20 "references": [
21 - {
22 - "path": "../fixture-test-utils"
23 - },
21 {
22 "path": "../babel-plugin-react-forget"
23 }
compiler/packages/sprout/package.json deleted
-52
@@ -1,52 +0,0 @@
1 -{
2 - "name": "sprout",
3 - "version": "0.0.1",
4 - "public": false,
5 - "main": "dist/main.js",
6 - "license": "MIT",
7 - "files": [
8 - "src"
9 - ],
10 - "scripts": {
11 - "build": "rimraf dist && tsc --build",
12 - "prettier": "prettier --write src",
13 - "test": "echo 'no tests'"
14 - },
15 - "repository": {
16 - "type": "git",
17 - "url": "git+https://github.com/facebook/react-forget.git"
18 - },
19 - "dependencies": {
20 - "@babel/generator": "7.2.0",
21 - "@babel/plugin-syntax-jsx": "^7.18.6",
22 - "@babel/preset-flow": "^7.7.4",
23 - "@babel/preset-typescript": "^7.18.6",
24 - "@babel/types": "^7.19.0",
25 - "@parcel/watcher": "^2.1.0",
26 - "@testing-library/react": "^13.4.0",
27 - "babel-plugin-react-forget": "*",
28 - "chalk": "4",
29 - "fbt": "^1.0.0",
30 - "fixture-test-utils": "*",
31 - "jsdom": "^22.1.0",
32 - "react": "^0.0.0-experimental-493f72b0a-20230727",
33 - "react-dom": "^0.0.0-experimental-493f72b0a-20230727",
34 - "readline": "^1.3.0",
35 - "typescript": "^5.1.0",
36 - "yargs": "^17.7.1"
37 - },
38 - "devDependencies": {
39 - "@babel/core": "^7.19.1",
40 - "@babel/parser": "^7.19.1",
41 - "@babel/plugin-syntax-typescript": "^7.18.6",
42 - "@babel/plugin-transform-modules-commonjs": "^7.18.6",
43 - "@babel/preset-react": "^7.18.6",
44 - "@babel/traverse": "^7.19.1",
45 - "@types/fbt": "^1.0.4",
46 - "@types/node": "^18.7.18",
47 - "@typescript-eslint/eslint-plugin": "^5.51.0",
48 - "@typescript-eslint/parser": "^5.51.0",
49 - "prettier": "2.8.8",
50 - "rimraf": "^3.0.2"
51 - }
52 -}
compiler/packages/sprout/src/main.ts deleted
-52
@@ -1,52 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and 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 { fork } from "child_process";
9 -import invariant from "invariant";
10 -import process from "process";
11 -import * as readline from "readline";
12 -import { hideBin } from "yargs/helpers";
13 -
14 -readline.emitKeypressEvents(process.stdin);
15 -
16 -if (process.stdin.isTTY) {
17 - process.stdin.setRawMode(true);
18 -}
19 -
20 -process.stdin.on("keypress", function (_, key) {
21 - if (key && key.name === "c" && key.ctrl) {
22 - // handle sigint
23 - if (childProc) {
24 - console.log("Interrupted!!");
25 - childProc.kill("SIGINT");
26 - childProc.unref();
27 - process.exit(-1);
28 - }
29 - }
30 -});
31 -
32 -const childProc = fork(require.resolve("./runner.js"), hideBin(process.argv), {
33 - // for some reason, keypress events aren't sent to handlers in both processes
34 - // when we `inherit` stdin.
35 - // pipe stdout and stderr so we can silence child process after parent exits
36 - stdio: ["pipe", "pipe", "pipe", "ipc"],
37 - // forward existing env variables, like `NODE_OPTIONS` which VSCode uses to attach
38 - // its debugger
39 - env: { ...process.env, FORCE_COLOR: "true" },
40 -});
41 -
42 -invariant(
43 - childProc.stdin && childProc.stdout && childProc.stderr,
44 - "Expected forked process to have piped stdio"
45 -);
46 -process.stdin.pipe(childProc.stdin);
47 -childProc.stdout.pipe(process.stdout);
48 -childProc.stderr.pipe(process.stderr);
49 -
50 -childProc.on("exit", (code) => {
51 - process.exit(code ?? -1);
52 -});
compiler/packages/sprout/src/runner-worker.ts deleted
-258
@@ -1,258 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and 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 { NodePath, PluginItem, transformFromAstSync } from "@babel/core";
9 -import * as parser from "@babel/parser";
10 -import * as t from "@babel/types";
11 -import type { runReactForgetBabelPlugin as RunReactForgetBabelPlugin } from "babel-plugin-react-forget/src/Babel/RunReactForgetBabelPlugin";
12 -import type { parseConfigPragma as ParseConfigPragma } from "babel-plugin-react-forget/src/HIR/Environment";
13 -import {
14 - COMPILER_PATH,
15 - PARSE_CONFIG_PRAGMA_PATH,
16 - TestFixture,
17 - parseLanguage,
18 - transformFixtureInput,
19 -} from "fixture-test-utils";
20 -import { EvaluatorResult, doEval } from "./runner-evaluator";
21 -import path from "path";
22 -
23 -const { runReactForgetBabelPlugin } = require(COMPILER_PATH) as {
24 - runReactForgetBabelPlugin: typeof RunReactForgetBabelPlugin;
25 -};
26 -
27 -const { parseConfigPragma } = require(PARSE_CONFIG_PRAGMA_PATH) as {
28 - parseConfigPragma: typeof ParseConfigPragma;
29 -};
30 -
31 -export type SproutFixtureResult =
32 - | {
33 - nonForgetResult: EvaluatorResult;
34 - forgetResult: EvaluatorResult;
35 - unexpectedError: null;
36 - snapshot: string | null;
37 - snapshotPath: string;
38 - }
39 - | {
40 - nonForgetResult: null;
41 - forgetResult: null;
42 - unexpectedError: string;
43 - snapshot: string | null;
44 - snapshotPath: string;
45 - };
46 -
47 -type TransformResult =
48 - | {
49 - type: "Ok";
50 - value: string;
51 - }
52 - | {
53 - type: "UnexpectedError";
54 - value: string;
55 - };
56 -
57 -// Transforms that should run on both forget and non-forget
58 -// source code.
59 -function transformAST(
60 - ast: t.File,
61 - sourceCode: string,
62 - filename: string,
63 - language: "typescript" | "flow",
64 - transformJSX: boolean
65 -): string {
66 - const presets: Array<PluginItem> = [
67 - {
68 - plugins: ["babel-plugin-fbt", "babel-plugin-fbt-runtime"],
69 - },
70 - ];
71 - presets.push(
72 - language === "typescript"
73 - ? [
74 - "@babel/preset-typescript",
75 - {
76 - /**
77 - * onlyRemoveTypeImports needs to be set as fbt imports
78 - * would otherwise be removed by this pass.
79 - * https://github.com/facebook/fbt/issues/49
80 - * https://github.com/facebook/sfbt/issues/72
81 - * https://dev.to/retyui/how-to-add-support-typescript-for-fbt-an-internationalization-framework-3lo0
82 - */
83 - onlyRemoveTypeImports: true,
84 - },
85 - ]
86 - : "@babel/preset-flow"
87 - );
88 -
89 - if (transformJSX) {
90 - presets.push({
91 - plugins: ["@babel/plugin-syntax-jsx"],
92 - });
93 - }
94 - presets.push(
95 - ["@babel/preset-react", { throwIfNamespace: false }],
96 - {
97 - plugins: ["@babel/plugin-transform-modules-commonjs"],
98 - },
99 - {
100 - plugins: [
101 - function BabelPluginRewriteRequirePath() {
102 - return {
103 - visitor: {
104 - CallExpression(path: NodePath<t.CallExpression>) {
105 - const { callee } = path.node;
106 - if (callee.type === "Identifier" && callee.name === "require") {
107 - const arg = path.node.arguments[0];
108 - if (arg.type === "StringLiteral") {
109 - // rewrite to use relative import
110 - if (arg.value === "shared-runtime") {
111 - arg.value = "./shared-runtime";
112 - } else if (arg.value === "ReactForgetFeatureFlag") {
113 - arg.value = "./ReactForgetFeatureFlag";
114 - }
115 - }
116 - }
117 - },
118 - },
119 - };
120 - },
121 - ],
122 - }
123 - );
124 - const transformResult = transformFromAstSync(ast, sourceCode, {
125 - presets,
126 - filename: filename,
127 - });
128 -
129 - const code = transformResult?.code;
130 - if (code == null) {
131 - throw new Error(
132 - `Expected custom transform to codegen successfully, got: ${transformResult}`
133 - );
134 - }
135 - return code;
136 -}
137 -
138 -function transformFixtureForget(
139 - input: string,
140 - filename: string
141 -): TransformResult {
142 - try {
143 - const language = parseLanguage(input.split("\n", 1)[0]);
144 -
145 - const forgetResult = transformFixtureInput(
146 - input,
147 - filename,
148 - runReactForgetBabelPlugin,
149 - parseConfigPragma,
150 - true
151 - );
152 -
153 - if (forgetResult.ast == null) {
154 - return {
155 - type: "UnexpectedError",
156 - value: "Unexpected - no babel ast",
157 - };
158 - }
159 -
160 - if (forgetResult.code == null) {
161 - return {
162 - type: "UnexpectedError",
163 - value: "Unexpected - no code emitted",
164 - };
165 - }
166 -
167 - const code = transformAST(
168 - forgetResult.ast,
169 - forgetResult.code,
170 - filename,
171 - language,
172 - false
173 - );
174 - return {
175 - type: "Ok",
176 - value: code,
177 - };
178 - } catch (e) {
179 - return {
180 - type: "UnexpectedError",
181 - value: "Error in Forget transform pipeline: " + e.message,
182 - };
183 - }
184 -}
185 -
186 -function transformFixtureNoForget(
187 - input: string,
188 - filename: string
189 -): TransformResult {
190 - try {
191 - const language = parseLanguage(input.split("\n", 1)[0]);
192 - const ast = parser.parse(input, {
193 - sourceFilename: filename,
194 - plugins: ["jsx", language],
195 - sourceType: "module",
196 - });
197 -
198 - const code = transformAST(ast, input, filename, language, true);
199 - return {
200 - type: "Ok",
201 - value: code,
202 - };
203 - } catch (e) {
204 - return {
205 - type: "UnexpectedError",
206 - value: "Error in non-Forget transform pipeline: " + e.message,
207 - };
208 - }
209 -}
210 -
211 -export async function run(fixture: TestFixture): Promise<SproutFixtureResult> {
212 - const seenConsoleErrors: Array<string> = [];
213 - console.error = (...messages: Array<string>) => {
214 - seenConsoleErrors.push(...messages);
215 - };
216 - const { input, inputPath, snapshot, snapshotPath } = fixture;
217 - if (input == null) {
218 - return {
219 - nonForgetResult: null,
220 - forgetResult: null,
221 - unexpectedError: "No input for fixture " + fixture.snapshotPath,
222 - snapshot,
223 - snapshotPath,
224 - };
225 - }
226 - // We need to include the file extension as it determines typescript
227 - // babel plugin's mode (e.g. stripping types, parsing rules for brackets)
228 - const filename = path.basename(inputPath);
229 - const forgetCode = transformFixtureForget(input, filename);
230 - const noForgetCode = transformFixtureNoForget(input, filename);
231 - if (forgetCode.type === "UnexpectedError") {
232 - return {
233 - nonForgetResult: null,
234 - forgetResult: null,
235 - unexpectedError: forgetCode.value,
236 - snapshot,
237 - snapshotPath,
238 - };
239 - }
240 - if (noForgetCode.type === "UnexpectedError") {
241 - return {
242 - nonForgetResult: null,
243 - forgetResult: null,
244 - unexpectedError: noForgetCode.value,
245 - snapshot,
246 - snapshotPath,
247 - };
248 - }
249 - const nonForgetResult = doEval(noForgetCode.value);
250 - const forgetResult = doEval(forgetCode.value);
251 - return {
252 - nonForgetResult,
253 - forgetResult,
254 - unexpectedError: null,
255 - snapshot,
256 - snapshotPath,
257 - };
258 -}
compiler/packages/sprout/src/runner.ts deleted
-187
@@ -1,187 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and 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 {
9 - FILTER_FILENAME,
10 - TestResult,
11 - UpdateSnapshotKind,
12 - getFixtures,
13 - getUpdatedSnapshot,
14 - isExpectError,
15 - readTestFilter,
16 - report,
17 - update,
18 -} from "fixture-test-utils";
19 -import { Worker } from "jest-worker";
20 -import process from "process";
21 -import * as readline from "readline";
22 -import yargs from "yargs";
23 -import { hideBin } from "yargs/helpers";
24 -import SproutTodoFilter from "./SproutTodoFilter";
25 -import { EvaluatorResult } from "./runner-evaluator";
26 -import type { SproutFixtureResult } from "./runner-worker";
27 -import * as RunnerWorker from "./runner-worker";
28 -
29 -const WORKER_PATH = require.resolve("./runner-worker");
30 -readline.emitKeypressEvents(process.stdin);
31 -
32 -process.stdin.on("keypress", function (_, key) {
33 - if (key && key.name === "c" && key.ctrl) {
34 - process.exit(1);
35 - }
36 -});
37 -process.on("SIGINT", function () {
38 - // Parent process may send SIGINT
39 - process.exit(1);
40 -});
41 -
42 -process.on("SIGTERM", function () {
43 - process.exit(1);
44 -});
45 -
46 -type RunnerOptions = {
47 - sync: boolean;
48 - mode: "update" | "filter" | undefined;
49 -};
50 -
51 -const opts: RunnerOptions = yargs
52 - .boolean("sync")
53 - .describe(
54 - "sync",
55 - "Run compiler in main thread (instead of using worker threads)."
56 - )
57 - .default("sync", false)
58 - .option("mode", {
59 - type: "string",
60 - desc:
61 - "Sprout tester modes:\n" +
62 - " [default] - test all test fixtures\n" +
63 - ` filter - test filtered fixtures ("${FILTER_FILENAME}")\n` +
64 - " update - update all test fixtures)\n",
65 - choices: ["update", "filter", undefined],
66 - default: undefined,
67 - })
68 - .help("help")
69 - .strict()
70 - .parseSync(hideBin(process.argv));
71 -
72 -function logsEqual(a: Array<string>, b: Array<string>) {
73 - if (a.length !== b.length) {
74 - return false;
75 - }
76 - return a.every((val, idx) => val === b[idx]);
77 -}
78 -function stringify(result: EvaluatorResult): string {
79 - return `(kind: ${result.kind}) ${result.value}${
80 - result.logs.length > 0 ? `\nlogs: [${result.logs.toString()}]` : ""
81 - }`;
82 -}
83 -
84 -function transformResult(result: SproutFixtureResult): TestResult {
85 - function makeError(description: string, value: string) {
86 - return {
87 - outputPath: result.snapshotPath,
88 - actual: null,
89 - expected: null,
90 - unexpectedError: `${description}\n${value}`,
91 - };
92 - }
93 - if (result.unexpectedError !== null) {
94 - return makeError("UnexpectedError in runner", result.unexpectedError);
95 - }
96 - const { forgetResult, nonForgetResult } = result;
97 - if (forgetResult.kind === "UnexpectedError") {
98 - return makeError("UnexpectedError in Forget runner", forgetResult.value);
99 - } else if (nonForgetResult.kind === "UnexpectedError") {
100 - return makeError(
101 - "UnexpectedError in non-forget runner",
102 - nonForgetResult.value
103 - );
104 - } else if (
105 - forgetResult.kind !== nonForgetResult.kind ||
106 - forgetResult.value !== nonForgetResult.value ||
107 - !logsEqual(forgetResult.logs, nonForgetResult.logs)
108 - ) {
109 - return makeError(
110 - "Found differences in evaluator results",
111 - `Non-forget (expected):
112 -${stringify(nonForgetResult)}
113 -Forget:
114 -${stringify(forgetResult)}
115 -`
116 - );
117 - } else {
118 - return {
119 - outputPath: result.snapshotPath,
120 - expected: result.snapshot,
121 - actual: getUpdatedSnapshot(
122 - result.snapshot,
123 - stringify(forgetResult),
124 - UpdateSnapshotKind.Sprout
125 - ),
126 - unexpectedError: null,
127 - };
128 - }
129 -}
130 -
131 -/**
132 - * Runs the compiler in watch or single-execution mode
133 - */
134 -export async function main(opts: RunnerOptions): Promise<void> {
135 - const worker: Worker & typeof RunnerWorker = new Worker(WORKER_PATH, {
136 - enableWorkerThreads: true,
137 - }) as any;
138 - worker.getStderr().pipe(process.stderr);
139 - worker.getStdout().pipe(process.stdout);
140 -
141 - const testFilter = opts.mode === "filter" ? await readTestFilter() : null;
142 - const allFixtures = await getFixtures(testFilter);
143 -
144 - const validFixtures = new Map(
145 - Array.from(allFixtures.entries()).filter(([fixtureName, fixture]) => {
146 - return !SproutTodoFilter.has(fixtureName) && !isExpectError(fixture);
147 - })
148 - );
149 - const sproutResults: Array<[string, SproutFixtureResult]> = [];
150 - if (!opts.sync) {
151 - const work: Array<Promise<[string, SproutFixtureResult]>> = [];
152 - for (const [fixtureName, fixture] of validFixtures) {
153 - work.push(worker.run(fixture).then((result) => [fixtureName, result]));
154 - }
155 - sproutResults.push(...(await Promise.all(work)));
156 - } else {
157 - for (const [fixtureName, fixture] of validFixtures) {
158 - const result: [string, SproutFixtureResult] = await RunnerWorker.run(
159 - fixture
160 - ).then((result) => [fixtureName, result]);
161 - sproutResults.push(result);
162 - }
163 - }
164 -
165 - const results = new Map(
166 - sproutResults.map(([fixtureName, result]) => [
167 - fixtureName,
168 - transformResult(result),
169 - ])
170 - );
171 - let isSuccess;
172 - if (opts.mode === "update") {
173 - isSuccess = true;
174 - update(results);
175 - } else {
176 - isSuccess = report(results);
177 - }
178 - /**
179 - * This is important, as we're using jsdom (which seems to be attaching some
180 - * tasks that require force exiting. If we do not await workers terminating,
181 - * we may miss some console logs from jest workers.
182 - */
183 - await worker.end();
184 - process.exit(isSuccess ? 0 : 1);
185 -}
186 -
187 -main(opts).catch((error) => console.error(error));
compiler/packages/sprout/tsconfig.json deleted
-29
@@ -1,29 +0,0 @@
1 -{
2 - "extends": "@tsconfig/node18-strictest/tsconfig.json",
3 - "compilerOptions": {
4 - "declaration": true,
5 - "outDir": "dist",
6 - "jsx": "react-jsxdev",
7 -
8 - // weaken strictness from preset
9 - "importsNotUsedAsValues": "remove",
10 - "noUncheckedIndexedAccess": false,
11 - "noUnusedParameters": false,
12 - "useUnknownInCatchVariables": false,
13 - "target": "ES2015",
14 - // ideally turn off only during dev, or on a per-file basis
15 - "noUnusedLocals": false,
16 - "sourceMap": true,
17 - "baseUrl": "."
18 - },
19 - "exclude": ["node_modules"],
20 - "include": ["src/**/*.ts"],
21 - "references": [
22 - {
23 - "path": "../fixture-test-utils"
24 - },
25 - {
26 - "path": "../babel-plugin-react-forget"
27 - }
28 - ]
29 -}
compiler/yarn.lock
+12
@@ -6415,6 +6415,11 @@ hermes-estree@0.19.1:
6415 resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.19.1.tgz#d5924f5fac2bf0532547ae9f506d6db8f3c96392"
6416 integrity sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==
6417
6418 +hermes-estree@0.19.2:
6419 + version "0.19.2"
6420 + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.19.2.tgz#b35f59c7fa184ac63a049a9ad37b370c5151bd20"
6421 + integrity sha512-dMDmDgaW9LXQxGOQ+gsAZUlXiPXkHvUrpKESCPhkm3pH0SjHy5uaBaT8psxmPol1EBh8GVM2lGC/W0bhuW/fpQ==
6422 +
6423 hermes-parser@0.14.0:
6424 version "0.14.0"
6425 resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.14.0.tgz#edb2e7172fce996d2c8bbba250d140b70cc1aaaf"
@@ -6443,6 +6448,13 @@ hermes-parser@^0.19.1:
6448 dependencies:
6449 hermes-estree "0.19.1"
6450
6451 +hermes-parser@^0.19.1:
6452 + version "0.19.2"
6453 + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.19.2.tgz#67b0fd92f4d7a374234f58c4f980f816734a7695"
6454 + integrity sha512-FxPcupAYzTks42tx8c29UGW59BbZQ67aKKS4jFi0eiD8Q3US3O8raxfNx0yi0ha0dTCq1WXK2MlJv+RCjkVp3Q==
6455 + dependencies:
6456 + hermes-estree "0.19.2"
6457 +
6458 hmac-drbg@^1.0.1:
6459 version "1.0.1"
6460 resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"