This page covers how n8n's monorepo manages dependency versions, applies patches to third-party packages, and compiles each workspace package into distributable artifacts. The focus is on tooling configuration: pnpm workspace layout, the catalog system, Turborepo task orchestration, and per-package TypeScript and Vite compilation strategies.
For CI/CD pipelines that consume these artifacts, see 9.1 and 9.2. For Docker image assembly, see 7.3 and 9.4. For the development environment setup, see 10.1.
n8n uses pnpm 10.22.0 (enforced by the packageManager field in the root package.json) with pnpm workspaces. The preinstall script at the root runs scripts/block-npm-install.js and exits with an error if npm or yarn is detected.
Workspace package roots are declared in pnpm-workspace.yaml:
| Glob | Contains |
|---|---|
packages/* | n8n-workflow, n8n-core, n8n-nodes-base, packages/cli (n8n), packages/node-dev |
packages/@n8n/* | @n8n/config, @n8n/db, @n8n/di, @n8n/task-runner, @n8n/api-types, @n8n/decorators, etc. |
packages/frontend/** | n8n-editor-ui, @n8n/design-system, @n8n/chat, @n8n/i18n, and other frontend packages |
packages/extensions/** | Extension packages |
packages/testing/** | n8n-playwright, n8n-containers |
Sources: pnpm-workspace.yaml:1-7, package.json:9
pnpm catalogs in pnpm-workspace.yaml define shared version pins referenced using the catalog: specifier in individual package.json files. This prevents version drift across packages without having to repeat exact versions in every manifest.
Catalog groups:
| Catalog | Referenced as | Example entries |
|---|---|---|
| default (unnamed) | catalog: | typescript, lodash, zod, vitest, vite, axios |
frontend | catalog:frontend | vue, pinia, vue-router, element-plus, vitest |
storybook | catalog:storybook | storybook, @storybook/vue3-vite |
e2e | catalog:e2e | @playwright/test, @currents/playwright, playwright |
sentry | catalog:sentry | @sentry/node, @sentry/profiling-node, @sentry/node-native |
Example usage in packages/cli/package.json:
Notable catalog entry ā Vite aliased to rolldown-vite:
The vite entry in the default catalog is:
vite: npm:rolldown-vite@latest
All workspace packages that declare "vite": "catalog:" transparently receive rolldown-vite (a Vite fork using the Rolldown bundler) without any changes to their own config.
Sources: pnpm-workspace.yaml:8-128, pnpm-lock.yaml:183-186
The root package.json pnpm.overrides section (mirrored in pnpm-lock.yaml overrides) forces specific versions of transitive dependencies across the entire install graph.
| Override | Effect |
|---|---|
typescript: 5.9.2 | Single TypeScript version for all tools |
zod: 3.25.67 | Prevents conflicting Zod instances |
axios: 1.13.5 | Pinned for security |
esbuild: ^0.25.0 | Consistent bundler version |
multer: ^2.0.2 | Security fix |
[email protected]: npm:[email protected] | Replaces expr-eval with a fork |
libphonenumber-js: npm:[email protected] | Strips a large unused dep |
chokidar: 4.0.3 | Pins file-watcher version |
Patches are .patch files in patches/ applied by pnpm at install time. The pnpm.patchedDependencies map in package.json associates each package+version to its patch file.
| Patched Package | Patch File |
|---|---|
[email protected] | patches/[email protected] |
[email protected] | patches/[email protected] |
@lezer/highlight | patches/@lezer__highlight.patch |
[email protected] | patches/[email protected] |
js-base64 | patches/js-base64.patch |
ics | patches/ics.patch |
[email protected] | patches/[email protected] |
@types/[email protected] | patches/@[email protected] |
@types/[email protected] | patches/@[email protected] |
[email protected] | patches/[email protected] |
[email protected] | patches/[email protected] |
minifaker | patches/minifaker.patch |
v-code-diff | patches/v-code-diff.patch |
z-vue-scan | patches/z-vue-scan.patch |
The pnpm.onlyBuiltDependencies field restricts native post-install build scripts to only isolated-vm and sqlite3, preventing unexpected native compilation from other packages.
Sources: package.json:84-166, pnpm-lock.yaml:308-413
Turborepo 2.8.9 (a root devDependency) orchestrates task execution across workspace packages in topological dependency order. All major root scripts delegate to Turbo:
| Root script | Turbo command |
|---|---|
build | turbo run build |
typecheck | turbo typecheck |
dev | turbo run dev --parallel --env-mode=loose |
lint | turbo run lint |
test | turbo run test |
clean | turbo run clean |
watch | turbo run watch --concurrency=30 |
Turbo determines task order from the dependencies and devDependencies fields in each package's package.json. A package's build task waits for all its workspace dependencies' build tasks to complete first.
Diagram: Key build task execution order (workspace package names ā build scripts)
Sources: package.json:10-51, pnpm-lock.yaml:415-1100 (importer sections), various package package.json dependency declarations
Different packages use different compilation toolchains depending on their target runtime and module format requirements.
Diagram: Build toolchain mapping ā package name ā compiler ā output format
Sources: packages/cli/package.json:10, packages/core/package.json:16, packages/workflow/package.json:26, packages/nodes-base/package.json:11, packages/@n8n/nodes-langchain/package.json:31, packages/frontend/editor-ui/package.json:9, packages/@n8n/task-runner/package.json:9, packages/@n8n/config/package.json:8
tsc-aliasMost backend packages compile with:
tsc -p tsconfig.build.json
tsc-alias -p tsconfig.build.json
tsc-alias is required because TypeScript does not rewrite paths aliases when compiling to CommonJS. tsc-alias post-processes the emitted .js files and replaces alias imports (e.g., @/services/...) with relative paths.
For watch mode, packages use tsc-watch:
tsc-watch -p tsconfig.build.json --onCompilationComplete "tsc-alias -p tsconfig.build.json"
n8n-workflow ā Dual ESM/CJS Outputpackages/workflow is the only core package that emits both ESM and CJS. The package.json exports field:
Two separate TypeScript project files drive the dual build: tsconfig.build.esm.json and tsconfig.build.cjs.json. Both are passed as arguments to a single tsc --build invocation.
Sources: packages/workflow/package.json:5-19, packages/workflow/package.json:26
After TypeScript compilation, n8n-nodes-base and @n8n/n8n-nodes-langchain run additional steps using binaries exposed by n8n-core in its bin field:
| Binary | Provided by | Purpose |
|---|---|---|
n8n-copy-static-files | n8n-core | Copies non-TypeScript assets to dist/ |
n8n-generate-translations | n8n-core | Generates locale translation metadata |
n8n-generate-metadata | n8n-core | Writes node type metadata consumed at startup |
n8n-generate-node-defs | n8n-core | Writes the node definition index |
The copy-nodes-json step copies the package's own package.json into dist/. This is required because the n8n node loader reads the n8n.nodes and n8n.credentials arrays from the installed package's package.json at runtime to discover which files to load. Placing this file in dist/ makes it available regardless of where dist/ is served from.
Sources: packages/core/package.json:7-12, packages/nodes-base/package.json:11, packages/@n8n/nodes-langchain/package.json:31-32
packages/cli ā Extra Data Build StepThe CLI build runs an additional step after TypeScript compilation:
pnpm run build:data ā node scripts/build.mjs
This script copies data files (templates and other static assets) into dist/. The published npm package includes only bin/, templates/, and dist/, as specified by the files field.
Sources: packages/cli/package.json:7-11, packages/cli/package.json:55-59
n8n-editor-ui build command:
cross-env VUE_APP_PUBLIC_PATH="/{{BASE_PATH}}/" vite build
The {{BASE_PATH}} placeholder is substituted at serve time by the packages/cli static file handler, allowing the editor to be served at configurable path prefixes (e.g., when n8n runs behind a reverse proxy at a subpath).
Sources: packages/frontend/editor-ui/package.json:9
Internal packages reference each other using "workspace:*". pnpm resolves these to symlinks in node_modules/ during pnpm install. During pnpm publish, pnpm replaces workspace:* with the actual resolved version number.
Example from packages/cli/package.json:
A @n8n/typescript-config workspace package provides base tsconfig.json files. Every package in the monorepo lists it in devDependencies as "workspace:*" and extends from it. This ensures a single source of truth for strict, target, module, and moduleResolution settings.
Sources: packages/@n8n/config/package.json:29-31, packages/cli/package.json:63, packages/workflow/package.json:41
| Script | Command | Use |
|---|---|---|
dev | turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner | All packages in watch mode (frontend + backend) |
dev:be | Same as dev plus --filter=!n8n-editor-ui | Backend-only watch mode |
dev:fe | turbo run dev --parallel --env-mode=loose --filter=n8n-editor-ui | Frontend editor watch mode |
dev:ai | Filters @n8n/nodes-langchain, n8n, n8n-core | AI nodes development |
build:n8n | node scripts/build-n8n.mjs | Production CLI artifact assembly |
build:docker | build-n8n.mjs && dockerize-n8n.mjs | Build + Docker packaging |
reset | zx scripts/reset.mjs | Clean all build artifacts and node_modules |
Sources: package.json:10-51
.github/scripts/bump-versions.mjs handles version increments before publishing. The algorithm:
pnpm ls -r --only-projects --json to list all workspace packages.n8n@* via git describe --tags.git diff --quiet HEAD <lastTag> -- <packagePath> to detect changes. Packages with changes are marked dirty.RELEASE_TYPE (patch, minor, major, experimental, premajor).package.json files and outputs the new version to stdout.For experimental type, versions use the format <major>.<minor>.<patch>-exp.<n>.
Sources: .github/scripts/bump-versions.mjs:1-65
The release pipeline in release-publish.yml publishes packages in a specific order:
The trim-fe-packageJson.js pre-publishing step removes development-only fields from frontend package.json files before they are published to npm.
Sources: .github/workflows/release-publish.yml:52-101
The generate:third-party-licenses root script runs node scripts/generate-third-party-licenses.mjs, producing THIRD_PARTY_LICENSES.md which is bundled into packages/cli/dist/.
At runtime, ThirdPartyLicensesController in packages/cli/src/controllers/third-party-licenses.controller.ts reads this file from resolve(CLI_DIR, 'THIRD_PARTY_LICENSES.md') and serves it at GET /rest/third-party-licenses. The frontend retrieves it via getThirdPartyLicenses() in packages/frontend/@n8n/rest-api-client/src/api/third-party-licenses.ts.
Sources: package.json:37, packages/cli/src/controllers/third-party-licenses.controller.ts:1-22, packages/frontend/@n8n/rest-api-client/src/api/third-party-licenses.ts:1-10
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.