Skip to content

Commit

Permalink
macro nodes (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
GabiGrin authored Jan 4, 2024
1 parent 4f7abb4 commit 6604db4
Show file tree
Hide file tree
Showing 61 changed files with 11,021 additions and 342 deletions.
21 changes: 15 additions & 6 deletions core/src/flow-schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod";
import { VisualNode, NodeDefinition, Node } from "./node";
import { VisualNode, NodeDefinition, Node, ResolvedVisualNode } from "./node";

const importSchema = z.record(z.string(), z.string().or(z.array(z.string())));
const position = z.strictObject({ x: z.number(), y: z.number() });
Expand Down Expand Up @@ -33,11 +33,20 @@ const instance = z
visibleOutputs: z.optional(z.array(z.string())),
nodeId: z.optional(z.string()),
node: z.optional(z.any()),
macroId: z.optional(z.string()),
macroData: z.optional(z.any()),
style: z.optional(nodeStyle),
})
.refine((val) => val.node || val.nodeId, {
message: "Instance must have either an inline node or refer to a nodeId",
});
.refine(
(val) =>
val.node ||
val.nodeId ||
(val.macroId && typeof val.macroData !== "undefined"),
{
message:
"Instance must have either an inline node or refer to a nodeId, or be a macro instance",
}
);

const inputPinSchema = z.union([
z.string(),
Expand Down Expand Up @@ -116,14 +125,14 @@ export type ResolvedDependenciesDefinitions = Record<
>;

export type ResolvedFlydeFlowDefinition = {
main: VisualNode;
main: ResolvedVisualNode;
dependencies: ResolvedDependenciesDefinitions;
};

export type ResolvedDependencies = Record<string, ImportedNode>;

export type ResolvedFlydeRuntimeFlow = {
main: VisualNode;
main: ResolvedVisualNode;
dependencies: ResolvedDependencies;
};

Expand Down
85 changes: 71 additions & 14 deletions core/src/node/node-instance.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { InputPinsConfig, Node, NodeDefinition, NodeStyle, Pos } from "..";
import {
CodeNode,
InputPinsConfig,
Node,
NodeDefinition,
NodeStyle,
Pos,
ResolvedVisualNode,
VisualNode,
} from "..";
import { slug } from "cuid";

export interface NodeInstanceConfig {
Expand All @@ -7,20 +16,42 @@ export interface NodeInstanceConfig {
visibleOutputs?: string[];
displayName?: string;
style?: NodeStyle;
id: string;
pos: Pos;
}

export interface RefNodeInstance extends NodeInstanceConfig {
id: string;
nodeId: string;
pos: Pos;
}

export interface InlineNodeInstance extends NodeInstanceConfig {
id: string;
node: Node;
pos: Pos;
node: VisualNode | CodeNode;
}

export interface ResolvedInlineNodeInstance extends NodeInstanceConfig {
node: ResolvedVisualNode | CodeNode;
}

export interface MacroNodeInstance extends NodeInstanceConfig {
macroId: string;
macroData: any;
}
export type NodeInstance = RefNodeInstance | InlineNodeInstance;

export interface ResolvedMacroNodeInstance extends NodeInstanceConfig {
nodeId: string;
macroId: string;
macroData: any;
}

export type NodeInstance =
| RefNodeInstance
| InlineNodeInstance
| MacroNodeInstance;

export type ResolvedNodeInstance =
| RefNodeInstance
| ResolvedInlineNodeInstance
| ResolvedMacroNodeInstance;

export const nodeInstance = (
id: string,
Expand All @@ -39,20 +70,46 @@ export const inlineNodeInstance = (
node: Node,
config?: InputPinsConfig,
pos?: Pos
): NodeInstance => ({
id,
node,
inputConfig: config || {},
pos: pos || { x: 0, y: 0 },
});
): NodeInstance =>
({
id,
node,
inputConfig: config || {},
pos: pos || { x: 0, y: 0 },
} as InlineNodeInstance);

export const macroNodeInstance = (
id: string,
macroId: string,
macroData: any,
config?: InputPinsConfig,
pos?: Pos
): ResolvedMacroNodeInstance =>
({
id,
macroId,
macroData,
inputConfig: config || {},
nodeId: `${macroId}__${id}`, // TODO: lift this concatenation to a higher level
pos: pos || { x: 0, y: 0 },
} as ResolvedMacroNodeInstance);

export const isInlineNodeInstance = (
ins: NodeInstance
): ins is InlineNodeInstance => {
return !!(ins as any).node;
};
export const isRefNodeInstance = (ins: NodeInstance): ins is RefNodeInstance =>
!isInlineNodeInstance(ins);
!!(ins as any).nodeId && !(ins as any).macroId;

export const isMacroNodeInstance = (
ins: NodeInstance
): ins is MacroNodeInstance => !!(ins as any).macroId;

export const isResolvedMacroNodeInstance = (
ins: ResolvedNodeInstance | NodeInstance
): ins is ResolvedMacroNodeInstance =>
!!(ins as any).macroId && !!(ins as any).nodeId;

export const NodeInstance = (
id: string,
Expand Down
78 changes: 73 additions & 5 deletions core/src/node/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import { Subject } from "rxjs";

import { CancelFn, InnerExecuteFn } from "../execute";
import { ConnectionData } from "../connect";
import { isInlineNodeInstance, NodeInstance } from "./node-instance";
import {
isInlineNodeInstance,
NodeInstance,
RefNodeInstance,
ResolvedMacroNodeInstance,
ResolvedNodeInstance,
} from "./node-instance";
import {
InputPin,
InputPinMap,
Expand Down Expand Up @@ -78,6 +84,11 @@ export interface BaseNode {
* Node's unique id. {@link VisualNode.instances } refer use this to refer to the correct node
*/
id: string;

/**
* A human readable name for the node. Used in the visual editor.
*/
displayName?: string;
/**
* Is displayed in the visual editor and used to search for nodes.
*/
Expand Down Expand Up @@ -174,6 +185,33 @@ export interface CodeNode extends BaseNode {
customView?: CustomNodeViewFn;
}

export interface MacroNode<T> {
id: string;
displayNameBuilder?: (data: T) => string;
defaultStyle?: NodeStyle;
description?: string;
definitionBuilder: (data: T) => Omit<CodeNodeDefinition, "id">;
runFnBuilder: (data: T) => CodeNode["run"];
defaultData: T;

/**
* Assumes you are bundling the editor component using webpack library+window config.
* The name of the window variable that holds the component should be __MacroNode__{id}
* The path should be relative to the root of the project (package.json location)
*/
editorComponentBundlePath: string;
}

export type MacroNodeDefinition<T> = Omit<
MacroNode<T>,
"definitionBuilder" | "runFnBuilder" | "editorComponentBundlePath"
> & {
/**
* Resolver will use this to load the editor component bundle into the editor
*/
editorComponentBundleContent: string;
};

export enum InlineValueNodeType {
VALUE = "value",
FUNCTION = "function",
Expand Down Expand Up @@ -211,6 +249,10 @@ export interface VisualNode extends BaseNode {
customView?: CustomNodeViewFn;
}

export interface ResolvedVisualNode extends VisualNode {
instances: ResolvedNodeInstance[];
}

export type Node = CodeNode | CustomNode;

export type ImportableSource = {
Expand All @@ -224,6 +266,7 @@ export type CustomNode = VisualNode | InlineValueNode;
export type CodeNodeDefinition = Omit<CodeNode, "run">;

export type NodeDefinition = CustomNode | CodeNodeDefinition;
export type NodeOrMacroDefinition = NodeDefinition | MacroNodeDefinition<any>;

export type NodeModuleMetaData = {
imported?: boolean;
Expand All @@ -240,6 +283,20 @@ export const isCodeNode = (p: Node | NodeDefinition | any): p is CodeNode => {
return isBaseNode(p) && typeof (p as CodeNode).run === "function";
};

export const isMacroNode = (p: any): p is MacroNode<any> => {
return p && typeof (p as MacroNode<any>).runFnBuilder === "function";
};

export const isMacroNodeDefinition = (
p: any
): p is MacroNodeDefinition<any> => {
return (
p &&
typeof (p as MacroNodeDefinition<any>).editorComponentBundleContent ===
"string"
);
};

export const isVisualNode = (p: Node | NodeDefinition): p is VisualNode => {
return !!(p as VisualNode).instances;
};
Expand Down Expand Up @@ -335,10 +392,18 @@ export const getNode = (
idOrIns: string | NodeInstance,
resolvedNodes: NodesCollection
): Node => {
if (typeof idOrIns !== "string" && isInlineNodeInstance(idOrIns)) {
return idOrIns.node;
const isOrInsResolved = idOrIns as string | ResolvedNodeInstance; // ugly type hack to avoid fixing the whole Resolved instances cases caused by macros. TODO: fix this by refactoring all places to use "ResolvedNodeInstance"
if (
typeof isOrInsResolved !== "string" &&
isInlineNodeInstance(isOrInsResolved)
) {
return isOrInsResolved.node;
}
const id = typeof idOrIns === "string" ? idOrIns : idOrIns.nodeId;
const id =
typeof isOrInsResolved === "string"
? isOrInsResolved
: isOrInsResolved.nodeId;

const node = resolvedNodes[id];
if (!node) {
throw new Error(`Node with id ${id} not found`);
Expand All @@ -353,7 +418,10 @@ export const getNodeDef = (
if (typeof idOrIns !== "string" && isInlineNodeInstance(idOrIns)) {
return idOrIns.node;
}
const id = typeof idOrIns === "string" ? idOrIns : idOrIns.nodeId;
const id =
typeof idOrIns === "string"
? idOrIns
: (idOrIns as RefNodeInstance | ResolvedMacroNodeInstance).nodeId;
const node = resolvedNodes[id];
if (!node) {
console.error(`Node with id ${id} not found`);
Expand Down
6 changes: 4 additions & 2 deletions dev-server/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
FlydeFlow,
ImportableSource,
ResolvedDependenciesDefinitions,
ResolvedFlydeFlow,
ResolvedFlydeFlowDefinition,
} from "@flyde/core";
import { FolderStructure } from "./fs-helper/shared";
import type { ImportablesResult } from "./service/scan-importable-nodes";
Expand All @@ -25,9 +27,9 @@ export const createDevServerClient = (baseUrl: string) => {
},
resolveDefinitions: (
filename: string
): Promise<ResolvedDependenciesDefinitions> => {
): Promise<ResolvedFlydeFlowDefinition> => {
return axios
.get(`${baseUrl}/resolveDefinitions?filename=${filename}`)
.get(`${baseUrl}/resolveFlow?filename=${filename}`)
.then((res) => res.data);
},
getImportables: (filename: string): Promise<ImportablesResult> => {
Expand Down
9 changes: 4 additions & 5 deletions dev-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createService } from "./service/service";
import { setupRemoteDebuggerServer } from "@flyde/remote-debugger/dist/setup-server";
import { createServer } from "http";
import { scanImportableNodes } from "./service/scan-importable-nodes";
import { deserializeFlow, resolveDependencies } from "@flyde/resolver";
import { deserializeFlow, resolveFlowByPath } from "@flyde/resolver";
import { join } from "path";

import resolveFrom = require("resolve-from");
Expand Down Expand Up @@ -68,7 +68,7 @@ export const runDevServer = (
}
});

app.get("/resolveDefinitions", async (req, res) => {
app.get("/resolveFlow", async (req, res) => {
try {
const { filename } = req.query as { filename: string };
if (!filename) {
Expand All @@ -77,9 +77,8 @@ export const runDevServer = (
}

const fullPath = resolveFrom(rootDir, filename);
const flow = deserializeFlow(readFileSync(fullPath, "utf-8"), fullPath);
const deps = await resolveDependencies(flow, "definition", fullPath);
res.send({ ...deps, [flow.node.id]: flow.node });
const resolvedFlow = await resolveFlowByPath(fullPath, "definition");
res.send(resolvedFlow);
} catch (e) {
console.error(e);
res.status(400).send(e);
Expand Down
5 changes: 2 additions & 3 deletions dev-server/src/service/generate-node-from-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import {
CodeNode,
ImportableSource,
ImportedNode,
RunNodeFunction,
randomInt,
} from "@flyde/core";
import { resolveCodeNodeDependencies } from "@flyde/resolver";
import { resolveCodeNodeDependencies, resolveFlow } from "@flyde/resolver";
import axios from "axios";
import { existsSync, writeFileSync } from "fs";

Expand Down Expand Up @@ -77,7 +76,7 @@ export async function generateAndSaveNode(
}

const node: ImportedNode = {
...maybeNode.node,
...(maybeNode.node as CodeNode),
source: { path: filePath, export: maybeNode.exportName },
};
return { node, module: `./${fileName}.flyde.ts` };
Expand Down
Loading

0 comments on commit 6604db4

Please sign in to comment.