7
* @flow
8
*/
9
10
+import acorn from 'acorn';
11
+
12
type ResolveContext = {
13
conditions: Array<string>,
14
parentURL: string | void,
18
string,
19
ResolveContext,
20
ResolveFunction,
19
-) => Promise<string>;
21
+) => Promise<{url: string}>;
22
23
type GetSourceContext = {
24
format: string,
23
- url: string,
25
};
26
27
type GetSourceFunction = (
30
GetSourceFunction,
31
) => Promise<{source: Source}>;
32
33
+type TransformSourceContext = {
34
+ format: string,
35
+ url: string,
36
+};
37
+
38
+type TransformSourceFunction = (
39
+ Source,
40
+ TransformSourceContext,
41
+ TransformSourceFunction,
42
+) => Promise<{source: Source}>;
43
+
44
type Source = string | ArrayBuffer | Uint8Array;
45
46
let warnedAboutConditionsFlag = false;
47
48
+let stashedGetSource: null | GetSourceFunction = null;
49
+let stashedResolve: null | ResolveFunction = null;
50
+
51
export async function resolve(
52
specifier: string,
53
context: ResolveContext,
54
defaultResolve: ResolveFunction,
40
-): Promise<string> {
55
+): Promise<{url: string}> {
56
+ // We stash this in case we end up needing to resolve export * statements later.
57
+ stashedResolve = defaultResolve;
58
+
59
if (!context.conditions.includes('react-server')) {
60
context = {
61
...context,
89
url: string,
90
context: GetSourceContext,
91
defaultGetSource: GetSourceFunction,
92
+) {
93
+ // We stash this in case we end up needing to resolve export * statements later.
94
+ stashedGetSource = defaultGetSource;
95
+ return defaultGetSource(url, context, defaultGetSource);
96
+}
97
+
98
+function addExportNames(names, node) {
99
+ switch (node.type) {
100
+ case 'Identifier':
101
+ names.push(node.name);
102
+ return;
103
+ case 'ObjectPattern':
104
+ for (let i = 0; i < node.properties.length; i++)
105
+ addExportNames(names, node.properties[i]);
106
+ return;
107
+ case 'ArrayPattern':
108
+ for (let i = 0; i < node.elements.length; i++) {
109
+ const element = node.elements[i];
110
+ if (element) addExportNames(names, element);
111
+ }
112
+ return;
113
+ case 'Property':
114
+ addExportNames(names, node.value);
115
+ return;
116
+ case 'AssignmentPattern':
117
+ addExportNames(names, node.left);
118
+ return;
119
+ case 'RestElement':
120
+ addExportNames(names, node.argument);
121
+ return;
122
+ case 'ParenthesizedExpression':
123
+ addExportNames(names, node.expression);
124
+ return;
125
+ }
126
+}
127
+
128
+function resolveClientImport(
129
+ specifier: string,
130
+ parentURL: string,
131
+): Promise<{url: string}> {
132
+ // Resolve an import specifier as if it was loaded by the client. This doesn't use
133
+ // the overrides that this loader does but instead reverts to the default.
134
+ // This resolution algorithm will not necessarily have the same configuration
135
+ // as the actual client loader. It should mostly work and if it doesn't you can
136
+ // always convert to explicit exported names instead.
137
+ const conditions = ['node', 'import'];
138
+ if (stashedResolve === null) {
139
+ throw new Error(
140
+ 'Expected resolve to have been called before transformSource',
141
+ );
142
+ }
143
+ return stashedResolve(specifier, {conditions, parentURL}, stashedResolve);
144
+}
145
+
146
+async function loadClientImport(
147
+ url: string,
148
+ defaultTransformSource: TransformSourceFunction,
149
): Promise<{source: Source}> {
75
- if (url.endsWith('.client.js')) {
76
- // TODO: Named exports.
77
- const src =
78
- "export default { $$typeof: Symbol.for('react.module.reference'), name: " +
79
- JSON.stringify(url) +
80
- '}';
81
- return {source: src};
150
+ if (stashedGetSource === null) {
151
+ throw new Error(
152
+ 'Expected getSource to have been called before transformSource',
153
+ );
154
}
83
- return defaultGetSource(url, context, defaultGetSource);
155
+ // TODO: Validate that this is another module by calling getFormat.
156
+ const {source} = await stashedGetSource(
157
+ url,
158
+ {format: 'module'},
159
+ stashedGetSource,
160
+ );
161
+ return defaultTransformSource(
162
+ source,
163
+ {format: 'module', url},
164
+ defaultTransformSource,
165
+ );
166
+}
167
+
168
+async function parseExportNamesInto(
169
+ transformedSource: string,
170
+ names: Array<string>,
171
+ parentURL: string,
172
+ defaultTransformSource,
173
+): Promise<void> {
174
+ const {body} = acorn.parse(transformedSource, {
175
+ ecmaVersion: '2019',
176
+ sourceType: 'module',
177
+ });
178
+ for (let i = 0; i < body.length; i++) {
179
+ const node = body[i];
180
+ switch (node.type) {
181
+ case 'ExportAllDeclaration':
182
+ if (node.exported) {
183
+ addExportNames(names, node.exported);
184
+ continue;
185
+ } else {
186
+ const {url} = await resolveClientImport(node.source.value, parentURL);
187
+ const {source} = await loadClientImport(url, defaultTransformSource);
188
+ if (typeof source !== 'string') {
189
+ throw new Error('Expected the transformed source to be a string.');
190
+ }
191
+ parseExportNamesInto(source, names, url, defaultTransformSource);
192
+ continue;
193
+ }
194
+ case 'ExportDefaultDeclaration':
195
+ names.push('default');
196
+ continue;
197
+ case 'ExportNamedDeclaration':
198
+ if (node.declaration) {
199
+ if (node.declaration.type === 'VariableDeclaration') {
200
+ const declarations = node.declaration.declarations;
201
+ for (let j = 0; j < declarations.length; j++) {
202
+ addExportNames(names, declarations[j].id);
203
+ }
204
+ } else {
205
+ addExportNames(names, node.declaration.id);
206
+ }
207
+ }
208
+ if (node.specificers) {
209
+ const specificers = node.specificers;
210
+ for (let j = 0; j < specificers.length; j++) {
211
+ addExportNames(names, specificers[j].exported);
212
+ }
213
+ }
214
+ continue;
215
+ }
216
+ }
217
+}
218
+
219
+export async function transformSource(
220
+ source: Source,
221
+ context: TransformSourceContext,
222
+ defaultTransformSource: TransformSourceFunction,
223
+): Promise<{source: Source}> {
224
+ const transformed = await defaultTransformSource(
225
+ source,
226
+ context,
227
+ defaultTransformSource,
228
+ );
229
+ if (context.format === 'module' && context.url.endsWith('.client.js')) {
230
+ const transformedSource = transformed.source;
231
+ if (typeof transformedSource !== 'string') {
232
+ throw new Error('Expected source to have been transformed to a string.');
233
+ }
234
+
235
+ const names = [];
236
+ await parseExportNamesInto(
237
+ transformedSource,
238
+ names,
239
+ context.url,
240
+ defaultTransformSource,
241
+ );
242
+
243
+ let newSrc =
244
+ "const MODULE_REFERENCE = Symbol.for('react.module.reference');\n";
245
+ for (let i = 0; i < names.length; i++) {
246
+ const name = names[i];
247
+ if (name === 'default') {
248
+ newSrc += 'export default ';
249
+ } else {
250
+ newSrc += 'export const ' + name + ' = ';
251
+ }
252
+ newSrc += '{ $$typeof: MODULE_REFERENCE, filepath: ';
253
+ newSrc += JSON.stringify(context.url);
254
+ newSrc += ', name: ';
255
+ newSrc += JSON.stringify(name);
256
+ newSrc += '};\n';
257
+ }
258
+
259
+ return {source: newSrc};
260
+ }
261
+ return transformed;
262
}