@zappie/check 0.2.0 → 0.2.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/CHANGELOG.md +23 -0
- package/README.md +22 -0
- package/dist/index.d.ts +42 -4
- package/dist/index.js +752 -160
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/schema/check-result.v1.json +30 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ function distance(left, right) {
|
|
|
42
42
|
}
|
|
43
43
|
return row[right.length] ?? Infinity;
|
|
44
44
|
}
|
|
45
|
-
function unknownKeys(value, allowed,
|
|
45
|
+
function unknownKeys(value, allowed, path15, issue) {
|
|
46
46
|
for (const key of Object.keys(value)) {
|
|
47
47
|
if (allowed.includes(key)) {
|
|
48
48
|
continue;
|
|
@@ -50,7 +50,7 @@ function unknownKeys(value, allowed, path14, issue) {
|
|
|
50
50
|
const closest = allowed.map((candidate) => ({ candidate, score: distance(key, candidate) })).sort((left, right) => left.score - right.score)[0];
|
|
51
51
|
const hint = closest && closest.score <= 2 ? closest.candidate : void 0;
|
|
52
52
|
issue({
|
|
53
|
-
path:
|
|
53
|
+
path: path15 ? `${path15}.${key}` : key,
|
|
54
54
|
code: "unknown_key",
|
|
55
55
|
message: hint ? `Unknown field "${key}". Rename it to "${hint}".` : `Unknown field "${key}". Remove it from the configuration.`,
|
|
56
56
|
severity: "error"
|
|
@@ -67,7 +67,7 @@ var globallyAllowed = (value) => GLOBAL_PACKAGE_ALLOWLIST.some(
|
|
|
67
67
|
function hostname(value) {
|
|
68
68
|
const wildcard = value.startsWith("*.");
|
|
69
69
|
const host = wildcard ? value.slice(2) : value;
|
|
70
|
-
if (!host.includes(".") || /^\d+(?:\.\d+){3}$/.test(host) || host.length > 253 || host.includes("..")) {
|
|
70
|
+
if (!host.includes(".") || /^\d+(?:\.\d+){3}$/.test(host) || host.length > 253 || host.includes("..") || wildcard && /^(?:ac|co|com|edu|gov|net|org)\.[^.]+$/i.test(host)) {
|
|
71
71
|
return false;
|
|
72
72
|
}
|
|
73
73
|
return host.split(".").every(
|
|
@@ -76,20 +76,20 @@ function hostname(value) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// ../sdk/src/site-config/sections.ts
|
|
79
|
-
function nested(value,
|
|
79
|
+
function nested(value, path15, keys, issue, validate) {
|
|
80
80
|
if (value === void 0) {
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
83
|
if (!record(value)) {
|
|
84
84
|
issue({
|
|
85
|
-
path:
|
|
85
|
+
path: path15,
|
|
86
86
|
code: "invalid_type",
|
|
87
|
-
message: `Expected an object. Set ${
|
|
87
|
+
message: `Expected an object. Set ${path15} to an object or remove it.`,
|
|
88
88
|
severity: "error"
|
|
89
89
|
});
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
|
-
unknownKeys(value, keys,
|
|
92
|
+
unknownKeys(value, keys, path15, issue);
|
|
93
93
|
validate(value);
|
|
94
94
|
}
|
|
95
95
|
function budgets(value, issue, error) {
|
|
@@ -126,18 +126,18 @@ function budgets(value, issue, error) {
|
|
|
126
126
|
`Expected a positive finite number. Set budgets.${key} to a number greater than zero.`
|
|
127
127
|
);
|
|
128
128
|
} else if (amount > DEFAULT_BUDGETS[key]) {
|
|
129
|
-
const
|
|
129
|
+
const message6 = `The ${key} budget exceeds the ${DEFAULT_BUDGETS[key]} default.`;
|
|
130
130
|
if (budget.override === void 0) {
|
|
131
131
|
error(
|
|
132
132
|
`budgets.${key}`,
|
|
133
133
|
"budget_override",
|
|
134
|
-
`${
|
|
134
|
+
`${message6} Add budgets.override.reason to explain the increase.`
|
|
135
135
|
);
|
|
136
136
|
} else {
|
|
137
137
|
issue({
|
|
138
138
|
path: `budgets.${key}`,
|
|
139
139
|
code: "budget_override",
|
|
140
|
-
message: `${
|
|
140
|
+
message: `${message6} Lower it to the default when the exception is no longer needed.`,
|
|
141
141
|
severity: "warning"
|
|
142
142
|
});
|
|
143
143
|
}
|
|
@@ -181,7 +181,9 @@ var rootKeys = [
|
|
|
181
181
|
"prerender",
|
|
182
182
|
"menu",
|
|
183
183
|
"packages",
|
|
184
|
+
"audit",
|
|
184
185
|
"outbound",
|
|
186
|
+
"security",
|
|
185
187
|
"budgets",
|
|
186
188
|
"readiness",
|
|
187
189
|
"analytics"
|
|
@@ -189,7 +191,7 @@ var rootKeys = [
|
|
|
189
191
|
function validateConfig(input) {
|
|
190
192
|
const issues = [];
|
|
191
193
|
const issue = (value) => issues.push(value);
|
|
192
|
-
const error = (
|
|
194
|
+
const error = (path15, code, message6) => issue({ path: path15, code, message: message6, severity: "error" });
|
|
193
195
|
if (!record(input)) {
|
|
194
196
|
return {
|
|
195
197
|
valid: false,
|
|
@@ -235,16 +237,20 @@ function validateConfig(input) {
|
|
|
235
237
|
});
|
|
236
238
|
});
|
|
237
239
|
nested(input.outbound, "outbound", ["allow"], issue, (value) => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
240
|
+
hostList(value.allow, "outbound.allow", error);
|
|
241
|
+
});
|
|
242
|
+
nested(input.security, "security", ["csp"], issue, (security) => {
|
|
243
|
+
nested(
|
|
244
|
+
security.csp,
|
|
245
|
+
"security.csp",
|
|
246
|
+
["imgSrc", "connectSrc", "fontSrc", "styleSrc", "frameSrc"],
|
|
247
|
+
issue,
|
|
248
|
+
(csp) => {
|
|
249
|
+
for (const [name, value] of Object.entries(csp)) {
|
|
250
|
+
hostList(value, `security.csp.${name}`, error);
|
|
251
|
+
}
|
|
246
252
|
}
|
|
247
|
-
|
|
253
|
+
);
|
|
248
254
|
});
|
|
249
255
|
budgets(input.budgets, issue, error);
|
|
250
256
|
nested(
|
|
@@ -284,12 +290,12 @@ function validateConfig(input) {
|
|
|
284
290
|
issues
|
|
285
291
|
};
|
|
286
292
|
}
|
|
287
|
-
function requiredSlug(value,
|
|
293
|
+
function requiredSlug(value, path15, error) {
|
|
288
294
|
if (!nonblank(value)) {
|
|
289
295
|
error(
|
|
290
|
-
|
|
296
|
+
path15,
|
|
291
297
|
value === void 0 ? "missing_required" : "invalid_type",
|
|
292
|
-
`Expected a non-empty storefront slug. Set ${
|
|
298
|
+
`Expected a non-empty storefront slug. Set ${path15} to the slug used by createZappie().`
|
|
293
299
|
);
|
|
294
300
|
}
|
|
295
301
|
}
|
|
@@ -308,29 +314,29 @@ function theme(value, error) {
|
|
|
308
314
|
);
|
|
309
315
|
}
|
|
310
316
|
}
|
|
311
|
-
function stringList(value,
|
|
317
|
+
function stringList(value, path15, error, options = {}) {
|
|
312
318
|
if (value === void 0) {
|
|
313
319
|
return void 0;
|
|
314
320
|
}
|
|
315
321
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
316
322
|
error(
|
|
317
|
-
|
|
323
|
+
path15,
|
|
318
324
|
"invalid_type",
|
|
319
|
-
`Expected an array of strings. Set ${
|
|
325
|
+
`Expected an array of strings. Set ${path15} to string values.`
|
|
320
326
|
);
|
|
321
327
|
return void 0;
|
|
322
328
|
}
|
|
323
329
|
if (options.nonempty && value.length === 0) {
|
|
324
330
|
error(
|
|
325
|
-
|
|
331
|
+
path15,
|
|
326
332
|
"invalid_format",
|
|
327
|
-
`Expected at least one value. Add a value to ${
|
|
333
|
+
`Expected at least one value. Add a value to ${path15} or remove the field.`
|
|
328
334
|
);
|
|
329
335
|
}
|
|
330
336
|
value.forEach((entry, index) => {
|
|
331
337
|
if (options.slug && !nonblank(entry)) {
|
|
332
338
|
error(
|
|
333
|
-
`${
|
|
339
|
+
`${path15}[${index}]`,
|
|
334
340
|
"invalid_format",
|
|
335
341
|
"Expected a non-empty branch slug. Remove the empty value or use a branch slug."
|
|
336
342
|
);
|
|
@@ -338,17 +344,28 @@ function stringList(value, path14, error, options = {}) {
|
|
|
338
344
|
});
|
|
339
345
|
if (options.unique && new Set(value).size !== value.length) {
|
|
340
346
|
error(
|
|
341
|
-
|
|
347
|
+
path15,
|
|
342
348
|
"duplicate",
|
|
343
|
-
`Expected unique values. Remove duplicates from ${
|
|
349
|
+
`Expected unique values. Remove duplicates from ${path15}.`
|
|
344
350
|
);
|
|
345
351
|
}
|
|
346
352
|
return value;
|
|
347
353
|
}
|
|
354
|
+
function hostList(value, path15, error) {
|
|
355
|
+
stringList(value, path15, error)?.forEach((host, index) => {
|
|
356
|
+
if (!hostname(host)) {
|
|
357
|
+
error(
|
|
358
|
+
`${path15}[${index}]`,
|
|
359
|
+
"invalid_format",
|
|
360
|
+
"Expected a hostname or leading *. subdomain pattern without a scheme, path, port or IP. Use a value such as api.example.com."
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
348
365
|
function paths(value, error) {
|
|
349
366
|
const values = stringList(value, "prerender", error, { unique: true });
|
|
350
|
-
values?.forEach((
|
|
351
|
-
if (!
|
|
367
|
+
values?.forEach((path15, index) => {
|
|
368
|
+
if (!path15.startsWith("/")) {
|
|
352
369
|
error(
|
|
353
370
|
`prerender[${index}]`,
|
|
354
371
|
"invalid_format",
|
|
@@ -851,11 +868,11 @@ var record2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
851
868
|
var includes = (values, key) => values.includes(key);
|
|
852
869
|
function validateTheme(input) {
|
|
853
870
|
const issues = [];
|
|
854
|
-
const issue = (
|
|
855
|
-
const keys = (obj, allowed,
|
|
871
|
+
const issue = (path15, code, message6, severity = "error") => issues.push({ path: path15, code, message: message6, severity });
|
|
872
|
+
const keys = (obj, allowed, path15) => {
|
|
856
873
|
for (const key of Object.keys(obj)) {
|
|
857
874
|
if (!allowed.includes(key)) {
|
|
858
|
-
issue(
|
|
875
|
+
issue(path15 ? `${path15}.${key}` : key, "invalid_theme", "Unknown field");
|
|
859
876
|
}
|
|
860
877
|
}
|
|
861
878
|
};
|
|
@@ -916,9 +933,9 @@ function validateTheme(input) {
|
|
|
916
933
|
}
|
|
917
934
|
}
|
|
918
935
|
for (const [key, value] of Object.entries(values)) {
|
|
919
|
-
const
|
|
936
|
+
const path15 = `${group}.${key}`;
|
|
920
937
|
if (!includes(TOKEN_NAMES, key)) {
|
|
921
|
-
issue(
|
|
938
|
+
issue(path15, "unknown_token", "Unknown semantic token");
|
|
922
939
|
continue;
|
|
923
940
|
}
|
|
924
941
|
let code, valid = false;
|
|
@@ -936,7 +953,7 @@ function validateTheme(input) {
|
|
|
936
953
|
valid = typeof value === "string" && safeValue(value) && (includes(DURATION_TOKENS, key) ? isDuration(value) : isEasing(value));
|
|
937
954
|
}
|
|
938
955
|
if (!valid) {
|
|
939
|
-
issue(
|
|
956
|
+
issue(path15, code, "Invalid or unsafe token value");
|
|
940
957
|
}
|
|
941
958
|
}
|
|
942
959
|
}
|
|
@@ -946,30 +963,30 @@ function validateTheme(input) {
|
|
|
946
963
|
} else {
|
|
947
964
|
const roles = /* @__PURE__ */ new Set();
|
|
948
965
|
input.fonts.forEach((font, i) => {
|
|
949
|
-
const
|
|
966
|
+
const path15 = `fonts[${i}]`;
|
|
950
967
|
if (!record2(font)) {
|
|
951
|
-
issue(
|
|
968
|
+
issue(path15, "invalid_font", "Expected a font object");
|
|
952
969
|
return;
|
|
953
970
|
}
|
|
954
|
-
keys(font, ["family", "source", "url", "weights", "role"],
|
|
971
|
+
keys(font, ["family", "source", "url", "weights", "role"], path15);
|
|
955
972
|
if (typeof font.family !== "string" || !isFontFamily(font.family) || font.family.includes(",")) {
|
|
956
973
|
issue(
|
|
957
|
-
`${
|
|
974
|
+
`${path15}.family`,
|
|
958
975
|
"invalid_font",
|
|
959
976
|
"Expected one unquoted font family"
|
|
960
977
|
);
|
|
961
978
|
}
|
|
962
979
|
if (typeof font.source !== "string" || !["google", "self", "system"].includes(font.source)) {
|
|
963
|
-
issue(`${
|
|
980
|
+
issue(`${path15}.source`, "invalid_font", "Unknown font source");
|
|
964
981
|
}
|
|
965
982
|
if (typeof font.role !== "string" || !["display", "body", "mono"].includes(font.role) || roles.has(font.role)) {
|
|
966
|
-
issue(`${
|
|
983
|
+
issue(`${path15}.role`, "invalid_font", "Expected a unique font role");
|
|
967
984
|
}
|
|
968
985
|
roles.add(String(font.role));
|
|
969
986
|
if (font.source === "self" || font.url !== void 0) {
|
|
970
987
|
if (typeof font.url !== "string" || !isAssetUrl(font.url, false) || !new URL(font.url).pathname.endsWith(".woff2")) {
|
|
971
988
|
issue(
|
|
972
|
-
`${
|
|
989
|
+
`${path15}.url`,
|
|
973
990
|
"invalid_font",
|
|
974
991
|
"Expected an HTTPS .woff2 URL"
|
|
975
992
|
);
|
|
@@ -979,7 +996,7 @@ function validateTheme(input) {
|
|
|
979
996
|
(w) => typeof w !== "number" || !Number.isInteger(w) || w < 1 || w > 1e3
|
|
980
997
|
))) {
|
|
981
998
|
issue(
|
|
982
|
-
`${
|
|
999
|
+
`${path15}.weights`,
|
|
983
1000
|
"invalid_font",
|
|
984
1001
|
"Expected 1\u20139 integer weights between 1 and 1000"
|
|
985
1002
|
);
|
|
@@ -1089,13 +1106,13 @@ function escape(value) {
|
|
|
1089
1106
|
}
|
|
1090
1107
|
|
|
1091
1108
|
// src/result.ts
|
|
1092
|
-
function problem(
|
|
1093
|
-
return { ...details, message:
|
|
1109
|
+
function problem(message6, fix, details = {}) {
|
|
1110
|
+
return { ...details, message: message6, fix };
|
|
1094
1111
|
}
|
|
1095
1112
|
function step(name, result, problems = []) {
|
|
1096
1113
|
return { name, result, problems };
|
|
1097
1114
|
}
|
|
1098
|
-
var skipped = (name,
|
|
1115
|
+
var skipped = (name, message6) => step(name, "skipped", [problem(message6, null)]);
|
|
1099
1116
|
|
|
1100
1117
|
// src/config.ts
|
|
1101
1118
|
function supportsTypeScriptImport(version = process.versions.node) {
|
|
@@ -1318,22 +1335,500 @@ async function installedManifest(cwd, name) {
|
|
|
1318
1335
|
}
|
|
1319
1336
|
}
|
|
1320
1337
|
|
|
1338
|
+
// src/audit.ts
|
|
1339
|
+
import { access as access2, readFile as readFile4 } from "fs/promises";
|
|
1340
|
+
import path4 from "path";
|
|
1341
|
+
|
|
1342
|
+
// src/command.ts
|
|
1343
|
+
import { spawn } from "child_process";
|
|
1344
|
+
function command(executable, args, cwd, signal) {
|
|
1345
|
+
return new Promise((resolve, reject) => {
|
|
1346
|
+
const child = spawn(executable, args, {
|
|
1347
|
+
cwd,
|
|
1348
|
+
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
1349
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1350
|
+
...signal ? { signal } : {}
|
|
1351
|
+
});
|
|
1352
|
+
let stdout = "";
|
|
1353
|
+
let stderr = "";
|
|
1354
|
+
child.stdout.on("data", (value) => {
|
|
1355
|
+
stdout += String(value);
|
|
1356
|
+
});
|
|
1357
|
+
child.stderr.on("data", (value) => {
|
|
1358
|
+
stderr += String(value);
|
|
1359
|
+
});
|
|
1360
|
+
child.on("error", reject);
|
|
1361
|
+
child.on("close", (code) => {
|
|
1362
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
function execArgs(manager, binary, args) {
|
|
1367
|
+
if (manager === "npm") {
|
|
1368
|
+
return { executable: "npm", args: ["exec", "--no", "--", binary, ...args] };
|
|
1369
|
+
}
|
|
1370
|
+
if (manager === "yarn") {
|
|
1371
|
+
return { executable: "yarn", args: ["exec", binary, ...args] };
|
|
1372
|
+
}
|
|
1373
|
+
if (manager === "bun") {
|
|
1374
|
+
return { executable: "bun", args: ["x", "--no-install", binary, ...args] };
|
|
1375
|
+
}
|
|
1376
|
+
return { executable: "pnpm", args: ["exec", binary, ...args] };
|
|
1377
|
+
}
|
|
1378
|
+
function scriptArgs(manager, name) {
|
|
1379
|
+
if (manager === "bun") {
|
|
1380
|
+
return { executable: "bun", args: ["run", name] };
|
|
1381
|
+
}
|
|
1382
|
+
if (manager === "npm") {
|
|
1383
|
+
return { executable: "npm", args: ["run", name] };
|
|
1384
|
+
}
|
|
1385
|
+
return { executable: manager, args: [name] };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/audit.ts
|
|
1389
|
+
var AUDIT_TIMEOUT_MS = 6e4;
|
|
1390
|
+
async function checkAudit(options, dependencies = {}) {
|
|
1391
|
+
const mode = options.audit ?? "report";
|
|
1392
|
+
if (mode === "off") {
|
|
1393
|
+
return skipped("audit", "Dependency auditing is off.");
|
|
1394
|
+
}
|
|
1395
|
+
options.signal?.throwIfAborted();
|
|
1396
|
+
let invalidIgnores = [];
|
|
1397
|
+
let ignores = [];
|
|
1398
|
+
try {
|
|
1399
|
+
const parsed = await auditIgnores(options.cwd);
|
|
1400
|
+
ignores = parsed.ignores;
|
|
1401
|
+
invalidIgnores = parsed.problems;
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
invalidIgnores.push(
|
|
1404
|
+
problem(
|
|
1405
|
+
`Could not use audit.ignore: ${message2(error)}`,
|
|
1406
|
+
"Fix or remove the invalid acknowledgement. The advisory remains unacknowledged.",
|
|
1407
|
+
{ rule: "audit/invalid-ignore", file: "zappie.config.ts", line: 1 }
|
|
1408
|
+
)
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
if (options.packageManager === "yarn" || options.packageManager === "bun") {
|
|
1412
|
+
return withProblems(
|
|
1413
|
+
unavailable(
|
|
1414
|
+
mode,
|
|
1415
|
+
`Could not audit dependencies: ${options.packageManager} is not supported.`,
|
|
1416
|
+
"Use npm or pnpm for dependency auditing, or run with --audit=off.",
|
|
1417
|
+
"package.json"
|
|
1418
|
+
),
|
|
1419
|
+
invalidIgnores,
|
|
1420
|
+
mode
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
const lockfile2 = await findLockfile(options.cwd, options.packageManager);
|
|
1424
|
+
if (!lockfile2) {
|
|
1425
|
+
return withProblems(
|
|
1426
|
+
unavailable(
|
|
1427
|
+
mode,
|
|
1428
|
+
"Could not audit dependencies: no lockfile was found.",
|
|
1429
|
+
"Commit the package manager lockfile and run zappie check again.",
|
|
1430
|
+
"package.json"
|
|
1431
|
+
),
|
|
1432
|
+
invalidIgnores,
|
|
1433
|
+
mode
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
const invocation = auditCommand(options.packageManager);
|
|
1437
|
+
const timeoutMs = dependencies.timeoutMs ?? AUDIT_TIMEOUT_MS;
|
|
1438
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
1439
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
1440
|
+
let result;
|
|
1441
|
+
try {
|
|
1442
|
+
result = await (dependencies.run ?? command)(
|
|
1443
|
+
invocation.executable,
|
|
1444
|
+
invocation.args,
|
|
1445
|
+
options.cwd,
|
|
1446
|
+
signal
|
|
1447
|
+
);
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
options.signal?.throwIfAborted();
|
|
1450
|
+
const detail = timeout.aborted ? `the audit timed out after ${Math.ceil(timeoutMs / 1e3)} seconds` : message2(error);
|
|
1451
|
+
return withProblems(
|
|
1452
|
+
unavailable(
|
|
1453
|
+
mode,
|
|
1454
|
+
`Could not audit dependencies: ${detail}.`,
|
|
1455
|
+
"Check registry access and run zappie check again.",
|
|
1456
|
+
relativeLockfile(options.cwd, lockfile2)
|
|
1457
|
+
),
|
|
1458
|
+
invalidIgnores,
|
|
1459
|
+
mode
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
options.signal?.throwIfAborted();
|
|
1463
|
+
let advisories;
|
|
1464
|
+
try {
|
|
1465
|
+
advisories = parseAuditOutput(options.packageManager, result.stdout);
|
|
1466
|
+
if (result.code > 1) {
|
|
1467
|
+
throw new Error(`the audit command exited with status ${result.code}`);
|
|
1468
|
+
}
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
return withProblems(
|
|
1471
|
+
unavailable(
|
|
1472
|
+
mode,
|
|
1473
|
+
`Could not audit dependencies: ${message2(error)}.`,
|
|
1474
|
+
"Check registry access and run zappie check again.",
|
|
1475
|
+
relativeLockfile(options.cwd, lockfile2)
|
|
1476
|
+
),
|
|
1477
|
+
invalidIgnores,
|
|
1478
|
+
mode
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
return withProblems(
|
|
1482
|
+
evaluateAudit(
|
|
1483
|
+
advisories,
|
|
1484
|
+
mode,
|
|
1485
|
+
ignores,
|
|
1486
|
+
relativeLockfile(options.cwd, lockfile2)
|
|
1487
|
+
),
|
|
1488
|
+
invalidIgnores,
|
|
1489
|
+
mode
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
function evaluateAudit(advisories, mode, ignores = [], file) {
|
|
1493
|
+
const problems = [];
|
|
1494
|
+
const applied = advisories.map((advisory) => {
|
|
1495
|
+
const acknowledged = ignores.find(
|
|
1496
|
+
({ id }) => normalizedId(id) === normalizedId(advisory.id)
|
|
1497
|
+
);
|
|
1498
|
+
if (acknowledged && malicious(advisory.id)) {
|
|
1499
|
+
problems.push(
|
|
1500
|
+
problem(
|
|
1501
|
+
`${advisory.id} is a malicious-package advisory and cannot be acknowledged.`,
|
|
1502
|
+
`Remove ${advisory.package} and replace it with a trusted package or release.`,
|
|
1503
|
+
{
|
|
1504
|
+
rule: "audit/invalid-ignore",
|
|
1505
|
+
advisory,
|
|
1506
|
+
...file ? { file, line: 1 } : {}
|
|
1507
|
+
}
|
|
1508
|
+
)
|
|
1509
|
+
);
|
|
1510
|
+
return advisory;
|
|
1511
|
+
}
|
|
1512
|
+
if (acknowledged && (advisory.fixedVersion || advisory.upgradeVersion || advisory.fixCommand)) {
|
|
1513
|
+
const upgradePackage = advisory.upgradePackage ?? advisory.package;
|
|
1514
|
+
const upgradeVersion = advisory.upgradeVersion ?? advisory.fixedVersion;
|
|
1515
|
+
const fix = advisory.fixCommand ? `Run ${advisory.fixCommand}.` : `Upgrade ${upgradePackage} to ${upgradeVersion} or later.`;
|
|
1516
|
+
const reason = advisory.fixCommand ? `${advisory.id} has an available fix and cannot be acknowledged.` : `${advisory.id} has a fixed version and cannot be acknowledged.`;
|
|
1517
|
+
problems.push(
|
|
1518
|
+
problem(reason, fix, {
|
|
1519
|
+
rule: "audit/invalid-ignore",
|
|
1520
|
+
advisory,
|
|
1521
|
+
...file ? { file, line: 1 } : {}
|
|
1522
|
+
})
|
|
1523
|
+
);
|
|
1524
|
+
return advisory;
|
|
1525
|
+
}
|
|
1526
|
+
return acknowledged ? { ...advisory, ignored: true, ignoredUntil: acknowledged.until } : advisory;
|
|
1527
|
+
});
|
|
1528
|
+
problems.push(...applied.map((advisory) => advisoryProblem(advisory, file)));
|
|
1529
|
+
const critical = applied.some(
|
|
1530
|
+
({ severity, ignored }) => severity === "critical" && !ignored
|
|
1531
|
+
);
|
|
1532
|
+
const invalidIgnore2 = problems.some(
|
|
1533
|
+
({ rule }) => rule === "audit/invalid-ignore"
|
|
1534
|
+
);
|
|
1535
|
+
return step(
|
|
1536
|
+
"audit",
|
|
1537
|
+
mode === "enforce" && (critical || invalidIgnore2) ? "failed" : "passed",
|
|
1538
|
+
problems
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
function unavailable(mode, message6, fix, file) {
|
|
1542
|
+
return step("audit", mode === "enforce" ? "failed" : "passed", [
|
|
1543
|
+
problem(message6, fix, { rule: "audit/unavailable", file, line: 1 })
|
|
1544
|
+
]);
|
|
1545
|
+
}
|
|
1546
|
+
function withProblems(checked, extra, mode) {
|
|
1547
|
+
if (extra.length === 0) {
|
|
1548
|
+
return checked;
|
|
1549
|
+
}
|
|
1550
|
+
return step("audit", mode === "enforce" ? "failed" : checked.result, [
|
|
1551
|
+
...extra,
|
|
1552
|
+
...checked.problems
|
|
1553
|
+
]);
|
|
1554
|
+
}
|
|
1555
|
+
async function findLockfile(cwd, manager) {
|
|
1556
|
+
const names = manager === "npm" ? ["package-lock.json", "npm-shrinkwrap.json"] : ["pnpm-lock.yaml"];
|
|
1557
|
+
let directory = path4.resolve(cwd);
|
|
1558
|
+
for (; ; ) {
|
|
1559
|
+
if (directory !== path4.resolve(cwd) && manager === "pnpm" && await workspaceExcludes(directory, path4.resolve(cwd))) {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
for (const name of names) {
|
|
1563
|
+
const candidate = path4.join(directory, name);
|
|
1564
|
+
try {
|
|
1565
|
+
await access2(candidate);
|
|
1566
|
+
return candidate;
|
|
1567
|
+
} catch {
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
const parent = path4.dirname(directory);
|
|
1571
|
+
if (parent === directory) {
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
directory = parent;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
async function workspaceExcludes(directory, cwd) {
|
|
1578
|
+
try {
|
|
1579
|
+
const source = await readFile4(
|
|
1580
|
+
path4.join(directory, "pnpm-workspace.yaml"),
|
|
1581
|
+
"utf8"
|
|
1582
|
+
);
|
|
1583
|
+
const block = /^packages:\s*\n((?:[ \t]+.*(?:\n|$))*)/m.exec(source)?.[1];
|
|
1584
|
+
if (!block) {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
const relative2 = path4.relative(directory, cwd).replaceAll(path4.sep, "/");
|
|
1588
|
+
const patterns = [
|
|
1589
|
+
...block.matchAll(/^\s*-\s*["']?([^"'\s]+)["']?\s*$/gm)
|
|
1590
|
+
].map((match) => match[1] ?? "");
|
|
1591
|
+
return !patterns.some((pattern) => workspacePattern(pattern, relative2));
|
|
1592
|
+
} catch {
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
function workspacePattern(pattern, relative2) {
|
|
1597
|
+
if (pattern === ".") {
|
|
1598
|
+
return relative2 === "";
|
|
1599
|
+
}
|
|
1600
|
+
const expression = pattern.split("*").map((part) => part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("[^/]+");
|
|
1601
|
+
return new RegExp(`^${expression}$`).test(relative2);
|
|
1602
|
+
}
|
|
1603
|
+
function relativeLockfile(cwd, lockfile2) {
|
|
1604
|
+
return path4.relative(cwd, lockfile2).replaceAll(path4.sep, "/");
|
|
1605
|
+
}
|
|
1606
|
+
function auditCommand(manager) {
|
|
1607
|
+
return manager === "pnpm" ? { executable: "pnpm", args: ["audit", "--json"] } : { executable: "npm", args: ["audit", "--json"] };
|
|
1608
|
+
}
|
|
1609
|
+
function parseAuditOutput(manager, source) {
|
|
1610
|
+
const value = JSON.parse(source);
|
|
1611
|
+
requireAuditReport(manager, value);
|
|
1612
|
+
const parsed = manager === "npm" ? npmAdvisories(value) : pnpmAdvisories(value);
|
|
1613
|
+
return parsed.filter(({ severity }) => severity === "high" || severity === "critical").sort((left, right) => left.id.localeCompare(right.id));
|
|
1614
|
+
}
|
|
1615
|
+
function requireAuditReport(manager, value) {
|
|
1616
|
+
if (record3(value.error)) {
|
|
1617
|
+
throw new Error(
|
|
1618
|
+
string(value.error.message) ?? string(value.error.code) ?? string(value.message) ?? "the package manager returned an error"
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
if (manager === "npm" && !record3(value.vulnerabilities)) {
|
|
1622
|
+
throw new Error("npm returned no vulnerability report");
|
|
1623
|
+
}
|
|
1624
|
+
if (manager === "pnpm" && !record3(value.advisories) && !record3(value.metadata)) {
|
|
1625
|
+
throw new Error("pnpm returned no advisory report");
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function pnpmAdvisories(value) {
|
|
1629
|
+
if (!record3(value.advisories)) {
|
|
1630
|
+
return [];
|
|
1631
|
+
}
|
|
1632
|
+
return Object.entries(value.advisories).flatMap(([key, raw]) => {
|
|
1633
|
+
if (!record3(raw)) {
|
|
1634
|
+
return [];
|
|
1635
|
+
}
|
|
1636
|
+
const findings = Array.isArray(raw.findings) ? raw.findings : [];
|
|
1637
|
+
const versions = findings.flatMap(
|
|
1638
|
+
(finding) => record3(finding) && typeof finding.version === "string" ? [finding.version] : []
|
|
1639
|
+
);
|
|
1640
|
+
const severity = normalizedSeverity(raw.severity);
|
|
1641
|
+
if (!severity) {
|
|
1642
|
+
return [];
|
|
1643
|
+
}
|
|
1644
|
+
return [
|
|
1645
|
+
{
|
|
1646
|
+
id: string(raw.github_advisory_id) ?? string(raw.id) ?? key,
|
|
1647
|
+
package: string(raw.module_name) ?? "unknown package",
|
|
1648
|
+
severity,
|
|
1649
|
+
vulnerableVersion: versions.join(", ") || string(raw.vulnerable_versions) || "unknown",
|
|
1650
|
+
fixedVersion: firstFixed(string(raw.patched_versions)),
|
|
1651
|
+
ignored: false
|
|
1652
|
+
}
|
|
1653
|
+
];
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
function npmAdvisories(value) {
|
|
1657
|
+
if (!record3(value.vulnerabilities)) {
|
|
1658
|
+
return [];
|
|
1659
|
+
}
|
|
1660
|
+
const advisories = /* @__PURE__ */ new Map();
|
|
1661
|
+
for (const [packageName2, raw] of Object.entries(value.vulnerabilities)) {
|
|
1662
|
+
if (!record3(raw) || !Array.isArray(raw.via)) {
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
for (const via of raw.via) {
|
|
1666
|
+
if (!record3(via)) {
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
const severity = normalizedSeverity(via.severity);
|
|
1670
|
+
if (!severity) {
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
const id = ghsaFrom(string(via.url)) ?? string(via.name) ?? String(via.source ?? packageName2);
|
|
1674
|
+
const upgrade = npmUpgrade(raw.fixAvailable);
|
|
1675
|
+
advisories.set(id, {
|
|
1676
|
+
id,
|
|
1677
|
+
package: packageName2,
|
|
1678
|
+
severity,
|
|
1679
|
+
vulnerableVersion: string(via.range) ?? string(raw.range) ?? "unknown",
|
|
1680
|
+
fixedVersion: upgrade.upgradePackage === packageName2 ? upgrade.upgradeVersion ?? null : null,
|
|
1681
|
+
...upgrade,
|
|
1682
|
+
ignored: false
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return [...advisories.values()];
|
|
1687
|
+
}
|
|
1688
|
+
function advisoryProblem(advisory, file) {
|
|
1689
|
+
const acknowledged = advisory.ignored ? ` Acknowledged until ${advisory.ignoredUntil}.` : "";
|
|
1690
|
+
const upgradePackage = advisory.upgradePackage ?? advisory.package;
|
|
1691
|
+
const upgradeVersion = advisory.upgradeVersion ?? advisory.fixedVersion;
|
|
1692
|
+
const fix = malicious(advisory.id) ? `Remove ${advisory.package} and replace it with a trusted package or release.` : advisory.fixCommand ? `Run ${advisory.fixCommand}.` : upgradeVersion ? `Upgrade ${upgradePackage} to ${upgradeVersion} or later.` : `Review ${advisory.id}. If no fix exists, add a reason and an expiry of at most 90 days to audit.ignore.`;
|
|
1693
|
+
return problem(
|
|
1694
|
+
`${advisory.id}: ${advisory.package} ${advisory.vulnerableVersion} has ${advisory.severity} severity.${acknowledged}`,
|
|
1695
|
+
fix,
|
|
1696
|
+
{
|
|
1697
|
+
rule: `audit/${advisory.id}`,
|
|
1698
|
+
advisory,
|
|
1699
|
+
...file ? { file, line: 1 } : {}
|
|
1700
|
+
}
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
async function auditIgnores(cwd) {
|
|
1704
|
+
let loaded;
|
|
1705
|
+
try {
|
|
1706
|
+
loaded = await loadConfig(cwd);
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
if (missing(error)) {
|
|
1709
|
+
return { ignores: [], problems: [] };
|
|
1710
|
+
}
|
|
1711
|
+
throw error;
|
|
1712
|
+
}
|
|
1713
|
+
if (!record3(loaded.value) || !record3(loaded.value.audit)) {
|
|
1714
|
+
return { ignores: [], problems: [] };
|
|
1715
|
+
}
|
|
1716
|
+
const problems = [];
|
|
1717
|
+
const ignores = parseAuditIgnores(loaded.value.audit.ignore, problems);
|
|
1718
|
+
return { ignores, problems };
|
|
1719
|
+
}
|
|
1720
|
+
function parseAuditIgnores(value, problems = []) {
|
|
1721
|
+
if (value === void 0) {
|
|
1722
|
+
return [];
|
|
1723
|
+
}
|
|
1724
|
+
if (!Array.isArray(value)) {
|
|
1725
|
+
problems.push(invalidIgnore("audit.ignore must be an array."));
|
|
1726
|
+
return [];
|
|
1727
|
+
}
|
|
1728
|
+
const today = Date.parse(
|
|
1729
|
+
`${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}T00:00:00.000Z`
|
|
1730
|
+
);
|
|
1731
|
+
const ninetyDays = 90 * 24 * 60 * 60 * 1e3;
|
|
1732
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1733
|
+
const ignores = [];
|
|
1734
|
+
value.forEach((entry, index) => {
|
|
1735
|
+
if (!ignoreEntry(entry) || !entry.id.trim() || !entry.reason.trim()) {
|
|
1736
|
+
problems.push(
|
|
1737
|
+
invalidIgnore(
|
|
1738
|
+
`audit.ignore[${index}] must have a non-blank id, reason and until.`
|
|
1739
|
+
)
|
|
1740
|
+
);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
const id = normalizedId(entry.id);
|
|
1744
|
+
if (ids.has(id)) {
|
|
1745
|
+
problems.push(
|
|
1746
|
+
invalidIgnore(`audit.ignore contains duplicate id ${entry.id}.`)
|
|
1747
|
+
);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
const expiry = Date.parse(`${entry.until}T00:00:00.000Z`);
|
|
1751
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.until) || !Number.isFinite(expiry) || new Date(expiry).toISOString().slice(0, 10) !== entry.until || expiry < today || expiry - today > ninetyDays) {
|
|
1752
|
+
problems.push(
|
|
1753
|
+
invalidIgnore(
|
|
1754
|
+
`audit.ignore[${index}].until must be a real, unexpired date at most 90 days away.`
|
|
1755
|
+
)
|
|
1756
|
+
);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
ids.add(id);
|
|
1760
|
+
ignores.push(entry);
|
|
1761
|
+
});
|
|
1762
|
+
return ignores;
|
|
1763
|
+
}
|
|
1764
|
+
function invalidIgnore(message6) {
|
|
1765
|
+
return problem(
|
|
1766
|
+
message6,
|
|
1767
|
+
"Fix or remove the invalid acknowledgement. The advisory remains unacknowledged.",
|
|
1768
|
+
{ rule: "audit/invalid-ignore", file: "zappie.config.ts", line: 1 }
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
function ignoreEntry(value) {
|
|
1772
|
+
return record3(value) && typeof value.id === "string" && typeof value.reason === "string" && typeof value.until === "string";
|
|
1773
|
+
}
|
|
1774
|
+
function npmUpgrade(value) {
|
|
1775
|
+
if (record3(value) && typeof value.name === "string" && typeof value.version === "string") {
|
|
1776
|
+
return { upgradePackage: value.name, upgradeVersion: value.version };
|
|
1777
|
+
}
|
|
1778
|
+
if (value === true) {
|
|
1779
|
+
return { fixCommand: "npm audit fix" };
|
|
1780
|
+
}
|
|
1781
|
+
return {};
|
|
1782
|
+
}
|
|
1783
|
+
function firstFixed(value) {
|
|
1784
|
+
if (!value || value === "<0.0.0") {
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
return /(?:^|\s)>=?\s*([^\s|]+)/.exec(value)?.[1] ?? null;
|
|
1788
|
+
}
|
|
1789
|
+
function ghsaFrom(value) {
|
|
1790
|
+
const id = value ? /GHSA-[\w-]+/i.exec(value)?.[0] : void 0;
|
|
1791
|
+
return id ? `GHSA-${id.slice(5).toLowerCase()}` : void 0;
|
|
1792
|
+
}
|
|
1793
|
+
function normalizedId(value) {
|
|
1794
|
+
return value.trim().toLowerCase();
|
|
1795
|
+
}
|
|
1796
|
+
function malicious(value) {
|
|
1797
|
+
return /^MAL-/i.test(value.trim());
|
|
1798
|
+
}
|
|
1799
|
+
function normalizedSeverity(value) {
|
|
1800
|
+
const severity = typeof value === "string" ? value.toLowerCase() : "";
|
|
1801
|
+
return severity === "high" || severity === "critical" ? severity : void 0;
|
|
1802
|
+
}
|
|
1803
|
+
function string(value) {
|
|
1804
|
+
return typeof value === "string" ? value : void 0;
|
|
1805
|
+
}
|
|
1806
|
+
function message2(error) {
|
|
1807
|
+
return error instanceof Error ? error.message : String(error);
|
|
1808
|
+
}
|
|
1809
|
+
function missing(error) {
|
|
1810
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
1811
|
+
}
|
|
1812
|
+
function record3(value) {
|
|
1813
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1321
1816
|
// src/budgets.ts
|
|
1322
|
-
import { access as
|
|
1323
|
-
import
|
|
1817
|
+
import { access as access3, readFile as readFile7 } from "fs/promises";
|
|
1818
|
+
import path7 from "path";
|
|
1324
1819
|
|
|
1325
1820
|
// src/budget-files.ts
|
|
1326
|
-
import { readFile as
|
|
1327
|
-
import
|
|
1821
|
+
import { readFile as readFile5, readdir } from "fs/promises";
|
|
1822
|
+
import path5 from "path";
|
|
1328
1823
|
import { gzipSync } from "zlib";
|
|
1329
1824
|
async function cssSizes(cwd) {
|
|
1330
|
-
const root =
|
|
1825
|
+
const root = path5.join(cwd, "build/client");
|
|
1331
1826
|
const files = [];
|
|
1332
1827
|
await collect(root, files);
|
|
1333
1828
|
return Promise.all(
|
|
1334
1829
|
files.filter((file) => file.endsWith(".css")).map(async (file) => ({
|
|
1335
|
-
file:
|
|
1336
|
-
bytes: gzipSync(await
|
|
1830
|
+
file: path5.relative(root, file).split(path5.sep).join("/"),
|
|
1831
|
+
bytes: gzipSync(await readFile5(file, "utf8")).byteLength
|
|
1337
1832
|
}))
|
|
1338
1833
|
);
|
|
1339
1834
|
}
|
|
@@ -1342,7 +1837,7 @@ async function collect(directory, files) {
|
|
|
1342
1837
|
if (entry.isSymbolicLink()) {
|
|
1343
1838
|
continue;
|
|
1344
1839
|
}
|
|
1345
|
-
const file =
|
|
1840
|
+
const file = path5.join(directory, entry.name);
|
|
1346
1841
|
if (entry.isDirectory()) {
|
|
1347
1842
|
await collect(file, files);
|
|
1348
1843
|
} else if (entry.isFile()) {
|
|
@@ -1352,8 +1847,8 @@ async function collect(directory, files) {
|
|
|
1352
1847
|
}
|
|
1353
1848
|
|
|
1354
1849
|
// src/budget-manifest.ts
|
|
1355
|
-
import { readFile as
|
|
1356
|
-
import
|
|
1850
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1851
|
+
import path6 from "path";
|
|
1357
1852
|
import { gzipSync as gzipSync2 } from "zlib";
|
|
1358
1853
|
function routeKeys(manifest) {
|
|
1359
1854
|
return Object.entries(manifest).filter(([key, entry]) => routeSource(entry.src ?? key)).map(([key]) => key).sort();
|
|
@@ -1367,7 +1862,7 @@ async function graphSizes(cwd, manifest, roots) {
|
|
|
1367
1862
|
[...keys].map((key) => manifest[key]).filter((entry) => Boolean(entry)).filter(({ file }) => file.endsWith(".js")).map(async ({ file }) => ({
|
|
1368
1863
|
file,
|
|
1369
1864
|
bytes: gzipSync2(
|
|
1370
|
-
await
|
|
1865
|
+
await readFile6(path6.join(cwd, "build/client", file), "utf8")
|
|
1371
1866
|
).byteLength
|
|
1372
1867
|
}))
|
|
1373
1868
|
);
|
|
@@ -1401,16 +1896,16 @@ async function checkBudgets({
|
|
|
1401
1896
|
signal
|
|
1402
1897
|
}) {
|
|
1403
1898
|
signal?.throwIfAborted();
|
|
1404
|
-
const manifestFile =
|
|
1899
|
+
const manifestFile = path7.join(cwd, "build/client/.vite/manifest.json");
|
|
1405
1900
|
try {
|
|
1406
|
-
await
|
|
1901
|
+
await access3(manifestFile);
|
|
1407
1902
|
} catch {
|
|
1408
1903
|
return skipped(
|
|
1409
1904
|
"budgets",
|
|
1410
1905
|
"No build/client/.vite/manifest.json is available."
|
|
1411
1906
|
);
|
|
1412
1907
|
}
|
|
1413
|
-
const manifest = JSON.parse(await
|
|
1908
|
+
const manifest = JSON.parse(await readFile7(manifestFile, "utf8"));
|
|
1414
1909
|
const config = (await loadConfig(cwd)).value;
|
|
1415
1910
|
const limits = { ...DEFAULT_BUDGETS, ...config.budgets };
|
|
1416
1911
|
const routes = routeKeys(manifest);
|
|
@@ -1465,56 +1960,8 @@ function cssProblem(sizes, limitKb, over) {
|
|
|
1465
1960
|
var total = (sizes) => sizes.reduce((sum, asset) => sum + asset.bytes, 0);
|
|
1466
1961
|
|
|
1467
1962
|
// src/build.ts
|
|
1468
|
-
import { readFile as
|
|
1469
|
-
import
|
|
1470
|
-
|
|
1471
|
-
// src/command.ts
|
|
1472
|
-
import { spawn } from "child_process";
|
|
1473
|
-
function command(executable, args, cwd, signal) {
|
|
1474
|
-
return new Promise((resolve, reject) => {
|
|
1475
|
-
const child = spawn(executable, args, {
|
|
1476
|
-
cwd,
|
|
1477
|
-
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
1478
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1479
|
-
...signal ? { signal } : {}
|
|
1480
|
-
});
|
|
1481
|
-
let stdout = "";
|
|
1482
|
-
let stderr = "";
|
|
1483
|
-
child.stdout.on("data", (value) => {
|
|
1484
|
-
stdout += String(value);
|
|
1485
|
-
});
|
|
1486
|
-
child.stderr.on("data", (value) => {
|
|
1487
|
-
stderr += String(value);
|
|
1488
|
-
});
|
|
1489
|
-
child.on("error", reject);
|
|
1490
|
-
child.on("close", (code) => {
|
|
1491
|
-
resolve({ code: code ?? 1, stdout, stderr });
|
|
1492
|
-
});
|
|
1493
|
-
});
|
|
1494
|
-
}
|
|
1495
|
-
function execArgs(manager, binary, args) {
|
|
1496
|
-
if (manager === "npm") {
|
|
1497
|
-
return { executable: "npm", args: ["exec", "--no", "--", binary, ...args] };
|
|
1498
|
-
}
|
|
1499
|
-
if (manager === "yarn") {
|
|
1500
|
-
return { executable: "yarn", args: ["exec", binary, ...args] };
|
|
1501
|
-
}
|
|
1502
|
-
if (manager === "bun") {
|
|
1503
|
-
return { executable: "bun", args: ["x", "--no-install", binary, ...args] };
|
|
1504
|
-
}
|
|
1505
|
-
return { executable: "pnpm", args: ["exec", binary, ...args] };
|
|
1506
|
-
}
|
|
1507
|
-
function scriptArgs(manager, name) {
|
|
1508
|
-
if (manager === "bun") {
|
|
1509
|
-
return { executable: "bun", args: ["run", name] };
|
|
1510
|
-
}
|
|
1511
|
-
if (manager === "npm") {
|
|
1512
|
-
return { executable: "npm", args: ["run", name] };
|
|
1513
|
-
}
|
|
1514
|
-
return { executable: manager, args: [name] };
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
// src/build.ts
|
|
1963
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1964
|
+
import path8 from "path";
|
|
1518
1965
|
async function checkBuild({
|
|
1519
1966
|
cwd,
|
|
1520
1967
|
packageManager,
|
|
@@ -1522,7 +1969,7 @@ async function checkBuild({
|
|
|
1522
1969
|
}) {
|
|
1523
1970
|
signal?.throwIfAborted();
|
|
1524
1971
|
const manifest = JSON.parse(
|
|
1525
|
-
await
|
|
1972
|
+
await readFile8(path8.join(cwd, "package.json"), "utf8")
|
|
1526
1973
|
);
|
|
1527
1974
|
const invocation = manifest.dependencies?.["react-router"] ? execArgs(packageManager, "react-router", ["build"]) : manifest.scripts?.build ? scriptArgs(packageManager, "build") : void 0;
|
|
1528
1975
|
if (!invocation) {
|
|
@@ -1557,8 +2004,8 @@ ${stderr}`.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean).slice(-1
|
|
|
1557
2004
|
}
|
|
1558
2005
|
|
|
1559
2006
|
// src/css-files.ts
|
|
1560
|
-
import { lstat, readFile as
|
|
1561
|
-
import
|
|
2007
|
+
import { lstat, readFile as readFile9, readdir as readdir2 } from "fs/promises";
|
|
2008
|
+
import path9 from "path";
|
|
1562
2009
|
var FILE_SIZE_CAP = 1024 * 1024;
|
|
1563
2010
|
var BUILD_FILE_SIZE_CAP = 8 * 1024 * 1024;
|
|
1564
2011
|
var ignoredDirectories = /* @__PURE__ */ new Set([
|
|
@@ -1582,7 +2029,7 @@ async function readCssInputs(cwd) {
|
|
|
1582
2029
|
return !name.startsWith(".") && !ignoredDirectories.has(name);
|
|
1583
2030
|
});
|
|
1584
2031
|
const built = [];
|
|
1585
|
-
await collect2(
|
|
2032
|
+
await collect2(path9.join(cwd, "build/client"), built, () => true);
|
|
1586
2033
|
const read = (files, cap) => texts(cwd, files, cap);
|
|
1587
2034
|
return {
|
|
1588
2035
|
sourceCss: await read(
|
|
@@ -1614,7 +2061,7 @@ async function texts(cwd, files, cap) {
|
|
|
1614
2061
|
}
|
|
1615
2062
|
result.push({
|
|
1616
2063
|
file: relative(cwd, file),
|
|
1617
|
-
text: await
|
|
2064
|
+
text: await readFile9(file, "utf8")
|
|
1618
2065
|
});
|
|
1619
2066
|
}
|
|
1620
2067
|
return result;
|
|
@@ -1633,7 +2080,7 @@ async function collect2(directory, files, enter) {
|
|
|
1633
2080
|
if (entry.isSymbolicLink()) {
|
|
1634
2081
|
continue;
|
|
1635
2082
|
}
|
|
1636
|
-
const file =
|
|
2083
|
+
const file = path9.join(directory, entry.name);
|
|
1637
2084
|
if (entry.isDirectory()) {
|
|
1638
2085
|
if (enter(entry.name)) {
|
|
1639
2086
|
await collect2(file, files, enter);
|
|
@@ -1747,9 +2194,9 @@ function important(css, code) {
|
|
|
1747
2194
|
}
|
|
1748
2195
|
return problems;
|
|
1749
2196
|
}
|
|
1750
|
-
function importantProblem(
|
|
2197
|
+
function importantProblem(message6, file, line) {
|
|
1751
2198
|
return problem(
|
|
1752
|
-
|
|
2199
|
+
message6,
|
|
1753
2200
|
"Raise the selector's specificity or reorder the rules instead. For a reviewed case, add /* zappie-allow-important: <reason> */ on the same line.",
|
|
1754
2201
|
{ rule: "css/important", file, ...line ? { line } : {} }
|
|
1755
2202
|
);
|
|
@@ -2333,7 +2780,7 @@ async function checkCss({
|
|
|
2333
2780
|
// src/lint.ts
|
|
2334
2781
|
import zappie from "eslint-plugin-zappie";
|
|
2335
2782
|
import { createRequire } from "module";
|
|
2336
|
-
import
|
|
2783
|
+
import path10 from "path";
|
|
2337
2784
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
2338
2785
|
async function checkLint({
|
|
2339
2786
|
cwd,
|
|
@@ -2342,18 +2789,23 @@ async function checkLint({
|
|
|
2342
2789
|
signal?.throwIfAborted();
|
|
2343
2790
|
let ESLint;
|
|
2344
2791
|
try {
|
|
2345
|
-
const require2 = createRequire(
|
|
2792
|
+
const require2 = createRequire(path10.join(cwd, "package.json"));
|
|
2346
2793
|
const loadedEslint = await import(pathToFileURL2(require2.resolve("eslint")).href);
|
|
2347
2794
|
ESLint = loadedEslint.ESLint;
|
|
2348
2795
|
} catch (error) {
|
|
2349
2796
|
return step("lint", "failed", [
|
|
2350
|
-
problem(
|
|
2797
|
+
problem(message3(error), "Install ESLint in the project.", {
|
|
2351
2798
|
rule: "lint/unavailable"
|
|
2352
2799
|
})
|
|
2353
2800
|
]);
|
|
2354
2801
|
}
|
|
2355
2802
|
try {
|
|
2356
|
-
const eslint = new ESLint({
|
|
2803
|
+
const eslint = new ESLint({
|
|
2804
|
+
cwd,
|
|
2805
|
+
overrideConfig: {
|
|
2806
|
+
settings: { zappie: { outboundAllow: await outboundAllow(cwd) } }
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2357
2809
|
const source = await sourceFile(cwd);
|
|
2358
2810
|
const config = await eslint.calculateConfigForFile(source);
|
|
2359
2811
|
const problems = recommendedProblems(config?.rules ?? {});
|
|
@@ -2378,7 +2830,7 @@ async function checkLint({
|
|
|
2378
2830
|
} catch (error) {
|
|
2379
2831
|
return step("lint", "failed", [
|
|
2380
2832
|
problem(
|
|
2381
|
-
|
|
2833
|
+
message3(error),
|
|
2382
2834
|
"Fix the project ESLint configuration and run lint again.",
|
|
2383
2835
|
{
|
|
2384
2836
|
rule: "lint/config"
|
|
@@ -2387,6 +2839,23 @@ async function checkLint({
|
|
|
2387
2839
|
]);
|
|
2388
2840
|
}
|
|
2389
2841
|
}
|
|
2842
|
+
async function outboundAllow(cwd) {
|
|
2843
|
+
try {
|
|
2844
|
+
const value = (await loadConfig(cwd)).value;
|
|
2845
|
+
if (!value || typeof value !== "object" || !("outbound" in value)) {
|
|
2846
|
+
return [];
|
|
2847
|
+
}
|
|
2848
|
+
const outbound = value.outbound;
|
|
2849
|
+
if (!outbound || typeof outbound !== "object" || !("allow" in outbound)) {
|
|
2850
|
+
return [];
|
|
2851
|
+
}
|
|
2852
|
+
return Array.isArray(outbound.allow) ? outbound.allow.filter(
|
|
2853
|
+
(entry) => typeof entry === "string"
|
|
2854
|
+
) : [];
|
|
2855
|
+
} catch {
|
|
2856
|
+
return [];
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2390
2859
|
function recommendedProblems(applied) {
|
|
2391
2860
|
const expected = recommendedRules();
|
|
2392
2861
|
return Object.entries(expected).flatMap(([rule, severity]) => {
|
|
@@ -2433,7 +2902,7 @@ async function sourceFile(cwd) {
|
|
|
2433
2902
|
for (const candidate of candidates) {
|
|
2434
2903
|
try {
|
|
2435
2904
|
await import("fs/promises").then(
|
|
2436
|
-
({ access:
|
|
2905
|
+
({ access: access4 }) => access4(path10.join(cwd, candidate))
|
|
2437
2906
|
);
|
|
2438
2907
|
return candidate;
|
|
2439
2908
|
} catch {
|
|
@@ -2442,13 +2911,13 @@ async function sourceFile(cwd) {
|
|
|
2442
2911
|
}
|
|
2443
2912
|
return "eslint.config.mjs";
|
|
2444
2913
|
}
|
|
2445
|
-
function
|
|
2914
|
+
function message3(error) {
|
|
2446
2915
|
return error instanceof Error ? error.message : String(error);
|
|
2447
2916
|
}
|
|
2448
2917
|
|
|
2449
2918
|
// src/readiness.ts
|
|
2450
|
-
import { readFile as
|
|
2451
|
-
import
|
|
2919
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2920
|
+
import path11 from "path";
|
|
2452
2921
|
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
2453
2922
|
async function checkReadiness({
|
|
2454
2923
|
cwd,
|
|
@@ -2466,9 +2935,9 @@ async function checkReadiness({
|
|
|
2466
2935
|
)
|
|
2467
2936
|
]);
|
|
2468
2937
|
}
|
|
2469
|
-
const file =
|
|
2938
|
+
const file = path11.join(cwd, "react-router.config.ts");
|
|
2470
2939
|
try {
|
|
2471
|
-
await
|
|
2940
|
+
await readFile10(file, "utf8");
|
|
2472
2941
|
const version = Date.now();
|
|
2473
2942
|
const config = await import(`${pathToFileURL3(file).href}?readiness=${version}`);
|
|
2474
2943
|
if (config.default?.routeDiscovery?.mode === "initial") {
|
|
@@ -2477,7 +2946,7 @@ async function checkReadiness({
|
|
|
2477
2946
|
} catch (error) {
|
|
2478
2947
|
return step("readiness", "failed", [
|
|
2479
2948
|
problem(
|
|
2480
|
-
|
|
2949
|
+
message4(error),
|
|
2481
2950
|
'Create react-router.config.ts and set routeDiscovery: { mode: "initial" }.',
|
|
2482
2951
|
{
|
|
2483
2952
|
rule: "readiness/route-discovery",
|
|
@@ -2494,13 +2963,15 @@ async function checkReadiness({
|
|
|
2494
2963
|
)
|
|
2495
2964
|
]);
|
|
2496
2965
|
}
|
|
2497
|
-
function
|
|
2966
|
+
function message4(error) {
|
|
2498
2967
|
return error instanceof Error ? error.message : String(error);
|
|
2499
2968
|
}
|
|
2500
2969
|
|
|
2501
2970
|
// src/secrets.ts
|
|
2502
|
-
import {
|
|
2503
|
-
import
|
|
2971
|
+
import { execFile } from "child_process";
|
|
2972
|
+
import { lstat as lstat2, readFile as readFile11, readdir as readdir3 } from "fs/promises";
|
|
2973
|
+
import path12 from "path";
|
|
2974
|
+
import { promisify } from "util";
|
|
2504
2975
|
|
|
2505
2976
|
// src/secret-patterns.ts
|
|
2506
2977
|
var FIX = "Move the value to a server environment variable and rotate it.";
|
|
@@ -2541,6 +3012,35 @@ function secretMatches(source, envFile) {
|
|
|
2541
3012
|
fix: FIX,
|
|
2542
3013
|
severity: "error"
|
|
2543
3014
|
}));
|
|
3015
|
+
const gitleaksDefaults = [
|
|
3016
|
+
[/\bghp_[A-Za-z0-9]{36}\b/g, "github-pat", "GitHub personal access token"],
|
|
3017
|
+
[
|
|
3018
|
+
/\bgithub_pat_[A-Za-z0-9_]{82}\b/g,
|
|
3019
|
+
"github-fine-grained-pat",
|
|
3020
|
+
"GitHub fine-grained access token"
|
|
3021
|
+
],
|
|
3022
|
+
[
|
|
3023
|
+
/\bglpat-[A-Za-z0-9_-]{20,}\b/g,
|
|
3024
|
+
"gitlab-pat",
|
|
3025
|
+
"GitLab personal access token"
|
|
3026
|
+
],
|
|
3027
|
+
[/\bnpm_[A-Za-z0-9]{36}\b/g, "npm-token", "npm access token"],
|
|
3028
|
+
[/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, "slack-token", "Slack token"],
|
|
3029
|
+
[/\bAIza[0-9A-Za-z_-]{35}\b/g, "google-api-key", "Google API key"],
|
|
3030
|
+
[
|
|
3031
|
+
/\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g,
|
|
3032
|
+
"sendgrid-api-key",
|
|
3033
|
+
"SendGrid API key"
|
|
3034
|
+
]
|
|
3035
|
+
];
|
|
3036
|
+
for (const [pattern, rule, label] of gitleaksDefaults) {
|
|
3037
|
+
add(matches2, source, pattern, () => ({
|
|
3038
|
+
rule: `secrets/${rule}`,
|
|
3039
|
+
message: `Detected a ${label}.`,
|
|
3040
|
+
fix: FIX,
|
|
3041
|
+
severity: "error"
|
|
3042
|
+
}));
|
|
3043
|
+
}
|
|
2544
3044
|
const occupied = matches2.map(({ index }) => index);
|
|
2545
3045
|
const assignment = /\b([A-Za-z_$][\w$]*(?:secret|token|password|api[_-]?key)[\w$]*)\s*[:=]\s*(["'])([^"'\r\n]{24,})\2/gi;
|
|
2546
3046
|
for (const found of source.matchAll(assignment)) {
|
|
@@ -2578,21 +3078,31 @@ function entropy(value) {
|
|
|
2578
3078
|
|
|
2579
3079
|
// src/secrets.ts
|
|
2580
3080
|
var SECRET_FILE_SIZE_CAP = 1024 * 1024;
|
|
2581
|
-
|
|
3081
|
+
var execFileAsync = promisify(execFile);
|
|
3082
|
+
var lockfile = /(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/;
|
|
3083
|
+
async function scanSecrets(dir, options = {}) {
|
|
2582
3084
|
const findings = [];
|
|
2583
3085
|
for (const file of await filesToScan(dir)) {
|
|
2584
|
-
|
|
3086
|
+
let info;
|
|
3087
|
+
try {
|
|
3088
|
+
info = await lstat2(file);
|
|
3089
|
+
} catch (error) {
|
|
3090
|
+
if (missing2(error)) {
|
|
3091
|
+
continue;
|
|
3092
|
+
}
|
|
3093
|
+
throw error;
|
|
3094
|
+
}
|
|
2585
3095
|
if (!info.isFile() || info.size > SECRET_FILE_SIZE_CAP) {
|
|
2586
3096
|
continue;
|
|
2587
3097
|
}
|
|
2588
|
-
const contents = await
|
|
3098
|
+
const contents = await readFile11(file);
|
|
2589
3099
|
if (contents.includes(0)) {
|
|
2590
3100
|
continue;
|
|
2591
3101
|
}
|
|
2592
3102
|
const source = contents.toString("utf8");
|
|
2593
3103
|
for (const match of secretMatches(
|
|
2594
3104
|
source,
|
|
2595
|
-
|
|
3105
|
+
path12.basename(file).startsWith(".env")
|
|
2596
3106
|
)) {
|
|
2597
3107
|
findings.push({
|
|
2598
3108
|
rule: match.rule,
|
|
@@ -2604,14 +3114,21 @@ async function scanSecrets(dir) {
|
|
|
2604
3114
|
});
|
|
2605
3115
|
}
|
|
2606
3116
|
}
|
|
3117
|
+
if (options.history) {
|
|
3118
|
+
findings.push(...await scanHistory(dir));
|
|
3119
|
+
}
|
|
2607
3120
|
return findings;
|
|
2608
3121
|
}
|
|
2609
3122
|
async function checkSecrets({
|
|
2610
3123
|
cwd,
|
|
3124
|
+
history,
|
|
2611
3125
|
signal
|
|
2612
3126
|
}) {
|
|
2613
3127
|
signal?.throwIfAborted();
|
|
2614
|
-
const findings = await scanSecrets(
|
|
3128
|
+
const findings = await scanSecrets(
|
|
3129
|
+
cwd,
|
|
3130
|
+
history === void 0 ? {} : { history }
|
|
3131
|
+
);
|
|
2615
3132
|
signal?.throwIfAborted();
|
|
2616
3133
|
const failed = findings.some(({ severity }) => severity === "error");
|
|
2617
3134
|
return step(
|
|
@@ -2621,25 +3138,93 @@ async function checkSecrets({
|
|
|
2621
3138
|
);
|
|
2622
3139
|
}
|
|
2623
3140
|
async function filesToScan(cwd) {
|
|
3141
|
+
const files = await trackedFiles(cwd);
|
|
3142
|
+
for (const directory of ["build/client", "build/server", "workers"]) {
|
|
3143
|
+
await collect3(path12.join(cwd, directory), files);
|
|
3144
|
+
}
|
|
3145
|
+
return [...new Set(files)].filter((file) => !lockfile.test(relative(cwd, file))).sort();
|
|
3146
|
+
}
|
|
3147
|
+
async function trackedFiles(cwd) {
|
|
3148
|
+
try {
|
|
3149
|
+
const { stdout } = await execFileAsync("git", ["ls-files", "-z"], {
|
|
3150
|
+
cwd,
|
|
3151
|
+
encoding: "utf8",
|
|
3152
|
+
maxBuffer: 32 * 1024 * 1024
|
|
3153
|
+
});
|
|
3154
|
+
const files = stdout.split("\0").filter(Boolean).map((file) => path12.join(cwd, file));
|
|
3155
|
+
return files.length > 0 ? files : fallbackFiles(cwd);
|
|
3156
|
+
} catch {
|
|
3157
|
+
return fallbackFiles(cwd);
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
async function fallbackFiles(cwd) {
|
|
2624
3161
|
const files = [];
|
|
2625
3162
|
for (const directory of ["app", "public"]) {
|
|
2626
|
-
await collect3(
|
|
3163
|
+
await collect3(path12.join(cwd, directory), files);
|
|
2627
3164
|
}
|
|
2628
|
-
await collect3(
|
|
2629
|
-
path11.join(cwd, "build/client"),
|
|
2630
|
-
files,
|
|
2631
|
-
(file) => file.endsWith(".js")
|
|
2632
|
-
);
|
|
2633
3165
|
try {
|
|
2634
3166
|
for (const entry of await readdir3(cwd, { withFileTypes: true })) {
|
|
2635
3167
|
if (entry.isFile() && rootConfig(entry.name)) {
|
|
2636
|
-
files.push(
|
|
3168
|
+
files.push(path12.join(cwd, entry.name));
|
|
2637
3169
|
}
|
|
2638
3170
|
}
|
|
2639
3171
|
} catch {
|
|
2640
|
-
return files
|
|
3172
|
+
return files;
|
|
3173
|
+
}
|
|
3174
|
+
return files;
|
|
3175
|
+
}
|
|
3176
|
+
async function scanHistory(cwd) {
|
|
3177
|
+
try {
|
|
3178
|
+
const { stdout } = await execFileAsync(
|
|
3179
|
+
"git",
|
|
3180
|
+
[
|
|
3181
|
+
"log",
|
|
3182
|
+
"--all",
|
|
3183
|
+
"--format=commit:%H",
|
|
3184
|
+
"-p",
|
|
3185
|
+
"--no-ext-diff",
|
|
3186
|
+
"--",
|
|
3187
|
+
".",
|
|
3188
|
+
":(exclude)package-lock.json",
|
|
3189
|
+
":(exclude)pnpm-lock.yaml",
|
|
3190
|
+
":(exclude)yarn.lock",
|
|
3191
|
+
":(exclude)bun.lock",
|
|
3192
|
+
":(exclude)bun.lockb"
|
|
3193
|
+
],
|
|
3194
|
+
{ cwd, encoding: "utf8", maxBuffer: 128 * 1024 * 1024 }
|
|
3195
|
+
);
|
|
3196
|
+
return secretMatches(stdout, false).map((match) => {
|
|
3197
|
+
const location2 = historyLocation(stdout, match.index);
|
|
3198
|
+
return {
|
|
3199
|
+
rule: match.rule,
|
|
3200
|
+
file: location2 ?? "git history",
|
|
3201
|
+
line: lineAt(stdout, match.index),
|
|
3202
|
+
message: match.message,
|
|
3203
|
+
fix: match.fix,
|
|
3204
|
+
severity: match.severity
|
|
3205
|
+
};
|
|
3206
|
+
});
|
|
3207
|
+
} catch {
|
|
3208
|
+
return [
|
|
3209
|
+
{
|
|
3210
|
+
rule: "secrets/history-unavailable",
|
|
3211
|
+
file: "git history",
|
|
3212
|
+
line: 1,
|
|
3213
|
+
message: "Could not scan Git history for secrets.",
|
|
3214
|
+
fix: "Check Git access and run zappie check --history again.",
|
|
3215
|
+
severity: "warning"
|
|
3216
|
+
}
|
|
3217
|
+
];
|
|
2641
3218
|
}
|
|
2642
|
-
|
|
3219
|
+
}
|
|
3220
|
+
function historyLocation(source, index) {
|
|
3221
|
+
const prefix = source.slice(0, index);
|
|
3222
|
+
const commit = [...prefix.matchAll(/^commit:([0-9a-f]+)$/gm)].at(-1)?.[1];
|
|
3223
|
+
const changedPaths = [
|
|
3224
|
+
...prefix.matchAll(/^(?:\+\+\+ b\/|--- a\/)(.+)$/gm)
|
|
3225
|
+
].map((match) => match[1]);
|
|
3226
|
+
const file = changedPaths.at(-1);
|
|
3227
|
+
return commit && file ? `${commit}:${file}` : void 0;
|
|
2643
3228
|
}
|
|
2644
3229
|
async function collect3(directory, files, accept = () => true) {
|
|
2645
3230
|
try {
|
|
@@ -2647,7 +3232,7 @@ async function collect3(directory, files, accept = () => true) {
|
|
|
2647
3232
|
if (entry.isSymbolicLink()) {
|
|
2648
3233
|
continue;
|
|
2649
3234
|
}
|
|
2650
|
-
const file =
|
|
3235
|
+
const file = path12.join(directory, entry.name);
|
|
2651
3236
|
if (entry.isDirectory()) {
|
|
2652
3237
|
await collect3(file, files, accept);
|
|
2653
3238
|
} else if (entry.isFile() && accept(file)) {
|
|
@@ -2655,11 +3240,14 @@ async function collect3(directory, files, accept = () => true) {
|
|
|
2655
3240
|
}
|
|
2656
3241
|
}
|
|
2657
3242
|
} catch (error) {
|
|
2658
|
-
if (!(error
|
|
3243
|
+
if (!missing2(error)) {
|
|
2659
3244
|
throw error;
|
|
2660
3245
|
}
|
|
2661
3246
|
}
|
|
2662
3247
|
}
|
|
3248
|
+
function missing2(error) {
|
|
3249
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
3250
|
+
}
|
|
2663
3251
|
function rootConfig(name) {
|
|
2664
3252
|
return name === "package.json" || name === "wrangler.json" || name === "wrangler.jsonc" || /(?:^|\.)config\.(?:[cm]?[jt]s|json)$/.test(name);
|
|
2665
3253
|
}
|
|
@@ -2668,13 +3256,13 @@ function lineAt(source, index) {
|
|
|
2668
3256
|
}
|
|
2669
3257
|
|
|
2670
3258
|
// src/test-step.ts
|
|
2671
|
-
import
|
|
3259
|
+
import path13 from "path";
|
|
2672
3260
|
async function checkTests({
|
|
2673
3261
|
cwd,
|
|
2674
3262
|
packageManager,
|
|
2675
3263
|
signal
|
|
2676
3264
|
}) {
|
|
2677
|
-
const manifest = await json(
|
|
3265
|
+
const manifest = await json(path13.join(cwd, "package.json"));
|
|
2678
3266
|
if (!manifest.scripts?.test) {
|
|
2679
3267
|
return skipped("test", "No test script is defined in package.json.");
|
|
2680
3268
|
}
|
|
@@ -2697,11 +3285,11 @@ async function checkTests({
|
|
|
2697
3285
|
}
|
|
2698
3286
|
|
|
2699
3287
|
// src/types-step.ts
|
|
2700
|
-
import
|
|
3288
|
+
import path14 from "path";
|
|
2701
3289
|
async function checkTypes(options) {
|
|
2702
3290
|
const { cwd, packageManager, signal } = options;
|
|
2703
3291
|
signal?.throwIfAborted();
|
|
2704
|
-
const manifest = await json(
|
|
3292
|
+
const manifest = await json(path14.join(cwd, "package.json"));
|
|
2705
3293
|
if (manifest.dependencies?.["react-router"]) {
|
|
2706
3294
|
const generated = execArgs(packageManager, "react-router", ["typegen"]);
|
|
2707
3295
|
const result2 = await command(
|
|
@@ -2755,6 +3343,7 @@ function outputProblems(output) {
|
|
|
2755
3343
|
var runners = [
|
|
2756
3344
|
{ name: "config", run: checkConfig },
|
|
2757
3345
|
{ name: "allowlist", run: checkAllowlist },
|
|
3346
|
+
{ name: "audit", run: checkAudit },
|
|
2758
3347
|
{ name: "lint", run: checkLint },
|
|
2759
3348
|
{ name: "types", run: checkTypes },
|
|
2760
3349
|
{ name: "test", run: checkTests },
|
|
@@ -2775,13 +3364,13 @@ async function runCheck(options) {
|
|
|
2775
3364
|
options.signal?.throwIfAborted();
|
|
2776
3365
|
checked = step(runner.name, "failed", [
|
|
2777
3366
|
problem(
|
|
2778
|
-
|
|
3367
|
+
message5(error),
|
|
2779
3368
|
"Fix the step setup and run zappie check again."
|
|
2780
3369
|
)
|
|
2781
3370
|
]);
|
|
2782
3371
|
}
|
|
2783
3372
|
steps.push(checked);
|
|
2784
|
-
if (checked.result === "failed" && !options.all) {
|
|
3373
|
+
if (checked.result === "failed" && !options.all && runner.name !== "audit") {
|
|
2785
3374
|
break;
|
|
2786
3375
|
}
|
|
2787
3376
|
}
|
|
@@ -2790,7 +3379,7 @@ async function runCheck(options) {
|
|
|
2790
3379
|
steps
|
|
2791
3380
|
};
|
|
2792
3381
|
}
|
|
2793
|
-
function
|
|
3382
|
+
function message5(error) {
|
|
2794
3383
|
return error instanceof Error ? error.message : String(error);
|
|
2795
3384
|
}
|
|
2796
3385
|
export {
|
|
@@ -2799,6 +3388,7 @@ export {
|
|
|
2799
3388
|
SECRET_FILE_SIZE_CAP,
|
|
2800
3389
|
analyseCss,
|
|
2801
3390
|
checkAllowlist,
|
|
3391
|
+
checkAudit,
|
|
2802
3392
|
checkBudgets,
|
|
2803
3393
|
checkBuild,
|
|
2804
3394
|
checkConfig,
|
|
@@ -2808,7 +3398,9 @@ export {
|
|
|
2808
3398
|
checkSecrets,
|
|
2809
3399
|
checkTests,
|
|
2810
3400
|
checkTypes,
|
|
3401
|
+
evaluateAudit,
|
|
2811
3402
|
loadConfig,
|
|
3403
|
+
parseAuditOutput,
|
|
2812
3404
|
runCheck,
|
|
2813
3405
|
scanSecrets,
|
|
2814
3406
|
supportsTypeScriptImport
|