no-mistakes 0.18.0 → 0.20.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/index.d.ts +4 -0
- package/index.js +5 -0
- package/package.json +1 -1
- package/report-types.d.ts +19 -0
- package/scripts/install/download.js +43 -30
- package/scripts/install/index.js +2 -0
- package/scripts/install/retry.js +94 -0
- package/traversal-types.d.ts +4 -0
package/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ProjectOptions,
|
|
13
13
|
QueueReport,
|
|
14
14
|
ReactComponentFacts,
|
|
15
|
+
ReactUsagesReport,
|
|
15
16
|
ReactViolation,
|
|
16
17
|
ServerRoutesReport,
|
|
17
18
|
SymbolsOptions,
|
|
@@ -55,5 +56,8 @@ export function serverRouteEdges(options?: ProjectOptions): Promise<GraphEdge[]>
|
|
|
55
56
|
export function serverRouteRelated(options: ProjectOptions): Promise<GraphEdge[]>;
|
|
56
57
|
export function reactAnalyze(options?: ProjectOptions): Promise<ReactComponentFacts[]>;
|
|
57
58
|
export function reactCheck(options?: ProjectOptions): Promise<ReactViolation[]>;
|
|
59
|
+
export function reactUsages(
|
|
60
|
+
options: ProjectOptions & { target: string },
|
|
61
|
+
): Promise<ReactUsagesReport>;
|
|
58
62
|
export function lockfileDiff(options: LockfileDiffOptions): Promise<LockfileDiffEntry[]>;
|
|
59
63
|
export function version(): Promise<string>;
|
package/index.js
CHANGED
|
@@ -114,6 +114,10 @@ async function reactCheck(options) {
|
|
|
114
114
|
return callJson(native.reactCheckJson, options);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
async function reactUsages(options) {
|
|
118
|
+
return callJson(native.reactUsagesJson, options);
|
|
119
|
+
}
|
|
120
|
+
|
|
117
121
|
async function lockfileDiff(options) {
|
|
118
122
|
return callJson(native.lockfileDiffJson, options);
|
|
119
123
|
}
|
|
@@ -139,6 +143,7 @@ module.exports = {
|
|
|
139
143
|
queueRelated,
|
|
140
144
|
reactAnalyze,
|
|
141
145
|
reactCheck,
|
|
146
|
+
reactUsages,
|
|
142
147
|
related,
|
|
143
148
|
serverRouteEdges,
|
|
144
149
|
serverRouteList,
|
package/package.json
CHANGED
package/report-types.d.ts
CHANGED
|
@@ -138,3 +138,22 @@ export interface ReactViolation {
|
|
|
138
138
|
rule: string;
|
|
139
139
|
detail?: string;
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
export interface ReactCallsite {
|
|
143
|
+
file: string;
|
|
144
|
+
line: number;
|
|
145
|
+
component: string;
|
|
146
|
+
props: string[];
|
|
147
|
+
hasSpread: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ReactUsagesReport {
|
|
151
|
+
target: { file: string; symbol?: string };
|
|
152
|
+
callsites: ReactCallsite[];
|
|
153
|
+
/** Story files importing the target. Omitted when `props`/`tests`-only `include`. */
|
|
154
|
+
stories?: string[];
|
|
155
|
+
/** Test files importing the target. */
|
|
156
|
+
tests?: string[];
|
|
157
|
+
/** Exported prop type/interface names declared in the target file. */
|
|
158
|
+
propTypes?: string[];
|
|
159
|
+
}
|
|
@@ -7,23 +7,29 @@ const https = require("node:https");
|
|
|
7
7
|
const { pipeline } = require("node:stream/promises");
|
|
8
8
|
const { fileURLToPath } = require("node:url");
|
|
9
9
|
|
|
10
|
+
const { HttpError, withRetry } = require("./retry");
|
|
11
|
+
|
|
10
12
|
const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
11
13
|
|
|
12
|
-
function download(url, destination, redirects = 0, validateUrl = () => {}) {
|
|
14
|
+
function download(url, destination, redirects = 0, validateUrl = () => {}, retryOptions) {
|
|
13
15
|
validateUrl(url);
|
|
14
16
|
if (isFileUrl(url)) {
|
|
15
17
|
return copyFile(fileURLToPath(url), destination);
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
return withRetry(
|
|
21
|
+
() =>
|
|
22
|
+
request(
|
|
23
|
+
url,
|
|
24
|
+
async (response) => {
|
|
25
|
+
await pipeline(response, createWriteStream(destination));
|
|
26
|
+
},
|
|
27
|
+
redirects,
|
|
28
|
+
{ http, https },
|
|
29
|
+
DOWNLOAD_TIMEOUT_MS,
|
|
30
|
+
validateUrl,
|
|
31
|
+
),
|
|
32
|
+
{ ...retryOptions, describe: () => `download of ${url}` },
|
|
27
33
|
);
|
|
28
34
|
}
|
|
29
35
|
|
|
@@ -67,7 +73,7 @@ function request(
|
|
|
67
73
|
|
|
68
74
|
if (response.statusCode !== 200) {
|
|
69
75
|
response.resume();
|
|
70
|
-
reject(new
|
|
76
|
+
reject(new HttpError(url, response.statusCode));
|
|
71
77
|
return;
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -75,7 +81,9 @@ function request(
|
|
|
75
81
|
});
|
|
76
82
|
|
|
77
83
|
req.setTimeout(timeoutMs, () => {
|
|
78
|
-
|
|
84
|
+
const timeoutError = new Error(`Download timed out after ${timeoutMs}ms: ${url}`);
|
|
85
|
+
timeoutError.retryable = true;
|
|
86
|
+
req.destroy(timeoutError);
|
|
79
87
|
});
|
|
80
88
|
req.on("error", reject);
|
|
81
89
|
});
|
|
@@ -85,32 +93,37 @@ function isRedirectStatus(statusCode) {
|
|
|
85
93
|
return [301, 302, 303, 307, 308].includes(statusCode);
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
async function fetchText(url, validateUrl = () => {}) {
|
|
96
|
+
async function fetchText(url, validateUrl = () => {}, retryOptions) {
|
|
89
97
|
validateUrl(url);
|
|
90
98
|
|
|
91
99
|
if (isFileUrl(url)) {
|
|
92
100
|
return readFile(fileURLToPath(url), "utf8");
|
|
93
101
|
}
|
|
94
|
-
const chunks = [];
|
|
95
|
-
let totalLength = 0;
|
|
96
102
|
const MAX_LENGTH = 1024 * 1024;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
return withRetry(
|
|
104
|
+
async () => {
|
|
105
|
+
const chunks = [];
|
|
106
|
+
let totalLength = 0;
|
|
107
|
+
await request(
|
|
108
|
+
url,
|
|
109
|
+
async (response) => {
|
|
110
|
+
for await (const chunk of response) {
|
|
111
|
+
totalLength += chunk.length;
|
|
112
|
+
if (totalLength > MAX_LENGTH) {
|
|
113
|
+
throw new Error(`Response exceeded maximum size of ${MAX_LENGTH} bytes`);
|
|
114
|
+
}
|
|
115
|
+
chunks.push(chunk);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
0,
|
|
119
|
+
{ http, https },
|
|
120
|
+
DOWNLOAD_TIMEOUT_MS,
|
|
121
|
+
validateUrl,
|
|
122
|
+
);
|
|
123
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
107
124
|
},
|
|
108
|
-
|
|
109
|
-
{ http, https },
|
|
110
|
-
DOWNLOAD_TIMEOUT_MS,
|
|
111
|
-
validateUrl,
|
|
125
|
+
{ ...retryOptions, describe: () => `fetch of ${url}` },
|
|
112
126
|
);
|
|
113
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
function isFileUrl(url) {
|
package/scripts/install/index.js
CHANGED
|
@@ -4,10 +4,12 @@ const assets = require("./assets");
|
|
|
4
4
|
const download = require("./download");
|
|
5
5
|
const installer = require("./installer");
|
|
6
6
|
const platform = require("./platform");
|
|
7
|
+
const retry = require("./retry");
|
|
7
8
|
|
|
8
9
|
module.exports = {
|
|
9
10
|
...assets,
|
|
10
11
|
...download,
|
|
11
12
|
...installer,
|
|
12
13
|
...platform,
|
|
14
|
+
...retry,
|
|
13
15
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_ATTEMPTS = 4;
|
|
4
|
+
const DEFAULT_RETRY_BASE_MS = 500;
|
|
5
|
+
const RETRY_MAX_DELAY_MS = 4_000;
|
|
6
|
+
const RETRYABLE_NETWORK_CODES = new Set([
|
|
7
|
+
"ECONNRESET",
|
|
8
|
+
"ECONNREFUSED",
|
|
9
|
+
"ETIMEDOUT",
|
|
10
|
+
"EAI_AGAIN",
|
|
11
|
+
"ENETUNREACH",
|
|
12
|
+
"EPIPE",
|
|
13
|
+
"ERR_STREAM_PREMATURE_CLOSE",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
class HttpError extends Error {
|
|
17
|
+
constructor(url, statusCode) {
|
|
18
|
+
super(`Download failed for ${url}: HTTP ${statusCode}`);
|
|
19
|
+
this.name = "HttpError";
|
|
20
|
+
this.statusCode = statusCode;
|
|
21
|
+
this.retryable = isRetryableStatus(statusCode);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isRetryableStatus(statusCode) {
|
|
26
|
+
if (typeof statusCode !== "number") return false;
|
|
27
|
+
if (statusCode >= 500 && statusCode < 600) return true;
|
|
28
|
+
return statusCode === 408 || statusCode === 429;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isRetryableError(error) {
|
|
32
|
+
if (!error) return false;
|
|
33
|
+
if (error.retryable === true) return true;
|
|
34
|
+
if (error.code && RETRYABLE_NETWORK_CODES.has(error.code)) return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sleep(ms) {
|
|
39
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parsePositiveInt(value, fallback) {
|
|
43
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
44
|
+
const n = Number(value);
|
|
45
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) return fallback;
|
|
46
|
+
return n;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function computeBackoffMs(attempt, baseDelayMs, random = Math.random) {
|
|
50
|
+
const exponential = baseDelayMs * 2 ** (attempt - 1);
|
|
51
|
+
const capped = Math.min(exponential, RETRY_MAX_DELAY_MS);
|
|
52
|
+
return Math.floor(random() * capped);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function withRetry(fn, options = {}) {
|
|
56
|
+
const maxAttempts = parsePositiveInt(
|
|
57
|
+
options.maxAttempts ?? process.env.NO_MISTAKES_DOWNLOAD_MAX_ATTEMPTS,
|
|
58
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
59
|
+
);
|
|
60
|
+
const baseDelayMs = parsePositiveInt(
|
|
61
|
+
options.baseDelayMs ?? process.env.NO_MISTAKES_DOWNLOAD_RETRY_BASE_MS,
|
|
62
|
+
DEFAULT_RETRY_BASE_MS,
|
|
63
|
+
);
|
|
64
|
+
const delay = options.delay ?? sleep;
|
|
65
|
+
const random = options.random ?? Math.random;
|
|
66
|
+
const logger = options.logger ?? console;
|
|
67
|
+
const describe = options.describe ?? (() => "download");
|
|
68
|
+
|
|
69
|
+
let attempt = 0;
|
|
70
|
+
for (;;) {
|
|
71
|
+
attempt += 1;
|
|
72
|
+
try {
|
|
73
|
+
return await fn();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (attempt >= maxAttempts || !isRetryableError(error)) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
const waitMs = computeBackoffMs(attempt, baseDelayMs, random);
|
|
79
|
+
logger.warn(
|
|
80
|
+
`no-mistakes: retrying ${describe()} after ${error.message} (attempt ${attempt + 1}/${maxAttempts}, waiting ~${waitMs}ms)`,
|
|
81
|
+
);
|
|
82
|
+
await delay(waitMs);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
HttpError,
|
|
89
|
+
computeBackoffMs,
|
|
90
|
+
isRetryableError,
|
|
91
|
+
isRetryableStatus,
|
|
92
|
+
parsePositiveInt,
|
|
93
|
+
withRetry,
|
|
94
|
+
};
|
package/traversal-types.d.ts
CHANGED
|
@@ -114,6 +114,10 @@ export interface ProjectOptions {
|
|
|
114
114
|
depth?: number;
|
|
115
115
|
assertNoFetch?: boolean;
|
|
116
116
|
direction?: "deps" | "dependents" | "both";
|
|
117
|
+
/** `reactUsages` target component (`path` or `path#Symbol`). */
|
|
118
|
+
target?: string;
|
|
119
|
+
/** `reactUsages` `--include` spec: comma-separated `stories,tests,props`. */
|
|
120
|
+
include?: string;
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
type BatchedProjectOptions = Omit<ProjectOptions, "root" | "tsconfig" | "config">;
|