fallow-code-scan 0.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/README.md +35 -0
- package/package.json +38 -0
- package/public/app-copy.js +31 -0
- package/public/app-findings.js +54 -0
- package/public/app-format.js +148 -0
- package/public/app-icons.js +32 -0
- package/public/app.js +395 -0
- package/public/index.html +97 -0
- package/public/styles.css +791 -0
- package/src/fallowBinary.js +68 -0
- package/src/fallowReport.js +336 -0
- package/src/paths.js +30 -0
- package/src/server.js +309 -0
- package/src/start.js +87 -0
package/src/server.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const fsSync = require("node:fs");
|
|
3
|
+
const http = require("node:http");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawn } = require("node:child_process");
|
|
6
|
+
const { URL } = require("node:url");
|
|
7
|
+
|
|
8
|
+
const { resolveFallowBinary } = require("./fallowBinary");
|
|
9
|
+
const { normalizeFallowReport } = require("./fallowReport");
|
|
10
|
+
const { PACKAGE_ROOT, PID_FILE_PATH, PROJECT_ROOT, resolvePublicPath } = require("./paths");
|
|
11
|
+
|
|
12
|
+
const HOST = process.env.HOST || "127.0.0.1";
|
|
13
|
+
const PORT = Number(process.env.PORT || 5179);
|
|
14
|
+
const FALLOW_ARGS = ["--format", "json", "--quiet"];
|
|
15
|
+
const COMMAND_LABEL = "fallow --format json --quiet";
|
|
16
|
+
const LUCIDE_SCRIPT_PATH = resolveLucideScriptPath();
|
|
17
|
+
|
|
18
|
+
let activeScan = null;
|
|
19
|
+
let latestSnapshot = {
|
|
20
|
+
state: "idle",
|
|
21
|
+
report: null,
|
|
22
|
+
error: null,
|
|
23
|
+
startedAt: null,
|
|
24
|
+
finishedAt: null
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const MIME_TYPES = new Map([
|
|
28
|
+
[".css", "text/css; charset=utf-8"],
|
|
29
|
+
[".html", "text/html; charset=utf-8"],
|
|
30
|
+
[".js", "text/javascript; charset=utf-8"],
|
|
31
|
+
[".json", "application/json; charset=utf-8"],
|
|
32
|
+
[".svg", "image/svg+xml"]
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const API_ROUTES = new Map([
|
|
36
|
+
["GET /api/health", handleHealthApi],
|
|
37
|
+
["GET /api/report", handleReportApi],
|
|
38
|
+
["POST /api/scan", handleScanApi]
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function sendJson(response, statusCode, payload) {
|
|
42
|
+
response.writeHead(statusCode, {
|
|
43
|
+
"cache-control": "no-store",
|
|
44
|
+
"content-type": "application/json; charset=utf-8"
|
|
45
|
+
});
|
|
46
|
+
response.end(`${JSON.stringify(payload, null, 2)}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function currentPayload() {
|
|
50
|
+
return {
|
|
51
|
+
...latestSnapshot,
|
|
52
|
+
running: Boolean(activeScan)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function startFallowScan() {
|
|
57
|
+
if (activeScan) return currentPayload();
|
|
58
|
+
|
|
59
|
+
const startedAt = new Date();
|
|
60
|
+
latestSnapshot = {
|
|
61
|
+
state: "running",
|
|
62
|
+
report: latestSnapshot.report,
|
|
63
|
+
error: null,
|
|
64
|
+
startedAt: startedAt.toISOString(),
|
|
65
|
+
finishedAt: null
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
activeScan = runFallow(startedAt)
|
|
69
|
+
.then((report) => {
|
|
70
|
+
latestSnapshot = {
|
|
71
|
+
state: "ready",
|
|
72
|
+
report,
|
|
73
|
+
error: null,
|
|
74
|
+
startedAt: latestSnapshot.startedAt,
|
|
75
|
+
finishedAt: new Date().toISOString()
|
|
76
|
+
};
|
|
77
|
+
})
|
|
78
|
+
.catch((error) => {
|
|
79
|
+
latestSnapshot = {
|
|
80
|
+
state: "failed",
|
|
81
|
+
report: latestSnapshot.report,
|
|
82
|
+
error: error.message || String(error),
|
|
83
|
+
startedAt: latestSnapshot.startedAt,
|
|
84
|
+
finishedAt: new Date().toISOString()
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
.finally(() => {
|
|
88
|
+
activeScan = null;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return currentPayload();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runFallow(startedAt) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
let command;
|
|
97
|
+
try {
|
|
98
|
+
command = resolveFallowBinary(PACKAGE_ROOT);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
reject(error);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const child = spawn(command, FALLOW_ARGS, {
|
|
105
|
+
cwd: PROJECT_ROOT,
|
|
106
|
+
env: process.env,
|
|
107
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
108
|
+
});
|
|
109
|
+
const stdout = [];
|
|
110
|
+
const stderr = [];
|
|
111
|
+
|
|
112
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
113
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
114
|
+
child.once("error", reject);
|
|
115
|
+
child.once("close", (exitCode) => {
|
|
116
|
+
const finishedAt = Date.now();
|
|
117
|
+
const output = Buffer.concat(stdout).toString("utf8");
|
|
118
|
+
const errorText = Buffer.concat(stderr).toString("utf8").trim();
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const parsedReport = JSON.parse(output);
|
|
122
|
+
resolve(
|
|
123
|
+
normalizeFallowReport(parsedReport, {
|
|
124
|
+
command: COMMAND_LABEL,
|
|
125
|
+
durationMs: finishedAt - startedAt.getTime(),
|
|
126
|
+
exitCode,
|
|
127
|
+
generatedAt: new Date(finishedAt).toISOString()
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const detail = errorText ? `${error.message}: ${errorText}` : error.message;
|
|
132
|
+
reject(new Error(`Could not parse Fallow JSON output. ${detail}`));
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveLucideScriptPath() {
|
|
139
|
+
try {
|
|
140
|
+
return require.resolve("lucide/dist/umd/lucide.min.js");
|
|
141
|
+
} catch {
|
|
142
|
+
const manifestPath = require.resolve("lucide/package.json");
|
|
143
|
+
return path.join(path.dirname(manifestPath), "dist", "umd", "lucide.min.js");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function handleHealthApi(_request, response) {
|
|
148
|
+
sendJson(response, 200, { ok: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function handleReportApi(_request, response) {
|
|
152
|
+
sendJson(response, 200, currentPayload());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function handleScanApi(_request, response) {
|
|
156
|
+
sendJson(response, 202, startFallowScan());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function handleApi(request, response) {
|
|
160
|
+
const route = API_ROUTES.get(`${request.method} ${new URL(request.url, fallbackOrigin(request)).pathname}`);
|
|
161
|
+
if (!route) {
|
|
162
|
+
sendJson(response, 404, { error: "Unknown API endpoint" });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await route(request, response);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function serveStatic(response, pathname) {
|
|
170
|
+
const filePath = pathname === "/vendor/lucide.js"
|
|
171
|
+
? LUCIDE_SCRIPT_PATH
|
|
172
|
+
: resolvePublicPath(pathname);
|
|
173
|
+
const content = await readStatic(filePath);
|
|
174
|
+
|
|
175
|
+
if (!content) {
|
|
176
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
177
|
+
response.end("Not found\n");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
response.writeHead(200, {
|
|
182
|
+
"cache-control": "no-store",
|
|
183
|
+
"content-type": MIME_TYPES.get(path.extname(filePath)) || "application/octet-stream"
|
|
184
|
+
});
|
|
185
|
+
response.end(content);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function readStatic(filePath) {
|
|
189
|
+
try {
|
|
190
|
+
return await fs.readFile(filePath);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error.code === "ENOENT" || error.code === "EISDIR") return null;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function createServer() {
|
|
198
|
+
return http.createServer(handleRequest);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function handleRequest(request, response) {
|
|
202
|
+
try {
|
|
203
|
+
const url = new URL(request.url, fallbackOrigin(request));
|
|
204
|
+
await routeRequest(request, response, url);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
sendJson(response, 500, { error: error.message || String(error) });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function routeRequest(request, response, url) {
|
|
211
|
+
if (url.pathname.startsWith("/api/")) {
|
|
212
|
+
await handleApi(request, response);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await serveStatic(response, url.pathname);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function fallbackOrigin(request) {
|
|
220
|
+
return `http://${request.headers.host || "localhost"}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function removePreviousPidFile() {
|
|
224
|
+
const previousPid = readPreviousPid();
|
|
225
|
+
if (!previousPid || previousPid === process.pid || !isProcessRunning(previousPid)) {
|
|
226
|
+
removePidFile();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readPreviousPid() {
|
|
231
|
+
const content = readPidFileContent();
|
|
232
|
+
const pid = Number(content);
|
|
233
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function readPidFileContent() {
|
|
237
|
+
try {
|
|
238
|
+
return fsSync.readFileSync(PID_FILE_PATH, "utf8").trim();
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error.code === "ENOENT") return "";
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isProcessRunning(pid) {
|
|
246
|
+
try {
|
|
247
|
+
process.kill(pid, 0);
|
|
248
|
+
return true;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return error.code === "EPERM";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function removePidFile() {
|
|
255
|
+
try {
|
|
256
|
+
fsSync.unlinkSync(PID_FILE_PATH);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error.code !== "ENOENT") throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function writePidFile() {
|
|
263
|
+
fsSync.writeFileSync(PID_FILE_PATH, `${process.pid}\n`, "utf8");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function cleanupPidFile() {
|
|
267
|
+
if (readPreviousPid() === process.pid) removePidFile();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function startServer(port) {
|
|
271
|
+
const server = createServer();
|
|
272
|
+
attachServerErrorHandler(server, port);
|
|
273
|
+
listenOnPort(server, port);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function attachServerErrorHandler(server, port) {
|
|
277
|
+
server.once("error", (error) => handleServerError(port, error));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function listenOnPort(server, port) {
|
|
281
|
+
server.listen(port, HOST, () => {
|
|
282
|
+
writePidFile();
|
|
283
|
+
console.log(`Code Scan: http://${HOST}:${port}`);
|
|
284
|
+
console.log(`Project: ${PROJECT_ROOT}`);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function handleServerError(port, error) {
|
|
289
|
+
const message = error.code === "EADDRINUSE"
|
|
290
|
+
? `Code Scan could not start: http://${HOST}:${port} is already in use.`
|
|
291
|
+
: error.stack || error.message || String(error);
|
|
292
|
+
console.error(message);
|
|
293
|
+
process.exitCode = 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function exitAfterCleanup(exitCode) {
|
|
297
|
+
cleanupPidFile();
|
|
298
|
+
process.exit(exitCode);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function registerProcessCleanup() {
|
|
302
|
+
process.once("exit", cleanupPidFile);
|
|
303
|
+
process.once("SIGINT", () => exitAfterCleanup(130));
|
|
304
|
+
process.once("SIGTERM", () => exitAfterCleanup(143));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
registerProcessCleanup();
|
|
308
|
+
removePreviousPidFile();
|
|
309
|
+
startServer(PORT);
|
package/src/start.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const { PID_FILE_PATH } = require("./paths");
|
|
7
|
+
|
|
8
|
+
const SERVER_PATH = path.join(__dirname, "server.js");
|
|
9
|
+
const STOP_TIMEOUT_MS = 3000;
|
|
10
|
+
const STOP_POLL_MS = 50;
|
|
11
|
+
|
|
12
|
+
async function start() {
|
|
13
|
+
await stopPreviousServer();
|
|
14
|
+
startServer();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function stopPreviousServer() {
|
|
18
|
+
const pid = readPreviousPid();
|
|
19
|
+
if (!pid || pid === process.pid || !isProcessRunning(pid)) {
|
|
20
|
+
removePidFile();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
stopProcess(pid);
|
|
25
|
+
await waitForExit(pid);
|
|
26
|
+
removePidFile();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stopProcess(pid) {
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, "SIGTERM");
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === "EPERM") {
|
|
34
|
+
throw new Error(`Cannot stop previous Code Scan process ${pid}. Stop it once, then rerun npm run code-scan.`);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readPreviousPid() {
|
|
41
|
+
try {
|
|
42
|
+
const pid = Number(fs.readFileSync(PID_FILE_PATH, "utf8").trim());
|
|
43
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.code === "ENOENT") return null;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isProcessRunning(pid) {
|
|
51
|
+
try {
|
|
52
|
+
process.kill(pid, 0);
|
|
53
|
+
return true;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return error.code === "EPERM";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function waitForExit(pid) {
|
|
60
|
+
const deadline = Date.now() + STOP_TIMEOUT_MS;
|
|
61
|
+
while (isProcessRunning(pid) && Date.now() < deadline) {
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, STOP_POLL_MS));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (isProcessRunning(pid)) {
|
|
66
|
+
throw new Error(`Could not stop previous Code Scan process ${pid}.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removePidFile() {
|
|
71
|
+
try {
|
|
72
|
+
fs.unlinkSync(PID_FILE_PATH);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.code !== "ENOENT") throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function startServer() {
|
|
79
|
+
require(SERVER_PATH);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fail(error) {
|
|
83
|
+
console.error(error.message || String(error));
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
start().catch(fail);
|