klaim 1.0.0

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.
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://unpkg.com/release-it/schema/release-it.json",
3
+ "github": {
4
+ "release": true
5
+ },
6
+ "npm": {
7
+ "publish": true
8
+ },
9
+ "git": {
10
+ "requiredBranch": "main",
11
+ "commitMessage": "chore: release v%s"
12
+ },
13
+ "hooks": {
14
+ "before:init": [
15
+ "git pull",
16
+ "npm run lint"
17
+ ]
18
+ }
19
+ }
package/Makefile ADDED
@@ -0,0 +1,5 @@
1
+ dev:
2
+ nr dev
3
+
4
+ dev-test:
5
+ cd ../klaim-test && nr dev
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # klaim
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.1.17. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
package/bun.lockb ADDED
Binary file
@@ -0,0 +1,298 @@
1
+ var f = Object.defineProperty;
2
+ var A = (n, t, e) => t in n ? f(n, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : n[t] = e;
3
+ var i = (n, t, e) => A(n, typeof t != "symbol" ? t + "" : t, e);
4
+ function l(n) {
5
+ return n.trim().replace(/^\/|\/$/g, "");
6
+ }
7
+ function p(n) {
8
+ return n.replace(/([-_][a-z])/gi, (t) => t.toUpperCase().replace("-", "").replace("_", "")).replace(/(^\w)/, (t) => t.toLowerCase());
9
+ }
10
+ var m = /* @__PURE__ */ ((n) => (n.GET = "GET", n.POST = "POST", n.PUT = "PUT", n.DELETE = "DELETE", n.PATCH = "PATCH", n.OPTIONS = "OPTIONS", n))(m || {});
11
+ class d {
12
+ /**
13
+ * Constructor
14
+ *
15
+ * @param name - The name of the route
16
+ * @param url - The URL of the route
17
+ * @param headers - The headers to be sent with the request
18
+ * @param method - The HTTP method of the route
19
+ */
20
+ constructor(t, e, r, a = "GET") {
21
+ i(this, "api", "undefined");
22
+ i(this, "name");
23
+ i(this, "url");
24
+ i(this, "method");
25
+ i(this, "headers");
26
+ i(this, "arguments", /* @__PURE__ */ new Set());
27
+ this.name = p(t), this.name !== t && console.warn(`Route name "${t}" has been camelCased to "${this.name}"`), this.url = l(e), this.headers = r || {}, this.method = a, this.detectArguments();
28
+ }
29
+ /**
30
+ * Creates a new route
31
+ *
32
+ * @param name - The name of the route
33
+ * @param url - The URL of the route
34
+ * @param headers - The headers to be sent with the request
35
+ * @param method - The HTTP method of the route
36
+ * @returns The new route
37
+ */
38
+ static createRoute(t, e, r, a) {
39
+ const s = new d(t, e, r, a);
40
+ return o.i.registerRoute(s), s;
41
+ }
42
+ /**
43
+ * Creates a new route with the GET method
44
+ *
45
+ * @param name - The name of the route
46
+ * @param url - The URL of the route
47
+ * @param headers - The headers to be sent with the request
48
+ * @returns The new route
49
+ */
50
+ static get(t, e, r = {}) {
51
+ return this.createRoute(
52
+ t,
53
+ e,
54
+ r,
55
+ "GET"
56
+ /* GET */
57
+ );
58
+ }
59
+ /**
60
+ * Creates a new route with the POST method
61
+ *
62
+ * @param name - The name of the route
63
+ * @param url - The URL of the route
64
+ * @param headers - The headers to be sent with the request
65
+ * @returns The new route
66
+ */
67
+ static post(t, e, r) {
68
+ return this.createRoute(
69
+ t,
70
+ e,
71
+ r,
72
+ "POST"
73
+ /* POST */
74
+ );
75
+ }
76
+ /**
77
+ * Creates a new route with the PUT method
78
+ *
79
+ * @param name - The name of the route
80
+ * @param url - The URL of the route
81
+ * @param headers - The headers to be sent with the request
82
+ * @returns The new route
83
+ */
84
+ static put(t, e, r) {
85
+ return this.createRoute(
86
+ t,
87
+ e,
88
+ r,
89
+ "PUT"
90
+ /* PUT */
91
+ );
92
+ }
93
+ /**
94
+ * Creates a new route with the DELETE method
95
+ *
96
+ * @param name - The name of the route
97
+ * @param url - The URL of the route
98
+ * @param headers - The headers to be sent with the request
99
+ * @returns The new route
100
+ */
101
+ static delete(t, e, r) {
102
+ return this.createRoute(
103
+ t,
104
+ e,
105
+ r,
106
+ "DELETE"
107
+ /* DELETE */
108
+ );
109
+ }
110
+ /**
111
+ * Creates a new route with the PATCH method
112
+ *
113
+ * @param name - The name of the route
114
+ * @param url - The URL of the route
115
+ * @param headers - The headers to be sent with the request
116
+ * @returns The new route
117
+ */
118
+ static patch(t, e, r) {
119
+ return this.createRoute(
120
+ t,
121
+ e,
122
+ r,
123
+ "PATCH"
124
+ /* PATCH */
125
+ );
126
+ }
127
+ /**
128
+ * Creates a new route with the OPTIONS method
129
+ *
130
+ * @param name - The name of the route
131
+ * @param url - The URL of the route
132
+ * @param headers - The headers to be sent with the request
133
+ * @returns The new route
134
+ */
135
+ static options(t, e, r) {
136
+ return this.createRoute(
137
+ t,
138
+ e,
139
+ r,
140
+ "OPTIONS"
141
+ /* OPTIONS */
142
+ );
143
+ }
144
+ /**
145
+ * Detects the arguments in the URL
146
+ */
147
+ detectArguments() {
148
+ const t = this.url.match(/\[([^\]]+)]/g);
149
+ t && t.forEach((e) => {
150
+ const r = e.replace(/\[|]/g, "");
151
+ this.arguments.add(r);
152
+ });
153
+ }
154
+ }
155
+ const h = {};
156
+ async function T(n, t, e = {}, r = {}) {
157
+ const a = g(`${n.url}/${t.url}`, t, e), s = {};
158
+ return r && t.method !== m.GET && (s.body = JSON.stringify(r)), s.headers = {
159
+ "Content-Type": "application/json",
160
+ ...n.headers,
161
+ ...t.headers
162
+ }, s.method = t.method, await (await fetch(a, s)).json();
163
+ }
164
+ function g(n, t, e) {
165
+ let r = n;
166
+ return t.arguments.forEach((a) => {
167
+ if (e[a] === void 0)
168
+ throw new Error(`Argument ${a} is missing`);
169
+ r = r.replace(`[${a}]`, e[a]);
170
+ }), r;
171
+ }
172
+ const c = class c {
173
+ /**
174
+ * Constructor
175
+ */
176
+ constructor() {
177
+ i(this, "_apis", /* @__PURE__ */ new Map());
178
+ i(this, "_currentApi", null);
179
+ }
180
+ /**
181
+ * Singleton instance
182
+ *
183
+ * @returns The singleton instance
184
+ */
185
+ static get i() {
186
+ return c._instance || (c._instance = new c()), c._instance;
187
+ }
188
+ /**
189
+ * Registers an API
190
+ *
191
+ * @param api - The API to register
192
+ */
193
+ registerApi(t) {
194
+ this._apis.set(t.name, t), h[t.name] = {};
195
+ }
196
+ /**
197
+ * Sets the current API
198
+ *
199
+ * @param name - The name of the API
200
+ */
201
+ setCurrent(t) {
202
+ const e = this._apis.get(t);
203
+ if (!e)
204
+ throw new Error(`API ${t} not found`);
205
+ this._currentApi = e;
206
+ }
207
+ /**
208
+ * Clears the current API
209
+ */
210
+ clearCurrent() {
211
+ this._currentApi = null;
212
+ }
213
+ /**
214
+ * Registers a route
215
+ *
216
+ * @param route - The route to register
217
+ */
218
+ registerRoute(t) {
219
+ if (!this._currentApi)
220
+ throw new Error("No current API set, use Route only inside Api.create callback");
221
+ t.api = this._currentApi.name, this._currentApi.routes.set(t.name, t), this.addToKlaimRoute(t.api, t);
222
+ }
223
+ /**
224
+ * Gets an API
225
+ *
226
+ * @param name - The name of the API
227
+ * @returns The API
228
+ */
229
+ getApi(t) {
230
+ return this._apis.get(t);
231
+ }
232
+ /**
233
+ * Gets a route
234
+ *
235
+ * @param api - The name of the API
236
+ * @param name - The name of the route
237
+ * @returns The route
238
+ */
239
+ getRoute(t, e) {
240
+ const r = this._apis.get(t);
241
+ if (!r)
242
+ throw new Error(`API ${t} not found`);
243
+ return r.routes.get(e);
244
+ }
245
+ /**
246
+ * Adds a route to Klaim object
247
+ *
248
+ * @param apiName - The name of the API
249
+ * @param route - The route to add
250
+ */
251
+ addToKlaimRoute(t, e) {
252
+ h[t][e.name] = async (r = {}, a = {}) => {
253
+ const s = c.i._apis.get(t);
254
+ if (!s)
255
+ throw new Error(`API ${e.api} not found`);
256
+ return T(s, e, r, a);
257
+ };
258
+ }
259
+ };
260
+ i(c, "_instance");
261
+ let o = c;
262
+ class w {
263
+ /**
264
+ * Constructor
265
+ *
266
+ * @param name - The name of the API
267
+ * @param url - The base URL of the API
268
+ * @param headers - The headers to be sent with each request
269
+ */
270
+ constructor(t, e, r) {
271
+ i(this, "name");
272
+ i(this, "url");
273
+ i(this, "headers");
274
+ i(this, "routes", /* @__PURE__ */ new Map());
275
+ this.name = t, this.url = l(e), this.headers = r || {};
276
+ }
277
+ /**
278
+ * Creates a new API
279
+ *
280
+ * @param name - The name of the API
281
+ * @param url - The base URL of the API
282
+ * @param callback - The callback to define the routes
283
+ * @param headers - The headers to be sent with each request
284
+ * @returns The new API
285
+ */
286
+ static create(t, e, r, a = {}) {
287
+ const s = p(t);
288
+ s !== t && console.warn(`API name "${t}" has been camelCased to "${s}"`);
289
+ const u = new w(s, e, a);
290
+ return o.i.registerApi(u), o.i.setCurrent(s), r(), o.i.clearCurrent(), u;
291
+ }
292
+ }
293
+ export {
294
+ w as Api,
295
+ h as Klaim,
296
+ o as Registry,
297
+ d as Route
298
+ };
@@ -0,0 +1 @@
1
+ (function(i,a){typeof exports=="object"&&typeof module<"u"?a(exports):typeof define=="function"&&define.amd?define(["exports"],a):(i=typeof globalThis<"u"?globalThis:i||self,a(i.klaim={}))})(this,function(i){"use strict";var A=Object.defineProperty;var E=(i,a,l)=>a in i?A(i,a,{enumerable:!0,configurable:!0,writable:!0,value:l}):i[a]=l;var o=(i,a,l)=>E(i,typeof a!="symbol"?a+"":a,l);function a(r){return r.trim().replace(/^\/|\/$/g,"")}function l(r){return r.replace(/([-_][a-z])/gi,e=>e.toUpperCase().replace("-","").replace("_","")).replace(/(^\w)/,e=>e.toLowerCase())}var T=(r=>(r.GET="GET",r.POST="POST",r.PUT="PUT",r.DELETE="DELETE",r.PATCH="PATCH",r.OPTIONS="OPTIONS",r))(T||{});class p{constructor(e,t,n,c="GET"){o(this,"api","undefined");o(this,"name");o(this,"url");o(this,"method");o(this,"headers");o(this,"arguments",new Set);this.name=l(e),this.name!==e&&console.warn(`Route name "${e}" has been camelCased to "${this.name}"`),this.url=a(t),this.headers=n||{},this.method=c,this.detectArguments()}static createRoute(e,t,n,c){const s=new p(e,t,n,c);return h.i.registerRoute(s),s}static get(e,t,n={}){return this.createRoute(e,t,n,"GET")}static post(e,t,n){return this.createRoute(e,t,n,"POST")}static put(e,t,n){return this.createRoute(e,t,n,"PUT")}static delete(e,t,n){return this.createRoute(e,t,n,"DELETE")}static patch(e,t,n){return this.createRoute(e,t,n,"PATCH")}static options(e,t,n){return this.createRoute(e,t,n,"OPTIONS")}detectArguments(){const e=this.url.match(/\[([^\]]+)]/g);e&&e.forEach(t=>{const n=t.replace(/\[|]/g,"");this.arguments.add(n)})}}const d={};async function g(r,e,t={},n={}){const c=w(`${r.url}/${e.url}`,e,t),s={};return n&&e.method!==T.GET&&(s.body=JSON.stringify(n)),s.headers={"Content-Type":"application/json",...r.headers,...e.headers},s.method=e.method,await(await fetch(c,s)).json()}function w(r,e,t){let n=r;return e.arguments.forEach(c=>{if(t[c]===void 0)throw new Error(`Argument ${c} is missing`);n=n.replace(`[${c}]`,t[c])}),n}const u=class u{constructor(){o(this,"_apis",new Map);o(this,"_currentApi",null)}static get i(){return u._instance||(u._instance=new u),u._instance}registerApi(e){this._apis.set(e.name,e),d[e.name]={}}setCurrent(e){const t=this._apis.get(e);if(!t)throw new Error(`API ${e} not found`);this._currentApi=t}clearCurrent(){this._currentApi=null}registerRoute(e){if(!this._currentApi)throw new Error("No current API set, use Route only inside Api.create callback");e.api=this._currentApi.name,this._currentApi.routes.set(e.name,e),this.addToKlaimRoute(e.api,e)}getApi(e){return this._apis.get(e)}getRoute(e,t){const n=this._apis.get(e);if(!n)throw new Error(`API ${e} not found`);return n.routes.get(t)}addToKlaimRoute(e,t){d[e][t.name]=async(n={},c={})=>{const s=u.i._apis.get(e);if(!s)throw new Error(`API ${t.api} not found`);return g(s,t,n,c)}}};o(u,"_instance");let h=u;class f{constructor(e,t,n){o(this,"name");o(this,"url");o(this,"headers");o(this,"routes",new Map);this.name=e,this.url=a(t),this.headers=n||{}}static create(e,t,n,c={}){const s=l(e);s!==e&&console.warn(`API name "${e}" has been camelCased to "${s}"`);const m=new f(s,t,c);return h.i.registerApi(m),h.i.setCurrent(s),n(),h.i.clearCurrent(),m}}i.Api=f,i.Klaim=d,i.Registry=h,i.Route=p,Object.defineProperty(i,Symbol.toStringTag,{value:"Module"})});
@@ -0,0 +1,210 @@
1
+ // @ts-check
2
+ import stylistic from "@stylistic/eslint-plugin";
3
+ import simpleImportSort from "eslint-plugin-simple-import-sort";
4
+ import JSdoc from "eslint-plugin-jsdoc";
5
+
6
+
7
+ import eslint from '@eslint/js';
8
+ import tseslint from 'typescript-eslint';
9
+
10
+ export default tseslint.config(
11
+ eslint.configs.recommended,
12
+ ...tseslint.configs.recommended,
13
+ {
14
+ plugins: {
15
+ "@stylistic": stylistic,
16
+ "simple-import-sort": simpleImportSort,
17
+ "jsdoc": JSdoc
18
+ },
19
+ files: ["src/**/*.ts"],
20
+ rules: {
21
+ // ----------------------------------------
22
+ // ---------------------- Simple Import Sort
23
+ // ----------------------------------------
24
+ "simple-import-sort/exports": "error",
25
+ "simple-import-sort/imports": [
26
+ "error",
27
+ {
28
+ "groups": [
29
+ ["^\u0000"],
30
+ ["^@?\\w"],
31
+ ["^/"],
32
+ ["^\\.\\.(?!/?$)", "^\\.\\./?$"],
33
+ [
34
+ "^\\./(?=.*/)(?!/?$)",
35
+ "^\\.(?!/?$)",
36
+ "^\\./?$"
37
+ ],
38
+ ["^.+\\.s?css$"]
39
+ ]
40
+ }
41
+ ],
42
+ // ----------------------------------------
43
+ // ---------------------- TypeScript
44
+ // ----------------------------------------
45
+ "@typescript-eslint/no-explicit-any": "off",
46
+ "@typescript-eslint/ban-ts-comment": "off",
47
+ "@typescript-eslint/no-dynamic-delete": "off",
48
+ "@typescript-eslint/no-namespace": "off",
49
+ "@typescript-eslint/ban-types": "error",
50
+ "@typescript-eslint/explicit-function-return-type": "error",
51
+ "@typescript-eslint/consistent-indexed-object-style": "error",
52
+
53
+ // ----------------------------------------
54
+ // ---------------------- Stylistic
55
+ // ----------------------------------------
56
+ "@stylistic/array-bracket-spacing": ["error", "always"],
57
+ "@stylistic/function-paren-newline": ["error", "multiline"],
58
+ "@stylistic/multiline-ternary": ["error", "always-multiline"],
59
+ "@stylistic/quotes": [
60
+ "error",
61
+ "double",
62
+ {
63
+ "avoidEscape": true, "allowTemplateLiterals": true
64
+ }
65
+ ],
66
+ "@stylistic/semi": [
67
+ "error",
68
+ "always",
69
+ {
70
+ "omitLastInOneLineBlock": true, "omitLastInOneLineClassBody": true
71
+ }
72
+ ],
73
+ "@stylistic/space-before-function-paren": ["error", "always"],
74
+ "@stylistic/space-in-parens": ["error", "never"],
75
+ "@stylistic/space-infix-ops": ["error", {"int32Hint": false}],
76
+ "@stylistic/space-before-blocks": ["error", "always"],
77
+ "@stylistic/space-unary-ops": ["error", {"words": true, "nonwords": false}],
78
+ "@stylistic/keyword-spacing": ["error", {"before": true, "after": true}],
79
+ "@stylistic/block-spacing": ["error", "always"],
80
+ "@stylistic/comma-dangle": ["error", "never"],
81
+ "@stylistic/array-bracket-newline": ["error", {"multiline": true}],
82
+ "@stylistic/array-element-newline": ["error", {"multiline": true, "minItems": 3}],
83
+ "@stylistic/object-curly-newline": ["error", {"multiline": true, "consistent": true}],
84
+ "@stylistic/max-len": [
85
+ "error",
86
+ {
87
+ "code": 120,
88
+ "tabWidth": 4,
89
+ "ignoreComments": true,
90
+ "ignoreUrls": true,
91
+ "ignoreStrings": true,
92
+ "ignoreTemplateLiterals": true,
93
+ "ignoreRegExpLiterals": true,
94
+ "ignorePattern": "d=.*"
95
+ }
96
+ ],
97
+ "@stylistic/padded-blocks": ["error", "never"],
98
+ "@stylistic/no-multiple-empty-lines": ["error", {"max": 1, "maxEOF": 0}],
99
+ "@stylistic/eol-last": ["error", "always"],
100
+ "@stylistic/lines-between-class-members": ["error", "always"],
101
+ "@stylistic/brace-style": [
102
+ "error",
103
+ "1tbs",
104
+ {"allowSingleLine": true}
105
+ ],
106
+ "@stylistic/object-curly-spacing": ["error", "always"],
107
+ "@stylistic/arrow-spacing": ["error", {"before": true, "after": true}],
108
+ "@stylistic/implicit-arrow-linebreak": ["error", "beside"],
109
+ "@stylistic/arrow-parens": ["error", "as-needed"],
110
+ "@stylistic/no-trailing-spaces": ["error"],
111
+ "@stylistic/no-tabs": ["error"],
112
+ "@stylistic/no-whitespace-before-property": ["error"],
113
+ "@stylistic/template-curly-spacing": ["error", "never"],
114
+ "@stylistic/rest-spread-spacing": ["error", "never"],
115
+ "@stylistic/operator-linebreak": ["error", "before"],
116
+ "@stylistic/type-annotation-spacing": [
117
+ "error",
118
+ {
119
+ "before": false, "after": true, "overrides": {
120
+ "arrow": {
121
+ "before": true, "after": true
122
+ }
123
+ }
124
+ }
125
+ ],
126
+ "@stylistic/type-generic-spacing": ["error"],
127
+ "@stylistic/type-named-tuple-spacing": ["error"],
128
+
129
+ // ----------------------------------------
130
+ // ---------------------- JSdoc
131
+ // ----------------------------------------
132
+ "jsdoc/check-access": "error",
133
+ "jsdoc/check-alignment": "error",
134
+ "jsdoc/check-param-names": "error",
135
+ "jsdoc/check-property-names": "error",
136
+ "jsdoc/check-tag-names": "error",
137
+ "jsdoc/check-types": "error",
138
+ "jsdoc/check-values": "error",
139
+ "jsdoc/empty-tags": "error",
140
+ "jsdoc/implements-on-classes": "error",
141
+ "jsdoc/multiline-blocks": "error",
142
+ "jsdoc/no-defaults": "error",
143
+ "jsdoc/no-multi-asterisks": "error",
144
+ "jsdoc/require-jsdoc": [
145
+ "error",
146
+ {
147
+ "require": {
148
+ "FunctionDeclaration": true,
149
+ "MethodDefinition": true,
150
+ "ClassDeclaration": true,
151
+ "ArrowFunctionExpression": true,
152
+ "FunctionExpression": true
153
+ }
154
+ }
155
+ ],
156
+ "jsdoc/require-param": "error",
157
+ "jsdoc/require-param-description": "error",
158
+ "jsdoc/require-param-name": "error",
159
+ "jsdoc/require-property": "error",
160
+ "jsdoc/require-property-description": "error",
161
+ "jsdoc/require-property-name": "error",
162
+ "jsdoc/require-returns": "error",
163
+ "jsdoc/require-returns-check": "error",
164
+ "jsdoc/require-returns-description": "error",
165
+ "jsdoc/require-yields": "error",
166
+ "jsdoc/require-yields-check": "error",
167
+ "jsdoc/tag-lines": [
168
+ "error",
169
+ "never",
170
+ {
171
+ "applyToEndTag": false,
172
+ "count": 1,
173
+ "startLines": 1,
174
+ "endLines": 0
175
+ }
176
+ ],
177
+ "jsdoc/valid-types": "error",
178
+
179
+ // ----------------------------------------
180
+ // ---------------------- General
181
+ // ----------------------------------------
182
+
183
+ "no-void": "off",
184
+ "no-undef": "off",
185
+ "indent": ["error", 4],
186
+ "no-console": [
187
+ "error",
188
+ {
189
+ allow: [
190
+ "warn",
191
+ "error",
192
+ "info",
193
+ "table"
194
+ ]
195
+ }
196
+ ],
197
+ "camelcase": [
198
+ "error",
199
+ {
200
+ "properties": "never", "ignoreDestructuring": true, "allow": ["^_[a-z]+_[a-z]+$"]
201
+ }
202
+ ],
203
+ "dot-notation": "off",
204
+ "no-underscore-dangle": "off",
205
+ "func-style": ["error", "declaration"]
206
+
207
+ }
208
+ }
209
+ );
210
+
package/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ // src/index.ts
2
+
3
+ export function getHello (): string {
4
+ return 'Hello, world! ???';
5
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "klaim",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "main": "dist/klaim.cjs.js",
6
+ "module": "dist/klaim.es.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "vite build",
10
+ "dev": "vite build --watch",
11
+ "test": "vitest",
12
+ "lint": "eslint src --fix",
13
+ "release": "release-it"
14
+ },
15
+ "devDependencies": {
16
+ "@eslint/js": "^9.6.0",
17
+ "@stylistic/eslint-plugin": "^2.3.0",
18
+ "@types/bun": "latest",
19
+ "@types/eslint__js": "^8.42.3",
20
+ "@types/node": "^20.14.9",
21
+ "@vitest/coverage-v8": "^1.6.0",
22
+ "eslint": "^9.6.0",
23
+ "eslint-plugin-jsdoc": "^48.5.0",
24
+ "eslint-plugin-simple-import-sort": "^12.1.0",
25
+ "jsdom": "^24.1.0",
26
+ "release-it": "^17.4.1",
27
+ "typescript-eslint": "^7.15.0",
28
+ "vite": "^5.3.2",
29
+ "vitest": "^1.6.0"
30
+ },
31
+ "peerDependencies": {
32
+ "typescript": "^5.5.3"
33
+ }
34
+ }
@@ -0,0 +1,63 @@
1
+ import cleanUrl from "../tools/cleanUrl";
2
+ import toCamelCase from "../tools/toCamelCase";
3
+
4
+ import { Registry } from "./Registry";
5
+ import { Route } from "./Route";
6
+
7
+ interface IApi {
8
+ name: string;
9
+ url: string;
10
+ headers: IHeaders;
11
+ routes: Map<string, Route>;
12
+ }
13
+
14
+ export type IApiCallback = () => void;
15
+ export type IHeaders = Record<string, string>;
16
+
17
+ /**
18
+ * Represents an API
19
+ */
20
+ export class Api implements IApi {
21
+ public name: string;
22
+
23
+ public url: string;
24
+
25
+ public headers: IHeaders;
26
+
27
+ public routes: Map<string, Route> = new Map<string, Route>();
28
+
29
+ /**
30
+ * Constructor
31
+ *
32
+ * @param name - The name of the API
33
+ * @param url - The base URL of the API
34
+ * @param headers - The headers to be sent with each request
35
+ */
36
+ private constructor (name: string, url: string, headers: IHeaders) {
37
+ this.name = name;
38
+ this.url = cleanUrl(url);
39
+ this.headers = headers || {};
40
+ }
41
+
42
+ /**
43
+ * Creates a new API
44
+ *
45
+ * @param name - The name of the API
46
+ * @param url - The base URL of the API
47
+ * @param callback - The callback to define the routes
48
+ * @param headers - The headers to be sent with each request
49
+ * @returns The new API
50
+ */
51
+ public static create (name: string, url: string, callback: IApiCallback, headers: IHeaders = {}): Api {
52
+ const newName = toCamelCase(name);
53
+ if (newName !== name) {
54
+ console.warn(`API name "${name}" has been camelCased to "${newName}"`);
55
+ }
56
+ const api = new Api(newName, url, headers);
57
+ Registry.i.registerApi(api);
58
+ Registry.i.setCurrent(newName);
59
+ callback();
60
+ Registry.i.clearCurrent();
61
+ return api;
62
+ }
63
+ }
@@ -0,0 +1,66 @@
1
+ import { Api } from "./Api";
2
+ import { Route, RouteMethod } from "./Route";
3
+
4
+ export type IArgs = Record<string, unknown>;
5
+
6
+ export type IBody = Record<string, unknown>;
7
+
8
+ export type IRouteReference = Record<string, <T>(args?: IArgs, body?: IBody) => Promise<T>>;
9
+
10
+ export type IApiReference = Record<string, IRouteReference>;
11
+
12
+ export const Klaim: IApiReference = {};
13
+
14
+ /**
15
+ * Calls an API route
16
+ *
17
+ * @param api - The API to call
18
+ * @param route - The route to call
19
+ * @param args - The arguments to pass to the route
20
+ * @param body - The body to pass to the route
21
+ * @returns The response
22
+ */
23
+ export async function callApi<T> (api: Api, route: Route, args: IArgs = {}, body: IBody = {}): Promise<T> {
24
+ const url = applyArgs(`${api.url}/${route.url}`, route, args);
25
+
26
+ const config: Record<string, unknown> = {};
27
+
28
+ if (body && route.method !== RouteMethod.GET) {
29
+ config.body = JSON.stringify(body);
30
+ }
31
+
32
+ config.headers = {
33
+ "Content-Type": "application/json",
34
+ ...api.headers,
35
+ ...route.headers
36
+ };
37
+
38
+ config.method = route.method;
39
+
40
+ const response = await fetch(url, config);
41
+
42
+ const data = await response.json();
43
+ return data as T;
44
+ }
45
+
46
+ /**
47
+ * Applies the arguments to the URL
48
+ *
49
+ * @param url - The URL to apply the arguments to
50
+ * @param route - The route to apply the arguments to
51
+ * @param args - The arguments to apply
52
+ * @returns The new URL
53
+ */
54
+ function applyArgs (url: string, route: Route, args: IArgs): string {
55
+ let newUrl = url;
56
+ route.arguments.forEach(arg => {
57
+ const value = args[arg];
58
+ if (value === undefined) {
59
+ throw new Error(`Argument ${arg} is missing`);
60
+ }
61
+
62
+ newUrl = newUrl.replace(`[${arg}]`, <string>args[arg]);
63
+ });
64
+
65
+ return newUrl;
66
+ }
@@ -0,0 +1,126 @@
1
+ import { Api } from "./Api";
2
+ import { callApi, IArgs, IBody, Klaim } from "./Klaim";
3
+ import { Route } from "./Route";
4
+
5
+ /**
6
+ * Represents the registry
7
+ */
8
+ export class Registry {
9
+ private static _instance: Registry;
10
+
11
+ private _apis: Map<string, Api> = new Map<string, Api>();
12
+
13
+ private _currentApi: Api | null = null;
14
+
15
+ /**
16
+ * Constructor
17
+ */
18
+ private constructor () {
19
+ }
20
+
21
+ /**
22
+ * Singleton instance
23
+ *
24
+ * @returns The singleton instance
25
+ */
26
+ public static get i (): Registry {
27
+ if (!Registry._instance) {
28
+ Registry._instance = new Registry();
29
+ }
30
+ return Registry._instance;
31
+ }
32
+
33
+ /**
34
+ * Registers an API
35
+ *
36
+ * @param api - The API to register
37
+ */
38
+ public registerApi (api: Api): void {
39
+ this._apis.set(api.name, api);
40
+ Klaim[api.name] = {};
41
+ }
42
+
43
+ /**
44
+ * Sets the current API
45
+ *
46
+ * @param name - The name of the API
47
+ */
48
+ public setCurrent (name: string): void {
49
+ const api = this._apis.get(name);
50
+ if (!api) {
51
+ throw new Error(`API ${name} not found`);
52
+ }
53
+ this._currentApi = api;
54
+ }
55
+
56
+ /**
57
+ * Clears the current API
58
+ */
59
+ public clearCurrent (): void {
60
+ this._currentApi = null;
61
+ }
62
+
63
+ /**
64
+ * Registers a route
65
+ *
66
+ * @param route - The route to register
67
+ */
68
+ public registerRoute (route: Route): void {
69
+ if (!this._currentApi) {
70
+ throw new Error(`No current API set, use Route only inside Api.create callback`);
71
+ }
72
+
73
+ route.api = this._currentApi.name;
74
+ this._currentApi.routes.set(route.name, route);
75
+
76
+ this.addToKlaimRoute(route.api, route);
77
+ }
78
+
79
+ /**
80
+ * Gets an API
81
+ *
82
+ * @param name - The name of the API
83
+ * @returns The API
84
+ */
85
+ public getApi (name: string): Api | undefined {
86
+ return this._apis.get(name);
87
+ }
88
+
89
+ /**
90
+ * Gets a route
91
+ *
92
+ * @param api - The name of the API
93
+ * @param name - The name of the route
94
+ * @returns The route
95
+ */
96
+ public getRoute (api: string, name: string): Route | undefined {
97
+ const apiObj = this._apis.get(api);
98
+ if (!apiObj) {
99
+ throw new Error(`API ${api} not found`);
100
+ }
101
+ return apiObj.routes.get(name) as Route;
102
+ }
103
+
104
+ /**
105
+ * Adds a route to Klaim object
106
+ *
107
+ * @param apiName - The name of the API
108
+ * @param route - The route to add
109
+ */
110
+ private addToKlaimRoute (apiName: string, route: Route): void {
111
+ /**
112
+ * The route function
113
+ *
114
+ * @param args - The arguments to pass to the route
115
+ * @param body - The body to pass to the route
116
+ * @returns The response
117
+ */
118
+ Klaim[apiName][route.name] = async <T>(args: IArgs = {}, body: IBody = {}): Promise<T> => {
119
+ const api = Registry.i._apis.get(apiName);
120
+ if (!api) {
121
+ throw new Error(`API ${route.api} not found`);
122
+ }
123
+ return callApi(api, route, args, body);
124
+ };
125
+ }
126
+ }
@@ -0,0 +1,161 @@
1
+ import cleanUrl from "../tools/cleanUrl";
2
+ import toCamelCase from "../tools/toCamelCase";
3
+
4
+ import { Api, IHeaders } from "./Api";
5
+ import { Registry } from "./Registry";
6
+
7
+ export enum RouteMethod {
8
+ GET = "GET",
9
+ POST = "POST",
10
+ PUT = "PUT",
11
+ DELETE = "DELETE",
12
+ PATCH = "PATCH",
13
+ OPTIONS = "OPTIONS"
14
+ }
15
+
16
+ interface IRoute {
17
+ api: Api["name"];
18
+ name: string;
19
+ url: string;
20
+ method: RouteMethod;
21
+ headers: IHeaders;
22
+ arguments: Set<string>;
23
+ }
24
+
25
+ /**
26
+ * Represents a route
27
+ */
28
+ export class Route implements IRoute {
29
+ public api: Api["name"] = "undefined";
30
+
31
+ public name: string;
32
+
33
+ public url: string;
34
+
35
+ public method: RouteMethod;
36
+
37
+ public headers: IHeaders;
38
+
39
+ public arguments: Set<string> = new Set<string>();
40
+
41
+ /**
42
+ * Constructor
43
+ *
44
+ * @param name - The name of the route
45
+ * @param url - The URL of the route
46
+ * @param headers - The headers to be sent with the request
47
+ * @param method - The HTTP method of the route
48
+ */
49
+ private constructor (name: string, url: string, headers: IHeaders, method: RouteMethod = RouteMethod.GET) {
50
+ this.name = toCamelCase(name);
51
+ if (this.name !== name) {
52
+ console.warn(`Route name "${name}" has been camelCased to "${this.name}"`);
53
+ }
54
+
55
+ this.url = cleanUrl(url);
56
+ this.headers = headers || {};
57
+ this.method = method;
58
+
59
+ this.detectArguments();
60
+ }
61
+
62
+ /**
63
+ * Creates a new route
64
+ *
65
+ * @param name - The name of the route
66
+ * @param url - The URL of the route
67
+ * @param headers - The headers to be sent with the request
68
+ * @param method - The HTTP method of the route
69
+ * @returns The new route
70
+ */
71
+ private static createRoute (name: string, url: string, headers: IHeaders, method: RouteMethod): Route {
72
+ const route = new Route(name, url, headers, method);
73
+ Registry.i.registerRoute(route as Route);
74
+ return route;
75
+ }
76
+
77
+ /**
78
+ * Creates a new route with the GET method
79
+ *
80
+ * @param name - The name of the route
81
+ * @param url - The URL of the route
82
+ * @param headers - The headers to be sent with the request
83
+ * @returns The new route
84
+ */
85
+ public static get (name: string, url: string, headers: IHeaders = {}): Route {
86
+ return this.createRoute(name, url, headers, RouteMethod.GET);
87
+ }
88
+
89
+ /**
90
+ * Creates a new route with the POST method
91
+ *
92
+ * @param name - The name of the route
93
+ * @param url - The URL of the route
94
+ * @param headers - The headers to be sent with the request
95
+ * @returns The new route
96
+ */
97
+ public static post (name: string, url: string, headers: IHeaders): Route {
98
+ return this.createRoute(name, url, headers, RouteMethod.POST);
99
+ }
100
+
101
+ /**
102
+ * Creates a new route with the PUT method
103
+ *
104
+ * @param name - The name of the route
105
+ * @param url - The URL of the route
106
+ * @param headers - The headers to be sent with the request
107
+ * @returns The new route
108
+ */
109
+ public static put (name: string, url: string, headers: IHeaders): Route {
110
+ return this.createRoute(name, url, headers, RouteMethod.PUT);
111
+ }
112
+
113
+ /**
114
+ * Creates a new route with the DELETE method
115
+ *
116
+ * @param name - The name of the route
117
+ * @param url - The URL of the route
118
+ * @param headers - The headers to be sent with the request
119
+ * @returns The new route
120
+ */
121
+ public static delete (name: string, url: string, headers: IHeaders): Route {
122
+ return this.createRoute(name, url, headers, RouteMethod.DELETE);
123
+ }
124
+
125
+ /**
126
+ * Creates a new route with the PATCH method
127
+ *
128
+ * @param name - The name of the route
129
+ * @param url - The URL of the route
130
+ * @param headers - The headers to be sent with the request
131
+ * @returns The new route
132
+ */
133
+ public static patch (name: string, url: string, headers: IHeaders): Route {
134
+ return this.createRoute(name, url, headers, RouteMethod.PATCH);
135
+ }
136
+
137
+ /**
138
+ * Creates a new route with the OPTIONS method
139
+ *
140
+ * @param name - The name of the route
141
+ * @param url - The URL of the route
142
+ * @param headers - The headers to be sent with the request
143
+ * @returns The new route
144
+ */
145
+ public static options (name: string, url: string, headers: IHeaders): Route {
146
+ return this.createRoute(name, url, headers, RouteMethod.OPTIONS);
147
+ }
148
+
149
+ /**
150
+ * Detects the arguments in the URL
151
+ */
152
+ private detectArguments (): void {
153
+ const matches = this.url.match(/\[([^\]]+)]/g);
154
+ if (matches) {
155
+ matches.forEach(match => {
156
+ const key = match.replace(/\[|]/g, "");
157
+ this.arguments.add(key);
158
+ });
159
+ }
160
+ }
161
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { Api, type IApi } from "./core/Api";
2
+ export { Klaim } from "./core/Klaim";
3
+ export { Registry } from "./core/Registry";
4
+ export { type IRoute,Route } from "./core/Route";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Clean the URL by removing slashes at the beginning and end of the string.
3
+ *
4
+ * @param url - The URL to clean
5
+ * @returns The cleaned URL
6
+ */
7
+ export default function (url: string): string {
8
+ return url
9
+ .trim()
10
+ // remove slashes at the beginning and end of the string
11
+ .replace(/^\/|\/$/g, "");
12
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Slugify a text
3
+ *
4
+ * @param text - The text to slugify
5
+ * @param splitCharacter - The character to use to split the text
6
+ * @returns The slugified text
7
+ */
8
+ export default function (text: string, splitCharacter: string = "_"): string {
9
+ return text
10
+ .toString()
11
+ .normalize("NFD") // split an accented letter in the base letter and the acent
12
+ .replace(/[\u0300-\u036f]/g, "") // remove all previously split accents
13
+ .toLowerCase()
14
+ .trim()
15
+ .replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced)
16
+ .replace(/\s+/g, splitCharacter); // separator
17
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Convert a string to camel case
3
+ *
4
+ * @param text - The text to convert
5
+ * @returns The text in camel case
6
+ */
7
+ export default function (text: string): string {
8
+ return text
9
+ .replace(/([-_][a-z])/gi, match => match.toUpperCase().replace("-", "").replace("_", ""))
10
+ .replace(/(^\w)/, match => match.toLowerCase());
11
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Generate a new token
3
+ *
4
+ * @param {number} length The length of the token
5
+ * @returns {string} The generated token
6
+ */
7
+ export default function (length?: number): string {
8
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
9
+ let token = "";
10
+ for (let i = 0; i < (length || 32); i++) {
11
+ token += chars.charAt(Math.floor(Math.random() * chars.length));
12
+ }
13
+ return token;
14
+ }
@@ -0,0 +1,89 @@
1
+ import {describe, expect, it, vi} from 'vitest';
2
+ import {Route, RouteArgumentType} from '../src/core/Route';
3
+
4
+ // Mocks
5
+ vi.mock('../tools/token', () => ({
6
+ default: vi.fn(() => 'mocked-token'),
7
+ }));
8
+
9
+ vi.mock('../tools/slugify', () => ({
10
+ default: vi.fn((str: string) => str.replace(/\s+/g, '-').toLowerCase()),
11
+ }));
12
+
13
+ describe('Route.detectArgumentsOfUrl', () => {
14
+ it('should detect arguments of type ANY', () => {
15
+ const route = new Route('/user/[id]');
16
+ const args = route._arguments;
17
+
18
+ expect(args.size).toBe(1);
19
+ expect(Array.from(args)).toEqual([
20
+ {name: 'id', type: RouteArgumentType.ANY, default: null, required: true},
21
+ ]);
22
+ });
23
+
24
+ it('should detect multiple arguments', () => {
25
+ const route = new Route('/posts/[post]/comments/[comment]');
26
+ const args = route._arguments;
27
+
28
+ expect(args.size).toBe(2);
29
+ expect(Array.from(args)).toEqual([
30
+ {name: 'post', type: RouteArgumentType.ANY, default: null, required: true},
31
+ {name: 'comment', type: RouteArgumentType.ANY, default: null, required: true},
32
+ ]);
33
+ });
34
+
35
+ it('should detect arguments with specific types', () => {
36
+ const route = new Route('/user/[id:number]');
37
+ const args = route._arguments;
38
+
39
+ expect(args.size).toBe(1);
40
+ expect(Array.from(args)).toEqual([
41
+ {name: 'id', type: RouteArgumentType.NUMBER, default: null, required: true},
42
+ ]);
43
+ });
44
+
45
+ it('should detect arguments with default values', () => {
46
+ const route = new Route('/user/[id:number=1]');
47
+ const args = route._arguments;
48
+
49
+ expect(args.size).toBe(1);
50
+ expect(Array.from(args)).toEqual([
51
+ {name: 'id', type: RouteArgumentType.NUMBER, default: 1, required: true},
52
+ ]);
53
+ });
54
+
55
+ it('should detect optional arguments', () => {
56
+ const route = new Route('/user/[name?]');
57
+ const args = route._arguments;
58
+
59
+ expect(args.size).toBe(1);
60
+ expect(Array.from(args)).toEqual([
61
+ {name: 'name', type: RouteArgumentType.ANY, default: null, required: false},
62
+ ]);
63
+ });
64
+
65
+ it('should detect multiple arguments with different types', () => {
66
+ const route = new Route('/profile/[user:string]/settings/[setting:boolean]');
67
+ const args = route._arguments;
68
+
69
+ expect(args.size).toBe(2);
70
+ expect(Array.from(args)).toEqual([
71
+ {name: 'user', type: RouteArgumentType.STRING, default: null, required: true},
72
+ {name: 'setting', type: RouteArgumentType.BOOLEAN, default: null, required: true},
73
+ ]);
74
+ });
75
+
76
+ it('should handle invalid JSON in default value gracefully', () => {
77
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {
78
+ });
79
+ const route = new Route('/user/[id:number=invalid]');
80
+ const args = route._arguments;
81
+
82
+ expect(args.size).toBe(1);
83
+ expect(Array.from(args)).toEqual([
84
+ {name: 'id', type: RouteArgumentType.NUMBER, default: 'invalid', required: true},
85
+ ]);
86
+ expect(consoleSpy).toHaveBeenCalled();
87
+ consoleSpy.mockRestore();
88
+ });
89
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "lib": ["ESNext", "DOM"],
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "moduleResolution": "node",
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src"]
15
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from 'vite';
2
+ import { configDefaults } from 'vitest/config';
3
+
4
+ export default defineConfig({
5
+ build: {
6
+ lib: {
7
+ entry: 'src/index.ts',
8
+ name: 'klaim',
9
+ fileName: format => `klaim.${format}.js`
10
+ },
11
+ rollupOptions: {
12
+ external: [],
13
+ output: {
14
+ globals: {}
15
+ }
16
+ }
17
+ },
18
+ test: {
19
+ globals: true,
20
+ environment: 'jsdom',
21
+ coverage: {
22
+ provider: 'v8'
23
+ },
24
+ exclude: [...configDefaults.exclude, 'tests/e2e/**'],
25
+ },
26
+ });