rspack-plugin-mock
Version:
inject api mock server to development server
558 lines (542 loc) • 15.4 kB
JavaScript
import {
baseMiddleware,
createLogger,
doesProxyContextMatchUrl,
lookupFile,
normalizePath,
packageDir,
transformMockData,
transformRawData,
urlParse,
vfs
} from "./chunk-OGWV5ZGG.js";
// src/core/build.ts
import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { toArray } from "@pengzhanbo/utils";
import { createFilter } from "@rollup/pluginutils";
import fg from "fast-glob";
import color2 from "picocolors";
// src/core/createRspackCompiler.ts
import { createRequire } from "node:module";
import * as rspackCore from "@rspack/core";
import isCore from "is-core-module";
import color from "picocolors";
var require2 = createRequire(import.meta.url);
function createCompiler(options, callback) {
const rspackOptions = resolveRspackOptions(options);
const isWatch = rspackOptions.watch === true;
async function handler(err, stats) {
const name = "[rspack:mock]";
const logError = (...args) => {
if (stats) {
stats.compilation.getLogger(name).error(...args);
} else {
console.error(color.red(name), ...args);
}
};
if (err) {
logError(err.stack || err);
if ("details" in err) {
logError(err.details);
}
return;
}
if (stats?.hasErrors()) {
const info = stats.toJson();
logError(info.errors);
}
const code = vfs.readFileSync("/output.js", "utf-8");
const externals = [];
if (!isWatch) {
const modules = stats?.toJson().modules || [];
const aliasList = Object.keys(options.alias || {}).map((key) => key.replace(/\$$/g, ""));
for (const { name: name2 } of modules) {
if (name2?.startsWith("external")) {
const packageName = normalizePackageName(name2);
if (!isCore(packageName) && !aliasList.includes(packageName))
externals.push(normalizePackageName(name2));
}
}
}
await callback({ code, externals });
}
const compiler = rspackCore.rspack(rspackOptions, isWatch ? handler : void 0);
if (compiler)
compiler.outputFileSystem = vfs;
if (!isWatch) {
compiler?.run(async (...args) => {
await handler(...args);
compiler.close(() => {
});
});
}
return compiler;
}
function transformWithRspack(options) {
return new Promise((resolve) => {
createCompiler({ ...options, watch: false }, (result) => {
resolve(result);
});
});
}
function normalizePackageName(name) {
const filepath = name.replace("external ", "").slice(1, -1);
const [scope, packageName] = filepath.split("/");
if (filepath[0] === "@") {
return `${scope}/${packageName}`;
}
return scope;
}
function resolveRspackOptions({
cwd,
isEsm = true,
entryFile,
plugins,
alias,
watch = false
}) {
const targets = ["node >= 18.0.0"];
if (alias && "@swc/helpers" in alias) {
delete alias["@swc/helpers"];
}
return {
mode: "production",
context: cwd,
entry: entryFile,
watch,
target: "node18.0",
externalsType: isEsm ? "module" : "commonjs2",
resolve: {
alias,
extensions: [".js", ".ts", ".cjs", ".mjs", ".json5", ".json"]
},
plugins,
output: {
library: { type: !isEsm ? "commonjs2" : "module" },
filename: "output.js",
path: "/"
},
experiments: { outputModule: isEsm },
optimization: { minimize: !watch },
module: {
rules: [
{
test: /\.json5?$/,
loader: require2.resolve("#json5-loader"),
type: "javascript/auto"
},
{
test: /\.[cm]?js$/,
use: [
{
loader: "builtin:swc-loader",
options: {
jsc: { parser: { syntax: "ecmascript" } },
env: { targets }
}
}
]
},
{
test: /\.[cm]?ts$/,
use: [
{
loader: "builtin:swc-loader",
options: {
jsc: { parser: { syntax: "typescript" } },
env: { targets }
}
}
]
}
]
}
};
}
// src/core/build.ts
async function buildMockServer(options, outputDir) {
const entryFile = path.resolve(process.cwd(), "node_modules/.cache/mock-server/mock-server.ts");
const mockFileList = await getMockFileList(options);
await writeMockEntryFile(entryFile, mockFileList, options.cwd);
const { code, externals } = await transformWithRspack({
entryFile,
cwd: options.cwd,
plugins: options.plugins,
alias: options.alias
});
await fsp.unlink(entryFile);
const outputList = [
{ filename: "mock-data.js", source: code },
{ filename: "index.js", source: generatorServerEntryCode(options) },
{ filename: "package.json", source: generatePackageJson(options, externals) }
];
const dist = path.resolve(outputDir, options.build.dist);
options.logger.info(
`${color2.green("\u2713")} generate mock server in ${color2.cyan(path.relative(process.cwd(), dist))}`
);
if (!fs.existsSync(dist)) {
await fsp.mkdir(dist, { recursive: true });
}
for (const { filename, source } of outputList) {
await fsp.writeFile(path.join(dist, filename), source, "utf8");
const sourceSize = (source.length / 1024).toFixed(2);
const space = filename.length < 24 ? " ".repeat(24 - filename.length) : "";
options.logger.info(` ${color2.green(filename)}${space}${color2.bold(color2.dim(`${sourceSize} kB`))}`);
}
}
function generatePackageJson(options, externals) {
const deps = getHostDependencies(options.cwd);
const { name, version } = getPluginPackageInfo();
const exclude = [name, "connect", "cors"];
const mockPkg = {
name: "mock-server",
type: "module",
scripts: {
start: "node index.js"
},
dependencies: {
connect: "^3.7.0",
[name]: `^${version}`,
cors: "^2.8.5"
}
};
externals.filter((dep) => !exclude.includes(dep)).forEach((dep) => {
mockPkg.dependencies[dep] = deps[dep] || "latest";
});
return JSON.stringify(mockPkg, null, 2);
}
function generatorServerEntryCode({
proxies,
wsPrefix,
cookiesOptions,
bodyParserOptions,
priority,
build
}) {
const { serverPort, log } = build;
return `import { createServer } from 'node:http';
import connect from 'connect';
import corsMiddleware from 'cors';
import {
baseMiddleware,
createLogger,
mockWebSocket,
transformMockData,
transformRawData
} from 'rspack-plugin-mock/server';
import rawData from './mock-data.js';
const app = connect();
const server = createServer(app);
const logger = createLogger('mock-server', '${log}');
const proxies = ${JSON.stringify(proxies)};
const wsProxies = ${JSON.stringify(toArray(wsPrefix))};
const cookiesOptions = ${JSON.stringify(cookiesOptions)};
const bodyParserOptions = ${JSON.stringify(bodyParserOptions)};
const priority = ${JSON.stringify(priority)};
const mockConfig = {
mockData: transformMockData(transformRawData(rawData)),
on: () => {},
};
mockWebSocket(mockConfig, server, { wsProxies, cookiesOptions, logger });
app.use(corsMiddleware());
app.use(baseMiddleware(mockConfig, {
formidableOptions: { multiples: true },
proxies,
priority,
cookiesOptions,
bodyParserOptions,
logger,
}));
server.listen(${serverPort});
console.log('listen: http://localhost:${serverPort}');
`;
}
async function getMockFileList({ cwd, include, exclude }) {
const filter = createFilter(include, exclude, { resolve: false });
return await fg(include, { cwd }).then((files) => files.filter(filter));
}
async function writeMockEntryFile(entryFile, files, cwd) {
const importers = [];
const exporters = [];
for (const [index, filepath] of files.entries()) {
const file = normalizePath(path.join(cwd, filepath));
importers.push(`import * as m${index} from '${file}'`);
exporters.push(`[m${index}, '${filepath}']`);
}
const code = `${importers.join("\n")}
export default [
${exporters.join(",\n ")}
]`;
const dirname = path.dirname(entryFile);
if (!fs.existsSync(dirname)) {
await fsp.mkdir(dirname, { recursive: true });
}
await fsp.writeFile(entryFile, code, "utf8");
}
function getPluginPackageInfo() {
let pkg = {};
try {
const filepath = path.join(packageDir, "../package.json");
if (fs.existsSync(filepath)) {
pkg = JSON.parse(fs.readFileSync(filepath, "utf8"));
}
} catch {
}
return {
name: pkg.name || "rspack-plugin-mock",
version: pkg.version || "latest"
};
}
function getHostDependencies(context) {
let pkg = {};
try {
const content = lookupFile(context, ["package.json"]);
if (content)
pkg = JSON.parse(content);
} catch {
}
return { ...pkg.dependencies, ...pkg.devDependencies };
}
// src/core/mockCompiler.ts
import EventEmitter from "node:events";
import path3 from "node:path";
import process2 from "node:process";
import { toArray as toArray2 } from "@pengzhanbo/utils";
import { createFilter as createFilter2 } from "@rollup/pluginutils";
import chokidar from "chokidar";
import fastGlob from "fast-glob";
// src/core/loadFromCode.ts
import fs2, { promises as fsp2 } from "node:fs";
import path2 from "node:path";
import { pathToFileURL } from "node:url";
async function loadFromCode({
filepath,
code,
isESM,
cwd
}) {
filepath = path2.resolve(cwd, filepath);
const ext = isESM ? ".mjs" : ".cjs";
const filepathTmp = `${filepath}.timestamp-${Date.now()}${ext}`;
const file = pathToFileURL(filepathTmp).toString();
await fsp2.writeFile(filepathTmp, code, "utf8");
try {
const mod = await import(file);
return mod.default || mod;
} finally {
try {
fs2.unlinkSync(filepathTmp);
} catch {
}
}
}
// src/core/mockCompiler.ts
function createMockCompiler(options) {
return new MockCompiler(options);
}
var MockCompiler = class extends EventEmitter {
constructor(options) {
super();
this.options = options;
this.cwd = options.cwd || process2.cwd();
const { include, exclude } = this.options;
this.fileFilter = createFilter2(include, exclude, { resolve: false });
try {
const pkg = lookupFile(this.cwd, ["package.json"]);
this.moduleType = !!pkg && JSON.parse(pkg).type === "module" ? "esm" : "cjs";
} catch {
}
this.entryFile = path3.resolve(process2.cwd(), "node_modules/.cache/mock-server/mock-server.ts");
}
cwd;
mockWatcher;
moduleType = "cjs";
entryFile;
_mockData = {};
fileFilter;
watchInfo;
compiler;
get mockData() {
return this._mockData;
}
async run() {
await this.updateMockEntry();
this.watchMockFiles();
const { plugins, alias } = this.options;
const options = {
isEsm: this.moduleType === "esm",
cwd: this.cwd,
plugins,
entryFile: this.entryFile,
alias,
watch: true
};
this.compiler = createCompiler(options, async ({ code }) => {
try {
const result = await loadFromCode({
filepath: "mock.bundle.js",
code,
isESM: this.moduleType === "esm",
cwd: this.cwd
});
this._mockData = transformMockData(transformRawData(result));
this.emit("update", this.watchInfo || {});
} catch (e) {
this.options.logger.error(e.stack || e.message);
}
});
}
close() {
this.mockWatcher.close();
this.compiler?.close(() => {
});
this.emit("close");
}
updateAlias(alias) {
this.options.alias = {
...this.options.alias,
...alias
};
}
async updateMockEntry() {
const files = await this.getMockFiles();
await writeMockEntryFile(this.entryFile, files, this.cwd);
}
async getMockFiles() {
const { include } = this.options;
const files = await fastGlob(include, { cwd: this.cwd });
return files.filter(this.fileFilter);
}
watchMockFiles() {
const { include } = this.options;
const [firstGlob, ...otherGlob] = toArray2(include);
const watcher = this.mockWatcher = chokidar.watch(firstGlob, {
ignoreInitial: true,
cwd: this.cwd
});
if (otherGlob.length > 0)
otherGlob.forEach((glob) => watcher.add(glob));
watcher.on("add", (filepath) => {
if (this.fileFilter(filepath)) {
this.watchInfo = { filepath, type: "add" };
this.updateMockEntry();
}
});
watcher.on("change", (filepath) => {
if (this.fileFilter(filepath)) {
this.watchInfo = { filepath, type: "change" };
}
});
watcher.on("unlink", async (filepath) => {
this.watchInfo = { filepath, type: "unlink" };
this.updateMockEntry();
});
}
};
// src/core/mockMiddleware.ts
import cors from "cors";
import { pathToRegexp } from "path-to-regexp";
function createMockMiddleware(compiler, options) {
function mockMiddleware(middlewares, reload) {
middlewares.unshift(baseMiddleware(compiler, options));
const corsMiddleware = createCorsMiddleware(compiler, options);
if (corsMiddleware) {
middlewares.unshift(corsMiddleware);
}
if (options.reload) {
compiler.on("update", () => reload?.());
}
return middlewares;
}
return mockMiddleware;
}
function createCorsMiddleware(compiler, options) {
let corsOptions = {};
const enabled = options.cors !== false;
if (enabled) {
corsOptions = {
...corsOptions,
...typeof options.cors === "boolean" ? {} : options.cors
};
}
const proxies = options.proxies;
return !enabled ? void 0 : function(req, res, next) {
const { pathname } = urlParse(req.url);
if (!pathname || proxies.length === 0 || !proxies.some(
(context) => doesProxyContextMatchUrl(context, req.url, req)
)) {
return next();
}
const mockData = compiler.mockData;
const mockUrl = Object.keys(mockData).find(
(key) => pathToRegexp(key).test(pathname)
);
if (!mockUrl)
return next();
cors(corsOptions)(req, res, next);
};
}
// src/core/resolvePluginOptions.ts
import process3 from "node:process";
import { isBoolean, toArray as toArray3 } from "@pengzhanbo/utils";
function resolvePluginOptions({
prefix = [],
wsPrefix = [],
cwd,
include = ["mock/**/*.mock.{js,ts,cjs,mjs,json,json5}"],
exclude = ["**/node_modules/**", "**/.vscode/**", "**/.git/**"],
reload = false,
log = "info",
cors: cors2 = true,
formidableOptions = {},
build = false,
cookiesOptions = {},
bodyParserOptions = {},
priority = {}
} = {}, { alias, context, plugins, proxies }) {
const logger = createLogger(
"rspack:mock",
isBoolean(log) ? log ? "info" : "error" : log
);
prefix = toArray3(prefix);
return {
prefix,
wsPrefix,
cwd: cwd || context || process3.cwd(),
include,
exclude,
reload,
cors: cors2,
cookiesOptions,
log,
formidableOptions: {
multiples: true,
...formidableOptions
},
bodyParserOptions,
priority,
build: build ? Object.assign(
{
serverPort: 8080,
dist: "mockServer",
log: "error"
},
typeof build === "object" ? build : {}
) : false,
alias,
plugins,
proxies: [...proxies, ...prefix],
wsProxies: toArray3(wsPrefix),
logger
};
}
export {
buildMockServer,
createMockCompiler,
MockCompiler,
createMockMiddleware,
resolvePluginOptions
};