Recipe: Adding a Package
CruzJS is a monorepo managed with pnpm workspaces. This recipe walks through creating a new package and integrating it with the rest of the project.
Step 1: Create the Package Directory
Section titled “Step 1: Create the Package Directory”mkdir -p packages/my-package/srcStep 2: Create package.json
Section titled “Step 2: Create package.json”{ "name": "@cruzjs/my-package", "version": "0.0.1", "private": true, "type": "module", "exports": { ".": { "types": "./src/index.ts", "default": "./src/index.ts" }, "./*": { "types": "./src/*.ts", "default": "./src/*.ts" } }, "scripts": { "typecheck": "tsc --noEmit", "test": "vitest run" }, "dependencies": { "inversify": "^6.2.0", "reflect-metadata": "^0.2.0" }, "devDependencies": { "typescript": "^5.6.0", "vitest": "^2.0.0" }}Key points:
- Use the
@cruzjs/scope for consistency "private": trueprevents accidental publishing- The
exportsfield maps import paths to source files (no build step needed for internal packages) - The wildcard
"./*"export allows deep imports like@cruzjs/my-package/services/my-service
Step 3: Create tsconfig.json
Section titled “Step 3: Create tsconfig.json”{ "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist", "composite": true, "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "**/*.test.ts"]}If you don’t have a shared tsconfig.base.json, create the full config:
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "esModuleInterop": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "rootDir": "src", "outDir": "dist", "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "**/*.test.ts"]}Step 4: Create the Entry Point
Section titled “Step 4: Create the Entry Point”export { MyService } from './services/my-service';export { MyModule } from './my-package.module';export type { MyConfig } from './types';Step 5: Add Services
Section titled “Step 5: Add Services”import { injectable, inject } from 'inversify';import { DRIZZLE, type DrizzleDatabase,} from '@cruzjs/core/shared/database/drizzle.service';
@injectable()export class MyService { constructor(@inject(DRIZZLE) private readonly db: DrizzleDatabase) {}
async doSomething(): Promise<string> { return 'Hello from @cruzjs/my-package'; }}Step 6: Create the Module
Section titled “Step 6: Create the Module”import { Module } from '@cruzjs/core/di';import { MyService } from './services/my-service';
@Module({ providers: [MyService],})export class MyModule {}Step 7: Add to Workspace
Section titled “Step 7: Add to Workspace”Ensure the package is included in the workspace root pnpm-workspace.yaml:
packages: - 'packages/*' - 'apps/*' - 'external-processes/*'If the packages/* glob is already there (it is by default), your new package is automatically included.
Step 8: Add as a Dependency
Section titled “Step 8: Add as a Dependency”Add your package as a dependency in the consuming app:
cd apps/webpnpm add @cruzjs/my-package --workspaceThis adds a workspace reference to apps/web/package.json:
{ "dependencies": { "@cruzjs/my-package": "workspace:*" }}Step 9: Import and Use
Section titled “Step 9: Import and Use”import { createCruzApp } from '@cruzjs/core';import { CloudflareAdapter } from '@cruzjs/adapter-cloudflare';import * as schema from './database/schema';import { MyModule } from '@cruzjs/my-package';
export default createCruzApp({ schema, modules: [MyModule], adapter: new CloudflareAdapter(), pages: () => import('virtual:react-router/server-build'),});// In any service or routerimport { MyService } from '@cruzjs/my-package';
const myService = ctx.container.get(MyService);const result = await myService.doSomething();Step 10: Add Database Schema (Optional)
Section titled “Step 10: Add Database Schema (Optional)”If your package needs its own database tables:
import { sqliteTable, text, index } from 'drizzle-orm/sqlite-core';import { createId } from '@paralleldrive/cuid2';
const generateId = () => createId();const nowISO = () => new Date().toISOString();
export const myTable = sqliteTable('MyTable', { id: text('id').primaryKey().$defaultFn(generateId), name: text('name').notNull(), createdAt: text('createdAt').notNull().$defaultFn(nowISO),});Re-export from the app’s schema:
export * from '@cruzjs/start/database/schema';export * from '@cruzjs/my-package/database/schema';Then generate migrations:
cruz db generatecruz db migratePackage Structure Template
Section titled “Package Structure Template”packages/my-package/ src/ database/ schema.ts # Drizzle tables (optional) services/ my-service.ts # Business logic events/ my-event.ts # Domain events (optional) my-package.module.ts # @Module declaration index.ts # Public exports types.ts # Shared types package.json tsconfig.json- No build step needed for internal packages — Vite and TypeScript resolve source files directly via the
exportsfield - Avoid circular dependencies between packages. If
@cruzjs/my-packageneeds@cruzjs/core, that is fine. If@cruzjs/coreneeds@cruzjs/my-package, you have a circular dependency and should restructure - Keep packages focused — each package should have a single responsibility (analytics, notifications, integrations, etc.)
- Re-export types from
index.tsso consumers have a clean import path
Next Steps
Section titled “Next Steps”- CRUD Feature Recipe — Build features within packages
- Feature Module Recipe — Register package modules
- Architecture — Understand module loading