@eighty4/dank 0.0.5-3 → 0.0.5-5
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/ServiceWorker.ts +89 -0
- package/lib/bin.ts +6 -5
- package/lib/build.ts +57 -4
- package/lib/config.ts +80 -13
- package/lib/dank.ts +34 -0
- package/lib/esbuild.ts +1 -0
- package/lib/http.ts +3 -3
- package/lib/public.ts +4 -4
- package/lib/registry.ts +62 -35
- package/lib/service_worker.ts +63 -0
- package/lib_js/bin.js +6 -5
- package/lib_js/build.js +40 -3
- package/lib_js/config.js +57 -6
- package/lib_js/dank.js +2 -0
- package/lib_js/esbuild.js +1 -0
- package/lib_js/http.js +2 -2
- package/lib_js/public.js +1 -1
- package/lib_js/registry.js +40 -15
- package/lib_js/service_worker.js +47 -0
- package/lib_types/dank.d.ts +17 -0
- package/lib_types/service_worker.d.ts +10 -0
- package/package.json +3 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import website from 'DANK:sw'
|
|
2
|
+
|
|
3
|
+
declare const self: ServiceWorkerGlobalScope
|
|
4
|
+
|
|
5
|
+
self.addEventListener('install', (e: ExtendableEvent) =>
|
|
6
|
+
e.waitUntil(populateCache()),
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
self.addEventListener('activate', (e: ExtendableEvent) =>
|
|
10
|
+
e.waitUntil(cleanupCaches()),
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
self.addEventListener('fetch', (e: FetchEvent) =>
|
|
14
|
+
e.respondWith(handleRequest(e.request)),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const PREFIX_APP_CACHE_KEY = 'DANK-website-'
|
|
18
|
+
const APP_CACHE_KEY: string = PREFIX_APP_CACHE_KEY + website.cacheKey
|
|
19
|
+
|
|
20
|
+
async function populateCache() {
|
|
21
|
+
const cache = await self.caches.open(APP_CACHE_KEY)
|
|
22
|
+
const previousCacheKey = await swapCurrentCacheKey()
|
|
23
|
+
if (!previousCacheKey) {
|
|
24
|
+
await cache.addAll(website.files)
|
|
25
|
+
} else {
|
|
26
|
+
const previousCache = await self.caches.open(previousCacheKey)
|
|
27
|
+
await Promise.all(
|
|
28
|
+
website.files.map(async f => {
|
|
29
|
+
const previouslyCached = await previousCache.match(f)
|
|
30
|
+
if (previouslyCached) {
|
|
31
|
+
await cache.put(f, previouslyCached)
|
|
32
|
+
} else {
|
|
33
|
+
await cache.add(f)
|
|
34
|
+
}
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function swapCurrentCacheKey(): Promise<string | null> {
|
|
41
|
+
const META_CACHE_KEY = 'DANK-meta'
|
|
42
|
+
const CACHE_KEY_URL = '/DANK/current'
|
|
43
|
+
const metaCache = await self.caches.open(META_CACHE_KEY)
|
|
44
|
+
const previousCacheKeyResponse = await metaCache.match(CACHE_KEY_URL)
|
|
45
|
+
const previousCacheKey = previousCacheKeyResponse
|
|
46
|
+
? await previousCacheKeyResponse.text()
|
|
47
|
+
: null
|
|
48
|
+
await metaCache.put(
|
|
49
|
+
CACHE_KEY_URL,
|
|
50
|
+
new Response(APP_CACHE_KEY, {
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'text/plain',
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
return previousCacheKey
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function cleanupCaches() {
|
|
60
|
+
const cacheKeys = await self.caches.keys()
|
|
61
|
+
for (const cacheKey of cacheKeys) {
|
|
62
|
+
if (cacheKey !== APP_CACHE_KEY) {
|
|
63
|
+
await self.caches.delete(cacheKey)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// todo implement page mapping url rewrites here
|
|
69
|
+
// url.pathname = mappedUrlPath
|
|
70
|
+
async function handleRequest(req: Request): Promise<Response> {
|
|
71
|
+
const url = new URL(req.url)
|
|
72
|
+
if (req.method === 'GET' && !bypassCache(url)) {
|
|
73
|
+
const cache = await caches.open(APP_CACHE_KEY)
|
|
74
|
+
const fromCache = await cache.match(url)
|
|
75
|
+
if (fromCache) {
|
|
76
|
+
return fromCache
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return fetch(req)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// todo support RegExp
|
|
83
|
+
function bypassCache(url: URL): boolean {
|
|
84
|
+
return (
|
|
85
|
+
website.bypassCache?.hosts?.includes(url.host) ||
|
|
86
|
+
website.bypassCache?.paths?.includes(url.pathname as `/${string}`) ||
|
|
87
|
+
false
|
|
88
|
+
)
|
|
89
|
+
}
|
package/lib/bin.ts
CHANGED
|
@@ -6,20 +6,21 @@ import { serveWebsite } from './serve.ts'
|
|
|
6
6
|
|
|
7
7
|
function printHelp(task?: 'build' | 'serve'): never {
|
|
8
8
|
if (!task || task === 'build') {
|
|
9
|
-
console.log('dank build [--minify] [--production]')
|
|
9
|
+
console.log('dank build [--minify] [--production] [--service-worker]')
|
|
10
10
|
}
|
|
11
11
|
if (!task || task === 'serve') {
|
|
12
12
|
console.log(
|
|
13
13
|
// 'dank serve [--minify] [--preview] [--production]',
|
|
14
|
-
'dank serve [--minify] [--production]',
|
|
14
|
+
'dank serve [--minify] [--production] [--service-worker]',
|
|
15
15
|
)
|
|
16
16
|
}
|
|
17
17
|
console.log('\nOPTIONS:')
|
|
18
18
|
if (!task || task === 'serve')
|
|
19
|
-
console.log(' --log-http
|
|
20
|
-
console.log(' --minify
|
|
19
|
+
console.log(' --log-http print access logs')
|
|
20
|
+
console.log(' --minify minify sources')
|
|
21
21
|
// if (!task || task === 'serve') console.log(' --preview pre-bundle and build ServiceWorker')
|
|
22
|
-
console.log(' --production
|
|
22
|
+
console.log(' --production build for production release')
|
|
23
|
+
console.log(' --service-worker build service worker')
|
|
23
24
|
if (task) {
|
|
24
25
|
console.log()
|
|
25
26
|
console.log('use `dank -h` for details on all commands')
|
package/lib/build.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { loadConfig, type ResolvedDankConfig } from './config.ts'
|
|
4
|
+
import type { ServiceWorkerBuild, WebsiteManifest } from './dank.ts'
|
|
4
5
|
import { type DefineDankGlobal, createGlobalDefinitions } from './define.ts'
|
|
5
6
|
import type { DankDirectories } from './dirs.ts'
|
|
6
7
|
import { esbuildWebpages, esbuildWorkers } from './esbuild.ts'
|
|
7
8
|
import { copyAssets } from './public.ts'
|
|
8
|
-
import {
|
|
9
|
+
import { WebsiteRegistry } from './registry.ts'
|
|
9
10
|
|
|
10
11
|
export async function buildWebsite(
|
|
11
12
|
c?: ResolvedDankConfig,
|
|
@@ -13,7 +14,6 @@ export async function buildWebsite(
|
|
|
13
14
|
if (!c) {
|
|
14
15
|
c = await loadConfig('build', process.cwd())
|
|
15
16
|
}
|
|
16
|
-
const buildTag = await c.buildTag()
|
|
17
17
|
console.log(
|
|
18
18
|
c.flags.minify
|
|
19
19
|
? c.flags.production
|
|
@@ -21,7 +21,7 @@ export async function buildWebsite(
|
|
|
21
21
|
: 'minified'
|
|
22
22
|
: 'unminified',
|
|
23
23
|
'build',
|
|
24
|
-
buildTag,
|
|
24
|
+
await c.buildTag(),
|
|
25
25
|
'building in ./build/dist',
|
|
26
26
|
)
|
|
27
27
|
await rm(c.dirs.buildRoot, { recursive: true, force: true })
|
|
@@ -31,7 +31,7 @@ export async function buildWebsite(
|
|
|
31
31
|
}
|
|
32
32
|
await mkdir(join(c.dirs.buildRoot, 'metafiles'), { recursive: true })
|
|
33
33
|
const registry = await buildWebpages(c, createGlobalDefinitions(c))
|
|
34
|
-
return await registry.writeManifest(
|
|
34
|
+
return await registry.writeManifest()
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// builds all webpage entrypoints in one esbuild.build context to support code splitting
|
|
@@ -62,6 +62,7 @@ async function buildWebpages(
|
|
|
62
62
|
)
|
|
63
63
|
}),
|
|
64
64
|
)
|
|
65
|
+
await buildServiceWorker(registry)
|
|
65
66
|
return registry
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -127,3 +128,55 @@ export function createWorkerRegex(
|
|
|
127
128
|
'g',
|
|
128
129
|
)
|
|
129
130
|
}
|
|
131
|
+
|
|
132
|
+
async function buildServiceWorker(registry: WebsiteRegistry) {
|
|
133
|
+
const serviceWorkerBuilder = registry.config.serviceWorkerBuilder
|
|
134
|
+
if (serviceWorkerBuilder) {
|
|
135
|
+
const website = await registry.manifest()
|
|
136
|
+
const serviceWorkerBuild = await serviceWorkerBuilder({ website })
|
|
137
|
+
validateServiceWorkerBuild(serviceWorkerBuild)
|
|
138
|
+
serviceWorkerBuild.outputs.map(async (output, i) => {
|
|
139
|
+
try {
|
|
140
|
+
return await registry.addBuildOutput(output.url, output.content)
|
|
141
|
+
} catch {
|
|
142
|
+
console.log(
|
|
143
|
+
`ServiceWorkerBuild.outputs[${i}].url \`${output.url}\` is already a url in the build output.`,
|
|
144
|
+
)
|
|
145
|
+
process.exit(1)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateServiceWorkerBuild(
|
|
152
|
+
serviceWorkerBuild: ServiceWorkerBuild,
|
|
153
|
+
): void | never {
|
|
154
|
+
if (
|
|
155
|
+
serviceWorkerBuild === null ||
|
|
156
|
+
typeof serviceWorkerBuild === 'undefined'
|
|
157
|
+
) {
|
|
158
|
+
console.log(`ServiceWorkerBuild is ${serviceWorkerBuild}.`)
|
|
159
|
+
console.log(
|
|
160
|
+
'\nMake sure the builder function \`serviceWorker\` in \`dank.config.ts\` is returning a ServiceWorkerBuild.',
|
|
161
|
+
)
|
|
162
|
+
process.exit(1)
|
|
163
|
+
}
|
|
164
|
+
const testUrlPattern = /^\/.*\.js$/
|
|
165
|
+
const valid = true
|
|
166
|
+
serviceWorkerBuild.outputs.forEach((output, i) => {
|
|
167
|
+
if (!output.content?.length) {
|
|
168
|
+
console.log(`ServiceWorkerBuild.outputs[${i}].content is empty.`)
|
|
169
|
+
}
|
|
170
|
+
if (!output.url?.length || !testUrlPattern.test(output.url)) {
|
|
171
|
+
console.log(
|
|
172
|
+
`ServiceWorkerBuild.outputs[${i}].url is not a valid \`/*.js\` path.`,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
if (!valid) {
|
|
177
|
+
console.log(
|
|
178
|
+
'\nCheck your \`serviceWorker\` config in \`dank.config.ts\`.',
|
|
179
|
+
)
|
|
180
|
+
process.exit(1)
|
|
181
|
+
}
|
|
182
|
+
}
|
package/lib/config.ts
CHANGED
|
@@ -3,8 +3,10 @@ import { createBuildTag } from './build_tag.ts'
|
|
|
3
3
|
import type {
|
|
4
4
|
DankConfig,
|
|
5
5
|
DankDetails,
|
|
6
|
+
DevPageMapping,
|
|
6
7
|
EsbuildConfig,
|
|
7
8
|
PageMapping,
|
|
9
|
+
ServiceWorkerBuilder,
|
|
8
10
|
} from './dank.ts'
|
|
9
11
|
import { LOG } from './developer.ts'
|
|
10
12
|
import { defaultProjectDirs, type DankDirectories } from './dirs.ts'
|
|
@@ -22,7 +24,7 @@ const DEFAULT_CONFIG_PATH = './dank.config.ts'
|
|
|
22
24
|
export type { DevService } from './dank.ts'
|
|
23
25
|
|
|
24
26
|
export type ResolvedDankConfig = {
|
|
25
|
-
// static
|
|
27
|
+
// static config that does not hot reload during `dank serve`
|
|
26
28
|
get dirs(): Readonly<DankDirectories>
|
|
27
29
|
get flags(): Readonly<Omit<DankFlags, 'dankPort' | 'esbuildPort'>>
|
|
28
30
|
get mode(): 'build' | 'serve'
|
|
@@ -32,11 +34,16 @@ export type ResolvedDankConfig = {
|
|
|
32
34
|
get esbuildPort(): number
|
|
33
35
|
get esbuild(): Readonly<Omit<EsbuildConfig, 'port'>> | undefined
|
|
34
36
|
get pages(): Readonly<Record<`/${string}`, PageMapping>>
|
|
35
|
-
get devPages(): Readonly<
|
|
37
|
+
get devPages(): Readonly<
|
|
38
|
+
Record<`/${string}`, Omit<DevPageMapping & PageMapping, 'pattern'>>
|
|
39
|
+
>
|
|
36
40
|
get services(): Readonly<DankConfig['services']>
|
|
41
|
+
get serviceWorkerBuilder(): DankConfig['serviceWorker']
|
|
37
42
|
|
|
38
43
|
buildTag(): Promise<string>
|
|
39
44
|
|
|
45
|
+
pageMappings(): Record<`/${string}`, PageMapping>
|
|
46
|
+
|
|
40
47
|
reload(): Promise<void>
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -63,17 +70,19 @@ export async function loadConfig(
|
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
class DankConfigInternal implements ResolvedDankConfig {
|
|
66
|
-
#buildTag:
|
|
73
|
+
#buildTag: Promise<string> | null = null
|
|
74
|
+
#buildTagBuilder: DankConfig['buildTag']
|
|
67
75
|
#dirs: Readonly<DankDirectories>
|
|
68
76
|
#flags: Readonly<DankFlags>
|
|
69
77
|
#mode: 'build' | 'serve'
|
|
70
78
|
#modulePath: string
|
|
79
|
+
#serviceWorkerBuilder?: ServiceWorkerBuilder
|
|
71
80
|
|
|
72
81
|
#dankPort: number = DEFAULT_DEV_PORT
|
|
73
82
|
#esbuildPort: number = DEFAULT_ESBUILD_PORT
|
|
74
83
|
#esbuild: Readonly<Omit<EsbuildConfig, 'port'>> | undefined
|
|
75
84
|
#pages: Readonly<Record<`/${string}`, PageMapping>> = {}
|
|
76
|
-
#devPages: Readonly<
|
|
85
|
+
#devPages: Readonly<ResolvedDankConfig['devPages']> = {}
|
|
77
86
|
#services: Readonly<DankConfig['services']>
|
|
78
87
|
|
|
79
88
|
constructor(
|
|
@@ -115,7 +124,7 @@ class DankConfigInternal implements ResolvedDankConfig {
|
|
|
115
124
|
return this.#pages
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
get devPages(): Readonly<
|
|
127
|
+
get devPages(): Readonly<ResolvedDankConfig['devPages']> {
|
|
119
128
|
return this.#devPages
|
|
120
129
|
}
|
|
121
130
|
|
|
@@ -123,12 +132,30 @@ class DankConfigInternal implements ResolvedDankConfig {
|
|
|
123
132
|
return this.#services
|
|
124
133
|
}
|
|
125
134
|
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
135
|
+
get serviceWorkerBuilder(): DankConfig['serviceWorker'] {
|
|
136
|
+
return this.#serviceWorkerBuilder
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
buildTag(): Promise<string> {
|
|
140
|
+
if (this.#buildTag === null) {
|
|
141
|
+
this.#buildTag = createBuildTag(
|
|
142
|
+
this.#dirs.projectRootAbs,
|
|
143
|
+
this.#flags,
|
|
144
|
+
this.#buildTagBuilder,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return this.#buildTag
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
pageMappings(): ResolvedDankConfig['pages'] {
|
|
151
|
+
if (this.#mode === 'serve') {
|
|
152
|
+
return {
|
|
153
|
+
...this.#pages,
|
|
154
|
+
...this.#devPages,
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
return this.#pages
|
|
158
|
+
}
|
|
132
159
|
}
|
|
133
160
|
|
|
134
161
|
async reload() {
|
|
@@ -136,13 +163,15 @@ class DankConfigInternal implements ResolvedDankConfig {
|
|
|
136
163
|
this.#modulePath,
|
|
137
164
|
resolveDankDetails(this.#mode, this.#flags),
|
|
138
165
|
)
|
|
139
|
-
this.#buildTag =
|
|
166
|
+
this.#buildTag = null
|
|
167
|
+
this.#buildTagBuilder = userConfig.buildTag
|
|
140
168
|
this.#dankPort = resolveDankPort(this.#flags, userConfig)
|
|
141
169
|
this.#esbuildPort = resolveEsbuildPort(this.#flags, userConfig)
|
|
142
170
|
this.#esbuild = Object.freeze(userConfig.esbuild)
|
|
143
171
|
this.#pages = Object.freeze(normalizePages(userConfig.pages))
|
|
144
|
-
this.#devPages = Object.freeze(userConfig.devPages)
|
|
172
|
+
this.#devPages = Object.freeze(normalizeDevPages(userConfig.devPages))
|
|
145
173
|
this.#services = Object.freeze(userConfig.services)
|
|
174
|
+
this.#serviceWorkerBuilder = userConfig.serviceWorker
|
|
146
175
|
}
|
|
147
176
|
}
|
|
148
177
|
|
|
@@ -191,6 +220,7 @@ function validateDankConfig(c: Partial<DankConfig>) {
|
|
|
191
220
|
validateDevPages(c.devPages)
|
|
192
221
|
validateDevServices(c.services)
|
|
193
222
|
validateEsbuildConfig(c.esbuild)
|
|
223
|
+
validateServiceWorker(c.serviceWorker)
|
|
194
224
|
} catch (e: any) {
|
|
195
225
|
LOG({
|
|
196
226
|
realm: 'config',
|
|
@@ -230,6 +260,19 @@ function validateBuildTag(buildTag: DankConfig['buildTag']) {
|
|
|
230
260
|
}
|
|
231
261
|
}
|
|
232
262
|
|
|
263
|
+
function validateServiceWorker(serviceWorker: DankConfig['serviceWorker']) {
|
|
264
|
+
if (serviceWorker === null) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
switch (typeof serviceWorker) {
|
|
268
|
+
case 'undefined':
|
|
269
|
+
case 'function':
|
|
270
|
+
return
|
|
271
|
+
default:
|
|
272
|
+
throw Error('DankConfig.serviceWorker must be a function')
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
233
276
|
function validateEsbuildConfig(esbuild?: EsbuildConfig) {
|
|
234
277
|
if (esbuild?.loaders !== null && typeof esbuild?.loaders !== 'undefined') {
|
|
235
278
|
if (typeof esbuild.loaders !== 'object') {
|
|
@@ -406,3 +449,27 @@ function normalizePages(
|
|
|
406
449
|
}
|
|
407
450
|
return result
|
|
408
451
|
}
|
|
452
|
+
|
|
453
|
+
function normalizeDevPages(
|
|
454
|
+
pages: DankConfig['devPages'],
|
|
455
|
+
): Record<string, Omit<DevPageMapping & PageMapping, 'pattern'>> {
|
|
456
|
+
if (pages) {
|
|
457
|
+
const result: Record<
|
|
458
|
+
string,
|
|
459
|
+
Omit<DevPageMapping & PageMapping, 'pattern'>
|
|
460
|
+
> = {}
|
|
461
|
+
for (const [url, mapping] of Object.entries(pages)) {
|
|
462
|
+
if (typeof mapping === 'string') {
|
|
463
|
+
result[url] = {
|
|
464
|
+
label: '',
|
|
465
|
+
webpage: mapping,
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
result[url] = mapping
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return result
|
|
472
|
+
} else {
|
|
473
|
+
return {}
|
|
474
|
+
}
|
|
475
|
+
}
|
package/lib/dank.ts
CHANGED
|
@@ -24,6 +24,10 @@ export type DankConfig = {
|
|
|
24
24
|
|
|
25
25
|
// dev services launched during `dank serve`
|
|
26
26
|
services?: Array<DevService>
|
|
27
|
+
|
|
28
|
+
// generate a service worker for `dank build --production`
|
|
29
|
+
// and when previewing with `dank serve --preview`
|
|
30
|
+
serviceWorker?: ServiceWorkerBuilder
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export type BuildTagParams = {
|
|
@@ -99,3 +103,33 @@ export function defineConfig(
|
|
|
99
103
|
): Partial<DankConfig> | DankConfigFunction {
|
|
100
104
|
return config
|
|
101
105
|
}
|
|
106
|
+
|
|
107
|
+
// summary of a website build, written to `build` dir
|
|
108
|
+
// and provided via ServiceWorkerParams to build a service worker from
|
|
109
|
+
export type WebsiteManifest = {
|
|
110
|
+
buildTag: string
|
|
111
|
+
files: Array<`/${string}`>
|
|
112
|
+
pageUrls: Array<`/${string}`>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ServiceWorkerParams = {
|
|
116
|
+
website: WebsiteManifest
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type ServiceWorkerBuild = {
|
|
120
|
+
// outputs will be written to the build's dist
|
|
121
|
+
// and added to the manifest written to website.json
|
|
122
|
+
outputs: Array<{
|
|
123
|
+
url: `/${string}.js`
|
|
124
|
+
content: string
|
|
125
|
+
}>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type ServiceWorkerBuilder = (
|
|
129
|
+
params: ServiceWorkerParams,
|
|
130
|
+
) => ServiceWorkerBuild | Promise<ServiceWorkerBuild>
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
createServiceWorker,
|
|
134
|
+
type ServiceWorkerCaching,
|
|
135
|
+
} from './service_worker.ts'
|
package/lib/esbuild.ts
CHANGED
|
@@ -75,6 +75,7 @@ export async function esbuildWorkers(
|
|
|
75
75
|
function commonBuildOptions(r: WebsiteRegistry): BuildOptions {
|
|
76
76
|
const p = workersPlugin(r.buildRegistry())
|
|
77
77
|
return {
|
|
78
|
+
absWorkingDir: r.config.dirs.projectRootAbs,
|
|
78
79
|
assetNames: 'assets/[name]-[hash]',
|
|
79
80
|
bundle: true,
|
|
80
81
|
format: 'esm',
|
package/lib/http.ts
CHANGED
|
@@ -10,12 +10,12 @@ import {
|
|
|
10
10
|
import { extname, join } from 'node:path'
|
|
11
11
|
import { Readable } from 'node:stream'
|
|
12
12
|
import mime from 'mime'
|
|
13
|
+
import type { WebsiteManifest } from './dank.ts'
|
|
13
14
|
import type { DankDirectories } from './dirs.ts'
|
|
14
15
|
import type { DankFlags } from './flags.ts'
|
|
15
16
|
import type {
|
|
16
17
|
UrlRewrite,
|
|
17
18
|
UrlRewriteProvider,
|
|
18
|
-
WebsiteManifest,
|
|
19
19
|
WebsiteRegistry,
|
|
20
20
|
} from './registry.ts'
|
|
21
21
|
import type { DevServices } from './services.ts'
|
|
@@ -194,7 +194,7 @@ export function createBuiltDistFilesFetcher(
|
|
|
194
194
|
res: ServerResponse,
|
|
195
195
|
notFound: () => void,
|
|
196
196
|
) => {
|
|
197
|
-
if (manifest.pageUrls.
|
|
197
|
+
if (manifest.pageUrls.includes(url.pathname as `/${string}`)) {
|
|
198
198
|
streamFile(
|
|
199
199
|
join(
|
|
200
200
|
dirs.projectRootAbs,
|
|
@@ -204,7 +204,7 @@ export function createBuiltDistFilesFetcher(
|
|
|
204
204
|
),
|
|
205
205
|
res,
|
|
206
206
|
)
|
|
207
|
-
} else if (manifest.files.
|
|
207
|
+
} else if (manifest.files.includes(url.pathname as `/${string}`)) {
|
|
208
208
|
streamFile(
|
|
209
209
|
join(dirs.projectRootAbs, dirs.buildDist, url.pathname),
|
|
210
210
|
res,
|
package/lib/public.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { DankDirectories } from './dirs.ts'
|
|
|
5
5
|
|
|
6
6
|
export async function copyAssets(
|
|
7
7
|
dirs: DankDirectories,
|
|
8
|
-
): Promise<Array
|
|
8
|
+
): Promise<Array<`/${string}`> | null> {
|
|
9
9
|
try {
|
|
10
10
|
const stats = await stat(dirs.public)
|
|
11
11
|
if (stats.isDirectory()) {
|
|
@@ -24,8 +24,8 @@ const IGNORE = platform() === 'darwin' ? ['.DS_Store'] : []
|
|
|
24
24
|
async function recursiveCopyAssets(
|
|
25
25
|
dirs: DankDirectories,
|
|
26
26
|
dir: string = '',
|
|
27
|
-
): Promise<Array
|
|
28
|
-
const copied: Array
|
|
27
|
+
): Promise<Array<`/${string}`>> {
|
|
28
|
+
const copied: Array<`/${string}`> = []
|
|
29
29
|
const to = join(dirs.buildDist, dir)
|
|
30
30
|
let madeDir = dir === ''
|
|
31
31
|
const listingDir = join(dirs.public, dir)
|
|
@@ -45,7 +45,7 @@ async function recursiveCopyAssets(
|
|
|
45
45
|
madeDir = true
|
|
46
46
|
}
|
|
47
47
|
await copyFile(join(listingDir, p), join(to, p))
|
|
48
|
-
copied.push(
|
|
48
|
+
copied.push(`/${join(dir, p).replaceAll('\\', '/')}`)
|
|
49
49
|
}
|
|
50
50
|
} catch (e) {
|
|
51
51
|
console.error('stat error', e)
|
package/lib/registry.ts
CHANGED
|
@@ -3,19 +3,12 @@ import { writeFile } from 'node:fs/promises'
|
|
|
3
3
|
import { join } from 'node:path/posix'
|
|
4
4
|
import type { BuildResult } from 'esbuild'
|
|
5
5
|
import type { ResolvedDankConfig } from './config.ts'
|
|
6
|
-
import type { PageMapping } from './dank.ts'
|
|
6
|
+
import type { PageMapping, WebsiteManifest } from './dank.ts'
|
|
7
7
|
import { LOG } from './developer.ts'
|
|
8
8
|
import { Resolver, type DankDirectories } from './dirs.ts'
|
|
9
9
|
import type { EntryPoint } from './esbuild.ts'
|
|
10
10
|
import { HtmlEntrypoint } from './html.ts'
|
|
11
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
12
|
// result of an esbuild build from the context of the config's entrypoints
|
|
20
13
|
// path of entrypoint is the reference point to lookup from a dependent page
|
|
21
14
|
export type BuildManifest = {
|
|
@@ -67,12 +60,13 @@ export type UrlRewriteProvider = {
|
|
|
67
60
|
// manages website resources during `dank build` and `dank serve`
|
|
68
61
|
export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
|
|
69
62
|
// paths of bundled esbuild outputs, as built by esbuild
|
|
70
|
-
#bundles: Set
|
|
63
|
+
#bundles: Set<`/${string}`> = new Set()
|
|
71
64
|
#c: ResolvedDankConfig
|
|
72
65
|
// public dir assets
|
|
73
|
-
#copiedAssets: Set
|
|
66
|
+
#copiedAssets: Set<`/${string}`> | null = null
|
|
74
67
|
// map of entrypoints to their output path
|
|
75
68
|
#entrypointHrefs: Record<string, string | null> = {}
|
|
69
|
+
#otherOutputs: Set<`/${string}`> | null = null
|
|
76
70
|
#pages: Record<`/${string}`, WebpageRegistration> = {}
|
|
77
71
|
readonly #resolver: Resolver
|
|
78
72
|
#workers: Array<WorkerManifest> | null = null
|
|
@@ -87,7 +81,7 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
|
|
|
87
81
|
return this.#c
|
|
88
82
|
}
|
|
89
83
|
|
|
90
|
-
set copiedAssets(copiedAssets: Array
|
|
84
|
+
set copiedAssets(copiedAssets: Array<`/${string}`> | null) {
|
|
91
85
|
this.#copiedAssets =
|
|
92
86
|
copiedAssets === null ? null : new Set(copiedAssets)
|
|
93
87
|
}
|
|
@@ -96,6 +90,14 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
|
|
|
96
90
|
return Object.values(this.#pages).map(p => p.html)
|
|
97
91
|
}
|
|
98
92
|
|
|
93
|
+
async manifest(): Promise<WebsiteManifest> {
|
|
94
|
+
return {
|
|
95
|
+
buildTag: await this.#c.buildTag(),
|
|
96
|
+
files: this.files(),
|
|
97
|
+
pageUrls: Object.keys(this.#pages) as Array<`/${string}`>,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
99
101
|
get pageUrls(): Array<string> {
|
|
100
102
|
return Object.keys(this.#pages)
|
|
101
103
|
}
|
|
@@ -159,6 +161,27 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
|
|
|
159
161
|
return this.#workers
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
// add a build output that does is manually injected into build output,
|
|
165
|
+
// not from HTML processing, public directory, or esbuild entrypoints
|
|
166
|
+
async addBuildOutput(url: `/${string}`, content: string) {
|
|
167
|
+
if (
|
|
168
|
+
this.#pages[url] ||
|
|
169
|
+
this.#bundles.has(url) ||
|
|
170
|
+
this.#otherOutputs?.has(url) ||
|
|
171
|
+
this.#copiedAssets?.has(url)
|
|
172
|
+
) {
|
|
173
|
+
throw Error('build already has a ' + url)
|
|
174
|
+
}
|
|
175
|
+
if (this.#otherOutputs === null) this.#otherOutputs = new Set()
|
|
176
|
+
this.#otherOutputs.add(url)
|
|
177
|
+
const outputPath = join(
|
|
178
|
+
this.#c.dirs.projectRootAbs,
|
|
179
|
+
this.#c.dirs.buildDist,
|
|
180
|
+
url,
|
|
181
|
+
)
|
|
182
|
+
await writeFile(outputPath, content)
|
|
183
|
+
}
|
|
184
|
+
|
|
162
185
|
buildRegistry(): BuildRegistry {
|
|
163
186
|
return new BuildRegistry(
|
|
164
187
|
this.#c.dirs,
|
|
@@ -171,13 +194,18 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
|
|
|
171
194
|
this.#configDiff()
|
|
172
195
|
}
|
|
173
196
|
|
|
174
|
-
files():
|
|
175
|
-
const files = new Set
|
|
197
|
+
files(): Array<`/${string}`> {
|
|
198
|
+
const files = new Set<`/${string}`>()
|
|
176
199
|
for (const pageUrl of Object.keys(this.#pages))
|
|
177
|
-
files.add(
|
|
200
|
+
files.add(
|
|
201
|
+
pageUrl === '/'
|
|
202
|
+
? '/index.html'
|
|
203
|
+
: (`${pageUrl}/index.html` as `/${string}`),
|
|
204
|
+
)
|
|
178
205
|
for (const f of this.#bundles) files.add(f)
|
|
179
206
|
if (this.#copiedAssets) for (const f of this.#copiedAssets) files.add(f)
|
|
180
|
-
|
|
207
|
+
if (this.#otherOutputs) for (const f of this.#otherOutputs) files.add(f)
|
|
208
|
+
return Array.from(files)
|
|
181
209
|
}
|
|
182
210
|
|
|
183
211
|
mappedHref(lookup: string): string {
|
|
@@ -189,33 +217,24 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
|
|
|
189
217
|
}
|
|
190
218
|
}
|
|
191
219
|
|
|
192
|
-
async writeManifest(
|
|
193
|
-
const manifest = this.#manifest(
|
|
220
|
+
async writeManifest(): Promise<WebsiteManifest> {
|
|
221
|
+
const manifest = await this.#manifest()
|
|
194
222
|
await writeFile(
|
|
195
223
|
join(
|
|
196
224
|
this.#c.dirs.projectRootAbs,
|
|
197
225
|
this.#c.dirs.buildRoot,
|
|
198
226
|
'website.json',
|
|
199
227
|
),
|
|
200
|
-
JSON.stringify(
|
|
201
|
-
{
|
|
202
|
-
buildTag,
|
|
203
|
-
files: Array.from(manifest.files),
|
|
204
|
-
pageUrls: Array.from(manifest.pageUrls),
|
|
205
|
-
},
|
|
206
|
-
null,
|
|
207
|
-
4,
|
|
208
|
-
),
|
|
228
|
+
JSON.stringify(manifest, null, 4),
|
|
209
229
|
)
|
|
210
230
|
return manifest
|
|
211
231
|
}
|
|
212
232
|
|
|
213
233
|
#configDiff() {
|
|
214
|
-
const updatePages: ResolvedDankConfig['pages'] = this.#c.devPages
|
|
215
|
-
? { ...this.#c.pages, ...this.#c.devPages }
|
|
216
|
-
: { ...this.#c.pages }
|
|
217
234
|
const prevPages = new Set(Object.keys(this.#pages))
|
|
218
|
-
for (const [urlPath, mapping] of Object.entries(
|
|
235
|
+
for (const [urlPath, mapping] of Object.entries(
|
|
236
|
+
this.#c.pageMappings(),
|
|
237
|
+
)) {
|
|
219
238
|
const existingPage = prevPages.delete(urlPath as `/${string}`)
|
|
220
239
|
if (existingPage) {
|
|
221
240
|
this.#configPageUpdate(urlPath as `/${string}`, mapping)
|
|
@@ -303,18 +322,18 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
|
|
|
303
322
|
delete this.#pages[urlPath]
|
|
304
323
|
}
|
|
305
324
|
|
|
306
|
-
#manifest(
|
|
325
|
+
async #manifest(): Promise<WebsiteManifest> {
|
|
307
326
|
return {
|
|
308
|
-
buildTag,
|
|
327
|
+
buildTag: await this.#c.buildTag(),
|
|
309
328
|
files: this.files(),
|
|
310
|
-
pageUrls:
|
|
329
|
+
pageUrls: Object.keys(this.#pages) as Array<`/${string}`>,
|
|
311
330
|
}
|
|
312
331
|
}
|
|
313
332
|
|
|
314
333
|
#onBuildManifest: OnBuildComplete = (build: BuildManifest) => {
|
|
315
334
|
// collect built bundle entrypoint hrefs
|
|
316
335
|
for (const [outPath, entrypoint] of Object.entries(build.bundles)) {
|
|
317
|
-
this.#bundles.add(outPath)
|
|
336
|
+
this.#bundles.add(ensurePath(outPath))
|
|
318
337
|
if (entrypoint) {
|
|
319
338
|
this.#entrypointHrefs[entrypoint] = outPath
|
|
320
339
|
}
|
|
@@ -413,7 +432,7 @@ export class BuildRegistry {
|
|
|
413
432
|
for (const [outPath, output] of Object.entries(
|
|
414
433
|
result.metafile.outputs,
|
|
415
434
|
)) {
|
|
416
|
-
bundles[outPath.replace(/^build[/\\]dist/, '')] =
|
|
435
|
+
bundles[outPath.replace(/^build[/\\](dist|watch)/, '')] =
|
|
417
436
|
output.entryPoint || null
|
|
418
437
|
}
|
|
419
438
|
let workers: BuildManifest['workers'] = null
|
|
@@ -438,3 +457,11 @@ export class BuildRegistry {
|
|
|
438
457
|
})
|
|
439
458
|
}
|
|
440
459
|
}
|
|
460
|
+
|
|
461
|
+
function ensurePath(path: string): `/${string}` {
|
|
462
|
+
if (path.startsWith('/')) {
|
|
463
|
+
return path as `/${string}`
|
|
464
|
+
} else {
|
|
465
|
+
throw Error(`expect build dist path ${path} to start with /`)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import esbuild from 'esbuild'
|
|
3
|
+
import type { ServiceWorkerBuild } from './dank.ts'
|
|
4
|
+
|
|
5
|
+
export type ServiceWorkerCaching = {
|
|
6
|
+
cacheKey: string
|
|
7
|
+
bypassCache?: {
|
|
8
|
+
hosts?: Array<string>
|
|
9
|
+
paths?: Array<`/${string}`>
|
|
10
|
+
}
|
|
11
|
+
files: Array<`/${string}`>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function createServiceWorker(
|
|
15
|
+
caching: ServiceWorkerCaching,
|
|
16
|
+
): Promise<ServiceWorkerBuild> {
|
|
17
|
+
return {
|
|
18
|
+
outputs: [
|
|
19
|
+
{
|
|
20
|
+
content: await buildServiceWorkerBackend(caching),
|
|
21
|
+
url: '/sw.js',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function buildServiceWorkerBackend(
|
|
28
|
+
caching: ServiceWorkerCaching,
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
const result = await esbuild.build({
|
|
31
|
+
logLevel: 'silent',
|
|
32
|
+
absWorkingDir: join(import.meta.dirname, '../client'),
|
|
33
|
+
entryPoints: ['ServiceWorker.ts'],
|
|
34
|
+
treeShaking: true,
|
|
35
|
+
target: 'ES2022',
|
|
36
|
+
bundle: true,
|
|
37
|
+
minify: true,
|
|
38
|
+
format: 'iife',
|
|
39
|
+
platform: 'browser',
|
|
40
|
+
write: false,
|
|
41
|
+
metafile: true,
|
|
42
|
+
plugins: [
|
|
43
|
+
{
|
|
44
|
+
name: 'DANK:sw',
|
|
45
|
+
setup(build: esbuild.PluginBuild) {
|
|
46
|
+
build.onResolve({ filter: /DANK:sw/ }, () => {
|
|
47
|
+
return {
|
|
48
|
+
path: join(import.meta.dirname, 'DANK.sw.json'),
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
build.onLoad(
|
|
52
|
+
{ filter: /DANK\.sw\.json$/, namespace: 'file' },
|
|
53
|
+
async () => ({
|
|
54
|
+
contents: JSON.stringify(caching),
|
|
55
|
+
loader: 'json',
|
|
56
|
+
}),
|
|
57
|
+
)
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
return new TextDecoder().decode(result.outputFiles[0].contents)
|
|
63
|
+
}
|
package/lib_js/bin.js
CHANGED
|
@@ -4,19 +4,20 @@ import { DankError } from "./errors.js";
|
|
|
4
4
|
import { serveWebsite } from "./serve.js";
|
|
5
5
|
function printHelp(task2) {
|
|
6
6
|
if (!task2 || task2 === "build") {
|
|
7
|
-
console.log("dank build [--minify] [--production]");
|
|
7
|
+
console.log("dank build [--minify] [--production] [--service-worker]");
|
|
8
8
|
}
|
|
9
9
|
if (!task2 || task2 === "serve") {
|
|
10
10
|
console.log(
|
|
11
11
|
// 'dank serve [--minify] [--preview] [--production]',
|
|
12
|
-
"dank serve [--minify] [--production]"
|
|
12
|
+
"dank serve [--minify] [--production] [--service-worker]"
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
15
|
console.log("\nOPTIONS:");
|
|
16
16
|
if (!task2 || task2 === "serve")
|
|
17
|
-
console.log(" --log-http
|
|
18
|
-
console.log(" --minify
|
|
19
|
-
console.log(" --production
|
|
17
|
+
console.log(" --log-http print access logs");
|
|
18
|
+
console.log(" --minify minify sources");
|
|
19
|
+
console.log(" --production build for production release");
|
|
20
|
+
console.log(" --service-worker build service worker");
|
|
20
21
|
if (task2) {
|
|
21
22
|
console.log();
|
|
22
23
|
console.log("use `dank -h` for details on all commands");
|
package/lib_js/build.js
CHANGED
|
@@ -9,8 +9,7 @@ async function buildWebsite(c) {
|
|
|
9
9
|
if (!c) {
|
|
10
10
|
c = await loadConfig("build", process.cwd());
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
console.log(c.flags.minify ? c.flags.production ? "minified production" : "minified" : "unminified", "build", buildTag, "building in ./build/dist");
|
|
12
|
+
console.log(c.flags.minify ? c.flags.production ? "minified production" : "minified" : "unminified", "build", await c.buildTag(), "building in ./build/dist");
|
|
14
13
|
await rm(c.dirs.buildRoot, { recursive: true, force: true });
|
|
15
14
|
await mkdir(c.dirs.buildDist, { recursive: true });
|
|
16
15
|
for (const subdir of Object.keys(c.pages).filter((url) => url !== "/")) {
|
|
@@ -18,7 +17,7 @@ async function buildWebsite(c) {
|
|
|
18
17
|
}
|
|
19
18
|
await mkdir(join(c.dirs.buildRoot, "metafiles"), { recursive: true });
|
|
20
19
|
const registry = await buildWebpages(c, createGlobalDefinitions(c));
|
|
21
|
-
return await registry.writeManifest(
|
|
20
|
+
return await registry.writeManifest();
|
|
22
21
|
}
|
|
23
22
|
async function buildWebpages(c, define) {
|
|
24
23
|
const registry = new WebsiteRegistry(c);
|
|
@@ -34,6 +33,7 @@ async function buildWebpages(c, define) {
|
|
|
34
33
|
await Promise.all(registry.htmlEntrypoints.map(async (html) => {
|
|
35
34
|
await writeFile(join(c.dirs.buildDist, html.url, "index.html"), html.output(registry));
|
|
36
35
|
}));
|
|
36
|
+
await buildServiceWorker(registry);
|
|
37
37
|
return registry;
|
|
38
38
|
}
|
|
39
39
|
async function rewriteWorkerUrls(dirs, registry) {
|
|
@@ -64,6 +64,43 @@ async function rewriteWorkerUrls(dirs, registry) {
|
|
|
64
64
|
function createWorkerRegex(workerCtor, workerUrl) {
|
|
65
65
|
return new RegExp(`new(?:\\s|\\r?\\n)+${workerCtor}(?:\\s|\\r?\\n)*\\((?:\\s|\\r?\\n)*['"]${workerUrl}['"](?:\\s|\\r?\\n)*\\)`, "g");
|
|
66
66
|
}
|
|
67
|
+
async function buildServiceWorker(registry) {
|
|
68
|
+
const serviceWorkerBuilder = registry.config.serviceWorkerBuilder;
|
|
69
|
+
if (serviceWorkerBuilder) {
|
|
70
|
+
const website = await registry.manifest();
|
|
71
|
+
const serviceWorkerBuild = await serviceWorkerBuilder({ website });
|
|
72
|
+
validateServiceWorkerBuild(serviceWorkerBuild);
|
|
73
|
+
serviceWorkerBuild.outputs.map(async (output, i) => {
|
|
74
|
+
try {
|
|
75
|
+
return await registry.addBuildOutput(output.url, output.content);
|
|
76
|
+
} catch {
|
|
77
|
+
console.log(`ServiceWorkerBuild.outputs[${i}].url \`${output.url}\` is already a url in the build output.`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function validateServiceWorkerBuild(serviceWorkerBuild) {
|
|
84
|
+
if (serviceWorkerBuild === null || typeof serviceWorkerBuild === "undefined") {
|
|
85
|
+
console.log(`ServiceWorkerBuild is ${serviceWorkerBuild}.`);
|
|
86
|
+
console.log("\nMake sure the builder function `serviceWorker` in `dank.config.ts` is returning a ServiceWorkerBuild.");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const testUrlPattern = /^\/.*\.js$/;
|
|
90
|
+
const valid = true;
|
|
91
|
+
serviceWorkerBuild.outputs.forEach((output, i) => {
|
|
92
|
+
if (!output.content?.length) {
|
|
93
|
+
console.log(`ServiceWorkerBuild.outputs[${i}].content is empty.`);
|
|
94
|
+
}
|
|
95
|
+
if (!output.url?.length || !testUrlPattern.test(output.url)) {
|
|
96
|
+
console.log(`ServiceWorkerBuild.outputs[${i}].url is not a valid \`/*.js\` path.`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (!valid) {
|
|
100
|
+
console.log("\nCheck your `serviceWorker` config in `dank.config.ts`.");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
67
104
|
export {
|
|
68
105
|
buildWebsite,
|
|
69
106
|
createWorkerRegex,
|
package/lib_js/config.js
CHANGED
|
@@ -25,16 +25,18 @@ async function loadConfig(mode, projectRootAbs) {
|
|
|
25
25
|
return c;
|
|
26
26
|
}
|
|
27
27
|
class DankConfigInternal {
|
|
28
|
-
#buildTag;
|
|
28
|
+
#buildTag = null;
|
|
29
|
+
#buildTagBuilder;
|
|
29
30
|
#dirs;
|
|
30
31
|
#flags;
|
|
31
32
|
#mode;
|
|
32
33
|
#modulePath;
|
|
34
|
+
#serviceWorkerBuilder;
|
|
33
35
|
#dankPort = DEFAULT_DEV_PORT;
|
|
34
36
|
#esbuildPort = DEFAULT_ESBUILD_PORT;
|
|
35
37
|
#esbuild;
|
|
36
38
|
#pages = {};
|
|
37
|
-
#devPages;
|
|
39
|
+
#devPages = {};
|
|
38
40
|
#services;
|
|
39
41
|
constructor(mode, modulePath, dirs) {
|
|
40
42
|
this.#dirs = dirs;
|
|
@@ -69,18 +71,36 @@ class DankConfigInternal {
|
|
|
69
71
|
get services() {
|
|
70
72
|
return this.#services;
|
|
71
73
|
}
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
+
get serviceWorkerBuilder() {
|
|
75
|
+
return this.#serviceWorkerBuilder;
|
|
76
|
+
}
|
|
77
|
+
buildTag() {
|
|
78
|
+
if (this.#buildTag === null) {
|
|
79
|
+
this.#buildTag = createBuildTag(this.#dirs.projectRootAbs, this.#flags, this.#buildTagBuilder);
|
|
80
|
+
}
|
|
81
|
+
return this.#buildTag;
|
|
82
|
+
}
|
|
83
|
+
pageMappings() {
|
|
84
|
+
if (this.#mode === "serve") {
|
|
85
|
+
return {
|
|
86
|
+
...this.#pages,
|
|
87
|
+
...this.#devPages
|
|
88
|
+
};
|
|
89
|
+
} else {
|
|
90
|
+
return this.#pages;
|
|
91
|
+
}
|
|
74
92
|
}
|
|
75
93
|
async reload() {
|
|
76
94
|
const userConfig = await resolveConfig(this.#modulePath, resolveDankDetails(this.#mode, this.#flags));
|
|
77
|
-
this.#buildTag =
|
|
95
|
+
this.#buildTag = null;
|
|
96
|
+
this.#buildTagBuilder = userConfig.buildTag;
|
|
78
97
|
this.#dankPort = resolveDankPort(this.#flags, userConfig);
|
|
79
98
|
this.#esbuildPort = resolveEsbuildPort(this.#flags, userConfig);
|
|
80
99
|
this.#esbuild = Object.freeze(userConfig.esbuild);
|
|
81
100
|
this.#pages = Object.freeze(normalizePages(userConfig.pages));
|
|
82
|
-
this.#devPages = Object.freeze(userConfig.devPages);
|
|
101
|
+
this.#devPages = Object.freeze(normalizeDevPages(userConfig.devPages));
|
|
83
102
|
this.#services = Object.freeze(userConfig.services);
|
|
103
|
+
this.#serviceWorkerBuilder = userConfig.serviceWorker;
|
|
84
104
|
}
|
|
85
105
|
}
|
|
86
106
|
function resolveDankPort(flags, userConfig) {
|
|
@@ -110,6 +130,7 @@ function validateDankConfig(c) {
|
|
|
110
130
|
validateDevPages(c.devPages);
|
|
111
131
|
validateDevServices(c.services);
|
|
112
132
|
validateEsbuildConfig(c.esbuild);
|
|
133
|
+
validateServiceWorker(c.serviceWorker);
|
|
113
134
|
} catch (e) {
|
|
114
135
|
throw e;
|
|
115
136
|
}
|
|
@@ -139,6 +160,18 @@ function validateBuildTag(buildTag) {
|
|
|
139
160
|
throw Error("DankConfig.buildTag must be a string or function");
|
|
140
161
|
}
|
|
141
162
|
}
|
|
163
|
+
function validateServiceWorker(serviceWorker) {
|
|
164
|
+
if (serviceWorker === null) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
switch (typeof serviceWorker) {
|
|
168
|
+
case "undefined":
|
|
169
|
+
case "function":
|
|
170
|
+
return;
|
|
171
|
+
default:
|
|
172
|
+
throw Error("DankConfig.serviceWorker must be a function");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
142
175
|
function validateEsbuildConfig(esbuild) {
|
|
143
176
|
if (esbuild?.loaders !== null && typeof esbuild?.loaders !== "undefined") {
|
|
144
177
|
if (typeof esbuild.loaders !== "object") {
|
|
@@ -257,6 +290,24 @@ function normalizePages(pages) {
|
|
|
257
290
|
}
|
|
258
291
|
return result;
|
|
259
292
|
}
|
|
293
|
+
function normalizeDevPages(pages) {
|
|
294
|
+
if (pages) {
|
|
295
|
+
const result = {};
|
|
296
|
+
for (const [url, mapping] of Object.entries(pages)) {
|
|
297
|
+
if (typeof mapping === "string") {
|
|
298
|
+
result[url] = {
|
|
299
|
+
label: "",
|
|
300
|
+
webpage: mapping
|
|
301
|
+
};
|
|
302
|
+
} else {
|
|
303
|
+
result[url] = mapping;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
} else {
|
|
308
|
+
return {};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
260
311
|
export {
|
|
261
312
|
loadConfig
|
|
262
313
|
};
|
package/lib_js/dank.js
CHANGED
package/lib_js/esbuild.js
CHANGED
|
@@ -44,6 +44,7 @@ async function esbuildWorkers(r, define, entryPoints) {
|
|
|
44
44
|
function commonBuildOptions(r) {
|
|
45
45
|
const p = workersPlugin(r.buildRegistry());
|
|
46
46
|
return {
|
|
47
|
+
absWorkingDir: r.config.dirs.projectRootAbs,
|
|
47
48
|
assetNames: "assets/[name]-[hash]",
|
|
48
49
|
bundle: true,
|
|
49
50
|
format: "esm",
|
package/lib_js/http.js
CHANGED
|
@@ -92,9 +92,9 @@ function createLogWrapper(handler) {
|
|
|
92
92
|
}
|
|
93
93
|
function createBuiltDistFilesFetcher(dirs, manifest) {
|
|
94
94
|
return (url, _headers, res, notFound) => {
|
|
95
|
-
if (manifest.pageUrls.
|
|
95
|
+
if (manifest.pageUrls.includes(url.pathname)) {
|
|
96
96
|
streamFile(join(dirs.projectRootAbs, dirs.buildDist, url.pathname, "index.html"), res);
|
|
97
|
-
} else if (manifest.files.
|
|
97
|
+
} else if (manifest.files.includes(url.pathname)) {
|
|
98
98
|
streamFile(join(dirs.projectRootAbs, dirs.buildDist, url.pathname), res);
|
|
99
99
|
} else {
|
|
100
100
|
notFound();
|
package/lib_js/public.js
CHANGED
|
@@ -36,7 +36,7 @@ async function recursiveCopyAssets(dirs, dir = "") {
|
|
|
36
36
|
madeDir = true;
|
|
37
37
|
}
|
|
38
38
|
await copyFile(join(listingDir, p), join(to, p));
|
|
39
|
-
copied.push(
|
|
39
|
+
copied.push(`/${join(dir, p).replaceAll("\\", "/")}`);
|
|
40
40
|
}
|
|
41
41
|
} catch (e) {
|
|
42
42
|
console.error("stat error", e);
|
package/lib_js/registry.js
CHANGED
|
@@ -11,6 +11,7 @@ class WebsiteRegistry extends EventEmitter {
|
|
|
11
11
|
#copiedAssets = null;
|
|
12
12
|
// map of entrypoints to their output path
|
|
13
13
|
#entrypointHrefs = {};
|
|
14
|
+
#otherOutputs = null;
|
|
14
15
|
#pages = {};
|
|
15
16
|
#resolver;
|
|
16
17
|
#workers = null;
|
|
@@ -28,6 +29,13 @@ class WebsiteRegistry extends EventEmitter {
|
|
|
28
29
|
get htmlEntrypoints() {
|
|
29
30
|
return Object.values(this.#pages).map((p) => p.html);
|
|
30
31
|
}
|
|
32
|
+
async manifest() {
|
|
33
|
+
return {
|
|
34
|
+
buildTag: await this.#c.buildTag(),
|
|
35
|
+
files: this.files(),
|
|
36
|
+
pageUrls: Object.keys(this.#pages)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
31
39
|
get pageUrls() {
|
|
32
40
|
return Object.keys(this.#pages);
|
|
33
41
|
}
|
|
@@ -71,6 +79,18 @@ class WebsiteRegistry extends EventEmitter {
|
|
|
71
79
|
get workers() {
|
|
72
80
|
return this.#workers;
|
|
73
81
|
}
|
|
82
|
+
// add a build output that does is manually injected into build output,
|
|
83
|
+
// not from HTML processing, public directory, or esbuild entrypoints
|
|
84
|
+
async addBuildOutput(url, content) {
|
|
85
|
+
if (this.#pages[url] || this.#bundles.has(url) || this.#otherOutputs?.has(url) || this.#copiedAssets?.has(url)) {
|
|
86
|
+
throw Error("build already has a " + url);
|
|
87
|
+
}
|
|
88
|
+
if (this.#otherOutputs === null)
|
|
89
|
+
this.#otherOutputs = /* @__PURE__ */ new Set();
|
|
90
|
+
this.#otherOutputs.add(url);
|
|
91
|
+
const outputPath = join(this.#c.dirs.projectRootAbs, this.#c.dirs.buildDist, url);
|
|
92
|
+
await writeFile(outputPath, content);
|
|
93
|
+
}
|
|
74
94
|
buildRegistry() {
|
|
75
95
|
return new BuildRegistry(this.#c.dirs, this.#resolver, this.#onBuildManifest);
|
|
76
96
|
}
|
|
@@ -86,7 +106,10 @@ class WebsiteRegistry extends EventEmitter {
|
|
|
86
106
|
if (this.#copiedAssets)
|
|
87
107
|
for (const f of this.#copiedAssets)
|
|
88
108
|
files.add(f);
|
|
89
|
-
|
|
109
|
+
if (this.#otherOutputs)
|
|
110
|
+
for (const f of this.#otherOutputs)
|
|
111
|
+
files.add(f);
|
|
112
|
+
return Array.from(files);
|
|
90
113
|
}
|
|
91
114
|
mappedHref(lookup) {
|
|
92
115
|
const found = this.#entrypointHrefs[lookup];
|
|
@@ -96,19 +119,14 @@ class WebsiteRegistry extends EventEmitter {
|
|
|
96
119
|
throw Error(`mapped href for ${lookup} not found`);
|
|
97
120
|
}
|
|
98
121
|
}
|
|
99
|
-
async writeManifest(
|
|
100
|
-
const manifest = this.#manifest(
|
|
101
|
-
await writeFile(join(this.#c.dirs.projectRootAbs, this.#c.dirs.buildRoot, "website.json"), JSON.stringify(
|
|
102
|
-
buildTag,
|
|
103
|
-
files: Array.from(manifest.files),
|
|
104
|
-
pageUrls: Array.from(manifest.pageUrls)
|
|
105
|
-
}, null, 4));
|
|
122
|
+
async writeManifest() {
|
|
123
|
+
const manifest = await this.#manifest();
|
|
124
|
+
await writeFile(join(this.#c.dirs.projectRootAbs, this.#c.dirs.buildRoot, "website.json"), JSON.stringify(manifest, null, 4));
|
|
106
125
|
return manifest;
|
|
107
126
|
}
|
|
108
127
|
#configDiff() {
|
|
109
|
-
const updatePages = this.#c.devPages ? { ...this.#c.pages, ...this.#c.devPages } : { ...this.#c.pages };
|
|
110
128
|
const prevPages = new Set(Object.keys(this.#pages));
|
|
111
|
-
for (const [urlPath, mapping] of Object.entries(
|
|
129
|
+
for (const [urlPath, mapping] of Object.entries(this.#c.pageMappings())) {
|
|
112
130
|
const existingPage = prevPages.delete(urlPath);
|
|
113
131
|
if (existingPage) {
|
|
114
132
|
this.#configPageUpdate(urlPath, mapping);
|
|
@@ -155,16 +173,16 @@ class WebsiteRegistry extends EventEmitter {
|
|
|
155
173
|
registration.html.removeAllListeners();
|
|
156
174
|
delete this.#pages[urlPath];
|
|
157
175
|
}
|
|
158
|
-
#manifest(
|
|
176
|
+
async #manifest() {
|
|
159
177
|
return {
|
|
160
|
-
buildTag,
|
|
178
|
+
buildTag: await this.#c.buildTag(),
|
|
161
179
|
files: this.files(),
|
|
162
|
-
pageUrls:
|
|
180
|
+
pageUrls: Object.keys(this.#pages)
|
|
163
181
|
};
|
|
164
182
|
}
|
|
165
183
|
#onBuildManifest = (build) => {
|
|
166
184
|
for (const [outPath, entrypoint] of Object.entries(build.bundles)) {
|
|
167
|
-
this.#bundles.add(outPath);
|
|
185
|
+
this.#bundles.add(ensurePath(outPath));
|
|
168
186
|
if (entrypoint) {
|
|
169
187
|
this.#entrypointHrefs[entrypoint] = outPath;
|
|
170
188
|
}
|
|
@@ -229,7 +247,7 @@ class BuildRegistry {
|
|
|
229
247
|
completeBuild(result) {
|
|
230
248
|
const bundles = {};
|
|
231
249
|
for (const [outPath, output] of Object.entries(result.metafile.outputs)) {
|
|
232
|
-
bundles[outPath.replace(/^build[/\\]dist/, "")] = output.entryPoint || null;
|
|
250
|
+
bundles[outPath.replace(/^build[/\\](dist|watch)/, "")] = output.entryPoint || null;
|
|
233
251
|
}
|
|
234
252
|
let workers = null;
|
|
235
253
|
if (this.#workers) {
|
|
@@ -254,6 +272,13 @@ class BuildRegistry {
|
|
|
254
272
|
});
|
|
255
273
|
}
|
|
256
274
|
}
|
|
275
|
+
function ensurePath(path) {
|
|
276
|
+
if (path.startsWith("/")) {
|
|
277
|
+
return path;
|
|
278
|
+
} else {
|
|
279
|
+
throw Error(`expect build dist path ${path} to start with /`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
257
282
|
export {
|
|
258
283
|
BuildRegistry,
|
|
259
284
|
WebsiteRegistry
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import esbuild from "esbuild";
|
|
3
|
+
async function createServiceWorker(caching) {
|
|
4
|
+
return {
|
|
5
|
+
outputs: [
|
|
6
|
+
{
|
|
7
|
+
content: await buildServiceWorkerBackend(caching),
|
|
8
|
+
url: "/sw.js"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async function buildServiceWorkerBackend(caching) {
|
|
14
|
+
const result = await esbuild.build({
|
|
15
|
+
logLevel: "silent",
|
|
16
|
+
absWorkingDir: join(import.meta.dirname, "../client"),
|
|
17
|
+
entryPoints: ["ServiceWorker.ts"],
|
|
18
|
+
treeShaking: true,
|
|
19
|
+
target: "ES2022",
|
|
20
|
+
bundle: true,
|
|
21
|
+
minify: true,
|
|
22
|
+
format: "iife",
|
|
23
|
+
platform: "browser",
|
|
24
|
+
write: false,
|
|
25
|
+
metafile: true,
|
|
26
|
+
plugins: [
|
|
27
|
+
{
|
|
28
|
+
name: "DANK:sw",
|
|
29
|
+
setup(build) {
|
|
30
|
+
build.onResolve({ filter: /DANK:sw/ }, () => {
|
|
31
|
+
return {
|
|
32
|
+
path: join(import.meta.dirname, "DANK.sw.json")
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
build.onLoad({ filter: /DANK\.sw\.json$/, namespace: "file" }, async () => ({
|
|
36
|
+
contents: JSON.stringify(caching),
|
|
37
|
+
loader: "json"
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
});
|
|
43
|
+
return new TextDecoder().decode(result.outputFiles[0].contents);
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
createServiceWorker
|
|
47
|
+
};
|
package/lib_types/dank.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type DankConfig = {
|
|
|
7
7
|
port?: number;
|
|
8
8
|
previewPort?: number;
|
|
9
9
|
services?: Array<DevService>;
|
|
10
|
+
serviceWorker?: ServiceWorkerBuilder;
|
|
10
11
|
};
|
|
11
12
|
export type BuildTagParams = {
|
|
12
13
|
production: boolean;
|
|
@@ -42,3 +43,19 @@ export type DankDetails = {
|
|
|
42
43
|
export type DankConfigFunction = (dank: DankDetails) => Partial<DankConfig> | Promise<Partial<DankConfig>>;
|
|
43
44
|
export declare function defineConfig(config: Partial<DankConfig>): Partial<DankConfig>;
|
|
44
45
|
export declare function defineConfig(config: DankConfigFunction): DankConfigFunction;
|
|
46
|
+
export type WebsiteManifest = {
|
|
47
|
+
buildTag: string;
|
|
48
|
+
files: Array<`/${string}`>;
|
|
49
|
+
pageUrls: Array<`/${string}`>;
|
|
50
|
+
};
|
|
51
|
+
export type ServiceWorkerParams = {
|
|
52
|
+
website: WebsiteManifest;
|
|
53
|
+
};
|
|
54
|
+
export type ServiceWorkerBuild = {
|
|
55
|
+
outputs: Array<{
|
|
56
|
+
url: `/${string}.js`;
|
|
57
|
+
content: string;
|
|
58
|
+
}>;
|
|
59
|
+
};
|
|
60
|
+
export type ServiceWorkerBuilder = (params: ServiceWorkerParams) => ServiceWorkerBuild | Promise<ServiceWorkerBuild>;
|
|
61
|
+
export { createServiceWorker, type ServiceWorkerCaching, } from './service_worker.ts';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ServiceWorkerBuild } from './dank.ts';
|
|
2
|
+
export type ServiceWorkerCaching = {
|
|
3
|
+
cacheKey: string;
|
|
4
|
+
bypassCache?: {
|
|
5
|
+
hosts?: Array<string>;
|
|
6
|
+
paths?: Array<`/${string}`>;
|
|
7
|
+
};
|
|
8
|
+
files: Array<`/${string}`>;
|
|
9
|
+
};
|
|
10
|
+
export declare function createServiceWorker(caching: ServiceWorkerCaching): Promise<ServiceWorkerBuild>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eighty4/dank",
|
|
3
|
-
"version": "0.0.5-
|
|
3
|
+
"version": "0.0.5-5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Adam McKee Bennett <adam.be.g84d@gmail.com>",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"client/client.js",
|
|
41
|
+
"client/ServiceWorker.ts",
|
|
41
42
|
"lib/*.ts",
|
|
42
43
|
"lib_js/*.js",
|
|
43
44
|
"lib_types/*.d.ts"
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"scripts": {
|
|
46
47
|
"build": "pnpm build:client && pnpm build:lib",
|
|
47
48
|
"build:client": "node scripts/build_client.ts",
|
|
48
|
-
"build:lib": "tsc && tsc -p tsconfig.exports.json",
|
|
49
|
+
"build:lib": "tsc && tsc -p tsconfig.exports.json && tsc -p tsconfig.sw.json",
|
|
49
50
|
"build:release": "pnpm build && node scripts/prepare_release.ts",
|
|
50
51
|
"fmt": "prettier --write .",
|
|
51
52
|
"fmtcheck": "prettier --check .",
|