@eighty4/dank 0.0.3 → 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/client/esbuild.js +8 -5
- package/lib/build.ts +128 -72
- package/lib/{tag.ts → build_tag.ts} +3 -3
- package/lib/dank.ts +147 -11
- package/lib/define.ts +4 -5
- package/lib/esbuild.ts +192 -41
- package/lib/flags.ts +148 -10
- package/lib/html.ts +325 -117
- package/lib/http.ts +195 -47
- package/lib/metadata.ts +284 -0
- package/lib/public.ts +21 -13
- package/lib/serve.ts +204 -144
- package/lib/services.ts +28 -4
- package/lib_js/build.js +82 -57
- package/lib_js/{tag.js → build_tag.js} +2 -3
- package/lib_js/dank.js +73 -5
- package/lib_js/define.js +3 -5
- package/lib_js/esbuild.js +139 -34
- package/lib_js/flags.js +118 -8
- package/lib_js/html.js +203 -88
- package/lib_js/http.js +111 -30
- package/lib_js/metadata.js +198 -0
- package/lib_js/public.js +19 -11
- package/lib_js/serve.js +135 -110
- package/lib_js/services.js +13 -2
- package/lib_types/dank.d.ts +18 -1
- package/package.json +7 -1
- package/lib/manifest.ts +0 -61
- package/lib_js/manifest.js +0 -37
package/lib/serve.ts
CHANGED
|
@@ -1,55 +1,95 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
64
|
-
c.pages[urlPath as `/${string}`] =
|
|
65
|
-
|
|
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].
|
|
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(
|
|
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,
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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<
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
.flatMap(
|
|
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 (
|
|
204
|
+
if (unique.has(entryPoint.in)) {
|
|
142
205
|
return false
|
|
143
206
|
} else {
|
|
144
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
229
|
+
const disposing = buildContext.dispose()
|
|
160
230
|
buildContext = 'disposing'
|
|
161
|
-
|
|
231
|
+
disposing.then(() => {
|
|
162
232
|
buildContext = null
|
|
163
233
|
resetBuildContext()
|
|
164
234
|
})
|
|
165
235
|
} else {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
-
|
|
294
|
+
c: DankConfig,
|
|
295
|
+
registry: WebsiteRegistry,
|
|
296
|
+
serve: DankServe,
|
|
297
|
+
entryPoints: Array<EntryPoint>,
|
|
243
298
|
): Promise<BuildContext> {
|
|
244
299
|
const ctx = await esbuildDevContext(
|
|
245
|
-
|
|
300
|
+
serve,
|
|
301
|
+
registry,
|
|
302
|
+
createGlobalDefinitions(serve),
|
|
246
303
|
entryPoints,
|
|
247
|
-
|
|
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:
|
|
311
|
+
port: serve.esbuildPort,
|
|
255
312
|
cors: {
|
|
256
|
-
origin: '
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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/build.js
CHANGED
|
@@ -1,70 +1,95 @@
|
|
|
1
|
-
import { mkdir, rm } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import { copyAssets } from "./public.js";
|
|
5
|
-
import { createBuildTag } from "./tag.js";
|
|
6
|
-
import { writeBuildManifest, writeMetafile } from "./manifest.js";
|
|
3
|
+
import { createBuildTag } from "./build_tag.js";
|
|
7
4
|
import { createGlobalDefinitions } from "./define.js";
|
|
5
|
+
import { esbuildWebpages, esbuildWorkers } from "./esbuild.js";
|
|
6
|
+
import { resolveBuildFlags } from "./flags.js";
|
|
8
7
|
import { HtmlEntrypoint } from "./html.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
? isProductionBuild()
|
|
8
|
+
import { WebsiteRegistry } from "./metadata.js";
|
|
9
|
+
import { copyAssets } from "./public.js";
|
|
10
|
+
export async function buildWebsite(c, build = resolveBuildFlags()) {
|
|
11
|
+
const buildTag = await createBuildTag(build);
|
|
12
|
+
console.log(build.minify
|
|
13
|
+
? build.production
|
|
16
14
|
? 'minified production'
|
|
17
15
|
: 'minified'
|
|
18
16
|
: 'unminified', 'build', buildTag, 'building in ./build/dist');
|
|
19
|
-
await rm(
|
|
20
|
-
await mkdir(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const buildUrls = [];
|
|
24
|
-
buildUrls.push(...(await buildWebpages(distDir, createGlobalDefinitions(), c.pages)));
|
|
25
|
-
if (staticAssets) {
|
|
26
|
-
buildUrls.push(...staticAssets);
|
|
17
|
+
await rm(build.dirs.buildRoot, { recursive: true, force: true });
|
|
18
|
+
await mkdir(build.dirs.buildDist, { recursive: true });
|
|
19
|
+
for (const subdir of Object.keys(c.pages).filter(url => url !== '/')) {
|
|
20
|
+
await mkdir(join(build.dirs.buildDist, subdir), { recursive: true });
|
|
27
21
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
await mkdir(join(build.dirs.buildRoot, 'metafiles'), { recursive: true });
|
|
23
|
+
const registry = new WebsiteRegistry(build);
|
|
24
|
+
registry.pageUrls = Object.keys(c.pages);
|
|
25
|
+
registry.copiedAssets = await copyAssets(build);
|
|
26
|
+
await buildWebpages(c, registry, build, createGlobalDefinitions(build));
|
|
27
|
+
return await registry.writeManifest(buildTag);
|
|
34
28
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
// builds all webpage entrypoints in one esbuild.build context
|
|
30
|
+
// to support code splitting
|
|
31
|
+
// returns all built assets URLs and webpage URLs from DankConfig.pages
|
|
32
|
+
async function buildWebpages(c, registry, build, define) {
|
|
33
|
+
// create HtmlEntrypoint for each webpage and collect awaitable esbuild entrypoints
|
|
34
|
+
const loadingEntryPoints = [];
|
|
35
|
+
const htmlEntrypoints = [];
|
|
36
|
+
for (const [urlPath, mapping] of Object.entries(c.pages)) {
|
|
37
|
+
const fsPath = typeof mapping === 'string' ? mapping : mapping.webpage;
|
|
38
|
+
const html = new HtmlEntrypoint(build, urlPath, fsPath);
|
|
39
|
+
loadingEntryPoints.push(new Promise(res => html.on('entrypoints', res)));
|
|
40
|
+
htmlEntrypoints.push(html);
|
|
41
|
+
}
|
|
42
|
+
// collect esbuild entrypoints from every HtmlEntrypoint
|
|
43
|
+
const uniqueEntryPoints = new Set();
|
|
44
|
+
const buildEntryPoints = [];
|
|
45
|
+
for (const pageEntryPoints of await Promise.all(loadingEntryPoints)) {
|
|
46
|
+
for (const entryPoint of pageEntryPoints) {
|
|
47
|
+
if (!uniqueEntryPoints.has(entryPoint.in)) {
|
|
48
|
+
buildEntryPoints.push(entryPoint);
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
|
-
html.collectScripts()
|
|
45
|
-
.filter(scriptImport => !entryPointUrls.has(scriptImport.in))
|
|
46
|
-
.forEach(scriptImport => {
|
|
47
|
-
entryPointUrls.add(scriptImport.in);
|
|
48
|
-
entryPoints.push({
|
|
49
|
-
in: scriptImport.in,
|
|
50
|
-
out: scriptImport.out,
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
return html;
|
|
54
|
-
}));
|
|
55
|
-
const metafile = await esbuildWebpages(define, entryPoints, distDir);
|
|
56
|
-
await writeMetafile(`pages.json`, metafile);
|
|
57
|
-
// todo these hrefs would have \ path separators on windows
|
|
58
|
-
const buildUrls = [...Object.keys(pages)];
|
|
59
|
-
const mapInToOutHrefs = {};
|
|
60
|
-
for (const [outputFile, { entryPoint }] of Object.entries(metafile.outputs)) {
|
|
61
|
-
const outputUrl = outputFile.replace(/^build\/dist/, '');
|
|
62
|
-
buildUrls.push(outputUrl);
|
|
63
|
-
mapInToOutHrefs[entryPoint] = outputUrl;
|
|
64
51
|
}
|
|
52
|
+
await esbuildWebpages(build, registry, define, buildEntryPoints, c.esbuild);
|
|
53
|
+
// todo recursively build workers on building workers that create workers
|
|
54
|
+
const workerEntryPoints = registry.workerEntryPoints();
|
|
55
|
+
if (workerEntryPoints?.length) {
|
|
56
|
+
await esbuildWorkers(build, registry, define, workerEntryPoints, c.esbuild);
|
|
57
|
+
}
|
|
58
|
+
await rewriteWorkerUrls(build, registry);
|
|
59
|
+
// write out html output with rewritten hrefs
|
|
65
60
|
await Promise.all(htmlEntrypoints.map(async (html) => {
|
|
66
|
-
html.
|
|
67
|
-
|
|
61
|
+
await writeFile(join(build.dirs.buildDist, html.url, 'index.html'), html.output(registry));
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
export async function rewriteWorkerUrls(build, registry) {
|
|
65
|
+
const workers = registry.workers();
|
|
66
|
+
if (!workers) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const dependentBundlePaths = workers.map(w => registry.mappedHref(w.dependentEntryPoint));
|
|
70
|
+
const bundleOutputs = {};
|
|
71
|
+
// collect all js file contents concurrently
|
|
72
|
+
const readingFiles = Promise.all(dependentBundlePaths.map(async (p) => {
|
|
73
|
+
bundleOutputs[p] = await readFile(join(build.dirs.projectRootAbs, build.dirs.buildDist, p), 'utf8');
|
|
74
|
+
}));
|
|
75
|
+
// build regex replacements during file reads
|
|
76
|
+
const rewriteChains = {};
|
|
77
|
+
for (const p of dependentBundlePaths)
|
|
78
|
+
rewriteChains[p] = [];
|
|
79
|
+
for (const w of workers) {
|
|
80
|
+
rewriteChains[registry.mappedHref(w.dependentEntryPoint)].push(s => s.replace(createWorkerRegex(w.workerUrlPlaceholder), `new Worker('${registry.mappedHref(w.workerEntryPoint)}')`));
|
|
81
|
+
}
|
|
82
|
+
// wait for file reads
|
|
83
|
+
await readingFiles;
|
|
84
|
+
// run rewrite regex chain and write back to dist
|
|
85
|
+
await Promise.all(Object.entries(bundleOutputs).map(async ([p, content]) => {
|
|
86
|
+
let result = content;
|
|
87
|
+
for (const rewriteFn of rewriteChains[p]) {
|
|
88
|
+
result = rewriteFn(result);
|
|
89
|
+
}
|
|
90
|
+
await writeFile(join(build.dirs.projectRootAbs, build.dirs.buildDist, p), result);
|
|
68
91
|
}));
|
|
69
|
-
|
|
92
|
+
}
|
|
93
|
+
export function createWorkerRegex(workerUrl) {
|
|
94
|
+
return new RegExp(`new(?:\\s|\\r?\\n)+Worker(?:\\s|\\r?\\n)*\\((?:\\s|\\r?\\n)*['"]${workerUrl}['"](?:\\s|\\r?\\n)*\\)`, 'g');
|
|
70
95
|
}
|