@eighty4/dank 0.0.1-2 → 0.0.1-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/README.md +61 -0
- package/client/esbuild.js +77 -0
- package/lib/build.ts +1 -1
- package/lib/esbuild.ts +9 -1
- package/lib/html.ts +24 -3
- package/lib/http.ts +6 -0
- package/lib/serve.ts +8 -2
- package/lib_js/build.js +1 -1
- package/lib_js/esbuild.js +9 -1
- package/lib_js/html.js +14 -3
- package/lib_js/http.js +6 -0
- package/lib_js/serve.js +4 -2
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Build DANK webpages
|
|
2
|
+
|
|
3
|
+
DANK has some perks:
|
|
4
|
+
|
|
5
|
+
- Webpage-first development for multi-page websites
|
|
6
|
+
- Code splitting via `esbuild` bundler across all webpages
|
|
7
|
+
- `dank serve` updates CSS in real-time (hot-reloading)
|
|
8
|
+
- `dank serve` launches development processes and merges their stdio
|
|
9
|
+
- `dank serve --preview` builds the website and serves the output from `dist`
|
|
10
|
+
- `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
|
|
12
|
+
|
|
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
|
|
21
|
+
|
|
22
|
+
```shell
|
|
23
|
+
bun create dank --out-dir www
|
|
24
|
+
|
|
25
|
+
npm create dank -- --out-dir www
|
|
26
|
+
|
|
27
|
+
pnpm create dank --out-dir www
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## `dank.config.ts` examples
|
|
31
|
+
|
|
32
|
+
Webpages and their URLs are configured explicitly to keep your URLs
|
|
33
|
+
and workspace organized independently:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { defineConfig } from '@eighty4/dank'
|
|
37
|
+
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
pages: {
|
|
40
|
+
'/': './home.html',
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Streamline development with `dank serve` launching API when starting your dev server:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { defineConfig } from '@eighty4/dank'
|
|
49
|
+
|
|
50
|
+
export default defineConfig({
|
|
51
|
+
pages: {
|
|
52
|
+
'/': './home.html',
|
|
53
|
+
},
|
|
54
|
+
services: [
|
|
55
|
+
{
|
|
56
|
+
command: 'node --watch --env-file-if-exists=.env.dev server.ts',
|
|
57
|
+
cwd: './api',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
```
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
new EventSource('http://127.0.0.1:2999/esbuild').addEventListener('change', (e) => {
|
|
2
|
+
const change = JSON.parse(e.data);
|
|
3
|
+
const cssUpdates = change.updated.filter(p => p.endsWith('.css'));
|
|
4
|
+
if (cssUpdates.length) {
|
|
5
|
+
console.log('esbuild css updates', cssUpdates);
|
|
6
|
+
const cssLinks = {};
|
|
7
|
+
for (const elem of document.getElementsByTagName('link')) {
|
|
8
|
+
if (elem.getAttribute('rel') === 'stylesheet') {
|
|
9
|
+
const url = new URL(elem.href);
|
|
10
|
+
if ((url.host = location.host)) {
|
|
11
|
+
cssLinks[url.pathname] = elem;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
let swappedCss = false;
|
|
16
|
+
for (const cssUpdate of cssUpdates) {
|
|
17
|
+
const cssLink = cssLinks[cssUpdate];
|
|
18
|
+
if (cssLink) {
|
|
19
|
+
const next = cssLink.cloneNode();
|
|
20
|
+
next.href = `${cssUpdate}?${Math.random().toString(36).slice(2)}`;
|
|
21
|
+
next.onload = () => cssLink.remove();
|
|
22
|
+
cssLink.parentNode.insertBefore(next, cssLink.nextSibling);
|
|
23
|
+
swappedCss = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (swappedCss) {
|
|
27
|
+
addCssUpdateIndicator();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (cssUpdates.length < change.updated.length) {
|
|
31
|
+
const jsUpdates = change.updated.filter(p => !p.endsWith('.css'));
|
|
32
|
+
const jsScripts = new Set();
|
|
33
|
+
for (const elem of document.getElementsByTagName('script')) {
|
|
34
|
+
if (elem.src.length) {
|
|
35
|
+
const url = new URL(elem.src);
|
|
36
|
+
if ((url.host = location.host)) {
|
|
37
|
+
jsScripts.add(url.pathname);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (jsUpdates.some(jsUpdate => jsScripts.has(jsUpdate))) {
|
|
42
|
+
console.log('esbuild js updates require reload');
|
|
43
|
+
addJsReloadIndicator();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
function addCssUpdateIndicator() {
|
|
48
|
+
const indicator = createUpdateIndicator('green', '9999');
|
|
49
|
+
indicator.style.transition = 'opacity ease-in-out .38s';
|
|
50
|
+
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
|
+
};
|
|
61
|
+
document.body.appendChild(indicator);
|
|
62
|
+
setTimeout(() => (indicator.style.opacity = '1'), 0);
|
|
63
|
+
}
|
|
64
|
+
function addJsReloadIndicator() {
|
|
65
|
+
document.body.appendChild(createUpdateIndicator('orange', '9000'));
|
|
66
|
+
}
|
|
67
|
+
function createUpdateIndicator(color, zIndex) {
|
|
68
|
+
const indicator = document.createElement('div');
|
|
69
|
+
indicator.style.border = '6px dashed ' + color;
|
|
70
|
+
indicator.style.zIndex = zIndex;
|
|
71
|
+
indicator.style.position = 'fixed';
|
|
72
|
+
indicator.style.top = indicator.style.left = '1px';
|
|
73
|
+
indicator.style.height = indicator.style.width = 'calc(100% - 2px)';
|
|
74
|
+
indicator.style.boxSizing = 'border-box';
|
|
75
|
+
return indicator;
|
|
76
|
+
}
|
|
77
|
+
export {};
|
package/lib/build.ts
CHANGED
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
|
|
|
@@ -32,7 +37,7 @@ export async function esbuildDevContext(
|
|
|
32
37
|
return await esbuild.context({
|
|
33
38
|
define,
|
|
34
39
|
entryNames: '[dir]/[name]',
|
|
35
|
-
entryPoints,
|
|
40
|
+
entryPoints: removeEntryPointOutExt(entryPoints),
|
|
36
41
|
outdir,
|
|
37
42
|
...webpageBuildOptions,
|
|
38
43
|
})
|
|
@@ -54,6 +59,9 @@ export async function esbuildWebpages(
|
|
|
54
59
|
return buildResult.metafile
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
// esbuild will append the .js or .css to output filenames
|
|
63
|
+
// keeping extension on entryPoints data for consistency
|
|
64
|
+
// and removing and mapping entryPoints to pass to esbuild
|
|
57
65
|
function removeEntryPointOutExt(
|
|
58
66
|
entryPoints: Array<{ in: string; out: string }>,
|
|
59
67
|
) {
|
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,16 +76,29 @@ 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
|
}
|
|
87
88
|
|
|
89
|
+
appendScript(clientJS: string) {
|
|
90
|
+
const scriptNode = parseFragment(
|
|
91
|
+
`<script type="module">${clientJS}</script>`,
|
|
92
|
+
).childNodes[0]
|
|
93
|
+
const htmlNode = this.#document.childNodes.find(
|
|
94
|
+
node => node.nodeName === 'html',
|
|
95
|
+
) as ParentNode
|
|
96
|
+
const headNode = htmlNode.childNodes.find(
|
|
97
|
+
node => node.nodeName === 'head',
|
|
98
|
+
) as ParentNode | undefined
|
|
99
|
+
defaultTreeAdapter.appendChild(headNode || htmlNode, scriptNode)
|
|
100
|
+
}
|
|
101
|
+
|
|
88
102
|
async writeTo(buildDir: string): Promise<void> {
|
|
89
103
|
await writeFile(
|
|
90
104
|
join(buildDir, this.#url, 'index.html'),
|
|
@@ -169,12 +183,19 @@ export class HtmlEntrypoint {
|
|
|
169
183
|
|
|
170
184
|
#addScript(type: ImportedScript['type'], href: string, elem: Element) {
|
|
171
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
|
+
}
|
|
172
193
|
this.#scripts.push({
|
|
173
194
|
type,
|
|
174
195
|
href,
|
|
175
196
|
elem,
|
|
176
197
|
in: inPath,
|
|
177
|
-
out:
|
|
198
|
+
out: outPath,
|
|
178
199
|
})
|
|
179
200
|
}
|
|
180
201
|
}
|
package/lib/http.ts
CHANGED
|
@@ -90,6 +90,12 @@ function resolveMimeType(url: URL): string {
|
|
|
90
90
|
return 'image/svg+xml'
|
|
91
91
|
case '.png':
|
|
92
92
|
return 'image/png'
|
|
93
|
+
case '.ttf':
|
|
94
|
+
return 'font/ttf'
|
|
95
|
+
case '.woff':
|
|
96
|
+
return 'font/woff'
|
|
97
|
+
case '.woff2':
|
|
98
|
+
return 'font/woff2'
|
|
93
99
|
default:
|
|
94
100
|
console.warn('? mime type for', url.pathname)
|
|
95
101
|
if (!isProductionBuild()) process.exit(1)
|
package/lib/serve.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdir, rm } from 'node:fs/promises'
|
|
2
|
-
import { join } from 'node:path'
|
|
1
|
+
import { mkdir, readFile, rm } from 'node:fs/promises'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
3
|
import { buildWebsite } from './build.ts'
|
|
4
4
|
import type { DankConfig } from './dank.ts'
|
|
5
5
|
import { createGlobalDefinitions } from './define.ts'
|
|
@@ -47,6 +47,11 @@ async function startEsbuildWatch(c: DankConfig): Promise<{ port: number }> {
|
|
|
47
47
|
await mkdir(watchDir, { recursive: true })
|
|
48
48
|
await copyAssets(watchDir)
|
|
49
49
|
|
|
50
|
+
const clientJS = await readFile(
|
|
51
|
+
resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')),
|
|
52
|
+
'utf-8',
|
|
53
|
+
)
|
|
54
|
+
|
|
50
55
|
const entryPointUrls: Set<string> = new Set()
|
|
51
56
|
const entryPoints: Array<{ in: string; out: string }> = []
|
|
52
57
|
|
|
@@ -70,6 +75,7 @@ async function startEsbuildWatch(c: DankConfig): Promise<{ port: number }> {
|
|
|
70
75
|
})
|
|
71
76
|
})
|
|
72
77
|
html.rewriteHrefs()
|
|
78
|
+
html.appendScript(clientJS)
|
|
73
79
|
await html.writeTo(watchDir)
|
|
74
80
|
return html
|
|
75
81
|
}),
|
package/lib_js/build.js
CHANGED
package/lib_js/esbuild.js
CHANGED
|
@@ -12,13 +12,18 @@ const jsBuildOptions = {
|
|
|
12
12
|
const webpageBuildOptions = {
|
|
13
13
|
assetNames: 'assets/[name]-[hash]',
|
|
14
14
|
format: 'esm',
|
|
15
|
+
loader: {
|
|
16
|
+
'.tff': 'file',
|
|
17
|
+
'.woff': 'file',
|
|
18
|
+
'.woff2': 'file',
|
|
19
|
+
},
|
|
15
20
|
...jsBuildOptions,
|
|
16
21
|
};
|
|
17
22
|
export async function esbuildDevContext(define, entryPoints, outdir) {
|
|
18
23
|
return await esbuild.context({
|
|
19
24
|
define,
|
|
20
25
|
entryNames: '[dir]/[name]',
|
|
21
|
-
entryPoints,
|
|
26
|
+
entryPoints: removeEntryPointOutExt(entryPoints),
|
|
22
27
|
outdir,
|
|
23
28
|
...webpageBuildOptions,
|
|
24
29
|
});
|
|
@@ -34,6 +39,9 @@ export async function esbuildWebpages(define, entryPoints, outdir) {
|
|
|
34
39
|
esbuildResultChecks(buildResult);
|
|
35
40
|
return buildResult.metafile;
|
|
36
41
|
}
|
|
42
|
+
// esbuild will append the .js or .css to output filenames
|
|
43
|
+
// keeping extension on entryPoints data for consistency
|
|
44
|
+
// and removing and mapping entryPoints to pass to esbuild
|
|
37
45
|
function removeEntryPointOutExt(entryPoints) {
|
|
38
46
|
return entryPoints.map(entryPoint => {
|
|
39
47
|
return {
|
package/lib_js/html.js
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 { defaultTreeAdapter, parse, parseFragment, serialize, } from 'parse5';
|
|
4
5
|
// unenforced but necessary sequence:
|
|
5
6
|
// injectPartials
|
|
@@ -44,14 +45,20 @@ export class HtmlEntrypoint {
|
|
|
44
45
|
if (importScript.type === 'script') {
|
|
45
46
|
if (importScript.in.endsWith('.tsx') ||
|
|
46
47
|
importScript.in.endsWith('.ts')) {
|
|
47
|
-
importScript.elem.attrs.find(attr => attr.name === 'src').value = rewriteTo || `/${importScript.out}
|
|
48
|
+
importScript.elem.attrs.find(attr => attr.name === 'src').value = rewriteTo || `/${importScript.out}`;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
else if (importScript.type === 'style') {
|
|
51
|
-
importScript.elem.attrs.find(attr => attr.name === 'href').value = rewriteTo || `/${importScript.out}
|
|
52
|
+
importScript.elem.attrs.find(attr => attr.name === 'href').value = rewriteTo || `/${importScript.out}`;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
}
|
|
56
|
+
appendScript(clientJS) {
|
|
57
|
+
const scriptNode = parseFragment(`<script type="module">${clientJS}</script>`).childNodes[0];
|
|
58
|
+
const htmlNode = this.#document.childNodes.find(node => node.nodeName === 'html');
|
|
59
|
+
const headNode = htmlNode.childNodes.find(node => node.nodeName === 'head');
|
|
60
|
+
defaultTreeAdapter.appendChild(headNode || htmlNode, scriptNode);
|
|
61
|
+
}
|
|
55
62
|
async writeTo(buildDir) {
|
|
56
63
|
await writeFile(join(buildDir, this.#url, 'index.html'), serialize(this.#document));
|
|
57
64
|
}
|
|
@@ -115,12 +122,16 @@ export class HtmlEntrypoint {
|
|
|
115
122
|
}
|
|
116
123
|
#addScript(type, href, elem) {
|
|
117
124
|
const inPath = join(dirname(this.#fsPath), href);
|
|
125
|
+
let outPath = inPath.replace(/^pages\//, '');
|
|
126
|
+
if (type === 'script' && !outPath.endsWith('.js')) {
|
|
127
|
+
outPath = outPath.replace(new RegExp(extname(outPath).substring(1) + '$'), 'js');
|
|
128
|
+
}
|
|
118
129
|
this.#scripts.push({
|
|
119
130
|
type,
|
|
120
131
|
href,
|
|
121
132
|
elem,
|
|
122
133
|
in: inPath,
|
|
123
|
-
out:
|
|
134
|
+
out: outPath,
|
|
124
135
|
});
|
|
125
136
|
}
|
|
126
137
|
}
|
package/lib_js/http.js
CHANGED
|
@@ -64,6 +64,12 @@ function resolveMimeType(url) {
|
|
|
64
64
|
return 'image/svg+xml';
|
|
65
65
|
case '.png':
|
|
66
66
|
return 'image/png';
|
|
67
|
+
case '.ttf':
|
|
68
|
+
return 'font/ttf';
|
|
69
|
+
case '.woff':
|
|
70
|
+
return 'font/woff';
|
|
71
|
+
case '.woff2':
|
|
72
|
+
return 'font/woff2';
|
|
67
73
|
default:
|
|
68
74
|
console.warn('? mime type for', url.pathname);
|
|
69
75
|
if (!isProductionBuild())
|
package/lib_js/serve.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdir, rm } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
3
|
import { buildWebsite } from "./build.js";
|
|
4
4
|
import { createGlobalDefinitions } from "./define.js";
|
|
5
5
|
import { esbuildDevContext } from "./esbuild.js";
|
|
@@ -33,6 +33,7 @@ async function startEsbuildWatch(c) {
|
|
|
33
33
|
const watchDir = join('build', 'watch');
|
|
34
34
|
await mkdir(watchDir, { recursive: true });
|
|
35
35
|
await copyAssets(watchDir);
|
|
36
|
+
const clientJS = await readFile(resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')), 'utf-8');
|
|
36
37
|
const entryPointUrls = new Set();
|
|
37
38
|
const entryPoints = [];
|
|
38
39
|
await Promise.all(Object.entries(c.pages).map(async ([url, srcPath]) => {
|
|
@@ -51,6 +52,7 @@ async function startEsbuildWatch(c) {
|
|
|
51
52
|
});
|
|
52
53
|
});
|
|
53
54
|
html.rewriteHrefs();
|
|
55
|
+
html.appendScript(clientJS);
|
|
54
56
|
await html.writeTo(watchDir);
|
|
55
57
|
return html;
|
|
56
58
|
}));
|
package/package.json
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eighty4/dank",
|
|
3
|
-
"version": "0.0.1-
|
|
3
|
+
"version": "0.0.1-4",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"description": "Multi-page development system for CDN-deployed websites",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"frontend",
|
|
8
|
+
"bundler",
|
|
9
|
+
"esbuild",
|
|
10
|
+
"dank"
|
|
11
|
+
],
|
|
5
12
|
"bin": "./lib_js/bin.js",
|
|
6
13
|
"exports": {
|
|
7
14
|
".": {
|
|
@@ -20,12 +27,13 @@
|
|
|
20
27
|
"typescript": "^5.9.2"
|
|
21
28
|
},
|
|
22
29
|
"files": [
|
|
30
|
+
"client/esbuild.js",
|
|
23
31
|
"lib/*.ts",
|
|
24
32
|
"lib_js/*.js",
|
|
25
33
|
"lib_types/*.d.ts"
|
|
26
34
|
],
|
|
27
35
|
"scripts": {
|
|
28
|
-
"build": "tsc && tsc -p tsconfig.exports.json",
|
|
36
|
+
"build": "tsc && tsc -p tsconfig.client.json && tsc -p tsconfig.exports.json",
|
|
29
37
|
"fmt": "prettier --write .",
|
|
30
38
|
"fmtcheck": "prettier --check .",
|
|
31
39
|
"typecheck": "tsc --noEmit"
|