@eighty4/dank 0.0.5-3 → 0.0.5-4
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 +36 -9
- 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 +59 -31
- 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 +27 -4
- 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 +39 -13
- 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
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
DankDetails,
|
|
6
6
|
EsbuildConfig,
|
|
7
7
|
PageMapping,
|
|
8
|
+
ServiceWorkerBuilder,
|
|
8
9
|
} from './dank.ts'
|
|
9
10
|
import { LOG } from './developer.ts'
|
|
10
11
|
import { defaultProjectDirs, type DankDirectories } from './dirs.ts'
|
|
@@ -22,7 +23,7 @@ const DEFAULT_CONFIG_PATH = './dank.config.ts'
|
|
|
22
23
|
export type { DevService } from './dank.ts'
|
|
23
24
|
|
|
24
25
|
export type ResolvedDankConfig = {
|
|
25
|
-
// static
|
|
26
|
+
// static config that does not hot reload during `dank serve`
|
|
26
27
|
get dirs(): Readonly<DankDirectories>
|
|
27
28
|
get flags(): Readonly<Omit<DankFlags, 'dankPort' | 'esbuildPort'>>
|
|
28
29
|
get mode(): 'build' | 'serve'
|
|
@@ -34,6 +35,7 @@ export type ResolvedDankConfig = {
|
|
|
34
35
|
get pages(): Readonly<Record<`/${string}`, PageMapping>>
|
|
35
36
|
get devPages(): Readonly<DankConfig['devPages']>
|
|
36
37
|
get services(): Readonly<DankConfig['services']>
|
|
38
|
+
get serviceWorkerBuilder(): DankConfig['serviceWorker']
|
|
37
39
|
|
|
38
40
|
buildTag(): Promise<string>
|
|
39
41
|
|
|
@@ -63,11 +65,13 @@ export async function loadConfig(
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
class DankConfigInternal implements ResolvedDankConfig {
|
|
66
|
-
#buildTag:
|
|
68
|
+
#buildTag: Promise<string> | null = null
|
|
69
|
+
#buildTagBuilder: DankConfig['buildTag']
|
|
67
70
|
#dirs: Readonly<DankDirectories>
|
|
68
71
|
#flags: Readonly<DankFlags>
|
|
69
72
|
#mode: 'build' | 'serve'
|
|
70
73
|
#modulePath: string
|
|
74
|
+
#serviceWorkerBuilder?: ServiceWorkerBuilder
|
|
71
75
|
|
|
72
76
|
#dankPort: number = DEFAULT_DEV_PORT
|
|
73
77
|
#esbuildPort: number = DEFAULT_ESBUILD_PORT
|
|
@@ -123,12 +127,19 @@ class DankConfigInternal implements ResolvedDankConfig {
|
|
|
123
127
|
return this.#services
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
130
|
+
get serviceWorkerBuilder(): DankConfig['serviceWorker'] {
|
|
131
|
+
return this.#serviceWorkerBuilder
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
buildTag(): Promise<string> {
|
|
135
|
+
if (this.#buildTag === null) {
|
|
136
|
+
this.#buildTag = createBuildTag(
|
|
137
|
+
this.#dirs.projectRootAbs,
|
|
138
|
+
this.#flags,
|
|
139
|
+
this.#buildTagBuilder,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
return this.#buildTag
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
async reload() {
|
|
@@ -136,13 +147,15 @@ class DankConfigInternal implements ResolvedDankConfig {
|
|
|
136
147
|
this.#modulePath,
|
|
137
148
|
resolveDankDetails(this.#mode, this.#flags),
|
|
138
149
|
)
|
|
139
|
-
this.#buildTag =
|
|
150
|
+
this.#buildTag = null
|
|
151
|
+
this.#buildTagBuilder = userConfig.buildTag
|
|
140
152
|
this.#dankPort = resolveDankPort(this.#flags, userConfig)
|
|
141
153
|
this.#esbuildPort = resolveEsbuildPort(this.#flags, userConfig)
|
|
142
154
|
this.#esbuild = Object.freeze(userConfig.esbuild)
|
|
143
155
|
this.#pages = Object.freeze(normalizePages(userConfig.pages))
|
|
144
156
|
this.#devPages = Object.freeze(userConfig.devPages)
|
|
145
157
|
this.#services = Object.freeze(userConfig.services)
|
|
158
|
+
this.#serviceWorkerBuilder = userConfig.serviceWorker
|
|
146
159
|
}
|
|
147
160
|
}
|
|
148
161
|
|
|
@@ -191,6 +204,7 @@ function validateDankConfig(c: Partial<DankConfig>) {
|
|
|
191
204
|
validateDevPages(c.devPages)
|
|
192
205
|
validateDevServices(c.services)
|
|
193
206
|
validateEsbuildConfig(c.esbuild)
|
|
207
|
+
validateServiceWorker(c.serviceWorker)
|
|
194
208
|
} catch (e: any) {
|
|
195
209
|
LOG({
|
|
196
210
|
realm: 'config',
|
|
@@ -230,6 +244,19 @@ function validateBuildTag(buildTag: DankConfig['buildTag']) {
|
|
|
230
244
|
}
|
|
231
245
|
}
|
|
232
246
|
|
|
247
|
+
function validateServiceWorker(serviceWorker: DankConfig['serviceWorker']) {
|
|
248
|
+
if (serviceWorker === null) {
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
switch (typeof serviceWorker) {
|
|
252
|
+
case 'undefined':
|
|
253
|
+
case 'function':
|
|
254
|
+
return
|
|
255
|
+
default:
|
|
256
|
+
throw Error('DankConfig.serviceWorker must be a function')
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
233
260
|
function validateEsbuildConfig(esbuild?: EsbuildConfig) {
|
|
234
261
|
if (esbuild?.loaders !== null && typeof esbuild?.loaders !== 'undefined') {
|
|
235
262
|
if (typeof esbuild.loaders !== 'object') {
|
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,23 +217,15 @@ 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
|
}
|
|
@@ -303,18 +323,18 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
|
|
|
303
323
|
delete this.#pages[urlPath]
|
|
304
324
|
}
|
|
305
325
|
|
|
306
|
-
#manifest(
|
|
326
|
+
async #manifest(): Promise<WebsiteManifest> {
|
|
307
327
|
return {
|
|
308
|
-
buildTag,
|
|
328
|
+
buildTag: await this.#c.buildTag(),
|
|
309
329
|
files: this.files(),
|
|
310
|
-
pageUrls:
|
|
330
|
+
pageUrls: Object.keys(this.#pages) as Array<`/${string}`>,
|
|
311
331
|
}
|
|
312
332
|
}
|
|
313
333
|
|
|
314
334
|
#onBuildManifest: OnBuildComplete = (build: BuildManifest) => {
|
|
315
335
|
// collect built bundle entrypoint hrefs
|
|
316
336
|
for (const [outPath, entrypoint] of Object.entries(build.bundles)) {
|
|
317
|
-
this.#bundles.add(outPath)
|
|
337
|
+
this.#bundles.add(ensurePath(outPath))
|
|
318
338
|
if (entrypoint) {
|
|
319
339
|
this.#entrypointHrefs[entrypoint] = outPath
|
|
320
340
|
}
|
|
@@ -413,7 +433,7 @@ export class BuildRegistry {
|
|
|
413
433
|
for (const [outPath, output] of Object.entries(
|
|
414
434
|
result.metafile.outputs,
|
|
415
435
|
)) {
|
|
416
|
-
bundles[outPath.replace(/^build[/\\]dist/, '')] =
|
|
436
|
+
bundles[outPath.replace(/^build[/\\](dist|watch)/, '')] =
|
|
417
437
|
output.entryPoint || null
|
|
418
438
|
}
|
|
419
439
|
let workers: BuildManifest['workers'] = null
|
|
@@ -438,3 +458,11 @@ export class BuildRegistry {
|
|
|
438
458
|
})
|
|
439
459
|
}
|
|
440
460
|
}
|
|
461
|
+
|
|
462
|
+
function ensurePath(path: string): `/${string}` {
|
|
463
|
+
if (path.startsWith('/')) {
|
|
464
|
+
return path as `/${string}`
|
|
465
|
+
} else {
|
|
466
|
+
throw Error(`expect build dist path ${path} to start with /`)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
@@ -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,11 +25,13 @@ 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;
|
|
@@ -69,18 +71,26 @@ 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;
|
|
74
82
|
}
|
|
75
83
|
async reload() {
|
|
76
84
|
const userConfig = await resolveConfig(this.#modulePath, resolveDankDetails(this.#mode, this.#flags));
|
|
77
|
-
this.#buildTag =
|
|
85
|
+
this.#buildTag = null;
|
|
86
|
+
this.#buildTagBuilder = userConfig.buildTag;
|
|
78
87
|
this.#dankPort = resolveDankPort(this.#flags, userConfig);
|
|
79
88
|
this.#esbuildPort = resolveEsbuildPort(this.#flags, userConfig);
|
|
80
89
|
this.#esbuild = Object.freeze(userConfig.esbuild);
|
|
81
90
|
this.#pages = Object.freeze(normalizePages(userConfig.pages));
|
|
82
91
|
this.#devPages = Object.freeze(userConfig.devPages);
|
|
83
92
|
this.#services = Object.freeze(userConfig.services);
|
|
93
|
+
this.#serviceWorkerBuilder = userConfig.serviceWorker;
|
|
84
94
|
}
|
|
85
95
|
}
|
|
86
96
|
function resolveDankPort(flags, userConfig) {
|
|
@@ -110,6 +120,7 @@ function validateDankConfig(c) {
|
|
|
110
120
|
validateDevPages(c.devPages);
|
|
111
121
|
validateDevServices(c.services);
|
|
112
122
|
validateEsbuildConfig(c.esbuild);
|
|
123
|
+
validateServiceWorker(c.serviceWorker);
|
|
113
124
|
} catch (e) {
|
|
114
125
|
throw e;
|
|
115
126
|
}
|
|
@@ -139,6 +150,18 @@ function validateBuildTag(buildTag) {
|
|
|
139
150
|
throw Error("DankConfig.buildTag must be a string or function");
|
|
140
151
|
}
|
|
141
152
|
}
|
|
153
|
+
function validateServiceWorker(serviceWorker) {
|
|
154
|
+
if (serviceWorker === null) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
switch (typeof serviceWorker) {
|
|
158
|
+
case "undefined":
|
|
159
|
+
case "function":
|
|
160
|
+
return;
|
|
161
|
+
default:
|
|
162
|
+
throw Error("DankConfig.serviceWorker must be a function");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
142
165
|
function validateEsbuildConfig(esbuild) {
|
|
143
166
|
if (esbuild?.loaders !== null && typeof esbuild?.loaders !== "undefined") {
|
|
144
167
|
if (typeof esbuild.loaders !== "object") {
|
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,13 +119,9 @@ 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() {
|
|
@@ -155,16 +174,16 @@ class WebsiteRegistry extends EventEmitter {
|
|
|
155
174
|
registration.html.removeAllListeners();
|
|
156
175
|
delete this.#pages[urlPath];
|
|
157
176
|
}
|
|
158
|
-
#manifest(
|
|
177
|
+
async #manifest() {
|
|
159
178
|
return {
|
|
160
|
-
buildTag,
|
|
179
|
+
buildTag: await this.#c.buildTag(),
|
|
161
180
|
files: this.files(),
|
|
162
|
-
pageUrls:
|
|
181
|
+
pageUrls: Object.keys(this.#pages)
|
|
163
182
|
};
|
|
164
183
|
}
|
|
165
184
|
#onBuildManifest = (build) => {
|
|
166
185
|
for (const [outPath, entrypoint] of Object.entries(build.bundles)) {
|
|
167
|
-
this.#bundles.add(outPath);
|
|
186
|
+
this.#bundles.add(ensurePath(outPath));
|
|
168
187
|
if (entrypoint) {
|
|
169
188
|
this.#entrypointHrefs[entrypoint] = outPath;
|
|
170
189
|
}
|
|
@@ -229,7 +248,7 @@ class BuildRegistry {
|
|
|
229
248
|
completeBuild(result) {
|
|
230
249
|
const bundles = {};
|
|
231
250
|
for (const [outPath, output] of Object.entries(result.metafile.outputs)) {
|
|
232
|
-
bundles[outPath.replace(/^build[/\\]dist/, "")] = output.entryPoint || null;
|
|
251
|
+
bundles[outPath.replace(/^build[/\\](dist|watch)/, "")] = output.entryPoint || null;
|
|
233
252
|
}
|
|
234
253
|
let workers = null;
|
|
235
254
|
if (this.#workers) {
|
|
@@ -254,6 +273,13 @@ class BuildRegistry {
|
|
|
254
273
|
});
|
|
255
274
|
}
|
|
256
275
|
}
|
|
276
|
+
function ensurePath(path) {
|
|
277
|
+
if (path.startsWith("/")) {
|
|
278
|
+
return path;
|
|
279
|
+
} else {
|
|
280
|
+
throw Error(`expect build dist path ${path} to start with /`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
257
283
|
export {
|
|
258
284
|
BuildRegistry,
|
|
259
285
|
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-4",
|
|
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 .",
|