This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
When working on complex tasks, use extended thinking for deeper analysis:
- ultrathink / think harder - Maximum depth reasoning for architectural decisions, complex debugging, and thorough code review
- think - Standard extended thinking for moderate complexity tasks
Example prompts:
- "ultrathink about the best way to implement this feature"
- "think harder about potential edge cases in this code"
Use ultrathink for:
- Architectural design decisions
- Complex refactoring across multiple files
- Debugging intricate issues
- Security vulnerability analysis
- Performance optimization strategies
cnpmcore is a TypeScript-based private NPM registry implementation for enterprise use. It's built on the Egg.js framework using Domain-Driven Design (DDD) architecture principles and supports both MySQL and PostgreSQL databases.
# Start development server (MySQL)
npm run dev
# Start development server (PostgreSQL)
npm run dev:postgresql
# Lint code
npm run lint
# Fix linting issues
npm run lint:fix
# TypeScript type checking
npm run typecheck# Run all tests with MySQL (takes 4+ minutes)
npm run test
# Run all tests with PostgreSQL (takes 4+ minutes)
npm run test:postgresql
# Run single test file (faster iteration, ~12 seconds)
npm run test:local test/path/to/file.test.ts
# Generate coverage report
npm run cov# MySQL setup
docker compose -f docker-compose.yml up -d
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh
# PostgreSQL setup
docker compose -f docker-compose-postgres.yml up -d
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-postgresql.sh# Clean build artifacts
npm run clean
# Development build
npm run tsc
# Production build
npm run tsc:prodThe codebase follows strict DDD layering with clear separation of concerns:
Controller (app/port/controller/) ← HTTP interface, validation, auth
↓ depends on
Service (app/core/service/) ← Business logic orchestration
↓ depends on
Repository (app/repository/) ← Data access layer
↓ depends on
Model (app/repository/model/) ← ORM/Database mapping
Entity (app/core/entity/) ← Pure domain models (no dependencies)
Common (app/common/) ← Utilities and adapters (all layers)
Controller Layer (app/port/controller/):
- Handle HTTP requests/responses
- Validate inputs using
@eggjs/typebox-validate - Authenticate users and verify authorization
- Delegate business logic to Services
- All controllers extend
AbstractController→MiddlewareController - Auto-applied middlewares:
AlwaysAuth,Tracing,ErrorHandler
Key AbstractController methods:
ensurePublishAccess(ctx, fullname)- Authorization check for package publishgetPackageEntity(scope, name)- Fetch package with error handlingsetCDNHeaders(ctx)- Set cache control headersgetAllowSync(ctx)- Check if sync should be triggered
Service Layer (app/core/service/):
- Implement core business logic
- Orchestrate multiple repositories
- Publish domain events via
EventBus - Manage transactions
- Use
@SingletonProto({ accessLevel: AccessLevel.PUBLIC })scope - Define command interfaces (e.g.,
PublishPackageCmd) for complex operations
Repository Layer (app/repository/):
- CRUD operations on Models
- Data access and persistence
- Query building and optimization
- Methods named:
findX,saveX,removeX,listXs
Entity Layer (app/core/entity/):
- Pure domain models with business behavior
- No infrastructure dependencies
- Factory pattern:
static create(data)method on each entity - Use
EntityUtil.defaultData(data, 'idField')for ID/timestamp defaults - Type guards for union types (e.g.,
isGranularToken()) EasyData<T, Id>type helper for optional create-time fields
Model Layer (app/repository/model/):
- ORM definitions using Leoric
- Database schema mapping
- No business logic
Model-Entity Bridge (app/repository/util/):
ModelConvertor.convertEntityToModel(entity, ModelClass)- Persist entityModelConvertor.convertModelToEntity(model, EntityClass)- Load entity- Uses
@EntityProperty()decorator for complex nested property mapping ModelMetadataUtilfor reflection-based property discovery
Enterprise customization layer for PaaS integration:
- NFSClientAdapter: File storage (local/S3/OSS)
- QueueAdapter: Message queue integration
- AuthAdapter: Authentication system
- BinaryAdapter: Binary package storage
- SearchAdapter: Elasticsearch integration
- CacheAdapter: Redis caching with distributed locking
HTTP Layer (from @eggjs/tegg):
@HTTPController()- Marks class as HTTP controller@HTTPMethod({ path, method })- Route definition@HTTPBody()/@HTTPQuery()/@HTTPParam()- Request data injection with typebox validation@HTTPContext() ctx: Context- Inject Egg.js context@Middleware(MiddlewareClass)- Apply middleware to controller
Dependency Injection:
@Inject()- Field-level dependency injection@SingletonProto({ accessLevel: AccessLevel.PUBLIC })- Singleton service scope@ContextProto()- Request-scoped service (e.g.,UserRoleManager)
ORM Layer (from leoric):
@Model()- Marks class as ORM model@Attribute(DataTypes.TYPE, options)- Column definition@EntityProperty('nested.path')- Maps model field to entity property
Lifecycle:
@LifecycleInit()- Post-construction initialization hook
Always validate requests in this exact order:
- Parameter Validation - Use
@eggjs/typebox-validatefor type-safe validation - Authentication - Get authorized user with token role verification
- Authorization - Check resource-level permissions to prevent privilege escalation
// Example controller method
async someMethod(@HTTPQuery() params: QueryType) {
// 1. Params already validated by @HTTPQuery with typebox
// 2. Authenticate
const user = await this.userRoleManager.requiredAuthorizedUser(this.ctx, 'publish');
// 3. Authorize (if needed)
const { pkg } = await this.ensurePublishAccess(this.ctx, fullname);
// 4. Execute business logic
return await this.service.doSomething(params);
}findSomething- Query single entitysaveSomething- Create or update entityremoveSomething- Delete entitylistSomethings- Query multiple entities (plural)
When changing a Model, update all 3 locations:
- SQL migrations:
sql/mysql/*.sqlANDsql/postgresql/*.sql - ORM Model:
app/repository/model/*.ts - Domain Entity:
app/core/entity/*.ts
- Linter: Oxlint (Rust-based, very fast) with type-aware checking
- Formatter: Oxfmt (sole formatter, no Prettier)
- Pre-commit: Husky + lint-staged runs both
oxfmtandoxlint --fix
Style rules:
- Single quotes (
') - 2-space indentation
- 120 character line width
- ES5 trailing commas
- Max 6 function parameters
- No console statements (use logger)
- Automatic import sorting (type imports first, then builtin, external, relative)
- Strict TypeScript enabled
- Avoid
anytypes - use proper typing orunknown - ES modules (
import/export) throughout - Comprehensive type definitions in all files
- Test files use
.test.tssuffix - Tests mirror source structure in
test/directory - Use
@eggjs/mockfor mocking - Use
assertfromnode:assert/strict - Test both success and error cases
Test Infrastructure:
test/.setup.ts- Global beforeEach/afterEach hookstest/TestUtil.ts- Comprehensive test utilitiestest/fixtures/- Mock data and responses
Key TestUtil Methods:
TestUtil.createUser(options)- Create test user with auth tokensTestUtil.createPackage(options)- Create full package in systemTestUtil.getFullPackage(options)- Get mock package JSONTestUtil.truncateDatabase()- Clear all tables between testsTestUtil.query(sql)- Execute raw SQL (MySQL/PostgreSQL)TestUtil.getFixtures(name)- Get path to shared fixture (read-only access)TestUtil.copyFixtures(name)- Copy fixture to temp directory (when test needs to write)TestUtil.cleanupFixtures()- Remove all temp directories created bycopyFixtures()
Test Fixture Access:
- Use
getFixtures()for read-only access (reading JSON, tarballs, etc.) — safe for parallel execution - Use
copyFixtures()when the test will write to the fixture directory (e.g.,npm installcreatingnode_modules, writing.npmrc) — each test gets an isolated copy - Always call
cleanupFixtures()inafter()when usingcopyFixtures()to clean up temp directories - Never write files directly into
test/fixtures/during test execution; always copy to a temp directory first
Mocking Patterns:
// Config mocking
mock(app.config.cnpmcore, 'propertyName', newValue);
// HTTP mocking
app.mockHttpclient('https://example.com/path', 'GET', {
data: await TestUtil.readFixturesFile('fixture.json'),
persist: false,
});
// Log assertions
app.mockLog();
await someOperation();
app.expectLog(/pattern/);Getting DI Objects in Tests:
const service = await app.getEggObject(PackageManagerService);Test Pattern:
describe('test/path/to/SourceFile.test.ts', () => {
describe('[HTTP_METHOD /api/path] functionName()', () => {
it('should handle expected behavior', async () => {
const res = await app
.httpRequest()
.put('/path')
.set('authorization', user.authorization)
.send(payload)
.expect(201);
assert.equal(res.body.success, true);
});
});
});Scheduled Task Testing:
await app.runSchedule(SyncPackageWorkerPath);app/
├── common/ # Global utilities and adapters
│ ├── adapter/ # External service adapters
│ └── enum/ # Shared enumerations
├── core/ # Business logic layer
│ ├── entity/ # Domain models
│ ├── event/ # Event handlers
│ ├── service/ # Business services
│ └── util/ # Internal utilities
├── port/ # Interface layer
│ ├── controller/ # HTTP controllers
│ ├── middleware/ # Middleware
│ └── schedule/ # Background jobs
├── repository/ # Data access layer
│ └── model/ # ORM models
└── infra/ # Infrastructure adapters
config/ # Configuration files
sql/ # Database migrations
├── mysql/ # MySQL migrations
└── postgresql/ # PostgreSQL migrations
test/ # Test files (mirrors app/ structure)
config/config.default.ts- Main application configurationconfig/database.ts- Database connection settingsconfig/binaries.ts- Binary package mirror configurations.env- Environment-specific variables (copy from.env.example)tsconfig.json- TypeScript settings (target: ES2021 for Leoric compatibility)
- Setup: Copy
.env.exampleto.env, start Docker services, initialize database - Feature Development: Follow bottom-up approach (Model → Entity → Repository → Service → Controller)
- Testing: Write tests at appropriate layer, run individual tests for fast iteration
- Validation: Run linter, typecheck, relevant tests before committing
- Commit: Use semantic commit messages (feat/fix/docs/test/chore)
cnpmcore can be integrated into Egg.js/Tegg applications as an NPM package, allowing enterprises to:
- Customize infrastructure adapters (storage, auth, queue)
- Override default behavior while receiving updates
- Integrate with existing enterprise systems
See INTEGRATE.md for detailed integration guide.
Typical command execution times:
- Development server startup: ~20 seconds
- TypeScript build: ~6 seconds
- Full test suite: 4-15 minutes
- Single test file: ~12 seconds
- Linting: <1 second
- Database initialization: <2 seconds
- Node.js: ^20.18.0 or ^22.18.0 or ^24.11.0
- Database: MySQL 5.7+ or PostgreSQL 17+ (SQLite support in progress)
- Cache: Redis 6+
- Optional: Elasticsearch 8.x
Core components to understand:
- PackageController: Package CRUD operations
- PackageManagerService: Core package management logic
- BinarySyncerService: Binary package synchronization
- ChangesStreamService: NPM registry change stream processing
- UserController: User authentication and profiles