-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support named exports from client references #20312
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
86cbf6c
Rename "name"->"filepath" field on Webpack module references
sebmarkbage aac1d3e
Switch back to transformSource instead of getSource
sebmarkbage ac4762d
Add acorn dependency
sebmarkbage cded3bb
Parse exported names of ESM modules
sebmarkbage 403501f
Handle imported names one level deep in CommonJS using a Proxy
sebmarkbage 0afb1b4
Add export name to module reference and Webpack map
sebmarkbage d4e4585
Special case plain CJS requires and conditional imports using __esModule
sebmarkbage a5f7228
Dedupe acorn-related deps
Andarist File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Counter.client.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,8 @@ | |
* @flow | ||
*/ | ||
|
||
import acorn from 'acorn'; | ||
|
||
type ResolveContext = { | ||
conditions: Array<string>, | ||
parentURL: string | void, | ||
|
@@ -16,11 +18,10 @@ type ResolveFunction = ( | |
string, | ||
ResolveContext, | ||
ResolveFunction, | ||
) => Promise<string>; | ||
) => Promise<{url: string}>; | ||
|
||
type GetSourceContext = { | ||
format: string, | ||
url: string, | ||
}; | ||
|
||
type GetSourceFunction = ( | ||
|
@@ -29,15 +30,32 @@ type GetSourceFunction = ( | |
GetSourceFunction, | ||
) => Promise<{source: Source}>; | ||
|
||
type TransformSourceContext = { | ||
format: string, | ||
url: string, | ||
}; | ||
|
||
type TransformSourceFunction = ( | ||
Source, | ||
TransformSourceContext, | ||
TransformSourceFunction, | ||
) => Promise<{source: Source}>; | ||
|
||
type Source = string | ArrayBuffer | Uint8Array; | ||
|
||
let warnedAboutConditionsFlag = false; | ||
|
||
let stashedGetSource: null | GetSourceFunction = null; | ||
let stashedResolve: null | ResolveFunction = null; | ||
|
||
export async function resolve( | ||
specifier: string, | ||
context: ResolveContext, | ||
defaultResolve: ResolveFunction, | ||
): Promise<string> { | ||
): Promise<{url: string}> { | ||
// We stash this in case we end up needing to resolve export * statements later. | ||
stashedResolve = defaultResolve; | ||
|
||
if (!context.conditions.includes('react-server')) { | ||
context = { | ||
...context, | ||
|
@@ -71,14 +89,174 @@ export async function getSource( | |
url: string, | ||
context: GetSourceContext, | ||
defaultGetSource: GetSourceFunction, | ||
) { | ||
// We stash this in case we end up needing to resolve export * statements later. | ||
stashedGetSource = defaultGetSource; | ||
return defaultGetSource(url, context, defaultGetSource); | ||
} | ||
|
||
function addExportNames(names, node) { | ||
switch (node.type) { | ||
case 'Identifier': | ||
names.push(node.name); | ||
return; | ||
case 'ObjectPattern': | ||
for (let i = 0; i < node.properties.length; i++) | ||
addExportNames(names, node.properties[i]); | ||
return; | ||
case 'ArrayPattern': | ||
for (let i = 0; i < node.elements.length; i++) { | ||
const element = node.elements[i]; | ||
if (element) addExportNames(names, element); | ||
} | ||
return; | ||
case 'Property': | ||
addExportNames(names, node.value); | ||
return; | ||
case 'AssignmentPattern': | ||
addExportNames(names, node.left); | ||
return; | ||
case 'RestElement': | ||
addExportNames(names, node.argument); | ||
return; | ||
case 'ParenthesizedExpression': | ||
addExportNames(names, node.expression); | ||
return; | ||
} | ||
} | ||
|
||
function resolveClientImport( | ||
specifier: string, | ||
parentURL: string, | ||
): Promise<{url: string}> { | ||
// Resolve an import specifier as if it was loaded by the client. This doesn't use | ||
// the overrides that this loader does but instead reverts to the default. | ||
// This resolution algorithm will not necessarily have the same configuration | ||
// as the actual client loader. It should mostly work and if it doesn't you can | ||
// always convert to explicit exported names instead. | ||
const conditions = ['node', 'import']; | ||
if (stashedResolve === null) { | ||
throw new Error( | ||
'Expected resolve to have been called before transformSource', | ||
); | ||
} | ||
return stashedResolve(specifier, {conditions, parentURL}, stashedResolve); | ||
} | ||
|
||
async function loadClientImport( | ||
url: string, | ||
defaultTransformSource: TransformSourceFunction, | ||
): Promise<{source: Source}> { | ||
if (url.endsWith('.client.js')) { | ||
// TODO: Named exports. | ||
const src = | ||
"export default { $$typeof: Symbol.for('react.module.reference'), name: " + | ||
JSON.stringify(url) + | ||
'}'; | ||
return {source: src}; | ||
if (stashedGetSource === null) { | ||
throw new Error( | ||
'Expected getSource to have been called before transformSource', | ||
); | ||
} | ||
return defaultGetSource(url, context, defaultGetSource); | ||
// TODO: Validate that this is another module by calling getFormat. | ||
const {source} = await stashedGetSource( | ||
url, | ||
{format: 'module'}, | ||
stashedGetSource, | ||
); | ||
return defaultTransformSource( | ||
source, | ||
{format: 'module', url}, | ||
defaultTransformSource, | ||
); | ||
} | ||
|
||
async function parseExportNamesInto( | ||
transformedSource: string, | ||
names: Array<string>, | ||
parentURL: string, | ||
defaultTransformSource, | ||
): Promise<void> { | ||
const {body} = acorn.parse(transformedSource, { | ||
ecmaVersion: '2019', | ||
sourceType: 'module', | ||
}); | ||
for (let i = 0; i < body.length; i++) { | ||
const node = body[i]; | ||
switch (node.type) { | ||
case 'ExportAllDeclaration': | ||
if (node.exported) { | ||
addExportNames(names, node.exported); | ||
continue; | ||
} else { | ||
const {url} = await resolveClientImport(node.source.value, parentURL); | ||
const {source} = await loadClientImport(url, defaultTransformSource); | ||
if (typeof source !== 'string') { | ||
throw new Error('Expected the transformed source to be a string.'); | ||
} | ||
parseExportNamesInto(source, names, url, defaultTransformSource); | ||
continue; | ||
} | ||
case 'ExportDefaultDeclaration': | ||
names.push('default'); | ||
continue; | ||
case 'ExportNamedDeclaration': | ||
if (node.declaration) { | ||
if (node.declaration.type === 'VariableDeclaration') { | ||
const declarations = node.declaration.declarations; | ||
for (let j = 0; j < declarations.length; j++) { | ||
addExportNames(names, declarations[j].id); | ||
} | ||
} else { | ||
addExportNames(names, node.declaration.id); | ||
} | ||
} | ||
if (node.specificers) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sebmarkbage Is this broken? |
||
const specificers = node.specificers; | ||
for (let j = 0; j < specificers.length; j++) { | ||
addExportNames(names, specificers[j].exported); | ||
} | ||
} | ||
continue; | ||
} | ||
} | ||
} | ||
|
||
export async function transformSource( | ||
source: Source, | ||
context: TransformSourceContext, | ||
defaultTransformSource: TransformSourceFunction, | ||
): Promise<{source: Source}> { | ||
const transformed = await defaultTransformSource( | ||
source, | ||
context, | ||
defaultTransformSource, | ||
); | ||
if (context.format === 'module' && context.url.endsWith('.client.js')) { | ||
const transformedSource = transformed.source; | ||
if (typeof transformedSource !== 'string') { | ||
throw new Error('Expected source to have been transformed to a string.'); | ||
} | ||
|
||
const names = []; | ||
await parseExportNamesInto( | ||
transformedSource, | ||
names, | ||
context.url, | ||
defaultTransformSource, | ||
); | ||
|
||
let newSrc = | ||
"const MODULE_REFERENCE = Symbol.for('react.module.reference');\n"; | ||
for (let i = 0; i < names.length; i++) { | ||
const name = names[i]; | ||
if (name === 'default') { | ||
newSrc += 'export default '; | ||
} else { | ||
newSrc += 'export const ' + name + ' = '; | ||
} | ||
newSrc += '{ $$typeof: MODULE_REFERENCE, filepath: '; | ||
newSrc += JSON.stringify(context.url); | ||
newSrc += ', name: '; | ||
newSrc += JSON.stringify(name); | ||
newSrc += '};\n'; | ||
} | ||
|
||
return {source: newSrc}; | ||
} | ||
return transformed; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually test CJS server paths by removing the
"type": "module"
definition in package.json. In this case I need to use raw CJS (require(...)
+module.exports
) without using Babel/Webpack. Because otherwise we fall into the case below.I couldn't test the Webpack client paths for true CJS. I spent a few hours trying to figure out how to make the react-script config let me write raw CJS instead of transpiling from ESM and I couldn't because webpack kept treating them as ESM which means that assigning to module.exports causes and error.
I think this works though.