@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/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,284 @@
|
|
|
1
|
+
import EventEmitter from 'node:events'
|
|
2
|
+
import { writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
4
|
+
import type { BuildResult } from 'esbuild'
|
|
5
|
+
import type { EntryPoint } from './esbuild.ts'
|
|
6
|
+
import type { DankBuild } from './flags.ts'
|
|
7
|
+
|
|
8
|
+
export type Resolver = {
|
|
9
|
+
resolve(from: string, href: string): string | 'outofbounds'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// summary of a website build
|
|
13
|
+
export type WebsiteManifest = {
|
|
14
|
+
buildTag: string
|
|
15
|
+
files: Set<string>
|
|
16
|
+
pageUrls: Set<string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// result of an esbuild build from the context of the config's entrypoints
|
|
20
|
+
// path of entrypoint is the reference point to lookup from a dependent page
|
|
21
|
+
export type BuildManifest = {
|
|
22
|
+
// css and js bundles mapping output path to
|
|
23
|
+
// entrypoint path or null for code splitting chunks
|
|
24
|
+
bundles: Record<string, string | null>
|
|
25
|
+
|
|
26
|
+
// web worker urls mapped by dependent entrypoints
|
|
27
|
+
// to be built with a subsequent esbuild context
|
|
28
|
+
workers: Array<WorkerManifest> | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type OnBuildComplete = (manifest: BuildManifest) => void
|
|
32
|
+
|
|
33
|
+
type WorkerManifest = {
|
|
34
|
+
// path to module dependent on worker
|
|
35
|
+
clientScript: string
|
|
36
|
+
// path to bundled entrypoint dependent on `clientScript`
|
|
37
|
+
dependentEntryPoint: string
|
|
38
|
+
workerEntryPoint: string
|
|
39
|
+
workerUrl: string
|
|
40
|
+
workerUrlPlaceholder: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type WebsiteRegistryEvents = {
|
|
44
|
+
workers: []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// manages website resources during `dank build` and `dank serve`
|
|
48
|
+
export class WebsiteRegistry
|
|
49
|
+
extends EventEmitter<WebsiteRegistryEvents>
|
|
50
|
+
implements Resolver
|
|
51
|
+
{
|
|
52
|
+
#build: DankBuild
|
|
53
|
+
// paths of bundled esbuild outputs
|
|
54
|
+
#bundles: Set<string> = new Set()
|
|
55
|
+
// public dir assets
|
|
56
|
+
#copiedAssets: Set<string> | null = null
|
|
57
|
+
// map of entrypoints to their output path
|
|
58
|
+
#entrypointHrefs: Record<string, string | null> = {}
|
|
59
|
+
#pageUrls: Array<string> = []
|
|
60
|
+
#workers: Array<WorkerManifest> | null = null
|
|
61
|
+
|
|
62
|
+
constructor(build: DankBuild) {
|
|
63
|
+
super()
|
|
64
|
+
this.#build = build
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// bundleOutputs(type?: 'css' | 'js'): Array<string> {
|
|
68
|
+
// if (!type) {
|
|
69
|
+
// return Array.from(this.#bundles)
|
|
70
|
+
// } else {
|
|
71
|
+
// return Array.from(this.#bundles).filter(p => p.endsWith(type))
|
|
72
|
+
// }
|
|
73
|
+
// }
|
|
74
|
+
|
|
75
|
+
buildRegistry(): BuildRegistry {
|
|
76
|
+
return new BuildRegistry(this.#build, this.#onBuildManifest)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
files(): Set<string> {
|
|
80
|
+
const files = new Set<string>()
|
|
81
|
+
for (const pageUrl of this.#pageUrls)
|
|
82
|
+
files.add(pageUrl === '/' ? '/index.html' : `${pageUrl}/index.html`)
|
|
83
|
+
for (const f of this.#bundles) files.add(f)
|
|
84
|
+
if (this.#copiedAssets) for (const f of this.#copiedAssets) files.add(f)
|
|
85
|
+
return files
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
mappedHref(lookup: string): string {
|
|
89
|
+
const found = this.#entrypointHrefs[lookup]
|
|
90
|
+
if (found) {
|
|
91
|
+
return found
|
|
92
|
+
} else {
|
|
93
|
+
throw Error(`mapped href for ${lookup} not found`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
resolve(from: string, href: string): string {
|
|
98
|
+
return resolveImpl(this.#build, from, href)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
workerEntryPoints(): Array<EntryPoint> | null {
|
|
102
|
+
return (
|
|
103
|
+
this.#workers?.map(({ workerEntryPoint }) => ({
|
|
104
|
+
in: workerEntryPoint,
|
|
105
|
+
out: workerEntryPoint
|
|
106
|
+
.replace(/^pages[\//]/, '')
|
|
107
|
+
.replace(/\.(mj|t)s$/, '.js'),
|
|
108
|
+
})) || null
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
workers(): Array<WorkerManifest> | null {
|
|
113
|
+
return this.#workers
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async writeManifest(buildTag: string): Promise<WebsiteManifest> {
|
|
117
|
+
const manifest = this.#manifest(buildTag)
|
|
118
|
+
await writeFile(
|
|
119
|
+
join(
|
|
120
|
+
this.#build.dirs.projectRootAbs,
|
|
121
|
+
this.#build.dirs.buildRoot,
|
|
122
|
+
'website.json',
|
|
123
|
+
),
|
|
124
|
+
JSON.stringify(
|
|
125
|
+
{
|
|
126
|
+
buildTag,
|
|
127
|
+
files: Array.from(manifest.files),
|
|
128
|
+
pageUrls: Array.from(manifest.pageUrls),
|
|
129
|
+
},
|
|
130
|
+
null,
|
|
131
|
+
4,
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
return manifest
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
set copiedAssets(copiedAssets: Array<string> | null) {
|
|
138
|
+
this.#copiedAssets =
|
|
139
|
+
copiedAssets === null ? null : new Set(copiedAssets)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
set pageUrls(pageUrls: Array<string>) {
|
|
143
|
+
this.#pageUrls = pageUrls
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#manifest(buildTag: string): WebsiteManifest {
|
|
147
|
+
return {
|
|
148
|
+
buildTag,
|
|
149
|
+
files: this.files(),
|
|
150
|
+
pageUrls: new Set(this.#pageUrls),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#onBuildManifest: OnBuildComplete = (build: BuildManifest) => {
|
|
155
|
+
// collect built bundle entrypoint hrefs
|
|
156
|
+
for (const [outPath, entrypoint] of Object.entries(build.bundles)) {
|
|
157
|
+
this.#bundles.add(outPath)
|
|
158
|
+
if (entrypoint) {
|
|
159
|
+
this.#entrypointHrefs[entrypoint] = outPath
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// determine if worker entrypoints have changed
|
|
164
|
+
let updatedWorkerEntrypoints = false
|
|
165
|
+
const previousWorkers =
|
|
166
|
+
this.#workers === null
|
|
167
|
+
? null
|
|
168
|
+
: new Set(this.#workers.map(w => w.workerEntryPoint))
|
|
169
|
+
if (build.workers) {
|
|
170
|
+
if (
|
|
171
|
+
!previousWorkers ||
|
|
172
|
+
previousWorkers.size !==
|
|
173
|
+
new Set(build.workers.map(w => w.workerEntryPoint)).size
|
|
174
|
+
) {
|
|
175
|
+
updatedWorkerEntrypoints = true
|
|
176
|
+
} else {
|
|
177
|
+
updatedWorkerEntrypoints = !build.workers.every(w =>
|
|
178
|
+
previousWorkers.has(w.workerEntryPoint),
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
} else if (previousWorkers) {
|
|
182
|
+
updatedWorkerEntrypoints = true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// merge unique entrypoints from built workers with registry state
|
|
186
|
+
// todo filtering out unique occurrences of clientScript and workerUrl
|
|
187
|
+
// drops reporting/summary/debugging capabilities, but currently
|
|
188
|
+
// this.#workers is used for unique worker/client entrypoints
|
|
189
|
+
if (build.workers) {
|
|
190
|
+
if (!this.#workers) {
|
|
191
|
+
this.#workers = build.workers
|
|
192
|
+
} else {
|
|
193
|
+
for (const w of build.workers) {
|
|
194
|
+
const found = this.#workers.find(w2 => {
|
|
195
|
+
return (
|
|
196
|
+
w.dependentEntryPoint === w2.dependentEntryPoint &&
|
|
197
|
+
w.workerEntryPoint === w2.workerEntryPoint
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
if (!found) {
|
|
201
|
+
this.#workers.push(w)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (updatedWorkerEntrypoints) {
|
|
208
|
+
this.emit('workers')
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// result accumulator of an esbuild `build` or `Context.rebuild`
|
|
214
|
+
export class BuildRegistry implements Resolver {
|
|
215
|
+
#build: DankBuild
|
|
216
|
+
#onComplete: OnBuildComplete
|
|
217
|
+
#workers: Array<Omit<WorkerManifest, 'dependentEntryPoint'>> | null = null
|
|
218
|
+
|
|
219
|
+
constructor(
|
|
220
|
+
build: DankBuild,
|
|
221
|
+
onComplete: (manifest: BuildManifest) => void,
|
|
222
|
+
) {
|
|
223
|
+
this.#build = build
|
|
224
|
+
this.#onComplete = onComplete
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// resolve web worker imported by a webpage module
|
|
228
|
+
addWorker(worker: Omit<WorkerManifest, 'dependentEntryPoint'>) {
|
|
229
|
+
// todo normalize path
|
|
230
|
+
if (!this.#workers) {
|
|
231
|
+
this.#workers = [worker]
|
|
232
|
+
} else {
|
|
233
|
+
this.#workers.push(worker)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
completeBuild(result: BuildResult<{ metafile: true }>) {
|
|
238
|
+
const bundles: Record<string, string | null> = {}
|
|
239
|
+
for (const [outPath, output] of Object.entries(
|
|
240
|
+
result.metafile.outputs,
|
|
241
|
+
)) {
|
|
242
|
+
bundles[outPath.replace(/^build[/\\]dist/, '')] =
|
|
243
|
+
output.entryPoint || null
|
|
244
|
+
}
|
|
245
|
+
let workers: BuildManifest['workers'] = null
|
|
246
|
+
if (this.#workers) {
|
|
247
|
+
workers = []
|
|
248
|
+
for (const output of Object.values(result.metafile.outputs)) {
|
|
249
|
+
if (!output.entryPoint) continue
|
|
250
|
+
const inputs = Object.keys(output.inputs)
|
|
251
|
+
for (const worker of this.#workers) {
|
|
252
|
+
if (inputs.includes(worker.clientScript)) {
|
|
253
|
+
workers.push({
|
|
254
|
+
...worker,
|
|
255
|
+
dependentEntryPoint: output.entryPoint,
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.#onComplete({
|
|
263
|
+
bundles,
|
|
264
|
+
workers,
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
resolve(from: string, href: string): string {
|
|
269
|
+
return resolveImpl(this.#build, from, href)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function resolveImpl(build: DankBuild, from: string, href: string): string {
|
|
274
|
+
const { pagesResolved, projectRootAbs } = build.dirs
|
|
275
|
+
const fromDir = dirname(from)
|
|
276
|
+
const resolvedFromProjectRoot = join(projectRootAbs, fromDir, href)
|
|
277
|
+
if (!resolve(resolvedFromProjectRoot).startsWith(pagesResolved)) {
|
|
278
|
+
throw Error(
|
|
279
|
+
`href ${href} cannot be resolved from pages${sep}${from} to a path outside of the pages directory`,
|
|
280
|
+
)
|
|
281
|
+
} else {
|
|
282
|
+
return join(fromDir, href)
|
|
283
|
+
}
|
|
284
|
+
}
|
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) {
|