@samitouri / QOS-React-1 / commits / 579cc2a44c

[playground] Add support for "use no memo" (#31561)

Fixes #31331 ## Summary There is a bug in playground(https://github.com/facebook/react/issues/31331) which doesnt support 'use memo' or 'use no memo' directives. Its misleading while debugging components in the playground ## How did you test this change? Ran test cases and added a few extra test cases as well ## Changes 1) Adds support for 'use memo' and 'use no memo' 2) Cleanup E2E test cases a bit 3) Adds test cases for use memo 4) Added documentation to run test cases ## Implementation `parseFunctions` returns a set of functions to be compiled. But, it doesnt filter out/handle memoized opted/un-opted functions using directives. ive just created a `compile` flag to enable/disable compiling [here](https://github.com/facebook/react/pull/31561/files#diff-305de47a3fe3ce778e22d5c5cf438419a59de8e7f785b45f659e7b41b1e30b03R113) Then I am just skipping those functions from getting compile [here](https://github.com/facebook/react/pull/31561/files#diff-305de47a3fe3ce778e22d5c5cf438419a59de8e7f785b45f659e7b41b1e30b03R253)

Aditya Subramanyam committed Nov 19, 2024 at 02:08 UTC 579cc2a44c17f848bb3ad3b82121c87eb415935a
15 files changed +494 -35
compiler/apps/playground/README.md
+7
@@ -26,6 +26,13 @@ $ npm run dev
26 $ yarn
27 ```
28
29 +## Testing
30 +```sh
31 +# Install playwright browser binaries
32 +$ npx playwright install --with-deps
33 +# Run tests
34 +$ yarn test
35 +```
36 ## Deployment
37
38 This project has been deployed using Vercel. Vercel does the exact same thing as we would
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/01-user-output.txt renamed
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/02-default-output.txt renamed
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-no-directive-arrow-function-expression-output.txt new
+20
@@ -0,0 +1,20 @@
1 +function anonymous_1() {
2 +  "use no memo";
3 +  const Avatar = () => {
4 +    return <div>Avatar Content</div>;
5 +  };
6 +  const MemoizedAvatar = React.memo(Avatar);
7 +  const Bio = () => {
8 +    const handleBioUpdate = () => {
9 +      console.log("Bio updated");
10 +    };
11 +    return <div onClick={handleBioUpdate}>Bio Content</div>;
12 +  };
13 +  const MemoizedBio = React.memo(Bio);
14 +  return (
15 +    <div>
16 +      <MemoizedAvatar />
17 +      <MemoizedBio />
18 +    </div>
19 +  );
20 +}
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-use-memo-arrow-function-expression-output.txt new
+32
@@ -0,0 +1,32 @@
1 +function anonymous_1() {
2 +  "use memo";
3 +  const $ = _c(3);
4 +  const Chart = _temp2;
5 +  let t0;
6 +  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
7 +    t0 = React.memo(Chart);
8 +    $[0] = t0;
9 +  } else {
10 +    t0 = $[0];
11 +  }
12 +  const MemoizedChart = t0;
13 +  const Graph = _temp3;
14 +  let t1;
15 +  if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
16 +    t1 = React.memo(Graph);
17 +    $[1] = t1;
18 +  } else {
19 +    t1 = $[1];
20 +  }
21 +  const MemoizedGraph = t1;
22 +  let t2;
23 +  if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
24 +    t2 = (
25 +      <div>
26 +        <MemoizedChart />
27 +        <MemoizedGraph />
28 +      </div>
29 +    );
30 +    $[2] = t2;
31 +  } else {
32 +    t2 = $[2];
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-use-memo-function-declaration-output.txt new
+32
@@ -0,0 +1,32 @@
1 +function App() {
2 +  "use memo";
3 +  const $ = _c(3);
4 +  let t0;
5 +  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
6 +    const Sidebar = function Sidebar() {
7 +      const handleToggle = _temp;
8 +      return <aside onClick={handleToggle}>Sidebar Content</
9 +          aside>;
10 +    };
11 +    t0 = React.memo(Sidebar);
12 +    $[0] = t0;
13 +  } else {
14 +    t0 = $[0];
15 +  }
16 +  const MemoizedSidebar = t0;
17 +  let t1;
18 +  if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
19 +    const Content = function Content() {
20 +      return <main>Main Content</main>;
21 +    };
22 +    t1 = React.memo(Content);
23 +    $[1] = t1;
24 +  } else {
25 +    t1 = $[1];
26 +  }
27 +  const MemoizedContent = t1;
28 +  let t2;
29 +  if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
30 +    t2 = (
31 +      <div>
32 +        <MemoizedSidebar />
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-use-no-memo-function-declaration-output.txt new
+32
@@ -0,0 +1,32 @@
1 +function Settings() {
2 +  "use memo";
3 +  const $ = _c(3);
4 +  let t0;
5 +  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
6 +    t0 = function Preferences() {
7 +      const handleSave = _temp;
8 +      return <div onClick={handleSave}>Preferences Content</
9 +          div>;
10 +    };
11 +    $[0] = t0;
12 +  } else {
13 +    t0 = $[0];
14 +  }
15 +  const Preferences = t0;
16 +  let t1;
17 +  if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
18 +    t1 = function Notifications() {
19 +      return <div>Notifications Settings</div>;
20 +    };
21 +    $[1] = t1;
22 +  } else {
23 +    t1 = $[1];
24 +  }
25 +  const Notifications = t1;
26 +  let t2;
27 +  if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
28 +    t2 = (
29 +      <div>
30 +        <Preferences />
31 +        <Notifications />
32 +      </div>
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-use-no-memo-function-expression-output.txt new
+18
@@ -0,0 +1,18 @@
1 +function anonymous_1() {
2 +  "use no memo";
3 +  const Widget = function () {
4 +    const handleExpand = () => {
5 +      console.log("Widget expanded");
6 +    };
7 +    return <div onClick={handleExpand}>Widget Content</div>;
8 +  };
9 +  const Panel = function () {
10 +    return <section>Panel Information</section>;
11 +  };
12 +  return (
13 +    <div>
14 +      <Widget />
15 +      <Panel />
16 +    </div>
17 +  );
18 +}
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-memo-output.txt new
+15
@@ -0,0 +1,15 @@
1 +function anonymous_1() {
2 +  const $ = _c(1);
3 +  const handleClick = _temp;
4 +  let t0;
5 +  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
6 +    t0 = <h1 onClick={handleClick}>Welcome to the App!</h1>;
7 +    $[0] = t0;
8 +  } else {
9 +    t0 = $[0];
10 +  }
11 +  return t0;
12 +}
13 +function _temp() {
14 +  console.log("Header clicked");
15 +}
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-no-memo-function-expression-output.txt new
+3
@@ -0,0 +1,3 @@
1 +function anonymous_1() {
2 +  return <aside>Sidebar Information</aside>;
3 +}
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-no-memo-output.txt new
+7
@@ -0,0 +1,7 @@
1 +function anonymous_1() {
2 +  const handleMouseOver = () => {
3 +    console.log("Footer hovered");
4 +  };
5 +  return <footer onMouseOver={handleMouseOver}>Footer 
6 +      Information</footer>;
7 +}
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/page.spec.ts
+217 -14
@@ -8,42 +8,245 @@
8 import {expect, test} from '@playwright/test';
9 import {encodeStore, type Store} from '../../lib/stores';
10
11 -const STORE: Store = {
12 - source: `export default function TestComponent({ x }) {
13 - return <Button>{x}</Button>;
14 -}
15 -`,
16 -};
17 -const HASH = encodeStore(STORE);
11 +test.describe.configure({mode: 'parallel'});
12
13 function concat(data: Array<string>): string {
14 return data.join('');
15 }
16 +const DIRECTIVE_TEST_CASES = [
17 + {
18 + name: 'module-scope-use-memo',
19 + input: `'use memo';
20 +
21 +const Header = () => {
22 + const handleClick = () => {
23 + console.log('Header clicked');
24 + };
25 +
26 + return <h1 onClick={handleClick}>Welcome to the App!</h1>;
27 +};`,
28 + },
29 + {
30 + name: 'module-scope-use-no-memo',
31 + input: `'use no memo';
32 +
33 +const Footer = () => {
34 + const handleMouseOver = () => {
35 + console.log('Footer hovered');
36 + };
37 +
38 + return <footer onMouseOver={handleMouseOver}>Footer Information</footer>;
39 +};
40 +`,
41 + },
42 + {
43 + name: 'function-scope-use-memo-function-declaration',
44 + input: `function App() {
45 + 'use memo';
46 +
47 + function Sidebar() {
48 + const handleToggle = () => {
49 + console.log('Sidebar toggled');
50 + };
51 +
52 + return <aside onClick={handleToggle}>Sidebar Content</aside>;
53 + }
54 +
55 + const MemoizedSidebar = React.memo(Sidebar);
56 +
57 + function Content() {
58 + return <main>Main Content</main>;
59 + }
60 +
61 + const MemoizedContent = React.memo(Content);
62 +
63 + return (
64 + <div>
65 + <MemoizedSidebar />
66 + <MemoizedContent />
67 + </div>
68 + );
69 +}`,
70 + },
71 + {
72 + name: 'function-scope-use-no-memo-function-expression',
73 + input: `const Dashboard = function() {
74 + 'use no memo';
75 + const Widget = function() {
76 + const handleExpand = () => {
77 + console.log('Widget expanded');
78 + };
79 +
80 + return <div onClick={handleExpand}>Widget Content</div>;
81 + };
82 +
83 + const Panel = function() {
84 + return <section>Panel Information</section>;
85 + };
86 +
87 + return (
88 + <div>
89 + <Widget />
90 + <Panel />
91 + </div>
92 + );
93 +};`,
94 + },
95 + {
96 + name: 'function-scope-use-memo-arrow-function-expression',
97 + input: `const Analytics = () => {
98 + 'use memo';
99 +
100 + const Chart = () => {
101 + const handleRefresh = () => {
102 + console.log('Chart refreshed');
103 + };
104
23 -test('editor should compile successfully', async ({page}) => {
24 - await page.goto(`/#${HASH}`, {waitUntil: 'networkidle'});
105 + return <div onClick={handleRefresh}>Chart Content</div>;
106 + };
107 +
108 + const MemoizedChart = React.memo(Chart);
109 +
110 + const Graph = () => {
111 + return <div>Graph Content</div>;
112 + };
113 +
114 + const MemoizedGraph = React.memo(Graph);
115 +
116 + return (
117 + <div>
118 + <MemoizedChart />
119 + <MemoizedGraph />
120 + </div>
121 + );
122 +};`,
123 + },
124 + {
125 + name: 'module-scope-use-no-memo-function-expression',
126 + input: `'use no memo';
127 +
128 +const Sidebar = function() {
129 + return <aside>Sidebar Information</aside>;
130 +};`,
131 + },
132 + {
133 + name: 'function-scope-no-directive-arrow-function-expression',
134 + input: `
135 +const Profile = () => {
136 +'use no memo';
137 + const Avatar = () => {
138 + return <div>Avatar Content</div>;
139 + };
140 +
141 + const MemoizedAvatar = React.memo(Avatar);
142 +
143 + const Bio = () => {
144 + const handleBioUpdate = () => {
145 + console.log('Bio updated');
146 + };
147 +
148 + return <div onClick={handleBioUpdate}>Bio Content</div>;
149 + };
150 +
151 + const MemoizedBio = React.memo(Bio);
152 +
153 + return (
154 + <div>
155 + <MemoizedAvatar />
156 + <MemoizedBio />
157 + </div>
158 + );
159 +};`,
160 + },
161 + {
162 + name: 'function-scope-use-no-memo-function-declaration',
163 + input: `'use no memo';
164 +
165 +function Settings() {
166 + 'use memo';
167 +
168 + function Preferences() {
169 + const handleSave = () => {
170 + console.log('Preferences saved');
171 + };
172 +
173 + return <div onClick={handleSave}>Preferences Content</div>;
174 + }
175 +
176 + function Notifications() {
177 + return <div>Notifications Settings</div>;
178 + }
179 +
180 + return (
181 + <div>
182 + <Preferences />
183 + <Notifications />
184 + </div>
185 + );
186 +}`,
187 + },
188 +];
189 +test('editor should open successfully', async ({page}) => {
190 + await page.goto(`/`, {waitUntil: 'networkidle'});
191 await page.screenshot({
192 fullPage: true,
27 - path: 'test-results/00-on-networkidle.png',
193 + path: 'test-results/00-fresh-page.png',
194 });
195 +});
196 +test('editor should compile from hash successfully', async ({page}) => {
197 + const store: Store = {
198 + source: `export default function TestComponent({ x }) {
199 + return <Button>{x}</Button>;
200 + }
201 + `,
202 + };
203 + const hash = encodeStore(store);
204 + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
205
206 // User input from hash compiles
207 await page.screenshot({
208 fullPage: true,
33 - path: 'test-results/01-show-js-before.png',
209 + path: 'test-results/01-compiles-from-hash.png',
210 });
211 const userInput =
212 (await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
37 - expect(concat(userInput)).toMatchSnapshot('user-input.txt');
213 + expect(concat(userInput)).toMatchSnapshot('01-user-output.txt');
214 +});
215 +test('reset button works', async ({page}) => {
216 + const store: Store = {
217 + source: `export default function TestComponent({ x }) {
218 + return <Button>{x}</Button>;
219 + }
220 + `,
221 + };
222 + const hash = encodeStore(store);
223 + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
224
225 // Reset button works
226 page.on('dialog', dialog => dialog.accept());
227 await page.getByRole('button', {name: 'Reset'}).click();
228 await page.screenshot({
229 fullPage: true,
44 - path: 'test-results/02-show-js-after.png',
230 + path: 'test-results/02-reset-button-works.png',
231 });
232 const defaultInput =
233 (await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
48 - expect(concat(defaultInput)).toMatchSnapshot('default-input.txt');
234 + expect(concat(defaultInput)).toMatchSnapshot('02-default-output.txt');
235 });
236 +DIRECTIVE_TEST_CASES.forEach((t, idx) =>
237 + test(`directives work: ${t.name}`, async ({page}) => {
238 + const store: Store = {
239 + source: t.input,
240 + };
241 + const hash = encodeStore(store);
242 + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
243 + await page.screenshot({
244 + fullPage: true,
245 + path: `test-results/03-0${idx}-${t.name}.png`,
246 + });
247 +
248 + const useMemoOutput =
249 + (await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
250 + expect(concat(useMemoOutput)).toMatchSnapshot(`${t.name}-output.txt`);
251 + }),
252 +);
compiler/apps/playground/components/Editor/EditorImpl.tsx
+105 -18
@@ -18,6 +18,8 @@ import {
18 ValueKind,
19 runPlayground,
20 type Hook,
21 + findDirectiveDisablingMemoization,
22 + findDirectiveEnablingMemoization,
23 } from 'babel-plugin-react-compiler/src';
24 import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
25 import clsx from 'clsx';
@@ -43,6 +45,25 @@ import {
45 import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
46 import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
47
48 +type FunctionLike =
49 + | NodePath<t.FunctionDeclaration>
50 + | NodePath<t.ArrowFunctionExpression>
51 + | NodePath<t.FunctionExpression>;
52 +enum MemoizeDirectiveState {
53 + Enabled = 'Enabled',
54 + Disabled = 'Disabled',
55 + Undefined = 'Undefined',
56 +}
57 +
58 +const MEMOIZE_ENABLED_OR_UNDEFINED_STATES = new Set([
59 + MemoizeDirectiveState.Enabled,
60 + MemoizeDirectiveState.Undefined,
61 +]);
62 +
63 +const MEMOIZE_ENABLED_OR_DISABLED_STATES = new Set([
64 + MemoizeDirectiveState.Enabled,
65 + MemoizeDirectiveState.Disabled,
66 +]);
67 function parseInput(input: string, language: 'flow' | 'typescript'): any {
68 // Extract the first line to quickly check for custom test directives
69 if (language === 'flow') {
@@ -63,29 +84,36 @@ function parseInput(input: string, language: 'flow' | 'typescript'): any {
84 function parseFunctions(
85 source: string,
86 language: 'flow' | 'typescript',
66 -): Array<
67 - | NodePath<t.FunctionDeclaration>
68 - | NodePath<t.ArrowFunctionExpression>
69 - | NodePath<t.FunctionExpression>
70 -> {
71 - const items: Array<
72 - | NodePath<t.FunctionDeclaration>
73 - | NodePath<t.ArrowFunctionExpression>
74 - | NodePath<t.FunctionExpression>
75 - > = [];
87 +): Array<{
88 + compilationEnabled: boolean;
89 + fn: FunctionLike;
90 +}> {
91 + const items: Array<{
92 + compilationEnabled: boolean;
93 + fn: FunctionLike;
94 + }> = [];
95 try {
96 const ast = parseInput(source, language);
97 traverse(ast, {
98 FunctionDeclaration(nodePath) {
80 - items.push(nodePath);
99 + items.push({
100 + compilationEnabled: shouldCompile(nodePath),
101 + fn: nodePath,
102 + });
103 nodePath.skip();
104 },
105 ArrowFunctionExpression(nodePath) {
84 - items.push(nodePath);
106 + items.push({
107 + compilationEnabled: shouldCompile(nodePath),
108 + fn: nodePath,
109 + });
110 nodePath.skip();
111 },
112 FunctionExpression(nodePath) {
88 - items.push(nodePath);
113 + items.push({
114 + compilationEnabled: shouldCompile(nodePath),
115 + fn: nodePath,
116 + });
117 nodePath.skip();
118 },
119 });
@@ -98,9 +126,48 @@ function parseFunctions(
126 suggestions: null,
127 });
128 }
129 +
130 return items;
131 }
132
133 +function shouldCompile(fn: FunctionLike): boolean {
134 + const {body} = fn.node;
135 + if (t.isBlockStatement(body)) {
136 + const selfCheck = checkExplicitMemoizeDirectives(body.directives);
137 + if (selfCheck === MemoizeDirectiveState.Enabled) return true;
138 + if (selfCheck === MemoizeDirectiveState.Disabled) return false;
139 +
140 + const parentWithDirective = fn.findParent(parentPath => {
141 + if (parentPath.isBlockStatement() || parentPath.isProgram()) {
142 + const directiveCheck = checkExplicitMemoizeDirectives(
143 + parentPath.node.directives,
144 + );
145 + return MEMOIZE_ENABLED_OR_DISABLED_STATES.has(directiveCheck);
146 + }
147 + return false;
148 + });
149 +
150 + if (!parentWithDirective) return true;
151 + const parentDirectiveCheck = checkExplicitMemoizeDirectives(
152 + (parentWithDirective.node as t.Program | t.BlockStatement).directives,
153 + );
154 + return MEMOIZE_ENABLED_OR_UNDEFINED_STATES.has(parentDirectiveCheck);
155 + }
156 + return false;
157 +}
158 +
159 +function checkExplicitMemoizeDirectives(
160 + directives: Array<t.Directive>,
161 +): MemoizeDirectiveState {
162 + if (findDirectiveEnablingMemoization(directives).length) {
163 + return MemoizeDirectiveState.Enabled;
164 + }
165 + if (findDirectiveDisablingMemoization(directives).length) {
166 + return MemoizeDirectiveState.Disabled;
167 + }
168 + return MemoizeDirectiveState.Undefined;
169 +}
170 +
171 const COMMON_HOOKS: Array<[string, Hook]> = [
172 [
173 'useFragment',
@@ -209,18 +276,38 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
276 // Extract the first line to quickly check for custom test directives
277 const pragma = source.substring(0, source.indexOf('\n'));
278 const config = parseConfigPragmaForTests(pragma);
212 -
213 - for (const fn of parseFunctions(source, language)) {
214 - const id = withIdentifier(getFunctionIdentifier(fn));
279 + const parsedFunctions = parseFunctions(source, language);
280 + for (const func of parsedFunctions) {
281 + const id = withIdentifier(getFunctionIdentifier(func.fn));
282 + const fnName = id.name;
283 + if (!func.compilationEnabled) {
284 + upsert({
285 + kind: 'ast',
286 + fnName,
287 + name: 'CodeGen',
288 + value: {
289 + type: 'FunctionDeclaration',
290 + id:
291 + func.fn.isArrowFunctionExpression() ||
292 + func.fn.isFunctionExpression()
293 + ? withIdentifier(null)
294 + : func.fn.node.id,
295 + async: func.fn.node.async,
296 + generator: !!func.fn.node.generator,
297 + body: func.fn.node.body as t.BlockStatement,
298 + params: func.fn.node.params,
299 + },
300 + });
301 + continue;
302 + }
303 for (const result of runPlayground(
216 - fn,
304 + func.fn,
305 {
306 ...config,
307 customHooks: new Map([...COMMON_HOOKS]),
308 },
309 getReactFunctionType(id),
310 )) {
223 - const fnName = id.name;
311 switch (result.kind) {
312 case 'ast': {
313 upsert({
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+3 -3
@@ -42,10 +42,10 @@ export type CompilerPass = {
42 comments: Array<t.CommentBlock | t.CommentLine>;
43 code: string | null;
44 };
45 -const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
45 +export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
46 export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
47
48 -function findDirectiveEnablingMemoization(
48 +export function findDirectiveEnablingMemoization(
49 directives: Array<t.Directive>,
50 ): Array<t.Directive> {
51 return directives.filter(directive =>
@@ -53,7 +53,7 @@ function findDirectiveEnablingMemoization(
53 );
54 }
55
56 -function findDirectiveDisablingMemoization(
56 +export function findDirectiveDisablingMemoization(
57 directives: Array<t.Directive>,
58 ): Array<t.Directive> {
59 return directives.filter(directive =>
compiler/packages/babel-plugin-react-compiler/src/index.ts
+3
@@ -20,6 +20,9 @@ export {
20 run,
21 runPlayground,
22 OPT_OUT_DIRECTIVES,
23 + OPT_IN_DIRECTIVES,
24 + findDirectiveEnablingMemoization,
25 + findDirectiveDisablingMemoization,
26 type CompilerPipelineValue,
27 type PluginOptions,
28 } from './Entrypoint';