@quiesco/web 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +17 -2
- package/dist/expo-server.d.ts +17 -0
- package/dist/expo-server.d.ts.map +1 -0
- package/dist/expo-server.js +81 -0
- package/dist/expo-server.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +138 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +618 -0
- package/dist/provider.js.map +1 -0
- package/dist/static-server.d.ts +8 -0
- package/dist/static-server.d.ts.map +1 -0
- package/dist/static-server.js +79 -0
- package/dist/static-server.js.map +1 -0
- package/package.json +57 -3
- package/src/expo-server.ts +96 -0
- package/src/index.ts +4 -0
- package/src/provider.ts +633 -0
- package/src/static-server.ts +85 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static file server for `npx expo export --platform web` output (requirements.md §6.8).
|
|
3
|
+
* Route resolution, in order: the file itself; `<path>.html` and `<path>/index.html` (Expo Router
|
|
4
|
+
* static rendering writes one HTML file per route); otherwise `index.html` (SPA fallback).
|
|
5
|
+
* Requests that resolve outside the root directory are rejected.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import type { AddressInfo } from 'node:net';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const TYPES: Record<string, string> = {
|
|
13
|
+
'.html': 'text/html; charset=utf-8',
|
|
14
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
15
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
16
|
+
'.css': 'text/css; charset=utf-8',
|
|
17
|
+
'.json': 'application/json',
|
|
18
|
+
'.map': 'application/json',
|
|
19
|
+
'.png': 'image/png',
|
|
20
|
+
'.jpg': 'image/jpeg',
|
|
21
|
+
'.svg': 'image/svg+xml',
|
|
22
|
+
'.ico': 'image/x-icon',
|
|
23
|
+
'.ttf': 'font/ttf',
|
|
24
|
+
'.woff': 'font/woff',
|
|
25
|
+
'.woff2': 'font/woff2',
|
|
26
|
+
'.wasm': 'application/wasm',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface StaticServer {
|
|
30
|
+
url: string;
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isFile(p: string): boolean {
|
|
35
|
+
try {
|
|
36
|
+
return fs.statSync(p).isFile();
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** File to serve for a URL path, or null when the request escapes the root. */
|
|
43
|
+
export function resolveRequest(root: string, urlPath: string): string | null {
|
|
44
|
+
let decoded: string;
|
|
45
|
+
try {
|
|
46
|
+
decoded = decodeURIComponent(urlPath.split('?')[0] ?? '/');
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const target = path.resolve(root, `.${path.posix.normalize(`/${decoded}`)}`);
|
|
51
|
+
if (target !== root && !target.startsWith(root + path.sep)) return null;
|
|
52
|
+
for (const candidate of [target, `${target}.html`, path.join(target, 'index.html')]) {
|
|
53
|
+
if (isFile(candidate)) return candidate;
|
|
54
|
+
}
|
|
55
|
+
return path.join(root, 'index.html');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function startStaticServer(dir: string, port = 0): Promise<StaticServer> {
|
|
59
|
+
const root = path.resolve(dir);
|
|
60
|
+
if (!isFile(path.join(root, 'index.html'))) {
|
|
61
|
+
throw new Error(`${root} has no index.html; build it with \`EXPO_PUBLIC_QUIESCO=1 npx expo export --platform web\``);
|
|
62
|
+
}
|
|
63
|
+
const server = http.createServer((req, res) => {
|
|
64
|
+
const file = resolveRequest(root, req.url ?? '/');
|
|
65
|
+
if (file === null || !isFile(file)) {
|
|
66
|
+
res.writeHead(file === null ? 403 : 404).end();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
res.writeHead(200, { 'Content-Type': TYPES[path.extname(file)] ?? 'application/octet-stream', 'Cache-Control': 'no-store' });
|
|
70
|
+
fs.createReadStream(file).pipe(res);
|
|
71
|
+
});
|
|
72
|
+
await new Promise<void>((resolve, reject) => {
|
|
73
|
+
server.once('error', reject);
|
|
74
|
+
server.listen(port, '127.0.0.1', resolve);
|
|
75
|
+
});
|
|
76
|
+
const { port: actual } = server.address() as AddressInfo;
|
|
77
|
+
return {
|
|
78
|
+
url: `http://localhost:${actual}`,
|
|
79
|
+
close: () =>
|
|
80
|
+
new Promise((resolve) => {
|
|
81
|
+
server.closeAllConnections();
|
|
82
|
+
server.close(() => resolve());
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
}
|