@eighty4/dank 0.0.3 → 0.0.4-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,198 @@
1
+ import EventEmitter from 'node:events';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { dirname, join, resolve, sep } from 'node:path';
4
+ // manages website resources during `dank build` and `dank serve`
5
+ export class WebsiteRegistry extends EventEmitter {
6
+ #build;
7
+ // paths of bundled esbuild outputs
8
+ #bundles = new Set();
9
+ // public dir assets
10
+ #copiedAssets = null;
11
+ // map of entrypoints to their output path
12
+ #entrypointHrefs = {};
13
+ #pageUrls = [];
14
+ #workers = null;
15
+ constructor(build) {
16
+ super();
17
+ this.#build = build;
18
+ }
19
+ // bundleOutputs(type?: 'css' | 'js'): Array<string> {
20
+ // if (!type) {
21
+ // return Array.from(this.#bundles)
22
+ // } else {
23
+ // return Array.from(this.#bundles).filter(p => p.endsWith(type))
24
+ // }
25
+ // }
26
+ buildRegistry() {
27
+ return new BuildRegistry(this.#build, this.#onBuildManifest);
28
+ }
29
+ files() {
30
+ const files = new Set();
31
+ for (const pageUrl of this.#pageUrls)
32
+ files.add(pageUrl === '/' ? '/index.html' : `${pageUrl}/index.html`);
33
+ for (const f of this.#bundles)
34
+ files.add(f);
35
+ if (this.#copiedAssets)
36
+ for (const f of this.#copiedAssets)
37
+ files.add(f);
38
+ return files;
39
+ }
40
+ mappedHref(lookup) {
41
+ const found = this.#entrypointHrefs[lookup];
42
+ if (found) {
43
+ return found;
44
+ }
45
+ else {
46
+ throw Error(`mapped href for ${lookup} not found`);
47
+ }
48
+ }
49
+ resolve(from, href) {
50
+ return resolveImpl(this.#build, from, href);
51
+ }
52
+ workerEntryPoints() {
53
+ return (this.#workers?.map(({ workerEntryPoint }) => ({
54
+ in: workerEntryPoint,
55
+ out: workerEntryPoint
56
+ .replace(/^pages[\//]/, '')
57
+ .replace(/\.(mj|t)s$/, '.js'),
58
+ })) || null);
59
+ }
60
+ workers() {
61
+ return this.#workers;
62
+ }
63
+ async writeManifest(buildTag) {
64
+ const manifest = this.#manifest(buildTag);
65
+ await writeFile(join(this.#build.dirs.projectRootAbs, this.#build.dirs.buildRoot, 'website.json'), JSON.stringify({
66
+ buildTag,
67
+ files: Array.from(manifest.files),
68
+ pageUrls: Array.from(manifest.pageUrls),
69
+ }, null, 4));
70
+ return manifest;
71
+ }
72
+ set copiedAssets(copiedAssets) {
73
+ this.#copiedAssets =
74
+ copiedAssets === null ? null : new Set(copiedAssets);
75
+ }
76
+ set pageUrls(pageUrls) {
77
+ this.#pageUrls = pageUrls;
78
+ }
79
+ #manifest(buildTag) {
80
+ return {
81
+ buildTag,
82
+ files: this.files(),
83
+ pageUrls: new Set(this.#pageUrls),
84
+ };
85
+ }
86
+ #onBuildManifest = (build) => {
87
+ // collect built bundle entrypoint hrefs
88
+ for (const [outPath, entrypoint] of Object.entries(build.bundles)) {
89
+ this.#bundles.add(outPath);
90
+ if (entrypoint) {
91
+ this.#entrypointHrefs[entrypoint] = outPath;
92
+ }
93
+ }
94
+ // determine if worker entrypoints have changed
95
+ let updatedWorkerEntrypoints = false;
96
+ const previousWorkers = this.#workers === null
97
+ ? null
98
+ : new Set(this.#workers.map(w => w.workerEntryPoint));
99
+ if (build.workers) {
100
+ if (!previousWorkers ||
101
+ previousWorkers.size !==
102
+ new Set(build.workers.map(w => w.workerEntryPoint)).size) {
103
+ updatedWorkerEntrypoints = true;
104
+ }
105
+ else {
106
+ updatedWorkerEntrypoints = !build.workers.every(w => previousWorkers.has(w.workerEntryPoint));
107
+ }
108
+ }
109
+ else if (previousWorkers) {
110
+ updatedWorkerEntrypoints = true;
111
+ }
112
+ // merge unique entrypoints from built workers with registry state
113
+ // todo filtering out unique occurrences of clientScript and workerUrl
114
+ // drops reporting/summary/debugging capabilities, but currently
115
+ // this.#workers is used for unique worker/client entrypoints
116
+ if (build.workers) {
117
+ if (!this.#workers) {
118
+ this.#workers = build.workers;
119
+ }
120
+ else {
121
+ for (const w of build.workers) {
122
+ const found = this.#workers.find(w2 => {
123
+ return (w.dependentEntryPoint === w2.dependentEntryPoint &&
124
+ w.workerEntryPoint === w2.workerEntryPoint);
125
+ });
126
+ if (!found) {
127
+ this.#workers.push(w);
128
+ }
129
+ }
130
+ }
131
+ }
132
+ if (updatedWorkerEntrypoints) {
133
+ this.emit('workers');
134
+ }
135
+ };
136
+ }
137
+ // result accumulator of an esbuild `build` or `Context.rebuild`
138
+ export class BuildRegistry {
139
+ #build;
140
+ #onComplete;
141
+ #workers = null;
142
+ constructor(build, onComplete) {
143
+ this.#build = build;
144
+ this.#onComplete = onComplete;
145
+ }
146
+ // resolve web worker imported by a webpage module
147
+ addWorker(worker) {
148
+ // todo normalize path
149
+ if (!this.#workers) {
150
+ this.#workers = [worker];
151
+ }
152
+ else {
153
+ this.#workers.push(worker);
154
+ }
155
+ }
156
+ completeBuild(result) {
157
+ const bundles = {};
158
+ for (const [outPath, output] of Object.entries(result.metafile.outputs)) {
159
+ bundles[outPath.replace(/^build[/\\]dist/, '')] =
160
+ output.entryPoint || null;
161
+ }
162
+ let workers = null;
163
+ if (this.#workers) {
164
+ workers = [];
165
+ for (const output of Object.values(result.metafile.outputs)) {
166
+ if (!output.entryPoint)
167
+ continue;
168
+ const inputs = Object.keys(output.inputs);
169
+ for (const worker of this.#workers) {
170
+ if (inputs.includes(worker.clientScript)) {
171
+ workers.push({
172
+ ...worker,
173
+ dependentEntryPoint: output.entryPoint,
174
+ });
175
+ }
176
+ }
177
+ }
178
+ }
179
+ this.#onComplete({
180
+ bundles,
181
+ workers,
182
+ });
183
+ }
184
+ resolve(from, href) {
185
+ return resolveImpl(this.#build, from, href);
186
+ }
187
+ }
188
+ function resolveImpl(build, from, href) {
189
+ const { pagesResolved, projectRootAbs } = build.dirs;
190
+ const fromDir = dirname(from);
191
+ const resolvedFromProjectRoot = join(projectRootAbs, fromDir, href);
192
+ if (!resolve(resolvedFromProjectRoot).startsWith(pagesResolved)) {
193
+ throw Error(`href ${href} cannot be resolved from pages${sep}${from} to a path outside of the pages directory`);
194
+ }
195
+ else {
196
+ return join(fromDir, href);
197
+ }
198
+ }
package/lib_js/public.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { copyFile, mkdir, readdir, stat } from 'node:fs/promises';
2
+ import { platform } from 'node:os';
2
3
  import { join } from 'node:path';
3
- export async function copyAssets(outRoot) {
4
+ export async function copyAssets(build) {
4
5
  try {
5
- const stats = await stat('public');
6
+ const stats = await stat(build.dirs.public);
6
7
  if (stats.isDirectory()) {
7
- await mkdir(outRoot, { recursive: true });
8
- return await recursiveCopyAssets(outRoot);
8
+ await mkdir(build.dirs.buildDist, { recursive: true });
9
+ return await recursiveCopyAssets(build);
9
10
  }
10
11
  else {
11
12
  throw Error('./public cannot be a file');
@@ -15,22 +16,29 @@ export async function copyAssets(outRoot) {
15
16
  return null;
16
17
  }
17
18
  }
18
- async function recursiveCopyAssets(outRoot, dir = '') {
19
+ const IGNORE = platform() === 'darwin' ? ['.DS_Store'] : [];
20
+ async function recursiveCopyAssets(build, dir = '') {
19
21
  const copied = [];
20
- const to = join(outRoot, dir);
22
+ const to = join(build.dirs.buildDist, dir);
21
23
  let madeDir = dir === '';
22
- for (const p of await readdir(join('public', dir))) {
24
+ const listingDir = join(build.dirs.public, dir);
25
+ for (const p of await readdir(listingDir)) {
26
+ if (IGNORE.includes(p)) {
27
+ continue;
28
+ }
23
29
  try {
24
- const stats = await stat(join('public', dir, p));
30
+ const stats = await stat(join(listingDir, p));
25
31
  if (stats.isDirectory()) {
26
- copied.push(...(await recursiveCopyAssets(outRoot, join(dir, p))));
32
+ copied.push(...(await recursiveCopyAssets(build, join(dir, p))));
27
33
  }
28
34
  else {
29
35
  if (!madeDir) {
30
- await mkdir(join(outRoot, dir));
36
+ await mkdir(join(build.dirs.buildDist, dir), {
37
+ recursive: true,
38
+ });
31
39
  madeDir = true;
32
40
  }
33
- await copyFile(join('public', dir, p), join(to, p));
41
+ await copyFile(join(listingDir, p), join(to, p));
34
42
  copied.push('/' + join(dir, p).replaceAll('\\', '/'));
35
43
  }
36
44
  }
package/lib_js/serve.js CHANGED
@@ -1,43 +1,57 @@
1
- import { mkdir, readFile, rm, watch as _watch } from 'node:fs/promises';
1
+ import { mkdir, readFile, rm, watch as _watch, writeFile, } from 'node:fs/promises';
2
2
  import { extname, join, resolve } 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 { isPreviewBuild } from "./flags.js";
7
+ import { resolveServeFlags } from "./flags.js";
8
8
  import { HtmlEntrypoint } from "./html.js";
9
- import { createBuiltDistFilesFetcher, createDevServeFilesFetcher, createWebServer, } from "./http.js";
9
+ import { createBuiltDistFilesFetcher, createDevServeFilesFetcher, startWebServer, } from "./http.js";
10
+ import { WebsiteRegistry } from "./metadata.js";
10
11
  import { startDevServices, updateDevServices } from "./services.js";
11
- const isPreview = isPreviewBuild();
12
- // alternate port for --preview bc of service worker
13
- const PORT = isPreview ? 4000 : 3000;
14
- // port for esbuild.serve
15
- const ESBUILD_PORT = 2999;
16
12
  export async function serveWebsite(c) {
17
- await rm('build', { force: true, recursive: true });
18
- if (isPreview) {
19
- await startPreviewMode(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);
20
19
  }
21
20
  else {
22
- const abortController = new AbortController();
23
- await startDevMode(c, abortController.signal);
21
+ await startDevMode(c, serve, abortController.signal);
24
22
  }
25
23
  return new Promise(() => { });
26
24
  }
27
- async function startPreviewMode(c) {
28
- const { dir, files } = await buildWebsite(c);
29
- const frontend = createBuiltDistFilesFetcher(dir, files);
30
- createWebServer(PORT, frontend).listen(PORT);
31
- console.log(`preview is live at http://127.0.0.1:${PORT}`);
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
+ });
33
+ }
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);
32
44
  }
33
45
  // todo changing partials triggers update on html pages
34
- async function startDevMode(c, signal) {
35
- const watchDir = join('build', 'watch');
36
- await mkdir(watchDir, { recursive: true });
37
- const clientJS = await loadClientJS();
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);
38
50
  const pagesByUrlPath = {};
51
+ const partialsByUrlPath = {};
39
52
  const entryPointsByUrlPath = {};
40
53
  let buildContext = null;
54
+ registry.on('workers', resetBuildContext);
41
55
  watch('dank.config.ts', signal, async () => {
42
56
  let updated;
43
57
  try {
@@ -47,17 +61,18 @@ async function startDevMode(c, signal) {
47
61
  return;
48
62
  }
49
63
  const prevPages = new Set(Object.keys(pagesByUrlPath));
50
- await Promise.all(Object.entries(updated.pages).map(async ([urlPath, srcPath]) => {
51
- c.pages[urlPath] = srcPath;
52
- if (pagesByUrlPath[urlPath]) {
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 {
53
71
  prevPages.delete(urlPath);
54
- if (pagesByUrlPath[urlPath].srcPath !== srcPath) {
72
+ if (pagesByUrlPath[urlPath].fsPath !== srcPath) {
55
73
  await updatePage(urlPath);
56
74
  }
57
75
  }
58
- else {
59
- await addPage(urlPath, srcPath);
60
- }
61
76
  }));
62
77
  for (const prevPage of Array.from(prevPages)) {
63
78
  delete c.pages[prevPage];
@@ -65,84 +80,101 @@ async function startDevMode(c, signal) {
65
80
  }
66
81
  updateDevServices(updated);
67
82
  });
68
- watch('pages', signal, filename => {
83
+ watch(serve.dirs.pages, signal, filename => {
69
84
  if (extname(filename) === '.html') {
70
85
  for (const [urlPath, srcPath] of Object.entries(c.pages)) {
71
86
  if (srcPath === filename) {
72
87
  updatePage(urlPath);
73
88
  }
74
89
  }
90
+ for (const [urlPath, partials] of Object.entries(partialsByUrlPath)) {
91
+ if (partials.includes(filename)) {
92
+ updatePage(urlPath, filename);
93
+ }
94
+ }
75
95
  }
76
96
  });
77
- await Promise.all(Object.entries(c.pages).map(([urlPath, srcPath]) => addPage(urlPath, srcPath)));
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
+ }));
78
102
  async function addPage(urlPath, srcPath) {
79
- const metadata = await processWebpage({
80
- clientJS,
81
- outDir: watchDir,
82
- pagesDir: 'pages',
83
- srcPath,
84
- urlPath,
103
+ await mkdir(join(serve.dirs.buildWatch, urlPath), { recursive: true });
104
+ const htmlEntrypoint = (pagesByUrlPath[urlPath] = new HtmlEntrypoint(serve, 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
+ }
85
112
  });
86
- pagesByUrlPath[urlPath] = metadata;
87
- entryPointsByUrlPath[urlPath] = new Set(metadata.entryPoints.map(e => e.in));
88
- if (buildContext !== null) {
89
- resetBuildContext();
90
- }
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));
91
121
  }
92
122
  function deletePage(urlPath) {
123
+ pagesByUrlPath[urlPath].removeAllListeners();
93
124
  delete pagesByUrlPath[urlPath];
94
125
  delete entryPointsByUrlPath[urlPath];
95
126
  resetBuildContext();
96
127
  }
97
- async function updatePage(urlPath) {
98
- const update = await processWebpage({
99
- clientJS,
100
- outDir: watchDir,
101
- pagesDir: 'pages',
102
- srcPath: c.pages[urlPath],
103
- urlPath,
104
- });
105
- const entryPointUrls = new Set(update.entryPoints.map(e => e.in));
106
- if (!hasSameValues(entryPointUrls, entryPointsByUrlPath[urlPath])) {
107
- entryPointsByUrlPath[urlPath] = entryPointUrls;
108
- resetBuildContext();
109
- }
128
+ async function updatePage(urlPath, partial) {
129
+ pagesByUrlPath[urlPath].emit('change', partial);
110
130
  }
111
131
  function collectEntrypoints() {
112
- const sources = new Set();
113
- return Object.values(pagesByUrlPath)
114
- .flatMap(({ entryPoints }) => entryPoints)
132
+ const unique = new Set();
133
+ const pageBundles = Object.values(entryPointsByUrlPath)
134
+ .flatMap(entrypointState => entrypointState.entrypoints)
115
135
  .filter(entryPoint => {
116
- if (sources.has(entryPoint.in)) {
136
+ if (unique.has(entryPoint.in)) {
117
137
  return false;
118
138
  }
119
139
  else {
120
- sources.add(entryPoint.in);
140
+ unique.add(entryPoint.in);
121
141
  return true;
122
142
  }
123
143
  });
144
+ const workerBundles = registry.workerEntryPoints();
145
+ if (workerBundles) {
146
+ return [...pageBundles, ...workerBundles];
147
+ }
148
+ else {
149
+ return pageBundles;
150
+ }
124
151
  }
125
152
  function resetBuildContext() {
126
- if (buildContext === 'starting' || buildContext === 'dirty') {
127
- buildContext = 'dirty';
128
- return;
129
- }
130
- if (buildContext === 'disposing') {
131
- return;
153
+ switch (buildContext) {
154
+ case 'starting':
155
+ buildContext = 'dirty';
156
+ return;
157
+ case 'dirty':
158
+ case 'disposing':
159
+ return;
132
160
  }
133
161
  if (buildContext !== null) {
134
- const prev = buildContext;
162
+ const disposing = buildContext.dispose();
135
163
  buildContext = 'disposing';
136
- prev.dispose().then(() => {
164
+ disposing.then(() => {
137
165
  buildContext = null;
138
166
  resetBuildContext();
139
167
  });
140
168
  }
141
169
  else {
142
- startEsbuildWatch(collectEntrypoints()).then(ctx => {
170
+ buildContext = 'starting';
171
+ startEsbuildWatch(c, registry, serve, collectEntrypoints()).then(ctx => {
143
172
  if (buildContext === 'dirty') {
144
- buildContext = null;
145
- resetBuildContext();
173
+ buildContext = 'disposing';
174
+ ctx.dispose().then(() => {
175
+ buildContext = null;
176
+ resetBuildContext();
177
+ });
146
178
  }
147
179
  else {
148
180
  buildContext = ctx;
@@ -150,18 +182,32 @@ async function startDevMode(c, signal) {
150
182
  });
151
183
  }
152
184
  }
153
- buildContext = await startEsbuildWatch(collectEntrypoints());
154
- const frontend = createDevServeFilesFetcher({
155
- pages: c.pages,
156
- pagesDir: watchDir,
157
- proxyPort: ESBUILD_PORT,
158
- publicDir: 'public',
159
- });
160
- createWebServer(PORT, frontend).listen(PORT);
161
- console.log(`dev server is live at http://127.0.0.1:${PORT}`);
162
- startDevServices(c, signal);
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
196
+ 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);
163
209
  }
164
- function hasSameValues(a, b) {
210
+ function matchingEntrypoints(a, b) {
165
211
  if (a.size !== b.size) {
166
212
  return false;
167
213
  }
@@ -172,42 +218,21 @@ function hasSameValues(a, b) {
172
218
  }
173
219
  return true;
174
220
  }
175
- async function processWebpage(inputs) {
176
- const html = await HtmlEntrypoint.readFrom(inputs.urlPath, join(inputs.pagesDir, inputs.srcPath));
177
- await html.injectPartials();
178
- if (inputs.urlPath !== '/') {
179
- await mkdir(join(inputs.outDir, inputs.urlPath), { recursive: true });
180
- }
181
- const entryPoints = [];
182
- html.collectScripts().forEach(scriptImport => {
183
- entryPoints.push({
184
- in: scriptImport.in,
185
- out: scriptImport.out,
186
- });
187
- });
188
- html.rewriteHrefs();
189
- html.appendScript(inputs.clientJS);
190
- await html.writeTo(inputs.outDir);
191
- return {
192
- entryPoints,
193
- srcPath: inputs.srcPath,
194
- urlPath: inputs.urlPath,
195
- };
196
- }
197
- async function startEsbuildWatch(entryPoints) {
198
- const ctx = await esbuildDevContext(createGlobalDefinitions(), entryPoints, 'build/watch');
221
+ async function startEsbuildWatch(c, registry, serve, entryPoints) {
222
+ const ctx = await esbuildDevContext(serve, registry, createGlobalDefinitions(serve), entryPoints, c.esbuild);
199
223
  await ctx.watch();
200
224
  await ctx.serve({
201
225
  host: '127.0.0.1',
202
- port: ESBUILD_PORT,
226
+ port: serve.esbuildPort,
203
227
  cors: {
204
- origin: 'http://127.0.0.1:' + PORT,
228
+ origin: ['127.0.0.1', 'localhost'].map(hostname => `http://${hostname}:${serve.dankPort}`),
205
229
  },
206
230
  });
207
231
  return ctx;
208
232
  }
209
- async function loadClientJS() {
210
- return await readFile(resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')), 'utf-8');
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}`);
211
236
  }
212
237
  async function watch(p, signal, fire) {
213
238
  const delayFire = 90;
@@ -12,6 +12,13 @@ export function startDevServices(c, _signal) {
12
12
  running.push({ s, process: startService(s) });
13
13
  }
14
14
  }
15
+ return {
16
+ http: {
17
+ get running() {
18
+ return running.map(({ s }) => s.http).filter(http => !!http);
19
+ },
20
+ },
21
+ };
15
22
  }
16
23
  export function updateDevServices(c) {
17
24
  if (!c.services?.length) {
@@ -130,8 +137,12 @@ function startService(s) {
130
137
  const stderrLabel = logLabel(s.cwd, cmd, args, 31);
131
138
  spawned.stderr.on('data', chunk => printChunk(stderrLabel, chunk));
132
139
  spawned.on('error', e => {
133
- const cause = 'code' in e && e.code === 'ENOENT' ? 'program not found' : e.message;
134
- opPrint(s, 'error: ' + cause);
140
+ if (e.name !== 'AbortError') {
141
+ const cause = 'code' in e && e.code === 'ENOENT'
142
+ ? 'program not found'
143
+ : e.message;
144
+ opPrint(s, 'error: ' + cause);
145
+ }
135
146
  removeFromRunning(s);
136
147
  });
137
148
  spawned.on('exit', () => {
@@ -1,10 +1,27 @@
1
+ import type { Plugin as EsbuildPlugin } from 'esbuild';
1
2
  export type DankConfig = {
2
- pages: Record<`/${string}`, `${string}.html`>;
3
+ esbuild?: EsbuildConfig;
4
+ pages: Record<`/${string}`, `${string}.html` | PageMapping>;
5
+ port?: number;
6
+ previewPort?: number;
3
7
  services?: Array<DevService>;
4
8
  };
9
+ export type PageMapping = {
10
+ pattern?: RegExp;
11
+ webpage: `${string}.html`;
12
+ };
5
13
  export type DevService = {
6
14
  command: string;
7
15
  cwd?: string;
8
16
  env?: Record<string, string>;
17
+ http?: {
18
+ port: number;
19
+ };
20
+ };
21
+ export type EsbuildConfig = {
22
+ loaders?: Record<`.${string}`, EsbuildLoader>;
23
+ plugins?: Array<EsbuildPlugin>;
24
+ port?: number;
9
25
  };
26
+ export type EsbuildLoader = 'base64' | 'binary' | 'copy' | 'dataurl' | 'empty' | 'file' | 'json' | 'text';
10
27
  export declare function defineConfig(c: Partial<DankConfig>): Promise<DankConfig>;