@simpleapps-com/augur-config 2.0.11 → 2.0.12
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/bin/augur-doctor.mjs +621 -0
- package/package.json +5 -1
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* augur-doctor -- Site health check against the Augur platform standard.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx augur-doctor (runs against current directory)
|
|
8
|
+
* npx augur-doctor /path/to/site
|
|
9
|
+
*
|
|
10
|
+
* Checks a consumer site for alignment with the reference implementation
|
|
11
|
+
* (ampro-online). Outputs a pass/fail/warn scorecard.
|
|
12
|
+
*
|
|
13
|
+
* See: https://github.com/simpleapps-com/augur-packages/wiki/guide-site-assessment
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
17
|
+
import { resolve, basename } from "node:path";
|
|
18
|
+
|
|
19
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const sitePath = process.argv[2] || ".";
|
|
22
|
+
const siteDir = resolve(sitePath);
|
|
23
|
+
|
|
24
|
+
if (!existsSync(siteDir)) {
|
|
25
|
+
console.error(`Site directory not found: ${siteDir}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Verify this looks like a site (has package.json)
|
|
30
|
+
if (!existsSync(resolve(siteDir, "package.json"))) {
|
|
31
|
+
console.error(`No package.json found in: ${siteDir}`);
|
|
32
|
+
console.error("");
|
|
33
|
+
console.error("Usage: augur-doctor [/path/to/site]");
|
|
34
|
+
console.error("");
|
|
35
|
+
console.error("Run from a site directory, or pass the path as an argument.");
|
|
36
|
+
console.error("Examples:");
|
|
37
|
+
console.error(" npx augur-doctor");
|
|
38
|
+
console.error(" npx augur-doctor ./sites/pyebarker_store");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const siteName = basename(siteDir);
|
|
43
|
+
|
|
44
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function fileExists(relPath) {
|
|
47
|
+
return existsSync(resolve(siteDir, relPath));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readFile(relPath) {
|
|
51
|
+
const fullPath = resolve(siteDir, relPath);
|
|
52
|
+
if (!existsSync(fullPath)) return null;
|
|
53
|
+
return readFileSync(fullPath, "utf-8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readJson(relPath) {
|
|
57
|
+
const content = readFile(relPath);
|
|
58
|
+
if (!content) return null;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(content);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Find a file matching any of the given relative paths. */
|
|
67
|
+
function findFile(...candidates) {
|
|
68
|
+
for (const c of candidates) {
|
|
69
|
+
if (fileExists(c)) return c;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Check if file content contains a pattern. */
|
|
75
|
+
function fileContains(relPath, pattern) {
|
|
76
|
+
const content = readFile(relPath);
|
|
77
|
+
if (!content) return false;
|
|
78
|
+
if (typeof pattern === "string") return content.includes(pattern);
|
|
79
|
+
return pattern.test(content);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Results ──────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
const results = [];
|
|
85
|
+
|
|
86
|
+
function pass(id, label, detail) {
|
|
87
|
+
results.push({ status: "PASS", id, label, detail });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function fail(id, label, detail) {
|
|
91
|
+
results.push({ status: "FAIL", id, label, detail });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function warn(id, label, detail) {
|
|
95
|
+
results.push({ status: "WARN", id, label, detail });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function skip(id, label, detail) {
|
|
99
|
+
results.push({ status: "SKIP", id, label, detail });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Checks ───────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
// 1. Site config with defineSite()
|
|
105
|
+
const configFile = findFile(
|
|
106
|
+
"lib/augur-config.ts",
|
|
107
|
+
"src/lib/augur-config.ts",
|
|
108
|
+
"config/augur-config.ts"
|
|
109
|
+
);
|
|
110
|
+
if (configFile && fileContains(configFile, "defineSite")) {
|
|
111
|
+
pass("site-config", "augur-config.ts with defineSite()", configFile);
|
|
112
|
+
} else if (configFile) {
|
|
113
|
+
warn(
|
|
114
|
+
"site-config",
|
|
115
|
+
"augur-config.ts exists but does not use defineSite()",
|
|
116
|
+
configFile
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
fail(
|
|
120
|
+
"site-config",
|
|
121
|
+
"augur-config.ts with defineSite()",
|
|
122
|
+
"Expected lib/augur-config.ts"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. Server site with createServerSite()
|
|
127
|
+
const siteFile = findFile(
|
|
128
|
+
"lib/augur-site.ts",
|
|
129
|
+
"src/lib/augur-site.ts",
|
|
130
|
+
"config/augur-site.ts"
|
|
131
|
+
);
|
|
132
|
+
if (siteFile && fileContains(siteFile, "createServerSite")) {
|
|
133
|
+
pass("server-site", "augur-site.ts using createServerSite()", siteFile);
|
|
134
|
+
} else if (siteFile) {
|
|
135
|
+
warn(
|
|
136
|
+
"server-site",
|
|
137
|
+
"augur-site.ts exists but does not use createServerSite()",
|
|
138
|
+
siteFile
|
|
139
|
+
);
|
|
140
|
+
} else {
|
|
141
|
+
fail(
|
|
142
|
+
"server-site",
|
|
143
|
+
"augur-site.ts using createServerSite()",
|
|
144
|
+
"Expected lib/augur-site.ts"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 3. Actions re-export
|
|
149
|
+
const actionsFile = findFile(
|
|
150
|
+
"lib/augur-actions.ts",
|
|
151
|
+
"src/lib/augur-actions.ts",
|
|
152
|
+
"lib/actions.ts"
|
|
153
|
+
);
|
|
154
|
+
if (actionsFile && fileContains(actionsFile, /site\.actions|actions\./)) {
|
|
155
|
+
pass("actions", "augur-actions.ts re-exporting site.actions", actionsFile);
|
|
156
|
+
} else if (actionsFile) {
|
|
157
|
+
warn(
|
|
158
|
+
"actions",
|
|
159
|
+
"augur-actions.ts exists but may not re-export site.actions",
|
|
160
|
+
actionsFile
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
fail(
|
|
164
|
+
"actions",
|
|
165
|
+
"augur-actions.ts re-exporting site.actions.*",
|
|
166
|
+
"Expected lib/augur-actions.ts"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 4. Client hooks with createSiteHooks()
|
|
171
|
+
const hooksFile = findFile(
|
|
172
|
+
"lib/augur.ts",
|
|
173
|
+
"src/lib/augur.ts",
|
|
174
|
+
"lib/augur-hooks.ts",
|
|
175
|
+
"src/lib/augur-hooks.ts"
|
|
176
|
+
);
|
|
177
|
+
if (hooksFile && fileContains(hooksFile, "createSiteHooks")) {
|
|
178
|
+
pass("site-hooks", "augur.ts with createSiteHooks()", hooksFile);
|
|
179
|
+
} else if (
|
|
180
|
+
hooksFile &&
|
|
181
|
+
fileContains(hooksFile, /AugurHooksProvider|augur-hooks/)
|
|
182
|
+
) {
|
|
183
|
+
pass(
|
|
184
|
+
"site-hooks",
|
|
185
|
+
"augur.ts with hooks setup",
|
|
186
|
+
hooksFile
|
|
187
|
+
);
|
|
188
|
+
} else if (hooksFile) {
|
|
189
|
+
warn(
|
|
190
|
+
"site-hooks",
|
|
191
|
+
"augur.ts exists but does not use createSiteHooks()",
|
|
192
|
+
hooksFile
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
fail(
|
|
196
|
+
"site-hooks",
|
|
197
|
+
"augur.ts with createSiteHooks()",
|
|
198
|
+
"Expected lib/augur.ts"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 5. SSR prefetch options
|
|
203
|
+
const optionsFile = findFile(
|
|
204
|
+
"lib/augur-options.ts",
|
|
205
|
+
"src/lib/augur-options.ts"
|
|
206
|
+
);
|
|
207
|
+
if (optionsFile && fileContains(optionsFile, /Options/)) {
|
|
208
|
+
pass("ssr-options", "augur-options.ts for SSR prefetching", optionsFile);
|
|
209
|
+
} else if (optionsFile) {
|
|
210
|
+
warn(
|
|
211
|
+
"ssr-options",
|
|
212
|
+
"augur-options.ts exists but may not export options",
|
|
213
|
+
optionsFile
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
fail(
|
|
217
|
+
"ssr-options",
|
|
218
|
+
"augur-options.ts for SSR prefetching",
|
|
219
|
+
"Expected lib/augur-options.ts"
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 6. tsconfig extends augur-config
|
|
224
|
+
const tsconfig = readJson("tsconfig.json");
|
|
225
|
+
if (tsconfig) {
|
|
226
|
+
const extendsVal = tsconfig.extends || "";
|
|
227
|
+
if (extendsVal.includes("augur-config/tsconfig/next")) {
|
|
228
|
+
pass(
|
|
229
|
+
"tsconfig",
|
|
230
|
+
"tsconfig.json extends augur-config/tsconfig/next",
|
|
231
|
+
""
|
|
232
|
+
);
|
|
233
|
+
} else if (extendsVal.includes("augur-config")) {
|
|
234
|
+
warn(
|
|
235
|
+
"tsconfig",
|
|
236
|
+
"tsconfig.json extends augur-config but not /tsconfig/next",
|
|
237
|
+
`extends: ${extendsVal}`
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
fail(
|
|
241
|
+
"tsconfig",
|
|
242
|
+
"tsconfig.json extends augur-config/tsconfig/next",
|
|
243
|
+
extendsVal ? `extends: ${extendsVal}` : "No extends field"
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
fail("tsconfig", "tsconfig.json exists", "File not found");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 7. ESLint uses shared config
|
|
251
|
+
const eslintFile = findFile("eslint.config.mjs", "eslint.config.js");
|
|
252
|
+
if (eslintFile && fileContains(eslintFile, "augur-config/eslint")) {
|
|
253
|
+
pass("eslint", "eslint.config.mjs uses augur-config/eslint/next", eslintFile);
|
|
254
|
+
} else if (eslintFile) {
|
|
255
|
+
fail(
|
|
256
|
+
"eslint",
|
|
257
|
+
"eslint.config.mjs uses augur-config/eslint/next",
|
|
258
|
+
"Config exists but does not import augur-config/eslint"
|
|
259
|
+
);
|
|
260
|
+
} else if (
|
|
261
|
+
findFile(".eslintrc.json", ".eslintrc.js", ".eslintrc", ".eslintrc.cjs")
|
|
262
|
+
) {
|
|
263
|
+
fail(
|
|
264
|
+
"eslint",
|
|
265
|
+
"eslint.config.mjs uses augur-config/eslint/next",
|
|
266
|
+
"Site uses legacy .eslintrc format -- migrate to eslint.config.mjs"
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
fail("eslint", "eslint.config.mjs exists", "No ESLint config found");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 8-12. next.config checks
|
|
273
|
+
const nextConfigFile = findFile(
|
|
274
|
+
"next.config.ts",
|
|
275
|
+
"next.config.mjs",
|
|
276
|
+
"next.config.js"
|
|
277
|
+
);
|
|
278
|
+
if (nextConfigFile) {
|
|
279
|
+
const nextConfig = readFile(nextConfigFile);
|
|
280
|
+
|
|
281
|
+
// 8. serverExternalPackages: no augur-server
|
|
282
|
+
if (nextConfig.includes("serverExternalPackages")) {
|
|
283
|
+
if (nextConfig.includes("augur-server")) {
|
|
284
|
+
fail(
|
|
285
|
+
"next-external",
|
|
286
|
+
"next.config: augur-server NOT in serverExternalPackages",
|
|
287
|
+
"augur-server must be bundled, not externalized"
|
|
288
|
+
);
|
|
289
|
+
} else {
|
|
290
|
+
pass(
|
|
291
|
+
"next-external",
|
|
292
|
+
"next.config: serverExternalPackages correct",
|
|
293
|
+
""
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check for ioredis (if site uses Redis)
|
|
298
|
+
const pkg = readJson("package.json");
|
|
299
|
+
const hasIoredis =
|
|
300
|
+
pkg?.dependencies?.ioredis || pkg?.devDependencies?.ioredis;
|
|
301
|
+
if (hasIoredis && !nextConfig.includes("ioredis")) {
|
|
302
|
+
warn(
|
|
303
|
+
"next-ioredis",
|
|
304
|
+
"next.config: ioredis should be in serverExternalPackages",
|
|
305
|
+
"Site uses ioredis but it is not externalized"
|
|
306
|
+
);
|
|
307
|
+
} else if (hasIoredis) {
|
|
308
|
+
pass("next-ioredis", "next.config: ioredis in serverExternalPackages", "");
|
|
309
|
+
} else {
|
|
310
|
+
skip("next-ioredis", "next.config: ioredis check", "Site does not use ioredis");
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
warn(
|
|
314
|
+
"next-external",
|
|
315
|
+
"next.config: no serverExternalPackages found",
|
|
316
|
+
"Add serverExternalPackages for native Node modules"
|
|
317
|
+
);
|
|
318
|
+
skip("next-ioredis", "next.config: ioredis check", "No serverExternalPackages");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 9. Security headers
|
|
322
|
+
if (
|
|
323
|
+
nextConfig.includes("Strict-Transport-Security") &&
|
|
324
|
+
nextConfig.includes("X-Content-Type-Options") &&
|
|
325
|
+
nextConfig.includes("Permissions-Policy")
|
|
326
|
+
) {
|
|
327
|
+
pass("next-headers", "next.config: security headers", "");
|
|
328
|
+
} else if (nextConfig.includes("headers")) {
|
|
329
|
+
warn(
|
|
330
|
+
"next-headers",
|
|
331
|
+
"next.config: security headers incomplete",
|
|
332
|
+
"Missing HSTS, X-Content-Type-Options, or Permissions-Policy"
|
|
333
|
+
);
|
|
334
|
+
} else {
|
|
335
|
+
fail(
|
|
336
|
+
"next-headers",
|
|
337
|
+
"next.config: security headers",
|
|
338
|
+
"No headers() function found"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 10. optimizePackageImports
|
|
343
|
+
if (nextConfig.includes("optimizePackageImports")) {
|
|
344
|
+
pass("next-optimize", "next.config: optimizePackageImports", "");
|
|
345
|
+
} else {
|
|
346
|
+
fail(
|
|
347
|
+
"next-optimize",
|
|
348
|
+
"next.config: optimizePackageImports",
|
|
349
|
+
"Add experimental.optimizePackageImports for augur packages"
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 11. reactStrictMode
|
|
354
|
+
if (nextConfig.includes("reactStrictMode")) {
|
|
355
|
+
pass("next-strict", "next.config: reactStrictMode", "");
|
|
356
|
+
} else {
|
|
357
|
+
warn(
|
|
358
|
+
"next-strict",
|
|
359
|
+
"next.config: reactStrictMode not set",
|
|
360
|
+
"Recommended for development error detection"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
fail("next-external", "next.config exists", "No next.config found");
|
|
365
|
+
skip("next-ioredis", "next.config: ioredis check", "No next.config");
|
|
366
|
+
fail("next-headers", "next.config: security headers", "No next.config");
|
|
367
|
+
fail("next-optimize", "next.config: optimizePackageImports", "No next.config");
|
|
368
|
+
skip("next-strict", "next.config: reactStrictMode", "No next.config");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 12. SDK contract tests
|
|
372
|
+
const hasContractTests =
|
|
373
|
+
fileExists("__tests__/sdk-contracts") ||
|
|
374
|
+
fileExists("src/__tests__/sdk-contracts") ||
|
|
375
|
+
fileExists("__tests__/sdk-contract.test.ts") ||
|
|
376
|
+
fileExists("src/__tests__/sdk-contract.test.ts");
|
|
377
|
+
if (hasContractTests) {
|
|
378
|
+
pass("contract-tests", "SDK contract tests present", "");
|
|
379
|
+
} else {
|
|
380
|
+
fail(
|
|
381
|
+
"contract-tests",
|
|
382
|
+
"SDK contract tests in __tests__/sdk-contracts/",
|
|
383
|
+
"No contract tests found"
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 13. No custom AugurClient wrapper
|
|
388
|
+
const libDir = findFile("lib", "src/lib");
|
|
389
|
+
if (libDir) {
|
|
390
|
+
const augurClientFile = findFile(
|
|
391
|
+
"lib/augur-client.ts",
|
|
392
|
+
"src/lib/augur-client.ts",
|
|
393
|
+
"lib/augurClient.ts",
|
|
394
|
+
"src/lib/augurClient.ts"
|
|
395
|
+
);
|
|
396
|
+
if (augurClientFile) {
|
|
397
|
+
const content = readFile(augurClientFile);
|
|
398
|
+
// Check for wrapper patterns that suggest custom SDK wrapping
|
|
399
|
+
if (
|
|
400
|
+
content &&
|
|
401
|
+
(content.includes("class AugurClient") ||
|
|
402
|
+
content.includes("extends AugurAPI"))
|
|
403
|
+
) {
|
|
404
|
+
fail(
|
|
405
|
+
"no-wrapper",
|
|
406
|
+
"No custom AugurClient wrapper",
|
|
407
|
+
`${augurClientFile} wraps AugurAPI -- use augur-api directly`
|
|
408
|
+
);
|
|
409
|
+
} else {
|
|
410
|
+
pass(
|
|
411
|
+
"no-wrapper",
|
|
412
|
+
"No custom AugurClient wrapper",
|
|
413
|
+
augurClientFile ? "Uses augur-api directly" : ""
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
pass("no-wrapper", "No custom AugurClient wrapper", "");
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
skip("no-wrapper", "No custom AugurClient wrapper", "No lib/ directory");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 14. Auth uses site.auth or createAuthConfig
|
|
424
|
+
const authFile = findFile(
|
|
425
|
+
"auth.ts",
|
|
426
|
+
"lib/auth.ts",
|
|
427
|
+
"src/lib/auth.ts",
|
|
428
|
+
"src/auth.ts"
|
|
429
|
+
);
|
|
430
|
+
if (authFile) {
|
|
431
|
+
const authContent = readFile(authFile);
|
|
432
|
+
if (
|
|
433
|
+
authContent &&
|
|
434
|
+
(authContent.includes("site.auth") ||
|
|
435
|
+
authContent.includes("createAuthConfig") ||
|
|
436
|
+
authContent.includes("createServerSite"))
|
|
437
|
+
) {
|
|
438
|
+
pass(
|
|
439
|
+
"auth",
|
|
440
|
+
"Auth uses createAuthConfig or site.auth",
|
|
441
|
+
authFile
|
|
442
|
+
);
|
|
443
|
+
} else if (authContent && authContent.includes("NextAuth")) {
|
|
444
|
+
fail(
|
|
445
|
+
"auth",
|
|
446
|
+
"Auth uses createAuthConfig or site.auth",
|
|
447
|
+
`${authFile} uses raw NextAuth -- migrate to createAuthConfig()`
|
|
448
|
+
);
|
|
449
|
+
} else {
|
|
450
|
+
warn("auth", "Auth pattern unclear", authFile);
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
warn("auth", "No auth.ts found", "Expected auth.ts or lib/auth.ts");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 15. Image loader for agr.media CDN
|
|
457
|
+
if (nextConfigFile) {
|
|
458
|
+
const nextConfig = readFile(nextConfigFile);
|
|
459
|
+
if (nextConfig.includes("agr.media") || nextConfig.includes("agrMedia")) {
|
|
460
|
+
pass("image-loader", "Image loader for agr.media CDN", "");
|
|
461
|
+
} else if (nextConfig.includes("images")) {
|
|
462
|
+
warn(
|
|
463
|
+
"image-loader",
|
|
464
|
+
"next.config has images config but no agr.media",
|
|
465
|
+
"Add agr.media to remotePatterns if site has product images"
|
|
466
|
+
);
|
|
467
|
+
} else {
|
|
468
|
+
warn(
|
|
469
|
+
"image-loader",
|
|
470
|
+
"No image configuration found",
|
|
471
|
+
"Add agr.media CDN to next.config images.remotePatterns"
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
skip("image-loader", "Image loader check", "No next.config");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 16. Quality script
|
|
479
|
+
const pkg = readJson("package.json");
|
|
480
|
+
if (pkg?.scripts?.quality) {
|
|
481
|
+
const quality = pkg.scripts.quality;
|
|
482
|
+
if (
|
|
483
|
+
quality.includes("typecheck") &&
|
|
484
|
+
quality.includes("lint") &&
|
|
485
|
+
quality.includes("test")
|
|
486
|
+
) {
|
|
487
|
+
pass("quality-script", "quality script: typecheck + lint + test", "");
|
|
488
|
+
} else {
|
|
489
|
+
warn(
|
|
490
|
+
"quality-script",
|
|
491
|
+
"quality script exists but may be incomplete",
|
|
492
|
+
`"quality": "${quality}"`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
fail(
|
|
497
|
+
"quality-script",
|
|
498
|
+
"quality script in package.json",
|
|
499
|
+
'Add "quality": "pnpm typecheck && pnpm lint && pnpm test"'
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 17. Icons: react-icons (not heroicons or lucide-react)
|
|
504
|
+
if (pkg) {
|
|
505
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
506
|
+
const hasReactIcons = !!allDeps["react-icons"];
|
|
507
|
+
const hasHeroicons = !!allDeps["@heroicons/react"];
|
|
508
|
+
const hasLucide = !!allDeps["lucide-react"];
|
|
509
|
+
|
|
510
|
+
if (hasReactIcons && !hasHeroicons && !hasLucide) {
|
|
511
|
+
pass("icons", "Uses react-icons (platform standard)", "");
|
|
512
|
+
} else if (hasHeroicons) {
|
|
513
|
+
fail(
|
|
514
|
+
"icons",
|
|
515
|
+
"Uses react-icons (platform standard)",
|
|
516
|
+
"Site uses @heroicons/react -- migrate to react-icons/lu"
|
|
517
|
+
);
|
|
518
|
+
} else if (hasLucide) {
|
|
519
|
+
fail(
|
|
520
|
+
"icons",
|
|
521
|
+
"Uses react-icons (platform standard)",
|
|
522
|
+
"Site uses lucide-react -- migrate to react-icons/lu"
|
|
523
|
+
);
|
|
524
|
+
} else if (!hasReactIcons) {
|
|
525
|
+
warn("icons", "No icon library found", "Expected react-icons");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 18. Tailwind v3.4 (not v4)
|
|
530
|
+
if (pkg) {
|
|
531
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
532
|
+
const tailwindVersion = allDeps["tailwindcss"] || "";
|
|
533
|
+
if (tailwindVersion.startsWith("^4") || tailwindVersion.startsWith("4")) {
|
|
534
|
+
fail(
|
|
535
|
+
"tailwind",
|
|
536
|
+
"Tailwind v3.4 (not v4)",
|
|
537
|
+
`Found tailwindcss: ${tailwindVersion} -- downgrade to ^3.4`
|
|
538
|
+
);
|
|
539
|
+
} else if (
|
|
540
|
+
tailwindVersion.startsWith("^3") ||
|
|
541
|
+
tailwindVersion.startsWith("3")
|
|
542
|
+
) {
|
|
543
|
+
pass("tailwind", "Tailwind v3.4 (platform standard)", "");
|
|
544
|
+
} else if (tailwindVersion) {
|
|
545
|
+
warn("tailwind", "Tailwind version unclear", `tailwindcss: ${tailwindVersion}`);
|
|
546
|
+
} else {
|
|
547
|
+
warn("tailwind", "No tailwindcss dependency found", "");
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ── Output ───────────────────────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
const ICONS = {
|
|
554
|
+
PASS: "\x1b[32m[PASS]\x1b[0m",
|
|
555
|
+
FAIL: "\x1b[31m[FAIL]\x1b[0m",
|
|
556
|
+
WARN: "\x1b[33m[WARN]\x1b[0m",
|
|
557
|
+
SKIP: "\x1b[90m[SKIP]\x1b[0m",
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
console.log("");
|
|
561
|
+
console.log(`\x1b[1mAugur Doctor: ${siteName}\x1b[0m`);
|
|
562
|
+
console.log(`Path: ${siteDir}`);
|
|
563
|
+
console.log("");
|
|
564
|
+
|
|
565
|
+
const groups = {
|
|
566
|
+
"Site Architecture": [
|
|
567
|
+
"site-config",
|
|
568
|
+
"server-site",
|
|
569
|
+
"actions",
|
|
570
|
+
"site-hooks",
|
|
571
|
+
"ssr-options",
|
|
572
|
+
],
|
|
573
|
+
"Config Alignment": ["tsconfig", "eslint", "tailwind", "icons"],
|
|
574
|
+
"Next.js Config": [
|
|
575
|
+
"next-external",
|
|
576
|
+
"next-ioredis",
|
|
577
|
+
"next-headers",
|
|
578
|
+
"next-optimize",
|
|
579
|
+
"next-strict",
|
|
580
|
+
],
|
|
581
|
+
"Testing & Quality": ["contract-tests", "quality-script"],
|
|
582
|
+
"Patterns": ["no-wrapper", "auth", "image-loader"],
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
for (const [groupName, ids] of Object.entries(groups)) {
|
|
586
|
+
console.log(`\x1b[1m${groupName}\x1b[0m`);
|
|
587
|
+
for (const id of ids) {
|
|
588
|
+
const result = results.find((r) => r.id === id);
|
|
589
|
+
if (!result) continue;
|
|
590
|
+
const icon = ICONS[result.status];
|
|
591
|
+
const detail = result.detail ? ` \x1b[90m${result.detail}\x1b[0m` : "";
|
|
592
|
+
console.log(` ${icon} ${result.label}${detail}`);
|
|
593
|
+
}
|
|
594
|
+
console.log("");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Summary
|
|
598
|
+
const passing = results.filter((r) => r.status === "PASS").length;
|
|
599
|
+
const failing = results.filter((r) => r.status === "FAIL").length;
|
|
600
|
+
const warnings = results.filter((r) => r.status === "WARN").length;
|
|
601
|
+
const skipped = results.filter((r) => r.status === "SKIP").length;
|
|
602
|
+
const total = results.length - skipped;
|
|
603
|
+
|
|
604
|
+
console.log(
|
|
605
|
+
`\x1b[1mScore: ${passing}/${total}\x1b[0m` +
|
|
606
|
+
(failing > 0 ? ` \x1b[31m(${failing} failing)\x1b[0m` : "") +
|
|
607
|
+
(warnings > 0 ? ` \x1b[33m(${warnings} warnings)\x1b[0m` : "")
|
|
608
|
+
);
|
|
609
|
+
console.log("");
|
|
610
|
+
|
|
611
|
+
if (failing > 0) {
|
|
612
|
+
console.log(
|
|
613
|
+
"See the migration runbook for fixes:"
|
|
614
|
+
);
|
|
615
|
+
console.log(
|
|
616
|
+
"https://github.com/simpleapps-com/augur-packages/wiki/guide-migration-runbook"
|
|
617
|
+
);
|
|
618
|
+
console.log("");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
process.exit(failing > 0 ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simpleapps-com/augur-config",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.12",
|
|
4
4
|
"description": "Shared tooling configuration presets for Augur ecommerce sites",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"directory": "packages/augur-config"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"augur-doctor": "./bin/augur-doctor.mjs"
|
|
14
|
+
},
|
|
12
15
|
"exports": {
|
|
13
16
|
"./eslint": "./src/eslint.js",
|
|
14
17
|
"./eslint/next": "./src/eslint-next.js",
|
|
@@ -22,6 +25,7 @@
|
|
|
22
25
|
},
|
|
23
26
|
"files": [
|
|
24
27
|
"src",
|
|
28
|
+
"bin",
|
|
25
29
|
"EXPORTS.md",
|
|
26
30
|
"llms.txt"
|
|
27
31
|
],
|