@eighty4/dank 0.0.2 → 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.
package/lib/serve.ts CHANGED
@@ -1,55 +1,95 @@
1
- import { mkdir, readFile, rm, watch as _watch } from 'node:fs/promises'
1
+ import {
2
+ mkdir,
3
+ readFile,
4
+ rm,
5
+ watch as _watch,
6
+ writeFile,
7
+ } from 'node:fs/promises'
2
8
  import { extname, join, resolve } from 'node:path'
3
9
  import type { BuildContext } from 'esbuild'
4
10
  import { buildWebsite } from './build.ts'
5
11
  import { loadConfig } from './config.ts'
6
12
  import type { DankConfig } from './dank.ts'
7
13
  import { createGlobalDefinitions } from './define.ts'
8
- import { esbuildDevContext } from './esbuild.ts'
9
- import { isPreviewBuild } from './flags.ts'
14
+ import { esbuildDevContext, type EntryPoint } from './esbuild.ts'
15
+ import { resolveServeFlags, type DankServe } from './flags.ts'
10
16
  import { HtmlEntrypoint } from './html.ts'
11
17
  import {
12
18
  createBuiltDistFilesFetcher,
13
19
  createDevServeFilesFetcher,
14
- createWebServer,
20
+ startWebServer,
21
+ type PageRouteState,
22
+ type UrlRewrite,
15
23
  } from './http.ts'
24
+ import { WebsiteRegistry } from './metadata.ts'
16
25
  import { startDevServices, updateDevServices } from './services.ts'
17
26
 
18
- const isPreview = isPreviewBuild()
19
-
20
- // alternate port for --preview bc of service worker
21
- const PORT = isPreview ? 4000 : 3000
22
-
23
- // port for esbuild.serve
24
- const ESBUILD_PORT = 2999
25
-
26
27
  export async function serveWebsite(c: DankConfig): Promise<never> {
27
- await rm('build', { force: true, recursive: true })
28
- if (isPreview) {
29
- await startPreviewMode(c)
28
+ const serve = resolveServeFlags(c)
29
+ await rm(serve.dirs.buildRoot, { force: true, recursive: true })
30
+ const abortController = new AbortController()
31
+ process.once('exit', () => abortController.abort())
32
+ if (serve.preview) {
33
+ await startPreviewMode(c, serve, abortController.signal)
30
34
  } else {
31
- const abortController = new AbortController()
32
- await startDevMode(c, abortController.signal)
35
+ await startDevMode(c, serve, abortController.signal)
33
36
  }
34
37
  return new Promise(() => {})
35
38
  }
36
39
 
37
- async function startPreviewMode(c: DankConfig) {
38
- const { dir, files } = await buildWebsite(c)
39
- const frontend = createBuiltDistFilesFetcher(dir, files)
40
- createWebServer(PORT, frontend).listen(PORT)
41
- console.log(`preview is live at http://127.0.0.1:${PORT}`)
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)
56
+ .sort()
57
+ .map(url => {
58
+ const mapping = c.pages[url as `/${string}`]
59
+ return typeof mapping !== 'object' || !mapping.pattern
60
+ ? null
61
+ : { url, pattern: mapping.pattern }
62
+ })
63
+ .filter(mapping => mapping !== null)
64
+ }
65
+
66
+ type BuildContextState =
67
+ | BuildContext
68
+ | 'starting'
69
+ | 'dirty'
70
+ | 'disposing'
71
+ | null
72
+
73
+ type EntrypointsState = {
74
+ entrypoints: Array<EntryPoint>
75
+ pathsIn: Set<string>
42
76
  }
43
77
 
44
78
  // todo changing partials triggers update on html pages
45
- async function startDevMode(c: DankConfig, signal: AbortSignal) {
46
- const watchDir = join('build', 'watch')
47
- await mkdir(watchDir, { recursive: true })
48
- const clientJS = await loadClientJS()
49
- const pagesByUrlPath: Record<string, WebpageMetadata> = {}
50
- const entryPointsByUrlPath: Record<string, Set<string>> = {}
51
- let buildContext: BuildContext | 'starting' | 'dirty' | 'disposing' | null =
52
- null
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> = {}
90
+ let buildContext: BuildContextState = null
91
+
92
+ registry.on('workers', resetBuildContext)
53
93
 
54
94
  watch('dank.config.ts', signal, async () => {
55
95
  let updated: DankConfig
@@ -60,15 +100,17 @@ async function startDevMode(c: DankConfig, signal: AbortSignal) {
60
100
  }
61
101
  const prevPages = new Set(Object.keys(pagesByUrlPath))
62
102
  await Promise.all(
63
- Object.entries(updated.pages).map(async ([urlPath, srcPath]) => {
64
- c.pages[urlPath as `/${string}`] = srcPath
65
- if (pagesByUrlPath[urlPath]) {
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 {
66
110
  prevPages.delete(urlPath)
67
- if (pagesByUrlPath[urlPath].srcPath !== srcPath) {
111
+ if (pagesByUrlPath[urlPath].fsPath !== srcPath) {
68
112
  await updatePage(urlPath)
69
113
  }
70
- } else {
71
- await addPage(urlPath, srcPath)
72
114
  }
73
115
  }),
74
116
  )
@@ -79,114 +121,164 @@ async function startDevMode(c: DankConfig, signal: AbortSignal) {
79
121
  updateDevServices(updated)
80
122
  })
81
123
 
82
- watch('pages', signal, filename => {
124
+ watch(serve.dirs.pages, signal, filename => {
83
125
  if (extname(filename) === '.html') {
84
126
  for (const [urlPath, srcPath] of Object.entries(c.pages)) {
85
127
  if (srcPath === filename) {
86
128
  updatePage(urlPath)
87
129
  }
88
130
  }
131
+ for (const [urlPath, partials] of Object.entries(
132
+ partialsByUrlPath,
133
+ )) {
134
+ if (partials.includes(filename)) {
135
+ updatePage(urlPath, filename)
136
+ }
137
+ }
89
138
  }
90
139
  })
91
140
 
92
141
  await Promise.all(
93
- Object.entries(c.pages).map(([urlPath, srcPath]) =>
94
- addPage(urlPath, srcPath),
95
- ),
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
+ }),
96
150
  )
97
151
 
98
152
  async function addPage(urlPath: string, srcPath: string) {
99
- const metadata = await processWebpage({
100
- clientJS,
101
- outDir: watchDir,
102
- pagesDir: 'pages',
103
- srcPath,
153
+ await mkdir(join(serve.dirs.buildWatch, urlPath), { recursive: true })
154
+ const htmlEntrypoint = (pagesByUrlPath[urlPath] = new HtmlEntrypoint(
155
+ serve,
104
156
  urlPath,
157
+ srcPath,
158
+ [{ type: 'script', js: clientJS }],
159
+ ))
160
+ htmlEntrypoint.on('entrypoints', entrypoints => {
161
+ const pathsIn = new Set(entrypoints.map(e => e.in))
162
+ if (
163
+ !entryPointsByUrlPath[urlPath] ||
164
+ !matchingEntrypoints(
165
+ entryPointsByUrlPath[urlPath].pathsIn,
166
+ pathsIn,
167
+ )
168
+ ) {
169
+ entryPointsByUrlPath[urlPath] = { entrypoints, pathsIn }
170
+ resetBuildContext()
171
+ }
172
+ })
173
+ htmlEntrypoint.on('partial', partial => {
174
+ if (!partialsByUrlPath[urlPath]) {
175
+ partialsByUrlPath[urlPath] = []
176
+ }
177
+ partialsByUrlPath[urlPath].push(partial)
105
178
  })
106
- pagesByUrlPath[urlPath] = metadata
107
- entryPointsByUrlPath[urlPath] = new Set(
108
- metadata.entryPoints.map(e => e.in),
179
+ htmlEntrypoint.on(
180
+ 'partials',
181
+ partials => (partialsByUrlPath[urlPath] = partials),
182
+ )
183
+ htmlEntrypoint.on('output', html =>
184
+ writeFile(join(serve.dirs.buildWatch, urlPath, 'index.html'), html),
109
185
  )
110
- if (buildContext !== null) {
111
- resetBuildContext()
112
- }
113
186
  }
114
187
 
115
188
  function deletePage(urlPath: string) {
189
+ pagesByUrlPath[urlPath].removeAllListeners()
116
190
  delete pagesByUrlPath[urlPath]
117
191
  delete entryPointsByUrlPath[urlPath]
118
192
  resetBuildContext()
119
193
  }
120
194
 
121
- async function updatePage(urlPath: string) {
122
- const update = await processWebpage({
123
- clientJS,
124
- outDir: watchDir,
125
- pagesDir: 'pages',
126
- srcPath: c.pages[urlPath as `/${string}`],
127
- urlPath,
128
- })
129
- const entryPointUrls = new Set(update.entryPoints.map(e => e.in))
130
- if (!hasSameValues(entryPointUrls, entryPointsByUrlPath[urlPath])) {
131
- entryPointsByUrlPath[urlPath] = entryPointUrls
132
- resetBuildContext()
133
- }
195
+ async function updatePage(urlPath: string, partial?: string) {
196
+ pagesByUrlPath[urlPath].emit('change', partial)
134
197
  }
135
198
 
136
- function collectEntrypoints(): Array<{ in: string; out: string }> {
137
- const sources: Set<string> = new Set()
138
- return Object.values(pagesByUrlPath)
139
- .flatMap(({ entryPoints }) => entryPoints)
199
+ function collectEntrypoints(): Array<EntryPoint> {
200
+ const unique: Set<string> = new Set()
201
+ const pageBundles = Object.values(entryPointsByUrlPath)
202
+ .flatMap(entrypointState => entrypointState.entrypoints)
140
203
  .filter(entryPoint => {
141
- if (sources.has(entryPoint.in)) {
204
+ if (unique.has(entryPoint.in)) {
142
205
  return false
143
206
  } else {
144
- sources.add(entryPoint.in)
207
+ unique.add(entryPoint.in)
145
208
  return true
146
209
  }
147
210
  })
211
+ const workerBundles = registry.workerEntryPoints()
212
+ if (workerBundles) {
213
+ return [...pageBundles, ...workerBundles]
214
+ } else {
215
+ return pageBundles
216
+ }
148
217
  }
149
218
 
150
219
  function resetBuildContext() {
151
- if (buildContext === 'starting' || buildContext === 'dirty') {
152
- buildContext = 'dirty'
153
- return
154
- }
155
- if (buildContext === 'disposing') {
156
- return
220
+ switch (buildContext) {
221
+ case 'starting':
222
+ buildContext = 'dirty'
223
+ return
224
+ case 'dirty':
225
+ case 'disposing':
226
+ return
157
227
  }
158
228
  if (buildContext !== null) {
159
- const prev = buildContext
229
+ const disposing = buildContext.dispose()
160
230
  buildContext = 'disposing'
161
- prev.dispose().then(() => {
231
+ disposing.then(() => {
162
232
  buildContext = null
163
233
  resetBuildContext()
164
234
  })
165
235
  } else {
166
- startEsbuildWatch(collectEntrypoints()).then(ctx => {
167
- if (buildContext === 'dirty') {
168
- buildContext = null
169
- resetBuildContext()
170
- } else {
171
- buildContext = ctx
172
- }
173
- })
236
+ buildContext = 'starting'
237
+ startEsbuildWatch(c, registry, serve, collectEntrypoints()).then(
238
+ ctx => {
239
+ if (buildContext === 'dirty') {
240
+ buildContext = 'disposing'
241
+ ctx.dispose().then(() => {
242
+ buildContext = null
243
+ resetBuildContext()
244
+ })
245
+ } else {
246
+ buildContext = ctx
247
+ }
248
+ },
249
+ )
174
250
  }
175
251
  }
176
252
 
177
- buildContext = await startEsbuildWatch(collectEntrypoints())
178
- const frontend = createDevServeFilesFetcher({
179
- pages: c.pages,
180
- pagesDir: watchDir,
181
- proxyPort: ESBUILD_PORT,
182
- publicDir: 'public',
183
- })
184
- createWebServer(PORT, frontend).listen(PORT)
185
- console.log(`dev server is live at http://127.0.0.1:${PORT}`)
186
- startDevServices(c, signal)
253
+ // function removePartialFromPage(partial: string, urlPath: string) {
254
+ // const deleteIndex = urlPathsByPartials[partial].indexOf(urlPath)
255
+ // if (deleteIndex !== -1) {
256
+ // if (urlPathsByPartials[partial].length === 1) {
257
+ // delete urlPathsByPartials[partial]
258
+ // } else {
259
+ // urlPathsByPartials[partial].splice(deleteIndex, 1)
260
+ // }
261
+ // }
262
+ // }
263
+
264
+ // inital start of esbuild ctx
265
+ resetBuildContext()
266
+
267
+ // todo this page route state could be built on change and reused
268
+ const pageRoutes: PageRouteState = {
269
+ get urls(): Array<string> {
270
+ return Object.keys(c.pages)
271
+ },
272
+ get urlRewrites(): Array<UrlRewrite> {
273
+ return collectUrlRewrites(c)
274
+ },
275
+ }
276
+ const frontend = createDevServeFilesFetcher(pageRoutes, serve)
277
+ const devServices = startDevServices(c, signal)
278
+ startWebServer(serve, frontend, devServices.http, pageRoutes)
187
279
  }
188
280
 
189
- function hasSameValues(a: Set<string>, b: Set<string>): boolean {
281
+ function matchingEntrypoints(a: Set<string>, b: Set<string>): boolean {
190
282
  if (a.size !== b.size) {
191
283
  return false
192
284
  }
@@ -198,73 +290,41 @@ function hasSameValues(a: Set<string>, b: Set<string>): boolean {
198
290
  return true
199
291
  }
200
292
 
201
- type WebpageInputs = {
202
- clientJS: string
203
- outDir: string
204
- pagesDir: string
205
- srcPath: string
206
- urlPath: string
207
- }
208
-
209
- type WebpageMetadata = {
210
- entryPoints: Array<{ in: string; out: string }>
211
- srcPath: string
212
- urlPath: string
213
- }
214
-
215
- async function processWebpage(inputs: WebpageInputs): Promise<WebpageMetadata> {
216
- const html = await HtmlEntrypoint.readFrom(
217
- inputs.urlPath,
218
- join(inputs.pagesDir, inputs.srcPath),
219
- )
220
- await html.injectPartials()
221
- if (inputs.urlPath !== '/') {
222
- await mkdir(join(inputs.outDir, inputs.urlPath), { recursive: true })
223
- }
224
- const entryPoints: Array<{ in: string; out: string }> = []
225
- html.collectScripts().forEach(scriptImport => {
226
- entryPoints.push({
227
- in: scriptImport.in,
228
- out: scriptImport.out,
229
- })
230
- })
231
- html.rewriteHrefs()
232
- html.appendScript(inputs.clientJS)
233
- await html.writeTo(inputs.outDir)
234
- return {
235
- entryPoints,
236
- srcPath: inputs.srcPath,
237
- urlPath: inputs.urlPath,
238
- }
239
- }
240
-
241
293
  async function startEsbuildWatch(
242
- entryPoints: Array<{ in: string; out: string }>,
294
+ c: DankConfig,
295
+ registry: WebsiteRegistry,
296
+ serve: DankServe,
297
+ entryPoints: Array<EntryPoint>,
243
298
  ): Promise<BuildContext> {
244
299
  const ctx = await esbuildDevContext(
245
- createGlobalDefinitions(),
300
+ serve,
301
+ registry,
302
+ createGlobalDefinitions(serve),
246
303
  entryPoints,
247
- 'build/watch',
304
+ c.esbuild,
248
305
  )
249
306
 
250
307
  await ctx.watch()
251
308
 
252
309
  await ctx.serve({
253
310
  host: '127.0.0.1',
254
- port: ESBUILD_PORT,
311
+ port: serve.esbuildPort,
255
312
  cors: {
256
- origin: 'http://127.0.0.1:' + PORT,
313
+ origin: ['127.0.0.1', 'localhost'].map(
314
+ hostname => `http://${hostname}:${serve.dankPort}`,
315
+ ),
257
316
  },
258
317
  })
259
318
 
260
319
  return ctx
261
320
  }
262
321
 
263
- async function loadClientJS() {
264
- return await readFile(
322
+ async function loadClientJS(esbuildPort: number) {
323
+ const clientJS = await readFile(
265
324
  resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')),
266
325
  'utf-8',
267
326
  )
327
+ return clientJS.replace('3995', `${esbuildPort}`)
268
328
  }
269
329
 
270
330
  async function watch(
package/lib/services.ts CHANGED
@@ -2,6 +2,16 @@ import { type ChildProcess, spawn } from 'node:child_process'
2
2
  import { basename, isAbsolute, resolve } from 'node:path'
3
3
  import type { DankConfig, DevService } from './dank.ts'
4
4
 
5
+ export type DevServices = {
6
+ http: HttpServices
7
+ }
8
+
9
+ export type HttpServices = {
10
+ running: Array<HttpService>
11
+ }
12
+
13
+ export type HttpService = NonNullable<DevService['http']>
14
+
5
15
  // up to date representation of dank.config.ts services
6
16
  const running: Array<{ s: DevService; process: ChildProcess | null }> = []
7
17
 
@@ -13,13 +23,23 @@ let updating: null | {
13
23
  starting: Array<DevService>
14
24
  } = null
15
25
 
16
- export function startDevServices(c: DankConfig, _signal: AbortSignal) {
26
+ export function startDevServices(
27
+ c: DankConfig,
28
+ _signal: AbortSignal,
29
+ ): DevServices {
17
30
  signal = _signal
18
31
  if (c.services?.length) {
19
32
  for (const s of c.services) {
20
33
  running.push({ s, process: startService(s) })
21
34
  }
22
35
  }
36
+ return {
37
+ http: {
38
+ get running(): Array<NonNullable<DevService['http']>> {
39
+ return running.map(({ s }) => s.http).filter(http => !!http)
40
+ },
41
+ },
42
+ }
23
43
  }
24
44
 
25
45
  export function updateDevServices(c: DankConfig) {
@@ -138,9 +158,13 @@ function startService(s: DevService): ChildProcess {
138
158
  spawned.stderr.on('data', chunk => printChunk(stderrLabel, chunk))
139
159
 
140
160
  spawned.on('error', e => {
141
- const cause =
142
- 'code' in e && e.code === 'ENOENT' ? 'program not found' : e.message
143
- opPrint(s, 'error: ' + cause)
161
+ if (e.name !== 'AbortError') {
162
+ const cause =
163
+ 'code' in e && e.code === 'ENOENT'
164
+ ? 'program not found'
165
+ : e.message
166
+ opPrint(s, 'error: ' + cause)
167
+ }
144
168
  removeFromRunning(s)
145
169
  })
146
170
 
package/lib_js/bin.js CHANGED
@@ -12,10 +12,10 @@ function printHelp(task) {
12
12
  'dank serve [--minify] [--production]');
13
13
  }
14
14
  console.log('\nOPTIONS:');
15
+ if (!task || task === 'serve')
16
+ console.log(' --log-http print access logs');
15
17
  console.log(' --minify minify sources');
16
- // if (!task || task === 'serve') {
17
- // console.log(' --preview pre-bundle and build ServiceWorker')
18
- // }
18
+ // if (!task || task === 'serve') console.log(' --preview pre-bundle and build ServiceWorker')
19
19
  console.log(' --production build for production release');
20
20
  if (task) {
21
21
  console.log();