no-mistakes 0.11.1 → 0.12.1
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/package.json +1 -1
- package/report-types.d.ts +67 -0
- package/scripts/install/assets.js +22 -1
- package/scripts/install/download.js +41 -12
- package/scripts/install/installer.js +89 -4
package/package.json
CHANGED
package/report-types.d.ts
CHANGED
|
@@ -49,6 +49,73 @@ export interface ServerRoutesReport {
|
|
|
49
49
|
diagnostics: unknown[];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export type FetchSourceType =
|
|
53
|
+
| "page"
|
|
54
|
+
| "layout"
|
|
55
|
+
| "loading"
|
|
56
|
+
| "error"
|
|
57
|
+
| "template"
|
|
58
|
+
| "route"
|
|
59
|
+
| "module";
|
|
60
|
+
|
|
61
|
+
export type CacheKind =
|
|
62
|
+
| "none"
|
|
63
|
+
| "fetch-cache"
|
|
64
|
+
| "fetch-next-revalidate"
|
|
65
|
+
| "fetch-next-tags"
|
|
66
|
+
| "react-cache"
|
|
67
|
+
| "cache"
|
|
68
|
+
| "unstable-cache";
|
|
69
|
+
|
|
70
|
+
export interface FetchOccurrence {
|
|
71
|
+
path: string;
|
|
72
|
+
rawPath: string;
|
|
73
|
+
method: string;
|
|
74
|
+
file: string;
|
|
75
|
+
line: number;
|
|
76
|
+
side: "server" | "client";
|
|
77
|
+
rsc: boolean;
|
|
78
|
+
cached: boolean;
|
|
79
|
+
cacheKind: CacheKind;
|
|
80
|
+
cachedFunction?: string;
|
|
81
|
+
dynamic: boolean;
|
|
82
|
+
unsupported: boolean;
|
|
83
|
+
functionName?: string;
|
|
84
|
+
conditional: boolean;
|
|
85
|
+
inPromiseAll: boolean;
|
|
86
|
+
errorHandled: boolean;
|
|
87
|
+
sourceType: FetchSourceType;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface FetchRouteReport {
|
|
91
|
+
route: string;
|
|
92
|
+
file: string;
|
|
93
|
+
apiCalls: FetchOccurrence[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface FetchSummary {
|
|
97
|
+
totalRoutes: number;
|
|
98
|
+
routesWithApiCalls: number;
|
|
99
|
+
totalApiCalls: number;
|
|
100
|
+
uniqueApiCalls: number;
|
|
101
|
+
duplicateApiCalls: number;
|
|
102
|
+
dynamicApiCalls: number;
|
|
103
|
+
cachedApiCalls: number;
|
|
104
|
+
clientApiCalls: number;
|
|
105
|
+
serverApiCalls: number;
|
|
106
|
+
rscApiCalls: number;
|
|
107
|
+
conditionalApiCalls: number;
|
|
108
|
+
parallelApiCalls: number;
|
|
109
|
+
errorHandledApiCalls: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface FetchReport {
|
|
113
|
+
summary: FetchSummary;
|
|
114
|
+
routes: FetchRouteReport[];
|
|
115
|
+
duplicates: unknown[];
|
|
116
|
+
unsupported: unknown[];
|
|
117
|
+
}
|
|
118
|
+
|
|
52
119
|
export interface ReactComponentFacts {
|
|
53
120
|
name: string;
|
|
54
121
|
file: string;
|
|
@@ -2,7 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
const { basename } = require("node:path");
|
|
4
4
|
|
|
5
|
-
function assetName(
|
|
5
|
+
function assetName(binNameOrOptions, version, target, assetExtension) {
|
|
6
|
+
if (
|
|
7
|
+
typeof binNameOrOptions === "object" &&
|
|
8
|
+
binNameOrOptions !== null &&
|
|
9
|
+
!Array.isArray(binNameOrOptions)
|
|
10
|
+
) {
|
|
11
|
+
return assetNameFromOptions(binNameOrOptions);
|
|
12
|
+
}
|
|
13
|
+
return assetNameFromLegacyArgs(binNameOrOptions, version, target, assetExtension);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function assetNameFromLegacyArgs(binName, version, target, assetExtension) {
|
|
17
|
+
return assetNameFromOptions({ binName, version, target, assetExtension });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assetNameFromOptions({ binName, version, target, assetExtension }) {
|
|
21
|
+
if (typeof binName !== "string" || typeof version !== "string" || typeof target !== "string") {
|
|
22
|
+
throw new TypeError(
|
|
23
|
+
"assetName requires (binName, version, target) or an equivalent options object.",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
6
27
|
const ext = assetExtension ?? (target.endsWith("windows-msvc") ? ".exe" : "");
|
|
7
28
|
return `${binName}-v${version}-${target}${ext}`;
|
|
8
29
|
}
|
|
@@ -9,8 +9,9 @@ const { fileURLToPath } = require("node:url");
|
|
|
9
9
|
|
|
10
10
|
const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
11
11
|
|
|
12
|
-
function download(url, destination, redirects = 0) {
|
|
13
|
-
|
|
12
|
+
function download(url, destination, redirects = 0, validateUrl = () => {}) {
|
|
13
|
+
validateUrl(url);
|
|
14
|
+
if (isFileUrl(url)) {
|
|
14
15
|
return copyFile(fileURLToPath(url), destination);
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -20,6 +21,9 @@ function download(url, destination, redirects = 0) {
|
|
|
20
21
|
await pipeline(response, createWriteStream(destination));
|
|
21
22
|
},
|
|
22
23
|
redirects,
|
|
24
|
+
{ http, https },
|
|
25
|
+
DOWNLOAD_TIMEOUT_MS,
|
|
26
|
+
validateUrl,
|
|
23
27
|
);
|
|
24
28
|
}
|
|
25
29
|
|
|
@@ -29,8 +33,15 @@ function request(
|
|
|
29
33
|
redirects = 0,
|
|
30
34
|
clients = { http, https },
|
|
31
35
|
timeoutMs = DOWNLOAD_TIMEOUT_MS,
|
|
36
|
+
validateUrl = () => {},
|
|
32
37
|
) {
|
|
33
38
|
return new Promise((resolve, reject) => {
|
|
39
|
+
try {
|
|
40
|
+
validateUrl(url);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
reject(error);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
34
45
|
const client = url.startsWith("http://") ? clients.http : clients.https;
|
|
35
46
|
const req = client.get(url, (response) => {
|
|
36
47
|
if (isRedirectStatus(response.statusCode)) {
|
|
@@ -49,6 +60,7 @@ function request(
|
|
|
49
60
|
redirects + 1,
|
|
50
61
|
clients,
|
|
51
62
|
timeoutMs,
|
|
63
|
+
validateUrl,
|
|
52
64
|
).then(resolve, reject);
|
|
53
65
|
return;
|
|
54
66
|
}
|
|
@@ -73,25 +85,42 @@ function isRedirectStatus(statusCode) {
|
|
|
73
85
|
return [301, 302, 303, 307, 308].includes(statusCode);
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
async function fetchText(url) {
|
|
77
|
-
|
|
88
|
+
async function fetchText(url, validateUrl = () => {}) {
|
|
89
|
+
validateUrl(url);
|
|
90
|
+
|
|
91
|
+
if (isFileUrl(url)) {
|
|
78
92
|
return readFile(fileURLToPath(url), "utf8");
|
|
79
93
|
}
|
|
80
94
|
const chunks = [];
|
|
81
95
|
let totalLength = 0;
|
|
82
96
|
const MAX_LENGTH = 1024 * 1024;
|
|
83
|
-
await request(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
await request(
|
|
98
|
+
url,
|
|
99
|
+
async (response) => {
|
|
100
|
+
for await (const chunk of response) {
|
|
101
|
+
totalLength += chunk.length;
|
|
102
|
+
if (totalLength > MAX_LENGTH) {
|
|
103
|
+
throw new Error(`Response exceeded maximum size of ${MAX_LENGTH} bytes`);
|
|
104
|
+
}
|
|
105
|
+
chunks.push(chunk);
|
|
88
106
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
},
|
|
108
|
+
0,
|
|
109
|
+
{ http, https },
|
|
110
|
+
DOWNLOAD_TIMEOUT_MS,
|
|
111
|
+
validateUrl,
|
|
112
|
+
);
|
|
92
113
|
return Buffer.concat(chunks).toString("utf8");
|
|
93
114
|
}
|
|
94
115
|
|
|
116
|
+
function isFileUrl(url) {
|
|
117
|
+
try {
|
|
118
|
+
return new URL(url).protocol === "file:";
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
95
124
|
module.exports = {
|
|
96
125
|
download,
|
|
97
126
|
fetchText,
|
|
@@ -52,19 +52,25 @@ async function install(binName, repository, options = {}) {
|
|
|
52
52
|
return destination;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const asset = assetName(binName, version, target, options.assetExtension);
|
|
56
|
-
const baseUrl =
|
|
55
|
+
const asset = assetName({ binName, version, target, assetExtension: options.assetExtension });
|
|
56
|
+
const baseUrl = normalizeBaseUrl(
|
|
57
|
+
options.baseUrl || releaseBaseUrl(repository, version, options.envVar),
|
|
58
|
+
);
|
|
59
|
+
validateReleaseBaseUrl(baseUrl, repository, { enforcePath: true });
|
|
60
|
+
const validateReleaseDownloadUrl = (url) =>
|
|
61
|
+
validateReleaseBaseUrl(url, repository, { enforcePath: false });
|
|
62
|
+
|
|
57
63
|
const temp = `${destination}.tmp-${process.pid}`;
|
|
58
64
|
|
|
59
65
|
await mkdir(vendorDir, { recursive: true });
|
|
60
66
|
|
|
61
67
|
try {
|
|
62
68
|
console.log(`Downloading ${binName} v${version} for ${target}...`);
|
|
63
|
-
await download(`${baseUrl}/${asset}`, temp);
|
|
69
|
+
await download(`${baseUrl}/${asset}`, temp, 0, validateReleaseDownloadUrl);
|
|
64
70
|
|
|
65
71
|
let checksumText;
|
|
66
72
|
try {
|
|
67
|
-
checksumText = await fetchText(`${baseUrl}/${asset}.sha256
|
|
73
|
+
checksumText = await fetchText(`${baseUrl}/${asset}.sha256`, validateReleaseDownloadUrl);
|
|
68
74
|
} catch (e) {
|
|
69
75
|
throw new Error(`Failed to fetch checksum for ${asset}: ${e.message}`);
|
|
70
76
|
}
|
|
@@ -85,6 +91,84 @@ async function install(binName, repository, options = {}) {
|
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
function normalizeBaseUrl(baseUrl) {
|
|
95
|
+
const url = String(baseUrl);
|
|
96
|
+
if (url.endsWith("://")) {
|
|
97
|
+
return url;
|
|
98
|
+
}
|
|
99
|
+
return url.replace(/\/+$/, "");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateReleaseBaseUrl(baseUrl, repository, options = {}) {
|
|
103
|
+
const enforcePath = options.enforcePath ?? true;
|
|
104
|
+
const allowedPublicHost = "github.com";
|
|
105
|
+
const allowedRedirectHostSuffixes = ["githubusercontent.com"];
|
|
106
|
+
const allowedLocalHosts = ["127.0.0.1", "example.test"];
|
|
107
|
+
const publicPathPrefix = `/${repository.toLowerCase()}/releases/download`;
|
|
108
|
+
|
|
109
|
+
let parsedUrl;
|
|
110
|
+
try {
|
|
111
|
+
parsedUrl = new URL(baseUrl);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`Invalid release base URL: ${baseUrl}. It must be a valid absolute URL.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (parsedUrl.protocol === "file:") {
|
|
117
|
+
if (
|
|
118
|
+
parsedUrl.username ||
|
|
119
|
+
parsedUrl.password ||
|
|
120
|
+
parsedUrl.pathname.startsWith("//") ||
|
|
121
|
+
!String(baseUrl).toLowerCase().startsWith("file:///")
|
|
122
|
+
) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Untrusted base URL: ${baseUrl}. File URLs must use canonical 'file:///path/to/asset' form and must not include credentials.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (parsedUrl.username || parsedUrl.password) {
|
|
130
|
+
throw new Error(`Untrusted base URL: ${baseUrl}. Credentials are not allowed in URLs.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
134
|
+
const isLocalHost = allowedLocalHosts.includes(hostname);
|
|
135
|
+
if (isLocalHost) {
|
|
136
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
137
|
+
throw new Error(`Untrusted base URL: ${baseUrl}. Expected http: or https: protocol.`);
|
|
138
|
+
}
|
|
139
|
+
} else if (parsedUrl.protocol !== "https:") {
|
|
140
|
+
throw new Error(`Untrusted base URL: ${baseUrl}. Expected https: protocol.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (enforcePath && hostname !== allowedPublicHost && !isLocalHost) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Untrusted base URL: ${baseUrl}. When enforcePath is enabled, expected base URL host ${allowedPublicHost} unless host is a local testing host.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const isAllowedHost =
|
|
150
|
+
hostname === allowedPublicHost ||
|
|
151
|
+
allowedLocalHosts.includes(hostname) ||
|
|
152
|
+
allowedRedirectHostSuffixes.some(
|
|
153
|
+
(suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`),
|
|
154
|
+
);
|
|
155
|
+
if (!isAllowedHost) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Untrusted base URL: ${baseUrl}. Allowed hosts are: ${[allowedPublicHost, ...allowedLocalHosts, ...allowedRedirectHostSuffixes.map((suffix) => `*.${suffix}`)].join(", ")}.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
enforcePath &&
|
|
163
|
+
!isLocalHost &&
|
|
164
|
+
!parsedUrl.pathname.toLowerCase().startsWith(`${publicPathPrefix}/`)
|
|
165
|
+
) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Untrusted GitHub repository in base URL: ${baseUrl}. For github.com, expected base URL prefix ${publicPathPrefix}.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
88
172
|
function isPlaceholder(path) {
|
|
89
173
|
let fd;
|
|
90
174
|
try {
|
|
@@ -121,4 +205,5 @@ module.exports = {
|
|
|
121
205
|
isPlaceholder,
|
|
122
206
|
sha256,
|
|
123
207
|
unsupportedPlatformMessage,
|
|
208
|
+
validateReleaseBaseUrl,
|
|
124
209
|
};
|