mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
996b00b787
<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory. Before submitting a pull request, please make sure the following is done: 1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`. 2. Run `yarn` in the repository root. 3. If you've fixed a bug or added code that should be tested, add tests! 4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development. 5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`. 6. If you need a debugger, run `yarn debug-test --watch TestName`, open `chrome://inspect`, and press "Inspect". 7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`). 8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files. 9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`). 10. If you haven't already, complete the CLA. Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html --> ## Summary In https://github.com/facebook/react/pull/25504, `react-server-dom-webpack/` was deprecated in favor of `react-server-dom-webpack/client`, but a remaining import wasn't adjusted accordingly. As a result, the remaining conditions within the file are no longer firing appropriately, which I ran into while playing around with a fork of [server-components-demo](https://github.com/reactjs/server-components-demo). The `index.js` file now contains a [placeholder](https://github.com/facebook/react/blob/main/packages/react-server-dom-webpack/index.js) and the actual logic of the client now sits in `/client`. ## How did you test this change? I replaced `require.resolve('../')` with `require.resolve('../client')` in the `react-server-dom-webpack` package in `node_modules` and confirmed that the output of the build looked good again.
363 lines
11 KiB
JavaScript
363 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import {join} from 'path';
|
|
import {pathToFileURL} from 'url';
|
|
|
|
import asyncLib from 'neo-async';
|
|
|
|
import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency';
|
|
import NullDependency from 'webpack/lib/dependencies/NullDependency';
|
|
import Template from 'webpack/lib/Template';
|
|
import {
|
|
sources,
|
|
WebpackError,
|
|
Compilation,
|
|
AsyncDependenciesBlock,
|
|
} from 'webpack';
|
|
|
|
import isArray from 'shared/isArray';
|
|
|
|
class ClientReferenceDependency extends ModuleDependency {
|
|
constructor(request: mixed) {
|
|
super(request);
|
|
}
|
|
|
|
get type(): string {
|
|
return 'client-reference';
|
|
}
|
|
}
|
|
|
|
// This is the module that will be used to anchor all client references to.
|
|
// I.e. it will have all the client files as async deps from this point on.
|
|
// We use the Flight client implementation because you can't get to these
|
|
// without the client runtime so it's the first time in the loading sequence
|
|
// you might want them.
|
|
const clientImportName = 'react-server-dom-webpack/client';
|
|
const clientFileName = require.resolve('../client');
|
|
|
|
type ClientReferenceSearchPath = {
|
|
directory: string,
|
|
recursive?: boolean,
|
|
include: RegExp,
|
|
exclude?: RegExp,
|
|
};
|
|
|
|
type ClientReferencePath = string | ClientReferenceSearchPath;
|
|
|
|
type Options = {
|
|
isServer: boolean,
|
|
clientReferences?: ClientReferencePath | $ReadOnlyArray<ClientReferencePath>,
|
|
chunkName?: string,
|
|
manifestFilename?: string,
|
|
};
|
|
|
|
const PLUGIN_NAME = 'React Server Plugin';
|
|
|
|
export default class ReactFlightWebpackPlugin {
|
|
clientReferences: $ReadOnlyArray<ClientReferencePath>;
|
|
chunkName: string;
|
|
manifestFilename: string;
|
|
|
|
constructor(options: Options) {
|
|
if (!options || typeof options.isServer !== 'boolean') {
|
|
throw new Error(
|
|
PLUGIN_NAME + ': You must specify the isServer option as a boolean.',
|
|
);
|
|
}
|
|
if (options.isServer) {
|
|
throw new Error('TODO: Implement the server compiler.');
|
|
}
|
|
if (!options.clientReferences) {
|
|
this.clientReferences = [
|
|
{
|
|
directory: '.',
|
|
recursive: true,
|
|
include: /\.client\.(js|ts|jsx|tsx)$/,
|
|
},
|
|
];
|
|
} else if (
|
|
typeof options.clientReferences === 'string' ||
|
|
!isArray(options.clientReferences)
|
|
) {
|
|
this.clientReferences = [(options.clientReferences: $FlowFixMe)];
|
|
} else {
|
|
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
|
this.clientReferences = options.clientReferences;
|
|
}
|
|
if (typeof options.chunkName === 'string') {
|
|
this.chunkName = options.chunkName;
|
|
if (!/\[(index|request)\]/.test(this.chunkName)) {
|
|
this.chunkName += '[index]';
|
|
}
|
|
} else {
|
|
this.chunkName = 'client[index]';
|
|
}
|
|
this.manifestFilename =
|
|
options.manifestFilename || 'react-client-manifest.json';
|
|
}
|
|
|
|
apply(compiler: any) {
|
|
const _this = this;
|
|
let resolvedClientReferences;
|
|
let clientFileNameFound = false;
|
|
|
|
// Find all client files on the file system
|
|
compiler.hooks.beforeCompile.tapAsync(
|
|
PLUGIN_NAME,
|
|
({contextModuleFactory}, callback) => {
|
|
const contextResolver = compiler.resolverFactory.get('context', {});
|
|
|
|
_this.resolveAllClientFiles(
|
|
compiler.context,
|
|
contextResolver,
|
|
compiler.inputFileSystem,
|
|
contextModuleFactory,
|
|
function(err, resolvedClientRefs) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
resolvedClientReferences = resolvedClientRefs;
|
|
callback();
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
compiler.hooks.thisCompilation.tap(
|
|
PLUGIN_NAME,
|
|
(compilation, {normalModuleFactory}) => {
|
|
compilation.dependencyFactories.set(
|
|
ClientReferenceDependency,
|
|
normalModuleFactory,
|
|
);
|
|
compilation.dependencyTemplates.set(
|
|
ClientReferenceDependency,
|
|
new NullDependency.Template(),
|
|
);
|
|
|
|
const handler = parser => {
|
|
// We need to add all client references as dependency of something in the graph so
|
|
// Webpack knows which entries need to know about the relevant chunks and include the
|
|
// map in their runtime. The things that actually resolves the dependency is the Flight
|
|
// client runtime. So we add them as a dependency of the Flight client runtime.
|
|
// Anything that imports the runtime will be made aware of these chunks.
|
|
parser.hooks.program.tap(PLUGIN_NAME, () => {
|
|
const module = parser.state.module;
|
|
|
|
if (module.resource !== clientFileName) {
|
|
return;
|
|
}
|
|
|
|
clientFileNameFound = true;
|
|
|
|
if (resolvedClientReferences) {
|
|
// $FlowFixMe[incompatible-use] found when upgrading Flow
|
|
for (let i = 0; i < resolvedClientReferences.length; i++) {
|
|
// $FlowFixMe[incompatible-use] found when upgrading Flow
|
|
const dep = resolvedClientReferences[i];
|
|
|
|
const chunkName = _this.chunkName
|
|
.replace(/\[index\]/g, '' + i)
|
|
.replace(/\[request\]/g, Template.toPath(dep.userRequest));
|
|
|
|
const block = new AsyncDependenciesBlock(
|
|
{
|
|
name: chunkName,
|
|
},
|
|
null,
|
|
dep.request,
|
|
);
|
|
|
|
block.addDependency(dep);
|
|
module.addBlock(block);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
normalModuleFactory.hooks.parser
|
|
.for('javascript/auto')
|
|
.tap('HarmonyModulesPlugin', handler);
|
|
|
|
normalModuleFactory.hooks.parser
|
|
.for('javascript/esm')
|
|
.tap('HarmonyModulesPlugin', handler);
|
|
|
|
normalModuleFactory.hooks.parser
|
|
.for('javascript/dynamic')
|
|
.tap('HarmonyModulesPlugin', handler);
|
|
},
|
|
);
|
|
|
|
compiler.hooks.make.tap(PLUGIN_NAME, compilation => {
|
|
compilation.hooks.processAssets.tap(
|
|
{
|
|
name: PLUGIN_NAME,
|
|
stage: Compilation.PROCESS_ASSETS_STAGE_REPORT,
|
|
},
|
|
function() {
|
|
if (clientFileNameFound === false) {
|
|
compilation.warnings.push(
|
|
new WebpackError(
|
|
`Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.manifestFilename} was not created.`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const json = {};
|
|
compilation.chunkGroups.forEach(function(chunkGroup) {
|
|
const chunkIds = chunkGroup.chunks.map(function(c) {
|
|
return c.id;
|
|
});
|
|
|
|
function recordModule(id, module) {
|
|
// TODO: Hook into deps instead of the target module.
|
|
// That way we know by the type of dep whether to include.
|
|
// It also resolves conflicts when the same module is in multiple chunks.
|
|
|
|
if (!/\.client\.(js|ts)x?$/.test(module.resource)) {
|
|
return;
|
|
}
|
|
|
|
const moduleProvidedExports = compilation.moduleGraph
|
|
.getExportsInfo(module)
|
|
.getProvidedExports();
|
|
|
|
const moduleExports = {};
|
|
['', '*']
|
|
.concat(
|
|
Array.isArray(moduleProvidedExports)
|
|
? moduleProvidedExports
|
|
: [],
|
|
)
|
|
.forEach(function(name) {
|
|
moduleExports[name] = {
|
|
id,
|
|
chunks: chunkIds,
|
|
name: name,
|
|
};
|
|
});
|
|
const href = pathToFileURL(module.resource).href;
|
|
|
|
if (href !== undefined) {
|
|
json[href] = moduleExports;
|
|
}
|
|
}
|
|
|
|
chunkGroup.chunks.forEach(function(chunk) {
|
|
const chunkModules = compilation.chunkGraph.getChunkModulesIterable(
|
|
chunk,
|
|
);
|
|
|
|
Array.from(chunkModules).forEach(function(module) {
|
|
const moduleId = compilation.chunkGraph.getModuleId(module);
|
|
|
|
recordModule(moduleId, module);
|
|
// If this is a concatenation, register each child to the parent ID.
|
|
if (module.modules) {
|
|
module.modules.forEach(concatenatedMod => {
|
|
recordModule(moduleId, concatenatedMod);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
const output = JSON.stringify(json, null, 2);
|
|
compilation.emitAsset(
|
|
_this.manifestFilename,
|
|
new sources.RawSource(output, false),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
// This attempts to replicate the dynamic file path resolution used for other wildcard
|
|
// resolution in Webpack is using.
|
|
resolveAllClientFiles(
|
|
context: string,
|
|
contextResolver: any,
|
|
fs: any,
|
|
contextModuleFactory: any,
|
|
callback: (
|
|
err: null | Error,
|
|
result?: $ReadOnlyArray<ClientReferenceDependency>,
|
|
) => void,
|
|
) {
|
|
asyncLib.map(
|
|
this.clientReferences,
|
|
(
|
|
clientReferencePath: string | ClientReferenceSearchPath,
|
|
cb: (
|
|
err: null | Error,
|
|
result?: $ReadOnlyArray<ClientReferenceDependency>,
|
|
) => void,
|
|
): void => {
|
|
if (typeof clientReferencePath === 'string') {
|
|
cb(null, [new ClientReferenceDependency(clientReferencePath)]);
|
|
return;
|
|
}
|
|
const clientReferenceSearch: ClientReferenceSearchPath = clientReferencePath;
|
|
contextResolver.resolve(
|
|
{},
|
|
context,
|
|
clientReferencePath.directory,
|
|
{},
|
|
(err, resolvedDirectory) => {
|
|
if (err) return cb(err);
|
|
const options = {
|
|
resource: resolvedDirectory,
|
|
resourceQuery: '',
|
|
recursive:
|
|
clientReferenceSearch.recursive === undefined
|
|
? true
|
|
: clientReferenceSearch.recursive,
|
|
regExp: clientReferenceSearch.include,
|
|
include: undefined,
|
|
exclude: clientReferenceSearch.exclude,
|
|
};
|
|
contextModuleFactory.resolveDependencies(
|
|
fs,
|
|
options,
|
|
(err2: null | Error, deps: Array<any /*ModuleDependency*/>) => {
|
|
if (err2) return cb(err2);
|
|
const clientRefDeps = deps.map(dep => {
|
|
// use userRequest instead of request. request always end with undefined which is wrong
|
|
const request = join(resolvedDirectory, dep.userRequest);
|
|
const clientRefDep = new ClientReferenceDependency(request);
|
|
clientRefDep.userRequest = dep.userRequest;
|
|
return clientRefDep;
|
|
});
|
|
cb(null, clientRefDeps);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
(
|
|
err: null | Error,
|
|
result: $ReadOnlyArray<$ReadOnlyArray<ClientReferenceDependency>>,
|
|
): void => {
|
|
if (err) return callback(err);
|
|
const flat = [];
|
|
for (let i = 0; i < result.length; i++) {
|
|
// $FlowFixMe[method-unbinding]
|
|
flat.push.apply(flat, result[i]);
|
|
}
|
|
callback(null, flat);
|
|
},
|
|
);
|
|
}
|
|
}
|