fluxion-ts 0.0.4
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/.oxlintrc.json +64 -0
- package/.prettierrc +6 -0
- package/AGENTS.md +3 -0
- package/Dockerfile +13 -0
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/document/index.html +16 -0
- package/document/src/main.tsx +8 -0
- package/document/src/styles.css +321 -0
- package/document/src/view/App.tsx +304 -0
- package/document/src/view/CodeBlock.tsx +11 -0
- package/document/src/view/Section.tsx +18 -0
- package/document/vite.config.ts +23 -0
- package/draft/vibe.md +50 -0
- package/package.json +66 -0
- package/rollup.config.mjs +102 -0
- package/scripts/build-image.ts +13 -0
- package/scripts/bump-version.ts +12 -0
- package/scripts/configs.ts +79 -0
- package/scripts/lines.ts +54 -0
- package/scripts/publish.ts +6 -0
- package/src/common/consts.ts +30 -0
- package/src/common/dtm.ts +10 -0
- package/src/common/logger.ts +145 -0
- package/src/core/meta-api.ts +48 -0
- package/src/core/server.ts +447 -0
- package/src/core/types.d.ts +6 -0
- package/src/core/utils/headers.ts +34 -0
- package/src/core/utils/request.ts +145 -0
- package/src/core/utils/send-json.ts +21 -0
- package/src/index.ts +11 -0
- package/src/workers/file-runtime.ts +1071 -0
- package/src/workers/handler-worker-pool.ts +754 -0
- package/src/workers/handler-worker.ts +1029 -0
- package/src/workers/options.ts +77 -0
- package/src/workers/protocol.d.ts +186 -0
- package/tests/core/dynamic-directory.test.ts +48 -0
- package/tests/core/file-runtime.test.ts +347 -0
- package/tests/core/server-options.test.ts +204 -0
- package/tests/e2e/fluxion-server.e2e-spec.ts +225 -0
- package/tests/helpers/test-utils.ts +81 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +24 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { once } from 'node:events';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { fluxion } from '@/core/server.js';
|
|
8
|
+
import type { FluxionDatabaseInput } from '@/core/server.js';
|
|
9
|
+
import type { LogEntry, LoggerOption } from '@/common/logger.js';
|
|
10
|
+
|
|
11
|
+
import { closeServer, createTempDirectory, removeDirectory, writeFile } from '../helpers/test-utils.js';
|
|
12
|
+
|
|
13
|
+
interface StartServerOptions {
|
|
14
|
+
maxRequestBytes?: number;
|
|
15
|
+
logger?: LoggerOption;
|
|
16
|
+
databases?: FluxionDatabaseInput[];
|
|
17
|
+
dbConfigPath?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function startServer(
|
|
21
|
+
dynamicDirectory: string,
|
|
22
|
+
options: StartServerOptions = {},
|
|
23
|
+
): Promise<{ server: http.Server; baseUrl: string }> {
|
|
24
|
+
const server = fluxion({
|
|
25
|
+
dir: dynamicDirectory,
|
|
26
|
+
host: '127.0.0.1',
|
|
27
|
+
port: 0,
|
|
28
|
+
maxRequestBytes: options.maxRequestBytes,
|
|
29
|
+
logger: options.logger,
|
|
30
|
+
databases: options.databases,
|
|
31
|
+
dbConfigPath: options.dbConfigPath,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!server.listening) {
|
|
35
|
+
await once(server, 'listening');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const address = server.address();
|
|
39
|
+
if (address === null || typeof address === 'string') {
|
|
40
|
+
throw new Error('Failed to resolve server address');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
server,
|
|
45
|
+
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('server options', () => {
|
|
50
|
+
const tempDirectories: string[] = [];
|
|
51
|
+
const servers: http.Server[] = [];
|
|
52
|
+
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(async () => {
|
|
59
|
+
for (const server of servers.splice(0)) {
|
|
60
|
+
await closeServer(server);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const tempDirectory of tempDirectories.splice(0)) {
|
|
64
|
+
await removeDirectory(tempDirectory);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
vi.restoreAllMocks();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('enforces maxRequestBytes and returns 413 for oversized body', async () => {
|
|
71
|
+
const dynamicDirectory = await createTempDirectory('fluxion-server-max-body-');
|
|
72
|
+
tempDirectories.push(dynamicDirectory);
|
|
73
|
+
|
|
74
|
+
await writeFile(
|
|
75
|
+
path.join(dynamicDirectory, 'echo.mjs'),
|
|
76
|
+
[
|
|
77
|
+
'export default function handler(req, res) {',
|
|
78
|
+
' const chunks = [];',
|
|
79
|
+
" req.on('data', (chunk) => chunks.push(Buffer.from(chunk)));",
|
|
80
|
+
" req.on('end', () => res.end(String(Buffer.concat(chunks).byteLength)));",
|
|
81
|
+
'}',
|
|
82
|
+
].join('\n'),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const { server, baseUrl } = await startServer(dynamicDirectory, { maxRequestBytes: 8 });
|
|
86
|
+
servers.push(server);
|
|
87
|
+
|
|
88
|
+
const oversizedResponse = await fetch(`${baseUrl}/echo`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
body: '123456789',
|
|
91
|
+
headers: {
|
|
92
|
+
'content-type': 'text/plain',
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(oversizedResponse.status).toBe(413);
|
|
97
|
+
|
|
98
|
+
const oversizedPayload = (await oversizedResponse.json()) as { message?: string };
|
|
99
|
+
expect(oversizedPayload.message).toContain('request body too large');
|
|
100
|
+
|
|
101
|
+
const okResponse = await fetch(`${baseUrl}/echo`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
body: '12345',
|
|
104
|
+
headers: {
|
|
105
|
+
'content-type': 'text/plain',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(okResponse.status).toBe(200);
|
|
110
|
+
expect(await okResponse.text()).toBe('5');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('rejects invalid maxRequestBytes at startup', async () => {
|
|
114
|
+
const dynamicDirectory = await createTempDirectory('fluxion-server-max-body-invalid-');
|
|
115
|
+
tempDirectories.push(dynamicDirectory);
|
|
116
|
+
|
|
117
|
+
expect(() =>
|
|
118
|
+
fluxion({
|
|
119
|
+
dir: dynamicDirectory,
|
|
120
|
+
host: '127.0.0.1',
|
|
121
|
+
port: 0,
|
|
122
|
+
maxRequestBytes: 0,
|
|
123
|
+
}),
|
|
124
|
+
).toThrow('Invalid maxRequestBytes');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('loads db config from private file path', async () => {
|
|
128
|
+
const dynamicDirectory = await createTempDirectory('fluxion-server-db-config-');
|
|
129
|
+
tempDirectories.push(dynamicDirectory);
|
|
130
|
+
|
|
131
|
+
await writeFile(
|
|
132
|
+
path.join(dynamicDirectory, 'ctx.mjs'),
|
|
133
|
+
[
|
|
134
|
+
'export default {',
|
|
135
|
+
' handler(_req, res, context) {',
|
|
136
|
+
" const ready = typeof context.worker?.id === 'string';",
|
|
137
|
+
" res.end(ready ? 'worker-ready' : 'worker-missing');",
|
|
138
|
+
' },',
|
|
139
|
+
'};',
|
|
140
|
+
].join('\n'),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const privateConfigPath = path.join(dynamicDirectory, '.fluxion-private', 'db.config.cjs');
|
|
144
|
+
await writeFile(
|
|
145
|
+
privateConfigPath,
|
|
146
|
+
[
|
|
147
|
+
'module.exports = {',
|
|
148
|
+
" main: {",
|
|
149
|
+
" driver: 'pg',",
|
|
150
|
+
' options: {},',
|
|
151
|
+
' },',
|
|
152
|
+
'};',
|
|
153
|
+
].join('\n'),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const { server, baseUrl } = await startServer(dynamicDirectory, {
|
|
157
|
+
dbConfigPath: privateConfigPath,
|
|
158
|
+
});
|
|
159
|
+
servers.push(server);
|
|
160
|
+
|
|
161
|
+
const response = await fetch(`${baseUrl}/ctx`);
|
|
162
|
+
expect(response.status).toBe(200);
|
|
163
|
+
expect(await response.text()).toBe('worker-ready');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('supports json-line logger mode', async () => {
|
|
167
|
+
const dynamicDirectory = await createTempDirectory('fluxion-server-logger-json-');
|
|
168
|
+
tempDirectories.push(dynamicDirectory);
|
|
169
|
+
|
|
170
|
+
const { server, baseUrl } = await startServer(dynamicDirectory, { logger: 'json-line' });
|
|
171
|
+
servers.push(server);
|
|
172
|
+
|
|
173
|
+
await fetch(`${baseUrl}/missing`);
|
|
174
|
+
|
|
175
|
+
const lines = consoleLogSpy.mock.calls
|
|
176
|
+
.map((call: unknown[]) => call[0])
|
|
177
|
+
.filter((value: unknown): value is string => typeof value === 'string');
|
|
178
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
179
|
+
|
|
180
|
+
const entries = lines.map((line: string) => JSON.parse(line) as Record<string, unknown>);
|
|
181
|
+
expect(entries.some((entry: Record<string, unknown>) => entry.event === 'ServerStarted')).toBe(true);
|
|
182
|
+
expect(entries.some((entry: Record<string, unknown>) => entry.event === 'RequestCompleted')).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('supports custom logger function', async () => {
|
|
186
|
+
const dynamicDirectory = await createTempDirectory('fluxion-server-logger-custom-');
|
|
187
|
+
tempDirectories.push(dynamicDirectory);
|
|
188
|
+
|
|
189
|
+
const entries: LogEntry[] = [];
|
|
190
|
+
|
|
191
|
+
const { server, baseUrl } = await startServer(dynamicDirectory, {
|
|
192
|
+
logger(entry) {
|
|
193
|
+
entries.push(entry);
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
servers.push(server);
|
|
197
|
+
|
|
198
|
+
await fetch(`${baseUrl}/missing`);
|
|
199
|
+
|
|
200
|
+
expect(entries.some((entry) => entry.event === 'ServerStarted')).toBe(true);
|
|
201
|
+
expect(entries.some((entry) => entry.event === 'RequestCompleted')).toBe(true);
|
|
202
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { once } from 'node:events';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import axios, { type AxiosInstance } from 'axios';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { fluxion } from '@/core/server.js';
|
|
10
|
+
|
|
11
|
+
import { closeServer, createTempDirectory, removeDirectory, sleep, waitFor, writeFile } from '../helpers/test-utils.js';
|
|
12
|
+
|
|
13
|
+
async function startFluxion(dynamicDirectory: string): Promise<{ server: http.Server; client: AxiosInstance }> {
|
|
14
|
+
const server = fluxion({
|
|
15
|
+
dir: dynamicDirectory,
|
|
16
|
+
host: '127.0.0.1',
|
|
17
|
+
port: 0,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!server.listening) {
|
|
21
|
+
await once(server, 'listening');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const address = server.address();
|
|
25
|
+
if (address === null || typeof address === 'string') {
|
|
26
|
+
throw new Error('Failed to resolve server address');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const baseURL = `http://127.0.0.1:${address.port}`;
|
|
30
|
+
|
|
31
|
+
const client = axios.create({
|
|
32
|
+
baseURL,
|
|
33
|
+
timeout: 2500,
|
|
34
|
+
validateStatus: () => true,
|
|
35
|
+
proxy: false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return { server, client };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('fluxion e2e', () => {
|
|
42
|
+
const tempDirectories: string[] = [];
|
|
43
|
+
const servers: http.Server[] = [];
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
for (const server of servers.splice(0)) {
|
|
51
|
+
await closeServer(server);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const tempDirectory of tempDirectories.splice(0)) {
|
|
55
|
+
await removeDirectory(tempDirectory);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
vi.restoreAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('loads startup routes, serves static files, and exposes meta apis', async () => {
|
|
62
|
+
const dynamicDirectory = await createTempDirectory('fluxion-e2e-startup-');
|
|
63
|
+
tempDirectories.push(dynamicDirectory);
|
|
64
|
+
|
|
65
|
+
await writeFile(
|
|
66
|
+
path.join(dynamicDirectory, 'aaa', 'bb', 'cc', 'index.mjs'),
|
|
67
|
+
"export default function handler(_req, res) { res.end('startup-ok'); }",
|
|
68
|
+
);
|
|
69
|
+
await writeFile(path.join(dynamicDirectory, 'aaa', 'public', 'app.js'), "console.log('startup');");
|
|
70
|
+
|
|
71
|
+
const { server, client } = await startFluxion(dynamicDirectory);
|
|
72
|
+
servers.push(server);
|
|
73
|
+
|
|
74
|
+
const nestedResponse = await client.get('/aaa/bb/cc');
|
|
75
|
+
expect(nestedResponse.status).toBe(200);
|
|
76
|
+
expect(nestedResponse.data).toBe('startup-ok');
|
|
77
|
+
|
|
78
|
+
const staticResponse = await client.get('/aaa/public/app.js');
|
|
79
|
+
expect(staticResponse.status).toBe(200);
|
|
80
|
+
expect(staticResponse.data).toContain("console.log('startup')");
|
|
81
|
+
|
|
82
|
+
const routesResponse = await client.get('/_fluxion/routes');
|
|
83
|
+
expect(routesResponse.status).toBe(200);
|
|
84
|
+
expect(routesResponse.data).toMatchObject({
|
|
85
|
+
routes: {
|
|
86
|
+
handlers: [
|
|
87
|
+
{
|
|
88
|
+
route: '/aaa/bb/cc',
|
|
89
|
+
file: 'aaa/bb/cc/index.mjs',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
expect(
|
|
95
|
+
routesResponse.data.routes.staticFiles.some(
|
|
96
|
+
(item: { route: string; file: string }) =>
|
|
97
|
+
item.route === '/aaa/public/app.js' && item.file === 'aaa/public/app.js',
|
|
98
|
+
),
|
|
99
|
+
).toBe(true);
|
|
100
|
+
|
|
101
|
+
const healthzResponse = await client.get('/_fluxion/healthz');
|
|
102
|
+
expect(healthzResponse.status).toBe(200);
|
|
103
|
+
expect(healthzResponse.data?.ok).toBe(true);
|
|
104
|
+
|
|
105
|
+
const workersResponse = await client.get('/_fluxion/workers');
|
|
106
|
+
expect(workersResponse.status).toBe(200);
|
|
107
|
+
expect(workersResponse.data).toMatchObject({
|
|
108
|
+
workers: {
|
|
109
|
+
dir: dynamicDirectory,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
expect(Array.isArray(workersResponse.data.workers?.workers)).toBe(true);
|
|
113
|
+
expect(workersResponse.data.workers?.workers.length).toBeGreaterThanOrEqual(1);
|
|
114
|
+
|
|
115
|
+
const missingRouteResponse = await client.get('/missing/path');
|
|
116
|
+
expect(missingRouteResponse.status).toBe(404);
|
|
117
|
+
expect(missingRouteResponse.data).toMatchObject({
|
|
118
|
+
message: 'Route not found',
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('reflects route add and remove by file changes', async () => {
|
|
123
|
+
const dynamicDirectory = await createTempDirectory('fluxion-e2e-add-remove-');
|
|
124
|
+
tempDirectories.push(dynamicDirectory);
|
|
125
|
+
|
|
126
|
+
const { server, client } = await startFluxion(dynamicDirectory);
|
|
127
|
+
servers.push(server);
|
|
128
|
+
|
|
129
|
+
await writeFile(
|
|
130
|
+
path.join(dynamicDirectory, 'bbb', 'hello.mjs'),
|
|
131
|
+
"export default function handler(_req, res) { res.end('watch-mounted'); }",
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
await waitFor(async () => {
|
|
135
|
+
const response = await client.get('/bbb/hello');
|
|
136
|
+
return response.status === 200 && response.data === 'watch-mounted';
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const routesAfterAdd = await client.get('/_fluxion/routes');
|
|
140
|
+
expect(
|
|
141
|
+
routesAfterAdd.data.routes.handlers.some(
|
|
142
|
+
(item: { route: string; file: string }) => item.route === '/bbb/hello' && item.file === 'bbb/hello.mjs',
|
|
143
|
+
),
|
|
144
|
+
).toBe(true);
|
|
145
|
+
|
|
146
|
+
await fs.rm(path.join(dynamicDirectory, 'bbb', 'hello.mjs'), { force: true });
|
|
147
|
+
|
|
148
|
+
await waitFor(async () => {
|
|
149
|
+
const response = await client.get('/bbb/hello');
|
|
150
|
+
return response.status === 404 && response.data?.message === 'Route not found';
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const routesAfterRemove = await client.get('/_fluxion/routes');
|
|
154
|
+
expect(routesAfterRemove.data.routes.handlers.some((item: { route: string }) => item.route === '/bbb/hello')).toBe(
|
|
155
|
+
false,
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('hot reloads mjs handler by mtime+size and returns 500 for invalid default export', async () => {
|
|
160
|
+
const dynamicDirectory = await createTempDirectory('fluxion-e2e-reload-');
|
|
161
|
+
tempDirectories.push(dynamicDirectory);
|
|
162
|
+
|
|
163
|
+
const handlerFile = path.join(dynamicDirectory, 'ccc', 'task.mjs');
|
|
164
|
+
|
|
165
|
+
await writeFile(handlerFile, "export default function handler(_req, res) { res.end('v1'); }");
|
|
166
|
+
|
|
167
|
+
const { server, client } = await startFluxion(dynamicDirectory);
|
|
168
|
+
servers.push(server);
|
|
169
|
+
|
|
170
|
+
const firstResponse = await client.get('/ccc/task');
|
|
171
|
+
expect(firstResponse.status).toBe(200);
|
|
172
|
+
expect(firstResponse.data).toBe('v1');
|
|
173
|
+
|
|
174
|
+
await sleep(20);
|
|
175
|
+
await writeFile(handlerFile, "export default function handler(_req, res) { res.end('v2-hot'); }");
|
|
176
|
+
|
|
177
|
+
await waitFor(async () => {
|
|
178
|
+
const response = await client.get('/ccc/task');
|
|
179
|
+
return response.status === 200 && response.data === 'v2-hot';
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await sleep(20);
|
|
183
|
+
await writeFile(handlerFile, 'export default { broken: true };');
|
|
184
|
+
|
|
185
|
+
await waitFor(async () => {
|
|
186
|
+
const response = await client.get('/ccc/task');
|
|
187
|
+
return response.status === 500 && response.data?.message === 'Internal Server Error';
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('blocks routing for underscore-prefixed directories', async () => {
|
|
192
|
+
const dynamicDirectory = await createTempDirectory('fluxion-e2e-underscore-');
|
|
193
|
+
tempDirectories.push(dynamicDirectory);
|
|
194
|
+
|
|
195
|
+
await writeFile(
|
|
196
|
+
path.join(dynamicDirectory, '_lib', 'secret.mjs'),
|
|
197
|
+
"export default function handler(_req, res) { res.end('hidden'); }",
|
|
198
|
+
);
|
|
199
|
+
await writeFile(path.join(dynamicDirectory, '_lib', 'tool.js'), "console.log('hidden-tool');");
|
|
200
|
+
await writeFile(
|
|
201
|
+
path.join(dynamicDirectory, 'public', 'ping.mjs'),
|
|
202
|
+
"export default function handler(_req, res) { res.end('public-ok'); }",
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const { server, client } = await startFluxion(dynamicDirectory);
|
|
206
|
+
servers.push(server);
|
|
207
|
+
|
|
208
|
+
const publicResponse = await client.get('/public/ping');
|
|
209
|
+
expect(publicResponse.status).toBe(200);
|
|
210
|
+
expect(publicResponse.data).toBe('public-ok');
|
|
211
|
+
|
|
212
|
+
const hiddenHandlerResponse = await client.get('/_lib/secret');
|
|
213
|
+
expect(hiddenHandlerResponse.status).toBe(404);
|
|
214
|
+
|
|
215
|
+
const hiddenStaticResponse = await client.get('/_lib/tool.js');
|
|
216
|
+
expect(hiddenStaticResponse.status).toBe(404);
|
|
217
|
+
|
|
218
|
+
const routesResponse = await client.get('/_fluxion/routes');
|
|
219
|
+
const hasHiddenRoute =
|
|
220
|
+
routesResponse.data.routes.handlers.some((item: { file: string }) => item.file.startsWith('_lib/')) ||
|
|
221
|
+
routesResponse.data.routes.staticFiles.some((item: { file: string }) => item.file.startsWith('_lib/'));
|
|
222
|
+
|
|
223
|
+
expect(hasHiddenRoute).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { once } from 'node:events';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
export async function createTempDirectory(prefix: string): Promise<string> {
|
|
8
|
+
return fs.promises.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function removeDirectory(targetPath: string): Promise<void> {
|
|
12
|
+
await fs.promises.rm(targetPath, { recursive: true, force: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function writeFile(targetPath: string, content: string): Promise<void> {
|
|
16
|
+
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
17
|
+
await fs.promises.writeFile(targetPath, content, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function waitFor(predicate: () => Promise<boolean>, timeoutMs = 5000, stepMs = 50): Promise<void> {
|
|
21
|
+
const startAt = Date.now();
|
|
22
|
+
|
|
23
|
+
while (Date.now() - startAt <= timeoutMs) {
|
|
24
|
+
if (await predicate()) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await sleep(stepMs);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
throw new Error(`Condition not met within ${timeoutMs}ms`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function sleep(ms: number): Promise<void> {
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function listenEphemeral(
|
|
39
|
+
server: http.Server,
|
|
40
|
+
host = '127.0.0.1',
|
|
41
|
+
): Promise<{ host: string; port: number; baseUrl: string }> {
|
|
42
|
+
server.listen(0, host);
|
|
43
|
+
|
|
44
|
+
if (!server.listening) {
|
|
45
|
+
await once(server, 'listening');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const address = server.address();
|
|
49
|
+
|
|
50
|
+
if (address === null || typeof address === 'string') {
|
|
51
|
+
throw new Error('Failed to resolve server address');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
host,
|
|
56
|
+
port: address.port,
|
|
57
|
+
baseUrl: `http://${host}:${address.port}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function closeServer(server: http.Server): Promise<void> {
|
|
62
|
+
if (!server.listening) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await new Promise<void>((resolve, reject) => {
|
|
67
|
+
server.close((error) => {
|
|
68
|
+
if (error !== undefined) {
|
|
69
|
+
reject(error);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resolve();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export function ensureDir(dir: string): void {
|
|
78
|
+
if (!fs.existsSync(dir)) {
|
|
79
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"jsxImportSource": "kt.js",
|
|
11
|
+
"plugins": [{ "name": "@ktjs/ts-plugin" }],
|
|
12
|
+
|
|
13
|
+
"declaration": false,
|
|
14
|
+
"outDir": "dist",
|
|
15
|
+
"baseUrl": ".",
|
|
16
|
+
"paths": {
|
|
17
|
+
"@/*": ["src/*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["./src/**/*.ts", "*.config.ts", "./tests/**/*.ts", "./document/**/*.ts", "./document/**/*.tsx"],
|
|
21
|
+
"exclude": ["**/dist/**/*.js", "**/dist/**/*.d.ts", "**/example/**/code/**"]
|
|
22
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig(() => {
|
|
5
|
+
const packageDir = import.meta.dirname;
|
|
6
|
+
return {
|
|
7
|
+
test: {
|
|
8
|
+
include: [path.join(packageDir, '**', '*.{test,spec,e2e-spec}.?(c|m)[jt]s?(x)')],
|
|
9
|
+
exclude: [
|
|
10
|
+
path.join(packageDir, '**', 'node_modules', '**'),
|
|
11
|
+
path.join(packageDir, '**', 'dist', '**'),
|
|
12
|
+
path.join(packageDir, '**', 'build', '**'),
|
|
13
|
+
path.join(packageDir, '**', 'coverage', '**'),
|
|
14
|
+
],
|
|
15
|
+
silent: false,
|
|
16
|
+
},
|
|
17
|
+
define: {},
|
|
18
|
+
resolve: {
|
|
19
|
+
alias: {
|
|
20
|
+
'@': path.join(packageDir, 'src'),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
});
|