@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.
@@ -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 print access logs')
20
- console.log(' --minify minify sources')
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 build for production release')
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 { type WebsiteManifest, WebsiteRegistry } from './registry.ts'
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(buildTag)
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 from process boot
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<DankConfig['devPages']>
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: DankConfig['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<DankConfig['devPages']>
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<DankConfig['devPages']> {
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
- async buildTag(): Promise<string> {
127
- return await createBuildTag(
128
- this.#dirs.projectRootAbs,
129
- this.#flags,
130
- this.#buildTag,
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 = userConfig.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.has(url.pathname)) {
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.has(url.pathname)) {
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<string> | null> {
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<string>> {
28
- const copied: Array<string> = []
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('/' + join(dir, p).replaceAll('\\', '/'))
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<string> = new Set()
63
+ #bundles: Set<`/${string}`> = new Set()
71
64
  #c: ResolvedDankConfig
72
65
  // public dir assets
73
- #copiedAssets: Set<string> | null = null
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<string> | null) {
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(): Set<string> {
175
- const files = new Set<string>()
197
+ files(): Array<`/${string}`> {
198
+ const files = new Set<`/${string}`>()
176
199
  for (const pageUrl of Object.keys(this.#pages))
177
- files.add(pageUrl === '/' ? '/index.html' : `${pageUrl}/index.html`)
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
- return files
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(buildTag: string): Promise<WebsiteManifest> {
193
- const manifest = this.#manifest(buildTag)
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(updatePages)) {
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(buildTag: string): WebsiteManifest {
325
+ async #manifest(): Promise<WebsiteManifest> {
307
326
  return {
308
- buildTag,
327
+ buildTag: await this.#c.buildTag(),
309
328
  files: this.files(),
310
- pageUrls: new Set(Object.keys(this.#pages)),
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 print access logs");
18
- console.log(" --minify minify sources");
19
- console.log(" --production build for production release");
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
- const buildTag = await c.buildTag();
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(buildTag);
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
- async buildTag() {
73
- return await createBuildTag(this.#dirs.projectRootAbs, this.#flags, this.#buildTag);
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 = userConfig.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
@@ -1,6 +1,8 @@
1
+ import { createServiceWorker } from "./service_worker.js";
1
2
  function defineConfig(config) {
2
3
  return config;
3
4
  }
4
5
  export {
6
+ createServiceWorker,
5
7
  defineConfig
6
8
  };
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.has(url.pathname)) {
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.has(url.pathname)) {
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("/" + join(dir, p).replaceAll("\\", "/"));
39
+ copied.push(`/${join(dir, p).replaceAll("\\", "/")}`);
40
40
  }
41
41
  } catch (e) {
42
42
  console.error("stat error", e);
@@ -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
- return files;
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(buildTag) {
100
- const manifest = this.#manifest(buildTag);
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(updatePages)) {
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(buildTag) {
176
+ async #manifest() {
159
177
  return {
160
- buildTag,
178
+ buildTag: await this.#c.buildTag(),
161
179
  files: this.files(),
162
- pageUrls: new Set(Object.keys(this.#pages))
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
+ };
@@ -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",
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 .",