Set up npm workspaces for monorepo architecture. Use when creating a new monorepo, migrating to workspaces, or replicating the standard workspace pattern defined in this skill...
A comprehensive skill for setting up npm workspaces following the standardized monorepo architecture defined here. This skill provides an interactive workflow to analyze the target repository and generate a complete workspace configuration including TypeScript project references, subpath exports, and cross-package dependency management.
"type": "module" and subpath exports for tree-shaking*) for local package referencesexamples/ecom/backend)[!IMPORTANT] Before creating any files, you MUST complete the Requirements Gathering phase and wait for user responses.
First, analyze the target repository to understand its current state:
Before asking questions, quickly scan the repo to detect what already exists. Capture:
package.json (name, scripts, workspaces, engines, publishConfig)tsconfig.json (compilerOptions and paths).npmrc and registry configsrc/packages/ and any examples/ appsIf the repo already has workspaces, treat this as a migration or alignment task, not a greenfield setup.
Ask the user:
Setting up npm workspaces for the current repository.
Default: Current VS Code workspace / git repo
(Auto-detected from your active workspace)
Please confirm or provide:
1. Use current repo? (Y/n, or provide different path/URL)
2. Primary language: TypeScript (default) or specify other
3. Current structure: fresh repo / existing code to migrate / adding to existing workspace
Your answers (press Enter for defaults):
Ask the user:
How should packages be organized?
Default: **Nested** - Packages in `src/packages/` with examples in `examples/`
(Standard pattern: src/packages/{package-name})
Options:
1. **Use default nested structure** (recommended)
2. **Custom**: Describe your preferred structure
Your choice (1 or 2, describe if custom):
If the auto-detect step finds an existing structure, propose a migration plan instead of forcing a rewrite.
Ask the user:
What packages should be created?
Naming conventions:
- Library packages: Use descriptive names (e.g., core, utils, server)
- Web app packages: Use `app-*` prefix (e.g., app-dashboard, app-admin, app-landing)
- Example packages: Place in examples/ directory (e.g., examples/ecom/backend)
If packages already exist, list them and confirm whether they should stay as-is or be migrated.
For each package, provide:
- Package name (following conventions above)
- Description
- Dependencies on other workspace packages
Example:
- core: Base utilities, no dependencies
- utils: Shared utilities, depends on core
- server: HTTP server, depends on core, utils
- app-dashboard: Admin dashboard web app, depends on core, server
Your packages:
Ask the user:
What npm organization scope should be used?
Default: @your-scope (press Enter to use default)
Override with custom scope? (leave blank for default, or provide scope like @myorg):
Ask the user:
Which npm registry will packages be published to?
Default: **GitHub Package Registry** (https://npm.pkg.github.com)
Options:
1. **Use GitHub Package Registry** (default, press Enter)
2. **Custom private registry** (provide URL)
3. **No publishing** (private packages only, no registry config)
Your choice (1, 2 with URL, or 3):
Before changing files, produce a plan that mirrors the standard pattern in this skill and is tailored to the target repo. Include:
package.json, tsconfig.json, .npmrc, package configs)files entries derived from packagesOnly after the user approves the plan should file changes proceed.
After gathering requirements, generate the following files:
Create the root package.json with workspace configuration:
{
"name": "@{scope}/{repo-name}",
"version": "1.0.0",
"description": "{description}",
"type": "module",
"sideEffects": false,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./{package-1}": {
"types": "./src/packages/{package-1}/dist/index.d.ts",
"import": "./src/packages/{package-1}/dist/index.js"
}
},
"files": [
"dist",
"src/packages/{package-1}/dist",
"src/packages/{package-2}/dist"
],
"workspaces": [
"src/packages/{package-1}",
"src/packages/{package-2}",
"examples/{example-1}"
],
"scripts": {
"build": "npm run type-check && tsgo -b .",
"type-check": "tsgo -b . --noEmit",
"build:all:workspaces": "npm run build --workspaces",
"clean": "rimraf dist tsconfig.tsbuildinfo src/packages/*/.rslib src/packages/*/dist src/packages/*/tsconfig.tsbuildinfo src/packages/*/tsconfig.build.tsbuildinfo",
"format": "prettier --write 'src/packages/**/*.{ts,js,md}'",
"format:check": "prettier --check 'src/packages/**/*.{ts,js,md}'",
"lint": "eslint . --ext .ts --flag unstable_native_nodejs_ts_config --cache --cache-strategy content --cache-location .eslintcache --report-unused-disable-directives --no-warn-ignored",
"prepublishOnly": "npm run build",
"prepare": "husky"
},
"lint-staged": {
"*.ts": ["eslint --fix", "prettier --write"]
},
"engines": {
"node": ">=24.11.1"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
Keep exports, files, and workspaces in sync. This pattern uses explicit entries per package rather than wildcards to control what is published.
For a pixel-perfect match, mirror additional root scripts and devDependencies as needed for the target repo (for example, any repo-specific test commands).
Create strict TypeScript configuration:
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"composite": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"inlineSources": false,
"isolatedModules": true,
"lib": ["esnext", "DOM", "DOM.Iterable"],
"module": "NodeNext",
"moduleDetection": "force",
"moduleResolution": "NodeNext",
"newLine": "lf",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "./dist",
"paths": {
"*": ["./*"],
"@{scope}/*": ["./src/packages/*/src"]
},
"preserveConstEnums": true,
"removeComments": false,
"resolveJsonModule": true,
"rootDir": "src",
"sourceMap": true,
"strict": true,
"stripInternal": false,
"target": "esnext",
"typeRoots": ["./node_modules/@types"],
"types": ["node"],
"useDefineForClassFields": true,
"useUnknownInCatchVariables": true,
"verbatimModuleSyntax": true,
"noUncheckedSideEffectImports": true,
"strictBuiltinIteratorReturn": true,
"assumeChangesOnlyAffectDirectDependencies": true,
"noErrorTruncation": true,
"resolvePackageJsonExports": true,
"resolvePackageJsonImports": true
},
"include": ["src/index.ts"]
}
For each workspace package, create two configs:
tsconfig.json (development):
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": ".",
"composite": true,
"verbatimModuleSyntax": true,
"paths": {}
},
"include": ["src/**/*", "test/**/*"]
}
tsconfig.build.json (production):
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"emitDeclarationOnly": false,
"noEmit": false
},
"include": ["src/**/*"],
"exclude": ["test/**/*", "**/*.test.ts"]
}
For each workspace package:
{
"name": "@{scope}/{package-name}",
"version": "1.0.0",
"type": "module",
"sideEffects": false,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./{submodule}": {
"source": "./src/{submodule}/index.ts",
"types": "./dist/{submodule}/index.d.ts",
"import": "./dist/{submodule}/index.js"
}
},
"files": ["dist"],
"private": true,
"scripts": {
"build": "tsgo -p tsconfig.build.json",
"lint": "eslint .",
"format": "prettier --write ."
},
"dependencies": {
"@{scope}/{dep-package}": "*"
}
}
Create .npmrc for registry authentication:
@{scope}:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GH_TOKEN}
Generate the complete directory structure:
{repo-root}/
āāā package.json # Root workspace config
āāā package-lock.json # Lock file (npm install generates)
āāā tsconfig.json # Root TypeScript config
āāā .npmrc # Registry configuration
āāā .gitignore # Git ignores
āāā .editorconfig # Editor settings
āāā .prettierrc.json # Prettier config
āāā eslint.config.ts # ESLint config
āāā src/
ā āāā index.ts # Root re-exports (recommended, keep in sync with exports)
ā āāā packages/
ā āāā {package-1}/
ā ā āāā package.json
ā ā āāā tsconfig.json
ā ā āāā tsconfig.build.json
ā ā āāā README.md
ā ā āāā src/
ā ā āāā index.ts
ā āāā {package-2}/
ā āāā package.json
ā āāā tsconfig.json
ā āāā tsconfig.build.json
ā āāā README.md
ā āāā src/
ā āāā index.ts
āāā examples/ # (Optional) Example apps
āāā {example-1}/
āāā package.json
āāā tsconfig.json
āāā src/
āāā index.ts
Execute setup commands:
# 1. Install all dependencies (creates symlinks for workspace packages)
npm install
# 2. Build all packages in topological order
npm run build --workspaces
# 3. Verify workspace setup
npm ls --all
# 4. Run type check
npm run type-check
Verify the workspace setup:
package.json has "workspaces" array listing all packagespackage.json with correct "name"@{scope}/{package}"*" (workspace protocol): "@scope/other": "*""type": "module" for ESM"exports" with types and import conditionstsconfig.json files are properly linked via extendsnpm run build --workspaces.npmrc is configured for the target registrynpm ls shows no missing or invalid dependencies# Create a new repo with core, utils, and server packages
# 1. Initialize root
npm init -y
npm pkg set type="module"
npm pkg set workspaces='["src/packages/core", "src/packages/utils", "src/packages/server"]'
# 2. Create package directories
mkdir -p src/packages/{core,utils,server}/src
# 3. Initialize each package
cd src/packages/core && npm init -y && npm pkg set name="@scope/core" type="module"
cd ../utils && npm init -y && npm pkg set name="@scope/utils" type="module"
cd ../server && npm init -y && npm pkg set name="@scope/server" type="module"
# 4. Add cross-package dependency
npm pkg set dependencies.@scope/core="*" dependencies.@scope/utils="*"
# 5. Install from root
cd ../../../
npm install
Use the templates in templates/ as the canonical reference for configuration structure and placeholders.
| Issue | Solution |
|---|---|
ERESOLVE unable to resolve |
Run npm install --force or check workspace configurations |
| Package not found in workspace | Ensure package is listed in root workspaces array |
| TypeScript can't find module | Check paths in tsconfig and package exports |
| Circular dependency | Restructure packages or use interface-based injection |
| Build order wrong | npm workspaces builds in topological order automatically |
# Show workspace structure
npm ls --all
# Explain a package resolution
npm explain @scope/package-name
# Check for issues
npm doctor
# Clean rebuild
npm run clean && npm install && npm run build --workspaces