every-plugin
Version:
308 lines (262 loc) • 10.1 kB
text/typescript
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";
import type { Compiler, RspackPluginInstance } from "@rspack/core";
import { setupPluginMiddleware } from "./dev-server-middleware";
import { buildSharedDependencies } from "./module-federation";
import { getPluginInfo, loadDevConfig } from "./utils";
export interface EveryPluginOptions {
devConfigPath?: string;
port?: number;
pluginId?: string;
dts?: boolean;
}
export interface AdditionalExport {
srcPath: string;
exportNames: string[];
}
export interface PluginManifestEmitterOptions {
manifestFileName?: string;
contractFileName?: string;
additionalExports?: AdditionalExport[];
}
export class EmitPluginManifest implements RspackPluginInstance {
name = "EmitPluginManifest";
constructor(private options: PluginManifestEmitterOptions = {}) {}
apply(compiler: Compiler) {
compiler.hooks.thisCompilation.tap(this.name, (compilation) => {
const webpack = (compiler as Compiler & { webpack?: any }).webpack;
const rawSource = webpack?.sources?.RawSource;
const stage = webpack?.Compilation?.PROCESS_ASSETS_STAGE_ADDITIONS ?? 1000;
compilation.hooks.processAssets.tapPromise({ name: this.name, stage }, async () => {
const context = compiler.options.context || process.cwd();
const pluginInfo = getPluginInfo(context);
const contractFileName = this.options.contractFileName ?? "contract.d.ts";
const manifestFileName = this.options.manifestFileName ?? "plugin.manifest.json";
const sourceContractPath = path.join(context, "types", contractFileName);
let contractTypes: string;
const tryReadFile = async (filePath: string): Promise<string | null> => {
if (!fs.existsSync(filePath)) {
return null;
}
const stats = fs.statSync(filePath);
if (!stats.isFile()) {
return null;
}
try {
return await fs.promises.readFile(filePath, "utf8");
} catch {
return null;
}
};
contractTypes = (await tryReadFile(sourceContractPath)) ?? "";
if (!contractTypes) {
const packageDir = context.split("/").pop();
const nestedPath = path.join(context, "types", packageDir ?? "", "src", contractFileName);
contractTypes = (await tryReadFile(nestedPath)) ?? "";
if (!contractTypes) {
console.warn(
`[EmitPluginManifest] Contract file not found at ${sourceContractPath} or ${nestedPath}. ` +
`Skipping manifest generation.`,
);
return;
}
}
const contractSha256 = crypto.createHash("sha256").update(contractTypes).digest("hex");
const manifest: Record<string, unknown> = {
schemaVersion: 1,
kind: "every-plugin/manifest",
plugin: {
name: pluginInfo.name,
version: pluginInfo.version,
},
runtime: {
remoteEntry: "./remoteEntry.js",
},
contract: {
kind: "orpc",
types: {
path: `./types/${contractFileName}`,
exportName: "contract",
typeName: "ContractType",
sha256: contractSha256,
},
},
};
const additionalExportsEntries: Array<{ path: string; exports: string[]; sha256: string }> =
[];
if (this.options.additionalExports?.length) {
for (const additional of this.options.additionalExports) {
const sourcePath = path.join(context, "types", additional.srcPath);
const content = await tryReadFile(sourcePath);
if (!content) {
console.warn(
`[EmitPluginManifest] Additional export file not found at ${sourcePath}. Skipping.`,
);
continue;
}
const sha256 = crypto.createHash("sha256").update(content).digest("hex");
const distPath = `./types/${additional.srcPath}`;
additionalExportsEntries.push({
path: distPath,
exports: additional.exportNames,
sha256,
});
if (rawSource) {
compilation.emitAsset(`types/${additional.srcPath}`, new rawSource(content));
}
}
if (additionalExportsEntries.length > 0) {
manifest.additionalExports = additionalExportsEntries;
}
}
if (rawSource) {
compilation.emitAsset(
manifestFileName,
new rawSource(`${JSON.stringify(manifest, null, 2)}\n`),
);
compilation.emitAsset(`types/${contractFileName}`, new rawSource(`${contractTypes}`));
}
});
});
}
}
export class EveryPluginDevServer implements RspackPluginInstance {
name = "EveryPluginDevServer";
constructor(private options: EveryPluginOptions = {}) {}
apply(compiler: Compiler) {
const pluginInfo = getPluginInfo(compiler.options.context || process.cwd());
const devConfig = loadDevConfig(this.options.devConfigPath || "./plugin.dev.ts");
const port = Number(process.env.PORT) || this.options.port || devConfig?.port || 3999;
this.configureDefaults(compiler, pluginInfo);
if (!compiler.options.devServer) {
compiler.options.devServer = {};
}
this.configureDevServer(compiler, pluginInfo, devConfig, port);
new ModuleFederationPlugin({
name: pluginInfo.normalizedName,
filename: "remoteEntry.js",
dts: this.options.dts !== false,
manifest: {},
runtimePlugins: [require.resolve("@module-federation/node/runtimePlugin")],
library: { type: "commonjs-module" },
exposes: {
"./plugin": "./src/index.ts",
},
shared: buildSharedDependencies(pluginInfo),
shareStrategy: "version-first",
}).apply(compiler);
if (this.options.dts === false) {
compiler.options.plugins = (compiler.options.plugins ?? []).filter(
(p) =>
!p ||
typeof p !== "object" ||
((p as any).name !== "MFDevPlugin" && (p as any).name !== "ModuleFederationDtsPlugin"),
);
}
}
private configureDefaults(compiler: Compiler, pluginInfo: any) {
const context = compiler.options.context || process.cwd();
if (!compiler.options.output) {
compiler.options.output = {};
}
compiler.options.output.uniqueName = pluginInfo.normalizedName;
compiler.options.output.publicPath = "auto";
compiler.options.output.path = path.resolve(context, "dist");
compiler.options.output.clean = true;
compiler.options.output.library = { type: "commonjs-module" };
if (!compiler.options.target) {
compiler.options.target = "async-node";
}
if (!compiler.options.mode) {
compiler.options.mode = process.env.NODE_ENV === "development" ? "development" : "production";
}
if (compiler.options.devtool === undefined) {
compiler.options.devtool = "source-map";
}
if (!compiler.options.infrastructureLogging) {
compiler.options.infrastructureLogging = {
level: "warn",
};
}
this.ensureTypeScriptLoader(compiler);
if (!compiler.options.resolve) {
compiler.options.resolve = {};
}
compiler.options.resolve.extensions = ["...", ".tsx", ".ts"];
compiler.options.resolve.conditionNames = [
"webpack",
"import",
"module",
"require",
"node",
"default",
];
if (compiler.options.resolve.byDependency) {
for (const depType of Object.keys(compiler.options.resolve.byDependency)) {
const depConfig = (
compiler.options.resolve.byDependency as Record<string, { conditionNames?: string[] }>
)[depType];
if (depConfig?.conditionNames) {
depConfig.conditionNames = depConfig.conditionNames.filter((c) => c !== "development");
}
}
}
compiler.options.resolve.fallback = {
...compiler.options.resolve.fallback,
bufferutil: false,
"utf-8-validate": false,
};
}
private ensureTypeScriptLoader(compiler: Compiler) {
if (!compiler.options.module) {
compiler.options.module = { rules: [] } as any;
}
if (!compiler.options.module.rules) {
compiler.options.module.rules = [];
}
const hasTsLoader = compiler.options.module.rules.some(
(rule: any) =>
typeof rule === "object" &&
rule !== null &&
"test" in rule &&
rule.test instanceof RegExp &&
rule.test.test(".ts"),
);
if (!hasTsLoader) {
compiler.options.module.rules.push({
test: /\.tsx?$/,
use: "builtin:swc-loader",
exclude: /node_modules/,
});
}
}
private configureDevServer(compiler: Compiler, pluginInfo: any, devConfig: any, port: number) {
if (!compiler.options.devServer) {
return;
}
const context = compiler.options.context || process.cwd();
const originalSetup = compiler.options.devServer.setupMiddlewares;
compiler.options.devServer.port = port;
compiler.options.devServer.static = path.join(context, "dist");
compiler.options.devServer.hot = true;
compiler.options.devServer.devMiddleware = { writeToDisk: true };
compiler.options.devServer.headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization",
};
compiler.options.devServer.client = {
logging: "warn",
overlay: {
warnings: false,
errors: true,
},
};
compiler.options.devServer.setupMiddlewares = (middlewares, devServer) => {
setupPluginMiddleware(devServer, pluginInfo, devConfig, port);
return originalSetup ? originalSetup(middlewares, devServer) : middlewares;
};
}
}