@tuongaz/seeflow 0.1.42 → 0.1.47
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/dist/web/assets/{index-BPUoNIBm.js → index-BYeYJkCQ.js} +17 -17
- package/dist/web/assets/{index-BlkUOp7f.css → index-DSfixlbD.css} +1 -1
- package/dist/web/assets/{index.es-mje3R_63.js → index.es-CqkMwhBu.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-DX3imOs2.js → jspdf.es.min-DLHTB6Rk.js} +3 -3
- package/dist/web/index.html +2 -2
- package/examples/ecommerce-platform/.seeflow/flow.json +8 -8
- package/examples/order-pipeline/.seeflow/flow.json +4 -4
- package/package.json +2 -1
- package/src/cli-e2e.ts +10 -6
- package/src/file-ref.ts +27 -16
- package/src/node-files.ts +5 -2
- package/src/operations.ts +21 -7
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"kind": "request"
|
|
13
13
|
},
|
|
14
14
|
"description": "Creates order, kicks off the pipeline.",
|
|
15
|
-
"detail": "file://
|
|
15
|
+
"detail": "file://detail.md",
|
|
16
16
|
"playAction": {
|
|
17
17
|
"kind": "script",
|
|
18
18
|
"interpreter": "bun",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"kind": "event"
|
|
34
34
|
},
|
|
35
35
|
"description": "Reserves stock.",
|
|
36
|
-
"detail": "file://
|
|
36
|
+
"detail": "file://detail.md",
|
|
37
37
|
"icon": "a-arrow-down-icon"
|
|
38
38
|
}
|
|
39
39
|
},
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"kind": "event"
|
|
48
48
|
},
|
|
49
49
|
"description": "Charges card.",
|
|
50
|
-
"detail": "file://
|
|
50
|
+
"detail": "file://detail.md"
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
{
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"kind": "event"
|
|
61
61
|
},
|
|
62
62
|
"description": "Enqueues shipment.",
|
|
63
|
-
"detail": "file://
|
|
63
|
+
"detail": "file://detail.md"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuongaz/seeflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
4
4
|
"description": "Local studio that hosts file-defined demos as React Flow canvases wired to a running app via REST + SSE + Zod schema.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"seeflow",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"zod-to-json-schema": "^3.25.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
+
"@playwright/test": "^1.60.0",
|
|
52
53
|
"@tailwindcss/browser": "^4.3.0",
|
|
53
54
|
"@types/bun": "^1.1.14",
|
|
54
55
|
"typescript": "^5.6.3"
|
package/src/cli-e2e.ts
CHANGED
|
@@ -63,7 +63,7 @@ interface NodeShape {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
interface
|
|
66
|
+
interface FlowBody {
|
|
67
67
|
nodes?: NodeShape[];
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -71,7 +71,11 @@ interface FlowGetResponse {
|
|
|
71
71
|
id?: string;
|
|
72
72
|
valid?: boolean;
|
|
73
73
|
error?: string | null;
|
|
74
|
-
|
|
74
|
+
// GET /api/flows/:id returns the resolved flow under the `flow` key (see
|
|
75
|
+
// FlowGetResponse in operations.ts). Older versions of this file used `demo`,
|
|
76
|
+
// which left the validator effectively broken (every call returned `ok:false`
|
|
77
|
+
// with "demo not valid"). Renamed to match the wire format.
|
|
78
|
+
flow?: FlowBody | null;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
interface SseEvent {
|
|
@@ -200,21 +204,21 @@ export async function validateEndToEnd(options: ValidateOptions): Promise<Valida
|
|
|
200
204
|
};
|
|
201
205
|
}
|
|
202
206
|
const demoData = (await demoRes.json()) as FlowGetResponse;
|
|
203
|
-
if (!demoData.valid || !demoData.
|
|
207
|
+
if (!demoData.valid || !demoData.flow) {
|
|
204
208
|
return {
|
|
205
209
|
ok: false,
|
|
206
210
|
plays,
|
|
207
211
|
statuses,
|
|
208
212
|
skipped: [
|
|
209
213
|
{
|
|
210
|
-
nodeId: '<
|
|
211
|
-
reason: `
|
|
214
|
+
nodeId: '<flow>',
|
|
215
|
+
reason: `flow not valid: ${demoData.error ?? '<no error>'}`,
|
|
212
216
|
},
|
|
213
217
|
],
|
|
214
218
|
};
|
|
215
219
|
}
|
|
216
220
|
|
|
217
|
-
const nodes = demoData.
|
|
221
|
+
const nodes = demoData.flow.nodes ?? [];
|
|
218
222
|
|
|
219
223
|
const skipSet = new Set(options.skipNodes ?? []);
|
|
220
224
|
const playTargets: string[] = [];
|
package/src/file-ref.ts
CHANGED
|
@@ -14,13 +14,18 @@ const isCleanRelativePath = (p: string): boolean => {
|
|
|
14
14
|
const invalidMarker = (rawPath: string) => `[seeflow: invalid file:// path '${rawPath}']`;
|
|
15
15
|
const missingMarker = (rawPath: string) => `[seeflow: missing file '${rawPath}']`;
|
|
16
16
|
|
|
17
|
+
const looksLikeFlowNode = (obj: Record<string, unknown>): obj is { id: string; data: object } =>
|
|
18
|
+
typeof obj.id === 'string' && obj.data !== null && typeof obj.data === 'object';
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* Resolve every `file://<relative-path>` string in `raw` by reading the file
|
|
19
|
-
* under `<seeflowRoot
|
|
20
|
-
*
|
|
22
|
+
* under `<seeflowRoot>/nodes/<nodeId>/` (node-relative) and substituting its
|
|
23
|
+
* UTF-8 content. Strings outside any enclosing flow node are treated as
|
|
24
|
+
* invalid — every supported file:// ref currently lives inside `node.data`.
|
|
21
25
|
*
|
|
22
|
-
* Returns the mutated tree plus the sorted, de-duplicated list of relative
|
|
23
|
-
* paths that resolved cleanly (the watcher tracks these for live reload
|
|
26
|
+
* Returns the mutated tree plus the sorted, de-duplicated list of seeflow-root-relative
|
|
27
|
+
* paths that resolved cleanly (the watcher tracks these for live reload, so the
|
|
28
|
+
* external contract uses `nodes/<id>/<file>` even though the source string is short).
|
|
24
29
|
*/
|
|
25
30
|
export function resolveFileRefs(
|
|
26
31
|
raw: unknown,
|
|
@@ -34,46 +39,52 @@ export function resolveFileRefs(
|
|
|
34
39
|
seeflowRealRoot = seeflowRoot;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
const resolveString = (s: string): string => {
|
|
42
|
+
const resolveString = (s: string, nodeId: string | null): string => {
|
|
38
43
|
if (!s.startsWith(FILE_PREFIX)) return s;
|
|
39
44
|
const relPath = s.slice(FILE_PREFIX.length);
|
|
40
45
|
if (!isCleanRelativePath(relPath)) return invalidMarker(relPath);
|
|
46
|
+
if (nodeId === null) return invalidMarker(relPath);
|
|
41
47
|
|
|
42
|
-
const
|
|
43
|
-
|
|
48
|
+
const seeflowRelPath = `nodes/${nodeId}/${relPath}`;
|
|
49
|
+
const abs = join(seeflowRoot, seeflowRelPath);
|
|
50
|
+
if (!existsSync(abs)) return missingMarker(seeflowRelPath);
|
|
44
51
|
|
|
45
52
|
// Symlink-escape defense: resolve realpath and confirm it stays inside root.
|
|
46
53
|
let realAbs: string;
|
|
47
54
|
try {
|
|
48
55
|
realAbs = realpathSync(abs);
|
|
49
56
|
} catch {
|
|
50
|
-
return missingMarker(
|
|
57
|
+
return missingMarker(seeflowRelPath);
|
|
51
58
|
}
|
|
52
59
|
const rel = relative(seeflowRealRoot, realAbs);
|
|
53
60
|
if (rel.startsWith('..') || isAbsolute(rel)) return invalidMarker(relPath);
|
|
54
61
|
|
|
55
62
|
try {
|
|
56
63
|
const content = readFileSync(realAbs, 'utf8');
|
|
57
|
-
refs.add(
|
|
64
|
+
refs.add(seeflowRelPath);
|
|
58
65
|
return content;
|
|
59
66
|
} catch {
|
|
60
|
-
return missingMarker(
|
|
67
|
+
return missingMarker(seeflowRelPath);
|
|
61
68
|
}
|
|
62
69
|
};
|
|
63
70
|
|
|
64
|
-
const walk = (node: unknown): unknown => {
|
|
65
|
-
if (typeof node === 'string') return resolveString(node);
|
|
66
|
-
if (Array.isArray(node)) return node.map(walk);
|
|
71
|
+
const walk = (node: unknown, nodeId: string | null): unknown => {
|
|
72
|
+
if (typeof node === 'string') return resolveString(node, nodeId);
|
|
73
|
+
if (Array.isArray(node)) return node.map((v) => walk(v, nodeId));
|
|
67
74
|
if (node && typeof node === 'object') {
|
|
75
|
+
const obj = node as Record<string, unknown>;
|
|
76
|
+
// Entering a flow node carves out a new resolution context for its subtree:
|
|
77
|
+
// any file:// inside `data` now resolves relative to nodes/<id>/.
|
|
78
|
+
const childNodeId = looksLikeFlowNode(obj) ? obj.id : nodeId;
|
|
68
79
|
const out: Record<string, unknown> = {};
|
|
69
|
-
for (const [k, v] of Object.entries(
|
|
70
|
-
out[k] = walk(v);
|
|
80
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
81
|
+
out[k] = walk(v, childNodeId);
|
|
71
82
|
}
|
|
72
83
|
return out;
|
|
73
84
|
}
|
|
74
85
|
return node;
|
|
75
86
|
};
|
|
76
87
|
|
|
77
|
-
const resolved = walk(raw);
|
|
88
|
+
const resolved = walk(raw, null);
|
|
78
89
|
return { resolved, refs: [...refs].sort() };
|
|
79
90
|
}
|
package/src/node-files.ts
CHANGED
|
@@ -29,8 +29,11 @@ export type ExternalizedFieldName = (typeof EXTERNALIZED_NODE_FIELDS)[number]['f
|
|
|
29
29
|
export const nodeFileRelPath = (nodeId: string, fileName: string): string =>
|
|
30
30
|
`nodes/${nodeId}/${fileName}`;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// Node-relative ref: the resolver knows the enclosing node id from the flow.json
|
|
33
|
+
// shape (nodes[i].id), so the on-disk string only needs the filename. Kept as a
|
|
34
|
+
// 2-arg helper so call sites don't change shape and the spec stays explicit
|
|
35
|
+
// that the file lives under the given node.
|
|
36
|
+
export const nodeFileRef = (_nodeId: string, fileName: string): string => `file://${fileName}`;
|
|
34
37
|
|
|
35
38
|
export const nodeFileAbsPath = (repoPath: string, nodeId: string, fileName: string): string =>
|
|
36
39
|
join(repoPath, '.seeflow', nodeFileRelPath(nodeId, fileName));
|
package/src/operations.ts
CHANGED
|
@@ -658,13 +658,27 @@ export async function mutateMergedFlow<E extends { kind: string }>(
|
|
|
658
658
|
return { kind: 'writeFailed', message: err instanceof Error ? err.message : String(err) };
|
|
659
659
|
}
|
|
660
660
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
661
|
+
// Re-read through readMergedFlow so the snapshot we hand to notifyWritten
|
|
662
|
+
// carries file://-resolved content (detail.md, view.html, …). The in-memory
|
|
663
|
+
// `merged` tree above still holds raw `file://<name>` strings — broadcasting
|
|
664
|
+
// it would clobber the watcher's resolved seed and ship unresolved refs to
|
|
665
|
+
// every SSE subscriber until the next reparse.
|
|
666
|
+
const reread = readMergedFlow(flowPath);
|
|
667
|
+
const snap: FlowSnapshot = reread.valid
|
|
668
|
+
? {
|
|
669
|
+
flow: reread.flow,
|
|
670
|
+
valid: true,
|
|
671
|
+
error: null,
|
|
672
|
+
filePath: flowPath,
|
|
673
|
+
parsedAt: Date.now(),
|
|
674
|
+
}
|
|
675
|
+
: {
|
|
676
|
+
flow: finalParse.data as ResolvedFlow,
|
|
677
|
+
valid: true,
|
|
678
|
+
error: null,
|
|
679
|
+
filePath: flowPath,
|
|
680
|
+
parsedAt: Date.now(),
|
|
681
|
+
};
|
|
668
682
|
return { kind: 'ok', snap, flowContent, styleContent };
|
|
669
683
|
}
|
|
670
684
|
|