@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.
@@ -0,0 +1 @@
1
+ new EventSource("http://127.0.0.1:3995/esbuild").addEventListener("change",o=>{let{updated:a}=JSON.parse(o.data),e=new Set;for(let s of a)e.add(s);let d=Array.from(e).filter(s=>s.endsWith(".css"));if(d.length){console.log("esbuild css updates",d);let s={};for(let t of document.getElementsByTagName("link"))if(t.getAttribute("rel")==="stylesheet"){let n=new URL(t.href);(n.host=location.host)&&(s[n.pathname]=t)}let r=!1;for(let t of d){let n=s[t];if(n){let c=n.cloneNode();c.href=`${t}?${Math.random().toString(36).slice(2)}`,c.onload=()=>n.remove(),n.parentNode.insertBefore(c,n.nextSibling),r=!0}}r&&p()}if(d.length<e.size){let s=Array.from(e).filter(t=>!t.endsWith(".css")),r=new Set;for(let t of document.getElementsByTagName("script"))if(t.src.length){let n=new URL(t.src);(n.host=location.host)&&r.add(n.pathname)}s.some(t=>r.has(t))&&(console.log("esbuild js updates require reload"),y())}});function p(){let o=l("green","23995");o.style.opacity="0",o.animate([{opacity:0},{opacity:1},{opacity:1},{opacity:1},{opacity:.75},{opacity:.5},{opacity:.25},{opacity:0}],{duration:400,iterations:1,direction:"normal",easing:"linear"}),document.body.appendChild(o),Promise.all(o.getAnimations().map(a=>a.finished)).then(()=>o.remove())}var i=null;function y(){i||(i=l("orange","33995"),i.style.opacity="0",i.style.pointerEvents="none",i.animate([{opacity:0},{opacity:1}],{duration:400,iterations:1,direction:"normal",easing:"ease-in",fill:"forwards"}),document.body.appendChild(i))}function l(o,a){let e=document.createElement("div");return e.style.border="6px dashed "+o,e.style.zIndex=a,e.style.position="fixed",e.style.top=e.style.left="1px",e.style.height=e.style.width="calc(100% - 2px)",e.style.boxSizing="border-box",e}
package/lib/bin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { buildWebsite } from './build.ts'
4
- import { loadConfig } from './config.ts'
4
+ import { DankError } from './errors.ts'
5
5
  import { serveWebsite } from './serve.ts'
6
6
 
7
7
  function printHelp(task?: 'build' | 'serve'): never {
@@ -71,16 +71,14 @@ const task: 'build' | 'serve' = (function resolveTask() {
71
71
  return task
72
72
  })()
73
73
 
74
- const c = await loadConfig()
75
-
76
74
  try {
77
75
  switch (task) {
78
76
  case 'build':
79
- await buildWebsite(c)
77
+ await buildWebsite()
80
78
  console.log(green('done'))
81
79
  process.exit(0)
82
80
  case 'serve':
83
- await serveWebsite(c)
81
+ await serveWebsite()
84
82
  }
85
83
  } catch (e: unknown) {
86
84
  errorExit(e)
@@ -88,13 +86,12 @@ try {
88
86
 
89
87
  function printError(e: unknown) {
90
88
  if (e !== null) {
91
- if (typeof e === 'string') {
92
- console.error(red('error:'), e)
93
- } else if (e instanceof Error) {
89
+ if (e instanceof DankError) {
94
90
  console.error(red('error:'), e.message)
95
- if (e.stack) {
96
- console.error(e.stack)
97
- }
91
+ } else if (e instanceof Error) {
92
+ console.error(red('error:'), e.stack ?? e.message)
93
+ } else {
94
+ console.error(red('error:'), e)
98
95
  }
99
96
  }
100
97
  }
package/lib/build.ts CHANGED
@@ -1,22 +1,23 @@
1
1
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
  import { createBuildTag } from './build_tag.ts'
4
- import type { DankConfig } from './dank.ts'
4
+ import { loadConfig, type ResolvedDankConfig } from './config.ts'
5
5
  import { type DefineDankGlobal, createGlobalDefinitions } from './define.ts'
6
- import { esbuildWebpages, esbuildWorkers, type EntryPoint } from './esbuild.ts'
7
- import { resolveBuildFlags, type DankBuild } from './flags.ts'
8
- import { HtmlEntrypoint } from './html.ts'
9
- import { type WebsiteManifest, WebsiteRegistry } from './metadata.ts'
6
+ import type { DankDirectories } from './dirs.ts'
7
+ import { esbuildWebpages, esbuildWorkers } from './esbuild.ts'
10
8
  import { copyAssets } from './public.ts'
9
+ import { type WebsiteManifest, WebsiteRegistry } from './registry.ts'
11
10
 
12
11
  export async function buildWebsite(
13
- c: DankConfig,
14
- build: DankBuild = resolveBuildFlags(),
12
+ c?: ResolvedDankConfig,
15
13
  ): Promise<WebsiteManifest> {
16
- const buildTag = await createBuildTag(build)
14
+ if (!c) {
15
+ c = await loadConfig('build', process.cwd())
16
+ }
17
+ const buildTag = await createBuildTag(c.flags)
17
18
  console.log(
18
- build.minify
19
- ? build.production
19
+ c.flags.minify
20
+ ? c.flags.production
20
21
  ? 'minified production'
21
22
  : 'minified'
22
23
  : 'unminified',
@@ -24,85 +25,52 @@ export async function buildWebsite(
24
25
  buildTag,
25
26
  'building in ./build/dist',
26
27
  )
27
- await rm(build.dirs.buildRoot, { recursive: true, force: true })
28
- await mkdir(build.dirs.buildDist, { recursive: true })
28
+ await rm(c.dirs.buildRoot, { recursive: true, force: true })
29
+ await mkdir(c.dirs.buildDist, { recursive: true })
29
30
  for (const subdir of Object.keys(c.pages).filter(url => url !== '/')) {
30
- await mkdir(join(build.dirs.buildDist, subdir), { recursive: true })
31
+ await mkdir(join(c.dirs.buildDist, subdir), { recursive: true })
31
32
  }
32
- await mkdir(join(build.dirs.buildRoot, 'metafiles'), { recursive: true })
33
- const registry = new WebsiteRegistry(build)
34
- registry.pageUrls = Object.keys(c.pages)
35
- registry.copiedAssets = await copyAssets(build)
36
- await buildWebpages(c, registry, build, createGlobalDefinitions(build))
33
+ await mkdir(join(c.dirs.buildRoot, 'metafiles'), { recursive: true })
34
+ const registry = await buildWebpages(c, createGlobalDefinitions(c))
37
35
  return await registry.writeManifest(buildTag)
38
36
  }
39
37
 
40
- // builds all webpage entrypoints in one esbuild.build context
41
- // to support code splitting
38
+ // builds all webpage entrypoints in one esbuild.build context to support code splitting
42
39
  // returns all built assets URLs and webpage URLs from DankConfig.pages
43
40
  async function buildWebpages(
44
- c: DankConfig,
45
- registry: WebsiteRegistry,
46
- build: DankBuild,
41
+ c: ResolvedDankConfig,
47
42
  define: DefineDankGlobal,
48
- ) {
49
- // create HtmlEntrypoint for each webpage and collect awaitable esbuild entrypoints
50
- const loadingEntryPoints: Array<Promise<Array<EntryPoint>>> = []
51
- const htmlEntrypoints: Array<HtmlEntrypoint> = []
52
- for (const [urlPath, mapping] of Object.entries(c.pages)) {
53
- const fsPath = typeof mapping === 'string' ? mapping : mapping.webpage
54
- const html = new HtmlEntrypoint(
55
- build,
56
- registry.resolver,
57
- urlPath,
58
- fsPath,
59
- )
60
- loadingEntryPoints.push(new Promise(res => html.on('entrypoints', res)))
61
- htmlEntrypoints.push(html)
62
- }
63
-
64
- // collect esbuild entrypoints from every HtmlEntrypoint
65
- const uniqueEntryPoints: Set<string> = new Set()
66
- const buildEntryPoints: Array<EntryPoint> = []
67
- for (const pageEntryPoints of await Promise.all(loadingEntryPoints)) {
68
- for (const entryPoint of pageEntryPoints) {
69
- if (!uniqueEntryPoints.has(entryPoint.in)) {
70
- buildEntryPoints.push(entryPoint)
71
- }
72
- }
73
- }
74
-
75
- await esbuildWebpages(build, registry, define, buildEntryPoints, c.esbuild)
43
+ ): Promise<WebsiteRegistry> {
44
+ const registry = new WebsiteRegistry(c)
45
+ registry.configSync()
46
+ registry.copiedAssets = await copyAssets(c.dirs)
47
+ await Promise.all(registry.htmlEntrypoints.map(html => html.process()))
48
+ await esbuildWebpages(registry, define, registry.webpageEntryPoints)
76
49
 
77
50
  // todo recursively build workers on building workers that create workers
78
- const workerEntryPoints = registry.workerEntryPoints()
51
+ const workerEntryPoints = registry.workerEntryPoints
79
52
  if (workerEntryPoints?.length) {
80
- await esbuildWorkers(
81
- build,
82
- registry,
83
- define,
84
- workerEntryPoints,
85
- c.esbuild,
86
- )
53
+ await esbuildWorkers(registry, define, workerEntryPoints)
87
54
  }
88
- await rewriteWorkerUrls(build, registry)
55
+ await rewriteWorkerUrls(c.dirs, registry)
89
56
 
90
57
  // write out html output with rewritten hrefs
91
58
  await Promise.all(
92
- htmlEntrypoints.map(async html => {
59
+ registry.htmlEntrypoints.map(async html => {
93
60
  await writeFile(
94
- join(build.dirs.buildDist, html.url, 'index.html'),
61
+ join(c.dirs.buildDist, html.url, 'index.html'),
95
62
  html.output(registry),
96
63
  )
97
64
  }),
98
65
  )
66
+ return registry
99
67
  }
100
68
 
101
69
  export async function rewriteWorkerUrls(
102
- build: DankBuild,
70
+ dirs: DankDirectories,
103
71
  registry: WebsiteRegistry,
104
72
  ) {
105
- const workers = registry.workers()
73
+ const workers = registry.workers
106
74
  if (!workers) {
107
75
  return
108
76
  }
@@ -115,7 +83,7 @@ export async function rewriteWorkerUrls(
115
83
  const readingFiles = Promise.all(
116
84
  dependentBundlePaths.map(async p => {
117
85
  bundleOutputs[p] = await readFile(
118
- join(build.dirs.projectRootAbs, build.dirs.buildDist, p),
86
+ join(dirs.projectRootAbs, dirs.buildDist, p),
119
87
  'utf8',
120
88
  )
121
89
  }),
@@ -127,8 +95,8 @@ export async function rewriteWorkerUrls(
127
95
  for (const w of workers) {
128
96
  rewriteChains[registry.mappedHref(w.dependentEntryPoint)].push(s =>
129
97
  s.replace(
130
- createWorkerRegex(w.workerUrlPlaceholder),
131
- `new Worker('${registry.mappedHref(w.workerEntryPoint)}')`,
98
+ createWorkerRegex(w.workerCtor, w.workerUrlPlaceholder),
99
+ `new ${w.workerCtor}('${registry.mappedHref(w.workerEntryPoint)}')`,
132
100
  ),
133
101
  )
134
102
  }
@@ -144,16 +112,19 @@ export async function rewriteWorkerUrls(
144
112
  result = rewriteFn(result)
145
113
  }
146
114
  await writeFile(
147
- join(build.dirs.projectRootAbs, build.dirs.buildDist, p),
115
+ join(dirs.projectRootAbs, dirs.buildDist, p),
148
116
  result,
149
117
  )
150
118
  }),
151
119
  )
152
120
  }
153
121
 
154
- export function createWorkerRegex(workerUrl: string): RegExp {
122
+ export function createWorkerRegex(
123
+ workerCtor: 'Worker' | 'SharedWorker',
124
+ workerUrl: string,
125
+ ): RegExp {
155
126
  return new RegExp(
156
- `new(?:\\s|\\r?\\n)+Worker(?:\\s|\\r?\\n)*\\((?:\\s|\\r?\\n)*['"]${workerUrl}['"](?:\\s|\\r?\\n)*\\)`,
127
+ `new(?:\\s|\\r?\\n)+${workerCtor}(?:\\s|\\r?\\n)*\\((?:\\s|\\r?\\n)*['"]${workerUrl}['"](?:\\s|\\r?\\n)*\\)`,
157
128
  'g',
158
129
  )
159
130
  }
package/lib/build_tag.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { exec } from 'node:child_process'
2
- import type { DankBuild } from './flags.ts'
2
+ import type { DankFlags } from './flags.ts'
3
3
 
4
- export async function createBuildTag(build: DankBuild): Promise<string> {
4
+ export async function createBuildTag(flags: DankFlags): Promise<string> {
5
5
  const now = new Date()
6
6
  const ms =
7
7
  now.getUTCMilliseconds() +
@@ -11,7 +11,7 @@ export async function createBuildTag(build: DankBuild): Promise<string> {
11
11
  const date = now.toISOString().substring(0, 10)
12
12
  const time = String(ms).padStart(8, '0')
13
13
  const when = `${date}-${time}`
14
- if (build.production) {
14
+ if (flags.production) {
15
15
  const gitHash = await new Promise((res, rej) =>
16
16
  exec('git rev-parse --short HEAD', (err, stdout) => {
17
17
  if (err) rej(err)
package/lib/config.ts CHANGED
@@ -1,18 +1,379 @@
1
1
  import { isAbsolute, resolve } from 'node:path'
2
- import type { DankConfig } from './dank.ts'
2
+ import type {
3
+ DankConfig,
4
+ DankDetails,
5
+ EsbuildConfig,
6
+ PageMapping,
7
+ } from './dank.ts'
8
+ import { LOG } from './developer.ts'
9
+ import { defaultProjectDirs, type DankDirectories } from './dirs.ts'
10
+ import {
11
+ resolveFlags as lookupDankFlags,
12
+ type DankFlags as DankFlags,
13
+ } from './flags.ts'
3
14
 
4
- const CFG_P = './dank.config.ts'
15
+ const DEFAULT_DEV_PORT = 3000
16
+ const DEFAULT_PREVIEW_PORT = 4000
17
+ const DEFAULT_ESBUILD_PORT = 3995
5
18
 
6
- export async function loadConfig(path: string = CFG_P): Promise<DankConfig> {
7
- const modulePath = `${resolveConfigPath(path)}?${Date.now()}`
8
- const module = await import(modulePath)
9
- return await module.default
19
+ const DEFAULT_CONFIG_PATH = './dank.config.ts'
20
+
21
+ export type { DevService } from './dank.ts'
22
+
23
+ export type ResolvedDankConfig = {
24
+ // static from process boot
25
+ get dirs(): Readonly<DankDirectories>
26
+ get flags(): Readonly<Omit<DankFlags, 'dankPort' | 'esbuildPort'>>
27
+ get mode(): 'build' | 'serve'
28
+
29
+ // reloadable from `dank.config.ts` with `reload()`
30
+ get dankPort(): number
31
+ get esbuildPort(): number
32
+ get esbuild(): Readonly<Omit<EsbuildConfig, 'port'>> | undefined
33
+ get pages(): Readonly<Record<`/${string}`, PageMapping>>
34
+ get devPages(): Readonly<DankConfig['devPages']>
35
+ get services(): Readonly<DankConfig['services']>
36
+
37
+ reload(): Promise<void>
38
+ }
39
+
40
+ export async function loadConfig(
41
+ mode: 'build' | 'serve',
42
+ projectRootAbs: string,
43
+ ): Promise<ResolvedDankConfig> {
44
+ if (!isAbsolute(projectRootAbs)) {
45
+ throw Error()
46
+ }
47
+ const modulePath = resolve(projectRootAbs, DEFAULT_CONFIG_PATH)
48
+ LOG({
49
+ realm: 'config',
50
+ message: 'loading config module',
51
+ data: {
52
+ modulePath,
53
+ },
54
+ })
55
+ const dirs = await defaultProjectDirs(projectRootAbs)
56
+ const c = new DankConfigInternal(mode, modulePath, dirs)
57
+ await c.reload()
58
+ return c
59
+ }
60
+
61
+ class DankConfigInternal implements ResolvedDankConfig {
62
+ #dirs: Readonly<DankDirectories>
63
+ #flags: Readonly<DankFlags>
64
+ #mode: 'build' | 'serve'
65
+ #modulePath: string
66
+
67
+ #dankPort: number = DEFAULT_DEV_PORT
68
+ #esbuildPort: number = DEFAULT_ESBUILD_PORT
69
+ #esbuild: Readonly<Omit<EsbuildConfig, 'port'>> | undefined
70
+ #pages: Readonly<Record<`/${string}`, PageMapping>> = {}
71
+ #devPages: Readonly<DankConfig['devPages']>
72
+ #services: Readonly<DankConfig['services']>
73
+
74
+ constructor(
75
+ mode: 'build' | 'serve',
76
+ modulePath: string,
77
+ dirs: DankDirectories,
78
+ ) {
79
+ this.#dirs = dirs
80
+ this.#flags = lookupDankFlags()
81
+ this.#mode = mode
82
+ this.#modulePath = modulePath
83
+ }
84
+
85
+ get dankPort(): number {
86
+ return this.#dankPort
87
+ }
88
+
89
+ get esbuildPort(): number {
90
+ return this.#esbuildPort
91
+ }
92
+
93
+ get esbuild(): Omit<EsbuildConfig, 'port'> | undefined {
94
+ return this.#esbuild
95
+ }
96
+
97
+ get dirs(): Readonly<DankDirectories> {
98
+ return this.#dirs
99
+ }
100
+
101
+ get flags(): Readonly<Omit<DankFlags, 'dankPort' | 'esbuildPort'>> {
102
+ return this.#flags
103
+ }
104
+
105
+ get mode(): 'build' | 'serve' {
106
+ return this.#mode
107
+ }
108
+
109
+ get pages(): Readonly<Record<`/${string}`, PageMapping>> {
110
+ return this.#pages
111
+ }
112
+
113
+ get devPages(): Readonly<DankConfig['devPages']> {
114
+ return this.#devPages
115
+ }
116
+
117
+ get services(): Readonly<DankConfig['services']> {
118
+ return this.#services
119
+ }
120
+
121
+ async reload() {
122
+ const userConfig = await resolveConfig(
123
+ this.#modulePath,
124
+ resolveDankDetails(this.#mode, this.#flags),
125
+ )
126
+ this.#dankPort = resolveDankPort(this.#flags, userConfig)
127
+ this.#esbuildPort = resolveEsbuildPort(this.#flags, userConfig)
128
+ this.#esbuild = Object.freeze(userConfig.esbuild)
129
+ this.#pages = Object.freeze(normalizePages(userConfig.pages))
130
+ this.#devPages = Object.freeze(userConfig.devPages)
131
+ this.#services = Object.freeze(userConfig.services)
132
+ }
133
+ }
134
+
135
+ function resolveDankPort(flags: DankFlags, userConfig: DankConfig): number {
136
+ return (
137
+ flags.dankPort ||
138
+ (flags.preview
139
+ ? userConfig.previewPort || userConfig.port || DEFAULT_PREVIEW_PORT
140
+ : userConfig.port || DEFAULT_DEV_PORT)
141
+ )
142
+ }
143
+
144
+ function resolveEsbuildPort(flags: DankFlags, userConfig: DankConfig): number {
145
+ return flags.esbuildPort || userConfig.esbuild?.port || DEFAULT_ESBUILD_PORT
146
+ }
147
+
148
+ async function resolveConfig(
149
+ modulePath: string,
150
+ details: DankDetails,
151
+ ): Promise<DankConfig> {
152
+ const module = await import(`${modulePath}?${Date.now()}`)
153
+ const c: Partial<DankConfig> =
154
+ typeof module.default === 'function'
155
+ ? await module.default(details)
156
+ : module.default
157
+ validateDankConfig(c)
158
+ return c as DankConfig
159
+ }
160
+
161
+ function resolveDankDetails(
162
+ mode: 'build' | 'serve',
163
+ flags: DankFlags,
164
+ ): DankDetails {
165
+ return {
166
+ dev: !flags.production,
167
+ production: flags.production,
168
+ mode,
169
+ }
170
+ }
171
+
172
+ function validateDankConfig(c: Partial<DankConfig>) {
173
+ try {
174
+ validatePorts(c)
175
+ validatePages(c.pages)
176
+ validateDevPages(c.devPages)
177
+ validateDevServices(c.services)
178
+ validateEsbuildConfig(c.esbuild)
179
+ } catch (e: any) {
180
+ LOG({
181
+ realm: 'config',
182
+ message: 'validation error',
183
+ data: {
184
+ error: e.message,
185
+ },
186
+ })
187
+ throw e
188
+ }
189
+ }
190
+
191
+ function validatePorts(c: Partial<DankConfig>) {
192
+ if (c.port !== null && typeof c.port !== 'undefined') {
193
+ if (typeof c.port !== 'number') {
194
+ throw Error('DankConfig.port must be a number')
195
+ }
196
+ }
197
+ if (c.previewPort !== null && typeof c.previewPort !== 'undefined') {
198
+ if (typeof c.previewPort !== 'number') {
199
+ throw Error('DankConfig.previewPort must be a number')
200
+ }
201
+ }
202
+ }
203
+
204
+ function validateEsbuildConfig(esbuild?: EsbuildConfig) {
205
+ if (esbuild?.loaders !== null && typeof esbuild?.loaders !== 'undefined') {
206
+ if (typeof esbuild.loaders !== 'object') {
207
+ throw Error(
208
+ 'DankConfig.esbuild.loaders must be a map of extensions to esbuild loaders',
209
+ )
210
+ } else {
211
+ for (const [ext, loader] of Object.entries(esbuild.loaders)) {
212
+ if (typeof loader !== 'string') {
213
+ throw Error(
214
+ `DankConfig.esbuild.loaders['${ext}'] must be a string of a loader name`,
215
+ )
216
+ }
217
+ }
218
+ }
219
+ }
220
+ if (esbuild?.plugins !== null && typeof esbuild?.plugins !== 'undefined') {
221
+ if (!Array.isArray(esbuild.plugins)) {
222
+ throw Error(
223
+ 'DankConfig.esbuild.plugins must be an array of esbuild plugins',
224
+ )
225
+ }
226
+ }
227
+ if (esbuild?.port !== null && typeof esbuild?.port !== 'undefined') {
228
+ if (typeof esbuild.port !== 'number') {
229
+ throw Error('DankConfig.esbuild.port must be a number')
230
+ }
231
+ }
232
+ }
233
+
234
+ function validatePages(pages?: DankConfig['pages']) {
235
+ if (
236
+ pages === null ||
237
+ typeof pages === 'undefined' ||
238
+ Object.keys(pages).length === 0
239
+ ) {
240
+ throw Error('DankConfig.pages is required')
241
+ }
242
+ for (const [urlPath, mapping] of Object.entries(pages)) {
243
+ if (typeof mapping === 'string' && mapping.endsWith('.html')) {
244
+ continue
245
+ }
246
+ if (typeof mapping === 'object') {
247
+ validatePageMapping(urlPath, mapping)
248
+ continue
249
+ }
250
+ throw Error(
251
+ `DankConfig.pages['${urlPath}'] must configure an html file`,
252
+ )
253
+ }
254
+ }
255
+
256
+ function validateDevPages(devPages?: DankConfig['devPages']) {
257
+ if (devPages) {
258
+ for (const [urlPath, mapping] of Object.entries(devPages)) {
259
+ if (!urlPath.startsWith('/__')) {
260
+ throw Error(
261
+ `DankConfig.devPages['${urlPath}'] must start \`${urlPath}\` with a \`/__\` path prefix`,
262
+ )
263
+ }
264
+ if (typeof mapping === 'string') {
265
+ if (!mapping.endsWith('.html')) {
266
+ throw Error(
267
+ `DankConfig.devPages['${urlPath}'] must configure an html file or DevPageMapping config`,
268
+ )
269
+ }
270
+ } else if (typeof mapping === 'object') {
271
+ if (
272
+ typeof mapping.label !== 'string' ||
273
+ !mapping.label.length
274
+ ) {
275
+ throw Error(
276
+ `DankConfig.devPages['${urlPath}'].label must declare a label`,
277
+ )
278
+ }
279
+ if (
280
+ typeof mapping.webpage !== 'string' ||
281
+ !mapping.webpage.endsWith('.html')
282
+ ) {
283
+ throw Error(
284
+ `DankConfig.devPages['${urlPath}'].webpage must configure an html file`,
285
+ )
286
+ }
287
+ } else {
288
+ throw Error(
289
+ `DankConfig.devPages['${urlPath}'] must be a DevPageMapping config with \`label\` and \`webpage\` values`,
290
+ )
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ function validatePageMapping(urlPath: string, mapping: PageMapping) {
297
+ if (
298
+ mapping.webpage === null ||
299
+ typeof mapping.webpage !== 'string' ||
300
+ !mapping.webpage.endsWith('.html')
301
+ ) {
302
+ throw Error(
303
+ `DankConfig.pages['${urlPath}'].webpage must configure an html file`,
304
+ )
305
+ }
306
+ if (mapping.pattern === null || typeof mapping.pattern === 'undefined') {
307
+ return
308
+ }
309
+ if (
310
+ typeof mapping.pattern === 'object' &&
311
+ mapping.pattern.constructor.name === 'RegExp'
312
+ ) {
313
+ return
314
+ }
315
+ throw Error(`DankConfig.pages['${urlPath}'].pattern must be a RegExp`)
316
+ }
317
+
318
+ function validateDevServices(services: DankConfig['services']) {
319
+ if (services === null || typeof services === 'undefined') {
320
+ return
321
+ }
322
+ if (!Array.isArray(services)) {
323
+ throw Error(`DankConfig.services must be an array`)
324
+ }
325
+ for (let i = 0; i < services.length; i++) {
326
+ const s = services[i]
327
+ if (s.command === null || typeof s.command === 'undefined') {
328
+ throw Error(`DankConfig.services[${i}].command is required`)
329
+ } else if (typeof s.command !== 'string' || s.command.length === 0) {
330
+ throw Error(
331
+ `DankConfig.services[${i}].command must be a non-empty string`,
332
+ )
333
+ }
334
+ if (s.cwd !== null && typeof s.cwd !== 'undefined') {
335
+ if (typeof s.cwd !== 'string' || s.cwd.trim().length === 0) {
336
+ throw Error(
337
+ `DankConfig.services[${i}].cwd must be a non-empty string`,
338
+ )
339
+ }
340
+ }
341
+ if (s.env !== null && typeof s.env !== 'undefined') {
342
+ if (typeof s.env !== 'object') {
343
+ throw Error(
344
+ `DankConfig.services[${i}].env must be an env variable map`,
345
+ )
346
+ }
347
+ for (const [k, v] of Object.entries(s.env)) {
348
+ if (typeof v !== 'string') {
349
+ throw Error(
350
+ `DankConfig.services[${i}].env[${k}] must be a string`,
351
+ )
352
+ }
353
+ }
354
+ }
355
+ if (s.http !== null && typeof s.http !== 'undefined') {
356
+ if (typeof s.http.port !== 'number') {
357
+ throw Error(
358
+ `DankConfig.services[${i}].http.port must be a number`,
359
+ )
360
+ }
361
+ }
362
+ }
10
363
  }
11
364
 
12
- export function resolveConfigPath(path: string): string {
13
- if (isAbsolute(path)) {
14
- return path
15
- } else {
16
- return resolve(process.cwd(), path)
365
+ function normalizePages(
366
+ pages: DankConfig['pages'],
367
+ ): Record<`/${string}`, PageMapping> {
368
+ const result: Record<`/${string}`, PageMapping> = {}
369
+ for (const [pageUrl, mapping] of Object.entries(pages)) {
370
+ const mappedMapping =
371
+ typeof mapping === 'string' ? { webpage: mapping } : mapping
372
+ mappedMapping.webpage = mappedMapping.webpage.replace(
373
+ /^\.\//,
374
+ '',
375
+ ) as `${string}.html`
376
+ result[pageUrl as `/${string}`] = mappedMapping
17
377
  }
378
+ return result
18
379
  }