fakelab 0.0.17 → 0.0.19

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
@@ -30,9 +30,12 @@ export default defineConfig({
30
30
  sourcePath: ["./types", "./fixtures/**/*.ts"], // supports glob pattern
31
31
  faker: { locale: "en" }, // optional
32
32
  server: { pathPrefix: "api/v1", port: 8080 }, // optional
33
+ browser: { expose: { mode: "module" } }, // optional
33
34
  });
34
35
  ```
35
36
 
37
+ ## Faker Annotations
38
+
36
39
  Fakelab allows you to control generated mock data using JSDoc tags.
37
40
  You simply annotate your TypeScript interfaces with the @faker tag, and Fakelab uses the corresponding [faker](https://fakerjs.dev/)
38
41
  method when generating mock values.
@@ -85,14 +88,14 @@ export interface User {
85
88
 
86
89
  ## Fakelab Runtime
87
90
 
88
- `fakelab/runtime` enables a **global fakelab object** at runtime, allowing your frontend or Node environment to communicate with the running Fakelab mock server.
91
+ `fakelab/runtime` enables `fakelab` module at runtime, allowing your frontend or Node environment to communicate with the running Fakelab mock server.
89
92
 
90
- ## `fakelab.URL`
93
+ ## `fakelab.url()`
91
94
 
92
95
  The base URL of the running Fakelab server.
93
96
 
94
97
  ```ts
95
- fakelab.URL;
98
+ fakelab.url();
96
99
  // e.g. "http://localhost:50000/api"
97
100
  ```
98
101
 
@@ -116,6 +119,15 @@ fakelab.fetch(name: string, count?: number): Promise<T>
116
119
  ### Basic example
117
120
 
118
121
  ```ts
122
+ import { fakelab } from "fakelab/runtime";
123
+
124
+ const users = await fakelab.fetch("User", 10);
125
+
126
+ console.log(users);
127
+
128
+ // or
129
+
130
+ // can be enabled as a global object
119
131
  import "fakelab/runtime";
120
132
 
121
133
  const users = await fakelab.fetch("User", 10);
@@ -125,6 +137,67 @@ console.log(users);
125
137
 
126
138
  **NOTE:** Set count to a negative number to get an empty array.
127
139
 
140
+ ## Database Mode
141
+
142
+ Fakelab can persist generated mock data to a local database.
143
+
144
+ Under the hood, Fakelab uses the lightweight [lowdb](https://github.com/typicode/lowdb)
145
+ library for persistence, ensuring fast reads, simple JSON storage, and zero external dependencies.
146
+
147
+ ### Database Options
148
+
149
+ ```ts
150
+ export type DatabaseOptions = {
151
+ enabled: boolean;
152
+ dest?: string;
153
+ };
154
+ ```
155
+
156
+ ### Basic example
157
+
158
+ ```ts
159
+ export default defineConfig({
160
+ database: { enabled: true, dest: "db" },
161
+ });
162
+ ```
163
+
164
+ ## Network Simulation
165
+
166
+ Fakelab can simulate real-world network conditions such as latency, random failures, timeouts, and offline mode.
167
+ This is useful for testing loading states, retry logic, and poor network UX without changing frontend code.
168
+
169
+ ### Network Options
170
+
171
+ ```ts
172
+ type NetworkBehaviourOptions = {
173
+ delay?: number | [number, number];
174
+ errorRate?: number;
175
+ timeoutRate?: number;
176
+ offline?: boolean;
177
+ };
178
+
179
+ export type NetworkOptions = NetworkBehaviourOptions & {
180
+ preset?: string;
181
+ presets?: Record<string, NetworkBehaviourOptions>;
182
+ };
183
+ ```
184
+
185
+ ### Basic example
186
+
187
+ ```ts
188
+ export default defineConfig({
189
+ network: {
190
+ delay: [300, 1200],
191
+ errorRate: 0.1,
192
+ timeoutRate: 0.05,
193
+ presets: { wifi: { errorRate: 1 } },
194
+ preset: "wifi",
195
+ },
196
+ });
197
+ ```
198
+
199
+ **NOTE:** When both inline network options and a `preset` are defined, inline options always take precedence and override the preset values.
200
+
128
201
  ## Server Command
129
202
 
130
203
  Run:
package/lib/cli.js CHANGED
@@ -1,7 +1,7 @@
1
- import h from'path';import O from'fs-extra';import {Command}from'commander';import C from'express';import z from'cors';import K from'express-ejs-layouts';import V from'http';import'ejs';import {Project}from'ts-morph';import m from'winston';import b from'picocolors';import {JSONFilePreset}from'lowdb/node';import {fileURLToPath}from'url';import G from'qs';import W from'figlet';import {bundleRequire}from'bundle-require';import re from'joycon';function S(s){switch(s){case "info":return b.blueBright(s.toUpperCase());case "error":return b.redBright(s.toUpperCase());case "warn":return b.yellowBright(s.toUpperCase());default:return s.toUpperCase()}}var D=m.format.printf(({level:s,message:e,timestamp:t})=>`${b.dim(`[${t}]`)} ${S(s)} ${e}`),d=m.createLogger({format:m.format.combine(m.format.timestamp(),m.format.splat(),D),transports:[new m.transports.Console]}),j=new Intl.ListFormat("en",{style:"long",type:"unit"}),p=class{static info(e,...t){return d.info(e,...t)}static warn(e,...t){return d.warn(e,...t)}static error(e,...t){return d.error(e,...t)}static list(e){return j.format(e)}static close(){d.removeAllListeners(),d.close();}};var w=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 r={};return await Promise.all(e.map(async n=>{let o=n.getTypeAtLocation(n.getValueDeclarationOrThrow());r[n.getName()]=await t(o,this,this.readJSDocTags(n));})),r}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(r=>r.getName()===this.JSDOC_FAKER_FIELD);return t.length===0?[]:t.map(r=>{let[n]=r.getText();if(!n)return;let o=n.text.trim().match(this.FAKER_TAG_REGEX);if(!o)return;let[,a,c]=o,i=this.evalArgs(c);return {path:a,args:i}})}execute(e,t){if(!e)return t();let r=e.path.split("."),n=this.faker;for(let o of r)n=n[o],n||(p.error("Invalid faker module path:",e.path),process.exit(1));typeof n!="function"&&(p.error("Unresolvable faker function.",e.path),process.exit(1));try{return e.args?n(e.args):n()}catch{return p.error("Passed invalid arguments to faker function."),n()}}};var u=h.dirname(fileURLToPath(import.meta.url)),F=process.cwd();var x=class{constructor(e,t){this.files=e;this.config=t;let n=new Project({tsConfigFilePath:"tsconfig.json"}).addSourceFilesAtPaths(e);this.__targets=n.flatMap(o=>{let a=o.getInterfaces(),c=o.getTypeAliases(),i=o.getExportDeclarations().flatMap(l=>l.getNamedExports().flatMap(f=>f.getLocalTargetDeclarations()));return [...a,...c,...i]}),this.generateInFileEntitiyMap(this.__targets);}__targets;async run(e){return await e()}normalizePath(e){return e.split(h.sep).join(h.posix.sep)}generateInFileEntitiyMap(e){let t=[...new Set(e.map(o=>{let a=o.getName(),c=o.getSourceFile().getFilePath();return `${a}: import("${c}").${a}`}))],{expose:r}=this.config.browserOpts(),n=`
1
+ import g from'path';import O from'fs-extra';import {Command}from'commander';import R from'express';import K from'cors';import X from'express-ejs-layouts';import W from'http';import {Project}from'ts-morph';import d from'winston';import b from'picocolors';import {JSONFilePreset}from'lowdb/node';import {fileURLToPath}from'url';import U from'qs';import Z from'figlet';import {bundleRequire}from'bundle-require';import Q from'joycon';function D(o){switch(o){case "info":return b.blueBright(o.toUpperCase());case "error":return b.redBright(o.toUpperCase());case "warn":return b.yellowBright(o.toUpperCase());default:return o.toUpperCase()}}var I=d.format.printf(({level:o,message:e,timestamp:t})=>`${b.dim(`[${t}]`)} ${D(o)} ${e}`),m=d.createLogger({format:d.format.combine(d.format.timestamp(),d.format.splat(),I),transports:[new d.transports.Console]}),S=new Intl.ListFormat("en",{style:"long",type:"unit"}),p=class{static info(e,...t){return m.info(e,...t)}static warn(e,...t){return m.warn(e,...t)}static error(e,...t){return m.error(e,...t)}static debug(e,...t){if(!(typeof process>"u"||!process.env.DEBUG))return m.info(e,...t)}static list(e){return S.format(e)}static close(){m.removeAllListeners(),m.close();}};var w=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 r={};return await Promise.all(e.map(async s=>{let i=s.getTypeAtLocation(s.getValueDeclarationOrThrow());r[s.getName()]=await t(i,this,this.readJSDocTags(s));})),r}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(r=>r.getName()===this.JSDOC_FAKER_FIELD);return t.length===0?[]:t.map(r=>{let[s]=r.getText();if(!s)return;let i=s.text.trim().match(this.FAKER_TAG_REGEX);if(!i)return;let[,n,a]=i,c=this.evalArgs(a);return {path:n,args:c}})}execute(e,t){if(!e)return t();let r=e.path.split("."),s=this.faker;for(let i of r)s=s[i],s||(p.error("Invalid faker module path: (%s)",e.path),process.exit(1));typeof s!="function"&&(p.error("Unresolvable faker function. (%s)",e.path),process.exit(1));try{return e.args?s(e.args):s()}catch{return p.error("Passed invalid arguments to faker function."),s()}}};var u=g.dirname(fileURLToPath(import.meta.url)),L=process.cwd();var v=class{constructor(e,t){this.files=e;this.config=t;let s=new Project({tsConfigFilePath:"tsconfig.json"}).addSourceFilesAtPaths(e);this.__targets=s.flatMap(i=>{let n=i.getInterfaces(),a=i.getTypeAliases(),c=i.getExportDeclarations().flatMap(l=>l.getNamedExports().flatMap(f=>f.getLocalTargetDeclarations()));return [...n,...a,...c]}),this.generateInFileEntitiyMap(this.__targets);}__targets;async run(e){return await e()}normalizePath(e){return e.split(g.sep).join(g.posix.sep)}generateInFileEntitiyMap(e){let t=[...new Set(e.map(i=>{let n=i.getName(),a=i.getSourceFile().getFilePath();return `${n}: import("${a}").${n}`}))],{expose:r}=this.config.browserOpts(),s=`
2
2
  interface Runtime$ {
3
3
  ${t.join(`
4
4
  `)}
5
- }`;r.mode==="global"&&(n=`
6
- declare global {${n}
7
- }`),O.appendFile(h.resolve(u,this.config.RUNTIME_DECL_FILENAME),n);}address(e,t){let r=this.normalizePath(F);return `${e.replace(r,"")}/${t}`}async entities(){let e=await Promise.all(this.__targets.map(async t=>{let r=t.getName().toLowerCase(),n=t.getType(),o=this.normalizePath(t.getSourceFile().getDirectoryPath()),a=t.getSourceFile().getBaseName(),c=this.address(o,a),i=this.config.getDatabaseDirectoryPath(),l=h.resolve(i,`${r}.json`),f=this.address(this.normalizePath(i),h.basename(l)),y=await JSONFilePreset(l,[]);return [r,{type:n,filepath:c,table:y,tablepath:f}]}));return new Map(e)}async initFakerLibrary(e){let{faker:t}=await import(`@faker-js/faker/locale/${e.locale}`);return t}};async function g(s,e,t=[],r=0){if(s.isString())return e.string(t[r]);if(s.isNumber())return e.int(t[r]);if(s.isBoolean())return e.bool(t[r]);if(s.isBigInt())return e.bigInt(t[r]);if(s.isBooleanLiteral())return e.litbool(s.getText());if(s.isLiteral())return s.getLiteralValue();if(!s.isUndefined()){if(s.isUnion()){let n=s.getUnionTypes();return await e.union(n.map((o,a)=>g(o,e,t,a)))}if(s.isIntersection()){let n=s.getIntersectionTypes();return await e.intersection(n.map((o,a)=>g(o,e,t,a)))}if(s.isArray()){let n=s.getArrayElementTypeOrThrow();return [await g(n,e,t,r)]}if(s.isObject()){let n=s.getProperties();return await e.object(n,(o,a,c)=>g(o,a,c,r))}return null}}function J({each:s}){return {resolve:async t=>await Promise.all(Array.from({length:t},s))}}async function R(s,e){let t=await s.files(e.source),r=new x(t,s),n=await r.entities(),o=await r.initFakerLibrary(s.fakerOpts(e.locale)),a=new w(o);async function c(i,l){let f=J({each:()=>g(i,a)}),y=await(l.count?f.resolve(parseInt(l.count)):g(i,a)),I=JSON.stringify(y,null,2);return {data:y,json:I}}return {entities:n,forge:c}}var v=class{constructor(e,t,r){this.builder=e;this.config=t;this.pkg=r;}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.config.databaseEnabled()});}}preview(e){return async(t,r)=>{let n=`${t.protocol}://${t.host}/`,o=t.params.name,a=await this.handleQueries(t),c=G.stringify(a,{addQueryPrefix:true}),i=this.builder.entities.get(o.toLowerCase());if(i){let{json:l}=await this.builder.forge(i.type,a),f=i.filepath;r.render("preview",{name:o,filepath:f,address:n,search:c,json:l,prefix:e,entities:this.builder.entities,version:this.pkg.version,enabled:this.config.databaseEnabled()});}else r.redirect("/");}}database(){return (e,t)=>{this.config.databaseEnabled()?t.render("database",{name:null,entities:this.builder.entities,version:this.pkg.version}):t.redirect("/");}}table(e){return async(t,r)=>{let n=`${t.protocol}://${t.host}/`,o=t.params.name,a=this.builder.entities.get(o.toLowerCase());if(!this.config.databaseEnabled())r.redirect("/");else if(a){await a.table.read();let i=a.table.data.length>0,l=JSON.stringify(a.table.data,null,2),f=a.filepath;r.render("table",{name:o,filepath:f,address:n,prefix:e,json:l,hasData:i,entities:this.builder.entities,version:this.pkg.version});}else r.redirect("/database");}}};var k=class{constructor(e){this.builder=e;}async handleQueries(e){let t=e.query.count;return t?{count:t.toString()}:{}}entity(){return async(e,t)=>{try{let r=e.params.name,n=await this.handleQueries(e),o=this.builder.entities.get(r.toLowerCase());if(o){let{data:a}=await this.builder.forge(o.type,n);t.status(200).json(a);}else t.status(400).json({message:"The entity is not exists"});}catch(r){t.status(500).send(r);}}}getTable(){return async(e,t)=>{try{let r=e.params.name,n=this.builder.entities.get(r.toLowerCase());n?(await n.table.read(),t.status(200).json(n.table.data)):t.status(400).json({message:"The table is not exists"});}catch(r){t.status(500).send(r);}}}updateTable(e=false){return async(t,r)=>{try{let n=t.params.name,o=await this.handleQueries(t),a=this.builder.entities.get(n.toLowerCase());if(a){let{data:c}=await this.builder.forge(a.type,o);await a.table.update(i=>i.push(c)),e?r.status(301).redirect(`/database/${n.toLowerCase()}`):r.status(200).json({success:!0});}else e?r.status(400).redirect("/database"):r.status(400).json({success:!1,message:"The table is not exists"});}catch(n){e?r.status(500).redirect("/database"):r.status(500).send(n);}}}clearTable(){return async(e,t)=>{try{let r=e.params.name,n=this.builder.entities.get(r.toLowerCase());n?(await n.table.read(),n.table.data.length>0&&await n.table.update(o=>o.length=0),t.status(301).redirect(`/database/${r.toLowerCase()}`)):t.status(400).redirect("/database");}catch{t.status(500).redirect("/database");}}}};var B=O.readJSONSync(h.join(u,"../package.json")),T=class{constructor(e,t,r){this.router=e;this.config=t;this.opts=r;let{pathPrefix:n}=this.config.serverOpts(this.opts.pathPrefix,this.opts.port);this.prefix=n;}prefix;async register(){let e=await R(this.config,this.opts),t=new v(e,this.config,B),r=new k(e);this.router.get("/",t.index()),this.router.get("/entities/:name",t.preview(this.prefix)),this.router.get("/database",t.database()),this.router.get("/database/:name",t.table(this.prefix)),this.router.get(`/${this.prefix}/:name`,r.entity()),this.router.get(`/${this.prefix}/database/:name`,r.getTable()),this.router.post(`/${this.prefix}/database/:name`,r.updateTable()),this.router.post("/__update/:name",r.updateTable(true)),this.router.post("/__delete/:name",r.clearTable());}};function X(s,e){s.databaseEnabled()&&p.info(`database: ${s.getDatabaseDirectoryPath()}`),p.info(`server: http://localhost:${e}`),console.log(W.textSync("FAKELAB"));}function H(s,e,t){let{port:r}=e.serverOpts(t.pathPrefix,t.port);s.listen(r,"localhost",()=>X(e,r)),s.on("close",()=>{p.close();});}function Z(s,e,t){e.setHeader("x-powered-by","fakelab"),t();}function Y(s){s.disable("x-powered-by"),s.use(C.json()),s.use(z({methods:"GET"})),s.use(C.static(u+"/public")),s.use(Z);}function ee(s){s.set("views",h.join(u,"views")),s.set("view engine","ejs"),s.use(K),s.set("layout","layouts/main");}async function A(s,e){let t=C(),r=C.Router(),n=V.createServer(t);Y(t),ee(t),await s.generateInFileRuntimeConfig(u,e),await new T(r,s,e).register(),t.use(r),H(n,s,e);}async function L(){try{let e=await new re().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 P=new Command,E=O.readJSONSync(h.join(u,"../package.json"));P.name(E.name).description(E.description).version(E.version);P.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 s=>{let e=await L();A(e,s);});P.parse();
5
+ }`;r.mode==="global"&&(s=`
6
+ declare global {${s}
7
+ }`),O.appendFile(g.resolve(u,this.config.RUNTIME_DECL_FILENAME),s);}address(e,t){let r=this.normalizePath(L);return `${e.replace(r,"")}/${t}`}async entities(){let e=await Promise.all(this.__targets.map(async t=>{let r=t.getName().toLowerCase(),s=t.getType(),i=this.normalizePath(t.getSourceFile().getDirectoryPath()),n=t.getSourceFile().getBaseName(),a=this.address(i,n),c=this.config.database.directoryPath(),l=g.resolve(c,`${r}.json`),f=this.address(this.normalizePath(c),g.basename(l)),y=await JSONFilePreset(l,[]);return [r,{type:s,filepath:a,table:y,tablepath:f}]}));return new Map(e)}async initFakerLibrary(e){let{faker:t}=await import(`@faker-js/faker/locale/${e.locale}`);return t}};async function h(o,e,t=[],r=0){if(o.isString())return e.string(t[r]);if(o.isNumber())return e.int(t[r]);if(o.isBoolean())return e.bool(t[r]);if(o.isBigInt())return e.bigInt(t[r]);if(o.isBooleanLiteral())return e.litbool(o.getText());if(o.isLiteral())return o.getLiteralValue();if(!o.isUndefined()){if(o.isUnion()){let s=o.getUnionTypes();return await e.union(s.map((i,n)=>h(i,e,t,n)))}if(o.isIntersection()){let s=o.getIntersectionTypes();return await e.intersection(s.map((i,n)=>h(i,e,t,n)))}if(o.isArray()){let s=o.getArrayElementTypeOrThrow();return [await h(s,e,t,r)]}if(o.isObject()){let s=o.getProperties();return await e.object(s,(i,n,a)=>h(i,n,a,r))}return null}}function J({each:o}){return {resolve:async t=>await Promise.all(Array.from({length:t},o))}}async function F(o,e){let t=await o.files(e.source),r=new v(t,o),s=await r.entities(),i=await r.initFakerLibrary(o.fakerOpts(e.locale)),n=new w(i);async function a(c,l){let f=J({each:()=>h(c,n)}),y=await(l.count?f.resolve(parseInt(l.count)):h(c,n)),j=JSON.stringify(y,null,2);return {data:y,json:j}}return {entities:s,build:a}}var k=class{constructor(e,t,r){this.builder=e;this.config=t;this.pkg=r;}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.config.database.enabled()});}}preview(e){return async(t,r)=>{let s=`${t.protocol}://${t.host}/`,i=t.params.name,n=await this.handleQueries(t),a=U.stringify(n,{addQueryPrefix:true}),c=this.builder.entities.get(i.toLowerCase());if(c){let{json:l}=await this.builder.build(c.type,n),f=c.filepath;r.render("preview",{name:i,filepath:f,address:s,search:a,json:l,prefix:e,entities:this.builder.entities,version:this.pkg.version,enabled:this.config.database.enabled()});}else r.redirect("/");}}database(){return (e,t)=>{this.config.database.enabled()?t.render("database",{name:null,entities:this.builder.entities,version:this.pkg.version}):t.redirect("/");}}table(e){return async(t,r)=>{let s=`${t.protocol}://${t.host}/`,i=t.params.name,n=this.builder.entities.get(i.toLowerCase());if(!this.config.database.enabled())r.redirect("/");else if(n){await n.table.read();let c=n.table.data.length>0,l=JSON.stringify(n.table.data,null,2),f=n.filepath;r.render("table",{name:i,filepath:f,address:s,prefix:e,json:l,hasData:c,entities:this.builder.entities,version:this.pkg.version});}else r.redirect("/database");}}};var x=class{constructor(e,t){this.builder=e;this.network=t;}async handleQueries(e){let t=e.query.count;return t?{count:t.toString()}:{}}async applyNetworkHandlers(e){if(this.network.offline()){let{status:t,message:r}=this.network.state("offline");return e.status(t).json({message:r}),true}if(await this.network.wait(),this.network.timeout())return true;if(this.network.error()){let{status:t,message:r}=this.network.state("error");return e.status(t).json({message:r}),true}return false}entity(){return async(e,t)=>{try{if(await this.applyNetworkHandlers(t))return;let r=e.params.name,s=await this.handleQueries(e),i=this.builder.entities.get(r.toLowerCase());if(i){let{data:n}=await this.builder.build(i.type,s);t.status(200).json(n);}else t.status(400).json({message:"The entity is not exists"});}catch(r){t.status(500).send(r);}}}getTable(){return async(e,t)=>{try{if(await this.applyNetworkHandlers(t))return;let r=e.params.name,s=this.builder.entities.get(r.toLowerCase());s?(await s.table.read(),t.status(200).json(s.table.data)):t.status(400).json({message:"The table is not exists"});}catch(r){t.status(500).send(r);}}}updateTable(){return async(e,t)=>{try{if(await this.applyNetworkHandlers(t))return;let r=e.params.name,s=await this.handleQueries(e),i=this.builder.entities.get(r.toLowerCase());if(i){let{data:n}=await this.builder.build(i.type,s);await i.table.update(a=>a.push(...Array.isArray(n)?n:[n])),t.status(200).json({success:!0});}else t.status(400).json({success:!1,message:"The table is not exists"});}catch(r){t.status(500).send(r);}}}seedTable(){return async(e,t)=>{try{let r=e.body.count||1,s=e.params.name,i=this.builder.entities.get(s.toLowerCase());if(i){let{data:n}=await this.builder.build(i.type,{count:r});await i.table.update(a=>a.push(...Array.isArray(n)?n:[n])),t.status(200).json({success:!0});}else t.status(400).json({success:!1,message:"The table is not exists"});}catch(r){console.log({error:r}),t.status(500).send(r);}}}_update(){return async(e,t)=>{try{let r=e.params.name,s=await this.handleQueries(e),i=this.builder.entities.get(r.toLowerCase());if(i){let{data:n}=await this.builder.build(i.type,s);await i.table.update(a=>a.push(n)),t.status(301).redirect(`/database/${r.toLowerCase()}`);}else t.status(400).redirect("/database");}catch{t.status(500).redirect("/database");}}}_clear(){return async(e,t)=>{try{let r=e.params.name,s=this.builder.entities.get(r.toLowerCase());s?(await s.table.read(),s.table.data.length>0&&await s.table.update(i=>i.length=0),t.status(301).redirect(`/database/${r.toLowerCase()}`)):t.status(400).redirect("/database");}catch{t.status(500).redirect("/database");}}}};var G=O.readJSONSync(g.join(u,"../package.json")),T=class{constructor(e,t,r,s){this.router=e;this.config=t;this.opts=r;this.network=s;let{pathPrefix:i}=this.config.serverOpts(this.opts.pathPrefix,this.opts.port);this.prefix=i;}prefix;async register(){let e=await F(this.config,this.opts),t=new k(e,this.config,G),r=new x(e,this.network);this.router.get("/",t.index()),this.router.get("/entities/:name",t.preview(this.prefix)),this.router.get("/database",t.database()),this.router.get("/database/:name",t.table(this.prefix)),this.router.get(`/${this.prefix}/:name`,r.entity()),this.router.get(`/${this.prefix}/database/:name`,r.getTable()),this.router.post(`/${this.prefix}/database/:name`,r.updateTable()),this.router.post(`/${this.prefix}/database/seed/:name`,r.seedTable()),this.router.post("/__update/:name",r._update()),this.router.post("/__delete/:name",r._clear());}};var C=class o{constructor(e){this.config=e;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(e){return new o(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,r){let s=this.options?.errorRate||0,i=this.options?.timeoutRate||0,n=this.options?.offline??false,a=`delay=${this.resolveDelay()},error=${s},timeout=${i},offline=${n}`;t.setHeader("X-Fakelab-Network",a),r();}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 N(){try{let e=await new Q().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 E=class o{constructor(e){this.options=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 o(e)}async start(){let e=R(),t=R.Router(),r=W.createServer(e),s=await N(),i=C.initHandlers(s);this.setupApplication(e,i),this.setupTemplateEngine(e),await s.generateInFileRuntimeConfig(u,this.options),await new T(t,s,this.options,i).register(),e.use(t),this.run(r,s,this.options);}setupApplication(e,t){e.disable("x-powered-by"),e.use(R.json()),e.use(K({methods:"GET"})),e.use(R.static(u+"/public")),e.use(this.xPoweredMiddleware),e.use(t.middleware);}setupTemplateEngine(e){e.set("views",g.join(u,"views")),e.set("view engine","ejs"),e.use(X),e.set("layout","layouts/main");}listen(e,t){e.database.enabled()&&p.info("database: %s",e.database.directoryPath()),p.info("server: http://localhost:%d",t),console.log(Z.textSync("FAKELAB"));}run(e,t,r){let{port:s}=t.serverOpts(r.pathPrefix,r.port);e.listen(s,"localhost",()=>this.listen(t,s)),e.on("close",()=>{p.close();});}xPoweredMiddleware(e,t,r){t.setHeader("x-powered-by","fakelab"),r();}loadLocalEnv(){}};var A=new Command,P=O.readJSONSync(g.join(u,"../package.json"));A.name(P.name).description(P.description).version(P.version);A.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 o=>{E.init(o).start();});A.parse();
package/lib/main.d.ts CHANGED
@@ -3,34 +3,112 @@ type FakerLocale = (typeof FAKER_LOCALES)[number];
3
3
 
4
4
  type ServerOptions = {
5
5
  /**
6
+ * Port number that the mock server will listen on.
6
7
  * @default 5200
7
8
  */
8
9
  port?: number;
9
10
  /**
11
+ * URL path prefix used for all generated endpoints.
10
12
  * @default `api`
11
13
  */
12
14
  pathPrefix?: string;
13
15
  };
14
16
  type FakerEngineOptions = {
17
+ /**
18
+ * Locale used by the faker engine when generating mock data.
19
+ * Controls language-specific values such as names, addresses, etc.
20
+ */
15
21
  locale?: FakerLocale;
16
22
  };
17
23
  type DatabaseOptions = {
24
+ /**
25
+ * Enables persistent storage for mock data.
26
+ * When enabled, `POST` mutation will be stored.
27
+ */
18
28
  enabled: boolean;
29
+ /**
30
+ * Destination directory for the local database files.
31
+ * @default `<ROOT>/db`
32
+ */
19
33
  dest?: string;
20
34
  };
21
35
  type BrowserExposeOptions = {
36
+ /**
37
+ * Name of the exposed object or module in the browser.
38
+ * @example `fakelab`
39
+ */
22
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
+ */
23
46
  mode: "module" | "global";
24
47
  };
25
48
  type BrowserOptions = {
49
+ /**
50
+ * Controls how the runtime API is exposed in the browser.
51
+ */
26
52
  expose?: Partial<BrowserExposeOptions>;
27
53
  };
54
+ type NetworkBehaviourOptions = {
55
+ /**
56
+ * Artificial response delay in milliseconds.
57
+ * Can be a fixed number or a range `[min, max]`.
58
+ * @example 300 or [200, 800]
59
+ */
60
+ delay?: number | [number, number];
61
+ /**
62
+ * Probability (0–1) that a request will fail with an error response.
63
+ * @example `0.1` → 10% error rate
64
+ */
65
+ errorRate?: number;
66
+ /**
67
+ * Probability (0–1) that a request will timeout.
68
+ * @example `0.05` → 5% timeout rate
69
+ */
70
+ timeoutRate?: number;
71
+ /**
72
+ * When enabled, all requests will behave as if the network is offline.
73
+ */
74
+ offline?: boolean;
75
+ };
76
+ type NetworkOptions = NetworkBehaviourOptions & {
77
+ /**
78
+ * Name of the active network preset.
79
+ */
80
+ preset?: string;
81
+ /**
82
+ * Collection of predefined network behavior presets.
83
+ * Each preset can simulate different network conditions.
84
+ */
85
+ presets?: Record<string, NetworkBehaviourOptions>;
86
+ };
28
87
  type ConfigOptions = {
88
+ /**
89
+ * Path or paths to the source files that define the typescript types.
90
+ */
29
91
  sourcePath: string | string[];
92
+ /**
93
+ * Server-related configuration.
94
+ */
30
95
  server?: ServerOptions;
96
+ /**
97
+ * Faker engine configuration for data generation.
98
+ */
31
99
  faker?: FakerEngineOptions;
100
+ /**
101
+ * Database persistence configuration.
102
+ */
32
103
  database?: DatabaseOptions;
104
+ /**
105
+ * Browser runtime exposure options.
106
+ */
33
107
  browser?: BrowserOptions;
108
+ /**
109
+ * Network simulation configuration.
110
+ */
111
+ network?: NetworkOptions;
34
112
  };
35
113
  type ServerCLIOptions = {
36
114
  source?: string;
@@ -43,15 +121,20 @@ declare class Config {
43
121
  private readonly opts;
44
122
  readonly RUNTIME_SOURCE_FILENAME = "runtime.js";
45
123
  readonly RUNTIME_DECL_FILENAME = "runtime.d.ts";
124
+ readonly FAKELAB_PERSIST_DIR = ".fakelab";
125
+ NETWORK_DEFAULT_OPTIONS: Readonly<NetworkOptions>;
46
126
  constructor(opts: ConfigOptions);
47
127
  files(_sourcePath?: string): Promise<string[]>;
48
128
  serverOpts(prefix?: string, port?: number): Required<ServerOptions>;
49
129
  browserOpts(name?: string, mode?: "module" | "global"): Required<BrowserOptions>;
130
+ networkOpts(): NetworkOptions;
50
131
  fakerOpts(locale?: FakerLocale): Required<FakerEngineOptions>;
51
132
  generateInFileRuntimeConfig(dirname: string, options: ServerCLIOptions): Promise<void>;
52
- getDatabaseDirectoryPath(): string;
53
- databaseEnabled(): boolean;
54
- private tryPrepareDatabase;
133
+ get database(): {
134
+ enabled: () => boolean;
135
+ directoryPath: () => string;
136
+ };
137
+ private initializeDatabase;
55
138
  private modifyGitignoreFile;
56
139
  private tryStat;
57
140
  private isReadable;
package/lib/main.js CHANGED
@@ -1,10 +1,10 @@
1
- import R from'fast-glob';import i from'path';import p from'fs-extra';import {stat,access,constants}from'fs/promises';import C from'is-glob';import l from'winston';import u from'picocolors';import {transform}from'esbuild';function N(o){switch(o){case "info":return u.blueBright(o.toUpperCase());case "error":return u.redBright(o.toUpperCase());case "warn":return u.yellowBright(o.toUpperCase());default:return o.toUpperCase()}}var k=l.format.printf(({level:o,message:t,timestamp:e})=>`${u.dim(`[${e}]`)} ${N(o)} ${t}`),f=l.createLogger({format:l.format.combine(l.format.timestamp(),l.format.splat(),k),transports:[new l.transports.Console]}),D=new Intl.ListFormat("en",{style:"long",type:"unit"}),s=class{static info(t,...e){return f.info(t,...e)}static warn(t,...e){return f.warn(t,...e)}static error(t,...e){return f.error(t,...e)}static list(t){return D.format(t)}static close(){f.removeAllListeners(),f.close();}};var w="fakelab",y="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 T(){let o=Intl.DateTimeFormat().resolvedOptions().locale;if(!o)return "en";let[t]=o.split("-"),e=t.toLowerCase();return h.includes(e)?e:"en"}var O=`let fl = {};
1
+ import L from'fast-glob';import i from'path';import d from'fs-extra';import {stat,access,constants}from'fs/promises';import C from'is-glob';import p from'winston';import u from'picocolors';import {transform}from'esbuild';function F(o){switch(o){case "info":return u.blueBright(o.toUpperCase());case "error":return u.redBright(o.toUpperCase());case "warn":return u.yellowBright(o.toUpperCase());default:return o.toUpperCase()}}var M=p.format.printf(({level:o,message:e,timestamp:t})=>`${u.dim(`[${t}]`)} ${F(o)} ${e}`),c=p.createLogger({format:p.format.combine(p.format.timestamp(),p.format.splat(),M),transports:[new p.transports.Console]}),_=new Intl.ListFormat("en",{style:"long",type:"unit"}),s=class{static info(e,...t){return c.info(e,...t)}static warn(e,...t){return c.warn(e,...t)}static error(e,...t){return c.error(e,...t)}static debug(e,...t){if(!(typeof process>"u"||!process.env.DEBUG))return c.info(e,...t)}static list(e){return _.format(e)}static close(){c.removeAllListeners(),c.close();}};var y="fakelab",g="module",E=["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 T(){let o=Intl.DateTimeFormat().resolvedOptions().locale;if(!o)return "en";let[e]=o.split("-"),t=e.toLowerCase();return E.includes(t)?t:"en"}var O=`let fl = {};
2
2
  let db = {};
3
3
  fl.url = () => "http://localhost:PORT/PREFIX/";
4
4
  fl.fetch = async function (name, count) {
5
5
  const search = count ? "?count=" + count : "";
6
6
 
7
- const response = await fetch(fl.url() + name + search);
7
+ const response = await fetch(fl.url() + name + search, { headers: {"Content-Type": "application/json" }});
8
8
 
9
9
  if (!response.ok) throw new Error("[fakelab] Failed to retreived mock data.");
10
10
 
@@ -17,7 +17,7 @@ db.enabled = () => ENABLED_COND;
17
17
  db.get = async function (name) {
18
18
  if (!db.enabled()) throw new Error("[fakelab] Database is not enabled.");
19
19
 
20
- const response = await fetch(NAME.url() + "database/" + name);
20
+ const response = await fetch(NAME.url() + "database/" + name, { headers: {"Content-Type": "application/json" }});
21
21
 
22
22
  if (!response.ok) throw new Error("[fakelab] Failed to retreived data from database.");
23
23
 
@@ -28,10 +28,18 @@ db.get = async function (name) {
28
28
  db.post = async function (name) {
29
29
  if (!db.enabled()) throw new Error("[fakelab] Database is not enabled.");
30
30
 
31
- const response = await fetch(NAME.url() + "database/" + name, { method: "POST" });
31
+ const response = await fetch(NAME.url() + "database/" + name, { method: "POST", headers: {"Content-Type": "application/json" } });
32
32
 
33
33
  if (!response.ok) throw new Error("[fakelab] Failed to post data to database.");
34
34
  };
35
+ db.seed = async function (name, count) {
36
+ if (!db.enabled()) throw new Error("[fakelab] Database is not enabled.");
37
+
38
+ const response = await fetch(NAME.url() + "database/seed/" + name, { method: "POST", body: JSON.stringify({ count }), headers: {"Content-Type": "application/json" } });
39
+
40
+ if (!response.ok) throw new Error("[fakelab] Failed to seed data to database.");
41
+ };
42
+
35
43
 
36
44
  const NAME = Object.freeze(fl);
37
45
  const database = Object.freeze(db);
@@ -39,6 +47,7 @@ const database = Object.freeze(db);
39
47
  export { NAME, database };`,A=`declare function fetch<T extends keyof Runtime$, CT extends number | undefined = undefined>(name: T, count?: CT): Promise<Result$<Runtime$[T], CT>>;
40
48
  declare function get<T extends keyof Runtime$>(name: T): Promise<Runtime$[T]>;
41
49
  declare function post<T extends keyof Runtime$>(name: T): Promise<void>;
50
+ declare function seed<T extends keyof Runtime$>(name: T, count?: number): Promise<void>;
42
51
  declare function enabled(): boolean;
43
52
  declare function url(): string;
44
53
  declare const NAME: {
@@ -48,18 +57,19 @@ declare const NAME: {
48
57
  declare const database: {
49
58
  get: typeof get;
50
59
  post: typeof post;
60
+ seed: typeof seed;
51
61
  enabled: typeof enabled;
52
62
  };
53
63
  type Result$<T, CT> = CT extends number ? (CT extends 0 ? T : T[]) : T;
54
64
  interface Runtime$ {}
55
65
 
56
- export { NAME, database };`,L=`global.NAME = {};
66
+ export { NAME, database };`,k=`global.NAME = {};
57
67
  global.NAME.database = {};
58
68
  global.NAME.url = () => "http://localhost:PORT/PREFIX/";
59
69
  global.NAME.fetch = async function (name, count) {
60
70
  const search = count ? "?count=" + count : "";
61
71
 
62
- const response = await fetch(global.NAME.url() + name + search);
72
+ const response = await fetch(global.NAME.url() + name + search, { headers: {"Content-Type": "application/json" } });
63
73
 
64
74
  if (!response.ok) throw new Error("[fakelab] Failed to retreived mock data.");
65
75
 
@@ -71,7 +81,7 @@ global.NAME.database.enabled = () => ENABLED_COND;
71
81
  global.NAME.database.get = async function (name) {
72
82
  if (!global.NAME.database.enabled()) throw new Error("[fakelab] Database is not enabled.");
73
83
 
74
- const response = await fetch(global.NAME.url() + "database/" + name);
84
+ const response = await fetch(global.NAME.url() + "database/" + name, { headers: {"Content-Type": "application/json" } });
75
85
 
76
86
  if (!response.ok) throw new Error("[fakelab] Failed to retreived data from database.");
77
87
 
@@ -82,21 +92,29 @@ global.NAME.database.get = async function (name) {
82
92
  global.NAME.database.post = async function (name) {
83
93
  if (!global.NAME.database.enabled()) throw new Error("[fakelab] Database is not enabled.");
84
94
 
85
- const response = await fetch(global.NAME.url() + "database/" + name, { method: "POST" });
95
+ const response = await fetch(global.NAME.url() + "database/" + name, { method: "POST", headers: {"Content-Type": "application/json" } });
86
96
 
87
97
  if (!response.ok) throw new Error("[fakelab] Failed to post data to database.");
88
98
  };
99
+ global.NAME.database.seed = async function (name, count) {
100
+ if (!global.NAME.database.enabled()) throw new Error("[fakelab] Database is not enabled.");
101
+
102
+ const response = await fetch(global.NAME.url() + "database/seed/" + name, { method: "POST", body: JSON.stringify({ count }), headers: {"Content-Type": "application/json" } });
103
+
104
+ if (!response.ok) throw new Error("[fakelab] Failed to seed data to database.");
105
+ };
89
106
 
90
107
  const NAME = Object.freeze(fl);
91
108
  const database = Object.freeze(db);
92
109
 
93
- export { NAME, database };`,M=`export {};
110
+ export { NAME, database };`,N=`export {};
94
111
 
95
112
  declare global {
96
113
  const database: {
97
114
  enabled(): boolean;
98
115
  get<T extends keyof Runtime$>(name: T): Promise<Runtime$[T]>;
99
116
  post(name: keyof Runtime$): Promise<void>;
117
+ seed(name: keyof Runtime$, count?: number): Promise<void>;
100
118
  };
101
119
  const NAME: {
102
120
  fetch<T extends keyof Runtime$, CT extends number | undefined = undefined>(name: T, count?: CT): Promise<Result$<Runtime$[T], CT>>;
@@ -114,6 +132,6 @@ declare global {
114
132
  NAME: typeof NAME;
115
133
  }
116
134
  }
117
- }`;var g=class{constructor(t,e,r,a){this.port=t;this.prefix=e;this.browserOptions=r;this.dbOptions=a;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 a in e)r=r.replace(new RegExp(a,"g"),e[a].toString().trim());return r}async prepareGlobalSource(){let t=this.replacer(L,{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&&s.warn(e.message),t}}globalDeclaration(){return this.replacer(M,{NAME:this.name})}},b=class o extends g{constructor(e,r,a,c){super(e,r,a,c);this.port=e;this.prefix=r;this.browserOptions=a;this.dbOptions=c;}static init(e,r,a,c){let E=new o(e,r,a,c);return new Proxy(E,{get(n,d){return d==="prepareSource"?a?.expose?.mode==="global"?n.prepareGlobalSource:n.prepareSource:d==="declaration"?a?.expose?.mode==="global"?n.globalDeclaration:n.declaration:n[d]}})}async prepareSource(){let e=this.replacer(O,{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&&s.warn(r.message),e}}declaration(){return this.replacer(A,{NAME:this.name})}};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);}RUNTIME_SOURCE_FILENAME="runtime.js";RUNTIME_DECL_FILENAME="runtime.d.ts";async files(t){let e=this.resolveSourcePath(t||this.opts.sourcePath),r=Array.from(new Set((await Promise.all(e.map(a=>this.resolveTSFiles(a)))).flat()));return r.length===0&&(s.error("No Typescript files found in: %s",s.list(e.map(a=>i.basename(a)))),process.exit(1)),r}serverOpts(t,e){return {pathPrefix:t||this.opts.server?.pathPrefix||"api",port:e||this.opts.server?.port||5200}}browserOpts(t,e){return {expose:{mode:e||this.opts.browser?.expose?.mode||y,name:t||this.opts.browser?.expose?.name||w}}}fakerOpts(t){let e=(t||this.opts.faker?.locale)?.toLowerCase();return e&&h.includes(e)?{locale:e}:{locale:T()}}async generateInFileRuntimeConfig(t,e){let{port:r,pathPrefix:a}=this.serverOpts(e.pathPrefix,e.port);await this.tryPrepareDatabase();let c=i.resolve(t,this.RUNTIME_SOURCE_FILENAME),E=i.resolve(t,this.RUNTIME_DECL_FILENAME),n=b.init(r,a,this.opts.browser,this.opts.database),d=await n.prepareSource();await Promise.all([p.writeFile(c,d),p.writeFile(E,n.declaration())]);}getDatabaseDirectoryPath(){let t=this.opts.database?.dest||"db";return i.resolve(process.cwd(),t)}databaseEnabled(){return this.opts.database?.enabled??false}async tryPrepareDatabase(){if(this.databaseEnabled())try{let t=this.opts.database?.dest||"db";await p.ensureDir(this.getDatabaseDirectoryPath()),await this.modifyGitignoreFile(t);}catch{s.error("Could not create database.");}else (!this.opts.database||!this.databaseEnabled())&&await p.rm(this.getDatabaseDirectoryPath(),{force:true,recursive:true});}async modifyGitignoreFile(t){try{let e=i.resolve(process.cwd(),".gitignore");if((await p.readFile(e,{encoding:"utf8"})).split(`
118
- `).some(a=>a.trim()===t.trim()))return;await p.appendFile(e,`
119
- ${t}`);}catch{s.error("Could not modify .gitignore file.");}}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=>C(r,{strict:true})?r:i.resolve(r))}async resolveTSFiles(t){if(C(t,{strict:true}))return s.info("source: %s",t),R(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)||(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),R("**/*.ts",{cwd:e,absolute:true,ignore:["**/*.d.ts"]})):(s.warn("invalid source: [REDACTED]/%s",i.basename(r)),[])}};function U(o){return new m(o)}export{U as defineConfig};
135
+ }`;var w=class{constructor(e,t,r,a){this.port=e;this.prefix=t;this.browserOptions=r;this.dbOptions=a;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 a in t)r=r.replace(new RegExp(a,"g"),t[a].toString().trim());return r}async prepareGlobalSource(){let e=this.replacer(k,{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(N,{NAME:this.name})}},b=class o extends w{constructor(t,r,a,l){super(t,r,a,l);this.port=t;this.prefix=r;this.browserOptions=a;this.dbOptions=l;}static init(t,r,a,l){let h=new o(t,r,a,l);return new Proxy(h,{get(n,f){return f==="prepareSource"?a?.expose?.mode==="global"?n.prepareGlobalSource:n.prepareSource:f==="declaration"?a?.expose?.mode==="global"?n.globalDeclaration:n.declaration:n[f]}})}async prepareSource(){let t=this.replacer(O,{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(A,{NAME:this.name})}};var m=class{constructor(e){this.opts=e;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(e){let t=this.resolveSourcePath(e||this.opts.sourcePath),r=Array.from(new Set((await Promise.all(t.map(a=>this.resolveTSFiles(a)))).flat()));return r.length===0&&(s.error("No Typescript files found in: %s",s.list(t.map(a=>i.basename(a)))),process.exit(1)),r}serverOpts(e,t){return {pathPrefix:e||this.opts.server?.pathPrefix||"api",port:t||this.opts.server?.port||5200}}browserOpts(e,t){return {expose:{mode:t||this.opts.browser?.expose?.mode||g,name:e||this.opts.browser?.expose?.name||y}}}networkOpts(){let e=this.opts.network?.preset,t=this.opts.network?.presets??{};return !e||!t[e]?this.NETWORK_DEFAULT_OPTIONS:{...t[e],...this.opts.network??{}}}fakerOpts(e){let t=(e||this.opts.faker?.locale)?.toLowerCase();return t&&E.includes(t)?{locale:t}:{locale:T()}}async generateInFileRuntimeConfig(e,t){let{port:r,pathPrefix:a}=this.serverOpts(t.pathPrefix,t.port);await this.initializeDatabase();let l=i.resolve(e,this.RUNTIME_SOURCE_FILENAME),h=i.resolve(e,this.RUNTIME_DECL_FILENAME),n=b.init(r,a,this.opts.browser,this.opts.database),f=await n.prepareSource();await Promise.all([d.writeFile(l,f),d.writeFile(h,n.declaration())]);}get database(){return {enabled:()=>this.opts.database?.enabled??false,directoryPath:()=>{let e=this.opts.database?.dest||"db";return i.resolve(process.cwd(),e)}}}async initializeDatabase(){if(this.database.enabled())try{let e=this.opts.database?.dest||"db";await d.ensureDir(this.database.directoryPath()),await this.modifyGitignoreFile(e);}catch{s.error("Could not create database.");}else (!this.opts.database||!this.database.enabled())&&await d.rm(this.database.directoryPath(),{force:true,recursive:true});}async modifyGitignoreFile(e){try{let t=i.resolve(process.cwd(),".gitignore");if((await d.readFile(t,{encoding:"utf8"})).split(`
136
+ `).some(a=>a.trim()===e.trim()))return;await d.appendFile(t,`
137
+ ${e}`);}catch{s.error("Could not modify .gitignore file.");}}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=>C(r,{strict:true})?r:i.resolve(r))}async resolveTSFiles(e){if(C(e,{strict:true}))return s.info("source: %s",e),L(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),L("**/*.ts",{cwd:t,absolute:true,ignore:["**/*.d.ts"]})):(s.warn("invalid source: [REDACTED]/%s",i.basename(r)),[])}};function U(o){return new m(o)}export{U as defineConfig};
@@ -9,7 +9,7 @@
9
9
  </div>
10
10
  <div class="sidebar_content_leading">
11
11
  <div class="sidebar_content_indicator_icon"></div>
12
- <span class="sidebar_content_title">Registered entities</span>
12
+ <span class="sidebar_content_title">Registered tables</span>
13
13
  </div>
14
14
  <div class="sidebar_content_list">
15
15
  <% entities.forEach((entity,entityName) => { %>
@@ -3,7 +3,7 @@
3
3
  <header class="header"></header>
4
4
  <div class="preview">
5
5
  <div class="nodata">
6
- <span>Select a entity to load the table.</span>
6
+ <span>Select a table to load the data.</span>
7
7
  </div>
8
8
  </div>
9
9
  </main>
@@ -3,7 +3,7 @@
3
3
  <header class="header"></header>
4
4
  <div class="preview">
5
5
  <div class="nodata">
6
- <span>Select a interface to load data.</span>
6
+ <span>Select a interface to load the data.</span>
7
7
  </div>
8
8
  </div>
9
9
  </main>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fakelab",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A easy-config mock server for frontend developers.",
@@ -33,10 +33,9 @@
33
33
  "url": "https://github.com/alirezahematidev/fakelab/issues"
34
34
  },
35
35
  "scripts": {
36
- "build": "tsup",
36
+ "build": "tsc && tsup",
37
37
  "release": "release-it --ci",
38
- "dev": "node --import=tsx src/cli.ts serve",
39
- "serve": "yarn build && node lib/cli.js serve"
38
+ "serve": "cross-env NODE_ENV=development yarn build && node lib/cli.js serve"
40
39
  },
41
40
  "directories": {
42
41
  "lib": "./lib"
@@ -62,6 +61,7 @@
62
61
  "@types/fs-extra": "^11.0.4",
63
62
  "@types/is-glob": "^4.0.4",
64
63
  "@types/node": "^24.10.1",
64
+ "cross-env": "^10.1.0",
65
65
  "release-it": "^19.0.6",
66
66
  "tsup": "^8.5.1",
67
67
  "tsx": "^4.21.0",