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.
Files changed (30) hide show
  1. package/dist/server/index.js +2 -2
  2. package/dist/server/index.js.map +1 -1
  3. package/dist/server/master/api.d.ts +5 -10
  4. package/dist/server/master/api.js +19 -18
  5. package/dist/server/master/api.js.map +1 -1
  6. package/dist/server/master/handlers/capture-handler.d.ts +5 -2
  7. package/dist/server/master/handlers/capture-handler.js +6 -16
  8. package/dist/server/master/handlers/capture-handler.js.map +1 -1
  9. package/dist/server/master/handlers/ping-handler.d.ts +2 -2
  10. package/dist/server/master/handlers/ping-handler.js +2 -1
  11. package/dist/server/master/handlers/ping-handler.js.map +1 -1
  12. package/dist/server/master/handlers/static-handler.d.ts +1 -2
  13. package/dist/server/master/handlers/static-handler.js +10 -20
  14. package/dist/server/master/handlers/static-handler.js.map +1 -1
  15. package/dist/server/master/handlers/stories-handler.d.ts +4 -2
  16. package/dist/server/master/handlers/stories-handler.js +13 -27
  17. package/dist/server/master/handlers/stories-handler.js.map +1 -1
  18. package/dist/server/master/server.js +182 -70
  19. package/dist/server/master/server.js.map +1 -1
  20. package/dist/server/playwright/docker-file.js +2 -2
  21. package/dist/server/playwright/docker-file.js.map +1 -1
  22. package/package.json +3 -3
  23. package/src/server/index.ts +2 -2
  24. package/src/server/master/api.ts +24 -27
  25. package/src/server/master/handlers/capture-handler.ts +5 -24
  26. package/src/server/master/handlers/ping-handler.ts +4 -3
  27. package/src/server/master/handlers/static-handler.ts +10 -21
  28. package/src/server/master/handlers/stories-handler.ts +12 -40
  29. package/src/server/master/server.ts +194 -78
  30. 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 createStoriesHandler() {
8
- let setStoriesCounter = 0;
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
- // TODO We need this handler for getting stories updates from a browser
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
- setStoriesCounter = counter;
37
- emitStoriesMessage({ type: 'update', payload: deserializedStories });
38
-
39
- Object.values(cluster.workers ?? {})
40
- .filter(isDefined)
41
- .filter((worker) => worker.isConnected())
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 HyperExpress from 'hyper-express';
4
+ import { IncomingMessage, ServerResponse, createServer } from 'http';
5
+ import { WebSocketServer, WebSocket, RawData } from 'ws';
3
6
  import { fileURLToPath, pathToFileURL } from 'url';
4
- import { CreeveyApi } from './api.js';
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 { pingHandler, createStoriesHandler, captureHandler, createStaticFileHandler } from './handlers/index.js';
11
+ import { CreeveyApi } from './api.js';
12
+ import { pingHandler, captureHandler, storiesHandler, staticHandler } from './handlers/index.js';
9
13
 
10
- const importMetaUrl = pathToFileURL(__filename).href;
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
- export function start(reportDir: string, port: number, ui: boolean): (api: CreeveyApi) => void {
13
- let resolveApi: (api: CreeveyApi) => void = noop;
14
- const creeveyApi = new Promise<CreeveyApi>((resolve) => (resolveApi = resolve));
21
+ request.on('data', (chunk: Buffer) => {
22
+ chunks.push(chunk);
23
+ });
15
24
 
16
- // Create HyperExpress server instance
17
- const server = new HyperExpress.Server();
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
- // Store active WebSocket connections
20
- const activeConnections = new Set<HyperExpress.Websocket>();
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
- // Enable CORS for all routes
23
- server.use((request, response, next) => {
24
- response.header('Access-Control-Allow-Origin', '*');
25
- response.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
26
- response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
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
- if (request.method === 'OPTIONS') {
29
- return response.status(200).send();
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
- next();
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
- // Serve report files
45
- server.get('/report/*', createStaticFileHandler(reportDir, '/report/'));
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.get('/*', createStaticFileHandler(webDir));
50
-
51
- // If UI mode, wait for CreeveyApi to be resolved
52
- if (ui) {
53
- // Create a custom broadcast function that works with our connections
54
- const broadcast = (message: string) => {
55
- for (const connection of activeConnections) {
56
- connection.send(message);
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
- // Create a custom WebSocket server that simulates the standard behavior
61
- const customWsServer = {
62
- clients: activeConnections,
63
- publish: broadcast,
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
- let api: CreeveyApi | null = null;
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
- server.use(async (request, _response, next) => {
69
- if (!api && request.path === '/') {
70
- api = await creeveyApi;
71
- api.subscribe(customWsServer);
72
- }
73
- next();
74
- });
175
+ if (request.method === 'OPTIONS') {
176
+ response.statusCode = 200;
177
+ response.end();
178
+ return;
179
+ }
75
180
 
76
- // Create WebSocket listener
77
- server.ws('/', (ws) => {
78
- // Add connection to the set of active connections
79
- activeConnections.add(ws);
181
+ router(request, response);
182
+ });
80
183
 
81
- // Handle message events
82
- ws.on('message', (message: string | Buffer) => {
83
- api?.handleMessage(ws, message);
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
- // Handle close events to clean up connections
87
- ws.on('close', () => {
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
- // Close all WebSocket connections
96
- for (const connection of activeConnections) {
97
- try {
98
- connection.close();
99
- } catch (error) {
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
- .catch((error: unknown) => {
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 { exec } from 'shelljs';
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
  }