@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/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
- #build;
8
- #decorations;
8
+ #c;
9
+ #clientJS;
9
10
  #document = defaultTreeAdapter.createDocument();
10
- // todo cache entrypoints set for quicker diffing
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(build, resolver, url, fsPath, decorations) {
19
+ constructor(c, resolver, url, fsPath) {
20
20
  super({ captureRejections: true });
21
- this.#build = build;
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 #html() {
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
- return await readFile(join(this.#build.dirs.pages, this.#fsPath), "utf8");
49
+ html = await this.#readFromPages(this.#fsPath);
38
50
  } catch (e) {
39
- errorExit(this.#fsPath + " does not exist");
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 partials = await this.#resolvePartialContent(imports.partials);
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.emit("entrypoints", entrypoints);
63
- this.emit("partials", this.#partials.map((p) => p.fsPath));
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
- // Emits `partial` on detecting a partial reference for `dank serve` file watches
69
- // to respond to dependent changes
70
- // todo safeguard recursive partials that cause circular imports
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
- this.emit("partial", p.fsPath);
74
- const html = await readFile(join(this.#build.dirs.pages, p.fsPath), "utf8");
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
- errorExit(`partial ${p.fsPath} cannot recursively import partials`);
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
- #addDecorations(document) {
110
- if (!this.#decorations?.length) {
111
- return;
112
- }
113
- for (const decoration of this.#decorations) {
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.#build.production) {
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.#build.production) {
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 partialSpecifier = partialMatch.groups.pp.trim();
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
- fsPath: partialPath,
168
- commentNode: childNode
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.#build.dirs.pages, dirname(this.#fsPath), href);
223
+ const inPath = join(this.#c.dirs.pages, dirname(this.#fsPath), href);
188
224
  if (!this.#resolver.isProjectSubpathInPagesDir(inPath)) {
189
- errorExit(`href ${href} in webpage ${this.#fsPath} cannot reference sources outside of the pages directory`);
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
- function errorExit(msg) {
234
- console.log(`\x1B[31merror:\x1B[0m`, msg);
235
- process.exit(1);
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(serve, frontendFetcher, httpServices, pageRoutes) {
8
- const serverAddress = "http://localhost:" + serve.dankPort;
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, pageRoutes, serve, res));
15
+ frontendFetcher(url, headers, res, () => onNotFound(req, url, headers, httpServices, flags, dirs, urlRewriteProvider, res));
16
16
  }
17
17
  };
18
- createServer(serve.logHttp ? createLogWrapper(handler) : handler).listen(serve.dankPort);
19
- console.log(serve.preview ? "preview" : "dev", `server is live at http://127.0.0.1:${serve.dankPort}`);
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, pageRoutes, serve, res) {
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(url, pageRoutes, serve);
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(url, pageRoutes, serve) {
46
- const urlRewrite = pageRoutes.urlRewrites.find((urlRewrite2) => urlRewrite2.pattern.test(url.pathname));
47
- return urlRewrite ? join(serve.dirs.buildWatch, urlRewrite.url, "index.html") : null;
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(dir, manifest) {
94
+ function createBuiltDistFilesFetcher(dirs, manifest) {
95
95
  return (url, _headers, res, notFound) => {
96
96
  if (manifest.pageUrls.has(url.pathname)) {
97
- streamFile(join(dir, url.pathname, "index.html"), res);
97
+ streamFile(join(dirs.projectResolved, dirs.buildDist, url.pathname, "index.html"), res);
98
98
  } else if (manifest.files.has(url.pathname)) {
99
- streamFile(join(dir, url.pathname), res);
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(pageRoutes, serve) {
106
- const proxyAddress = "http://127.0.0.1:" + serve.esbuildPort;
105
+ function createDevServeFilesFetcher(esbuildPort, dirs, registry) {
106
+ const proxyAddress = "http://127.0.0.1:" + esbuildPort;
107
107
  return (url, _headers, res, notFound) => {
108
- if (pageRoutes.urls.includes(url.pathname)) {
109
- streamFile(join(serve.dirs.buildWatch, url.pathname, "index.html"), res);
108
+ if (registry.pageUrls.includes(url.pathname)) {
109
+ streamFile(join(dirs.buildWatch, url.pathname, "index.html"), res);
110
110
  } else {
111
- const maybePublicPath = join(serve.dirs.public, url.pathname);
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(build) {
4
+ async function copyAssets(dirs) {
5
5
  try {
6
- const stats = await stat(build.dirs.public);
6
+ const stats = await stat(dirs.public);
7
7
  if (stats.isDirectory()) {
8
- await mkdir(build.dirs.buildDist, { recursive: true });
9
- return await recursiveCopyAssets(build);
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(build, dir = "") {
18
+ async function recursiveCopyAssets(dirs, dir = "") {
19
19
  const copied = [];
20
- const to = join(build.dirs.buildDist, dir);
20
+ const to = join(dirs.buildDist, dir);
21
21
  let madeDir = dir === "";
22
- const listingDir = join(build.dirs.public, dir);
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(build, join(dir, p)));
30
+ copied.push(...await recursiveCopyAssets(dirs, join(dir, p)));
31
31
  } else {
32
32
  if (!madeDir) {
33
- await mkdir(join(build.dirs.buildDist, dir), {
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 { dirname, join, resolve } from "node:path";
4
- class ResolverImpl {
5
- #dirs;
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
- #build;
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
- #pageUrls = [];
14
+ #pages = {};
33
15
  #resolver;
34
16
  #workers = null;
35
- constructor(build) {
17
+ constructor(config) {
36
18
  super();
37
- this.#build = build;
38
- this.#resolver = new ResolverImpl(build.dirs);
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
- set pageUrls(pageUrls) {
44
- this.#pageUrls = pageUrls;
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
- // bundleOutputs(type?: 'css' | 'js'): Array<string> {
50
- // if (!type) {
51
- // return Array.from(this.#bundles)
52
- // } else {
53
- // return Array.from(this.#bundles).filter(p => p.endsWith(type))
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.#build, this.#onBuildManifest);
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.#pageUrls)
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.#build.dirs.projectRootAbs, this.#build.dirs.buildRoot, "website.json"), JSON.stringify({
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.#pageUrls)
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(build, onComplete) {
210
+ constructor(dirs, resolver, onComplete) {
211
+ this.#dirs = dirs;
145
212
  this.#onComplete = onComplete;
146
- this.#resolver = new ResolverImpl(build.dirs);
213
+ this.#resolver = resolver;
214
+ }
215
+ get dirs() {
216
+ return this.#dirs;
147
217
  }
148
218
  get resolver() {
149
219
  return this.#resolver;