eaa-kit 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/astro/index.d.ts +7 -34
- package/dist/astro/index.js +6 -26
- package/dist/audit/runners/worker.js +1 -1
- package/dist/audit-CuG2hYyo.js +2 -0
- package/dist/audit-DcL73mOC.js +635 -0
- package/dist/{baseline-CV_3lbER.js → baseline-CuKFq4IF.js} +1 -1
- package/dist/{baseline-CgBmzFTr.js → baseline-s9F3fXTN.js} +16 -26
- package/dist/cli/index.js +80 -143
- package/dist/collect-BkAQ0viT.js +123 -0
- package/dist/command-Dxpa00Ha.js +77 -0
- package/dist/component-7kEBjv_y.js +111 -0
- package/dist/{crawl-CtJbMNNb.js → crawl-Oxt2Gaqo.js} +13 -23
- package/dist/frameworks-BYa3tULg.js +243 -0
- package/dist/frameworks-DaDqrJOw.js +2 -0
- package/dist/fs-BmPtmFke.js +40 -0
- package/dist/{html-BLEuzep6.js → html-DKiI_3gs.js} +167 -86
- package/dist/{impact-DvgBjupx.js → impact-YdoOtFqm.js} +15 -1
- package/dist/index.js +2 -2
- package/dist/{init-DRKIdpK1.js → init-DiLiYvN1.js} +13 -10
- package/dist/{jsdom-BEu6Ra_2.js → jsdom-BjpF-2V-.js} +2 -7
- package/dist/jsdom-X4KYfTp8.js +3 -0
- package/dist/json-B0Y7rNjt.js +2 -0
- package/dist/{json-C9xS1PNC.js → json-Cnv9nd6U.js} +13 -21
- package/dist/{load-sCkKsvGQ.js → load-UYXLqGV9.js} +2 -8
- package/dist/manual-Vz-oX1I_.js +239 -0
- package/dist/{playwright-DSRnXmcd.js → playwright-DYFsGUNd.js} +70 -23
- package/dist/{pool-DixLeu8L.js → pool-BWkWZiJW.js} +4 -12
- package/dist/{project-CufCqIE2.js → project-DW08TseF.js} +8 -21
- package/dist/{render-BO0nVrrZ.js → render-DI_aCnAZ.js} +7 -15
- package/dist/{result-2aZPfM8w.js → result-DLxd2Eip.js} +38 -1
- package/dist/{routes-BxbSZKXC.js → routes-C2Cgf6Ko.js} +4 -8
- package/dist/run-BMASMmwO.d.ts +45 -0
- package/dist/run-BW6CVuND.js +36 -0
- package/dist/{sarif-eSCuI0eX.js → sarif-DB3WG7T9.js} +11 -33
- package/dist/text-BFmNtMsV.js +43 -0
- package/dist/vite/index.d.ts +39 -0
- package/dist/vite/index.js +40 -0
- package/package.json +8 -3
- package/dist/audit-B2dIKpJ5.js +0 -2
- package/dist/audit-DXpKkXsC.js +0 -950
- package/dist/escape-Dm1o_RAk.js +0 -21
- package/dist/jsdom-C6dIyaxN.js +0 -3
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { i as toPosix } from "./fs-BmPtmFke.js";
|
|
2
|
+
import { t as collapse } from "./text-BFmNtMsV.js";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { glob } from "tinyglobby";
|
|
6
|
+
//#region src/audit/component.ts
|
|
7
|
+
/**
|
|
8
|
+
* Which source file a failing element was written in.
|
|
9
|
+
*
|
|
10
|
+
* Route mapping names the page, and on a site built from components the page is
|
|
11
|
+
* not where the fix goes: a header with a missing `alt` appears on every page
|
|
12
|
+
* that renders it and is written in none of them. The Issues view can already
|
|
13
|
+
* tell that one element is shared across pages; this says which file to open.
|
|
14
|
+
*
|
|
15
|
+
* The method is deliberately dumb. Take a literal out of the failing markup —
|
|
16
|
+
* an image path, a link target, an id — and look for it in the project's own
|
|
17
|
+
* source. Frameworks do emit source positions, but only in development builds,
|
|
18
|
+
* and an auditor runs against production output. A literal survives every
|
|
19
|
+
* compiler.
|
|
20
|
+
*
|
|
21
|
+
* It never guesses. A literal found in two files names neither, because a wrong
|
|
22
|
+
* file is worse than none: it sends somebody to edit code that was not the
|
|
23
|
+
* cause, and costs more than the minute it saved.
|
|
24
|
+
*/
|
|
25
|
+
/** Where component source is expected to live. */
|
|
26
|
+
const SOURCE_GLOBS = ["**/*.{tsx,jsx,ts,js,mjs,vue,svelte,astro,mdx,md,html,php,erb,twig,liquid,hbs}"];
|
|
27
|
+
/** Never source: dependencies, build output, version control. */
|
|
28
|
+
const NEVER = [
|
|
29
|
+
"**/node_modules/**",
|
|
30
|
+
"**/.git/**",
|
|
31
|
+
"**/dist/**",
|
|
32
|
+
"**/build/**",
|
|
33
|
+
"**/out/**",
|
|
34
|
+
"**/_site/**",
|
|
35
|
+
"**/.next/**",
|
|
36
|
+
"**/.nuxt/**",
|
|
37
|
+
"**/.svelte-kit/**",
|
|
38
|
+
"**/.output/**",
|
|
39
|
+
"**/coverage/**",
|
|
40
|
+
"**/*.min.js"
|
|
41
|
+
];
|
|
42
|
+
/** Files read into the index. A ceiling, not a target. */
|
|
43
|
+
const MAX_FILES = 2e3;
|
|
44
|
+
/** Bytes per file. A source file past this is generated or vendored. */
|
|
45
|
+
const MAX_BYTES = 524288;
|
|
46
|
+
/**
|
|
47
|
+
* Read the project's source once, so every element can be looked up against it.
|
|
48
|
+
*
|
|
49
|
+
* Once rather than per element: a site with forty violations would otherwise
|
|
50
|
+
* walk the tree forty times for an answer that does not change.
|
|
51
|
+
*/
|
|
52
|
+
async function buildComponentIndex(cwd) {
|
|
53
|
+
const found = await glob([...SOURCE_GLOBS], {
|
|
54
|
+
cwd,
|
|
55
|
+
ignore: [...NEVER],
|
|
56
|
+
onlyFiles: true,
|
|
57
|
+
dot: false
|
|
58
|
+
});
|
|
59
|
+
const files = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const relative of found.slice(0, MAX_FILES).sort()) try {
|
|
61
|
+
const source = await readFile(path.resolve(cwd, relative), "utf8");
|
|
62
|
+
if (source.length <= MAX_BYTES) files.set(toPosix(relative), source);
|
|
63
|
+
} catch {}
|
|
64
|
+
return { files };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Literals worth searching for, most distinctive first.
|
|
68
|
+
*
|
|
69
|
+
* An image path or a link target is written by hand and survives compilation.
|
|
70
|
+
* A class name may be generated, and text content may be interpolated, so both
|
|
71
|
+
* come after. Anything short is dropped: `/` appears in every file.
|
|
72
|
+
*/
|
|
73
|
+
function searchTermsFor(html) {
|
|
74
|
+
const terms = [];
|
|
75
|
+
const add = (value) => {
|
|
76
|
+
if (value === void 0) return;
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if (trimmed.length < 4 || trimmed.includes("${")) return;
|
|
79
|
+
if (!terms.includes(trimmed)) terms.push(trimmed);
|
|
80
|
+
};
|
|
81
|
+
for (const attribute of [
|
|
82
|
+
"src",
|
|
83
|
+
"href",
|
|
84
|
+
"id",
|
|
85
|
+
"data-testid",
|
|
86
|
+
"name",
|
|
87
|
+
"action"
|
|
88
|
+
]) add(new RegExp(`\\s${attribute}\\s*=\\s*["']([^"']+)["']`, "i").exec(html)?.[1]);
|
|
89
|
+
const text = collapse(html.replace(/<[^>]*>/g, " "));
|
|
90
|
+
if (text.length >= 8) add(text.slice(0, 60));
|
|
91
|
+
return terms;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The one source file this element was written in, if exactly one claims it.
|
|
95
|
+
*
|
|
96
|
+
* Returns undefined when nothing matches and when more than one does. The
|
|
97
|
+
* second case is the important one: naming a file that merely happens to
|
|
98
|
+
* contain the same path sends somebody to edit the wrong component.
|
|
99
|
+
*/
|
|
100
|
+
function componentFor(index, html) {
|
|
101
|
+
for (const term of searchTermsFor(html)) {
|
|
102
|
+
const matches = [];
|
|
103
|
+
for (const [file, source] of index.files) if (source.includes(term)) {
|
|
104
|
+
matches.push(file);
|
|
105
|
+
if (matches.length > 1) break;
|
|
106
|
+
}
|
|
107
|
+
if (matches.length === 1) return matches[0];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { buildComponentIndex, componentFor };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { i as stripBom } from "./collect-BkAQ0viT.js";
|
|
1
2
|
/** Requests in flight at once. Politeness, not throughput. */
|
|
2
3
|
const REQUEST_CONCURRENCY = 4;
|
|
3
4
|
var CrawlError = class extends Error {
|
|
@@ -142,31 +143,18 @@ function disallowedPaths(robots) {
|
|
|
142
143
|
return paths;
|
|
143
144
|
}
|
|
144
145
|
/**
|
|
145
|
-
*
|
|
146
|
+
* One of the two files a site publishes about itself, or undefined.
|
|
146
147
|
*
|
|
147
148
|
* Read directly rather than through fetchPage, which correctly refuses anything
|
|
148
|
-
* that is not a page. A site
|
|
149
|
+
* that is not a page. A site that does not publish one is the ordinary case,
|
|
150
|
+
* not a failure, so it comes back empty either way.
|
|
149
151
|
*/
|
|
150
|
-
async function
|
|
152
|
+
async function fetchSiteFile(entry, impl, name) {
|
|
151
153
|
try {
|
|
152
|
-
const response = await impl(new URL(
|
|
153
|
-
return response.ok ?
|
|
154
|
+
const response = await impl(new URL(name, entry).href, { redirect: "follow" });
|
|
155
|
+
return response.ok ? await response.text() : void 0;
|
|
154
156
|
} catch {
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Page URLs the site lists for itself.
|
|
160
|
-
*
|
|
161
|
-
* Worth one request: a sitemap finds pages nothing links to, which link
|
|
162
|
-
* following alone never reaches.
|
|
163
|
-
*/
|
|
164
|
-
async function sitemapUrls(entry, impl) {
|
|
165
|
-
try {
|
|
166
|
-
const response = await impl(new URL("/sitemap.xml", entry).href, { redirect: "follow" });
|
|
167
|
-
return response.ok ? urlsFromSitemap(await response.text(), entry) : [];
|
|
168
|
-
} catch {
|
|
169
|
-
return [];
|
|
157
|
+
return;
|
|
170
158
|
}
|
|
171
159
|
}
|
|
172
160
|
/**
|
|
@@ -182,7 +170,8 @@ async function crawlSite(entry, options = {}) {
|
|
|
182
170
|
const maxDepth = options.maxDepth ?? 3;
|
|
183
171
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
184
172
|
const failures = [];
|
|
185
|
-
const
|
|
173
|
+
const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt");
|
|
174
|
+
const blocked = robots === void 0 ? [] : disallowedPaths(robots);
|
|
186
175
|
const allowed = (url) => !blocked.some((path) => url.pathname.startsWith(path));
|
|
187
176
|
let discovery = "links";
|
|
188
177
|
const queue = [];
|
|
@@ -195,7 +184,8 @@ async function crawlSite(entry, options = {}) {
|
|
|
195
184
|
depth
|
|
196
185
|
});
|
|
197
186
|
};
|
|
198
|
-
const
|
|
187
|
+
const sitemap = await fetchSiteFile(entry, impl, "/sitemap.xml");
|
|
188
|
+
const listed = sitemap === void 0 ? [] : urlsFromSitemap(sitemap, entry);
|
|
199
189
|
if (listed.length > 0) {
|
|
200
190
|
discovery = "sitemap";
|
|
201
191
|
for (const url of listed) enqueue(url, 0);
|
|
@@ -220,7 +210,7 @@ async function crawlSite(entry, options = {}) {
|
|
|
220
210
|
pages.push({
|
|
221
211
|
absolutePath: url.href,
|
|
222
212
|
relativePath: pageIdentity(url),
|
|
223
|
-
html:
|
|
213
|
+
html: stripBom(html)
|
|
224
214
|
});
|
|
225
215
|
options.onProgress?.(pages.length, queue.length);
|
|
226
216
|
if (item.depth < maxDepth) for (const link of linksFrom(html, url)) enqueue(link, item.depth + 1);
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { t as exists } from "./fs-BmPtmFke.js";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
//#region src/audit/frameworks.ts
|
|
5
|
+
/**
|
|
6
|
+
* Most specific first. Several of these depend on Vite, so Vite is last: a
|
|
7
|
+
* SvelteKit project is a SvelteKit project, not a Vite one.
|
|
8
|
+
*
|
|
9
|
+
* Several carry `files` as well as `packages` even though they are npm
|
|
10
|
+
* packages. A config file named after the framework is as good an identifier as
|
|
11
|
+
* the dependency, and it still works where package.json is missing, unreadable,
|
|
12
|
+
* or belongs to a workspace root rather than the project in front of us.
|
|
13
|
+
*/
|
|
14
|
+
const FRAMEWORKS = [
|
|
15
|
+
{
|
|
16
|
+
id: "next",
|
|
17
|
+
name: "Next.js",
|
|
18
|
+
packages: ["next"],
|
|
19
|
+
files: [
|
|
20
|
+
"next.config.js",
|
|
21
|
+
"next.config.mjs",
|
|
22
|
+
"next.config.ts"
|
|
23
|
+
],
|
|
24
|
+
outputs: ["out"],
|
|
25
|
+
configs: [
|
|
26
|
+
"next.config.js",
|
|
27
|
+
"next.config.mjs",
|
|
28
|
+
"next.config.ts"
|
|
29
|
+
],
|
|
30
|
+
outputPattern: /distDir\s*:\s*['"`]([^'"`]+)['"`]/,
|
|
31
|
+
staticOutput: {
|
|
32
|
+
needs: "output: 'export'",
|
|
33
|
+
how: "add output: 'export' to your Next config, then build"
|
|
34
|
+
},
|
|
35
|
+
serves: true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "nuxt",
|
|
39
|
+
name: "Nuxt",
|
|
40
|
+
packages: ["nuxt"],
|
|
41
|
+
files: ["nuxt.config.ts", "nuxt.config.js"],
|
|
42
|
+
outputs: [".output/public", "dist"],
|
|
43
|
+
staticOutput: {
|
|
44
|
+
needs: "nuxt generate",
|
|
45
|
+
how: "run nuxt generate rather than nuxt build"
|
|
46
|
+
},
|
|
47
|
+
serves: true
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "sveltekit",
|
|
51
|
+
name: "SvelteKit",
|
|
52
|
+
packages: ["@sveltejs/kit"],
|
|
53
|
+
files: ["svelte.config.js"],
|
|
54
|
+
outputs: ["build", ".svelte-kit/output/prerendered/pages"],
|
|
55
|
+
staticOutput: {
|
|
56
|
+
needs: "@sveltejs/adapter-static",
|
|
57
|
+
how: "use @sveltejs/adapter-static, then build"
|
|
58
|
+
},
|
|
59
|
+
serves: true
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "remix",
|
|
63
|
+
name: "React Router / Remix",
|
|
64
|
+
packages: [
|
|
65
|
+
"@remix-run/react",
|
|
66
|
+
"react-router",
|
|
67
|
+
"@react-router/dev"
|
|
68
|
+
],
|
|
69
|
+
outputs: ["build/client"],
|
|
70
|
+
serves: true
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "astro",
|
|
74
|
+
name: "Astro",
|
|
75
|
+
packages: ["astro"],
|
|
76
|
+
files: [
|
|
77
|
+
"astro.config.mjs",
|
|
78
|
+
"astro.config.js",
|
|
79
|
+
"astro.config.ts"
|
|
80
|
+
],
|
|
81
|
+
outputs: ["dist"],
|
|
82
|
+
configs: [
|
|
83
|
+
"astro.config.mjs",
|
|
84
|
+
"astro.config.js",
|
|
85
|
+
"astro.config.ts"
|
|
86
|
+
],
|
|
87
|
+
outputPattern: /outDir\s*:\s*['"`]([^'"`]+)['"`]/,
|
|
88
|
+
serves: true
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "gatsby",
|
|
92
|
+
name: "Gatsby",
|
|
93
|
+
packages: ["gatsby"],
|
|
94
|
+
outputs: ["public"],
|
|
95
|
+
serves: false
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "docusaurus",
|
|
99
|
+
name: "Docusaurus",
|
|
100
|
+
packages: ["@docusaurus/core"],
|
|
101
|
+
outputs: ["build"],
|
|
102
|
+
serves: false
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "vitepress",
|
|
106
|
+
name: "VitePress",
|
|
107
|
+
packages: ["vitepress"],
|
|
108
|
+
files: [".vitepress/config.ts", "docs/.vitepress/config.ts"],
|
|
109
|
+
outputs: [".vitepress/dist", "docs/.vitepress/dist"],
|
|
110
|
+
serves: false
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "eleventy",
|
|
114
|
+
name: "Eleventy",
|
|
115
|
+
packages: ["@11ty/eleventy"],
|
|
116
|
+
outputs: ["_site"],
|
|
117
|
+
configs: [
|
|
118
|
+
".eleventy.js",
|
|
119
|
+
"eleventy.config.js",
|
|
120
|
+
"eleventy.config.mjs"
|
|
121
|
+
],
|
|
122
|
+
outputPattern: /output\s*:\s*['"`]([^'"`]+)['"`]/,
|
|
123
|
+
serves: false
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "angular",
|
|
127
|
+
name: "Angular",
|
|
128
|
+
packages: ["@angular/core"],
|
|
129
|
+
outputs: ["dist"],
|
|
130
|
+
serves: false
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "cra",
|
|
134
|
+
name: "Create React App",
|
|
135
|
+
packages: ["react-scripts"],
|
|
136
|
+
outputs: ["build"],
|
|
137
|
+
serves: false
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "hugo",
|
|
141
|
+
name: "Hugo",
|
|
142
|
+
packages: [],
|
|
143
|
+
files: [
|
|
144
|
+
"hugo.toml",
|
|
145
|
+
"hugo.yaml",
|
|
146
|
+
"config.toml"
|
|
147
|
+
],
|
|
148
|
+
outputs: ["public"],
|
|
149
|
+
serves: false
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "jekyll",
|
|
153
|
+
name: "Jekyll",
|
|
154
|
+
packages: [],
|
|
155
|
+
files: ["_config.yml"],
|
|
156
|
+
outputs: ["_site"],
|
|
157
|
+
serves: false
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "vite",
|
|
161
|
+
name: "Vite",
|
|
162
|
+
packages: ["vite"],
|
|
163
|
+
outputs: ["dist"],
|
|
164
|
+
configs: [
|
|
165
|
+
"vite.config.ts",
|
|
166
|
+
"vite.config.js",
|
|
167
|
+
"vite.config.mjs"
|
|
168
|
+
],
|
|
169
|
+
outputPattern: /outDir\s*:\s*['"`]([^'"`]+)['"`]/,
|
|
170
|
+
serves: true
|
|
171
|
+
}
|
|
172
|
+
];
|
|
173
|
+
/** Output directories to try when nothing was recognised. */
|
|
174
|
+
const FALLBACK_OUTPUTS = [
|
|
175
|
+
"dist",
|
|
176
|
+
"out",
|
|
177
|
+
"build",
|
|
178
|
+
"_site",
|
|
179
|
+
"public",
|
|
180
|
+
".output/public"
|
|
181
|
+
];
|
|
182
|
+
/**
|
|
183
|
+
* The framework this project uses, if it is one this knows.
|
|
184
|
+
*
|
|
185
|
+
* Dependencies decide it where there are any, because a package.json states
|
|
186
|
+
* what a project is far more reliably than a file lying in the root. The
|
|
187
|
+
* file-based entries exist for Hugo and Jekyll, which have no package.json to
|
|
188
|
+
* read.
|
|
189
|
+
*/
|
|
190
|
+
async function detectFramework(cwd, pkg) {
|
|
191
|
+
const deps = {
|
|
192
|
+
...pkg?.dependencies,
|
|
193
|
+
...pkg?.devDependencies
|
|
194
|
+
};
|
|
195
|
+
for (const framework of FRAMEWORKS) {
|
|
196
|
+
const byPackage = framework.packages.some((name) => deps[name] !== void 0);
|
|
197
|
+
let byFile = false;
|
|
198
|
+
if (!byPackage && framework.files !== void 0) {
|
|
199
|
+
for (const file of framework.files) if (await exists(file, cwd)) {
|
|
200
|
+
byFile = true;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!byPackage && !byFile) continue;
|
|
205
|
+
const configured = await outputFromConfig(cwd, framework);
|
|
206
|
+
const outputs = configured === void 0 ? [...framework.outputs] : [configured, ...framework.outputs];
|
|
207
|
+
return {
|
|
208
|
+
framework,
|
|
209
|
+
outputs: [...new Set(outputs)]
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* A custom output directory, read out of the framework's config file.
|
|
215
|
+
*
|
|
216
|
+
* Read with a pattern rather than executed. A config file is code, and this runs
|
|
217
|
+
* before anything has decided the project is worth trusting; a regex that
|
|
218
|
+
* misses a computed value is a directory not found, which the caller already
|
|
219
|
+
* handles, while running the file to find out is a different class of risk
|
|
220
|
+
* entirely.
|
|
221
|
+
*/
|
|
222
|
+
async function outputFromConfig(cwd, framework) {
|
|
223
|
+
if (framework.configs === void 0 || framework.outputPattern === void 0) return void 0;
|
|
224
|
+
for (const name of framework.configs) {
|
|
225
|
+
let source;
|
|
226
|
+
try {
|
|
227
|
+
source = await readFile(path.resolve(cwd, name), "utf8");
|
|
228
|
+
} catch {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const value = framework.outputPattern.exec(source)?.[1];
|
|
232
|
+
if (value === void 0 || value === "") continue;
|
|
233
|
+
if (path.isAbsolute(value)) continue;
|
|
234
|
+
return value.replace(/^\.\//, "");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Every output directory worth trying, framework-aware, most likely first. */
|
|
238
|
+
async function candidateOutputs(cwd, pkg) {
|
|
239
|
+
const detected = await detectFramework(cwd, pkg);
|
|
240
|
+
return [.../* @__PURE__ */ new Set([...detected?.outputs ?? [], ...FALLBACK_OUTPUTS])];
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
export { outputFromConfig as a, detectFramework as i, FRAMEWORKS as n, candidateOutputs as r, FALLBACK_OUTPUTS as t };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/fs.ts
|
|
4
|
+
/**
|
|
5
|
+
* Asking the filesystem a yes-or-no question.
|
|
6
|
+
*
|
|
7
|
+
* Six modules had their own copy of the same try/stat/catch, each answering a
|
|
8
|
+
* slightly different question and each getting it right by hand. A path that
|
|
9
|
+
* cannot be stat'd is not a path of the kind asked about, which is the only
|
|
10
|
+
* behaviour any caller here wants from an unreadable directory.
|
|
11
|
+
*/
|
|
12
|
+
/** Whether anything is there, resolved against `root` when one is given. */
|
|
13
|
+
async function exists(target, root) {
|
|
14
|
+
try {
|
|
15
|
+
await stat(root === void 0 ? target : path.resolve(root, target));
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function isFile(target) {
|
|
22
|
+
try {
|
|
23
|
+
return (await stat(target)).isFile();
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function isDirectory(target) {
|
|
29
|
+
try {
|
|
30
|
+
return (await stat(target)).isDirectory();
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Platform-native separators to POSIX, so paths in reports match everywhere. */
|
|
36
|
+
function toPosix(filePath) {
|
|
37
|
+
return filePath.split(path.sep).join("/");
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { toPosix as i, isDirectory as n, isFile as r, exists as t };
|