Skip to content

Commit 089b972

Browse files
committed
Split out dev server implementation
1 parent f6ab8dd commit 089b972

File tree

2 files changed

+151
-69
lines changed

2 files changed

+151
-69
lines changed

packages/core/src/helpers/server.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import fs from 'fs';
2+
import http from 'http';
3+
import path from 'path';
4+
5+
const MIME_TYPES = {
6+
default: 'application/octet-stream',
7+
html: 'text/html; charset=UTF-8',
8+
js: 'application/javascript',
9+
css: 'text/css',
10+
png: 'image/png',
11+
jpg: 'image/jpg',
12+
gif: 'image/gif',
13+
ico: 'image/x-icon',
14+
svg: 'image/svg+xml',
15+
} as const;
16+
17+
type File = {
18+
found: boolean;
19+
ext: keyof typeof MIME_TYPES;
20+
content: string;
21+
};
22+
type RouteVerb = 'get' | 'post' | 'put' | 'patch' | 'delete';
23+
type Routes = Record<
24+
string,
25+
Record<RouteVerb, (req: http.IncomingMessage, res: http.ServerResponse) => void | Promise<void>>
26+
>;
27+
type Response = {
28+
statusCode: number;
29+
headers: Record<string, string>;
30+
body: string;
31+
error?: Error;
32+
};
33+
type RunServerOptions = {
34+
port: number;
35+
root: string;
36+
routes?: Routes;
37+
middleware?: (
38+
resp: Response,
39+
req: http.IncomingMessage,
40+
res: http.ServerResponse,
41+
) => Partial<Response> | Promise<Partial<Response>>;
42+
};
43+
44+
// Promise to boolean.
45+
const toBool = [() => true, () => false];
46+
47+
export const prepareFile = async (root: string, requestUrl: string): Promise<File> => {
48+
const staticPath = root
49+
? path.isAbsolute(root)
50+
? root
51+
: path.resolve(process.cwd(), root)
52+
: process.cwd();
53+
const url = new URL(requestUrl, 'http://127.0.0.1');
54+
const paths = [staticPath, url.pathname];
55+
56+
if (url.pathname.endsWith('/')) {
57+
paths.push('index.html');
58+
}
59+
60+
const filePath = path.join(...paths);
61+
const pathTraversal = !filePath.startsWith(staticPath);
62+
const exists = await fs.promises.access(filePath).then(...toBool);
63+
const found = !pathTraversal && exists;
64+
const ext = path.extname(filePath).substring(1).toLowerCase() as File['ext'];
65+
const fileContent = found ? await fs.promises.readFile(filePath, { encoding: 'utf-8' }) : '';
66+
67+
return { found, ext, content: fileContent };
68+
};
69+
70+
export const runServer = ({ port, root, middleware, routes }: RunServerOptions) => {
71+
const server = http.createServer(async (req, res) => {
72+
const response: Response = {
73+
statusCode: 200,
74+
headers: {},
75+
body: '',
76+
};
77+
78+
try {
79+
// Handle routes.
80+
const route = routes?.[req.url || '/'];
81+
if (route) {
82+
const verb = req.method?.toLowerCase() as RouteVerb;
83+
const handler = route[verb];
84+
if (handler) {
85+
// Hands off to the route handler.
86+
await handler(req, res);
87+
return;
88+
}
89+
}
90+
91+
// Fallback to files.
92+
const file = await prepareFile(root, req.url || '/');
93+
const statusCode = file.found ? 200 : 404;
94+
const mimeType = MIME_TYPES[file.ext] || MIME_TYPES.default;
95+
96+
response.statusCode = statusCode;
97+
response.headers['Content-Type'] = mimeType;
98+
response.body = file.content;
99+
} catch (e: any) {
100+
response.statusCode = 500;
101+
response.headers['Content-Type'] = MIME_TYPES.html;
102+
response.body = 'Internal Server Error';
103+
response.error = e;
104+
}
105+
106+
if (middleware) {
107+
const middlewareResponse = await middleware(response, req, res);
108+
response.statusCode = middlewareResponse.statusCode ?? response.statusCode;
109+
response.headers = {
110+
...response.headers,
111+
...(middlewareResponse.headers ?? {}),
112+
};
113+
response.body = middlewareResponse.body ?? response.body;
114+
}
115+
116+
res.writeHead(response.statusCode, response.headers);
117+
res.end(response.body);
118+
});
119+
120+
server.listen(port);
121+
return server;
122+
};

packages/tools/src/commands/dev-server/index.ts

Lines changed: 29 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,12 @@
33
// Copyright 2019-Present Datadog, Inc.
44

55
import { FULL_NAME_BUNDLERS } from '@dd/core/constants';
6+
import { runServer } from '@dd/core/helpers/server';
67
import { ROOT } from '@dd/tools/constants';
78
import chalk from 'chalk';
89
import { Command, Option } from 'clipanion';
9-
import fs from 'fs';
10-
import http from 'http';
10+
import type http from 'http';
1111
import template from 'lodash.template';
12-
import path from 'path';
13-
14-
const MIME_TYPES = {
15-
default: 'application/octet-stream',
16-
html: 'text/html; charset=UTF-8',
17-
js: 'application/javascript',
18-
css: 'text/css',
19-
png: 'image/png',
20-
jpg: 'image/jpg',
21-
gif: 'image/gif',
22-
ico: 'image/x-icon',
23-
svg: 'image/svg+xml',
24-
} as const;
2512

2613
// Some context to use for templating content with {{something}}.
2714
const CONTEXT: Record<string, readonly string[]> = {
@@ -31,15 +18,6 @@ const CONTEXT: Record<string, readonly string[]> = {
3118
// Templating regex.
3219
const INTERPOLATE_RX = /{{([\s\S]+?)}}/g;
3320

34-
// Promise to boolean.
35-
const toBool = [() => true, () => false];
36-
37-
type File = {
38-
found: boolean;
39-
ext: keyof typeof MIME_TYPES;
40-
content: string;
41-
};
42-
4321
class DevServer extends Command {
4422
static paths = [['dev-server']];
4523

@@ -115,57 +93,39 @@ class DevServer extends Command {
11593
return fileContext;
11694
}
11795

118-
async prepareFile(requestUrl: string, context: Record<string, string>): Promise<File> {
119-
const staticPath = this.root
120-
? path.isAbsolute(this.root)
121-
? this.root
122-
: path.resolve(ROOT, this.root)
123-
: ROOT;
124-
const url = new URL(requestUrl, 'http://127.0.0.1');
125-
const paths = [staticPath, url.pathname];
126-
127-
if (url.pathname.endsWith('/')) {
128-
paths.push('index.html');
129-
}
130-
131-
const filePath = path.join(...paths);
132-
const pathTraversal = !filePath.startsWith(staticPath);
133-
const exists = await fs.promises.access(filePath).then(...toBool);
134-
const found = !pathTraversal && exists;
135-
const finalPath = found ? filePath : `${staticPath}/404.html`;
136-
const ext = path.extname(finalPath).substring(1).toLowerCase() as File['ext'];
137-
const fileContent = template(await fs.promises.readFile(finalPath, { encoding: 'utf-8' }), {
138-
interpolate: INTERPOLATE_RX,
139-
})(context);
140-
141-
return { found, ext, content: fileContent };
142-
}
143-
14496
async execute() {
145-
http.createServer(async (req, res) => {
146-
try {
97+
runServer({
98+
port: +this.port,
99+
root: this.root,
100+
middleware: async (resp, req) => {
101+
const statusCode = resp.statusCode;
147102
const context = this.getContext(req);
148-
const file = await this.prepareFile(req.url || '/', context);
149-
const statusCode = file.found ? 200 : 404;
150-
const mimeType = MIME_TYPES[file.ext] || MIME_TYPES.default;
151-
const c = statusCode === 200 ? chalk.green : chalk.yellow.bold;
152-
153-
res.writeHead(statusCode, {
103+
const content = template(resp.body, {
104+
interpolate: INTERPOLATE_RX,
105+
})(context);
106+
const headers = {
154107
'Set-Cookie': `context_cookie=${encodeURIComponent(JSON.stringify(context))};SameSite=Strict;`,
155-
'Content-Type': mimeType,
156-
});
108+
};
157109

158-
res.end(file.content);
110+
const c =
111+
{
112+
200: chalk.green,
113+
404: chalk.yellow.bold,
114+
500: chalk.red.bold,
115+
}[statusCode] || chalk.white;
159116

160117
console.log(` -> [${c(statusCode.toString())}] ${req.method} ${req.url}`);
161-
} catch (e: any) {
162-
res.writeHead(500, { 'Content-Type': MIME_TYPES.html });
163-
res.end('Internal Server Error');
164-
const c = chalk.red.bold;
165-
console.log(` -> [${c('500')}] ${req.method} ${req.url}: ${e.message}`);
166-
console.log(e);
167-
}
168-
}).listen(this.port);
118+
if (resp.error) {
119+
console.log(resp.error);
120+
}
121+
122+
return {
123+
statusCode: resp.statusCode,
124+
headers,
125+
body: content,
126+
};
127+
},
128+
});
169129
console.log(`Server running at http://127.0.0.1:${this.port}/`);
170130
}
171131
}

0 commit comments

Comments
 (0)