fake-api-middleware
Version:
Express/Connect middleware for dummy responses
213 lines (206 loc) • 5.93 kB
JavaScript
// src/middleware.ts
import { ServerResponse } from "http";
import * as querystring from "querystring";
import bodyParse from "co-body";
// src/responsesLoader.ts
import { EventEmitter } from "events";
import * as chokidar from "chokidar";
// src/helpers.ts
var delay = (time) => {
return new Promise((r) => {
setTimeout(r, time);
});
};
var isString = (value) => {
return typeof value === "string" || value instanceof String;
};
var debounce = (func, wait, immediate = false) => {
let timeout = null;
return (...args) => {
const later = () => {
timeout = null;
if (!immediate) {
func.apply(void 0, args);
}
};
const callNow = immediate && !timeout;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(void 0, args);
}
};
};
// src/module.ts
import * as path from "path";
import * as fs from "fs";
import * as module from "module";
import { build as esbuildBuild } from "esbuild";
var _require = module.createRequire(process.cwd());
var executeModule = (filePath, bundledCode) => {
filePath = path.resolve(process.cwd(), filePath);
const extension = path.extname(filePath);
const extensions = module.Module._extensions;
const defaultLoader = extensions[extension];
extensions[extension] = (module2, filename) => {
if (filename === filePath) {
module2._compile(bundledCode, filename);
} else {
defaultLoader(module2, filename);
}
};
if (_require && _require.cache) {
delete _require.cache[filePath];
}
const raw = _require(filePath);
const config = raw.__esModule ? raw.default : raw;
if (defaultLoader) {
extensions[extension] = defaultLoader;
}
return config;
};
var extendedRequire = async (filePath) => {
const pkg = JSON.parse(
fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8")
);
const srcCode = await (async () => {
const result = await esbuildBuild({
entryPoints: [filePath],
outfile: "out.js",
write: false,
platform: "node",
bundle: true,
format: "cjs",
metafile: true,
target: "es2015",
external: [
"esbuild",
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.devDependencies || {}),
...Object.keys(pkg.peerDependencies || {})
],
logLevel: "silent"
});
const { text } = result.outputFiles[0];
return text;
})();
return await executeModule(filePath, srcCode);
};
// src/responsesLoader.ts
var ResponsesLoader = class extends EventEmitter {
constructor({
responsesFile,
watchFiles
}) {
super();
this.responsesFile = responsesFile;
if (isString(watchFiles)) {
watchFiles = [watchFiles];
}
this.watchFiles = watchFiles || [];
chokidar.watch([this.responsesFile, ...this.watchFiles]).on(
"all",
debounce(async () => {
try {
const responsesConfig = await extendedRequire(this.responsesFile);
this.emit("update", responsesConfig);
} catch (e) {
this.emit("error", e);
}
}, 100)
);
}
};
// src/middleware.ts
import { match } from "path-to-regexp";
var middleware = (middlewareOptions) => {
const prepareResponses = (responses) => {
const result = [];
for (const [key, response] of Object.entries(responses)) {
const [method, apiPath] = key.split(" ");
result.push({
method,
apiPath,
matchUrlFn: match(apiPath, { decode: decodeURIComponent }),
response
});
}
return result;
};
let preparedResponses = prepareResponses(middlewareOptions.responses || {});
if (middlewareOptions.responsesFile) {
const responsesConfigLoader = new ResponsesLoader({
responsesFile: middlewareOptions.responsesFile,
watchFiles: middlewareOptions.watchFiles
});
let wasErrorLastTime = false;
responsesConfigLoader.on("update", (newResponses) => {
preparedResponses = prepareResponses(newResponses);
if (wasErrorLastTime) {
console.info("[FakeResponses]", "Responses successfully loaded");
}
});
responsesConfigLoader.on("error", (err) => {
console.error("[FakeResponses]", err);
wasErrorLastTime = true;
});
}
return async (req, res, next) => {
if (middlewareOptions.enable !== void 0 && !middlewareOptions.enable) {
return next();
}
for (const { method, matchUrlFn, response } of preparedResponses) {
const [url, queryStr] = req.url.split("?");
const matchResult = matchUrlFn(url);
if (matchResult && req.method === method) {
if (middlewareOptions.responseDelay) {
await delay(middlewareOptions.responseDelay);
}
let body = {};
try {
body = await bodyParse(req);
} catch {
}
if (typeof response === "function") {
const responseResult = await response({
body,
query: querystring.parse(queryStr),
headers: req.headers,
params: matchResult.params,
req,
res
});
if (responseResult === void 0) {
return;
}
if (responseResult instanceof ServerResponse) {
return responseResult;
}
res.setHeader("Content-Type", "application/json");
return res.end(JSON.stringify(responseResult));
}
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(response));
}
}
return next();
};
};
// src/vitePlugin.ts
var vitePlugin = (middlewareOptions) => {
return {
name: "vite-plugin-fake-response",
configureServer(server) {
server.middlewares.use(middleware(middlewareOptions));
}
};
};
export {
debounce,
delay,
isString,
middleware,
vitePlugin
};