Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"name": "config",
"version": "0.0.0",
"scripts": {
"test": "vitest run"
},
"exports": {
"./*": "./*",
"./vite": "./vite"
Expand All @@ -17,7 +20,8 @@
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-tailwindcss": "^3.12.0",
"eslint-utils": "^3.0.0"
"eslint-utils": "^3.0.0",
"vitest": "~2.1.9"
},
"dependencies": {
"@vitejs/plugin-react": "^4.0.3",
Expand Down
70 changes: 70 additions & 0 deletions packages/config/vite/relativeAliasResolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import resolver, {
clearRelativeAliasResolverCacheForTesting,
} from "./relativeAliasResolver";

let tempDir: string;

beforeEach(async () => {
clearRelativeAliasResolverCacheForTesting();
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "cap-config-vite-"));
});

afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});

const resolveAlias = async (source: string, importer: string) => {
const result = await resolver.customResolver?.(source, importer, {});
if (typeof result !== "string") throw new Error("Expected string resolution");
return result;
Comment on lines +1 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Module-level pkgJsonCache not cleared between tests

relativeAliasResolver.ts stores a module-level pkgJsonCache that persists for the lifetime of the module instance. Within a Vitest test file, module state is shared across all test cases, so entries cached in one test remain visible to subsequent tests. The current three tests are safe (unique mkdtemp paths prevent cross-contamination), but any future test that (1) asserts a path has no package.json, (2) then creates one at that path, and (3) checks again would receive a stale false from the cache. A vi.resetModules() in beforeEach, or exporting a clearCacheForTesting() helper, would make the fixture fully isolated.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/config/vite/relativeAliasResolver.test.ts
Line: 1-20

Comment:
**Module-level `pkgJsonCache` not cleared between tests**

`relativeAliasResolver.ts` stores a module-level `pkgJsonCache` that persists for the lifetime of the module instance. Within a Vitest test file, module state is shared across all test cases, so entries cached in one test remain visible to subsequent tests. The current three tests are safe (unique `mkdtemp` paths prevent cross-contamination), but any future test that (1) asserts a path has no `package.json`, (2) then creates one at that path, and (3) checks again would receive a stale `false` from the cache. A `vi.resetModules()` in `beforeEach`, or exporting a `clearCacheForTesting()` helper, would make the fixture fully isolated.

How can I resolve this? If you propose a fix, please make it concise.

};

describe("relativeAliasResolver", () => {
it("resolves ~/ imports from a package src directory", async () => {
const srcDir = path.join(tempDir, "pkg", "src");
await fs.mkdir(path.join(srcDir, "components"), { recursive: true });
await fs.writeFile(path.join(srcDir, "components", "Button.tsx"), "");

await expect(
resolveAlias(
"~/components/Button",
path.join(srcDir, "pages", "index.tsx"),
),
).resolves.toBe(path.join(srcDir, "components", "Button.tsx"));
});

it("normalizes Windows-style importers before resolving from src", async () => {
const srcDir = path.join(tempDir, "pkg", "src");
await fs.mkdir(path.join(srcDir, "components"), { recursive: true });
await fs.writeFile(path.join(srcDir, "components", "Card.tsx"), "");

const windowsImporter = path
.join(srcDir, "pages", "index.tsx")
.replaceAll(path.sep, "\\");

await expect(
resolveAlias("~/components/Card", windowsImporter),
).resolves.toBe(path.join(srcDir, "components", "Card.tsx"));
});

it("resolves ~/ imports from the nearest package root", async () => {
const pkgDir = path.join(tempDir, "pkg");
await fs.mkdir(path.join(pkgDir, "src", "utils"), { recursive: true });
await fs.writeFile(path.join(pkgDir, "package.json"), "{}");
await fs.writeFile(path.join(pkgDir, "src", "utils", "index.ts"), "");

await expect(
resolveAlias("~/src/utils", path.join(pkgDir, "tests", "unit.test.ts")),
).resolves.toBe(path.join(pkgDir, "src", "utils", "index.ts"));
});

it("stops at the filesystem root when no package.json can be found", async () => {
await expect(
resolveAlias("~/missing/file", path.join(tempDir, "loose", "test.ts")),
).rejects.toThrow("Failed to resolve import path ~/missing/file");
});
});
Comment on lines +26 to +70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Windows path normalization is not exercised by any test

The PR description specifically calls out "make the resolver handle Windows-style importer paths" as a fix, and the implementation adds importer?.replace(/\\/g, "/"). None of the three tests pass a backslash-style path as importer, so the new normalization code goes untested. On Linux/macOS CI, path.join always produces forward slashes, meaning a backslash-containing string like "C:\\Users\\user\\project\\src\\pages\\index.tsx" would never be exercised. Adding one test case that uses a hardcoded Windows-style path string for the src/ branch would lock in the intended behaviour.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/config/vite/relativeAliasResolver.test.ts
Line: 23-53

Comment:
**Windows path normalization is not exercised by any test**

The PR description specifically calls out "make the resolver handle Windows-style importer paths" as a fix, and the implementation adds `importer?.replace(/\\/g, "/")`. None of the three tests pass a backslash-style path as `importer`, so the new normalization code goes untested. On Linux/macOS CI, `path.join` always produces forward slashes, meaning a backslash-containing string like `"C:\\Users\\user\\project\\src\\pages\\index.tsx"` would never be exercised. Adding one test case that uses a hardcoded Windows-style path string for the `src/` branch would lock in the intended behaviour.

How can I resolve this? If you propose a fix, please make it concise.

51 changes: 38 additions & 13 deletions packages/config/vite/relativeAliasResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,43 @@ import type { Alias } from "vite";

const pkgJsonCache = new Map();

const isPathRoot = (value: string) => path.dirname(value) === value;

export const clearRelativeAliasResolverCacheForTesting = () => {
pkgJsonCache.clear();
};

const resolver: Alias = {
find: /^(~\/.+)/,
replacement: "$1",
async customResolver(source, importer) {
let root: null | string = null;
const normalizedImporter = importer?.replace(/\\/g, "/");

const [_, sourcePath] = source.split("~/");

if (importer?.includes("/src/")) {
const [pkg] = importer?.split("/src/");
if (normalizedImporter?.includes("/src/")) {
const [pkg] = normalizedImporter.split("/src/");

root = `${pkg!}/src`;
root = path.normalize(`${pkg}/src`);
} else {
let parent = importer!;
if (!importer) throw new Error(`Failed to resolve import path ${source}`);

while (parent !== "/") {
let parent = importer;

while (!isPathRoot(parent)) {
parent = path.dirname(parent);

let hasPkgJson = pkgJsonCache.get(parent);

if (hasPkgJson === undefined)
try {
await fs.stat(`${parent}/package.json`);
pkgJsonCache.set(parent, (hasPkgJson = true));
await fs.stat(path.join(parent, "package.json"));
hasPkgJson = true;
pkgJsonCache.set(parent, hasPkgJson);
} catch {
pkgJsonCache.set(parent, (hasPkgJson = false));
hasPkgJson = false;
pkgJsonCache.set(parent, hasPkgJson);
}

if (hasPkgJson) {
Expand All @@ -44,13 +55,22 @@ const resolver: Alias = {
);
}

const absolutePath = `${root}/${sourcePath}`;
const absolutePath = path.join(root, sourcePath);

const folderItems = await fs.readdir(path.join(absolutePath, "../"));
const basename = sourcePath.split("/").at(-1);

if (!basename)
throw new Error(
`Failed to resolve import path ${source} in file ${importer}`,
);

const item = folderItems.find((i) =>
i.startsWith(sourcePath.split("/").at(-1)!),
)!;
const item = folderItems.find((i) => i.startsWith(basename));

if (!item)
throw new Error(
`Failed to resolve import path ${source} in file ${importer}`,
);

const fullPath = absolutePath + path.extname(item);

Expand All @@ -63,7 +83,12 @@ const resolver: Alias = {

const indexFile = directoryItems.find((i) => i.startsWith("index"));

return `${absolutePath}/${indexFile}`;
if (!indexFile)
throw new Error(
`Failed to resolve index file for ${source} in file ${importer}`,
);

return path.join(absolutePath, indexFile);
} else {
return fullPath;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/config/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
environment: "node",
exclude: ["**/node_modules/**", "**/dist/**"],
},
});
4 changes: 3 additions & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
".": "./src/index.ts"
},
"scripts": {
"test": "vitest run",
"typecheck": "tsc -b",
"build": "tsdown"
},
Expand All @@ -16,7 +17,8 @@
"react-dom": "^19.1.1",
"react-router-dom": "^6.18.0",
"tsconfig": "workspace:*",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"vitest": "~2.1.9"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.485.0",
Expand Down
48 changes: 48 additions & 0 deletions packages/utils/src/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import {
calculateStrokeDashoffset,
classNames,
getDisplayProgress,
getProgressCircleConfig,
isEmailAllowedByRestriction,
uuidFormat,
uuidParse,
} from "./helpers";

describe("helpers", () => {
it("parses and formats UUIDs", () => {
const formatted = "123e4567-e89b-12d3-a456-426614174000";
const compact = "123e4567e89b12d3a456426614174000";

expect(uuidParse(formatted)).toBe(compact);
expect(uuidFormat(compact)).toBe(formatted);
});

it("calculates circular progress values", () => {
const { radius, circumference } = getProgressCircleConfig();

expect(radius).toBe(8);
expect(circumference).toBe(2 * Math.PI * 8);
expect(calculateStrokeDashoffset(25, 80)).toBe(60);
});

it("prefers upload progress over processing progress", () => {
expect(getDisplayProgress(42, 10)).toBe(42);
expect(getDisplayProgress(undefined, 10)).toBe(10);
});

it("matches email restrictions by exact address or domain", () => {
expect(isEmailAllowedByRestriction("Member@Cap.so", "member@cap.so")).toBe(
true,
);
expect(isEmailAllowedByRestriction("hello@cap.so", "cap.so")).toBe(true);
expect(isEmailAllowedByRestriction("hello@example.com", "cap.so")).toBe(
false,
);
expect(isEmailAllowedByRestriction("hello@example.com", "")).toBe(true);
});

it("merges conditional Tailwind class names", () => {
expect(classNames("px-2", "px-4", false && "hidden")).toBe("px-4");
});
});
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.