Workspaces & TypeScript Setup¶
This document explains how the monorepo is structured with npm workspaces and how TypeScript and the various build tools are configured.
npm Workspaces¶
The root package.json declares two workspace globs:
npm automatically creates symlinks under node_modules/@platform/<name> for every module it finds in those directories. This means importing @platform/core/client resolves to modules/platform/core/src/client/index.ts through the exports field in each module's package.json.
Module package.json¶
Each module has a minimal package.json:
{
"name": "@platform/core",
"version": "1.0.0",
"private": true,
"type": "module",
"exports": {
"./client": "./src/client/index.ts",
"./server": "./src/server/index.ts",
"./common": "./src/common/index.ts"
},
"dependencies": {
"@platform/schema": "*"
}
}
Key points:
- exports lists only the contexts that physically exist in src/.
- dependencies contains only inter-module references. Third-party packages are declared in the root package.json and hoisted by npm.
- The version "*" (or "workspace:*" in yarn/pnpm) resolves to the local symlink.
Third-Party Dependencies¶
All third-party packages (React, Express, MongoDB, Socket.io, MUI, RxJS, Zod, …) are declared only in the root package.json. npm workspaces hoist them into the root node_modules/, ensuring a single installed version.
Native TypeScript — No Intermediate Compilation¶
The project uses native TypeScript throughout the build pipeline. Modules are never compiled individually:
- The
exportsfields point at.tssource files. - There is no
dist/folder inside modules and notsc --buildstep between modules. - In development,
tsxruns the server directly from TypeScript sources. - At production build time:
- Client: Vite compiles and bundles from TypeScript sources.
- Server: esbuild bundles the server entry point into
dist/index.cjs.
TypeScript Configuration¶
Root tsconfig.json¶
{
"compilerOptions": {
"target": "ESNext",
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"incremental": true,
"noEmit": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["node", "jest"],
"paths": {
"@platform/*/client": ["./modules/platform/*/src/client/index.ts"],
"@platform/*/server": ["./modules/platform/*/src/server/index.ts"],
"@platform/*/common": ["./modules/platform/*/src/common/index.ts"],
"@metaltura/core": ["./modules/platform/metaltura/src/common/context/types/core.d.ts"]
}
},
"include": ["modules/**/*"],
"exclude": ["dist", "node_modules"]
}
The paths mapping gives TypeScript the same module resolution as npm workspaces, so IDE tooling and type-checking work without compiling.
Per-Module tsconfig.json¶
The extends path depth depends on the module's nesting level (../../tsconfig.json for two levels, ../../../tsconfig.json for three levels).
Import Conventions¶
Inter-Module Imports¶
Always import from the context barrel — never from an internal file:
// Correct
import { Doc } from '@platform/doc/common'
import { socketServerService } from '@platform/socket/server'
import { CoreButton } from '@platform/core/client'
// Wrong — never import from internal paths
import { Doc } from '@platform/doc/src/common/entities/Doc'
Intra-Module Imports¶
Inside a single module, use relative paths for neighbouring files:
// Inside modules/platform/core/src/client/workbench/Workbench.tsx
import { SideBar } from './sideBar/SideBar'
import { TranslationText } from '../../common/i18n'
Self-Import via #common / #client / #server¶
When a file deep inside src/client/ needs to import from the same module's src/common/ (or src/server/), using a long relative path (../../common/...) is fragile and verbose. Instead, every module's package.json declares an imports field that creates shorthand aliases pointing to the module's own barrel exports:
{
"imports": {
"#client": "./src/client/index.ts",
"#server": "./src/server/index.ts",
"#common": "./src/common/index.ts"
}
}
This means any file in the module can write:
// Instead of: import { Element } from '../../common/context/classes/Element'
import { Element, Workspace } from '#common'
// Instead of: import { useWorkspace } from '../../client/hooks/useWorkspace'
import { useWorkspace } from '#client'
The # prefix is a Node.js / bundler convention for package self-referencing. TypeScript resolves it because Vite and tsx both honour the imports field.
Rule: use #common / #client / #server for cross-context imports within the same module. Use @platform/<name>/common (etc.) for imports from a different module. Never mix the two directions.
Vite (Client Build)¶
vite.config.ts at the root:
export default defineConfig({
plugins: [react()],
root: './modules/application/app/src/client',
server: {
host: '0.0.0.0',
port: 8082,
proxy: {
'/socket.io': { target: 'http://localhost:8081', changeOrigin: true }
}
},
build: {
outDir: path.resolve(__dirname, 'dist/public'),
emptyOutDir: true
}
})
Vite resolves @platform/* imports through the npm workspace symlinks — no alias configuration needed.
Jest (Tests)¶
jest.config.ts uses ts-jest in ESM mode:
{
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
roots: ['<rootDir>/modules'],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
moduleNameMapper: {
'\\.(css|less|sass|scss)$': '<rootDir>/test/__mocks__/styleMock.js',
'\\.(gif|ttf|eot|svg|png)$': '<rootDir>/test/__mocks__/fileMock.js'
}
}
Development Scripts¶
| Script | Description |
|---|---|
npm run start-server |
Runs the Express server with tsx in watch mode |
npm run start-client |
Starts the Vite dev server on port 8082 |
npm run build-server |
Bundles the server with esbuild → dist/index.cjs |
npm run build-client |
Builds the client with Vite → dist/public/ |
npm test |
Runs Jest with coverage |
npm run test-watch |
Runs Jest in watch mode |
npm run type-check |
Type-checks all modules without emitting output |
Dependency Rules¶
| Rule | Rationale |
|---|---|
| Platform modules must not depend on application modules | Platform modules are reusable infrastructure; making them depend on business logic would create coupling |
| Application modules may depend on platform modules and other application modules | Business features build on infrastructure and may compose together |
| Circular dependencies are forbidden | Guaranteed by the unidirectional dependency graph |