@eighty4/dank 0.0.1-4 → 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 CHANGED
@@ -1,31 +1,35 @@
1
1
  # Build DANK webpages
2
2
 
3
- DANK has some perks:
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 mins
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
- ```shell
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
- npm create dank -- --out-dir www
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
- pnpm create dank --out-dir www
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 API when starting your dev server:
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.ontransitionend = () => {
52
- if (indicator.style.opacity === '1') {
53
- indicator.style.opacity = '0';
54
- }
55
- else {
56
- indicator.remove();
57
- indicator.onload = null;
58
- indicator.ontransitionend = null;
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
- setTimeout(() => (indicator.style.opacity = '1'), 0);
66
+ Promise.all(indicator.getAnimations().map(a => a.finished)).then(() => indicator.remove());
63
67
  }
64
68
  function addJsReloadIndicator() {
65
- document.body.appendChild(createUpdateIndicator('orange', '9000'));
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 module = await import(resolveConfigPath(path))
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}`, `./${string}.html`>
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
@@ -40,6 +40,8 @@ export async function esbuildDevContext(
40
40
  entryPoints: removeEntryPointOutExt(entryPoints),
41
41
  outdir,
42
42
  ...webpageBuildOptions,
43
+ metafile: false,
44
+ write: false,
43
45
  })
44
46
  }
45
47
 
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 as fsJoin } from 'node:path'
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 mimeType = resolveMimeType(url)
48
- res.setHeader('Content-Type', mimeType)
49
- const reading = createReadStream(
50
- mimeType === 'text/html'
51
- ? fsJoin(dir, url.pathname, 'index.html')
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
- export function createLocalProxyFilesFetcher(port: number): FrontendFetcher {
67
- const proxyAddress = 'http://127.0.0.1:' + port
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
- fetch(proxyAddress + url.pathname).then(fetchResponse => {
70
- res.writeHead(
71
- fetchResponse.status,
72
- convertHeadersFromFetch(fetchResponse.headers),
73
- )
74
- fetchResponse.bytes().then(data => res.end(data))
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
- function resolveMimeType(url: URL): string {
80
- switch (extname(url.pathname)) {
81
- case '':
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'
@@ -97,7 +179,7 @@ function resolveMimeType(url: URL): string {
97
179
  case '.woff2':
98
180
  return 'font/woff2'
99
181
  default:
100
- console.warn('? mime type for', url.pathname)
182
+ console.warn('? mime type for', p)
101
183
  if (!isProductionBuild()) process.exit(1)
102
184
  return 'application/octet-stream'
103
185
  }