dx-devkit 0.0.0-stage → 1.1.0
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/CHANGELOG.md +81 -0
- package/LICENSE +21 -0
- package/README.md +475 -2
- package/bin/dx-mcp.js +2 -0
- package/dist/command.d.ts +9 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +2 -0
- package/dist/command.js.map +1 -0
- package/dist/commands/ai.d.ts +7 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +148 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/clean.d.ts +3 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +72 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/deps.d.ts +3 -0
- package/dist/commands/deps.d.ts.map +1 -0
- package/dist/commands/deps.js +98 -0
- package/dist/commands/deps.js.map +1 -0
- package/dist/commands/go.d.ts +3 -0
- package/dist/commands/go.d.ts.map +1 -0
- package/dist/commands/go.js +202 -0
- package/dist/commands/go.js.map +1 -0
- package/dist/commands/log.d.ts +3 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +51 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/new.d.ts +3 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +117 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/port.d.ts +4 -0
- package/dist/commands/port.d.ts.map +1 -0
- package/dist/commands/port.js +106 -0
- package/dist/commands/port.js.map +1 -0
- package/dist/commands/projects.d.ts +5 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +46 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/stack.d.ts +34 -0
- package/dist/commands/stack.d.ts.map +1 -0
- package/dist/commands/stack.js +140 -0
- package/dist/commands/stack.js.map +1 -0
- package/dist/commands/summary.d.ts +3 -0
- package/dist/commands/summary.d.ts.map +1 -0
- package/dist/commands/summary.js +88 -0
- package/dist/commands/summary.js.map +1 -0
- package/dist/commands/system.d.ts +5 -0
- package/dist/commands/system.d.ts.map +1 -0
- package/dist/commands/system.js +104 -0
- package/dist/commands/system.js.map +1 -0
- package/dist/commands/ui.d.ts +3 -0
- package/dist/commands/ui.d.ts.map +1 -0
- package/dist/commands/ui.js +96 -0
- package/dist/commands/ui.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/dist/ui.d.ts +27 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +104 -0
- package/dist/ui.js.map +1 -0
- package/node_modules/@dx/core/dist/config.d.ts +46 -0
- package/node_modules/@dx/core/dist/config.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/config.js +131 -0
- package/node_modules/@dx/core/dist/config.js.map +1 -0
- package/node_modules/@dx/core/dist/fuzzy.d.ts +15 -0
- package/node_modules/@dx/core/dist/fuzzy.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/fuzzy.js +63 -0
- package/node_modules/@dx/core/dist/fuzzy.js.map +1 -0
- package/node_modules/@dx/core/dist/index.d.ts +17 -0
- package/node_modules/@dx/core/dist/index.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/index.js +19 -0
- package/node_modules/@dx/core/dist/index.js.map +1 -0
- package/node_modules/@dx/core/dist/runs.d.ts +19 -0
- package/node_modules/@dx/core/dist/runs.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/runs.js +41 -0
- package/node_modules/@dx/core/dist/runs.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/aiSetup.d.ts +58 -0
- package/node_modules/@dx/core/dist/tools/aiSetup.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/aiSetup.js +243 -0
- package/node_modules/@dx/core/dist/tools/aiSetup.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/cleanup.d.ts +61 -0
- package/node_modules/@dx/core/dist/tools/cleanup.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/cleanup.js +171 -0
- package/node_modules/@dx/core/dist/tools/cleanup.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/deps.d.ts +57 -0
- package/node_modules/@dx/core/dist/tools/deps.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/deps.js +151 -0
- package/node_modules/@dx/core/dist/tools/deps.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/devErrors.d.ts +74 -0
- package/node_modules/@dx/core/dist/tools/devErrors.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/devErrors.js +261 -0
- package/node_modules/@dx/core/dist/tools/devErrors.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/doctor.d.ts +12 -0
- package/node_modules/@dx/core/dist/tools/doctor.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/doctor.js +176 -0
- package/node_modules/@dx/core/dist/tools/doctor.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/github.d.ts +31 -0
- package/node_modules/@dx/core/dist/tools/github.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/github.js +80 -0
- package/node_modules/@dx/core/dist/tools/github.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/journal.d.ts +23 -0
- package/node_modules/@dx/core/dist/tools/journal.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/journal.js +80 -0
- package/node_modules/@dx/core/dist/tools/journal.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/launch.d.ts +24 -0
- package/node_modules/@dx/core/dist/tools/launch.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/launch.js +137 -0
- package/node_modules/@dx/core/dist/tools/launch.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/ports.d.ts +42 -0
- package/node_modules/@dx/core/dist/tools/ports.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/ports.js +220 -0
- package/node_modules/@dx/core/dist/tools/ports.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/projects.d.ts +58 -0
- package/node_modules/@dx/core/dist/tools/projects.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/projects.js +208 -0
- package/node_modules/@dx/core/dist/tools/projects.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/scaffold.d.ts +40 -0
- package/node_modules/@dx/core/dist/tools/scaffold.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/scaffold.js +188 -0
- package/node_modules/@dx/core/dist/tools/scaffold.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/stacks.d.ts +119 -0
- package/node_modules/@dx/core/dist/tools/stacks.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/stacks.js +386 -0
- package/node_modules/@dx/core/dist/tools/stacks.js.map +1 -0
- package/node_modules/@dx/core/dist/tools/summary.d.ts +87 -0
- package/node_modules/@dx/core/dist/tools/summary.d.ts.map +1 -0
- package/node_modules/@dx/core/dist/tools/summary.js +331 -0
- package/node_modules/@dx/core/dist/tools/summary.js.map +1 -0
- package/node_modules/@dx/core/package.json +20 -0
- package/node_modules/@dx/mcp/dist/index.d.ts +3 -0
- package/node_modules/@dx/mcp/dist/index.d.ts.map +1 -0
- package/node_modules/@dx/mcp/dist/index.js +540 -0
- package/node_modules/@dx/mcp/dist/index.js.map +1 -0
- package/node_modules/@dx/mcp/package.json +8 -0
- package/node_modules/@dx/web/dist/api.d.ts +13 -0
- package/node_modules/@dx/web/dist/api.d.ts.map +1 -0
- package/node_modules/@dx/web/dist/api.js +351 -0
- package/node_modules/@dx/web/dist/api.js.map +1 -0
- package/node_modules/@dx/web/dist/background.d.ts +2 -0
- package/node_modules/@dx/web/dist/background.d.ts.map +1 -0
- package/node_modules/@dx/web/dist/background.js +24 -0
- package/node_modules/@dx/web/dist/background.js.map +1 -0
- package/node_modules/@dx/web/dist/check.d.ts +39 -0
- package/node_modules/@dx/web/dist/check.d.ts.map +1 -0
- package/node_modules/@dx/web/dist/check.js +97 -0
- package/node_modules/@dx/web/dist/check.js.map +1 -0
- package/node_modules/@dx/web/dist/dev.d.ts +109 -0
- package/node_modules/@dx/web/dist/dev.d.ts.map +1 -0
- package/node_modules/@dx/web/dist/dev.js +645 -0
- package/node_modules/@dx/web/dist/dev.js.map +1 -0
- package/node_modules/@dx/web/dist/link.d.ts +45 -0
- package/node_modules/@dx/web/dist/link.d.ts.map +1 -0
- package/node_modules/@dx/web/dist/link.js +150 -0
- package/node_modules/@dx/web/dist/link.js.map +1 -0
- package/node_modules/@dx/web/dist/proxy.d.ts +21 -0
- package/node_modules/@dx/web/dist/proxy.d.ts.map +1 -0
- package/node_modules/@dx/web/dist/proxy.js +140 -0
- package/node_modules/@dx/web/dist/proxy.js.map +1 -0
- package/node_modules/@dx/web/dist/server.d.ts +33 -0
- package/node_modules/@dx/web/dist/server.d.ts.map +1 -0
- package/node_modules/@dx/web/dist/server.js +184 -0
- package/node_modules/@dx/web/dist/server.js.map +1 -0
- package/node_modules/@dx/web/package.json +26 -0
- package/node_modules/@dx/web/public/app.js +1553 -0
- package/node_modules/@dx/web/public/icon.svg +1 -0
- package/node_modules/@dx/web/public/index.html +47 -0
- package/node_modules/@dx/web/public/styles.css +1124 -0
- package/package.json +52 -4
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { detectStack, ErrorTracker, getProjectPort, isPortOpen, killProcess, mainService, recordRun, serviceName, splitRunnerLine, startOrder, } from "@dx/core";
|
|
3
|
+
const MAX_LINES = 2000;
|
|
4
|
+
// eslint-disable-next-line no-control-regex
|
|
5
|
+
const ANSI = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*\x07/g;
|
|
6
|
+
const URL_IN_TEXT = /(https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):(\d{2,5}))/;
|
|
7
|
+
const procs = new Map();
|
|
8
|
+
const stacks = new Map();
|
|
9
|
+
let lineCounter = 0;
|
|
10
|
+
let localPort;
|
|
11
|
+
// ------------------------------------------------------------ local domains
|
|
12
|
+
/** Tells the dev servers the local domains proxy's port, or undefined when it isn't running. */
|
|
13
|
+
export function setLocalDomainsPort(port) {
|
|
14
|
+
localPort = port;
|
|
15
|
+
}
|
|
16
|
+
function localUrlOf(host) {
|
|
17
|
+
if (localPort === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
return `http://${host}${localPort === 80 ? "" : `:${localPort}`}`;
|
|
20
|
+
}
|
|
21
|
+
/** Every *.localhost name dx serves, and the address it points at. */
|
|
22
|
+
export function localRoutes() {
|
|
23
|
+
const routes = new Map();
|
|
24
|
+
for (const p of procs.values()) {
|
|
25
|
+
if (p.stackKey || !p.child || !p.url)
|
|
26
|
+
continue;
|
|
27
|
+
routes.set(`${serviceName(p.project)}.localhost`, { url: p.url, dir: p.dir });
|
|
28
|
+
}
|
|
29
|
+
for (const run of stacks.values()) {
|
|
30
|
+
if (run.phase === "stopped")
|
|
31
|
+
continue;
|
|
32
|
+
const base = serviceName(run.name);
|
|
33
|
+
const main = mainService(run.stack)?.name;
|
|
34
|
+
for (const svc of serviceInfos(run)) {
|
|
35
|
+
if (!svc.url || svc.status === "stopped" || svc.status === "failed")
|
|
36
|
+
continue;
|
|
37
|
+
routes.set(`${svc.service}.${base}.localhost`, { url: svc.url, dir: run.dir, service: svc.service });
|
|
38
|
+
if (svc.service === main)
|
|
39
|
+
routes.set(`${base}.localhost`, { url: svc.url, dir: run.dir, service: svc.service });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return routes;
|
|
43
|
+
}
|
|
44
|
+
// ------------------------------------------------------------ reading
|
|
45
|
+
export function listDevServers() {
|
|
46
|
+
const singles = [...procs.values()].filter((p) => !p.stackKey).map(info);
|
|
47
|
+
const stackInfos = [...stacks.values()].map(stackInfo);
|
|
48
|
+
return [...singles, ...stackInfos].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
49
|
+
}
|
|
50
|
+
export function devServer(dir) {
|
|
51
|
+
const run = stacks.get(key(dir));
|
|
52
|
+
if (run)
|
|
53
|
+
return stackInfo(run);
|
|
54
|
+
const p = procs.get(key(dir));
|
|
55
|
+
return p && info(p);
|
|
56
|
+
}
|
|
57
|
+
/** The process (or stack service) listening on a port, for matching a request to its server. */
|
|
58
|
+
export function serverForPort(port) {
|
|
59
|
+
for (const p of procs.values()) {
|
|
60
|
+
if (!p.child && !p.docker)
|
|
61
|
+
continue;
|
|
62
|
+
const urls = p.split ? [...p.subUrls.entries()] : p.url ? [[p.service ?? "", p.url]] : [];
|
|
63
|
+
for (const [service, url] of urls) {
|
|
64
|
+
if (portOf(url) !== port)
|
|
65
|
+
continue;
|
|
66
|
+
const run = p.stackKey ? stacks.get(p.stackKey) : undefined;
|
|
67
|
+
return { dir: run?.dir ?? p.dir, service: service || undefined, project: run?.name ?? p.project };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
export function devLogs(dir, after = 0, service) {
|
|
73
|
+
const list = procsFor(dir);
|
|
74
|
+
if (!list.length)
|
|
75
|
+
return { lines: [] };
|
|
76
|
+
const lines = list
|
|
77
|
+
.flatMap((p) => p.lines.filter((l) => l.n > after && (!service || (l.service ?? p.service) === service)).map((l) => (p.service && !l.service ? { ...l, service: p.service } : l)))
|
|
78
|
+
.sort((a, b) => a.n - b.n);
|
|
79
|
+
return { lines, server: devServer(dir) };
|
|
80
|
+
}
|
|
81
|
+
/** Errors found in one server's output (a stack's services included), or in every server's when `dir` is empty. */
|
|
82
|
+
export function devErrors(dir, options = {}) {
|
|
83
|
+
const tops = dir ? [devServer(dir)].filter((x) => !!x) : listDevServers();
|
|
84
|
+
return tops.map((top) => ({
|
|
85
|
+
project: top.project,
|
|
86
|
+
dir: top.dir,
|
|
87
|
+
errors: errorsOf(procsFor(top.dir), options),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
function errorsOf(list, options) {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const p of list) {
|
|
93
|
+
for (const [svc, tracker] of p.trackers) {
|
|
94
|
+
tracker.flush();
|
|
95
|
+
const service = svc || p.service;
|
|
96
|
+
if (options.service && service !== options.service)
|
|
97
|
+
continue;
|
|
98
|
+
for (const e of tracker.errors(options))
|
|
99
|
+
out.push(service ? { ...e, service } : e);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out.sort((a, b) => b.lastLine - a.lastLine);
|
|
103
|
+
}
|
|
104
|
+
// ------------------------------------------------------------ starting
|
|
105
|
+
/** Starts a project: its stack when it has one, else its dev command. */
|
|
106
|
+
export function startDevServer(project, startedBy = "dashboard") {
|
|
107
|
+
const stack = detectStack(project);
|
|
108
|
+
if (stack)
|
|
109
|
+
return startStack(project, stack, startedBy);
|
|
110
|
+
if (!project.devCommand)
|
|
111
|
+
throw new Error(`${project.name} has no dev command. Add one in .devrc.json: { "dev": "..." }`);
|
|
112
|
+
const existing = procs.get(key(project.dir));
|
|
113
|
+
if (existing && (existing.status === "starting" || existing.status === "ready"))
|
|
114
|
+
return info(existing);
|
|
115
|
+
const p = spawnProc({
|
|
116
|
+
key: key(project.dir),
|
|
117
|
+
project: project.name,
|
|
118
|
+
dir: project.dir,
|
|
119
|
+
command: project.devCommand,
|
|
120
|
+
url: project.url,
|
|
121
|
+
port: getProjectPort(project),
|
|
122
|
+
startedBy,
|
|
123
|
+
});
|
|
124
|
+
return info(p);
|
|
125
|
+
}
|
|
126
|
+
function startStack(project, stack, startedBy) {
|
|
127
|
+
const k = key(project.dir);
|
|
128
|
+
const existing = stacks.get(k);
|
|
129
|
+
if (existing && (existing.phase === "starting" || existing.phase === "running"))
|
|
130
|
+
return stackInfo(existing);
|
|
131
|
+
for (const p of [...procs.values()])
|
|
132
|
+
if (p.stackKey === k && !p.child)
|
|
133
|
+
procs.delete(p.key);
|
|
134
|
+
const run = { key: k, dir: project.dir, name: project.name, stack, startedBy, phase: "starting", startedAt: new Date().toISOString() };
|
|
135
|
+
stacks.set(k, run);
|
|
136
|
+
void runStack(run).catch((err) => {
|
|
137
|
+
run.note = `Couldn't start the stack: ${err.message}`;
|
|
138
|
+
run.phase = "running";
|
|
139
|
+
});
|
|
140
|
+
return stackInfo(run);
|
|
141
|
+
}
|
|
142
|
+
async function runStack(run) {
|
|
143
|
+
const { stack } = run;
|
|
144
|
+
const crashed = new Set();
|
|
145
|
+
const skipped = [];
|
|
146
|
+
const docker = stack.services.filter((s) => s.kind === "docker");
|
|
147
|
+
const waves = stack.runner ? [...(docker.length ? [docker] : []), "runner"] : startOrder(stack);
|
|
148
|
+
const unavailable = (d) => crashed.has(d) || skipped.some((s) => s.startsWith(`${d} `));
|
|
149
|
+
for (const wave of waves) {
|
|
150
|
+
if (run.phase !== "starting")
|
|
151
|
+
return;
|
|
152
|
+
const started = [];
|
|
153
|
+
for (const svc of wave === "runner" ? [] : wave) {
|
|
154
|
+
const blockedBy = svc.dependsOn.filter(unavailable);
|
|
155
|
+
if (blockedBy.length) {
|
|
156
|
+
skipped.push(`${svc.name} (needs ${blockedBy.join(", ")})`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
started.push(startService(run, svc));
|
|
160
|
+
}
|
|
161
|
+
if (wave === "runner" && stack.runner) {
|
|
162
|
+
// Every app service through the project's own runner, after the databases.
|
|
163
|
+
started.push(spawnProc({
|
|
164
|
+
key: `${run.key}#*`,
|
|
165
|
+
project: run.name,
|
|
166
|
+
dir: stack.dir,
|
|
167
|
+
command: stack.runner.command,
|
|
168
|
+
startedBy: run.startedBy,
|
|
169
|
+
stackKey: run.key,
|
|
170
|
+
split: stack.services.filter((s) => s.kind === "process"),
|
|
171
|
+
env: urlEnv(run),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
await Promise.all(started.map((p) => waitSettled(p, 120_000)));
|
|
175
|
+
for (const p of started)
|
|
176
|
+
if (p.status === "failed" && p.service)
|
|
177
|
+
crashed.add(p.service);
|
|
178
|
+
}
|
|
179
|
+
if (run.phase !== "starting")
|
|
180
|
+
return;
|
|
181
|
+
run.phase = "running";
|
|
182
|
+
const notes = [];
|
|
183
|
+
if (crashed.size)
|
|
184
|
+
notes.push(`Failed to start: ${[...crashed].join(", ")}.`);
|
|
185
|
+
if (skipped.length)
|
|
186
|
+
notes.push(`Not started: ${skipped.join("; ")}.`);
|
|
187
|
+
run.note = notes.length ? notes.join(" ") : undefined;
|
|
188
|
+
recordRun("go", crashed.size === 0, `${run.name} stack started (${stack.services.length} services)`, "web", { project: run.name });
|
|
189
|
+
}
|
|
190
|
+
function startService(run, svc) {
|
|
191
|
+
const env = { ...(svc.env ?? {}), ...urlEnv(run) };
|
|
192
|
+
delete env.DX_LABEL;
|
|
193
|
+
return spawnProc({
|
|
194
|
+
key: `${run.key}#${svc.name}`,
|
|
195
|
+
project: run.name,
|
|
196
|
+
dir: svc.dir,
|
|
197
|
+
command: svc.command,
|
|
198
|
+
url: svc.kind === "docker" ? undefined : svc.url,
|
|
199
|
+
port: svc.kind === "docker" ? undefined : portOf(svc.url),
|
|
200
|
+
startedBy: run.startedBy,
|
|
201
|
+
stackKey: run.key,
|
|
202
|
+
service: svc.name,
|
|
203
|
+
role: svc.role,
|
|
204
|
+
env,
|
|
205
|
+
docker: svc.kind === "docker" ? { compose: svc.composeService ?? svc.name, hostPort: svc.hostPort } : undefined,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Where the stack's other services are: DX_API_URL is the direct address (for server code, since
|
|
210
|
+
* not every system resolves *.localhost), DX_API_PUBLIC_URL the local domain (for the browser).
|
|
211
|
+
*/
|
|
212
|
+
function urlEnv(run) {
|
|
213
|
+
const env = {};
|
|
214
|
+
const base = serviceName(run.name);
|
|
215
|
+
for (const svc of serviceInfos(run)) {
|
|
216
|
+
if (!svc.url || svc.status !== "ready" || !svc.service)
|
|
217
|
+
continue;
|
|
218
|
+
const name = svc.service.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
219
|
+
env[`DX_${name}_URL`] = svc.url;
|
|
220
|
+
const local = svc.kind === "process" ? localUrlOf(`${svc.service}.${base}.localhost`) : undefined;
|
|
221
|
+
if (local)
|
|
222
|
+
env[`DX_${name}_PUBLIC_URL`] = local;
|
|
223
|
+
}
|
|
224
|
+
return env;
|
|
225
|
+
}
|
|
226
|
+
function spawnProc(spec) {
|
|
227
|
+
const p = {
|
|
228
|
+
...spec,
|
|
229
|
+
status: "starting",
|
|
230
|
+
startedAt: new Date().toISOString(),
|
|
231
|
+
lastLine: 0,
|
|
232
|
+
lines: [],
|
|
233
|
+
trackers: new Map([["", new ErrorTracker()]]),
|
|
234
|
+
lastOutputAt: Date.now(),
|
|
235
|
+
subUrls: new Map(),
|
|
236
|
+
};
|
|
237
|
+
procs.set(spec.key, p);
|
|
238
|
+
push(p, "dx", `$ ${spec.command}`);
|
|
239
|
+
const child = spawn(spec.command, {
|
|
240
|
+
cwd: spec.dir,
|
|
241
|
+
shell: true,
|
|
242
|
+
windowsHide: true,
|
|
243
|
+
// Its own process group on macOS/Linux, so Stop can end the whole tree (npm → node → vite).
|
|
244
|
+
detached: process.platform !== "win32",
|
|
245
|
+
env: { ...process.env, FORCE_COLOR: "0", BROWSER: "none", ...spec.env },
|
|
246
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
247
|
+
});
|
|
248
|
+
p.child = child;
|
|
249
|
+
p.pid = child.pid;
|
|
250
|
+
child.stdout?.on("data", (d) => pushChunk(p, "out", d));
|
|
251
|
+
child.stderr?.on("data", (d) => pushChunk(p, "err", d));
|
|
252
|
+
child.once("error", (err) => {
|
|
253
|
+
p.status = "failed";
|
|
254
|
+
push(p, "dx", `Couldn't start: ${err.message}`);
|
|
255
|
+
});
|
|
256
|
+
child.once("exit", (code, signal) => {
|
|
257
|
+
p.exitCode = code;
|
|
258
|
+
p.child = undefined;
|
|
259
|
+
const ms = Date.now() - Date.parse(p.startedAt);
|
|
260
|
+
if (p.docker && !p.stopping) {
|
|
261
|
+
// `docker compose up -d` finishes once the container runs; the service is ready when its port opens.
|
|
262
|
+
if (code !== 0) {
|
|
263
|
+
p.status = "failed";
|
|
264
|
+
push(p, "dx", `docker compose exited with ${code ?? signal}. Is Docker running?`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
void (async () => {
|
|
268
|
+
if (p.docker?.hostPort) {
|
|
269
|
+
const deadline = Date.now() + 60_000;
|
|
270
|
+
while (Date.now() < deadline && !(await isPortOpen(p.docker.hostPort)))
|
|
271
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
272
|
+
}
|
|
273
|
+
if (p.status === "starting") {
|
|
274
|
+
p.status = "ready";
|
|
275
|
+
p.url = p.docker?.hostPort ? `http://localhost:${p.docker.hostPort}` : undefined;
|
|
276
|
+
push(p, "dx", p.docker?.hostPort ? `Container ready on port ${p.docker.hostPort}.` : "Container started.");
|
|
277
|
+
}
|
|
278
|
+
})();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
p.stoppedAt = new Date().toISOString();
|
|
282
|
+
const label = p.service ? `${p.project}/${p.service}` : p.project;
|
|
283
|
+
if (p.stopping) {
|
|
284
|
+
p.status = "stopped";
|
|
285
|
+
push(p, "dx", "Stopped.");
|
|
286
|
+
if (!p.stackKey)
|
|
287
|
+
recordRun("go", true, `${label} dev stopped from the dashboard`, "web", { project: p.project, ms });
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
p.status = code === 0 ? "stopped" : "failed";
|
|
291
|
+
push(p, "dx", `Exited with ${code ?? signal}.`);
|
|
292
|
+
recordRun("go", code === 0, `${label} dev exited with ${code ?? signal}`, "web", { project: p.project, ms });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
if (!p.docker) {
|
|
296
|
+
void (async () => {
|
|
297
|
+
// If something else already holds the usual port, the dev server will pick another one and say so.
|
|
298
|
+
if (p.port !== undefined && (await isPortOpen(p.port))) {
|
|
299
|
+
push(p, "dx", `Port ${p.port} is already in use by another program; waiting for the dev server to say where it runs.`);
|
|
300
|
+
p.port = undefined;
|
|
301
|
+
p.url = undefined;
|
|
302
|
+
}
|
|
303
|
+
await watchReady(p);
|
|
304
|
+
})();
|
|
305
|
+
}
|
|
306
|
+
return p;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Marks a process ready once its port opens. One that never says where it listens (a worker, a
|
|
310
|
+
* watcher) counts as ready when it's still running and its output has gone quiet.
|
|
311
|
+
*/
|
|
312
|
+
async function watchReady(p) {
|
|
313
|
+
const deadline = Date.now() + 180_000;
|
|
314
|
+
while (Date.now() < deadline && p.status === "starting" && p.child) {
|
|
315
|
+
if (p.port !== undefined && (await isPortOpen(p.port))) {
|
|
316
|
+
if (p.status === "starting") {
|
|
317
|
+
p.status = "ready";
|
|
318
|
+
push(p, "dx", `Ready at ${p.url}`);
|
|
319
|
+
if (!p.stackKey)
|
|
320
|
+
recordRun("go", true, `${p.project} started from the dashboard`, "web", { project: p.project });
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const age = Date.now() - Date.parse(p.startedAt);
|
|
325
|
+
const quiet = Date.now() - p.lastOutputAt;
|
|
326
|
+
if (p.port === undefined && age > 5000 && quiet > 2500) {
|
|
327
|
+
p.status = "ready";
|
|
328
|
+
push(p, "dx", p.split ? "Running." : "Running (it didn't say where it listens).");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** Waits until a process is ready, failed or gone. */
|
|
335
|
+
async function waitSettled(p, timeoutMs) {
|
|
336
|
+
const deadline = Date.now() + timeoutMs;
|
|
337
|
+
while (p.status === "starting" && Date.now() < deadline)
|
|
338
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
339
|
+
}
|
|
340
|
+
// ------------------------------------------------------------ stopping
|
|
341
|
+
export async function stopDevServer(dir) {
|
|
342
|
+
const run = stacks.get(key(dir));
|
|
343
|
+
if (run) {
|
|
344
|
+
await stopStack(run);
|
|
345
|
+
return stackInfo(run);
|
|
346
|
+
}
|
|
347
|
+
const p = procs.get(key(dir));
|
|
348
|
+
if (p)
|
|
349
|
+
await stopProc(p);
|
|
350
|
+
return p && info(p);
|
|
351
|
+
}
|
|
352
|
+
async function stopProc(p) {
|
|
353
|
+
if (!p.child?.pid)
|
|
354
|
+
return;
|
|
355
|
+
p.stopping = true;
|
|
356
|
+
push(p, "dx", "Stopping…");
|
|
357
|
+
if (process.platform === "win32") {
|
|
358
|
+
await killProcess(p.child.pid);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
try {
|
|
362
|
+
process.kill(-p.child.pid, "SIGTERM");
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
p.child.kill("SIGTERM");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function stopStack(run) {
|
|
370
|
+
if (run.phase === "stopped")
|
|
371
|
+
return;
|
|
372
|
+
run.phase = "stopping";
|
|
373
|
+
const list = procsFor(run.dir);
|
|
374
|
+
await Promise.all(list.filter((p) => !p.docker).map(stopProc));
|
|
375
|
+
// Containers keep running until asked: stop the ones this stack started.
|
|
376
|
+
const docker = list.filter((p) => p.docker && p.status !== "failed");
|
|
377
|
+
if (docker.length) {
|
|
378
|
+
for (const p of docker)
|
|
379
|
+
push(p, "dx", "Stopping the container…");
|
|
380
|
+
const code = await new Promise((resolve) => {
|
|
381
|
+
const child = spawn(`docker compose stop ${docker.map((p) => p.docker.compose).join(" ")}`, { cwd: run.stack.composeFile ? dirOf(run.stack.composeFile) : run.dir, shell: true, windowsHide: true, stdio: "ignore" });
|
|
382
|
+
child.once("error", () => resolve(1));
|
|
383
|
+
child.once("exit", (c) => resolve(c ?? 1));
|
|
384
|
+
});
|
|
385
|
+
for (const p of docker) {
|
|
386
|
+
p.status = "stopped";
|
|
387
|
+
p.stoppedAt = new Date().toISOString();
|
|
388
|
+
push(p, "dx", code === 0 ? "Container stopped." : "docker compose stop failed; the container may still be running.");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const exited = list.filter((p) => p.child).map((p) => new Promise((resolve) => p.child?.once("exit", () => resolve())));
|
|
392
|
+
await Promise.race([Promise.all(exited), new Promise((r) => setTimeout(r, 10_000))]);
|
|
393
|
+
run.phase = "stopped";
|
|
394
|
+
run.stoppedAt = new Date().toISOString();
|
|
395
|
+
recordRun("go", true, `${run.name} stack stopped`, "web", { project: run.name, ms: Date.now() - Date.parse(run.startedAt) });
|
|
396
|
+
}
|
|
397
|
+
/** Stops every dev server; called when the dashboard shuts down. */
|
|
398
|
+
export async function stopAllDevServers() {
|
|
399
|
+
await Promise.all([...stacks.values()].map(stopStack));
|
|
400
|
+
await Promise.all([...procs.values()].filter((p) => p.child && !p.stackKey).map(stopProc));
|
|
401
|
+
}
|
|
402
|
+
/** Stops the server (or stack), waits for it to exit, and starts it again. */
|
|
403
|
+
export async function restartDevServer(project, startedBy = "dashboard") {
|
|
404
|
+
const k = key(project.dir);
|
|
405
|
+
const run = stacks.get(k);
|
|
406
|
+
const by = run?.startedBy ?? procs.get(k)?.startedBy ?? startedBy;
|
|
407
|
+
if (run) {
|
|
408
|
+
await stopStack(run);
|
|
409
|
+
clearDevServer(project.dir);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const p = procs.get(k);
|
|
413
|
+
if (p?.child) {
|
|
414
|
+
const exited = new Promise((resolve) => p.child?.once("exit", () => resolve()));
|
|
415
|
+
await stopProc(p);
|
|
416
|
+
await Promise.race([exited, new Promise((r) => setTimeout(r, 10_000))]);
|
|
417
|
+
}
|
|
418
|
+
procs.delete(k);
|
|
419
|
+
}
|
|
420
|
+
return startDevServer(project, by);
|
|
421
|
+
}
|
|
422
|
+
/** Forgets a stopped server or stack, so its card and output go away. */
|
|
423
|
+
export function clearDevServer(dir) {
|
|
424
|
+
const k = key(dir);
|
|
425
|
+
const run = stacks.get(k);
|
|
426
|
+
if (run) {
|
|
427
|
+
if (run.phase !== "stopped")
|
|
428
|
+
return;
|
|
429
|
+
for (const p of [...procs.values()])
|
|
430
|
+
if (p.stackKey === k)
|
|
431
|
+
procs.delete(p.key);
|
|
432
|
+
stacks.delete(k);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const p = procs.get(k);
|
|
436
|
+
if (p && !p.child)
|
|
437
|
+
procs.delete(k);
|
|
438
|
+
}
|
|
439
|
+
/** True while any dev server or stack is running. */
|
|
440
|
+
export function anyDevServerRunning() {
|
|
441
|
+
return [...procs.values()].some((p) => p.child) || [...stacks.values()].some((r) => r.phase !== "stopped");
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Waits for a dev server (or a stack's services) to settle, then says what they printed.
|
|
445
|
+
* `until: "ready"` waits for starting to finish; `until: "quiet"` waits for output to begin (a
|
|
446
|
+
* rebuild after a file change), then for it to stop for `quietMs`.
|
|
447
|
+
*/
|
|
448
|
+
export async function waitForDevServer(dir, options = {}) {
|
|
449
|
+
const quietMs = options.quietMs ?? 1000;
|
|
450
|
+
const settleMs = options.settleMs ?? 4000;
|
|
451
|
+
const deadline = Date.now() + (options.timeoutMs ?? 60_000);
|
|
452
|
+
const sleep = () => new Promise((r) => setTimeout(r, 100));
|
|
453
|
+
const last = () => Math.max(0, ...procsFor(dir).map((p) => p.lastLine));
|
|
454
|
+
const after = options.after ?? last();
|
|
455
|
+
const lastOutput = () => Math.max(0, ...procsFor(dir).map((p) => p.lastOutputAt));
|
|
456
|
+
const alive = () => procsFor(dir).some((p) => p.child);
|
|
457
|
+
const run = stacks.get(key(dir));
|
|
458
|
+
let timedOut = false;
|
|
459
|
+
if (procsFor(dir).length || run) {
|
|
460
|
+
const begun = Date.now();
|
|
461
|
+
if (options.until === "ready") {
|
|
462
|
+
while ((run?.phase === "starting" || procsFor(dir).some((p) => p.status === "starting")) && Date.now() < deadline)
|
|
463
|
+
await sleep();
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
while (last() <= after && alive() && Date.now() - begun < settleMs && Date.now() < deadline)
|
|
467
|
+
await sleep();
|
|
468
|
+
}
|
|
469
|
+
while (alive() && last() > after && Date.now() - lastOutput() < quietMs && Date.now() < deadline)
|
|
470
|
+
await sleep();
|
|
471
|
+
timedOut = Date.now() >= deadline;
|
|
472
|
+
}
|
|
473
|
+
const list = procsFor(dir);
|
|
474
|
+
for (const p of list)
|
|
475
|
+
clearTimeout(p.flushTimer);
|
|
476
|
+
const lines = devLogs(dir, after).lines.slice(-200);
|
|
477
|
+
return {
|
|
478
|
+
server: devServer(dir),
|
|
479
|
+
lines,
|
|
480
|
+
newErrors: errorsOf(list, { after }),
|
|
481
|
+
errors: errorsOf(list, {}),
|
|
482
|
+
sawOutput: lines.length > 0,
|
|
483
|
+
timedOut,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
// ------------------------------------------------------------ output
|
|
487
|
+
const partial = new WeakMap();
|
|
488
|
+
function pushChunk(p, stream, chunk) {
|
|
489
|
+
const carry = partial.get(p) ?? {};
|
|
490
|
+
const text = (carry[stream] ?? "") + chunk.toString("utf8");
|
|
491
|
+
const parts = text.split(/\r?\n/);
|
|
492
|
+
carry[stream] = parts.pop() ?? "";
|
|
493
|
+
partial.set(p, carry);
|
|
494
|
+
for (const line of parts)
|
|
495
|
+
push(p, stream, line);
|
|
496
|
+
}
|
|
497
|
+
function push(p, stream, raw) {
|
|
498
|
+
let text = raw.replace(ANSI, "").replace(/\r/g, "");
|
|
499
|
+
let service;
|
|
500
|
+
if (p.split && stream !== "dx") {
|
|
501
|
+
const split = splitRunnerLine(text, p.split);
|
|
502
|
+
service = split.service;
|
|
503
|
+
text = split.text;
|
|
504
|
+
}
|
|
505
|
+
p.lastLine = ++lineCounter;
|
|
506
|
+
const line = service ? { n: p.lastLine, stream, text, service } : { n: p.lastLine, stream, text };
|
|
507
|
+
p.lines.push(line);
|
|
508
|
+
if (p.lines.length > MAX_LINES)
|
|
509
|
+
p.lines.splice(0, p.lines.length - MAX_LINES);
|
|
510
|
+
p.lastOutputAt = Date.now();
|
|
511
|
+
let tracker = p.trackers.get(service ?? "");
|
|
512
|
+
if (!tracker)
|
|
513
|
+
p.trackers.set(service ?? "", (tracker = new ErrorTracker()));
|
|
514
|
+
tracker.push(line);
|
|
515
|
+
// An error's last line is only known once the output pauses.
|
|
516
|
+
clearTimeout(p.flushTimer);
|
|
517
|
+
p.flushTimer = setTimeout(() => {
|
|
518
|
+
for (const t of p.trackers.values())
|
|
519
|
+
t.flush();
|
|
520
|
+
}, 300);
|
|
521
|
+
p.flushTimer.unref?.();
|
|
522
|
+
// The URL a dev server prints wins over the guess: it may have moved to another port.
|
|
523
|
+
if (stream === "dx")
|
|
524
|
+
return;
|
|
525
|
+
const m = URL_IN_TEXT.exec(text);
|
|
526
|
+
if (!m)
|
|
527
|
+
return;
|
|
528
|
+
// "Connecting to the API at http://localhost:4000" names another server, not this one's address.
|
|
529
|
+
if (portInUseByOther(p, Number(m[2])))
|
|
530
|
+
return;
|
|
531
|
+
const url = m[1].replace("[::1]", "localhost").replace("127.0.0.1", "localhost");
|
|
532
|
+
if (p.split) {
|
|
533
|
+
if (service && !p.subUrls.has(service))
|
|
534
|
+
p.subUrls.set(service, url);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if ((p.status === "starting" && Number(m[2]) !== p.port) || (!p.url && p.child)) {
|
|
538
|
+
p.url = url;
|
|
539
|
+
p.port = Number(m[2]);
|
|
540
|
+
void watchReady(p);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// ------------------------------------------------------------ info
|
|
544
|
+
function info(p) {
|
|
545
|
+
const { project, dir, command, url, pid, status, startedBy, startedAt, stoppedAt, exitCode, lastLine, service, role } = p;
|
|
546
|
+
const errorCount = errorsOf([p], {}).length;
|
|
547
|
+
const out = { project, dir, command, url, pid, status, startedBy, errorCount, startedAt, stoppedAt, exitCode, lastLine, service, role };
|
|
548
|
+
if (!p.stackKey && url && p.child)
|
|
549
|
+
out.localUrl = localUrlOf(`${serviceName(project)}.localhost`);
|
|
550
|
+
return out;
|
|
551
|
+
}
|
|
552
|
+
/** A stack's services as the page and agents see them, runner services included. */
|
|
553
|
+
function serviceInfos(run) {
|
|
554
|
+
const base = serviceName(run.name);
|
|
555
|
+
const list = procsFor(run.dir);
|
|
556
|
+
const runner = list.find((p) => p.split);
|
|
557
|
+
return run.stack.services.map((svc) => {
|
|
558
|
+
const p = list.find((x) => x.service === svc.name);
|
|
559
|
+
let i;
|
|
560
|
+
if (p) {
|
|
561
|
+
i = info(p);
|
|
562
|
+
}
|
|
563
|
+
else if (runner && svc.kind === "process") {
|
|
564
|
+
const r = info(runner);
|
|
565
|
+
i = { ...r, command: svc.command, url: runner.subUrls.get(svc.name), errorCount: errorsOf([runner], { service: svc.name }).length, lastLine: r.lastLine };
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
i = { project: run.name, dir: svc.dir, command: svc.command, status: run.phase === "starting" ? "starting" : "stopped", startedBy: run.startedBy, errorCount: 0, startedAt: run.startedAt, lastLine: 0 };
|
|
569
|
+
}
|
|
570
|
+
const alive = i.status === "starting" || i.status === "ready";
|
|
571
|
+
return {
|
|
572
|
+
...i,
|
|
573
|
+
project: run.name,
|
|
574
|
+
service: svc.name,
|
|
575
|
+
role: svc.role,
|
|
576
|
+
kind: svc.kind,
|
|
577
|
+
localUrl: svc.kind === "process" && i.url && alive ? localUrlOf(`${svc.name}.${base}.localhost`) : undefined,
|
|
578
|
+
};
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
function stackInfo(run) {
|
|
582
|
+
const services = serviceInfos(run);
|
|
583
|
+
const main = mainService(run.stack);
|
|
584
|
+
const mainInfo = services.find((s) => s.service === main?.name);
|
|
585
|
+
let status;
|
|
586
|
+
if (run.phase === "starting")
|
|
587
|
+
status = "starting";
|
|
588
|
+
else if (run.phase === "stopped" || run.phase === "stopping")
|
|
589
|
+
status = "stopped";
|
|
590
|
+
else if (services.some((s) => s.status === "failed"))
|
|
591
|
+
status = "failed";
|
|
592
|
+
else if (services.some((s) => s.status === "starting"))
|
|
593
|
+
status = "starting";
|
|
594
|
+
// A watcher that finished (exit 0) doesn't make the stack stopped while the others run.
|
|
595
|
+
else
|
|
596
|
+
status = services.some((s) => s.status === "ready") ? "ready" : "stopped";
|
|
597
|
+
const base = serviceName(run.name);
|
|
598
|
+
return {
|
|
599
|
+
project: run.name,
|
|
600
|
+
dir: run.dir,
|
|
601
|
+
command: run.stack.runner?.command ?? `${services.length} services`,
|
|
602
|
+
url: mainInfo?.url,
|
|
603
|
+
localUrl: mainInfo?.url && status !== "stopped" ? localUrlOf(`${base}.localhost`) : undefined,
|
|
604
|
+
status,
|
|
605
|
+
startedBy: run.startedBy,
|
|
606
|
+
errorCount: services.reduce((n, s) => n + s.errorCount, 0),
|
|
607
|
+
startedAt: run.startedAt,
|
|
608
|
+
stoppedAt: run.stoppedAt,
|
|
609
|
+
lastLine: Math.max(0, ...procsFor(run.dir).map((p) => p.lastLine)),
|
|
610
|
+
services: services.map(({ kind: _kind, ...s }) => s),
|
|
611
|
+
stackSource: run.stack.source,
|
|
612
|
+
note: run.note,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function portInUseByOther(p, port) {
|
|
616
|
+
for (const other of procs.values()) {
|
|
617
|
+
if (other === p || (!other.child && !other.docker))
|
|
618
|
+
continue;
|
|
619
|
+
if (other.port === port || portOf(other.url) === port || [...other.subUrls.values()].some((u) => portOf(u) === port))
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
function procsFor(dir) {
|
|
625
|
+
const k = key(dir);
|
|
626
|
+
return [...procs.values()].filter((p) => p.key === k || p.stackKey === k);
|
|
627
|
+
}
|
|
628
|
+
function portOf(url) {
|
|
629
|
+
if (!url)
|
|
630
|
+
return undefined;
|
|
631
|
+
try {
|
|
632
|
+
const u = new URL(url);
|
|
633
|
+
return Number(u.port || (u.protocol === "https:" ? 443 : 80));
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function dirOf(file) {
|
|
640
|
+
return file.replace(/[\\/][^\\/]+$/, "");
|
|
641
|
+
}
|
|
642
|
+
function key(dir) {
|
|
643
|
+
return process.platform === "win32" ? dir.toLowerCase() : dir;
|
|
644
|
+
}
|
|
645
|
+
//# sourceMappingURL=dev.js.map
|