@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.
package/lib/serve.ts CHANGED
@@ -1,58 +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
- import { esbuildDevContext, type EntryPoint } from './esbuild.ts'
15
- import { resolveServeFlags, type DankServe } from './flags.ts'
16
- import { HtmlEntrypoint } from './html.ts'
7
+ import { LOG } from './developer.ts'
8
+ import { esbuildDevContext } from './esbuild.ts'
9
+ import type { HtmlEntrypoint } from './html.ts'
17
10
  import {
18
11
  createBuiltDistFilesFetcher,
19
12
  createDevServeFilesFetcher,
20
13
  startWebServer,
21
- type PageRouteState,
22
- type UrlRewrite,
23
14
  } from './http.ts'
24
- import { WebsiteRegistry } from './metadata.ts'
15
+ import { WebsiteRegistry, type UrlRewrite } from './registry.ts'
25
16
  import { startDevServices, updateDevServices } from './services.ts'
17
+ import { watch } from './watch.ts'
18
+
19
+ let c: ResolvedDankConfig
26
20
 
27
- export async function serveWebsite(c: DankConfig): Promise<never> {
28
- const serve = resolveServeFlags(c)
29
- 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 })
30
24
  const abortController = new AbortController()
31
25
  process.once('exit', () => abortController.abort())
32
- if (serve.preview) {
33
- await startPreviewMode(c, serve, abortController.signal)
26
+ if (c.flags.preview) {
27
+ await startPreviewMode(abortController.signal)
34
28
  } else {
35
- await startDevMode(c, serve, abortController.signal)
29
+ await startDevMode(abortController.signal)
36
30
  }
37
31
  return new Promise(() => {})
38
32
  }
39
33
 
40
- async function startPreviewMode(
41
- c: DankConfig,
42
- serve: DankServe,
43
- signal: AbortSignal,
44
- ) {
45
- const manifest = await buildWebsite(c, serve)
46
- const frontend = createBuiltDistFilesFetcher(serve.dirs.buildDist, manifest)
47
- const devServices = startDevServices(c, signal)
48
- startWebServer(serve, frontend, devServices.http, {
49
- urls: Object.keys(c.pages),
50
- urlRewrites: collectUrlRewrites(c),
51
- })
52
- }
53
-
54
- function collectUrlRewrites(c: DankConfig): Array<UrlRewrite> {
55
- return Object.keys(c.pages)
34
+ async function startPreviewMode(signal: AbortSignal) {
35
+ const manifest = await buildWebsite(c)
36
+ const frontend = createBuiltDistFilesFetcher(c.dirs, manifest)
37
+ const devServices = startDevServices(c.services, signal)
38
+ const urlRewrites: Array<UrlRewrite> = Object.keys(c.pages)
56
39
  .sort()
57
40
  .map(url => {
58
41
  const mapping = c.pages[url as `/${string}`]
@@ -61,6 +44,14 @@ function collectUrlRewrites(c: DankConfig): Array<UrlRewrite> {
61
44
  : { url, pattern: mapping.pattern }
62
45
  })
63
46
  .filter(mapping => mapping !== null)
47
+ startWebServer(
48
+ c.dankPort,
49
+ c.flags,
50
+ c.dirs,
51
+ { urlRewrites },
52
+ frontend,
53
+ devServices.http,
54
+ )
64
55
  }
65
56
 
66
57
  type BuildContextState =
@@ -70,152 +61,39 @@ type BuildContextState =
70
61
  | 'disposing'
71
62
  | null
72
63
 
73
- type EntrypointsState = {
74
- entrypoints: Array<EntryPoint>
75
- pathsIn: Set<string>
76
- }
77
-
78
- // todo changing partials triggers update on html pages
79
- async function startDevMode(
80
- c: DankConfig,
81
- serve: DankServe,
82
- signal: AbortSignal,
83
- ) {
84
- await mkdir(serve.dirs.buildWatch, { recursive: true })
85
- const registry = new WebsiteRegistry(serve)
86
- const clientJS = await loadClientJS(serve.esbuildPort)
87
- const pagesByUrlPath: Record<string, HtmlEntrypoint> = {}
88
- const partialsByUrlPath: Record<string, Array<string>> = {}
89
- 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 })
90
67
  let buildContext: BuildContextState = null
91
68
 
92
- registry.on('workers', resetBuildContext)
93
-
94
69
  watch('dank.config.ts', signal, async () => {
95
- let updated: DankConfig
96
70
  try {
97
- updated = await loadConfig()
71
+ await c.reload()
98
72
  } catch (ignore) {
99
73
  return
100
74
  }
101
- const prevPages = new Set(Object.keys(pagesByUrlPath))
102
- await Promise.all(
103
- Object.entries(updated.pages).map(async ([urlPath, mapping]) => {
104
- c.pages[urlPath as `/${string}`] = mapping
105
- const srcPath =
106
- typeof mapping === 'string' ? mapping : mapping.webpage
107
- if (!pagesByUrlPath[urlPath]) {
108
- await addPage(urlPath, srcPath)
109
- } else {
110
- prevPages.delete(urlPath)
111
- if (pagesByUrlPath[urlPath].fsPath !== srcPath) {
112
- await updatePage(urlPath)
113
- }
114
- }
115
- }),
116
- )
117
- for (const prevPage of Array.from(prevPages)) {
118
- delete c.pages[prevPage as `/${string}`]
119
- deletePage(prevPage)
120
- }
121
- updateDevServices(updated)
75
+ registry.configSync()
76
+ updateDevServices(c.services)
122
77
  })
123
78
 
124
- watch(serve.dirs.pages, signal, filename => {
125
- if (extname(filename) === '.html') {
126
- for (const [urlPath, srcPath] of Object.entries(c.pages)) {
127
- if (srcPath === filename) {
128
- updatePage(urlPath)
129
- }
130
- }
131
- for (const [urlPath, partials] of Object.entries(
132
- partialsByUrlPath,
133
- )) {
134
- if (partials.includes(filename)) {
135
- updatePage(urlPath, filename)
136
- }
137
- }
138
- }
139
- })
140
-
141
- await Promise.all(
142
- Object.entries(c.pages).map(async ([urlPath, mapping]) => {
143
- const srcPath =
144
- typeof mapping === 'string' ? mapping : mapping.webpage
145
- await addPage(urlPath, srcPath)
146
- return new Promise(res =>
147
- pagesByUrlPath[urlPath].once('entrypoints', res),
148
- )
149
- }),
150
- )
151
-
152
- async function addPage(urlPath: string, srcPath: string) {
153
- await mkdir(join(serve.dirs.buildWatch, urlPath), { recursive: true })
154
- const htmlEntrypoint = (pagesByUrlPath[urlPath] = new HtmlEntrypoint(
155
- serve,
156
- registry.resolver,
157
- urlPath,
158
- srcPath,
159
- [{ type: 'script', js: clientJS }],
160
- ))
161
- htmlEntrypoint.on('entrypoints', entrypoints => {
162
- const pathsIn = new Set(entrypoints.map(e => e.in))
163
- if (
164
- !entryPointsByUrlPath[urlPath] ||
165
- !matchingEntrypoints(
166
- entryPointsByUrlPath[urlPath].pathsIn,
167
- pathsIn,
168
- )
169
- ) {
170
- entryPointsByUrlPath[urlPath] = { entrypoints, pathsIn }
171
- resetBuildContext()
172
- }
173
- })
174
- htmlEntrypoint.on('partial', partial => {
175
- if (!partialsByUrlPath[urlPath]) {
176
- partialsByUrlPath[urlPath] = []
177
- }
178
- partialsByUrlPath[urlPath].push(partial)
79
+ watch(c.dirs.pages, signal, filename => {
80
+ LOG({
81
+ realm: 'serve',
82
+ message: 'pages dir watch event',
83
+ data: {
84
+ file: filename,
85
+ },
179
86
  })
180
- htmlEntrypoint.on(
181
- 'partials',
182
- partials => (partialsByUrlPath[urlPath] = partials),
183
- )
184
- htmlEntrypoint.on('output', html =>
185
- writeFile(join(serve.dirs.buildWatch, urlPath, 'index.html'), html),
186
- )
187
- }
188
-
189
- function deletePage(urlPath: string) {
190
- pagesByUrlPath[urlPath].removeAllListeners()
191
- delete pagesByUrlPath[urlPath]
192
- delete entryPointsByUrlPath[urlPath]
193
- resetBuildContext()
194
- }
195
-
196
- async function updatePage(urlPath: string, partial?: string) {
197
- pagesByUrlPath[urlPath].emit('change', partial)
198
- }
199
-
200
- function collectEntrypoints(): Array<EntryPoint> {
201
- const unique: Set<string> = new Set()
202
- const pageBundles = Object.values(entryPointsByUrlPath)
203
- .flatMap(entrypointState => entrypointState.entrypoints)
204
- .filter(entryPoint => {
205
- if (unique.has(entryPoint.in)) {
206
- return false
207
- } else {
208
- unique.add(entryPoint.in)
209
- return true
87
+ if (extname(filename) === '.html') {
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)
210
93
  }
211
94
  })
212
- const workerBundles = registry.workerEntryPoints()
213
- if (workerBundles) {
214
- return [...pageBundles, ...workerBundles]
215
- } else {
216
- return pageBundles
217
95
  }
218
- }
96
+ })
219
97
 
220
98
  function resetBuildContext() {
221
99
  switch (buildContext) {
@@ -227,6 +105,7 @@ async function startDevMode(
227
105
  return
228
106
  }
229
107
  if (buildContext !== null) {
108
+ LOG({ realm: 'serve', message: 'disposing esbuild context' })
230
109
  const disposing = buildContext.dispose()
231
110
  buildContext = 'disposing'
232
111
  disposing.then(() => {
@@ -235,84 +114,84 @@ async function startDevMode(
235
114
  })
236
115
  } else {
237
116
  buildContext = 'starting'
238
- startEsbuildWatch(c, registry, serve, collectEntrypoints()).then(
239
- ctx => {
240
- if (buildContext === 'dirty') {
241
- buildContext = 'disposing'
242
- ctx.dispose().then(() => {
243
- buildContext = null
244
- resetBuildContext()
245
- })
246
- } else {
247
- buildContext = ctx
248
- }
249
- },
250
- )
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
+ })
251
128
  }
252
129
  }
253
130
 
254
- // function removePartialFromPage(partial: string, urlPath: string) {
255
- // const deleteIndex = urlPathsByPartials[partial].indexOf(urlPath)
256
- // if (deleteIndex !== -1) {
257
- // if (urlPathsByPartials[partial].length === 1) {
258
- // delete urlPathsByPartials[partial]
259
- // } else {
260
- // urlPathsByPartials[partial].splice(deleteIndex, 1)
261
- // }
262
- // }
263
- // }
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())
264
154
 
265
155
  // inital start of esbuild ctx
266
156
  resetBuildContext()
267
157
 
268
- // todo this page route state could be built on change and reused
269
- const pageRoutes: PageRouteState = {
270
- get urls(): Array<string> {
271
- return Object.keys(c.pages)
272
- },
273
- get urlRewrites(): Array<UrlRewrite> {
274
- return collectUrlRewrites(c)
275
- },
276
- }
277
- const frontend = createDevServeFilesFetcher(pageRoutes, serve)
278
- const devServices = startDevServices(c, signal)
279
- startWebServer(serve, frontend, devServices.http, pageRoutes)
280
- }
281
-
282
- function matchingEntrypoints(a: Set<string>, b: Set<string>): boolean {
283
- if (a.size !== b.size) {
284
- return false
285
- }
286
- for (const v in a) {
287
- if (!b.has(v)) {
288
- return false
289
- }
290
- }
291
- 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
+ )
292
168
  }
293
169
 
294
170
  async function startEsbuildWatch(
295
- c: DankConfig,
296
171
  registry: WebsiteRegistry,
297
- serve: DankServe,
298
- entryPoints: Array<EntryPoint>,
299
172
  ): Promise<BuildContext> {
173
+ const entryPoints = registry.webpageAndWorkerEntryPoints
174
+ LOG({
175
+ realm: 'serve',
176
+ message: 'starting esbuild watch',
177
+ data: {
178
+ entrypoints: entryPoints.map(ep => ep.in),
179
+ },
180
+ })
300
181
  const ctx = await esbuildDevContext(
301
- serve,
302
182
  registry,
303
- createGlobalDefinitions(serve),
183
+ createGlobalDefinitions(c),
304
184
  entryPoints,
305
- c.esbuild,
306
185
  )
307
186
 
308
187
  await ctx.watch()
309
188
 
310
189
  await ctx.serve({
311
190
  host: '127.0.0.1',
312
- port: serve.esbuildPort,
191
+ port: c.esbuildPort,
313
192
  cors: {
314
193
  origin: ['127.0.0.1', 'localhost'].map(
315
- hostname => `http://${hostname}:${serve.dankPort}`,
194
+ hostname => `http://${hostname}:${c.dankPort}`,
316
195
  ),
317
196
  },
318
197
  })
@@ -320,48 +199,17 @@ async function startEsbuildWatch(
320
199
  return ctx
321
200
  }
322
201
 
323
- async function loadClientJS(esbuildPort: number) {
324
- const clientJS = await readFile(
325
- resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')),
326
- 'utf-8',
327
- )
328
- return clientJS.replace('3995', `${esbuildPort}`)
329
- }
330
-
331
- async function watch(
332
- p: string,
333
- signal: AbortSignal,
334
- fire: (filename: string) => void,
335
- ) {
336
- const delayFire = 90
337
- const timeout = 100
338
- let changes: Record<string, number> = {}
339
- try {
340
- for await (const { filename } of _watch(p, {
341
- recursive: true,
342
- signal,
343
- })) {
344
- if (filename) {
345
- if (!changes[filename]) {
346
- const now = Date.now()
347
- changes[filename] = now + delayFire
348
- setTimeout(() => {
349
- const now = Date.now()
350
- for (const [filename, then] of Object.entries(
351
- changes,
352
- )) {
353
- if (then <= now) {
354
- fire(filename)
355
- delete changes[filename]
356
- }
357
- }
358
- }, timeout)
359
- }
360
- }
361
- }
362
- } catch (e: any) {
363
- if (e.name !== 'AbortError') {
364
- throw e
365
- }
366
- }
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)
367
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
+ }