@eighty4/dank 0.0.5-0 → 0.0.5-2

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/developer.ts CHANGED
@@ -32,44 +32,101 @@ export type LogEvent = {
32
32
 
33
33
  type LogEventData =
34
34
  | LogEventDatum
35
- | Array<LogEventDatum>
36
- | Set<LogEventDatum>
37
- | Record<string, LogEventDatum>
35
+ | LogEventDataArray
36
+ | LogEventDataRecord
37
+ | LogEventDataSet
38
38
 
39
39
  type LogEventDatum = boolean | number | string | null | undefined
40
40
 
41
+ type LogEventDataArray = Array<LogEventDatum> | Array<LogEventDataRecord>
42
+
43
+ type LogEventDataRecord = Record<
44
+ string,
45
+ LogEventDatum | LogEventDataArray | LogEventDataSet
46
+ >
47
+
48
+ type LogEventDataSet = Set<LogEventDatum>
49
+
41
50
  function toStringLogEvent(logEvent: LogEvent): string {
42
51
  const when = new Date().toISOString()
43
52
  const message = `[${logEvent.realm}] ${logEvent.message}\n${when}\n`
44
- if (!logEvent.data) {
53
+ if (logEvent.data) {
54
+ const data: string = Object.keys(logEvent.data)
55
+ .sort()
56
+ .map(key => toStringData(key, logEvent.data![key]))
57
+ .join('')
58
+ return `${message}\n${data}`
59
+ } else {
45
60
  return message
46
61
  }
47
- let data = ''
48
- for (const k of Object.keys(logEvent.data).sort()) {
49
- data += `\n ${k} = ${toStringData(logEvent.data[k])}`
50
- }
51
- return `${message}${data}\n`
52
62
  }
53
63
 
54
- function toStringData(datum: LogEventData): string {
55
- if (datum instanceof Set) {
56
- datum = Array.from(datum)
64
+ const PAD = ' '
65
+ const nextIndent = (pad: string) => pad + PAD
66
+
67
+ function toStringData(
68
+ key: string,
69
+ data: LogEventData,
70
+ pad: string = PAD,
71
+ ): string {
72
+ const prepend = `${pad}${key} = `
73
+ if (isDataAbsentOrScalar(data)) {
74
+ return `${prepend}${toStringDatum(data)}`
57
75
  }
58
- if (
59
- datum !== null &&
60
- typeof datum === 'object' &&
61
- datum.constructor.name === 'Object'
62
- ) {
63
- datum = Object.entries(datum).map(([k, v]) => `${k} = ${v}`)
76
+ if (data instanceof Set) {
77
+ data = Array.from(data)
64
78
  }
65
- if (Array.isArray(datum)) {
66
- if (datum.length === 0) {
67
- return '[]'
68
- } else {
69
- return `[\n ${datum.join('\n ')}\n ]`
70
- }
79
+ if (Array.isArray(data)) {
80
+ return `${prepend}${toStringArray(data, pad)}`
71
81
  } else {
72
- return `${datum}`
82
+ return `${prepend}${toStringRecord(data, pad)}`
83
+ }
84
+ }
85
+
86
+ function toStringDatum(datum: LogEventDatum): string {
87
+ return `${datum}\n`
88
+ }
89
+
90
+ function toStringArray(array: LogEventDataArray, padEnding: string): string {
91
+ if (array.length === 0) {
92
+ return '[]'
93
+ }
94
+ const padIndent = nextIndent(padEnding)
95
+ const content = array
96
+ .map(datum => {
97
+ if (isDataAbsentOrScalar(datum)) {
98
+ return toStringDatum(datum)
99
+ } else {
100
+ return toStringRecord(datum, padIndent)
101
+ }
102
+ })
103
+ .join(padIndent)
104
+ return `[\n${padIndent}${content}${padEnding}]\n`
105
+ }
106
+
107
+ function toStringRecord(record: LogEventDataRecord, padEnding: string): string {
108
+ const keys = Object.keys(record)
109
+ if (keys.length === 0) {
110
+ return '{}'
111
+ }
112
+ const padIndent = nextIndent(padEnding)
113
+ const content = keys
114
+ .map(key => toStringData(key, record[key], padIndent))
115
+ .join('')
116
+ return `{\n${content}${padEnding}}\n`
117
+ }
118
+
119
+ function isDataAbsentOrScalar(
120
+ data: LogEventData,
121
+ ): data is undefined | null | string | boolean | number {
122
+ switch (typeof data) {
123
+ case 'undefined':
124
+ case 'string':
125
+ case 'boolean':
126
+ case 'number':
127
+ return true
128
+ default:
129
+ return data === null
73
130
  }
74
131
  }
75
132
 
@@ -79,7 +136,7 @@ function logToConsoleAndFile(out: string) {
79
136
  }
80
137
 
81
138
  function logToConsole(out: string) {
82
- console.log('\n' + out)
139
+ console.log(out)
83
140
  }
84
141
 
85
142
  function logToFile(out: string) {
package/lib/dirs.ts CHANGED
@@ -7,8 +7,7 @@ export type DankDirectories = {
7
7
  buildWatch: string
8
8
  buildDist: string
9
9
  pages: string
10
- pagesResolved: string
11
- projectResolved: string
10
+ pagesAbs: string
12
11
  projectRootAbs: string
13
12
  public: string
14
13
  }
@@ -17,18 +16,18 @@ export async function defaultProjectDirs(
17
16
  projectRootAbs: string,
18
17
  ): Promise<Readonly<DankDirectories>> {
19
18
  if (!isAbsolute(projectRootAbs)) {
20
- throw Error()
19
+ throw Error('must use an absolute project root path')
20
+ }
21
+ if ((await realpath(projectRootAbs)) !== projectRootAbs) {
22
+ throw Error('must use a real project root path')
21
23
  }
22
- const projectResolved = await realpath(projectRootAbs)
23
24
  const pages = 'pages'
24
- const pagesResolved = join(projectResolved, pages)
25
25
  return Object.freeze({
26
26
  buildRoot: 'build',
27
27
  buildDist: join('build', 'dist'),
28
28
  buildWatch: join('build', 'watch'),
29
29
  pages,
30
- pagesResolved,
31
- projectResolved,
30
+ pagesAbs: join(projectRootAbs, pages),
32
31
  projectRootAbs,
33
32
  public: 'public',
34
33
  })
@@ -63,8 +62,8 @@ export class Resolver {
63
62
 
64
63
  // `p` is expected to be a relative path resolvable from the project dir
65
64
  isProjectSubpathInPagesDir(p: string): boolean {
66
- return resolve(join(this.#dirs.projectResolved, p)).startsWith(
67
- this.#dirs.pagesResolved,
65
+ return resolve(join(this.#dirs.projectRootAbs, p)).startsWith(
66
+ this.#dirs.pagesAbs,
68
67
  )
69
68
  }
70
69
 
package/lib/http.ts CHANGED
@@ -18,7 +18,7 @@ import type {
18
18
  WebsiteManifest,
19
19
  WebsiteRegistry,
20
20
  } from './registry.ts'
21
- import type { HttpServices } from './services.ts'
21
+ import type { DevServices } from './services.ts'
22
22
 
23
23
  export type FrontendFetcher = (
24
24
  url: URL,
@@ -33,7 +33,7 @@ export function startWebServer(
33
33
  dirs: DankDirectories,
34
34
  urlRewriteProvider: UrlRewriteProvider,
35
35
  frontendFetcher: FrontendFetcher,
36
- httpServices: HttpServices,
36
+ devServices: DevServices,
37
37
  ) {
38
38
  const serverAddress = 'http://localhost:' + port
39
39
  const handler = (req: IncomingMessage, res: ServerResponse) => {
@@ -47,7 +47,7 @@ export function startWebServer(
47
47
  req,
48
48
  url,
49
49
  headers,
50
- httpServices,
50
+ devServices,
51
51
  flags,
52
52
  dirs,
53
53
  urlRewriteProvider,
@@ -69,7 +69,7 @@ async function onNotFound(
69
69
  req: IncomingMessage,
70
70
  url: URL,
71
71
  headers: Headers,
72
- httpServices: HttpServices,
72
+ devServices: DevServices,
73
73
  flags: DankFlags,
74
74
  dirs: DankDirectories,
75
75
  urlRewriteProvider: UrlRewriteProvider,
@@ -87,7 +87,7 @@ async function onNotFound(
87
87
  return
88
88
  }
89
89
  }
90
- const fetchResponse = await tryHttpServices(req, url, headers, httpServices)
90
+ const fetchResponse = await tryHttpServices(req, url, headers, devServices)
91
91
  if (fetchResponse) {
92
92
  sendFetchResponse(res, fetchResponse)
93
93
  } else {
@@ -131,14 +131,13 @@ async function tryHttpServices(
131
131
  req: IncomingMessage,
132
132
  url: URL,
133
133
  headers: Headers,
134
- httpServices: HttpServices,
134
+ devServices: DevServices,
135
135
  ): Promise<Response | null> {
136
136
  if (url.pathname.startsWith('/.well-known/')) {
137
137
  return null
138
138
  }
139
139
  const body = await collectReqBody(req)
140
- const { running } = httpServices
141
- for (const httpService of running) {
140
+ for (const httpService of devServices.httpServices) {
142
141
  const proxyUrl = new URL(url)
143
142
  proxyUrl.port = `${httpService.port}`
144
143
  try {
@@ -198,7 +197,7 @@ export function createBuiltDistFilesFetcher(
198
197
  if (manifest.pageUrls.has(url.pathname)) {
199
198
  streamFile(
200
199
  join(
201
- dirs.projectResolved,
200
+ dirs.projectRootAbs,
202
201
  dirs.buildDist,
203
202
  url.pathname,
204
203
  'index.html',
@@ -207,7 +206,7 @@ export function createBuiltDistFilesFetcher(
207
206
  )
208
207
  } else if (manifest.files.has(url.pathname)) {
209
208
  streamFile(
210
- join(dirs.projectResolved, dirs.buildDist, url.pathname),
209
+ join(dirs.projectRootAbs, dirs.buildDist, url.pathname),
211
210
  res,
212
211
  )
213
212
  } else {
package/lib/serve.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  startWebServer,
14
14
  } from './http.ts'
15
15
  import { WebsiteRegistry, type UrlRewrite } from './registry.ts'
16
- import { startDevServices, updateDevServices } from './services.ts'
16
+ import { DevServices, type ManagedServiceLabel } from './services.ts'
17
17
  import { watch } from './watch.ts'
18
18
 
19
19
  let c: ResolvedDankConfig
@@ -21,20 +21,18 @@ let c: ResolvedDankConfig
21
21
  export async function serveWebsite(): Promise<never> {
22
22
  c = await loadConfig('serve', process.cwd())
23
23
  await rm(c.dirs.buildRoot, { force: true, recursive: true })
24
- const abortController = new AbortController()
25
- process.once('exit', () => abortController.abort())
26
24
  if (c.flags.preview) {
27
- await startPreviewMode(abortController.signal)
25
+ await startPreviewMode()
28
26
  } else {
29
- await startDevMode(abortController.signal)
27
+ await startDevMode()
30
28
  }
31
29
  return new Promise(() => {})
32
30
  }
33
31
 
34
- async function startPreviewMode(signal: AbortSignal) {
32
+ async function startPreviewMode() {
35
33
  const manifest = await buildWebsite(c)
36
34
  const frontend = createBuiltDistFilesFetcher(c.dirs, manifest)
37
- const devServices = startDevServices(c.services, signal)
35
+ const devServices = launchDevServices()
38
36
  const urlRewrites: Array<UrlRewrite> = Object.keys(c.pages)
39
37
  .sort()
40
38
  .map(url => {
@@ -50,8 +48,17 @@ async function startPreviewMode(signal: AbortSignal) {
50
48
  c.dirs,
51
49
  { urlRewrites },
52
50
  frontend,
53
- devServices.http,
51
+ devServices,
54
52
  )
53
+ const controller = new AbortController()
54
+ watch('dank.config.ts', controller.signal, async filename => {
55
+ console.log(filename, 'was updated!')
56
+ console.log(
57
+ 'config updates are not hot reloaded during `dank serve --preview`',
58
+ )
59
+ console.log('restart DANK to reload configuration')
60
+ controller.abort()
61
+ })
55
62
  }
56
63
 
57
64
  type BuildContextState =
@@ -61,12 +68,12 @@ type BuildContextState =
61
68
  | 'disposing'
62
69
  | null
63
70
 
64
- async function startDevMode(signal: AbortSignal) {
71
+ async function startDevMode() {
65
72
  const registry = new WebsiteRegistry(c)
66
73
  await mkdir(c.dirs.buildWatch, { recursive: true })
67
74
  let buildContext: BuildContextState = null
68
75
 
69
- watch('dank.config.ts', signal, async filename => {
76
+ watch('dank.config.ts', async filename => {
70
77
  LOG({
71
78
  realm: 'serve',
72
79
  message: 'config watch event',
@@ -80,10 +87,10 @@ async function startDevMode(signal: AbortSignal) {
80
87
  return
81
88
  }
82
89
  registry.configSync()
83
- updateDevServices(c.services)
90
+ devServices.update(c.services)
84
91
  })
85
92
 
86
- watch(c.dirs.pages, signal, filename => {
93
+ watch(c.dirs.pages, { recursive: true }, filename => {
87
94
  LOG({
88
95
  realm: 'serve',
89
96
  message: 'pages dir watch event',
@@ -163,15 +170,8 @@ async function startDevMode(signal: AbortSignal) {
163
170
  resetBuildContext()
164
171
 
165
172
  const frontend = createDevServeFilesFetcher(c.esbuildPort, c.dirs, registry)
166
- const devServices = startDevServices(c.services, signal)
167
- startWebServer(
168
- c.dankPort,
169
- c.flags,
170
- c.dirs,
171
- registry,
172
- frontend,
173
- devServices.http,
174
- )
173
+ const devServices = launchDevServices()
174
+ startWebServer(c.dankPort, c.flags, c.dirs, registry, frontend, devServices)
175
175
  }
176
176
 
177
177
  async function startEsbuildWatch(
@@ -220,3 +220,47 @@ async function writeHtml(html: HtmlEntrypoint, output: string) {
220
220
  })
221
221
  await writeFile(path, output)
222
222
  }
223
+
224
+ function launchDevServices(): DevServices {
225
+ const services = new DevServices(c.services)
226
+ services.on('error', (label, cause) =>
227
+ console.log(formatServiceLabel(label), 'errored:', cause),
228
+ )
229
+ services.on('exit', (label, code) => {
230
+ if (code) {
231
+ console.log(formatServiceLabel(label), 'exited', code)
232
+ } else {
233
+ console.log(formatServiceLabel(label), 'exited')
234
+ }
235
+ })
236
+ services.on('launch', label =>
237
+ console.log(formatServiceLabel(label), 'starting'),
238
+ )
239
+ services.on('stdout', (label, output) =>
240
+ printServiceOutput(label, 32, output),
241
+ )
242
+ services.on('stderr', (label, output) =>
243
+ printServiceOutput(label, 31, output),
244
+ )
245
+ return services
246
+ }
247
+
248
+ function formatServiceLabel(label: ManagedServiceLabel): string {
249
+ return `| \u001b[2m${label.cwd}\u001b[22m ${label.command} |`
250
+ }
251
+
252
+ function formatServiceOutputLabel(
253
+ label: ManagedServiceLabel,
254
+ color: 31 | 32,
255
+ ): string {
256
+ return `\u001b[${color}m${formatServiceLabel(label)}\u001b[39m`
257
+ }
258
+
259
+ function printServiceOutput(
260
+ label: ManagedServiceLabel,
261
+ color: 31 | 32,
262
+ output: Array<string>,
263
+ ) {
264
+ const formattedLabel = formatServiceOutputLabel(label, color)
265
+ for (const line of output) console.log(formattedLabel, line)
266
+ }