@eighty4/dank 0.0.1-3 → 0.0.1
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/README.md +65 -0
- package/client/esbuild.js +26 -15
- package/lib/build.ts +1 -1
- package/lib/config.ts +2 -1
- package/lib/dank.ts +8 -1
- package/lib/esbuild.ts +7 -0
- package/lib/html.ts +11 -3
- package/lib/http.ts +117 -29
- package/lib/serve.ts +255 -51
- package/lib/services.ts +156 -11
- package/lib_js/build.js +1 -1
- package/lib_js/config.js +2 -1
- package/lib_js/dank.js +6 -0
- package/lib_js/esbuild.js +7 -0
- package/lib_js/html.js +8 -3
- package/lib_js/http.js +95 -22
- package/lib_js/serve.js +211 -38
- package/lib_js/services.js +146 -10
- package/lib_types/dank.d.ts +1 -1
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Build DANK webpages
|
|
2
|
+
|
|
3
|
+
### Get developing right away:
|
|
4
|
+
|
|
5
|
+
```shell
|
|
6
|
+
bun create dank --out-dir www
|
|
7
|
+
|
|
8
|
+
npm create dank -- --out-dir www
|
|
9
|
+
|
|
10
|
+
pnpm create dank --out-dir www
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### DANK has some perks:
|
|
14
|
+
|
|
15
|
+
- Webpage-first development for multi-page websites
|
|
16
|
+
- TypeScript supported with `<script src="./dank.ts">`
|
|
17
|
+
- Code splitting via `esbuild` bundler across all webpages
|
|
18
|
+
- Hashes added to all bundled assets for efficient cache utilization
|
|
19
|
+
- `dank serve` updates CSS in real-time (hot-reloading)
|
|
20
|
+
- `dank serve` launches development processes and merges their stdio
|
|
21
|
+
- `dank serve --preview` builds the website and serves the output from `dist`
|
|
22
|
+
- `dank build --production` optimizes with `esbuild` minifying and tree-shaking
|
|
23
|
+
- DANK's codebase is so tiny you can read it all in 20 minutes
|
|
24
|
+
|
|
25
|
+
### DANK isn't for every use case!
|
|
26
|
+
|
|
27
|
+
[Vite](https://vite.dev) is the right move for building a Single-Page Application.
|
|
28
|
+
|
|
29
|
+
Dynamic content with Static-Site Generation or Server-Side Rendering should use
|
|
30
|
+
[Astro](https://astro.build), [Next.js](https://nextjs.org) or [SvelteKit](https://svelte.dev).
|
|
31
|
+
|
|
32
|
+
#### DANK is an ideal choice for multi-page websites deployed to a CDN that integrate with serverless components and APIs.
|
|
33
|
+
|
|
34
|
+
## `dank.config.ts` examples
|
|
35
|
+
|
|
36
|
+
Webpages and their URLs are configured explicitly to keep your URLs
|
|
37
|
+
and workspace organized independently:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { defineConfig } from '@eighty4/dank'
|
|
41
|
+
|
|
42
|
+
export default defineConfig({
|
|
43
|
+
pages: {
|
|
44
|
+
'/': './home.html',
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Streamline development with `dank serve` launching APIs and databases when starting your website's dev server:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { defineConfig } from '@eighty4/dank'
|
|
53
|
+
|
|
54
|
+
export default defineConfig({
|
|
55
|
+
pages: {
|
|
56
|
+
'/': './home.html',
|
|
57
|
+
},
|
|
58
|
+
services: [
|
|
59
|
+
{
|
|
60
|
+
command: 'node --watch --env-file-if-exists=.env.dev server.ts',
|
|
61
|
+
cwd: './api',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
```
|
package/client/esbuild.js
CHANGED
|
@@ -44,25 +44,37 @@ new EventSource('http://127.0.0.1:2999/esbuild').addEventListener('change', (e)
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
|
-
function addCssUpdateIndicator() {
|
|
47
|
+
export function addCssUpdateIndicator() {
|
|
48
48
|
const indicator = createUpdateIndicator('green', '9999');
|
|
49
|
-
indicator.style.transition = 'opacity ease-in-out .38s';
|
|
50
49
|
indicator.style.opacity = '0';
|
|
51
|
-
indicator.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
50
|
+
indicator.animate([
|
|
51
|
+
{ opacity: 0 },
|
|
52
|
+
{ opacity: 1 },
|
|
53
|
+
{ opacity: 1 },
|
|
54
|
+
{ opacity: 1 },
|
|
55
|
+
{ opacity: 0.75 },
|
|
56
|
+
{ opacity: 0.5 },
|
|
57
|
+
{ opacity: 0.25 },
|
|
58
|
+
{ opacity: 0 },
|
|
59
|
+
], {
|
|
60
|
+
duration: 400,
|
|
61
|
+
iterations: 1,
|
|
62
|
+
direction: 'normal',
|
|
63
|
+
easing: 'linear',
|
|
64
|
+
});
|
|
61
65
|
document.body.appendChild(indicator);
|
|
62
|
-
|
|
66
|
+
Promise.all(indicator.getAnimations().map(a => a.finished)).then(() => indicator.remove());
|
|
63
67
|
}
|
|
64
68
|
function addJsReloadIndicator() {
|
|
65
|
-
|
|
69
|
+
const indicator = createUpdateIndicator('orange', '9000');
|
|
70
|
+
indicator.style.opacity = '0';
|
|
71
|
+
indicator.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
72
|
+
duration: 400,
|
|
73
|
+
iterations: 1,
|
|
74
|
+
direction: 'normal',
|
|
75
|
+
easing: 'ease-in',
|
|
76
|
+
});
|
|
77
|
+
document.body.appendChild(indicator);
|
|
66
78
|
}
|
|
67
79
|
function createUpdateIndicator(color, zIndex) {
|
|
68
80
|
const indicator = document.createElement('div');
|
|
@@ -74,4 +86,3 @@ function createUpdateIndicator(color, zIndex) {
|
|
|
74
86
|
indicator.style.boxSizing = 'border-box';
|
|
75
87
|
return indicator;
|
|
76
88
|
}
|
|
77
|
-
export {};
|
package/lib/build.ts
CHANGED
package/lib/config.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { DankConfig } from './dank.ts'
|
|
|
4
4
|
const CFG_P = './dank.config.ts'
|
|
5
5
|
|
|
6
6
|
export async function loadConfig(path: string = CFG_P): Promise<DankConfig> {
|
|
7
|
-
const
|
|
7
|
+
const modulePath = `${resolveConfigPath(path)}?${Date.now()}`
|
|
8
|
+
const module = await import(modulePath)
|
|
8
9
|
return await module.default
|
|
9
10
|
}
|
|
10
11
|
|
package/lib/dank.ts
CHANGED
|
@@ -2,7 +2,7 @@ export type DankConfig = {
|
|
|
2
2
|
// used for releases and service worker caching
|
|
3
3
|
// buildTag?: (() => Promise<string> | string) | string
|
|
4
4
|
// mapping url to fs paths of webpages to build
|
|
5
|
-
pages: Record<`/${string}`,
|
|
5
|
+
pages: Record<`/${string}`, `${string}.html`>
|
|
6
6
|
|
|
7
7
|
services?: Array<DevService>
|
|
8
8
|
}
|
|
@@ -18,6 +18,7 @@ export async function defineConfig(
|
|
|
18
18
|
): Promise<DankConfig> {
|
|
19
19
|
validatePages(c.pages)
|
|
20
20
|
validateDevServices(c.services)
|
|
21
|
+
normalizePagePaths(c.pages)
|
|
21
22
|
return c as DankConfig
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -77,3 +78,9 @@ function validateDevServices(services: DankConfig['services']) {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
}
|
|
81
|
+
|
|
82
|
+
function normalizePagePaths(pages: any) {
|
|
83
|
+
for (const urlPath of Object.keys(pages)) {
|
|
84
|
+
pages[urlPath] = pages[urlPath].replace(/^\.\//, '')
|
|
85
|
+
}
|
|
86
|
+
}
|
package/lib/esbuild.ts
CHANGED
|
@@ -21,6 +21,11 @@ const jsBuildOptions: BuildOptions & { metafile: true; write: true } = {
|
|
|
21
21
|
const webpageBuildOptions: BuildOptions & { metafile: true; write: true } = {
|
|
22
22
|
assetNames: 'assets/[name]-[hash]',
|
|
23
23
|
format: 'esm',
|
|
24
|
+
loader: {
|
|
25
|
+
'.tff': 'file',
|
|
26
|
+
'.woff': 'file',
|
|
27
|
+
'.woff2': 'file',
|
|
28
|
+
},
|
|
24
29
|
...jsBuildOptions,
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -35,6 +40,8 @@ export async function esbuildDevContext(
|
|
|
35
40
|
entryPoints: removeEntryPointOutExt(entryPoints),
|
|
36
41
|
outdir,
|
|
37
42
|
...webpageBuildOptions,
|
|
43
|
+
metafile: false,
|
|
44
|
+
write: false,
|
|
38
45
|
})
|
|
39
46
|
}
|
|
40
47
|
|
package/lib/html.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { dirname, join, relative } from 'node:path'
|
|
3
|
+
import { extname } from 'node:path/posix'
|
|
3
4
|
import {
|
|
4
5
|
defaultTreeAdapter,
|
|
5
6
|
type DefaultTreeAdapterTypes,
|
|
@@ -75,12 +76,12 @@ export class HtmlEntrypoint {
|
|
|
75
76
|
) {
|
|
76
77
|
importScript.elem.attrs.find(
|
|
77
78
|
attr => attr.name === 'src',
|
|
78
|
-
)!.value = rewriteTo || `/${importScript.out}
|
|
79
|
+
)!.value = rewriteTo || `/${importScript.out}`
|
|
79
80
|
}
|
|
80
81
|
} else if (importScript.type === 'style') {
|
|
81
82
|
importScript.elem.attrs.find(
|
|
82
83
|
attr => attr.name === 'href',
|
|
83
|
-
)!.value = rewriteTo || `/${importScript.out}
|
|
84
|
+
)!.value = rewriteTo || `/${importScript.out}`
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
}
|
|
@@ -182,12 +183,19 @@ export class HtmlEntrypoint {
|
|
|
182
183
|
|
|
183
184
|
#addScript(type: ImportedScript['type'], href: string, elem: Element) {
|
|
184
185
|
const inPath = join(dirname(this.#fsPath), href)
|
|
186
|
+
let outPath = inPath.replace(/^pages\//, '')
|
|
187
|
+
if (type === 'script' && !outPath.endsWith('.js')) {
|
|
188
|
+
outPath = outPath.replace(
|
|
189
|
+
new RegExp(extname(outPath).substring(1) + '$'),
|
|
190
|
+
'js',
|
|
191
|
+
)
|
|
192
|
+
}
|
|
185
193
|
this.#scripts.push({
|
|
186
194
|
type,
|
|
187
195
|
href,
|
|
188
196
|
elem,
|
|
189
197
|
in: inPath,
|
|
190
|
-
out:
|
|
198
|
+
out: outPath,
|
|
191
199
|
})
|
|
192
200
|
}
|
|
193
201
|
}
|
package/lib/http.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs'
|
|
2
|
+
import { stat } from 'node:fs/promises'
|
|
2
3
|
import {
|
|
3
4
|
createServer,
|
|
4
5
|
type IncomingHttpHeaders,
|
|
@@ -6,7 +7,7 @@ import {
|
|
|
6
7
|
type OutgoingHttpHeaders,
|
|
7
8
|
type ServerResponse,
|
|
8
9
|
} from 'node:http'
|
|
9
|
-
import { extname, join
|
|
10
|
+
import { extname, join } from 'node:path'
|
|
10
11
|
import { isProductionBuild } from './flags.ts'
|
|
11
12
|
|
|
12
13
|
export type FrontendFetcher = (
|
|
@@ -44,41 +45,122 @@ export function createBuiltDistFilesFetcher(
|
|
|
44
45
|
res.writeHead(404)
|
|
45
46
|
res.end()
|
|
46
47
|
} else {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
: fsJoin(dir, url.pathname),
|
|
53
|
-
)
|
|
54
|
-
reading.pipe(res)
|
|
55
|
-
reading.on('error', err => {
|
|
56
|
-
console.error(
|
|
57
|
-
`${url.pathname} file read ${reading.path} error ${err.message}`,
|
|
58
|
-
)
|
|
59
|
-
res.statusCode = 500
|
|
60
|
-
res.end()
|
|
61
|
-
})
|
|
48
|
+
const p =
|
|
49
|
+
extname(url.pathname) === ''
|
|
50
|
+
? join(dir, url.pathname, 'index.html')
|
|
51
|
+
: join(dir, url.pathname)
|
|
52
|
+
streamFile(p, res)
|
|
62
53
|
}
|
|
63
54
|
}
|
|
64
55
|
}
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
|
|
57
|
+
type DevServeOpts = {
|
|
58
|
+
// ref of original DankConfig['pages'] mapping
|
|
59
|
+
// updated incrementally instead of replacing
|
|
60
|
+
pages: Record<string, string>
|
|
61
|
+
// dir processed html files are written to
|
|
62
|
+
pagesDir: string
|
|
63
|
+
// port to esbuild dev server
|
|
64
|
+
proxyPort: number
|
|
65
|
+
// dir of public assets
|
|
66
|
+
publicDir: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createDevServeFilesFetcher(
|
|
70
|
+
opts: DevServeOpts,
|
|
71
|
+
): FrontendFetcher {
|
|
72
|
+
const proxyAddress = 'http://127.0.0.1:' + opts.proxyPort
|
|
68
73
|
return (url: URL, _headers: Headers, res: ServerResponse) => {
|
|
69
|
-
|
|
70
|
-
res
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
if (opts.pages[url.pathname]) {
|
|
75
|
+
streamFile(join(opts.pagesDir, url.pathname + 'index.html'), res)
|
|
76
|
+
} else {
|
|
77
|
+
const maybePublicPath = join(opts.publicDir, url.pathname)
|
|
78
|
+
exists(join(opts.publicDir, url.pathname)).then(fromPublic => {
|
|
79
|
+
if (fromPublic) {
|
|
80
|
+
streamFile(maybePublicPath, res)
|
|
81
|
+
} else {
|
|
82
|
+
retryFetchWithTimeout(proxyAddress + url.pathname)
|
|
83
|
+
.then(fetchResponse => {
|
|
84
|
+
res.writeHead(
|
|
85
|
+
fetchResponse.status,
|
|
86
|
+
convertHeadersFromFetch(fetchResponse.headers),
|
|
87
|
+
)
|
|
88
|
+
fetchResponse.bytes().then(data => res.end(data))
|
|
89
|
+
})
|
|
90
|
+
.catch(e => {
|
|
91
|
+
if (e === 'retrytimeout') {
|
|
92
|
+
res.writeHead(504)
|
|
93
|
+
} else {
|
|
94
|
+
console.error(
|
|
95
|
+
'unknown frontend proxy fetch error:',
|
|
96
|
+
e,
|
|
97
|
+
)
|
|
98
|
+
res.writeHead(502)
|
|
99
|
+
}
|
|
100
|
+
res.end()
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
76
105
|
}
|
|
77
106
|
}
|
|
78
107
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
108
|
+
const PROXY_FETCH_RETRY_INTERVAL = 27
|
|
109
|
+
const PROXY_FETCH_RETRY_TIMEOUT = 1000
|
|
110
|
+
|
|
111
|
+
async function retryFetchWithTimeout(url: string): Promise<Response> {
|
|
112
|
+
let timeout = Date.now() + PROXY_FETCH_RETRY_TIMEOUT
|
|
113
|
+
while (true) {
|
|
114
|
+
try {
|
|
115
|
+
return await fetch(url)
|
|
116
|
+
} catch (e: any) {
|
|
117
|
+
if (isNodeFailedFetch(e) || isBunFailedFetch(e)) {
|
|
118
|
+
if (timeout < Date.now()) {
|
|
119
|
+
throw 'retrytimeout'
|
|
120
|
+
} else {
|
|
121
|
+
await new Promise(res =>
|
|
122
|
+
setTimeout(res, PROXY_FETCH_RETRY_INTERVAL),
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
throw e
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isBunFailedFetch(e: any): boolean {
|
|
133
|
+
return e.code === 'ConnectionRefused'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isNodeFailedFetch(e: any): boolean {
|
|
137
|
+
return e.message === 'fetch failed'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function exists(p: string): Promise<boolean> {
|
|
141
|
+
try {
|
|
142
|
+
const maybe = stat(p)
|
|
143
|
+
return (await maybe).isFile()
|
|
144
|
+
} catch (ignore) {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function streamFile(p: string, res: ServerResponse) {
|
|
150
|
+
const mimeType = resolveMimeType(p)
|
|
151
|
+
res.setHeader('Content-Type', mimeType)
|
|
152
|
+
const reading = createReadStream(p)
|
|
153
|
+
reading.pipe(res)
|
|
154
|
+
reading.on('error', err => {
|
|
155
|
+
console.error(`file read ${reading.path} error ${err.message}`)
|
|
156
|
+
res.statusCode = 500
|
|
157
|
+
res.end()
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function resolveMimeType(p: string): string {
|
|
162
|
+
switch (extname(p)) {
|
|
163
|
+
case '.html':
|
|
82
164
|
return 'text/html'
|
|
83
165
|
case '.js':
|
|
84
166
|
return 'text/javascript'
|
|
@@ -90,8 +172,14 @@ function resolveMimeType(url: URL): string {
|
|
|
90
172
|
return 'image/svg+xml'
|
|
91
173
|
case '.png':
|
|
92
174
|
return 'image/png'
|
|
175
|
+
case '.ttf':
|
|
176
|
+
return 'font/ttf'
|
|
177
|
+
case '.woff':
|
|
178
|
+
return 'font/woff'
|
|
179
|
+
case '.woff2':
|
|
180
|
+
return 'font/woff2'
|
|
93
181
|
default:
|
|
94
|
-
console.warn('? mime type for',
|
|
182
|
+
console.warn('? mime type for', p)
|
|
95
183
|
if (!isProductionBuild()) process.exit(1)
|
|
96
184
|
return 'application/octet-stream'
|
|
97
185
|
}
|