UNPKG

rspack-plugin-mock

Version:
558 lines (542 loc) 15.4 kB
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 };