Skip to content

Commit

Permalink
Add support for binary autocompletion
Browse files Browse the repository at this point in the history
  • Loading branch information
elsmr committed Dec 18, 2024
1 parent b90ea5a commit 9dceb02
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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')}
}
`);
Expand All @@ -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')}
}`);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ declare global {
itemMatching(itemIndex: number): N8nItem<J, B>;
}

// @ts-expect-error N8nInputItem is populated dynamically
type N8nInput = NodeData<{}, N8nInputItem, {}, {}>;
// @ts-expect-error N8nInputJson is populated dynamically
type N8nInput = NodeData<N8nInputContext, N8nInputJson, N8nInputBinaryKeys, N8nInputParams>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ declare global {
mimeType: string;
}

// TODO: populate dynamically
interface N8nVars {}

// TODO: populate dynamically
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;

Expand Down Expand Up @@ -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() {
Expand All @@ -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);
Expand All @@ -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) => {
Expand Down

0 comments on commit 9dceb02

Please sign in to comment.