@eighty4/dank 0.0.2 → 0.0.4-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 +7 -0
- package/client/esbuild.js +8 -5
- package/lib/bin.ts +3 -3
- package/lib/build.ts +128 -72
- package/lib/{tag.ts → build_tag.ts} +3 -3
- package/lib/dank.ts +147 -11
- package/lib/define.ts +4 -5
- package/lib/esbuild.ts +192 -41
- package/lib/flags.ts +149 -7
- package/lib/html.ts +325 -117
- package/lib/http.ts +208 -47
- package/lib/metadata.ts +284 -0
- package/lib/public.ts +21 -13
- package/lib/serve.ts +204 -144
- package/lib/services.ts +28 -4
- package/lib_js/bin.js +3 -3
- package/lib_js/build.js +82 -57
- package/lib_js/{tag.js → build_tag.js} +2 -3
- package/lib_js/dank.js +73 -5
- package/lib_js/define.js +3 -5
- package/lib_js/esbuild.js +139 -34
- package/lib_js/flags.js +118 -6
- package/lib_js/html.js +203 -88
- package/lib_js/http.js +121 -29
- package/lib_js/metadata.js +198 -0
- package/lib_js/public.js +19 -11
- package/lib_js/serve.js +135 -110
- package/lib_js/services.js +13 -2
- package/lib_types/dank.d.ts +18 -1
- package/package.json +9 -1
- package/lib/manifest.ts +0 -61
- package/lib_js/manifest.js +0 -37
package/lib_js/html.js
CHANGED
|
@@ -1,143 +1,258 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
3
4
|
import { extname } from 'node:path/posix';
|
|
4
5
|
import { defaultTreeAdapter, parse, parseFragment, serialize, } from 'parse5';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
static async readFrom(urlPath, fsPath) {
|
|
12
|
-
let html;
|
|
13
|
-
try {
|
|
14
|
-
html = await readFile(fsPath, 'utf-8');
|
|
15
|
-
}
|
|
16
|
-
catch (e) {
|
|
17
|
-
console.log(`\u001b[31merror:\u001b[0m`, fsPath, 'does not exist');
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
return new HtmlEntrypoint(urlPath, html, fsPath);
|
|
21
|
-
}
|
|
22
|
-
#document;
|
|
6
|
+
export class HtmlEntrypoint extends EventEmitter {
|
|
7
|
+
#build;
|
|
8
|
+
#decorations;
|
|
9
|
+
#document = defaultTreeAdapter.createDocument();
|
|
10
|
+
// todo cache entrypoints set for quicker diffing
|
|
11
|
+
// #entrypoints: Set<string> = new Set()
|
|
23
12
|
#fsPath;
|
|
24
13
|
#partials = [];
|
|
25
14
|
#scripts = [];
|
|
15
|
+
#update = Object();
|
|
26
16
|
#url;
|
|
27
|
-
constructor(url,
|
|
17
|
+
constructor(build, url, fsPath, decorations) {
|
|
18
|
+
super({ captureRejections: true });
|
|
19
|
+
this.#build = build;
|
|
20
|
+
this.#decorations = decorations;
|
|
28
21
|
this.#url = url;
|
|
29
|
-
this.#document = parse(html);
|
|
30
22
|
this.#fsPath = fsPath;
|
|
23
|
+
this.on('change', this.#onChange);
|
|
24
|
+
this.emit('change');
|
|
31
25
|
}
|
|
32
|
-
|
|
33
|
-
this.#
|
|
34
|
-
await this.#injectPartials();
|
|
26
|
+
get fsPath() {
|
|
27
|
+
return this.#fsPath;
|
|
35
28
|
}
|
|
36
|
-
|
|
37
|
-
this.#
|
|
38
|
-
return this.#scripts;
|
|
29
|
+
get url() {
|
|
30
|
+
return this.#url;
|
|
39
31
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
async #html() {
|
|
33
|
+
try {
|
|
34
|
+
return await readFile(join(this.#build.dirs.pages, this.#fsPath), 'utf8');
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
// todo error handling
|
|
38
|
+
errorExit(this.#fsPath + ' does not exist');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// todo if partial changes, hot swap content in page
|
|
42
|
+
#onChange = async (_partial) => {
|
|
43
|
+
const update = (this.#update = Object());
|
|
44
|
+
const html = await this.#html();
|
|
45
|
+
const document = parse(html);
|
|
46
|
+
const imports = {
|
|
47
|
+
partials: [],
|
|
48
|
+
scripts: [],
|
|
49
|
+
};
|
|
50
|
+
this.#collectImports(document, imports);
|
|
51
|
+
const partials = await this.#resolvePartialContent(imports.partials);
|
|
52
|
+
if (update !== this.#update) {
|
|
53
|
+
// another update has started so aborting this one
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.#addDecorations(document);
|
|
57
|
+
this.#update = update;
|
|
58
|
+
this.#document = document;
|
|
59
|
+
this.#partials = partials;
|
|
60
|
+
this.#scripts = imports.scripts;
|
|
61
|
+
const entrypoints = mergeEntrypoints(imports, ...partials.map(p => p.imports));
|
|
62
|
+
// this.#entrypoints = new Set(entrypoints.map(entrypoint => entrypoint.in))
|
|
63
|
+
this.emit('entrypoints', entrypoints);
|
|
64
|
+
this.emit('partials', this.#partials.map(p => p.fsPath));
|
|
65
|
+
if (this.listenerCount('output')) {
|
|
66
|
+
this.emit('output', this.output());
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
// Emits `partial` on detecting a partial reference for `dank serve` file watches
|
|
70
|
+
// to respond to dependent changes
|
|
71
|
+
// todo safeguard recursive partials that cause circular imports
|
|
72
|
+
async #resolvePartialContent(partials) {
|
|
73
|
+
return await Promise.all(partials.map(async (p) => {
|
|
74
|
+
this.emit('partial', p.fsPath);
|
|
75
|
+
const html = await readFile(join(this.#build.dirs.pages, p.fsPath), 'utf8');
|
|
76
|
+
const fragment = parseFragment(html);
|
|
77
|
+
const imports = {
|
|
78
|
+
partials: [],
|
|
79
|
+
scripts: [],
|
|
80
|
+
};
|
|
81
|
+
this.#collectImports(fragment, imports, node => {
|
|
82
|
+
this.#rewritePartialRelativePaths(node, p.fsPath);
|
|
83
|
+
});
|
|
84
|
+
if (imports.partials.length) {
|
|
85
|
+
// todo recursive partials?
|
|
86
|
+
// await this.#resolvePartialContent(imports.partials)
|
|
87
|
+
errorExit(`partial ${p.fsPath} cannot recursively import partials`);
|
|
88
|
+
}
|
|
89
|
+
const content = {
|
|
90
|
+
...p,
|
|
91
|
+
fragment,
|
|
92
|
+
imports,
|
|
93
|
+
};
|
|
94
|
+
return content;
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
// rewrite hrefs in a partial to be relative to the html entrypoint instead of the partial
|
|
98
|
+
#rewritePartialRelativePaths(elem, partialPath) {
|
|
99
|
+
let rewritePath = null;
|
|
100
|
+
if (elem.nodeName === 'script') {
|
|
101
|
+
rewritePath = 'src';
|
|
102
|
+
}
|
|
103
|
+
else if (elem.nodeName === 'link' &&
|
|
104
|
+
hasAttr(elem, 'rel', 'stylesheet')) {
|
|
105
|
+
rewritePath = 'href';
|
|
106
|
+
}
|
|
107
|
+
if (rewritePath !== null) {
|
|
108
|
+
const attr = getAttr(elem, rewritePath);
|
|
109
|
+
if (attr) {
|
|
110
|
+
attr.value = join(relative(dirname(this.#fsPath), dirname(partialPath)), attr.value);
|
|
50
111
|
}
|
|
51
|
-
|
|
52
|
-
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
#addDecorations(document) {
|
|
115
|
+
if (!this.#decorations?.length) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const decoration of this.#decorations) {
|
|
119
|
+
switch (decoration.type) {
|
|
120
|
+
case 'script':
|
|
121
|
+
const scriptNode = parseFragment(`<script type="module">${decoration.js}</script>`).childNodes[0];
|
|
122
|
+
const htmlNode = document.childNodes.find(node => node.nodeName === 'html');
|
|
123
|
+
const headNode = htmlNode.childNodes.find(node => node.nodeName === 'head');
|
|
124
|
+
defaultTreeAdapter.appendChild(headNode || htmlNode, scriptNode);
|
|
125
|
+
break;
|
|
53
126
|
}
|
|
54
127
|
}
|
|
55
128
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
defaultTreeAdapter.appendChild(headNode || htmlNode, scriptNode);
|
|
129
|
+
output(hrefs) {
|
|
130
|
+
this.#injectPartials();
|
|
131
|
+
this.#rewriteHrefs(hrefs);
|
|
132
|
+
return serialize(this.#document);
|
|
61
133
|
}
|
|
62
|
-
|
|
63
|
-
|
|
134
|
+
// rewrites hrefs to content hashed urls
|
|
135
|
+
// call without hrefs to rewrite tsx? ext to js
|
|
136
|
+
#rewriteHrefs(hrefs) {
|
|
137
|
+
rewriteHrefs(this.#scripts, hrefs);
|
|
138
|
+
for (const partial of this.#partials) {
|
|
139
|
+
rewriteHrefs(partial.imports.scripts, hrefs);
|
|
140
|
+
}
|
|
64
141
|
}
|
|
65
142
|
async #injectPartials() {
|
|
66
|
-
for (const commentNode of this.#partials) {
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
const fragment = parseFragment(await readFile(pp, 'utf-8'));
|
|
143
|
+
for (const { commentNode, fragment } of this.#partials) {
|
|
144
|
+
if (!this.#build.production) {
|
|
145
|
+
defaultTreeAdapter.insertBefore(commentNode.parentNode, defaultTreeAdapter.createCommentNode(commentNode.data), commentNode);
|
|
146
|
+
}
|
|
71
147
|
for (const node of fragment.childNodes) {
|
|
72
|
-
if (node.nodeName === 'script') {
|
|
73
|
-
this.#rewritePathFromPartial(pp, node, 'src');
|
|
74
|
-
}
|
|
75
|
-
else if (node.nodeName === 'link' &&
|
|
76
|
-
hasAttr(node, 'rel', 'stylesheet')) {
|
|
77
|
-
this.#rewritePathFromPartial(pp, node, 'href');
|
|
78
|
-
}
|
|
79
148
|
defaultTreeAdapter.insertBefore(commentNode.parentNode, node, commentNode);
|
|
80
149
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// rewrite a ts or css href relative to an html partial to be relative to the html entrypoint
|
|
85
|
-
#rewritePathFromPartial(pp, elem, attrName) {
|
|
86
|
-
const attr = getAttr(elem, attrName);
|
|
87
|
-
if (attr) {
|
|
88
|
-
attr.value = join(relative(dirname(this.#fsPath), dirname(pp)), attr.value);
|
|
150
|
+
if (this.#build.production) {
|
|
151
|
+
defaultTreeAdapter.detachNode(commentNode);
|
|
152
|
+
}
|
|
89
153
|
}
|
|
90
154
|
}
|
|
91
|
-
#
|
|
155
|
+
#collectImports(node, collection, forEach) {
|
|
92
156
|
for (const childNode of node.childNodes) {
|
|
157
|
+
if (forEach && 'tagName' in childNode) {
|
|
158
|
+
forEach(childNode);
|
|
159
|
+
}
|
|
93
160
|
if (childNode.nodeName === '#comment' && 'data' in childNode) {
|
|
94
|
-
|
|
95
|
-
|
|
161
|
+
const partialMatch = childNode.data.match(/\{\{(?<pp>.+)\}\}/);
|
|
162
|
+
if (partialMatch) {
|
|
163
|
+
const partialSpecifier = partialMatch.groups.pp.trim();
|
|
164
|
+
if (partialSpecifier.startsWith('/')) {
|
|
165
|
+
errorExit(`partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be an absolute path`);
|
|
166
|
+
}
|
|
167
|
+
const partialPath = join(dirname(this.#fsPath), partialSpecifier);
|
|
168
|
+
if (!isPagesSubpathInPagesDir(this.#build, partialPath)) {
|
|
169
|
+
errorExit(`partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be outside of the pages directory`);
|
|
170
|
+
}
|
|
171
|
+
collection.partials.push({
|
|
172
|
+
fsPath: partialPath,
|
|
173
|
+
commentNode: childNode,
|
|
174
|
+
});
|
|
96
175
|
}
|
|
97
176
|
}
|
|
98
|
-
else if ('
|
|
99
|
-
this.#collectPartials(childNode);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
#collectScripts(node) {
|
|
104
|
-
for (const childNode of node.childNodes) {
|
|
105
|
-
if (childNode.nodeName === 'script') {
|
|
177
|
+
else if (childNode.nodeName === 'script') {
|
|
106
178
|
const srcAttr = childNode.attrs.find(attr => attr.name === 'src');
|
|
107
179
|
if (srcAttr) {
|
|
108
|
-
this.#
|
|
180
|
+
collection.scripts.push(this.#parseImport('script', srcAttr.value, childNode));
|
|
109
181
|
}
|
|
110
182
|
}
|
|
111
183
|
else if (childNode.nodeName === 'link' &&
|
|
112
184
|
hasAttr(childNode, 'rel', 'stylesheet')) {
|
|
113
185
|
const hrefAttr = getAttr(childNode, 'href');
|
|
114
186
|
if (hrefAttr) {
|
|
115
|
-
this.#
|
|
187
|
+
collection.scripts.push(this.#parseImport('style', hrefAttr.value, childNode));
|
|
116
188
|
}
|
|
117
189
|
}
|
|
118
190
|
else if ('childNodes' in childNode) {
|
|
119
|
-
this.#
|
|
191
|
+
this.#collectImports(childNode, collection);
|
|
120
192
|
}
|
|
121
193
|
}
|
|
122
194
|
}
|
|
123
|
-
#
|
|
124
|
-
const inPath = join(dirname(this.#fsPath), href);
|
|
125
|
-
|
|
195
|
+
#parseImport(type, href, elem) {
|
|
196
|
+
const inPath = join(this.#build.dirs.pages, dirname(this.#fsPath), href);
|
|
197
|
+
if (!isPathInPagesDir(this.#build, inPath)) {
|
|
198
|
+
errorExit(`href ${href} in webpage ${this.#fsPath} cannot reference sources outside of the pages directory`);
|
|
199
|
+
}
|
|
200
|
+
let outPath = join(dirname(this.#fsPath), href);
|
|
126
201
|
if (type === 'script' && !outPath.endsWith('.js')) {
|
|
127
202
|
outPath = outPath.replace(new RegExp(extname(outPath).substring(1) + '$'), 'js');
|
|
128
203
|
}
|
|
129
|
-
|
|
204
|
+
return {
|
|
130
205
|
type,
|
|
131
206
|
href,
|
|
132
207
|
elem,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
208
|
+
entrypoint: {
|
|
209
|
+
in: inPath,
|
|
210
|
+
out: outPath,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
136
213
|
}
|
|
137
214
|
}
|
|
215
|
+
// check if relative dir is a subpath of pages dir when joined with pages dir
|
|
216
|
+
// used if the joined pages dir path is only used for the pages dir check
|
|
217
|
+
function isPagesSubpathInPagesDir(build, subpath) {
|
|
218
|
+
return isPathInPagesDir(build, join(build.dirs.pages, subpath));
|
|
219
|
+
}
|
|
220
|
+
// check if subpath joined with pages dir is a subpath of pages dir
|
|
221
|
+
function isPathInPagesDir(build, p) {
|
|
222
|
+
return resolve(p).startsWith(build.dirs.pagesResolved);
|
|
223
|
+
}
|
|
138
224
|
function getAttr(elem, name) {
|
|
139
225
|
return elem.attrs.find(attr => attr.name === name);
|
|
140
226
|
}
|
|
141
227
|
function hasAttr(elem, name, value) {
|
|
142
228
|
return elem.attrs.some(attr => attr.name === name && attr.value === value);
|
|
143
229
|
}
|
|
230
|
+
function mergeEntrypoints(...imports) {
|
|
231
|
+
const entrypoints = [];
|
|
232
|
+
for (const { scripts } of imports) {
|
|
233
|
+
for (const script of scripts) {
|
|
234
|
+
entrypoints.push(script.entrypoint);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return entrypoints;
|
|
238
|
+
}
|
|
239
|
+
function rewriteHrefs(scripts, hrefs) {
|
|
240
|
+
for (const { elem, entrypoint, type } of scripts) {
|
|
241
|
+
const rewriteTo = hrefs ? hrefs.mappedHref(entrypoint.in) : null;
|
|
242
|
+
if (type === 'script') {
|
|
243
|
+
if (entrypoint.in.endsWith('.tsx') ||
|
|
244
|
+
entrypoint.in.endsWith('.ts')) {
|
|
245
|
+
elem.attrs.find(attr => attr.name === 'src').value =
|
|
246
|
+
rewriteTo || `/${entrypoint.out}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (type === 'style') {
|
|
250
|
+
elem.attrs.find(attr => attr.name === 'href').value =
|
|
251
|
+
rewriteTo || `/${entrypoint.out}`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function errorExit(msg) {
|
|
256
|
+
console.log(`\u001b[31merror:\u001b[0m`, msg);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
package/lib_js/http.js
CHANGED
|
@@ -2,59 +2,144 @@ import { createReadStream } from 'node:fs';
|
|
|
2
2
|
import { stat } from 'node:fs/promises';
|
|
3
3
|
import { createServer, } from 'node:http';
|
|
4
4
|
import { extname, join } from 'node:path';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
5
6
|
import mime from 'mime';
|
|
6
|
-
export function
|
|
7
|
-
const serverAddress = 'http://localhost:' +
|
|
8
|
-
|
|
7
|
+
export function startWebServer(serve, frontendFetcher, httpServices, pageRoutes) {
|
|
8
|
+
const serverAddress = 'http://localhost:' + serve.dankPort;
|
|
9
|
+
const handler = (req, res) => {
|
|
9
10
|
if (!req.url || !req.method) {
|
|
10
11
|
res.end();
|
|
11
12
|
}
|
|
12
13
|
else {
|
|
13
14
|
const url = new URL(serverAddress + req.url);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const headers = convertHeadersToFetch(req.headers);
|
|
16
|
+
frontendFetcher(url, headers, res, () => onNotFound(req, url, headers, httpServices, pageRoutes, serve, res));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
createServer(serve.logHttp ? createLogWrapper(handler) : handler).listen(serve.dankPort);
|
|
20
|
+
console.log(serve.preview ? 'preview' : 'dev', `server is live at http://127.0.0.1:${serve.dankPort}`);
|
|
21
|
+
}
|
|
22
|
+
async function onNotFound(req, url, headers, httpServices, pageRoutes, serve, res) {
|
|
23
|
+
if (req.method === 'GET' && extname(url.pathname) === '') {
|
|
24
|
+
const urlRewrite = tryUrlRewrites(url, pageRoutes, serve);
|
|
25
|
+
if (urlRewrite) {
|
|
26
|
+
streamFile(urlRewrite, res);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const fetchResponse = await tryHttpServices(req, url, headers, httpServices);
|
|
31
|
+
if (fetchResponse) {
|
|
32
|
+
sendFetchResponse(res, fetchResponse);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
res.writeHead(404);
|
|
36
|
+
res.end();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function sendFetchResponse(res, fetchResponse) {
|
|
40
|
+
res.writeHead(fetchResponse.status, undefined, convertHeadersFromFetch(fetchResponse.headers));
|
|
41
|
+
if (fetchResponse.body) {
|
|
42
|
+
Readable.fromWeb(fetchResponse.body).pipe(res);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
res.end();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function tryUrlRewrites(url, pageRoutes, serve) {
|
|
49
|
+
const urlRewrite = pageRoutes.urlRewrites.find(urlRewrite => urlRewrite.pattern.test(url.pathname));
|
|
50
|
+
return urlRewrite
|
|
51
|
+
? join(serve.dirs.buildWatch, urlRewrite.url, 'index.html')
|
|
52
|
+
: null;
|
|
53
|
+
}
|
|
54
|
+
async function tryHttpServices(req, url, headers, httpServices) {
|
|
55
|
+
if (url.pathname.startsWith('/.well-known/')) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const body = await collectReqBody(req);
|
|
59
|
+
const { running } = httpServices;
|
|
60
|
+
for (const httpService of running) {
|
|
61
|
+
const proxyUrl = new URL(url);
|
|
62
|
+
proxyUrl.port = `${httpService.port}`;
|
|
63
|
+
try {
|
|
64
|
+
const response = await retryFetchWithTimeout(proxyUrl, {
|
|
65
|
+
body,
|
|
66
|
+
headers,
|
|
67
|
+
method: req.method,
|
|
68
|
+
redirect: 'manual',
|
|
69
|
+
});
|
|
70
|
+
if (response.status === 404 || response.status === 405) {
|
|
71
|
+
continue;
|
|
17
72
|
}
|
|
18
73
|
else {
|
|
19
|
-
|
|
74
|
+
return response;
|
|
20
75
|
}
|
|
21
76
|
}
|
|
22
|
-
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (e === 'retrytimeout') {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
errorExit(`unexpected error http proxying to port ${httpService.port}: ${e.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
23
87
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
88
|
+
function collectReqBody(req) {
|
|
89
|
+
let body = '';
|
|
90
|
+
req.on('data', data => (body += data.toString()));
|
|
91
|
+
return new Promise(res => req.on('end', () => res(body.length ? body : null)));
|
|
92
|
+
}
|
|
93
|
+
function createLogWrapper(handler) {
|
|
94
|
+
return (req, res) => {
|
|
95
|
+
console.log(' > ', req.method, req.url);
|
|
96
|
+
res.on('close', () => {
|
|
97
|
+
console.log('', res.statusCode, req.method, req.url);
|
|
98
|
+
});
|
|
99
|
+
handler(req, res);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function createBuiltDistFilesFetcher(dir, manifest) {
|
|
103
|
+
return (url, _headers, res, notFound) => {
|
|
104
|
+
if (manifest.pageUrls.has(url.pathname)) {
|
|
105
|
+
streamFile(join(dir, url.pathname, 'index.html'), res);
|
|
106
|
+
}
|
|
107
|
+
else if (manifest.files.has(url.pathname)) {
|
|
108
|
+
streamFile(join(dir, url.pathname), res);
|
|
29
109
|
}
|
|
30
110
|
else {
|
|
31
|
-
|
|
32
|
-
? join(dir, url.pathname, 'index.html')
|
|
33
|
-
: join(dir, url.pathname);
|
|
34
|
-
streamFile(p, res);
|
|
111
|
+
notFound();
|
|
35
112
|
}
|
|
36
113
|
};
|
|
37
114
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
115
|
+
// todo replace PageRouteState with WebsiteRegistry
|
|
116
|
+
export function createDevServeFilesFetcher(pageRoutes, serve) {
|
|
117
|
+
const proxyAddress = 'http://127.0.0.1:' + serve.esbuildPort;
|
|
118
|
+
return (url, _headers, res, notFound) => {
|
|
119
|
+
if (pageRoutes.urls.includes(url.pathname)) {
|
|
120
|
+
streamFile(join(serve.dirs.buildWatch, url.pathname, 'index.html'), res);
|
|
43
121
|
}
|
|
44
122
|
else {
|
|
45
|
-
const maybePublicPath = join(
|
|
46
|
-
exists(
|
|
123
|
+
const maybePublicPath = join(serve.dirs.public, url.pathname);
|
|
124
|
+
exists(maybePublicPath).then(fromPublic => {
|
|
47
125
|
if (fromPublic) {
|
|
48
126
|
streamFile(maybePublicPath, res);
|
|
49
127
|
}
|
|
50
128
|
else {
|
|
51
129
|
retryFetchWithTimeout(proxyAddress + url.pathname)
|
|
52
130
|
.then(fetchResponse => {
|
|
53
|
-
|
|
54
|
-
|
|
131
|
+
if (fetchResponse.status === 404) {
|
|
132
|
+
notFound();
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
res.writeHead(fetchResponse.status, convertHeadersFromFetch(fetchResponse.headers));
|
|
136
|
+
fetchResponse
|
|
137
|
+
.bytes()
|
|
138
|
+
.then(data => res.end(data));
|
|
139
|
+
}
|
|
55
140
|
})
|
|
56
141
|
.catch(e => {
|
|
57
|
-
if (e
|
|
142
|
+
if (isFetchRetryTimeout(e)) {
|
|
58
143
|
res.writeHead(504);
|
|
59
144
|
}
|
|
60
145
|
else {
|
|
@@ -70,11 +155,11 @@ export function createDevServeFilesFetcher(opts) {
|
|
|
70
155
|
}
|
|
71
156
|
const PROXY_FETCH_RETRY_INTERVAL = 27;
|
|
72
157
|
const PROXY_FETCH_RETRY_TIMEOUT = 1000;
|
|
73
|
-
async function retryFetchWithTimeout(url) {
|
|
158
|
+
async function retryFetchWithTimeout(url, requestInit) {
|
|
74
159
|
let timeout = Date.now() + PROXY_FETCH_RETRY_TIMEOUT;
|
|
75
160
|
while (true) {
|
|
76
161
|
try {
|
|
77
|
-
return await fetch(url);
|
|
162
|
+
return await fetch(url, requestInit);
|
|
78
163
|
}
|
|
79
164
|
catch (e) {
|
|
80
165
|
if (isNodeFailedFetch(e) || isBunFailedFetch(e)) {
|
|
@@ -91,6 +176,9 @@ async function retryFetchWithTimeout(url) {
|
|
|
91
176
|
}
|
|
92
177
|
}
|
|
93
178
|
}
|
|
179
|
+
function isFetchRetryTimeout(e) {
|
|
180
|
+
return e === 'retrytimeout';
|
|
181
|
+
}
|
|
94
182
|
function isBunFailedFetch(e) {
|
|
95
183
|
return e.code === 'ConnectionRefused';
|
|
96
184
|
}
|
|
@@ -136,3 +224,7 @@ function convertHeadersToFetch(from) {
|
|
|
136
224
|
}
|
|
137
225
|
return to;
|
|
138
226
|
}
|
|
227
|
+
function errorExit(msg) {
|
|
228
|
+
console.log(`\u001b[31merror:\u001b[0m`, msg);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|