10
import {PluginOptions} from './Options';
11
import {CompilerError} from '../CompilerError';
12
13
+/**
14
+ * Gating rewrite for function declarations which are referenced before their
15
+ * declaration site.
16
+ *
17
+ * ```js
18
+ * // original
19
+ * export default React.memo(Foo);
20
+ * function Foo() { ... }
21
+ *
22
+ * // React compiler optimized + gated
23
+ * import {gating} from 'myGating';
24
+ * export default React.memo(Foo);
25
+ * const gating_result = gating(); <- inserted
26
+ * function Foo_optimized() {} <- inserted
27
+ * function Foo_unoptimized() {} <- renamed from Foo
28
+ * function Foo() { <- inserted function, which can be hoisted by JS engines
29
+ * if (gating_result) return Foo_optimized();
30
+ * else return Foo_unoptimized();
31
+ * }
32
+ * ```
33
+ */
34
+function insertAdditionalFunctionDeclaration(
35
+ fnPath: NodePath<t.FunctionDeclaration>,
36
+ compiled: t.FunctionDeclaration,
37
+ gating: NonNullable<PluginOptions['gating']>,
38
+): void {
39
+ const originalFnName = fnPath.node.id;
40
+ const originalFnParams = fnPath.node.params;
41
+ const compiledParams = fnPath.node.params;
42
+ /**
43
+ * Note that other than `export default function() {}`, all other function
44
+ * declarations must have a binding identifier. Since default exports cannot
45
+ * be referenced, it's safe to assume that all function declarations passed
46
+ * here will have an identifier.
47
+ * https://tc39.es/ecma262/multipage/ecmascript-language-functions-and-classes.html#sec-function-definitions
48
+ */
49
+ CompilerError.invariant(originalFnName != null && compiled.id != null, {
50
+ reason:
51
+ 'Expected function declarations that are referenced elsewhere to have a named identifier',
52
+ loc: fnPath.node.loc ?? null,
53
+ });
54
+ CompilerError.invariant(originalFnParams.length === compiledParams.length, {
55
+ reason:
56
+ 'Expected React Compiler optimized function declarations to have the same number of parameters as source',
57
+ loc: fnPath.node.loc ?? null,
58
+ });
59
+
60
+ const gatingCondition = fnPath.scope.generateUidIdentifier(
61
+ `${gating.importSpecifierName}_result`,
62
+ );
63
+ const unoptimizedFnName = fnPath.scope.generateUidIdentifier(
64
+ `${originalFnName.name}_unoptimized`,
65
+ );
66
+ const optimizedFnName = fnPath.scope.generateUidIdentifier(
67
+ `${originalFnName.name}_optimized`,
68
+ );
69
+ /**
70
+ * Step 1: rename existing functions
71
+ */
72
+ compiled.id.name = optimizedFnName.name;
73
+ fnPath.get('id').replaceInline(unoptimizedFnName);
74
+
75
+ /**
76
+ * Step 2: insert new function declaration
77
+ */
78
+ const newParams: Array<t.Identifier | t.RestElement> = [];
79
+ const genNewArgs: Array<() => t.Identifier | t.SpreadElement> = [];
80
+ for (let i = 0; i < originalFnParams.length; i++) {
81
+ const argName = `arg${i}`;
82
+ if (originalFnParams[i].type === 'RestElement') {
83
+ newParams.push(t.restElement(t.identifier(argName)));
84
+ genNewArgs.push(() => t.spreadElement(t.identifier(argName)));
85
+ } else {
86
+ newParams.push(t.identifier(argName));
87
+ genNewArgs.push(() => t.identifier(argName));
88
+ }
89
+ }
90
+ // insertAfter called in reverse order of how nodes should appear in program
91
+ fnPath.insertAfter(
92
+ t.functionDeclaration(
93
+ originalFnName,
94
+ newParams,
95
+ t.blockStatement([
96
+ t.ifStatement(
97
+ gatingCondition,
98
+ t.returnStatement(
99
+ t.callExpression(
100
+ compiled.id,
101
+ genNewArgs.map(fn => fn()),
102
+ ),
103
+ ),
104
+ t.returnStatement(
105
+ t.callExpression(
106
+ unoptimizedFnName,
107
+ genNewArgs.map(fn => fn()),
108
+ ),
109
+ ),
110
+ ),
111
+ ]),
112
+ ),
113
+ );
114
+ fnPath.insertBefore(
115
+ t.variableDeclaration('const', [
116
+ t.variableDeclarator(
117
+ gatingCondition,
118
+ t.callExpression(t.identifier(gating.importSpecifierName), []),
119
+ ),
120
+ ]),
121
+ );
122
+ fnPath.insertBefore(compiled);
123
+}
124
export function insertGatedFunctionDeclaration(
125
fnPath: NodePath<
126
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
132
gating: NonNullable<PluginOptions['gating']>,
133
referencedBeforeDeclaration: boolean,
134
): void {
24
- if (referencedBeforeDeclaration) {
25
- const identifier =
26
- fnPath.node.type === 'FunctionDeclaration' ? fnPath.node.id : null;
27
- CompilerError.invariant(false, {
28
- reason: `Encountered a function used before its declaration, which breaks Forget's gating codegen due to hoisting`,
29
- description: `Rewrite the reference to ${identifier?.name ?? 'this function'} to not rely on hoisting to fix this issue`,
30
- loc: identifier?.loc ?? null,
31
- suggestions: null,
135
+ if (referencedBeforeDeclaration && fnPath.isFunctionDeclaration()) {
136
+ CompilerError.invariant(compiled.type === 'FunctionDeclaration', {
137
+ reason: 'Expected compiled node type to match input type',
138
+ description: `Got ${compiled.type} but expected FunctionDeclaration`,
139
+ loc: fnPath.node.loc ?? null,
140
});
141
+ insertAdditionalFunctionDeclaration(fnPath, compiled, gating);
142
} else {
143
const gatingExpression = t.conditionalExpression(
144
t.callExpression(t.identifier(gating.importSpecifierName), []),