Aller au contenu

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:

{
  "workspaces": [
    "modules/platform/*",
    "modules/application/*"
  ]
}

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 exports fields point at .ts source files.
  • There is no dist/ folder inside modules and no tsc --build step between modules.
  • In development, tsx runs 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

{
  "extends": "../../../tsconfig.json",
  "include": ["src/**/*"]
}

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