@eighty4/dank 0.0.4-2 → 0.0.4-3
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/client.js +1 -0
- package/lib/bin.ts +8 -11
- package/lib/build.ts +43 -70
- package/lib/build_tag.ts +3 -3
- package/lib/config.ts +184 -29
- package/lib/dank.ts +7 -0
- package/lib/define.ts +6 -4
- package/lib/developer.ts +33 -4
- package/lib/dirs.ts +83 -0
- package/lib/errors.ts +6 -0
- package/lib/esbuild.ts +19 -29
- package/lib/flags.ts +16 -122
- package/lib/html.ts +196 -112
- package/lib/http.ts +59 -43
- package/lib/public.ts +10 -10
- package/lib/{metadata.ts → registry.ts} +216 -83
- package/lib/serve.ts +101 -336
- package/lib/services.ts +8 -8
- package/lib/watch.ts +39 -0
- package/lib_js/bin.js +8 -10
- package/lib_js/build.js +31 -45
- package/lib_js/build_tag.js +2 -2
- package/lib_js/config.js +108 -29
- package/lib_js/define.js +3 -3
- package/lib_js/dirs.js +61 -0
- package/lib_js/errors.js +9 -0
- package/lib_js/esbuild.js +17 -19
- package/lib_js/flags.js +9 -98
- package/lib_js/html.js +127 -64
- package/lib_js/http.js +18 -18
- package/lib_js/public.js +9 -9
- package/lib_js/{metadata.js → registry.js} +121 -51
- package/lib_js/serve.js +53 -177
- package/lib_js/services.js +6 -6
- package/lib_js/watch.js +35 -0
- package/lib_types/dank.d.ts +5 -0
- package/package.json +6 -4
- package/client/esbuild.js +0 -1
package/lib_js/html.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { extname } from "node:path/posix";
|
|
5
5
|
import { defaultTreeAdapter, parse, parseFragment, serialize } from "parse5";
|
|
6
|
+
import { DankError } from "./errors.js";
|
|
6
7
|
class HtmlEntrypoint extends EventEmitter {
|
|
7
|
-
#
|
|
8
|
-
#
|
|
8
|
+
#c;
|
|
9
|
+
#clientJS;
|
|
9
10
|
#document = defaultTreeAdapter.createDocument();
|
|
10
|
-
|
|
11
|
-
// #entrypoints: Set<string> = new Set()
|
|
11
|
+
#entrypoints = /* @__PURE__ */ new Set();
|
|
12
12
|
// path within pages dir omitting pages/ segment
|
|
13
13
|
#fsPath;
|
|
14
14
|
#partials = [];
|
|
@@ -16,15 +16,14 @@ class HtmlEntrypoint extends EventEmitter {
|
|
|
16
16
|
#scripts = [];
|
|
17
17
|
#update = Object();
|
|
18
18
|
#url;
|
|
19
|
-
constructor(
|
|
19
|
+
constructor(c, resolver, url, fsPath) {
|
|
20
20
|
super({ captureRejections: true });
|
|
21
|
-
this.#
|
|
21
|
+
this.#c = c;
|
|
22
|
+
this.#clientJS = ClientJS.initialize(c);
|
|
22
23
|
this.#resolver = resolver;
|
|
23
|
-
this.#decorations = decorations;
|
|
24
24
|
this.#url = url;
|
|
25
25
|
this.#fsPath = fsPath;
|
|
26
26
|
this.on("change", this.#onChange);
|
|
27
|
-
this.emit("change");
|
|
28
27
|
}
|
|
29
28
|
get fsPath() {
|
|
30
29
|
return this.#fsPath;
|
|
@@ -32,46 +31,103 @@ class HtmlEntrypoint extends EventEmitter {
|
|
|
32
31
|
get url() {
|
|
33
32
|
return this.#url;
|
|
34
33
|
}
|
|
35
|
-
async
|
|
34
|
+
async process() {
|
|
35
|
+
await this.#onChange();
|
|
36
|
+
}
|
|
37
|
+
output(hrefs) {
|
|
38
|
+
this.#injectPartials();
|
|
39
|
+
this.#rewriteHrefs(hrefs);
|
|
40
|
+
return serialize(this.#document);
|
|
41
|
+
}
|
|
42
|
+
usesPartial(fsPath) {
|
|
43
|
+
return this.#partials.some((partial) => partial.fsPath === fsPath);
|
|
44
|
+
}
|
|
45
|
+
#onChange = async () => {
|
|
46
|
+
const update = this.#update = Object();
|
|
47
|
+
let html;
|
|
36
48
|
try {
|
|
37
|
-
|
|
49
|
+
html = await this.#readFromPages(this.#fsPath);
|
|
38
50
|
} catch (e) {
|
|
39
|
-
|
|
51
|
+
this.#error(`url \`${this.#url}\` html file \`${join(this.#c.dirs.pages, this.#fsPath)}\` does not exist`);
|
|
52
|
+
return;
|
|
40
53
|
}
|
|
41
|
-
}
|
|
42
|
-
// todo if partial changes, hot swap content in page
|
|
43
|
-
#onChange = async (_partial) => {
|
|
44
|
-
const update = this.#update = Object();
|
|
45
|
-
const html = await this.#html();
|
|
46
54
|
const document = parse(html);
|
|
47
55
|
const imports = {
|
|
48
56
|
partials: [],
|
|
49
57
|
scripts: []
|
|
50
58
|
};
|
|
59
|
+
let partials;
|
|
51
60
|
this.#collectImports(document, imports);
|
|
52
|
-
const
|
|
61
|
+
const partialResults = await this.#resolvePartialContent(imports.partials);
|
|
62
|
+
partials = partialResults.filter((p) => p !== false);
|
|
63
|
+
if (partials.length !== partialResults.length) {
|
|
64
|
+
this.#error(`update to \`${join(this.#c.dirs.pages, this.#fsPath)}\` did not update to \`${this.#url}\` because of unresolved partials`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (this.#clientJS !== null) {
|
|
68
|
+
const decoration = await this.#clientJS.retrieve(this.#c.esbuildPort);
|
|
69
|
+
this.#addScriptDecoration(document, decoration.js);
|
|
70
|
+
}
|
|
53
71
|
if (update !== this.#update) {
|
|
54
72
|
return;
|
|
55
73
|
}
|
|
56
|
-
this.#addDecorations(document);
|
|
57
|
-
this.#update = update;
|
|
58
74
|
this.#document = document;
|
|
59
75
|
this.#partials = partials;
|
|
60
76
|
this.#scripts = imports.scripts;
|
|
61
77
|
const entrypoints = mergeEntrypoints(imports, ...partials.map((p) => p.imports));
|
|
62
|
-
this
|
|
63
|
-
|
|
78
|
+
if (this.#haveEntrypointsChanged(entrypoints)) {
|
|
79
|
+
this.emit("entrypoints", entrypoints);
|
|
80
|
+
}
|
|
64
81
|
if (this.listenerCount("output")) {
|
|
65
82
|
this.emit("output", this.output());
|
|
66
83
|
}
|
|
67
84
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
#error(e) {
|
|
86
|
+
let message;
|
|
87
|
+
let error;
|
|
88
|
+
if (typeof e === "string") {
|
|
89
|
+
message = e;
|
|
90
|
+
error = new DankError(e);
|
|
91
|
+
} else {
|
|
92
|
+
message = e.message;
|
|
93
|
+
error = e;
|
|
94
|
+
}
|
|
95
|
+
if (this.listenerCount("error")) {
|
|
96
|
+
this.emit("error", error);
|
|
97
|
+
} else {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
#haveEntrypointsChanged(entrypoints) {
|
|
102
|
+
const set = new Set(entrypoints.map((entrypoint) => entrypoint.in));
|
|
103
|
+
const changed = set.symmetricDifference(this.#entrypoints).size > 0;
|
|
104
|
+
this.#entrypoints = set;
|
|
105
|
+
return changed;
|
|
106
|
+
}
|
|
107
|
+
async #readFromPages(p) {
|
|
108
|
+
return await readFile(this.#resolver.absPagesPath(p), "utf8");
|
|
109
|
+
}
|
|
71
110
|
async #resolvePartialContent(partials) {
|
|
72
111
|
return await Promise.all(partials.map(async (p) => {
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
let html;
|
|
113
|
+
if (isAbsolute(p.specifier)) {
|
|
114
|
+
this.#error(`partials cannot be referenced with an absolute path like \`${p.specifier}\` in webpage \`${join(this.#c.dirs.pages, this.#fsPath)}\``);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (!this.#resolver.isPagesSubpathInPagesDir(p.fsPath)) {
|
|
118
|
+
this.#error(`partials cannot be referenced from outside the pages dir like \`${p.specifier}\` in webpage \`${join(this.#c.dirs.pages, this.#fsPath)}\``);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (extname(p.fsPath) !== ".html") {
|
|
122
|
+
this.#error(`partial path \`${p.fsPath}\` referenced by \`${this.#fsPath}\` is not a valid partial path`);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
html = await this.#readFromPages(p.fsPath);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
this.#error(`partial \`${join("pages", p.fsPath)}\` imported by \`${join("pages", this.#fsPath)}\` does not exist`);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
75
131
|
const fragment = parseFragment(html);
|
|
76
132
|
const imports = {
|
|
77
133
|
partials: [],
|
|
@@ -81,7 +137,7 @@ class HtmlEntrypoint extends EventEmitter {
|
|
|
81
137
|
this.#rewritePartialRelativePaths(node, p.fsPath);
|
|
82
138
|
});
|
|
83
139
|
if (imports.partials.length) {
|
|
84
|
-
|
|
140
|
+
this.#error(`partials cannot import another partial like \`${join(this.#c.dirs.pages, p.fsPath)}\``);
|
|
85
141
|
}
|
|
86
142
|
const content = {
|
|
87
143
|
...p,
|
|
@@ -106,25 +162,11 @@ class HtmlEntrypoint extends EventEmitter {
|
|
|
106
162
|
}
|
|
107
163
|
}
|
|
108
164
|
}
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
switch (decoration.type) {
|
|
115
|
-
case "script":
|
|
116
|
-
const scriptNode = parseFragment(`<script type="module">${decoration.js}</script>`).childNodes[0];
|
|
117
|
-
const htmlNode = document.childNodes.find((node) => node.nodeName === "html");
|
|
118
|
-
const headNode = htmlNode.childNodes.find((node) => node.nodeName === "head");
|
|
119
|
-
defaultTreeAdapter.appendChild(headNode || htmlNode, scriptNode);
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
output(hrefs) {
|
|
125
|
-
this.#injectPartials();
|
|
126
|
-
this.#rewriteHrefs(hrefs);
|
|
127
|
-
return serialize(this.#document);
|
|
165
|
+
#addScriptDecoration(document, js) {
|
|
166
|
+
const scriptNode = parseFragment(`<script type="module">${js}</script>`).childNodes[0];
|
|
167
|
+
const htmlNode = document.childNodes.find((node) => node.nodeName === "html");
|
|
168
|
+
const headNode = htmlNode.childNodes.find((node) => node.nodeName === "head");
|
|
169
|
+
defaultTreeAdapter.appendChild(headNode || htmlNode, scriptNode);
|
|
128
170
|
}
|
|
129
171
|
// rewrites hrefs to content hashed urls
|
|
130
172
|
// call without hrefs to rewrite tsx? ext to js
|
|
@@ -136,13 +178,13 @@ class HtmlEntrypoint extends EventEmitter {
|
|
|
136
178
|
}
|
|
137
179
|
async #injectPartials() {
|
|
138
180
|
for (const { commentNode, fragment } of this.#partials) {
|
|
139
|
-
if (!this.#
|
|
181
|
+
if (!this.#c.flags.production) {
|
|
140
182
|
defaultTreeAdapter.insertBefore(commentNode.parentNode, defaultTreeAdapter.createCommentNode(commentNode.data), commentNode);
|
|
141
183
|
}
|
|
142
184
|
for (const node of fragment.childNodes) {
|
|
143
185
|
defaultTreeAdapter.insertBefore(commentNode.parentNode, node, commentNode);
|
|
144
186
|
}
|
|
145
|
-
if (this.#
|
|
187
|
+
if (this.#c.flags.production) {
|
|
146
188
|
defaultTreeAdapter.detachNode(commentNode);
|
|
147
189
|
}
|
|
148
190
|
}
|
|
@@ -155,17 +197,11 @@ class HtmlEntrypoint extends EventEmitter {
|
|
|
155
197
|
if (childNode.nodeName === "#comment" && "data" in childNode) {
|
|
156
198
|
const partialMatch = childNode.data.match(/\{\{(?<pp>.+)\}\}/);
|
|
157
199
|
if (partialMatch) {
|
|
158
|
-
const
|
|
159
|
-
if (partialSpecifier.startsWith("/")) {
|
|
160
|
-
errorExit(`partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be an absolute path`);
|
|
161
|
-
}
|
|
162
|
-
const partialPath = join(dirname(this.#fsPath), partialSpecifier);
|
|
163
|
-
if (!this.#resolver.isPagesSubpathInPagesDir(partialPath)) {
|
|
164
|
-
errorExit(`partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be outside of the pages directory`);
|
|
165
|
-
}
|
|
200
|
+
const specifier = partialMatch.groups.pp.trim();
|
|
166
201
|
collection.partials.push({
|
|
167
|
-
|
|
168
|
-
|
|
202
|
+
commentNode: childNode,
|
|
203
|
+
fsPath: join(dirname(this.#fsPath), specifier),
|
|
204
|
+
specifier
|
|
169
205
|
});
|
|
170
206
|
}
|
|
171
207
|
} else if (childNode.nodeName === "script") {
|
|
@@ -184,9 +220,9 @@ class HtmlEntrypoint extends EventEmitter {
|
|
|
184
220
|
}
|
|
185
221
|
}
|
|
186
222
|
#parseImport(type, href, elem) {
|
|
187
|
-
const inPath = join(this.#
|
|
223
|
+
const inPath = join(this.#c.dirs.pages, dirname(this.#fsPath), href);
|
|
188
224
|
if (!this.#resolver.isProjectSubpathInPagesDir(inPath)) {
|
|
189
|
-
|
|
225
|
+
throw new DankError(`href \`${href}\` in webpage \`${join(this.#c.dirs.pages, this.#fsPath)}\` cannot reference sources outside of the pages directory`);
|
|
190
226
|
}
|
|
191
227
|
let outPath = join(dirname(this.#fsPath), href);
|
|
192
228
|
if (type === "script" && !outPath.endsWith(".js")) {
|
|
@@ -230,9 +266,36 @@ function rewriteHrefs(scripts, hrefs) {
|
|
|
230
266
|
}
|
|
231
267
|
}
|
|
232
268
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
269
|
+
class ClientJS {
|
|
270
|
+
static #instance = null;
|
|
271
|
+
static initialize(c) {
|
|
272
|
+
if (c.mode === "build" || c.flags.preview) {
|
|
273
|
+
return null;
|
|
274
|
+
} else if (!ClientJS.#instance) {
|
|
275
|
+
ClientJS.#instance = new ClientJS(c.esbuildPort);
|
|
276
|
+
}
|
|
277
|
+
return ClientJS.#instance;
|
|
278
|
+
}
|
|
279
|
+
#esbuildPort;
|
|
280
|
+
#read;
|
|
281
|
+
#result;
|
|
282
|
+
constructor(esbuildPort) {
|
|
283
|
+
this.#esbuildPort = esbuildPort;
|
|
284
|
+
this.#read = readFile(resolve(import.meta.dirname, join("..", "client", "client.js")), "utf-8");
|
|
285
|
+
this.#result = this.#read.then(this.#transform);
|
|
286
|
+
}
|
|
287
|
+
async retrieve(esbuildPort) {
|
|
288
|
+
if (esbuildPort !== this.#esbuildPort) {
|
|
289
|
+
this.#result = this.#read.then(this.#transform);
|
|
290
|
+
}
|
|
291
|
+
return await this.#result;
|
|
292
|
+
}
|
|
293
|
+
#transform = (js) => {
|
|
294
|
+
return {
|
|
295
|
+
type: "script",
|
|
296
|
+
js: js.replace("3995", "" + this.#esbuildPort)
|
|
297
|
+
};
|
|
298
|
+
};
|
|
236
299
|
}
|
|
237
300
|
export {
|
|
238
301
|
HtmlEntrypoint
|
package/lib_js/http.js
CHANGED
|
@@ -4,23 +4,23 @@ import { createServer } from "node:http";
|
|
|
4
4
|
import { extname, join } from "node:path";
|
|
5
5
|
import { Readable } from "node:stream";
|
|
6
6
|
import mime from "mime";
|
|
7
|
-
function startWebServer(
|
|
8
|
-
const serverAddress = "http://localhost:" +
|
|
7
|
+
function startWebServer(port, flags, dirs, urlRewriteProvider, frontendFetcher, httpServices) {
|
|
8
|
+
const serverAddress = "http://localhost:" + port;
|
|
9
9
|
const handler = (req, res) => {
|
|
10
10
|
if (!req.url || !req.method) {
|
|
11
11
|
res.end();
|
|
12
12
|
} else {
|
|
13
13
|
const url = new URL(serverAddress + req.url);
|
|
14
14
|
const headers = convertHeadersToFetch(req.headers);
|
|
15
|
-
frontendFetcher(url, headers, res, () => onNotFound(req, url, headers, httpServices,
|
|
15
|
+
frontendFetcher(url, headers, res, () => onNotFound(req, url, headers, httpServices, flags, dirs, urlRewriteProvider, res));
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
|
-
createServer(
|
|
19
|
-
console.log(
|
|
18
|
+
createServer(flags.logHttp ? createLogWrapper(handler) : handler).listen(port);
|
|
19
|
+
console.log(flags.preview ? "preview" : "dev", `server is live at http://127.0.0.1:${port}`);
|
|
20
20
|
}
|
|
21
|
-
async function onNotFound(req, url, headers, httpServices,
|
|
21
|
+
async function onNotFound(req, url, headers, httpServices, flags, dirs, urlRewriteProvider, res) {
|
|
22
22
|
if (req.method === "GET" && extname(url.pathname) === "") {
|
|
23
|
-
const urlRewrite = tryUrlRewrites(
|
|
23
|
+
const urlRewrite = tryUrlRewrites(flags, dirs, urlRewriteProvider.urlRewrites, url);
|
|
24
24
|
if (urlRewrite) {
|
|
25
25
|
streamFile(urlRewrite, res);
|
|
26
26
|
return;
|
|
@@ -42,9 +42,9 @@ async function sendFetchResponse(res, fetchResponse) {
|
|
|
42
42
|
res.end();
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
function tryUrlRewrites(
|
|
46
|
-
const urlRewrite =
|
|
47
|
-
return urlRewrite ? join(
|
|
45
|
+
function tryUrlRewrites(flags, dirs, urlRewrites, url) {
|
|
46
|
+
const urlRewrite = urlRewrites.find((urlRewrite2) => urlRewrite2.pattern.test(url.pathname));
|
|
47
|
+
return urlRewrite ? join(flags.preview ? dirs.buildDist : dirs.buildWatch, urlRewrite.url, "index.html") : null;
|
|
48
48
|
}
|
|
49
49
|
async function tryHttpServices(req, url, headers, httpServices) {
|
|
50
50
|
if (url.pathname.startsWith("/.well-known/")) {
|
|
@@ -91,24 +91,24 @@ function createLogWrapper(handler) {
|
|
|
91
91
|
handler(req, res);
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
-
function createBuiltDistFilesFetcher(
|
|
94
|
+
function createBuiltDistFilesFetcher(dirs, manifest) {
|
|
95
95
|
return (url, _headers, res, notFound) => {
|
|
96
96
|
if (manifest.pageUrls.has(url.pathname)) {
|
|
97
|
-
streamFile(join(
|
|
97
|
+
streamFile(join(dirs.projectResolved, dirs.buildDist, url.pathname, "index.html"), res);
|
|
98
98
|
} else if (manifest.files.has(url.pathname)) {
|
|
99
|
-
streamFile(join(
|
|
99
|
+
streamFile(join(dirs.projectResolved, dirs.buildDist, url.pathname), res);
|
|
100
100
|
} else {
|
|
101
101
|
notFound();
|
|
102
102
|
}
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
|
-
function createDevServeFilesFetcher(
|
|
106
|
-
const proxyAddress = "http://127.0.0.1:" +
|
|
105
|
+
function createDevServeFilesFetcher(esbuildPort, dirs, registry) {
|
|
106
|
+
const proxyAddress = "http://127.0.0.1:" + esbuildPort;
|
|
107
107
|
return (url, _headers, res, notFound) => {
|
|
108
|
-
if (
|
|
109
|
-
streamFile(join(
|
|
108
|
+
if (registry.pageUrls.includes(url.pathname)) {
|
|
109
|
+
streamFile(join(dirs.buildWatch, url.pathname, "index.html"), res);
|
|
110
110
|
} else {
|
|
111
|
-
const maybePublicPath = join(
|
|
111
|
+
const maybePublicPath = join(dirs.public, url.pathname);
|
|
112
112
|
exists(maybePublicPath).then((fromPublic) => {
|
|
113
113
|
if (fromPublic) {
|
|
114
114
|
streamFile(maybePublicPath, res);
|
package/lib_js/public.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { copyFile, mkdir, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import { platform } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
async function copyAssets(
|
|
4
|
+
async function copyAssets(dirs) {
|
|
5
5
|
try {
|
|
6
|
-
const stats = await stat(
|
|
6
|
+
const stats = await stat(dirs.public);
|
|
7
7
|
if (stats.isDirectory()) {
|
|
8
|
-
await mkdir(
|
|
9
|
-
return await recursiveCopyAssets(
|
|
8
|
+
await mkdir(dirs.buildDist, { recursive: true });
|
|
9
|
+
return await recursiveCopyAssets(dirs);
|
|
10
10
|
} else {
|
|
11
11
|
throw Error("./public cannot be a file");
|
|
12
12
|
}
|
|
@@ -15,11 +15,11 @@ async function copyAssets(build) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
const IGNORE = platform() === "darwin" ? [".DS_Store"] : [];
|
|
18
|
-
async function recursiveCopyAssets(
|
|
18
|
+
async function recursiveCopyAssets(dirs, dir = "") {
|
|
19
19
|
const copied = [];
|
|
20
|
-
const to = join(
|
|
20
|
+
const to = join(dirs.buildDist, dir);
|
|
21
21
|
let madeDir = dir === "";
|
|
22
|
-
const listingDir = join(
|
|
22
|
+
const listingDir = join(dirs.public, dir);
|
|
23
23
|
for (const p of await readdir(listingDir)) {
|
|
24
24
|
if (IGNORE.includes(p)) {
|
|
25
25
|
continue;
|
|
@@ -27,10 +27,10 @@ async function recursiveCopyAssets(build, dir = "") {
|
|
|
27
27
|
try {
|
|
28
28
|
const stats = await stat(join(listingDir, p));
|
|
29
29
|
if (stats.isDirectory()) {
|
|
30
|
-
copied.push(...await recursiveCopyAssets(
|
|
30
|
+
copied.push(...await recursiveCopyAssets(dirs, join(dir, p)));
|
|
31
31
|
} else {
|
|
32
32
|
if (!madeDir) {
|
|
33
|
-
await mkdir(join(
|
|
33
|
+
await mkdir(join(dirs.buildDist, dir), {
|
|
34
34
|
recursive: true
|
|
35
35
|
});
|
|
36
36
|
madeDir = true;
|
|
@@ -1,64 +1,85 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
import { writeFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
constructor(dirs) {
|
|
7
|
-
this.#dirs = dirs;
|
|
8
|
-
}
|
|
9
|
-
isProjectSubpathInPagesDir(p) {
|
|
10
|
-
return resolve(join(this.#dirs.projectResolved, p)).startsWith(this.#dirs.pagesResolved);
|
|
11
|
-
}
|
|
12
|
-
isPagesSubpathInPagesDir(p) {
|
|
13
|
-
return this.isProjectSubpathInPagesDir(join(this.#dirs.pages, p));
|
|
14
|
-
}
|
|
15
|
-
resolveHrefInPagesDir(from, href) {
|
|
16
|
-
const p = join(dirname(from), href);
|
|
17
|
-
if (this.isProjectSubpathInPagesDir(p)) {
|
|
18
|
-
return p;
|
|
19
|
-
} else {
|
|
20
|
-
return "outofbounds";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Resolver } from "./dirs.js";
|
|
5
|
+
import { HtmlEntrypoint } from "./html.js";
|
|
24
6
|
class WebsiteRegistry extends EventEmitter {
|
|
25
|
-
|
|
26
|
-
// paths of bundled esbuild outputs
|
|
7
|
+
// paths of bundled esbuild outputs, as built by esbuild
|
|
27
8
|
#bundles = /* @__PURE__ */ new Set();
|
|
9
|
+
#c;
|
|
28
10
|
// public dir assets
|
|
29
11
|
#copiedAssets = null;
|
|
30
12
|
// map of entrypoints to their output path
|
|
31
13
|
#entrypointHrefs = {};
|
|
32
|
-
#
|
|
14
|
+
#pages = {};
|
|
33
15
|
#resolver;
|
|
34
16
|
#workers = null;
|
|
35
|
-
constructor(
|
|
17
|
+
constructor(config) {
|
|
36
18
|
super();
|
|
37
|
-
this.#
|
|
38
|
-
this.#resolver = new
|
|
19
|
+
this.#c = config;
|
|
20
|
+
this.#resolver = new Resolver(config.dirs);
|
|
21
|
+
}
|
|
22
|
+
get config() {
|
|
23
|
+
return this.#c;
|
|
39
24
|
}
|
|
40
25
|
set copiedAssets(copiedAssets) {
|
|
41
26
|
this.#copiedAssets = copiedAssets === null ? null : new Set(copiedAssets);
|
|
42
27
|
}
|
|
43
|
-
|
|
44
|
-
this.#
|
|
28
|
+
get htmlEntrypoints() {
|
|
29
|
+
return Object.values(this.#pages).map((p) => p.html);
|
|
30
|
+
}
|
|
31
|
+
get pageUrls() {
|
|
32
|
+
return Object.keys(this.#pages);
|
|
45
33
|
}
|
|
46
34
|
get resolver() {
|
|
47
35
|
return this.#resolver;
|
|
48
36
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
37
|
+
get urlRewrites() {
|
|
38
|
+
return Object.values(this.#pages).filter((pr) => typeof pr.urlRewrite !== "undefined").map((pr) => pr.urlRewrite);
|
|
39
|
+
}
|
|
40
|
+
get webpageEntryPoints() {
|
|
41
|
+
const unique = /* @__PURE__ */ new Set();
|
|
42
|
+
return Object.values(this.#pages).flatMap((p) => p.bundles).filter((entryPoint) => {
|
|
43
|
+
if (unique.has(entryPoint.in)) {
|
|
44
|
+
return false;
|
|
45
|
+
} else {
|
|
46
|
+
unique.add(entryPoint.in);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
get webpageAndWorkerEntryPoints() {
|
|
52
|
+
const unique = /* @__PURE__ */ new Set();
|
|
53
|
+
const pageBundles = Object.values(this.#pages).flatMap((p) => p.bundles);
|
|
54
|
+
const workerBundles = this.workerEntryPoints;
|
|
55
|
+
const bundles = workerBundles ? [...pageBundles, ...workerBundles] : pageBundles;
|
|
56
|
+
return bundles.filter((entryPoint) => {
|
|
57
|
+
if (unique.has(entryPoint.in)) {
|
|
58
|
+
return false;
|
|
59
|
+
} else {
|
|
60
|
+
unique.add(entryPoint.in);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
get workerEntryPoints() {
|
|
66
|
+
return this.#workers?.map(({ workerEntryPoint }) => ({
|
|
67
|
+
in: workerEntryPoint,
|
|
68
|
+
out: workerEntryPoint.replace(/^pages[\//]/, "").replace(/\.(mj|t)s$/, ".js")
|
|
69
|
+
})) || null;
|
|
70
|
+
}
|
|
71
|
+
get workers() {
|
|
72
|
+
return this.#workers;
|
|
73
|
+
}
|
|
56
74
|
buildRegistry() {
|
|
57
|
-
return new BuildRegistry(this.#
|
|
75
|
+
return new BuildRegistry(this.#c.dirs, this.#resolver, this.#onBuildManifest);
|
|
76
|
+
}
|
|
77
|
+
configSync() {
|
|
78
|
+
this.#configDiff();
|
|
58
79
|
}
|
|
59
80
|
files() {
|
|
60
81
|
const files = /* @__PURE__ */ new Set();
|
|
61
|
-
for (const pageUrl of this.#
|
|
82
|
+
for (const pageUrl of Object.keys(this.#pages))
|
|
62
83
|
files.add(pageUrl === "/" ? "/index.html" : `${pageUrl}/index.html`);
|
|
63
84
|
for (const f of this.#bundles)
|
|
64
85
|
files.add(f);
|
|
@@ -75,29 +96,69 @@ class WebsiteRegistry extends EventEmitter {
|
|
|
75
96
|
throw Error(`mapped href for ${lookup} not found`);
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
|
-
workerEntryPoints() {
|
|
79
|
-
return this.#workers?.map(({ workerEntryPoint }) => ({
|
|
80
|
-
in: workerEntryPoint,
|
|
81
|
-
out: workerEntryPoint.replace(/^pages[\//]/, "").replace(/\.(mj|t)s$/, ".js")
|
|
82
|
-
})) || null;
|
|
83
|
-
}
|
|
84
|
-
workers() {
|
|
85
|
-
return this.#workers;
|
|
86
|
-
}
|
|
87
99
|
async writeManifest(buildTag) {
|
|
88
100
|
const manifest = this.#manifest(buildTag);
|
|
89
|
-
await writeFile(join(this.#
|
|
101
|
+
await writeFile(join(this.#c.dirs.projectRootAbs, this.#c.dirs.buildRoot, "website.json"), JSON.stringify({
|
|
90
102
|
buildTag,
|
|
91
103
|
files: Array.from(manifest.files),
|
|
92
104
|
pageUrls: Array.from(manifest.pageUrls)
|
|
93
105
|
}, null, 4));
|
|
94
106
|
return manifest;
|
|
95
107
|
}
|
|
108
|
+
#configDiff() {
|
|
109
|
+
const updatePages = this.#c.devPages ? { ...this.#c.pages, ...this.#c.devPages } : { ...this.#c.pages };
|
|
110
|
+
const prevPages = new Set(Object.keys(this.#pages));
|
|
111
|
+
for (const [urlPath, mapping] of Object.entries(updatePages)) {
|
|
112
|
+
const existingPage = prevPages.delete(urlPath);
|
|
113
|
+
if (existingPage) {
|
|
114
|
+
this.#configPageUpdate(urlPath, mapping);
|
|
115
|
+
} else {
|
|
116
|
+
this.#configPageAdd(urlPath, mapping);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const prevPage of prevPages) {
|
|
120
|
+
this.#configPageRemove(prevPage);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
#configPageAdd(urlPath, mapping) {
|
|
124
|
+
const html = new HtmlEntrypoint(this.#c, this.#resolver, urlPath, mapping.webpage);
|
|
125
|
+
const urlRewrite = mapping.pattern ? { pattern: mapping.pattern, url: urlPath } : void 0;
|
|
126
|
+
this.#pages[urlPath] = {
|
|
127
|
+
pageUrl: urlPath,
|
|
128
|
+
fsPath: mapping.webpage,
|
|
129
|
+
html,
|
|
130
|
+
urlRewrite,
|
|
131
|
+
bundles: []
|
|
132
|
+
};
|
|
133
|
+
html.on("entrypoints", (entrypoints) => this.#setWebpageBundles(html.url, entrypoints));
|
|
134
|
+
this.emit("webpage", html);
|
|
135
|
+
}
|
|
136
|
+
#configPageUpdate(urlPath, mapping) {
|
|
137
|
+
const existingRegistration = this.#pages[urlPath];
|
|
138
|
+
if (existingRegistration.fsPath !== mapping.webpage) {
|
|
139
|
+
this.#configPageRemove(urlPath);
|
|
140
|
+
this.#configPageAdd(urlPath, mapping);
|
|
141
|
+
} else if (existingRegistration.urlRewrite?.pattern.source !== mapping.pattern?.source) {
|
|
142
|
+
if (mapping.pattern) {
|
|
143
|
+
existingRegistration.urlRewrite = {
|
|
144
|
+
url: urlPath,
|
|
145
|
+
pattern: mapping.pattern
|
|
146
|
+
};
|
|
147
|
+
} else {
|
|
148
|
+
existingRegistration.urlRewrite = void 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
#configPageRemove(urlPath) {
|
|
153
|
+
const registration = this.#pages[urlPath];
|
|
154
|
+
registration.html.removeAllListeners();
|
|
155
|
+
delete this.#pages[urlPath];
|
|
156
|
+
}
|
|
96
157
|
#manifest(buildTag) {
|
|
97
158
|
return {
|
|
98
159
|
buildTag,
|
|
99
160
|
files: this.files(),
|
|
100
|
-
pageUrls: new Set(this.#
|
|
161
|
+
pageUrls: new Set(Object.keys(this.#pages))
|
|
101
162
|
};
|
|
102
163
|
}
|
|
103
164
|
#onBuildManifest = (build) => {
|
|
@@ -136,14 +197,23 @@ class WebsiteRegistry extends EventEmitter {
|
|
|
136
197
|
this.emit("workers");
|
|
137
198
|
}
|
|
138
199
|
};
|
|
200
|
+
#setWebpageBundles(url, bundles) {
|
|
201
|
+
this.#pages[url].bundles = bundles;
|
|
202
|
+
this.emit("entrypoints");
|
|
203
|
+
}
|
|
139
204
|
}
|
|
140
205
|
class BuildRegistry {
|
|
206
|
+
#dirs;
|
|
141
207
|
#onComplete;
|
|
142
208
|
#resolver;
|
|
143
209
|
#workers = null;
|
|
144
|
-
constructor(
|
|
210
|
+
constructor(dirs, resolver, onComplete) {
|
|
211
|
+
this.#dirs = dirs;
|
|
145
212
|
this.#onComplete = onComplete;
|
|
146
|
-
this.#resolver =
|
|
213
|
+
this.#resolver = resolver;
|
|
214
|
+
}
|
|
215
|
+
get dirs() {
|
|
216
|
+
return this.#dirs;
|
|
147
217
|
}
|
|
148
218
|
get resolver() {
|
|
149
219
|
return this.#resolver;
|