@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.
@@ -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
@@ -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 from process boot
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: DankConfig['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
- async buildTag(): Promise<string> {
127
- return await createBuildTag(
128
- this.#dirs.projectRootAbs,
129
- this.#flags,
130
- this.#buildTag,
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 = userConfig.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.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,23 +217,15 @@ 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
  }
@@ -303,18 +323,18 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
303
323
  delete this.#pages[urlPath]
304
324
  }
305
325
 
306
- #manifest(buildTag: string): WebsiteManifest {
326
+ async #manifest(): Promise<WebsiteManifest> {
307
327
  return {
308
- buildTag,
328
+ buildTag: await this.#c.buildTag(),
309
329
  files: this.files(),
310
- pageUrls: new Set(Object.keys(this.#pages)),
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 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,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
- 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;
74
82
  }
75
83
  async reload() {
76
84
  const userConfig = await resolveConfig(this.#modulePath, resolveDankDetails(this.#mode, this.#flags));
77
- this.#buildTag = userConfig.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
@@ -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,13 +119,9 @@ 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() {
@@ -155,16 +174,16 @@ class WebsiteRegistry extends EventEmitter {
155
174
  registration.html.removeAllListeners();
156
175
  delete this.#pages[urlPath];
157
176
  }
158
- #manifest(buildTag) {
177
+ async #manifest() {
159
178
  return {
160
- buildTag,
179
+ buildTag: await this.#c.buildTag(),
161
180
  files: this.files(),
162
- pageUrls: new Set(Object.keys(this.#pages))
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
+ };
@@ -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-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 .",