@eighty4/dank 0.0.1-0 → 0.0.1-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/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
- if (typeof c.pages === 'undefined' || Object.keys(c.pages).length === 0) {
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(c.pages)) {
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
- return c as DankConfig
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/serve.ts CHANGED
@@ -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
 
@@ -73,8 +75,6 @@ async function startEsbuildWatch(c: DankConfig): Promise<{ port: number }> {
73
75
  }),
74
76
  )
75
77
 
76
- console.log(entryPoints)
77
-
78
78
  const ctx = await esbuildDevContext(
79
79
  createGlobalDefinitions(),
80
80
  entryPoints,
@@ -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
- if (typeof c.pages === 'undefined' || Object.keys(c.pages).length === 0) {
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(c.pages)) {
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
- return c;
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/serve.js CHANGED
@@ -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,6 +26,7 @@ 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) {
@@ -52,7 +54,6 @@ async function startEsbuildWatch(c) {
52
54
  await html.writeTo(watchDir);
53
55
  return html;
54
56
  }));
55
- console.log(entryPoints);
56
57
  const ctx = await esbuildDevContext(createGlobalDefinitions(), entryPoints, watchDir);
57
58
  await ctx.watch();
58
59
  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
+ }
@@ -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-0",
3
+ "version": "0.0.1-2",
4
4
  "type": "module",
5
5
  "bin": "./lib_js/bin.js",
6
6
  "exports": {