@eighty4/dank 0.0.5-2 → 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 -5
- package/lib/build_tag.ts +119 -15
- package/lib/config.ts +56 -1
- package/lib/dank.ts +44 -2
- 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 -4
- package/lib_js/build_tag.js +74 -14
- package/lib_js/config.js +43 -0
- 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 +22 -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,12 +1,12 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { createBuildTag } from './build_tag.ts'
|
|
4
3
|
import { loadConfig, type ResolvedDankConfig } from './config.ts'
|
|
4
|
+
import type { ServiceWorkerBuild, WebsiteManifest } from './dank.ts'
|
|
5
5
|
import { type DefineDankGlobal, createGlobalDefinitions } from './define.ts'
|
|
6
6
|
import type { DankDirectories } from './dirs.ts'
|
|
7
7
|
import { esbuildWebpages, esbuildWorkers } from './esbuild.ts'
|
|
8
8
|
import { copyAssets } from './public.ts'
|
|
9
|
-
import {
|
|
9
|
+
import { WebsiteRegistry } from './registry.ts'
|
|
10
10
|
|
|
11
11
|
export async function buildWebsite(
|
|
12
12
|
c?: ResolvedDankConfig,
|
|
@@ -14,7 +14,6 @@ export async function buildWebsite(
|
|
|
14
14
|
if (!c) {
|
|
15
15
|
c = await loadConfig('build', process.cwd())
|
|
16
16
|
}
|
|
17
|
-
const buildTag = await createBuildTag(c.flags)
|
|
18
17
|
console.log(
|
|
19
18
|
c.flags.minify
|
|
20
19
|
? c.flags.production
|
|
@@ -22,7 +21,7 @@ export async function buildWebsite(
|
|
|
22
21
|
: 'minified'
|
|
23
22
|
: 'unminified',
|
|
24
23
|
'build',
|
|
25
|
-
buildTag,
|
|
24
|
+
await c.buildTag(),
|
|
26
25
|
'building in ./build/dist',
|
|
27
26
|
)
|
|
28
27
|
await rm(c.dirs.buildRoot, { recursive: true, force: true })
|
|
@@ -32,7 +31,7 @@ export async function buildWebsite(
|
|
|
32
31
|
}
|
|
33
32
|
await mkdir(join(c.dirs.buildRoot, 'metafiles'), { recursive: true })
|
|
34
33
|
const registry = await buildWebpages(c, createGlobalDefinitions(c))
|
|
35
|
-
return await registry.writeManifest(
|
|
34
|
+
return await registry.writeManifest()
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
// builds all webpage entrypoints in one esbuild.build context to support code splitting
|
|
@@ -63,6 +62,7 @@ async function buildWebpages(
|
|
|
63
62
|
)
|
|
64
63
|
}),
|
|
65
64
|
)
|
|
65
|
+
await buildServiceWorker(registry)
|
|
66
66
|
return registry
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -128,3 +128,55 @@ export function createWorkerRegex(
|
|
|
128
128
|
'g',
|
|
129
129
|
)
|
|
130
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/build_tag.ts
CHANGED
|
@@ -1,25 +1,129 @@
|
|
|
1
1
|
import { exec } from 'node:child_process'
|
|
2
|
+
import type { DankConfig } from './dank.ts'
|
|
2
3
|
import type { DankFlags } from './flags.ts'
|
|
3
4
|
|
|
4
|
-
export async function createBuildTag(
|
|
5
|
+
export async function createBuildTag(
|
|
6
|
+
projectDir: string,
|
|
7
|
+
flags: DankFlags,
|
|
8
|
+
buildTagSource?: DankConfig['buildTag'],
|
|
9
|
+
): Promise<string> {
|
|
10
|
+
if (typeof buildTagSource === 'function') {
|
|
11
|
+
buildTagSource = await buildTagSource({ production: flags.production })
|
|
12
|
+
}
|
|
13
|
+
if (typeof buildTagSource === 'undefined' || buildTagSource === null) {
|
|
14
|
+
buildTagSource = await resolveExpressionDefault(projectDir)
|
|
15
|
+
}
|
|
16
|
+
if (typeof buildTagSource !== 'string') {
|
|
17
|
+
throw TypeError(
|
|
18
|
+
'DankConfig.buildTag must resolve to a string expession',
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
const params: BuildTagParams = {}
|
|
5
22
|
const now = new Date()
|
|
23
|
+
const paramPattern = new RegExp(/{{\s*(?<name>[a-z][A-Za-z]+)\s*}}/g)
|
|
24
|
+
let paramMatch: RegExpExecArray | null
|
|
25
|
+
let buildTag = buildTagSource
|
|
26
|
+
let offset = 0
|
|
27
|
+
while ((paramMatch = paramPattern.exec(buildTagSource)) != null) {
|
|
28
|
+
const paramName = paramMatch.groups!.name.trim() as keyof BuildTagParams
|
|
29
|
+
let paramValue: string
|
|
30
|
+
if (params[paramName]) {
|
|
31
|
+
paramValue = params[paramName]
|
|
32
|
+
} else {
|
|
33
|
+
paramValue = params[paramName] = await getParamValue(
|
|
34
|
+
projectDir,
|
|
35
|
+
paramName,
|
|
36
|
+
now,
|
|
37
|
+
buildTagSource,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
buildTag =
|
|
41
|
+
buildTag.substring(0, paramMatch.index + offset) +
|
|
42
|
+
paramValue +
|
|
43
|
+
buildTag.substring(paramMatch.index + paramMatch[0].length + offset)
|
|
44
|
+
offset += paramValue.length - paramMatch[0].length
|
|
45
|
+
}
|
|
46
|
+
const validate = /^[A-Za-z\d][A-Za-z\d-_\.]+$/
|
|
47
|
+
if (!validate.test(buildTag)) {
|
|
48
|
+
throw Error(
|
|
49
|
+
`build tag ${buildTag} does not pass pattern ${validate.source} validation`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
return buildTag
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function resolveExpressionDefault(projectDir: string): Promise<string> {
|
|
56
|
+
const base = '{{ date }}-{{ timeMS }}'
|
|
57
|
+
const isGitRepo = await new Promise(res =>
|
|
58
|
+
exec('git rev-parse --is-inside-work-tree', { cwd: projectDir }, err =>
|
|
59
|
+
res(!err),
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
return isGitRepo ? base + '-{{ gitHash }}' : base
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type BuildTagParams = {
|
|
66
|
+
date?: string
|
|
67
|
+
gitHash?: string
|
|
68
|
+
timeMS?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function getParamValue(
|
|
72
|
+
projectDir: string,
|
|
73
|
+
name: keyof BuildTagParams,
|
|
74
|
+
now: Date,
|
|
75
|
+
buildTagSource: string,
|
|
76
|
+
): Promise<string> {
|
|
77
|
+
switch (name) {
|
|
78
|
+
case 'date':
|
|
79
|
+
return getDate(now)
|
|
80
|
+
case 'gitHash':
|
|
81
|
+
try {
|
|
82
|
+
return await getGitHash(projectDir)
|
|
83
|
+
} catch (e) {
|
|
84
|
+
if (e === 'not-repo') {
|
|
85
|
+
throw Error(
|
|
86
|
+
`buildTag cannot use \`gitHash\` in \`${buildTagSource}\` outside of a git repository`,
|
|
87
|
+
)
|
|
88
|
+
} else {
|
|
89
|
+
throw e
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
case 'timeMS':
|
|
93
|
+
return getTimeMS(now)
|
|
94
|
+
default:
|
|
95
|
+
throw Error(name + ' is not a supported build tag param')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getDate(now: Date): string {
|
|
100
|
+
return now.toISOString().substring(0, 10)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function getGitHash(projectDir: string): Promise<string> {
|
|
104
|
+
return await new Promise((res, rej) =>
|
|
105
|
+
exec(
|
|
106
|
+
'git rev-parse --short HEAD',
|
|
107
|
+
{ cwd: projectDir },
|
|
108
|
+
(err, stdout, stderr) => {
|
|
109
|
+
if (err) {
|
|
110
|
+
if (stderr.includes('not a git repository')) {
|
|
111
|
+
rej('not-repo')
|
|
112
|
+
} else {
|
|
113
|
+
rej(err)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
res(stdout.trim())
|
|
117
|
+
},
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getTimeMS(now: Date): string {
|
|
6
123
|
const ms =
|
|
7
124
|
now.getUTCMilliseconds() +
|
|
8
125
|
now.getUTCSeconds() * 1000 +
|
|
9
126
|
now.getUTCMinutes() * 1000 * 60 +
|
|
10
127
|
now.getUTCHours() * 1000 * 60 * 60
|
|
11
|
-
|
|
12
|
-
const time = String(ms).padStart(8, '0')
|
|
13
|
-
const when = `${date}-${time}`
|
|
14
|
-
if (flags.production) {
|
|
15
|
-
const gitHash = await new Promise((res, rej) =>
|
|
16
|
-
exec('git rev-parse --short HEAD', (err, stdout) => {
|
|
17
|
-
if (err) rej(err)
|
|
18
|
-
res(stdout.trim())
|
|
19
|
-
}),
|
|
20
|
-
)
|
|
21
|
-
return `${when}-${gitHash}`
|
|
22
|
-
} else {
|
|
23
|
-
return when
|
|
24
|
-
}
|
|
128
|
+
return String(ms).padStart(8, '0')
|
|
25
129
|
}
|
package/lib/config.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { isAbsolute, resolve } from 'node:path'
|
|
2
|
+
import { createBuildTag } from './build_tag.ts'
|
|
2
3
|
import type {
|
|
3
4
|
DankConfig,
|
|
4
5
|
DankDetails,
|
|
5
6
|
EsbuildConfig,
|
|
6
7
|
PageMapping,
|
|
8
|
+
ServiceWorkerBuilder,
|
|
7
9
|
} from './dank.ts'
|
|
8
10
|
import { LOG } from './developer.ts'
|
|
9
11
|
import { defaultProjectDirs, type DankDirectories } from './dirs.ts'
|
|
@@ -21,7 +23,7 @@ const DEFAULT_CONFIG_PATH = './dank.config.ts'
|
|
|
21
23
|
export type { DevService } from './dank.ts'
|
|
22
24
|
|
|
23
25
|
export type ResolvedDankConfig = {
|
|
24
|
-
// static
|
|
26
|
+
// static config that does not hot reload during `dank serve`
|
|
25
27
|
get dirs(): Readonly<DankDirectories>
|
|
26
28
|
get flags(): Readonly<Omit<DankFlags, 'dankPort' | 'esbuildPort'>>
|
|
27
29
|
get mode(): 'build' | 'serve'
|
|
@@ -33,6 +35,9 @@ export type ResolvedDankConfig = {
|
|
|
33
35
|
get pages(): Readonly<Record<`/${string}`, PageMapping>>
|
|
34
36
|
get devPages(): Readonly<DankConfig['devPages']>
|
|
35
37
|
get services(): Readonly<DankConfig['services']>
|
|
38
|
+
get serviceWorkerBuilder(): DankConfig['serviceWorker']
|
|
39
|
+
|
|
40
|
+
buildTag(): Promise<string>
|
|
36
41
|
|
|
37
42
|
reload(): Promise<void>
|
|
38
43
|
}
|
|
@@ -60,10 +65,13 @@ export async function loadConfig(
|
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
class DankConfigInternal implements ResolvedDankConfig {
|
|
68
|
+
#buildTag: Promise<string> | null = null
|
|
69
|
+
#buildTagBuilder: DankConfig['buildTag']
|
|
63
70
|
#dirs: Readonly<DankDirectories>
|
|
64
71
|
#flags: Readonly<DankFlags>
|
|
65
72
|
#mode: 'build' | 'serve'
|
|
66
73
|
#modulePath: string
|
|
74
|
+
#serviceWorkerBuilder?: ServiceWorkerBuilder
|
|
67
75
|
|
|
68
76
|
#dankPort: number = DEFAULT_DEV_PORT
|
|
69
77
|
#esbuildPort: number = DEFAULT_ESBUILD_PORT
|
|
@@ -119,17 +127,35 @@ class DankConfigInternal implements ResolvedDankConfig {
|
|
|
119
127
|
return this.#services
|
|
120
128
|
}
|
|
121
129
|
|
|
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
|
|
143
|
+
}
|
|
144
|
+
|
|
122
145
|
async reload() {
|
|
123
146
|
const userConfig = await resolveConfig(
|
|
124
147
|
this.#modulePath,
|
|
125
148
|
resolveDankDetails(this.#mode, this.#flags),
|
|
126
149
|
)
|
|
150
|
+
this.#buildTag = null
|
|
151
|
+
this.#buildTagBuilder = userConfig.buildTag
|
|
127
152
|
this.#dankPort = resolveDankPort(this.#flags, userConfig)
|
|
128
153
|
this.#esbuildPort = resolveEsbuildPort(this.#flags, userConfig)
|
|
129
154
|
this.#esbuild = Object.freeze(userConfig.esbuild)
|
|
130
155
|
this.#pages = Object.freeze(normalizePages(userConfig.pages))
|
|
131
156
|
this.#devPages = Object.freeze(userConfig.devPages)
|
|
132
157
|
this.#services = Object.freeze(userConfig.services)
|
|
158
|
+
this.#serviceWorkerBuilder = userConfig.serviceWorker
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
161
|
|
|
@@ -173,10 +199,12 @@ function resolveDankDetails(
|
|
|
173
199
|
function validateDankConfig(c: Partial<DankConfig>) {
|
|
174
200
|
try {
|
|
175
201
|
validatePorts(c)
|
|
202
|
+
validateBuildTag(c.buildTag)
|
|
176
203
|
validatePages(c.pages)
|
|
177
204
|
validateDevPages(c.devPages)
|
|
178
205
|
validateDevServices(c.services)
|
|
179
206
|
validateEsbuildConfig(c.esbuild)
|
|
207
|
+
validateServiceWorker(c.serviceWorker)
|
|
180
208
|
} catch (e: any) {
|
|
181
209
|
LOG({
|
|
182
210
|
realm: 'config',
|
|
@@ -202,6 +230,33 @@ function validatePorts(c: Partial<DankConfig>) {
|
|
|
202
230
|
}
|
|
203
231
|
}
|
|
204
232
|
|
|
233
|
+
function validateBuildTag(buildTag: DankConfig['buildTag']) {
|
|
234
|
+
if (buildTag === null) {
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
switch (typeof buildTag) {
|
|
238
|
+
case 'undefined':
|
|
239
|
+
case 'string':
|
|
240
|
+
case 'function':
|
|
241
|
+
return
|
|
242
|
+
default:
|
|
243
|
+
throw Error('DankConfig.buildTag must be a string or function')
|
|
244
|
+
}
|
|
245
|
+
}
|
|
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
|
+
|
|
205
260
|
function validateEsbuildConfig(esbuild?: EsbuildConfig) {
|
|
206
261
|
if (esbuild?.loaders !== null && typeof esbuild?.loaders !== 'undefined') {
|
|
207
262
|
if (typeof esbuild.loaders !== 'object') {
|
package/lib/dank.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Plugin as EsbuildPlugin } from 'esbuild'
|
|
2
2
|
|
|
3
3
|
export type DankConfig = {
|
|
4
|
-
// used for
|
|
5
|
-
|
|
4
|
+
// used for service worker caching
|
|
5
|
+
buildTag?: string | BuildTagBuilder
|
|
6
6
|
|
|
7
7
|
// customize esbuild configs
|
|
8
8
|
esbuild?: EsbuildConfig
|
|
@@ -24,8 +24,20 @@ 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
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type BuildTagParams = {
|
|
34
|
+
production: boolean
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
export type BuildTagBuilder = (
|
|
38
|
+
build: BuildTagParams,
|
|
39
|
+
) => Promise<string> | string
|
|
40
|
+
|
|
29
41
|
// extend an html entrypoint with url rewriting similar to cdn configurations
|
|
30
42
|
// after trying all webpage, bundle and asset paths, mapping patterns
|
|
31
43
|
// will be tested in the alphabetical order of the webpage paths
|
|
@@ -91,3 +103,33 @@ export function defineConfig(
|
|
|
91
103
|
): Partial<DankConfig> | DankConfigFunction {
|
|
92
104
|
return config
|
|
93
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)
|