creevey 0.10.0-beta.44 → 0.10.0-beta.45
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/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/master/api.d.ts +5 -10
- package/dist/server/master/api.js +19 -18
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/handlers/capture-handler.d.ts +5 -2
- package/dist/server/master/handlers/capture-handler.js +6 -16
- package/dist/server/master/handlers/capture-handler.js.map +1 -1
- package/dist/server/master/handlers/ping-handler.d.ts +2 -2
- package/dist/server/master/handlers/ping-handler.js +2 -1
- package/dist/server/master/handlers/ping-handler.js.map +1 -1
- package/dist/server/master/handlers/static-handler.d.ts +1 -2
- package/dist/server/master/handlers/static-handler.js +10 -20
- package/dist/server/master/handlers/static-handler.js.map +1 -1
- package/dist/server/master/handlers/stories-handler.d.ts +4 -2
- package/dist/server/master/handlers/stories-handler.js +13 -27
- package/dist/server/master/handlers/stories-handler.js.map +1 -1
- package/dist/server/master/server.js +182 -70
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/playwright/docker-file.js +2 -2
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/package.json +3 -3
- package/src/server/index.ts +2 -2
- package/src/server/master/api.ts +24 -27
- package/src/server/master/handlers/capture-handler.ts +5 -24
- package/src/server/master/handlers/ping-handler.ts +4 -3
- package/src/server/master/handlers/static-handler.ts +10 -21
- package/src/server/master/handlers/stories-handler.ts +12 -40
- package/src/server/master/server.ts +194 -78
- package/src/server/playwright/docker-file.ts +2 -2
@@ -1,48 +1,20 @@
|
|
1
|
-
import { Request, Response } from 'hyper-express';
|
2
1
|
import cluster from 'cluster';
|
3
2
|
import { emitStoriesMessage, sendStoriesMessage } from '../../messages.js';
|
4
3
|
import { isDefined, StoryInput } from '../../../types.js';
|
5
4
|
import { deserializeStory } from '../../../shared/index.js';
|
6
5
|
|
7
|
-
export function
|
8
|
-
|
6
|
+
export function storiesHandler({ stories }: { stories: [string, StoryInput[]][] }): void {
|
7
|
+
const deserializedStories = stories.map<[string, StoryInput[]]>(([file, stories]) => [
|
8
|
+
file,
|
9
|
+
stories.map(deserializeStory),
|
10
|
+
]);
|
9
11
|
|
10
|
-
|
11
|
-
return async (request: Request, response: Response): Promise<void> => {
|
12
|
-
const { setStoriesCounter: counter, stories } = await request.json<
|
13
|
-
{
|
14
|
-
setStoriesCounter: number;
|
15
|
-
stories: [string, StoryInput[]][];
|
16
|
-
},
|
17
|
-
{
|
18
|
-
setStoriesCounter: number;
|
19
|
-
stories: [string, StoryInput[]][];
|
20
|
-
}
|
21
|
-
>({
|
22
|
-
setStoriesCounter: 0,
|
23
|
-
stories: [],
|
24
|
-
});
|
25
|
-
|
26
|
-
if (setStoriesCounter >= counter) {
|
27
|
-
response.send();
|
28
|
-
return;
|
29
|
-
}
|
30
|
-
|
31
|
-
const deserializedStories = stories.map<[string, StoryInput[]]>(([file, stories]) => [
|
32
|
-
file,
|
33
|
-
stories.map(deserializeStory),
|
34
|
-
]);
|
12
|
+
emitStoriesMessage({ type: 'update', payload: deserializedStories });
|
35
13
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
.forEach((worker) => {
|
43
|
-
sendStoriesMessage(worker, { type: 'update', payload: deserializedStories });
|
44
|
-
});
|
45
|
-
|
46
|
-
response.send();
|
47
|
-
};
|
14
|
+
Object.values(cluster.workers ?? {})
|
15
|
+
.filter(isDefined)
|
16
|
+
.filter((worker) => worker.isConnected())
|
17
|
+
.forEach((worker) => {
|
18
|
+
sendStoriesMessage(worker, { type: 'update', payload: deserializedStories });
|
19
|
+
});
|
48
20
|
}
|
@@ -1,121 +1,237 @@
|
|
1
|
+
import fs from 'fs';
|
2
|
+
import url from 'url';
|
1
3
|
import path from 'path';
|
2
|
-
import
|
4
|
+
import { IncomingMessage, ServerResponse, createServer } from 'http';
|
5
|
+
import { WebSocketServer, WebSocket, RawData } from 'ws';
|
3
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
4
|
-
import {
|
7
|
+
import { shutdownOnException } from '../utils.js';
|
5
8
|
import { subscribeOn } from '../messages.js';
|
6
9
|
import { noop } from '../../types.js';
|
7
10
|
import { logger } from '../logger.js';
|
8
|
-
import {
|
11
|
+
import { CreeveyApi } from './api.js';
|
12
|
+
import { pingHandler, captureHandler, storiesHandler, staticHandler } from './handlers/index.js';
|
9
13
|
|
10
|
-
|
14
|
+
function json<T = unknown>(
|
15
|
+
handler: (data: T) => void,
|
16
|
+
defaultValue: T,
|
17
|
+
): (request: IncomingMessage, response: ServerResponse) => void {
|
18
|
+
return (request: IncomingMessage, response: ServerResponse) => {
|
19
|
+
const chunks: Buffer[] = [];
|
11
20
|
|
12
|
-
|
13
|
-
|
14
|
-
|
21
|
+
request.on('data', (chunk: Buffer) => {
|
22
|
+
chunks.push(chunk);
|
23
|
+
});
|
15
24
|
|
16
|
-
|
17
|
-
|
25
|
+
request.on('end', () => {
|
26
|
+
try {
|
27
|
+
const body = Buffer.concat(chunks);
|
28
|
+
const value = body.length === 0 ? defaultValue : (JSON.parse(body.toString('utf-8')) as T);
|
18
29
|
|
19
|
-
|
20
|
-
|
30
|
+
handler(value);
|
31
|
+
response.end();
|
32
|
+
} catch (error) {
|
33
|
+
logger().error('Failed to parse JSON', error);
|
34
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
35
|
+
response.statusCode = 500;
|
36
|
+
response.setHeader('Content-Type', 'text/plain');
|
37
|
+
response.end(`Failed to parse JSON: ${errorMessage}`);
|
38
|
+
}
|
39
|
+
});
|
21
40
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
41
|
+
request.on('error', (error) => {
|
42
|
+
logger().error('Failed to parse JSON', error);
|
43
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
44
|
+
response.statusCode = 500;
|
45
|
+
response.setHeader('Content-Type', 'text/plain');
|
46
|
+
response.end(`Failed to parse JSON: ${errorMessage}`);
|
47
|
+
});
|
48
|
+
};
|
49
|
+
}
|
27
50
|
|
28
|
-
|
29
|
-
|
51
|
+
function file(handler: (requestedPath: string) => string | undefined) {
|
52
|
+
return (request: IncomingMessage, response: ServerResponse) => {
|
53
|
+
const parsedUrl = url.parse(request.url ?? '/', true);
|
54
|
+
const requestedPath = parsedUrl.pathname ?? '/';
|
55
|
+
|
56
|
+
try {
|
57
|
+
const filePath = handler(requestedPath);
|
58
|
+
if (filePath) {
|
59
|
+
const stat = fs.statSync(filePath);
|
60
|
+
// Set appropriate MIME type
|
61
|
+
const ext = path.extname(filePath).toLowerCase();
|
62
|
+
const mimeTypes: Record<string, string> = {
|
63
|
+
'.html': 'text/html',
|
64
|
+
'.js': 'application/javascript',
|
65
|
+
'.css': 'text/css',
|
66
|
+
'.json': 'application/json',
|
67
|
+
'.png': 'image/png',
|
68
|
+
'.jpg': 'image/jpeg',
|
69
|
+
'.jpeg': 'image/jpeg',
|
70
|
+
'.gif': 'image/gif',
|
71
|
+
'.svg': 'image/svg+xml',
|
72
|
+
'.ico': 'image/x-icon',
|
73
|
+
};
|
74
|
+
|
75
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
76
|
+
|
77
|
+
response.statusCode = 200;
|
78
|
+
response.setHeader('Content-Type', contentType);
|
79
|
+
response.setHeader('Content-Length', stat.size);
|
80
|
+
|
81
|
+
// Stream the file
|
82
|
+
const stream = fs.createReadStream(filePath);
|
83
|
+
stream.pipe(response);
|
84
|
+
|
85
|
+
stream.on('error', (error) => {
|
86
|
+
logger().error('Error streaming file', error);
|
87
|
+
if (!response.headersSent) {
|
88
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
89
|
+
response.statusCode = 500;
|
90
|
+
response.setHeader('Content-Type', 'text/plain');
|
91
|
+
response.end(`Internal server error: ${errorMessage}`);
|
92
|
+
}
|
93
|
+
});
|
94
|
+
} else {
|
95
|
+
logger().error('File not found', requestedPath);
|
96
|
+
response.statusCode = 404;
|
97
|
+
response.setHeader('Content-Type', 'text/plain');
|
98
|
+
response.end('File not found');
|
99
|
+
}
|
100
|
+
} catch (error) {
|
101
|
+
logger().error('Failed to serve file', error);
|
102
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
103
|
+
response.statusCode = 500;
|
104
|
+
response.setHeader('Content-Type', 'text/plain');
|
105
|
+
response.end(`Failed to serve file: ${errorMessage}`);
|
30
106
|
}
|
107
|
+
};
|
108
|
+
}
|
31
109
|
|
32
|
-
|
33
|
-
});
|
34
|
-
|
35
|
-
// Health check endpoint
|
36
|
-
server.get('/ping', pingHandler);
|
37
|
-
|
38
|
-
// Stories endpoint
|
39
|
-
server.post('/stories', createStoriesHandler());
|
40
|
-
|
41
|
-
// Capture endpoint
|
42
|
-
server.post('/capture', captureHandler);
|
110
|
+
const importMetaUrl = pathToFileURL(__filename).href;
|
43
111
|
|
44
|
-
|
45
|
-
|
112
|
+
export function start(reportDir: string, port: number, ui: boolean): (api: CreeveyApi) => void {
|
113
|
+
let wss: WebSocketServer | null = null;
|
114
|
+
let creeveyApi: CreeveyApi | null = null;
|
115
|
+
let resolveApi: (api: CreeveyApi) => void = noop;
|
46
116
|
|
47
|
-
// Serve static files
|
48
117
|
const webDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../client/web');
|
49
|
-
server
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
118
|
+
const server = createServer();
|
119
|
+
|
120
|
+
const routes = [
|
121
|
+
{
|
122
|
+
path: '/ping',
|
123
|
+
method: 'GET',
|
124
|
+
handler: pingHandler,
|
125
|
+
},
|
126
|
+
{
|
127
|
+
path: '/stories',
|
128
|
+
method: 'POST',
|
129
|
+
handler: json(storiesHandler, { stories: [] }),
|
130
|
+
},
|
131
|
+
{
|
132
|
+
path: '/capture',
|
133
|
+
method: 'POST',
|
134
|
+
handler: json(captureHandler, { workerId: 0, options: undefined }),
|
135
|
+
},
|
136
|
+
{
|
137
|
+
path: '/report/',
|
138
|
+
method: 'GET',
|
139
|
+
handler: file(staticHandler(reportDir, '/report/')),
|
140
|
+
},
|
141
|
+
{
|
142
|
+
path: '/',
|
143
|
+
method: 'GET',
|
144
|
+
handler: file(staticHandler(webDir)),
|
145
|
+
},
|
146
|
+
];
|
147
|
+
|
148
|
+
const router = (request: IncomingMessage, response: ServerResponse): void => {
|
149
|
+
const parsedUrl = url.parse(request.url ?? '/', true);
|
150
|
+
const path = parsedUrl.pathname ?? '/';
|
151
|
+
const method = request.method ?? 'GET';
|
152
|
+
|
153
|
+
try {
|
154
|
+
const route = routes.find((route) => path.startsWith(route.path) && route.method === method);
|
155
|
+
if (route) {
|
156
|
+
route.handler(request, response);
|
157
|
+
} else {
|
158
|
+
response.statusCode = 404;
|
159
|
+
response.setHeader('Content-Type', 'text/plain');
|
160
|
+
response.end('Not Found');
|
57
161
|
}
|
58
|
-
}
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
162
|
+
} catch (error) {
|
163
|
+
logger().error('Request handling error', error);
|
164
|
+
response.statusCode = 500;
|
165
|
+
response.setHeader('Content-Type', 'text/plain');
|
166
|
+
response.end('Internal Server Error');
|
167
|
+
}
|
168
|
+
};
|
65
169
|
|
66
|
-
|
170
|
+
server.on('request', (request: IncomingMessage, response: ServerResponse): void => {
|
171
|
+
response.setHeader('Access-Control-Allow-Origin', '*');
|
172
|
+
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
173
|
+
response.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
67
174
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
next();
|
74
|
-
});
|
175
|
+
if (request.method === 'OPTIONS') {
|
176
|
+
response.statusCode = 200;
|
177
|
+
response.end();
|
178
|
+
return;
|
179
|
+
}
|
75
180
|
|
76
|
-
|
77
|
-
|
78
|
-
// Add connection to the set of active connections
|
79
|
-
activeConnections.add(ws);
|
181
|
+
router(request, response);
|
182
|
+
});
|
80
183
|
|
81
|
-
|
82
|
-
|
83
|
-
|
184
|
+
if (ui) {
|
185
|
+
wss = new WebSocketServer({ server });
|
186
|
+
wss.on('connection', (ws: WebSocket) => {
|
187
|
+
ws.on('message', (message: RawData, isBinary: boolean) => {
|
188
|
+
if (creeveyApi) {
|
189
|
+
// NOTE Text messages are passed as Buffer https://github.com/websockets/ws/releases/tag/8.0.0
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
191
|
+
creeveyApi.handleMessage(ws, isBinary ? message : message.toString('utf-8'));
|
192
|
+
return;
|
193
|
+
}
|
84
194
|
});
|
85
195
|
|
86
|
-
|
87
|
-
|
88
|
-
activeConnections.delete(ws);
|
196
|
+
ws.on('error', (error) => {
|
197
|
+
logger().error('WebSocket error', error);
|
89
198
|
});
|
90
199
|
});
|
200
|
+
|
201
|
+
wss.on('error', (error) => {
|
202
|
+
logger().error('WebSocket error', error);
|
203
|
+
});
|
91
204
|
}
|
92
205
|
|
93
|
-
// Shutdown handling
|
94
206
|
subscribeOn('shutdown', () => {
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
logger().error('Error closing WebSocket connection', error);
|
101
|
-
}
|
207
|
+
if (wss) {
|
208
|
+
wss.clients.forEach((ws) => {
|
209
|
+
ws.close();
|
210
|
+
});
|
211
|
+
wss.close();
|
102
212
|
}
|
103
213
|
|
104
|
-
// Close the server
|
105
214
|
server.close();
|
106
215
|
});
|
107
216
|
|
108
|
-
// Start server
|
109
217
|
server
|
110
|
-
.listen(port)
|
111
|
-
.then(() => {
|
218
|
+
.listen(port, () => {
|
112
219
|
logger().info(`Server starting on port ${port}`);
|
113
220
|
})
|
114
|
-
.
|
221
|
+
.on('error', (error: unknown) => {
|
115
222
|
logger().error('Failed to start server', error);
|
116
223
|
process.exit(1);
|
117
224
|
});
|
118
225
|
|
226
|
+
void new Promise<CreeveyApi>((resolve) => (resolveApi = resolve))
|
227
|
+
.then((api) => {
|
228
|
+
creeveyApi = api;
|
229
|
+
if (wss) {
|
230
|
+
creeveyApi.subscribe(wss);
|
231
|
+
}
|
232
|
+
})
|
233
|
+
.catch(shutdownOnException);
|
234
|
+
|
119
235
|
// Return the function to resolve the API
|
120
236
|
return resolveApi;
|
121
237
|
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { readFile } from 'fs/promises';
|
2
2
|
import { pathToFileURL } from 'url';
|
3
3
|
import semver from 'semver';
|
4
|
-
import
|
4
|
+
import sh from 'shelljs';
|
5
5
|
|
6
6
|
const importMetaUrl = pathToFileURL(__filename).href;
|
7
7
|
|
@@ -11,7 +11,7 @@ export async function playwrightDockerFile(browser: string, version: string): Pr
|
|
11
11
|
|
12
12
|
let npmRegistry;
|
13
13
|
try {
|
14
|
-
npmRegistry = exec('npm config get registry', { silent: true }).stdout.trim();
|
14
|
+
npmRegistry = sh.exec('npm config get registry', { silent: true }).stdout.trim();
|
15
15
|
} catch {
|
16
16
|
/* noop */
|
17
17
|
}
|