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.
- package/README.md +42 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.js +44 -0
- package/dist/app.js.map +1 -0
- package/dist/codegen-helpers.d.ts +55 -0
- package/dist/codegen-helpers.js +142 -0
- package/dist/codegen-helpers.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -0
- package/dist/connect/compile-function-graph.service.d.ts +14 -0
- package/dist/connect/compile-function-graph.service.js +106 -0
- package/dist/connect/compile-function-graph.service.js.map +1 -0
- package/dist/connect/files.service.d.ts +7 -0
- package/dist/connect/files.service.js +175 -0
- package/dist/connect/files.service.js.map +1 -0
- package/dist/connect/node-registry-flow.service.d.ts +14 -0
- package/dist/connect/node-registry-flow.service.js +142 -0
- package/dist/connect/node-registry-flow.service.js.map +1 -0
- package/dist/connect/plugins.service.d.ts +24 -0
- package/dist/connect/plugins.service.js +73 -0
- package/dist/connect/plugins.service.js.map +1 -0
- package/dist/connect/run.service.d.ts +3 -0
- package/dist/connect/run.service.js +159 -0
- package/dist/connect/run.service.js.map +1 -0
- package/dist/connect/validate-generate.service.d.ts +19 -0
- package/dist/connect/validate-generate.service.js +102 -0
- package/dist/connect/validate-generate.service.js.map +1 -0
- package/dist/file-tree.d.ts +23 -0
- package/dist/file-tree.js +63 -0
- package/dist/file-tree.js.map +1 -0
- package/dist/flow-shape.d.ts +8 -0
- package/dist/flow-shape.js +13 -0
- package/dist/flow-shape.js.map +1 -0
- package/dist/path-safety.d.ts +12 -0
- package/dist/path-safety.js +26 -0
- package/dist/path-safety.js.map +1 -0
- package/dist/plugin-loading.d.ts +18 -0
- package/dist/plugin-loading.js +47 -0
- package/dist/plugin-loading.js.map +1 -0
- package/dist/plugin-readme.d.ts +9 -0
- package/dist/plugin-readme.js +235 -0
- package/dist/plugin-readme.js.map +1 -0
- package/dist/public/assets/index-6_2vDtnd.css +1 -0
- package/dist/public/assets/index-BpXY8lVq.js +101 -0
- package/dist/public/index.html +13 -0
- package/dist/runner.d.ts +23 -0
- package/dist/runner.js +65 -0
- package/dist/runner.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +34 -0
- package/dist/server.js.map +1 -0
- package/dist/static.d.ts +12 -0
- package/dist/static.js +28 -0
- package/dist/static.js.map +1 -0
- 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
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
|
package/dist/app.js.map
ADDED
|
@@ -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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"}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|