fakelab 0.0.22 → 0.0.24

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
@@ -15,7 +17,6 @@
15
17
 
16
18
  Check out the [React + TypeScript + Vite example](./examples/react-typescript-vite) to see Fakelab in action!
17
19
 
18
-
19
20
  ### Quick Demo
20
21
 
21
22
  1. Define your types with Faker annotations:
@@ -43,8 +44,8 @@ export interface User {
43
44
  import { defineConfig } from "fakelab";
44
45
 
45
46
  export default defineConfig({
46
- sourcePath: ["./fixtures/**/*.ts"],
47
- server: { port: 8080 },
47
+ sourcePath: ["./fixtures"],
48
+ server: { port: 50001 },
48
49
  });
49
50
  ```
50
51
 
@@ -63,16 +64,11 @@ Or start the server manually:
63
64
  `./examples/react-typescript-vite`:
64
65
 
65
66
  ```bash
66
- npx fakelab serve
67
- ```
68
-
69
- 4. Use in your frontend:
70
-
71
- ```typescript
72
- import { fakelab } from "fakelab/browser";
73
-
74
- const users = await fakelab.fetch("User", 10);
75
- console.log(users); // Array of 10 mock users
67
+ npm run serve
68
+ # or
69
+ yarn serve
70
+ # or
71
+ pnpm run serve
76
72
  ```
77
73
 
78
74
  ## Installation
@@ -93,11 +89,10 @@ create `fakelab.config.ts` file in the project root. and reference your typescri
93
89
  import { defineConfig } from "fakelab";
94
90
 
95
91
  export default defineConfig({
96
- sourcePath: ["./fixtures"],
97
- server: { port: 50001 },
98
- network: { delay: [500, 1500] },
92
+ sourcePath: ["./types", "./fixtures/**/*.ts"], // supports glob pattern
93
+ faker: { locale: "en" }, // optional
94
+ server: { pathPrefix: "api/v1", port: 8080 }, // optional
99
95
  });
100
-
101
96
  ```
102
97
 
103
98
  ## Faker Annotations
@@ -189,15 +184,6 @@ import { fakelab } from "fakelab/browser";
189
184
 
190
185
  const users = await fakelab.fetch("User", 10);
191
186
 
192
- console.log(users);
193
-
194
- // or
195
-
196
- // can be enabled as a global object
197
- import "fakelab/browser";
198
-
199
- const users = await fakelab.fetch("User", 10);
200
-
201
187
  console.log(users);
202
188
  ```
203
189
 
package/lib/cli.js CHANGED
@@ -1,12 +1,10 @@
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=`
1
+ import m from'path';import d from'fs-extra';import {Command}from'commander';import R from'express';import X from'cors';import Z from'express-ejs-layouts';import tt from'http';import {Project}from'ts-morph';import y from'picocolors';import {JSONFilePreset}from'lowdb/node';import {fileURLToPath}from'url';import q from'qs';import et 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(t){switch(t){case "info":return y.blueBright(t.toUpperCase());case "error":return y.redBright(t.toUpperCase());case "warn":return y.yellowBright(t.toUpperCase())}}static log(t,e){return [y.dim(`[${new Date().toISOString()}]`),this.label(t),e].join(" ")}static info(t,...e){console.log(this.log("info",t),...e);}static warn(t,...e){console.log(this.log("warn",t),...e);}static error(t,...e){console.error(this.log("error",t),...e);}static debug(t,...e){typeof process>"u"||!process.env.DEBUG||console.log(this.log("info",t),...e);}static list(t){return M.format(t)}};var w=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 h=m.dirname(fileURLToPath(import.meta.url)),f=process.cwd();var v=class{constructor(t,e){this.files=t;this.database=e;let r=new Project({tsConfigFilePath:"tsconfig.json"}).addSourceFilesAtPaths(t);this.__targets=r.flatMap(a=>{let i=a.getInterfaces(),o=a.getTypeAliases(),c=a.getExportDeclarations().flatMap(l=>l.getNamedExports().flatMap(u=>u.getLocalTargetDeclarations()));return [...i,...o,...c]}),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 s=`
2
2
  interface Runtime$ {
3
- ${t.join(`
3
+ ${[...new Set(t.map(r=>{let a=r.getName(),i=r.getSourceFile().getFilePath();return `${a}: import("${i}").${a}`}))].join(`
4
4
  `)}
5
- }`;s.mode==="global"&&(r=`
6
- declare global {${r}
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();
5
+ }`;d.appendFile(m.resolve(h,"runtime.d.ts"),s);}address(t,e){let s=this.normalizePath(f),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=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"),N=await JSONFilePreset(c,[]);return [s,{type:r,filepath:o,snapshot:u,table:N,tablepath:l}]}));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 g(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)=>g(a,t,e,i)))}if(n.isIntersection()){let r=n.getIntersectionTypes();return await t.intersection(r.map((a,i)=>g(a,t,e,i)))}if(n.isArray()){let r=n.getArrayElementTypeOrThrow();return [await g(r,t,e,s)]}if(n.isObject()){let r=n.getProperties();return await t.object(r,(a,i,o)=>g(a,i,o,s))}return null}}function J({each:n}){return {resolve:async e=>await Promise.all(Array.from({length:e},n))}}async function _(n,t,e){let s=await n.files(t.source),r=new v(s,e),a=await r.entities(),i=await r.initFakerLibrary(n.options.faker(t.locale)),o=new w(i);async function c(l,u){let N=J({each:()=>g(l,o)}),P=await(u.count?N.resolve(parseInt(u.count)):g(l,o)),O=JSON.stringify(P,null,2);return {data:P,json:O}}return {entities:a,build:c}}var x=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=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: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 k=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 K=d.readJSONSync(m.join(h,"../package.json")),A=class{constructor(t,e,s,r,a){this.router=t;this.config=e;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 t=await _(this.config,this.serverCLIOptions,this.database),e=new x(t,this.database,K),s=new k(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 S=class n{constructor(t){this.config=t;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(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 D(){try{let t=await new V().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 E=class n{constructor(t){this.config=t;this.enabled=this.enabled.bind(this),this.options=this.config.options.database(),this.options.enabled||d.rmSync(this.DATABASE_DIR,{force:true,recursive:true});}options;DATABASE_DIR=m.resolve(f,".fakelab/db");static register(t){return new n(t)}enabled(){return this.options.enabled??false}async initialize(){if(this.enabled())try{await d.ensureDir(this.DATABASE_DIR),await this.modifyGitignoreFile(".fakelab/*");}catch{p.error("Could not create database.");}}async modifyGitignoreFile(t){try{let e=m.resolve(f,".gitignore");if((await d.readFile(e,{encoding:"utf8"})).split(`
6
+ `).some(r=>r.trim()===t.trim()))return;await d.appendFile(e,`
7
+ ${t}`);}catch{}}};var L=class n{constructor(t){this.serverCLIOptions=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=R(),e=R.Router(),s=tt.createServer(t),r=await D(),a=S.initHandlers(r),i=E.register(r);this.setupApplication(t,a),this.setupTemplateEngine(t),await r.generateInFileRuntimeConfig(h,this.serverCLIOptions),await i.initialize(),await new A(e,r,a,i,this.serverCLIOptions).register(),t.use(e),this.run(s,r,i,this.serverCLIOptions);}setupApplication(t,e){t.disable("x-powered-by"),t.use(R.json()),t.use(X({methods:"GET"})),t.use(R.static(h+"/public")),t.use(this.xPoweredMiddleware),t.use(e.middleware);}setupTemplateEngine(t){t.set("views",m.join(h,"views")),t.set("view engine","ejs"),t.use(Z),t.set("layout","layouts/main");}listen(t,e){t.enabled()&&p.info("database: %s",t.DATABASE_DIR),p.info("server listening to http://localhost:%d",e),console.log(et.textSync("FAKELAB"));}run(t,e,s,r){let{port:a}=e.options.server(r.pathPrefix,r.port);t.listen(a,"localhost",()=>this.listen(s,a));}xPoweredMiddleware(t,e,s){e.setHeader("x-powered-by","fakelab"),s();}loadLocalEnv(){}};var C=class n{constructor(t,e){this.url=t;this.options=e;}TARGET_LANGUAGE="typescript";DEFAULT_TYPE_NAME="Interface";SNAPSHOT_DIR=m.resolve(f,".fakelab/snapshots");static init(t,e){return new n(t,e)}async fetch(t,e){this.invalidUrl(t)&&(p.error("snapshot url is invalid."),process.exit(1));let r=await(await fetch(t)).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:e||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(`
8
+ `)}async capture(){if(!(await D()).options.snapshot().enabled)return;if(!this.url)return await this.refetch();let e=await this.fetch(this.url,this.options?.name);await this.save(this.url,e,this.options),await this.modifyGitignoreFile(".fakelab/*");}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{}}async write(t,e,s){await d.writeFile(m.resolve(this.SNAPSHOT_DIR,this.snapshotName(t)),e),await this.updateSnapshotMeta(t,s);}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,{});}}));}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}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);}snapshotName(t,e=true){return t.replace(/^https?:\/\//,"").replace(/[\/:?.&=#]/g,"_")+(e?".ts":"")}suffix(t){return `${this.DEFAULT_TYPE_NAME}${t.toString().toUpperCase()}`}invalidUrl(t){try{return new URL(t),!1}catch{return true}}invalidJSON(t){try{return JSON.parse(t),!1}catch{return true}}async modifyGitignoreFile(t){try{let e=m.resolve(f,".gitignore");if((await d.readFile(e,{encoding:"utf8"})).split(`
9
+ `).some(r=>r.trim()===t.trim()))return;await d.appendFile(e,`
10
+ ${t}`);}catch{}}};var I=new Command,F=d.readJSONSync(m.join(h,"../package.json"));I.name(F.name).description(F.description).version(F.version);I.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=>{L.init(n).start();});I.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)=>{C.init(n,t).capture();});I.parse();
package/lib/main.d.ts CHANGED
@@ -32,25 +32,6 @@ type DatabaseOptions = {
32
32
  */
33
33
  enabled: boolean;
34
34
  };
35
- type BrowserExposeOptions = {
36
- /**
37
- * Name of the exposed object or module in the browser.
38
- * @example `fakelab`
39
- */
40
- name: string;
41
- /**
42
- * Exposure mode in the browser environment.
43
- * - `"module"`: Exposed as an ES module
44
- * - `"global"`: Attached to the global window object
45
- */
46
- mode: "module" | "global";
47
- };
48
- type BrowserOptions = {
49
- /**
50
- * Controls how the runtime API is exposed in the browser.
51
- */
52
- expose?: Partial<BrowserExposeOptions>;
53
- };
54
35
  type NetworkBehaviourOptions = {
55
36
  /**
56
37
  * Artificial response delay in milliseconds.
@@ -97,26 +78,27 @@ type ConfigOptions = {
97
78
  sourcePath: string | string[];
98
79
  /**
99
80
  * Server-related configuration.
81
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/server-command|Server Documentation}
100
82
  */
101
83
  server?: ServerOptions;
102
84
  /**
103
85
  * Faker engine configuration for data generation.
86
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/faker-annotations|Faker Documentation}
104
87
  */
105
88
  faker?: FakerEngineOptions;
106
89
  /**
107
90
  * Database persistence configuration.
91
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/database-mode|Database Documentation}
108
92
  */
109
93
  database?: DatabaseOptions;
110
- /**
111
- * Browser runtime exposure options.
112
- */
113
- browser?: BrowserOptions;
114
94
  /**
115
95
  * Network simulation configuration.
96
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/network-simulation|Network Documentation}
116
97
  */
117
98
  network?: NetworkOptions;
118
99
  /**
119
100
  * Snapshot configuration.
101
+ * @see {@link https://alirezahematidev.github.io/fakelab/docs/guides/snapshot|Snapshot Documentation}
120
102
  */
121
103
  snapshot?: SnapshotOptions;
122
104
  };
@@ -130,20 +112,17 @@ type ServerCLIOptions = {
130
112
  declare class Config {
131
113
  private readonly configOptions;
132
114
  readonly RUNTIME_SOURCE_FILENAME = "runtime.js";
133
- readonly RUNTIME_DECL_FILENAME = "runtime.d.ts";
134
115
  readonly FAKELAB_PERSIST_DIR = ".fakelab";
135
116
  NETWORK_DEFAULT_OPTIONS: Readonly<NetworkOptions>;
136
117
  constructor(configOptions: ConfigOptions);
137
118
  get options(): {
138
119
  server: (prefix?: string, port?: number) => Required<ServerOptions>;
139
- browser: (name?: string, mode?: "module" | "global") => Required<BrowserOptions>;
140
120
  database: () => Required<DatabaseOptions>;
141
121
  network: () => NetworkOptions;
142
122
  snapshot: () => Required<SnapshotOptions>;
143
123
  faker: (locale?: FakerLocale) => Required<FakerEngineOptions>;
144
124
  };
145
125
  private _serverOptions;
146
- private _browserOptions;
147
126
  private _databaseOptions;
148
127
  private _networkOptions;
149
128
  private _snapshotOptions;
package/lib/main.js CHANGED
@@ -1,4 +1,4 @@
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 = {};
1
+ import f from'fast-glob';import a from'path';import _ from'fs-extra';import {stat,access,constants}from'fs/promises';import b from'is-glob';import i from'picocolors';import {transform}from'esbuild';import {fileURLToPath}from'url';var g=new Intl.ListFormat("en",{style:"long",type:"unit"}),s=class{static label(t){switch(t){case "info":return i.blueBright(t.toUpperCase());case "error":return i.redBright(t.toUpperCase());case "warn":return i.yellowBright(t.toUpperCase())}}static log(t,e){return [i.dim(`[${new Date().toISOString()}]`),this.label(t),e].join(" ")}static info(t,...e){console.log(this.log("info",t),...e);}static warn(t,...e){console.log(this.log("warn",t),...e);}static error(t,...e){console.error(this.log("error",t),...e);}static debug(t,...e){typeof process>"u"||!process.env.DEBUG||console.log(this.log("info",t),...e);}static list(t){return g.format(t)}};var l=["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 h(){let n=Intl.DateTimeFormat().resolvedOptions().locale;if(!n)return "en";let[t]=n.split("-"),e=t.toLowerCase();return l.includes(e)?e:"en"}var d=`let fl = {};
2
2
  let db = {};
3
3
  fl.url = () => "http://localhost:PORT/PREFIX/";
4
4
  fl.fetch = async function (name, count) {
@@ -13,170 +13,41 @@ fl.fetch = async function (name, count) {
13
13
  return result;
14
14
  };
15
15
 
16
- db.enabled = () => ENABLED_COND;
16
+ db.enabled = () => ENABLED;
17
17
 
18
- db.type$ = function (name) {}
18
+ db.type$ = function (_name) {};
19
19
 
20
20
  db.get = async function (name, predicate) {
21
- const response = await fetch(NAME.url() + "database/" + name);
21
+ const response = await fetch(fakelab.url() + "database/" + name);
22
22
 
23
23
  if (!response.ok) throw new Error("[fakelab] Failed to retreived data from database.");
24
24
 
25
25
  const result = await response.json();
26
26
 
27
- if(!Array.isArray(result)) throw new Error("[fakelab] Database table data must be an array.");
27
+ if (!Array.isArray(result)) throw new Error("[fakelab] Database table data must be an array.");
28
28
 
29
- if (typeof predicate === 'function') return result.find(predicate) ?? null;
29
+ if (typeof predicate === "function") return result.find(predicate) ?? null;
30
30
 
31
31
  return result;
32
32
  };
33
33
  db.post = async function (name) {
34
- const response = await fetch(NAME.url() + "database/" + name, { method: "POST", headers: {"Content-Type": "application/json" } });
34
+ const response = await fetch(fakelab.url() + "database/" + name, { method: "POST", headers: { "Content-Type": "application/json" } });
35
35
 
36
36
  if (!response.ok) throw new Error("[fakelab] Failed to post data to database.");
37
37
  };
38
38
  db.seed = async function (name, options) {
39
- const response = await fetch(NAME.url() + "database/insert/" + name, { method: "POST", body: JSON.stringify(options), headers: {"Content-Type": "application/json" } });
39
+ const response = await fetch(fakelab.url() + "database/insert/" + name, { method: "POST", body: JSON.stringify(options), headers: { "Content-Type": "application/json" } });
40
40
 
41
41
  if (!response.ok) throw new Error("[fakelab] Failed to seed data to database.");
42
42
  };
43
43
  db.flush = async function (name) {
44
- const response = await fetch(NAME.url() + "database/flush/" + name, { method: "POST" });
44
+ const response = await fetch(fakelab.url() + "database/flush/" + name, { method: "POST" });
45
45
 
46
46
  if (!response.ok) throw new Error("[fakelab] Failed to flush seeded data from database.");
47
47
  };
48
48
 
49
- const NAME = Object.freeze(fl);
49
+ const fakelab = Object.freeze(fl);
50
50
  const database = Object.freeze(db);
51
51
 
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];
54
- declare function get<T extends keyof Runtime$>(name: T): Promise<Array<Runtime$[T]>>;
55
- declare function get<T extends keyof Runtime$>(name: T, predicate: (value: Runtime$[T]) => boolean): Promise<Runtime$[T] | null>;
56
- declare function post<T extends keyof Runtime$>(name: T): Promise<void>;
57
- declare function seed<T extends keyof Runtime$>(name: T, options?: SeedOptions): Promise<void>;
58
- declare function flush<T extends keyof Runtime$>(name: T): Promise<void>;
59
- declare function enabled(): boolean;
60
- declare function url(): string;
61
- declare const NAME: {
62
- fetch: typeof fetch;
63
- url: typeof url;
64
- };
65
- declare const database: {
66
- get: typeof get;
67
- post: typeof post;
68
- seed: typeof seed;
69
- flush: typeof flush;
70
- enabled: typeof enabled;
71
- };
72
- type SeedOptions = {
73
- /**
74
- * Number of records to generate.
75
- */
76
- count?: number;
77
- /**
78
- * Defines how seeding interacts with existing database data.
79
- * - \`"reset"\`: Removes all existing data and recreates it from scratch.
80
- * - \`"once"\`: Seeds data only if the database is empty.
81
- * - \`"merge"\`: Inserts new records and updates existing ones. The total number of items per table
82
- * is limited to \`1000\` records.
83
- * @default "reset"
84
- */
85
- strategy?: "reset" | "once" | "merge"
86
- }
87
- type Result$<T, CT> = CT extends number ? (CT extends 0 ? T : T[]) : T;
88
- interface Runtime$ {}
89
-
90
- type Typeof<T extends keyof Runtime$> = ReturnType<typeof type$<T>>
91
-
92
- export type { Typeof }
93
- export { NAME, database };`,T=`global.NAME = {};
94
- global.NAME.database = {};
95
- global.NAME.url = () => "http://localhost:PORT/PREFIX/";
96
- global.NAME.fetch = async function (name, count) {
97
- const search = count ? "?count=" + count : "";
98
-
99
- const response = await fetch(global.NAME.url() + name + search);
100
-
101
- if (!response.ok) throw new Error("[fakelab] Failed to retreived mock data.");
102
-
103
- const result = await response.json();
104
-
105
- return result;
106
- };
107
- global.NAME.database.enabled = () => ENABLED_COND;
108
- global.NAME.database.get = async function (name, predicate) {
109
- const response = await fetch(global.NAME.url() + "database/" + name);
110
-
111
- if (!response.ok) throw new Error("[fakelab] Failed to retreived data from database.");
112
-
113
- const result = await response.json();
114
-
115
- if(!Array.isArray(result)) throw new Error("[fakelab] Database table data must be an array.");
116
-
117
- if (typeof predicate === 'function') return result.find(predicate) ?? null;
118
-
119
- return result;
120
- };
121
- global.NAME.database.post = async function (name) {
122
- const response = await fetch(global.NAME.url() + "database/" + name, { method: "POST", headers: {"Content-Type": "application/json" } });
123
-
124
- if (!response.ok) throw new Error("[fakelab] Failed to post data to database.");
125
- };
126
- global.NAME.database.seed = async function (name, options) {
127
- const response = await fetch(global.NAME.url() + "database/insert/" + name, { method: "POST", body: JSON.stringify(options), headers: {"Content-Type": "application/json" } });
128
-
129
- if (!response.ok) throw new Error("[fakelab] Failed to seed data to database.");
130
- };
131
- global.NAME.database.flush = async function (name) {
132
- const response = await fetch(global.NAME.url() + "database/flush/" + name, { method: "POST" });
133
-
134
- if (!response.ok) throw new Error("[fakelab] Failed to flush seeded data from database.");
135
- };
136
-
137
- const NAME = Object.freeze(fl);
138
- const database = Object.freeze(db);
139
-
140
- export { NAME, database };`,A=`export {};
141
-
142
- declare global {
143
- const database: {
144
- get<T extends keyof Runtime$>(name: T): Promise<Array<Runtime$[T]>>;
145
- get<T extends keyof Runtime$>(name: T, predicate: (value: Runtime$[T]) => boolean): Promise<Runtime$[T] | null>;
146
- post(name: keyof Runtime$): Promise<void>;
147
- seed(name: keyof Runtime$, options?: SeedOptions): Promise<void>;
148
- flush(name: keyof Runtime$): Promise<void>;
149
- enabled(): boolean;
150
- };
151
- const NAME: {
152
- fetch<T extends keyof Runtime$, C extends number | undefined = undefined>(name: T, count?: C): Promise<Result$<Runtime$[T], C>>;
153
- url(): string;
154
- database: typeof database;
155
- };
156
- type SeedOptions = {
157
- /**
158
- * Number of records to generate.
159
- */
160
- count?: number;
161
- /**
162
- * Defines how seeding interacts with existing database data.
163
- * - \`"reset"\`: Removes all existing data and recreates it from scratch.
164
- * - \`"once"\`: Seeds data only if the database is empty.
165
- * - \`"merge"\`: Inserts new records and updates existing ones. The total number of items per table
166
- * is limited to \`1000\` records.
167
- * @default "reset"
168
- */
169
- strategy?: "reset" | "once" | "merge"
170
- }
171
- type Result$<T, CT> = CT extends number ? (CT extends 0 ? T : T[]) : T;
172
- interface Runtime$ {}
173
- interface Window {
174
- readonly NAME: typeof NAME;
175
- }
176
-
177
- namespace NodeJS {
178
- interface Global {
179
- NAME: typeof NAME;
180
- }
181
- }
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};
52
+ export { fakelab, database };
53
+ `;var p=class n{constructor(t,e,r){this.port=t;this.prefix=e;this.dbOptions=r;this.enabled=this.dbOptions?.enabled??true?"true":"false";}enabled;transformOptions={minify:true,platform:"browser",target:"es2022"};static init(t,e,r){return new n(t,e,r)}replacer(t,e){let r=t;for(let o in e)r=r.replace(new RegExp(o,"g"),e[o].toString().trim());return r}async prepareSource(){let t=this.replacer(d,{PORT:this.port,PREFIX:this.prefix,ENABLED:this.enabled});try{let{code:e}=await transform(t,this.transformOptions);return e}catch(e){return e instanceof Error&&s.warn(e.message),t}}};a.dirname(fileURLToPath(import.meta.url));var m=process.cwd();var c=class{constructor(t){this.configOptions=t;this.files=this.files.bind(this),this._serverOptions=this._serverOptions.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";FAKELAB_PERSIST_DIR=".fakelab";NETWORK_DEFAULT_OPTIONS;get options(){return {server:this._serverOptions,database:this._databaseOptions,network:this._networkOptions,snapshot:this._snapshotOptions,faker:this._fakerOptions}}_serverOptions(t,e){return {pathPrefix:t||this.configOptions.server?.pathPrefix||"api",port:e||this.configOptions.server?.port||5200,includeSnapshots:this.configOptions.server?.includeSnapshots??true}}_databaseOptions(){return {enabled:this.configOptions.database?.enabled??false}}_networkOptions(){let t=this.configOptions.network?.preset,e=this.configOptions.network?.presets??{};return !t||!e[t]?this.NETWORK_DEFAULT_OPTIONS:{...e[t],...this.configOptions.network??{}}}_snapshotOptions(){return {enabled:this.configOptions.snapshot?.enabled??false}}_fakerOptions(t){let e=(t||this.configOptions.faker?.locale)?.toLowerCase();return e&&l.includes(e)?{locale:e}:{locale:h()}}async files(t){let e=this.resolveSourcePath(t||this.configOptions.sourcePath),r=Array.from(new Set((await Promise.all(e.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(e.map(o=>a.basename(o)))),process.exit(1)),r}async generateInFileRuntimeConfig(t,e){let{port:r,pathPrefix:o}=this._serverOptions(e.pathPrefix,e.port),u=a.resolve(t,this.RUNTIME_SOURCE_FILENAME),O=await p.init(r,o,this.configOptions.database).prepareSource();await _.writeFile(u,O);}async getSnapshotSourceFiles(){let t=await f(".fakelab/snapshots/**/*.ts",{absolute:true,ignore:["**/*.d.ts"],cwd:m});return t.length>0&&s.info("snapshot(s): %s",s.list(t.map(e=>a.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=>b(r,{strict:true})?r:a.resolve(r))}async resolveTSFiles(t){if(b(t,{strict:true}))return s.info("source: %s",t),f(t,{absolute:true,ignore:["**/*.d.ts"]});let e=a.resolve(t),r=e.endsWith(".ts")?e:e+".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(e))?.isDirectory()?(s.info("source: %s",e),f("**/*.ts",{cwd:e,absolute:true,ignore:["**/*.d.ts"]})):(s.warn("invalid source: [REDACTED]/%s",a.basename(r)),[])}};function R(n){return new c(n)}export{R as defineConfig};
@@ -26,7 +26,7 @@ pre {
26
26
  display: flex;
27
27
  width: 100%;
28
28
  height: 100%;
29
- background: oklch(27.8% 0.033 256.848);
29
+ background: #1e2939;
30
30
  }
31
31
 
32
32
  .sidebar {
@@ -0,0 +1,41 @@
1
+ declare function fetch<T extends keyof Runtime$, C extends number | undefined = undefined>(name: T, count?: C): Promise<Result$<Runtime$[T], C>>;
2
+ declare function type$<T extends keyof Runtime$>(): Runtime$[T];
3
+ declare function get<T extends keyof Runtime$>(name: T): Promise<Array<Runtime$[T]>>;
4
+ declare function get<T extends keyof Runtime$>(name: T, predicate: (value: Runtime$[T]) => boolean): Promise<Runtime$[T] | null>;
5
+ declare function post<T extends keyof Runtime$>(name: T): Promise<void>;
6
+ declare function seed<T extends keyof Runtime$>(name: T, options?: SeedOptions): Promise<void>;
7
+ declare function flush<T extends keyof Runtime$>(name: T): Promise<void>;
8
+ declare function enabled(): boolean;
9
+ declare function url(): string;
10
+ declare const fakelab: {
11
+ fetch: typeof fetch;
12
+ url: typeof url;
13
+ };
14
+ declare const database: {
15
+ get: typeof get;
16
+ post: typeof post;
17
+ seed: typeof seed;
18
+ flush: typeof flush;
19
+ enabled: typeof enabled;
20
+ };
21
+ type SeedOptions = {
22
+ /**
23
+ * Number of records to generate.
24
+ */
25
+ count?: number;
26
+ /**
27
+ * Defines how seeding interacts with existing database data.
28
+ * - `"reset"`: Removes all existing data and recreates it from scratch.
29
+ * - `"once"`: Seeds data only if the database is empty.
30
+ * - `"merge"`: Inserts new records and updates existing ones. The total number of items per table
31
+ * is limited to `1000` records.
32
+ * @default "reset"
33
+ */
34
+ strategy?: "reset" | "once" | "merge";
35
+ };
36
+ type Result$<T, CT> = CT extends number ? (CT extends 0 ? T : T[]) : T;
37
+ interface Runtime$ {}
38
+
39
+ type Typeof<T extends keyof Runtime$> = ReturnType<typeof type$<T>>;
40
+
41
+ export { type Typeof, database, fakelab };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "fakelab",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "description": "A easy-config mock server for frontend developers.",
6
+ "description": "A fast, easy-config mock server for frontend developers.",
7
7
  "main": "lib/main.js",
8
8
  "module": "lib/main.js",
9
9
  "types": "lib/main.d.ts",