@zappie/check 0.0.0-stage → 0.1.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 +21 -0
- package/LICENSE +21 -0
- package/README.md +50 -2
- package/dist/index.d.ts +62 -0
- package/dist/index.js +1950 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -3
- package/schema/check-result.v1.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1950 @@
|
|
|
1
|
+
// ../sdk/src/site-config/types.ts
|
|
2
|
+
var DEFAULT_BUDGETS = {
|
|
3
|
+
jsKbGz: 200,
|
|
4
|
+
cssKbGz: 50,
|
|
5
|
+
lcpMs: 2500
|
|
6
|
+
};
|
|
7
|
+
var GLOBAL_PACKAGE_ALLOWLIST = [
|
|
8
|
+
"react",
|
|
9
|
+
"react-dom",
|
|
10
|
+
"react-router",
|
|
11
|
+
"@react-router/*",
|
|
12
|
+
"@zappie/*",
|
|
13
|
+
"tailwindcss",
|
|
14
|
+
"@tailwindcss/*",
|
|
15
|
+
"clsx",
|
|
16
|
+
"tailwind-merge",
|
|
17
|
+
"class-variance-authority",
|
|
18
|
+
"lucide-react",
|
|
19
|
+
"date-fns",
|
|
20
|
+
"embla-carousel-react",
|
|
21
|
+
"framer-motion/motion",
|
|
22
|
+
"zod",
|
|
23
|
+
"@radix-ui/*",
|
|
24
|
+
"sonner",
|
|
25
|
+
"vaul"
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// ../sdk/src/site-config/helpers.ts
|
|
29
|
+
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
function distance(left, right) {
|
|
31
|
+
let row = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
32
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
33
|
+
const next = [leftIndex];
|
|
34
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
35
|
+
next[rightIndex] = Math.min(
|
|
36
|
+
(next[rightIndex - 1] ?? 0) + 1,
|
|
37
|
+
(row[rightIndex] ?? 0) + 1,
|
|
38
|
+
(row[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
row = next;
|
|
42
|
+
}
|
|
43
|
+
return row[right.length] ?? Infinity;
|
|
44
|
+
}
|
|
45
|
+
function unknownKeys(value, allowed, path12, issue) {
|
|
46
|
+
for (const key of Object.keys(value)) {
|
|
47
|
+
if (allowed.includes(key)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const closest = allowed.map((candidate) => ({ candidate, score: distance(key, candidate) })).sort((left, right) => left.score - right.score)[0];
|
|
51
|
+
const hint = closest && closest.score <= 2 ? closest.candidate : void 0;
|
|
52
|
+
issue({
|
|
53
|
+
path: path12 ? `${path12}.${key}` : key,
|
|
54
|
+
code: "unknown_key",
|
|
55
|
+
message: hint ? `Unknown field "${key}". Rename it to "${hint}".` : `Unknown field "${key}". Remove it from the configuration.`,
|
|
56
|
+
severity: "error"
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
var nonblank = (value) => typeof value === "string" && value.trim().length > 0;
|
|
61
|
+
var packageName = (value) => value.length <= 214 && /^(?:@[a-z0-9][a-z0-9._~-]*\/[a-z0-9][a-z0-9._~-]*|[a-z0-9][a-z0-9._~-]*)$/.test(
|
|
62
|
+
value
|
|
63
|
+
);
|
|
64
|
+
var globallyAllowed = (value) => GLOBAL_PACKAGE_ALLOWLIST.some(
|
|
65
|
+
(allowed) => allowed.endsWith("/*") ? value.startsWith(allowed.slice(0, -1)) : value === allowed
|
|
66
|
+
);
|
|
67
|
+
function hostname(value) {
|
|
68
|
+
const wildcard = value.startsWith("*.");
|
|
69
|
+
const host = wildcard ? value.slice(2) : value;
|
|
70
|
+
if (!host.includes(".") || /^\d+(?:\.\d+){3}$/.test(host) || host.length > 253 || host.includes("..")) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return host.split(".").every(
|
|
74
|
+
(label) => label.length > 0 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ../sdk/src/site-config/sections.ts
|
|
79
|
+
function nested(value, path12, keys, issue, validate) {
|
|
80
|
+
if (value === void 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!record(value)) {
|
|
84
|
+
issue({
|
|
85
|
+
path: path12,
|
|
86
|
+
code: "invalid_type",
|
|
87
|
+
message: `Expected an object. Set ${path12} to an object or remove it.`,
|
|
88
|
+
severity: "error"
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
unknownKeys(value, keys, path12, issue);
|
|
93
|
+
validate(value);
|
|
94
|
+
}
|
|
95
|
+
function budgets(value, issue, error) {
|
|
96
|
+
nested(
|
|
97
|
+
value,
|
|
98
|
+
"budgets",
|
|
99
|
+
["jsKbGz", "cssKbGz", "lcpMs", "override"],
|
|
100
|
+
issue,
|
|
101
|
+
(budget) => {
|
|
102
|
+
nested(
|
|
103
|
+
budget.override,
|
|
104
|
+
"budgets.override",
|
|
105
|
+
["reason"],
|
|
106
|
+
issue,
|
|
107
|
+
(override) => {
|
|
108
|
+
if (typeof override.reason !== "string" || !override.reason.trim()) {
|
|
109
|
+
error(
|
|
110
|
+
"budgets.override.reason",
|
|
111
|
+
"invalid_format",
|
|
112
|
+
"Expected a non-blank reason. Explain why the default budget must be raised."
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
for (const key of ["jsKbGz", "cssKbGz", "lcpMs"]) {
|
|
118
|
+
const amount = budget[key];
|
|
119
|
+
if (amount === void 0) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) {
|
|
123
|
+
error(
|
|
124
|
+
`budgets.${key}`,
|
|
125
|
+
"invalid_type",
|
|
126
|
+
`Expected a positive finite number. Set budgets.${key} to a number greater than zero.`
|
|
127
|
+
);
|
|
128
|
+
} else if (amount > DEFAULT_BUDGETS[key]) {
|
|
129
|
+
const message4 = `The ${key} budget exceeds the ${DEFAULT_BUDGETS[key]} default.`;
|
|
130
|
+
if (budget.override === void 0) {
|
|
131
|
+
error(
|
|
132
|
+
`budgets.${key}`,
|
|
133
|
+
"budget_override",
|
|
134
|
+
`${message4} Add budgets.override.reason to explain the increase.`
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
issue({
|
|
138
|
+
path: `budgets.${key}`,
|
|
139
|
+
code: "budget_override",
|
|
140
|
+
message: `${message4} Lower it to the default when the exception is no longer needed.`,
|
|
141
|
+
severity: "warning"
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
function analytics(value, issue, error) {
|
|
150
|
+
nested(value, "analytics", ["forward"], issue, (analyticsValue) => {
|
|
151
|
+
nested(
|
|
152
|
+
analyticsValue.forward,
|
|
153
|
+
"analytics.forward",
|
|
154
|
+
["ga4", "metaPixel"],
|
|
155
|
+
issue,
|
|
156
|
+
(forward) => {
|
|
157
|
+
if (forward.ga4 !== void 0 && (typeof forward.ga4 !== "string" || !/^G-[A-Z0-9]+$/.test(forward.ga4))) {
|
|
158
|
+
error(
|
|
159
|
+
"analytics.forward.ga4",
|
|
160
|
+
"invalid_format",
|
|
161
|
+
"Expected a GA4 measurement id beginning with G-. Use a value such as G-XXXXXXXXXX."
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (forward.metaPixel !== void 0 && (typeof forward.metaPixel !== "string" || !/^\d+$/.test(forward.metaPixel))) {
|
|
165
|
+
error(
|
|
166
|
+
"analytics.forward.metaPixel",
|
|
167
|
+
"invalid_format",
|
|
168
|
+
"Expected a numeric Meta Pixel id. Use digits only."
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ../sdk/src/site-config/validate.ts
|
|
177
|
+
var rootKeys = [
|
|
178
|
+
"store",
|
|
179
|
+
"branches",
|
|
180
|
+
"theme",
|
|
181
|
+
"prerender",
|
|
182
|
+
"menu",
|
|
183
|
+
"packages",
|
|
184
|
+
"outbound",
|
|
185
|
+
"budgets",
|
|
186
|
+
"analytics"
|
|
187
|
+
];
|
|
188
|
+
function validateConfig(input) {
|
|
189
|
+
const issues = [];
|
|
190
|
+
const issue = (value) => issues.push(value);
|
|
191
|
+
const error = (path12, code, message4) => issue({ path: path12, code, message: message4, severity: "error" });
|
|
192
|
+
if (!record(input)) {
|
|
193
|
+
return {
|
|
194
|
+
valid: false,
|
|
195
|
+
issues: [
|
|
196
|
+
{
|
|
197
|
+
path: "$",
|
|
198
|
+
code: "invalid_type",
|
|
199
|
+
message: "Expected a configuration object. Export an object from zappie.config.ts.",
|
|
200
|
+
severity: "error"
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
unknownKeys(input, rootKeys, "", issue);
|
|
206
|
+
requiredSlug(input.store, "store", error);
|
|
207
|
+
theme(input.theme, error);
|
|
208
|
+
stringList(input.branches, "branches", error, {
|
|
209
|
+
nonempty: true,
|
|
210
|
+
unique: true,
|
|
211
|
+
slug: true
|
|
212
|
+
});
|
|
213
|
+
paths(input.prerender, error);
|
|
214
|
+
nested(input.menu, "menu", ["hiddenCategories"], issue, (value) => {
|
|
215
|
+
stringList(value.hiddenCategories, "menu.hiddenCategories", error);
|
|
216
|
+
});
|
|
217
|
+
nested(input.packages, "packages", ["allow"], issue, (value) => {
|
|
218
|
+
const allow = stringList(value.allow, "packages.allow", error);
|
|
219
|
+
allow?.forEach((name, index) => {
|
|
220
|
+
if (globallyAllowed(name)) {
|
|
221
|
+
issue({
|
|
222
|
+
path: `packages.allow[${index}]`,
|
|
223
|
+
code: "redundant_allowlist",
|
|
224
|
+
message: `${name} is already globally allowed. Remove it from packages.allow.`,
|
|
225
|
+
severity: "warning"
|
|
226
|
+
});
|
|
227
|
+
} else if (!packageName(name)) {
|
|
228
|
+
error(
|
|
229
|
+
`packages.allow[${index}]`,
|
|
230
|
+
"invalid_format",
|
|
231
|
+
"Expected a lowercase npm package name. Use a package name such as lucide-react or @scope/name."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
nested(input.outbound, "outbound", ["allow"], issue, (value) => {
|
|
237
|
+
const allow = stringList(value.allow, "outbound.allow", error);
|
|
238
|
+
allow?.forEach((host, index) => {
|
|
239
|
+
if (!hostname(host)) {
|
|
240
|
+
error(
|
|
241
|
+
`outbound.allow[${index}]`,
|
|
242
|
+
"invalid_format",
|
|
243
|
+
"Expected a hostname or leading *. subdomain pattern without a scheme, path, port or IP. Use a value such as api.example.com."
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
budgets(input.budgets, issue, error);
|
|
249
|
+
analytics(input.analytics, issue, error);
|
|
250
|
+
return {
|
|
251
|
+
valid: !issues.some((entry) => entry.severity === "error"),
|
|
252
|
+
issues
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function requiredSlug(value, path12, error) {
|
|
256
|
+
if (!nonblank(value)) {
|
|
257
|
+
error(
|
|
258
|
+
path12,
|
|
259
|
+
value === void 0 ? "missing_required" : "invalid_type",
|
|
260
|
+
`Expected a non-empty storefront slug. Set ${path12} to the slug used by createZappie().`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function theme(value, error) {
|
|
265
|
+
if (typeof value !== "string") {
|
|
266
|
+
error(
|
|
267
|
+
"theme",
|
|
268
|
+
value === void 0 ? "missing_required" : "invalid_type",
|
|
269
|
+
"Expected a relative .json path. Set theme to a value such as ./theme.json."
|
|
270
|
+
);
|
|
271
|
+
} else if (!/\.json$/.test(value) || /^(?:[a-z][a-z\d+.-]*:|\/|\\)/i.test(value) || value.includes("\\")) {
|
|
272
|
+
error(
|
|
273
|
+
"theme",
|
|
274
|
+
"invalid_format",
|
|
275
|
+
"Expected a relative .json path. Use a value such as ./theme.json."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function stringList(value, path12, error, options = {}) {
|
|
280
|
+
if (value === void 0) {
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
283
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
284
|
+
error(
|
|
285
|
+
path12,
|
|
286
|
+
"invalid_type",
|
|
287
|
+
`Expected an array of strings. Set ${path12} to string values.`
|
|
288
|
+
);
|
|
289
|
+
return void 0;
|
|
290
|
+
}
|
|
291
|
+
if (options.nonempty && value.length === 0) {
|
|
292
|
+
error(
|
|
293
|
+
path12,
|
|
294
|
+
"invalid_format",
|
|
295
|
+
`Expected at least one value. Add a value to ${path12} or remove the field.`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
value.forEach((entry, index) => {
|
|
299
|
+
if (options.slug && !nonblank(entry)) {
|
|
300
|
+
error(
|
|
301
|
+
`${path12}[${index}]`,
|
|
302
|
+
"invalid_format",
|
|
303
|
+
"Expected a non-empty branch slug. Remove the empty value or use a branch slug."
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
if (options.unique && new Set(value).size !== value.length) {
|
|
308
|
+
error(
|
|
309
|
+
path12,
|
|
310
|
+
"duplicate",
|
|
311
|
+
`Expected unique values. Remove duplicates from ${path12}.`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
function paths(value, error) {
|
|
317
|
+
const values = stringList(value, "prerender", error, { unique: true });
|
|
318
|
+
values?.forEach((path12, index) => {
|
|
319
|
+
if (!path12.startsWith("/")) {
|
|
320
|
+
error(
|
|
321
|
+
`prerender[${index}]`,
|
|
322
|
+
"invalid_format",
|
|
323
|
+
"Expected a route starting with /. Prefix the route with /."
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/allowlist.ts
|
|
330
|
+
import { access, readFile as readFile3 } from "fs/promises";
|
|
331
|
+
import path3 from "path";
|
|
332
|
+
|
|
333
|
+
// ../tokens/src/names.ts
|
|
334
|
+
var COLOR_TOKENS = [
|
|
335
|
+
"--z-bg",
|
|
336
|
+
"--z-surface",
|
|
337
|
+
"--z-surface-raised",
|
|
338
|
+
"--z-overlay",
|
|
339
|
+
"--z-text",
|
|
340
|
+
"--z-text-muted",
|
|
341
|
+
"--z-text-inverse",
|
|
342
|
+
"--z-accent",
|
|
343
|
+
"--z-accent-contrast",
|
|
344
|
+
"--z-brand-1",
|
|
345
|
+
"--z-brand-2",
|
|
346
|
+
"--z-brand-3",
|
|
347
|
+
"--z-success",
|
|
348
|
+
"--z-warning",
|
|
349
|
+
"--z-danger",
|
|
350
|
+
"--z-border",
|
|
351
|
+
"--z-focus-ring"
|
|
352
|
+
];
|
|
353
|
+
var LENGTH_TOKENS = [
|
|
354
|
+
"--z-radius-sm",
|
|
355
|
+
"--z-radius-md",
|
|
356
|
+
"--z-radius-lg",
|
|
357
|
+
"--z-radius-pill",
|
|
358
|
+
"--z-space-1",
|
|
359
|
+
"--z-space-2",
|
|
360
|
+
"--z-space-3",
|
|
361
|
+
"--z-space-4",
|
|
362
|
+
"--z-space-5",
|
|
363
|
+
"--z-space-6",
|
|
364
|
+
"--z-space-7",
|
|
365
|
+
"--z-space-8",
|
|
366
|
+
"--z-text-xs",
|
|
367
|
+
"--z-text-sm",
|
|
368
|
+
"--z-text-md",
|
|
369
|
+
"--z-text-lg",
|
|
370
|
+
"--z-text-xl",
|
|
371
|
+
"--z-text-2xl",
|
|
372
|
+
"--z-text-3xl",
|
|
373
|
+
"--z-drawer-width",
|
|
374
|
+
"--z-dialog-width",
|
|
375
|
+
"--z-dialog-height",
|
|
376
|
+
"--z-media-sm"
|
|
377
|
+
];
|
|
378
|
+
var FONT_TOKENS = [
|
|
379
|
+
"--z-font-display",
|
|
380
|
+
"--z-font-body",
|
|
381
|
+
"--z-font-mono"
|
|
382
|
+
];
|
|
383
|
+
var DURATION_TOKENS = [
|
|
384
|
+
"--z-duration-fast",
|
|
385
|
+
"--z-duration-base",
|
|
386
|
+
"--z-duration-slow"
|
|
387
|
+
];
|
|
388
|
+
var EASING_TOKENS = ["--z-ease-out", "--z-ease-in-out"];
|
|
389
|
+
var MOTION_TOKENS = [...DURATION_TOKENS, ...EASING_TOKENS];
|
|
390
|
+
var TOKEN_NAMES = [
|
|
391
|
+
...COLOR_TOKENS,
|
|
392
|
+
...LENGTH_TOKENS,
|
|
393
|
+
...FONT_TOKENS,
|
|
394
|
+
...MOTION_TOKENS
|
|
395
|
+
];
|
|
396
|
+
var REQUIRED_TOKENS = [
|
|
397
|
+
"--z-bg",
|
|
398
|
+
"--z-text",
|
|
399
|
+
"--z-accent",
|
|
400
|
+
"--z-accent-contrast"
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
// ../tokens/src/defaults.ts
|
|
404
|
+
var DEFAULT_TOKENS = Object.freeze({
|
|
405
|
+
"--z-bg": "#F9FAFB",
|
|
406
|
+
"--z-surface": "#FFFFFF",
|
|
407
|
+
"--z-surface-raised": "#FFFFFF",
|
|
408
|
+
"--z-overlay": "#00000080",
|
|
409
|
+
"--z-text": "#101828",
|
|
410
|
+
"--z-text-muted": "#344054",
|
|
411
|
+
"--z-text-inverse": "#FFFFFF",
|
|
412
|
+
"--z-accent": "#9FE870",
|
|
413
|
+
"--z-accent-contrast": "#163300",
|
|
414
|
+
"--z-brand-1": "#9FE870",
|
|
415
|
+
"--z-brand-2": "#163300",
|
|
416
|
+
"--z-brand-3": "#EDC843",
|
|
417
|
+
"--z-success": "#027A48",
|
|
418
|
+
"--z-warning": "#ED7A37",
|
|
419
|
+
"--z-danger": "#F04438",
|
|
420
|
+
"--z-border": "#EAECF0",
|
|
421
|
+
"--z-focus-ring": "#163300",
|
|
422
|
+
"--z-radius-sm": "6px",
|
|
423
|
+
"--z-radius-md": "12px",
|
|
424
|
+
"--z-radius-lg": "20px",
|
|
425
|
+
"--z-radius-pill": "9999px",
|
|
426
|
+
"--z-space-1": "4px",
|
|
427
|
+
"--z-space-2": "8px",
|
|
428
|
+
"--z-space-3": "12px",
|
|
429
|
+
"--z-space-4": "16px",
|
|
430
|
+
"--z-space-5": "24px",
|
|
431
|
+
"--z-space-6": "32px",
|
|
432
|
+
"--z-space-7": "48px",
|
|
433
|
+
"--z-space-8": "64px",
|
|
434
|
+
"--z-font-display": "system-ui, sans-serif",
|
|
435
|
+
"--z-font-body": "system-ui, sans-serif",
|
|
436
|
+
"--z-font-mono": "ui-monospace, monospace",
|
|
437
|
+
"--z-text-xs": "12px",
|
|
438
|
+
"--z-text-sm": "14px",
|
|
439
|
+
"--z-text-md": "16px",
|
|
440
|
+
"--z-text-lg": "18px",
|
|
441
|
+
"--z-text-xl": "20px",
|
|
442
|
+
"--z-text-2xl": "24px",
|
|
443
|
+
"--z-text-3xl": "32px",
|
|
444
|
+
"--z-duration-fast": "120ms",
|
|
445
|
+
"--z-duration-base": "200ms",
|
|
446
|
+
"--z-duration-slow": "320ms",
|
|
447
|
+
"--z-ease-out": "cubic-bezier(0, 0, 0.2, 1)",
|
|
448
|
+
"--z-ease-in-out": "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
449
|
+
"--z-drawer-width": "420px",
|
|
450
|
+
"--z-dialog-width": "min(1000px, calc(100vw - 32px))",
|
|
451
|
+
"--z-dialog-height": "min(780px, 90dvh)",
|
|
452
|
+
"--z-media-sm": "64px"
|
|
453
|
+
});
|
|
454
|
+
var DEFAULT_THEME = Object.freeze({
|
|
455
|
+
version: 1,
|
|
456
|
+
name: "Zappie",
|
|
457
|
+
tokens: DEFAULT_TOKENS,
|
|
458
|
+
darkMode: "light",
|
|
459
|
+
dark: Object.freeze({
|
|
460
|
+
"--z-bg": "#000000",
|
|
461
|
+
"--z-surface": "#242424",
|
|
462
|
+
"--z-surface-raised": "#333333",
|
|
463
|
+
"--z-text": "#FFFFFF",
|
|
464
|
+
"--z-text-muted": "#BFBFBF",
|
|
465
|
+
"--z-text-inverse": "#101828",
|
|
466
|
+
"--z-border": "#474747",
|
|
467
|
+
"--z-focus-ring": "#9FE870"
|
|
468
|
+
})
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// ../tokens/src/named-colors.ts
|
|
472
|
+
var NAMED_COLORS = {
|
|
473
|
+
aliceblue: "f0f8ff",
|
|
474
|
+
antiquewhite: "faebd7",
|
|
475
|
+
aqua: "00ffff",
|
|
476
|
+
aquamarine: "7fffd4",
|
|
477
|
+
azure: "f0ffff",
|
|
478
|
+
beige: "f5f5dc",
|
|
479
|
+
bisque: "ffe4c4",
|
|
480
|
+
black: "000000",
|
|
481
|
+
blanchedalmond: "ffebcd",
|
|
482
|
+
blue: "0000ff",
|
|
483
|
+
blueviolet: "8a2be2",
|
|
484
|
+
brown: "a52a2a",
|
|
485
|
+
burlywood: "deb887",
|
|
486
|
+
cadetblue: "5f9ea0",
|
|
487
|
+
chartreuse: "7fff00",
|
|
488
|
+
chocolate: "d2691e",
|
|
489
|
+
coral: "ff7f50",
|
|
490
|
+
cornflowerblue: "6495ed",
|
|
491
|
+
cornsilk: "fff8dc",
|
|
492
|
+
crimson: "dc143c",
|
|
493
|
+
cyan: "00ffff",
|
|
494
|
+
darkblue: "00008b",
|
|
495
|
+
darkcyan: "008b8b",
|
|
496
|
+
darkgoldenrod: "b8860b",
|
|
497
|
+
darkgray: "a9a9a9",
|
|
498
|
+
darkgreen: "006400",
|
|
499
|
+
darkgrey: "a9a9a9",
|
|
500
|
+
darkkhaki: "bdb76b",
|
|
501
|
+
darkmagenta: "8b008b",
|
|
502
|
+
darkolivegreen: "556b2f",
|
|
503
|
+
darkorange: "ff8c00",
|
|
504
|
+
darkorchid: "9932cc",
|
|
505
|
+
darkred: "8b0000",
|
|
506
|
+
darksalmon: "e9967a",
|
|
507
|
+
darkseagreen: "8fbc8f",
|
|
508
|
+
darkslateblue: "483d8b",
|
|
509
|
+
darkslategray: "2f4f4f",
|
|
510
|
+
darkslategrey: "2f4f4f",
|
|
511
|
+
darkturquoise: "00ced1",
|
|
512
|
+
darkviolet: "9400d3",
|
|
513
|
+
deeppink: "ff1493",
|
|
514
|
+
deepskyblue: "00bfff",
|
|
515
|
+
dimgray: "696969",
|
|
516
|
+
dimgrey: "696969",
|
|
517
|
+
dodgerblue: "1e90ff",
|
|
518
|
+
firebrick: "b22222",
|
|
519
|
+
floralwhite: "fffaf0",
|
|
520
|
+
forestgreen: "228b22",
|
|
521
|
+
fuchsia: "ff00ff",
|
|
522
|
+
gainsboro: "dcdcdc",
|
|
523
|
+
ghostwhite: "f8f8ff",
|
|
524
|
+
gold: "ffd700",
|
|
525
|
+
goldenrod: "daa520",
|
|
526
|
+
gray: "808080",
|
|
527
|
+
green: "008000",
|
|
528
|
+
greenyellow: "adff2f",
|
|
529
|
+
grey: "808080",
|
|
530
|
+
honeydew: "f0fff0",
|
|
531
|
+
hotpink: "ff69b4",
|
|
532
|
+
indianred: "cd5c5c",
|
|
533
|
+
indigo: "4b0082",
|
|
534
|
+
ivory: "fffff0",
|
|
535
|
+
khaki: "f0e68c",
|
|
536
|
+
lavender: "e6e6fa",
|
|
537
|
+
lavenderblush: "fff0f5",
|
|
538
|
+
lawngreen: "7cfc00",
|
|
539
|
+
lemonchiffon: "fffacd",
|
|
540
|
+
lightblue: "add8e6",
|
|
541
|
+
lightcoral: "f08080",
|
|
542
|
+
lightcyan: "e0ffff",
|
|
543
|
+
lightgoldenrodyellow: "fafad2",
|
|
544
|
+
lightgray: "d3d3d3",
|
|
545
|
+
lightgreen: "90ee90",
|
|
546
|
+
lightgrey: "d3d3d3",
|
|
547
|
+
lightpink: "ffb6c1",
|
|
548
|
+
lightsalmon: "ffa07a",
|
|
549
|
+
lightseagreen: "20b2aa",
|
|
550
|
+
lightskyblue: "87cefa",
|
|
551
|
+
lightslategray: "778899",
|
|
552
|
+
lightslategrey: "778899",
|
|
553
|
+
lightsteelblue: "b0c4de",
|
|
554
|
+
lightyellow: "ffffe0",
|
|
555
|
+
lime: "00ff00",
|
|
556
|
+
limegreen: "32cd32",
|
|
557
|
+
linen: "faf0e6",
|
|
558
|
+
magenta: "ff00ff",
|
|
559
|
+
maroon: "800000",
|
|
560
|
+
mediumaquamarine: "66cdaa",
|
|
561
|
+
mediumblue: "0000cd",
|
|
562
|
+
mediumorchid: "ba55d3",
|
|
563
|
+
mediumpurple: "9370db",
|
|
564
|
+
mediumseagreen: "3cb371",
|
|
565
|
+
mediumslateblue: "7b68ee",
|
|
566
|
+
mediumspringgreen: "00fa9a",
|
|
567
|
+
mediumturquoise: "48d1cc",
|
|
568
|
+
mediumvioletred: "c71585",
|
|
569
|
+
midnightblue: "191970",
|
|
570
|
+
mintcream: "f5fffa",
|
|
571
|
+
mistyrose: "ffe4e1",
|
|
572
|
+
moccasin: "ffe4b5",
|
|
573
|
+
navajowhite: "ffdead",
|
|
574
|
+
navy: "000080",
|
|
575
|
+
oldlace: "fdf5e6",
|
|
576
|
+
olive: "808000",
|
|
577
|
+
olivedrab: "6b8e23",
|
|
578
|
+
orange: "ffa500",
|
|
579
|
+
orangered: "ff4500",
|
|
580
|
+
orchid: "da70d6",
|
|
581
|
+
palegoldenrod: "eee8aa",
|
|
582
|
+
palegreen: "98fb98",
|
|
583
|
+
paleturquoise: "afeeee",
|
|
584
|
+
palevioletred: "db7093",
|
|
585
|
+
papayawhip: "ffefd5",
|
|
586
|
+
peachpuff: "ffdab9",
|
|
587
|
+
peru: "cd853f",
|
|
588
|
+
pink: "ffc0cb",
|
|
589
|
+
plum: "dda0dd",
|
|
590
|
+
powderblue: "b0e0e6",
|
|
591
|
+
purple: "800080",
|
|
592
|
+
rebeccapurple: "663399",
|
|
593
|
+
red: "ff0000",
|
|
594
|
+
rosybrown: "bc8f8f",
|
|
595
|
+
royalblue: "4169e1",
|
|
596
|
+
saddlebrown: "8b4513",
|
|
597
|
+
salmon: "fa8072",
|
|
598
|
+
sandybrown: "f4a460",
|
|
599
|
+
seagreen: "2e8b57",
|
|
600
|
+
seashell: "fff5ee",
|
|
601
|
+
sienna: "a0522d",
|
|
602
|
+
silver: "c0c0c0",
|
|
603
|
+
skyblue: "87ceeb",
|
|
604
|
+
slateblue: "6a5acd",
|
|
605
|
+
slategray: "708090",
|
|
606
|
+
slategrey: "708090",
|
|
607
|
+
snow: "fffafa",
|
|
608
|
+
springgreen: "00ff7f",
|
|
609
|
+
steelblue: "4682b4",
|
|
610
|
+
tan: "d2b48c",
|
|
611
|
+
teal: "008080",
|
|
612
|
+
thistle: "d8bfd8",
|
|
613
|
+
tomato: "ff6347",
|
|
614
|
+
turquoise: "40e0d0",
|
|
615
|
+
violet: "ee82ee",
|
|
616
|
+
wheat: "f5deb3",
|
|
617
|
+
white: "ffffff",
|
|
618
|
+
whitesmoke: "f5f5f5",
|
|
619
|
+
yellow: "ffff00",
|
|
620
|
+
yellowgreen: "9acd32"
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// ../tokens/src/parse.ts
|
|
624
|
+
var numberPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
625
|
+
var safeValue = (value) => value.length <= 512 && !/[;{}<>\\\x00-\x1f]|\/\*|url\s*\(|expression\s*\(/i.test(value);
|
|
626
|
+
var clamp = (n) => Math.max(0, Math.min(1, n));
|
|
627
|
+
function numeric(value, scale = 1) {
|
|
628
|
+
const percent = value.endsWith("%");
|
|
629
|
+
const raw = percent ? value.slice(0, -1) : value;
|
|
630
|
+
const parsed = numberPattern.test(raw) ? Number(raw) / (percent ? 100 : scale) : NaN;
|
|
631
|
+
return Number.isFinite(parsed) ? parsed : NaN;
|
|
632
|
+
}
|
|
633
|
+
function hue(value) {
|
|
634
|
+
const match = /^(.*?)(deg|rad|grad|turn)?$/.exec(value);
|
|
635
|
+
if (!match || !numberPattern.test(match[1])) {
|
|
636
|
+
return NaN;
|
|
637
|
+
}
|
|
638
|
+
const factors = {
|
|
639
|
+
deg: 1,
|
|
640
|
+
rad: 180 / Math.PI,
|
|
641
|
+
grad: 0.9,
|
|
642
|
+
turn: 360
|
|
643
|
+
};
|
|
644
|
+
return Number(match[1]) * (factors[match[2] ?? "deg"] ?? 1);
|
|
645
|
+
}
|
|
646
|
+
function parseColor(input) {
|
|
647
|
+
if (!safeValue(input)) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
let value = input.trim().toLowerCase();
|
|
651
|
+
if (value === "transparent") {
|
|
652
|
+
return [0, 0, 0, 0];
|
|
653
|
+
}
|
|
654
|
+
if (Object.prototype.hasOwnProperty.call(NAMED_COLORS, value)) {
|
|
655
|
+
value = "#" + NAMED_COLORS[value];
|
|
656
|
+
}
|
|
657
|
+
if (/^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/.test(value)) {
|
|
658
|
+
let hex = value.slice(1);
|
|
659
|
+
if (hex.length < 5) {
|
|
660
|
+
hex = [...hex].map((c) => c + c).join("");
|
|
661
|
+
}
|
|
662
|
+
if (hex.length === 6) {
|
|
663
|
+
hex += "ff";
|
|
664
|
+
}
|
|
665
|
+
return [0, 2, 4, 6].map(
|
|
666
|
+
(i) => parseInt(hex.slice(i, i + 2), 16) / 255
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
const match = /^(rgba?|hsla?|oklch)\(([^()]*)\)$/.exec(value);
|
|
670
|
+
if (!match) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
const kind = match[1];
|
|
674
|
+
const body = match[2].trim();
|
|
675
|
+
const comma = body.includes(",");
|
|
676
|
+
if (comma && (body.includes("/") || kind === "oklch")) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
const parts = comma ? body.split(",").map((s) => s.trim()) : body.split(/\s*\/\s*/);
|
|
680
|
+
let channels, alpha;
|
|
681
|
+
if (comma) {
|
|
682
|
+
if (parts.length !== 3 && parts.length !== 4) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
channels = parts.slice(0, 3);
|
|
686
|
+
alpha = parts[3];
|
|
687
|
+
} else {
|
|
688
|
+
if (parts.length > 2) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
channels = parts[0].split(/\s+/);
|
|
692
|
+
alpha = parts[1];
|
|
693
|
+
}
|
|
694
|
+
if (channels.length !== 3) {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
const opacity = alpha === void 0 ? 1 : numeric(alpha);
|
|
698
|
+
let rgb;
|
|
699
|
+
if (kind.startsWith("rgb")) {
|
|
700
|
+
if (comma && channels.some((c) => c.endsWith("%")) && !channels.every((c) => c.endsWith("%"))) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
rgb = channels.map((c) => numeric(c, 255));
|
|
704
|
+
} else if (kind.startsWith("hsl")) {
|
|
705
|
+
if (!channels[1].endsWith("%") || !channels[2].endsWith("%")) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const h = (hue(channels[0]) % 360 + 360) % 360;
|
|
709
|
+
const s = clamp(numeric(channels[1])), l = clamp(numeric(channels[2]));
|
|
710
|
+
const a = s * Math.min(l, 1 - l);
|
|
711
|
+
rgb = [0, 8, 4].map((n) => {
|
|
712
|
+
const k = (n + h / 30) % 12;
|
|
713
|
+
return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
|
714
|
+
});
|
|
715
|
+
} else {
|
|
716
|
+
const l = clamp(numeric(channels[0]));
|
|
717
|
+
const c = Math.max(
|
|
718
|
+
0,
|
|
719
|
+
numeric(channels[1]) * (channels[1].endsWith("%") ? 0.4 : 1)
|
|
720
|
+
);
|
|
721
|
+
const h = hue(channels[2]) * Math.PI / 180;
|
|
722
|
+
const a = c * Math.cos(h), b = c * Math.sin(h);
|
|
723
|
+
const x = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
724
|
+
const y = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
725
|
+
const z = (l - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
726
|
+
rgb = [
|
|
727
|
+
4.0767416621 * x - 3.3077115913 * y + 0.2309699292 * z,
|
|
728
|
+
-1.2684380046 * x + 2.6097574011 * y - 0.3413193965 * z,
|
|
729
|
+
-0.0041960863 * x - 0.7034186147 * y + 1.707614701 * z
|
|
730
|
+
].map((v) => v <= 31308e-7 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055);
|
|
731
|
+
}
|
|
732
|
+
return [...rgb, opacity].every(Number.isFinite) ? [...rgb.map(clamp), clamp(opacity)] : null;
|
|
733
|
+
}
|
|
734
|
+
function isLength(value, depth = 0) {
|
|
735
|
+
if (!safeValue(value) || depth > 8) {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
const text = value.trim();
|
|
739
|
+
if (/^(?:0|(?:\d+(?:\.\d*)?|\.\d+)(?:px|rem|em|%|dvh|vh|vw))$/.test(text)) {
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
const fn = /^(min|max|clamp|calc)\((.*)\)$/.exec(text);
|
|
743
|
+
if (!fn) {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
let level2 = 0, start = 0;
|
|
747
|
+
const args = [];
|
|
748
|
+
for (let i = 0; i < fn[2].length; i++) {
|
|
749
|
+
const c = fn[2][i];
|
|
750
|
+
if (c === "(") {
|
|
751
|
+
level2++;
|
|
752
|
+
}
|
|
753
|
+
if (c === ")" && --level2 < 0) {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
const separator = fn[1] === "calc" ? (c === "+" || c === "-") && /\s/.test(fn[2][i - 1] ?? "") && /\s/.test(fn[2][i + 1] ?? "") : c === ",";
|
|
757
|
+
if (level2 === 0 && separator) {
|
|
758
|
+
args.push(fn[2].slice(start, i));
|
|
759
|
+
start = i + 1;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
args.push(fn[2].slice(start));
|
|
763
|
+
return level2 === 0 && (fn[1] !== "clamp" || args.length === 3) && args.every((arg) => isLength(arg, depth + 1));
|
|
764
|
+
}
|
|
765
|
+
var cssWideKeyword = /^(?:inherit|initial|unset|revert|revert-layer|default)$/i;
|
|
766
|
+
function isFontFamily(value) {
|
|
767
|
+
return safeValue(value) && value.split(",").every((part) => {
|
|
768
|
+
const family = part.trim();
|
|
769
|
+
return /^[a-zA-Z][a-zA-Z0-9 -]*$/.test(family) && !cssWideKeyword.test(family);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
function isDuration(value) {
|
|
773
|
+
return /^(?:\d+(?:\.\d+)?|\.\d+)(?:ms|s)$/.test(value);
|
|
774
|
+
}
|
|
775
|
+
function isEasing(value) {
|
|
776
|
+
if (/^(linear|ease|ease-in|ease-out|ease-in-out|step-start|step-end)$/.test(
|
|
777
|
+
value
|
|
778
|
+
)) {
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
const m = /^cubic-bezier\((.*)\)$/.exec(value);
|
|
782
|
+
if (!m) {
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
const ns = m[1].split(",").map((s) => numberPattern.test(s.trim()) ? numeric(s.trim()) : NaN);
|
|
786
|
+
return ns.length === 4 && ns.every(Number.isFinite) && ns[0] >= 0 && ns[0] <= 1 && ns[2] >= 0 && ns[2] <= 1;
|
|
787
|
+
}
|
|
788
|
+
function isAssetUrl(value, relative2 = true) {
|
|
789
|
+
if (!value || !safeValue(value) || /[\s"'()]/.test(value)) {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
if (relative2 && !/^[a-z][\w+.-]*:/i.test(value) && !value.startsWith("//")) {
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
const url = new URL(value);
|
|
797
|
+
return url.protocol === "https:" && !url.username && !url.password;
|
|
798
|
+
} catch {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ../tokens/src/contrast.ts
|
|
804
|
+
function contrastRatio(fg, bg) {
|
|
805
|
+
const foreground = parseColor(fg), background = parseColor(bg);
|
|
806
|
+
if (!foreground || !background || background[3] !== 1) {
|
|
807
|
+
return NaN;
|
|
808
|
+
}
|
|
809
|
+
const luminance = (channels) => channels.map((c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4).reduce((sum, c, i) => sum + c * [0.2126, 0.7152, 0.0722][i], 0);
|
|
810
|
+
const a = luminance(
|
|
811
|
+
foreground.slice(0, 3).map((c, i) => c * foreground[3] + background[i] * (1 - foreground[3]))
|
|
812
|
+
);
|
|
813
|
+
const b = luminance(background.slice(0, 3));
|
|
814
|
+
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ../tokens/src/validate.ts
|
|
818
|
+
var record2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
819
|
+
var includes = (values, key) => values.includes(key);
|
|
820
|
+
function validateTheme(input) {
|
|
821
|
+
const issues = [];
|
|
822
|
+
const issue = (path12, code, message4, severity = "error") => issues.push({ path: path12, code, message: message4, severity });
|
|
823
|
+
const keys = (obj, allowed, path12) => {
|
|
824
|
+
for (const key of Object.keys(obj)) {
|
|
825
|
+
if (!allowed.includes(key)) {
|
|
826
|
+
issue(path12 ? `${path12}.${key}` : key, "invalid_theme", "Unknown field");
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
if (!record2(input)) {
|
|
831
|
+
return {
|
|
832
|
+
ok: false,
|
|
833
|
+
issues: [
|
|
834
|
+
{
|
|
835
|
+
path: "",
|
|
836
|
+
code: "invalid_theme",
|
|
837
|
+
message: "Expected a theme object",
|
|
838
|
+
severity: "error"
|
|
839
|
+
}
|
|
840
|
+
]
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
keys(
|
|
844
|
+
input,
|
|
845
|
+
[
|
|
846
|
+
"$schema",
|
|
847
|
+
"version",
|
|
848
|
+
"name",
|
|
849
|
+
"tokens",
|
|
850
|
+
"dark",
|
|
851
|
+
"darkMode",
|
|
852
|
+
"fonts",
|
|
853
|
+
"logo",
|
|
854
|
+
"motion"
|
|
855
|
+
],
|
|
856
|
+
""
|
|
857
|
+
);
|
|
858
|
+
if (input.version !== 1) {
|
|
859
|
+
issue("version", "invalid_theme", "Expected version 1");
|
|
860
|
+
}
|
|
861
|
+
if (typeof input.name !== "string" || !input.name.trim() || input.name.length > 100) {
|
|
862
|
+
issue("name", "missing_required", "Expected a name of 1\u2013100 characters");
|
|
863
|
+
}
|
|
864
|
+
if (input.$schema !== void 0 && (typeof input.$schema !== "string" || !isAssetUrl(input.$schema, false))) {
|
|
865
|
+
issue("$schema", "invalid_theme", "Expected an HTTPS schema URL");
|
|
866
|
+
}
|
|
867
|
+
if (input.darkMode !== void 0 && (typeof input.darkMode !== "string" || !["auto", "light", "dark"].includes(input.darkMode))) {
|
|
868
|
+
issue("darkMode", "invalid_theme", "Expected auto, light or dark");
|
|
869
|
+
}
|
|
870
|
+
for (const group of ["tokens", "dark"]) {
|
|
871
|
+
const values = input[group];
|
|
872
|
+
if (group === "dark" && values === void 0) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (!record2(values)) {
|
|
876
|
+
issue(group, "missing_required", "Expected a token object");
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (group === "tokens") {
|
|
880
|
+
for (const key of REQUIRED_TOKENS) {
|
|
881
|
+
if (!Object.prototype.hasOwnProperty.call(values, key)) {
|
|
882
|
+
issue(`${group}.${key}`, "missing_required", "Required token");
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
for (const [key, value] of Object.entries(values)) {
|
|
887
|
+
const path12 = `${group}.${key}`;
|
|
888
|
+
if (!includes(TOKEN_NAMES, key)) {
|
|
889
|
+
issue(path12, "unknown_token", "Unknown semantic token");
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
let code, valid = false;
|
|
893
|
+
if (includes(COLOR_TOKENS, key)) {
|
|
894
|
+
code = "invalid_color";
|
|
895
|
+
valid = typeof value === "string" && parseColor(value) !== null;
|
|
896
|
+
} else if (includes(LENGTH_TOKENS, key)) {
|
|
897
|
+
code = "invalid_length";
|
|
898
|
+
valid = typeof value === "string" && isLength(value);
|
|
899
|
+
} else if (includes(FONT_TOKENS, key)) {
|
|
900
|
+
code = "invalid_font";
|
|
901
|
+
valid = typeof value === "string" && isFontFamily(value);
|
|
902
|
+
} else {
|
|
903
|
+
code = "invalid_motion";
|
|
904
|
+
valid = typeof value === "string" && safeValue(value) && (includes(DURATION_TOKENS, key) ? isDuration(value) : isEasing(value));
|
|
905
|
+
}
|
|
906
|
+
if (!valid) {
|
|
907
|
+
issue(path12, code, "Invalid or unsafe token value");
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (input.fonts !== void 0) {
|
|
912
|
+
if (!Array.isArray(input.fonts) || input.fonts.length > 3) {
|
|
913
|
+
issue("fonts", "invalid_font", "Expected at most three fonts");
|
|
914
|
+
} else {
|
|
915
|
+
const roles = /* @__PURE__ */ new Set();
|
|
916
|
+
input.fonts.forEach((font, i) => {
|
|
917
|
+
const path12 = `fonts[${i}]`;
|
|
918
|
+
if (!record2(font)) {
|
|
919
|
+
issue(path12, "invalid_font", "Expected a font object");
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
keys(font, ["family", "source", "url", "weights", "role"], path12);
|
|
923
|
+
if (typeof font.family !== "string" || !isFontFamily(font.family) || font.family.includes(",")) {
|
|
924
|
+
issue(
|
|
925
|
+
`${path12}.family`,
|
|
926
|
+
"invalid_font",
|
|
927
|
+
"Expected one unquoted font family"
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
if (typeof font.source !== "string" || !["google", "self", "system"].includes(font.source)) {
|
|
931
|
+
issue(`${path12}.source`, "invalid_font", "Unknown font source");
|
|
932
|
+
}
|
|
933
|
+
if (typeof font.role !== "string" || !["display", "body", "mono"].includes(font.role) || roles.has(font.role)) {
|
|
934
|
+
issue(`${path12}.role`, "invalid_font", "Expected a unique font role");
|
|
935
|
+
}
|
|
936
|
+
roles.add(String(font.role));
|
|
937
|
+
if (font.source === "self" || font.url !== void 0) {
|
|
938
|
+
if (typeof font.url !== "string" || !isAssetUrl(font.url, false) || !new URL(font.url).pathname.endsWith(".woff2")) {
|
|
939
|
+
issue(
|
|
940
|
+
`${path12}.url`,
|
|
941
|
+
"invalid_font",
|
|
942
|
+
"Expected an HTTPS .woff2 URL"
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (font.weights !== void 0 && (!Array.isArray(font.weights) || !font.weights.length || font.weights.length > 9 || font.weights.some(
|
|
947
|
+
(w) => typeof w !== "number" || !Number.isInteger(w) || w < 1 || w > 1e3
|
|
948
|
+
))) {
|
|
949
|
+
issue(
|
|
950
|
+
`${path12}.weights`,
|
|
951
|
+
"invalid_font",
|
|
952
|
+
"Expected 1\u20139 integer weights between 1 and 1000"
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (input.logo !== void 0) {
|
|
959
|
+
if (!record2(input.logo)) {
|
|
960
|
+
issue("logo", "invalid_logo", "Expected a logo object");
|
|
961
|
+
} else {
|
|
962
|
+
keys(input.logo, ["light", "dark", "mark", "alt"], "logo");
|
|
963
|
+
if (typeof input.logo.alt !== "string" || !input.logo.alt.trim()) {
|
|
964
|
+
issue("logo.alt", "invalid_logo", "Logo alternative text is required");
|
|
965
|
+
}
|
|
966
|
+
for (const key of ["light", "dark", "mark"]) {
|
|
967
|
+
const value = input.logo[key];
|
|
968
|
+
if (key === "light" || value !== void 0) {
|
|
969
|
+
if (typeof value !== "string" || !isAssetUrl(value)) {
|
|
970
|
+
issue(
|
|
971
|
+
`logo.${key}`,
|
|
972
|
+
"invalid_logo",
|
|
973
|
+
"Expected HTTPS or a relative asset path"
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (input.motion !== void 0) {
|
|
981
|
+
if (!record2(input.motion)) {
|
|
982
|
+
issue("motion", "invalid_motion", "Expected a motion object");
|
|
983
|
+
} else {
|
|
984
|
+
keys(input.motion, ["reduce"], "motion");
|
|
985
|
+
if (input.motion.reduce !== void 0 && typeof input.motion.reduce !== "boolean") {
|
|
986
|
+
issue("motion.reduce", "invalid_motion", "Expected a boolean");
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const tokenGroup = (value) => record2(value) ? value : {};
|
|
991
|
+
for (const group of record2(input.dark) ? ["tokens", "dark"] : ["tokens"]) {
|
|
992
|
+
const values = {
|
|
993
|
+
...DEFAULT_TOKENS,
|
|
994
|
+
...tokenGroup(input.tokens),
|
|
995
|
+
...group === "dark" ? tokenGroup(input.dark) : {}
|
|
996
|
+
};
|
|
997
|
+
const pairs = [
|
|
998
|
+
["--z-text", "--z-bg", 4.5, "error"],
|
|
999
|
+
["--z-text", "--z-surface", 4.5, "error"],
|
|
1000
|
+
["--z-accent-contrast", "--z-accent", 4.5, "error"],
|
|
1001
|
+
["--z-text-muted", "--z-bg", 3, "warning"]
|
|
1002
|
+
];
|
|
1003
|
+
for (const [fg, bg, minimum, severity] of pairs) {
|
|
1004
|
+
const foreground = values[fg], background = values[bg];
|
|
1005
|
+
if (typeof foreground !== "string" || typeof background !== "string" || !parseColor(foreground) || !parseColor(background)) {
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const ratio = contrastRatio(foreground, background);
|
|
1009
|
+
if (!Number.isFinite(ratio)) {
|
|
1010
|
+
issue(
|
|
1011
|
+
`${group}.${bg}`,
|
|
1012
|
+
"unverified_contrast",
|
|
1013
|
+
"A contrast background must be opaque",
|
|
1014
|
+
"error"
|
|
1015
|
+
);
|
|
1016
|
+
} else if (ratio < minimum) {
|
|
1017
|
+
issue(
|
|
1018
|
+
`${group}.${fg}`,
|
|
1019
|
+
"low_contrast",
|
|
1020
|
+
`Contrast against ${bg} must be at least ${minimum}:1`,
|
|
1021
|
+
severity
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return issues.some((i) => i.severity === "error") ? { ok: false, issues } : { ok: true, theme: input, warnings: issues };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/config.ts
|
|
1030
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
1031
|
+
import path2 from "path";
|
|
1032
|
+
import { pathToFileURL } from "url";
|
|
1033
|
+
|
|
1034
|
+
// src/files.ts
|
|
1035
|
+
import { readFile } from "fs/promises";
|
|
1036
|
+
import path from "path";
|
|
1037
|
+
async function json(file) {
|
|
1038
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
1039
|
+
}
|
|
1040
|
+
function relative(cwd, file) {
|
|
1041
|
+
return path.relative(cwd, file).split(path.sep).join("/");
|
|
1042
|
+
}
|
|
1043
|
+
function lineFor(source, issuePath) {
|
|
1044
|
+
const key = issuePath.split(".").at(-1)?.replace(/\[\d+\]$/, "");
|
|
1045
|
+
if (!key || key === "$") {
|
|
1046
|
+
return void 0;
|
|
1047
|
+
}
|
|
1048
|
+
const quoted = JSON.stringify(key);
|
|
1049
|
+
const lines = source.split(/\r?\n/);
|
|
1050
|
+
const index = lines.findIndex(
|
|
1051
|
+
(line) => line.includes(quoted) || new RegExp(`\\b${escape(key)}\\b`).test(line)
|
|
1052
|
+
);
|
|
1053
|
+
return index < 0 ? void 0 : index + 1;
|
|
1054
|
+
}
|
|
1055
|
+
function escape(value) {
|
|
1056
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/result.ts
|
|
1060
|
+
function problem(message4, fix, details = {}) {
|
|
1061
|
+
return { ...details, message: message4, fix };
|
|
1062
|
+
}
|
|
1063
|
+
function step(name, result, problems = []) {
|
|
1064
|
+
return { name, result, problems };
|
|
1065
|
+
}
|
|
1066
|
+
var skipped = (name, message4) => step(name, "skipped", [problem(message4, null)]);
|
|
1067
|
+
|
|
1068
|
+
// src/config.ts
|
|
1069
|
+
function supportsTypeScriptImport(version = process.versions.node) {
|
|
1070
|
+
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
1071
|
+
return major > 22 || major === 22 && minor >= 18;
|
|
1072
|
+
}
|
|
1073
|
+
async function loadConfig(cwd) {
|
|
1074
|
+
const file = path2.join(cwd, "zappie.config.ts");
|
|
1075
|
+
if (!supportsTypeScriptImport()) {
|
|
1076
|
+
throw new Error(
|
|
1077
|
+
"zappie check needs Node 22.18 or later to read zappie.config.ts"
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
const source = await readFile2(file, "utf8");
|
|
1081
|
+
const version = (await stat(file)).mtimeMs;
|
|
1082
|
+
const loaded = await import(`${pathToFileURL(file).href}?check=${version}`);
|
|
1083
|
+
return { value: loaded.default, source, file };
|
|
1084
|
+
}
|
|
1085
|
+
async function checkConfig({
|
|
1086
|
+
cwd,
|
|
1087
|
+
signal
|
|
1088
|
+
}) {
|
|
1089
|
+
signal?.throwIfAborted();
|
|
1090
|
+
let loaded;
|
|
1091
|
+
try {
|
|
1092
|
+
loaded = await loadConfig(cwd);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
return step("config", "failed", [
|
|
1095
|
+
problem(
|
|
1096
|
+
message(error),
|
|
1097
|
+
"Create a readable zappie.config.ts in the project root.",
|
|
1098
|
+
{
|
|
1099
|
+
file: "zappie.config.ts"
|
|
1100
|
+
}
|
|
1101
|
+
)
|
|
1102
|
+
]);
|
|
1103
|
+
}
|
|
1104
|
+
const validation = validateConfig(loaded.value);
|
|
1105
|
+
const problems = validation.issues.map(
|
|
1106
|
+
(issue) => configProblem(issue, loaded.source)
|
|
1107
|
+
);
|
|
1108
|
+
let themeFailed = false;
|
|
1109
|
+
const theme2 = configTheme(loaded.value);
|
|
1110
|
+
if (theme2) {
|
|
1111
|
+
const checkedTheme = await themeProblems(cwd, theme2);
|
|
1112
|
+
problems.push(...checkedTheme.problems);
|
|
1113
|
+
themeFailed = checkedTheme.failed;
|
|
1114
|
+
}
|
|
1115
|
+
signal?.throwIfAborted();
|
|
1116
|
+
return step(
|
|
1117
|
+
"config",
|
|
1118
|
+
validation.issues.some((issue) => issue.severity === "error") || themeFailed ? "failed" : "passed",
|
|
1119
|
+
problems
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
function configProblem(issue, source) {
|
|
1123
|
+
const line = lineFor(source, issue.path);
|
|
1124
|
+
return problem(`${issue.path}: ${issue.message}`, issue.message, {
|
|
1125
|
+
rule: `config/${issue.code}`,
|
|
1126
|
+
file: "zappie.config.ts",
|
|
1127
|
+
...line ? { line } : {}
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
async function themeProblems(cwd, theme2) {
|
|
1131
|
+
const file = path2.resolve(cwd, theme2);
|
|
1132
|
+
try {
|
|
1133
|
+
const source = await readFile2(file, "utf8");
|
|
1134
|
+
const result = validateTheme(JSON.parse(source));
|
|
1135
|
+
const issues = result.ok ? result.warnings : result.issues;
|
|
1136
|
+
return {
|
|
1137
|
+
failed: issues.some((issue) => issue.severity === "error"),
|
|
1138
|
+
problems: issues.map((issue) => themeProblem(issue, source, theme2))
|
|
1139
|
+
};
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
return {
|
|
1142
|
+
failed: true,
|
|
1143
|
+
problems: [
|
|
1144
|
+
problem(message(error), `Make ${theme2} a readable JSON theme file.`, {
|
|
1145
|
+
rule: "theme/unreadable",
|
|
1146
|
+
file: theme2
|
|
1147
|
+
})
|
|
1148
|
+
]
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function themeProblem(issue, source, file) {
|
|
1153
|
+
const line = lineFor(source, issue.path);
|
|
1154
|
+
return problem(`${issue.path}: ${issue.message}`, issue.message, {
|
|
1155
|
+
rule: `theme/${issue.code}`,
|
|
1156
|
+
file,
|
|
1157
|
+
...line ? { line } : {}
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
function configTheme(value) {
|
|
1161
|
+
return value && typeof value === "object" && "theme" in value && typeof value.theme === "string" ? value.theme : void 0;
|
|
1162
|
+
}
|
|
1163
|
+
function message(error) {
|
|
1164
|
+
return error instanceof Error ? error.message : String(error);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/allowlist.ts
|
|
1168
|
+
var BUILTIN_ALLOWLIST_SOURCE = {
|
|
1169
|
+
load: async () => GLOBAL_PACKAGE_ALLOWLIST
|
|
1170
|
+
};
|
|
1171
|
+
async function checkAllowlist({ cwd, signal }, source = BUILTIN_ALLOWLIST_SOURCE) {
|
|
1172
|
+
signal?.throwIfAborted();
|
|
1173
|
+
const problems = [];
|
|
1174
|
+
let manifest;
|
|
1175
|
+
try {
|
|
1176
|
+
manifest = await json(path3.join(cwd, "package.json"));
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
return step("allowlist", "failed", [
|
|
1179
|
+
problem(String(error), "Add a valid package.json to the project root.", {
|
|
1180
|
+
file: "package.json"
|
|
1181
|
+
})
|
|
1182
|
+
]);
|
|
1183
|
+
}
|
|
1184
|
+
if (!await hasLockfile(cwd)) {
|
|
1185
|
+
problems.push(
|
|
1186
|
+
problem(
|
|
1187
|
+
"No supported lockfile was found.",
|
|
1188
|
+
"Install dependencies and commit the generated lockfile."
|
|
1189
|
+
)
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const global = await source.load(signal);
|
|
1193
|
+
const local = await localAllowlist(cwd);
|
|
1194
|
+
for (const name of Object.keys(manifest.dependencies ?? {})) {
|
|
1195
|
+
if (![...global, ...local].some((entry) => matches(entry, name))) {
|
|
1196
|
+
problems.push(
|
|
1197
|
+
problem(
|
|
1198
|
+
`${name} is not in the package allowlist.`,
|
|
1199
|
+
`Add ${name} to packages.allow in zappie.config.ts after reviewing it.`,
|
|
1200
|
+
{ rule: "allowlist/package", file: "package.json" }
|
|
1201
|
+
)
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
await checkInstallScripts(cwd, name, problems);
|
|
1205
|
+
}
|
|
1206
|
+
signal?.throwIfAborted();
|
|
1207
|
+
return step("allowlist", problems.length ? "failed" : "passed", problems);
|
|
1208
|
+
}
|
|
1209
|
+
async function localAllowlist(cwd) {
|
|
1210
|
+
try {
|
|
1211
|
+
const loaded = await loadConfig(cwd);
|
|
1212
|
+
const value = loaded.value;
|
|
1213
|
+
return Array.isArray(value?.packages?.allow) ? value.packages.allow.filter(
|
|
1214
|
+
(entry) => typeof entry === "string"
|
|
1215
|
+
) : [];
|
|
1216
|
+
} catch {
|
|
1217
|
+
return [];
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
async function hasLockfile(cwd) {
|
|
1221
|
+
for (let directory = cwd; ; directory = path3.dirname(directory)) {
|
|
1222
|
+
for (const name of [
|
|
1223
|
+
"pnpm-lock.yaml",
|
|
1224
|
+
"package-lock.json",
|
|
1225
|
+
"yarn.lock",
|
|
1226
|
+
"bun.lock",
|
|
1227
|
+
"bun.lockb"
|
|
1228
|
+
]) {
|
|
1229
|
+
try {
|
|
1230
|
+
await access(path3.join(directory, name));
|
|
1231
|
+
return true;
|
|
1232
|
+
} catch {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (path3.dirname(directory) === directory) {
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async function checkInstallScripts(cwd, name, problems) {
|
|
1242
|
+
try {
|
|
1243
|
+
const dependency = await installedManifest(cwd, name);
|
|
1244
|
+
for (const script of ["preinstall", "install", "postinstall"]) {
|
|
1245
|
+
if (dependency.scripts?.[script]) {
|
|
1246
|
+
problems.push(
|
|
1247
|
+
problem(
|
|
1248
|
+
`${name} declares a ${script} script.`,
|
|
1249
|
+
`Remove ${name} or use a dependency without install scripts.`,
|
|
1250
|
+
{ rule: "allowlist/install-script", file: "package.json" }
|
|
1251
|
+
)
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
} catch {
|
|
1256
|
+
problems.push(
|
|
1257
|
+
problem(
|
|
1258
|
+
`${name} is not installed.`,
|
|
1259
|
+
"Install dependencies from the committed lockfile.",
|
|
1260
|
+
{
|
|
1261
|
+
rule: "allowlist/not-installed",
|
|
1262
|
+
file: "package.json"
|
|
1263
|
+
}
|
|
1264
|
+
)
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function matches(pattern, value) {
|
|
1269
|
+
return pattern.endsWith("/*") ? value.startsWith(pattern.slice(0, -1)) : pattern === value;
|
|
1270
|
+
}
|
|
1271
|
+
async function installedManifest(cwd, name) {
|
|
1272
|
+
for (let directory = cwd; ; directory = path3.dirname(directory)) {
|
|
1273
|
+
const file = path3.join(
|
|
1274
|
+
directory,
|
|
1275
|
+
"node_modules",
|
|
1276
|
+
...name.split("/"),
|
|
1277
|
+
"package.json"
|
|
1278
|
+
);
|
|
1279
|
+
try {
|
|
1280
|
+
return JSON.parse(await readFile3(file, "utf8"));
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
if (path3.dirname(directory) === directory) {
|
|
1283
|
+
throw error;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/budgets.ts
|
|
1290
|
+
import { access as access2, readFile as readFile6 } from "fs/promises";
|
|
1291
|
+
import path6 from "path";
|
|
1292
|
+
|
|
1293
|
+
// src/budget-files.ts
|
|
1294
|
+
import { readFile as readFile4, readdir } from "fs/promises";
|
|
1295
|
+
import path4 from "path";
|
|
1296
|
+
import { gzipSync } from "zlib";
|
|
1297
|
+
async function cssSizes(cwd) {
|
|
1298
|
+
const root = path4.join(cwd, "build/client");
|
|
1299
|
+
const files = [];
|
|
1300
|
+
await collect(root, files);
|
|
1301
|
+
return Promise.all(
|
|
1302
|
+
files.filter((file) => file.endsWith(".css")).map(async (file) => ({
|
|
1303
|
+
file: path4.relative(root, file).split(path4.sep).join("/"),
|
|
1304
|
+
bytes: gzipSync(await readFile4(file, "utf8")).byteLength
|
|
1305
|
+
}))
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
async function collect(directory, files) {
|
|
1309
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
1310
|
+
if (entry.isSymbolicLink()) {
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
const file = path4.join(directory, entry.name);
|
|
1314
|
+
if (entry.isDirectory()) {
|
|
1315
|
+
await collect(file, files);
|
|
1316
|
+
} else if (entry.isFile()) {
|
|
1317
|
+
files.push(file);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/budget-manifest.ts
|
|
1323
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1324
|
+
import path5 from "path";
|
|
1325
|
+
import { gzipSync as gzipSync2 } from "zlib";
|
|
1326
|
+
function routeKeys(manifest) {
|
|
1327
|
+
return Object.entries(manifest).filter(([key, entry]) => routeSource(entry.src ?? key)).map(([key]) => key).sort();
|
|
1328
|
+
}
|
|
1329
|
+
function clientKeys(manifest) {
|
|
1330
|
+
return Object.entries(manifest).filter(([key, entry]) => entry.isEntry && !routeSource(entry.src ?? key)).map(([key]) => key).sort();
|
|
1331
|
+
}
|
|
1332
|
+
async function graphSizes(cwd, manifest, roots) {
|
|
1333
|
+
const keys = closure(manifest, roots);
|
|
1334
|
+
return Promise.all(
|
|
1335
|
+
[...keys].map((key) => manifest[key]).filter((entry) => Boolean(entry)).filter(({ file }) => file.endsWith(".js")).map(async ({ file }) => ({
|
|
1336
|
+
file,
|
|
1337
|
+
bytes: gzipSync2(
|
|
1338
|
+
await readFile5(path5.join(cwd, "build/client", file), "utf8")
|
|
1339
|
+
).byteLength
|
|
1340
|
+
}))
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
function closure(manifest, roots) {
|
|
1344
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1345
|
+
const visit = (key) => {
|
|
1346
|
+
if (visited.has(key)) {
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const entry = manifest[key];
|
|
1350
|
+
if (!entry) {
|
|
1351
|
+
throw new Error(`Manifest import ${key} has no entry.`);
|
|
1352
|
+
}
|
|
1353
|
+
visited.add(key);
|
|
1354
|
+
for (const dependency of entry.imports ?? []) {
|
|
1355
|
+
visit(dependency);
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
roots.forEach(visit);
|
|
1359
|
+
return visited;
|
|
1360
|
+
}
|
|
1361
|
+
function routeSource(value) {
|
|
1362
|
+
return value.replace(/^\/?/, "").startsWith("app/routes/");
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// src/budgets.ts
|
|
1366
|
+
var KIB = 1024;
|
|
1367
|
+
async function checkBudgets({
|
|
1368
|
+
cwd,
|
|
1369
|
+
signal
|
|
1370
|
+
}) {
|
|
1371
|
+
signal?.throwIfAborted();
|
|
1372
|
+
const manifestFile = path6.join(cwd, "build/client/.vite/manifest.json");
|
|
1373
|
+
try {
|
|
1374
|
+
await access2(manifestFile);
|
|
1375
|
+
} catch {
|
|
1376
|
+
return skipped(
|
|
1377
|
+
"budgets",
|
|
1378
|
+
"No build/client/.vite/manifest.json is available."
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
const manifest = JSON.parse(await readFile6(manifestFile, "utf8"));
|
|
1382
|
+
const config = (await loadConfig(cwd)).value;
|
|
1383
|
+
const limits = { ...DEFAULT_BUDGETS, ...config.budgets };
|
|
1384
|
+
const routes = routeKeys(manifest);
|
|
1385
|
+
const clients = clientKeys(manifest);
|
|
1386
|
+
if (routes.length === 0 || clients.length === 0) {
|
|
1387
|
+
throw new Error(
|
|
1388
|
+
"The Vite manifest needs a client entry and app/routes entries."
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
const problems = [];
|
|
1392
|
+
let failed = false;
|
|
1393
|
+
for (const route of routes) {
|
|
1394
|
+
const sizes = await graphSizes(cwd, manifest, [...clients, route]);
|
|
1395
|
+
const over = total(sizes) > limits.jsKbGz * KIB;
|
|
1396
|
+
failed ||= over;
|
|
1397
|
+
problems.push(routeProblem(route, sizes, limits.jsKbGz, over));
|
|
1398
|
+
}
|
|
1399
|
+
const styles = await cssSizes(cwd);
|
|
1400
|
+
const cssOver = total(styles) > limits.cssKbGz * KIB;
|
|
1401
|
+
failed ||= cssOver;
|
|
1402
|
+
problems.push(cssProblem(styles, limits.cssKbGz, cssOver));
|
|
1403
|
+
problems.push(
|
|
1404
|
+
problem(
|
|
1405
|
+
`LCP is not measured by zappie check; zappie verify enforces the ${limits.lcpMs} ms budget.`,
|
|
1406
|
+
null,
|
|
1407
|
+
{ rule: "budgets/lcp" }
|
|
1408
|
+
)
|
|
1409
|
+
);
|
|
1410
|
+
signal?.throwIfAborted();
|
|
1411
|
+
return step("budgets", failed ? "failed" : "passed", problems);
|
|
1412
|
+
}
|
|
1413
|
+
function routeProblem(route, sizes, limitKb, over) {
|
|
1414
|
+
const bytes = total(sizes);
|
|
1415
|
+
const largest = [...sizes].sort((left, right) => right.bytes - left.bytes).slice(0, 3).map(({ file, bytes: size }) => `${file} (${size} B)`).join(", ");
|
|
1416
|
+
return problem(
|
|
1417
|
+
`${route}: ${bytes} B gzip (limit ${limitKb * KIB} B).${over ? ` Largest chunks: ${largest}.` : ""}`,
|
|
1418
|
+
over ? "Reduce the route's client JavaScript or raise the reviewed budget override." : null,
|
|
1419
|
+
{
|
|
1420
|
+
rule: over ? "budgets/js-over" : "budgets/js-route",
|
|
1421
|
+
file: "build/client/.vite/manifest.json"
|
|
1422
|
+
}
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
function cssProblem(sizes, limitKb, over) {
|
|
1426
|
+
const bytes = total(sizes);
|
|
1427
|
+
return problem(
|
|
1428
|
+
`Client CSS: ${bytes} B gzip (limit ${limitKb * KIB} B).`,
|
|
1429
|
+
over ? "Reduce client CSS or raise the reviewed budget override." : null,
|
|
1430
|
+
{ rule: over ? "budgets/css-over" : "budgets/css" }
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
var total = (sizes) => sizes.reduce((sum, asset) => sum + asset.bytes, 0);
|
|
1434
|
+
|
|
1435
|
+
// src/build.ts
|
|
1436
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1437
|
+
import path7 from "path";
|
|
1438
|
+
|
|
1439
|
+
// src/command.ts
|
|
1440
|
+
import { spawn } from "child_process";
|
|
1441
|
+
function command(executable, args, cwd, signal) {
|
|
1442
|
+
return new Promise((resolve, reject) => {
|
|
1443
|
+
const child = spawn(executable, args, {
|
|
1444
|
+
cwd,
|
|
1445
|
+
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
1446
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1447
|
+
...signal ? { signal } : {}
|
|
1448
|
+
});
|
|
1449
|
+
let stdout = "";
|
|
1450
|
+
let stderr = "";
|
|
1451
|
+
child.stdout.on("data", (value) => {
|
|
1452
|
+
stdout += String(value);
|
|
1453
|
+
});
|
|
1454
|
+
child.stderr.on("data", (value) => {
|
|
1455
|
+
stderr += String(value);
|
|
1456
|
+
});
|
|
1457
|
+
child.on("error", reject);
|
|
1458
|
+
child.on("close", (code) => {
|
|
1459
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
1460
|
+
});
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
function execArgs(manager, binary, args) {
|
|
1464
|
+
if (manager === "npm") {
|
|
1465
|
+
return { executable: "npm", args: ["exec", "--no", "--", binary, ...args] };
|
|
1466
|
+
}
|
|
1467
|
+
if (manager === "yarn") {
|
|
1468
|
+
return { executable: "yarn", args: ["exec", binary, ...args] };
|
|
1469
|
+
}
|
|
1470
|
+
if (manager === "bun") {
|
|
1471
|
+
return { executable: "bun", args: ["x", "--no-install", binary, ...args] };
|
|
1472
|
+
}
|
|
1473
|
+
return { executable: "pnpm", args: ["exec", binary, ...args] };
|
|
1474
|
+
}
|
|
1475
|
+
function scriptArgs(manager, name) {
|
|
1476
|
+
if (manager === "bun") {
|
|
1477
|
+
return { executable: "bun", args: ["run", name] };
|
|
1478
|
+
}
|
|
1479
|
+
if (manager === "npm") {
|
|
1480
|
+
return { executable: "npm", args: ["run", name] };
|
|
1481
|
+
}
|
|
1482
|
+
return { executable: manager, args: [name] };
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/build.ts
|
|
1486
|
+
async function checkBuild({
|
|
1487
|
+
cwd,
|
|
1488
|
+
packageManager,
|
|
1489
|
+
signal
|
|
1490
|
+
}) {
|
|
1491
|
+
signal?.throwIfAborted();
|
|
1492
|
+
const manifest = JSON.parse(
|
|
1493
|
+
await readFile7(path7.join(cwd, "package.json"), "utf8")
|
|
1494
|
+
);
|
|
1495
|
+
const invocation = manifest.dependencies?.["react-router"] ? execArgs(packageManager, "react-router", ["build"]) : manifest.scripts?.build ? scriptArgs(packageManager, "build") : void 0;
|
|
1496
|
+
if (!invocation) {
|
|
1497
|
+
return skipped(
|
|
1498
|
+
"build",
|
|
1499
|
+
"No react-router dependency or build script is defined in package.json."
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
const result = await command(
|
|
1503
|
+
invocation.executable,
|
|
1504
|
+
invocation.args,
|
|
1505
|
+
cwd,
|
|
1506
|
+
signal
|
|
1507
|
+
);
|
|
1508
|
+
signal?.throwIfAborted();
|
|
1509
|
+
if (result.code === 0) {
|
|
1510
|
+
return step("build", "passed");
|
|
1511
|
+
}
|
|
1512
|
+
return step("build", "failed", [
|
|
1513
|
+
problem(
|
|
1514
|
+
`The project build failed.
|
|
1515
|
+
${usefulTail(result.stdout, result.stderr)}`,
|
|
1516
|
+
"Fix the reported build errors and run zappie check again.",
|
|
1517
|
+
{ rule: "build/failed" }
|
|
1518
|
+
)
|
|
1519
|
+
]);
|
|
1520
|
+
}
|
|
1521
|
+
function usefulTail(stdout, stderr) {
|
|
1522
|
+
const lines = `${stdout}
|
|
1523
|
+
${stderr}`.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean).slice(-12);
|
|
1524
|
+
return lines.length > 0 ? lines.join("\n") : "The build exited without output.";
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// src/lint.ts
|
|
1528
|
+
import zappie from "eslint-plugin-zappie";
|
|
1529
|
+
import { createRequire } from "module";
|
|
1530
|
+
import path8 from "path";
|
|
1531
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1532
|
+
async function checkLint({
|
|
1533
|
+
cwd,
|
|
1534
|
+
signal
|
|
1535
|
+
}) {
|
|
1536
|
+
signal?.throwIfAborted();
|
|
1537
|
+
let ESLint;
|
|
1538
|
+
try {
|
|
1539
|
+
const require2 = createRequire(path8.join(cwd, "package.json"));
|
|
1540
|
+
const loadedEslint = await import(pathToFileURL2(require2.resolve("eslint")).href);
|
|
1541
|
+
ESLint = loadedEslint.ESLint;
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
return step("lint", "failed", [
|
|
1544
|
+
problem(message2(error), "Install ESLint in the project.", {
|
|
1545
|
+
rule: "lint/unavailable"
|
|
1546
|
+
})
|
|
1547
|
+
]);
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
const eslint = new ESLint({ cwd });
|
|
1551
|
+
const source = await sourceFile(cwd);
|
|
1552
|
+
const config = await eslint.calculateConfigForFile(source);
|
|
1553
|
+
const problems = recommendedProblems(config?.rules ?? {});
|
|
1554
|
+
if (problems.length > 0) {
|
|
1555
|
+
return step("lint", "failed", problems);
|
|
1556
|
+
}
|
|
1557
|
+
const results = await eslint.lintFiles(["."]);
|
|
1558
|
+
signal?.throwIfAborted();
|
|
1559
|
+
const lintProblems = results.flatMap(
|
|
1560
|
+
(result) => result.messages.map(
|
|
1561
|
+
(entry) => problem(entry.message, null, {
|
|
1562
|
+
...entry.ruleId ? { rule: entry.ruleId } : {},
|
|
1563
|
+
file: relative(cwd, result.filePath),
|
|
1564
|
+
...entry.line ? { line: entry.line } : {}
|
|
1565
|
+
})
|
|
1566
|
+
)
|
|
1567
|
+
);
|
|
1568
|
+
const failed = results.some(
|
|
1569
|
+
(result) => result.messages.some((entry) => entry.fatal || entry.severity === 2)
|
|
1570
|
+
);
|
|
1571
|
+
return step("lint", failed ? "failed" : "passed", lintProblems);
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
return step("lint", "failed", [
|
|
1574
|
+
problem(
|
|
1575
|
+
message2(error),
|
|
1576
|
+
"Fix the project ESLint configuration and run lint again.",
|
|
1577
|
+
{
|
|
1578
|
+
rule: "lint/config"
|
|
1579
|
+
}
|
|
1580
|
+
)
|
|
1581
|
+
]);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
function recommendedProblems(applied) {
|
|
1585
|
+
const expected = recommendedRules();
|
|
1586
|
+
return Object.entries(expected).flatMap(([rule, severity]) => {
|
|
1587
|
+
if (level(applied[rule]) >= level(severity)) {
|
|
1588
|
+
return [];
|
|
1589
|
+
}
|
|
1590
|
+
return [
|
|
1591
|
+
problem(
|
|
1592
|
+
`${rule} is not enabled at the recommended severity.`,
|
|
1593
|
+
"Apply every entry from zappie.configs.recommended in eslint.config.mjs.",
|
|
1594
|
+
{ rule, file: "eslint.config.mjs" }
|
|
1595
|
+
)
|
|
1596
|
+
];
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
function recommendedRules() {
|
|
1600
|
+
const result = {};
|
|
1601
|
+
for (const config of zappie.configs.recommended ?? []) {
|
|
1602
|
+
Object.assign(result, config.rules ?? {});
|
|
1603
|
+
}
|
|
1604
|
+
return Object.fromEntries(
|
|
1605
|
+
Object.entries(result).filter(([name]) => name.startsWith("zappie/"))
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
function level(value) {
|
|
1609
|
+
const selected = Array.isArray(value) ? value[0] : value;
|
|
1610
|
+
if (selected === "error" || selected === 2) {
|
|
1611
|
+
return 2;
|
|
1612
|
+
}
|
|
1613
|
+
if (selected === "warn" || selected === 1) {
|
|
1614
|
+
return 1;
|
|
1615
|
+
}
|
|
1616
|
+
return 0;
|
|
1617
|
+
}
|
|
1618
|
+
async function sourceFile(cwd) {
|
|
1619
|
+
const candidates = [
|
|
1620
|
+
"app/index.ts",
|
|
1621
|
+
"app/root.tsx",
|
|
1622
|
+
"src/index.ts",
|
|
1623
|
+
"index.ts"
|
|
1624
|
+
];
|
|
1625
|
+
for (const candidate of candidates) {
|
|
1626
|
+
try {
|
|
1627
|
+
await import("fs/promises").then(
|
|
1628
|
+
({ access: access3 }) => access3(path8.join(cwd, candidate))
|
|
1629
|
+
);
|
|
1630
|
+
return candidate;
|
|
1631
|
+
} catch {
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return "eslint.config.mjs";
|
|
1636
|
+
}
|
|
1637
|
+
function message2(error) {
|
|
1638
|
+
return error instanceof Error ? error.message : String(error);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// src/secrets.ts
|
|
1642
|
+
import { lstat, readFile as readFile8, readdir as readdir2 } from "fs/promises";
|
|
1643
|
+
import path9 from "path";
|
|
1644
|
+
|
|
1645
|
+
// src/secret-patterns.ts
|
|
1646
|
+
var FIX = "Move the value to a server environment variable and rotate it.";
|
|
1647
|
+
function secretMatches(source, envFile) {
|
|
1648
|
+
const matches2 = [];
|
|
1649
|
+
add(matches2, source, /sk_(test|live)_[A-Za-z0-9]{16,}/g, (value) => {
|
|
1650
|
+
const live = value.startsWith("sk_live_");
|
|
1651
|
+
return {
|
|
1652
|
+
rule: live ? "secrets/sk-live" : "secrets/sk-test",
|
|
1653
|
+
message: `Detected ${live ? "sk_live_" : "sk_test_"}\u2026 secret.`,
|
|
1654
|
+
fix: FIX,
|
|
1655
|
+
severity: "error"
|
|
1656
|
+
};
|
|
1657
|
+
});
|
|
1658
|
+
if (!envFile) {
|
|
1659
|
+
add(matches2, source, /pk_live_[A-Za-z0-9]{16,}/g, () => ({
|
|
1660
|
+
rule: "secrets/pk-live",
|
|
1661
|
+
message: "Warning: detected pk_live_\u2026 outside an .env file.",
|
|
1662
|
+
fix: "Move the publishable key to a server-provided environment variable.",
|
|
1663
|
+
severity: "warning"
|
|
1664
|
+
}));
|
|
1665
|
+
}
|
|
1666
|
+
add(matches2, source, /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/g, () => ({
|
|
1667
|
+
rule: "secrets/private-key",
|
|
1668
|
+
message: "Detected a PEM private key header.",
|
|
1669
|
+
fix: FIX,
|
|
1670
|
+
severity: "error"
|
|
1671
|
+
}));
|
|
1672
|
+
add(matches2, source, /AKIA[0-9A-Z]{16}/g, () => ({
|
|
1673
|
+
rule: "secrets/aws-access-key",
|
|
1674
|
+
message: "Detected an AKIA\u2026 access key.",
|
|
1675
|
+
fix: FIX,
|
|
1676
|
+
severity: "error"
|
|
1677
|
+
}));
|
|
1678
|
+
add(matches2, source, /FLWSECK-[A-Za-z0-9_-]{8,}/g, () => ({
|
|
1679
|
+
rule: "secrets/flutterwave-live",
|
|
1680
|
+
message: "Detected an FLWSECK-\u2026 secret.",
|
|
1681
|
+
fix: FIX,
|
|
1682
|
+
severity: "error"
|
|
1683
|
+
}));
|
|
1684
|
+
const occupied = matches2.map(({ index }) => index);
|
|
1685
|
+
const assignment = /\b([A-Za-z_$][\w$]*(?:secret|token|password|api[_-]?key)[\w$]*)\s*[:=]\s*(["'])([^"'\r\n]{24,})\2/gi;
|
|
1686
|
+
for (const found of source.matchAll(assignment)) {
|
|
1687
|
+
const value = found[3] ?? "";
|
|
1688
|
+
const valueIndex = (found.index ?? 0) + found[0].indexOf(value);
|
|
1689
|
+
if (entropy(value) > 4 && !occupied.some(
|
|
1690
|
+
(index) => index >= valueIndex && index < valueIndex + value.length
|
|
1691
|
+
)) {
|
|
1692
|
+
matches2.push({
|
|
1693
|
+
index: valueIndex,
|
|
1694
|
+
rule: "secrets/high-entropy",
|
|
1695
|
+
message: `Detected a high-entropy value assigned to ${found[1] ?? "a secret-shaped name"}.`,
|
|
1696
|
+
fix: FIX,
|
|
1697
|
+
severity: "error"
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return matches2.sort((left, right) => left.index - right.index);
|
|
1702
|
+
}
|
|
1703
|
+
function add(output, source, pattern, details) {
|
|
1704
|
+
for (const match of source.matchAll(pattern)) {
|
|
1705
|
+
output.push({ index: match.index ?? 0, ...details(match[0]) });
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
function entropy(value) {
|
|
1709
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1710
|
+
for (const character of value) {
|
|
1711
|
+
counts.set(character, (counts.get(character) ?? 0) + 1);
|
|
1712
|
+
}
|
|
1713
|
+
return [...counts.values()].reduce((total2, count) => {
|
|
1714
|
+
const probability = count / value.length;
|
|
1715
|
+
return total2 - probability * Math.log2(probability);
|
|
1716
|
+
}, 0);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/secrets.ts
|
|
1720
|
+
var SECRET_FILE_SIZE_CAP = 1024 * 1024;
|
|
1721
|
+
async function scanSecrets(dir) {
|
|
1722
|
+
const findings = [];
|
|
1723
|
+
for (const file of await filesToScan(dir)) {
|
|
1724
|
+
const info = await lstat(file);
|
|
1725
|
+
if (!info.isFile() || info.size > SECRET_FILE_SIZE_CAP) {
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
const contents = await readFile8(file);
|
|
1729
|
+
if (contents.includes(0)) {
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
const source = contents.toString("utf8");
|
|
1733
|
+
for (const match of secretMatches(
|
|
1734
|
+
source,
|
|
1735
|
+
path9.basename(file).startsWith(".env")
|
|
1736
|
+
)) {
|
|
1737
|
+
findings.push({
|
|
1738
|
+
rule: match.rule,
|
|
1739
|
+
file: relative(dir, file),
|
|
1740
|
+
line: lineAt(source, match.index),
|
|
1741
|
+
message: match.message,
|
|
1742
|
+
fix: match.fix,
|
|
1743
|
+
severity: match.severity
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return findings;
|
|
1748
|
+
}
|
|
1749
|
+
async function checkSecrets({
|
|
1750
|
+
cwd,
|
|
1751
|
+
signal
|
|
1752
|
+
}) {
|
|
1753
|
+
signal?.throwIfAborted();
|
|
1754
|
+
const findings = await scanSecrets(cwd);
|
|
1755
|
+
signal?.throwIfAborted();
|
|
1756
|
+
const failed = findings.some(({ severity }) => severity === "error");
|
|
1757
|
+
return step(
|
|
1758
|
+
"secrets",
|
|
1759
|
+
failed ? "failed" : "passed",
|
|
1760
|
+
findings.map(({ severity: _severity, ...finding }) => finding)
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
async function filesToScan(cwd) {
|
|
1764
|
+
const files = [];
|
|
1765
|
+
for (const directory of ["app", "public"]) {
|
|
1766
|
+
await collect2(path9.join(cwd, directory), files);
|
|
1767
|
+
}
|
|
1768
|
+
await collect2(
|
|
1769
|
+
path9.join(cwd, "build/client"),
|
|
1770
|
+
files,
|
|
1771
|
+
(file) => file.endsWith(".js")
|
|
1772
|
+
);
|
|
1773
|
+
try {
|
|
1774
|
+
for (const entry of await readdir2(cwd, { withFileTypes: true })) {
|
|
1775
|
+
if (entry.isFile() && rootConfig(entry.name)) {
|
|
1776
|
+
files.push(path9.join(cwd, entry.name));
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
} catch {
|
|
1780
|
+
return files.sort();
|
|
1781
|
+
}
|
|
1782
|
+
return [...new Set(files)].sort();
|
|
1783
|
+
}
|
|
1784
|
+
async function collect2(directory, files, accept = () => true) {
|
|
1785
|
+
try {
|
|
1786
|
+
for (const entry of await readdir2(directory, { withFileTypes: true })) {
|
|
1787
|
+
if (entry.isSymbolicLink()) {
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
const file = path9.join(directory, entry.name);
|
|
1791
|
+
if (entry.isDirectory()) {
|
|
1792
|
+
await collect2(file, files, accept);
|
|
1793
|
+
} else if (entry.isFile() && accept(file)) {
|
|
1794
|
+
files.push(file);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
1799
|
+
throw error;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function rootConfig(name) {
|
|
1804
|
+
return name === "package.json" || name === "wrangler.json" || name === "wrangler.jsonc" || /(?:^|\.)config\.(?:[cm]?[jt]s|json)$/.test(name);
|
|
1805
|
+
}
|
|
1806
|
+
function lineAt(source, index) {
|
|
1807
|
+
return source.slice(0, index).split(/\r?\n/).length;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// src/test-step.ts
|
|
1811
|
+
import path10 from "path";
|
|
1812
|
+
async function checkTests({
|
|
1813
|
+
cwd,
|
|
1814
|
+
packageManager,
|
|
1815
|
+
signal
|
|
1816
|
+
}) {
|
|
1817
|
+
const manifest = await json(path10.join(cwd, "package.json"));
|
|
1818
|
+
if (!manifest.scripts?.test) {
|
|
1819
|
+
return skipped("test", "No test script is defined in package.json.");
|
|
1820
|
+
}
|
|
1821
|
+
const invocation = scriptArgs(packageManager, "test");
|
|
1822
|
+
const result = await command(
|
|
1823
|
+
invocation.executable,
|
|
1824
|
+
invocation.args,
|
|
1825
|
+
cwd,
|
|
1826
|
+
signal
|
|
1827
|
+
);
|
|
1828
|
+
signal?.throwIfAborted();
|
|
1829
|
+
if (result.code === 0) {
|
|
1830
|
+
return step("test", "passed");
|
|
1831
|
+
}
|
|
1832
|
+
return step("test", "failed", [
|
|
1833
|
+
problem(result.stdout + result.stderr, "Fix the failing project tests.", {
|
|
1834
|
+
rule: "test/failed"
|
|
1835
|
+
})
|
|
1836
|
+
]);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// src/types-step.ts
|
|
1840
|
+
import path11 from "path";
|
|
1841
|
+
async function checkTypes(options) {
|
|
1842
|
+
const { cwd, packageManager, signal } = options;
|
|
1843
|
+
signal?.throwIfAborted();
|
|
1844
|
+
const manifest = await json(path11.join(cwd, "package.json"));
|
|
1845
|
+
if (manifest.dependencies?.["react-router"]) {
|
|
1846
|
+
const generated = execArgs(packageManager, "react-router", ["typegen"]);
|
|
1847
|
+
const result2 = await command(
|
|
1848
|
+
generated.executable,
|
|
1849
|
+
generated.args,
|
|
1850
|
+
cwd,
|
|
1851
|
+
signal
|
|
1852
|
+
);
|
|
1853
|
+
if (result2.code !== 0) {
|
|
1854
|
+
return step(
|
|
1855
|
+
"types",
|
|
1856
|
+
"failed",
|
|
1857
|
+
outputProblems(result2.stdout + result2.stderr)
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
const typed = execArgs(packageManager, "tsc", ["--noEmit"]);
|
|
1862
|
+
const result = await command(typed.executable, typed.args, cwd, signal);
|
|
1863
|
+
signal?.throwIfAborted();
|
|
1864
|
+
if (result.code === 0) {
|
|
1865
|
+
return step("types", "passed");
|
|
1866
|
+
}
|
|
1867
|
+
return step("types", "failed", outputProblems(result.stdout + result.stderr));
|
|
1868
|
+
}
|
|
1869
|
+
function outputProblems(output) {
|
|
1870
|
+
const parsed = output.split(/\r?\n/).flatMap((line) => {
|
|
1871
|
+
const match = /^(.*)\((\d+),(\d+)\): error (TS\d+): (.*)$/.exec(line);
|
|
1872
|
+
if (!match) {
|
|
1873
|
+
return [];
|
|
1874
|
+
}
|
|
1875
|
+
return [
|
|
1876
|
+
problem(match[5] ?? line, "Fix the TypeScript error.", {
|
|
1877
|
+
...match[4] ? { rule: match[4] } : {},
|
|
1878
|
+
...match[1] ? { file: match[1] } : {},
|
|
1879
|
+
line: Number(match[2])
|
|
1880
|
+
})
|
|
1881
|
+
];
|
|
1882
|
+
});
|
|
1883
|
+
if (parsed.length > 0) {
|
|
1884
|
+
return parsed;
|
|
1885
|
+
}
|
|
1886
|
+
return [
|
|
1887
|
+
problem(
|
|
1888
|
+
output.trim() || "TypeScript failed.",
|
|
1889
|
+
"Fix the TypeScript errors."
|
|
1890
|
+
)
|
|
1891
|
+
];
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/run.ts
|
|
1895
|
+
var runners = [
|
|
1896
|
+
{ name: "config", run: checkConfig },
|
|
1897
|
+
{ name: "allowlist", run: checkAllowlist },
|
|
1898
|
+
{ name: "lint", run: checkLint },
|
|
1899
|
+
{ name: "types", run: checkTypes },
|
|
1900
|
+
{ name: "test", run: checkTests },
|
|
1901
|
+
{ name: "build", run: checkBuild },
|
|
1902
|
+
{ name: "secrets", run: checkSecrets },
|
|
1903
|
+
{ name: "budgets", run: checkBudgets }
|
|
1904
|
+
];
|
|
1905
|
+
async function runCheck(options) {
|
|
1906
|
+
const steps = [];
|
|
1907
|
+
for (const runner of runners) {
|
|
1908
|
+
options.signal?.throwIfAborted();
|
|
1909
|
+
let checked;
|
|
1910
|
+
try {
|
|
1911
|
+
checked = await runner.run(options);
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
options.signal?.throwIfAborted();
|
|
1914
|
+
checked = step(runner.name, "failed", [
|
|
1915
|
+
problem(
|
|
1916
|
+
message3(error),
|
|
1917
|
+
"Fix the step setup and run zappie check again."
|
|
1918
|
+
)
|
|
1919
|
+
]);
|
|
1920
|
+
}
|
|
1921
|
+
steps.push(checked);
|
|
1922
|
+
if (checked.result === "failed" && !options.all) {
|
|
1923
|
+
break;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return {
|
|
1927
|
+
result: steps.some((entry) => entry.result === "failed") ? "failed" : "passed",
|
|
1928
|
+
steps
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
function message3(error) {
|
|
1932
|
+
return error instanceof Error ? error.message : String(error);
|
|
1933
|
+
}
|
|
1934
|
+
export {
|
|
1935
|
+
BUILTIN_ALLOWLIST_SOURCE,
|
|
1936
|
+
SECRET_FILE_SIZE_CAP,
|
|
1937
|
+
checkAllowlist,
|
|
1938
|
+
checkBudgets,
|
|
1939
|
+
checkBuild,
|
|
1940
|
+
checkConfig,
|
|
1941
|
+
checkLint,
|
|
1942
|
+
checkSecrets,
|
|
1943
|
+
checkTests,
|
|
1944
|
+
checkTypes,
|
|
1945
|
+
loadConfig,
|
|
1946
|
+
runCheck,
|
|
1947
|
+
scanSecrets,
|
|
1948
|
+
supportsTypeScriptImport
|
|
1949
|
+
};
|
|
1950
|
+
//# sourceMappingURL=index.js.map
|