@eighty4/dank 0.0.4-1 → 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.
@@ -0,0 +1,260 @@
1
+ import EventEmitter from "node:events";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { Resolver } from "./dirs.js";
5
+ import { HtmlEntrypoint } from "./html.js";
6
+ class WebsiteRegistry extends EventEmitter {
7
+ // paths of bundled esbuild outputs, as built by esbuild
8
+ #bundles = /* @__PURE__ */ new Set();
9
+ #c;
10
+ // public dir assets
11
+ #copiedAssets = null;
12
+ // map of entrypoints to their output path
13
+ #entrypointHrefs = {};
14
+ #pages = {};
15
+ #resolver;
16
+ #workers = null;
17
+ constructor(config) {
18
+ super();
19
+ this.#c = config;
20
+ this.#resolver = new Resolver(config.dirs);
21
+ }
22
+ get config() {
23
+ return this.#c;
24
+ }
25
+ set copiedAssets(copiedAssets) {
26
+ this.#copiedAssets = copiedAssets === null ? null : new Set(copiedAssets);
27
+ }
28
+ get htmlEntrypoints() {
29
+ return Object.values(this.#pages).map((p) => p.html);
30
+ }
31
+ get pageUrls() {
32
+ return Object.keys(this.#pages);
33
+ }
34
+ get resolver() {
35
+ return this.#resolver;
36
+ }
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
+ }
74
+ buildRegistry() {
75
+ return new BuildRegistry(this.#c.dirs, this.#resolver, this.#onBuildManifest);
76
+ }
77
+ configSync() {
78
+ this.#configDiff();
79
+ }
80
+ files() {
81
+ const files = /* @__PURE__ */ new Set();
82
+ for (const pageUrl of Object.keys(this.#pages))
83
+ files.add(pageUrl === "/" ? "/index.html" : `${pageUrl}/index.html`);
84
+ for (const f of this.#bundles)
85
+ files.add(f);
86
+ if (this.#copiedAssets)
87
+ for (const f of this.#copiedAssets)
88
+ files.add(f);
89
+ return files;
90
+ }
91
+ mappedHref(lookup) {
92
+ const found = this.#entrypointHrefs[lookup];
93
+ if (found) {
94
+ return found;
95
+ } else {
96
+ throw Error(`mapped href for ${lookup} not found`);
97
+ }
98
+ }
99
+ async writeManifest(buildTag) {
100
+ const manifest = this.#manifest(buildTag);
101
+ await writeFile(join(this.#c.dirs.projectRootAbs, this.#c.dirs.buildRoot, "website.json"), JSON.stringify({
102
+ buildTag,
103
+ files: Array.from(manifest.files),
104
+ pageUrls: Array.from(manifest.pageUrls)
105
+ }, null, 4));
106
+ return manifest;
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
+ }
157
+ #manifest(buildTag) {
158
+ return {
159
+ buildTag,
160
+ files: this.files(),
161
+ pageUrls: new Set(Object.keys(this.#pages))
162
+ };
163
+ }
164
+ #onBuildManifest = (build) => {
165
+ for (const [outPath, entrypoint] of Object.entries(build.bundles)) {
166
+ this.#bundles.add(outPath);
167
+ if (entrypoint) {
168
+ this.#entrypointHrefs[entrypoint] = outPath;
169
+ }
170
+ }
171
+ let updatedWorkerEntrypoints = false;
172
+ const previousWorkers = this.#workers === null ? null : new Set(this.#workers.map((w) => w.workerEntryPoint));
173
+ if (build.workers) {
174
+ if (!previousWorkers || previousWorkers.size !== new Set(build.workers.map((w) => w.workerEntryPoint)).size) {
175
+ updatedWorkerEntrypoints = true;
176
+ } else {
177
+ updatedWorkerEntrypoints = !build.workers.every((w) => previousWorkers.has(w.workerEntryPoint));
178
+ }
179
+ } else if (previousWorkers) {
180
+ updatedWorkerEntrypoints = true;
181
+ }
182
+ if (build.workers) {
183
+ if (!this.#workers) {
184
+ this.#workers = build.workers;
185
+ } else {
186
+ for (const w of build.workers) {
187
+ const found = this.#workers.find((w2) => {
188
+ return w.dependentEntryPoint === w2.dependentEntryPoint && w.workerEntryPoint === w2.workerEntryPoint;
189
+ });
190
+ if (!found) {
191
+ this.#workers.push(w);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ if (updatedWorkerEntrypoints) {
197
+ this.emit("workers");
198
+ }
199
+ };
200
+ #setWebpageBundles(url, bundles) {
201
+ this.#pages[url].bundles = bundles;
202
+ this.emit("entrypoints");
203
+ }
204
+ }
205
+ class BuildRegistry {
206
+ #dirs;
207
+ #onComplete;
208
+ #resolver;
209
+ #workers = null;
210
+ constructor(dirs, resolver, onComplete) {
211
+ this.#dirs = dirs;
212
+ this.#onComplete = onComplete;
213
+ this.#resolver = resolver;
214
+ }
215
+ get dirs() {
216
+ return this.#dirs;
217
+ }
218
+ get resolver() {
219
+ return this.#resolver;
220
+ }
221
+ // resolve web worker imported by a webpage module
222
+ addWorker(worker) {
223
+ if (!this.#workers) {
224
+ this.#workers = [worker];
225
+ } else {
226
+ this.#workers.push(worker);
227
+ }
228
+ }
229
+ completeBuild(result) {
230
+ const bundles = {};
231
+ for (const [outPath, output] of Object.entries(result.metafile.outputs)) {
232
+ bundles[outPath.replace(/^build[/\\]dist/, "")] = output.entryPoint || null;
233
+ }
234
+ let workers = null;
235
+ if (this.#workers) {
236
+ workers = [];
237
+ for (const output of Object.values(result.metafile.outputs)) {
238
+ if (!output.entryPoint)
239
+ continue;
240
+ const inputs = Object.keys(output.inputs);
241
+ for (const worker of this.#workers) {
242
+ if (inputs.includes(worker.clientScript)) {
243
+ workers.push({
244
+ ...worker,
245
+ dependentEntryPoint: output.entryPoint
246
+ });
247
+ }
248
+ }
249
+ }
250
+ }
251
+ this.#onComplete({
252
+ bundles,
253
+ workers
254
+ });
255
+ }
256
+ }
257
+ export {
258
+ BuildRegistry,
259
+ WebsiteRegistry
260
+ };
package/lib_js/serve.js CHANGED
@@ -1,268 +1,126 @@
1
- import { mkdir, readFile, rm, watch as _watch, writeFile, } from 'node:fs/promises';
2
- import { extname, join, resolve } from 'node:path';
1
+ import { mkdir, rm, writeFile } from "node:fs/promises";
2
+ import { extname, join } from "node:path";
3
3
  import { buildWebsite } from "./build.js";
4
4
  import { loadConfig } from "./config.js";
5
5
  import { createGlobalDefinitions } from "./define.js";
6
6
  import { esbuildDevContext } from "./esbuild.js";
7
- import { resolveServeFlags } from "./flags.js";
8
- import { HtmlEntrypoint } from "./html.js";
9
- import { createBuiltDistFilesFetcher, createDevServeFilesFetcher, startWebServer, } from "./http.js";
10
- import { WebsiteRegistry } from "./metadata.js";
7
+ import { createBuiltDistFilesFetcher, createDevServeFilesFetcher, startWebServer } from "./http.js";
8
+ import { WebsiteRegistry } from "./registry.js";
11
9
  import { startDevServices, updateDevServices } from "./services.js";
12
- export async function serveWebsite(c) {
13
- const serve = resolveServeFlags(c);
14
- await rm(serve.dirs.buildRoot, { force: true, recursive: true });
15
- const abortController = new AbortController();
16
- process.once('exit', () => abortController.abort());
17
- if (serve.preview) {
18
- await startPreviewMode(c, serve, abortController.signal);
19
- }
20
- else {
21
- await startDevMode(c, serve, abortController.signal);
22
- }
23
- return new Promise(() => { });
10
+ import { watch } from "./watch.js";
11
+ let c;
12
+ async function serveWebsite() {
13
+ c = await loadConfig("serve", process.cwd());
14
+ await rm(c.dirs.buildRoot, { force: true, recursive: true });
15
+ const abortController = new AbortController();
16
+ process.once("exit", () => abortController.abort());
17
+ if (c.flags.preview) {
18
+ await startPreviewMode(abortController.signal);
19
+ } else {
20
+ await startDevMode(abortController.signal);
21
+ }
22
+ return new Promise(() => {
23
+ });
24
24
  }
25
- async function startPreviewMode(c, serve, signal) {
26
- const manifest = await buildWebsite(c, serve);
27
- const frontend = createBuiltDistFilesFetcher(serve.dirs.buildDist, manifest);
28
- const devServices = startDevServices(c, signal);
29
- startWebServer(serve, frontend, devServices.http, {
30
- urls: Object.keys(c.pages),
31
- urlRewrites: collectUrlRewrites(c),
32
- });
25
+ async function startPreviewMode(signal) {
26
+ const manifest = await buildWebsite(c);
27
+ const frontend = createBuiltDistFilesFetcher(c.dirs, manifest);
28
+ const devServices = startDevServices(c.services, signal);
29
+ const urlRewrites = Object.keys(c.pages).sort().map((url) => {
30
+ const mapping = c.pages[url];
31
+ return typeof mapping !== "object" || !mapping.pattern ? null : { url, pattern: mapping.pattern };
32
+ }).filter((mapping) => mapping !== null);
33
+ startWebServer(c.dankPort, c.flags, c.dirs, { urlRewrites }, frontend, devServices.http);
33
34
  }
34
- function collectUrlRewrites(c) {
35
- return Object.keys(c.pages)
36
- .sort()
37
- .map(url => {
38
- const mapping = c.pages[url];
39
- return typeof mapping !== 'object' || !mapping.pattern
40
- ? null
41
- : { url, pattern: mapping.pattern };
42
- })
43
- .filter(mapping => mapping !== null);
44
- }
45
- // todo changing partials triggers update on html pages
46
- async function startDevMode(c, serve, signal) {
47
- await mkdir(serve.dirs.buildWatch, { recursive: true });
48
- const registry = new WebsiteRegistry(serve);
49
- const clientJS = await loadClientJS(serve.esbuildPort);
50
- const pagesByUrlPath = {};
51
- const partialsByUrlPath = {};
52
- const entryPointsByUrlPath = {};
53
- let buildContext = null;
54
- registry.on('workers', resetBuildContext);
55
- watch('dank.config.ts', signal, async () => {
56
- let updated;
57
- try {
58
- updated = await loadConfig();
59
- }
60
- catch (ignore) {
61
- return;
62
- }
63
- const prevPages = new Set(Object.keys(pagesByUrlPath));
64
- await Promise.all(Object.entries(updated.pages).map(async ([urlPath, mapping]) => {
65
- c.pages[urlPath] = mapping;
66
- const srcPath = typeof mapping === 'string' ? mapping : mapping.webpage;
67
- if (!pagesByUrlPath[urlPath]) {
68
- await addPage(urlPath, srcPath);
69
- }
70
- else {
71
- prevPages.delete(urlPath);
72
- if (pagesByUrlPath[urlPath].fsPath !== srcPath) {
73
- await updatePage(urlPath);
74
- }
75
- }
76
- }));
77
- for (const prevPage of Array.from(prevPages)) {
78
- delete c.pages[prevPage];
79
- deletePage(prevPage);
80
- }
81
- updateDevServices(updated);
82
- });
83
- watch(serve.dirs.pages, signal, filename => {
84
- if (extname(filename) === '.html') {
85
- for (const [urlPath, srcPath] of Object.entries(c.pages)) {
86
- if (srcPath === filename) {
87
- updatePage(urlPath);
88
- }
89
- }
90
- for (const [urlPath, partials] of Object.entries(partialsByUrlPath)) {
91
- if (partials.includes(filename)) {
92
- updatePage(urlPath, filename);
93
- }
94
- }
95
- }
96
- });
97
- await Promise.all(Object.entries(c.pages).map(async ([urlPath, mapping]) => {
98
- const srcPath = typeof mapping === 'string' ? mapping : mapping.webpage;
99
- await addPage(urlPath, srcPath);
100
- return new Promise(res => pagesByUrlPath[urlPath].once('entrypoints', res));
101
- }));
102
- async function addPage(urlPath, srcPath) {
103
- await mkdir(join(serve.dirs.buildWatch, urlPath), { recursive: true });
104
- const htmlEntrypoint = (pagesByUrlPath[urlPath] = new HtmlEntrypoint(serve, registry.resolver, urlPath, srcPath, [{ type: 'script', js: clientJS }]));
105
- htmlEntrypoint.on('entrypoints', entrypoints => {
106
- const pathsIn = new Set(entrypoints.map(e => e.in));
107
- if (!entryPointsByUrlPath[urlPath] ||
108
- !matchingEntrypoints(entryPointsByUrlPath[urlPath].pathsIn, pathsIn)) {
109
- entryPointsByUrlPath[urlPath] = { entrypoints, pathsIn };
110
- resetBuildContext();
111
- }
112
- });
113
- htmlEntrypoint.on('partial', partial => {
114
- if (!partialsByUrlPath[urlPath]) {
115
- partialsByUrlPath[urlPath] = [];
116
- }
117
- partialsByUrlPath[urlPath].push(partial);
118
- });
119
- htmlEntrypoint.on('partials', partials => (partialsByUrlPath[urlPath] = partials));
120
- htmlEntrypoint.on('output', html => writeFile(join(serve.dirs.buildWatch, urlPath, 'index.html'), html));
121
- }
122
- function deletePage(urlPath) {
123
- pagesByUrlPath[urlPath].removeAllListeners();
124
- delete pagesByUrlPath[urlPath];
125
- delete entryPointsByUrlPath[urlPath];
126
- resetBuildContext();
127
- }
128
- async function updatePage(urlPath, partial) {
129
- pagesByUrlPath[urlPath].emit('change', partial);
35
+ async function startDevMode(signal) {
36
+ const registry = new WebsiteRegistry(c);
37
+ await mkdir(c.dirs.buildWatch, { recursive: true });
38
+ let buildContext = null;
39
+ watch("dank.config.ts", signal, async () => {
40
+ try {
41
+ await c.reload();
42
+ } catch (ignore) {
43
+ return;
130
44
  }
131
- function collectEntrypoints() {
132
- const unique = new Set();
133
- const pageBundles = Object.values(entryPointsByUrlPath)
134
- .flatMap(entrypointState => entrypointState.entrypoints)
135
- .filter(entryPoint => {
136
- if (unique.has(entryPoint.in)) {
137
- return false;
138
- }
139
- else {
140
- unique.add(entryPoint.in);
141
- return true;
142
- }
143
- });
144
- const workerBundles = registry.workerEntryPoints();
145
- if (workerBundles) {
146
- return [...pageBundles, ...workerBundles];
147
- }
148
- else {
149
- return pageBundles;
45
+ registry.configSync();
46
+ updateDevServices(c.services);
47
+ });
48
+ watch(c.dirs.pages, signal, (filename) => {
49
+ if (extname(filename) === ".html") {
50
+ registry.htmlEntrypoints.forEach((html) => {
51
+ if (html.fsPath === filename) {
52
+ html.emit("change");
53
+ } else if (html.usesPartial(filename)) {
54
+ html.emit("change", filename);
150
55
  }
56
+ });
151
57
  }
152
- function resetBuildContext() {
153
- switch (buildContext) {
154
- case 'starting':
155
- buildContext = 'dirty';
156
- return;
157
- case 'dirty':
158
- case 'disposing':
159
- return;
160
- }
161
- if (buildContext !== null) {
162
- const disposing = buildContext.dispose();
163
- buildContext = 'disposing';
164
- disposing.then(() => {
165
- buildContext = null;
166
- resetBuildContext();
167
- });
168
- }
169
- else {
170
- buildContext = 'starting';
171
- startEsbuildWatch(c, registry, serve, collectEntrypoints()).then(ctx => {
172
- if (buildContext === 'dirty') {
173
- buildContext = 'disposing';
174
- ctx.dispose().then(() => {
175
- buildContext = null;
176
- resetBuildContext();
177
- });
178
- }
179
- else {
180
- buildContext = ctx;
181
- }
182
- });
58
+ });
59
+ function resetBuildContext() {
60
+ switch (buildContext) {
61
+ case "starting":
62
+ buildContext = "dirty";
63
+ return;
64
+ case "dirty":
65
+ case "disposing":
66
+ return;
67
+ }
68
+ if (buildContext !== null) {
69
+ const disposing = buildContext.dispose();
70
+ buildContext = "disposing";
71
+ disposing.then(() => {
72
+ buildContext = null;
73
+ resetBuildContext();
74
+ });
75
+ } else {
76
+ buildContext = "starting";
77
+ startEsbuildWatch(registry).then((ctx) => {
78
+ if (buildContext === "dirty") {
79
+ buildContext = "disposing";
80
+ ctx.dispose().then(() => {
81
+ buildContext = null;
82
+ resetBuildContext();
83
+ });
84
+ } else {
85
+ buildContext = ctx;
183
86
  }
87
+ });
184
88
  }
185
- // function removePartialFromPage(partial: string, urlPath: string) {
186
- // const deleteIndex = urlPathsByPartials[partial].indexOf(urlPath)
187
- // if (deleteIndex !== -1) {
188
- // if (urlPathsByPartials[partial].length === 1) {
189
- // delete urlPathsByPartials[partial]
190
- // } else {
191
- // urlPathsByPartials[partial].splice(deleteIndex, 1)
192
- // }
193
- // }
194
- // }
195
- // inital start of esbuild ctx
89
+ }
90
+ registry.on("webpage", (html) => {
91
+ html.on("error", (e) => console.log(`\x1B[31merror:\x1B[0m`, e.message));
92
+ html.on("output", (output) => writeHtml(html, output));
93
+ });
94
+ registry.on("workers", () => {
196
95
  resetBuildContext();
197
- // todo this page route state could be built on change and reused
198
- const pageRoutes = {
199
- get urls() {
200
- return Object.keys(c.pages);
201
- },
202
- get urlRewrites() {
203
- return collectUrlRewrites(c);
204
- },
205
- };
206
- const frontend = createDevServeFilesFetcher(pageRoutes, serve);
207
- const devServices = startDevServices(c, signal);
208
- startWebServer(serve, frontend, devServices.http, pageRoutes);
96
+ });
97
+ registry.configSync();
98
+ await Promise.all(registry.htmlEntrypoints.map((html) => html.process()));
99
+ registry.on("entrypoints", () => resetBuildContext());
100
+ resetBuildContext();
101
+ const frontend = createDevServeFilesFetcher(c.esbuildPort, c.dirs, registry);
102
+ const devServices = startDevServices(c.services, signal);
103
+ startWebServer(c.dankPort, c.flags, c.dirs, registry, frontend, devServices.http);
209
104
  }
210
- function matchingEntrypoints(a, b) {
211
- if (a.size !== b.size) {
212
- return false;
105
+ async function startEsbuildWatch(registry) {
106
+ const entryPoints = registry.webpageAndWorkerEntryPoints;
107
+ const ctx = await esbuildDevContext(registry, createGlobalDefinitions(c), entryPoints);
108
+ await ctx.watch();
109
+ await ctx.serve({
110
+ host: "127.0.0.1",
111
+ port: c.esbuildPort,
112
+ cors: {
113
+ origin: ["127.0.0.1", "localhost"].map((hostname) => `http://${hostname}:${c.dankPort}`)
213
114
  }
214
- for (const v in a) {
215
- if (!b.has(v)) {
216
- return false;
217
- }
218
- }
219
- return true;
220
- }
221
- async function startEsbuildWatch(c, registry, serve, entryPoints) {
222
- const ctx = await esbuildDevContext(serve, registry, createGlobalDefinitions(serve), entryPoints, c.esbuild);
223
- await ctx.watch();
224
- await ctx.serve({
225
- host: '127.0.0.1',
226
- port: serve.esbuildPort,
227
- cors: {
228
- origin: ['127.0.0.1', 'localhost'].map(hostname => `http://${hostname}:${serve.dankPort}`),
229
- },
230
- });
231
- return ctx;
115
+ });
116
+ return ctx;
232
117
  }
233
- async function loadClientJS(esbuildPort) {
234
- const clientJS = await readFile(resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')), 'utf-8');
235
- return clientJS.replace('3995', `${esbuildPort}`);
236
- }
237
- async function watch(p, signal, fire) {
238
- const delayFire = 90;
239
- const timeout = 100;
240
- let changes = {};
241
- try {
242
- for await (const { filename } of _watch(p, {
243
- recursive: true,
244
- signal,
245
- })) {
246
- if (filename) {
247
- if (!changes[filename]) {
248
- const now = Date.now();
249
- changes[filename] = now + delayFire;
250
- setTimeout(() => {
251
- const now = Date.now();
252
- for (const [filename, then] of Object.entries(changes)) {
253
- if (then <= now) {
254
- fire(filename);
255
- delete changes[filename];
256
- }
257
- }
258
- }, timeout);
259
- }
260
- }
261
- }
262
- }
263
- catch (e) {
264
- if (e.name !== 'AbortError') {
265
- throw e;
266
- }
267
- }
118
+ async function writeHtml(html, output) {
119
+ const dir = join(c.dirs.buildWatch, html.url);
120
+ await mkdir(dir, { recursive: true });
121
+ const path = join(dir, "index.html");
122
+ await writeFile(path, output);
268
123
  }
124
+ export {
125
+ serveWebsite
126
+ };