@samitouri / QOS-React-2 / commits / fa07eb0b3b

[be] make evaluator and worker easier to debug in sprout

--- jsdom and other libraries seem to cause jest workers to exit with `forceExit:true`. Not sure what option I set (or global I've overwritten) but console logs aren't flushed as a result, making debugging a bit confusing. This PR: - Moves some code from `eval(...)` to a typechecked real js function. I always had trouble debugging the `eval`ed code, so smaller code snippet is better here. - waits for jest workers to end before exiting

Mofei Zhang committed Nov 16, 2023 at 18:12 UTC fa07eb0b3b65189b2446df080adf1b92e6cc157b
7 files changed +76 -75
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-hoisting-variable-collision.expect.md
-2
@@ -11,7 +11,6 @@ function Component(props) {
11 export const FIXTURE_ENTRYPOINT = {
12 fn: Component,
13 params: [{ items: [0, 42, null, undefined, { object: true }] }],
14 - isComponent: "Component",
14 };
15
16 ```
@@ -52,7 +51,6 @@ function Component(props) {
51 export const FIXTURE_ENTRYPOINT = {
52 fn: Component,
53 params: [{ items: [0, 42, null, undefined, { object: true }] }],
55 - isComponent: "Component",
54 };
55
56 ```
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-hoisting-variable-collision.js
-1
@@ -7,5 +7,4 @@ function Component(props) {
7 export const FIXTURE_ENTRYPOINT = {
8 fn: Component,
9 params: [{ items: [0, 42, null, undefined, { object: true }] }],
10 - isComponent: "Component",
10 };
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-hoisting.expect.md
-2
@@ -17,7 +17,6 @@ function Component(props) {
17 export const FIXTURE_ENTRYPOINT = {
18 fn: Component,
19 params: [{ wat: "/dev/null", itemID: 42 }],
20 - isComponent: "Component",
20 };
21
22 ```
@@ -60,7 +59,6 @@ function Component(props) {
59 export const FIXTURE_ENTRYPOINT = {
60 fn: Component,
61 params: [{ wat: "/dev/null", itemID: 42 }],
63 - isComponent: "Component",
62 };
63
64 ```
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-hoisting.js
-1
@@ -13,5 +13,4 @@ function Component(props) {
13 export const FIXTURE_ENTRYPOINT = {
14 fn: Component,
15 params: [{ wat: "/dev/null", itemID: 42 }],
16 - isComponent: "Component",
16 };
compiler/packages/sprout/src/SproutTodoFilter.ts
+7 -1
@@ -509,7 +509,13 @@ const skipFilter = new Set([
509 "rules-of-hooks/todo.invalid.invalid-rules-of-hooks-e69ffce323c3",
510 "todo.unnecessary-lambda-memoization",
511 "rules-of-hooks/rules-of-hooks-93dc5d5e538a",
512 - "rules-of-hooks/rules-of-hooks-69521d94fa03"
512 + "rules-of-hooks/rules-of-hooks-69521d94fa03",
513 +
514 + // TODO: remove
515 + "capture_mutate-across-fns-iife",
516 + "capture-indirect-mutate-alias-iife",
517 + "capturing-function-alias-computed-load-3-iife",
518 + "capturing-function-alias-computed-load-iife",
519 ]);
520
521 export default skipFilter;
compiler/packages/sprout/src/runner-evaluator.ts
+63 -68
@@ -5,14 +5,22 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 +import { render } from "@testing-library/react";
9 import { JSDOM } from "jsdom";
10 import util from "util";
11 +import { z } from "zod";
12 +import { fromZodError } from "zod-validation-error";
13 import { initFbt, toJSON } from "./shared-runtime";
14 const React = require("react");
12 -const render = require("@testing-library/react").render;
15
16 +/**
17 + * Set up the global environment for JSDOM tests.
18 + * This is a hack to let us share code and setup between the test
19 + * and runner environments. As an alternative, we could evaluate all setup
20 + * in the jsdom test environment (which provides more isolation), but that
21 + * may be slower.
22 + */
23 const { window: testWindow } = new JSDOM(undefined);
15 -
24 (globalThis as any).document = testWindow.document;
25 (globalThis as any).window = testWindow.window;
26 (globalThis as any).navigator = testWindow.navigator;
@@ -20,20 +28,29 @@ const { window: testWindow } = new JSDOM(undefined);
28 (globalThis as any).render = render;
29 initFbt();
30
31 +(globalThis as any).placeholderFn = function (..._args: Array<any>) {
32 + throw new Error("Fixture not implemented!");
33 +};
34 +
35 export type EvaluatorResult = {
36 kind: "ok" | "exception" | "UnexpectedError";
37 value: string;
38 logs: Array<string>;
39 };
40
29 -const PLACEHOLDER_VALUE = Symbol();
30 -(globalThis as any).placeholderFn = function (..._args: Array<any>) {
31 - throw PLACEHOLDER_VALUE;
32 -};
33 -(globalThis as any).WrapperTestComponent = function (props: {
34 - fn: any;
35 - params: Array<any>;
36 -}) {
41 +/**
42 + * Define types and schemas for fixture entrypoint
43 + */
44 +const EntrypointSchema = z.strictObject({
45 + fn: z.union([z.function(), z.object({})]),
46 + params: z.array(z.any()),
47 + isComponent: z.optional(z.boolean()),
48 +});
49 +const ExportSchema = z.object({
50 + FIXTURE_ENTRYPOINT: EntrypointSchema,
51 +});
52 +
53 +function WrapperTestComponent(props: { fn: any; params: Array<any> }) {
54 const result = props.fn(...props.params);
55 // Hacky solution to determine whether the fixture returned jsx (which
56 // needs to passed through to React's runtime as-is) or a non-jsx value
@@ -43,24 +60,44 @@ const PLACEHOLDER_VALUE = Symbol();
60 } else {
61 return toJSON(result);
62 }
46 -};
63 +}
64 +type FixtureEvaluatorResult = Omit<EvaluatorResult, "logs">;
65 +(globalThis as any).evaluateFixtureExport = function (
66 + exports: unknown
67 +): FixtureEvaluatorResult {
68 + const parsedExportResult = ExportSchema.safeParse(exports);
69 + if (!parsedExportResult.success) {
70 + const exportDetail =
71 + typeof exports === "object" && exports != null
72 + ? `object ${util.inspect(exports)}`
73 + : `${exports}`;
74 + return {
75 + kind: "UnexpectedError",
76 + value: `${fromZodError(parsedExportResult.error)}\nFound ` + exportDetail,
77 + };
78 + }
79 + const entrypoint = parsedExportResult.data.FIXTURE_ENTRYPOINT;
80 + if (typeof entrypoint.fn === "object") {
81 + // Try to run fixture as a react component. This is necessary because not
82 + // all components are functions (some are ForwardRef or Memo objects).
83 + const result = render(
84 + React.createElement(entrypoint.fn, entrypoint.params[0])
85 + ).container.innerHTML;
86
48 -function validateEntrypoint(entrypoint: object) {
49 - if (!("params" in entrypoint)) {
50 - return "missing `params` property";
51 - } else if (!Array.isArray(entrypoint.params)) {
52 - return "unexpected type for `params` property";
53 - } else if (!(`fn` in entrypoint) || entrypoint == null) {
54 - return "missing `fn` property";
55 - } else if (
56 - typeof entrypoint.fn !== "function" &&
57 - typeof entrypoint.fn !== "object"
58 - ) {
59 - return "expected `fn` property to be a function or React object";
87 + return {
88 + kind: "ok",
89 + value: result ?? "null",
90 + };
91 } else {
61 - return null;
92 + const result = render(React.createElement(WrapperTestComponent, entrypoint))
93 + .container.innerHTML;
94 +
95 + return {
96 + kind: "ok",
97 + value: result ?? "null",
98 + };
99 }
63 -}
100 +};
101
102 export function doEval(source: string): EvaluatorResult {
103 "use strict";
@@ -95,49 +132,7 @@ export function doEval(source: string): EvaluatorResult {
132 // run in an iife to avoid naming collisions
133 (() => {${source}})();
134 reachedInvoke = true;
98 - if (exports.FIXTURE_ENTRYPOINT == null ||
99 - exports.FIXTURE_ENTRYPOINT.fn === globalThis.placeholderFn
100 - ) {
101 - return {
102 - kind: "UnexpectedError",
103 - value: 'FIXTURE_ENTRYPOINT not exported! Found {'
104 - + Object.keys(exports).filter(e => e !== 'FIXTURE_ENTRYPOINT').toString()
105 - + '}',
106 - };
107 - }
108 - const validationError = validateEntrypoint(exports.FIXTURE_ENTRYPOINT);
109 - if (validationError) {
110 - return {
111 - kind: "UnexpectedError",
112 - value: 'Bad shape for FIXTURE_ENTRYPOINT (' + validationError + ').',
113 - };
114 - }
115 -
116 - if (typeof exports.FIXTURE_ENTRYPOINT.fn === 'object') {
117 - // try to run fixture as a react component
118 - const result = render(
119 - React.createElement(
120 - exports.FIXTURE_ENTRYPOINT.fn,
121 - exports.FIXTURE_ENTRYPOINT.params[0])
122 - ).container.innerHTML;
123 -
124 - return {
125 - kind: "ok",
126 - value: result ?? 'null',
127 - };
128 - } else {
129 - const result = render(
130 - React.createElement(
131 - WrapperTestComponent,
132 - exports.FIXTURE_ENTRYPOINT
133 - )
134 - ).container.innerHTML;
135 -
136 - return {
137 - kind: "ok",
138 - value: result ?? 'null',
139 - };
140 - }
135 + return evaluateFixtureExport(exports);
136 } catch (e) {
137 if (!reachedInvoke) {
138 return {
compiler/packages/sprout/src/runner.ts
+6
@@ -216,6 +216,12 @@ export async function main(opts: RunnerOptions): Promise<void> {
216 }
217
218 const isSuccess = reportResults(results, opts.verbose);
219 + /**
220 + * This is important, as we're using jsdom (which seems to be attaching some
221 + * tasks that require force exiting. If we do not await workers terminating,
222 + * we may miss some console logs from jest workers.
223 + */
224 + await worker.end();
225 process.exit(isSuccess ? 0 : 1);
226 }
227