fakelab 0.0.21 โ†’ 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  โšก A fast, easy-config mock API server for frontend developers.
4
4
 
5
+ **[Documentation Website](https://alirezahematidev.github.io/fakelab/)**
6
+
5
7
  ## Features
6
8
 
7
9
  - ๐Ÿš€ Instant mock server
@@ -11,6 +13,64 @@
11
13
  - ๐Ÿ“ธ Snapshot real APIs into mocks
12
14
  - ๐Ÿงช Perfect for local development, prototyping, and frontend testing
13
15
 
16
+ ## Demo
17
+
18
+ Check out the [React + TypeScript + Vite example](./examples/react-typescript-vite) to see Fakelab in action!
19
+
20
+ ### Quick Demo
21
+
22
+ 1. Define your types with Faker annotations:
23
+
24
+ ```typescript
25
+ // fixtures/user.ts
26
+ export interface User {
27
+ /** @faker string.ulid */
28
+ id: string;
29
+ /** @faker person.fullName */
30
+ name: string;
31
+ /** @faker location.streetAddress */
32
+ address: string;
33
+ /** @faker phone.number */
34
+ phone: string;
35
+ /** @faker number.int({min:10,max:80}) */
36
+ age: number;
37
+ }
38
+ ```
39
+
40
+ 2. Configure Fakelab:
41
+
42
+ ```typescript
43
+ // fakelab.config.ts
44
+ import { defineConfig } from "fakelab";
45
+
46
+ export default defineConfig({
47
+ sourcePath: ["./fixtures"],
48
+ server: { port: 50001 },
49
+ });
50
+ ```
51
+
52
+ 3. Run the example:
53
+
54
+ ```bash
55
+ npm run example
56
+ # or
57
+ yarn example
58
+ # or
59
+ pnpm run example
60
+ ```
61
+
62
+ Or start the server manually:
63
+
64
+ `./examples/react-typescript-vite`:
65
+
66
+ ```bash
67
+ npm run serve
68
+ # or
69
+ yarn serve
70
+ # or
71
+ pnpm run serve
72
+ ```
73
+
14
74
  ## Installation
15
75
 
16
76
  ```bash
package/lib/cli.js CHANGED
@@ -1,12 +1,12 @@
1
- import m from'path';import d from'fs-extra';import {Command}from'commander';import F from'express';import st from'cors';import at from'express-ejs-layouts';import it from'http';import {Project}from'ts-morph';import y from'winston';import S from'picocolors';import {JSONFilePreset}from'lowdb/node';import {fileURLToPath}from'url';import V from'qs';import nt from'figlet';import {bundleRequire}from'bundle-require';import et from'joycon';import*as w from'quicktype-core';function G(n){switch(n){case "info":return S.blueBright(n.toUpperCase());case "error":return S.redBright(n.toUpperCase());case "warn":return S.yellowBright(n.toUpperCase());default:return n.toUpperCase()}}var B=y.format.printf(({level:n,message:t,timestamp:e})=>`${S.dim(`[${e}]`)} ${G(n)} ${t}`),g=y.createLogger({format:y.format.combine(y.format.timestamp(),y.format.splat(),B),transports:[new y.transports.Console]}),H=new Intl.ListFormat("en",{style:"long",type:"unit"}),p=class{static info(t,...e){return g.info(t,...e)}static warn(t,...e){return g.warn(t,...e)}static error(t,...e){return g.error(t,...e)}static debug(t,...e){if(!(typeof process>"u"||!process.env.DEBUG))return g.info(t,...e)}static list(t){return H.format(t)}static close(){g.removeAllListeners(),g.close();}};var T=class{constructor(t){this.faker=t;}JSDOC_FAKER_FIELD="faker";FAKER_TAG_REGEX=/^([a-zA-Z0-9._]+)(?:\((.*)\))?$/;boolMapping={true:true,false:false};string(t){return this.execute(t,this.faker.word.noun)}int(t){return this.execute(t,this.faker.number.int)}bool(t){return this.execute(t,this.faker.datatype.boolean)}bigInt(t){return this.execute(t,this.faker.number.bigInt)}litbool(t){return this.boolMapping[t]}async object(t,e){let s={};return await Promise.all(t.map(async r=>{let a=r.getTypeAtLocation(r.getValueDeclarationOrThrow());s[r.getName()]=await e(a,this,this.readJSDocTags(r));})),s}async union(t){let e=await Promise.all(t);return e[Math.floor(Math.random()*e.length)]}async intersection(t){let e=await Promise.all(t);return e[Math.floor(Math.random()*e.length)]}evalArgs(t){if(!(!t||!t.trim()))return Function(`"use strict"; return (${t});`)()}readJSDocTags(t){let e=t.getJsDocTags().filter(s=>s.getName()===this.JSDOC_FAKER_FIELD);return e.length===0?[]:e.map(s=>{let[r]=s.getText();if(!r)return;let a=r.text.trim().match(this.FAKER_TAG_REGEX);if(!a)return;let[,i,o]=a,c=this.evalArgs(o);return {path:i,args:c}})}execute(t,e){if(!t)return e();let s=t.path.split("."),r=this.faker;for(let a of s)r=r[a],r||(p.error("Invalid faker module path: (%s)",t.path),process.exit(1));typeof r!="function"&&(p.error("Unresolvable faker function. (%s)",t.path),process.exit(1));try{return t.args?r(t.args):r()}catch{return p.error("Passed invalid arguments to faker function."),r()}}};var f=m.dirname(fileURLToPath(import.meta.url)),h=process.cwd();var E=class{constructor(t,e,s){this.files=t;this.config=e;this.database=s;let a=new Project({tsConfigFilePath:"tsconfig.json"}).addSourceFilesAtPaths(t);this.__targets=a.flatMap(i=>{let o=i.getInterfaces(),c=i.getTypeAliases(),l=i.getExportDeclarations().flatMap(u=>u.getNamedExports().flatMap(v=>v.getLocalTargetDeclarations()));return [...o,...c,...l]}),this.generateInFileEntitiyMap(this.__targets);}__targets;async run(t){return await t()}normalizePath(t){return t.split(m.sep).join(m.posix.sep)}generateInFileEntitiyMap(t){let e=[...new Set(t.map(a=>{let i=a.getName(),o=a.getSourceFile().getFilePath();return `${i}: import("${o}").${i}`}))],{expose:s}=this.config.browserOpts(),r=`
1
+ import m from'path';import f from'fs-extra';import {Command}from'commander';import L from'express';import X from'cors';import Z from'express-ejs-layouts';import ee from'http';import {Project}from'ts-morph';import w from'picocolors';import {JSONFilePreset}from'lowdb/node';import {fileURLToPath}from'url';import q from'qs';import te from'figlet';import {bundleRequire}from'bundle-require';import V from'joycon';var M=new Intl.ListFormat("en",{style:"long",type:"unit"}),p=class{static label(e){switch(e){case "info":return w.blueBright(e.toUpperCase());case "error":return w.redBright(e.toUpperCase());case "warn":return w.yellowBright(e.toUpperCase())}}static log(e,t){return [w.dim(`[${new Date().toISOString()}]`),this.label(e),t].join(" ")}static info(e,...t){console.log(this.log("info",e),...t);}static warn(e,...t){console.log(this.log("warn",e),...t);}static error(e,...t){console.error(this.log("error",e),...t);}static debug(e,...t){typeof process>"u"||!process.env.DEBUG||console.log(this.log("info",e),...t);}static list(e){return M.format(e)}};var v=class{constructor(e){this.faker=e;}JSDOC_FAKER_FIELD="faker";FAKER_TAG_REGEX=/^([a-zA-Z0-9._]+)(?:\((.*)\))?$/;boolMapping={true:true,false:false};string(e){return this.execute(e,this.faker.word.noun)}int(e){return this.execute(e,this.faker.number.int)}bool(e){return this.execute(e,this.faker.datatype.boolean)}bigInt(e){return this.execute(e,this.faker.number.bigInt)}litbool(e){return this.boolMapping[e]}async object(e,t){let s={};return await Promise.all(e.map(async r=>{let a=r.getTypeAtLocation(r.getValueDeclarationOrThrow());s[r.getName()]=await t(a,this,this.readJSDocTags(r));})),s}async union(e){let t=await Promise.all(e);return t[Math.floor(Math.random()*t.length)]}async intersection(e){let t=await Promise.all(e);return t[Math.floor(Math.random()*t.length)]}evalArgs(e){if(!(!e||!e.trim()))return Function(`"use strict"; return (${e});`)()}readJSDocTags(e){let t=e.getJsDocTags().filter(s=>s.getName()===this.JSDOC_FAKER_FIELD);return t.length===0?[]:t.map(s=>{let[r]=s.getText();if(!r)return;let a=r.text.trim().match(this.FAKER_TAG_REGEX);if(!a)return;let[,i,o]=a,c=this.evalArgs(o);return {path:i,args:c}})}execute(e,t){if(!e)return t();let s=e.path.split("."),r=this.faker;for(let a of s)r=r[a],r||(p.error("Invalid faker module path: (%s)",e.path),process.exit(1));typeof r!="function"&&(p.error("Unresolvable faker function. (%s)",e.path),process.exit(1));try{return e.args?r(e.args):r()}catch{return p.error("Passed invalid arguments to faker function."),r()}}};var h=m.dirname(fileURLToPath(import.meta.url)),d=process.cwd();var x=class{constructor(e,t,s){this.files=e;this.config=t;this.database=s;let a=new Project({tsConfigFilePath:"tsconfig.json"}).addSourceFilesAtPaths(e);this.__targets=a.flatMap(i=>{let o=i.getInterfaces(),c=i.getTypeAliases(),l=i.getExportDeclarations().flatMap(u=>u.getNamedExports().flatMap(b=>b.getLocalTargetDeclarations()));return [...o,...c,...l]}),this.generateInFileEntitiyMap(this.__targets);}__targets;async run(e){return await e()}normalizePath(e){return e.split(m.sep).join(m.posix.sep)}generateInFileEntitiyMap(e){let t=[...new Set(e.map(a=>{let i=a.getName(),o=a.getSourceFile().getFilePath();return `${i}: import("${o}").${i}`}))],{expose:s}=this.config.options.browser(),r=`
2
2
  interface Runtime$ {
3
- ${e.join(`
3
+ ${t.join(`
4
4
  `)}
5
5
  }`;s.mode==="global"&&(r=`
6
6
  declare global {${r}
7
- }`),d.appendFile(m.resolve(f,this.config.RUNTIME_DECL_FILENAME),r);}address(t,e){let s=this.normalizePath(h),a=`${t.replace(s,"")}/${e}`;return a.startsWith("/.fakelab")&&(a=a.replace("/.fakelab","")),a}async entities(){let t=await Promise.all(this.__targets.map(async e=>{let s=e.getName().toLowerCase(),r=e.getType(),a=this.normalizePath(e.getSourceFile().getDirectoryPath()),i=e.getSourceFile().getBaseName(),o=this.address(a,i),c=this.database.directoryPath(),l=m.resolve(c,`${s}.json`),u=this.address(this.normalizePath(c),m.basename(l)),v=a.includes("/.fakelab/snapshots"),k=await JSONFilePreset(l,[]);return [s,{type:r,filepath:o,snapshot:v,table:k,tablepath:u}]}));return new Map(t.sort((e,s)=>Number(e[1].snapshot)-Number(s[1].snapshot)))}async initFakerLibrary(t){let{faker:e}=await import(`@faker-js/faker/locale/${t.locale}`);return e}};async function b(n,t,e=[],s=0){if(n.isString())return t.string(e[s]);if(n.isNumber())return t.int(e[s]);if(n.isBoolean())return t.bool(e[s]);if(n.isBigInt())return t.bigInt(e[s]);if(n.isBooleanLiteral())return t.litbool(n.getText());if(n.isLiteral())return n.getLiteralValue();if(!n.isUndefined()){if(n.isUnion()){let r=n.getUnionTypes();return await t.union(r.map((a,i)=>b(a,t,e,i)))}if(n.isIntersection()){let r=n.getIntersectionTypes();return await t.intersection(r.map((a,i)=>b(a,t,e,i)))}if(n.isArray()){let r=n.getArrayElementTypeOrThrow();return [await b(r,t,e,s)]}if(n.isObject()){let r=n.getProperties();return await t.object(r,(a,i,o)=>b(a,i,o,s))}return null}}function W({each:n}){return {resolve:async e=>await Promise.all(Array.from({length:e},n))}}async function M(n,t,e){let s=await n.files(t.source),r=new E(s,n,e),a=await r.entities(),i=await r.initFakerLibrary(n.fakerOpts(t.locale)),o=new T(i);async function c(l,u){let v=W({each:()=>b(l,o)}),k=await(u.count?v.resolve(parseInt(u.count)):b(l,o)),U=JSON.stringify(k,null,2);return {data:k,json:U}}return {entities:a,build:c}}var A=class{constructor(t,e,s){this.builder=t;this.database=e;this.pkg=s;}async handleQueries(t){let e=t.query.count;return e?{count:e.toString()}:{}}index(){return (t,e)=>{e.render("index",{name:null,entities:this.builder.entities,version:this.pkg.version,enabled:this.database.enabled()});}}preview(t){return async(e,s)=>{let r=`${e.protocol}://${e.host}/`,a=e.params.name,i=await this.handleQueries(e),o=V.stringify(i,{addQueryPrefix:true}),c=this.builder.entities.get(a.toLowerCase());if(c){let{json:l}=await this.builder.build(c.type,i),u=c.filepath;s.render("preview",{name:a,suffix:c.snapshot?"(snapshot)":"",filepath:u,address:r,search:o,json:l,prefix:t,entities:this.builder.entities,version:this.pkg.version,enabled:this.database.enabled()});}else s.redirect("/");}}db(){return (t,e)=>{this.database.enabled()?e.render("database",{name:null,entities:this.builder.entities,version:this.pkg.version}):e.redirect("/");}}table(t){return async(e,s)=>{let r=`${e.protocol}://${e.host}/`,a=e.params.name,i=this.builder.entities.get(a.toLowerCase());if(!this.database.enabled())s.redirect("/");else if(i){await i.table.read();let c=i.table.data.length>0,l=JSON.stringify(i.table.data,null,2),u=i.filepath;s.render("table",{name:a,filepath:u,address:r,prefix:t,json:l,hasData:c,entities:this.builder.entities,version:this.pkg.version});}else s.redirect("/database");}}};var R=class{constructor(t,e,s){this.builder=t;this.network=e;this.database=s;}SEED_MERGE_THRESHOLD=1e3;async handleQueries(t){let e=t.query.count;return e?{count:e.toString()}:{}}async applyNetworkHandlers(t){if(this.network.offline()){let{status:e,message:s}=this.network.state("offline");return t.status(e).json({message:s}),true}if(await this.network.wait(),this.network.timeout())return true;if(this.network.error()){let{status:e,message:s}=this.network.state("error");return t.status(e).json({message:s}),true}return false}entity(){return async(t,e)=>{try{if(await this.applyNetworkHandlers(e))return;let s=t.params.name,r=await this.handleQueries(t),a=this.builder.entities.get(s.toLowerCase());if(a){let{data:i}=await this.builder.build(a.type,r);e.status(200).json(i);}else e.status(400).json({message:"The entity is not exists"});}catch(s){e.status(500).send(s);}}}getTable(){return async(t,e)=>{try{if(!this.database.enabled())return e.status(403).json({message:"database is not enabled or initialized."});if(await this.applyNetworkHandlers(e))return;let s=t.params.name,r=this.builder.entities.get(s.toLowerCase());r?(await r.table.read(),e.status(200).json(r.table.data)):e.status(400).json({message:`${s} table is not exists`});}catch(s){e.status(500).send(s);}}}updateTable(){return async(t,e)=>{try{if(!this.database.enabled())return e.status(403).json({message:"database is not enabled or initialized."});if(await this.applyNetworkHandlers(e))return;let s=t.params.name,r=await this.handleQueries(t),a=this.builder.entities.get(s.toLowerCase());if(a){let{data:i}=await this.builder.build(a.type,r);await a.table.update(o=>o.push(...Array.isArray(i)?i:[i])),e.status(200).json({success:!0});}else e.status(400).json({success:!1,message:`${s} table is not exists`});}catch(s){e.status(500).send(s);}}}insert(){return async(t,e)=>{try{if(!this.database.enabled())return e.status(403).json({message:"database is not enabled or initialized."});let s=t.body?.count||1,r=t.body?.strategy||"reset",a=t.params.name,i=this.builder.entities.get(a.toLowerCase());if(i){let{data:o}=await this.builder.build(i.type,{count:s});if(await i.table.read(),r==="once"&&i.table.data.length>0)return e.status(200).json({message:`${a} entity was seeded once before.`});await i.table.update(c=>{r!=="merge"&&(c.length=0);let l=Array.isArray(o)?o:[o];c.length+l.length<this.SEED_MERGE_THRESHOLD&&c.push(...l);}),e.status(200).json({success:!0});}else e.status(400).json({success:!1,message:"The table is not exists"});}catch(s){e.status(500).send(s);}}}flush(){return async(t,e)=>{try{if(!this.database.enabled())return e.status(403).json({message:"database is not enabled or initialized."});let s=t.params.name,r=this.builder.entities.get(s.toLowerCase());r?(await r.table.update(a=>a.length=0),e.status(200).json({success:!0})):e.status(400).json({success:!1,message:"The table is not exists"});}catch(s){console.log({error:s}),e.status(500).send(s);}}}_update(){return async(t,e)=>{try{if(!this.database.enabled())return e.status(403).json({message:"database is not enabled or initialized."});let s=t.params.name,r=await this.handleQueries(t),a=this.builder.entities.get(s.toLowerCase());if(a){let{data:i}=await this.builder.build(a.type,r);await a.table.update(o=>o.push(i)),e.status(301).redirect(`/database/${s.toLowerCase()}`);}else e.status(400).redirect("/database");}catch{e.status(500).redirect("/database");}}}_clear(){return async(t,e)=>{try{if(!this.database.enabled())return e.status(403).json({message:"database is not enabled or initialized."});let s=t.params.name,r=this.builder.entities.get(s.toLowerCase());r?(await r.table.read(),r.table.data.length>0&&await r.table.update(a=>a.length=0),e.status(301).redirect(`/database/${s.toLowerCase()}`)):e.status(400).redirect("/database");}catch{e.status(500).redirect("/database");}}}};var Z=d.readJSONSync(m.join(f,"../package.json")),D=class{constructor(t,e,s,r,a){this.router=t;this.config=e;this.network=s;this.database=r;this.opts=a;let{pathPrefix:i}=this.config.serverOpts(this.opts.pathPrefix,this.opts.port);this.prefix=i;}prefix;async register(){let t=await M(this.config,this.opts,this.database),e=new A(t,this.database,Z),s=new R(t,this.network,this.database);this.router.get("/",e.index()),this.router.get("/entities/:name",e.preview(this.prefix)),this.router.get("/database",e.db()),this.router.get("/database/:name",e.table(this.prefix)),this.router.get(`/${this.prefix}/:name`,s.entity()),this.router.get(`/${this.prefix}/database/:name`,s.getTable()),this.router.post(`/${this.prefix}/database/:name`,s.updateTable()),this.router.post(`/${this.prefix}/database/insert/:name`,s.insert()),this.router.post(`/${this.prefix}/database/flush/:name`,s.flush()),this.router.post("/__update/:name",s._update()),this.router.post("/__delete/:name",s._clear());}};var P=class n{constructor(t){this.config=t;this.options=this.config.networkOpts(),this.timeout=this.timeout.bind(this),this.error=this.error.bind(this),this.state=this.state.bind(this),this.middleware=this.middleware.bind(this),this.wait=this.wait.bind(this),this.offline=this.offline.bind(this);}options;static initHandlers(t){return new n(t)}timeout(){let t=this.chance(this.options?.timeoutRate);return t&&p.debug("Network timeout..."),t}error(){let t=this.chance(this.options?.errorRate);return t&&p.debug("Network error..."),t}state(t){switch(t){case "error":return {status:500,message:"Network error"};case "offline":return {status:503,message:"Network offline"};default:return {status:500,message:"Unknown error"}}}async wait(){let t=this.resolveDelay();t>0&&(p.debug("Network waiting (%d ms)...",t),await this.sleep(t));}offline(){return this.options?.offline??false}middleware(t,e,s){let r=this.options?.errorRate||0,a=this.options?.timeoutRate||0,i=this.options?.offline??false,o=`delay=${this.resolveDelay()},error=${r},timeout=${a},offline=${i}`;e.setHeader("X-Fakelab-Network",o),s();}resolveDelay(){let t=this.options?.delay;return typeof t=="number"?Math.round(t):Array.isArray(t)?this.random(t):0}chance(t=0){return Math.random()<t}random([t,e]){return Math.round(Math.random()*(e-t)+t)}sleep(t){return new Promise(e=>setTimeout(e,t))}};async function N(){try{let t=await new et().resolve({files:["fakelab.config.ts"]});return t||(p.error("No fakelab config file is detected."),process.exit(1)),(await bundleRequire({filepath:t})).mod.default}catch{p.error("Could not load the config file."),process.exit(1);}}var L=class n{constructor(t){this.config=t;this.enabled=this.enabled.bind(this),this.directoryPath=this.directoryPath.bind(this),this.options=this.config.databaseOpts(),this.options.enabled||d.rmSync(this.directoryPath(),{force:true,recursive:true});}options;static register(t){return new n(t)}enabled(){return this.options.enabled??false}directoryPath(){return m.resolve(h,".fakelab/db")}async initialize(){if(this.enabled())try{await d.ensureDir(this.directoryPath()),await this.modifyGitignoreFile(".fakelab/*");}catch{p.error("Could not create database.");}}async modifyGitignoreFile(t){try{let e=m.resolve(h,".gitignore");if((await d.readFile(e,{encoding:"utf8"})).split(`
8
- `).some(r=>r.trim()===t.trim()))return;await d.appendFile(e,`
9
- ${t}`);}catch{}}};var _=class n{constructor(t){this.options=t;this.start=this.start.bind(this),this.xPoweredMiddleware=this.xPoweredMiddleware.bind(this),this.setupApplication=this.setupApplication.bind(this),this.setupTemplateEngine=this.setupTemplateEngine.bind(this),this.loadLocalEnv();}static init(t){return new n(t)}async start(){let t=F(),e=F.Router(),s=it.createServer(t),r=await N(),a=P.initHandlers(r),i=L.register(r);this.setupApplication(t,a),this.setupTemplateEngine(t),await r.generateInFileRuntimeConfig(f,this.options),await i.initialize(),await new D(e,r,a,i,this.options).register(),t.use(e),this.run(s,r,i,this.options);}setupApplication(t,e){t.disable("x-powered-by"),t.use(F.json()),t.use(st({methods:"GET"})),t.use(F.static(f+"/public")),t.use(this.xPoweredMiddleware),t.use(e.middleware);}setupTemplateEngine(t){t.set("views",m.join(f,"views")),t.set("view engine","ejs"),t.use(at),t.set("layout","layouts/main");}listen(t,e){t.enabled()&&p.info("database: %s",t.directoryPath()),p.info("server: http://localhost:%d",e),console.log(nt.textSync("FAKELAB"));}run(t,e,s,r){let{port:a}=e.serverOpts(r.pathPrefix,r.port);t.listen(a,"localhost",()=>this.listen(s,a)),t.on("close",()=>{p.close();});}xPoweredMiddleware(t,e,s){e.setHeader("x-powered-by","fakelab"),s();}loadLocalEnv(){}};var j=class{static TARGET_LANGUAGE="typescript";static DEFAULT_TYPE_NAME="Interface";static SNAPSHOT_DIR=m.resolve(h,".fakelab/snapshots");static async fetch(t,e){let s=w.jsonInputForTargetLanguage(this.TARGET_LANGUAGE);this.invalidUrl(t)&&(p.error("snapshot url is invalid."),process.exit(1));let a=await(await fetch(t)).text();this.invalidJSON(a)&&(p.error("snapshot response data is invalid json."),process.exit(1));let i=await this.readSnapshotMeta();await s.addSource({name:e||this.suffix((i?.snapshot?.sources||[]).length||0),samples:[a]});let o=new w.InputData;o.addInput(s);let{lines:c}=await w.quicktype({inputData:o,lang:this.TARGET_LANGUAGE,rendererOptions:{"just-types":true}});return c.join(`
10
- `)}static async capture(t,e){let s=await N(),{enabled:r}=s.snapshotOpts();if(!r)return;if(!t)return await this.refetch();let a=await this.fetch(t,e?.name);await this.save(t,a,e),await this.modifyGitignoreFile("./fakelab/*");}static directoryPath(){return this.SNAPSHOT_DIR}static async save(t,e,s){try{if(await d.ensureDir(this.SNAPSHOT_DIR),await d.ensureFile(m.resolve(this.SNAPSHOT_DIR,"__meta.json")),(await this.readSnapshotMeta()).snapshot.sources.some(a=>a.url===t.trim())){if(s?.update){p.info("Updating %s snapshot...",this.snapshotName(t,!1));let a=await this.fetch(t,s.name);await this.write(t,a,s?.name);return}p.warn("%s snapshot is already exists. Use --update flag to update.",t);return}p.info("Saving %s snapshot...",this.snapshotName(t,!1)),await this.write(t,e,s?.name);}catch{}}static async write(t,e,s){await d.writeFile(m.resolve(this.SNAPSHOT_DIR,this.snapshotName(t)),e),await this.updateSnapshotMeta(t,s);}static async refetch(){let t=await this.readSnapshotMeta(),e=[...new Set(t.snapshot.sources||[])];p.info("Updating all snapshots..."),await Promise.all(e.map(async s=>{let r=m.resolve(this.SNAPSHOT_DIR,this.snapshotName(s.url));if(await d.exists(r)){let i=await this.fetch(s.url,s.name);await d.writeFile(r,i);}else {let i=await this.fetch(s.url,s.name);await this.save(s.url,i,{});}}));}static async readSnapshotMeta(){let t={snapshot:{sources:[]}};try{t=await d.readJSON(m.resolve(this.SNAPSHOT_DIR,"__meta.json"));}catch{}return t.snapshot||(t.snapshot={sources:[]}),t.snapshot.sources||(t.snapshot.sources=[]),t}static async updateSnapshotMeta(t,e){let s=await this.readSnapshotMeta(),r=s.snapshot.sources||[],a=r.findIndex(i=>i.url===t);a===-1?r.push({name:e||this.suffix(r.length),url:t}):r.splice(a,1,{...r[a],name:e||r[a].name}),s.snapshot.sources=r,await d.writeJSON(m.resolve(this.SNAPSHOT_DIR,"__meta.json"),s);}static snapshotName(t,e=true){return t.replace(/^https?:\/\//,"").replace(/[\/:?.&=#]/g,"_")+(e?".ts":"")}static suffix(t){return `${this.DEFAULT_TYPE_NAME}${t.toString().toUpperCase()}`}static invalidUrl(t){try{return new URL(t),!1}catch{return true}}static invalidJSON(t){try{return JSON.parse(t),!1}catch{return true}}static async modifyGitignoreFile(t){try{let e=m.resolve(h,".gitignore");if((await d.readFile(e,{encoding:"utf8"})).split(`
11
- `).some(r=>r.trim()===t.trim()))return;await d.appendFile(e,`
12
- ${t}`);}catch{}}};var O=new Command,I=d.readJSONSync(m.join(f,"../package.json"));O.name(I.name).description(I.description).version(I.version);O.command("serve").description("start server").option("-s, --source <char>","config source path").option("-x, --pathPrefix <char>","server url path prefix").option("-p, --port <number>","server port number",parseInt).option("-l, --locale <char>","faker custom locale").action(async n=>{_.init(n).start();});O.command("snapshot").description("snapshot a endpoint json response to a typescript type").argument("[string]","url to snapshot").option("-n, --name <char>","snapshot type name").option("-u, --update","force update the snapshot").action(async(n,t)=>{j.capture(n,t);});O.parse();
7
+ }`),f.appendFile(m.resolve(h,this.config.RUNTIME_DECL_FILENAME),r);}address(e,t){let s=this.normalizePath(d),a=`${e.replace(s,"")}/${t}`;return a.startsWith("/.fakelab")&&(a=a.replace("/.fakelab","")),a}async entities(){let e=await Promise.all(this.__targets.map(async t=>{let s=t.getName().toLowerCase(),r=t.getType(),a=this.normalizePath(t.getSourceFile().getDirectoryPath()),i=t.getSourceFile().getBaseName(),o=this.address(a,i),c=m.resolve(this.database.DATABASE_DIR,`${s}.json`),l=this.address(this.normalizePath(this.database.DATABASE_DIR),m.basename(c)),u=a.includes("/.fakelab/snapshots"),b=await JSONFilePreset(c,[]);return [s,{type:r,filepath:o,snapshot:u,table:b,tablepath:l}]}));return new Map(e.sort((t,s)=>Number(t[1].snapshot)-Number(s[1].snapshot)))}async initFakerLibrary(e){let{faker:t}=await import(`@faker-js/faker/locale/${e.locale}`);return t}};async function g(n,e,t=[],s=0){if(n.isString())return e.string(t[s]);if(n.isNumber())return e.int(t[s]);if(n.isBoolean())return e.bool(t[s]);if(n.isBigInt())return e.bigInt(t[s]);if(n.isBooleanLiteral())return e.litbool(n.getText());if(n.isLiteral())return n.getLiteralValue();if(!n.isUndefined()){if(n.isUnion()){let r=n.getUnionTypes();return await e.union(r.map((a,i)=>g(a,e,t,i)))}if(n.isIntersection()){let r=n.getIntersectionTypes();return await e.intersection(r.map((a,i)=>g(a,e,t,i)))}if(n.isArray()){let r=n.getArrayElementTypeOrThrow();return [await g(r,e,t,s)]}if(n.isObject()){let r=n.getProperties();return await e.object(r,(a,i,o)=>g(a,i,o,s))}return null}}function J({each:n}){return {resolve:async t=>await Promise.all(Array.from({length:t},n))}}async function P(n,e,t){let s=await n.files(e.source),r=new x(s,n,t),a=await r.entities(),i=await r.initFakerLibrary(n.options.faker(e.locale)),o=new v(i);async function c(l,u){let b=J({each:()=>g(l,o)}),F=await(u.count?b.resolve(parseInt(u.count)):g(l,o)),O=JSON.stringify(F,null,2);return {data:F,json:O}}return {entities:a,build:c}}var k=class{constructor(e,t,s){this.builder=e;this.database=t;this.pkg=s;}async handleQueries(e){let t=e.query.count;return t?{count:t.toString()}:{}}index(){return (e,t)=>{t.render("index",{name:null,entities:this.builder.entities,version:this.pkg.version,enabled:this.database.enabled()});}}preview(e){return async(t,s)=>{let r=`${t.protocol}://${t.host}/`,a=t.params.name,i=await this.handleQueries(t),o=q.stringify(i,{addQueryPrefix:true}),c=this.builder.entities.get(a.toLowerCase());if(c){let{json:l}=await this.builder.build(c.type,i),u=c.filepath;s.render("preview",{name:a,suffix:c.snapshot?"(snapshot)":"",filepath:u,address:r,search:o,json:l,prefix:e,entities:this.builder.entities,version:this.pkg.version,enabled:this.database.enabled()});}else s.redirect("/");}}db(){return (e,t)=>{this.database.enabled()?t.render("database",{name:null,entities:this.builder.entities,version:this.pkg.version}):t.redirect("/");}}table(e){return async(t,s)=>{let r=`${t.protocol}://${t.host}/`,a=t.params.name,i=this.builder.entities.get(a.toLowerCase());if(!this.database.enabled())s.redirect("/");else if(i){await i.table.read();let c=i.table.data.length>0,l=JSON.stringify(i.table.data,null,2),u=i.filepath;s.render("table",{name:a,filepath:u,address:r,prefix:e,json:l,hasData:c,entities:this.builder.entities,version:this.pkg.version});}else s.redirect("/database");}}};var A=class{constructor(e,t,s){this.builder=e;this.network=t;this.database=s;}SEED_MERGE_THRESHOLD=1e3;async handleQueries(e){let t=e.query.count;return t?{count:t.toString()}:{}}async applyNetworkHandlers(e){if(this.network.offline()){let{status:t,message:s}=this.network.state("offline");return e.status(t).json({message:s}),true}if(await this.network.wait(),this.network.timeout())return true;if(this.network.error()){let{status:t,message:s}=this.network.state("error");return e.status(t).json({message:s}),true}return false}entity(){return async(e,t)=>{try{if(await this.applyNetworkHandlers(t))return;let s=e.params.name,r=await this.handleQueries(e),a=this.builder.entities.get(s.toLowerCase());if(a){let{data:i}=await this.builder.build(a.type,r);t.status(200).json(i);}else t.status(400).json({message:"The entity is not exists"});}catch(s){t.status(500).send(s);}}}getTable(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});if(await this.applyNetworkHandlers(t))return;let s=e.params.name,r=this.builder.entities.get(s.toLowerCase());r?(await r.table.read(),t.status(200).json(r.table.data)):t.status(400).json({message:`${s} table is not exists`});}catch(s){t.status(500).send(s);}}}updateTable(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});if(await this.applyNetworkHandlers(t))return;let s=e.params.name,r=await this.handleQueries(e),a=this.builder.entities.get(s.toLowerCase());if(a){let{data:i}=await this.builder.build(a.type,r);await a.table.update(o=>o.push(...Array.isArray(i)?i:[i])),t.status(200).json({success:!0});}else t.status(400).json({success:!1,message:`${s} table is not exists`});}catch(s){t.status(500).send(s);}}}insert(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});let s=e.body?.count||1,r=e.body?.strategy||"reset",a=e.params.name,i=this.builder.entities.get(a.toLowerCase());if(i){let{data:o}=await this.builder.build(i.type,{count:s});if(await i.table.read(),r==="once"&&i.table.data.length>0)return t.status(200).json({message:`${a} entity was seeded once before.`});await i.table.update(c=>{r!=="merge"&&(c.length=0);let l=Array.isArray(o)?o:[o];c.length+l.length<this.SEED_MERGE_THRESHOLD&&c.push(...l);}),t.status(200).json({success:!0});}else t.status(400).json({success:!1,message:"The table is not exists"});}catch(s){t.status(500).send(s);}}}flush(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});let s=e.params.name,r=this.builder.entities.get(s.toLowerCase());r?(await r.table.update(a=>a.length=0),t.status(200).json({success:!0})):t.status(400).json({success:!1,message:"The table is not exists"});}catch(s){console.log({error:s}),t.status(500).send(s);}}}_update(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});let s=e.params.name,r=await this.handleQueries(e),a=this.builder.entities.get(s.toLowerCase());if(a){let{data:i}=await this.builder.build(a.type,r);await a.table.update(o=>o.push(i)),t.status(301).redirect(`/database/${s.toLowerCase()}`);}else t.status(400).redirect("/database");}catch{t.status(500).redirect("/database");}}}_clear(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});let s=e.params.name,r=this.builder.entities.get(s.toLowerCase());r?(await r.table.read(),r.table.data.length>0&&await r.table.update(a=>a.length=0),t.status(301).redirect(`/database/${s.toLowerCase()}`)):t.status(400).redirect("/database");}catch{t.status(500).redirect("/database");}}}};var K=f.readJSONSync(m.join(h,"../package.json")),S=class{constructor(e,t,s,r,a){this.router=e;this.config=t;this.network=s;this.database=r;this.serverCLIOptions=a;let{pathPrefix:i}=this.config.options.server(this.serverCLIOptions.pathPrefix,this.serverCLIOptions.port);this.prefix=i;}prefix;async register(){let e=await P(this.config,this.serverCLIOptions,this.database),t=new k(e,this.database,K),s=new A(e,this.network,this.database);this.router.get("/",t.index()),this.router.get("/entities/:name",t.preview(this.prefix)),this.router.get("/database",t.db()),this.router.get("/database/:name",t.table(this.prefix)),this.router.get(`/${this.prefix}/:name`,s.entity()),this.router.get(`/${this.prefix}/database/:name`,s.getTable()),this.router.post(`/${this.prefix}/database/:name`,s.updateTable()),this.router.post(`/${this.prefix}/database/insert/:name`,s.insert()),this.router.post(`/${this.prefix}/database/flush/:name`,s.flush()),this.router.post("/__update/:name",s._update()),this.router.post("/__delete/:name",s._clear());}};var D=class n{constructor(e){this.config=e;this.options=this.config.options.network(),this.timeout=this.timeout.bind(this),this.error=this.error.bind(this),this.state=this.state.bind(this),this.middleware=this.middleware.bind(this),this.wait=this.wait.bind(this),this.offline=this.offline.bind(this);}options;static initHandlers(e){return new n(e)}timeout(){let e=this.chance(this.options?.timeoutRate);return e&&p.debug("Network timeout..."),e}error(){let e=this.chance(this.options?.errorRate);return e&&p.debug("Network error..."),e}state(e){switch(e){case "error":return {status:500,message:"Network error"};case "offline":return {status:503,message:"Network offline"};default:return {status:500,message:"Unknown error"}}}async wait(){let e=this.resolveDelay();e>0&&(p.debug("Network waiting (%d ms)...",e),await this.sleep(e));}offline(){return this.options?.offline??false}middleware(e,t,s){let r=this.options?.errorRate||0,a=this.options?.timeoutRate||0,i=this.options?.offline??false,o=`delay=${this.resolveDelay()},error=${r},timeout=${a},offline=${i}`;t.setHeader("X-Fakelab-Network",o),s();}resolveDelay(){let e=this.options?.delay;return typeof e=="number"?Math.round(e):Array.isArray(e)?this.random(e):0}chance(e=0){return Math.random()<e}random([e,t]){return Math.round(Math.random()*(t-e)+e)}sleep(e){return new Promise(t=>setTimeout(t,e))}};async function T(){try{let e=await new V().resolve({files:["fakelab.config.ts"]});return e||(p.error("No fakelab config file is detected."),process.exit(1)),(await bundleRequire({filepath:e})).mod.default}catch{p.error("Could not load the config file."),process.exit(1);}}var R=class n{constructor(e){this.config=e;this.enabled=this.enabled.bind(this),this.options=this.config.options.database(),this.options.enabled||f.rmSync(this.DATABASE_DIR,{force:true,recursive:true});}options;DATABASE_DIR=m.resolve(d,".fakelab/db");static register(e){return new n(e)}enabled(){return this.options.enabled??false}async initialize(){if(this.enabled())try{await f.ensureDir(this.DATABASE_DIR),await this.modifyGitignoreFile(".fakelab/*");}catch{p.error("Could not create database.");}}async modifyGitignoreFile(e){try{let t=m.resolve(d,".gitignore");if((await f.readFile(t,{encoding:"utf8"})).split(`
8
+ `).some(r=>r.trim()===e.trim()))return;await f.appendFile(t,`
9
+ ${e}`);}catch{}}};var C=class n{constructor(e){this.serverCLIOptions=e;this.start=this.start.bind(this),this.xPoweredMiddleware=this.xPoweredMiddleware.bind(this),this.setupApplication=this.setupApplication.bind(this),this.setupTemplateEngine=this.setupTemplateEngine.bind(this),this.loadLocalEnv();}static init(e){return new n(e)}async start(){let e=L(),t=L.Router(),s=ee.createServer(e),r=await T(),a=D.initHandlers(r),i=R.register(r);this.setupApplication(e,a),this.setupTemplateEngine(e),await r.generateInFileRuntimeConfig(h,this.serverCLIOptions),await i.initialize(),await new S(t,r,a,i,this.serverCLIOptions).register(),e.use(t),this.run(s,r,i,this.serverCLIOptions);}setupApplication(e,t){e.disable("x-powered-by"),e.use(L.json()),e.use(X({methods:"GET"})),e.use(L.static(h+"/public")),e.use(this.xPoweredMiddleware),e.use(t.middleware);}setupTemplateEngine(e){e.set("views",m.join(h,"views")),e.set("view engine","ejs"),e.use(Z),e.set("layout","layouts/main");}listen(e,t){e.enabled()&&p.info("database: %s",e.DATABASE_DIR),p.info("server listening to http://localhost:%d",t),console.log(te.textSync("FAKELAB"));}run(e,t,s,r){let{port:a}=t.options.server(r.pathPrefix,r.port);e.listen(a,"localhost",()=>this.listen(s,a));}xPoweredMiddleware(e,t,s){t.setHeader("x-powered-by","fakelab"),s();}loadLocalEnv(){}};var I=class n{constructor(e,t){this.url=e;this.options=t;}TARGET_LANGUAGE="typescript";DEFAULT_TYPE_NAME="Interface";SNAPSHOT_DIR=m.resolve(d,".fakelab/snapshots");static init(e,t){return new n(e,t)}async fetch(e,t){this.invalidUrl(e)&&(p.error("snapshot url is invalid."),process.exit(1));let r=await(await fetch(e)).text();this.invalidJSON(r)&&(p.error("snapshot response data is invalid json."),process.exit(1));let a=await this.readSnapshotMeta(),i=await import('quicktype-core'),o=i.jsonInputForTargetLanguage(this.TARGET_LANGUAGE);await o.addSource({name:t||this.suffix((a?.snapshot?.sources||[]).length||0),samples:[r]});let c=new i.InputData;c.addInput(o);let{lines:l}=await i.quicktype({inputData:c,lang:this.TARGET_LANGUAGE,rendererOptions:{"just-types":true}});return l.join(`
10
+ `)}async capture(){if(!(await T()).options.snapshot().enabled)return;if(!this.url)return await this.refetch();let t=await this.fetch(this.url,this.options?.name);await this.save(this.url,t,this.options),await this.modifyGitignoreFile(".fakelab/*");}async save(e,t,s){try{if(await f.ensureDir(this.SNAPSHOT_DIR),await f.ensureFile(m.resolve(this.SNAPSHOT_DIR,"__meta.json")),(await this.readSnapshotMeta()).snapshot.sources.some(a=>a.url===e.trim())){if(s?.update){p.info("Updating %s snapshot...",this.snapshotName(e,!1));let a=await this.fetch(e,s.name);await this.write(e,a,s?.name);return}p.warn("%s snapshot is already exists. Use --update flag to update.",e);return}p.info("Saving %s snapshot...",this.snapshotName(e,!1)),await this.write(e,t,s?.name);}catch{}}async write(e,t,s){await f.writeFile(m.resolve(this.SNAPSHOT_DIR,this.snapshotName(e)),t),await this.updateSnapshotMeta(e,s);}async refetch(){let e=await this.readSnapshotMeta(),t=[...new Set(e.snapshot.sources||[])];p.info("Updating all snapshots..."),await Promise.all(t.map(async s=>{let r=m.resolve(this.SNAPSHOT_DIR,this.snapshotName(s.url));if(await f.exists(r)){let i=await this.fetch(s.url,s.name);await f.writeFile(r,i);}else {let i=await this.fetch(s.url,s.name);await this.save(s.url,i,{});}}));}async readSnapshotMeta(){let e={snapshot:{sources:[]}};try{e=await f.readJSON(m.resolve(this.SNAPSHOT_DIR,"__meta.json"));}catch{}return e.snapshot||(e.snapshot={sources:[]}),e.snapshot.sources||(e.snapshot.sources=[]),e}async updateSnapshotMeta(e,t){let s=await this.readSnapshotMeta(),r=s.snapshot.sources||[],a=r.findIndex(i=>i.url===e);a===-1?r.push({name:t||this.suffix(r.length),url:e}):r.splice(a,1,{...r[a],name:t||r[a].name}),s.snapshot.sources=r,await f.writeJSON(m.resolve(this.SNAPSHOT_DIR,"__meta.json"),s);}snapshotName(e,t=true){return e.replace(/^https?:\/\//,"").replace(/[\/:?.&=#]/g,"_")+(t?".ts":"")}suffix(e){return `${this.DEFAULT_TYPE_NAME}${e.toString().toUpperCase()}`}invalidUrl(e){try{return new URL(e),!1}catch{return true}}invalidJSON(e){try{return JSON.parse(e),!1}catch{return true}}async modifyGitignoreFile(e){try{let t=m.resolve(d,".gitignore");if((await f.readFile(t,{encoding:"utf8"})).split(`
11
+ `).some(r=>r.trim()===e.trim()))return;await f.appendFile(t,`
12
+ ${e}`);}catch{}}};var N=new Command,_=f.readJSONSync(m.join(h,"../package.json"));N.name(_.name).description(_.description).version(_.version);N.command("serve").description("start server").option("-s, --source <char>","config source path").option("-x, --pathPrefix <char>","server url path prefix").option("-p, --port <number>","server port number",parseInt).option("-l, --locale <char>","faker custom locale").action(async n=>{C.init(n).start();});N.command("snapshot").description("snapshot a endpoint json response to a typescript type").argument("[string]","url to snapshot").option("-n, --name <char>","snapshot type name").option("-u, --update","force update the snapshot").action(async(n,e)=>{I.init(n,e).capture();});N.parse();
package/lib/main.d.ts CHANGED
@@ -97,26 +97,32 @@ type ConfigOptions = {
97
97
  sourcePath: string | string[];
98
98
  /**
99
99
  * Server-related configuration.
100
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/server-command|Server Documentation}
100
101
  */
101
102
  server?: ServerOptions;
102
103
  /**
103
104
  * Faker engine configuration for data generation.
105
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/faker-annotations|Faker Documentation}
104
106
  */
105
107
  faker?: FakerEngineOptions;
106
108
  /**
107
109
  * Database persistence configuration.
110
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/database-mode|Database Documentation}
108
111
  */
109
112
  database?: DatabaseOptions;
110
113
  /**
111
114
  * Browser runtime exposure options.
115
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/runtime-api|Browser Documentation}
112
116
  */
113
117
  browser?: BrowserOptions;
114
118
  /**
115
119
  * Network simulation configuration.
120
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/network-simulation|Network Documentation}
116
121
  */
117
122
  network?: NetworkOptions;
118
123
  /**
119
124
  * Snapshot configuration.
125
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/snapshot|Snapshot Documentation}
120
126
  */
121
127
  snapshot?: SnapshotOptions;
122
128
  };
@@ -128,19 +134,27 @@ type ServerCLIOptions = {
128
134
  };
129
135
 
130
136
  declare class Config {
131
- private readonly opts;
137
+ private readonly configOptions;
132
138
  readonly RUNTIME_SOURCE_FILENAME = "runtime.js";
133
139
  readonly RUNTIME_DECL_FILENAME = "runtime.d.ts";
134
140
  readonly FAKELAB_PERSIST_DIR = ".fakelab";
135
141
  NETWORK_DEFAULT_OPTIONS: Readonly<NetworkOptions>;
136
- constructor(opts: ConfigOptions);
142
+ constructor(configOptions: ConfigOptions);
143
+ get options(): {
144
+ server: (prefix?: string, port?: number) => Required<ServerOptions>;
145
+ browser: (name?: string, mode?: "module" | "global") => Required<BrowserOptions>;
146
+ database: () => Required<DatabaseOptions>;
147
+ network: () => NetworkOptions;
148
+ snapshot: () => Required<SnapshotOptions>;
149
+ faker: (locale?: FakerLocale) => Required<FakerEngineOptions>;
150
+ };
151
+ private _serverOptions;
152
+ private _browserOptions;
153
+ private _databaseOptions;
154
+ private _networkOptions;
155
+ private _snapshotOptions;
156
+ private _fakerOptions;
137
157
  files(_sourcePath?: string): Promise<string[]>;
138
- serverOpts(prefix?: string, port?: number): Required<ServerOptions>;
139
- browserOpts(name?: string, mode?: "module" | "global"): Required<BrowserOptions>;
140
- databaseOpts(): Required<DatabaseOptions>;
141
- networkOpts(): NetworkOptions;
142
- snapshotOpts(): Required<SnapshotOptions>;
143
- fakerOpts(locale?: FakerLocale): Required<FakerEngineOptions>;
144
158
  generateInFileRuntimeConfig(dirname: string, options: ServerCLIOptions): Promise<void>;
145
159
  private getSnapshotSourceFiles;
146
160
  private tryStat;
package/lib/main.js CHANGED
@@ -1,4 +1,4 @@
1
- import y from'fast-glob';import i from'path';import F from'fs-extra';import {stat,access,constants}from'fs/promises';import M from'is-glob';import c from'winston';import f from'picocolors';import {transform}from'esbuild';import {fileURLToPath}from'url';function x(o){switch(o){case "info":return f.blueBright(o.toUpperCase());case "error":return f.redBright(o.toUpperCase());case "warn":return f.yellowBright(o.toUpperCase());default:return o.toUpperCase()}}var S=c.format.printf(({level:o,message:t,timestamp:e})=>`${f.dim(`[${e}]`)} ${x(o)} ${t}`),l=c.createLogger({format:c.format.combine(c.format.timestamp(),c.format.splat(),S),transports:[new c.transports.Console]}),C=new Intl.ListFormat("en",{style:"long",type:"unit"}),a=class{static info(t,...e){return l.info(t,...e)}static warn(t,...e){return l.warn(t,...e)}static error(t,...e){return l.error(t,...e)}static debug(t,...e){if(!(typeof process>"u"||!process.env.DEBUG))return l.info(t,...e)}static list(t){return C.format(t)}static close(){l.removeAllListeners(),l.close();}};var g="fakelab",w="module",h=["af","ar","az","bn","cs","cy","da","de","dv","el","en","eo","es","fa","fi","fr","he","hr","hu","hy","id","it","ja","ka","ko","ku","lv","mk","nb","ne","nl","pl","pt","ro"];function O(){let o=Intl.DateTimeFormat().resolvedOptions().locale;if(!o)return "en";let[t]=o.split("-"),e=t.toLowerCase();return h.includes(e)?e:"en"}var T=`let fl = {};
1
+ import b from'fast-glob';import i from'path';import _ from'fs-extra';import {stat,access,constants}from'fs/promises';import L from'is-glob';import c from'picocolors';import {transform}from'esbuild';import {fileURLToPath}from'url';var N=new Intl.ListFormat("en",{style:"long",type:"unit"}),s=class{static label(e){switch(e){case "info":return c.blueBright(e.toUpperCase());case "error":return c.redBright(e.toUpperCase());case "warn":return c.yellowBright(e.toUpperCase())}}static log(e,t){return [c.dim(`[${new Date().toISOString()}]`),this.label(e),t].join(" ")}static info(e,...t){console.log(this.log("info",e),...t);}static warn(e,...t){console.log(this.log("warn",e),...t);}static error(e,...t){console.error(this.log("error",e),...t);}static debug(e,...t){typeof process>"u"||!process.env.DEBUG||console.log(this.log("info",e),...t);}static list(e){return N.format(e)}};var E="fakelab",g="module",m=["af","ar","az","bn","cs","cy","da","de","dv","el","en","eo","es","fa","fi","fr","he","hr","hu","hy","id","it","ja","ka","ko","ku","lv","mk","nb","ne","nl","pl","pt","ro"];function O(){let n=Intl.DateTimeFormat().resolvedOptions().locale;if(!n)return "en";let[e]=n.split("-"),t=e.toLowerCase();return m.includes(t)?t:"en"}var y=`let fl = {};
2
2
  let db = {};
3
3
  fl.url = () => "http://localhost:PORT/PREFIX/";
4
4
  fl.fetch = async function (name, count) {
@@ -15,6 +15,8 @@ fl.fetch = async function (name, count) {
15
15
 
16
16
  db.enabled = () => ENABLED_COND;
17
17
 
18
+ db.type$ = function (name) {}
19
+
18
20
  db.get = async function (name, predicate) {
19
21
  const response = await fetch(NAME.url() + "database/" + name);
20
22
 
@@ -47,7 +49,8 @@ db.flush = async function (name) {
47
49
  const NAME = Object.freeze(fl);
48
50
  const database = Object.freeze(db);
49
51
 
50
- export { NAME, database };`,A=`declare function fetch<T extends keyof Runtime$, C extends number | undefined = undefined>(name: T, count?: C): Promise<Result$<Runtime$[T], C>>;
52
+ export { NAME, database };`,w=`declare function fetch<T extends keyof Runtime$, C extends number | undefined = undefined>(name: T, count?: C): Promise<Result$<Runtime$[T], C>>;
53
+ declare function type$<T extends keyof Runtime$>(): Runtime$[T];
51
54
  declare function get<T extends keyof Runtime$>(name: T): Promise<Array<Runtime$[T]>>;
52
55
  declare function get<T extends keyof Runtime$>(name: T, predicate: (value: Runtime$[T]) => boolean): Promise<Runtime$[T] | null>;
53
56
  declare function post<T extends keyof Runtime$>(name: T): Promise<void>;
@@ -84,7 +87,10 @@ type SeedOptions = {
84
87
  type Result$<T, CT> = CT extends number ? (CT extends 0 ? T : T[]) : T;
85
88
  interface Runtime$ {}
86
89
 
87
- export { NAME, database };`,R=`global.NAME = {};
90
+ type Typeof<T extends keyof Runtime$> = ReturnType<typeof type$<T>>
91
+
92
+ export type { Typeof }
93
+ export { NAME, database };`,T=`global.NAME = {};
88
94
  global.NAME.database = {};
89
95
  global.NAME.url = () => "http://localhost:PORT/PREFIX/";
90
96
  global.NAME.fetch = async function (name, count) {
@@ -131,7 +137,7 @@ global.NAME.database.flush = async function (name) {
131
137
  const NAME = Object.freeze(fl);
132
138
  const database = Object.freeze(db);
133
139
 
134
- export { NAME, database };`,k=`export {};
140
+ export { NAME, database };`,A=`export {};
135
141
 
136
142
  declare global {
137
143
  const database: {
@@ -173,4 +179,4 @@ declare global {
173
179
  NAME: typeof NAME;
174
180
  }
175
181
  }
176
- }`;var E=class{constructor(t,e,r,s){this.port=t;this.prefix=e;this.browserOptions=r;this.dbOptions=s;this.name=this.browserOptions?.expose?.name||"fakelab",this.databaseEnabled=this.dbOptions?.enabled??true?"true":"false";}name;databaseEnabled;transformOptions={minify:true,platform:"browser",target:"es2022"};replacer(t,e){let r=t;for(let s in e)r=r.replace(new RegExp(s,"g"),e[s].toString().trim());return r}async prepareGlobalSource(){let t=this.replacer(R,{NAME:this.name,PORT:this.port,PREFIX:this.prefix,ENABLED_COND:this.databaseEnabled});try{let{code:e}=await transform(t,this.transformOptions);return e}catch(e){return e instanceof Error&&a.warn(e.message),t}}globalDeclaration(){return this.replacer(k,{NAME:this.name})}},u=class o extends E{constructor(e,r,s,p){super(e,r,s,p);this.port=e;this.prefix=r;this.browserOptions=s;this.dbOptions=p;}static init(e,r,s,p){let b=new o(e,r,s,p);return new Proxy(b,{get(n,d){return d==="prepareSource"?s?.expose?.mode==="global"?n.prepareGlobalSource:n.prepareSource:d==="declaration"?s?.expose?.mode==="global"?n.globalDeclaration:n.declaration:n[d]}})}async prepareSource(){let e=this.replacer(T,{NAME:this.name,PORT:this.port,PREFIX:this.prefix,ENABLED_COND:this.databaseEnabled});try{let{code:r}=await transform(e,this.transformOptions);return r}catch(r){return r instanceof Error&&a.warn(r.message),e}}declaration(){return this.replacer(A,{NAME:this.name})}};i.dirname(fileURLToPath(import.meta.url));var L=process.cwd();var m=class{constructor(t){this.opts=t;this.files=this.files.bind(this),this.serverOpts=this.serverOpts.bind(this),this.fakerOpts=this.fakerOpts.bind(this),this.browserOpts=this.browserOpts.bind(this),this.NETWORK_DEFAULT_OPTIONS=Object.freeze({delay:this.opts.network?.delay||0,errorRate:this.opts.network?.errorRate||0,timeoutRate:this.opts.network?.timeoutRate||0,offline:this.opts.network?.offline??false});}RUNTIME_SOURCE_FILENAME="runtime.js";RUNTIME_DECL_FILENAME="runtime.d.ts";FAKELAB_PERSIST_DIR=".fakelab";NETWORK_DEFAULT_OPTIONS;async files(t){let e=this.resolveSourcePath(t||this.opts.sourcePath),r=Array.from(new Set((await Promise.all(e.map(s=>this.resolveTSFiles(s)))).flat()));if(this.serverOpts().includeSnapshots){let s=await this.getSnapshotSourceFiles();r.push(...s);}return r.length===0&&(a.error("No Typescript files found in: %s",a.list(e.map(s=>i.basename(s)))),process.exit(1)),r}serverOpts(t,e){return {pathPrefix:t||this.opts.server?.pathPrefix||"api",port:e||this.opts.server?.port||5200,includeSnapshots:this.opts.server?.includeSnapshots??true}}browserOpts(t,e){return {expose:{mode:e||this.opts.browser?.expose?.mode||w,name:t||this.opts.browser?.expose?.name||g}}}databaseOpts(){return {enabled:this.opts.database?.enabled??false}}networkOpts(){let t=this.opts.network?.preset,e=this.opts.network?.presets??{};return !t||!e[t]?this.NETWORK_DEFAULT_OPTIONS:{...e[t],...this.opts.network??{}}}snapshotOpts(){return {enabled:this.opts.snapshot?.enabled??false}}fakerOpts(t){let e=(t||this.opts.faker?.locale)?.toLowerCase();return e&&h.includes(e)?{locale:e}:{locale:O()}}async generateInFileRuntimeConfig(t,e){let{port:r,pathPrefix:s}=this.serverOpts(e.pathPrefix,e.port),p=i.resolve(t,this.RUNTIME_SOURCE_FILENAME),b=i.resolve(t,this.RUNTIME_DECL_FILENAME),n=u.init(r,s,this.opts.browser,this.opts.database),d=await n.prepareSource();await Promise.all([F.writeFile(p,d),F.writeFile(b,n.declaration())]);}async getSnapshotSourceFiles(){let t=await y(".fakelab/snapshots/**/*.ts",{absolute:true,ignore:["**/*.d.ts"],cwd:L});return t.length>0&&a.info("snapshot(s): %s",a.list(t.map(e=>i.parse(e).name))),t}async tryStat(t){try{return await stat(t)}catch{return null}}async isReadable(t){try{return await access(t,constants.R_OK),!0}catch{return false}}resolveSourcePath(t){return (Array.isArray(t)?t:[t]).map(r=>M(r,{strict:true})?r:i.resolve(r))}async resolveTSFiles(t){if(M(t,{strict:true}))return a.info("source: %s",t),y(t,{absolute:true,ignore:["**/*.d.ts"]});let e=i.resolve(t),r=e.endsWith(".ts")?e:e+".ts";return (await this.tryStat(r))?.isFile()?(await this.isReadable(r)||(a.error("Cannot read file: %s",r),process.exit(1)),a.info("source: %s",r),[r]):(await this.tryStat(e))?.isDirectory()?(a.info("source: %s",e),y("**/*.ts",{cwd:e,absolute:true,ignore:["**/*.d.ts"]})):(a.warn("invalid source: [REDACTED]/%s",i.basename(r)),[])}};function B(o){return new m(o)}export{B as defineConfig};
182
+ }`;var h=class{constructor(e,t,r,o){this.port=e;this.prefix=t;this.browserOptions=r;this.dbOptions=o;this.name=this.browserOptions?.expose?.name||"fakelab",this.databaseEnabled=this.dbOptions?.enabled??true?"true":"false";}name;databaseEnabled;transformOptions={minify:true,platform:"browser",target:"es2022"};replacer(e,t){let r=e;for(let o in t)r=r.replace(new RegExp(o,"g"),t[o].toString().trim());return r}async prepareGlobalSource(){let e=this.replacer(T,{NAME:this.name,PORT:this.port,PREFIX:this.prefix,ENABLED_COND:this.databaseEnabled});try{let{code:t}=await transform(e,this.transformOptions);return t}catch(t){return t instanceof Error&&s.warn(t.message),e}}globalDeclaration(){return this.replacer(A,{NAME:this.name})}},d=class n extends h{constructor(t,r,o,p){super(t,r,o,p);this.port=t;this.prefix=r;this.browserOptions=o;this.dbOptions=p;}static init(t,r,o,p){let u=new n(t,r,o,p);return new Proxy(u,{get(a,l){return l==="prepareSource"?o?.expose?.mode==="global"?a.prepareGlobalSource:a.prepareSource:l==="declaration"?o?.expose?.mode==="global"?a.globalDeclaration:a.declaration:a[l]}})}async prepareSource(){let t=this.replacer(y,{NAME:this.name,PORT:this.port,PREFIX:this.prefix,ENABLED_COND:this.databaseEnabled});try{let{code:r}=await transform(t,this.transformOptions);return r}catch(r){return r instanceof Error&&s.warn(r.message),t}}declaration(){return this.replacer(w,{NAME:this.name})}};i.dirname(fileURLToPath(import.meta.url));var k=process.cwd();var f=class{constructor(e){this.configOptions=e;this.files=this.files.bind(this),this._serverOptions=this._serverOptions.bind(this),this._browserOptions=this._browserOptions.bind(this),this._databaseOptions=this._databaseOptions.bind(this),this._networkOptions=this._networkOptions.bind(this),this._snapshotOptions=this._snapshotOptions.bind(this),this._fakerOptions=this._fakerOptions.bind(this),this.NETWORK_DEFAULT_OPTIONS=Object.freeze({delay:this.configOptions.network?.delay||0,errorRate:this.configOptions.network?.errorRate||0,timeoutRate:this.configOptions.network?.timeoutRate||0,offline:this.configOptions.network?.offline??false});}RUNTIME_SOURCE_FILENAME="runtime.js";RUNTIME_DECL_FILENAME="runtime.d.ts";FAKELAB_PERSIST_DIR=".fakelab";NETWORK_DEFAULT_OPTIONS;get options(){return {server:this._serverOptions,browser:this._browserOptions,database:this._databaseOptions,network:this._networkOptions,snapshot:this._snapshotOptions,faker:this._fakerOptions}}_serverOptions(e,t){return {pathPrefix:e||this.configOptions.server?.pathPrefix||"api",port:t||this.configOptions.server?.port||5200,includeSnapshots:this.configOptions.server?.includeSnapshots??true}}_browserOptions(e,t){return {expose:{mode:t||this.configOptions.browser?.expose?.mode||g,name:e||this.configOptions.browser?.expose?.name||E}}}_databaseOptions(){return {enabled:this.configOptions.database?.enabled??false}}_networkOptions(){let e=this.configOptions.network?.preset,t=this.configOptions.network?.presets??{};return !e||!t[e]?this.NETWORK_DEFAULT_OPTIONS:{...t[e],...this.configOptions.network??{}}}_snapshotOptions(){return {enabled:this.configOptions.snapshot?.enabled??false}}_fakerOptions(e){let t=(e||this.configOptions.faker?.locale)?.toLowerCase();return t&&m.includes(t)?{locale:t}:{locale:O()}}async files(e){let t=this.resolveSourcePath(e||this.configOptions.sourcePath),r=Array.from(new Set((await Promise.all(t.map(o=>this.resolveTSFiles(o)))).flat()));if(this._serverOptions().includeSnapshots){let o=await this.getSnapshotSourceFiles();r.push(...o);}return r.length===0&&(s.error("No Typescript files found in: %s",s.list(t.map(o=>i.basename(o)))),process.exit(1)),r}async generateInFileRuntimeConfig(e,t){let{port:r,pathPrefix:o}=this._serverOptions(t.pathPrefix,t.port),p=i.resolve(e,this.RUNTIME_SOURCE_FILENAME),u=i.resolve(e,this.RUNTIME_DECL_FILENAME),a=d.init(r,o,this.configOptions.browser,this.configOptions.database),l=await a.prepareSource();await Promise.all([_.writeFile(p,l),_.writeFile(u,a.declaration())]);}async getSnapshotSourceFiles(){let e=await b(".fakelab/snapshots/**/*.ts",{absolute:true,ignore:["**/*.d.ts"],cwd:k});return e.length>0&&s.info("snapshot(s): %s",s.list(e.map(t=>i.parse(t).name))),e}async tryStat(e){try{return await stat(e)}catch{return null}}async isReadable(e){try{return await access(e,constants.R_OK),!0}catch{return false}}resolveSourcePath(e){return (Array.isArray(e)?e:[e]).map(r=>L(r,{strict:true})?r:i.resolve(r))}async resolveTSFiles(e){if(L(e,{strict:true}))return s.info("source: %s",e),b(e,{absolute:true,ignore:["**/*.d.ts"]});let t=i.resolve(e),r=t.endsWith(".ts")?t:t+".ts";return (await this.tryStat(r))?.isFile()?(await this.isReadable(r)||(s.error("Cannot read file: %s",r),process.exit(1)),s.info("source: %s",r),[r]):(await this.tryStat(t))?.isDirectory()?(s.info("source: %s",t),b("**/*.ts",{cwd:t,absolute:true,ignore:["**/*.d.ts"]})):(s.warn("invalid source: [REDACTED]/%s",i.basename(r)),[])}};function D(n){return new f(n)}export{D as defineConfig};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fakelab",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A easy-config mock server for frontend developers.",
@@ -34,9 +34,9 @@
34
34
  },
35
35
  "scripts": {
36
36
  "build": "tsc && tsup",
37
+ "prepack": "yarn build",
37
38
  "release": "release-it --ci",
38
- "serve": "cross-env NODE_ENV=development yarn build && node lib/cli.js serve",
39
- "snapshot": "yarn build && node lib/cli.js snapshot"
39
+ "example": "node --import=tsx scripts/example.ts"
40
40
  },
41
41
  "directories": {
42
42
  "lib": "./lib"
@@ -63,6 +63,8 @@
63
63
  "@types/is-glob": "^4.0.4",
64
64
  "@types/node": "^24.10.1",
65
65
  "cross-env": "^10.1.0",
66
+ "execa": "^9.6.1",
67
+ "inquirer": "^13.1.0",
66
68
  "release-it": "^19.0.6",
67
69
  "tsup": "^8.5.1",
68
70
  "tsx": "^4.21.0",
@@ -87,8 +89,7 @@
87
89
  "picocolors": "^1.1.1",
88
90
  "qs": "^6.14.0",
89
91
  "quicktype-core": "23.2.6",
90
- "ts-morph": "^27.0.2",
91
- "winston": "^3.19.0"
92
+ "ts-morph": "^27.0.2"
92
93
  },
93
94
  "bin": {
94
95
  "fakelab": "bin/run.js"