refactor(editor): Remove part of getCurrentWorkflow usages (#16148)

This commit is contained in:
Alex Grozav
2025-07-24 12:51:32 +03:00
committed by GitHub
parent b09f73701d
commit c30dbc6dd4
29 changed files with 578 additions and 391 deletions

View File

@@ -123,6 +123,7 @@
"ics": "patches/ics.patch",
"minifaker": "patches/minifaker.patch",
"z-vue-scan": "patches/z-vue-scan.patch",
"@lezer/highlight": "patches/@lezer__highlight.patch",
"v-code-diff": "patches/v-code-diff.patch"
}
}

View File

@@ -174,8 +174,7 @@ function useJsonFieldCompletions() {
try {
const activeNode = ndvStore.activeNode;
if (activeNode) {
const workflow = workflowsStore.getCurrentWorkflow();
const input = workflow.connectionsByDestinationNode[activeNode.name];
const input = workflowsStore.connectionsByDestinationNode[activeNode.name];
return input.main[0] ? input.main[0][0].node : null;
}
} catch (e) {

View File

@@ -84,7 +84,7 @@ function getMultipleNodesText(nodeName: string): string {
return '';
const activeNodeConnections =
props.workflow.connectionsByDestinationNode[activeNode.value.name].main || [];
workflowsStore.connectionsByDestinationNode[activeNode.value.name].main || [];
// Collect indexes of connected nodes
const connectedInputIndexes = activeNodeConnections.reduce((acc: number[], node, index) => {
if (node?.[0] && node[0].node === nodeName) return [...acc, index];

View File

@@ -804,7 +804,8 @@ function getNodeHints(): NodeHint[] {
node: node.value,
nodeType: nodeType.value,
nodeOutputData,
workflow: props.workflow,
nodes: workflowsStore.allNodes,
connections: workflowsStore.connectionsBySourceNode,
hasNodeRun: hasNodeRun.value,
hasMultipleInputItems,
});

View File

@@ -114,6 +114,7 @@ const expressionResolveCtx = computed<ExpressionLocalResolveContext | undefined>
nodeName,
additionalKeys: {},
inputNode: findInputNode(),
connections: workflowsStore.connectionsBySourceNode,
};
});

View File

@@ -216,6 +216,8 @@ describe('useCanvasMapping', () => {
});
it('should handle input and output connections', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
const nodes = [manualTriggerNode, setNode];
const connections = {
@@ -225,6 +227,9 @@ describe('useCanvasMapping', () => {
],
},
};
workflowsStore.workflow.connections = connections;
const workflowObject = createTestWorkflowObject({
nodes,
connections,

View File

@@ -59,6 +59,7 @@ import { useNodeHelpers } from './useNodeHelpers';
import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils';
import { useNodeDirtiness } from '@/composables/useNodeDirtiness';
import { getNodeIconSource } from '../utils/nodeIcon';
import * as workflowUtils from 'n8n-workflow/common';
export function useCanvasMapping({
nodes,
@@ -571,56 +572,62 @@ export function useCanvasMapping({
}, {});
});
const mappedNodes = computed<CanvasNode[]>(() => [
...nodes.value.map<CanvasNode>((node) => {
const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {};
const outputConnections = workflowObject.value.connectionsBySourceNode[node.name] ?? {};
const mappedNodes = computed<CanvasNode[]>(() => {
const connectionsBySourceNode = connections.value;
const connectionsByDestinationNode =
workflowUtils.mapConnectionsByDestination(connectionsBySourceNode);
const data: CanvasNodeData = {
id: node.id,
name: node.name,
subtitle: nodeSubtitleById.value[node.id] ?? '',
type: node.type,
typeVersion: node.typeVersion,
disabled: node.disabled,
inputs: nodeInputsById.value[node.id] ?? [],
outputs: nodeOutputsById.value[node.id] ?? [],
connections: {
[CanvasConnectionMode.Input]: inputConnections,
[CanvasConnectionMode.Output]: outputConnections,
},
issues: {
items: nodeIssuesById.value[node.id],
visible: nodeHasIssuesById.value[node.id],
},
pinnedData: {
count: nodePinnedDataById.value[node.id]?.length ?? 0,
visible: !!nodePinnedDataById.value[node.id],
},
execution: {
status: nodeExecutionStatusById.value[node.id],
waiting: nodeExecutionWaitingById.value[node.id],
waitingForNext: nodeExecutionWaitingForNextById.value[node.id],
running: nodeExecutionRunningById.value[node.id],
},
runData: {
outputMap: nodeExecutionRunDataOutputMapById.value[node.id],
iterations: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
visible: !!nodeExecutionRunDataById.value[node.id],
},
render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} },
};
return [
...nodes.value.map<CanvasNode>((node) => {
const outputConnections = connectionsBySourceNode[node.name] ?? {};
const inputConnections = connectionsByDestinationNode[node.name] ?? {};
return {
id: node.id,
label: node.name,
type: 'canvas-node',
position: { x: node.position[0], y: node.position[1] },
data,
...additionalNodePropertiesById.value[node.id],
};
}),
]);
const data: CanvasNodeData = {
id: node.id,
name: node.name,
subtitle: nodeSubtitleById.value[node.id] ?? '',
type: node.type,
typeVersion: node.typeVersion,
disabled: node.disabled,
inputs: nodeInputsById.value[node.id] ?? [],
outputs: nodeOutputsById.value[node.id] ?? [],
connections: {
[CanvasConnectionMode.Input]: inputConnections,
[CanvasConnectionMode.Output]: outputConnections,
},
issues: {
items: nodeIssuesById.value[node.id],
visible: nodeHasIssuesById.value[node.id],
},
pinnedData: {
count: nodePinnedDataById.value[node.id]?.length ?? 0,
visible: !!nodePinnedDataById.value[node.id],
},
execution: {
status: nodeExecutionStatusById.value[node.id],
waiting: nodeExecutionWaitingById.value[node.id],
waitingForNext: nodeExecutionWaitingForNextById.value[node.id],
running: nodeExecutionRunningById.value[node.id],
},
runData: {
outputMap: nodeExecutionRunDataOutputMapById.value[node.id],
iterations: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
visible: !!nodeExecutionRunDataById.value[node.id],
},
render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} },
};
return {
id: node.id,
label: node.name,
type: 'canvas-node',
position: { x: node.position[0], y: node.position[1] },
data,
...additionalNodePropertiesById.value[node.id],
};
}),
];
});
const mappedConnections = computed<CanvasConnection[]>(() => {
return mapLegacyConnectionsToCanvasConnections(connections.value ?? [], nodes.value ?? []).map(

View File

@@ -253,6 +253,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
if (sourceData === null) {
const parentNodes = workflow.getParentNodes(name, NodeConnectionTypes.Main, 1);
const executeData = workflowHelpers.executeData(
workflow.connectionsBySourceNode,
parentNodes,
name,
NodeConnectionTypes.Main,

View File

@@ -14,11 +14,8 @@ import {
createTestWorkflowExecutionResponse,
createTestWorkflowObject,
} from '@/__tests__/mocks';
import {
NodeConnectionTypes,
WEBHOOK_NODE_TYPE,
type AssignmentCollectionValue,
} from 'n8n-workflow';
import { NodeConnectionTypes, WEBHOOK_NODE_TYPE } from 'n8n-workflow';
import type { AssignmentCollectionValue, IConnections } from 'n8n-workflow';
import * as apiWebhooks from '@n8n/rest-api-client/api/webhooks';
import { mockedStore } from '@/__tests__/utils';
@@ -519,7 +516,7 @@ describe('useWorkflowHelpers', () => {
const inputName = 'main';
const runIndex = 0;
const result = executeData(parentNodes, currentNode, inputName, runIndex);
const result = executeData({}, parentNodes, currentNode, inputName, runIndex);
expect(result).toEqual({
node: {},
@@ -538,18 +535,15 @@ describe('useWorkflowHelpers', () => {
const jsonData = {
name: 'Test',
};
workflowsStore.getCurrentWorkflow.mockReturnValue({
connectionsByDestinationNode: {
Set: {
main: [
[
{ node: 'Start', index: 0, type: 'main' },
{ node: 'Set', index: 0, type: 'main' },
],
],
},
const connectionsBySourceNode: IConnections = {
Start: {
main: [[{ node: 'Set', index: 0, type: 'main' }]],
},
} as never);
Set: {
main: [[{ node: 'Start', index: 0, type: 'main' }]],
},
};
workflowsStore.workflowExecutionData = {
data: {
@@ -575,7 +569,13 @@ describe('useWorkflowHelpers', () => {
},
} as unknown as IExecutionResponse;
const result = executeData(parentNodes, currentNode, inputName, runIndex);
const result = executeData(
connectionsBySourceNode,
parentNodes,
currentNode,
inputName,
runIndex,
);
expect(result).toEqual({
node: {},
@@ -609,18 +609,15 @@ describe('useWorkflowHelpers', () => {
const jsonData = {
name: 'Test',
};
workflowsStore.getCurrentWorkflow.mockReturnValue({
connectionsByDestinationNode: {
Set: {
main: [
[
{ node: 'Start', index: 0, type: 'main' },
{ node: 'Set', index: 0, type: 'main' },
],
],
},
const connectionsBySourceNode: IConnections = {
Start: {
main: [[{ node: 'Set', index: 0, type: 'main' }]],
},
} as never);
Set: {
main: [[{ node: 'Start', index: 0, type: 'main' }]],
},
};
workflowsStore.workflowExecutionData = {
data: {
@@ -646,7 +643,13 @@ describe('useWorkflowHelpers', () => {
},
} as unknown as IExecutionResponse;
const result = executeData(parentNodes, currentNode, inputName, runIndex);
const result = executeData(
connectionsBySourceNode,
parentNodes,
currentNode,
inputName,
runIndex,
);
expect(result).toEqual({
node: {},
@@ -686,22 +689,20 @@ describe('useWorkflowHelpers', () => {
name: 'Test B',
};
workflowsStore.getCurrentWorkflow.mockReturnValue({
connectionsByDestinationNode: {
Set: {
main: [
[
{ node: 'Parent A', index: 0, type: 'main' },
{ node: 'Set', index: 0, type: 'main' },
],
[
{ node: 'Parent B', index: 0, type: 'main' },
{ node: 'Set', index: 0, type: 'main' },
],
],
},
const connectionsBySourceNode: IConnections = {
'Parent A': {
main: [[{ node: 'Set', type: 'main', index: 0 }]],
},
} as never);
'Parent B': {
main: [[{ node: 'Set', type: 'main', index: 1 }]],
},
Set: {
main: [
[{ node: 'Set', type: 'main', index: 0 }],
[{ node: 'Set', type: 'main', index: 1 }],
],
},
};
workflowsStore.workflowExecutionData = {
data: {
@@ -742,7 +743,13 @@ describe('useWorkflowHelpers', () => {
},
} as unknown as IExecutionResponse;
const result = executeData(parentNodes, currentNode, inputName, runIndex);
const result = executeData(
connectionsBySourceNode,
parentNodes,
currentNode,
inputName,
runIndex,
);
expect(result).toEqual({
node: {},
@@ -779,7 +786,7 @@ describe('useWorkflowHelpers', () => {
};
workflowsStore.shouldReplaceInputDataWithPinData = true;
const result = executeData(parentNodes, currentNode, inputName, runIndex);
const result = executeData({}, parentNodes, currentNode, inputName, runIndex);
expect(result.data).toEqual({ main: [[{ json: { key: 'value' } }]] });
expect(result.source).toEqual({ main: [{ previousNode: 'ParentNode' }] });
@@ -802,20 +809,23 @@ describe('useWorkflowHelpers', () => {
} as never,
],
};
workflowsStore.getCurrentWorkflow.mockReturnValue({
connectionsByDestinationNode: {
CurrentNode: {
main: [
[
{ node: 'ParentNode', index: 0, type: 'main' },
{ node: 'CurrentNode', index: 0, type: 'main' },
],
],
},
},
} as never);
const result = executeData(parentNodes, currentNode, inputName, runIndex);
const connectionsBySourceNode: IConnections = {
CurrentNode: {
main: [[{ node: 'CurrentNode', index: 0, type: 'main' }]],
},
ParentNode: {
main: [[{ node: 'CurrentNode', index: 0, type: 'main' }]],
},
};
const result = executeData(
connectionsBySourceNode,
parentNodes,
currentNode,
inputName,
runIndex,
);
expect(result.data).toEqual({ main: [[{ json: { key: 'valueFromRunData' } }]] });
expect(result.source).toEqual({
@@ -841,20 +851,24 @@ describe('useWorkflowHelpers', () => {
} as never,
],
};
workflowsStore.getCurrentWorkflow.mockReturnValue({
connectionsByDestinationNode: {
CurrentNode: {
main: [
[
{ node: 'ParentNode', index: 1, type: 'main' },
{ node: 'CurrentNode', index: 0, type: 'main' },
],
],
},
},
} as never);
const result = executeData(parentNodes, currentNode, inputName, runIndex, parentRunIndex);
const connectionsBySourceNode: IConnections = {
CurrentNode: {
main: [[{ node: 'CurrentNode', index: 0, type: 'main' }]],
},
ParentNode: {
main: [[], [{ node: 'CurrentNode', index: 1, type: 'main' }]],
},
};
const result = executeData(
connectionsBySourceNode,
parentNodes,
currentNode,
inputName,
runIndex,
parentRunIndex,
);
expect(result.data).toEqual({ main: [[{ json: { key: 'valueFromRunData' } }]] });
expect(result.source).toEqual({
@@ -874,7 +888,7 @@ describe('useWorkflowHelpers', () => {
workflowsStore.shouldReplaceInputDataWithPinData = false;
workflowsStore.getWorkflowRunData = null;
const result = executeData(parentNodes, currentNode, inputName, runIndex);
const result = executeData({}, parentNodes, currentNode, inputName, runIndex);
expect(result.data).toEqual({});
expect(result.source).toBeNull();

View File

@@ -29,6 +29,7 @@ import {
NodeHelpers,
WEBHOOK_NODE_TYPE,
} from 'n8n-workflow';
import * as workflowUtils from 'n8n-workflow/common';
import type {
ICredentialsResponse,
@@ -71,6 +72,7 @@ export type ResolveParameterOptions = {
additionalKeys?: IWorkflowDataProxyAdditionalKeys;
isForCredential?: boolean;
contextNodeName?: string;
connections?: IConnections;
};
export function resolveParameter<T = IDataObject>(
@@ -81,6 +83,7 @@ export function resolveParameter<T = IDataObject>(
return resolveParameterImpl(
parameter,
() => opts.workflow,
opts.connections,
opts.envVars,
opts.workflow.getNode(opts.nodeName),
opts.execution,
@@ -100,6 +103,7 @@ export function resolveParameter<T = IDataObject>(
return resolveParameterImpl(
parameter,
workflowsStore.getCurrentWorkflow,
workflowsStore.connectionsBySourceNode,
useEnvironmentsStore().variablesAsObject,
useNDVStore().activeNode,
workflowsStore.workflowExecutionData,
@@ -113,6 +117,7 @@ export function resolveParameter<T = IDataObject>(
function resolveParameterImpl<T = IDataObject>(
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
getContextWorkflow: () => Workflow,
connections: IConnections,
envVars: Record<string, string | boolean | number>,
ndvActiveNode: INodeUi | null,
executionData: IExecutionResponse | null,
@@ -200,11 +205,11 @@ function resolveParameterImpl<T = IDataObject>(
}
let _connectionInputData = connectionInputData(
connections,
parentNode,
contextNode!.name,
inputName,
runIndexParent,
getContextWorkflow,
shouldReplaceInputDataWithPinData,
pinData,
executionData?.data?.resultData.runData ?? null,
@@ -215,11 +220,11 @@ function resolveParameterImpl<T = IDataObject>(
// For Sub-Nodes connected to Trigger-Nodes use the data of the root-node
// (Gets for example used by the Memory connected to the Chat-Trigger-Node)
const _executeData = executeDataImpl(
connections,
[contextNode.name],
contextNode.name,
inputName,
0,
getContextWorkflow,
shouldReplaceInputDataWithPinData,
pinData,
executionData?.data?.resultData.runData ?? null,
@@ -265,11 +270,11 @@ function resolveParameterImpl<T = IDataObject>(
runIndexCurrent = workflowRunData[contextNode!.name].length - 1;
}
let _executeData = executeDataImpl(
connections,
parentNode,
contextNode!.name,
inputName,
runIndexCurrent,
getContextWorkflow,
shouldReplaceInputDataWithPinData,
pinData,
executionData?.data?.resultData.runData ?? null,
@@ -279,11 +284,11 @@ function resolveParameterImpl<T = IDataObject>(
if (!_executeData.source) {
// fallback to parent's run index for multi-output case
_executeData = executeDataImpl(
connections,
parentNode,
contextNode!.name,
inputName,
runIndexParent,
getContextWorkflow,
shouldReplaceInputDataWithPinData,
pinData,
executionData?.data?.resultData.runData ?? null,
@@ -310,6 +315,7 @@ export function resolveRequiredParameters(
currentParameter: INodeProperties,
parameters: INodeParameters,
opts: {
connections?: IConnections;
targetItem?: TargetItem;
inputNodeName?: string;
inputRunIndex?: number;
@@ -382,11 +388,11 @@ function getNodeTypes(): INodeTypes {
// TODO: move to separate file
// Returns connectionInputData to be able to execute an expression.
function connectionInputData(
connections: IConnections,
parentNode: string[],
currentNode: string,
inputName: string,
runIndex: number,
getContextWorkflow: () => Workflow,
shouldReplaceInputDataWithPinData: boolean,
pinData: IPinData | undefined,
workflowRunData: IRunData | null,
@@ -394,11 +400,11 @@ function connectionInputData(
): INodeExecutionData[] | null {
let connectionInputData: INodeExecutionData[] | null = null;
const _executeData = executeDataImpl(
connections,
parentNode,
currentNode,
inputName,
runIndex,
getContextWorkflow,
shouldReplaceInputDataWithPinData,
pinData,
workflowRunData,
@@ -431,6 +437,7 @@ function connectionInputData(
}
export function executeData(
connections: IConnections,
parentNodes: string[],
currentNode: string,
inputName: string,
@@ -440,11 +447,11 @@ export function executeData(
const workflowsStore = useWorkflowsStore();
return executeDataImpl(
connections,
parentNodes,
currentNode,
inputName,
runIndex,
workflowsStore.getCurrentWorkflow,
workflowsStore.shouldReplaceInputDataWithPinData,
workflowsStore.pinnedWorkflowData,
workflowsStore.getWorkflowRunData,
@@ -454,16 +461,18 @@ export function executeData(
// TODO: move to separate file
function executeDataImpl(
connections: IConnections,
parentNodes: string[],
currentNode: string,
inputName: string,
runIndex: number,
getContextWorkflow: () => Workflow,
shouldReplaceInputDataWithPinData: boolean,
pinData: IPinData | undefined,
workflowRunData: IRunData | null,
parentRunIndex?: number,
): IExecuteData {
const connectionsByDestinationNode = workflowUtils.mapConnectionsByDestination(connections);
const executeData = {
node: {},
data: {},
@@ -507,15 +516,12 @@ function executeDataImpl(
[inputName]: workflowRunData[currentNode][runIndex].source,
};
} else {
const workflow = getContextWorkflow();
let previousNodeOutput: number | undefined;
// As the node can be connected through either of the outputs find the correct one
// and set it to make pairedItem work on not executed nodes
if (workflow.connectionsByDestinationNode[currentNode]?.main) {
mainConnections: for (const mainConnections of workflow.connectionsByDestinationNode[
currentNode
].main) {
if (connectionsByDestinationNode[currentNode]?.main) {
mainConnections: for (const mainConnections of connectionsByDestinationNode[currentNode]
.main) {
for (const connection of mainConnections ?? []) {
if (
connection.type === NodeConnectionTypes.Main &&

View File

@@ -149,9 +149,8 @@ export const useNDVStore = defineStore(STORES.NDV, () => {
const ndvNodeInputNumber = computed(() => {
const returnData: { [nodeName: string]: number[] } = {};
const workflow = workflowsStore.getCurrentWorkflow();
const activeNodeConections = (
workflow.connectionsByDestinationNode[activeNode.value?.name || ''] ?? {}
workflowsStore.connectionsByDestinationNode[activeNode.value?.name || ''] ?? {}
).main;
if (!activeNodeConections || activeNodeConections.length < 2) return returnData;

View File

@@ -66,6 +66,7 @@ import {
Workflow,
TelemetryHelpers,
} from 'n8n-workflow';
import * as workflowUtils from 'n8n-workflow/common';
import findLast from 'lodash/findLast';
import isEqual from 'lodash/isEqual';
import pick from 'lodash/pick';
@@ -294,10 +295,17 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const getPastChatMessages = computed(() => Array.from(new Set(chatMessages.value)));
/**
* This section contains functions migrated from the workflow class
*/
const connectionsBySourceNode = computed(() => workflow.value.connections);
const connectionsByDestinationNode = computed(() =>
Workflow.getConnectionsByDestination(workflow.value.connections),
workflowUtils.mapConnectionsByDestination(workflow.value.connections),
);
// End section
const selectableTriggerNodes = computed(() =>
workflowTriggerNodes.value.filter((node) => !node.disabled && !isChatNode(node)),
);
@@ -384,7 +392,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
}
function getNodeByName(nodeName: string): INodeUi | null {
return nodesByName.value[nodeName] || null;
return workflowUtils.getNodeByName(nodesByName.value, nodeName);
}
function getNodeById(nodeId: string): INodeUi | undefined {
@@ -1955,6 +1963,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
getWorkflowResultDataByNodeName,
allConnections,
allNodes,
connectionsBySourceNode,
connectionsByDestinationNode,
isWaitingExecution,
isWorkflowRunning,
canvasNames,

View File

@@ -1,5 +1,5 @@
import type { Basic, IExecutionResponse } from '@/Interface';
import type { IWorkflowDataProxyAdditionalKeys, Workflow } from 'n8n-workflow';
import type { IConnections, IWorkflowDataProxyAdditionalKeys, Workflow } from 'n8n-workflow';
type Range = { from: number; to: number };
@@ -40,6 +40,7 @@ export interface ExpressionLocalResolveContext {
envVars: Record<string, Basic>;
additionalKeys: IWorkflowDataProxyAdditionalKeys;
workflow: Workflow;
connections: IConnections;
execution: IExecutionResponse | null;
nodeName: string;
/**

View File

@@ -23,6 +23,8 @@ import { SET_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import { createTestNode } from '@/__tests__/mocks';
import type { GraphNode } from '@vue-flow/core';
import { v4 as uuid } from 'uuid';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
describe('getGenericHints', () => {
let mockWorkflowNode: MockProxy<INode>;
@@ -34,6 +36,9 @@ describe('getGenericHints', () => {
let hasNodeRun: boolean;
beforeEach(() => {
const pinia = createTestingPinia({});
setActivePinia(pinia);
mockWorkflowNode = mock<INode>();
mockNode = mock<INodeUi>({ type: 'test' });
mockNodeType = mock<INodeTypeDescription>();
@@ -55,8 +60,9 @@ describe('getGenericHints', () => {
nodeType: mockNodeType,
nodeOutputData: mockNodeOutputData,
hasMultipleInputItems,
workflow: mockWorkflow,
hasNodeRun,
nodes: [],
connections: {},
});
expect(hints).toEqual([
@@ -80,8 +86,9 @@ describe('getGenericHints', () => {
nodeType: mockNodeType,
nodeOutputData: mockNodeOutputData,
hasMultipleInputItems,
workflow: mockWorkflow,
hasNodeRun,
nodes: [],
connections: {},
});
expect(hints).toEqual([
@@ -118,8 +125,9 @@ describe('getGenericHints', () => {
nodeType: mockNodeType,
nodeOutputData: mockNodeOutputData,
hasMultipleInputItems,
workflow: mockWorkflow,
hasNodeRun,
nodes: [],
connections: {},
});
expect(hints).toEqual([
@@ -142,8 +150,9 @@ describe('getGenericHints', () => {
nodeType: mockNodeType,
nodeOutputData: mockNodeOutputData,
hasMultipleInputItems,
workflow: mockWorkflow,
hasNodeRun,
nodes: [],
connections: {},
});
expect(hints).toEqual([
@@ -166,8 +175,9 @@ describe('getGenericHints', () => {
nodeType: mockNodeType,
nodeOutputData: mockNodeOutputData,
hasMultipleInputItems,
workflow: mockWorkflow,
hasNodeRun,
nodes: [],
connections: {},
});
expect(hints).toEqual([
@@ -191,8 +201,9 @@ describe('getGenericHints', () => {
nodeType: mockNodeType,
nodeOutputData: mockNodeOutputData,
hasMultipleInputItems,
workflow: mockWorkflow,
hasNodeRun,
nodes: [],
connections: {},
});
expect(hints).toEqual([

View File

@@ -11,11 +11,11 @@ import {
import type { INodeUi, XYPosition } from '@/Interface';
import type {
AssignmentCollectionValue,
IConnections,
INode,
INodeExecutionData,
INodeTypeDescription,
NodeHint,
Workflow,
} from 'n8n-workflow';
import { NodeHelpers, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
import type { RouteLocation } from 'vue-router';
@@ -27,6 +27,7 @@ import {
type Rect,
type ViewportTransform,
} from '@vue-flow/core';
import * as workflowUtils from 'n8n-workflow/common';
/*
* Canvas constants and functions
@@ -371,7 +372,8 @@ export function getGenericHints({
nodeType,
nodeOutputData,
hasMultipleInputItems,
workflow,
nodes,
connections,
hasNodeRun,
}: {
workflowNode: INode;
@@ -379,7 +381,8 @@ export function getGenericHints({
nodeType: INodeTypeDescription;
nodeOutputData: INodeExecutionData[];
hasMultipleInputItems: boolean;
workflow: Workflow;
nodes: INode[];
connections: IConnections;
hasNodeRun: boolean;
}) {
const nodeHints: NodeHint[] = [];
@@ -417,7 +420,7 @@ export function getGenericHints({
hasMultipleInputItems &&
LIST_LIKE_NODE_OPERATIONS.includes((workflowNode.parameters.operation as string) || '')
) {
const executeOnce = workflow.getNode(node.name)?.executeOnce;
const executeOnce = workflowUtils.getNodeByName(nodes, node.name)?.executeOnce;
if (!executeOnce) {
nodeHints.push({
message:
@@ -429,7 +432,7 @@ export function getGenericHints({
// add sendAndWait hint
if (hasMultipleInputItems && workflowNode.parameters.operation === SEND_AND_WAIT_OPERATION) {
const executeOnce = workflow.getNode(node.name)?.executeOnce;
const executeOnce = workflowUtils.getNodeByName(nodes, node.name)?.executeOnce;
if (!executeOnce) {
nodeHints.push({
message: 'This action will run only once, for the first input item',
@@ -470,9 +473,8 @@ export function getGenericHints({
// Split In Batches setup hints
if (node.type === SPLIT_IN_BATCHES_NODE_TYPE) {
const { connectionsBySourceNode } = workflow;
const firstNodesInLoop = connectionsBySourceNode[node.name]?.main[1] || [];
const firstNodesInLoop =
workflowUtils.mapConnectionsByDestination(connections)[node.name]?.main[1] || [];
if (!firstNodesInLoop.length) {
nodeHints.push({
@@ -482,7 +484,7 @@ export function getGenericHints({
});
} else {
for (const nodeInConnection of firstNodesInLoop || []) {
const nodeChilds = workflow.getChildNodes(nodeInConnection.node) || [];
const nodeChilds = workflowUtils.getChildNodes(connections, nodeInConnection.node) || [];
if (!nodeChilds.includes(node.name)) {
nodeHints.push({
message:

View File

@@ -2,6 +2,7 @@
"extends": "@n8n/typescript-config/tsconfig.frontend.json",
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "bundler",
"rootDirs": [
".",
"../@n8n/rest-api-client/src",

View File

@@ -11,6 +11,11 @@
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"./common": {
"types": "./dist/esm/common/index.d.ts",
"import": "./dist/esm/common/index.js",
"require": "./dist/cjs/common/index.js"
},
"./*": "./*"
},
"scripts": {

View File

@@ -0,0 +1,12 @@
import { getConnectedNodes } from './get-connected-nodes';
import { NodeConnectionTypes } from '../interfaces';
import type { IConnections, NodeConnectionType } from '../interfaces';
export function getChildNodes(
connectionsBySourceNode: IConnections,
nodeName: string,
type: NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionTypes.Main,
depth = -1,
): string[] {
return getConnectedNodes(connectionsBySourceNode, nodeName, type, depth);
}

View File

@@ -0,0 +1,98 @@
import { NodeConnectionTypes } from '../interfaces';
import type { IConnections, NodeConnectionType } from '../interfaces';
/**
* Gets all the nodes which are connected nodes starting from
* the given one
*
* @param {NodeConnectionType} [type='main']
* @param {*} [depth=-1]
*/
export function getConnectedNodes(
connections: IConnections,
nodeName: string,
connectionType: NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionTypes.Main,
depth = -1,
checkedNodesIncoming?: string[],
): string[] {
const newDepth = depth === -1 ? depth : depth - 1;
if (depth === 0) {
// Reached max depth
return [];
}
if (!connections.hasOwnProperty(nodeName)) {
// Node does not have incoming connections
return [];
}
let types: NodeConnectionType[];
if (connectionType === 'ALL') {
types = Object.keys(connections[nodeName]) as NodeConnectionType[];
} else if (connectionType === 'ALL_NON_MAIN') {
types = Object.keys(connections[nodeName]).filter(
(type) => type !== 'main',
) as NodeConnectionType[];
} else {
types = [connectionType];
}
let addNodes: string[];
let nodeIndex: number;
let i: number;
let parentNodeName: string;
const returnNodes: string[] = [];
types.forEach((type) => {
if (!connections[nodeName].hasOwnProperty(type)) {
// Node does not have incoming connections of given type
return;
}
const checkedNodes = checkedNodesIncoming ? [...checkedNodesIncoming] : [];
if (checkedNodes.includes(nodeName)) {
// Node got checked already before
return;
}
checkedNodes.push(nodeName);
connections[nodeName][type].forEach((connectionsByIndex) => {
connectionsByIndex?.forEach((connection) => {
if (checkedNodes.includes(connection.node)) {
// Node got checked already before
return;
}
returnNodes.unshift(connection.node);
addNodes = getConnectedNodes(
connections,
connection.node,
connectionType,
newDepth,
checkedNodes,
);
for (i = addNodes.length; i--; i > 0) {
// Because nodes can have multiple parents it is possible that
// parts of the tree is parent of both and to not add nodes
// twice check first if they already got added before.
parentNodeName = addNodes[i];
nodeIndex = returnNodes.indexOf(parentNodeName);
if (nodeIndex !== -1) {
// Node got found before so remove it from current location
// that node-order stays correct
returnNodes.splice(nodeIndex, 1);
}
returnNodes.unshift(parentNodeName);
}
});
});
});
return returnNodes;
}

View File

@@ -0,0 +1,19 @@
import type { INode, INodes } from '../interfaces';
/**
* Returns the node with the given name if it exists else null
*
* @param {INodes} nodes Nodes to search in
* @param {string} name Name of the node to return
*/
export function getNodeByName(nodes: INodes | INode[], name: string) {
if (Array.isArray(nodes)) {
return nodes.find((node) => node.name === name) || null;
}
if (nodes.hasOwnProperty(name)) {
return nodes[name];
}
return null;
}

View File

@@ -0,0 +1,18 @@
import { getConnectedNodes } from './get-connected-nodes';
import { NodeConnectionTypes } from '../interfaces';
import type { IConnections, NodeConnectionType } from '../interfaces';
/**
* Returns all the nodes before the given one
*
* @param {NodeConnectionType} [type='main']
* @param {*} [depth=-1]
*/
export function getParentNodes(
connectionsByDestinationNode: IConnections,
nodeName: string,
type: NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionTypes.Main,
depth = -1,
): string[] {
return getConnectedNodes(connectionsByDestinationNode, nodeName, type, depth);
}

View File

@@ -0,0 +1,5 @@
export * from './get-child-nodes';
export * from './get-connected-nodes';
export * from './get-node-by-name';
export * from './get-parent-nodes';
export * from './map-connections-by-destination';

View File

@@ -0,0 +1,49 @@
/* eslint-disable @typescript-eslint/no-for-in-array */
import type { IConnections, NodeConnectionType } from '../interfaces';
export function mapConnectionsByDestination(connections: IConnections) {
const returnConnection: IConnections = {};
let connectionInfo;
let maxIndex: number;
for (const sourceNode in connections) {
if (!connections.hasOwnProperty(sourceNode)) {
continue;
}
for (const type of Object.keys(connections[sourceNode]) as NodeConnectionType[]) {
if (!connections[sourceNode].hasOwnProperty(type)) {
continue;
}
for (const inputIndex in connections[sourceNode][type]) {
if (!connections[sourceNode][type].hasOwnProperty(inputIndex)) {
continue;
}
for (connectionInfo of connections[sourceNode][type][inputIndex] ?? []) {
if (!returnConnection.hasOwnProperty(connectionInfo.node)) {
returnConnection[connectionInfo.node] = {};
}
if (!returnConnection[connectionInfo.node].hasOwnProperty(connectionInfo.type)) {
returnConnection[connectionInfo.node][connectionInfo.type] = [];
}
maxIndex = returnConnection[connectionInfo.node][connectionInfo.type].length - 1;
for (let j = maxIndex; j < connectionInfo.index; j++) {
returnConnection[connectionInfo.node][connectionInfo.type].push([]);
}
returnConnection[connectionInfo.node][connectionInfo.type][connectionInfo.index]?.push({
node: sourceNode,
type,
index: parseInt(inputIndex, 10),
});
}
}
}
}
return returnConnection;
}

View File

@@ -5,6 +5,7 @@ import * as TelemetryHelpers from './telemetry-helpers';
export * from './errors';
export * from './constants';
export * from './common';
export * from './cron';
export * from './deferred-promise';
export * from './global-state';

View File

@@ -1,6 +1,14 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-for-in-array */
import {
getNodeByName,
getConnectedNodes,
getChildNodes,
getParentNodes,
mapConnectionsByDestination,
} from './common';
import {
MANUAL_CHAT_TRIGGER_LANGCHAIN_NODE_TYPE,
NODES_WITH_RENAMABLE_CONTENT,
@@ -123,9 +131,7 @@ export class Workflow {
this.connectionsBySourceNode = parameters.connections;
// Save also the connections by the destination nodes
this.connectionsByDestinationNode = Workflow.getConnectionsByDestination(
parameters.connections,
);
this.connectionsByDestinationNode = mapConnectionsByDestination(parameters.connections);
this.active = parameters.active || false;
@@ -146,11 +152,6 @@ export class Workflow {
this.staticData.__dataChanged = true;
}
/**
* The default connections are by source node. This function rewrites them by destination nodes
* to easily find parent nodes.
*
*/
static getConnectionsByDestination(connections: IConnections): IConnections {
const returnConnection: IConnections = {};
@@ -288,11 +289,7 @@ export class Workflow {
* @param {string} nodeName Name of the node to return
*/
getNode(nodeName: string): INode | null {
if (this.nodes.hasOwnProperty(nodeName)) {
return this.nodes[nodeName];
}
return null;
return getNodeByName(this.nodes, nodeName);
}
/**
@@ -477,9 +474,7 @@ export class Workflow {
}
// Use the updated connections to create updated connections by destination nodes
this.connectionsByDestinationNode = Workflow.getConnectionsByDestination(
this.connectionsBySourceNode,
);
this.connectionsByDestinationNode = mapConnectionsByDestination(this.connectionsBySourceNode);
}
/**
@@ -576,7 +571,7 @@ export class Workflow {
type: NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionTypes.Main,
depth = -1,
): string[] {
return this.getConnectedNodes(this.connectionsBySourceNode, nodeName, type, depth);
return getChildNodes(this.connectionsBySourceNode, nodeName, type, depth);
}
/**
@@ -590,7 +585,7 @@ export class Workflow {
type: NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionTypes.Main,
depth = -1,
): string[] {
return this.getConnectedNodes(this.connectionsByDestinationNode, nodeName, type, depth);
return getParentNodes(this.connectionsByDestinationNode, nodeName, type, depth);
}
/**
@@ -607,87 +602,7 @@ export class Workflow {
depth = -1,
checkedNodesIncoming?: string[],
): string[] {
depth = depth === -1 ? -1 : depth;
const newDepth = depth === -1 ? depth : depth - 1;
if (depth === 0) {
// Reached max depth
return [];
}
if (!connections.hasOwnProperty(nodeName)) {
// Node does not have incoming connections
return [];
}
let types: NodeConnectionType[];
if (connectionType === 'ALL') {
types = Object.keys(connections[nodeName]) as NodeConnectionType[];
} else if (connectionType === 'ALL_NON_MAIN') {
types = Object.keys(connections[nodeName]).filter(
(type) => type !== 'main',
) as NodeConnectionType[];
} else {
types = [connectionType];
}
let addNodes: string[];
let nodeIndex: number;
let i: number;
let parentNodeName: string;
const returnNodes: string[] = [];
types.forEach((type) => {
if (!connections[nodeName].hasOwnProperty(type)) {
// Node does not have incoming connections of given type
return;
}
const checkedNodes = checkedNodesIncoming ? [...checkedNodesIncoming] : [];
if (checkedNodes.includes(nodeName)) {
// Node got checked already before
return;
}
checkedNodes.push(nodeName);
connections[nodeName][type].forEach((connectionsByIndex) => {
connectionsByIndex?.forEach((connection) => {
if (checkedNodes.includes(connection.node)) {
// Node got checked already before
return;
}
returnNodes.unshift(connection.node);
addNodes = this.getConnectedNodes(
connections,
connection.node,
connectionType,
newDepth,
checkedNodes,
);
for (i = addNodes.length; i--; i > 0) {
// Because nodes can have multiple parents it is possible that
// parts of the tree is parent of both and to not add nodes
// twice check first if they already got added before.
parentNodeName = addNodes[i];
nodeIndex = returnNodes.indexOf(parentNodeName);
if (nodeIndex !== -1) {
// Node got found before so remove it from current location
// that node-order stays correct
returnNodes.splice(nodeIndex, 1);
}
returnNodes.unshift(parentNodeName);
}
});
});
});
return returnNodes;
return getConnectedNodes(connections, nodeName, connectionType, depth, checkedNodesIncoming);
}
/**

View File

@@ -0,0 +1,112 @@
import type { IConnections, IConnection } from '../src/interfaces';
import { NodeConnectionTypes } from '../src/interfaces';
import { mapConnectionsByDestination } from '../src/common';
describe('getConnectionsByDestination', () => {
it('should return empty object when there are no connections', () => {
const result = mapConnectionsByDestination({});
expect(result).toEqual({});
});
it('should return connections by destination node', () => {
const connections: IConnections = {
Node1: {
[NodeConnectionTypes.Main]: [
[
{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 },
{ node: 'Node3', type: NodeConnectionTypes.Main, index: 1 },
],
],
},
};
const result = mapConnectionsByDestination(connections);
expect(result).toEqual({
Node2: {
[NodeConnectionTypes.Main]: [[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }]],
},
Node3: {
[NodeConnectionTypes.Main]: [
[],
[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }],
],
},
});
});
it('should handle multiple connection types', () => {
const connections: IConnections = {
Node1: {
[NodeConnectionTypes.Main]: [[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }]],
[NodeConnectionTypes.AiAgent]: [
[{ node: 'Node3', type: NodeConnectionTypes.AiAgent, index: 0 }],
],
},
};
const result = mapConnectionsByDestination(connections);
expect(result).toEqual({
Node2: {
[NodeConnectionTypes.Main]: [[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }]],
},
Node3: {
[NodeConnectionTypes.AiAgent]: [
[{ node: 'Node1', type: NodeConnectionTypes.AiAgent, index: 0 }],
],
},
});
});
it('should handle nodes with no connections', () => {
const connections: IConnections = {
Node1: {
[NodeConnectionTypes.Main]: [[]],
},
};
const result = mapConnectionsByDestination(connections);
expect(result).toEqual({});
});
// @issue https://linear.app/n8n/issue/N8N-7880/cannot-load-some-templates
it('should handle nodes with null connections', () => {
const connections: IConnections = {
Node1: {
[NodeConnectionTypes.Main]: [
null as unknown as IConnection[],
[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }],
],
},
};
const result = mapConnectionsByDestination(connections);
expect(result).toEqual({
Node2: {
[NodeConnectionTypes.Main]: [[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 1 }]],
},
});
});
it('should handle nodes with multiple input connections', () => {
const connections: IConnections = {
Node1: {
[NodeConnectionTypes.Main]: [[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }]],
},
Node3: {
[NodeConnectionTypes.Main]: [[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }]],
},
};
const result = mapConnectionsByDestination(connections);
expect(result).toEqual({
Node2: {
[NodeConnectionTypes.Main]: [
[
{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 },
{ node: 'Node3', type: NodeConnectionTypes.Main, index: 0 },
],
],
},
});
});
});

View File

@@ -2106,127 +2106,6 @@ describe('Workflow', () => {
});
});
describe('getConnectionsByDestination', () => {
it('should return empty object when there are no connections', () => {
const result = Workflow.getConnectionsByDestination({});
expect(result).toEqual({});
});
it('should return connections by destination node', () => {
const connections: IConnections = {
Node1: {
[NodeConnectionTypes.Main]: [
[
{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 },
{ node: 'Node3', type: NodeConnectionTypes.Main, index: 1 },
],
],
},
};
const result = Workflow.getConnectionsByDestination(connections);
expect(result).toEqual({
Node2: {
[NodeConnectionTypes.Main]: [
[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }],
],
},
Node3: {
[NodeConnectionTypes.Main]: [
[],
[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }],
],
},
});
});
it('should handle multiple connection types', () => {
const connections: IConnections = {
Node1: {
[NodeConnectionTypes.Main]: [
[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }],
],
[NodeConnectionTypes.AiAgent]: [
[{ node: 'Node3', type: NodeConnectionTypes.AiAgent, index: 0 }],
],
},
};
const result = Workflow.getConnectionsByDestination(connections);
expect(result).toEqual({
Node2: {
[NodeConnectionTypes.Main]: [
[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }],
],
},
Node3: {
[NodeConnectionTypes.AiAgent]: [
[{ node: 'Node1', type: NodeConnectionTypes.AiAgent, index: 0 }],
],
},
});
});
it('should handle nodes with no connections', () => {
const connections: IConnections = {
Node1: {
[NodeConnectionTypes.Main]: [[]],
},
};
const result = Workflow.getConnectionsByDestination(connections);
expect(result).toEqual({});
});
// @issue https://linear.app/n8n/issue/N8N-7880/cannot-load-some-templates
it('should handle nodes with null connections', () => {
const connections: IConnections = {
Node1: {
[NodeConnectionTypes.Main]: [
null as unknown as IConnection[],
[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }],
],
},
};
const result = Workflow.getConnectionsByDestination(connections);
expect(result).toEqual({
Node2: {
[NodeConnectionTypes.Main]: [
[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 1 }],
],
},
});
});
it('should handle nodes with multiple input connections', () => {
const connections: IConnections = {
Node1: {
[NodeConnectionTypes.Main]: [
[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }],
],
},
Node3: {
[NodeConnectionTypes.Main]: [
[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }],
],
},
};
const result = Workflow.getConnectionsByDestination(connections);
expect(result).toEqual({
Node2: {
[NodeConnectionTypes.Main]: [
[
{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 },
{ node: 'Node3', type: NodeConnectionTypes.Main, index: 0 },
],
],
},
});
});
});
describe('getHighestNode', () => {
const createNode = (name: string, disabled = false) =>
({

View File

@@ -0,0 +1,12 @@
diff --git a/package.json b/package.json
index 2d52edb23f0c7defdfbf5f95ad1ee1fa75672b41..8d9dcbdc07cb5c73e629e22696235b19db98940d 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"main": "dist/index.cjs",
"type": "module",
"exports": {
+ "types": "./dist/highlight.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},

25
pnpm-lock.yaml generated
View File

@@ -204,6 +204,9 @@ overrides:
date-fns-tz: 2.0.0
patchedDependencies:
'@lezer/highlight':
hash: 97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c
path: patches/@lezer__highlight.patch
'@types/express-serve-static-core@5.0.6':
hash: d602248fcd302cf5a794d1e85a411633ba9635ea5d566d6f2e0429c7ae0fa3eb
path: patches/@types__express-serve-static-core@5.0.6.patch
@@ -590,7 +593,7 @@ importers:
version: 6.9.3
'@lezer/highlight':
specifier: '*'
version: 1.1.1
version: 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)
'@lezer/lr':
specifier: ^1.4.0
version: 1.4.0
@@ -18284,7 +18287,7 @@ snapshots:
'@codemirror/state': 6.4.1
'@codemirror/view': 6.26.3
'@lezer/common': 1.1.0
'@lezer/highlight': 1.1.1
'@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)
'@lezer/lr': 1.4.0
style-mod: 4.1.0
@@ -18293,7 +18296,7 @@ snapshots:
'@codemirror/state': 6.3.3
'@codemirror/view': 6.22.3
'@lezer/common': 1.1.0
'@lezer/highlight': 1.1.1
'@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)
'@lezer/lr': 1.4.0
style-mod: 4.1.0
@@ -19467,7 +19470,7 @@ snapshots:
'@lezer/css@1.1.1':
dependencies:
'@lezer/highlight': 1.1.1
'@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)
'@lezer/lr': 1.4.0
'@lezer/generator@1.7.0':
@@ -19475,24 +19478,24 @@ snapshots:
'@lezer/common': 1.1.0
'@lezer/lr': 1.4.0
'@lezer/highlight@1.1.1':
'@lezer/highlight@1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)':
dependencies:
'@lezer/common': 1.1.0
'@lezer/html@1.3.0':
dependencies:
'@lezer/common': 1.1.0
'@lezer/highlight': 1.1.1
'@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)
'@lezer/lr': 1.4.0
'@lezer/javascript@1.0.2':
dependencies:
'@lezer/highlight': 1.1.1
'@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)
'@lezer/lr': 1.4.0
'@lezer/json@1.0.0':
dependencies:
'@lezer/highlight': 1.1.1
'@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)
'@lezer/lr': 1.4.0
'@lezer/lr@1.4.0':
@@ -19501,7 +19504,7 @@ snapshots:
'@lezer/python@1.1.5':
dependencies:
'@lezer/highlight': 1.1.1
'@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)
'@lezer/lr': 1.4.0
'@mdx-js/react@3.0.1(@types/react@18.0.27)(react@18.2.0)':
@@ -19605,7 +19608,7 @@ snapshots:
'@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.1.0)
'@codemirror/language': 6.10.1
'@codemirror/state': 6.4.1
'@lezer/highlight': 1.1.1
'@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)
'@lezer/lr': 1.4.0
transitivePeerDependencies:
- '@codemirror/view'
@@ -23724,7 +23727,7 @@ snapshots:
'@codemirror/view': 6.26.3
'@lezer/common': 1.1.0
'@lezer/css': 1.1.1
'@lezer/highlight': 1.1.1
'@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)
'@lezer/html': 1.3.0
'@lezer/lr': 1.4.0