tempest-react-sdk 0.26.0 → 0.26.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.
@@ -0,0 +1,74 @@
1
+ // The `lucide-react` single-instance check for `tempest doctor`.
2
+ //
3
+ // Kept out of the generic stateful-deps check on purpose: two copies of React or
4
+ // of react-query break hooks and context, which is a different failure from what
5
+ // two copies of lucide cause. Lucide is stateless, so nothing "breaks" at
6
+ // runtime — instead the bytes are duplicated and, far worse, the generated slug
7
+ // tables behind `tempest-react-sdk/icons` can reference exports the second copy
8
+ // does not have.
9
+
10
+ /** Major version number from a semver string or range, or null. */
11
+ function major(spec) {
12
+ if (!spec) return null;
13
+ const match = /(\d+)\./.exec(String(spec).replace(/^[^\d]*/, ""));
14
+ return match ? Number(match[1]) : null;
15
+ }
16
+
17
+ /**
18
+ * Audit how many copies of `lucide-react` a project will end up with.
19
+ *
20
+ * @param {object} params
21
+ * @param {string | null} params.appSpec - Range the app declares, or `null`.
22
+ * @param {string | null} params.sdkSpec - Range the installed SDK declares, or `null`.
23
+ * @param {string | null} params.installedVersion - Version resolved at the app root, or `null`.
24
+ * @param {boolean} params.nestedCopy - Whether a copy exists under the SDK's own `node_modules`.
25
+ * @returns {Array<[status: string, label: string, detail?: string]>} Check rows.
26
+ */
27
+ export function checkLucide({ appSpec, sdkSpec, installedVersion, nestedCopy }) {
28
+ const rows = [];
29
+
30
+ // The SDK not declaring it means this project does not use the icons surface
31
+ // at all (or the SDK is not installed) — there is nothing to audit.
32
+ if (!sdkSpec) return rows;
33
+
34
+ if (nestedCopy) {
35
+ rows.push([
36
+ "warn",
37
+ "two copies of lucide-react",
38
+ "nested copy under tempest-react-sdk — duplicated bytes, and the generated " +
39
+ "icon slug tables may not match it; run `npm dedupe` or drop lucide-react " +
40
+ "from your package.json (the SDK ships it)",
41
+ ]);
42
+ }
43
+
44
+ if (appSpec) {
45
+ const sameRange = appSpec === sdkSpec;
46
+ rows.push([
47
+ sameRange ? "info" : "warn",
48
+ "lucide-react declared by the app",
49
+ sameRange
50
+ ? `matches the SDK's ${sdkSpec} so a single copy installs — the declaration is ` +
51
+ "redundant, but harmless (npm/yarn resolve it from the SDK without it)"
52
+ : `you declare ${appSpec}, the SDK declares ${sdkSpec} — two copies install. ` +
53
+ "Run `npm uninstall lucide-react`; only declare it under pnpm, and then " +
54
+ `use ${sdkSpec}`,
55
+ ]);
56
+ }
57
+
58
+ const installedMajor = major(installedVersion);
59
+ const requiredMajor = major(sdkSpec);
60
+ if (installedMajor !== null && requiredMajor !== null && installedMajor < requiredMajor) {
61
+ rows.push([
62
+ "fail",
63
+ `lucide-react v${installedVersion} is older than the SDK needs`,
64
+ `the icon slug tables are generated against ${sdkSpec}, so exports they ` +
65
+ "reference can be missing — the build fails with `… is not exported by " +
66
+ "lucide-react` pointing inside the SDK",
67
+ ]);
68
+ }
69
+
70
+ if (rows.length === 0) {
71
+ rows.push(["ok", "single lucide-react instance", `from the SDK — ${sdkSpec}`]);
72
+ }
73
+ return rows;
74
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { checkLucide } from "./lucide.mjs";
4
+
5
+ /** The healthy shape: the SDK brings lucide, the app declares nothing. */
6
+ const CLEAN = {
7
+ appSpec: null,
8
+ sdkSpec: "^1.26.0",
9
+ installedVersion: "1.26.0",
10
+ nestedCopy: false,
11
+ };
12
+
13
+ /** Statuses of the returned rows, in order. */
14
+ const statuses = (rows) => rows.map(([status]) => status);
15
+
16
+ /** All detail text joined, for asserting the advice is actually present. */
17
+ const details = (rows) => rows.map(([, , detail]) => detail ?? "").join(" ");
18
+
19
+ describe("checkLucide — nothing to audit", () => {
20
+ it("returns no rows when the SDK does not declare lucide", () => {
21
+ expect(checkLucide({ ...CLEAN, sdkSpec: null })).toEqual([]);
22
+ });
23
+
24
+ it("stays silent even if the app declares it, with no SDK to compare against", () => {
25
+ expect(checkLucide({ ...CLEAN, sdkSpec: null, appSpec: "^1.0.0" })).toEqual([]);
26
+ });
27
+ });
28
+
29
+ describe("checkLucide — healthy", () => {
30
+ it("reports a single instance and names where it came from", () => {
31
+ const rows = checkLucide(CLEAN);
32
+ expect(statuses(rows)).toEqual(["ok"]);
33
+ expect(rows[0][1]).toBe("single lucide-react instance");
34
+ expect(rows[0][2]).toContain("^1.26.0");
35
+ });
36
+
37
+ it("stays ok when the installed version is newer than required", () => {
38
+ expect(statuses(checkLucide({ ...CLEAN, installedVersion: "2.4.0" }))).toEqual(["ok"]);
39
+ });
40
+
41
+ it("stays ok when the version cannot be resolved at all", () => {
42
+ expect(statuses(checkLucide({ ...CLEAN, installedVersion: null }))).toEqual(["ok"]);
43
+ });
44
+ });
45
+
46
+ describe("checkLucide — the app declares it too", () => {
47
+ it("warns and says to uninstall when the range differs from the SDK's", () => {
48
+ const rows = checkLucide({ ...CLEAN, appSpec: "^0.575.0" });
49
+ expect(statuses(rows)).toEqual(["warn"]);
50
+ expect(details(rows)).toContain("npm uninstall lucide-react");
51
+ expect(details(rows)).toContain("^0.575.0");
52
+ expect(details(rows)).toContain("^1.26.0");
53
+ });
54
+
55
+ it("mentions pnpm, the one case where declaring it is correct", () => {
56
+ expect(details(checkLucide({ ...CLEAN, appSpec: "^0.575.0" }))).toContain("pnpm");
57
+ });
58
+
59
+ it("downgrades to info when the range matches — redundant, not broken", () => {
60
+ const rows = checkLucide({ ...CLEAN, appSpec: "^1.26.0" });
61
+ expect(statuses(rows)).toEqual(["info"]);
62
+ expect(details(rows)).toContain("redundant");
63
+ });
64
+ });
65
+
66
+ describe("checkLucide — a second physical copy", () => {
67
+ it("warns about the nested copy and points at both remedies", () => {
68
+ const rows = checkLucide({ ...CLEAN, nestedCopy: true });
69
+ expect(statuses(rows)).toEqual(["warn"]);
70
+ expect(rows[0][1]).toBe("two copies of lucide-react");
71
+ expect(details(rows)).toContain("npm dedupe");
72
+ expect(details(rows)).toContain("the SDK ships it");
73
+ });
74
+
75
+ it("reports the nested copy and the declaration as separate rows", () => {
76
+ const rows = checkLucide({ ...CLEAN, nestedCopy: true, appSpec: "^0.575.0" });
77
+ expect(statuses(rows)).toEqual(["warn", "warn"]);
78
+ });
79
+ });
80
+
81
+ describe("checkLucide — version older than the generated tables need", () => {
82
+ it("fails, because this one breaks the build rather than wasting bytes", () => {
83
+ const rows = checkLucide({ ...CLEAN, installedVersion: "0.575.0" });
84
+ expect(statuses(rows)).toEqual(["fail"]);
85
+ expect(rows[0][1]).toContain("0.575.0");
86
+ expect(details(rows)).toContain("is not exported by");
87
+ });
88
+
89
+ it("explains that the error surfaces inside the SDK, not in app code", () => {
90
+ expect(details(checkLucide({ ...CLEAN, installedVersion: "0.575.0" }))).toContain(
91
+ "inside the SDK",
92
+ );
93
+ });
94
+
95
+ it("reports the skew alongside the declaration that caused it", () => {
96
+ const rows = checkLucide({
97
+ ...CLEAN,
98
+ appSpec: "^0.575.0",
99
+ installedVersion: "0.575.0",
100
+ });
101
+ expect(statuses(rows)).toEqual(["warn", "fail"]);
102
+ });
103
+
104
+ it("does not fail on a malformed version string", () => {
105
+ expect(statuses(checkLucide({ ...CLEAN, installedVersion: "latest" }))).toEqual(["ok"]);
106
+ });
107
+ });
package/bin/tempest.mjs CHANGED
@@ -14,6 +14,7 @@ import { fileURLToPath } from "node:url";
14
14
  import { aliasImports } from "./lib/alias/index.mjs";
15
15
  import { loadTypeScript } from "./lib/alias/typescript.mjs";
16
16
  import { readTsconfig } from "./lib/alias/tsconfig.mjs";
17
+ import { checkLucide } from "./lib/doctor/lucide.mjs";
17
18
  import { generateRegistry, loadIconTables } from "./lib/icons/generate.mjs";
18
19
  import { generate } from "./lib/openapi/generate.mjs";
19
20
  import { loadSpec } from "./lib/openapi/load.mjs";
@@ -351,6 +352,21 @@ function doctor() {
351
352
  "nested copy under tempest-react-sdk — run `npm dedupe` or align versions; two instances break hooks/context",
352
353
  ],
353
354
  );
355
+
356
+ // lucide-react gets its own check: two copies of it do not break hooks the
357
+ // way a second React does, but they duplicate bytes and can leave the
358
+ // generated icon slug tables pointing at exports the older copy lacks.
359
+ const sdkPkg = readJSON(join(ROOT, "node_modules", "tempest-react-sdk", "package.json"));
360
+ checks.push(
361
+ ...checkLucide({
362
+ appSpec: deps["lucide-react"] ? String(deps["lucide-react"]) : null,
363
+ sdkSpec: sdkPkg?.dependencies?.["lucide-react"]
364
+ ? String(sdkPkg.dependencies["lucide-react"])
365
+ : null,
366
+ installedVersion: installedVersion("lucide-react"),
367
+ nestedCopy: nestedDupe("lucide-react"),
368
+ }),
369
+ );
354
370
  }
355
371
 
356
372
  // Declared-but-not-installed (package.json ↔ node_modules drift).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tempest-react-sdk",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
4
4
  "description": "SDK público da Tempest com componentes, hooks e integrações para projetos React.",
5
5
  "type": "module",
6
6
  "license": "MIT",