Skip to content

Commit 676dc44

Browse files
fengmk2claude
andauthored
feat: display optionalDependencies in deps page (#119)
Add optionalDependencies support to the dependencies info page, displaying it alongside dependencies and devDependencies in a three-column layout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <img width="3418" height="1870" alt="image" src="https://github.com/user-attachments/assets/5c7a4d9e-c1de-4026-803e-5dac143d9dde" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Displays optional dependencies alongside regular and dev dependencies in the dependency view. * **Tests** * Adds tests covering optional dependencies display, counts, links, and behavior when version data is missing. * **Chores** * Adds test tooling and configs, test scripts, and test environment setup; updates project config to exclude test configs from compilation. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2937035 commit 676dc44

File tree

7 files changed

+205
-7
lines changed

7 files changed

+205
-7
lines changed

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"build": "next build",
88
"start": "next start",
99
"lint": "next lint",
10-
"ci": "npm run lint && npm run build"
10+
"test": "vitest run",
11+
"test:watch": "vitest",
12+
"ci": "npm run lint && npm run build && npm test"
1113
},
1214
"dependencies": {
1315
"@ant-design/cssinjs": "^1.11.1",
@@ -38,16 +40,21 @@
3840
},
3941
"repository": "https://github.com/cnpm/cnpmweb.git",
4042
"devDependencies": {
43+
"@testing-library/jest-dom": "^6.9.1",
44+
"@testing-library/react": "^16.3.1",
4145
"@types/lodash": "^4.14.197",
4246
"@types/node": "^20.4.1",
4347
"@types/npm-package-arg": "^6.1.3",
4448
"@types/react": "^18.2.14",
4549
"@types/semver": "^7.5.0",
4650
"@vercel/node": "^2.15.5",
51+
"@vitejs/plugin-react": "^5.1.2",
4752
"eslint": "^8.44.0",
4853
"eslint-config-next": "^13.4.4",
54+
"jsdom": "^27.3.0",
4955
"prettier": "^3.0.3",
50-
"typescript": "^5.3.3"
56+
"typescript": "^5.3.3",
57+
"vitest": "^4.0.16"
5158
},
5259
"overrides": {
5360
"npm-package-arg": {

src/hooks/useManifest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface NpmPackageVersion {
2626
_cnpmcore_publish_time: string;
2727
dependencies: Record<string, string>;
2828
devDependencies: Record<string, string>;
29+
optionalDependencies?: Record<string, string>;
2930
}
3031

3132
export type PackageManifest = {

src/slugs/deps/index.test.tsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect, vi } from 'vitest';
4+
import Deps from './index';
5+
import { PackageManifest } from '@/hooks/useManifest';
6+
7+
// Mock next/link
8+
vi.mock('next/link', () => ({
9+
default: ({ children, href }: { children: React.ReactNode; href: string }) => {
10+
return <a href={href}>{children}</a>;
11+
},
12+
}));
13+
14+
// Mock SizeContainer
15+
vi.mock('@/components/SizeContainer', () => ({
16+
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
17+
}));
18+
19+
const createMockManifest = (
20+
versionData: Partial<{
21+
dependencies: Record<string, string>;
22+
devDependencies: Record<string, string>;
23+
optionalDependencies: Record<string, string>;
24+
}> = {}
25+
): PackageManifest => ({
26+
name: 'test-package',
27+
maintainers: [],
28+
description: 'A test package',
29+
_source_registry_name: 'npm',
30+
'dist-tags': { latest: '1.0.0' },
31+
versions: {
32+
'1.0.0': {
33+
name: 'test-package',
34+
version: '1.0.0',
35+
publish_time: Date.now(),
36+
keywords: [],
37+
_npmUser: {},
38+
_cnpmcore_publish_time: new Date().toISOString(),
39+
dependencies: versionData.dependencies || {},
40+
devDependencies: versionData.devDependencies || {},
41+
optionalDependencies: versionData.optionalDependencies,
42+
},
43+
},
44+
});
45+
46+
describe('Deps Component', () => {
47+
describe('optionalDependencies', () => {
48+
it('should display optionalDependencies section', () => {
49+
const manifest = createMockManifest({
50+
optionalDependencies: {
51+
'fsevents': '^2.3.0',
52+
},
53+
});
54+
55+
render(<Deps manifest={manifest} version="1.0.0" />);
56+
57+
expect(screen.getByText('OptionalDependencies (1)')).toBeInTheDocument();
58+
expect(screen.getByText('fsevents')).toBeInTheDocument();
59+
expect(screen.getByText('^2.3.0')).toBeInTheDocument();
60+
});
61+
62+
it('should display empty optionalDependencies section when none exist', () => {
63+
const manifest = createMockManifest({
64+
dependencies: { 'lodash': '^4.17.21' },
65+
});
66+
67+
render(<Deps manifest={manifest} version="1.0.0" />);
68+
69+
expect(screen.getByText('OptionalDependencies (0)')).toBeInTheDocument();
70+
});
71+
72+
it('should display multiple optionalDependencies', () => {
73+
const manifest = createMockManifest({
74+
optionalDependencies: {
75+
'fsevents': '^2.3.0',
76+
'chokidar': '^3.5.0',
77+
'esbuild': '^0.19.0',
78+
},
79+
});
80+
81+
render(<Deps manifest={manifest} version="1.0.0" />);
82+
83+
expect(screen.getByText('OptionalDependencies (3)')).toBeInTheDocument();
84+
expect(screen.getByText('fsevents')).toBeInTheDocument();
85+
expect(screen.getByText('chokidar')).toBeInTheDocument();
86+
expect(screen.getByText('esbuild')).toBeInTheDocument();
87+
});
88+
});
89+
90+
describe('all dependency types together', () => {
91+
it('should display all three dependency types', () => {
92+
const manifest = createMockManifest({
93+
dependencies: {
94+
'react': '^18.2.0',
95+
'next': '^13.4.0',
96+
},
97+
devDependencies: {
98+
'typescript': '^5.0.0',
99+
'jest': '^29.0.0',
100+
'eslint': '^8.0.0',
101+
},
102+
optionalDependencies: {
103+
'fsevents': '^2.3.0',
104+
},
105+
});
106+
107+
render(<Deps manifest={manifest} version="1.0.0" />);
108+
109+
expect(screen.getByText('Dependencies (2)')).toBeInTheDocument();
110+
expect(screen.getByText('DevDependencies (3)')).toBeInTheDocument();
111+
expect(screen.getByText('OptionalDependencies (1)')).toBeInTheDocument();
112+
});
113+
114+
it('should handle missing version data gracefully', () => {
115+
const manifest = createMockManifest({});
116+
// Remove the version data
117+
manifest.versions = {};
118+
119+
render(<Deps manifest={manifest} version="1.0.0" />);
120+
121+
expect(screen.getByText('Dependencies (0)')).toBeInTheDocument();
122+
expect(screen.getByText('DevDependencies (0)')).toBeInTheDocument();
123+
expect(screen.getByText('OptionalDependencies (0)')).toBeInTheDocument();
124+
});
125+
});
126+
127+
describe('dependency links', () => {
128+
it('should create correct links for optionalDependencies', () => {
129+
const manifest = createMockManifest({
130+
optionalDependencies: {
131+
'fsevents': '^2.3.0',
132+
},
133+
});
134+
135+
render(<Deps manifest={manifest} version="1.0.0" />);
136+
137+
const link = screen.getByRole('link', { name: 'fsevents' });
138+
expect(link).toHaveAttribute('href', '/package/fsevents?version=%5E2.3.0');
139+
});
140+
});
141+
});

src/slugs/deps/index.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ const columns: TableColumnsType<DepRecord> = [
3232
export default function Deps({ manifest, version }: PageProps) {
3333
const depsInfo = React.useMemo(() => {
3434
const versionData = manifest.versions?.[version!];
35-
if (!versionData) return { dependencies: [], devDependencies: [] };
35+
if (!versionData) return { dependencies: [], devDependencies: [], optionalDependencies: [] };
3636

3737
const deps = versionData.dependencies || {};
3838
const devDeps = versionData.devDependencies || {};
39+
const optionalDeps = versionData.optionalDependencies || {};
3940

4041
return {
4142
dependencies: Object.entries(deps).map(([pkg, spec]) => ({
@@ -46,15 +47,19 @@ export default function Deps({ manifest, version }: PageProps) {
4647
package: pkg,
4748
spec,
4849
})),
50+
optionalDependencies: Object.entries(optionalDeps).map(([pkg, spec]) => ({
51+
package: pkg,
52+
spec,
53+
})),
4954
};
5055
}, [manifest, version]);
5156

52-
const { dependencies, devDependencies } = depsInfo;
57+
const { dependencies, devDependencies, optionalDependencies } = depsInfo;
5358

5459
return (
5560
<SizeContainer maxWidth="90%">
5661
<Row gutter={[8, 8]}>
57-
<Col span={12}>
62+
<Col span={8}>
5863
<Card title={`Dependencies (${dependencies.length})`}>
5964
<Table
6065
dataSource={dependencies}
@@ -64,7 +69,7 @@ export default function Deps({ manifest, version }: PageProps) {
6469
/>
6570
</Card>
6671
</Col>
67-
<Col span={12}>
72+
<Col span={8}>
6873
<Card title={`DevDependencies (${devDependencies.length})`}>
6974
<Table
7075
dataSource={devDependencies}
@@ -74,6 +79,16 @@ export default function Deps({ manifest, version }: PageProps) {
7479
/>
7580
</Card>
7681
</Col>
82+
<Col span={8}>
83+
<Card title={`OptionalDependencies (${optionalDependencies.length})`}>
84+
<Table
85+
dataSource={optionalDependencies}
86+
columns={columns}
87+
rowKey={'package'}
88+
pagination={{ size: 'small' }}
89+
/>
90+
</Card>
91+
</Col>
7792
</Row>
7893
</SizeContainer>
7994
);

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@
2525
}
2626
},
2727
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28-
"exclude": ["node_modules"]
28+
"exclude": ["node_modules", "vitest.config.ts", "vitest.setup.ts"]
2929
}

vitest.config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { defineConfig } from 'vitest/config';
2+
import react from '@vitejs/plugin-react';
3+
import path from 'path';
4+
5+
export default defineConfig({
6+
plugins: [react()],
7+
test: {
8+
globals: true,
9+
environment: 'jsdom',
10+
setupFiles: ['./vitest.setup.ts'],
11+
},
12+
resolve: {
13+
alias: {
14+
'@': path.resolve(__dirname, './src'),
15+
},
16+
},
17+
});

vitest.setup.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import '@testing-library/jest-dom/vitest';
2+
import { vi } from 'vitest';
3+
4+
// Mock matchMedia for Ant Design responsive components
5+
Object.defineProperty(window, 'matchMedia', {
6+
writable: true,
7+
value: vi.fn().mockImplementation(query => ({
8+
matches: false,
9+
media: query,
10+
onchange: null,
11+
addListener: vi.fn(),
12+
removeListener: vi.fn(),
13+
addEventListener: vi.fn(),
14+
removeEventListener: vi.fn(),
15+
dispatchEvent: vi.fn(),
16+
})),
17+
});

0 commit comments

Comments
 (0)