@eighty4/dank 0.0.1-1 → 0.0.1-3
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/client/esbuild.js +77 -0
- package/lib/dank.ts +60 -3
- package/lib/esbuild.ts +4 -1
- package/lib/html.ts +13 -0
- package/lib/serve.ts +10 -4
- package/lib/services.ts +75 -0
- package/lib_js/dank.js +41 -3
- package/lib_js/esbuild.js +4 -1
- package/lib_js/html.js +6 -0
- package/lib_js/serve.js +6 -3
- package/lib_js/services.js +63 -0
- package/lib_types/dank.d.ts +6 -0
- package/package.json +3 -2
|
@@ -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/dank.ts
CHANGED
|
@@ -3,20 +3,77 @@ export type DankConfig = {
|
|
|
3
3
|
// buildTag?: (() => Promise<string> | string) | string
|
|
4
4
|
// mapping url to fs paths of webpages to build
|
|
5
5
|
pages: Record<`/${string}`, `./${string}.html`>
|
|
6
|
+
|
|
7
|
+
services?: Array<DevService>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type DevService = {
|
|
11
|
+
command: string
|
|
12
|
+
cwd?: string
|
|
13
|
+
env?: Record<string, string>
|
|
6
14
|
}
|
|
7
15
|
|
|
8
16
|
export async function defineConfig(
|
|
9
17
|
c: Partial<DankConfig>,
|
|
10
18
|
): Promise<DankConfig> {
|
|
11
|
-
|
|
19
|
+
validatePages(c.pages)
|
|
20
|
+
validateDevServices(c.services)
|
|
21
|
+
return c as DankConfig
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validatePages(pages?: DankConfig['pages']) {
|
|
25
|
+
if (
|
|
26
|
+
pages === null ||
|
|
27
|
+
typeof pages === 'undefined' ||
|
|
28
|
+
Object.keys(pages).length === 0
|
|
29
|
+
) {
|
|
12
30
|
throw Error('DankConfig.pages is required')
|
|
13
31
|
}
|
|
14
|
-
for (const [urlPath, htmlPath] of Object.entries(
|
|
32
|
+
for (const [urlPath, htmlPath] of Object.entries(pages)) {
|
|
15
33
|
if (typeof htmlPath !== 'string' || !htmlPath.endsWith('.html')) {
|
|
16
34
|
throw Error(
|
|
17
35
|
`DankConfig.pages['${urlPath}'] must configure an html file`,
|
|
18
36
|
)
|
|
19
37
|
}
|
|
20
38
|
}
|
|
21
|
-
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validateDevServices(services: DankConfig['services']) {
|
|
42
|
+
if (services === null || typeof services === 'undefined') {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(services)) {
|
|
46
|
+
throw Error(`DankConfig.services must be an array`)
|
|
47
|
+
}
|
|
48
|
+
for (let i = 0; i < services.length; i++) {
|
|
49
|
+
const s = services[i]
|
|
50
|
+
if (s.command === null || typeof s.command === 'undefined') {
|
|
51
|
+
throw Error(`DankConfig.services[${i}].command is required`)
|
|
52
|
+
} else if (typeof s.command !== 'string' || s.command.length === 0) {
|
|
53
|
+
throw Error(
|
|
54
|
+
`DankConfig.services[${i}].command must be a non-empty string`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if (s.cwd !== null && typeof s.cwd !== 'undefined') {
|
|
58
|
+
if (typeof s.cwd !== 'string' || s.cwd.trim().length === 0) {
|
|
59
|
+
throw Error(
|
|
60
|
+
`DankConfig.services[${i}].cwd must be a non-empty string`,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (s.env !== null && typeof s.env !== 'undefined') {
|
|
65
|
+
if (typeof s.env !== 'object') {
|
|
66
|
+
throw Error(
|
|
67
|
+
`DankConfig.services[${i}].env must be an env variable map`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
for (const [k, v] of Object.entries(s.env)) {
|
|
71
|
+
if (typeof v !== 'string') {
|
|
72
|
+
throw Error(
|
|
73
|
+
`DankConfig.services[${i}].env[${k}] must be a string`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
22
79
|
}
|
package/lib/esbuild.ts
CHANGED
|
@@ -32,7 +32,7 @@ export async function esbuildDevContext(
|
|
|
32
32
|
return await esbuild.context({
|
|
33
33
|
define,
|
|
34
34
|
entryNames: '[dir]/[name]',
|
|
35
|
-
entryPoints,
|
|
35
|
+
entryPoints: removeEntryPointOutExt(entryPoints),
|
|
36
36
|
outdir,
|
|
37
37
|
...webpageBuildOptions,
|
|
38
38
|
})
|
|
@@ -54,6 +54,9 @@ export async function esbuildWebpages(
|
|
|
54
54
|
return buildResult.metafile
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// esbuild will append the .js or .css to output filenames
|
|
58
|
+
// keeping extension on entryPoints data for consistency
|
|
59
|
+
// and removing and mapping entryPoints to pass to esbuild
|
|
57
60
|
function removeEntryPointOutExt(
|
|
58
61
|
entryPoints: Array<{ in: string; out: string }>,
|
|
59
62
|
) {
|
package/lib/html.ts
CHANGED
|
@@ -85,6 +85,19 @@ export class HtmlEntrypoint {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
appendScript(clientJS: string) {
|
|
89
|
+
const scriptNode = parseFragment(
|
|
90
|
+
`<script type="module">${clientJS}</script>`,
|
|
91
|
+
).childNodes[0]
|
|
92
|
+
const htmlNode = this.#document.childNodes.find(
|
|
93
|
+
node => node.nodeName === 'html',
|
|
94
|
+
) as ParentNode
|
|
95
|
+
const headNode = htmlNode.childNodes.find(
|
|
96
|
+
node => node.nodeName === 'head',
|
|
97
|
+
) as ParentNode | undefined
|
|
98
|
+
defaultTreeAdapter.appendChild(headNode || htmlNode, scriptNode)
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
async writeTo(buildDir: string): Promise<void> {
|
|
89
102
|
await writeFile(
|
|
90
103
|
join(buildDir, this.#url, 'index.html'),
|
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'
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type FrontendFetcher,
|
|
14
14
|
} from './http.ts'
|
|
15
15
|
import { copyAssets } from './public.ts'
|
|
16
|
+
import { startDevServices } from './services.ts'
|
|
16
17
|
|
|
17
18
|
const isPreview = isPreviewBuild()
|
|
18
19
|
|
|
@@ -37,6 +38,7 @@ export async function serveWebsite(c: DankConfig): Promise<never> {
|
|
|
37
38
|
isPreview ? 'preview' : 'dev server',
|
|
38
39
|
`is live at http://127.0.0.1:${PORT}`,
|
|
39
40
|
)
|
|
41
|
+
startDevServices(c)
|
|
40
42
|
return new Promise(() => {})
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -45,6 +47,11 @@ async function startEsbuildWatch(c: DankConfig): Promise<{ port: number }> {
|
|
|
45
47
|
await mkdir(watchDir, { recursive: true })
|
|
46
48
|
await copyAssets(watchDir)
|
|
47
49
|
|
|
50
|
+
const clientJS = await readFile(
|
|
51
|
+
resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')),
|
|
52
|
+
'utf-8',
|
|
53
|
+
)
|
|
54
|
+
|
|
48
55
|
const entryPointUrls: Set<string> = new Set()
|
|
49
56
|
const entryPoints: Array<{ in: string; out: string }> = []
|
|
50
57
|
|
|
@@ -68,13 +75,12 @@ async function startEsbuildWatch(c: DankConfig): Promise<{ port: number }> {
|
|
|
68
75
|
})
|
|
69
76
|
})
|
|
70
77
|
html.rewriteHrefs()
|
|
78
|
+
html.appendScript(clientJS)
|
|
71
79
|
await html.writeTo(watchDir)
|
|
72
80
|
return html
|
|
73
81
|
}),
|
|
74
82
|
)
|
|
75
83
|
|
|
76
|
-
console.log(entryPoints)
|
|
77
|
-
|
|
78
84
|
const ctx = await esbuildDevContext(
|
|
79
85
|
createGlobalDefinitions(),
|
|
80
86
|
entryPoints,
|
package/lib/services.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { basename, isAbsolute, resolve } from 'node:path'
|
|
3
|
+
import type { DankConfig, DevService } from './dank.ts'
|
|
4
|
+
|
|
5
|
+
export function startDevServices(c: DankConfig) {
|
|
6
|
+
if (c.services?.length) {
|
|
7
|
+
const ac = new AbortController()
|
|
8
|
+
try {
|
|
9
|
+
for (const s of c.services) {
|
|
10
|
+
startService(s, ac.signal)
|
|
11
|
+
}
|
|
12
|
+
} catch (e) {
|
|
13
|
+
ac.abort()
|
|
14
|
+
throw e
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function startService(s: DevService, signal: AbortSignal) {
|
|
20
|
+
const splitCmdAndArgs = s.command.split(/\s+/)
|
|
21
|
+
const cmd = splitCmdAndArgs[0]
|
|
22
|
+
const args = splitCmdAndArgs.length === 1 ? [] : splitCmdAndArgs.slice(1)
|
|
23
|
+
const spawned = spawn(cmd, args, {
|
|
24
|
+
cwd: resolveCwd(s.cwd),
|
|
25
|
+
env: s.env,
|
|
26
|
+
signal,
|
|
27
|
+
detached: false,
|
|
28
|
+
shell: false,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const stdoutLabel = logLabel(s.cwd, cmd, args, 32)
|
|
32
|
+
spawned.stdout.on('data', chunk => printChunk(stdoutLabel, chunk))
|
|
33
|
+
|
|
34
|
+
const stderrLabel = logLabel(s.cwd, cmd, args, 31)
|
|
35
|
+
spawned.stderr.on('data', chunk => printChunk(stderrLabel, chunk))
|
|
36
|
+
|
|
37
|
+
spawned.on('exit', () => {
|
|
38
|
+
console.log(`[${s.command}]`, 'exit')
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printChunk(label: string, c: Buffer) {
|
|
43
|
+
for (const l of parseChunk(c)) console.log(label, l)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseChunk(c: Buffer): Array<string> {
|
|
47
|
+
return c
|
|
48
|
+
.toString()
|
|
49
|
+
.replace(/\r?\n$/, '')
|
|
50
|
+
.split(/\r?\n/)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveCwd(p?: string): string | undefined {
|
|
54
|
+
if (!p || isAbsolute(p)) {
|
|
55
|
+
return p
|
|
56
|
+
} else {
|
|
57
|
+
return resolve(process.cwd(), p)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function logLabel(
|
|
62
|
+
cwd: string | undefined,
|
|
63
|
+
cmd: string,
|
|
64
|
+
args: Array<string>,
|
|
65
|
+
ansiColor: number,
|
|
66
|
+
): string {
|
|
67
|
+
cwd = !cwd
|
|
68
|
+
? './'
|
|
69
|
+
: cwd.startsWith('/')
|
|
70
|
+
? `/.../${basename(cwd)}`
|
|
71
|
+
: cwd.startsWith('.')
|
|
72
|
+
? cwd
|
|
73
|
+
: `./${cwd}`
|
|
74
|
+
return `\u001b[${ansiColor}m[\u001b[1m${cmd}\u001b[22m ${args.join(' ')} \u001b[2;3m${cwd}\u001b[22;23m]\u001b[0m`
|
|
75
|
+
}
|
package/lib_js/dank.js
CHANGED
|
@@ -1,11 +1,49 @@
|
|
|
1
1
|
export async function defineConfig(c) {
|
|
2
|
-
|
|
2
|
+
validatePages(c.pages);
|
|
3
|
+
validateDevServices(c.services);
|
|
4
|
+
return c;
|
|
5
|
+
}
|
|
6
|
+
function validatePages(pages) {
|
|
7
|
+
if (pages === null ||
|
|
8
|
+
typeof pages === 'undefined' ||
|
|
9
|
+
Object.keys(pages).length === 0) {
|
|
3
10
|
throw Error('DankConfig.pages is required');
|
|
4
11
|
}
|
|
5
|
-
for (const [urlPath, htmlPath] of Object.entries(
|
|
12
|
+
for (const [urlPath, htmlPath] of Object.entries(pages)) {
|
|
6
13
|
if (typeof htmlPath !== 'string' || !htmlPath.endsWith('.html')) {
|
|
7
14
|
throw Error(`DankConfig.pages['${urlPath}'] must configure an html file`);
|
|
8
15
|
}
|
|
9
16
|
}
|
|
10
|
-
|
|
17
|
+
}
|
|
18
|
+
function validateDevServices(services) {
|
|
19
|
+
if (services === null || typeof services === 'undefined') {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!Array.isArray(services)) {
|
|
23
|
+
throw Error(`DankConfig.services must be an array`);
|
|
24
|
+
}
|
|
25
|
+
for (let i = 0; i < services.length; i++) {
|
|
26
|
+
const s = services[i];
|
|
27
|
+
if (s.command === null || typeof s.command === 'undefined') {
|
|
28
|
+
throw Error(`DankConfig.services[${i}].command is required`);
|
|
29
|
+
}
|
|
30
|
+
else if (typeof s.command !== 'string' || s.command.length === 0) {
|
|
31
|
+
throw Error(`DankConfig.services[${i}].command must be a non-empty string`);
|
|
32
|
+
}
|
|
33
|
+
if (s.cwd !== null && typeof s.cwd !== 'undefined') {
|
|
34
|
+
if (typeof s.cwd !== 'string' || s.cwd.trim().length === 0) {
|
|
35
|
+
throw Error(`DankConfig.services[${i}].cwd must be a non-empty string`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (s.env !== null && typeof s.env !== 'undefined') {
|
|
39
|
+
if (typeof s.env !== 'object') {
|
|
40
|
+
throw Error(`DankConfig.services[${i}].env must be an env variable map`);
|
|
41
|
+
}
|
|
42
|
+
for (const [k, v] of Object.entries(s.env)) {
|
|
43
|
+
if (typeof v !== 'string') {
|
|
44
|
+
throw Error(`DankConfig.services[${i}].env[${k}] must be a string`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
11
49
|
}
|
package/lib_js/esbuild.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function esbuildDevContext(define, entryPoints, outdir) {
|
|
|
18
18
|
return await esbuild.context({
|
|
19
19
|
define,
|
|
20
20
|
entryNames: '[dir]/[name]',
|
|
21
|
-
entryPoints,
|
|
21
|
+
entryPoints: removeEntryPointOutExt(entryPoints),
|
|
22
22
|
outdir,
|
|
23
23
|
...webpageBuildOptions,
|
|
24
24
|
});
|
|
@@ -34,6 +34,9 @@ export async function esbuildWebpages(define, entryPoints, outdir) {
|
|
|
34
34
|
esbuildResultChecks(buildResult);
|
|
35
35
|
return buildResult.metafile;
|
|
36
36
|
}
|
|
37
|
+
// esbuild will append the .js or .css to output filenames
|
|
38
|
+
// keeping extension on entryPoints data for consistency
|
|
39
|
+
// and removing and mapping entryPoints to pass to esbuild
|
|
37
40
|
function removeEntryPointOutExt(entryPoints) {
|
|
38
41
|
return entryPoints.map(entryPoint => {
|
|
39
42
|
return {
|
package/lib_js/html.js
CHANGED
|
@@ -52,6 +52,12 @@ export class HtmlEntrypoint {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
appendScript(clientJS) {
|
|
56
|
+
const scriptNode = parseFragment(`<script type="module">${clientJS}</script>`).childNodes[0];
|
|
57
|
+
const htmlNode = this.#document.childNodes.find(node => node.nodeName === 'html');
|
|
58
|
+
const headNode = htmlNode.childNodes.find(node => node.nodeName === 'head');
|
|
59
|
+
defaultTreeAdapter.appendChild(headNode || htmlNode, scriptNode);
|
|
60
|
+
}
|
|
55
61
|
async writeTo(buildDir) {
|
|
56
62
|
await writeFile(join(buildDir, this.#url, 'index.html'), serialize(this.#document));
|
|
57
63
|
}
|
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";
|
|
@@ -7,6 +7,7 @@ import { isPreviewBuild } from "./flags.js";
|
|
|
7
7
|
import { HtmlEntrypoint } from "./html.js";
|
|
8
8
|
import { createBuiltDistFilesFetcher, createLocalProxyFilesFetcher, createWebServer, } from "./http.js";
|
|
9
9
|
import { copyAssets } from "./public.js";
|
|
10
|
+
import { startDevServices } from "./services.js";
|
|
10
11
|
const isPreview = isPreviewBuild();
|
|
11
12
|
// alternate port for --preview bc of service worker
|
|
12
13
|
const PORT = isPreview ? 4000 : 3000;
|
|
@@ -25,12 +26,14 @@ export async function serveWebsite(c) {
|
|
|
25
26
|
}
|
|
26
27
|
createWebServer(PORT, frontend).listen(PORT);
|
|
27
28
|
console.log(isPreview ? 'preview' : 'dev server', `is live at http://127.0.0.1:${PORT}`);
|
|
29
|
+
startDevServices(c);
|
|
28
30
|
return new Promise(() => { });
|
|
29
31
|
}
|
|
30
32
|
async function startEsbuildWatch(c) {
|
|
31
33
|
const watchDir = join('build', 'watch');
|
|
32
34
|
await mkdir(watchDir, { recursive: true });
|
|
33
35
|
await copyAssets(watchDir);
|
|
36
|
+
const clientJS = await readFile(resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')), 'utf-8');
|
|
34
37
|
const entryPointUrls = new Set();
|
|
35
38
|
const entryPoints = [];
|
|
36
39
|
await Promise.all(Object.entries(c.pages).map(async ([url, srcPath]) => {
|
|
@@ -49,10 +52,10 @@ async function startEsbuildWatch(c) {
|
|
|
49
52
|
});
|
|
50
53
|
});
|
|
51
54
|
html.rewriteHrefs();
|
|
55
|
+
html.appendScript(clientJS);
|
|
52
56
|
await html.writeTo(watchDir);
|
|
53
57
|
return html;
|
|
54
58
|
}));
|
|
55
|
-
console.log(entryPoints);
|
|
56
59
|
const ctx = await esbuildDevContext(createGlobalDefinitions(), entryPoints, watchDir);
|
|
57
60
|
await ctx.watch();
|
|
58
61
|
await ctx.serve({
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { basename, isAbsolute, resolve } from 'node:path';
|
|
3
|
+
export function startDevServices(c) {
|
|
4
|
+
if (c.services?.length) {
|
|
5
|
+
const ac = new AbortController();
|
|
6
|
+
try {
|
|
7
|
+
for (const s of c.services) {
|
|
8
|
+
startService(s, ac.signal);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
ac.abort();
|
|
13
|
+
throw e;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function startService(s, signal) {
|
|
18
|
+
const splitCmdAndArgs = s.command.split(/\s+/);
|
|
19
|
+
const cmd = splitCmdAndArgs[0];
|
|
20
|
+
const args = splitCmdAndArgs.length === 1 ? [] : splitCmdAndArgs.slice(1);
|
|
21
|
+
const spawned = spawn(cmd, args, {
|
|
22
|
+
cwd: resolveCwd(s.cwd),
|
|
23
|
+
env: s.env,
|
|
24
|
+
signal,
|
|
25
|
+
detached: false,
|
|
26
|
+
shell: false,
|
|
27
|
+
});
|
|
28
|
+
const stdoutLabel = logLabel(s.cwd, cmd, args, 32);
|
|
29
|
+
spawned.stdout.on('data', chunk => printChunk(stdoutLabel, chunk));
|
|
30
|
+
const stderrLabel = logLabel(s.cwd, cmd, args, 31);
|
|
31
|
+
spawned.stderr.on('data', chunk => printChunk(stderrLabel, chunk));
|
|
32
|
+
spawned.on('exit', () => {
|
|
33
|
+
console.log(`[${s.command}]`, 'exit');
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function printChunk(label, c) {
|
|
37
|
+
for (const l of parseChunk(c))
|
|
38
|
+
console.log(label, l);
|
|
39
|
+
}
|
|
40
|
+
function parseChunk(c) {
|
|
41
|
+
return c
|
|
42
|
+
.toString()
|
|
43
|
+
.replace(/\r?\n$/, '')
|
|
44
|
+
.split(/\r?\n/);
|
|
45
|
+
}
|
|
46
|
+
function resolveCwd(p) {
|
|
47
|
+
if (!p || isAbsolute(p)) {
|
|
48
|
+
return p;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return resolve(process.cwd(), p);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function logLabel(cwd, cmd, args, ansiColor) {
|
|
55
|
+
cwd = !cwd
|
|
56
|
+
? './'
|
|
57
|
+
: cwd.startsWith('/')
|
|
58
|
+
? `/.../${basename(cwd)}`
|
|
59
|
+
: cwd.startsWith('.')
|
|
60
|
+
? cwd
|
|
61
|
+
: `./${cwd}`;
|
|
62
|
+
return `\u001b[${ansiColor}m[\u001b[1m${cmd}\u001b[22m ${args.join(' ')} \u001b[2;3m${cwd}\u001b[22;23m]\u001b[0m`;
|
|
63
|
+
}
|
package/lib_types/dank.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export type DankConfig = {
|
|
2
2
|
pages: Record<`/${string}`, `./${string}.html`>;
|
|
3
|
+
services?: Array<DevService>;
|
|
4
|
+
};
|
|
5
|
+
export type DevService = {
|
|
6
|
+
command: string;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
env?: Record<string, string>;
|
|
3
9
|
};
|
|
4
10
|
export declare function defineConfig(c: Partial<DankConfig>): Promise<DankConfig>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eighty4/dank",
|
|
3
|
-
"version": "0.0.1-
|
|
3
|
+
"version": "0.0.1-3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": "./lib_js/bin.js",
|
|
6
6
|
"exports": {
|
|
@@ -20,12 +20,13 @@
|
|
|
20
20
|
"typescript": "^5.9.2"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
|
+
"client/esbuild.js",
|
|
23
24
|
"lib/*.ts",
|
|
24
25
|
"lib_js/*.js",
|
|
25
26
|
"lib_types/*.d.ts"
|
|
26
27
|
],
|
|
27
28
|
"scripts": {
|
|
28
|
-
"build": "tsc && tsc -p tsconfig.exports.json",
|
|
29
|
+
"build": "tsc && tsc -p tsconfig.client.json && tsc -p tsconfig.exports.json",
|
|
29
30
|
"fmt": "prettier --write .",
|
|
30
31
|
"fmtcheck": "prettier --check .",
|
|
31
32
|
"typecheck": "tsc --noEmit"
|