@eighty4/dank 0.0.5-1 → 0.0.5-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_js/serve.js CHANGED
@@ -6,46 +6,51 @@ import { createGlobalDefinitions } from "./define.js";
6
6
  import { esbuildDevContext } from "./esbuild.js";
7
7
  import { createBuiltDistFilesFetcher, createDevServeFilesFetcher, startWebServer } from "./http.js";
8
8
  import { WebsiteRegistry } from "./registry.js";
9
- import { startDevServices, updateDevServices } from "./services.js";
9
+ import { DevServices } from "./services.js";
10
10
  import { watch } from "./watch.js";
11
11
  let c;
12
12
  async function serveWebsite() {
13
13
  c = await loadConfig("serve", process.cwd());
14
14
  await rm(c.dirs.buildRoot, { force: true, recursive: true });
15
- const abortController = new AbortController();
16
- process.once("exit", () => abortController.abort());
17
15
  if (c.flags.preview) {
18
- await startPreviewMode(abortController.signal);
16
+ await startPreviewMode();
19
17
  } else {
20
- await startDevMode(abortController.signal);
18
+ await startDevMode();
21
19
  }
22
20
  return new Promise(() => {
23
21
  });
24
22
  }
25
- async function startPreviewMode(signal) {
23
+ async function startPreviewMode() {
26
24
  const manifest = await buildWebsite(c);
27
25
  const frontend = createBuiltDistFilesFetcher(c.dirs, manifest);
28
- const devServices = startDevServices(c.services, signal);
26
+ const devServices = launchDevServices();
29
27
  const urlRewrites = Object.keys(c.pages).sort().map((url) => {
30
28
  const mapping = c.pages[url];
31
29
  return typeof mapping !== "object" || !mapping.pattern ? null : { url, pattern: mapping.pattern };
32
30
  }).filter((mapping) => mapping !== null);
33
- startWebServer(c.dankPort, c.flags, c.dirs, { urlRewrites }, frontend, devServices.http);
31
+ startWebServer(c.dankPort, c.flags, c.dirs, { urlRewrites }, frontend, devServices);
32
+ const controller = new AbortController();
33
+ watch("dank.config.ts", controller.signal, async (filename) => {
34
+ console.log(filename, "was updated!");
35
+ console.log("config updates are not hot reloaded during `dank serve --preview`");
36
+ console.log("restart DANK to reload configuration");
37
+ controller.abort();
38
+ });
34
39
  }
35
- async function startDevMode(signal) {
40
+ async function startDevMode() {
36
41
  const registry = new WebsiteRegistry(c);
37
42
  await mkdir(c.dirs.buildWatch, { recursive: true });
38
43
  let buildContext = null;
39
- watch("dank.config.ts", signal, async (filename) => {
44
+ watch("dank.config.ts", async (filename) => {
40
45
  try {
41
46
  await c.reload();
42
47
  } catch (ignore) {
43
48
  return;
44
49
  }
45
50
  registry.configSync();
46
- updateDevServices(c.services);
51
+ devServices.update(c.services);
47
52
  });
48
- watch(c.dirs.pages, signal, (filename) => {
53
+ watch(c.dirs.pages, { recursive: true }, (filename) => {
49
54
  if (extname(filename) === ".html") {
50
55
  registry.htmlEntrypoints.forEach((html) => {
51
56
  if (html.fsPath === filename) {
@@ -99,8 +104,8 @@ async function startDevMode(signal) {
99
104
  registry.on("entrypoints", () => resetBuildContext());
100
105
  resetBuildContext();
101
106
  const frontend = createDevServeFilesFetcher(c.esbuildPort, c.dirs, registry);
102
- const devServices = startDevServices(c.services, signal);
103
- startWebServer(c.dankPort, c.flags, c.dirs, registry, frontend, devServices.http);
107
+ const devServices = launchDevServices();
108
+ startWebServer(c.dankPort, c.flags, c.dirs, registry, frontend, devServices);
104
109
  }
105
110
  async function startEsbuildWatch(registry) {
106
111
  const entryPoints = registry.webpageAndWorkerEntryPoints;
@@ -121,6 +126,32 @@ async function writeHtml(html, output) {
121
126
  const path = join(dir, "index.html");
122
127
  await writeFile(path, output);
123
128
  }
129
+ function launchDevServices() {
130
+ const services = new DevServices(c.services);
131
+ services.on("error", (label, cause) => console.log(formatServiceLabel(label), "errored:", cause));
132
+ services.on("exit", (label, code) => {
133
+ if (code) {
134
+ console.log(formatServiceLabel(label), "exited", code);
135
+ } else {
136
+ console.log(formatServiceLabel(label), "exited");
137
+ }
138
+ });
139
+ services.on("launch", (label) => console.log(formatServiceLabel(label), "starting"));
140
+ services.on("stdout", (label, output) => printServiceOutput(label, 32, output));
141
+ services.on("stderr", (label, output) => printServiceOutput(label, 31, output));
142
+ return services;
143
+ }
144
+ function formatServiceLabel(label) {
145
+ return `| \x1B[2m${label.cwd}\x1B[22m ${label.command} |`;
146
+ }
147
+ function formatServiceOutputLabel(label, color) {
148
+ return `\x1B[${color}m${formatServiceLabel(label)}\x1B[39m`;
149
+ }
150
+ function printServiceOutput(label, color, output) {
151
+ const formattedLabel = formatServiceOutputLabel(label, color);
152
+ for (const line of output)
153
+ console.log(formattedLabel, line);
154
+ }
124
155
  export {
125
156
  serveWebsite
126
157
  };
@@ -1,97 +1,130 @@
1
1
  import { execSync, spawn } from "node:child_process";
2
+ import EventEmitter from "node:events";
2
3
  import { basename, isAbsolute, resolve } from "node:path";
3
- const running = [];
4
- if (process.platform === "win32") {
5
- process.once("SIGINT", () => process.exit());
6
- process.once("exit", () => {
7
- for (const { process: process2 } of running) {
8
- if (process2) {
9
- execSync(`taskkill /pid ${process2.pid} /T /F`);
10
- }
11
- }
12
- });
4
+ class ManagedServiceLabel {
5
+ #command;
6
+ #cwd;
7
+ constructor(spec) {
8
+ this.#command = spec.command;
9
+ this.#cwd = !spec.cwd ? "./" : spec.cwd.startsWith("/") ? `/.../${basename(spec.cwd)}` : spec.cwd.startsWith(".") ? spec.cwd : `./${spec.cwd}`;
10
+ }
11
+ get command() {
12
+ return this.#command;
13
+ }
14
+ get cwd() {
15
+ return this.#cwd;
16
+ }
13
17
  }
14
- let signal;
15
- let updating = null;
16
- function startDevServices(services, _signal) {
17
- signal = _signal;
18
- if (services?.length) {
19
- for (const s of services) {
20
- running.push({ s, process: startService(s) });
21
- }
18
+ class ManagedService extends EventEmitter {
19
+ #label;
20
+ #process;
21
+ #spec;
22
+ // #status: ManagedServiceStatus = 'starting'
23
+ constructor(spec) {
24
+ super();
25
+ this.#label = new ManagedServiceLabel(spec);
26
+ this.#spec = spec;
27
+ this.#process = this.#start();
28
+ }
29
+ get spec() {
30
+ return this.#spec;
31
+ }
32
+ get httpSpec() {
33
+ return this.#spec.http;
22
34
  }
23
- return {
24
- http: {
25
- get running() {
26
- return running.map(({ s }) => s.http).filter((http) => !!http);
35
+ matches(other) {
36
+ return matchingConfig(this.#spec, other);
37
+ }
38
+ kill() {
39
+ if (this.#process)
40
+ killProcess(this.#process);
41
+ }
42
+ #start() {
43
+ const { path, args } = parseCommand(this.#spec.command);
44
+ const env = this.#spec.env ? { ...process.env, ...this.#spec.env } : void 0;
45
+ const cwd = !this.#spec.cwd || isAbsolute(this.#spec.cwd) ? this.#spec.cwd : resolve(process.cwd(), this.#spec.cwd);
46
+ const spawned = spawnProcess(path, args, env, cwd);
47
+ this.emit("launch", this.#label);
48
+ spawned.stdout.on("data", (chunk) => this.emit("stdout", this.#label, parseChunk(chunk)));
49
+ spawned.stderr.on("data", (chunk) => this.emit("stderr", this.#label, parseChunk(chunk)));
50
+ spawned.on("error", (e) => {
51
+ if (e.name === "AbortError") {
52
+ return;
27
53
  }
28
- }
29
- };
54
+ const cause = "code" in e && e.code === "ENOENT" ? "program not found" : e.message;
55
+ this.emit("error", this.#label, cause);
56
+ });
57
+ spawned.on("exit", (code, signal) => this.emit("exit", this.#label, code || signal));
58
+ return spawned;
59
+ }
30
60
  }
31
- function updateDevServices(services) {
32
- if (!services?.length) {
33
- if (running.length) {
34
- if (updating === null) {
35
- updating = { stopping: [], starting: [] };
36
- }
37
- running.forEach(({ s, process: process2 }) => {
38
- if (process2) {
39
- stopService(s, process2);
40
- } else {
41
- removeFromUpdating(s);
42
- }
43
- });
44
- running.length = 0;
61
+ const killProcess = process.platform === "win32" ? (p) => execSync(`taskkill /pid ${p.pid} /T /F`) : (p) => p.kill();
62
+ const spawnProcess = process.platform === "win32" ? (path, args, env, cwd) => spawn("cmd", ["/c", path, ...args], {
63
+ cwd,
64
+ env,
65
+ detached: false,
66
+ shell: false,
67
+ windowsHide: true
68
+ }) : (path, args, env, cwd) => spawn(path, args, {
69
+ cwd,
70
+ env,
71
+ detached: false,
72
+ shell: false
73
+ });
74
+ class DevServices extends EventEmitter {
75
+ #running;
76
+ constructor(services) {
77
+ super();
78
+ this.#running = services ? this.#start(services) : [];
79
+ if (process.platform === "win32") {
80
+ process.once("SIGINT", () => process.exit());
45
81
  }
46
- } else {
47
- if (updating === null) {
48
- updating = { stopping: [], starting: [] };
49
- }
50
- const keep = [];
51
- const next = [];
52
- for (const s of services) {
53
- let found = false;
54
- for (let i = 0; i < running.length; i++) {
55
- const p = running[i].s;
56
- if (matchingConfig(s, p)) {
57
- found = true;
58
- keep.push(i);
59
- break;
60
- }
61
- }
62
- if (!found) {
63
- next.push(s);
64
- }
65
- }
66
- for (let i = running.length - 1; i >= 0; i--) {
67
- if (!keep.includes(i)) {
68
- const { s, process: process2 } = running[i];
69
- if (process2) {
70
- stopService(s, process2);
71
- } else {
72
- removeFromUpdating(s);
73
- }
74
- running.splice(i, 1);
75
- }
82
+ process.once("exit", this.shutdown);
83
+ }
84
+ get httpServices() {
85
+ return this.#running.map((s) => s.httpSpec).filter((http) => !!http);
86
+ }
87
+ shutdown = () => {
88
+ this.#running.forEach((s) => {
89
+ s.kill();
90
+ s.removeAllListeners();
91
+ });
92
+ this.#running.length = 0;
93
+ };
94
+ update(services) {
95
+ if (!services?.length) {
96
+ this.shutdown();
97
+ } else if (!matchingConfigs(this.#running.map((s) => s.spec), services)) {
98
+ this.shutdown();
99
+ this.#running = this.#start(services);
76
100
  }
77
- if (updating.stopping.length) {
78
- for (const s of next) {
79
- if (!updating.starting.find((queued) => matchingConfig(queued, s))) {
80
- updating.starting.push(s);
81
- }
82
- }
101
+ }
102
+ #start(services) {
103
+ return services.map((spec) => {
104
+ const service = new ManagedService(spec);
105
+ service.on("error", (label, cause) => this.emit("error", label, cause));
106
+ service.on("exit", (label, code) => this.emit("exit", label, code));
107
+ service.on("launch", (label) => this.emit("launch", label));
108
+ service.on("stdout", (label, output) => this.emit("stdout", label, output));
109
+ service.on("stderr", (label, output) => this.emit("stderr", label, output));
110
+ return service;
111
+ });
112
+ }
113
+ }
114
+ function matchingConfigs(a, b) {
115
+ if (a.length !== b.length) {
116
+ return false;
117
+ }
118
+ const crossRef = [...a];
119
+ for (const toFind of b) {
120
+ const found = crossRef.findIndex((spec) => matchingConfig(spec, toFind));
121
+ if (found === -1) {
122
+ return false;
83
123
  } else {
84
- updating = null;
85
- for (const s of next) {
86
- running.push({ s, process: startService(s) });
87
- }
124
+ crossRef.splice(found, 1);
88
125
  }
89
126
  }
90
- }
91
- function stopService(s, process2) {
92
- opPrint(s, "stopping");
93
- updating.stopping.push(s);
94
- process2.kill();
127
+ return true;
95
128
  }
96
129
  function matchingConfig(a, b) {
97
130
  if (a.command !== b.command) {
@@ -119,99 +152,55 @@ function matchingConfig(a, b) {
119
152
  }
120
153
  return true;
121
154
  }
122
- function startService(s) {
123
- opPrint(s, "starting");
124
- const spawned = spawnService(s);
125
- const stdoutLabel = logLabel(s, 32);
126
- spawned.stdout.on("data", (chunk) => printChunk(stdoutLabel, chunk));
127
- const stderrLabel = logLabel(s, 31);
128
- spawned.stderr.on("data", (chunk) => printChunk(stderrLabel, chunk));
129
- spawned.on("error", (e) => {
130
- removeFromRunning(s);
131
- if (e.name === "AbortError") {
132
- return;
133
- }
134
- const cause = "code" in e && e.code === "ENOENT" ? "program not found" : e.message;
135
- opPrint(s, "error: " + cause);
136
- });
137
- spawned.on("exit", () => {
138
- opPrint(s, "exited");
139
- removeFromRunning(s);
140
- removeFromUpdating(s);
141
- });
142
- return spawned;
155
+ function parseChunk(c) {
156
+ return c.toString().replace(/\r?\n$/, "").split(/\r?\n/);
143
157
  }
144
- function spawnService(s) {
145
- const splitCmdAndArgs = s.command.split(/\s+/);
146
- const program = splitCmdAndArgs[0];
147
- const args = splitCmdAndArgs.length === 1 ? [] : splitCmdAndArgs.slice(1);
148
- const env = s.env ? { ...process.env, ...s.env } : void 0;
149
- const cwd = resolveCwd(s.cwd);
150
- if (process.platform === "win32") {
151
- return spawn("cmd", ["/c", program, ...args], {
152
- cwd,
153
- env,
154
- detached: false,
155
- shell: false,
156
- windowsHide: true
157
- });
158
- } else {
159
- return spawn(program, args, {
160
- cwd,
161
- env,
162
- signal,
163
- detached: false,
164
- shell: false
165
- });
158
+ function parseCommand(command) {
159
+ command = command.trimStart();
160
+ const programSplitIndex = command.indexOf(" ");
161
+ if (programSplitIndex === -1) {
162
+ return { path: command.trim(), args: [] };
166
163
  }
167
- }
168
- function removeFromRunning(s) {
169
- for (let i = 0; i < running.length; i++) {
170
- if (matchingConfig(running[i].s, s)) {
171
- running.splice(i, 1);
172
- return;
164
+ const path = command.substring(0, programSplitIndex);
165
+ const args = [];
166
+ let argStart = programSplitIndex + 1;
167
+ let withinLiteral = false;
168
+ for (let i = 0; i < command.length; i++) {
169
+ const c = command[i];
170
+ if (!withinLiteral) {
171
+ if (c === "'" || c === '"') {
172
+ withinLiteral = c;
173
+ continue;
174
+ }
175
+ if (c === "\\") {
176
+ i++;
177
+ continue;
178
+ }
173
179
  }
174
- }
175
- }
176
- function removeFromUpdating(s) {
177
- if (updating !== null) {
178
- for (let i = 0; i < updating.stopping.length; i++) {
179
- if (matchingConfig(updating.stopping[i], s)) {
180
- updating.stopping.splice(i, 1);
181
- if (!updating.stopping.length) {
182
- updating.starting.forEach(startService);
183
- updating = null;
184
- return;
185
- }
180
+ if (withinLiteral) {
181
+ if (c === withinLiteral) {
182
+ withinLiteral = false;
183
+ args.push(command.substring(argStart + 1, i));
184
+ argStart = i + 1;
186
185
  }
186
+ continue;
187
+ }
188
+ if (c === " " && i > argStart) {
189
+ const maybeArg2 = command.substring(argStart, i).trim();
190
+ if (maybeArg2.length) {
191
+ args.push(maybeArg2);
192
+ }
193
+ argStart = i + 1;
187
194
  }
188
195
  }
189
- }
190
- function printChunk(label, c) {
191
- for (const l of parseChunk(c))
192
- console.log(label, l);
193
- }
194
- function parseChunk(c) {
195
- return c.toString().replace(/\r?\n$/, "").split(/\r?\n/);
196
- }
197
- function resolveCwd(p) {
198
- if (!p || isAbsolute(p)) {
199
- return p;
200
- } else {
201
- return resolve(process.cwd(), p);
196
+ const maybeArg = command.substring(argStart, command.length).trim();
197
+ if (maybeArg.length) {
198
+ args.push(maybeArg);
202
199
  }
203
- }
204
- function opPrint(s, msg) {
205
- console.log(opLabel(s), msg);
206
- }
207
- function opLabel(s) {
208
- return `\`${s.cwd ? s.cwd + " " : ""}${s.command}\``;
209
- }
210
- function logLabel(s, ansiColor) {
211
- s.cwd = !s.cwd ? "./" : s.cwd.startsWith("/") ? `/.../${basename(s.cwd)}` : s.cwd.startsWith(".") ? s.cwd : `./${s.cwd}`;
212
- return `\x1B[${ansiColor}m[\x1B[1m${s.command}\x1B[22m \x1B[2;3m${s.cwd}\x1B[22;23m]\x1B[0m`;
200
+ return { path, args };
213
201
  }
214
202
  export {
215
- startDevServices,
216
- updateDevServices
203
+ DevServices,
204
+ ManagedServiceLabel,
205
+ parseCommand
217
206
  };
package/lib_js/watch.js CHANGED
@@ -1,13 +1,22 @@
1
1
  import { watch as createWatch } from "node:fs/promises";
2
- async function watch(p, signal, fire) {
2
+ async function watch(p, signalFireOrOpts, fireOrUndefined) {
3
+ let opts;
4
+ let fire;
5
+ if (signalFireOrOpts instanceof AbortSignal) {
6
+ opts = { signal: signalFireOrOpts };
7
+ } else if (typeof signalFireOrOpts === "object") {
8
+ opts = signalFireOrOpts;
9
+ } else {
10
+ fire = signalFireOrOpts;
11
+ }
12
+ if (opts && typeof fireOrUndefined === "function") {
13
+ fire = fireOrUndefined;
14
+ }
3
15
  const delayFire = 90;
4
16
  const timeout = 100;
5
17
  let changes = {};
6
18
  try {
7
- for await (const { filename } of createWatch(p, {
8
- recursive: true,
9
- signal
10
- })) {
19
+ for await (const { filename } of createWatch(p, opts)) {
11
20
  if (filename) {
12
21
  if (!changes[filename]) {
13
22
  const now = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eighty4/dank",
3
- "version": "0.0.5-1",
3
+ "version": "0.0.5-2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Adam McKee Bennett <adam.be.g84d@gmail.com>",