Skip to content

Commit e67158b

Browse files
committed
Launch an HTTP server when testing starts
1 parent 58ddd2d commit e67158b

File tree

2 files changed

+193
-7
lines changed

2 files changed

+193
-7
lines changed

core/HttpServer.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
2+
namespace Moduless
3+
{
4+
const Http = require("http") as typeof import("http");
5+
const Path = require("path") as typeof import("path");
6+
const Fs = require("fs") as typeof import("fs");
7+
8+
/**
9+
* Launches a static HTTP file server at the specified path, from the specified port number.
10+
*/
11+
export function createServer(options: {
12+
path: string,
13+
port?: number,
14+
route?: (path: string) => string | Buffer | void
15+
})
16+
{
17+
const server = Http.createServer((req, res) =>
18+
{
19+
const path = req.url || "";
20+
const joined = Path.join(options.path, path);
21+
if (!Fs.existsSync(joined))
22+
{
23+
res.writeHead(404);
24+
res.end();
25+
return;
26+
}
27+
28+
let size = 0;
29+
let body: string | Buffer = "";
30+
31+
const maybeOverride = options.route?.(path);
32+
if (maybeOverride !== undefined)
33+
{
34+
size = maybeOverride.length;
35+
body = maybeOverride;
36+
}
37+
else
38+
{
39+
const stat = Fs.lstatSync(joined);
40+
size = stat.size;
41+
body = Fs.readFileSync(joined);
42+
}
43+
44+
const mimeType = MimeType.fromFileName(joined) || "text/html";
45+
46+
res.writeHead(200, {
47+
"Content-Length": size,
48+
"Content-Type": mimeType
49+
});
50+
51+
res.end(body);
52+
});
53+
54+
server.listen(options.port || defaultHttpPort);
55+
}
56+
57+
export const defaultHttpPort = 54321;
58+
59+
/** */
60+
export enum MimeType
61+
{
62+
unknown = "",
63+
64+
gif = "image/gif",
65+
jpg = "image/jpeg",
66+
png = "image/png",
67+
svg = "image/svg+xml",
68+
webp = "image/webp",
69+
avif = "image/avif",
70+
71+
// Videos
72+
mp4 = "video/mp4",
73+
webm = "video/av1",
74+
75+
// Zip
76+
zip = "application/zip",
77+
78+
// Text
79+
html = "text/html",
80+
css = "text/css",
81+
js = "text/javascript",
82+
ts = "text/plain",
83+
json = "application/json",
84+
map = "application/json",
85+
}
86+
87+
/** */
88+
export const enum MimeClass
89+
{
90+
other = "",
91+
image = "image",
92+
video = "video",
93+
}
94+
95+
/** */
96+
export namespace MimeType
97+
{
98+
const mimes: Map<string, string> = new Map(Object.entries(MimeType)
99+
.filter(([k, v]) => typeof v === "string") as [string, string][]);
100+
101+
/** */
102+
export function from(mimeString: string)
103+
{
104+
for (const mime of mimes.values())
105+
if (mime === mimeString)
106+
return mime as MimeType;
107+
108+
return null;
109+
}
110+
111+
/** */
112+
export function getExtension(mimeType: MimeType)
113+
{
114+
for (const [ext, mime] of mimes)
115+
if (mime === mimeType)
116+
return ext;
117+
118+
return "";
119+
}
120+
121+
/** */
122+
export function getClass(mimeType: string)
123+
{
124+
const [cls] = mimeType.split("/");
125+
switch (cls)
126+
{
127+
case MimeClass.image: return MimeClass.image;
128+
case MimeClass.video: return MimeClass.video;
129+
}
130+
131+
return MimeClass.other;
132+
}
133+
134+
/**
135+
* Parses the specified file name and returns the mime
136+
* type that is likely associated with it, based on the file extension.
137+
*/
138+
export function fromFileName(fileName: string)
139+
{
140+
const ext = fileName.split(".").slice(-1)[0] || "";
141+
return (mimes.get(ext) || "") as MimeType;
142+
}
143+
}
144+
}

core/Run.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,19 +93,21 @@ namespace Moduless
9393
if (ex && typeof ex === "object" && !Array.isArray(ex))
9494
out.push({ project, exported: ex });
9595

96+
const g = globalThis as any;
97+
9698
// Globalize the exports of the project.
9799
for (const [name, value] of Object.entries(ex))
98100
{
99-
if (name in globalThis)
100-
{
101+
if (!(name in g))
102+
g[name] = value;
103+
104+
else if (g[name]?.constructor === Object)
105+
Object.assign(g[name], value);
106+
107+
else
101108
console.warn(
102109
`Skipping adding ${name} from ${project.projectPath} to global scope ` +
103110
`because another member with this name is already defined globally.`);
104-
105-
continue;
106-
}
107-
108-
(globalThis as any)[name] = value;
109111
}
110112
}
111113
catch (e)
@@ -130,6 +132,8 @@ namespace Moduless
130132
if (!startingProject)
131133
throw new Error("No projects found at location: " + target.projectPath);
132134

135+
startHttpServer(graph.map(entry => entry.project));
136+
133137
const tryResolveNamepace = (root: object) =>
134138
{
135139
let current: any = root;
@@ -180,6 +184,44 @@ namespace Moduless
180184
return true;
181185
}
182186

187+
/**
188+
* Starts an HTTP server that serves the outFiles associated with the specified
189+
* list of projects.
190+
*/
191+
function startHttpServer(projects: Project[])
192+
{
193+
const outFiles = projects.map(p => p.outFile);
194+
let charIndex = 0;
195+
for (;;)
196+
{
197+
if (outFiles.some(f => f.length <= charIndex))
198+
break;
199+
200+
if (!outFiles.every(f => f[charIndex] === outFiles[0][charIndex]))
201+
break;
202+
203+
charIndex++;
204+
}
205+
206+
const path = outFiles[0].slice(0, charIndex);
207+
208+
Moduless.createServer({
209+
path,
210+
route: path =>
211+
{
212+
if (path === "/")
213+
{
214+
return [
215+
"<!DOCTYPE html>",
216+
...outFiles.map(f => `<script src="${f.slice(charIndex)}"></script>`),
217+
].join("\n");
218+
}
219+
}
220+
});
221+
222+
console.log("HTTP server is available at: http://localhost:" + Moduless.defaultHttpPort);
223+
}
224+
183225
/**
184226
* Returns the name of the cover function currently being tested.
185227
*/

0 commit comments

Comments
 (0)