@eighty4/dank 0.0.3 → 0.0.4-1
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 +133 -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 +239 -60
- package/lib/flags.ts +154 -10
- package/lib/html.ts +320 -116
- package/lib/http.ts +195 -47
- package/lib/metadata.ts +309 -0
- package/lib/public.ts +21 -13
- package/lib/serve.ts +205 -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 +166 -54
- package/lib_js/flags.js +123 -8
- package/lib_js/html.js +197 -87
- package/lib_js/http.js +111 -30
- package/lib_js/metadata.js +210 -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/http.ts
CHANGED
|
@@ -8,34 +8,162 @@ import {
|
|
|
8
8
|
type ServerResponse,
|
|
9
9
|
} from 'node:http'
|
|
10
10
|
import { extname, join } from 'node:path'
|
|
11
|
+
import { Readable } from 'node:stream'
|
|
11
12
|
import mime from 'mime'
|
|
12
|
-
import {
|
|
13
|
+
import type { DankServe } from './flags.ts'
|
|
14
|
+
import type { WebsiteManifest } from './metadata.ts'
|
|
15
|
+
import type { HttpServices } from './services.ts'
|
|
13
16
|
|
|
14
17
|
export type FrontendFetcher = (
|
|
15
18
|
url: URL,
|
|
16
19
|
headers: Headers,
|
|
17
20
|
res: ServerResponse,
|
|
21
|
+
notFound: () => void,
|
|
18
22
|
) => void
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
// state needed to eval url rewriting after FrontendFetcher and before HttpServices
|
|
25
|
+
export type PageRouteState = {
|
|
26
|
+
// urls of html entrypoints
|
|
27
|
+
urls: Array<string>
|
|
28
|
+
urlRewrites: Array<UrlRewrite>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type UrlRewrite = {
|
|
32
|
+
pattern: RegExp
|
|
33
|
+
url: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function startWebServer(
|
|
37
|
+
serve: DankServe,
|
|
22
38
|
frontendFetcher: FrontendFetcher,
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
httpServices: HttpServices,
|
|
40
|
+
pageRoutes: PageRouteState,
|
|
41
|
+
) {
|
|
42
|
+
const serverAddress = 'http://localhost:' + serve.dankPort
|
|
25
43
|
const handler = (req: IncomingMessage, res: ServerResponse) => {
|
|
26
44
|
if (!req.url || !req.method) {
|
|
27
45
|
res.end()
|
|
28
46
|
} else {
|
|
29
47
|
const url = new URL(serverAddress + req.url)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
const headers = convertHeadersToFetch(req.headers)
|
|
49
|
+
frontendFetcher(url, headers, res, () =>
|
|
50
|
+
onNotFound(
|
|
51
|
+
req,
|
|
52
|
+
url,
|
|
53
|
+
headers,
|
|
54
|
+
httpServices,
|
|
55
|
+
pageRoutes,
|
|
56
|
+
serve,
|
|
57
|
+
res,
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
createServer(serve.logHttp ? createLogWrapper(handler) : handler).listen(
|
|
63
|
+
serve.dankPort,
|
|
64
|
+
)
|
|
65
|
+
console.log(
|
|
66
|
+
serve.preview ? 'preview' : 'dev',
|
|
67
|
+
`server is live at http://127.0.0.1:${serve.dankPort}`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function onNotFound(
|
|
72
|
+
req: IncomingMessage,
|
|
73
|
+
url: URL,
|
|
74
|
+
headers: Headers,
|
|
75
|
+
httpServices: HttpServices,
|
|
76
|
+
pageRoutes: PageRouteState,
|
|
77
|
+
serve: DankServe,
|
|
78
|
+
res: ServerResponse,
|
|
79
|
+
) {
|
|
80
|
+
if (req.method === 'GET' && extname(url.pathname) === '') {
|
|
81
|
+
const urlRewrite = tryUrlRewrites(url, pageRoutes, serve)
|
|
82
|
+
if (urlRewrite) {
|
|
83
|
+
streamFile(urlRewrite, res)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const fetchResponse = await tryHttpServices(req, url, headers, httpServices)
|
|
88
|
+
if (fetchResponse) {
|
|
89
|
+
sendFetchResponse(res, fetchResponse)
|
|
90
|
+
} else {
|
|
91
|
+
res.writeHead(404)
|
|
92
|
+
res.end()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function sendFetchResponse(res: ServerResponse, fetchResponse: Response) {
|
|
97
|
+
res.writeHead(
|
|
98
|
+
fetchResponse.status,
|
|
99
|
+
undefined,
|
|
100
|
+
convertHeadersFromFetch(fetchResponse.headers),
|
|
101
|
+
)
|
|
102
|
+
if (fetchResponse.body) {
|
|
103
|
+
Readable.fromWeb(fetchResponse.body).pipe(res)
|
|
104
|
+
} else {
|
|
105
|
+
res.end()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function tryUrlRewrites(
|
|
110
|
+
url: URL,
|
|
111
|
+
pageRoutes: PageRouteState,
|
|
112
|
+
serve: DankServe,
|
|
113
|
+
): string | null {
|
|
114
|
+
const urlRewrite = pageRoutes.urlRewrites.find(urlRewrite =>
|
|
115
|
+
urlRewrite.pattern.test(url.pathname),
|
|
116
|
+
)
|
|
117
|
+
return urlRewrite
|
|
118
|
+
? join(serve.dirs.buildWatch, urlRewrite.url, 'index.html')
|
|
119
|
+
: null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function tryHttpServices(
|
|
123
|
+
req: IncomingMessage,
|
|
124
|
+
url: URL,
|
|
125
|
+
headers: Headers,
|
|
126
|
+
httpServices: HttpServices,
|
|
127
|
+
): Promise<Response | null> {
|
|
128
|
+
if (url.pathname.startsWith('/.well-known/')) {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
const body = await collectReqBody(req)
|
|
132
|
+
const { running } = httpServices
|
|
133
|
+
for (const httpService of running) {
|
|
134
|
+
const proxyUrl = new URL(url)
|
|
135
|
+
proxyUrl.port = `${httpService.port}`
|
|
136
|
+
try {
|
|
137
|
+
const response = await retryFetchWithTimeout(proxyUrl, {
|
|
138
|
+
body,
|
|
139
|
+
headers,
|
|
140
|
+
method: req.method,
|
|
141
|
+
redirect: 'manual',
|
|
142
|
+
})
|
|
143
|
+
if (response.status === 404 || response.status === 405) {
|
|
144
|
+
continue
|
|
33
145
|
} else {
|
|
34
|
-
|
|
146
|
+
return response
|
|
147
|
+
}
|
|
148
|
+
} catch (e: any) {
|
|
149
|
+
if (e === 'retrytimeout') {
|
|
150
|
+
continue
|
|
151
|
+
} else {
|
|
152
|
+
errorExit(
|
|
153
|
+
`unexpected error http proxying to port ${httpService.port}: ${e.message}`,
|
|
154
|
+
)
|
|
35
155
|
}
|
|
36
156
|
}
|
|
37
157
|
}
|
|
38
|
-
return
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function collectReqBody(req: IncomingMessage): Promise<string | null> {
|
|
162
|
+
let body = ''
|
|
163
|
+
req.on('data', data => (body += data.toString()))
|
|
164
|
+
return new Promise(res =>
|
|
165
|
+
req.on('end', () => res(body.length ? body : null)),
|
|
166
|
+
)
|
|
39
167
|
}
|
|
40
168
|
|
|
41
169
|
type RequestListener = (req: IncomingMessage, res: ServerResponse) => void
|
|
@@ -51,57 +179,65 @@ function createLogWrapper(handler: RequestListener): RequestListener {
|
|
|
51
179
|
|
|
52
180
|
export function createBuiltDistFilesFetcher(
|
|
53
181
|
dir: string,
|
|
54
|
-
|
|
182
|
+
manifest: WebsiteManifest,
|
|
55
183
|
): FrontendFetcher {
|
|
56
|
-
return (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
184
|
+
return (
|
|
185
|
+
url: URL,
|
|
186
|
+
_headers: Headers,
|
|
187
|
+
res: ServerResponse,
|
|
188
|
+
notFound: () => void,
|
|
189
|
+
) => {
|
|
190
|
+
if (manifest.pageUrls.has(url.pathname)) {
|
|
191
|
+
streamFile(join(dir, url.pathname, 'index.html'), res)
|
|
192
|
+
} else if (manifest.files.has(url.pathname)) {
|
|
193
|
+
streamFile(join(dir, url.pathname), res)
|
|
60
194
|
} else {
|
|
61
|
-
|
|
62
|
-
extname(url.pathname) === ''
|
|
63
|
-
? join(dir, url.pathname, 'index.html')
|
|
64
|
-
: join(dir, url.pathname)
|
|
65
|
-
streamFile(p, res)
|
|
195
|
+
notFound()
|
|
66
196
|
}
|
|
67
197
|
}
|
|
68
198
|
}
|
|
69
199
|
|
|
70
|
-
|
|
71
|
-
// ref of original DankConfig['pages'] mapping
|
|
72
|
-
// updated incrementally instead of replacing
|
|
73
|
-
pages: Record<string, string>
|
|
74
|
-
// dir processed html files are written to
|
|
75
|
-
pagesDir: string
|
|
76
|
-
// port to esbuild dev server
|
|
77
|
-
proxyPort: number
|
|
78
|
-
// dir of public assets
|
|
79
|
-
publicDir: string
|
|
80
|
-
}
|
|
81
|
-
|
|
200
|
+
// todo replace PageRouteState with WebsiteRegistry
|
|
82
201
|
export function createDevServeFilesFetcher(
|
|
83
|
-
|
|
202
|
+
pageRoutes: PageRouteState,
|
|
203
|
+
serve: DankServe,
|
|
84
204
|
): FrontendFetcher {
|
|
85
|
-
const proxyAddress = 'http://127.0.0.1:' +
|
|
86
|
-
return (
|
|
87
|
-
|
|
88
|
-
|
|
205
|
+
const proxyAddress = 'http://127.0.0.1:' + serve.esbuildPort
|
|
206
|
+
return (
|
|
207
|
+
url: URL,
|
|
208
|
+
_headers: Headers,
|
|
209
|
+
res: ServerResponse,
|
|
210
|
+
notFound: () => void,
|
|
211
|
+
) => {
|
|
212
|
+
if (pageRoutes.urls.includes(url.pathname)) {
|
|
213
|
+
streamFile(
|
|
214
|
+
join(serve.dirs.buildWatch, url.pathname, 'index.html'),
|
|
215
|
+
res,
|
|
216
|
+
)
|
|
89
217
|
} else {
|
|
90
|
-
const maybePublicPath = join(
|
|
91
|
-
exists(
|
|
218
|
+
const maybePublicPath = join(serve.dirs.public, url.pathname)
|
|
219
|
+
exists(maybePublicPath).then(fromPublic => {
|
|
92
220
|
if (fromPublic) {
|
|
93
221
|
streamFile(maybePublicPath, res)
|
|
94
222
|
} else {
|
|
95
223
|
retryFetchWithTimeout(proxyAddress + url.pathname)
|
|
96
224
|
.then(fetchResponse => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
225
|
+
if (fetchResponse.status === 404) {
|
|
226
|
+
notFound()
|
|
227
|
+
} else {
|
|
228
|
+
res.writeHead(
|
|
229
|
+
fetchResponse.status,
|
|
230
|
+
convertHeadersFromFetch(
|
|
231
|
+
fetchResponse.headers,
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
fetchResponse
|
|
235
|
+
.bytes()
|
|
236
|
+
.then(data => res.end(data))
|
|
237
|
+
}
|
|
102
238
|
})
|
|
103
239
|
.catch(e => {
|
|
104
|
-
if (e
|
|
240
|
+
if (isFetchRetryTimeout(e)) {
|
|
105
241
|
res.writeHead(504)
|
|
106
242
|
} else {
|
|
107
243
|
console.error(
|
|
@@ -121,11 +257,14 @@ export function createDevServeFilesFetcher(
|
|
|
121
257
|
const PROXY_FETCH_RETRY_INTERVAL = 27
|
|
122
258
|
const PROXY_FETCH_RETRY_TIMEOUT = 1000
|
|
123
259
|
|
|
124
|
-
async function retryFetchWithTimeout(
|
|
260
|
+
async function retryFetchWithTimeout(
|
|
261
|
+
url: URL | string,
|
|
262
|
+
requestInit?: RequestInit,
|
|
263
|
+
): Promise<Response> {
|
|
125
264
|
let timeout = Date.now() + PROXY_FETCH_RETRY_TIMEOUT
|
|
126
265
|
while (true) {
|
|
127
266
|
try {
|
|
128
|
-
return await fetch(url)
|
|
267
|
+
return await fetch(url, requestInit)
|
|
129
268
|
} catch (e: any) {
|
|
130
269
|
if (isNodeFailedFetch(e) || isBunFailedFetch(e)) {
|
|
131
270
|
if (timeout < Date.now()) {
|
|
@@ -142,6 +281,10 @@ async function retryFetchWithTimeout(url: string): Promise<Response> {
|
|
|
142
281
|
}
|
|
143
282
|
}
|
|
144
283
|
|
|
284
|
+
function isFetchRetryTimeout(e: any): boolean {
|
|
285
|
+
return e === 'retrytimeout'
|
|
286
|
+
}
|
|
287
|
+
|
|
145
288
|
function isBunFailedFetch(e: any): boolean {
|
|
146
289
|
return e.code === 'ConnectionRefused'
|
|
147
290
|
}
|
|
@@ -189,3 +332,8 @@ function convertHeadersToFetch(from: IncomingHttpHeaders): Headers {
|
|
|
189
332
|
}
|
|
190
333
|
return to
|
|
191
334
|
}
|
|
335
|
+
|
|
336
|
+
function errorExit(msg: string): never {
|
|
337
|
+
console.log(`\u001b[31merror:\u001b[0m`, msg)
|
|
338
|
+
process.exit(1)
|
|
339
|
+
}
|
package/lib/metadata.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import EventEmitter from 'node:events'
|
|
2
|
+
import { writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join, resolve } from 'node:path'
|
|
4
|
+
import type { BuildResult } from 'esbuild'
|
|
5
|
+
import type { EntryPoint } from './esbuild.ts'
|
|
6
|
+
import type { DankBuild, ProjectDirs } from './flags.ts'
|
|
7
|
+
|
|
8
|
+
export type ResolveError = 'outofbounds'
|
|
9
|
+
|
|
10
|
+
export type Resolver = {
|
|
11
|
+
// `p` is expected to be a relative path resolvable from the project dir
|
|
12
|
+
isProjectSubpathInPagesDir(p: string): boolean
|
|
13
|
+
|
|
14
|
+
// `p` is expected to be a relative path resolvable from the pages dir
|
|
15
|
+
isPagesSubpathInPagesDir(p: string): boolean
|
|
16
|
+
|
|
17
|
+
// resolve a pages subpath from a resource within the pages directory by a relative href
|
|
18
|
+
// `from` is expected to be a pages resource fs path starting with `pages/` and ending with filename
|
|
19
|
+
// the result will be a pages subpath and will not have the pages dir prefix
|
|
20
|
+
// returns 'outofbounds' if the relative path does not resolve to a file within the pages dir
|
|
21
|
+
resolveHrefInPagesDir(from: string, href: string): string | ResolveError
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class ResolverImpl implements Resolver {
|
|
25
|
+
#dirs: ProjectDirs
|
|
26
|
+
|
|
27
|
+
constructor(dirs: ProjectDirs) {
|
|
28
|
+
this.#dirs = dirs
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
isProjectSubpathInPagesDir(p: string): boolean {
|
|
32
|
+
return resolve(join(this.#dirs.projectResolved, p)).startsWith(
|
|
33
|
+
this.#dirs.pagesResolved,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
isPagesSubpathInPagesDir(p: string): boolean {
|
|
38
|
+
return this.isProjectSubpathInPagesDir(join(this.#dirs.pages, p))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
resolveHrefInPagesDir(from: string, href: string): string | ResolveError {
|
|
42
|
+
const p = join(dirname(from), href)
|
|
43
|
+
if (this.isProjectSubpathInPagesDir(p)) {
|
|
44
|
+
return p
|
|
45
|
+
} else {
|
|
46
|
+
return 'outofbounds'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// summary of a website build
|
|
52
|
+
export type WebsiteManifest = {
|
|
53
|
+
buildTag: string
|
|
54
|
+
files: Set<string>
|
|
55
|
+
pageUrls: Set<string>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// result of an esbuild build from the context of the config's entrypoints
|
|
59
|
+
// path of entrypoint is the reference point to lookup from a dependent page
|
|
60
|
+
export type BuildManifest = {
|
|
61
|
+
// css and js bundles mapping output path to
|
|
62
|
+
// entrypoint path or null for code splitting chunks
|
|
63
|
+
bundles: Record<string, string | null>
|
|
64
|
+
|
|
65
|
+
// web worker urls mapped by dependent entrypoints
|
|
66
|
+
// to be built with a subsequent esbuild context
|
|
67
|
+
workers: Array<WorkerManifest> | null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type OnBuildComplete = (manifest: BuildManifest) => void
|
|
71
|
+
|
|
72
|
+
type WorkerManifest = {
|
|
73
|
+
// path to module dependent on worker
|
|
74
|
+
clientScript: string
|
|
75
|
+
// path to bundled entrypoint dependent on `clientScript`
|
|
76
|
+
dependentEntryPoint: string
|
|
77
|
+
workerEntryPoint: string
|
|
78
|
+
workerUrl: string
|
|
79
|
+
workerUrlPlaceholder: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type WebsiteRegistryEvents = {
|
|
83
|
+
workers: []
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// manages website resources during `dank build` and `dank serve`
|
|
87
|
+
export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
|
|
88
|
+
#build: DankBuild
|
|
89
|
+
// paths of bundled esbuild outputs
|
|
90
|
+
#bundles: Set<string> = new Set()
|
|
91
|
+
// public dir assets
|
|
92
|
+
#copiedAssets: Set<string> | null = null
|
|
93
|
+
// map of entrypoints to their output path
|
|
94
|
+
#entrypointHrefs: Record<string, string | null> = {}
|
|
95
|
+
#pageUrls: Array<string> = []
|
|
96
|
+
#resolver: Resolver
|
|
97
|
+
#workers: Array<WorkerManifest> | null = null
|
|
98
|
+
|
|
99
|
+
constructor(build: DankBuild) {
|
|
100
|
+
super()
|
|
101
|
+
this.#build = build
|
|
102
|
+
this.#resolver = new ResolverImpl(build.dirs)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
set copiedAssets(copiedAssets: Array<string> | null) {
|
|
106
|
+
this.#copiedAssets =
|
|
107
|
+
copiedAssets === null ? null : new Set(copiedAssets)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
set pageUrls(pageUrls: Array<string>) {
|
|
111
|
+
this.#pageUrls = pageUrls
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get resolver(): Resolver {
|
|
115
|
+
return this.#resolver
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// bundleOutputs(type?: 'css' | 'js'): Array<string> {
|
|
119
|
+
// if (!type) {
|
|
120
|
+
// return Array.from(this.#bundles)
|
|
121
|
+
// } else {
|
|
122
|
+
// return Array.from(this.#bundles).filter(p => p.endsWith(type))
|
|
123
|
+
// }
|
|
124
|
+
// }
|
|
125
|
+
|
|
126
|
+
buildRegistry(): BuildRegistry {
|
|
127
|
+
return new BuildRegistry(this.#build, this.#onBuildManifest)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
files(): Set<string> {
|
|
131
|
+
const files = new Set<string>()
|
|
132
|
+
for (const pageUrl of this.#pageUrls)
|
|
133
|
+
files.add(pageUrl === '/' ? '/index.html' : `${pageUrl}/index.html`)
|
|
134
|
+
for (const f of this.#bundles) files.add(f)
|
|
135
|
+
if (this.#copiedAssets) for (const f of this.#copiedAssets) files.add(f)
|
|
136
|
+
return files
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
mappedHref(lookup: string): string {
|
|
140
|
+
const found = this.#entrypointHrefs[lookup]
|
|
141
|
+
if (found) {
|
|
142
|
+
return found
|
|
143
|
+
} else {
|
|
144
|
+
throw Error(`mapped href for ${lookup} not found`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
workerEntryPoints(): Array<EntryPoint> | null {
|
|
149
|
+
return (
|
|
150
|
+
this.#workers?.map(({ workerEntryPoint }) => ({
|
|
151
|
+
in: workerEntryPoint,
|
|
152
|
+
out: workerEntryPoint
|
|
153
|
+
.replace(/^pages[\//]/, '')
|
|
154
|
+
.replace(/\.(mj|t)s$/, '.js'),
|
|
155
|
+
})) || null
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
workers(): Array<WorkerManifest> | null {
|
|
160
|
+
return this.#workers
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async writeManifest(buildTag: string): Promise<WebsiteManifest> {
|
|
164
|
+
const manifest = this.#manifest(buildTag)
|
|
165
|
+
await writeFile(
|
|
166
|
+
join(
|
|
167
|
+
this.#build.dirs.projectRootAbs,
|
|
168
|
+
this.#build.dirs.buildRoot,
|
|
169
|
+
'website.json',
|
|
170
|
+
),
|
|
171
|
+
JSON.stringify(
|
|
172
|
+
{
|
|
173
|
+
buildTag,
|
|
174
|
+
files: Array.from(manifest.files),
|
|
175
|
+
pageUrls: Array.from(manifest.pageUrls),
|
|
176
|
+
},
|
|
177
|
+
null,
|
|
178
|
+
4,
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
return manifest
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#manifest(buildTag: string): WebsiteManifest {
|
|
185
|
+
return {
|
|
186
|
+
buildTag,
|
|
187
|
+
files: this.files(),
|
|
188
|
+
pageUrls: new Set(this.#pageUrls),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#onBuildManifest: OnBuildComplete = (build: BuildManifest) => {
|
|
193
|
+
// collect built bundle entrypoint hrefs
|
|
194
|
+
for (const [outPath, entrypoint] of Object.entries(build.bundles)) {
|
|
195
|
+
this.#bundles.add(outPath)
|
|
196
|
+
if (entrypoint) {
|
|
197
|
+
this.#entrypointHrefs[entrypoint] = outPath
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// determine if worker entrypoints have changed
|
|
202
|
+
let updatedWorkerEntrypoints = false
|
|
203
|
+
const previousWorkers =
|
|
204
|
+
this.#workers === null
|
|
205
|
+
? null
|
|
206
|
+
: new Set(this.#workers.map(w => w.workerEntryPoint))
|
|
207
|
+
if (build.workers) {
|
|
208
|
+
if (
|
|
209
|
+
!previousWorkers ||
|
|
210
|
+
previousWorkers.size !==
|
|
211
|
+
new Set(build.workers.map(w => w.workerEntryPoint)).size
|
|
212
|
+
) {
|
|
213
|
+
updatedWorkerEntrypoints = true
|
|
214
|
+
} else {
|
|
215
|
+
updatedWorkerEntrypoints = !build.workers.every(w =>
|
|
216
|
+
previousWorkers.has(w.workerEntryPoint),
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
} else if (previousWorkers) {
|
|
220
|
+
updatedWorkerEntrypoints = true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// merge unique entrypoints from built workers with registry state
|
|
224
|
+
// todo filtering out unique occurrences of clientScript and workerUrl
|
|
225
|
+
// drops reporting/summary/debugging capabilities, but currently
|
|
226
|
+
// this.#workers is used for unique worker/client entrypoints
|
|
227
|
+
if (build.workers) {
|
|
228
|
+
if (!this.#workers) {
|
|
229
|
+
this.#workers = build.workers
|
|
230
|
+
} else {
|
|
231
|
+
for (const w of build.workers) {
|
|
232
|
+
const found = this.#workers.find(w2 => {
|
|
233
|
+
return (
|
|
234
|
+
w.dependentEntryPoint === w2.dependentEntryPoint &&
|
|
235
|
+
w.workerEntryPoint === w2.workerEntryPoint
|
|
236
|
+
)
|
|
237
|
+
})
|
|
238
|
+
if (!found) {
|
|
239
|
+
this.#workers.push(w)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (updatedWorkerEntrypoints) {
|
|
246
|
+
this.emit('workers')
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// result accumulator of an esbuild `build` or `Context.rebuild`
|
|
252
|
+
export class BuildRegistry {
|
|
253
|
+
#onComplete: OnBuildComplete
|
|
254
|
+
#resolver: Resolver
|
|
255
|
+
#workers: Array<Omit<WorkerManifest, 'dependentEntryPoint'>> | null = null
|
|
256
|
+
|
|
257
|
+
constructor(
|
|
258
|
+
build: DankBuild,
|
|
259
|
+
onComplete: (manifest: BuildManifest) => void,
|
|
260
|
+
) {
|
|
261
|
+
this.#onComplete = onComplete
|
|
262
|
+
this.#resolver = new ResolverImpl(build.dirs)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
get resolver(): Resolver {
|
|
266
|
+
return this.#resolver
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// resolve web worker imported by a webpage module
|
|
270
|
+
addWorker(worker: Omit<WorkerManifest, 'dependentEntryPoint'>) {
|
|
271
|
+
// todo normalize path
|
|
272
|
+
if (!this.#workers) {
|
|
273
|
+
this.#workers = [worker]
|
|
274
|
+
} else {
|
|
275
|
+
this.#workers.push(worker)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
completeBuild(result: BuildResult<{ metafile: true }>) {
|
|
280
|
+
const bundles: Record<string, string | null> = {}
|
|
281
|
+
for (const [outPath, output] of Object.entries(
|
|
282
|
+
result.metafile.outputs,
|
|
283
|
+
)) {
|
|
284
|
+
bundles[outPath.replace(/^build[/\\]dist/, '')] =
|
|
285
|
+
output.entryPoint || null
|
|
286
|
+
}
|
|
287
|
+
let workers: BuildManifest['workers'] = null
|
|
288
|
+
if (this.#workers) {
|
|
289
|
+
workers = []
|
|
290
|
+
for (const output of Object.values(result.metafile.outputs)) {
|
|
291
|
+
if (!output.entryPoint) continue
|
|
292
|
+
const inputs = Object.keys(output.inputs)
|
|
293
|
+
for (const worker of this.#workers) {
|
|
294
|
+
if (inputs.includes(worker.clientScript)) {
|
|
295
|
+
workers.push({
|
|
296
|
+
...worker,
|
|
297
|
+
dependentEntryPoint: output.entryPoint,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.#onComplete({
|
|
305
|
+
bundles,
|
|
306
|
+
workers,
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
}
|
package/lib/public.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { copyFile, mkdir, readdir, stat } from 'node:fs/promises'
|
|
2
|
+
import { platform } from 'node:os'
|
|
2
3
|
import { join } from 'node:path'
|
|
4
|
+
import type { DankBuild } from './flags.ts'
|
|
3
5
|
|
|
4
6
|
export async function copyAssets(
|
|
5
|
-
|
|
7
|
+
build: DankBuild,
|
|
6
8
|
): Promise<Array<string> | null> {
|
|
7
9
|
try {
|
|
8
|
-
const stats = await stat(
|
|
10
|
+
const stats = await stat(build.dirs.public)
|
|
9
11
|
if (stats.isDirectory()) {
|
|
10
|
-
await mkdir(
|
|
11
|
-
return await recursiveCopyAssets(
|
|
12
|
+
await mkdir(build.dirs.buildDist, { recursive: true })
|
|
13
|
+
return await recursiveCopyAssets(build)
|
|
12
14
|
} else {
|
|
13
15
|
throw Error('./public cannot be a file')
|
|
14
16
|
}
|
|
@@ -17,26 +19,32 @@ export async function copyAssets(
|
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
const IGNORE = platform() === 'darwin' ? ['.DS_Store'] : []
|
|
23
|
+
|
|
20
24
|
async function recursiveCopyAssets(
|
|
21
|
-
|
|
25
|
+
build: DankBuild,
|
|
22
26
|
dir: string = '',
|
|
23
27
|
): Promise<Array<string>> {
|
|
24
28
|
const copied: Array<string> = []
|
|
25
|
-
const to = join(
|
|
29
|
+
const to = join(build.dirs.buildDist, dir)
|
|
26
30
|
let madeDir = dir === ''
|
|
27
|
-
|
|
31
|
+
const listingDir = join(build.dirs.public, dir)
|
|
32
|
+
for (const p of await readdir(listingDir)) {
|
|
33
|
+
if (IGNORE.includes(p)) {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
28
36
|
try {
|
|
29
|
-
const stats = await stat(join(
|
|
37
|
+
const stats = await stat(join(listingDir, p))
|
|
30
38
|
if (stats.isDirectory()) {
|
|
31
|
-
copied.push(
|
|
32
|
-
...(await recursiveCopyAssets(outRoot, join(dir, p))),
|
|
33
|
-
)
|
|
39
|
+
copied.push(...(await recursiveCopyAssets(build, join(dir, p))))
|
|
34
40
|
} else {
|
|
35
41
|
if (!madeDir) {
|
|
36
|
-
await mkdir(join(
|
|
42
|
+
await mkdir(join(build.dirs.buildDist, dir), {
|
|
43
|
+
recursive: true,
|
|
44
|
+
})
|
|
37
45
|
madeDir = true
|
|
38
46
|
}
|
|
39
|
-
await copyFile(join(
|
|
47
|
+
await copyFile(join(listingDir, p), join(to, p))
|
|
40
48
|
copied.push('/' + join(dir, p).replaceAll('\\', '/'))
|
|
41
49
|
}
|
|
42
50
|
} catch (e) {
|