@eighty4/dank 0.0.1-4 → 0.0.2
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 +20 -16
- package/client/esbuild.js +26 -15
- package/lib/config.ts +2 -1
- package/lib/dank.ts +8 -1
- package/lib/esbuild.ts +2 -0
- package/lib/http.ts +104 -50
- package/lib/serve.ts +255 -51
- package/lib/services.ts +156 -11
- package/lib_js/config.js +2 -1
- package/lib_js/dank.js +6 -0
- package/lib_js/esbuild.js +2 -0
- package/lib_js/http.js +83 -44
- 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 +2 -1
package/README.md
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
# Build DANK webpages
|
|
2
2
|
|
|
3
|
-
|
|
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:
|
|
4
14
|
|
|
5
15
|
- Webpage-first development for multi-page websites
|
|
16
|
+
- TypeScript supported with `<script src="./dank.ts">`
|
|
6
17
|
- Code splitting via `esbuild` bundler across all webpages
|
|
18
|
+
- Hashes added to all bundled assets for efficient cache utilization
|
|
7
19
|
- `dank serve` updates CSS in real-time (hot-reloading)
|
|
8
20
|
- `dank serve` launches development processes and merges their stdio
|
|
9
21
|
- `dank serve --preview` builds the website and serves the output from `dist`
|
|
10
22
|
- `dank build --production` optimizes with `esbuild` minifying and tree-shaking
|
|
11
|
-
- DANK's codebase is so tiny you can read it all in 20
|
|
23
|
+
- DANK's codebase is so tiny you can read it all in 20 minutes
|
|
12
24
|
|
|
13
|
-
DANK isn't for every use case
|
|
14
|
-
[Vite](https://vite.dev) is the right move for building a Single-Page Application.
|
|
15
|
-
For SEO optimizing dynamic content with Server-Side Rendering,
|
|
16
|
-
check out [Next.js](https://nextjs.org) or [Astro](https://astro.build).
|
|
17
|
-
|
|
18
|
-
DANK is an ideal choice for building multi-page websites deployed to a CDN.
|
|
19
|
-
|
|
20
|
-
## Getting started
|
|
25
|
+
### DANK isn't for every use case!
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
bun create dank --out-dir www
|
|
27
|
+
[Vite](https://vite.dev) is the right move for building a Single-Page Application.
|
|
24
28
|
|
|
25
|
-
|
|
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).
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
```
|
|
32
|
+
#### DANK is an ideal choice for multi-page websites deployed to a CDN that integrate with serverless components and APIs.
|
|
29
33
|
|
|
30
34
|
## `dank.config.ts` examples
|
|
31
35
|
|
|
@@ -42,7 +46,7 @@ export default defineConfig({
|
|
|
42
46
|
})
|
|
43
47
|
```
|
|
44
48
|
|
|
45
|
-
Streamline development with `dank serve` launching
|
|
49
|
+
Streamline development with `dank serve` launching APIs and databases when starting your website's dev server:
|
|
46
50
|
|
|
47
51
|
```typescript
|
|
48
52
|
import { defineConfig } from '@eighty4/dank'
|
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/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
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,8 +7,8 @@ import {
|
|
|
6
7
|
type OutgoingHttpHeaders,
|
|
7
8
|
type ServerResponse,
|
|
8
9
|
} from 'node:http'
|
|
9
|
-
import { extname, join
|
|
10
|
-
import
|
|
10
|
+
import { extname, join } from 'node:path'
|
|
11
|
+
import mime from 'mime'
|
|
11
12
|
|
|
12
13
|
export type FrontendFetcher = (
|
|
13
14
|
url: URL,
|
|
@@ -44,65 +45,118 @@ 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
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
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
|
+
}
|
|
76
129
|
}
|
|
77
130
|
}
|
|
78
131
|
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
case '.ttf':
|
|
94
|
-
return 'font/ttf'
|
|
95
|
-
case '.woff':
|
|
96
|
-
return 'font/woff'
|
|
97
|
-
case '.woff2':
|
|
98
|
-
return 'font/woff2'
|
|
99
|
-
default:
|
|
100
|
-
console.warn('? mime type for', url.pathname)
|
|
101
|
-
if (!isProductionBuild()) process.exit(1)
|
|
102
|
-
return 'application/octet-stream'
|
|
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
|
|
103
146
|
}
|
|
104
147
|
}
|
|
105
148
|
|
|
149
|
+
function streamFile(p: string, res: ServerResponse) {
|
|
150
|
+
res.setHeader('Content-Type', mime.getType(p) || 'application/octet-stream')
|
|
151
|
+
const reading = createReadStream(p)
|
|
152
|
+
reading.pipe(res)
|
|
153
|
+
reading.on('error', err => {
|
|
154
|
+
console.error(`file read ${reading.path} error ${err.message}`)
|
|
155
|
+
res.statusCode = 500
|
|
156
|
+
res.end()
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
106
160
|
function convertHeadersFromFetch(from: Headers): OutgoingHttpHeaders {
|
|
107
161
|
const to: OutgoingHttpHeaders = {}
|
|
108
162
|
for (const name of from.keys()) {
|