diff --git a/docker/images/runners/Dockerfile b/docker/images/runners/Dockerfile index c3d9509feaa..5790bac8bb7 100644 --- a/docker/images/runners/Dockerfile +++ b/docker/images/runners/Dockerfile @@ -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 diff --git a/packages/@n8n/engine/package.json b/packages/@n8n/engine/package.json index c5a2103a2c8..98d0b1df744 100644 --- a/packages/@n8n/engine/package.json +++ b/packages/@n8n/engine/package.json @@ -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", diff --git a/packages/@n8n/module-cli/src/frontend.mjs b/packages/@n8n/module-cli/src/frontend.mjs index 8019ffdd850..5aff0e4536e 100644 --- a/packages/@n8n/module-cli/src/frontend.mjs +++ b/packages/@n8n/module-cli/src/frontend.mjs @@ -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. diff --git a/packages/@n8n/module-cli/src/frontend.test.mjs b/packages/@n8n/module-cli/src/frontend.test.mjs index 8e8281f5049..833b89f81e8 100644 --- a/packages/@n8n/module-cli/src/frontend.test.mjs +++ b/packages/@n8n/module-cli/src/frontend.test.mjs @@ -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) => { diff --git a/packages/@n8n/typeorm/package.json b/packages/@n8n/typeorm/package.json index 3e8f9a8d273..32883aa8608 100644 --- a/packages/@n8n/typeorm/package.json +++ b/packages/@n8n/typeorm/package.json @@ -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", diff --git a/packages/frontend/editor-ui/eslint.config.mjs b/packages/frontend/editor-ui/eslint.config.mjs index 8ae30276ee6..0a08b41c569 100644 --- a/packages/frontend/editor-ui/eslint.config.mjs +++ b/packages/frontend/editor-ui/eslint.config.mjs @@ -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', diff --git a/packages/frontend/editor-ui/package.json b/packages/frontend/editor-ui/package.json index 187bf809c28..f23c0a893a1 100644 --- a/packages/frontend/editor-ui/package.json +++ b/packages/frontend/editor-ui/package.json @@ -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" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0430b8946d..c83f13de214 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/scripts/build-n8n.mjs b/scripts/build-n8n.mjs index 91cc24e3491..55faaa6a619 100755 --- a/scripts/build-n8n.mjs +++ b/scripts/build-n8n.mjs @@ -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, + // `@file+packages+` on Linux and `@file++++` 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 / diff --git a/scripts/smoke-n8n-image.mjs b/scripts/smoke-n8n-image.mjs index 974042845a2..a7e7f4365fa 100644 --- a/scripts/smoke-n8n-image.mjs +++ b/scripts/smoke-n8n-image.mjs @@ -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: + // `@file++++home+runner+…+packages+_`. + 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);