fluxion-ts 0.0.4 → 0.0.6
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 +173 -0
- package/package.json +1 -1
- package/src/core/server.ts +4 -12
- package/src/workers/file-runtime.ts +44 -23
- package/tests/core/file-runtime.test.ts +27 -0
package/README.md
CHANGED
|
@@ -9,3 +9,176 @@
|
|
|
9
9
|
<img src="https://raw.githubusercontent.com/baendlorel/fluxion/refs/heads/main/assets/fluxion.svg" width="240px" alt="fluxion logo" />
|
|
10
10
|
</a>
|
|
11
11
|
</p>
|
|
12
|
+
|
|
13
|
+
Fluxion is a filesystem-routing dynamic server for Node.js.
|
|
14
|
+
|
|
15
|
+
- Use `.mjs` files directly as route handlers
|
|
16
|
+
- Run handlers inside worker runtime isolation
|
|
17
|
+
- Inject any npm module into handler `context` via `modules`
|
|
18
|
+
- If a handler returns a value, Fluxion auto-responds with `200 + JSON`
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install fluxion
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1) Start the server
|
|
29
|
+
|
|
30
|
+
Create `server.mjs`:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { fluxion } from 'fluxion';
|
|
34
|
+
|
|
35
|
+
fluxion({
|
|
36
|
+
dir: './dynamicDirectory',
|
|
37
|
+
host: '127.0.0.1',
|
|
38
|
+
port: 3000,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2) Create a route handler
|
|
43
|
+
|
|
44
|
+
Create `dynamicDirectory/hello.mjs`:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
export default function handler(_req, _res, context) {
|
|
48
|
+
return {
|
|
49
|
+
message: 'hello fluxion',
|
|
50
|
+
workerId: context.worker.id,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node server.mjs
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Test:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl http://127.0.0.1:3000/hello
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You will get a JSON response with status `200`.
|
|
68
|
+
|
|
69
|
+
## Routing Rules
|
|
70
|
+
|
|
71
|
+
- `dynamicDirectory/index.mjs` -> `/`
|
|
72
|
+
- `dynamicDirectory/user.mjs` -> `/user`
|
|
73
|
+
- `dynamicDirectory/user/index.mjs` -> `/user`
|
|
74
|
+
- Non-`.mjs` files are served as static files (`GET/HEAD`)
|
|
75
|
+
- Directories/files starting with `_` are private and not routable
|
|
76
|
+
|
|
77
|
+
## Handler Styles
|
|
78
|
+
|
|
79
|
+
### Function export
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
export default function handler(req, res, context) {
|
|
83
|
+
return { ok: true };
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Object export (with modules)
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
export default {
|
|
91
|
+
modules: [
|
|
92
|
+
{
|
|
93
|
+
module: 'node:crypto',
|
|
94
|
+
injectKey: 'crypto',
|
|
95
|
+
factory: (cryptoModule) => cryptoModule,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
handler(_req, _res, context) {
|
|
99
|
+
return {
|
|
100
|
+
hash: context.crypto.createHash('sha1').update('abc').digest('hex'),
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Automatic JSON Response
|
|
107
|
+
|
|
108
|
+
If a handler return value is not `undefined` and you do not manually call `res.end()`, Fluxion will automatically:
|
|
109
|
+
|
|
110
|
+
- set status to `200`
|
|
111
|
+
- set `content-type` to `application/json; charset=utf-8` (if missing)
|
|
112
|
+
- serialize the return value with `JSON.stringify(...)`
|
|
113
|
+
|
|
114
|
+
Recommended pattern per handler:
|
|
115
|
+
|
|
116
|
+
1. Return data directly (recommended)
|
|
117
|
+
2. Or fully control `res` manually (streaming, file download, etc.)
|
|
118
|
+
|
|
119
|
+
## Module Injection (Recommended)
|
|
120
|
+
|
|
121
|
+
Fluxion does not bundle database drivers. Install app dependencies yourself.
|
|
122
|
+
|
|
123
|
+
For example, to use MySQL in handlers:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm install mysql2
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
export default {
|
|
131
|
+
modules: [
|
|
132
|
+
{
|
|
133
|
+
module: 'mysql2/promise',
|
|
134
|
+
injectKey: 'mydb',
|
|
135
|
+
options: {
|
|
136
|
+
host: '127.0.0.1',
|
|
137
|
+
user: 'root',
|
|
138
|
+
password: '***',
|
|
139
|
+
database: 'demo',
|
|
140
|
+
},
|
|
141
|
+
factory: (mysql2, options) => mysql2.createPool(options),
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
async handler(_req, _res, context) {
|
|
145
|
+
const [rows] = await context.mydb.query('select 1 as ok');
|
|
146
|
+
return rows;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `modules` fields
|
|
152
|
+
|
|
153
|
+
- `module`: module id used by dynamic `import()`
|
|
154
|
+
- `injectKey`: target key in `context[injectKey]`
|
|
155
|
+
- `options`: custom config passed into `factory`
|
|
156
|
+
- `factory`: `(importedModule, options, runtime) => injectedValue`
|
|
157
|
+
|
|
158
|
+
`factory` runs inside the worker and is restored from source text. Keep it self-contained (do not depend on outer closures).
|
|
159
|
+
|
|
160
|
+
Each worker keeps its own module instances. On worker shutdown, Fluxion will attempt `dispose/close/end/destroy` if present.
|
|
161
|
+
|
|
162
|
+
## Common Options
|
|
163
|
+
|
|
164
|
+
Main `fluxion({...})` options:
|
|
165
|
+
|
|
166
|
+
- `dir`: dynamic directory (handler root)
|
|
167
|
+
- `host`: listen host
|
|
168
|
+
- `port`: listen port
|
|
169
|
+
- `maxRequestBytes`: max request body size (returns 413 when exceeded)
|
|
170
|
+
- `logger`: `one-line` / `json-line` / custom function
|
|
171
|
+
|
|
172
|
+
## Important
|
|
173
|
+
|
|
174
|
+
Legacy handler-level `db` declarations are removed:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
export default {
|
|
178
|
+
// db: ['main'] // no longer supported
|
|
179
|
+
modules: [],
|
|
180
|
+
handler() {},
|
|
181
|
+
};
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Use `modules` for dependency injection.
|
package/package.json
CHANGED
package/src/core/server.ts
CHANGED
|
@@ -131,11 +131,7 @@ function normalizeDatabaseRuntimeConfigItem(
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const rawDriver =
|
|
134
|
-
typeof input.driver === 'string'
|
|
135
|
-
? input.driver
|
|
136
|
-
: typeof input.type === 'string'
|
|
137
|
-
? input.type
|
|
138
|
-
: undefined;
|
|
134
|
+
typeof input.driver === 'string' ? input.driver : typeof input.type === 'string' ? input.type : undefined;
|
|
139
135
|
|
|
140
136
|
if (rawDriver === undefined) {
|
|
141
137
|
throw new Error(`Missing db driver for "${dbName}" in ${source}`);
|
|
@@ -209,11 +205,7 @@ function normalizeDatabaseConfigMap(input: unknown, source: string): protocol.Wo
|
|
|
209
205
|
throw new Error(`Invalid db config for "${name}" in "${source}"`);
|
|
210
206
|
}
|
|
211
207
|
|
|
212
|
-
normalized[name] = normalizeDatabaseRuntimeConfigItem(
|
|
213
|
-
name,
|
|
214
|
-
rawConfig as FluxionDatabaseRuntimeConfigInput,
|
|
215
|
-
source,
|
|
216
|
-
);
|
|
208
|
+
normalized[name] = normalizeDatabaseRuntimeConfigItem(name, rawConfig as FluxionDatabaseRuntimeConfigInput, source);
|
|
217
209
|
}
|
|
218
210
|
|
|
219
211
|
return normalized;
|
|
@@ -372,7 +364,7 @@ export function fluxion(options: FluxionOptions): http.Server {
|
|
|
372
364
|
|
|
373
365
|
const bodyCapture = createBodyPreviewCapture(req);
|
|
374
366
|
|
|
375
|
-
logger.write('INFO', '
|
|
367
|
+
logger.write('INFO', 'Req', { method, ip, path: url.pathname });
|
|
376
368
|
|
|
377
369
|
const start = performance.now();
|
|
378
370
|
res.once('finish', () => {
|
|
@@ -395,7 +387,7 @@ export function fluxion(options: FluxionOptions): http.Server {
|
|
|
395
387
|
fields.bodyTruncated = bodyPreview.truncated;
|
|
396
388
|
}
|
|
397
389
|
|
|
398
|
-
logger.write('INFO', '
|
|
390
|
+
logger.write('INFO', 'Res', fields);
|
|
399
391
|
});
|
|
400
392
|
|
|
401
393
|
void metaApi
|
|
@@ -532,6 +532,25 @@ function buildHandlerCandidates(dynamicDirectory: string, segments: readonly str
|
|
|
532
532
|
return [path.resolve(routePath, 'index.mjs'), `${routePath}.mjs`];
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Builds ordered static file candidates for one route.
|
|
537
|
+
*/
|
|
538
|
+
function buildStaticCandidates(dynamicDirectory: string, segments: readonly string[]): string[] {
|
|
539
|
+
if (segments.length === 0) {
|
|
540
|
+
return [path.resolve(dynamicDirectory, 'index.html')];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const routePath = path.resolve(dynamicDirectory, ...segments);
|
|
544
|
+
const candidates = [routePath];
|
|
545
|
+
const lastSegment = segments[segments.length - 1];
|
|
546
|
+
|
|
547
|
+
if (path.extname(lastSegment).length === 0) {
|
|
548
|
+
candidates.push(path.resolve(routePath, 'index.html'));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return candidates;
|
|
552
|
+
}
|
|
553
|
+
|
|
535
554
|
/**
|
|
536
555
|
* Streams static file to response.
|
|
537
556
|
*/
|
|
@@ -893,38 +912,40 @@ export function createFileRuntime(dir: string, options: FileRuntimeOptions = {})
|
|
|
893
912
|
return HandlerResult.NotFound;
|
|
894
913
|
}
|
|
895
914
|
|
|
896
|
-
|
|
897
|
-
return HandlerResult.NotFound;
|
|
898
|
-
}
|
|
915
|
+
const candidates = buildStaticCandidates(dir, parsedPath.segments);
|
|
899
916
|
|
|
900
|
-
|
|
917
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
918
|
+
const filePath = candidates[i];
|
|
901
919
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
920
|
+
if (!isUnderDirectory(filePath, dir)) {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
905
923
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
924
|
+
if (path.extname(filePath).toLowerCase() === '.mjs') {
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
909
927
|
|
|
910
|
-
|
|
911
|
-
|
|
928
|
+
try {
|
|
929
|
+
const stat = await fs.promises.stat(filePath);
|
|
912
930
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
931
|
+
if (!stat.isFile()) {
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
916
934
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
935
|
+
await streamStaticFile(filePath, stat, method, res);
|
|
936
|
+
return HandlerResult.Handled;
|
|
937
|
+
} catch (error) {
|
|
938
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
921
939
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
940
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
925
943
|
|
|
926
|
-
|
|
944
|
+
throw error;
|
|
945
|
+
}
|
|
927
946
|
}
|
|
947
|
+
|
|
948
|
+
return HandlerResult.NotFound;
|
|
928
949
|
};
|
|
929
950
|
|
|
930
951
|
/**
|
|
@@ -169,6 +169,33 @@ describe('file-runtime', () => {
|
|
|
169
169
|
expect(await hiddenStaticResponse.text()).toBe('not_found');
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
it('falls back to route index.html when direct file is not matched', async () => {
|
|
173
|
+
const dynamicDirectory = await createTempDirectory('fluxion-runtime-static-index-');
|
|
174
|
+
tempDirectories.push(dynamicDirectory);
|
|
175
|
+
|
|
176
|
+
await writeFile(
|
|
177
|
+
path.join(dynamicDirectory, 'docs', 'index.html'),
|
|
178
|
+
'<html><body><h1>docs-home</h1></body></html>',
|
|
179
|
+
);
|
|
180
|
+
await writeFile(
|
|
181
|
+
path.join(dynamicDirectory, 'index.html'),
|
|
182
|
+
'<html><body><h1>root-home</h1></body></html>',
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const { server, baseUrl } = await startRuntimeServer(dynamicDirectory);
|
|
186
|
+
servers.push(server);
|
|
187
|
+
|
|
188
|
+
const docsResponse = await fetch(`${baseUrl}/docs`);
|
|
189
|
+
expect(docsResponse.status).toBe(200);
|
|
190
|
+
expect(docsResponse.headers.get('content-type')).toContain('text/html');
|
|
191
|
+
expect(await docsResponse.text()).toContain('docs-home');
|
|
192
|
+
|
|
193
|
+
const rootResponse = await fetch(`${baseUrl}/`);
|
|
194
|
+
expect(rootResponse.status).toBe(200);
|
|
195
|
+
expect(rootResponse.headers.get('content-type')).toContain('text/html');
|
|
196
|
+
expect(await rootResponse.text()).toContain('root-home');
|
|
197
|
+
});
|
|
198
|
+
|
|
172
199
|
it('creates route snapshot from .mjs handlers and static files', async () => {
|
|
173
200
|
const dynamicDirectory = await createTempDirectory('fluxion-runtime-snapshot-');
|
|
174
201
|
tempDirectories.push(dynamicDirectory);
|