visual-node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +42 -0
  2. package/dist/app.d.ts +3 -0
  3. package/dist/app.js +44 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/codegen-helpers.d.ts +55 -0
  6. package/dist/codegen-helpers.js +142 -0
  7. package/dist/codegen-helpers.js.map +1 -0
  8. package/dist/config.d.ts +9 -0
  9. package/dist/config.js +13 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/connect/compile-function-graph.service.d.ts +14 -0
  12. package/dist/connect/compile-function-graph.service.js +106 -0
  13. package/dist/connect/compile-function-graph.service.js.map +1 -0
  14. package/dist/connect/files.service.d.ts +7 -0
  15. package/dist/connect/files.service.js +175 -0
  16. package/dist/connect/files.service.js.map +1 -0
  17. package/dist/connect/node-registry-flow.service.d.ts +14 -0
  18. package/dist/connect/node-registry-flow.service.js +142 -0
  19. package/dist/connect/node-registry-flow.service.js.map +1 -0
  20. package/dist/connect/plugins.service.d.ts +24 -0
  21. package/dist/connect/plugins.service.js +73 -0
  22. package/dist/connect/plugins.service.js.map +1 -0
  23. package/dist/connect/run.service.d.ts +3 -0
  24. package/dist/connect/run.service.js +159 -0
  25. package/dist/connect/run.service.js.map +1 -0
  26. package/dist/connect/validate-generate.service.d.ts +19 -0
  27. package/dist/connect/validate-generate.service.js +102 -0
  28. package/dist/connect/validate-generate.service.js.map +1 -0
  29. package/dist/file-tree.d.ts +23 -0
  30. package/dist/file-tree.js +63 -0
  31. package/dist/file-tree.js.map +1 -0
  32. package/dist/flow-shape.d.ts +8 -0
  33. package/dist/flow-shape.js +13 -0
  34. package/dist/flow-shape.js.map +1 -0
  35. package/dist/path-safety.d.ts +12 -0
  36. package/dist/path-safety.js +26 -0
  37. package/dist/path-safety.js.map +1 -0
  38. package/dist/plugin-loading.d.ts +18 -0
  39. package/dist/plugin-loading.js +47 -0
  40. package/dist/plugin-loading.js.map +1 -0
  41. package/dist/plugin-readme.d.ts +9 -0
  42. package/dist/plugin-readme.js +235 -0
  43. package/dist/plugin-readme.js.map +1 -0
  44. package/dist/public/assets/index-6_2vDtnd.css +1 -0
  45. package/dist/public/assets/index-BpXY8lVq.js +101 -0
  46. package/dist/public/index.html +13 -0
  47. package/dist/runner.d.ts +23 -0
  48. package/dist/runner.js +65 -0
  49. package/dist/runner.js.map +1 -0
  50. package/dist/server.d.ts +2 -0
  51. package/dist/server.js +34 -0
  52. package/dist/server.js.map +1 -0
  53. package/dist/static.d.ts +12 -0
  54. package/dist/static.js +28 -0
  55. package/dist/static.js.map +1 -0
  56. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ > **🤖 VIBE CODED.** This project was built end-to-end through AI-assisted ("vibe
2
+ > coding") development sessions. Review the generated code before trusting it in
3
+ > production, same as you would for any dependency.
4
+
5
+ # visual-node
6
+
7
+ Visual, node-based backend builder for Node.js. Drag-and-drop flows **compile** to real,
8
+ readable, git-friendly Express.js source — this is a codegen tool, not a runtime
9
+ interpreter. Nothing from this package ships inside the servers it generates.
10
+
11
+ ## Install & run
12
+
13
+ ```bash
14
+ npx visual-node [projectDir]
15
+ # or
16
+ npm install -g visual-node
17
+ visual-node [projectDir]
18
+ ```
19
+
20
+ Opens an editor at `http://localhost:4000` against `projectDir` (defaults to the current
21
+ directory). Build a flow, **Compile** it to a real Express server, **Run Server** to spawn
22
+ and hit it right there, or just read the generated files yourself.
23
+
24
+ ## Environment variables
25
+
26
+ | Variable | Default | Purpose |
27
+ | --- | --- | --- |
28
+ | `PORT` | `4000` | Port the editor listens on |
29
+ | `FLOWSERVER_PROJECT_DIR` | current directory | Project directory (overridden by a CLI arg) |
30
+
31
+ ## Examples
32
+
33
+ The source repository ships five worked examples under `examples/` — a minimal route, an
34
+ in-memory REST API using Variables, a custom-middleware logger, a visual Function Graph
35
+ with branching, and an npm-dependency example — each with its source flow and compiled
36
+ output committed side by side.
37
+
38
+ ## What this is not
39
+
40
+ Not a runtime interpreter (no Node-RED/n8n-style flow engine ships with generated
41
+ servers), not a hosted service, and not a multi-tenant tool — it's a local, single-project
42
+ codegen editor meant to hand you real source code you keep, read, and commit.
package/dist/app.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type Express } from "express";
2
+ import type { AppConfig } from "./config.js";
3
+ export declare function buildApp(config: AppConfig): Express;
package/dist/app.js ADDED
@@ -0,0 +1,44 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import { expressConnectMiddleware } from "@connectrpc/connect-express";
4
+ import { registerNodeRegistryFlowRoutes } from "./connect/node-registry-flow.service.js";
5
+ import { registerValidateGenerateRoutes } from "./connect/validate-generate.service.js";
6
+ import { registerRunRoutes } from "./connect/run.service.js";
7
+ import { registerFilesRoutes } from "./connect/files.service.js";
8
+ import { registerCompileFunctionGraphRoutes } from "./connect/compile-function-graph.service.js";
9
+ import { registerPluginsRoutes } from "./connect/plugins.service.js";
10
+ import { serveStatic } from "./static.js";
11
+ export function buildApp(config) {
12
+ const app = express();
13
+ app.use(cors());
14
+ // Buf Connect transport (gRPC / gRPC-Web / Connect protocol) — the only API surface as of
15
+ // Phase 8; see docs/phase8-backend-grpc-flatbuffers-plan.md. The REST routes this replaced
16
+ // (`routes/*.routes.ts`) were removed once parity was confirmed end-to-end. No
17
+ // `express.json()` is mounted: Connect owns its own body parsing entirely, and nothing
18
+ // else in this app reads `req.body`.
19
+ //
20
+ // All six service-group registrations share ONE router instance and use `router.rpc()`
21
+ // per method (not `router.service()`), since `router.service()` fills every method the
22
+ // service defines that's absent from its partial implementation with an "unimplemented"
23
+ // stub — calling it more than once per service on a shared router would let a later
24
+ // group's stub-fill silently overwrite an earlier group's real methods.
25
+ app.use(expressConnectMiddleware({
26
+ // editor-ui's dev server (vite.config.ts) only proxies "/api" to editor-server, and
27
+ // its browser Connect client (src/api/client.ts) is configured with `baseUrl: "/api"`
28
+ // to match — without this prefix, Connect requests would land at the Express app
29
+ // root (e.g. "/flowserver.v1.EditorService/GetNodeRegistry") and never reach this
30
+ // middleware through the dev proxy.
31
+ requestPathPrefix: "/api",
32
+ routes: (router) => {
33
+ registerNodeRegistryFlowRoutes(router, config);
34
+ registerValidateGenerateRoutes(router, config);
35
+ registerRunRoutes(router, config);
36
+ registerFilesRoutes(router, config);
37
+ registerCompileFunctionGraphRoutes(router, config);
38
+ registerPluginsRoutes(router, config);
39
+ },
40
+ }));
41
+ serveStatic(app);
42
+ return app;
43
+ }
44
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAyB,MAAM,SAAS,CAAC;AAChD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAEvE,OAAO,EAAE,8BAA8B,EAAE,MAAM,yCAAyC,CAAC;AACzF,OAAO,EAAE,8BAA8B,EAAE,MAAM,wCAAwC,CAAC;AACxF,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,kCAAkC,EAAE,MAAM,6CAA6C,CAAC;AACjG,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,UAAU,QAAQ,CAAC,MAAiB;IACxC,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IAEtB,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAEhB,0FAA0F;IAC1F,2FAA2F;IAC3F,+EAA+E;IAC/E,uFAAuF;IACvF,qCAAqC;IACrC,EAAE;IACF,uFAAuF;IACvF,uFAAuF;IACvF,wFAAwF;IACxF,oFAAoF;IACpF,wEAAwE;IACxE,GAAG,CAAC,GAAG,CACL,wBAAwB,CAAC;QACvB,oFAAoF;QACpF,sFAAsF;QACtF,iFAAiF;QACjF,kFAAkF;QAClF,oCAAoC;QACpC,iBAAiB,EAAE,MAAM;QACzB,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;YACjB,8BAA8B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC/C,8BAA8B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC/C,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAClC,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACpC,kCAAkC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACnD,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACxC,CAAC;KACF,CAAC,CACH,CAAC;IAEF,WAAW,CAAC,GAAG,CAAC,CAAC;IAEjB,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { type Flow, type ProjectCompileResult, type ProjectFile, type ValidationError } from "@visual-node/core";
2
+ export type CompileResult = {
3
+ valid: true;
4
+ code: string;
5
+ } | {
6
+ valid: false;
7
+ errors: ValidationError[];
8
+ };
9
+ export declare function compile(flow: Flow): Promise<CompileResult>;
10
+ export interface ProjectCompileFromDisk {
11
+ /** The raw, parsed `.blueprint` sources read from disk — same order as `result.files` when `result.valid`. */
12
+ sourceFiles: ProjectFile[];
13
+ result: ProjectCompileResult;
14
+ }
15
+ /**
16
+ * Reads every `.blueprint` file in the project, parses it, and runs core's project-wide
17
+ * `compileProject`. A read/parse failure on an individual file becomes a per-file error
18
+ * (excluded from the compiled batch) rather than 500ing the whole request. Shared by
19
+ * `/api/compile` and `/api/run/start` — both need "the whole project, compiled from disk",
20
+ * not just whatever flow happens to be open in the canvas.
21
+ */
22
+ export declare function compileProjectFromDisk(projectDir: string): Promise<ProjectCompileFromDisk>;
23
+ /**
24
+ * Finds the one file in the project responsible for starting the server — the file whose
25
+ * flow contains an "express.listen" node. That's the file "Run Server" needs to spawn;
26
+ * everything else (helpers required by it) just needs to be written to disk alongside it.
27
+ */
28
+ export declare function findEntryFile(sourceFiles: ProjectFile[]): ProjectFile | {
29
+ error: string;
30
+ };
31
+ /**
32
+ * Writes (or merges into) a CommonJS package.json in `projectDir`. Required for the
33
+ * generated `server.js` (which uses `require`) to run correctly with plain
34
+ * `node server.js` — without it, an ancestor directory's "type": "module" (or no manifest
35
+ * at all) can make Node misidentify the file's module type. Declaring dependencies (not
36
+ * just omitting `type: module`) is what makes `npm install && node server.js` actually
37
+ * work, instead of leaving the user to guess what to install.
38
+ *
39
+ * Unlike the original write-once behavior, this now merges on every call: an existing
40
+ * package.json's fields (including any hand-edited `dependencies` entries) are preserved,
41
+ * `express` is added only if missing, and `options.dependencies` (the flow/project's
42
+ * collected npm-mode `logic.require`/Custom Code declarations — see
43
+ * `@visual-node/core`'s `collectFlowDependencies`/`collectProjectDependencies`) is merged in
44
+ * without ever downgrading/overwriting an already-pinned real version. A dependency
45
+ * key is only written/updated when it's currently absent or currently the placeholder
46
+ * `"*"` (an earlier compile's "unpinned" marker) — a real pinned version a user hand-edited
47
+ * in always wins.
48
+ */
49
+ export declare function ensureCommonJsPackageJson(projectDir: string, name: string, options?: {
50
+ dependencies?: Record<string, string>;
51
+ }): Promise<void>;
52
+ export declare function nodeModulesInstalled(projectDir: string): Promise<{
53
+ installed: boolean;
54
+ missing: string[];
55
+ }>;
@@ -0,0 +1,142 @@
1
+ import { access, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { compileProject, decodeFlow, emitExpress, formatCode, validateFlow, } from "@visual-node/core";
4
+ import { listBlueprintFiles } from "./file-tree.js";
5
+ const EXPRESS_DEPENDENCY_VERSION = "^4.19.2";
6
+ export async function compile(flow) {
7
+ const validation = validateFlow(flow);
8
+ if (!validation.valid) {
9
+ return { valid: false, errors: validation.errors };
10
+ }
11
+ const { code } = emitExpress(flow);
12
+ const formatted = await formatCode(code);
13
+ return { valid: true, code: formatted };
14
+ }
15
+ /**
16
+ * Reads every `.blueprint` file in the project, parses it, and runs core's project-wide
17
+ * `compileProject`. A read/parse failure on an individual file becomes a per-file error
18
+ * (excluded from the compiled batch) rather than 500ing the whole request. Shared by
19
+ * `/api/compile` and `/api/run/start` — both need "the whole project, compiled from disk",
20
+ * not just whatever flow happens to be open in the canvas.
21
+ */
22
+ export async function compileProjectFromDisk(projectDir) {
23
+ const blueprintRefs = await listBlueprintFiles(projectDir);
24
+ const files = [];
25
+ const readErrors = [];
26
+ for (const ref of blueprintRefs) {
27
+ const absolutePath = path.join(projectDir, ref.relativePath);
28
+ let raw;
29
+ try {
30
+ raw = await readFile(absolutePath);
31
+ }
32
+ catch (err) {
33
+ readErrors.push({ relativePath: ref.relativePath, message: `Failed to read file: ${err.message}` });
34
+ continue;
35
+ }
36
+ try {
37
+ const flow = decodeFlow(new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength));
38
+ files.push({ relativePath: ref.relativePath, flow });
39
+ }
40
+ catch (err) {
41
+ readErrors.push({ relativePath: ref.relativePath, message: `Failed to decode: ${err.message}` });
42
+ }
43
+ }
44
+ const result = await compileProject(files);
45
+ if (readErrors.length === 0)
46
+ return { sourceFiles: files, result };
47
+ if (result.valid)
48
+ return { sourceFiles: files, result: { valid: false, errors: readErrors } };
49
+ return { sourceFiles: files, result: { valid: false, errors: [...readErrors, ...result.errors] } };
50
+ }
51
+ /**
52
+ * Finds the one file in the project responsible for starting the server — the file whose
53
+ * flow contains an "express.listen" node. That's the file "Run Server" needs to spawn;
54
+ * everything else (helpers required by it) just needs to be written to disk alongside it.
55
+ */
56
+ export function findEntryFile(sourceFiles) {
57
+ const candidates = sourceFiles.filter((f) => f.flow.nodes.some((n) => n.type === "express.listen"));
58
+ if (candidates.length === 0) {
59
+ return { error: 'No file in this project has an "express.listen" node yet — nothing to run.' };
60
+ }
61
+ if (candidates.length > 1) {
62
+ return {
63
+ error: `Multiple files call "express.listen" (${candidates
64
+ .map((f) => f.relativePath)
65
+ .join(", ")}) — only one entry file is supported.`,
66
+ };
67
+ }
68
+ return candidates[0];
69
+ }
70
+ async function pathExists(target) {
71
+ try {
72
+ await access(target);
73
+ return true;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ /**
80
+ * Writes (or merges into) a CommonJS package.json in `projectDir`. Required for the
81
+ * generated `server.js` (which uses `require`) to run correctly with plain
82
+ * `node server.js` — without it, an ancestor directory's "type": "module" (or no manifest
83
+ * at all) can make Node misidentify the file's module type. Declaring dependencies (not
84
+ * just omitting `type: module`) is what makes `npm install && node server.js` actually
85
+ * work, instead of leaving the user to guess what to install.
86
+ *
87
+ * Unlike the original write-once behavior, this now merges on every call: an existing
88
+ * package.json's fields (including any hand-edited `dependencies` entries) are preserved,
89
+ * `express` is added only if missing, and `options.dependencies` (the flow/project's
90
+ * collected npm-mode `logic.require`/Custom Code declarations — see
91
+ * `@visual-node/core`'s `collectFlowDependencies`/`collectProjectDependencies`) is merged in
92
+ * without ever downgrading/overwriting an already-pinned real version. A dependency
93
+ * key is only written/updated when it's currently absent or currently the placeholder
94
+ * `"*"` (an earlier compile's "unpinned" marker) — a real pinned version a user hand-edited
95
+ * in always wins.
96
+ */
97
+ export async function ensureCommonJsPackageJson(projectDir, name, options) {
98
+ const pkgPath = path.join(projectDir, "package.json");
99
+ let manifest;
100
+ if (await pathExists(pkgPath)) {
101
+ try {
102
+ const raw = await readFile(pkgPath, "utf8");
103
+ manifest = { ...JSON.parse(raw) };
104
+ }
105
+ catch {
106
+ manifest = { name, private: true };
107
+ }
108
+ }
109
+ else {
110
+ manifest = { name, private: true };
111
+ }
112
+ const dependencies = { ...(manifest.dependencies ?? {}) };
113
+ if (!dependencies.express) {
114
+ dependencies.express = EXPRESS_DEPENDENCY_VERSION;
115
+ }
116
+ for (const [pkg, version] of Object.entries(options?.dependencies ?? {})) {
117
+ if (!dependencies[pkg] || dependencies[pkg] === "*") {
118
+ dependencies[pkg] = version;
119
+ }
120
+ }
121
+ manifest.dependencies = dependencies;
122
+ await writeFile(pkgPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
123
+ }
124
+ export async function nodeModulesInstalled(projectDir) {
125
+ const pkgPath = path.join(projectDir, "package.json");
126
+ let dependencies = {};
127
+ try {
128
+ const raw = await readFile(pkgPath, "utf8");
129
+ dependencies = JSON.parse(raw).dependencies ?? {};
130
+ }
131
+ catch {
132
+ dependencies = {};
133
+ }
134
+ const missing = [];
135
+ for (const pkg of Object.keys(dependencies)) {
136
+ if (!(await pathExists(path.join(projectDir, "node_modules", pkg)))) {
137
+ missing.push(pkg);
138
+ }
139
+ }
140
+ return { installed: missing.length === 0, missing };
141
+ }
142
+ //# sourceMappingURL=codegen-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codegen-helpers.js","sourceRoot":"","sources":["../src/codegen-helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export interface AppConfig {
2
+ projectDir: string;
3
+ }
4
+ /**
5
+ * Resolves the single project directory this server instance operates on:
6
+ * first CLI arg, then FLOWSERVER_PROJECT_DIR, then cwd. This is a local,
7
+ * single-project tool — no multi-project registry.
8
+ */
9
+ export declare function resolveProjectDir(argv?: string[]): string;
package/dist/config.js ADDED
@@ -0,0 +1,13 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Resolves the single project directory this server instance operates on:
4
+ * first CLI arg, then FLOWSERVER_PROJECT_DIR, then cwd. This is a local,
5
+ * single-project tool — no multi-project registry.
6
+ */
7
+ export function resolveProjectDir(argv = process.argv.slice(2)) {
8
+ // `pnpm run dev -- <dir>` forwards a literal "--" through tsx watch into argv; skip it.
9
+ const fromArg = argv.find((a) => a !== "--");
10
+ const dir = fromArg ?? process.env.FLOWSERVER_PROJECT_DIR ?? process.cwd();
11
+ return path.resolve(dir);
12
+ }
13
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAM7B;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAiB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,wFAAwF;IACxF,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAG,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3E,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { ConnectRouter } from "@connectrpc/connect";
2
+ import type { AppConfig } from "../config.js";
3
+ /**
4
+ * Registers the `CompileProject`, `WriteCompiledProject`, and `PreviewFunctionGraph` RPCs
5
+ * from `EditorService` (see proto-gen's `editor_pb.ts` and
6
+ * docs/phase8-backend-grpc-flatbuffers-plan.md). Business logic is not duplicated here: every
7
+ * handler below is a thin Connect-shaped wrapper around the exact same helpers the REST routes
8
+ * call — `compileProjectFromDisk`/`ensureCommonJsPackageJson`
9
+ * (packages/editor-server/src/codegen-helpers.ts, also used by
10
+ * packages/editor-server/src/routes/compile.routes.ts) and `emitFunctionGraphBody`/
11
+ * `FunctionGraphError` (packages/core, also used by
12
+ * packages/editor-server/src/routes/function-graph.routes.ts).
13
+ */
14
+ export declare function registerCompileFunctionGraphRoutes(router: ConnectRouter, config: AppConfig): ConnectRouter;
@@ -0,0 +1,106 @@
1
+ import path from "node:path";
2
+ import { EditorService } from "@visual-node/proto-gen";
3
+ import { collectProjectDependencies, decodeFlow, emitFunctionGraphBody, FunctionGraphError, writeGeneratedFile, } from "@visual-node/core";
4
+ import { compileProjectFromDisk, ensureCommonJsPackageJson } from "../codegen-helpers.js";
5
+ /** Maps core/editor-server's `ProjectFileError` onto the generated `ValidationError` message init shape. */
6
+ function toValidationErrorInit(err) {
7
+ return {
8
+ nodeId: err.nodeId ?? "",
9
+ blueprintNodeId: err.blueprintNodeId ?? "",
10
+ message: err.message,
11
+ relativePath: err.relativePath,
12
+ };
13
+ }
14
+ /**
15
+ * Registers the `CompileProject`, `WriteCompiledProject`, and `PreviewFunctionGraph` RPCs
16
+ * from `EditorService` (see proto-gen's `editor_pb.ts` and
17
+ * docs/phase8-backend-grpc-flatbuffers-plan.md). Business logic is not duplicated here: every
18
+ * handler below is a thin Connect-shaped wrapper around the exact same helpers the REST routes
19
+ * call — `compileProjectFromDisk`/`ensureCommonJsPackageJson`
20
+ * (packages/editor-server/src/codegen-helpers.ts, also used by
21
+ * packages/editor-server/src/routes/compile.routes.ts) and `emitFunctionGraphBody`/
22
+ * `FunctionGraphError` (packages/core, also used by
23
+ * packages/editor-server/src/routes/function-graph.routes.ts).
24
+ */
25
+ export function registerCompileFunctionGraphRoutes(router, config) {
26
+ // POST /api/compile (preview) -> CompileProject. Mirrors compile.routes.ts's "/" handler:
27
+ // read+compile the whole project from disk, no request payload (the route operates on
28
+ // whatever's currently on disk, not on anything sent by the caller).
29
+ router.rpc(EditorService.method.compileProject, async () => {
30
+ const { result } = await compileProjectFromDisk(config.projectDir);
31
+ if (!result.valid) {
32
+ return { valid: false, results: [], errors: result.errors.map(toValidationErrorInit) };
33
+ }
34
+ return {
35
+ valid: true,
36
+ results: result.files.map((file) => ({ relativePath: file.relativePath, code: file.code })),
37
+ errors: [],
38
+ };
39
+ });
40
+ // POST /api/compile/write -> WriteCompiledProject. Mirrors compile.routes.ts's "/write"
41
+ // handler: recompute the same compile (no caching/reuse of a prior CompileProject call,
42
+ // exactly like the REST route recomputes on every write), then persist every file and
43
+ // ensure a CommonJS package.json exists.
44
+ router.rpc(EditorService.method.writeCompiledProject, async () => {
45
+ const { sourceFiles, result } = await compileProjectFromDisk(config.projectDir);
46
+ if (!result.valid) {
47
+ return { valid: false, written: false, files: [], errors: result.errors.map(toValidationErrorInit) };
48
+ }
49
+ for (const file of result.files) {
50
+ await writeGeneratedFile(path.join(config.projectDir, file.relativePath), file.code);
51
+ }
52
+ const { dependencies } = collectProjectDependencies(sourceFiles);
53
+ await ensureCommonJsPackageJson(config.projectDir, path.basename(config.projectDir) || "flowserver-app", {
54
+ dependencies,
55
+ });
56
+ return {
57
+ valid: true,
58
+ written: true,
59
+ files: result.files.map((file) => ({ relativePath: file.relativePath, outputPath: file.relativePath })),
60
+ errors: [],
61
+ };
62
+ });
63
+ // POST /api/function-graph/preview -> PreviewFunctionGraph. Mirrors
64
+ // function-graph.routes.ts: pure in-memory compile of a Function node's blueprint
65
+ // sub-graph, always resolves successfully (never throws a Connect error) — an
66
+ // incomplete/invalid graph is an expected, frequent state while a user is still wiring
67
+ // nodes together, surfaced via the response's `error` oneof branch instead.
68
+ //
69
+ // `PreviewFunctionGraphRequest.flatbuffer_flow` carries only a graph (`{ nodes, edges }`),
70
+ // not a full top-level `Flow` — it has no `meta`/`version` of its own. The caller is
71
+ // expected to have filled those in with placeholder values before calling `encodeFlow`
72
+ // (a full `Flow` is required to encode at all); `decodeFlow` here still returns a
73
+ // full `Flow` shape because it always parses the strict FlatBuffers envelope, but only
74
+ // `.nodes`/`.edges` carry real data for this RPC — `.meta`/`.version` are discarded.
75
+ router.rpc(EditorService.method.previewFunctionGraph, async (req) => {
76
+ try {
77
+ let nodes = [];
78
+ let edges = [];
79
+ if (req.flatbufferFlow && req.flatbufferFlow.length > 0) {
80
+ const flow = decodeFlow(req.flatbufferFlow);
81
+ nodes = flow.nodes;
82
+ edges = flow.edges;
83
+ }
84
+ const { code: body } = emitFunctionGraphBody({ nodes, edges });
85
+ return { result: { case: "body", value: body } };
86
+ }
87
+ catch (err) {
88
+ if (err instanceof FunctionGraphError) {
89
+ return {
90
+ result: {
91
+ case: "error",
92
+ value: { message: err.message, blueprintNodeId: err.nodeId ?? "" },
93
+ },
94
+ };
95
+ }
96
+ return {
97
+ result: {
98
+ case: "error",
99
+ value: { message: err instanceof Error ? err.message : String(err), blueprintNodeId: "" },
100
+ },
101
+ };
102
+ }
103
+ });
104
+ return router;
105
+ }
106
+ //# sourceMappingURL=compile-function-graph.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-function-graph.service.js","sourceRoot":"","sources":["../../src/connect/compile-function-graph.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { type ConnectRouter } from "@connectrpc/connect";
2
+ import type { AppConfig } from "../config.js";
3
+ /** Registers the file-tree CRUD RPCs on `router`. Uses `router.rpc()` per-method (not
4
+ * `router.service()`) so this registration only claims the seven methods it implements,
5
+ * leaving the rest of `EditorService` free for other registration modules to claim their
6
+ * own subset of methods on the same router without clobbering each other. */
7
+ export declare function registerFilesRoutes(router: ConnectRouter, config: AppConfig): ConnectRouter;
@@ -0,0 +1,175 @@
1
+ import { access, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ConnectError, Code } from "@connectrpc/connect";
4
+ import { decodeFlow, encodeFlow } from "@visual-node/core";
5
+ import { EditorService, FileTreeNode_Kind } from "@visual-node/proto-gen";
6
+ import { isPlausibleFlow } from "../flow-shape.js";
7
+ import { listTree } from "../file-tree.js";
8
+ import { resolveSafePath } from "../path-safety.js";
9
+ function toProtoTree(nodes) {
10
+ return nodes.map((n) => n.type === "folder"
11
+ ? {
12
+ kind: FileTreeNode_Kind.FOLDER,
13
+ name: n.name,
14
+ relativePath: n.relativePath,
15
+ children: toProtoTree(n.children),
16
+ }
17
+ : { kind: FileTreeNode_Kind.FILE, name: n.name, relativePath: n.relativePath, children: [] });
18
+ }
19
+ async function pathExists(target) {
20
+ try {
21
+ await access(target);
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ /** Mirrors files.routes.ts's (unexported) `emptyBlueprint()`. */
29
+ function emptyBlueprint(basename) {
30
+ return {
31
+ version: "1",
32
+ meta: { name: basename, target: "express" },
33
+ nodes: [],
34
+ edges: [],
35
+ variables: [],
36
+ };
37
+ }
38
+ function invalidPath() {
39
+ return new ConnectError("Invalid or missing path", Code.InvalidArgument);
40
+ }
41
+ /** Registers the file-tree CRUD RPCs on `router`. Uses `router.rpc()` per-method (not
42
+ * `router.service()`) so this registration only claims the seven methods it implements,
43
+ * leaving the rest of `EditorService` free for other registration modules to claim their
44
+ * own subset of methods on the same router without clobbering each other. */
45
+ export function registerFilesRoutes(router, config) {
46
+ return router
47
+ .rpc(EditorService.method.listFiles, async () => {
48
+ const tree = await listTree(config.projectDir);
49
+ return { tree: toProtoTree(tree) };
50
+ })
51
+ .rpc(EditorService.method.createFolder, async (req) => {
52
+ const requestedPath = req.path;
53
+ const target = resolveSafePath(config.projectDir, requestedPath);
54
+ if (!target)
55
+ throw invalidPath();
56
+ if (await pathExists(target)) {
57
+ throw new ConnectError(`A file or folder already exists at "${requestedPath}"`, Code.AlreadyExists);
58
+ }
59
+ await mkdir(target, { recursive: true });
60
+ return { ok: true, path: requestedPath };
61
+ })
62
+ .rpc(EditorService.method.createBlueprint, async (req) => {
63
+ const requestedPath = req.path;
64
+ if (typeof requestedPath !== "string" || !requestedPath.endsWith(".blueprint")) {
65
+ throw new ConnectError('`path` must end in ".blueprint"', Code.InvalidArgument);
66
+ }
67
+ const target = resolveSafePath(config.projectDir, requestedPath);
68
+ if (!target)
69
+ throw invalidPath();
70
+ if (await pathExists(target)) {
71
+ throw new ConnectError(`A file already exists at "${requestedPath}"`, Code.AlreadyExists);
72
+ }
73
+ const basename = path.basename(requestedPath, ".blueprint");
74
+ const flow = emptyBlueprint(basename);
75
+ const flatbufferFlow = encodeFlow(flow);
76
+ await mkdir(path.dirname(target), { recursive: true });
77
+ await writeFile(target, flatbufferFlow);
78
+ return { ok: true, path: requestedPath, flatbufferFlow };
79
+ })
80
+ .rpc(EditorService.method.getBlueprint, async (req) => {
81
+ const requestedPath = req.path;
82
+ if (typeof requestedPath !== "string" || !requestedPath.endsWith(".blueprint")) {
83
+ throw new ConnectError('`path` must end in ".blueprint"', Code.InvalidArgument);
84
+ }
85
+ const target = resolveSafePath(config.projectDir, requestedPath);
86
+ if (!target)
87
+ throw invalidPath();
88
+ let raw;
89
+ try {
90
+ raw = await readFile(target);
91
+ }
92
+ catch (err) {
93
+ if (err.code === "ENOENT") {
94
+ throw new ConnectError(`No file at "${requestedPath}"`, Code.NotFound);
95
+ }
96
+ throw err;
97
+ }
98
+ const flatbufferFlow = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
99
+ let flow;
100
+ try {
101
+ flow = decodeFlow(flatbufferFlow);
102
+ }
103
+ catch (err) {
104
+ throw new ConnectError(`"${requestedPath}" could not be decoded: ${err.message}`, Code.Internal);
105
+ }
106
+ if (!isPlausibleFlow(flow)) {
107
+ throw new ConnectError(`"${requestedPath}" is not a valid flow`, Code.Internal);
108
+ }
109
+ return { flatbufferFlow };
110
+ })
111
+ .rpc(EditorService.method.saveBlueprint, async (req) => {
112
+ const requestedPath = req.path;
113
+ if (typeof requestedPath !== "string" || !requestedPath.endsWith(".blueprint")) {
114
+ throw new ConnectError('`path` must end in ".blueprint"', Code.InvalidArgument);
115
+ }
116
+ const target = resolveSafePath(config.projectDir, requestedPath);
117
+ if (!target)
118
+ throw invalidPath();
119
+ let flow;
120
+ try {
121
+ flow = decodeFlow(req.flatbufferFlow);
122
+ }
123
+ catch (err) {
124
+ throw new ConnectError(`\`flatbuffer_flow\` is not a valid encoded flow: ${err.message}`, Code.InvalidArgument);
125
+ }
126
+ if (!isPlausibleFlow(flow)) {
127
+ throw new ConnectError("`flatbuffer_flow` must decode to a flow with `nodes`, `edges`, and `meta`", Code.InvalidArgument);
128
+ }
129
+ await mkdir(path.dirname(target), { recursive: true });
130
+ // Write the exact bytes the client sent (already validated above), not a re-encode of
131
+ // the decoded Flow — this preserves byte-for-byte round-tripping and avoids a pointless
132
+ // decode+re-encode cycle now that wire format and disk format are the same thing.
133
+ await writeFile(target, req.flatbufferFlow);
134
+ return { ok: true };
135
+ })
136
+ .rpc(EditorService.method.renamePath, async (req) => {
137
+ const from = req.from;
138
+ const to = req.to;
139
+ const fromTarget = resolveSafePath(config.projectDir, from);
140
+ const toTarget = resolveSafePath(config.projectDir, to);
141
+ if (!fromTarget || !toTarget)
142
+ throw invalidPath();
143
+ if (!(await pathExists(fromTarget))) {
144
+ throw new ConnectError(`No file or folder at "${from}"`, Code.NotFound);
145
+ }
146
+ if (await pathExists(toTarget)) {
147
+ throw new ConnectError(`A file or folder already exists at "${to}"`, Code.AlreadyExists);
148
+ }
149
+ await mkdir(path.dirname(toTarget), { recursive: true });
150
+ await rename(fromTarget, toTarget);
151
+ return { ok: true };
152
+ })
153
+ .rpc(EditorService.method.deletePath, async (req) => {
154
+ const requestedPath = req.path;
155
+ const target = resolveSafePath(config.projectDir, requestedPath);
156
+ if (!target)
157
+ throw invalidPath();
158
+ let exists = true;
159
+ try {
160
+ await stat(target);
161
+ }
162
+ catch (err) {
163
+ if (err.code === "ENOENT")
164
+ exists = false;
165
+ else
166
+ throw err;
167
+ }
168
+ if (!exists) {
169
+ throw new ConnectError(`No file or folder at "${requestedPath}"`, Code.NotFound);
170
+ }
171
+ await rm(target, { recursive: true, force: false });
172
+ return { ok: true };
173
+ });
174
+ }
175
+ //# sourceMappingURL=files.service.js.map