Skip to content

Commit 043f7e6

Browse files
lkostrowskiclaude
andauthored
Fix some ts-strict-ignore (#6440)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 75b85d9 commit 043f7e6

13 files changed

Lines changed: 775 additions & 52 deletions

File tree

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Finds all files with `@ts-strict-ignore` and traces their full importer tree.
5+
*
6+
* Uses dependency-cruiser to build a reverse import graph, then for each
7+
* strict-ignored file walks up the importer chain — following through
8+
* barrel files (index.ts) and re-exports so you see the real consumers.
9+
*
10+
* Usage: node scripts/strict-ignore-importers.mjs
11+
*/
12+
13+
import { execSync } from "node:child_process";
14+
import { basename } from "node:path";
15+
16+
const ROOT = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
17+
18+
// ── 1. Gather files with @ts-strict-ignore ──────────────────────────────────
19+
20+
function findStrictIgnoreFiles() {
21+
// Safe: no user input, hardcoded command
22+
const out = execSync(`grep -rl "@ts-strict-ignore" src/ --include="*.ts" --include="*.tsx"`, {
23+
cwd: ROOT,
24+
encoding: "utf8",
25+
});
26+
return new Set(out.trim().split("\n").filter(Boolean));
27+
}
28+
29+
// ── 2. Build reverse import graph via dependency-cruiser ────────────────────
30+
31+
function buildReverseGraph() {
32+
console.error("⏳ Running dependency-cruiser (this takes a moment)…");
33+
34+
// Safe: no user input, hardcoded command
35+
const json = execSync(`npx depcruise --output-type json --no-config src/`, {
36+
cwd: ROOT,
37+
encoding: "utf8",
38+
maxBuffer: 200 * 1024 * 1024,
39+
});
40+
41+
const { modules } = JSON.parse(json);
42+
43+
// reverseGraph: resolved target → Set of sources that import it
44+
const reverseGraph = new Map();
45+
46+
for (const mod of modules) {
47+
if (!mod.dependencies) continue;
48+
for (const dep of mod.dependencies) {
49+
if (!dep.resolved || dep.couldNotResolve) continue;
50+
if (!reverseGraph.has(dep.resolved)) {
51+
reverseGraph.set(dep.resolved, new Set());
52+
}
53+
reverseGraph.get(dep.resolved).add(mod.source);
54+
}
55+
}
56+
57+
console.error(`✅ Graph built: ${modules.length} modules, ${reverseGraph.size} imported files`);
58+
return reverseGraph;
59+
}
60+
61+
// ── 3. Walk the importer tree ───────────────────────────────────────────────
62+
63+
function isBarrelFile(filePath) {
64+
const name = basename(filePath);
65+
return /^index\.(ts|tsx|js|jsx)$/.test(name);
66+
}
67+
68+
/**
69+
* Recursively collect the full importer tree for `file`.
70+
* Barrel files (index.ts) are followed through — their importers are
71+
* included instead of (or in addition to) the barrel itself.
72+
*
73+
* Returns a Map: importer path → depth (shortest path distance).
74+
*/
75+
function traceImporters(file, reverseGraph) {
76+
const result = new Map();
77+
const visited = new Set();
78+
79+
function walk(current, depth) {
80+
if (visited.has(current)) return;
81+
visited.add(current);
82+
83+
const importers = reverseGraph.get(current);
84+
if (!importers) return;
85+
86+
for (const imp of importers) {
87+
if (isBarrelFile(current)) {
88+
if (!result.has(current)) result.set(current, depth);
89+
walk(imp, depth);
90+
} else if (isBarrelFile(imp)) {
91+
if (!result.has(imp)) result.set(imp, depth);
92+
walk(imp, depth + 1);
93+
} else {
94+
if (!result.has(imp)) result.set(imp, depth);
95+
}
96+
}
97+
}
98+
99+
walk(file, 1);
100+
return result;
101+
}
102+
103+
// ── 4. Main ─────────────────────────────────────────────────────────────────
104+
105+
const strictIgnoreFiles = findStrictIgnoreFiles();
106+
console.error(`📁 Found ${strictIgnoreFiles.size} files with @ts-strict-ignore`);
107+
108+
const reverseGraph = buildReverseGraph();
109+
110+
const results = [];
111+
112+
for (const file of [...strictIgnoreFiles].sort()) {
113+
const importers = traceImporters(file, reverseGraph);
114+
115+
const barrels = [];
116+
const consumers = [];
117+
for (const [imp, depth] of importers) {
118+
if (isBarrelFile(imp)) {
119+
barrels.push({ path: imp, depth });
120+
} else {
121+
consumers.push({ path: imp, depth });
122+
}
123+
}
124+
125+
const consumerDetails = consumers.map(c => ({
126+
...c,
127+
alsoStrictIgnored: strictIgnoreFiles.has(c.path),
128+
}));
129+
130+
const allConsumersStrict =
131+
consumers.length === 0 || consumerDetails.every(c => !c.alsoStrictIgnored);
132+
133+
results.push({
134+
file,
135+
directImporterCount: reverseGraph.get(file)?.size ?? 0,
136+
totalConsumers: consumers.length,
137+
barrels,
138+
consumers: consumerDetails,
139+
ready: allConsumersStrict,
140+
});
141+
}
142+
143+
// ── 5. Print output ─────────────────────────────────────────────────────────
144+
145+
const readyFiles = results.filter(r => r.ready);
146+
const byConsumerCount = [
147+
{ label: "0 consumers (leaf)", items: results.filter(r => r.totalConsumers === 0) },
148+
{ label: "1 consumer", items: results.filter(r => r.totalConsumers === 1) },
149+
{
150+
label: "2-4 consumers",
151+
items: results.filter(r => r.totalConsumers >= 2 && r.totalConsumers <= 4),
152+
},
153+
{ label: "5+ consumers", items: results.filter(r => r.totalConsumers >= 5) },
154+
];
155+
156+
console.log("=".repeat(80));
157+
console.log(" @ts-strict-ignore IMPORTER ANALYSIS");
158+
console.log("=".repeat(80));
159+
console.log();
160+
console.log(`Total files with @ts-strict-ignore: ${results.length}`);
161+
console.log(`Ready to fix (all consumers are already strict): ${readyFiles.length}`);
162+
console.log();
163+
164+
console.log("Distribution by consumer count:");
165+
for (const bucket of byConsumerCount) {
166+
console.log(` ${bucket.label}: ${bucket.items.length}`);
167+
}
168+
console.log();
169+
170+
console.log("─".repeat(80));
171+
console.log("READY TO FIX (no strict-ignored consumers):");
172+
console.log("─".repeat(80));
173+
for (const r of readyFiles) {
174+
const tag =
175+
r.totalConsumers === 0
176+
? "[leaf]"
177+
: `[${r.totalConsumers} consumer${r.totalConsumers > 1 ? "s" : ""}]`;
178+
console.log(` ${tag} ${r.file}`);
179+
for (const b of r.barrels) {
180+
console.log(` ↳ via barrel: ${b.path}`);
181+
}
182+
for (const c of r.consumers) {
183+
console.log(` → ${c.path}`);
184+
}
185+
}
186+
187+
console.log();
188+
console.log("─".repeat(80));
189+
console.log("ALL FILES (detailed importer tree):");
190+
console.log("─".repeat(80));
191+
192+
for (const r of results) {
193+
const readyMark = r.ready ? "✅" : " ";
194+
console.log(
195+
`\n${readyMark} ${r.file} (direct: ${r.directImporterCount}, total consumers: ${r.totalConsumers})`,
196+
);
197+
198+
if (r.barrels.length > 0) {
199+
for (const b of r.barrels) {
200+
console.log(` ↳ barrel: ${b.path}`);
201+
}
202+
}
203+
204+
if (r.consumers.length > 0) {
205+
for (const c of r.consumers) {
206+
const strictTag = c.alsoStrictIgnored ? " ⚠️ @ts-strict-ignore" : "";
207+
console.log(` → ${c.path}${strictTag}`);
208+
}
209+
} else {
210+
console.log(" (no consumers found — leaf file)");
211+
}
212+
}
213+
214+
console.log();
215+
console.log("─".repeat(80));
216+
console.log("TOP 20 BY CONSUMER COUNT:");
217+
console.log("─".repeat(80));
218+
const sorted = [...results].sort((a, b) => b.totalConsumers - a.totalConsumers);
219+
for (const r of sorted.slice(0, 20)) {
220+
const readyMark = r.ready ? "✅" : "❌";
221+
console.log(` ${readyMark} ${r.totalConsumers.toString().padStart(4)} consumers ${r.file}`);
222+
}

src/components/Date/DateTime.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// @ts-strict-ignore
21
import { useCurrentDate } from "@dashboard/hooks/useCurrentDate";
32
import { Tooltip } from "@saleor/macaw-ui-next";
43
import moment from "moment-timezone";
@@ -16,7 +15,7 @@ export const DateTime = ({ date, plain }: DateTimeProps) => {
1615
const currentDate = useCurrentDate();
1716

1817
const getTitle = (value: string, locale?: string, tz?: string) => {
19-
let date = moment(value).locale(locale);
18+
let date: moment.Moment = locale ? moment(value).locale(locale) : moment(value);
2019

2120
if (tz !== undefined) {
2221
date = date.tz(tz);
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { type ChangeEvent } from "@dashboard/hooks/useForm";
2+
3+
import { EventDataAction, EventDataField } from "./types";
4+
import { getDataKey, getMetadataTitle, parseEventData } from "./utils";
5+
6+
describe("parseEventData", () => {
7+
it("parses name field update event", () => {
8+
// Arrange
9+
const event: ChangeEvent<string> = {
10+
target: { name: "name:2", value: "my-key" },
11+
};
12+
13+
// Act
14+
const result = parseEventData(event);
15+
16+
// Assert
17+
expect(result).toEqual({
18+
action: EventDataAction.update,
19+
field: EventDataField.name,
20+
fieldIndex: 2,
21+
value: "my-key",
22+
});
23+
});
24+
25+
it("parses value field update event", () => {
26+
// Arrange
27+
const event: ChangeEvent<string> = {
28+
target: { name: "value:5", value: "my-value" },
29+
};
30+
31+
// Act
32+
const result = parseEventData(event);
33+
34+
// Assert
35+
expect(result).toEqual({
36+
action: EventDataAction.update,
37+
field: EventDataField.value,
38+
fieldIndex: 5,
39+
value: "my-value",
40+
});
41+
});
42+
43+
it("parses add action event", () => {
44+
// Arrange
45+
const event: ChangeEvent<string> = {
46+
target: { name: "add", value: "" },
47+
};
48+
49+
// Act
50+
const result = parseEventData(event);
51+
52+
// Assert
53+
expect(result).toEqual({
54+
action: EventDataAction.add,
55+
field: null,
56+
fieldIndex: null,
57+
value: "",
58+
});
59+
});
60+
61+
it("parses delete action event", () => {
62+
// Arrange
63+
const event: ChangeEvent<string> = {
64+
target: { name: "delete", value: "3" },
65+
};
66+
67+
// Act
68+
const result = parseEventData(event);
69+
70+
// Assert
71+
expect(result).toEqual({
72+
action: EventDataAction.delete,
73+
field: null,
74+
fieldIndex: 3,
75+
value: "",
76+
});
77+
});
78+
79+
it("throws on invalid event action", () => {
80+
// Arrange
81+
const event: ChangeEvent<string> = {
82+
target: { name: "unknown-action", value: "" },
83+
};
84+
85+
// Act & Assert
86+
expect(() => parseEventData(event)).toThrow('Invalid metadata event action: "unknown-action"');
87+
});
88+
});
89+
90+
describe("getDataKey", () => {
91+
it("returns 'privateMetadata' when isPrivate is true", () => {
92+
expect(getDataKey(true)).toBe("privateMetadata");
93+
});
94+
95+
it("returns 'metadata' when isPrivate is false", () => {
96+
expect(getDataKey(false)).toBe("metadata");
97+
});
98+
});
99+
100+
describe("getMetadataTitle", () => {
101+
it("returns private metadata message descriptor when isPrivate is true", () => {
102+
expect(getMetadataTitle(true)).toEqual({
103+
id: "ETHnjq",
104+
defaultMessage: "Private Metadata",
105+
description: "header",
106+
});
107+
});
108+
109+
it("returns metadata message descriptor when isPrivate is false", () => {
110+
expect(getMetadataTitle(false)).toEqual({
111+
id: "VcI+Zh",
112+
defaultMessage: "Metadata",
113+
description: "header",
114+
});
115+
});
116+
});

0 commit comments

Comments
 (0)