apex-auditor 0.1.1 → 0.1.3
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/dist/lighthouse-runner.js +2 -2
- package/dist/project-discovery.js +92 -0
- package/dist/route-detectors.js +116 -4
- package/dist/wizard-cli.js +31 -4
- package/package.json +4 -4
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import lighthouse from "lighthouse";
|
|
2
|
-
import
|
|
2
|
+
import { launch as launchChrome } from "chrome-launcher";
|
|
3
3
|
async function createChromeSession(chromePort) {
|
|
4
4
|
if (typeof chromePort === "number") {
|
|
5
5
|
return { port: chromePort };
|
|
6
6
|
}
|
|
7
|
-
const chrome = await
|
|
7
|
+
const chrome = await launchChrome({
|
|
8
8
|
chromeFlags: [
|
|
9
9
|
"--headless=new",
|
|
10
10
|
"--disable-gpu",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { pathExists } from "./fs-utils.js";
|
|
4
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
5
|
+
const IGNORED_DIRECTORIES = [
|
|
6
|
+
"node_modules",
|
|
7
|
+
".git",
|
|
8
|
+
".next",
|
|
9
|
+
".turbo",
|
|
10
|
+
".vercel",
|
|
11
|
+
"dist",
|
|
12
|
+
"build",
|
|
13
|
+
"out",
|
|
14
|
+
"coverage",
|
|
15
|
+
".cache",
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Discover Next.js projects below the given repository root.
|
|
19
|
+
* This performs a shallow breadth-first search and looks for either
|
|
20
|
+
* a next.config.* file or a package.json that depends on "next".
|
|
21
|
+
*/
|
|
22
|
+
export async function discoverNextProjects(options) {
|
|
23
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
24
|
+
const queue = [{ path: options.repoRoot, depth: 0 }];
|
|
25
|
+
const visited = new Set();
|
|
26
|
+
const projects = [];
|
|
27
|
+
while (queue.length > 0) {
|
|
28
|
+
const current = queue.shift();
|
|
29
|
+
if (visited.has(current.path)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
visited.add(current.path);
|
|
33
|
+
if (await isNextProjectRoot(current.path)) {
|
|
34
|
+
const name = basename(current.path) || ".";
|
|
35
|
+
projects.push({ name, root: current.path, framework: "next" });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (current.depth >= maxDepth) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
let entries;
|
|
42
|
+
try {
|
|
43
|
+
entries = await readdir(current.path, { withFileTypes: true });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!entry.isDirectory()) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (IGNORED_DIRECTORIES.includes(entry.name)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const childPath = join(current.path, entry.name);
|
|
56
|
+
queue.push({ path: childPath, depth: current.depth + 1 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return projects;
|
|
60
|
+
}
|
|
61
|
+
async function isNextProjectRoot(directory) {
|
|
62
|
+
const nextConfigCandidates = [
|
|
63
|
+
"next.config.js",
|
|
64
|
+
"next.config.mjs",
|
|
65
|
+
"next.config.cjs",
|
|
66
|
+
"next.config.ts",
|
|
67
|
+
];
|
|
68
|
+
for (const candidate of nextConfigCandidates) {
|
|
69
|
+
const configPath = join(directory, candidate);
|
|
70
|
+
if (await pathExists(configPath)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const packageJsonPath = join(directory, "package.json");
|
|
75
|
+
if (!(await pathExists(packageJsonPath))) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const raw = await readFile(packageJsonPath, "utf8");
|
|
80
|
+
const parsed = JSON.parse(raw);
|
|
81
|
+
if (!parsed || typeof parsed !== "object") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const maybePackage = parsed;
|
|
85
|
+
const hasNextDependency = Boolean((maybePackage.dependencies && typeof maybePackage.dependencies.next === "string") ||
|
|
86
|
+
(maybePackage.devDependencies && typeof maybePackage.devDependencies.next === "string"));
|
|
87
|
+
return hasNextDependency;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
package/dist/route-detectors.js
CHANGED
|
@@ -42,15 +42,43 @@ export async function detectRoutes(options) {
|
|
|
42
42
|
function createNextAppDetector() {
|
|
43
43
|
return {
|
|
44
44
|
id: SOURCE_NEXT_APP,
|
|
45
|
-
canDetect: async (options) =>
|
|
46
|
-
|
|
45
|
+
canDetect: async (options) => {
|
|
46
|
+
const roots = await findNextAppRoots(options.projectRoot);
|
|
47
|
+
return roots.length > 0;
|
|
48
|
+
},
|
|
49
|
+
detect: async (options) => {
|
|
50
|
+
const roots = await findNextAppRoots(options.projectRoot);
|
|
51
|
+
const allRoutes = [];
|
|
52
|
+
for (const root of roots) {
|
|
53
|
+
const routes = await detectAppRoutes(root, options.limit);
|
|
54
|
+
allRoutes.push(...routes);
|
|
55
|
+
if (allRoutes.length >= options.limit) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return allRoutes;
|
|
60
|
+
},
|
|
47
61
|
};
|
|
48
62
|
}
|
|
49
63
|
function createNextPagesDetector() {
|
|
50
64
|
return {
|
|
51
65
|
id: SOURCE_NEXT_PAGES,
|
|
52
|
-
canDetect: async (options) =>
|
|
53
|
-
|
|
66
|
+
canDetect: async (options) => {
|
|
67
|
+
const roots = await findNextPagesRoots(options.projectRoot);
|
|
68
|
+
return roots.length > 0;
|
|
69
|
+
},
|
|
70
|
+
detect: async (options) => {
|
|
71
|
+
const roots = await findNextPagesRoots(options.projectRoot);
|
|
72
|
+
const allRoutes = [];
|
|
73
|
+
for (const root of roots) {
|
|
74
|
+
const routes = await detectPagesRoutes(root, options.limit);
|
|
75
|
+
allRoutes.push(...routes);
|
|
76
|
+
if (allRoutes.length >= options.limit) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return allRoutes;
|
|
81
|
+
},
|
|
54
82
|
};
|
|
55
83
|
}
|
|
56
84
|
function createRemixRoutesDetector() {
|
|
@@ -67,6 +95,90 @@ function createSpaHtmlDetector() {
|
|
|
67
95
|
detect: async (options) => detectSpaRoutes(options.projectRoot, options.limit),
|
|
68
96
|
};
|
|
69
97
|
}
|
|
98
|
+
async function findNextAppRoots(projectRoot) {
|
|
99
|
+
const roots = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const addRoot = (candidate) => {
|
|
102
|
+
if (!seen.has(candidate)) {
|
|
103
|
+
seen.add(candidate);
|
|
104
|
+
roots.push(candidate);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const directCandidates = [
|
|
108
|
+
join(projectRoot, "app"),
|
|
109
|
+
join(projectRoot, "src", "app"),
|
|
110
|
+
];
|
|
111
|
+
for (const candidate of directCandidates) {
|
|
112
|
+
if (await pathExists(candidate)) {
|
|
113
|
+
addRoot(candidate);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const containers = ["apps", "packages"];
|
|
117
|
+
for (const container of containers) {
|
|
118
|
+
const containerPath = join(projectRoot, container);
|
|
119
|
+
if (!(await pathExists(containerPath))) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const entries = await readdir(containerPath, { withFileTypes: true });
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (!entry.isDirectory()) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const appRoot = join(containerPath, entry.name, "app");
|
|
128
|
+
const srcAppRoot = join(containerPath, entry.name, "src", "app");
|
|
129
|
+
if (await pathExists(appRoot)) {
|
|
130
|
+
addRoot(appRoot);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (await pathExists(srcAppRoot)) {
|
|
134
|
+
addRoot(srcAppRoot);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return roots;
|
|
139
|
+
}
|
|
140
|
+
async function findNextPagesRoots(projectRoot) {
|
|
141
|
+
const roots = [];
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
const addRoot = (candidate) => {
|
|
144
|
+
if (!seen.has(candidate)) {
|
|
145
|
+
seen.add(candidate);
|
|
146
|
+
roots.push(candidate);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const directCandidates = [
|
|
150
|
+
join(projectRoot, "pages"),
|
|
151
|
+
join(projectRoot, "src", "pages"),
|
|
152
|
+
];
|
|
153
|
+
for (const candidate of directCandidates) {
|
|
154
|
+
if (await pathExists(candidate)) {
|
|
155
|
+
addRoot(candidate);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const containers = ["apps", "packages"];
|
|
159
|
+
for (const container of containers) {
|
|
160
|
+
const containerPath = join(projectRoot, container);
|
|
161
|
+
if (!(await pathExists(containerPath))) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const entries = await readdir(containerPath, { withFileTypes: true });
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (!entry.isDirectory()) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const pagesRoot = join(containerPath, entry.name, "pages");
|
|
170
|
+
const srcPagesRoot = join(containerPath, entry.name, "src", "pages");
|
|
171
|
+
if (await pathExists(pagesRoot)) {
|
|
172
|
+
addRoot(pagesRoot);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (await pathExists(srcPagesRoot)) {
|
|
176
|
+
addRoot(srcPagesRoot);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return roots;
|
|
181
|
+
}
|
|
70
182
|
async function detectAppRoutes(appRoot, limit) {
|
|
71
183
|
const files = await collectRouteFiles(appRoot, limit, isAppPageFile);
|
|
72
184
|
return files.map((file) => buildRoute(file, appRoot, formatAppRoutePath, SOURCE_NEXT_APP));
|
package/dist/wizard-cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import prompts from "prompts";
|
|
4
4
|
import { detectRoutes } from "./route-detectors.js";
|
|
5
5
|
import { pathExists } from "./fs-utils.js";
|
|
6
|
+
import { discoverNextProjects } from "./project-discovery.js";
|
|
6
7
|
const PROFILE_TO_DETECTOR = {
|
|
7
8
|
"next-app": "next-app",
|
|
8
9
|
"next-pages": "next-pages",
|
|
@@ -194,18 +195,44 @@ async function maybeDetectPages(profile) {
|
|
|
194
195
|
}
|
|
195
196
|
const preferredDetector = await selectDetector(profile);
|
|
196
197
|
const projectRootAnswer = await ask(projectRootQuestion);
|
|
197
|
-
const
|
|
198
|
-
if (!(await pathExists(
|
|
199
|
-
console.log(`No project found at ${
|
|
198
|
+
const repoRoot = resolve(projectRootAnswer.projectRoot);
|
|
199
|
+
if (!(await pathExists(repoRoot))) {
|
|
200
|
+
console.log(`No project found at ${repoRoot}. Skipping auto-detection.`);
|
|
200
201
|
return [];
|
|
201
202
|
}
|
|
202
|
-
const
|
|
203
|
+
const detectionRoot = await chooseDetectionRoot({ profile, repoRoot });
|
|
204
|
+
const routes = await detectRoutes({ projectRoot: detectionRoot, preferredDetectorId: preferredDetector });
|
|
203
205
|
if (routes.length === 0) {
|
|
204
206
|
console.log("No routes detected. Add pages manually.");
|
|
205
207
|
return [];
|
|
206
208
|
}
|
|
207
209
|
return selectDetectedRoutes(routes);
|
|
208
210
|
}
|
|
211
|
+
async function chooseDetectionRoot({ profile, repoRoot, }) {
|
|
212
|
+
if (profile !== "next-app" && profile !== "next-pages") {
|
|
213
|
+
return repoRoot;
|
|
214
|
+
}
|
|
215
|
+
const projects = await discoverNextProjects({ repoRoot });
|
|
216
|
+
if (projects.length === 0) {
|
|
217
|
+
return repoRoot;
|
|
218
|
+
}
|
|
219
|
+
if (projects.length === 1) {
|
|
220
|
+
const onlyProject = projects[0];
|
|
221
|
+
console.log(`Detected Next.js app at ${onlyProject.root}.`);
|
|
222
|
+
return onlyProject.root;
|
|
223
|
+
}
|
|
224
|
+
const choices = projects.map((project) => ({
|
|
225
|
+
title: `${project.name} (${project.root})`,
|
|
226
|
+
value: project.root,
|
|
227
|
+
}));
|
|
228
|
+
const answer = await ask({
|
|
229
|
+
type: "select",
|
|
230
|
+
name: "projectRoot",
|
|
231
|
+
message: "Multiple Next.js apps found. Which one do you want to audit?",
|
|
232
|
+
choices,
|
|
233
|
+
});
|
|
234
|
+
return answer.projectRoot ?? repoRoot;
|
|
235
|
+
}
|
|
209
236
|
async function selectDetector(profile) {
|
|
210
237
|
const preset = PROFILE_TO_DETECTOR[profile];
|
|
211
238
|
if (preset) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apex-auditor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "CLI to run structured Lighthouse audits (Performance, Accessibility, Best Practices, SEO) across routes.",
|
|
6
6
|
"type": "module",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"chrome-launcher": "^
|
|
22
|
-
"lighthouse": "^
|
|
21
|
+
"chrome-launcher": "^1.2.1",
|
|
22
|
+
"lighthouse": "^13.0.1",
|
|
23
23
|
"prompts": "^2.4.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
@@ -27,6 +27,6 @@
|
|
|
27
27
|
"typescript": "^5.9.3",
|
|
28
28
|
"@types/node": "^22.0.0",
|
|
29
29
|
"@types/prompts": "^2.4.9",
|
|
30
|
-
"vitest": "^
|
|
30
|
+
"vitest": "^4.0.14"
|
|
31
31
|
}
|
|
32
32
|
}
|