@eighty4/dank 0.0.4-2 → 0.0.4

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/serve.ts CHANGED
@@ -1,59 +1,41 @@
1
- import {
2
- mkdir,
3
- readFile,
4
- rm,
5
- watch as _watch,
6
- writeFile,
7
- } from 'node:fs/promises'
8
- import { extname, join, resolve } from 'node:path'
1
+ import { mkdir, rm, writeFile } from 'node:fs/promises'
2
+ import { extname, join } from 'node:path'
9
3
  import type { BuildContext } from 'esbuild'
10
4
  import { buildWebsite } from './build.ts'
11
- import { loadConfig } from './config.ts'
12
- import type { DankConfig } from './dank.ts'
5
+ import { loadConfig, type ResolvedDankConfig } from './config.ts'
13
6
  import { createGlobalDefinitions } from './define.ts'
14
7
  import { LOG } from './developer.ts'
15
- import { esbuildDevContext, type EntryPoint } from './esbuild.ts'
16
- import { resolveServeFlags, type DankServe } from './flags.ts'
17
- import { HtmlEntrypoint } from './html.ts'
8
+ import { esbuildDevContext } from './esbuild.ts'
9
+ import type { HtmlEntrypoint } from './html.ts'
18
10
  import {
19
11
  createBuiltDistFilesFetcher,
20
12
  createDevServeFilesFetcher,
21
13
  startWebServer,
22
- type PageRouteState,
23
- type UrlRewrite,
24
14
  } from './http.ts'
25
- import { WebsiteRegistry } from './metadata.ts'
15
+ import { WebsiteRegistry, type UrlRewrite } from './registry.ts'
26
16
  import { startDevServices, updateDevServices } from './services.ts'
17
+ import { watch } from './watch.ts'
18
+
19
+ let c: ResolvedDankConfig
27
20
 
28
- export async function serveWebsite(c: DankConfig): Promise<never> {
29
- const serve = resolveServeFlags(c)
30
- await rm(serve.dirs.buildRoot, { force: true, recursive: true })
21
+ export async function serveWebsite(): Promise<never> {
22
+ c = await loadConfig('serve', process.cwd())
23
+ await rm(c.dirs.buildRoot, { force: true, recursive: true })
31
24
  const abortController = new AbortController()
32
25
  process.once('exit', () => abortController.abort())
33
- if (serve.preview) {
34
- await startPreviewMode(c, serve, abortController.signal)
26
+ if (c.flags.preview) {
27
+ await startPreviewMode(abortController.signal)
35
28
  } else {
36
- await startDevMode(c, serve, abortController.signal)
29
+ await startDevMode(abortController.signal)
37
30
  }
38
31
  return new Promise(() => {})
39
32
  }
40
33
 
41
- async function startPreviewMode(
42
- c: DankConfig,
43
- serve: DankServe,
44
- signal: AbortSignal,
45
- ) {
34
+ async function startPreviewMode(signal: AbortSignal) {
46
35
  const manifest = await buildWebsite(c)
47
- const frontend = createBuiltDistFilesFetcher(serve.dirs.buildDist, manifest)
48
- const devServices = startDevServices(c, signal)
49
- startWebServer(serve, frontend, devServices.http, {
50
- urls: Object.keys(c.pages),
51
- urlRewrites: collectUrlRewrites(c),
52
- })
53
- }
54
-
55
- function collectUrlRewrites(c: DankConfig): Array<UrlRewrite> {
56
- return Object.keys(c.pages)
36
+ const frontend = createBuiltDistFilesFetcher(c.dirs, manifest)
37
+ const devServices = startDevServices(c.services, signal)
38
+ const urlRewrites: Array<UrlRewrite> = Object.keys(c.pages)
57
39
  .sort()
58
40
  .map(url => {
59
41
  const mapping = c.pages[url as `/${string}`]
@@ -62,6 +44,14 @@ function collectUrlRewrites(c: DankConfig): Array<UrlRewrite> {
62
44
  : { url, pattern: mapping.pattern }
63
45
  })
64
46
  .filter(mapping => mapping !== null)
47
+ startWebServer(
48
+ c.dankPort,
49
+ c.flags,
50
+ c.dirs,
51
+ { urlRewrites },
52
+ frontend,
53
+ devServices.http,
54
+ )
65
55
  }
66
56
 
67
57
  type BuildContextState =
@@ -71,92 +61,22 @@ type BuildContextState =
71
61
  | 'disposing'
72
62
  | null
73
63
 
74
- type EntrypointsState = {
75
- entrypoints: Array<EntryPoint>
76
- pathsIn: Set<string>
77
- }
78
-
79
- // todo changing partials triggers update on html pages
80
- async function startDevMode(
81
- c: DankConfig,
82
- serve: DankServe,
83
- signal: AbortSignal,
84
- ) {
85
- await mkdir(serve.dirs.buildWatch, { recursive: true })
86
- const registry = new WebsiteRegistry(serve)
87
- const clientJS = await loadClientJS(serve.esbuildPort)
88
- const pagesByUrlPath: Record<string, HtmlEntrypoint> = {}
89
- const partialsByUrlPath: Record<string, Array<string>> = {}
90
- const entryPointsByUrlPath: Record<string, EntrypointsState> = {}
64
+ async function startDevMode(signal: AbortSignal) {
65
+ const registry = new WebsiteRegistry(c)
66
+ await mkdir(c.dirs.buildWatch, { recursive: true })
91
67
  let buildContext: BuildContextState = null
92
68
 
93
- registry.on('workers', () => {
94
- LOG({
95
- realm: 'serve',
96
- message: 'registry updated worker entrypoints',
97
- data: {
98
- workers: registry.workerEntryPoints()?.map(ep => ep.in) || null,
99
- },
100
- })
101
- resetBuildContext()
102
- })
103
-
104
69
  watch('dank.config.ts', signal, async () => {
105
- let updated: DankConfig
106
70
  try {
107
- updated = await loadConfig('serve')
71
+ await c.reload()
108
72
  } catch (ignore) {
109
73
  return
110
74
  }
111
- const prevPages = new Set(Object.keys(pagesByUrlPath))
112
- await Promise.all(
113
- Object.entries(updated.pages).map(async ([urlPath, mapping]) => {
114
- c.pages[urlPath as `/${string}`] = mapping
115
- const srcPath =
116
- typeof mapping === 'string' ? mapping : mapping.webpage
117
- if (!pagesByUrlPath[urlPath]) {
118
- LOG({
119
- realm: 'config',
120
- message: 'added page',
121
- data: {
122
- urlPath,
123
- srcPath,
124
- },
125
- })
126
- await addPage(urlPath, srcPath)
127
- } else {
128
- prevPages.delete(urlPath)
129
- if (pagesByUrlPath[urlPath].fsPath !== srcPath) {
130
- LOG({
131
- realm: 'config',
132
- message: 'updated page src',
133
- data: {
134
- urlPath,
135
- newSrcPath: srcPath,
136
- oldSrcPath: pagesByUrlPath[urlPath].fsPath,
137
- },
138
- })
139
- await updatePage(urlPath)
140
- }
141
- }
142
- }),
143
- )
144
- for (const prevPage of Array.from(prevPages)) {
145
- LOG({
146
- realm: 'config',
147
- message: 'removed page',
148
- data: {
149
- urlPath: prevPage,
150
- srcPath: c.pages[prevPage as `/${string}`] as string,
151
- },
152
- })
153
- delete c.pages[prevPage as `/${string}`]
154
- deletePage(prevPage)
155
- }
156
- updateDevServices(updated)
75
+ registry.configSync()
76
+ updateDevServices(c.services)
157
77
  })
158
78
 
159
- watch(serve.dirs.pages, signal, filename => {
79
+ watch(c.dirs.pages, signal, filename => {
160
80
  LOG({
161
81
  realm: 'serve',
162
82
  message: 'pages dir watch event',
@@ -165,132 +85,15 @@ async function startDevMode(
165
85
  },
166
86
  })
167
87
  if (extname(filename) === '.html') {
168
- for (const [urlPath, srcPath] of Object.entries(c.pages)) {
169
- if (srcPath === filename) {
170
- updatePage(urlPath)
171
- }
172
- }
173
- for (const [urlPath, partials] of Object.entries(
174
- partialsByUrlPath,
175
- )) {
176
- if (partials.includes(filename)) {
177
- updatePage(urlPath, filename)
178
- }
179
- }
180
- }
181
- })
182
-
183
- await Promise.all(
184
- Object.entries(c.pages).map(async ([urlPath, mapping]) => {
185
- const srcPath =
186
- typeof mapping === 'string' ? mapping : mapping.webpage
187
- await addPage(urlPath, srcPath)
188
- return new Promise(res =>
189
- pagesByUrlPath[urlPath].once('entrypoints', res),
190
- )
191
- }),
192
- )
193
-
194
- async function addPage(urlPath: string, srcPath: string) {
195
- await mkdir(join(serve.dirs.buildWatch, urlPath), { recursive: true })
196
- const htmlEntrypoint = (pagesByUrlPath[urlPath] = new HtmlEntrypoint(
197
- serve,
198
- registry.resolver,
199
- urlPath,
200
- srcPath,
201
- [{ type: 'script', js: clientJS }],
202
- ))
203
- htmlEntrypoint.on('entrypoints', entrypoints => {
204
- const pathsIn = new Set(entrypoints.map(e => e.in))
205
- if (
206
- !entryPointsByUrlPath[urlPath] ||
207
- !matchingEntrypoints(
208
- entryPointsByUrlPath[urlPath].pathsIn,
209
- pathsIn,
210
- )
211
- ) {
212
- LOG({
213
- realm: 'serve',
214
- message: 'html entrypoints event',
215
- data: {
216
- previous:
217
- entryPointsByUrlPath[urlPath]?.pathsIn || null,
218
- new: pathsIn,
219
- },
220
- })
221
- entryPointsByUrlPath[urlPath] = { entrypoints, pathsIn }
222
- resetBuildContext()
223
- }
224
- })
225
- htmlEntrypoint.on('partial', partial => {
226
- LOG({
227
- realm: 'serve',
228
- message: 'html partial event',
229
- data: {
230
- webpage: htmlEntrypoint.fsPath,
231
- partial,
232
- },
233
- })
234
- if (!partialsByUrlPath[urlPath]) {
235
- partialsByUrlPath[urlPath] = []
236
- }
237
- partialsByUrlPath[urlPath].push(partial)
238
- })
239
- htmlEntrypoint.on('partials', partials => {
240
- LOG({
241
- realm: 'serve',
242
- message: 'html partials event',
243
- data: {
244
- allPartials: partials.length === 0,
245
- partials,
246
- },
247
- })
248
- partialsByUrlPath[urlPath] = partials
249
- })
250
- htmlEntrypoint.on('output', html => {
251
- const path = join(serve.dirs.buildWatch, urlPath, 'index.html')
252
- LOG({
253
- realm: 'serve',
254
- message: 'html output event',
255
- data: {
256
- webpage: htmlEntrypoint.fsPath,
257
- path,
258
- },
259
- })
260
- writeFile(path, html)
261
- })
262
- }
263
-
264
- function deletePage(urlPath: string) {
265
- pagesByUrlPath[urlPath].removeAllListeners()
266
- delete pagesByUrlPath[urlPath]
267
- delete entryPointsByUrlPath[urlPath]
268
- resetBuildContext()
269
- }
270
-
271
- async function updatePage(urlPath: string, partial?: string) {
272
- pagesByUrlPath[urlPath].emit('change', partial)
273
- }
274
-
275
- function collectEntrypoints(): Array<EntryPoint> {
276
- const unique: Set<string> = new Set()
277
- const pageBundles = Object.values(entryPointsByUrlPath)
278
- .flatMap(entrypointState => entrypointState.entrypoints)
279
- .filter(entryPoint => {
280
- if (unique.has(entryPoint.in)) {
281
- return false
282
- } else {
283
- unique.add(entryPoint.in)
284
- return true
88
+ registry.htmlEntrypoints.forEach(html => {
89
+ if (html.fsPath === filename) {
90
+ html.emit('change')
91
+ } else if (html.usesPartial(filename)) {
92
+ html.emit('change', filename)
285
93
  }
286
94
  })
287
- const workerBundles = registry.workerEntryPoints()
288
- if (workerBundles) {
289
- return [...pageBundles, ...workerBundles]
290
- } else {
291
- return pageBundles
292
95
  }
293
- }
96
+ })
294
97
 
295
98
  function resetBuildContext() {
296
99
  switch (buildContext) {
@@ -311,68 +114,63 @@ async function startDevMode(
311
114
  })
312
115
  } else {
313
116
  buildContext = 'starting'
314
- startEsbuildWatch(c, registry, serve, collectEntrypoints()).then(
315
- ctx => {
316
- if (buildContext === 'dirty') {
317
- buildContext = 'disposing'
318
- ctx.dispose().then(() => {
319
- buildContext = null
320
- resetBuildContext()
321
- })
322
- } else {
323
- buildContext = ctx
324
- }
325
- },
326
- )
117
+ startEsbuildWatch(registry).then(ctx => {
118
+ if (buildContext === 'dirty') {
119
+ buildContext = 'disposing'
120
+ ctx.dispose().then(() => {
121
+ buildContext = null
122
+ resetBuildContext()
123
+ })
124
+ } else {
125
+ buildContext = ctx
126
+ }
127
+ })
327
128
  }
328
129
  }
329
130
 
330
- // function removePartialFromPage(partial: string, urlPath: string) {
331
- // const deleteIndex = urlPathsByPartials[partial].indexOf(urlPath)
332
- // if (deleteIndex !== -1) {
333
- // if (urlPathsByPartials[partial].length === 1) {
334
- // delete urlPathsByPartials[partial]
335
- // } else {
336
- // urlPathsByPartials[partial].splice(deleteIndex, 1)
337
- // }
338
- // }
339
- // }
131
+ registry.on('webpage', html => {
132
+ html.on('error', e =>
133
+ console.log(`\u001b[31merror:\u001b[0m`, e.message),
134
+ )
135
+ html.on('output', output => writeHtml(html, output))
136
+ })
137
+
138
+ registry.on('workers', () => {
139
+ LOG({
140
+ realm: 'serve',
141
+ message: 'registry updated worker entrypoints',
142
+ data: {
143
+ workers: registry.workerEntryPoints?.map(ep => ep.in) || null,
144
+ },
145
+ })
146
+ resetBuildContext()
147
+ })
148
+
149
+ registry.configSync()
150
+ await Promise.all(registry.htmlEntrypoints.map(html => html.process()))
151
+
152
+ // listen for entrypoint diffs after processing webpages
153
+ registry.on('entrypoints', () => resetBuildContext())
340
154
 
341
155
  // inital start of esbuild ctx
342
156
  resetBuildContext()
343
157
 
344
- // todo this page route state could be built on change and reused
345
- const pageRoutes: PageRouteState = {
346
- get urls(): Array<string> {
347
- return Object.keys(c.pages)
348
- },
349
- get urlRewrites(): Array<UrlRewrite> {
350
- return collectUrlRewrites(c)
351
- },
352
- }
353
- const frontend = createDevServeFilesFetcher(pageRoutes, serve)
354
- const devServices = startDevServices(c, signal)
355
- startWebServer(serve, frontend, devServices.http, pageRoutes)
356
- }
357
-
358
- function matchingEntrypoints(a: Set<string>, b: Set<string>): boolean {
359
- if (a.size !== b.size) {
360
- return false
361
- }
362
- for (const v in a) {
363
- if (!b.has(v)) {
364
- return false
365
- }
366
- }
367
- return true
158
+ const frontend = createDevServeFilesFetcher(c.esbuildPort, c.dirs, registry)
159
+ const devServices = startDevServices(c.services, signal)
160
+ startWebServer(
161
+ c.dankPort,
162
+ c.flags,
163
+ c.dirs,
164
+ registry,
165
+ frontend,
166
+ devServices.http,
167
+ )
368
168
  }
369
169
 
370
170
  async function startEsbuildWatch(
371
- c: DankConfig,
372
171
  registry: WebsiteRegistry,
373
- serve: DankServe,
374
- entryPoints: Array<EntryPoint>,
375
172
  ): Promise<BuildContext> {
173
+ const entryPoints = registry.webpageAndWorkerEntryPoints
376
174
  LOG({
377
175
  realm: 'serve',
378
176
  message: 'starting esbuild watch',
@@ -381,21 +179,19 @@ async function startEsbuildWatch(
381
179
  },
382
180
  })
383
181
  const ctx = await esbuildDevContext(
384
- serve,
385
182
  registry,
386
- createGlobalDefinitions(serve),
183
+ createGlobalDefinitions(c),
387
184
  entryPoints,
388
- c.esbuild,
389
185
  )
390
186
 
391
187
  await ctx.watch()
392
188
 
393
189
  await ctx.serve({
394
190
  host: '127.0.0.1',
395
- port: serve.esbuildPort,
191
+ port: c.esbuildPort,
396
192
  cors: {
397
193
  origin: ['127.0.0.1', 'localhost'].map(
398
- hostname => `http://${hostname}:${serve.dankPort}`,
194
+ hostname => `http://${hostname}:${c.dankPort}`,
399
195
  ),
400
196
  },
401
197
  })
@@ -403,48 +199,17 @@ async function startEsbuildWatch(
403
199
  return ctx
404
200
  }
405
201
 
406
- async function loadClientJS(esbuildPort: number) {
407
- const clientJS = await readFile(
408
- resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')),
409
- 'utf-8',
410
- )
411
- return clientJS.replace('3995', `${esbuildPort}`)
412
- }
413
-
414
- async function watch(
415
- p: string,
416
- signal: AbortSignal,
417
- fire: (filename: string) => void,
418
- ) {
419
- const delayFire = 90
420
- const timeout = 100
421
- let changes: Record<string, number> = {}
422
- try {
423
- for await (const { filename } of _watch(p, {
424
- recursive: true,
425
- signal,
426
- })) {
427
- if (filename) {
428
- if (!changes[filename]) {
429
- const now = Date.now()
430
- changes[filename] = now + delayFire
431
- setTimeout(() => {
432
- const now = Date.now()
433
- for (const [filename, then] of Object.entries(
434
- changes,
435
- )) {
436
- if (then <= now) {
437
- fire(filename)
438
- delete changes[filename]
439
- }
440
- }
441
- }, timeout)
442
- }
443
- }
444
- }
445
- } catch (e: any) {
446
- if (e.name !== 'AbortError') {
447
- throw e
448
- }
449
- }
202
+ async function writeHtml(html: HtmlEntrypoint, output: string) {
203
+ const dir = join(c.dirs.buildWatch, html.url)
204
+ await mkdir(dir, { recursive: true })
205
+ const path = join(dir, 'index.html')
206
+ LOG({
207
+ realm: 'serve',
208
+ message: 'writing html output',
209
+ data: {
210
+ webpage: html.fsPath,
211
+ path,
212
+ },
213
+ })
214
+ await writeFile(path, output)
450
215
  }
package/lib/services.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type ChildProcess, spawn } from 'node:child_process'
2
2
  import { basename, isAbsolute, resolve } from 'node:path'
3
- import type { DankConfig, DevService } from './dank.ts'
3
+ import type { DevService, ResolvedDankConfig } from './config.ts'
4
4
 
5
5
  export type DevServices = {
6
6
  http: HttpServices
@@ -24,26 +24,26 @@ let updating: null | {
24
24
  } = null
25
25
 
26
26
  export function startDevServices(
27
- c: DankConfig,
27
+ services: ResolvedDankConfig['services'],
28
28
  _signal: AbortSignal,
29
29
  ): DevServices {
30
30
  signal = _signal
31
- if (c.services?.length) {
32
- for (const s of c.services) {
31
+ if (services?.length) {
32
+ for (const s of services) {
33
33
  running.push({ s, process: startService(s) })
34
34
  }
35
35
  }
36
36
  return {
37
37
  http: {
38
- get running(): Array<NonNullable<DevService['http']>> {
38
+ get running(): Array<HttpService> {
39
39
  return running.map(({ s }) => s.http).filter(http => !!http)
40
40
  },
41
41
  },
42
42
  }
43
43
  }
44
44
 
45
- export function updateDevServices(c: DankConfig) {
46
- if (!c.services?.length) {
45
+ export function updateDevServices(services: ResolvedDankConfig['services']) {
46
+ if (!services?.length) {
47
47
  if (running.length) {
48
48
  if (updating === null) {
49
49
  updating = { stopping: [], starting: [] }
@@ -63,7 +63,7 @@ export function updateDevServices(c: DankConfig) {
63
63
  }
64
64
  const keep = []
65
65
  const next: Array<DevService> = []
66
- for (const s of c.services) {
66
+ for (const s of services) {
67
67
  let found = false
68
68
  for (let i = 0; i < running.length; i++) {
69
69
  const p = running[i].s
package/lib/watch.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { watch as createWatch } from 'node:fs/promises'
2
+
3
+ export async function watch(
4
+ p: string,
5
+ signal: AbortSignal,
6
+ fire: (filename: string) => void,
7
+ ) {
8
+ const delayFire = 90
9
+ const timeout = 100
10
+ let changes: Record<string, number> = {}
11
+ try {
12
+ for await (const { filename } of createWatch(p, {
13
+ recursive: true,
14
+ signal,
15
+ })) {
16
+ if (filename) {
17
+ if (!changes[filename]) {
18
+ const now = Date.now()
19
+ changes[filename] = now + delayFire
20
+ setTimeout(() => {
21
+ const now = Date.now()
22
+ for (const [filename, then] of Object.entries(
23
+ changes,
24
+ )) {
25
+ if (then <= now) {
26
+ fire(filename)
27
+ delete changes[filename]
28
+ }
29
+ }
30
+ }, timeout)
31
+ }
32
+ }
33
+ }
34
+ } catch (e: any) {
35
+ if (e.name !== 'AbortError') {
36
+ throw e
37
+ }
38
+ }
39
+ }
package/lib_js/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { buildWebsite } from "./build.js";
3
- import { loadConfig } from "./config.js";
3
+ import { DankError } from "./errors.js";
4
4
  import { serveWebsite } from "./serve.js";
5
5
  function printHelp(task2) {
6
6
  if (!task2 || task2 === "build") {
@@ -65,28 +65,26 @@ const task = (function resolveTask() {
65
65
  }
66
66
  return task2;
67
67
  })();
68
- const c = await loadConfig(task);
69
68
  try {
70
69
  switch (task) {
71
70
  case "build":
72
- await buildWebsite(c);
71
+ await buildWebsite();
73
72
  console.log(green("done"));
74
73
  process.exit(0);
75
74
  case "serve":
76
- await serveWebsite(c);
75
+ await serveWebsite();
77
76
  }
78
77
  } catch (e) {
79
78
  errorExit(e);
80
79
  }
81
80
  function printError(e) {
82
81
  if (e !== null) {
83
- if (typeof e === "string") {
84
- console.error(red("error:"), e);
85
- } else if (e instanceof Error) {
82
+ if (e instanceof DankError) {
86
83
  console.error(red("error:"), e.message);
87
- if (e.stack) {
88
- console.error(e.stack);
89
- }
84
+ } else if (e instanceof Error) {
85
+ console.error(red("error:"), e.stack ?? e.message);
86
+ } else {
87
+ console.error(red("error:"), e);
90
88
  }
91
89
  }
92
90
  }