@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/client/client.js +1 -0
- package/lib/bin.ts +8 -11
- package/lib/build.ts +41 -70
- package/lib/build_tag.ts +3 -3
- package/lib/config.ts +372 -11
- package/lib/dank.ts +21 -150
- package/lib/define.ts +6 -4
- package/lib/developer.ts +146 -0
- package/lib/dirs.ts +83 -0
- package/lib/errors.ts +6 -0
- package/lib/esbuild.ts +19 -29
- package/lib/flags.ts +15 -121
- package/lib/html.ts +196 -112
- package/lib/http.ts +59 -43
- package/lib/public.ts +10 -10
- package/lib/{metadata.ts → registry.ts} +216 -83
- package/lib/serve.ts +118 -270
- package/lib/services.ts +8 -8
- package/lib/watch.ts +39 -0
- package/lib_js/bin.js +79 -85
- package/lib_js/build.js +63 -86
- package/lib_js/build_tag.js +20 -21
- package/lib_js/config.js +237 -18
- package/lib_js/dank.js +5 -122
- package/lib_js/define.js +8 -5
- package/lib_js/dirs.js +61 -0
- package/lib_js/errors.js +9 -0
- package/lib_js/esbuild.js +155 -167
- package/lib_js/flags.js +30 -123
- package/lib_js/html.js +280 -231
- package/lib_js/http.js +176 -195
- package/lib_js/public.js +45 -46
- package/lib_js/registry.js +260 -0
- package/lib_js/serve.js +109 -251
- package/lib_js/services.js +152 -171
- package/lib_js/watch.js +35 -0
- package/lib_types/dank.d.ts +13 -1
- package/package.json +10 -4
- package/client/esbuild.js +0 -91
- package/lib_js/metadata.js +0 -210
package/client/client.js
ADDED
|
@@ -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 {
|
|
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(
|
|
77
|
+
await buildWebsite()
|
|
80
78
|
console.log(green('done'))
|
|
81
79
|
process.exit(0)
|
|
82
80
|
case 'serve':
|
|
83
|
-
await serveWebsite(
|
|
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 (
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
4
|
+
import { loadConfig, type ResolvedDankConfig } from './config.ts'
|
|
5
5
|
import { type DefineDankGlobal, createGlobalDefinitions } from './define.ts'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
|
14
|
-
build: DankBuild = resolveBuildFlags(),
|
|
12
|
+
c?: ResolvedDankConfig,
|
|
15
13
|
): Promise<WebsiteManifest> {
|
|
16
|
-
|
|
14
|
+
if (!c) {
|
|
15
|
+
c = await loadConfig('build', process.cwd())
|
|
16
|
+
}
|
|
17
|
+
const buildTag = await createBuildTag(c.flags)
|
|
17
18
|
console.log(
|
|
18
|
-
|
|
19
|
-
?
|
|
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(
|
|
28
|
-
await mkdir(
|
|
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(
|
|
31
|
+
await mkdir(join(c.dirs.buildDist, subdir), { recursive: true })
|
|
31
32
|
}
|
|
32
|
-
await mkdir(join(
|
|
33
|
-
const registry =
|
|
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:
|
|
45
|
-
registry: WebsiteRegistry,
|
|
46
|
-
build: DankBuild,
|
|
41
|
+
c: ResolvedDankConfig,
|
|
47
42
|
define: DefineDankGlobal,
|
|
48
|
-
) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
115
|
+
join(dirs.projectRootAbs, dirs.buildDist, p),
|
|
148
116
|
result,
|
|
149
117
|
)
|
|
150
118
|
}),
|
|
151
119
|
)
|
|
152
120
|
}
|
|
153
121
|
|
|
154
|
-
export function createWorkerRegex(
|
|
122
|
+
export function createWorkerRegex(
|
|
123
|
+
workerCtor: 'Worker' | 'SharedWorker',
|
|
124
|
+
workerUrl: string,
|
|
125
|
+
): RegExp {
|
|
155
126
|
return new RegExp(
|
|
156
|
-
`new(?:\\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 {
|
|
2
|
+
import type { DankFlags } from './flags.ts'
|
|
3
3
|
|
|
4
|
-
export async function createBuildTag(
|
|
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 (
|
|
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 {
|
|
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
|
|
15
|
+
const DEFAULT_DEV_PORT = 3000
|
|
16
|
+
const DEFAULT_PREVIEW_PORT = 4000
|
|
17
|
+
const DEFAULT_ESBUILD_PORT = 3995
|
|
5
18
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
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
|
}
|