@xdelivered/emberflow 0.4.0 → 0.5.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/dist/server/index.js +23 -2
- package/dist/server/nodesPayload.d.ts +14 -1
- package/dist/server/nodesPayload.js +15 -2
- package/dist/server/projectMode.js +5 -2
- package/dist/server/sourceNav.d.ts +77 -0
- package/dist/server/sourceNav.js +503 -0
- package/dist/src/engine/registry.d.ts +27 -2
- package/dist/src/engine/registry.js +84 -2
- package/dist/src/nodes/index.d.ts +8 -2
- package/dist/src/nodes/index.js +7 -3
- package/dist/src/nodes/login.d.ts +3 -1
- package/dist/src/nodes/login.js +2 -2
- package/package.json +2 -2
- package/server/index.ts +24 -2
- package/server/nodesPayload.test.ts +38 -0
- package/server/nodesPayload.ts +28 -3
- package/server/projectMode.ts +5 -2
- package/server/sourceNav.test.ts +382 -0
- package/server/sourceNav.ts +644 -0
- package/server/sourceNavRoute.test.ts +163 -0
- package/src/components/Inspector.tsx +9 -26
- package/src/components/SourceNavigator.test.tsx +226 -0
- package/src/components/SourceNavigator.tsx +520 -0
- package/src/engine/registry.sourceRef.test.ts +112 -0
- package/src/engine/registry.ts +103 -2
- package/src/nodes/index.ts +10 -3
- package/src/nodes/login.ts +5 -2
- package/src/store/builderStore.ts +2 -2
- package/src/store/nodeMeta.ts +10 -2
- package/src/store/sourceNavClient.ts +48 -0
- package/studio-dist/assets/{index-CGwEx82J.js → index-BbzppDpt.js} +25 -25
- package/studio-dist/assets/{index-CAIDjNhv.css → index-CLf6blcA.css} +1 -1
- package/studio-dist/index.html +2 -2
- package/templates/skills/emberflow-basics/SKILL.md +29 -1
- package/templates/skills/emberflow-model-process/SKILL.md +11 -1
- package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
- package/templates/skills/emberflow-review-workflow/SKILL.md +28 -1
package/src/engine/registry.ts
CHANGED
|
@@ -5,15 +5,98 @@ export interface RegisteredNode {
|
|
|
5
5
|
implementation: NodeImplementation;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/** Where a node was registered from: absolute file path + 1-based line of the register() call. */
|
|
9
|
+
export interface SourceRef {
|
|
10
|
+
file: string;
|
|
11
|
+
line?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RegisterOptions {
|
|
15
|
+
/** Explicit provenance escape hatch — wins over automatic capture. */
|
|
16
|
+
sourceRef?: SourceRef;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* V8 stack-frame shape (structural — this module must stay browser-safe, so
|
|
21
|
+
* no @types/node CallSite import).
|
|
22
|
+
*/
|
|
23
|
+
interface V8CallSite {
|
|
24
|
+
getFileName?: () => string | null | undefined;
|
|
25
|
+
getLineNumber?: () => number | null | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read the CALLER's file:line via a one-frame V8 stack capture.
|
|
30
|
+
* `Error.prepareStackTrace` is swapped for the duration of a single `new
|
|
31
|
+
* Error()` and restored in `finally`, so source-map hooks installed by
|
|
32
|
+
* tsx/vitest are back in place immediately. Frames arrive either as
|
|
33
|
+
* `file:///abs/path.ts` URLs (ESM) or plain absolute paths (CJS/transpilers);
|
|
34
|
+
* both normalize to a plain path. Returns undefined on any non-V8 runtime or
|
|
35
|
+
* anonymous/eval frames — capture is best-effort by design.
|
|
36
|
+
*/
|
|
37
|
+
function captureCallerSourceRef(skip: (...args: never[]) => unknown): SourceRef | undefined {
|
|
38
|
+
if (typeof Error.captureStackTrace !== 'function') return undefined;
|
|
39
|
+
const original = Error.prepareStackTrace;
|
|
40
|
+
try {
|
|
41
|
+
Error.prepareStackTrace = (_err, frames) => frames;
|
|
42
|
+
const holder: { stack?: unknown } = {};
|
|
43
|
+
Error.captureStackTrace(holder as Error, skip);
|
|
44
|
+
const frames = holder.stack;
|
|
45
|
+
if (!Array.isArray(frames) || frames.length === 0) return undefined;
|
|
46
|
+
const frame = frames[0] as V8CallSite;
|
|
47
|
+
const raw = typeof frame.getFileName === 'function' ? frame.getFileName() : undefined;
|
|
48
|
+
if (typeof raw !== 'string' || raw.length === 0) return undefined;
|
|
49
|
+
const lineRaw = typeof frame.getLineNumber === 'function' ? frame.getLineNumber() : undefined;
|
|
50
|
+
const line = typeof lineRaw === 'number' && lineRaw > 0 ? lineRaw : undefined;
|
|
51
|
+
let file = raw;
|
|
52
|
+
if (file.startsWith('file://')) {
|
|
53
|
+
// No node:url here (browser-safe module): URL parsing covers the
|
|
54
|
+
// file:///abs/path form these frames take on POSIX platforms.
|
|
55
|
+
try {
|
|
56
|
+
file = decodeURIComponent(new URL(file).pathname);
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Loaders (vite-node hot imports) may suffix ?t=... cache-busters.
|
|
62
|
+
const q = file.indexOf('?');
|
|
63
|
+
if (q !== -1) file = file.slice(0, q);
|
|
64
|
+
return line !== undefined ? { file, line } : { file };
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
} finally {
|
|
68
|
+
Error.prepareStackTrace = original;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
8
72
|
export class NodeRegistry {
|
|
9
73
|
nodes = new Map<string, RegisteredNode>();
|
|
10
74
|
sources = new Map<string, string>();
|
|
75
|
+
/** Registration provenance per node type. Populated only when capture is on or opts.sourceRef given. */
|
|
76
|
+
sourceRefs = new Map<string, SourceRef>();
|
|
77
|
+
/** Types the RUNNER flagged builtin (registered from the package, not the project). */
|
|
78
|
+
builtinTypes = new Set<string>();
|
|
79
|
+
/** Automatic caller capture is opt-in (server registries only) so browser registries never pay/leak. */
|
|
80
|
+
private captureSourceRefs: boolean;
|
|
81
|
+
|
|
82
|
+
constructor(opts?: { captureSourceRefs?: boolean }) {
|
|
83
|
+
this.captureSourceRefs = opts?.captureSourceRefs ?? false;
|
|
84
|
+
}
|
|
11
85
|
|
|
12
|
-
register(definition: NodeDefinition, implementation: NodeImplementation): void {
|
|
86
|
+
register(definition: NodeDefinition, implementation: NodeImplementation, opts?: RegisterOptions): void {
|
|
13
87
|
if (this.nodes.has(definition.type)) {
|
|
14
88
|
throw new Error(`Node type already registered: ${definition.type}`);
|
|
15
89
|
}
|
|
16
90
|
this.nodes.set(definition.type, { definition, implementation });
|
|
91
|
+
const ref =
|
|
92
|
+
opts?.sourceRef ??
|
|
93
|
+
(this.captureSourceRefs ? captureCallerSourceRef(this.register) : undefined);
|
|
94
|
+
if (ref) this.sourceRefs.set(definition.type, ref);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Where `type` was registered from, when known. */
|
|
98
|
+
getSourceRef(type: string): SourceRef | undefined {
|
|
99
|
+
return this.sourceRefs.get(type);
|
|
17
100
|
}
|
|
18
101
|
|
|
19
102
|
get(type: string): RegisteredNode {
|
|
@@ -36,6 +119,13 @@ export class NodeRegistry {
|
|
|
36
119
|
adopt(other: NodeRegistry): void {
|
|
37
120
|
this.nodes = other.nodes;
|
|
38
121
|
this.sources = other.sources;
|
|
122
|
+
this.sourceRefs = other.sourceRefs;
|
|
123
|
+
this.builtinTypes = other.builtinTypes;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Whether the runner flagged `type` as a package built-in (no servable source file). */
|
|
127
|
+
isBuiltin(type: string): boolean {
|
|
128
|
+
return this.builtinTypes.has(type);
|
|
39
129
|
}
|
|
40
130
|
|
|
41
131
|
list(): NodeDefinition[] {
|
|
@@ -47,7 +137,16 @@ export class NodeRegistry {
|
|
|
47
137
|
* implementation is a stub that fails loudly if browser-executed — such
|
|
48
138
|
* nodes run on the server. Never overwrites a real registration.
|
|
49
139
|
*/
|
|
50
|
-
registerDefinition(
|
|
140
|
+
registerDefinition(
|
|
141
|
+
definition: NodeDefinition,
|
|
142
|
+
source?: string,
|
|
143
|
+
opts?: { sourceRef?: SourceRef; builtin?: boolean },
|
|
144
|
+
): void {
|
|
145
|
+
// Provenance is recorded even for already-registered types: the studio's
|
|
146
|
+
// bundled built-ins are real registrations, and the runner's builtin flag
|
|
147
|
+
// (or a project sourceRef shadowing one) must still land on them.
|
|
148
|
+
if (opts?.sourceRef) this.sourceRefs.set(definition.type, opts.sourceRef);
|
|
149
|
+
if (opts?.builtin) this.builtinTypes.add(definition.type);
|
|
51
150
|
if (this.nodes.has(definition.type)) return;
|
|
52
151
|
this.nodes.set(definition.type, {
|
|
53
152
|
definition,
|
|
@@ -75,6 +174,8 @@ export class NodeRegistry {
|
|
|
75
174
|
const next = new NodeRegistry();
|
|
76
175
|
next.nodes = this.nodes;
|
|
77
176
|
next.sources = this.sources;
|
|
177
|
+
next.sourceRefs = this.sourceRefs;
|
|
178
|
+
next.builtinTypes = this.builtinTypes;
|
|
78
179
|
return next;
|
|
79
180
|
}
|
|
80
181
|
}
|
package/src/nodes/index.ts
CHANGED
|
@@ -8,9 +8,16 @@ import { registerPradarNodesModule } from './pradar';
|
|
|
8
8
|
import { registerResponseNodes } from './response';
|
|
9
9
|
import { registerRequireAuthNode } from './requireAuth';
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
/**
|
|
12
|
+
* All built-in nodes: login examples, Open-Meteo weather, anomaly-detection API, EV charging demo, HTTP Response, requireAuth.
|
|
13
|
+
* `opts.captureSourceRefs` is threaded from SERVER callers only (buildRegistries)
|
|
14
|
+
* — this function is shared with the browser bundle, where capture stays off.
|
|
15
|
+
*/
|
|
16
|
+
export function createDefaultRegistry(
|
|
17
|
+
delayMs?: number,
|
|
18
|
+
opts?: { captureSourceRefs?: boolean },
|
|
19
|
+
): NodeRegistry {
|
|
20
|
+
const registry = createLoginRegistry(delayMs, opts);
|
|
14
21
|
registerWeatherNodes(registry);
|
|
15
22
|
registerAnomalyNodes(registry);
|
|
16
23
|
registerFlowControlNodes(registry);
|
package/src/nodes/login.ts
CHANGED
|
@@ -12,8 +12,11 @@ function usernamePart(userId: string): string {
|
|
|
12
12
|
* `delayMs` is injected into every implementation via `await sleep(delayMs)`
|
|
13
13
|
* so tests can run with no delay by passing `0`.
|
|
14
14
|
*/
|
|
15
|
-
export function createLoginRegistry(
|
|
16
|
-
|
|
15
|
+
export function createLoginRegistry(
|
|
16
|
+
delayMs = 300,
|
|
17
|
+
opts?: { captureSourceRefs?: boolean },
|
|
18
|
+
): NodeRegistry {
|
|
19
|
+
const registry = new NodeRegistry(opts);
|
|
17
20
|
|
|
18
21
|
registry.register(
|
|
19
22
|
{
|
|
@@ -994,8 +994,8 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
994
994
|
if (meta.length === 0) return;
|
|
995
995
|
const registry = get().registry;
|
|
996
996
|
for (const m of meta) {
|
|
997
|
-
const { source, ...definition } = m;
|
|
998
|
-
registry.registerDefinition(definition, source);
|
|
997
|
+
const { source, sourceRef, builtin, ...definition } = m;
|
|
998
|
+
registry.registerDefinition(definition, source, { sourceRef, builtin });
|
|
999
999
|
}
|
|
1000
1000
|
// Force palette/inspector consumers to re-read the registry: a new
|
|
1001
1001
|
// top-level reference (Zustand uses Object.is) sharing the same node data.
|
package/src/store/nodeMeta.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { NodeDefinition } from '../engine';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* A runner node definition plus its implementation source (for display) and
|
|
5
|
+
* provenance: `sourceRef` (repo-relative registering file + line) when the
|
|
6
|
+
* node is project-owned, `builtin: true` when registered from the package.
|
|
7
|
+
*/
|
|
8
|
+
export type NodeMeta = NodeDefinition & {
|
|
9
|
+
source?: string;
|
|
10
|
+
sourceRef?: { file: string; line?: number };
|
|
11
|
+
builtin?: boolean;
|
|
12
|
+
};
|
|
5
13
|
|
|
6
14
|
/**
|
|
7
15
|
* Fetch the runner's node metadata (GET /api/nodes, same-origin via the
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the runner's source navigator (GET /source-file), reached through
|
|
3
|
+
* the Vite proxy at /api → 127.0.0.1:8092, same-origin as setupClient.ts.
|
|
4
|
+
* Powers the Inspector's source-reference navigation (SourceNavigator).
|
|
5
|
+
*
|
|
6
|
+
* Types are imported TYPE-ONLY from server/sourceNav.ts — erased at build, so
|
|
7
|
+
* the browser bundle never pulls server code, and the shapes can't drift.
|
|
8
|
+
*/
|
|
9
|
+
import type { SourceFilePayload } from '../../server/sourceNav';
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
SourceFilePayload,
|
|
13
|
+
SourceDeclaration,
|
|
14
|
+
SourceImport,
|
|
15
|
+
SourceReexport,
|
|
16
|
+
Resolution,
|
|
17
|
+
} from '../../server/sourceNav';
|
|
18
|
+
|
|
19
|
+
/** A fetch outcome the navigator can render either way — payload or error text. */
|
|
20
|
+
export type SourceFileFetchResult =
|
|
21
|
+
| { ok: true; payload: SourceFilePayload }
|
|
22
|
+
| { ok: false; error: string };
|
|
23
|
+
|
|
24
|
+
const BASE = '/api';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* GET /source-file?path=<repo-relative>. Never throws — errors come back as
|
|
28
|
+
* `{ok:false}` with status-aware text: 400 → denied, 404 → missing, network
|
|
29
|
+
* failure → runner unreachable.
|
|
30
|
+
*/
|
|
31
|
+
export async function fetchSourceFile(path: string): Promise<SourceFileFetchResult> {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(`${BASE}/source-file?${new URLSearchParams({ path })}`);
|
|
34
|
+
if (response.ok) {
|
|
35
|
+
return { ok: true, payload: (await response.json()) as SourceFilePayload };
|
|
36
|
+
}
|
|
37
|
+
const body = (await response.json().catch(() => ({}))) as { error?: string };
|
|
38
|
+
if (response.status === 400) {
|
|
39
|
+
return { ok: false, error: `Access denied: ${body.error ?? 'path not servable'}` };
|
|
40
|
+
}
|
|
41
|
+
if (response.status === 404) {
|
|
42
|
+
return { ok: false, error: `File not found: ${path}` };
|
|
43
|
+
}
|
|
44
|
+
return { ok: false, error: body.error ?? `Source fetch failed (HTTP ${response.status})` };
|
|
45
|
+
} catch {
|
|
46
|
+
return { ok: false, error: 'Runner unreachable — start the runner to view source' };
|
|
47
|
+
}
|
|
48
|
+
}
|