UNPKG

rspack-plugin-mock

Version:
488 lines (482 loc) 14.6 kB
import { c as processRawData, d as normalizePath, f as vfs, h as createMatcher, m as getPackageDeps, p as getPackageDepList, s as processMockData, t as createLogger } from "./logger-CzeuHKAL.js"; import { createRequire } from "node:module"; import path from "node:path"; import process from "node:process"; import { attemptAsync, isBoolean, isPlainObject, toArray, uniq } from "@pengzhanbo/utils"; import * as rspackCore from "@rspack/core"; import fs, { promises } from "node:fs"; import fsp from "node:fs/promises"; import ansis from "ansis"; import { glob } from "tinyglobby"; import isCore from "is-core-module"; import { loadPackageJSONSync } from "local-pkg"; import { pathToFileURL } from "node:url"; import { createHash } from "node:crypto"; import EventEmitter from "node:events"; import chokidar from "chokidar"; //#region src/compiler/createRspackCompiler.ts const require = 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(ansis.red(name), ...args); }; if (err) { logError(err.stack || err); if ("details" in err) logError(err.details); return; } if (stats?.hasErrors()) logError(stats.toJson().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 } of modules) if (name?.startsWith("external")) { const packageName = normalizePackageName(name); if (!isCore(packageName) && !aliasList.includes(packageName)) externals.push(normalizePackageName(name)); } } await callback({ code, externals }); } const compiler = rspackCore.rspack(rspackOptions); compiler.outputFileSystem = vfs; if (!isWatch) compiler.run(async (...args) => { await handler(...args); compiler.close(() => {}); }); else compiler.watch({}, handler); 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 >= 20.0.0"]; if (alias && "@swc/helpers" in alias) delete alias["@swc/helpers"]; const externals = getPackageDepList(cwd); const externalPattern = new RegExp(`^(${externals.join("|")})($|/)`, "i"); return { mode: "production", context: cwd, entry: entryFile, watch, target: `node${(process.versions.node || "").replace(/\.\d+$/, "")}`, externalsType: isEsm ? "module" : "commonjs2", externals: [externalPattern], resolve: { alias, extensions: [ ".js", ".ts", ".cjs", ".mjs", ".json5", ".json" ] }, plugins, output: { library: { type: !isEsm ? "commonjs2" : "module" }, filename: "output.js", module: isEsm, path: "/" }, optimization: { minimize: !watch }, node: { __dirname: false, __filename: false }, module: { rules: [ { test: /\.json5?$/, loader: require.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 } } }] } ] } }; } //#endregion //#region src/compiler/loadFromCode.ts async function loadFromCode({ filepath, code, isESM, cwd }) { filepath = path.resolve(cwd, filepath); const ext = isESM ? ".mjs" : ".cjs"; const filepathTmp = `${filepath}.${getHash(code)}${ext}`; await promises.writeFile(filepathTmp, code, "utf8"); const [, mod] = await attemptAsync(importDefault, String(pathToFileURL(filepathTmp))); await attemptAsync(promises.unlink, filepathTmp); return mod; } async function importDefault(filepath) { const mod = await import(filepath); return mod.default || mod; } function getHash(str) { return createHash("md5").update(str).digest("hex"); } //#endregion //#region src/compiler/mockCompiler.ts function createMockCompiler(options) { return new MockCompiler(options); } var MockCompiler = class extends EventEmitter { cwd; mockWatcher; entryFile; deps = []; isESM = false; _mockData = {}; watchInfo; compiler; constructor(options) { super(); this.options = options; this.cwd = options.cwd || process.cwd(); try { const pkg = loadPackageJSONSync(this.cwd); this.isESM = pkg?.type === "module"; } catch {} this.entryFile = path.resolve(process.cwd(), "node_modules/.cache/mock-server/mock-server.ts"); } get mockData() { return this._mockData; } async run() { const { include, exclude } = this.options; const { pattern, ignore, isMatch } = createMatcher(include, exclude); const files = await glob(pattern, { ignore, cwd: path.join(this.cwd, this.options.dir) }); this.deps = files.map((file) => normalizePath(file)); this.updateMockEntry(); this.watchMockFiles(isMatch); const { plugins, alias } = this.options; const options = { isEsm: this.isESM, 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.isESM, cwd: this.cwd }); this._mockData = processMockData(processRawData(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() { await writeMockEntryFile(this.entryFile, this.deps, this.cwd, this.options.dir); } watchMockFiles(isMatch) { const watcher = this.mockWatcher = chokidar.watch(this.options.dir, { ignoreInitial: true, cwd: this.cwd, ignored: (filepath, stats) => { if (filepath.includes("node_modules")) return true; return !!stats?.isFile() && !isMatch(filepath); } }); watcher.on("add", (filepath) => { filepath = normalizePath(filepath); if (isMatch(filepath)) { this.watchInfo = { filepath, type: "add" }; this.deps = uniq([...this.deps, filepath]); this.updateMockEntry(); } }); watcher.on("change", (filepath) => { filepath = normalizePath(filepath); if (isMatch(filepath)) this.watchInfo = { filepath, type: "change" }; }); watcher.on("unlink", async (filepath) => { filepath = normalizePath(filepath); if (isMatch(filepath)) { this.watchInfo = { filepath, type: "unlink" }; this.deps = this.deps.filter((dep) => dep !== filepath); this.updateMockEntry(); } }); } }; //#endregion //#region package.json var name = "rspack-plugin-mock"; var version = "2.1.0"; //#endregion //#region src/build/packageJson.ts function generatePackageJson(options, externals) { const deps = getPackageDeps(options.context); 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); } //#endregion //#region src/build/serverEntryCode.ts 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, processMockData, processRawData } 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: processMockData(processRawData(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}'); `; } //#endregion //#region src/build/writeEntryFile.ts async function writeMockEntryFile(entryFile, files, cwd, dir) { const importers = []; const exporters = []; for (const [index, filepath] of files.entries()) { const relative = normalizePath(path.join(dir, filepath)); const file = normalizePath(path.join(cwd, relative)); importers.push(`import * as m${index} from '${file}'`); exporters.push(`[m${index}, '${relative}']`); } const code = `${importers.join("\n")}\n\nexport default [\n ${exporters.join(",\n ")}\n]`; const dirname = path.dirname(entryFile); if (!fs.existsSync(dirname)) await fsp.mkdir(dirname, { recursive: true }); await fsp.writeFile(entryFile, code, "utf8"); } //#endregion //#region src/build/generate.ts async function buildMockServer(options, outputDir) { const buildOptions = options.build; const entryFile = path.resolve(process.cwd(), "node_modules/.cache/mock-server/mock-server.ts"); const { pattern, ignore } = createMatcher(options.include, options.exclude); await writeMockEntryFile(entryFile, await glob(pattern, { ignore, cwd: path.join(options.cwd, options.dir) }), options.cwd, options.dir); 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) } ]; if (options.record.enabled && buildOptions.includeRecord) { const files = await glob(path.join(options.record.dir, "**/*.json"), { cwd: options.cwd, dot: true }); for (const file of files) outputList.push({ filename: path.join(outputDir, file), source: await fsp.readFile(path.join(options.cwd, file), "utf-8") }); } const dist = path.resolve(outputDir, options.build.dist); options.logger.info(`${ansis.green("✓")} generate mock server in ${ansis.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(` ${ansis.green(filename)}${space}${ansis.bold.dim(`${sourceSize} kB`)}`); } } //#endregion //#region src/core/options.ts function resolvePluginOptions({ prefix = [], wsPrefix = [], cwd, dir = "mock", include = ["**/*.mock.{js,ts,cjs,mjs,json,json5}"], exclude = [], reload = false, log = "info", cors = true, formidableOptions = {}, build = false, cookiesOptions = {}, bodyParserOptions = {}, priority = {}, activeScene = [], record = false, replay }, { alias, context, plugins, proxies: rawProxies }) { const logger = createLogger("rspack:mock", isBoolean(log) ? log ? "info" : "error" : log); const proxies = [...toArray(prefix), ...rawProxies]; const wsProxies = toArray(wsPrefix); if (!proxies.length && !wsProxies.length) logger.warn(`No proxy was configured, mock server will not work. See ${ansis.cyan("https://vite-plugin-mock-dev-server.netlify.app/guide/usage")}`); const enabled = !!cors; let corsOptions = {}; if (enabled && isPlainObject(cors)) corsOptions = { ...corsOptions, ...cors }; cwd = cwd || context || process.cwd(); const resolvedRecord = resolveRecordOptions(cwd, dir, record); return { enabled: true, prefix, wsPrefix, cwd, dir, include, exclude, reload, cors: enabled ? corsOptions : false, cookiesOptions, log, formidableOptions: { multiples: true, ...formidableOptions }, bodyParserOptions, priority, build: build ? { serverPort: 8080, dist: "mockServer", log: "error", ...typeof build === "object" ? build : {} } : false, alias, plugins, proxies, wsProxies, logger, activeScene: toArray(activeScene), record: resolvedRecord, replay: replay ?? resolvedRecord.enabled ?? false }; } /** * Resolve record options * * 解析录制配置 * * @param cwd - Current working directory / 当前工作目录 * @param dir - Mock context directory / 模拟上下文目录 * @param record - Record options / 录制配置 * @returns Resolved record options / 解析后的录制配置 */ function resolveRecordOptions(cwd, dir, record) { const recordOptions = typeof record === "boolean" ? { enabled: record } : record; const expires = recordOptions?.expires ?? 0; return { enabled: recordOptions?.enabled ?? false, cwd, dir: path.join(dir, recordOptions?.dir || ".recordings"), overwrite: recordOptions?.overwrite ?? true, status: toArray(recordOptions?.status).map(Number), expires: expires === 0 ? Number.MAX_SAFE_INTEGER : expires * 1e3, gitignore: recordOptions?.gitignore ?? true, filter: recordOptions?.filter || (() => true) }; } //#endregion export { buildMockServer as n, createMockCompiler as r, resolvePluginOptions as t };