Files
react/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js
T
Sebastian Markbåge 172e89b4bf Reland Remove redundant initial of isArray (#21188)
* Remove redundant initial of isArray (#21163)

* Reapply prettier

* Type the isArray function with refinement support

This ensures that an argument gets refined just like it does if isArray is
used directly.

I'm not sure how to express with just a direct reference so I added a
function wrapper and confirmed that this does get inlined properly by
closure compiler.

* A few more

* Rename unit test to internal

This is not testing a bundle.

Co-authored-by: Behnam Mohammadi <itten@live.com>
2021-04-07 07:57:43 -07:00

296 lines
9.5 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its 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 AsyncDependenciesBlock from 'webpack/lib/AsyncDependenciesBlock';
import Template from 'webpack/lib/Template';
import isArray from 'shared/isArray';
class ClientReferenceDependency extends ModuleDependency {
constructor(request) {
super(request);
}
get type() {
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 clientFileName = require.resolve('../');
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 {
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) {
let resolvedClientReferences;
const run = (params, callback) => {
// First we need to find all client files on the file system. We do this early so
// that we have them synchronously available later when we need them. This might
// not be needed anymore since we no longer need to compile the module itself in
// a special way. So it's probably better to do this lazily and in parallel with
// other compilation.
const contextResolver = compiler.resolverFactory.get('context', {});
this.resolveAllClientFiles(
compiler.context,
contextResolver,
compiler.inputFileSystem,
compiler.createContextModuleFactory(),
(err, resolvedClientRefs) => {
if (err) {
callback(err);
return;
}
resolvedClientReferences = resolvedClientRefs;
callback();
},
);
};
compiler.hooks.run.tapAsync(PLUGIN_NAME, run);
compiler.hooks.watchRun.tapAsync(PLUGIN_NAME, run);
compiler.hooks.compilation.tap(
PLUGIN_NAME,
(compilation, {normalModuleFactory}) => {
compilation.dependencyFactories.set(
ClientReferenceDependency,
normalModuleFactory,
);
compilation.dependencyTemplates.set(
ClientReferenceDependency,
new NullDependency.Template(),
);
compilation.hooks.buildModule.tap(PLUGIN_NAME, module => {
// 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.
// TODO: Warn if we don't find this file anywhere in the compilation.
if (module.resource !== clientFileName) {
return;
}
if (resolvedClientReferences) {
for (let i = 0; i < resolvedClientReferences.length; i++) {
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,
},
module,
null,
dep.require,
);
block.addDependency(dep);
module.addBlock(block);
}
}
});
},
);
compiler.hooks.emit.tap(PLUGIN_NAME, compilation => {
const json = {};
compilation.chunkGroups.forEach(chunkGroup => {
const chunkIds = chunkGroup.chunks.map(c => c.id);
function recordModule(id, mod) {
// 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$/.test(mod.resource)) {
return;
}
const moduleExports = {};
['', '*'].concat(mod.buildMeta.providedExports).forEach(name => {
moduleExports[name] = {
id: id,
chunks: chunkIds,
name: name,
};
});
const href = pathToFileURL(mod.resource).href;
if (href !== undefined) {
json[href] = moduleExports;
}
}
chunkGroup.chunks.forEach(chunk => {
chunk.getModules().forEach(mod => {
recordModule(mod.id, mod);
// If this is a concatenation, register each child to the parent ID.
if (mod.modules) {
mod.modules.forEach(concatenatedMod => {
recordModule(mod.id, concatenatedMod);
});
}
});
});
});
const output = JSON.stringify(json, null, 2);
compilation.assets[this.manifestFilename] = {
source() {
return output;
},
size() {
return output.length;
},
};
});
}
// 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<ModuleDependency>) => {
if (err2) return cb(err2);
const clientRefDeps = deps.map(dep => {
const request = join(resolvedDirectory, dep.request);
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++) {
flat.push.apply(flat, result[i]);
}
callback(null, flat);
},
);
}
}