Skip to content

Commit

Permalink
update ReactFlightWebpackPlugin to be compatiable with webpack v5 (#2…
Browse files Browse the repository at this point in the history
  • Loading branch information
michenly authored Nov 25, 2021
1 parent ca106a0 commit 0cc724c
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 93 deletions.
2 changes: 1 addition & 1 deletion packages/react-server-dom-webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"peerDependencies": {
"react": "^17.0.0",
"react-dom": "^17.0.0",
"webpack": "^4.43.0"
"webpack": "^5.59.0"
},
"dependencies": {
"acorn": "^6.2.1",
Expand Down
247 changes: 155 additions & 92 deletions packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ 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 {
sources,
WebpackError,
Compilation,
AsyncDependenciesBlock,
} from 'webpack';

import isArray from 'shared/isArray';

Expand All @@ -34,6 +39,7 @@ class ClientReferenceDependency extends ModuleDependency {
// 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';
const clientFileName = require.resolve('../');

type ClientReferenceSearchPath = {
Expand Down Expand Up @@ -97,33 +103,35 @@ export default class ReactFlightWebpackPlugin {
}

apply(compiler: any) {
const _this = this;
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();
},
);
};
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.run.tapAsync(PLUGIN_NAME, run);
compiler.hooks.watchRun.tapAsync(PLUGIN_NAME, run);
compiler.hooks.compilation.tap(
compiler.hooks.thisCompilation.tap(
PLUGIN_NAME,
(compilation, {normalModuleFactory}) => {
compilation.dependencyFactories.set(
Expand All @@ -135,86 +143,140 @@ export default class ReactFlightWebpackPlugin {
new NullDependency.Template(),
);

compilation.hooks.buildModule.tap(PLUGIN_NAME, module => {
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.
// 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);
parser.hooks.program.tap(PLUGIN_NAME, () => {
const module = parser.state.module;

if (module.resource !== clientFileName) {
return;
}
}
});

clientFileNameFound = true;

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,
},
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.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)) {
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 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 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.assets[this.manifestFilename] = {
source() {
return output;
},
size() {
return output.length;

const output = JSON.stringify(json, null, 2);
compilation.emitAsset(
_this.manifestFilename,
new sources.RawSource(output, false),
);
},
};
);
});
}

Expand Down Expand Up @@ -268,7 +330,8 @@ export default class ReactFlightWebpackPlugin {
(err2: null | Error, deps: Array<ModuleDependency>) => {
if (err2) return cb(err2);
const clientRefDeps = deps.map(dep => {
const request = join(resolvedDirectory, dep.request);
// 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;
Expand Down

0 comments on commit 0cc724c

Please sign in to comment.