Skip to content

Commit 31f040f

Browse files
fix(ui-kit): declare react-hook-form as a dependency of @loopover/ui-kit
form.tsx imports react-hook-form directly, but it was never declared in package.json -- only apps/loopover-ui installs it, so resolution only succeeded here via npm workspace hoisting. Added it to dependencies (version-matched to apps/loopover-ui's existing range) and a manifest test asserting every external import across the package's non-test components has a matching dependencies/peerDependencies entry, so this class of gap is caught automatically going forward. Closes #8309
1 parent f1b5cc1 commit 31f040f

3 files changed

Lines changed: 65 additions & 0 deletions

File tree

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/loopover-ui-kit/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"input-otp": "^1.4.2",
8787
"lucide-react": "^0.577.0",
8888
"react-day-picker": "^9.14.0",
89+
"react-hook-form": "^7.80.0",
8990
"react-resizable-panels": "^4.12.0",
9091
"recharts": "^2.15.4",
9192
"sonner": "^2.0.7",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { readFileSync, readdirSync } from "node:fs";
2+
import { join } from "node:path";
3+
4+
// #8309: @loopover/ui-kit is a real, published npm package (publishConfig.access: "public") -- every
5+
// component that wraps an external library must declare that library in its own package.json, since a
6+
// consumer installing the package in isolation (or a workspace member that doesn't happen to hoist the
7+
// dependency from elsewhere, e.g. loopover-miner-ui) has no other guarantee module resolution succeeds.
8+
// This asserts every external import across the package's (non-test) components has a matching
9+
// dependencies/peerDependencies entry, so a future component that forgets to declare its import is
10+
// caught here instead of only failing for a consumer outside this monorepo's hoisting.
11+
12+
const COMPONENTS_DIR = join("packages", "loopover-ui-kit", "src", "components");
13+
14+
function packageNameFromSpecifier(specifier: string): string {
15+
const segments = specifier.split("/");
16+
// Scoped package (@scope/name) -- the package name is the first two path segments; everything else
17+
// (unscoped, e.g. "lucide-react", or a subpath import like "some-pkg/sub") is just the first segment.
18+
return specifier.startsWith("@")
19+
? segments.slice(0, 2).join("/")
20+
: (segments[0] ?? specifier);
21+
}
22+
23+
function externalImportsIn(source: string): string[] {
24+
const specifiers: string[] = [];
25+
for (const match of source.matchAll(
26+
/import\s[\s\S]*?from\s+["']([^"']+)["']/g,
27+
)) {
28+
const specifier = match[1];
29+
if (specifier && !specifier.startsWith("."))
30+
specifiers.push(packageNameFromSpecifier(specifier));
31+
}
32+
return specifiers;
33+
}
34+
35+
describe("packages/loopover-ui-kit package.json declares every component's external imports (#8309)", () => {
36+
it("every non-test component's external import has a dependencies or peerDependencies entry", () => {
37+
const pkg = JSON.parse(
38+
readFileSync(join("packages", "loopover-ui-kit", "package.json"), "utf8"),
39+
) as {
40+
dependencies?: Record<string, string>;
41+
peerDependencies?: Record<string, string>;
42+
};
43+
const declared = new Set([
44+
...Object.keys(pkg.dependencies ?? {}),
45+
...Object.keys(pkg.peerDependencies ?? {}),
46+
]);
47+
48+
const componentFiles = readdirSync(COMPONENTS_DIR).filter(
49+
(name) => name.endsWith(".tsx") && !name.endsWith(".test.tsx"),
50+
);
51+
expect(componentFiles.length).toBeGreaterThan(0); // guard against a glob/path typo silently checking nothing
52+
53+
const undeclared: string[] = [];
54+
for (const file of componentFiles) {
55+
const source = readFileSync(join(COMPONENTS_DIR, file), "utf8");
56+
for (const importedPackage of externalImportsIn(source)) {
57+
if (!declared.has(importedPackage))
58+
undeclared.push(`${file}: ${importedPackage}`);
59+
}
60+
}
61+
expect(undeclared).toEqual([]);
62+
});
63+
});

0 commit comments

Comments
 (0)