diff --git a/packages/editor-ui/src/plugins/codemirror/typescript/client/useTypescript.ts b/packages/editor-ui/src/plugins/codemirror/typescript/client/useTypescript.ts index 48167f4cb5a02..b095cad2b9f8f 100644 --- a/packages/editor-ui/src/plugins/codemirror/typescript/client/useTypescript.ts +++ b/packages/editor-ui/src/plugins/codemirror/typescript/client/useTypescript.ts @@ -82,7 +82,7 @@ export async function useTypescript( return { json: schema, - binary: Object.keys(binaryData), + binary: Object.keys(binaryData.reduce((acc, obj) => ({ ...acc, ...obj }), {})), params: getSchemaForExecutionData([node.parameters]), }; } diff --git a/packages/editor-ui/src/plugins/codemirror/typescript/worker/constants.ts b/packages/editor-ui/src/plugins/codemirror/typescript/worker/constants.ts index 30844c37b7919..215d7a2544284 100644 --- a/packages/editor-ui/src/plugins/codemirror/typescript/worker/constants.ts +++ b/packages/editor-ui/src/plugins/codemirror/typescript/worker/constants.ts @@ -18,7 +18,9 @@ export const TYPESCRIPT_AUTOCOMPLETE_THRESHOLD = '15'; export const TYPESCRIPT_FILES = { DYNAMIC_TYPES: 'n8n-dynamic.d.ts', DYNAMIC_INPUT_TYPES: 'n8n-dynamic-input.d.ts', + DYNAMIC_VARIABLES_TYPES: 'n8n-variables.d.ts', MODE_TYPES: 'n8n-mode-specific.d.ts', N8N_TYPES: 'n8n.d.ts', GLOBAL_TYPES: 'globals.d.ts', }; +export const LUXON_VERSION = '3.2.0'; diff --git a/packages/editor-ui/src/plugins/codemirror/typescript/worker/dynamicTypes.ts b/packages/editor-ui/src/plugins/codemirror/typescript/worker/dynamicTypes.ts index 14813733f139c..e8cde87824e12 100644 --- a/packages/editor-ui/src/plugins/codemirror/typescript/worker/dynamicTypes.ts +++ b/packages/editor-ui/src/plugins/codemirror/typescript/worker/dynamicTypes.ts @@ -64,7 +64,10 @@ ${Array.from(loadedNodes.values()) interface NodeDataMap { ${Array.from(loadedNodes.entries()) - .map(([nodeName, { typeName }]) => `'${nodeName}': NodeData<{}, ${typeName}, {}, {}>`) + .map( + ([nodeName, { typeName }]) => + `'${nodeName}': NodeData<${typeName}Context, ${typeName}Json, ${typeName}BinaryKeys, ${typeName}Params>`, + ) .join(';\n')} } `); @@ -73,5 +76,17 @@ interface NodeDataMap { export async function getDynamicInputNodeTypes(inputNodeNames: string[]) { const typeNames = inputNodeNames.map((nodeName) => pascalCase(nodeName)); - return globalTypeDefinition(`type N8nInputItem = ${typeNames.join(' | ')}`); + return globalTypeDefinition(` +type N8nInputJson = ${typeNames.map((typeName) => `${typeName}Json`).join(' | ')}; +type N8nInputBinaryKeys = ${typeNames.map((typeName) => `${typeName}BinaryKeys`).join(' | ')}; +type N8nInputContext = ${typeNames.map((typeName) => `${typeName}Context`).join(' | ')}; +type N8nInputParams = ${typeNames.map((typeName) => `${typeName}Params`).join(' | ')}; +`); +} + +export async function getDynamicVariableTypes(variables: string[]) { + return globalTypeDefinition(` + interface N8nVars { + ${variables.map((key) => `${key}: string;`).join('\n')} +}`); } diff --git a/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n-once-for-all-items.d.ts b/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n-once-for-all-items.d.ts index c58abe3f09983..1487c6e495d9f 100644 --- a/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n-once-for-all-items.d.ts +++ b/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n-once-for-all-items.d.ts @@ -10,6 +10,6 @@ declare global { itemMatching(itemIndex: number): N8nItem; } - // @ts-expect-error N8nInputItem is populated dynamically - type N8nInput = NodeData<{}, N8nInputItem, {}, {}>; + // @ts-expect-error N8nInputJson is populated dynamically + type N8nInput = NodeData; } diff --git a/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n-once-for-each-item.d.ts b/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n-once-for-each-item.d.ts index e6b7727c96b8d..b1d6f6ddf18b5 100644 --- a/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n-once-for-each-item.d.ts +++ b/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n-once-for-each-item.d.ts @@ -7,8 +7,8 @@ declare global { params: P; } - // @ts-expect-error N8nInputItem is populated dynamically - type N8nInput = NodeData<{}, N8nInputItem, {}, {}>; + // @ts-expect-error N8nInputJson is populated dynamically + type N8nInput = NodeData<{}, N8nInputJson, {}, {}>; const $itemIndex: number; const $json: N8nInput['item']['json']; diff --git a/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n.d.ts b/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n.d.ts index 427e952c33e41..341ed4a41ff2c 100644 --- a/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n.d.ts +++ b/packages/editor-ui/src/plugins/codemirror/typescript/worker/type-declarations/n8n.d.ts @@ -16,7 +16,6 @@ declare global { mimeType: string; } - // TODO: populate dynamically interface N8nVars {} // TODO: populate dynamically @@ -63,7 +62,7 @@ declare global { const $now: DateTime; const $today: DateTime; - const $parameter: N8nParameter; + const $parameter: N8nInput['params']; const $vars: N8nVars; const $nodeVersion: number; diff --git a/packages/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.ts b/packages/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.ts index 1f2564fae6efc..381fcb54b1007 100644 --- a/packages/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.ts +++ b/packages/editor-ui/src/plugins/codemirror/typescript/worker/typescript.worker.ts @@ -8,10 +8,11 @@ import type { CodeExecutionMode } from 'n8n-workflow'; import { pascalCase } from 'change-case'; import { computed, reactive, ref, watch } from 'vue'; import { getCompletionsAtPos } from './completions'; -import { TYPESCRIPT_FILES } from './constants'; +import { LUXON_VERSION, TYPESCRIPT_FILES } from './constants'; import { getDynamicInputNodeTypes, getDynamicNodeTypes, + getDynamicVariableTypes, schemaToTypescriptTypes, } from './dynamicTypes'; import { setupTypescriptEnv } from './env'; @@ -21,6 +22,7 @@ import { getUsedNodeNames } from './typescriptAst'; import runOnceForAllItemsTypes from './type-declarations/n8n-once-for-all-items.d.ts?raw'; import runOnceForEachItemTypes from './type-declarations/n8n-once-for-each-item.d.ts?raw'; +import { loadTypes } from './npmTypesLoader'; self.process = { env: {} } as NodeJS.Process; @@ -53,12 +55,20 @@ const worker: LanguageServiceWorkerInit = { async function loadNodeTypes(nodeName: string) { const data = await nodeDataFetcher(nodeName); - if (data?.json) { - const schema = data.json; - const typeName = pascalCase(nodeName); - const type = schemaToTypescriptTypes(schema, typeName); - loadedNodeTypesMap.set(nodeName, { type, typeName }); - } + const typeName = pascalCase(nodeName); + const jsonType = data?.json + ? schemaToTypescriptTypes(data.json, `${typeName}Json`) + : `type ${typeName}Json = N8nJson`; + const paramsType = data?.params + ? schemaToTypescriptTypes(data.params, `${typeName}Params`) + : `type ${typeName}Params = {}`; + + // Using || on purpose to handle empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const binaryType = `type ${typeName}BinaryKeys = ${data?.binary.map((key) => `'${key}'`).join(' | ') || 'string'}`; + const contextType = `type ${typeName}Context = {}`; + const type = [jsonType, binaryType, paramsType, contextType].join('\n'); + loadedNodeTypesMap.set(nodeName, { type, typeName }); } async function loadTypesIfNeeded() { @@ -75,10 +85,27 @@ const worker: LanguageServiceWorkerInit = { } } - await loadTypesIfNeeded(); - await Promise.all( - options.inputNodeNames.map(async (nodeName) => await loadNodeTypes(nodeName)), - ); + async function loadLuxonTypes() { + if (cache.getItem('/node_modules/@types/luxon/package.json')) { + const fileMap = await cache.getAllWithPrefix('/node_modules/@types/luxon'); + + for (const [path, content] of Object.entries(fileMap)) { + updateFile(path, content); + } + } else { + await loadTypes('luxon', LUXON_VERSION, (path, types) => { + cache.setItem(path, types); + updateFile(path, types); + }); + } + } + + async function setVariableTypes() { + updateFile( + TYPESCRIPT_FILES.DYNAMIC_VARIABLES_TYPES, + await getDynamicVariableTypes(options.variables), + ); + } function updateFile(fileName: string, content: string) { const exists = env.getSourceFile(fileName); @@ -89,6 +116,13 @@ const worker: LanguageServiceWorkerInit = { } } + const loadInputNodes = options.inputNodeNames.map( + async (nodeName) => await loadNodeTypes(nodeName), + ); + await Promise.all( + loadInputNodes.concat(loadTypesIfNeeded(), loadLuxonTypes(), setVariableTypes()), + ); + watch( loadedNodeTypesMap, async (loadedNodes) => {