build: Deploy production closures without pnpm deploy --legacy (no-changelog) (#37890)

This commit is contained in:
Declan Carroll 2026-09-07 07:01:04 +00:00 committed by GitHub
parent 30be927af8
commit 73ad071adf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 478 additions and 372 deletions

View File

@ -141,8 +141,10 @@ RUN apk add --no-cache ca-certificates tini libstdc++ libc6-compat && \
# Bring pnpm over, to make the image easier to extend. Node no longer bundles
# corepack, and this runtime has no npm, so pnpm is copied in as a package.
# Link the .mjs entry point: the package ships bin/pnpm.cjs without the execute
# bit, so a link to it fails with "Permission denied".
COPY --from=node-alpine /usr/local/lib/node_modules/pnpm /usr/local/lib/node_modules/pnpm
RUN ln -s ../lib/node_modules/pnpm/bin/pnpm.cjs /usr/local/bin/pnpm
RUN ln -s ../lib/node_modules/pnpm/bin/pnpm.mjs /usr/local/bin/pnpm
RUN addgroup -g 1000 -S runner \
&& adduser -u 1000 -S -G runner -h /home/runner -D runner

View File

@ -7,7 +7,7 @@
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"build:unchecked": "tsc -p tsconfig.build.json --noCheck",
"build:docker": "pnpm build && rimraf compiled && DOCKER_BUILD=true NODE_ENV=production pnpm --filter . --prod --legacy deploy --no-optional compiled && docker build -f ../../../docker/images/engine/Dockerfile -t n8n-engine:local .",
"build:docker": "pnpm build && rimraf compiled && DOCKER_BUILD=true NODE_ENV=production pnpm --filter . --prod --config.inject-workspace-packages=true deploy --no-optional compiled && rimraf compiled/pnpm-lock.yaml compiled/pnpm-workspace.yaml compiled/node_modules/.pnpm-workspace-state-v1.json && docker build -f ../../../docker/images/engine/Dockerfile -t n8n-engine:local .",
"start": "node dist/serve.js",
"format": "biome format --write src",
"format:check": "biome ci src",

View File

@ -58,8 +58,9 @@ const files = (name) => [
*
* 1. `@n8n/frontend-vite-config/index.ts` gets an entry in `modulePackages`. The shell then
* resolves the module to its `src`. The table is a hand-made list.
* 2. `editor-ui/package.json` gets the dependency. pnpm then links the package, and a bare
* import of it also resolves outside Vite, for vue-tsc and for Node.
* 2. `editor-ui/package.json` gets the devDependency. pnpm then links the package, and a bare
* import of it also resolves outside Vite, for vue-tsc and for Node. editor-ui bundles
* everything it imports, so all of its dependencies are devDependencies.
* 3. `editor-ui/tsconfig.json` gets the two `paths` entries. vue-tsc then reads the same source
* that Vite reads. `editor-ui/vite/aliases.test.ts` fails when 1 and 3 disagree.
* 4. `editor-ui/src/app/modules.manifest.ts` gets the descriptor.
@ -112,7 +113,7 @@ export const createFrontend = ({ name, packageDir, substitutions, root = repoRoo
if (lines.some((line) => line.includes(`"${packageName}"`))) return undefined;
const entry = ` "${packageName}": "workspace:*",`;
const start = lineIndex(lines, /^\s*"dependencies": \{/, 'editor-ui/package.json') + 1;
const start = lineIndex(lines, /^\s*"devDependencies": \{/, 'editor-ui/package.json') + 1;
let at = start;
while (at < lines.length && !/^\s*\},?\s*$/.test(lines[at])) {
const [, dependency] = /^\s*"([^"]+)":/.exec(lines[at]) ?? [];
@ -124,7 +125,7 @@ export const createFrontend = ({ name, packageDir, substitutions, root = repoRoo
return lines;
})
) {
record(target.editorUiPackage, 'editor-ui/package.json (dependency)');
record(target.editorUiPackage, 'editor-ui/package.json (devDependency)');
}
// 3. The tsconfig paths. Keep them next to the SDK, which every module depends on.

View File

@ -121,7 +121,7 @@ describe('createFrontend', () => {
expect(edits.map((edit) => edit.note)).toEqual([
'@n8n/frontend-vite-config/index.ts (Vite alias)',
'editor-ui/package.json (dependency)',
'editor-ui/package.json (devDependency)',
'editor-ui/tsconfig.json (paths)',
'editor-ui/src/app/modules.manifest.ts (registration)',
]);
@ -169,7 +169,7 @@ describe('createFrontend', () => {
scaffold(root, 'aaa-feature');
const lines = read(root, 'editorUiPackage').split('\n');
const opens = lines.findIndex((line) => /^\s*"dependencies": \{/.test(line));
const opens = lines.findIndex((line) => /^\s*"devDependencies": \{/.test(line));
const names = [];
for (let at = opens + 1; !/^\s*\},?\s*$/.test(lines[at]); at++) {
const [, dependency] = /^\s*"([^"]+)":/.exec(lines[at]) ?? [];
@ -239,7 +239,7 @@ describe('createFrontend', () => {
it.each([
['viteConfig', /^export const modulePackages/, 'frontend-vite-config/index.ts'],
['editorUiPackage', /^\s*"dependencies": \{/, 'editor-ui/package.json'],
['editorUiPackage', /^\s*"devDependencies": \{/, 'editor-ui/package.json'],
['editorUiTsconfig', SDK_PATHS, 'editor-ui/tsconfig.json'],
['manifest', /^\];/, 'modules.manifest.ts'],
])('names %s when its anchor is gone', (key, pattern, file) => {

View File

@ -76,6 +76,7 @@
"eslint": "^8.44.0",
"mocha": "^10.2.0",
"pg": "catalog:",
"pg-native": "^3.5.2",
"pg-query-stream": "^4.10.3",
"prettier": "^2.8.3",
"rimraf": "^4.1.2",

View File

@ -367,12 +367,11 @@ export default defineConfig(
},
},
{
// The CodeMirror TypeScript language service runs in a browser web worker, so
// Vite bundles `typescript` and `@typescript/vfs` into it and nothing resolves
// them from node_modules at runtime. They stay devDependencies to keep the
// ~24MB compiler out of the server image, which installs editor-ui's
// production closure via packages/cli.
files: ['src/features/shared/editors/plugins/codemirror/typescript/**'],
// Vite bundles everything this package imports and nothing resolves it from
// node_modules at runtime, so every dependency is a devDependency. That keeps
// the frontend libraries out of the server image, which installs editor-ui's
// production closure via packages/cli. The rule still catches imports of
// packages the manifest does not declare at all.
rules: {
'import-x/no-extraneous-dependencies': [
'error',

View File

@ -23,7 +23,8 @@
"test:changed": "janitor test-scoped",
"test:dev": "vitest --silent=false"
},
"dependencies": {
"devDependencies": {
"@codecov/vite-plugin": "^1.9.1",
"@codemirror/autocomplete": "catalog:",
"@codemirror/commands": "catalog:",
"@codemirror/lang-css": "catalog:",
@ -36,6 +37,8 @@
"@codemirror/state": "catalog:",
"@codemirror/view": "catalog:",
"@dagrejs/dagre": "^1.1.4",
"@faker-js/faker": "^8.0.2",
"@iconify/json": "catalog:",
"@internationalized/date": "catalog:frontend",
"@lezer/common": "catalog:",
"@lezer/highlight": "catalog:",
@ -48,100 +51,29 @@
"@n8n/composables": "workspace:*",
"@n8n/constants": "workspace:*",
"@n8n/design-system": "workspace:*",
"@n8n/eslint-config": "workspace:*",
"@n8n/frontend-constants": "workspace:*",
"@n8n/frontend-module-instance-registry": "workspace:*",
"@n8n/frontend-module-otel": "workspace:*",
"@n8n/frontend-module-sdk": "workspace:*",
"@n8n/frontend-test-utils": "workspace:*",
"@n8n/frontend-utils": "workspace:*",
"@n8n/frontend-vite-config": "workspace:*",
"@n8n/i18n": "workspace:*",
"@n8n/permissions": "workspace:*",
"@n8n/playwright-janitor": "workspace:*",
"@n8n/rest-api-client": "workspace:*",
"@n8n/stores": "workspace:*",
"@n8n/telemetry": "workspace:*",
"@n8n/utils": "workspace:*",
"@pinecone-database/connect": "0.0.4",
"@replit/codemirror-indentation-markers": "^6.5.3",
"@sentry/vue": "catalog:frontend",
"@types/semver": "catalog:",
"@vscode/markdown-it-katex": "^1.1.2",
"@vue-flow/background": "1.3.2",
"@vue-flow/controls": "1.1.3",
"@vue-flow/core": "1.48.0",
"@vue-flow/minimap": "1.5.4",
"@vue-flow/node-resizer": "1.5.0",
"@vueuse/components": "^10.11.0",
"@vueuse/core": "catalog:frontend",
"ag-grid-community": "^34.1.1",
"ag-grid-vue3": "^34.1.1",
"array.prototype.tosorted": "1.1.4",
"axios": "catalog:",
"bowser": "2.11.0",
"change-case": "catalog:",
"chart.js": "^4.4.0",
"comlink": "^4.4.1",
"core-js": "^3.40.0",
"cron": "catalog:",
"curlconverter": "^4.12.0",
"dateformat": "^3.0.3",
"element-plus": "catalog:frontend",
"email-providers": "^2.0.1",
"esprima-next": "5.8.4",
"fast-json-stable-stringify": "^2.1.0",
"file-saver": "^2.0.2",
"flatted": "catalog:",
"highlight.js": "catalog:frontend",
"humanize-duration": "^3.27.2",
"jsonpath-plus": "^10.3.0",
"katex": "0.18.2",
"lodash": "catalog:",
"luxon": "catalog:",
"markdown-it": "catalog:",
"markdown-it-footnote": "^4.0.0",
"n8n-workflow": "workspace:*",
"nanoid": "catalog:",
"pinia": "catalog:frontend",
"prettier": "catalog:",
"qrcode.vue": "^3.3.4",
"regenerator-runtime": "0.14.1",
"reka-ui": "^2.5.0",
"sanitize-html": "catalog:",
"semver": "catalog:",
"stream-browserify": "^3.0.0",
"timeago.js": "^4.0.2",
"uuid": "catalog:",
"v-code-diff": "^1.13.1",
"v3-infinite-loading": "^1.2.2",
"vue": "catalog:frontend",
"vue-agile": "^2.0.0",
"vue-chartjs": "^5.2.0",
"vue-component-type-helpers": "^2.2.10",
"vue-i18n": "catalog:frontend",
"vue-json-pretty": "2.6.0",
"vue-markdown-render": "catalog:frontend",
"vue-router": "catalog:frontend",
"vue-virtual-scroller": "2.0.0-beta.8",
"vue3-touch-events": "^4.1.3",
"vuedraggable": "4.1.0",
"wa-sqlite": "github:rhashimoto/wa-sqlite#779219540f66cecaa159da32b3b8936697ba10a7",
"web-tree-sitter": "0.24.3",
"xss": "catalog:",
"yaml": "catalog:",
"zod": "catalog:",
"z-vue-scan": "^0.0.35"
},
"devDependencies": {
"@codecov/vite-plugin": "^1.9.1",
"@faker-js/faker": "^8.0.2",
"@iconify/json": "catalog:",
"@n8n/eslint-config": "workspace:*",
"@n8n/frontend-test-utils": "workspace:*",
"@n8n/frontend-vite-config": "workspace:*",
"@n8n/playwright-janitor": "workspace:*",
"@n8n/stylelint-config": "workspace:*",
"@n8n/telemetry": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/utils": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@pinecone-database/connect": "0.0.4",
"@pinia/testing": "^0.1.6",
"@replit/codemirror-indentation-markers": "^6.5.3",
"@sentry/vite-plugin": "^4.3.0",
"@sentry/vue": "catalog:frontend",
"@storybook/types": "^8.6.14",
"@storybook/vue3-vite": "catalog:storybook",
"@testing-library/dom": "catalog:frontend",
@ -158,33 +90,99 @@
"@types/luxon": "catalog:",
"@types/markdown-it": "catalog:",
"@types/sanitize-html": "catalog:",
"@types/semver": "catalog:",
"@types/uuid": "catalog:",
"@typescript/vfs": "^1.6.0",
"@vitejs/plugin-legacy": "^8.0.0",
"@vitejs/plugin-vue": "catalog:frontend",
"@vitest/coverage-v8": "catalog:",
"@vscode/markdown-it-katex": "^1.1.2",
"@vue-flow/background": "1.3.2",
"@vue-flow/controls": "1.1.3",
"@vue-flow/core": "1.48.0",
"@vue-flow/minimap": "1.5.4",
"@vue-flow/node-resizer": "1.5.0",
"@vue/test-utils": "catalog:frontend",
"@vueuse/components": "^10.11.0",
"@vueuse/core": "catalog:frontend",
"ag-grid-community": "^34.1.1",
"ag-grid-vue3": "^34.1.1",
"array.prototype.tosorted": "1.1.4",
"autoprefixer": "catalog:frontend",
"axios": "catalog:",
"bowser": "2.11.0",
"browserslist": "4.28.1",
"browserslist-to-esbuild": "^2.1.1",
"change-case": "catalog:",
"chart.js": "^4.4.0",
"comlink": "^4.4.1",
"core-js": "^3.40.0",
"cron": "catalog:",
"curlconverter": "^4.12.0",
"dateformat": "^3.0.3",
"element-plus": "catalog:frontend",
"email-providers": "^2.0.1",
"eslint-plugin-oxlint": "catalog:",
"esprima-next": "5.8.4",
"fake-indexeddb": "^6.0.0",
"fast-json-stable-stringify": "^2.1.0",
"file-saver": "^2.0.2",
"flatted": "catalog:",
"highlight.js": "catalog:frontend",
"humanize-duration": "^3.27.2",
"jsonpath-plus": "^10.3.0",
"katex": "0.18.2",
"lodash": "catalog:",
"lodash-es": "catalog:",
"luxon": "catalog:",
"markdown-it": "catalog:",
"markdown-it-footnote": "^4.0.0",
"miragejs": "^0.1.48",
"n8n-workflow": "workspace:*",
"nanoid": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"pinia": "catalog:frontend",
"prettier": "catalog:",
"qrcode.vue": "^3.3.4",
"regenerator-runtime": "0.14.1",
"reka-ui": "^2.5.0",
"sanitize-html": "catalog:",
"sass-embedded": "catalog:",
"semver": "catalog:",
"storybook": "catalog:storybook",
"stream-browserify": "^3.0.0",
"stylelint": "catalog:",
"timeago.js": "^4.0.2",
"typescript": "catalog:",
"unplugin-icons": "catalog:frontend",
"uuid": "catalog:",
"v-code-diff": "^1.13.1",
"v3-infinite-loading": "^1.2.2",
"vite": "catalog:",
"vite-plugin-node-polyfills": "^0.25.0",
"vite-plugin-static-copy": "4.0.0",
"vite-svg-loader": "catalog:frontend",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:",
"vue-tsc": "catalog:frontend"
"vue": "catalog:frontend",
"vue-agile": "^2.0.0",
"vue-chartjs": "^5.2.0",
"vue-component-type-helpers": "^2.2.10",
"vue-i18n": "catalog:frontend",
"vue-json-pretty": "2.6.0",
"vue-markdown-render": "catalog:frontend",
"vue-router": "catalog:frontend",
"vue-tsc": "catalog:frontend",
"vue-virtual-scroller": "2.0.0-beta.8",
"vue3-touch-events": "^4.1.3",
"vuedraggable": "4.1.0",
"wa-sqlite": "github:rhashimoto/wa-sqlite#779219540f66cecaa159da32b3b8936697ba10a7",
"web-tree-sitter": "0.24.3",
"xss": "catalog:",
"yaml": "catalog:",
"z-vue-scan": "^0.0.35",
"zod": "catalog:"
},
"license": "LicenseRef-n8n-sustainable-use"
}

View File

@ -3931,9 +3931,6 @@ importers:
mkdirp:
specifier: ^2.1.3
version: 2.1.3
pg-native:
specifier: ^3.5.2
version: 3.8.0
reflect-metadata:
specifier: 'catalog:'
version: 0.2.2
@ -4007,6 +4004,9 @@ importers:
pg:
specifier: 8.21.0
version: 8.21.0(pg-native@3.8.0)
pg-native:
specifier: ^3.5.2
version: 3.8.0
pg-query-stream:
specifier: ^4.10.3
version: 4.16.0(pg@8.21.0(pg-native@3.8.0))
@ -6043,7 +6043,10 @@ importers:
version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@20.19.41)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(jsdom@23.0.1(bufferutil@4.0.9)(supports-color@8.1.1)(utf-8-validate@5.0.10))(vite@8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
packages/frontend/editor-ui:
dependencies:
devDependencies:
'@codecov/vite-plugin':
specifier: ^1.9.1
version: 1.9.1(vite@8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
'@codemirror/autocomplete':
specifier: 'catalog:'
version: 6.20.0
@ -6080,6 +6083,12 @@ importers:
'@dagrejs/dagre':
specifier: ^1.1.4
version: 1.1.4
'@faker-js/faker':
specifier: ^8.0.2
version: 8.4.1
'@iconify/json':
specifier: 'catalog:'
version: 2.2.488
'@internationalized/date':
specifier: catalog:frontend
version: 3.9.0
@ -6116,6 +6125,9 @@ importers:
'@n8n/design-system':
specifier: workspace:*
version: link:../@n8n/design-system
'@n8n/eslint-config':
specifier: workspace:*
version: link:../../@n8n/eslint-config
'@n8n/frontend-constants':
specifier: workspace:*
version: link:../@n8n/frontend-constants
@ -6128,271 +6140,60 @@ importers:
'@n8n/frontend-module-sdk':
specifier: workspace:*
version: link:../@n8n/frontend-module-sdk
'@n8n/frontend-test-utils':
specifier: workspace:*
version: link:../@n8n/frontend-test-utils
'@n8n/frontend-utils':
specifier: workspace:*
version: link:../@n8n/frontend-utils
'@n8n/frontend-vite-config':
specifier: workspace:*
version: link:../@n8n/frontend-vite-config
'@n8n/i18n':
specifier: workspace:*
version: link:../@n8n/i18n
'@n8n/permissions':
specifier: workspace:*
version: link:../../@n8n/permissions
'@n8n/playwright-janitor':
specifier: workspace:*
version: link:../../testing/janitor
'@n8n/rest-api-client':
specifier: workspace:*
version: link:../@n8n/rest-api-client
'@n8n/stores':
specifier: workspace:*
version: link:../@n8n/stores
'@n8n/telemetry':
specifier: workspace:*
version: link:../../@n8n/telemetry
'@n8n/utils':
specifier: workspace:*
version: link:../../@n8n/utils
'@pinecone-database/connect':
specifier: 0.0.4
version: 0.0.4
'@replit/codemirror-indentation-markers':
specifier: ^6.5.3
version: 6.5.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.8)
'@sentry/vue':
specifier: catalog:frontend
version: 10.70.0(pinia@2.2.4(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@types/semver':
specifier: 'catalog:'
version: 7.7.0
'@vscode/markdown-it-katex':
specifier: ^1.1.2
version: 1.1.2
'@vue-flow/background':
specifier: 1.3.2
version: 1.3.2(@vue-flow/core@1.48.0(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@vue-flow/controls':
specifier: 1.1.3
version: 1.1.3(@vue-flow/core@1.48.0(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@vue-flow/core':
specifier: 1.48.0
version: 1.48.0(vue@3.5.26(typescript@6.0.2))
'@vue-flow/minimap':
specifier: 1.5.4
version: 1.5.4(@vue-flow/core@1.48.0(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@vue-flow/node-resizer':
specifier: 1.5.0
version: 1.5.0(@vue-flow/core@1.48.0(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@vueuse/components':
specifier: ^10.11.0
version: 10.11.0(vue@3.5.26(typescript@6.0.2))
'@vueuse/core':
specifier: catalog:frontend
version: 14.3.0(vue@3.5.26(typescript@6.0.2))
ag-grid-community:
specifier: ^34.1.1
version: 34.1.1
ag-grid-vue3:
specifier: ^34.1.1
version: 34.1.1(vue@3.5.26(typescript@6.0.2))
array.prototype.tosorted:
specifier: 1.1.4
version: 1.1.4
axios:
specifier: 1.18.0
version: 1.18.0(patch_hash=149e256a2a7b632497650b32816716ced972ab02a5ab00fbd8a5158a51722c49)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)
bowser:
specifier: 2.11.0
version: 2.11.0
change-case:
specifier: 'catalog:'
version: 5.4.4
chart.js:
specifier: ^4.4.0
version: 4.4.0
comlink:
specifier: ^4.4.1
version: 4.4.1
core-js:
specifier: ^3.40.0
version: 3.49.0
cron:
specifier: 'catalog:'
version: 4.4.0
curlconverter:
specifier: ^4.12.0
version: 4.12.0
dateformat:
specifier: ^3.0.3
version: 3.0.3
element-plus:
specifier: catalog:frontend
version: 2.4.3(patch_hash=fbab57fe3750e430abd5d5e7c04cbf1b6a8f9f1c9676b14c73b77d3e06ba9eee)(vue@3.5.26(typescript@6.0.2))
email-providers:
specifier: ^2.0.1
version: 2.0.1
esprima-next:
specifier: 5.8.4
version: 5.8.4
fast-json-stable-stringify:
specifier: ^2.1.0
version: 2.1.0
file-saver:
specifier: ^2.0.2
version: 2.0.5
flatted:
specifier: 3.4.2
version: 3.4.2
highlight.js:
specifier: catalog:frontend
version: 11.12.0
humanize-duration:
specifier: ^3.27.2
version: 3.27.3
jsonpath-plus:
specifier: ^10.3.0
version: 10.4.0
katex:
specifier: 0.18.2
version: 0.18.2
lodash:
specifier: 4.18.1
version: 4.18.1(patch_hash=81ead1b07fc8efda87bd2724dbd876ab49f4a0aa03f96c3770060b2ea47d9e0b)
luxon:
specifier: 'catalog:'
version: 3.7.2
markdown-it:
specifier: 'catalog:'
version: 13.0.2
markdown-it-footnote:
specifier: ^4.0.0
version: 4.0.0
n8n-workflow:
specifier: workspace:*
version: link:../../workflow
nanoid:
specifier: 'catalog:'
version: 3.3.18
pinia:
specifier: catalog:frontend
version: 2.2.4(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2))
prettier:
specifier: 'catalog:'
version: 3.6.2
qrcode.vue:
specifier: ^3.3.4
version: 3.3.4(vue@3.5.26(typescript@6.0.2))
regenerator-runtime:
specifier: 0.14.1
version: 0.14.1
reka-ui:
specifier: ^2.5.0
version: 2.5.0(patch_hash=f1cec654cb01a5751dfc1de06604bb22f9518860ea9c72fb685e770509923a2f)(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2))
sanitize-html:
specifier: 'catalog:'
version: 2.17.7
semver:
specifier: 7.7.3
version: 7.7.3
stream-browserify:
specifier: ^3.0.0
version: 3.0.0
timeago.js:
specifier: ^4.0.2
version: 4.0.2
uuid:
specifier: 'catalog:'
version: 11.1.1
v-code-diff:
specifier: ^1.13.1
version: 1.13.1(patch_hash=21588de80e591bbc1e5a068d9bce311db5254686443652945d2c7887fdafe9d9)(vue@3.5.26(typescript@6.0.2))
v3-infinite-loading:
specifier: ^1.2.2
version: 1.2.2
vue:
specifier: catalog:frontend
version: 3.5.26(typescript@6.0.2)
vue-agile:
specifier: ^2.0.0
version: 2.0.0
vue-chartjs:
specifier: ^5.2.0
version: 5.2.0(chart.js@4.4.0)(vue@3.5.26(typescript@6.0.2))
vue-component-type-helpers:
specifier: ^2.2.10
version: 2.2.12
vue-i18n:
specifier: catalog:frontend
version: 11.4.5(vue@3.5.26(typescript@6.0.2))
vue-json-pretty:
specifier: 2.6.0
version: 2.6.0(vue@3.5.26(typescript@6.0.2))
vue-markdown-render:
specifier: catalog:frontend
version: 2.2.1(vue@3.5.26(typescript@6.0.2))
vue-router:
specifier: catalog:frontend
version: 4.5.0(vue@3.5.26(typescript@6.0.2))
vue-virtual-scroller:
specifier: 2.0.0-beta.8
version: 2.0.0-beta.8(vue@3.5.26(typescript@6.0.2))
vue3-touch-events:
specifier: ^4.1.3
version: 4.1.3
vuedraggable:
specifier: 4.1.0
version: 4.1.0(patch_hash=eaa2c80f80cdc4293b0f1c9c0410823960f839fc15f155c8cb23666d5950e049)(vue@3.5.26(typescript@6.0.2))
wa-sqlite:
specifier: github:rhashimoto/wa-sqlite#779219540f66cecaa159da32b3b8936697ba10a7
version: https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/779219540f66cecaa159da32b3b8936697ba10a7
web-tree-sitter:
specifier: 0.24.3
version: 0.24.3
xss:
specifier: 'catalog:'
version: 1.0.15
yaml:
specifier: 'catalog:'
version: 2.8.3
z-vue-scan:
specifier: ^0.0.35
version: 0.0.35(patch_hash=bbd573be7e456224f369aafb4d66374523c4847964a4655a6a1654fd40e8b0f8)(vue@3.5.26(typescript@6.0.2))
zod:
specifier: 3.25.76
version: 3.25.76
devDependencies:
'@codecov/vite-plugin':
specifier: ^1.9.1
version: 1.9.1(vite@8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
'@faker-js/faker':
specifier: ^8.0.2
version: 8.4.1
'@iconify/json':
specifier: 'catalog:'
version: 2.2.488
'@n8n/eslint-config':
specifier: workspace:*
version: link:../../@n8n/eslint-config
'@n8n/frontend-test-utils':
specifier: workspace:*
version: link:../@n8n/frontend-test-utils
'@n8n/frontend-vite-config':
specifier: workspace:*
version: link:../@n8n/frontend-vite-config
'@n8n/playwright-janitor':
specifier: workspace:*
version: link:../../testing/janitor
'@n8n/stylelint-config':
specifier: workspace:*
version: link:../../@n8n/stylelint-config
'@n8n/telemetry':
specifier: workspace:*
version: link:../../@n8n/telemetry
'@n8n/typescript-config':
specifier: workspace:*
version: link:../../@n8n/typescript-config
'@n8n/utils':
specifier: workspace:*
version: link:../../@n8n/utils
'@n8n/vitest-config':
specifier: workspace:*
version: link:../../@n8n/vitest-config
'@pinecone-database/connect':
specifier: 0.0.4
version: 0.0.4
'@pinia/testing':
specifier: ^0.1.6
version: 0.1.6(pinia@2.2.4(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@replit/codemirror-indentation-markers':
specifier: ^6.5.3
version: 6.5.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.8)
'@sentry/vite-plugin':
specifier: ^4.3.0
version: 4.3.0(encoding@0.1.13)(supports-color@8.1.1)
'@sentry/vue':
specifier: catalog:frontend
version: 10.70.0(pinia@2.2.4(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@storybook/types':
specifier: ^8.6.14
version: 8.6.14(storybook@10.5.10(@types/react@18.3.31)(bufferutil@4.0.9)(prettier@3.6.2)(react@18.2.0)(utf-8-validate@5.0.10))
@ -6441,6 +6242,9 @@ importers:
'@types/sanitize-html':
specifier: 'catalog:'
version: 2.11.0
'@types/semver':
specifier: 'catalog:'
version: 7.7.0
'@types/uuid':
specifier: 'catalog:'
version: 11.0.0
@ -6456,51 +6260,195 @@ importers:
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.9(@vitest/browser@4.1.10)(vitest@4.1.9)
'@vscode/markdown-it-katex':
specifier: ^1.1.2
version: 1.1.2
'@vue-flow/background':
specifier: 1.3.2
version: 1.3.2(@vue-flow/core@1.48.0(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@vue-flow/controls':
specifier: 1.1.3
version: 1.1.3(@vue-flow/core@1.48.0(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@vue-flow/core':
specifier: 1.48.0
version: 1.48.0(vue@3.5.26(typescript@6.0.2))
'@vue-flow/minimap':
specifier: 1.5.4
version: 1.5.4(@vue-flow/core@1.48.0(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@vue-flow/node-resizer':
specifier: 1.5.0
version: 1.5.0(@vue-flow/core@1.48.0(vue@3.5.26(typescript@6.0.2)))(vue@3.5.26(typescript@6.0.2))
'@vue/test-utils':
specifier: catalog:frontend
version: 2.4.6
'@vueuse/components':
specifier: ^10.11.0
version: 10.11.0(vue@3.5.26(typescript@6.0.2))
'@vueuse/core':
specifier: catalog:frontend
version: 14.3.0(vue@3.5.26(typescript@6.0.2))
ag-grid-community:
specifier: ^34.1.1
version: 34.1.1
ag-grid-vue3:
specifier: ^34.1.1
version: 34.1.1(vue@3.5.26(typescript@6.0.2))
array.prototype.tosorted:
specifier: 1.1.4
version: 1.1.4
autoprefixer:
specifier: catalog:frontend
version: 10.4.19(postcss@8.5.23)
axios:
specifier: 1.18.0
version: 1.18.0(patch_hash=149e256a2a7b632497650b32816716ced972ab02a5ab00fbd8a5158a51722c49)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)
bowser:
specifier: 2.11.0
version: 2.11.0
browserslist:
specifier: 4.28.1
version: 4.28.1
browserslist-to-esbuild:
specifier: ^2.1.1
version: 2.1.1(browserslist@4.28.1)
change-case:
specifier: 'catalog:'
version: 5.4.4
chart.js:
specifier: ^4.4.0
version: 4.4.0
comlink:
specifier: ^4.4.1
version: 4.4.1
core-js:
specifier: ^3.40.0
version: 3.49.0
cron:
specifier: 'catalog:'
version: 4.4.0
curlconverter:
specifier: ^4.12.0
version: 4.12.0
dateformat:
specifier: ^3.0.3
version: 3.0.3
element-plus:
specifier: catalog:frontend
version: 2.4.3(patch_hash=fbab57fe3750e430abd5d5e7c04cbf1b6a8f9f1c9676b14c73b77d3e06ba9eee)(vue@3.5.26(typescript@6.0.2))
email-providers:
specifier: ^2.0.1
version: 2.0.1
eslint-plugin-oxlint:
specifier: 'catalog:'
version: 1.78.0(oxlint@1.78.0(oxlint-tsgolint@0.21.1))
esprima-next:
specifier: 5.8.4
version: 5.8.4
fake-indexeddb:
specifier: ^6.0.0
version: 6.0.0
fast-json-stable-stringify:
specifier: ^2.1.0
version: 2.1.0
file-saver:
specifier: ^2.0.2
version: 2.0.5
flatted:
specifier: 3.4.2
version: 3.4.2
highlight.js:
specifier: catalog:frontend
version: 11.12.0
humanize-duration:
specifier: ^3.27.2
version: 3.27.3
jsonpath-plus:
specifier: ^10.3.0
version: 10.4.0
katex:
specifier: 0.18.2
version: 0.18.2
lodash:
specifier: 4.18.1
version: 4.18.1(patch_hash=81ead1b07fc8efda87bd2724dbd876ab49f4a0aa03f96c3770060b2ea47d9e0b)
lodash-es:
specifier: 'catalog:'
version: 4.18.1
luxon:
specifier: 'catalog:'
version: 3.7.2
markdown-it:
specifier: 'catalog:'
version: 13.0.2
markdown-it-footnote:
specifier: ^4.0.0
version: 4.0.0
miragejs:
specifier: ^0.1.48
version: 0.1.48
n8n-workflow:
specifier: workspace:*
version: link:../../workflow
nanoid:
specifier: 'catalog:'
version: 3.3.18
oxlint:
specifier: 'catalog:'
version: 1.78.0(oxlint-tsgolint@0.21.1)
oxlint-tsgolint:
specifier: 'catalog:'
version: 0.21.1
pinia:
specifier: catalog:frontend
version: 2.2.4(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2))
prettier:
specifier: 'catalog:'
version: 3.6.2
qrcode.vue:
specifier: ^3.3.4
version: 3.3.4(vue@3.5.26(typescript@6.0.2))
regenerator-runtime:
specifier: 0.14.1
version: 0.14.1
reka-ui:
specifier: ^2.5.0
version: 2.5.0(patch_hash=f1cec654cb01a5751dfc1de06604bb22f9518860ea9c72fb685e770509923a2f)(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2))
sanitize-html:
specifier: 'catalog:'
version: 2.17.7
sass-embedded:
specifier: 'catalog:'
version: 1.98.0
semver:
specifier: 7.7.3
version: 7.7.3
storybook:
specifier: catalog:storybook
version: 10.5.10(@types/react@18.3.31)(bufferutil@4.0.9)(prettier@3.6.2)(react@18.2.0)(utf-8-validate@5.0.10)
stream-browserify:
specifier: ^3.0.0
version: 3.0.0
stylelint:
specifier: 'catalog:'
version: 16.23.0(supports-color@8.1.1)(typescript@6.0.2)
timeago.js:
specifier: ^4.0.2
version: 4.0.2
typescript:
specifier: 'catalog:'
version: 6.0.2
unplugin-icons:
specifier: catalog:frontend
version: 23.0.1(@vue/compiler-sfc@3.5.26)
uuid:
specifier: 'catalog:'
version: 11.1.1
v-code-diff:
specifier: ^1.13.1
version: 1.13.1(patch_hash=21588de80e591bbc1e5a068d9bce311db5254686443652945d2c7887fdafe9d9)(vue@3.5.26(typescript@6.0.2))
v3-infinite-loading:
specifier: ^1.2.2
version: 1.2.2
vite:
specifier: 'catalog:'
version: 8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)
@ -6519,9 +6467,60 @@ importers:
vitest-mock-extended:
specifier: 'catalog:'
version: 3.1.0(typescript@6.0.2)(vitest@4.1.9)
vue:
specifier: catalog:frontend
version: 3.5.26(typescript@6.0.2)
vue-agile:
specifier: ^2.0.0
version: 2.0.0
vue-chartjs:
specifier: ^5.2.0
version: 5.2.0(chart.js@4.4.0)(vue@3.5.26(typescript@6.0.2))
vue-component-type-helpers:
specifier: ^2.2.10
version: 2.2.12
vue-i18n:
specifier: catalog:frontend
version: 11.4.5(vue@3.5.26(typescript@6.0.2))
vue-json-pretty:
specifier: 2.6.0
version: 2.6.0(vue@3.5.26(typescript@6.0.2))
vue-markdown-render:
specifier: catalog:frontend
version: 2.2.1(vue@3.5.26(typescript@6.0.2))
vue-router:
specifier: catalog:frontend
version: 4.5.0(vue@3.5.26(typescript@6.0.2))
vue-tsc:
specifier: ^2.2.8
version: 2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2)
vue-virtual-scroller:
specifier: 2.0.0-beta.8
version: 2.0.0-beta.8(vue@3.5.26(typescript@6.0.2))
vue3-touch-events:
specifier: ^4.1.3
version: 4.1.3
vuedraggable:
specifier: 4.1.0
version: 4.1.0(patch_hash=eaa2c80f80cdc4293b0f1c9c0410823960f839fc15f155c8cb23666d5950e049)(vue@3.5.26(typescript@6.0.2))
wa-sqlite:
specifier: github:rhashimoto/wa-sqlite#779219540f66cecaa159da32b3b8936697ba10a7
version: https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/779219540f66cecaa159da32b3b8936697ba10a7
web-tree-sitter:
specifier: 0.24.3
version: 0.24.3
xss:
specifier: 'catalog:'
version: 1.0.15
yaml:
specifier: 'catalog:'
version: 2.8.3
z-vue-scan:
specifier: ^0.0.35
version: 0.0.35(patch_hash=bbd573be7e456224f369aafb4d66374523c4847964a4655a6a1654fd40e8b0f8)(vue@3.5.26(typescript@6.0.2))
zod:
specifier: 3.25.76
version: 3.25.76
packages/modules/instance-registry/frontend:
dependencies:

View File

@ -4,7 +4,7 @@
* It will:
* 1. Clean the previous build output
* 2. Run pnpm install and build
* 3. Prepare for deployment - clean package.json files
* 3. Prepare for deployment - back up package.json files
* 4. Create a pruned production deployment in 'compiled'
*/
@ -114,8 +114,8 @@ const packageBuildTime = getElapsedTime('package_build');
echo(chalk.green(`✅ Package build completed in ${formatDuration(packageBuildTime)}`));
printDivider();
// 2. Prepare for deployment - clean package.json files
echo(chalk.yellow('INFO: Performing pre-deploy cleanup on package.json files...'));
// 2. Prepare for deployment - back up package.json files
echo(chalk.yellow('INFO: Backing up package.json files before deployment...'));
// Find and backup package.json files
const packageJsonFiles = await $`cd ${config.rootDir} && find . -name "package.json" \
@ -124,8 +124,9 @@ const packageJsonFiles = await $`cd ${config.rootDir} && find . -name "package.j
-not -path "./compiled/*" \
-type f`.lines();
// Backup all package.json files. The FE trim below mutates them, and pnpm verifies
// the lockfile before running any later script, which fails until they are restored.
// Backup all package.json files. The test-controller exclusion below mutates
// packages/cli/package.json, and pnpm verifies the lockfile before running any later
// script, which fails until it is restored.
// Backups live outside the workspace: siblings would be packed into the deployment.
const packageJsonBackupDir = await fs.mkdtemp(path.join(os.tmpdir(), 'n8n-build-pkgjson-'));
for (const file of packageJsonFiles) {
@ -135,11 +136,11 @@ for (const file of packageJsonFiles) {
}
// Restore package.json files from backup. Called from the `finally` below, so it
// always runs — on the success path and on any failure the trim → deploy → strip
// → SBOM sequence below can throw. A missing backup is NOT silently skipped:
// leaving a trimmed package.json on disk unnoticed is exactly the failure that
// always runs — on the success path and on any failure the deploy → strip → SBOM
// sequence below can throw. A missing backup is NOT silently skipped:
// leaving a mutated package.json on disk unnoticed is exactly the failure that
// broke a later `pnpm` command's lockfile check in production — it saw whatever
// the trim left behind and reported the gap as an "outdated lockfile", with no
// the mutation left behind and reported the gap as an "outdated lockfile", with no
// error anywhere here.
async function restorePackageJsonFiles() {
const missing = [];
@ -163,15 +164,12 @@ async function restorePackageJsonFiles() {
let packageDeployTime = 0;
// Everything from the FE trim through the SBOM step mutates package.json files or
// the production closure, and any step in between can throw (a failed deploy, a
// strip that finds nothing left to strip, a broken closure). Wrap it all in one
// try/finally so restoration always runs, instead of relying on every failure path
// remembering to call it before exiting.
// Everything from the test-controller exclusion through the SBOM step mutates
// package.json files or the production closure, and any step in between can throw (a
// failed deploy, a strip that finds nothing left to strip, a broken closure). Wrap it
// all in one try/finally so restoration always runs, instead of relying on every
// failure path remembering to call it before exiting.
try {
// Run FE trim script
await $`cd ${config.rootDir} && node .github/scripts/trim-fe-packageJson.js`;
echo(
chalk.yellow(`INFO: Creating pruned production deployment in '${config.compiledAppDir}'...`),
);
@ -200,7 +198,32 @@ try {
process.env.PNPM_CONFIG_SHAMEFULLY_HOIST = 'true';
}
await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=n8n --prod --legacy deploy --no-optional ./compiled`;
// Both closures use pnpm's dedicated-lockfile deploy instead of `--legacy`. The legacy
// path re-resolves the whole workspace against the registry on every build (~900MB of
// metadata on a cold CI cache); the dedicated lockfile is derived from pnpm-lock.yaml
// and only copies. `inject-workspace-packages` is scoped to this command because setting
// it in pnpm-workspace.yaml changes how the workspace itself links (#32318).
// The closure is lockfile-derived, so a workspace package only stays out of the image by
// declaring build-time deps as devDependencies — editing manifests at build time does
// nothing here.
const deployFlags = ['--prod', '--config.inject-workspace-packages=true'];
// The dedicated-lockfile deploy leaves pnpm's own files in the closure, which the
// legacy deploy did not write. They are not needed to run the closure, they point at
// patch files that only exist in this checkout, and a `pnpm-lock.yaml` would make
// cdxgen (`-t pnpm`, SBOM step below) parse the lockfile instead of inventorying
// node_modules as it does today.
const pnpmDeployFiles = [
'pnpm-workspace.yaml',
'pnpm-lock.yaml',
'node_modules/.pnpm-workspace-state-v1.json',
];
const removePnpmDeployFiles = async (dir) => {
for (const file of pnpmDeployFiles) await fs.remove(path.join(dir, file));
};
await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=n8n ${deployFlags} deploy --no-optional ./compiled`;
await removePnpmDeployFiles(config.compiledAppDir);
// Strip test/example/benchmark dirs shipped inside production deps that lack a
// `files` field in their package.json. These are valid runtime deps but their
@ -235,9 +258,12 @@ try {
// out of the build context.
// Third-party source maps are 20k files that only help when debugging inside a
// dependency. First-party maps stay: source-map-support uses them for our own
// stack traces. `file+packages` is how pnpm names the workspace packages.
// stack traces. pnpm names an injected workspace package after its `file:` path,
// `<name>@file+packages+<dir>` on Linux and `<name>@file++++<abs path>` on macOS;
// the glob covers both.
const workspacePackageGlob = '*@file+*packages+*';
echo(chalk.yellow('INFO: Stripping third-party source maps...'));
await $`find ${config.compiledAppDir}/node_modules/.pnpm -name "*.map" -not -path "*file+packages*" -delete 2>/dev/null || true`;
await $`find ${config.compiledAppDir}/node_modules/.pnpm -name "*.map" -not -path ${workspacePackageGlob} -delete 2>/dev/null || true`;
echo(chalk.green('✅ Third-party source maps stripped'));
// agent-browser ships one binary per platform. The image is Alpine, so only the
@ -304,7 +330,7 @@ try {
const strippedPatterns = [
{
label: 'third-party source maps',
find: ['-name', '*.map', '-not', '-path', '*file+packages*'],
find: ['-name', '*.map', '-not', '-path', workspacePackageGlob],
},
{
// The linux names survive as hard links onto the musl builds (see above),
@ -377,7 +403,7 @@ try {
'*/@n8n/instance-ai/knowledge-base/*',
'*/dist/node-definitions/*',
// source-map-support reads these for our own stack traces.
'*file+packages*/dist/*.js.map',
`${workspacePackageGlob}/dist/*.js.map`,
// The only agent-browser binary the Alpine image can run.
'*agent-browser/bin/*linux-musl*',
// The launcher that package.json's `bin` resolves to.
@ -423,7 +449,56 @@ try {
// orders of magnitude smaller than the n8n one, so the strips above are not
// worth duplicating here — the closure figure they report covers the n8n image
// only, not the shipped total.
await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=@n8n/task-runner --prod --legacy deploy --no-optional ${config.compiledTaskRunnerDir}`;
startTimer('task_runner_deploy');
await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=@n8n/task-runner ${deployFlags} deploy --no-optional ${config.compiledTaskRunnerDir}`;
echo(
chalk.green(
`✅ Task runner deployment completed in ${formatDuration(getElapsedTime('task_runner_deploy'))}`,
),
);
// The runners image and images derived from it extend this closure with `pnpm add`
// (docker/images/runners/Dockerfile, docs "Adding extra dependencies"). That flow
// was built on the shape the legacy deploy produced, so give the closure that shape:
// - package.json keeps the source specifiers (`workspace:*`, `catalog:`), not the
// `file:` and peer-suffixed specifiers the dedicated-lockfile deploy resolves them
// to, which pnpm cannot resolve again outside the workspace.
// - The virtual store's lockfile records the closure under the project's workspace
// path, with an empty root importer, as the legacy deploy did. `pnpm add` prunes
// packages no importer needs. It only rewrites the root importer, so the closure
// stays reachable through the other one; keyed under `.` instead, the workspace
// packages (not on the registry) would fall out of the wanted importer and pnpm
// would delete the runner.
// - The workspace settings and dedicated lockfile point at patch files that only
// exist in this checkout.
echo(chalk.yellow('INFO: Making the task runner closure extendable with pnpm add...'));
await removePnpmDeployFiles(config.compiledTaskRunnerDir);
const runnerProjectDir = 'packages/@n8n/task-runner';
const runnerManifestPath = path.join(config.compiledTaskRunnerDir, 'package.json');
const sourceManifest = await fs.readJson(
path.join(config.rootDir, runnerProjectDir, 'package.json'),
);
const runnerManifest = await fs.readJson(runnerManifestPath);
for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
if (sourceManifest[field]) runnerManifest[field] = sourceManifest[field];
else delete runnerManifest[field];
}
await fs.writeJson(runnerManifestPath, runnerManifest, { spaces: 2 });
const virtualStoreLockfilePath = path.join(
config.compiledTaskRunnerDir,
'node_modules/.pnpm/lock.yaml',
);
const virtualStoreLockfile = await fs.readFile(virtualStoreLockfilePath, 'utf8');
const rootImporter = /^importers:\n\n {2}\.:\n/m;
if (!rootImporter.test(virtualStoreLockfile)) {
throw new Error(`Unexpected importers block in ${virtualStoreLockfilePath}`);
}
await fs.writeFile(
virtualStoreLockfilePath,
virtualStoreLockfile.replace(rootImporter, `importers:\n\n .: {}\n\n ${runnerProjectDir}:\n`),
);
echo(chalk.green('✅ Task runner closure is extendable'));
// Check the production closure for single-instance dependency duplication. A curated
// library resolving to more than one physical copy silently breaks instanceof /

View File

@ -21,9 +21,7 @@ const RUNNERS_IMAGES = process.env.SMOKE_RUNNERS_IMAGES
.filter(Boolean)
: [
'n8nio/runners:local',
...(process.env.DOCKER_BUILD_DISTROLESS === 'true'
? ['n8nio/runners:local-distroless']
: []),
...(process.env.DOCKER_BUILD_DISTROLESS === 'true' ? ['n8nio/runners:local-distroless'] : []),
];
const TIMEOUT = '45s';
// Matches an n8n runtime image ref (e.g. `n8nio/n8n:2.4.4`, `ghcr.io/n8n-io/n8n@sha256:…`)
@ -72,15 +70,11 @@ async function fetchCloudInvocations() {
}
}
// Do not add to this list
const KNOWN_DUPLICATED = new Map([
// pnpm materializes a package once per distinct peer resolution, and
// @langchain/community has optional peers (qdrant, mongodb, redis, …) that only
// nodes-langchain installs, so it splits into two variants, and its dependent
// @n8n/ai-utilities gets materialized twice with it. Versions already align;
// the fix is dropping ai-utilities' @langchain/community dependency
['@n8n/ai-utilities', 2],
]);
// Do not add to this list. The dedicated-lockfile deploy materializes every
// workspace package once; the legacy deploy split @n8n/ai-utilities in two
// because @langchain/community resolved its optional peers differently per
// importer.
const KNOWN_DUPLICATED = new Map();
function reportFailure(name, err) {
echo(chalk.red(`✗ ${name}`));
@ -98,7 +92,10 @@ async function runWorkspaceDedupCheck() {
})`docker run --rm --entrypoint ls ${IMAGE} /usr/local/lib/node_modules/n8n/node_modules/.pnpm`;
const variants = new Map();
for (const entry of stdout.split('\n')) {
const match = entry.match(/^(.+?)@file\+packages\+/);
// pnpm names an injected workspace package after its `file:` path, which the
// dedicated-lockfile deploy writes as an absolute URL:
// `<name>@file++++home+runner+…+packages+<dir>_<hash>`.
const match = entry.match(/^(.+?)@file\+.*packages\+/);
if (!match) continue;
const pkg = match[1].replace('+', '/');
variants.set(pkg, [...(variants.get(pkg) ?? []), entry]);
@ -175,6 +172,36 @@ async function runRunnersInterpreterCheck(image) {
}
}
// Derived images add packages with `pnpm add` in the JS runner directory (docs:
// "Adding extra dependencies"). pnpm prunes packages the recorded importer no
// longer needs, so a deploy that records the closure differently makes this
// delete the runner instead of extending it. Fails when pnpm cannot run, prunes,
// or the added package and the runner no longer both resolve.
// `minimum-release-age=0` mirrors the Dockerfile: the check is about the closure
// shape, not about how fresh the latest uuid is.
const RUNNER_JS_DIR = '/opt/runners/task-runner-javascript';
async function runRunnersExtensionCheck(image) {
const name = `pnpm add extends the JS runner in ${image}`;
const script = [
`cd ${RUNNER_JS_DIR}`,
'before=$(ls node_modules/.pnpm | wc -l)',
'pnpm add uuid --prod --no-lockfile --config.minimum-release-age=0',
'after=$(ls node_modules/.pnpm | wc -l)',
'[ "$after" -gt "$before" ] || { echo "pnpm add pruned the closure: $before -> $after packages"; exit 1; }',
`node -e "require('uuid'); require('moment'); require.resolve('n8n-core')"`,
].join(' && ');
try {
await $({
timeout: '120s',
})`docker run --rm --user root --entrypoint sh ${image} -c ${script}`;
echo(chalk.green(`✓ ${name}`));
return true;
} catch (err) {
return reportFailure(name, err);
}
}
async function run({ name, user, entrypoint, args }) {
const dockerArgs = ['run', '--rm', '--user', `${user}:${user}`];
if (entrypoint) dockerArgs.push('--entrypoint', entrypoint);
@ -213,6 +240,10 @@ const ok = (
runWorkspaceDedupCheck(),
runKafkaBindingCheck(),
...RUNNERS_IMAGES.map(runRunnersInterpreterCheck),
// The distroless image ships no shell or pnpm; only the standard image is extendable.
...RUNNERS_IMAGES.filter((image) => !image.endsWith('-distroless')).map(
runRunnersExtensionCheck,
),
])
).every(Boolean);
if (!ok) process.exit(1);