@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 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
@@ -42,7 +42,7 @@ export async function buildWebsite(c: DankConfig): Promise<DankBuild> {
42
42
  const result = new Set(buildUrls)
43
43
  await writeBuildManifest(buildTag, result)
44
44
  return {
45
- dir: buildDir,
45
+ dir: distDir,
46
46
  files: result,
47
47
  }
48
48
  }
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}.js`
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}.css`
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: inPath.replace(/^pages\//, ''),
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
@@ -28,7 +28,7 @@ export async function buildWebsite(c) {
28
28
  const result = new Set(buildUrls);
29
29
  await writeBuildManifest(buildTag, result);
30
30
  return {
31
- dir: buildDir,
31
+ dir: distDir,
32
32
  files: result,
33
33
  };
34
34
  }
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}.js`;
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}.css`;
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: inPath.replace(/^pages\//, ''),
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-2",
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"