@eighty4/dank 0.0.1-4 → 0.0.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/README.md +20 -16
- package/client/esbuild.js +26 -15
- package/lib/config.ts +2 -1
- package/lib/dank.ts +8 -1
- package/lib/esbuild.ts +2 -0
- package/lib/http.ts +104 -50
- package/lib/serve.ts +255 -51
- package/lib/services.ts +156 -11
- package/lib_js/config.js +2 -1
- package/lib_js/dank.js +6 -0
- package/lib_js/esbuild.js +2 -0
- package/lib_js/http.js +83 -44
- package/lib_js/serve.js +211 -38
- package/lib_js/services.js +146 -10
- package/lib_types/dank.d.ts +1 -1
- package/package.json +2 -1
package/lib_js/http.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
2
3
|
import { createServer, } from 'node:http';
|
|
3
|
-
import { extname, join
|
|
4
|
-
import
|
|
4
|
+
import { extname, join } from 'node:path';
|
|
5
|
+
import mime from 'mime';
|
|
5
6
|
export function createWebServer(port, frontendFetcher) {
|
|
6
7
|
const serverAddress = 'http://localhost:' + port;
|
|
7
8
|
return createServer((req, res) => {
|
|
@@ -27,56 +28,94 @@ export function createBuiltDistFilesFetcher(dir, files) {
|
|
|
27
28
|
res.end();
|
|
28
29
|
}
|
|
29
30
|
else {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
: fsJoin(dir, url.pathname));
|
|
35
|
-
reading.pipe(res);
|
|
36
|
-
reading.on('error', err => {
|
|
37
|
-
console.error(`${url.pathname} file read ${reading.path} error ${err.message}`);
|
|
38
|
-
res.statusCode = 500;
|
|
39
|
-
res.end();
|
|
40
|
-
});
|
|
31
|
+
const p = extname(url.pathname) === ''
|
|
32
|
+
? join(dir, url.pathname, 'index.html')
|
|
33
|
+
: join(dir, url.pathname);
|
|
34
|
+
streamFile(p, res);
|
|
41
35
|
}
|
|
42
36
|
};
|
|
43
37
|
}
|
|
44
|
-
export function
|
|
45
|
-
const proxyAddress = 'http://127.0.0.1:' +
|
|
38
|
+
export function createDevServeFilesFetcher(opts) {
|
|
39
|
+
const proxyAddress = 'http://127.0.0.1:' + opts.proxyPort;
|
|
46
40
|
return (url, _headers, res) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
if (opts.pages[url.pathname]) {
|
|
42
|
+
streamFile(join(opts.pagesDir, url.pathname + 'index.html'), res);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const maybePublicPath = join(opts.publicDir, url.pathname);
|
|
46
|
+
exists(join(opts.publicDir, url.pathname)).then(fromPublic => {
|
|
47
|
+
if (fromPublic) {
|
|
48
|
+
streamFile(maybePublicPath, res);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
retryFetchWithTimeout(proxyAddress + url.pathname)
|
|
52
|
+
.then(fetchResponse => {
|
|
53
|
+
res.writeHead(fetchResponse.status, convertHeadersFromFetch(fetchResponse.headers));
|
|
54
|
+
fetchResponse.bytes().then(data => res.end(data));
|
|
55
|
+
})
|
|
56
|
+
.catch(e => {
|
|
57
|
+
if (e === 'retrytimeout') {
|
|
58
|
+
res.writeHead(504);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.error('unknown frontend proxy fetch error:', e);
|
|
62
|
+
res.writeHead(502);
|
|
63
|
+
}
|
|
64
|
+
res.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
51
69
|
};
|
|
52
70
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
const PROXY_FETCH_RETRY_INTERVAL = 27;
|
|
72
|
+
const PROXY_FETCH_RETRY_TIMEOUT = 1000;
|
|
73
|
+
async function retryFetchWithTimeout(url) {
|
|
74
|
+
let timeout = Date.now() + PROXY_FETCH_RETRY_TIMEOUT;
|
|
75
|
+
while (true) {
|
|
76
|
+
try {
|
|
77
|
+
return await fetch(url);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
if (isNodeFailedFetch(e) || isBunFailedFetch(e)) {
|
|
81
|
+
if (timeout < Date.now()) {
|
|
82
|
+
throw 'retrytimeout';
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
await new Promise(res => setTimeout(res, PROXY_FETCH_RETRY_INTERVAL));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function isBunFailedFetch(e) {
|
|
95
|
+
return e.code === 'ConnectionRefused';
|
|
96
|
+
}
|
|
97
|
+
function isNodeFailedFetch(e) {
|
|
98
|
+
return e.message === 'fetch failed';
|
|
99
|
+
}
|
|
100
|
+
async function exists(p) {
|
|
101
|
+
try {
|
|
102
|
+
const maybe = stat(p);
|
|
103
|
+
return (await maybe).isFile();
|
|
104
|
+
}
|
|
105
|
+
catch (ignore) {
|
|
106
|
+
return false;
|
|
78
107
|
}
|
|
79
108
|
}
|
|
109
|
+
function streamFile(p, res) {
|
|
110
|
+
res.setHeader('Content-Type', mime.getType(p) || 'application/octet-stream');
|
|
111
|
+
const reading = createReadStream(p);
|
|
112
|
+
reading.pipe(res);
|
|
113
|
+
reading.on('error', err => {
|
|
114
|
+
console.error(`file read ${reading.path} error ${err.message}`);
|
|
115
|
+
res.statusCode = 500;
|
|
116
|
+
res.end();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
80
119
|
function convertHeadersFromFetch(from) {
|
|
81
120
|
const to = {};
|
|
82
121
|
for (const name of from.keys()) {
|
package/lib_js/serve.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
1
|
+
import { mkdir, readFile, rm, watch as _watch } from 'node:fs/promises';
|
|
2
|
+
import { extname, join, resolve } from 'node:path';
|
|
3
3
|
import { buildWebsite } from "./build.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
4
5
|
import { createGlobalDefinitions } from "./define.js";
|
|
5
6
|
import { esbuildDevContext } from "./esbuild.js";
|
|
6
7
|
import { isPreviewBuild } from "./flags.js";
|
|
7
8
|
import { HtmlEntrypoint } from "./html.js";
|
|
8
|
-
import { createBuiltDistFilesFetcher,
|
|
9
|
-
import {
|
|
10
|
-
import { startDevServices } from "./services.js";
|
|
9
|
+
import { createBuiltDistFilesFetcher, createDevServeFilesFetcher, createWebServer, } from "./http.js";
|
|
10
|
+
import { startDevServices, updateDevServices } from "./services.js";
|
|
11
11
|
const isPreview = isPreviewBuild();
|
|
12
12
|
// alternate port for --preview bc of service worker
|
|
13
13
|
const PORT = isPreview ? 4000 : 3000;
|
|
@@ -15,56 +15,229 @@ const PORT = isPreview ? 4000 : 3000;
|
|
|
15
15
|
const ESBUILD_PORT = 2999;
|
|
16
16
|
export async function serveWebsite(c) {
|
|
17
17
|
await rm('build', { force: true, recursive: true });
|
|
18
|
-
let frontend;
|
|
19
18
|
if (isPreview) {
|
|
20
|
-
|
|
21
|
-
frontend = createBuiltDistFilesFetcher(dir, files);
|
|
19
|
+
await startPreviewMode(c);
|
|
22
20
|
}
|
|
23
21
|
else {
|
|
24
|
-
const
|
|
25
|
-
|
|
22
|
+
const abortController = new AbortController();
|
|
23
|
+
await startDevMode(c, abortController.signal);
|
|
26
24
|
}
|
|
27
|
-
createWebServer(PORT, frontend).listen(PORT);
|
|
28
|
-
console.log(isPreview ? 'preview' : 'dev server', `is live at http://127.0.0.1:${PORT}`);
|
|
29
|
-
startDevServices(c);
|
|
30
25
|
return new Promise(() => { });
|
|
31
26
|
}
|
|
32
|
-
async function
|
|
27
|
+
async function startPreviewMode(c) {
|
|
28
|
+
const { dir, files } = await buildWebsite(c);
|
|
29
|
+
const frontend = createBuiltDistFilesFetcher(dir, files);
|
|
30
|
+
createWebServer(PORT, frontend).listen(PORT);
|
|
31
|
+
console.log(`preview is live at http://127.0.0.1:${PORT}`);
|
|
32
|
+
}
|
|
33
|
+
// todo changing partials triggers update on html pages
|
|
34
|
+
async function startDevMode(c, signal) {
|
|
33
35
|
const watchDir = join('build', 'watch');
|
|
34
36
|
await mkdir(watchDir, { recursive: true });
|
|
35
|
-
await
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
const clientJS = await loadClientJS();
|
|
38
|
+
const pagesByUrlPath = {};
|
|
39
|
+
const entryPointsByUrlPath = {};
|
|
40
|
+
let buildContext = null;
|
|
41
|
+
watch('dank.config.ts', signal, async () => {
|
|
42
|
+
let updated;
|
|
43
|
+
try {
|
|
44
|
+
updated = await loadConfig();
|
|
45
|
+
}
|
|
46
|
+
catch (ignore) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const prevPages = new Set(Object.keys(pagesByUrlPath));
|
|
50
|
+
await Promise.all(Object.entries(updated.pages).map(async ([urlPath, srcPath]) => {
|
|
51
|
+
c.pages[urlPath] = srcPath;
|
|
52
|
+
if (pagesByUrlPath[urlPath]) {
|
|
53
|
+
prevPages.delete(urlPath);
|
|
54
|
+
if (pagesByUrlPath[urlPath].srcPath !== srcPath) {
|
|
55
|
+
await updatePage(urlPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
await addPage(urlPath, srcPath);
|
|
60
|
+
}
|
|
61
|
+
}));
|
|
62
|
+
for (const prevPage of Array.from(prevPages)) {
|
|
63
|
+
delete c.pages[prevPage];
|
|
64
|
+
deletePage(prevPage);
|
|
65
|
+
}
|
|
66
|
+
updateDevServices(updated);
|
|
67
|
+
});
|
|
68
|
+
watch('pages', signal, filename => {
|
|
69
|
+
if (extname(filename) === '.html') {
|
|
70
|
+
for (const [urlPath, srcPath] of Object.entries(c.pages)) {
|
|
71
|
+
if (srcPath === filename) {
|
|
72
|
+
updatePage(urlPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
await Promise.all(Object.entries(c.pages).map(([urlPath, srcPath]) => addPage(urlPath, srcPath)));
|
|
78
|
+
async function addPage(urlPath, srcPath) {
|
|
79
|
+
const metadata = await processWebpage({
|
|
80
|
+
clientJS,
|
|
81
|
+
outDir: watchDir,
|
|
82
|
+
pagesDir: 'pages',
|
|
83
|
+
srcPath,
|
|
84
|
+
urlPath,
|
|
85
|
+
});
|
|
86
|
+
pagesByUrlPath[urlPath] = metadata;
|
|
87
|
+
entryPointsByUrlPath[urlPath] = new Set(metadata.entryPoints.map(e => e.in));
|
|
88
|
+
if (buildContext !== null) {
|
|
89
|
+
resetBuildContext();
|
|
44
90
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
91
|
+
}
|
|
92
|
+
function deletePage(urlPath) {
|
|
93
|
+
delete pagesByUrlPath[urlPath];
|
|
94
|
+
delete entryPointsByUrlPath[urlPath];
|
|
95
|
+
resetBuildContext();
|
|
96
|
+
}
|
|
97
|
+
async function updatePage(urlPath) {
|
|
98
|
+
const update = await processWebpage({
|
|
99
|
+
clientJS,
|
|
100
|
+
outDir: watchDir,
|
|
101
|
+
pagesDir: 'pages',
|
|
102
|
+
srcPath: c.pages[urlPath],
|
|
103
|
+
urlPath,
|
|
104
|
+
});
|
|
105
|
+
const entryPointUrls = new Set(update.entryPoints.map(e => e.in));
|
|
106
|
+
if (!hasSameValues(entryPointUrls, entryPointsByUrlPath[urlPath])) {
|
|
107
|
+
entryPointsByUrlPath[urlPath] = entryPointUrls;
|
|
108
|
+
resetBuildContext();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function collectEntrypoints() {
|
|
112
|
+
const sources = new Set();
|
|
113
|
+
return Object.values(pagesByUrlPath)
|
|
114
|
+
.flatMap(({ entryPoints }) => entryPoints)
|
|
115
|
+
.filter(entryPoint => {
|
|
116
|
+
if (sources.has(entryPoint.in)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
sources.add(entryPoint.in);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function resetBuildContext() {
|
|
126
|
+
if (buildContext === 'starting' || buildContext === 'dirty') {
|
|
127
|
+
buildContext = 'dirty';
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (buildContext === 'disposing') {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (buildContext !== null) {
|
|
134
|
+
const prev = buildContext;
|
|
135
|
+
buildContext = 'disposing';
|
|
136
|
+
prev.dispose().then(() => {
|
|
137
|
+
buildContext = null;
|
|
138
|
+
resetBuildContext();
|
|
52
139
|
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
startEsbuildWatch(collectEntrypoints()).then(ctx => {
|
|
143
|
+
if (buildContext === 'dirty') {
|
|
144
|
+
buildContext = null;
|
|
145
|
+
resetBuildContext();
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
buildContext = ctx;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
buildContext = await startEsbuildWatch(collectEntrypoints());
|
|
154
|
+
const frontend = createDevServeFilesFetcher({
|
|
155
|
+
pages: c.pages,
|
|
156
|
+
pagesDir: watchDir,
|
|
157
|
+
proxyPort: ESBUILD_PORT,
|
|
158
|
+
publicDir: 'public',
|
|
159
|
+
});
|
|
160
|
+
createWebServer(PORT, frontend).listen(PORT);
|
|
161
|
+
console.log(`dev server is live at http://127.0.0.1:${PORT}`);
|
|
162
|
+
startDevServices(c, signal);
|
|
163
|
+
}
|
|
164
|
+
function hasSameValues(a, b) {
|
|
165
|
+
if (a.size !== b.size) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
for (const v in a) {
|
|
169
|
+
if (!b.has(v)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
async function processWebpage(inputs) {
|
|
176
|
+
const html = await HtmlEntrypoint.readFrom(inputs.urlPath, join(inputs.pagesDir, inputs.srcPath));
|
|
177
|
+
await html.injectPartials();
|
|
178
|
+
if (inputs.urlPath !== '/') {
|
|
179
|
+
await mkdir(join(inputs.outDir, inputs.urlPath), { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
const entryPoints = [];
|
|
182
|
+
html.collectScripts().forEach(scriptImport => {
|
|
183
|
+
entryPoints.push({
|
|
184
|
+
in: scriptImport.in,
|
|
185
|
+
out: scriptImport.out,
|
|
53
186
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
187
|
+
});
|
|
188
|
+
html.rewriteHrefs();
|
|
189
|
+
html.appendScript(inputs.clientJS);
|
|
190
|
+
await html.writeTo(inputs.outDir);
|
|
191
|
+
return {
|
|
192
|
+
entryPoints,
|
|
193
|
+
srcPath: inputs.srcPath,
|
|
194
|
+
urlPath: inputs.urlPath,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function startEsbuildWatch(entryPoints) {
|
|
198
|
+
const ctx = await esbuildDevContext(createGlobalDefinitions(), entryPoints, 'build/watch');
|
|
60
199
|
await ctx.watch();
|
|
61
200
|
await ctx.serve({
|
|
62
201
|
host: '127.0.0.1',
|
|
63
202
|
port: ESBUILD_PORT,
|
|
64
|
-
servedir: watchDir,
|
|
65
203
|
cors: {
|
|
66
204
|
origin: 'http://127.0.0.1:' + PORT,
|
|
67
205
|
},
|
|
68
206
|
});
|
|
69
|
-
return
|
|
207
|
+
return ctx;
|
|
208
|
+
}
|
|
209
|
+
async function loadClientJS() {
|
|
210
|
+
return await readFile(resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')), 'utf-8');
|
|
211
|
+
}
|
|
212
|
+
async function watch(p, signal, fire) {
|
|
213
|
+
const delayFire = 90;
|
|
214
|
+
const timeout = 100;
|
|
215
|
+
let changes = {};
|
|
216
|
+
try {
|
|
217
|
+
for await (const { filename } of _watch(p, {
|
|
218
|
+
recursive: true,
|
|
219
|
+
signal,
|
|
220
|
+
})) {
|
|
221
|
+
if (filename) {
|
|
222
|
+
if (!changes[filename]) {
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
changes[filename] = now + delayFire;
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
for (const [filename, then] of Object.entries(changes)) {
|
|
228
|
+
if (then <= now) {
|
|
229
|
+
fire(filename);
|
|
230
|
+
delete changes[filename];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}, timeout);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
if (e.name !== 'AbortError') {
|
|
240
|
+
throw e;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
70
243
|
}
|
package/lib_js/services.js
CHANGED
|
@@ -1,20 +1,120 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { basename, isAbsolute, resolve } from 'node:path';
|
|
3
|
-
|
|
3
|
+
// up to date representation of dank.config.ts services
|
|
4
|
+
const running = [];
|
|
5
|
+
let signal;
|
|
6
|
+
// batch of services that must be stopped before starting new services
|
|
7
|
+
let updating = null;
|
|
8
|
+
export function startDevServices(c, _signal) {
|
|
9
|
+
signal = _signal;
|
|
4
10
|
if (c.services?.length) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
for (const s of c.services) {
|
|
12
|
+
running.push({ s, process: startService(s) });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function updateDevServices(c) {
|
|
17
|
+
if (!c.services?.length) {
|
|
18
|
+
if (running.length) {
|
|
19
|
+
if (updating === null) {
|
|
20
|
+
updating = { stopping: [], starting: [] };
|
|
21
|
+
}
|
|
22
|
+
running.forEach(({ s, process }) => {
|
|
23
|
+
if (process) {
|
|
24
|
+
stopService(s, process);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
removeFromUpdating(s);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
running.length = 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (updating === null) {
|
|
35
|
+
updating = { stopping: [], starting: [] };
|
|
36
|
+
}
|
|
37
|
+
const keep = [];
|
|
38
|
+
const next = [];
|
|
39
|
+
for (const s of c.services) {
|
|
40
|
+
let found = false;
|
|
41
|
+
for (let i = 0; i < running.length; i++) {
|
|
42
|
+
const p = running[i].s;
|
|
43
|
+
if (matchingConfig(s, p)) {
|
|
44
|
+
found = true;
|
|
45
|
+
keep.push(i);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!found) {
|
|
50
|
+
next.push(s);
|
|
9
51
|
}
|
|
10
52
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
53
|
+
for (let i = running.length - 1; i >= 0; i--) {
|
|
54
|
+
if (!keep.includes(i)) {
|
|
55
|
+
const { s, process } = running[i];
|
|
56
|
+
if (process) {
|
|
57
|
+
stopService(s, process);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
removeFromUpdating(s);
|
|
61
|
+
}
|
|
62
|
+
running.splice(i, 1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (updating.stopping.length) {
|
|
66
|
+
for (const s of next) {
|
|
67
|
+
if (!updating.starting.find(queued => matchingConfig(queued, s))) {
|
|
68
|
+
updating.starting.push(s);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
updating = null;
|
|
74
|
+
for (const s of next) {
|
|
75
|
+
running.push({ s, process: startService(s) });
|
|
76
|
+
}
|
|
14
77
|
}
|
|
15
78
|
}
|
|
16
79
|
}
|
|
17
|
-
function
|
|
80
|
+
function stopService(s, process) {
|
|
81
|
+
opPrint(s, 'stopping');
|
|
82
|
+
updating.stopping.push(s);
|
|
83
|
+
process.kill();
|
|
84
|
+
}
|
|
85
|
+
function matchingConfig(a, b) {
|
|
86
|
+
if (a.command !== b.command) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (a.cwd !== b.cwd) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (!a.env && !b.env) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
else if (a.env && !b.env) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
else if (!a.env && b.env) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
else if (Object.keys(a.env).length !== Object.keys(b.env).length) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
for (const k of Object.keys(a.env)) {
|
|
106
|
+
if (!b.env[k]) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
else if (a.env[k] !== b.env[k]) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
function startService(s) {
|
|
117
|
+
opPrint(s, 'starting');
|
|
18
118
|
const splitCmdAndArgs = s.command.split(/\s+/);
|
|
19
119
|
const cmd = splitCmdAndArgs[0];
|
|
20
120
|
const args = splitCmdAndArgs.length === 1 ? [] : splitCmdAndArgs.slice(1);
|
|
@@ -29,9 +129,39 @@ function startService(s, signal) {
|
|
|
29
129
|
spawned.stdout.on('data', chunk => printChunk(stdoutLabel, chunk));
|
|
30
130
|
const stderrLabel = logLabel(s.cwd, cmd, args, 31);
|
|
31
131
|
spawned.stderr.on('data', chunk => printChunk(stderrLabel, chunk));
|
|
132
|
+
spawned.on('error', e => {
|
|
133
|
+
const cause = 'code' in e && e.code === 'ENOENT' ? 'program not found' : e.message;
|
|
134
|
+
opPrint(s, 'error: ' + cause);
|
|
135
|
+
removeFromRunning(s);
|
|
136
|
+
});
|
|
32
137
|
spawned.on('exit', () => {
|
|
33
|
-
|
|
138
|
+
opPrint(s, 'exited');
|
|
139
|
+
removeFromRunning(s);
|
|
140
|
+
removeFromUpdating(s);
|
|
34
141
|
});
|
|
142
|
+
return spawned;
|
|
143
|
+
}
|
|
144
|
+
function removeFromRunning(s) {
|
|
145
|
+
for (let i = 0; i < running.length; i++) {
|
|
146
|
+
if (matchingConfig(running[i].s, s)) {
|
|
147
|
+
running.splice(i, 1);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function removeFromUpdating(s) {
|
|
153
|
+
if (updating !== null) {
|
|
154
|
+
for (let i = 0; i < updating.stopping.length; i++) {
|
|
155
|
+
if (matchingConfig(updating.stopping[i], s)) {
|
|
156
|
+
updating.stopping.splice(i, 1);
|
|
157
|
+
if (!updating.stopping.length) {
|
|
158
|
+
updating.starting.forEach(startService);
|
|
159
|
+
updating = null;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
35
165
|
}
|
|
36
166
|
function printChunk(label, c) {
|
|
37
167
|
for (const l of parseChunk(c))
|
|
@@ -51,6 +181,12 @@ function resolveCwd(p) {
|
|
|
51
181
|
return resolve(process.cwd(), p);
|
|
52
182
|
}
|
|
53
183
|
}
|
|
184
|
+
function opPrint(s, msg) {
|
|
185
|
+
console.log(opLabel(s), msg);
|
|
186
|
+
}
|
|
187
|
+
function opLabel(s) {
|
|
188
|
+
return `\`${s.cwd ? s.cwd + ' ' : ''}${s.command}\``;
|
|
189
|
+
}
|
|
54
190
|
function logLabel(cwd, cmd, args, ansiColor) {
|
|
55
191
|
cwd = !cwd
|
|
56
192
|
? './'
|
package/lib_types/dank.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eighty4/dank",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-page development system for CDN-deployed websites",
|
|
6
6
|
"keywords": [
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"esbuild": "^0.25.10",
|
|
22
|
+
"mime": "^4.1.0",
|
|
22
23
|
"parse5": "^8.0.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|