From d3ead6805981140decf7f26ccda40cc9c1248356 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 4 Mar 2025 14:59:30 +0100 Subject: [PATCH] fix(editor): Fix code node displays lint messages in wrong location (#13664) --- packages/frontend/editor-ui/package.json | 5 +- .../frontend/editor-ui/src/__tests__/setup.ts | 12 +- .../typescript/client/useTypescript.ts | 14 ++- .../worker/typescript.worker.test.ts | 118 ++++++++++++++++++ .../typescript/worker/typescript.worker.ts | 6 +- pnpm-lock.yaml | 64 +++++----- 6 files changed, 174 insertions(+), 45 deletions(-) create mode 100644 packages/frontend/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.test.ts diff --git a/packages/frontend/editor-ui/package.json b/packages/frontend/editor-ui/package.json index 227efb76b4..e1e632d887 100644 --- a/packages/frontend/editor-ui/package.json +++ b/packages/frontend/editor-ui/package.json @@ -94,11 +94,11 @@ "xss": "catalog:" }, "devDependencies": { + "@faker-js/faker": "^8.0.2", + "@iconify/json": "^2.2.228", "@n8n/eslint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", - "@faker-js/faker": "^8.0.2", - "@iconify/json": "^2.2.228", "@pinia/testing": "^0.1.6", "@types/dateformat": "^3.0.0", "@types/file-saver": "^2.0.1", @@ -111,6 +111,7 @@ "@vitejs/plugin-vue": "catalog:frontend", "@vitest/coverage-v8": "catalog:frontend", "browserslist-to-esbuild": "^2.1.1", + "fake-indexeddb": "^6.0.0", "miragejs": "^0.1.48", "unplugin-icons": "^0.19.0", "unplugin-vue-components": "^0.27.2", diff --git a/packages/frontend/editor-ui/src/__tests__/setup.ts b/packages/frontend/editor-ui/src/__tests__/setup.ts index 934017e2ba..0e268a87d2 100644 --- a/packages/frontend/editor-ui/src/__tests__/setup.ts +++ b/packages/frontend/editor-ui/src/__tests__/setup.ts @@ -1,4 +1,5 @@ import '@testing-library/jest-dom'; +import 'fake-indexeddb/auto'; import { configure } from '@testing-library/vue'; import 'core-js/proposals/set-methods-v2'; @@ -64,20 +65,21 @@ Object.defineProperty(window, 'matchMedia', { }); class Worker { - onmessage: (message: string) => void; + onmessage = vi.fn(); url: string; constructor(url: string) { this.url = url; - this.onmessage = () => {}; } - postMessage(message: string) { + postMessage = vi.fn((message: string) => { this.onmessage(message); - } + }); - addEventListener() {} + addEventListener = vi.fn(); + + terminate = vi.fn(); } Object.defineProperty(window, 'Worker', { diff --git a/packages/frontend/editor-ui/src/plugins/codemirror/typescript/client/useTypescript.ts b/packages/frontend/editor-ui/src/plugins/codemirror/typescript/client/useTypescript.ts index aef7eb5126..cfec108d3f 100644 --- a/packages/frontend/editor-ui/src/plugins/codemirror/typescript/client/useTypescript.ts +++ b/packages/frontend/editor-ui/src/plugins/codemirror/typescript/client/useTypescript.ts @@ -14,7 +14,7 @@ import { Text, type Extension } from '@codemirror/state'; import { EditorView, hoverTooltip } from '@codemirror/view'; import * as Comlink from 'comlink'; import { NodeConnectionType, type CodeExecutionMode, type INodeExecutionData } from 'n8n-workflow'; -import { ref, toRef, toValue, watch, type MaybeRefOrGetter } from 'vue'; +import { onBeforeUnmount, ref, toRef, toValue, watch, type MaybeRefOrGetter } from 'vue'; import type { LanguageServiceWorker, RemoteLanguageServiceWorkerInit } from '../types'; import { typescriptCompletionSource } from './completions'; import { typescriptWorkerFacet } from './facet'; @@ -33,11 +33,13 @@ export function useTypescript( const { debounce } = useDebounce(); const activeNodeName = ndvStore.activeNodeName; const worker = ref>(); + const webWorker = ref(); async function createWorker(): Promise { - const { init } = Comlink.wrap( - new Worker(new URL('../worker/typescript.worker.ts', import.meta.url), { type: 'module' }), - ); + webWorker.value = new Worker(new URL('../worker/typescript.worker.ts', import.meta.url), { + type: 'module', + }); + const { init } = Comlink.wrap(webWorker.value); worker.value = await init( { id: toValue(id), @@ -125,6 +127,10 @@ export function useTypescript( forceParse(editor); }); + onBeforeUnmount(() => { + if (webWorker.value) webWorker.value.terminate(); + }); + return { createWorker, }; diff --git a/packages/frontend/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.test.ts b/packages/frontend/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.test.ts new file mode 100644 index 0000000000..74fb55d816 --- /dev/null +++ b/packages/frontend/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.test.ts @@ -0,0 +1,118 @@ +import type { WorkerInitOptions } from '../types'; +import { worker } from './typescript.worker'; +import { type ChangeSet, EditorState } from '@codemirror/state'; + +async function createWorker({ + doc, + options, +}: { doc?: string; options?: Partial } = {}) { + const defaultDoc = ` +function myFunction(){ + if (true){ + const myObj = {test: "value"} + } +} + +return $input.all();`; + const state = EditorState.create({ doc: doc ?? defaultDoc }); + + const tsWorker = worker.init( + { + allNodeNames: [], + content: state.doc.toJSON(), + id: 'id', + inputNodeNames: [], + mode: 'runOnceForAllItems', + variables: [], + ...options, + }, + async () => ({ + json: { path: '', type: 'string', value: '' }, + binary: [], + params: { path: '', type: 'string', value: '' }, + }), + ); + return await tsWorker; +} + +describe('Typescript Worker', () => { + it('should return diagnostics', async () => { + const tsWorker = await createWorker(); + + expect(tsWorker.getDiagnostics()).toEqual([ + { + from: 10, + markClass: 'cm-faded', + message: "'myFunction' is declared but its value is never read.", + severity: 'warning', + to: 20, + }, + { + from: 47, + markClass: 'cm-faded', + message: "'myObj' is declared but its value is never read.", + severity: 'warning', + to: 52, + }, + ]); + }); + + it('should accept updates from the client and buffer them', async () => { + const tsWorker = await createWorker(); + // Add if statement and remove indentation + const changes = [ + [75, [0, '', ''], 22], + [76, [0, '', ''], 22], + [77, [0, ' if (true){', ' const myObj = {test: "value"}', ' }'], 22], + [77, [1], 13, [2], 30, [2], 23], + ]; + + vi.useFakeTimers({ toFake: ['setTimeout', 'queueMicrotask', 'nextTick'] }); + + for (const change of changes) { + tsWorker.updateFile(change as unknown as ChangeSet); + } + + expect(tsWorker.getDiagnostics()).toHaveLength(2); + + vi.advanceTimersByTime(1000); + vi.runAllTicks(); + + expect(tsWorker.getDiagnostics()).toHaveLength(3); + expect(tsWorker.getDiagnostics()).toEqual([ + { + from: 10, + markClass: 'cm-faded', + message: "'myFunction' is declared but its value is never read.", + severity: 'warning', + to: 20, + }, + { + from: 47, + markClass: 'cm-faded', + message: "'myObj' is declared but its value is never read.", + severity: 'warning', + to: 52, + }, + { + from: 96, + markClass: 'cm-faded', + message: "'myObj' is declared but its value is never read.", + severity: 'warning', + to: 101, + }, + ]); + }); + + it('should return completions', async () => { + const doc = 'return $input.'; + const tsWorker = await createWorker({ doc }); + + const completionResult = await tsWorker.getCompletionsAtPos(doc.length); + assert(completionResult !== null); + + const completionLabels = completionResult.result.options.map((c) => c.label); + expect(completionLabels).toContain('all()'); + expect(completionLabels).toContain('first()'); + }); +}); diff --git a/packages/frontend/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.ts b/packages/frontend/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.ts index a6467d1cfd..2f7b57dc4b 100644 --- a/packages/frontend/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.ts +++ b/packages/frontend/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.ts @@ -28,7 +28,7 @@ import { until } from '@vueuse/core'; self.process = { env: {} } as NodeJS.Process; -const worker: LanguageServiceWorkerInit = { +export const worker: LanguageServiceWorkerInit = { async init(options, nodeDataFetcher) { const loadedNodeTypesMap: Map = reactive(new Map()); @@ -157,11 +157,11 @@ const worker: LanguageServiceWorkerInit = { }); const applyChangesToCode = bufferChangeSets((bufferedChanges) => { - bufferedChanges.iterChanges((start, end, _fromNew, _toNew, text) => { + bufferedChanges.iterChanges((start, end, fromNew, _toNew, text) => { const length = end - start; env.updateFile(codeFileName, text.toString(), { - start: editorPositionToTypescript(start), + start: editorPositionToTypescript(fromNew), length, }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ba06540d3..a341dbc522 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -493,7 +493,7 @@ importers: version: 3.666.0(@aws-sdk/client-sts@3.666.0) '@getzep/zep-cloud': specifier: 1.0.12 - version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(c10b80e38f5a8711ccad1e2174de91e6)) + version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(101f2d8395211f7761efa8fce29a53de)) '@getzep/zep-js': specifier: 0.9.0 version: 0.9.0 @@ -520,7 +520,7 @@ importers: version: 0.3.2(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13) '@langchain/community': specifier: 0.3.24 - version: 0.3.24(1ea346ff95b1be1e3f1f4333b25e2811) + version: 0.3.24(6c18f51b4fff56aeb9d6961328b4ede2) '@langchain/core': specifier: 'catalog:' version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) @@ -610,7 +610,7 @@ importers: version: 23.0.1 langchain: specifier: 0.3.11 - version: 0.3.11(c10b80e38f5a8711ccad1e2174de91e6) + version: 0.3.11(101f2d8395211f7761efa8fce29a53de) lodash: specifier: 'catalog:' version: 4.17.21 @@ -1829,6 +1829,9 @@ importers: browserslist-to-esbuild: specifier: ^2.1.1 version: 2.1.1(browserslist@4.24.2) + fake-indexeddb: + specifier: ^6.0.0 + version: 6.0.0 miragejs: specifier: ^0.1.48 version: 0.1.48 @@ -8467,6 +8470,10 @@ packages: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} + fake-indexeddb@6.0.0: + resolution: {integrity: sha512-YEboHE5VfopUclOck7LncgIqskAqnv4q0EWbYCaxKKjAvO93c+TJIaBuGy8CBFdbg9nKdpN3AuPRwVBJ4k7NrQ==} + engines: {node: '>=18'} + fake-xml-http-request@2.1.2: resolution: {integrity: sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==} @@ -13312,6 +13319,9 @@ packages: vue-component-type-helpers@2.2.4: resolution: {integrity: sha512-F66p0XLbAu92BRz6kakHyAcaUSF7HWpWX/THCqL0TxySSj7z/nok5UUMohfNkkCm1pZtawsdzoJ4p1cjNqCx0Q==} + vue-component-type-helpers@2.2.8: + resolution: {integrity: sha512-4bjIsC284coDO9om4HPA62M7wfsTvcmZyzdfR0aUlFXqq4tXxM1APyXpNVxPC8QazKw9OhmZNHBVDA6ODaZsrA==} + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -15922,7 +15932,7 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(c10b80e38f5a8711ccad1e2174de91e6))': + '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(101f2d8395211f7761efa8fce29a53de))': dependencies: form-data: 4.0.0 node-fetch: 2.7.0(encoding@0.1.13) @@ -15931,7 +15941,7 @@ snapshots: zod: 3.24.1 optionalDependencies: '@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) - langchain: 0.3.11(c10b80e38f5a8711ccad1e2174de91e6) + langchain: 0.3.11(101f2d8395211f7761efa8fce29a53de) transitivePeerDependencies: - encoding @@ -16450,7 +16460,7 @@ snapshots: - aws-crt - encoding - '@langchain/community@0.3.24(1ea346ff95b1be1e3f1f4333b25e2811)': + '@langchain/community@0.3.24(6c18f51b4fff56aeb9d6961328b4ede2)': dependencies: '@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1) '@ibm-cloud/watsonx-ai': 1.1.2 @@ -16461,7 +16471,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.1.0 js-yaml: 4.1.0 - langchain: 0.3.11(c10b80e38f5a8711ccad1e2174de91e6) + langchain: 0.3.11(101f2d8395211f7761efa8fce29a53de) langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) openai: 4.78.1(encoding@0.1.13)(zod@3.24.1) uuid: 10.0.0 @@ -16476,7 +16486,7 @@ snapshots: '@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0) '@azure/storage-blob': 12.18.0(encoding@0.1.13) '@browserbasehq/sdk': 2.0.0(encoding@0.1.13) - '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(c10b80e38f5a8711ccad1e2174de91e6)) + '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(101f2d8395211f7761efa8fce29a53de)) '@getzep/zep-js': 0.9.0 '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13) '@google-cloud/storage': 7.12.1(encoding@0.1.13) @@ -18341,7 +18351,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.5.13(typescript@5.7.2) - vue-component-type-helpers: 2.2.4 + vue-component-type-helpers: 2.2.8 '@supabase/auth-js@2.65.0': dependencies: @@ -19793,14 +19803,6 @@ snapshots: transitivePeerDependencies: - debug - axios@1.7.4(debug@4.4.0): - dependencies: - follow-redirects: 1.15.6(debug@4.4.0) - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axios@1.7.7: dependencies: follow-redirects: 1.15.6(debug@4.3.6) @@ -21465,7 +21467,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -21490,7 +21492,7 @@ snapshots: eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2) eslint: 8.57.0 @@ -21510,7 +21512,7 @@ snapshots: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -21857,6 +21859,8 @@ snapshots: extsprintf@1.3.0: {} + fake-indexeddb@6.0.0: {} + fake-xml-http-request@2.1.2: {} fast-deep-equal@3.1.3: {} @@ -22001,10 +22005,6 @@ snapshots: optionalDependencies: debug: 4.3.7 - follow-redirects@1.15.6(debug@4.4.0): - optionalDependencies: - debug: 4.4.0(supports-color@8.1.1) - for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -22294,7 +22294,7 @@ snapshots: array-parallel: 0.1.3 array-series: 0.1.5 cross-spawn: 4.0.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22596,7 +22596,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 18.16.16 '@types/tough-cookie': 4.0.2 - axios: 1.7.4(debug@4.4.0) + axios: 1.7.4 camelcase: 6.3.0 debug: 4.4.0(supports-color@8.1.1) dotenv: 16.4.5 @@ -22606,7 +22606,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.2 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.7.4) + retry-axios: 2.6.0(axios@1.7.4(debug@4.4.0)) tough-cookie: 4.1.3 transitivePeerDependencies: - supports-color @@ -23593,7 +23593,7 @@ snapshots: kuler@2.0.0: {} - langchain@0.3.11(c10b80e38f5a8711ccad1e2174de91e6): + langchain@0.3.11(101f2d8395211f7761efa8fce29a53de): dependencies: '@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) '@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13) @@ -25157,7 +25157,7 @@ snapshots: pdf-parse@1.1.1: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -25965,7 +25965,7 @@ snapshots: ret@0.1.15: {} - retry-axios@2.6.0(axios@1.7.4): + retry-axios@2.6.0(axios@1.7.4(debug@4.4.0)): dependencies: axios: 1.7.4 @@ -25992,7 +25992,7 @@ snapshots: rhea@1.0.24: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -27624,6 +27624,8 @@ snapshots: vue-component-type-helpers@2.2.4: {} + vue-component-type-helpers@2.2.8: {} + vue-demi@0.14.10(vue@3.5.13(typescript@5.7.2)): dependencies: vue: 3.5.13(typescript@5.7.2)