@syntax-syllogism/sfd 0.0.0-stage → 0.2.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 +29 -0
- package/LICENSE +21 -0
- package/README.md +144 -2
- package/bin/sfd.mjs +220 -0
- package/package.json +43 -4
- package/src/client.mjs +85 -0
- package/src/daemon.mjs +43 -0
- package/src/format.mjs +138 -0
- package/src/handlers.mjs +149 -0
- package/src/hooks.mjs +102 -0
- package/src/paths.mjs +16 -0
- package/src/project.mjs +33 -0
- package/src/rewrite.mjs +173 -0
- package/src/server.mjs +99 -0
- package/src/sf-root.mjs +26 -0
- package/src/usage-error.mjs +1 -0
- package/src/version.mjs +10 -0
package/src/server.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { chmodSync, unlinkSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
// Newline-delimited JSON over a Unix socket: one request and one response per connection.
|
|
5
|
+
// Responses are {status: 0, result} | {status: 1, name, message} | {restart: true}.
|
|
6
|
+
|
|
7
|
+
export class AlreadyRunning extends Error {}
|
|
8
|
+
|
|
9
|
+
function probe(socket) {
|
|
10
|
+
return new Promise(resolve => {
|
|
11
|
+
const s = net.connect(socket, () => { s.destroy(); resolve(true); });
|
|
12
|
+
s.on("error", () => resolve(false));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function listenOnce(server, socket) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
server.once("error", reject);
|
|
19
|
+
server.listen(socket, () => { server.off("error", reject); resolve(); });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function startServer({ socket, version, handlers, idleMs = 60 * 60_000, isStale = () => false, onExit = () => {} }) {
|
|
24
|
+
let active = 0;
|
|
25
|
+
let idleTimer;
|
|
26
|
+
let draining;
|
|
27
|
+
let exited = false;
|
|
28
|
+
|
|
29
|
+
const maybeExit = () => {
|
|
30
|
+
if (draining && active === 0 && !exited) {
|
|
31
|
+
exited = true;
|
|
32
|
+
onExit(draining);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Stop accepting connections and free the socket path right away, so a replacement
|
|
37
|
+
// daemon can bind it, but let in-flight requests (deploys, test runs) finish.
|
|
38
|
+
const drain = reason => {
|
|
39
|
+
if (draining) return;
|
|
40
|
+
draining = reason;
|
|
41
|
+
clearTimeout(idleTimer);
|
|
42
|
+
server.close();
|
|
43
|
+
try { unlinkSync(socket); } catch {}
|
|
44
|
+
maybeExit();
|
|
45
|
+
};
|
|
46
|
+
const armIdle = () => {
|
|
47
|
+
clearTimeout(idleTimer);
|
|
48
|
+
idleTimer = setTimeout(() => active === 0 && drain("idle"), idleMs);
|
|
49
|
+
idleTimer.unref?.();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
async function respond(req) {
|
|
53
|
+
if (req.op === "stop") return { status: 0, result: { stopping: true } };
|
|
54
|
+
if (req.v !== version) return { restart: true };
|
|
55
|
+
const handler = handlers[req.op];
|
|
56
|
+
if (!handler) return { status: 1, name: "UnknownOperation", message: `Unknown operation: ${req.op}` };
|
|
57
|
+
try {
|
|
58
|
+
return { status: 0, result: await handler(req) };
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return { status: 1, name: e.name ?? "Error", message: e.message ?? String(e), errorCode: e.errorCode };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const server = net.createServer(conn => {
|
|
65
|
+
conn.setEncoding("utf8");
|
|
66
|
+
let buf = "";
|
|
67
|
+
conn.on("error", () => {});
|
|
68
|
+
conn.on("data", async chunk => {
|
|
69
|
+
buf += chunk;
|
|
70
|
+
const nl = buf.indexOf("\n");
|
|
71
|
+
if (nl < 0) return;
|
|
72
|
+
conn.removeAllListeners("data");
|
|
73
|
+
active++;
|
|
74
|
+
clearTimeout(idleTimer);
|
|
75
|
+
let req;
|
|
76
|
+
try { req = JSON.parse(buf.slice(0, nl)); } catch { req = {}; }
|
|
77
|
+
const res = await respond(req);
|
|
78
|
+
conn.end(JSON.stringify(res) + "\n");
|
|
79
|
+
active--;
|
|
80
|
+
if (req.op === "stop") drain("stop");
|
|
81
|
+
else if (res.restart) drain("version");
|
|
82
|
+
else if (isStale()) drain("stale");
|
|
83
|
+
if (draining) maybeExit();
|
|
84
|
+
else if (active === 0) armIdle();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await listenOnce(server, socket);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (e.code !== "EADDRINUSE") throw e;
|
|
92
|
+
if (await probe(socket)) throw new AlreadyRunning("sfd is already running");
|
|
93
|
+
unlinkSync(socket);
|
|
94
|
+
await listenOnce(server, socket);
|
|
95
|
+
}
|
|
96
|
+
chmodSync(socket, 0o600);
|
|
97
|
+
armIdle();
|
|
98
|
+
return { server, drain };
|
|
99
|
+
}
|
package/src/sf-root.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
|
|
6
|
+
// sfd loads @salesforce/core, SDR, apex-node and source-tracking from the installed sf CLI,
|
|
7
|
+
// so auth files, API versions and metadata registries always match what `sf` itself uses.
|
|
8
|
+
export function findSfRoot(env = process.env) {
|
|
9
|
+
if (env.SFD_SF_ROOT) return env.SFD_SF_ROOT;
|
|
10
|
+
const bin = execFileSync("which", ["sf"], { encoding: "utf8" }).trim();
|
|
11
|
+
for (let dir = dirname(realpathSync(bin)); dirname(dir) !== dir; dir = dirname(dir)) {
|
|
12
|
+
try {
|
|
13
|
+
if (JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name === "@salesforce/cli") return dir;
|
|
14
|
+
} catch {}
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Could not locate the @salesforce/cli install behind ${bin}; set SFD_SF_ROOT.`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sfInfo(root) {
|
|
20
|
+
const file = join(root, "package.json");
|
|
21
|
+
return { root, version: JSON.parse(readFileSync(file, "utf8")).version, mtimeMs: statSync(file).mtimeMs };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function sfRequire(root) {
|
|
25
|
+
return createRequire(join(root, "package.json"));
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class UsageError extends Error {}
|
package/src/version.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const pkg = new URL("../package.json", import.meta.url);
|
|
4
|
+
export const VERSION = JSON.parse(readFileSync(pkg, "utf8")).version;
|
|
5
|
+
|
|
6
|
+
// The client and daemon must run the same code, not just the same package version:
|
|
7
|
+
// fold the newest source mtime in, so an edited or reinstalled checkout replaces a running daemon.
|
|
8
|
+
const src = new URL("./", import.meta.url);
|
|
9
|
+
const newest = Math.max(...readdirSync(src).map(f => statSync(new URL(f, src)).mtimeMs));
|
|
10
|
+
export const PROTOCOL = `${VERSION}+${Math.floor(newest)}`;
|