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