@spikard/wasm 0.6.2 → 0.7.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.
- package/README.md +356 -524
- package/dist/chunk-DYTZ4RA2.mjs +2267 -0
- package/dist/chunk-DYTZ4RA2.mjs.map +1 -0
- package/dist/index.d.mts +365 -0
- package/dist/index.d.ts +365 -0
- package/dist/index.js +2269 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node.d.mts +9 -0
- package/dist/node.d.ts +9 -0
- package/dist/node.js +2400 -0
- package/dist/node.js.map +1 -0
- package/dist/node.mjs +120 -0
- package/dist/node.mjs.map +1 -0
- package/{dist-node → dist}/package.json +1 -1
- package/{dist-node → dist}/spikard_wasm.js +7 -4
- package/{dist-node → dist}/spikard_wasm_bg.wasm +0 -0
- package/package.json +54 -46
- package/dist-bundler/package.json +0 -34
- package/dist-bundler/spikard_wasm.d.ts +0 -28
- package/dist-bundler/spikard_wasm.js +0 -5
- package/dist-bundler/spikard_wasm_bg.js +0 -914
- package/dist-bundler/spikard_wasm_bg.wasm +0 -0
- package/dist-bundler/spikard_wasm_bg.wasm.d.ts +0 -26
- package/dist-node/README.md +0 -739
- package/dist-node/spikard_wasm.d.ts +0 -28
- package/dist-node/spikard_wasm_bg.wasm.d.ts +0 -26
- package/dist-web/README.md +0 -739
- package/dist-web/package.json +0 -32
- package/dist-web/spikard_wasm.d.ts +0 -79
- package/dist-web/spikard_wasm.js +0 -921
- package/dist-web/spikard_wasm_bg.wasm +0 -0
- package/dist-web/spikard_wasm_bg.wasm.d.ts +0 -26
- /package/{dist-bundler → dist}/README.md +0 -0
package/README.md
CHANGED
|
@@ -1,74 +1,72 @@
|
|
|
1
|
-
# spikard
|
|
1
|
+
# @spikard/wasm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Note:** As of v0.2.1, this package has moved to `@spikard/wasm`. Update your imports from `'spikard-wasm'` to `'@spikard/wasm'`. See [MIGRATION-0.2.1.md](../../MIGRATION-0.2.1.md) for details.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
[](https://spikard.dev)
|
|
8
|
-
[](https://www.npmjs.com/package/spikard-wasm)
|
|
9
|
-
[](https://www.npmjs.com/package/spikard-wasm)
|
|
10
|
-
[](https://crates.io/crates/spikard-wasm)
|
|
11
|
-
[](https://docs.rs/spikard-wasm)
|
|
5
|
+
[](https://www.npmjs.com/package/@spikard/wasm)
|
|
6
|
+
[](https://www.npmjs.com/package/@spikard/wasm)
|
|
12
7
|
[](LICENSE)
|
|
13
|
-
[](https://codecov.io/gh/Goldziher/spikard)
|
|
9
|
+
[](https://pypi.org/project/spikard/)
|
|
10
|
+
[](https://crates.io/crates/spikard)
|
|
11
|
+
[](https://rubygems.org/gems/spikard)
|
|
12
|
+
[](https://packagist.org/packages/spikard/spikard)
|
|
13
|
+
|
|
14
|
+
Spikard HTTP framework compiled to **WebAssembly with full TypeScript support** for edge runtimes, browsers, and server-side JavaScript environments. Build type-safe web services that run anywhere.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **WASM-first**: Compiled from Rust to WebAssembly for maximum performance and portability
|
|
19
|
+
- **Type-safe routing**: Full TypeScript support with auto-completed route definitions
|
|
20
|
+
- **Edge runtime support**: Works in browsers, Cloudflare Workers, Deno, and Node.js
|
|
21
|
+
- **Zero Node.js dependencies**: Pure fetch API—no Node globals required
|
|
22
|
+
- **Async/await native**: Seamless async/await for handlers and middleware
|
|
23
|
+
- **Lightweight**: Optimized WASM binaries with aggressive tree-shaking
|
|
24
|
+
- **Schema validation**: Built-in request/response validation with Zod
|
|
25
|
+
- **WebSocket & SSE**: Full support for real-time features on compatible runtimes
|
|
26
|
+
- **Testing utilities**: In-memory test client for easy unit testing
|
|
27
|
+
- **Code generation**: Generate TypeScript apps and tests from OpenAPI/AsyncAPI
|
|
18
28
|
|
|
19
29
|
## Installation
|
|
20
30
|
|
|
21
|
-
|
|
31
|
+
### From npm
|
|
22
32
|
|
|
23
33
|
```bash
|
|
24
|
-
npm install spikard
|
|
25
|
-
# or
|
|
26
|
-
|
|
27
|
-
# or
|
|
28
|
-
|
|
29
|
-
# or
|
|
30
|
-
deno add npm:spikard-wasm
|
|
34
|
+
npm install @spikard/wasm
|
|
35
|
+
# or with yarn
|
|
36
|
+
yarn add @spikard/wasm
|
|
37
|
+
# or with pnpm
|
|
38
|
+
pnpm add @spikard/wasm
|
|
31
39
|
```
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
### From source
|
|
34
42
|
|
|
35
43
|
```bash
|
|
36
44
|
cd packages/wasm
|
|
37
45
|
pnpm install
|
|
38
|
-
pnpm build #
|
|
46
|
+
pnpm build # outputs to dist/
|
|
39
47
|
```
|
|
40
48
|
|
|
41
|
-
**Requirements:**
|
|
42
|
-
- Node.js 20+ / Deno 1.40+ / Bun 1.0+
|
|
43
|
-
- For Cloudflare Workers: Wrangler 3+
|
|
44
|
-
- For browsers: Modern browser with WASM support
|
|
45
|
-
|
|
46
49
|
## Quick Start
|
|
47
50
|
|
|
48
51
|
### Cloudflare Workers
|
|
49
52
|
|
|
50
53
|
```typescript
|
|
51
|
-
import { Spikard, get, post
|
|
52
|
-
import { z } from "zod";
|
|
54
|
+
import { Spikard, createFetchHandler, get, post } from "@spikard/wasm";
|
|
53
55
|
|
|
54
56
|
const app = new Spikard();
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
// Define routes with type safety
|
|
59
|
+
get("/hello", async (req) => ({
|
|
60
|
+
message: "Hello from the edge!",
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
58
62
|
}));
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
post("/users", {
|
|
66
|
-
bodySchema: UserSchema
|
|
67
|
-
})(async (req) => {
|
|
68
|
-
const user = req.json<z.infer<typeof UserSchema>>();
|
|
69
|
-
return { id: 1, ...user };
|
|
64
|
+
post("/echo", async (req) => {
|
|
65
|
+
const body = await req.json();
|
|
66
|
+
return { echo: body };
|
|
70
67
|
});
|
|
71
68
|
|
|
69
|
+
// Export as a Cloudflare Worker
|
|
72
70
|
export default {
|
|
73
71
|
fetch: createFetchHandler(app),
|
|
74
72
|
};
|
|
@@ -77,663 +75,497 @@ export default {
|
|
|
77
75
|
### Deno
|
|
78
76
|
|
|
79
77
|
```typescript
|
|
80
|
-
import { Spikard, get } from "npm
|
|
78
|
+
import { Spikard, get, post } from "npm:@spikard/wasm@0.2.1";
|
|
81
79
|
|
|
82
80
|
const app = new Spikard();
|
|
83
81
|
|
|
84
|
-
get("/"
|
|
85
|
-
message: "Hello from Deno
|
|
82
|
+
get("/hello", async (req) => ({
|
|
83
|
+
message: "Hello from Deno",
|
|
86
84
|
}));
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
post("/api/users", async (req) => {
|
|
87
|
+
const data = await req.json();
|
|
88
|
+
return { created: true, id: Math.random() };
|
|
90
89
|
});
|
|
90
|
+
|
|
91
|
+
Deno.serve({ port: 8000 }, (request) => app.handleRequest(request));
|
|
91
92
|
```
|
|
92
93
|
|
|
93
|
-
###
|
|
94
|
+
### Node.js / Bun
|
|
94
95
|
|
|
95
96
|
```typescript
|
|
96
|
-
import { Spikard,
|
|
97
|
+
import { Spikard, createFetchHandler, get } from "@spikard/wasm";
|
|
97
98
|
|
|
98
99
|
const app = new Spikard();
|
|
99
100
|
|
|
100
|
-
get("/api/
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
get("/api/status", async (req) => ({
|
|
102
|
+
status: "ok",
|
|
103
|
+
runtime: "node",
|
|
103
104
|
}));
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
const server = Bun.serve({
|
|
107
|
+
port: 3000,
|
|
108
|
+
fetch: createFetchHandler(app),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log(`Server running on http://localhost:${server.port}`);
|
|
109
112
|
```
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
### Browser (with bundler)
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
```typescript
|
|
117
|
+
import { Spikard, get } from "@spikard/wasm";
|
|
118
|
+
|
|
119
|
+
const app = new Spikard();
|
|
114
120
|
|
|
115
|
-
|
|
121
|
+
get("/worker", async (req) => ({
|
|
122
|
+
message: "Running in a browser Web Worker",
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
// Simulate incoming requests in a worker context
|
|
126
|
+
self.addEventListener("message", async (event) => {
|
|
127
|
+
const response = await app.handleRequest(event.data.request);
|
|
128
|
+
self.postMessage({ response });
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## API Documentation
|
|
133
|
+
|
|
134
|
+
### Routing Helpers
|
|
116
135
|
|
|
117
136
|
```typescript
|
|
118
|
-
import { get, post, put, patch,
|
|
137
|
+
import { Spikard, get, post, put, patch, delete_, head, options } from "@spikard/wasm";
|
|
138
|
+
|
|
139
|
+
const app = new Spikard();
|
|
119
140
|
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
// Define routes with automatic method binding
|
|
142
|
+
get("/users", async (req) => {
|
|
143
|
+
// GET /users
|
|
122
144
|
});
|
|
123
145
|
|
|
124
|
-
post("/users"
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
post("/users", async (req) => {
|
|
147
|
+
// POST /users with body parsing
|
|
148
|
+
const body = await req.json();
|
|
127
149
|
});
|
|
128
150
|
|
|
129
|
-
put("/users/:id"
|
|
130
|
-
|
|
131
|
-
return { id, updated: true };
|
|
151
|
+
put("/users/:id", async (req, { id }) => {
|
|
152
|
+
// PUT /users/:id with path params
|
|
132
153
|
});
|
|
133
154
|
|
|
134
|
-
patch("/users/:id"
|
|
135
|
-
|
|
155
|
+
patch("/users/:id", async (req, { id }) => {
|
|
156
|
+
// PATCH /users/:id
|
|
136
157
|
});
|
|
137
158
|
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
delete_("/users/:id", async (req, { id }) => {
|
|
160
|
+
// DELETE /users/:id (note: delete_ to avoid keyword)
|
|
140
161
|
});
|
|
141
162
|
```
|
|
142
163
|
|
|
143
|
-
###
|
|
144
|
-
|
|
145
|
-
For dynamic route registration:
|
|
164
|
+
### Request Handling
|
|
146
165
|
|
|
147
166
|
```typescript
|
|
148
|
-
|
|
167
|
+
// Access request properties
|
|
168
|
+
get("/example", async (req) => {
|
|
169
|
+
const method = req.method; // "GET"
|
|
170
|
+
const url = req.url; // Full URL
|
|
171
|
+
const headers = req.headers; // Headers object
|
|
149
172
|
|
|
150
|
-
|
|
173
|
+
// Parse JSON body
|
|
174
|
+
const json = await req.json();
|
|
151
175
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
176
|
+
// Parse form data
|
|
177
|
+
const form = await req.formData();
|
|
155
178
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
method: "GET",
|
|
159
|
-
path: "/users",
|
|
160
|
-
handler_name: "listUsers",
|
|
161
|
-
is_async: true,
|
|
162
|
-
},
|
|
163
|
-
listUsers
|
|
164
|
-
);
|
|
165
|
-
```
|
|
179
|
+
// Get raw text
|
|
180
|
+
const text = await req.text();
|
|
166
181
|
|
|
167
|
-
|
|
182
|
+
// Get ArrayBuffer
|
|
183
|
+
const buffer = await req.arrayBuffer();
|
|
168
184
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
- `PATCH` - Update resources
|
|
173
|
-
- `DELETE` - Delete resources
|
|
174
|
-
- `HEAD` - Get headers only
|
|
175
|
-
- `OPTIONS` - Get allowed methods
|
|
176
|
-
- `TRACE` - Echo the request
|
|
185
|
+
return { received: true };
|
|
186
|
+
});
|
|
187
|
+
```
|
|
177
188
|
|
|
178
|
-
###
|
|
189
|
+
### Response Building
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { Spikard, get, json, status, withHeaders } from "@spikard/wasm";
|
|
193
|
+
|
|
194
|
+
get("/users", async (req) => {
|
|
195
|
+
return json(
|
|
196
|
+
{
|
|
197
|
+
users: [
|
|
198
|
+
{ id: 1, name: "Alice" },
|
|
199
|
+
{ id: 2, name: "Bob" },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
status: 200,
|
|
204
|
+
headers: {
|
|
205
|
+
"X-Total-Count": "2",
|
|
206
|
+
"Cache-Control": "max-age=3600",
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
});
|
|
179
211
|
|
|
180
|
-
|
|
212
|
+
get("/created", async (req) => {
|
|
213
|
+
return status(201, { id: 123, created: true });
|
|
214
|
+
});
|
|
215
|
+
```
|
|
181
216
|
|
|
182
|
-
|
|
217
|
+
### Schema Validation with Zod
|
|
183
218
|
|
|
184
219
|
```typescript
|
|
185
|
-
import { post } from "spikard-wasm";
|
|
186
220
|
import { z } from "zod";
|
|
187
221
|
|
|
188
|
-
const
|
|
222
|
+
const userSchema = z.object({
|
|
189
223
|
name: z.string().min(1),
|
|
190
224
|
email: z.string().email(),
|
|
191
|
-
age: z.number().int().
|
|
225
|
+
age: z.number().int().positive().optional(),
|
|
192
226
|
});
|
|
193
227
|
|
|
194
|
-
post("/users", {
|
|
195
|
-
|
|
196
|
-
responseSchema: z.object({ id: z.number(), name: z.string() }),
|
|
197
|
-
})(async function createUser(req) {
|
|
198
|
-
const user = req.json<z.infer<typeof CreateUserSchema>>();
|
|
199
|
-
return { id: 1, name: user.name };
|
|
200
|
-
});
|
|
201
|
-
```
|
|
228
|
+
post("/users", async (req) => {
|
|
229
|
+
const body = await req.json();
|
|
202
230
|
|
|
203
|
-
|
|
231
|
+
// Validate with Zod
|
|
232
|
+
const result = userSchema.safeParse(body);
|
|
204
233
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
234
|
+
if (!result.success) {
|
|
235
|
+
return {
|
|
236
|
+
error: "Invalid user data",
|
|
237
|
+
issues: result.error.issues,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// result.data is now type-safe
|
|
242
|
+
const user = result.data;
|
|
214
243
|
|
|
215
|
-
|
|
216
|
-
const user = req.json<{ name: string; email: string }>();
|
|
217
|
-
return { id: 1, ...user };
|
|
244
|
+
return json({ id: 1, ...user }, { status: 201 });
|
|
218
245
|
});
|
|
219
246
|
```
|
|
220
247
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
### Accessing Request Data
|
|
248
|
+
### Testing with TestClient
|
|
224
249
|
|
|
225
250
|
```typescript
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const userId = req.pathParams.id;
|
|
251
|
+
import { describe, it, expect } from "vitest";
|
|
252
|
+
import { Spikard, TestClient, get } from "@spikard/wasm";
|
|
229
253
|
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
const q = params.get("q");
|
|
233
|
-
const limit = params.get("limit") ?? "10";
|
|
254
|
+
describe("API routes", () => {
|
|
255
|
+
const app = new Spikard();
|
|
234
256
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
257
|
+
get("/hello", async () => ({
|
|
258
|
+
message: "Hello",
|
|
259
|
+
}));
|
|
238
260
|
|
|
239
|
-
|
|
240
|
-
const sessionId = req.cookies?.session_id;
|
|
261
|
+
const client = new TestClient(app);
|
|
241
262
|
|
|
242
|
-
|
|
243
|
-
|
|
263
|
+
it("returns greeting", async () => {
|
|
264
|
+
const res = await client.get("/hello");
|
|
244
265
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
266
|
+
expect(res.status).toBe(200);
|
|
267
|
+
expect(res.json()).toEqual({
|
|
268
|
+
message: "Hello",
|
|
269
|
+
});
|
|
270
|
+
});
|
|
248
271
|
|
|
249
|
-
|
|
272
|
+
it("handles POST with body", async () => {
|
|
273
|
+
post("/echo", async (req) => {
|
|
274
|
+
const body = await req.json();
|
|
275
|
+
return { echo: body };
|
|
276
|
+
});
|
|
250
277
|
|
|
251
|
-
|
|
252
|
-
post("/users")(async function createUser(req) {
|
|
253
|
-
const body = req.json<{ name: string; email: string }>();
|
|
254
|
-
return { id: 1, ...body };
|
|
255
|
-
});
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### Form Data
|
|
278
|
+
const res = await client.post("/echo", { message: "test" });
|
|
259
279
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
password: form.password,
|
|
266
|
-
};
|
|
280
|
+
expect(res.status).toBe(200);
|
|
281
|
+
expect(res.json()).toEqual({
|
|
282
|
+
echo: { message: "test" },
|
|
283
|
+
});
|
|
284
|
+
});
|
|
267
285
|
});
|
|
268
286
|
```
|
|
269
287
|
|
|
270
|
-
##
|
|
271
|
-
|
|
272
|
-
For automatic parameter extraction:
|
|
288
|
+
## Bundle Size
|
|
273
289
|
|
|
274
|
-
|
|
275
|
-
import { wrapHandler, wrapBodyHandler } from "spikard-wasm";
|
|
276
|
-
|
|
277
|
-
interface CreateUserRequest {
|
|
278
|
-
name: string;
|
|
279
|
-
email: string;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Body-only wrapper
|
|
283
|
-
post("/users", {}, wrapBodyHandler(async (body: CreateUserRequest) => {
|
|
284
|
-
return { id: 1, name: body.name };
|
|
285
|
-
}));
|
|
290
|
+
Optimized for minimal bundle size:
|
|
286
291
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}));
|
|
291
|
-
```
|
|
292
|
+
- **Uncompressed**: ~200KB (varies by feature set)
|
|
293
|
+
- **Gzip**: ~60KB
|
|
294
|
+
- **Brotli**: ~45KB
|
|
292
295
|
|
|
293
|
-
|
|
296
|
+
Bundle size analysis:
|
|
294
297
|
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
interface UploadRequest {
|
|
299
|
-
file: UploadFile;
|
|
300
|
-
description: string;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
post("/upload")(async function upload(req) {
|
|
304
|
-
const body = req.json<UploadRequest>();
|
|
305
|
-
const content = body.file.read();
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
filename: body.file.filename,
|
|
309
|
-
size: body.file.size,
|
|
310
|
-
contentType: body.file.contentType,
|
|
311
|
-
};
|
|
312
|
-
});
|
|
298
|
+
```bash
|
|
299
|
+
# Use source-map-explorer or similar
|
|
300
|
+
npx source-map-explorer 'dist/**/*.js'
|
|
313
301
|
```
|
|
314
302
|
|
|
315
|
-
##
|
|
316
|
-
|
|
317
|
-
```typescript
|
|
318
|
-
import { StreamingResponse } from "spikard-wasm";
|
|
303
|
+
## WebAssembly Configuration
|
|
319
304
|
|
|
320
|
-
|
|
321
|
-
for (let i = 0; i < 10; i++) {
|
|
322
|
-
yield JSON.stringify({ count: i }) + "\n";
|
|
323
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
324
|
-
}
|
|
325
|
-
}
|
|
305
|
+
Compiled with aggressive optimizations in `Cargo.toml`:
|
|
326
306
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
headers: { "Content-Type": "application/x-ndjson" },
|
|
331
|
-
});
|
|
332
|
-
});
|
|
307
|
+
```toml
|
|
308
|
+
[package.metadata.wasm-pack.profile.release]
|
|
309
|
+
wasm-opt = ["-O3", "--enable-bulk-memory", "--enable-nontrapping-float-to-int", "--enable-simd"]
|
|
333
310
|
```
|
|
334
311
|
|
|
335
|
-
|
|
312
|
+
Build options:
|
|
336
313
|
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
for (let i = 0; i < 10; i++) {
|
|
341
|
-
yield `data: ${JSON.stringify({ count: i })}\n\n`;
|
|
342
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
343
|
-
}
|
|
344
|
-
}
|
|
314
|
+
```bash
|
|
315
|
+
# Development (debug symbols, fast compile)
|
|
316
|
+
wasm-pack build --dev
|
|
345
317
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
headers: {
|
|
349
|
-
"Content-Type": "text/event-stream",
|
|
350
|
-
"Cache-Control": "no-cache",
|
|
351
|
-
"Connection": "keep-alive",
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
});
|
|
318
|
+
# Release (optimized, minimal size)
|
|
319
|
+
wasm-pack build --release
|
|
355
320
|
```
|
|
356
321
|
|
|
357
|
-
##
|
|
358
|
-
|
|
359
|
-
```typescript
|
|
360
|
-
import { Spikard, type ServerConfig } from "spikard-wasm";
|
|
322
|
+
## Code Generation
|
|
361
323
|
|
|
362
|
-
|
|
324
|
+
Generate TypeScript applications and tests from OpenAPI/AsyncAPI specifications:
|
|
363
325
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
gzip: true,
|
|
370
|
-
brotli: true,
|
|
371
|
-
quality: 9,
|
|
372
|
-
minSize: 1024,
|
|
373
|
-
},
|
|
374
|
-
rateLimit: {
|
|
375
|
-
perSecond: 100,
|
|
376
|
-
burst: 200,
|
|
377
|
-
ipBased: true,
|
|
378
|
-
},
|
|
379
|
-
cors: {
|
|
380
|
-
allowOrigins: ["*"],
|
|
381
|
-
allowMethods: ["GET", "POST", "PUT", "DELETE"],
|
|
382
|
-
allowHeaders: ["Content-Type", "Authorization"],
|
|
383
|
-
maxAge: 86400,
|
|
384
|
-
},
|
|
385
|
-
openapi: {
|
|
386
|
-
enabled: true,
|
|
387
|
-
title: "Edge API",
|
|
388
|
-
version: "1.0.0",
|
|
389
|
-
},
|
|
390
|
-
};
|
|
326
|
+
```bash
|
|
327
|
+
# Generate from OpenAPI spec
|
|
328
|
+
spikard generate openapi \
|
|
329
|
+
--fixtures ../../testing_data \
|
|
330
|
+
--output ./generated
|
|
391
331
|
|
|
392
|
-
|
|
393
|
-
|
|
332
|
+
# Generate WebSocket handlers from AsyncAPI
|
|
333
|
+
spikard generate asyncapi \
|
|
334
|
+
--fixtures ../../testing_data/websockets \
|
|
335
|
+
--output ./generated
|
|
394
336
|
```
|
|
395
337
|
|
|
396
338
|
## Lifecycle Hooks
|
|
397
339
|
|
|
398
340
|
```typescript
|
|
399
|
-
|
|
400
|
-
console.log(`${request.method} ${request.path}`);
|
|
401
|
-
return request;
|
|
402
|
-
});
|
|
341
|
+
import { Spikard, HookTypes } from "@spikard/wasm";
|
|
403
342
|
|
|
404
|
-
app
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
body: { error: "Unauthorized" },
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
return request;
|
|
343
|
+
const app = new Spikard();
|
|
344
|
+
|
|
345
|
+
// On every request (before validation)
|
|
346
|
+
app.onRequest(async (req) => {
|
|
347
|
+
console.log(`${req.method} ${req.url}`);
|
|
413
348
|
});
|
|
414
349
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
request
|
|
418
|
-
return request;
|
|
350
|
+
// Before handler execution
|
|
351
|
+
app.preHandler(async (req) => {
|
|
352
|
+
// Add request ID, timing, etc.
|
|
419
353
|
});
|
|
420
354
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return response;
|
|
355
|
+
// After response
|
|
356
|
+
app.onResponse(async (req, res) => {
|
|
357
|
+
console.log(`${req.method} ${req.url} -> ${res.status}`);
|
|
425
358
|
});
|
|
426
359
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
360
|
+
// On error
|
|
361
|
+
app.onError(async (error, req) => {
|
|
362
|
+
console.error(`Error: ${error.message}`);
|
|
363
|
+
return { error: "Internal Server Error" };
|
|
430
364
|
});
|
|
431
365
|
```
|
|
432
366
|
|
|
433
|
-
##
|
|
367
|
+
## Real-Time Features
|
|
434
368
|
|
|
435
|
-
###
|
|
369
|
+
### WebSocket Support
|
|
436
370
|
|
|
437
371
|
```typescript
|
|
438
|
-
import {
|
|
439
|
-
import { expect } from "vitest";
|
|
372
|
+
import { Spikard, ws } from "@spikard/wasm";
|
|
440
373
|
|
|
441
374
|
const app = new Spikard();
|
|
442
375
|
|
|
443
|
-
|
|
444
|
-
|
|
376
|
+
ws("/chat", {
|
|
377
|
+
onOpen: (socket) => {
|
|
378
|
+
console.log("Client connected");
|
|
379
|
+
},
|
|
380
|
+
onMessage: (socket, data) => {
|
|
381
|
+
socket.broadcast(data);
|
|
382
|
+
},
|
|
383
|
+
onClose: (socket) => {
|
|
384
|
+
console.log("Client disconnected");
|
|
385
|
+
},
|
|
445
386
|
});
|
|
446
|
-
|
|
447
|
-
const client = new TestClient(app);
|
|
448
|
-
|
|
449
|
-
const response = await client.get("/users/123");
|
|
450
|
-
expect(response.statusCode).toBe(200);
|
|
451
|
-
expect(response.json()).toEqual({ id: "123", name: "Alice" });
|
|
452
387
|
```
|
|
453
388
|
|
|
454
|
-
###
|
|
389
|
+
### Server-Sent Events (SSE)
|
|
455
390
|
|
|
456
391
|
```typescript
|
|
457
|
-
import {
|
|
392
|
+
import { Spikard, sse } from "@spikard/wasm";
|
|
458
393
|
|
|
459
|
-
|
|
460
|
-
socket.on("message", (msg) => {
|
|
461
|
-
socket.send({ echo: msg });
|
|
462
|
-
});
|
|
463
|
-
});
|
|
394
|
+
const app = new Spikard();
|
|
464
395
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
await ws.sendJson({ message: "hello" });
|
|
468
|
-
const response = await ws.receiveJson();
|
|
469
|
-
expect(response.echo.message).toBe("hello");
|
|
470
|
-
await ws.close();
|
|
471
|
-
```
|
|
396
|
+
sse("/events", async (req, res) => {
|
|
397
|
+
res.write("data: " + JSON.stringify({ event: "connected" }) + "\n\n");
|
|
472
398
|
|
|
473
|
-
|
|
399
|
+
const interval = setInterval(() => {
|
|
400
|
+
res.write(
|
|
401
|
+
"data: " + JSON.stringify({ event: "ping", time: Date.now() }) + "\n\n"
|
|
402
|
+
);
|
|
403
|
+
}, 5000);
|
|
474
404
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const sse = new SseStream(response.text());
|
|
478
|
-
const events = sse.eventsAsJson();
|
|
479
|
-
expect(events.length).toBeGreaterThan(0);
|
|
405
|
+
return () => clearInterval(interval);
|
|
406
|
+
});
|
|
480
407
|
```
|
|
481
408
|
|
|
482
|
-
##
|
|
483
|
-
|
|
484
|
-
Full TypeScript support with auto-generated types:
|
|
409
|
+
## Error Handling
|
|
485
410
|
|
|
486
411
|
```typescript
|
|
487
|
-
import {
|
|
488
|
-
type Request,
|
|
489
|
-
type Response,
|
|
490
|
-
type ServerConfig,
|
|
491
|
-
type RouteOptions,
|
|
492
|
-
type HandlerFunction,
|
|
493
|
-
} from "spikard-wasm";
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
### Parameter Types
|
|
412
|
+
import { Spikard, HttpError } from "@spikard/wasm";
|
|
497
413
|
|
|
498
|
-
|
|
499
|
-
import { Query, Path, Body, QueryDefault } from "spikard-wasm";
|
|
500
|
-
|
|
501
|
-
function handler(
|
|
502
|
-
id: Path<number>,
|
|
503
|
-
limit: Query<string | undefined>,
|
|
504
|
-
body: Body<UserType>
|
|
505
|
-
) {
|
|
506
|
-
// Full type inference
|
|
507
|
-
}
|
|
508
|
-
```
|
|
414
|
+
const app = new Spikard();
|
|
509
415
|
|
|
510
|
-
|
|
416
|
+
get("/users/:id", async (req, { id }) => {
|
|
417
|
+
if (!id) {
|
|
418
|
+
throw new HttpError(400, "User ID is required");
|
|
419
|
+
}
|
|
511
420
|
|
|
512
|
-
|
|
513
|
-
import { z } from "zod";
|
|
421
|
+
const user = await fetchUser(id);
|
|
514
422
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
age: z.number().int().min(18).optional(),
|
|
519
|
-
tags: z.array(z.string()).default([]),
|
|
520
|
-
});
|
|
423
|
+
if (!user) {
|
|
424
|
+
throw new HttpError(404, `User ${id} not found`);
|
|
425
|
+
}
|
|
521
426
|
|
|
522
|
-
post("/users", { bodySchema: UserSchema })(async function createUser(req) {
|
|
523
|
-
const user = req.json<z.infer<typeof UserSchema>>();
|
|
524
|
-
// user is fully typed and validated
|
|
525
427
|
return user;
|
|
526
428
|
});
|
|
429
|
+
|
|
430
|
+
// Automatic error response
|
|
431
|
+
// 404 -> { error: "User 123 not found", status: 404 }
|
|
527
432
|
```
|
|
528
433
|
|
|
529
|
-
##
|
|
434
|
+
## CI Benchmarks (2025-12-20)
|
|
435
|
+
|
|
436
|
+
Run: `snapshots/benchmarks/20397054933` (commit `25e4fdf`, oha, 50 concurrency, 10s, Linux x86_64).
|
|
530
437
|
|
|
531
|
-
|
|
438
|
+
| Metric | Value |
|
|
439
|
+
| --- | --- |
|
|
440
|
+
| Avg RPS (all workloads) | 10,658 |
|
|
441
|
+
| Avg latency (ms) | 5.70 |
|
|
532
442
|
|
|
533
|
-
|
|
443
|
+
Category breakdown:
|
|
534
444
|
|
|
535
|
-
|
|
|
445
|
+
| Category | Avg RPS | Avg latency (ms) |
|
|
536
446
|
| --- | --- | --- |
|
|
537
|
-
|
|
|
538
|
-
|
|
|
539
|
-
|
|
|
540
|
-
|
|
|
541
|
-
|
|
|
542
|
-
| spikard-ruby | 8,271 | 6.50 |
|
|
447
|
+
| path-params | 15,841 | 3.19 |
|
|
448
|
+
| multipart | 10,838 | 5.24 |
|
|
449
|
+
| query-params | 8,082 | 6.88 |
|
|
450
|
+
| json-bodies | 6,890 | 7.37 |
|
|
451
|
+
| forms | 6,241 | 8.80 |
|
|
543
452
|
|
|
544
|
-
|
|
545
|
-
- **Advantages:** Portable across all runtimes, smaller bundle size, cold start friendly
|
|
546
|
-
- **Trade-offs:** ~2.3x lower RPS on traditional servers vs native napi-rs bindings
|
|
453
|
+
## Performance Tips
|
|
547
454
|
|
|
548
|
-
|
|
455
|
+
1. **Lazy load routes**: Only define routes you need to minimize WASM size
|
|
456
|
+
2. **Compression**: Enable Brotli/Gzip compression for responses
|
|
457
|
+
3. **Caching**: Use Cache-Control headers for static content
|
|
458
|
+
4. **Streaming**: For large responses, use streaming responses
|
|
459
|
+
5. **Worker threads**: Offload heavy computation to Web Workers
|
|
549
460
|
|
|
550
|
-
|
|
551
|
-
- **WebAssembly compilation** for near-native performance in edge runtimes
|
|
552
|
-
- **Zero-copy data structures** where supported by runtime
|
|
553
|
-
- **Shared memory optimization** for large payloads
|
|
554
|
-
- **Streaming support** for efficient data transfer
|
|
555
|
-
- **Tree-shakable ESM** for minimal bundle sizes
|
|
556
|
-
- **Multi-runtime support:** Cloudflare Workers, Deno Deploy, Vercel Edge, browsers, Node.js
|
|
461
|
+
## Environment Variables
|
|
557
462
|
|
|
558
|
-
|
|
463
|
+
Access environment variables based on runtime:
|
|
559
464
|
|
|
560
465
|
```typescript
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
## Platform-Specific Examples
|
|
567
|
-
|
|
568
|
-
### Cloudflare Workers
|
|
569
|
-
|
|
570
|
-
```typescript
|
|
571
|
-
import { Spikard, get, createFetchHandler } from "spikard-wasm";
|
|
572
|
-
|
|
573
|
-
const app = new Spikard();
|
|
466
|
+
// Cloudflare Workers
|
|
467
|
+
get("/env", async (req, { env }) => {
|
|
468
|
+
const apiKey = env.API_KEY;
|
|
469
|
+
return { apiKey };
|
|
470
|
+
});
|
|
574
471
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
};
|
|
472
|
+
// Deno
|
|
473
|
+
get("/env", async (req) => {
|
|
474
|
+
const apiKey = Deno.env.get("API_KEY");
|
|
475
|
+
return { apiKey };
|
|
580
476
|
});
|
|
581
477
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
478
|
+
// Node.js / Bun
|
|
479
|
+
get("/env", async (req) => {
|
|
480
|
+
const apiKey = process.env.API_KEY;
|
|
481
|
+
return { apiKey };
|
|
482
|
+
});
|
|
585
483
|
```
|
|
586
484
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
```typescript
|
|
590
|
-
import { Spikard, get } from "npm:spikard-wasm";
|
|
591
|
-
|
|
592
|
-
const app = new Spikard();
|
|
593
|
-
|
|
594
|
-
get("/")(async () => ({ message: "Hello from Deno Deploy" }));
|
|
595
|
-
|
|
596
|
-
Deno.serve(
|
|
597
|
-
{ port: 8000 },
|
|
598
|
-
(request: Request) => app.handleRequest(request)
|
|
599
|
-
);
|
|
600
|
-
```
|
|
485
|
+
## Debugging
|
|
601
486
|
|
|
602
|
-
|
|
487
|
+
Enable debug logging:
|
|
603
488
|
|
|
604
489
|
```typescript
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const app = new Spikard();
|
|
490
|
+
const app = new Spikard({ debug: true });
|
|
608
491
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
export const config = { runtime: "edge" };
|
|
612
|
-
export default createFetchHandler(app);
|
|
492
|
+
// Or via environment
|
|
493
|
+
// SPIKARD_DEBUG=1 npm run dev
|
|
613
494
|
```
|
|
614
495
|
|
|
615
|
-
|
|
496
|
+
## Examples
|
|
616
497
|
|
|
617
|
-
|
|
618
|
-
import { Spikard, get } from "spikard-wasm";
|
|
498
|
+
Full working examples for different runtimes:
|
|
619
499
|
|
|
620
|
-
|
|
500
|
+
- **[Rollup Bundler](https://github.com/Goldziher/spikard/tree/main/examples/wasm-rollup)** - Build with Rollup for browsers and Node.js
|
|
501
|
+
- **[Deno Runtime](https://github.com/Goldziher/spikard/tree/main/examples/wasm-deno)** - Native Deno with zero build step
|
|
502
|
+
- **[Cloudflare Workers](https://github.com/Goldziher/spikard/tree/main/examples/wasm-cloudflare)** - Deploy to the edge with Wrangler
|
|
621
503
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
504
|
+
Each example includes:
|
|
505
|
+
- Complete TypeScript source code with strict typing
|
|
506
|
+
- Configuration files (tsconfig.json, package.json)
|
|
507
|
+
- Comprehensive README with usage instructions
|
|
508
|
+
- Consistent API routes demonstrating core features
|
|
626
509
|
|
|
627
|
-
|
|
628
|
-
if (event.request.url.includes("/api/")) {
|
|
629
|
-
event.respondWith(app.handleRequest(event.request));
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
```
|
|
510
|
+
Browse all examples: [`examples/`](https://github.com/Goldziher/spikard/tree/main/examples)
|
|
633
511
|
|
|
634
|
-
##
|
|
512
|
+
## Testing
|
|
635
513
|
|
|
636
|
-
|
|
514
|
+
Run tests with Vitest:
|
|
637
515
|
|
|
638
516
|
```bash
|
|
639
|
-
#
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
--output ./generated \
|
|
643
|
-
--target wasm
|
|
644
|
-
|
|
645
|
-
# Generate from AsyncAPI
|
|
646
|
-
spikard generate asyncapi \
|
|
647
|
-
--fixtures ../../testing_data/websockets \
|
|
648
|
-
--output ./generated \
|
|
649
|
-
--target wasm
|
|
517
|
+
pnpm test # Run all tests
|
|
518
|
+
pnpm test:watch # Watch mode
|
|
519
|
+
pnpm test:coverage # With coverage
|
|
650
520
|
```
|
|
651
521
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
See `/examples/wasm/` for more examples:
|
|
655
|
-
- **Basic REST API** - Simple CRUD operations
|
|
656
|
-
- **Cloudflare Workers** - Edge deployment
|
|
657
|
-
- **Deno Deploy** - Deno-specific features
|
|
658
|
-
- **WebSocket Chat** - Real-time communication
|
|
659
|
-
- **SSE Dashboard** - Server-sent events
|
|
660
|
-
- **File Upload** - Multipart form handling
|
|
522
|
+
Test coverage minimum: **80%**
|
|
661
523
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
### Building from Source
|
|
524
|
+
Run integration tests:
|
|
665
525
|
|
|
666
526
|
```bash
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
# Build WASM module
|
|
671
|
-
cd crates/spikard-wasm
|
|
672
|
-
wasm-pack build --target web
|
|
673
|
-
|
|
674
|
-
# Build TypeScript wrapper
|
|
675
|
-
cd ../../packages/wasm
|
|
676
|
-
pnpm build
|
|
527
|
+
task test:wasm
|
|
528
|
+
task test:wasm:integration
|
|
677
529
|
```
|
|
678
530
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
```bash
|
|
682
|
-
# Run all tests
|
|
683
|
-
pnpm test
|
|
684
|
-
|
|
685
|
-
# Run specific test file
|
|
686
|
-
pnpm test -- routing.spec.ts
|
|
687
|
-
|
|
688
|
-
# Run with coverage
|
|
689
|
-
pnpm test:coverage
|
|
690
|
-
```
|
|
531
|
+
## Documentation
|
|
691
532
|
|
|
692
|
-
|
|
533
|
+
- **API Docs**: [docs/api.md](./docs/api.md)
|
|
534
|
+
- **Architecture**: [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)
|
|
535
|
+
- **Architecture Decision Records**: [../../docs/adr/](../../docs/adr/)
|
|
536
|
+
- [ADR 0001: Architecture & Layering](../../docs/adr/0001-architecture.md)
|
|
537
|
+
- [ADR 0002: Tower-HTTP & Middleware](../../docs/adr/0002-runtime-and-middleware.md)
|
|
538
|
+
- [ADR 0006: Async & Streaming](../../docs/adr/0006-async-and-streaming.md)
|
|
693
539
|
|
|
694
|
-
|
|
695
|
-
1. Open DevTools
|
|
696
|
-
2. Enable "WebAssembly Debugging" in Experiments
|
|
697
|
-
3. Reload the page
|
|
698
|
-
4. Set breakpoints in WASM code
|
|
540
|
+
## TypeScript Support
|
|
699
541
|
|
|
700
|
-
|
|
542
|
+
Full TypeScript support with strict type checking:
|
|
701
543
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
- Lifecycle hooks
|
|
707
|
-
- Test client API
|
|
544
|
+
```bash
|
|
545
|
+
pnpm typecheck # Run tsc
|
|
546
|
+
pnpm lint # Run Biome
|
|
547
|
+
```
|
|
708
548
|
|
|
709
|
-
|
|
710
|
-
- **No native modules** - Pure WASM, no Node.js addons
|
|
711
|
-
- **Fetch API only** - No Node.js `http` module
|
|
712
|
-
- **Smaller bundle** - Tree-shakable ESM exports
|
|
713
|
-
- **Platform-agnostic** - Works in browsers, Deno, Workers
|
|
714
|
-
- **Edge-optimized** - Designed for edge runtimes
|
|
549
|
+
Generated `.d.ts` files via wasm-bindgen for complete IDE support.
|
|
715
550
|
|
|
716
|
-
|
|
551
|
+
## Contributing
|
|
717
552
|
|
|
718
|
-
|
|
719
|
-
- Deploying to edge runtimes (Cloudflare, Vercel, Deno Deploy)
|
|
720
|
-
- Running in browsers or service workers
|
|
721
|
-
- Need maximum portability across platforms
|
|
722
|
-
- Want smallest possible bundle size
|
|
553
|
+
Contributions welcome! See [CONTRIBUTING.md](../../CONTRIBUTING.md)
|
|
723
554
|
|
|
724
|
-
|
|
725
|
-
-
|
|
726
|
-
-
|
|
727
|
-
-
|
|
728
|
-
-
|
|
555
|
+
Code standards:
|
|
556
|
+
- TypeScript 5.x with strict mode enabled
|
|
557
|
+
- Biome for linting and formatting
|
|
558
|
+
- Vitest for testing
|
|
559
|
+
- 80%+ test coverage required
|
|
729
560
|
|
|
730
|
-
##
|
|
561
|
+
## Related Packages
|
|
731
562
|
|
|
732
|
-
- [
|
|
733
|
-
- [
|
|
734
|
-
- [
|
|
735
|
-
- [
|
|
563
|
+
- **@spikard/node**: [npm.im/@spikard/node](https://npm.im/@spikard/node) - Node.js native bindings
|
|
564
|
+
- **spikard**: [pypi.org/project/spikard](https://pypi.org/project/spikard) - Python bindings
|
|
565
|
+
- **spikard**: [rubygems.org/gems/spikard](https://rubygems.org/gems/spikard) - Ruby bindings
|
|
566
|
+
- **spikard/spikard**: [packagist.org/packages/spikard/spikard](https://packagist.org/packages/spikard/spikard) - PHP bindings
|
|
567
|
+
- **spikard**: [crates.io/crates/spikard](https://crates.io/crates/spikard) - Rust native
|
|
736
568
|
|
|
737
569
|
## License
|
|
738
570
|
|
|
739
|
-
MIT
|
|
571
|
+
MIT - see [LICENSE](LICENSE) file
|