@eighty4/dank 0.0.4-0 → 0.0.4-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/client/esbuild.js +1 -91
- package/lib/bin.ts +1 -1
- package/lib/build.ts +8 -5
- package/lib/config.ts +211 -5
- package/lib/dank.ts +14 -150
- package/lib/developer.ts +117 -0
- package/lib/esbuild.ts +146 -118
- package/lib/flags.ts +10 -4
- package/lib/html.ts +10 -14
- package/lib/metadata.ts +65 -40
- package/lib/serve.ts +94 -10
- package/lib_js/bin.js +80 -84
- package/lib_js/build.js +72 -81
- package/lib_js/build_tag.js +20 -21
- package/lib_js/config.js +158 -18
- package/lib_js/dank.js +5 -122
- package/lib_js/define.js +8 -5
- package/lib_js/esbuild.js +161 -164
- package/lib_js/flags.js +115 -114
- package/lib_js/html.js +214 -233
- package/lib_js/http.js +174 -193
- package/lib_js/metadata.js +183 -191
- package/lib_js/public.js +45 -46
- package/lib_js/serve.js +212 -230
- package/lib_js/services.js +152 -171
- package/lib_types/dank.d.ts +8 -1
- package/package.json +5 -1
package/client/esbuild.js
CHANGED
|
@@ -1,91 +1 @@
|
|
|
1
|
-
new EventSource(
|
|
2
|
-
const { updated } = JSON.parse(e.data);
|
|
3
|
-
const changes = new Set();
|
|
4
|
-
for (const c of updated)
|
|
5
|
-
changes.add(c);
|
|
6
|
-
const cssUpdates = Array.from(changes).filter(p => p.endsWith('.css'));
|
|
7
|
-
if (cssUpdates.length) {
|
|
8
|
-
console.log('esbuild css updates', cssUpdates);
|
|
9
|
-
const cssLinks = {};
|
|
10
|
-
for (const elem of document.getElementsByTagName('link')) {
|
|
11
|
-
if (elem.getAttribute('rel') === 'stylesheet') {
|
|
12
|
-
const url = new URL(elem.href);
|
|
13
|
-
if ((url.host = location.host)) {
|
|
14
|
-
cssLinks[url.pathname] = elem;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
let swappedCss = false;
|
|
19
|
-
for (const cssUpdate of cssUpdates) {
|
|
20
|
-
const cssLink = cssLinks[cssUpdate];
|
|
21
|
-
if (cssLink) {
|
|
22
|
-
const next = cssLink.cloneNode();
|
|
23
|
-
next.href = `${cssUpdate}?${Math.random().toString(36).slice(2)}`;
|
|
24
|
-
next.onload = () => cssLink.remove();
|
|
25
|
-
cssLink.parentNode.insertBefore(next, cssLink.nextSibling);
|
|
26
|
-
swappedCss = true;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (swappedCss) {
|
|
30
|
-
addCssUpdateIndicator();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (cssUpdates.length < changes.size) {
|
|
34
|
-
const jsUpdates = Array.from(changes).filter(p => !p.endsWith('.css'));
|
|
35
|
-
const jsScripts = new Set();
|
|
36
|
-
for (const elem of document.getElementsByTagName('script')) {
|
|
37
|
-
if (elem.src.length) {
|
|
38
|
-
const url = new URL(elem.src);
|
|
39
|
-
if ((url.host = location.host)) {
|
|
40
|
-
jsScripts.add(url.pathname);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (jsUpdates.some(jsUpdate => jsScripts.has(jsUpdate))) {
|
|
45
|
-
console.log('esbuild js updates require reload');
|
|
46
|
-
addJsReloadIndicator();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
export function addCssUpdateIndicator() {
|
|
51
|
-
const indicator = createUpdateIndicator('green', '9999');
|
|
52
|
-
indicator.style.opacity = '0';
|
|
53
|
-
indicator.animate([
|
|
54
|
-
{ opacity: 0 },
|
|
55
|
-
{ opacity: 1 },
|
|
56
|
-
{ opacity: 1 },
|
|
57
|
-
{ opacity: 1 },
|
|
58
|
-
{ opacity: 0.75 },
|
|
59
|
-
{ opacity: 0.5 },
|
|
60
|
-
{ opacity: 0.25 },
|
|
61
|
-
{ opacity: 0 },
|
|
62
|
-
], {
|
|
63
|
-
duration: 400,
|
|
64
|
-
iterations: 1,
|
|
65
|
-
direction: 'normal',
|
|
66
|
-
easing: 'linear',
|
|
67
|
-
});
|
|
68
|
-
document.body.appendChild(indicator);
|
|
69
|
-
Promise.all(indicator.getAnimations().map(a => a.finished)).then(() => indicator.remove());
|
|
70
|
-
}
|
|
71
|
-
function addJsReloadIndicator() {
|
|
72
|
-
const indicator = createUpdateIndicator('orange', '9000');
|
|
73
|
-
indicator.style.opacity = '0';
|
|
74
|
-
indicator.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
75
|
-
duration: 400,
|
|
76
|
-
iterations: 1,
|
|
77
|
-
direction: 'normal',
|
|
78
|
-
easing: 'ease-in',
|
|
79
|
-
});
|
|
80
|
-
document.body.appendChild(indicator);
|
|
81
|
-
}
|
|
82
|
-
function createUpdateIndicator(color, zIndex) {
|
|
83
|
-
const indicator = document.createElement('div');
|
|
84
|
-
indicator.style.border = '6px dashed ' + color;
|
|
85
|
-
indicator.style.zIndex = zIndex;
|
|
86
|
-
indicator.style.position = 'fixed';
|
|
87
|
-
indicator.style.top = indicator.style.left = '1px';
|
|
88
|
-
indicator.style.height = indicator.style.width = 'calc(100% - 2px)';
|
|
89
|
-
indicator.style.boxSizing = 'border-box';
|
|
90
|
-
return indicator;
|
|
91
|
-
}
|
|
1
|
+
new EventSource("http://127.0.0.1:3995/esbuild").addEventListener("change",t=>{const{updated:i}=JSON.parse(t.data),e=new Set;for(const s of i)e.add(s);const c=Array.from(e).filter(s=>s.endsWith(".css"));if(c.length){console.log("esbuild css updates",c);const s={};for(const o of document.getElementsByTagName("link"))if(o.getAttribute("rel")==="stylesheet"){const n=new URL(o.href);(n.host=location.host)&&(s[n.pathname]=o)}let a=!1;for(const o of c){const n=s[o];if(n){const r=n.cloneNode();r.href=`${o}?${Math.random().toString(36).slice(2)}`,r.onload=()=>n.remove(),n.parentNode.insertBefore(r,n.nextSibling),a=!0}}a&&l()}if(c.length<e.size){const s=Array.from(e).filter(o=>!o.endsWith(".css")),a=new Set;for(const o of document.getElementsByTagName("script"))if(o.src.length){const n=new URL(o.src);(n.host=location.host)&&a.add(n.pathname)}s.some(o=>a.has(o))&&(console.log("esbuild js updates require reload"),p())}});function l(){const t=d("green","9999");t.style.opacity="0",t.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(t),Promise.all(t.getAnimations().map(i=>i.finished)).then(()=>t.remove())}function p(){const t=d("orange","9000");t.style.opacity="0",t.animate([{opacity:0},{opacity:1}],{duration:400,iterations:1,direction:"normal",easing:"ease-in"}),document.body.appendChild(t)}function d(t,i){const e=document.createElement("div");return e.style.border="6px dashed "+t,e.style.zIndex=i,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}export{l as addCssUpdateIndicator};
|
package/lib/bin.ts
CHANGED
package/lib/build.ts
CHANGED
|
@@ -9,10 +9,8 @@ import { HtmlEntrypoint } from './html.ts'
|
|
|
9
9
|
import { type WebsiteManifest, WebsiteRegistry } from './metadata.ts'
|
|
10
10
|
import { copyAssets } from './public.ts'
|
|
11
11
|
|
|
12
|
-
export async function buildWebsite(
|
|
13
|
-
|
|
14
|
-
build: DankBuild = resolveBuildFlags(),
|
|
15
|
-
): Promise<WebsiteManifest> {
|
|
12
|
+
export async function buildWebsite(c: DankConfig): Promise<WebsiteManifest> {
|
|
13
|
+
const build: DankBuild = resolveBuildFlags()
|
|
16
14
|
const buildTag = await createBuildTag(build)
|
|
17
15
|
console.log(
|
|
18
16
|
build.minify
|
|
@@ -51,7 +49,12 @@ async function buildWebpages(
|
|
|
51
49
|
const htmlEntrypoints: Array<HtmlEntrypoint> = []
|
|
52
50
|
for (const [urlPath, mapping] of Object.entries(c.pages)) {
|
|
53
51
|
const fsPath = typeof mapping === 'string' ? mapping : mapping.webpage
|
|
54
|
-
const html = new HtmlEntrypoint(
|
|
52
|
+
const html = new HtmlEntrypoint(
|
|
53
|
+
build,
|
|
54
|
+
registry.resolver,
|
|
55
|
+
urlPath,
|
|
56
|
+
fsPath,
|
|
57
|
+
)
|
|
55
58
|
loadingEntryPoints.push(new Promise(res => html.on('entrypoints', res)))
|
|
56
59
|
htmlEntrypoints.push(html)
|
|
57
60
|
}
|
package/lib/config.ts
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
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 { isProductionBuild } from './flags.ts'
|
|
3
10
|
|
|
4
11
|
const CFG_P = './dank.config.ts'
|
|
5
12
|
|
|
6
|
-
export async function loadConfig(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
export async function loadConfig(
|
|
14
|
+
mode: 'build' | 'serve',
|
|
15
|
+
path: string = CFG_P,
|
|
16
|
+
): Promise<DankConfig> {
|
|
17
|
+
const modulePath = resolveConfigPath(path)
|
|
18
|
+
LOG({
|
|
19
|
+
realm: 'config',
|
|
20
|
+
message: 'loading config module',
|
|
21
|
+
data: {
|
|
22
|
+
modulePath,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
const c = await resolveConfig(mode, modulePath)
|
|
26
|
+
normalizePagePaths(c.pages)
|
|
27
|
+
return c
|
|
10
28
|
}
|
|
11
29
|
|
|
12
30
|
export function resolveConfigPath(path: string): string {
|
|
@@ -16,3 +34,191 @@ export function resolveConfigPath(path: string): string {
|
|
|
16
34
|
return resolve(process.cwd(), path)
|
|
17
35
|
}
|
|
18
36
|
}
|
|
37
|
+
|
|
38
|
+
export async function resolveConfig(
|
|
39
|
+
mode: 'build' | 'serve',
|
|
40
|
+
modulePath: string,
|
|
41
|
+
): Promise<DankConfig> {
|
|
42
|
+
const module = await import(`${modulePath}?${Date.now()}`)
|
|
43
|
+
const c: Partial<DankConfig> =
|
|
44
|
+
typeof module.default === 'function'
|
|
45
|
+
? await module.default(resolveDankDetails(mode))
|
|
46
|
+
: module.default
|
|
47
|
+
validateDankConfig(c)
|
|
48
|
+
return c as DankConfig
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveDankDetails(mode: 'build' | 'serve'): DankDetails {
|
|
52
|
+
const production = isProductionBuild()
|
|
53
|
+
return {
|
|
54
|
+
dev: !production,
|
|
55
|
+
production,
|
|
56
|
+
mode,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateDankConfig(c: Partial<DankConfig>) {
|
|
61
|
+
try {
|
|
62
|
+
validatePorts(c)
|
|
63
|
+
validatePages(c.pages)
|
|
64
|
+
validateDevServices(c.services)
|
|
65
|
+
validateEsbuildConfig(c.esbuild)
|
|
66
|
+
} catch (e: any) {
|
|
67
|
+
LOG({
|
|
68
|
+
realm: 'config',
|
|
69
|
+
message: 'validation error',
|
|
70
|
+
data: {
|
|
71
|
+
error: e.message,
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
throw e
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validatePorts(c: Partial<DankConfig>) {
|
|
79
|
+
if (c.port !== null && typeof c.port !== 'undefined') {
|
|
80
|
+
if (typeof c.port !== 'number') {
|
|
81
|
+
throw Error('DankConfig.port must be a number')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (c.previewPort !== null && typeof c.previewPort !== 'undefined') {
|
|
85
|
+
if (typeof c.previewPort !== 'number') {
|
|
86
|
+
throw Error('DankConfig.previewPort must be a number')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateEsbuildConfig(esbuild?: EsbuildConfig) {
|
|
92
|
+
if (esbuild?.loaders !== null && typeof esbuild?.loaders !== 'undefined') {
|
|
93
|
+
if (typeof esbuild.loaders !== 'object') {
|
|
94
|
+
throw Error(
|
|
95
|
+
'DankConfig.esbuild.loaders must be a map of extensions to esbuild loaders',
|
|
96
|
+
)
|
|
97
|
+
} else {
|
|
98
|
+
for (const [ext, loader] of Object.entries(esbuild.loaders)) {
|
|
99
|
+
if (typeof loader !== 'string') {
|
|
100
|
+
throw Error(
|
|
101
|
+
`DankConfig.esbuild.loaders['${ext}'] must be a string of a loader name`,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (esbuild?.plugins !== null && typeof esbuild?.plugins !== 'undefined') {
|
|
108
|
+
if (!Array.isArray(esbuild.plugins)) {
|
|
109
|
+
throw Error(
|
|
110
|
+
'DankConfig.esbuild.plugins must be an array of esbuild plugins',
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (esbuild?.port !== null && typeof esbuild?.port !== 'undefined') {
|
|
115
|
+
if (typeof esbuild.port !== 'number') {
|
|
116
|
+
throw Error('DankConfig.esbuild.port must be a number')
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validatePages(pages?: DankConfig['pages']) {
|
|
122
|
+
if (
|
|
123
|
+
pages === null ||
|
|
124
|
+
typeof pages === 'undefined' ||
|
|
125
|
+
Object.keys(pages).length === 0
|
|
126
|
+
) {
|
|
127
|
+
throw Error('DankConfig.pages is required')
|
|
128
|
+
}
|
|
129
|
+
for (const [urlPath, mapping] of Object.entries(pages)) {
|
|
130
|
+
if (typeof mapping === 'string' && mapping.endsWith('.html')) {
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
if (typeof mapping === 'object') {
|
|
134
|
+
validatePageMapping(urlPath, mapping)
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
throw Error(
|
|
138
|
+
`DankConfig.pages['${urlPath}'] must configure an html file`,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function validatePageMapping(urlPath: string, mapping: PageMapping) {
|
|
144
|
+
if (
|
|
145
|
+
mapping.webpage === null ||
|
|
146
|
+
typeof mapping.webpage !== 'string' ||
|
|
147
|
+
!mapping.webpage.endsWith('.html')
|
|
148
|
+
) {
|
|
149
|
+
throw Error(
|
|
150
|
+
`DankConfig.pages['${urlPath}'].webpage must configure an html file`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
if (mapping.pattern === null || typeof mapping.pattern === 'undefined') {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
if (
|
|
157
|
+
typeof mapping.pattern === 'object' &&
|
|
158
|
+
mapping.pattern.constructor.name === 'RegExp'
|
|
159
|
+
) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
throw Error(`DankConfig.pages['${urlPath}'].pattern must be a RegExp`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validateDevServices(services: DankConfig['services']) {
|
|
166
|
+
if (services === null || typeof services === 'undefined') {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
if (!Array.isArray(services)) {
|
|
170
|
+
throw Error(`DankConfig.services must be an array`)
|
|
171
|
+
}
|
|
172
|
+
for (let i = 0; i < services.length; i++) {
|
|
173
|
+
const s = services[i]
|
|
174
|
+
if (s.command === null || typeof s.command === 'undefined') {
|
|
175
|
+
throw Error(`DankConfig.services[${i}].command is required`)
|
|
176
|
+
} else if (typeof s.command !== 'string' || s.command.length === 0) {
|
|
177
|
+
throw Error(
|
|
178
|
+
`DankConfig.services[${i}].command must be a non-empty string`,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
if (s.cwd !== null && typeof s.cwd !== 'undefined') {
|
|
182
|
+
if (typeof s.cwd !== 'string' || s.cwd.trim().length === 0) {
|
|
183
|
+
throw Error(
|
|
184
|
+
`DankConfig.services[${i}].cwd must be a non-empty string`,
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (s.env !== null && typeof s.env !== 'undefined') {
|
|
189
|
+
if (typeof s.env !== 'object') {
|
|
190
|
+
throw Error(
|
|
191
|
+
`DankConfig.services[${i}].env must be an env variable map`,
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
for (const [k, v] of Object.entries(s.env)) {
|
|
195
|
+
if (typeof v !== 'string') {
|
|
196
|
+
throw Error(
|
|
197
|
+
`DankConfig.services[${i}].env[${k}] must be a string`,
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (s.http !== null && typeof s.http !== 'undefined') {
|
|
203
|
+
if (typeof s.http.port !== 'number') {
|
|
204
|
+
throw Error(
|
|
205
|
+
`DankConfig.services[${i}].http.port must be a number`,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizePagePaths(pages: DankConfig['pages']) {
|
|
213
|
+
for (const [pageUrl, mapping] of Object.entries(pages)) {
|
|
214
|
+
if (typeof mapping === 'string') {
|
|
215
|
+
pages[pageUrl as `/${string}`] = normalizePagePath(mapping)
|
|
216
|
+
} else {
|
|
217
|
+
mapping.webpage = normalizePagePath(mapping.webpage)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizePagePath(p: `${string}.html`): `${string}.html` {
|
|
223
|
+
return p.replace(/^\.\//, '') as `${string}.html`
|
|
224
|
+
}
|
package/lib/dank.ts
CHANGED
|
@@ -66,157 +66,21 @@ export type EsbuildLoader =
|
|
|
66
66
|
| 'json'
|
|
67
67
|
| 'text'
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
throw Error('DankConfig.port must be a number')
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (c.previewPort !== null && typeof c.previewPort !== 'undefined') {
|
|
78
|
-
if (typeof c.previewPort !== 'number') {
|
|
79
|
-
throw Error('DankConfig.previewPort must be a number')
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
validatePages(c.pages)
|
|
83
|
-
validateDevServices(c.services)
|
|
84
|
-
validateEsbuildConfig(c.esbuild)
|
|
85
|
-
normalizePagePaths(c.pages!)
|
|
86
|
-
return c as DankConfig
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function validateEsbuildConfig(esbuild?: EsbuildConfig) {
|
|
90
|
-
if (esbuild?.loaders !== null && typeof esbuild?.loaders !== 'undefined') {
|
|
91
|
-
if (typeof esbuild.loaders !== 'object') {
|
|
92
|
-
throw Error(
|
|
93
|
-
'DankConfig.esbuild.loaders must be a map of extensions to esbuild loaders',
|
|
94
|
-
)
|
|
95
|
-
} else {
|
|
96
|
-
for (const [ext, loader] of Object.entries(esbuild.loaders)) {
|
|
97
|
-
if (typeof loader !== 'string') {
|
|
98
|
-
throw Error(
|
|
99
|
-
`DankConfig.esbuild.loaders['${ext}'] must be a string of a loader name`,
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (esbuild?.plugins !== null && typeof esbuild?.plugins !== 'undefined') {
|
|
106
|
-
if (!Array.isArray(esbuild.plugins)) {
|
|
107
|
-
throw Error(
|
|
108
|
-
'DankConfig.esbuild.plugins must be an array of esbuild plugins',
|
|
109
|
-
)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (esbuild?.port !== null && typeof esbuild?.port !== 'undefined') {
|
|
113
|
-
if (typeof esbuild.port !== 'number') {
|
|
114
|
-
throw Error('DankConfig.esbuild.port must be a number')
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function validatePages(pages?: DankConfig['pages']) {
|
|
120
|
-
if (
|
|
121
|
-
pages === null ||
|
|
122
|
-
typeof pages === 'undefined' ||
|
|
123
|
-
Object.keys(pages).length === 0
|
|
124
|
-
) {
|
|
125
|
-
throw Error('DankConfig.pages is required')
|
|
126
|
-
}
|
|
127
|
-
for (const [urlPath, mapping] of Object.entries(pages)) {
|
|
128
|
-
if (typeof mapping === 'string' && mapping.endsWith('.html')) {
|
|
129
|
-
continue
|
|
130
|
-
}
|
|
131
|
-
if (typeof mapping === 'object') {
|
|
132
|
-
validatePageMapping(urlPath, mapping)
|
|
133
|
-
continue
|
|
134
|
-
}
|
|
135
|
-
throw Error(
|
|
136
|
-
`DankConfig.pages['${urlPath}'] must configure an html file`,
|
|
137
|
-
)
|
|
138
|
-
}
|
|
69
|
+
// DankConfigFunction arg details about a dank process used when building DankConfig
|
|
70
|
+
export type DankDetails = {
|
|
71
|
+
dev: boolean
|
|
72
|
+
production: boolean
|
|
73
|
+
mode: 'build' | 'serve'
|
|
139
74
|
}
|
|
140
75
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
typeof mapping.webpage !== 'string' ||
|
|
145
|
-
!mapping.webpage.endsWith('.html')
|
|
146
|
-
) {
|
|
147
|
-
throw Error(
|
|
148
|
-
`DankConfig.pages['${urlPath}'].webpage must configure an html file`,
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
if (mapping.pattern === null || typeof mapping.pattern === 'undefined') {
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
if (
|
|
155
|
-
typeof mapping.pattern === 'object' &&
|
|
156
|
-
mapping.pattern.constructor.name === 'RegExp'
|
|
157
|
-
) {
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
throw Error(`DankConfig.pages['${urlPath}'].pattern must be a RegExp`)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function validateDevServices(services: DankConfig['services']) {
|
|
164
|
-
if (services === null || typeof services === 'undefined') {
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
if (!Array.isArray(services)) {
|
|
168
|
-
throw Error(`DankConfig.services must be an array`)
|
|
169
|
-
}
|
|
170
|
-
for (let i = 0; i < services.length; i++) {
|
|
171
|
-
const s = services[i]
|
|
172
|
-
if (s.command === null || typeof s.command === 'undefined') {
|
|
173
|
-
throw Error(`DankConfig.services[${i}].command is required`)
|
|
174
|
-
} else if (typeof s.command !== 'string' || s.command.length === 0) {
|
|
175
|
-
throw Error(
|
|
176
|
-
`DankConfig.services[${i}].command must be a non-empty string`,
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
if (s.cwd !== null && typeof s.cwd !== 'undefined') {
|
|
180
|
-
if (typeof s.cwd !== 'string' || s.cwd.trim().length === 0) {
|
|
181
|
-
throw Error(
|
|
182
|
-
`DankConfig.services[${i}].cwd must be a non-empty string`,
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (s.env !== null && typeof s.env !== 'undefined') {
|
|
187
|
-
if (typeof s.env !== 'object') {
|
|
188
|
-
throw Error(
|
|
189
|
-
`DankConfig.services[${i}].env must be an env variable map`,
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
for (const [k, v] of Object.entries(s.env)) {
|
|
193
|
-
if (typeof v !== 'string') {
|
|
194
|
-
throw Error(
|
|
195
|
-
`DankConfig.services[${i}].env[${k}] must be a string`,
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (s.http !== null && typeof s.http !== 'undefined') {
|
|
201
|
-
if (typeof s.http.port !== 'number') {
|
|
202
|
-
throw Error(
|
|
203
|
-
`DankConfig.services[${i}].http.port must be a number`,
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function normalizePagePaths(pages: DankConfig['pages']) {
|
|
211
|
-
for (const [pageUrl, mapping] of Object.entries(pages)) {
|
|
212
|
-
if (typeof mapping === 'string') {
|
|
213
|
-
pages[pageUrl as `/${string}`] = normalizePagePath(mapping)
|
|
214
|
-
} else {
|
|
215
|
-
mapping.webpage = normalizePagePath(mapping.webpage)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
76
|
+
export type DankConfigFunction = (
|
|
77
|
+
dank: DankDetails,
|
|
78
|
+
) => Partial<DankConfig> | Promise<Partial<DankConfig>>
|
|
219
79
|
|
|
220
|
-
function
|
|
221
|
-
|
|
80
|
+
export function defineConfig(config: Partial<DankConfig>): Partial<DankConfig>
|
|
81
|
+
export function defineConfig(config: DankConfigFunction): DankConfigFunction
|
|
82
|
+
export function defineConfig(
|
|
83
|
+
config: Partial<DankConfig> | DankConfigFunction,
|
|
84
|
+
): Partial<DankConfig> | DankConfigFunction {
|
|
85
|
+
return config
|
|
222
86
|
}
|
package/lib/developer.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createWriteStream, type WriteStream } from 'node:fs'
|
|
2
|
+
import { mkdir, rm } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import { dirname, resolve } from 'node:path'
|
|
5
|
+
import packageJson from '../package.json' with { type: 'json' }
|
|
6
|
+
|
|
7
|
+
const FILE = process.env.DANK_LOG_FILE
|
|
8
|
+
const ROLLING =
|
|
9
|
+
process.env.DANK_LOG_ROLLING === '1' ||
|
|
10
|
+
process.env.DANK_LOG_ROLLING === 'true'
|
|
11
|
+
|
|
12
|
+
const logs: Array<string> = []
|
|
13
|
+
let initialized = false
|
|
14
|
+
let preparing: Promise<void>
|
|
15
|
+
let stream: WriteStream
|
|
16
|
+
|
|
17
|
+
export type LogEvent = {
|
|
18
|
+
realm:
|
|
19
|
+
| 'build'
|
|
20
|
+
| 'serve'
|
|
21
|
+
| 'assets'
|
|
22
|
+
| 'config'
|
|
23
|
+
| 'html'
|
|
24
|
+
| 'registry'
|
|
25
|
+
| 'services'
|
|
26
|
+
message: string
|
|
27
|
+
data?: Record<string, LogEventData>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type LogEventData =
|
|
31
|
+
| LogEventDatum
|
|
32
|
+
| Array<LogEventDatum>
|
|
33
|
+
| Set<LogEventDatum>
|
|
34
|
+
| Record<string, LogEventDatum>
|
|
35
|
+
|
|
36
|
+
type LogEventDatum = boolean | number | string | null
|
|
37
|
+
|
|
38
|
+
function toStringLogEvent(logEvent: LogEvent): string {
|
|
39
|
+
const when = new Date().toISOString()
|
|
40
|
+
const message = `[${logEvent.realm}] ${logEvent.message}\n${when}\n`
|
|
41
|
+
if (!logEvent.data) {
|
|
42
|
+
return message
|
|
43
|
+
}
|
|
44
|
+
let data = ''
|
|
45
|
+
for (const k of Object.keys(logEvent.data).sort()) {
|
|
46
|
+
data += `\n ${k} = ${toStringData(logEvent.data[k])}`
|
|
47
|
+
}
|
|
48
|
+
return `${message}${data}\n`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toStringData(datum: LogEventData): string {
|
|
52
|
+
if (datum instanceof Set) {
|
|
53
|
+
datum = Array.from(datum)
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
datum !== null &&
|
|
57
|
+
typeof datum === 'object' &&
|
|
58
|
+
datum.constructor.name === 'Object'
|
|
59
|
+
) {
|
|
60
|
+
datum = Object.entries(datum).map(([k, v]) => `${k} = ${v}`)
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(datum)) {
|
|
63
|
+
if (datum.length === 0) {
|
|
64
|
+
return '[]'
|
|
65
|
+
} else {
|
|
66
|
+
return `[\n ${datum.join('\n ')}\n ]`
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
return `${datum}`
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function logToFile(logEvent: LogEvent) {
|
|
74
|
+
logs.push(toStringLogEvent(logEvent))
|
|
75
|
+
if (!initialized) {
|
|
76
|
+
initialized = true
|
|
77
|
+
preparing = prepareLogFile().catch(onPrepareLogFileError)
|
|
78
|
+
}
|
|
79
|
+
preparing.then(syncLogs)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function prepareLogFile() {
|
|
83
|
+
const path = resolve(FILE!)
|
|
84
|
+
if (!ROLLING) {
|
|
85
|
+
await rm(path, { force: true })
|
|
86
|
+
}
|
|
87
|
+
await mkdir(dirname(path), { recursive: true })
|
|
88
|
+
stream = createWriteStream(path, { flags: 'a' })
|
|
89
|
+
console.log('debug logging to', FILE)
|
|
90
|
+
logSystemDetails()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function logSystemDetails() {
|
|
94
|
+
stream.write(`\
|
|
95
|
+
---
|
|
96
|
+
os: ${os.type()}
|
|
97
|
+
build: ${os.version()}
|
|
98
|
+
cpu: ${os.arch()}
|
|
99
|
+
cores: ${os.availableParallelism()}
|
|
100
|
+
${process.versions.bun ? `bun ${process.versions.bun}` : `node ${process.version}`}
|
|
101
|
+
dank: ${packageJson.version}
|
|
102
|
+
\n`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function syncLogs() {
|
|
106
|
+
if (!logs.length) return
|
|
107
|
+
const content = logs.join('\n') + '\n'
|
|
108
|
+
logs.length = 0
|
|
109
|
+
stream.write(content)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onPrepareLogFileError(e: any) {
|
|
113
|
+
console.error(`init log file \`${FILE}\` error: ${e.message}`)
|
|
114
|
+
process.exit(1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const LOG = FILE?.length ? logToFile : () => {}
|