@flow-as-code/cli 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/LICENSE +202 -0
- package/README.md +446 -0
- package/dist/aws.d.ts +28 -0
- package/dist/aws.d.ts.map +1 -0
- package/dist/aws.js +104 -0
- package/dist/aws.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +132 -0
- package/dist/bin.js.map +1 -0
- package/dist/bridge/exportFiles.d.ts +16 -0
- package/dist/bridge/exportFiles.d.ts.map +1 -0
- package/dist/bridge/exportFiles.js +95 -0
- package/dist/bridge/exportFiles.js.map +1 -0
- package/dist/bridge/pair.d.ts +103 -0
- package/dist/bridge/pair.d.ts.map +1 -0
- package/dist/bridge/pair.js +219 -0
- package/dist/bridge/pair.js.map +1 -0
- package/dist/bridge/protocol.d.ts +187 -0
- package/dist/bridge/protocol.d.ts.map +1 -0
- package/dist/bridge/protocol.js +132 -0
- package/dist/bridge/protocol.js.map +1 -0
- package/dist/bridge/server.d.ts +95 -0
- package/dist/bridge/server.d.ts.map +1 -0
- package/dist/bridge/server.js +669 -0
- package/dist/bridge/server.js.map +1 -0
- package/dist/cdk-scaffold.d.ts +16 -0
- package/dist/cdk-scaffold.d.ts.map +1 -0
- package/dist/cdk-scaffold.js +28 -0
- package/dist/cdk-scaffold.js.map +1 -0
- package/dist/codegen.d.ts +5 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +27 -0
- package/dist/codegen.js.map +1 -0
- package/dist/diff.d.ts +33 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +225 -0
- package/dist/diff.js.map +1 -0
- package/dist/docs.d.ts +46 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +223 -0
- package/dist/docs.js.map +1 -0
- package/dist/emit.d.ts +8 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +55 -0
- package/dist/emit.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/export.d.ts +27 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +126 -0
- package/dist/export.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +6 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +31 -0
- package/dist/lint.js.map +1 -0
- package/dist/render.d.ts +6 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +49 -0
- package/dist/render.js.map +1 -0
- package/dist/run.d.ts +13 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +38 -0
- package/dist/run.js.map +1 -0
- package/dist/simulate.d.ts +35 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +194 -0
- package/dist/simulate.js.map +1 -0
- package/dist/studio.d.ts +23 -0
- package/dist/studio.d.ts.map +1 -0
- package/dist/studio.js +120 -0
- package/dist/studio.js.map +1 -0
- package/dist/synth-resolve-hook.d.ts +19 -0
- package/dist/synth-resolve-hook.d.ts.map +1 -0
- package/dist/synth-resolve-hook.js +127 -0
- package/dist/synth-resolve-hook.js.map +1 -0
- package/dist/synth-runner.d.ts +12 -0
- package/dist/synth-runner.d.ts.map +1 -0
- package/dist/synth-runner.js +145 -0
- package/dist/synth-runner.js.map +1 -0
- package/dist/synth.d.ts +55 -0
- package/dist/synth.d.ts.map +1 -0
- package/dist/synth.js +327 -0
- package/dist/synth.js.map +1 -0
- package/dist/watch.d.ts +66 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +293 -0
- package/dist/watch.js.map +1 -0
- package/package.json +85 -0
- package/schema/flowdoc-0.1.schema.json +669 -0
- package/schema/scenario-0.1.schema.json +318 -0
- package/src/aws.ts +149 -0
- package/src/bin.ts +179 -0
- package/src/bridge/exportFiles.ts +116 -0
- package/src/bridge/pair.ts +312 -0
- package/src/bridge/protocol.ts +289 -0
- package/src/bridge/server.ts +827 -0
- package/src/cdk-scaffold.ts +41 -0
- package/src/codegen.ts +36 -0
- package/src/diff.ts +279 -0
- package/src/docs.ts +249 -0
- package/src/emit.ts +69 -0
- package/src/errors.ts +27 -0
- package/src/export.ts +167 -0
- package/src/index.ts +27 -0
- package/src/lint.ts +42 -0
- package/src/render.ts +58 -0
- package/src/run.ts +42 -0
- package/src/simulate.ts +244 -0
- package/src/studio.ts +143 -0
- package/src/synth-resolve-hook.ts +152 -0
- package/src/synth-runner.ts +157 -0
- package/src/synth.ts +380 -0
- package/src/watch.ts +388 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 The flow-as-code Authors
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
// Reading and writing one <name>.flowdoc.json / <name>.flow.ts pair.
|
|
6
|
+
//
|
|
7
|
+
// This is the only place the studio bridge touches disk, and it is deliberately
|
|
8
|
+
// the mirror image of the A04 watch engine: the watcher owns the ts -> doc
|
|
9
|
+
// direction, this module owns doc -> ts.
|
|
10
|
+
//
|
|
11
|
+
// The rule that keeps the two directions from fighting each other is
|
|
12
|
+
// meta.sourceHash. A pair is "in sync" when the doc carries the sha256 of the
|
|
13
|
+
// exact builder source sitting next to it (docs/01-flowdoc-spec.md), which is
|
|
14
|
+
// what the watcher's dirty guard reads. So a canvas save must write BOTH
|
|
15
|
+
// halves and stamp the doc with the hash of the source it just generated;
|
|
16
|
+
// writing only the doc would leave a pair the watcher then reports as dirty
|
|
17
|
+
// in an unknowable direction.
|
|
18
|
+
//
|
|
19
|
+
// Generation goes through @flow-as-code/core's codegen with the existing file passed as
|
|
20
|
+
// options.previous, which is how `// @keep` comments a human added to the
|
|
21
|
+
// generated file survive a canvas edit (packages/core/src/codegen.ts).
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
23
|
+
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
24
|
+
import { join, resolve } from "node:path";
|
|
25
|
+
import { codegen } from "@flow-as-code/core";
|
|
26
|
+
import { flowDocProblems } from "../docs.js";
|
|
27
|
+
import { serializeWithMeta, sha256Hex, synthFile } from "../synth.js";
|
|
28
|
+
import { pickDoc } from "../watch.js";
|
|
29
|
+
import { DOC_SUFFIX, TS_SUFFIX, isBridgeDocName, } from "./protocol.js";
|
|
30
|
+
/** A failure with the HTTP status the bridge should answer with. */
|
|
31
|
+
export class BridgeError extends Error {
|
|
32
|
+
status;
|
|
33
|
+
constructor(status, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "BridgeError";
|
|
36
|
+
this.status = status;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Thrown by writePair when the builder file has moved since the document being
|
|
41
|
+
* written was generated from it. Both sides changed, so the write is refused
|
|
42
|
+
* and the caller turns this into the "which side wins" question.
|
|
43
|
+
*/
|
|
44
|
+
export class PairConflict extends Error {
|
|
45
|
+
name = "PairConflict";
|
|
46
|
+
docName;
|
|
47
|
+
reason;
|
|
48
|
+
constructor(docName, reason) {
|
|
49
|
+
super(reason);
|
|
50
|
+
this.docName = docName;
|
|
51
|
+
this.reason = reason;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Both file paths for a document name. The name is re-checked here rather than
|
|
56
|
+
* only at the route, because this function is what turns a client string into
|
|
57
|
+
* a filesystem path: a slug cannot contain a separator, a dot, or a NUL, so
|
|
58
|
+
* traversal is impossible by construction rather than by sanitizing.
|
|
59
|
+
*/
|
|
60
|
+
export function pairPaths(dir, name) {
|
|
61
|
+
if (!isBridgeDocName(name)) {
|
|
62
|
+
throw new BridgeError(400, `"${name}" is not a document name: names are lowercase words separated by single hyphens.`);
|
|
63
|
+
}
|
|
64
|
+
const base = resolve(dir);
|
|
65
|
+
return { docPath: join(base, name + DOC_SUFFIX), tsPath: join(base, name + TS_SUFFIX) };
|
|
66
|
+
}
|
|
67
|
+
/** Every FlowDoc name in the directory, sorted, non-recursive. */
|
|
68
|
+
export async function listDocNames(dir) {
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = await readdir(resolve(dir));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new BridgeError(500, `Cannot read ${resolve(dir)}.`);
|
|
75
|
+
}
|
|
76
|
+
return entries
|
|
77
|
+
.filter((f) => f.endsWith(DOC_SUFFIX))
|
|
78
|
+
.map((f) => f.slice(0, -DOC_SUFFIX.length))
|
|
79
|
+
.filter((name) => isBridgeDocName(name))
|
|
80
|
+
.sort();
|
|
81
|
+
}
|
|
82
|
+
/** Parses and schema-validates a FlowDoc, naming what is wrong with it. */
|
|
83
|
+
export function parseDoc(text, what) {
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
throw new BridgeError(422, `${what} is not JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
}
|
|
91
|
+
const problems = flowDocProblems(parsed);
|
|
92
|
+
if (problems.length > 0) {
|
|
93
|
+
throw new BridgeError(422, `${what} is not a valid FlowDoc: ${problems.join("; ")}`);
|
|
94
|
+
}
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
/** The document on disk, exactly as the file holds it. */
|
|
98
|
+
export async function readPair(dir, name) {
|
|
99
|
+
const { docPath } = pairPaths(dir, name);
|
|
100
|
+
let text;
|
|
101
|
+
try {
|
|
102
|
+
text = await readFile(docPath, "utf8");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new BridgeError(404, `No document named "${name}" in ${resolve(dir)}.`);
|
|
106
|
+
}
|
|
107
|
+
return { name, doc: parseDoc(text, `${name}${DOC_SUFFIX}`), text };
|
|
108
|
+
}
|
|
109
|
+
export async function writePair(dir, name, doc, watcher, options = {}) {
|
|
110
|
+
const { docPath, tsPath } = pairPaths(dir, name);
|
|
111
|
+
if (doc.name !== name) {
|
|
112
|
+
throw new BridgeError(400, `Refusing to write flow "${doc.name}" as ${name}${DOC_SUFFIX}: the watcher pairs ` +
|
|
113
|
+
`${name}${TS_SUFFIX} with ${name}${DOC_SUFFIX} by name, so the two must agree.`);
|
|
114
|
+
}
|
|
115
|
+
const problems = flowDocProblems(doc);
|
|
116
|
+
if (problems.length > 0) {
|
|
117
|
+
throw new BridgeError(422, `Refusing to write an invalid FlowDoc: ${problems.join("; ")}`);
|
|
118
|
+
}
|
|
119
|
+
// Read the current source first: @keep comments live in it and would be
|
|
120
|
+
// deleted by a blind overwrite.
|
|
121
|
+
const previous = existsSync(tsPath) ? await readFile(tsPath, "utf8") : undefined;
|
|
122
|
+
// The dirty guard, in the doc -> ts direction. The document says which
|
|
123
|
+
// builder source it was generated from; if that file has changed since,
|
|
124
|
+
// regenerating it would throw away an edit nobody has seen. That is the same
|
|
125
|
+
// dirty-both state the watcher detects in the other direction, and it gets
|
|
126
|
+
// the same answer: refuse, and let the user choose.
|
|
127
|
+
//
|
|
128
|
+
// A document with no meta.sourceHash has no provenance to check (it was
|
|
129
|
+
// authored outside this loop), so the write proceeds; @keep comments in the
|
|
130
|
+
// existing source still survive it.
|
|
131
|
+
const provenance = doc.meta?.sourceHash;
|
|
132
|
+
if (options.force !== true && previous !== undefined && typeof provenance === "string") {
|
|
133
|
+
const onDisk = `sha256:${sha256Hex(previous)}`;
|
|
134
|
+
if (onDisk !== provenance) {
|
|
135
|
+
throw new PairConflict(name, `both sides changed: ${name}${TS_SUFFIX} was edited since this document was ` +
|
|
136
|
+
`generated from it, and saving would overwrite that edit`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let tsText;
|
|
140
|
+
try {
|
|
141
|
+
tsText = codegen(doc, previous === undefined ? {} : { previous });
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
throw new BridgeError(422, `Cannot generate ${name}${TS_SUFFIX} from this document: ${err instanceof Error ? err.message : String(err)}`);
|
|
145
|
+
}
|
|
146
|
+
const text = serializeWithMeta(doc, `sha256:${sha256Hex(tsText)}`);
|
|
147
|
+
await writeFile(tsPath, tsText, "utf8");
|
|
148
|
+
await writeFile(docPath, text, "utf8");
|
|
149
|
+
watcher?.noteWrite(name, { tsContent: tsText, docContent: text });
|
|
150
|
+
return { name, doc: JSON.parse(text), text, docPath, tsPath, tsText };
|
|
151
|
+
}
|
|
152
|
+
/** The FlowDoc the builder file synths to right now. */
|
|
153
|
+
export async function synthPair(dir, name) {
|
|
154
|
+
const { tsPath } = pairPaths(dir, name);
|
|
155
|
+
const { flows } = await synthFile(tsPath);
|
|
156
|
+
const doc = pickDoc(flows, name);
|
|
157
|
+
if (doc === undefined) {
|
|
158
|
+
throw new BridgeError(422, `${name}${TS_SUFFIX} exports ${String(flows.length)} flows and none is named "${name}".`);
|
|
159
|
+
}
|
|
160
|
+
return doc;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolution in the code direction: the builder file wins, so the FlowDoc is
|
|
164
|
+
* rewritten from it. The source is NOT regenerated, because the user chose the
|
|
165
|
+
* source they have; only the doc moves.
|
|
166
|
+
*/
|
|
167
|
+
export async function adoptCode(dir, name, watcher) {
|
|
168
|
+
const { docPath, tsPath } = pairPaths(dir, name);
|
|
169
|
+
const tsText = await readFile(tsPath, "utf8").catch(() => {
|
|
170
|
+
throw new BridgeError(404, `No builder file named "${name}${TS_SUFFIX}" in ${resolve(dir)}.`);
|
|
171
|
+
});
|
|
172
|
+
const doc = await synthPair(dir, name);
|
|
173
|
+
const text = serializeWithMeta(doc, `sha256:${sha256Hex(tsText)}`);
|
|
174
|
+
await writeFile(docPath, text, "utf8");
|
|
175
|
+
watcher?.noteWrite(name, { tsContent: tsText, docContent: text });
|
|
176
|
+
return { name, doc: JSON.parse(text), text };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Writes `<name>.flow.ts` for every document in `dir` that has none.
|
|
180
|
+
*
|
|
181
|
+
* A directory holding only FlowDocs (an export from a live instance, a doc
|
|
182
|
+
* copied out of conformance/, a file a colleague sent) had no builder file, so
|
|
183
|
+
* the loop the studio is built around - edit the builder file, watch the canvas
|
|
184
|
+
* follow - could not start there: there was nothing to edit. Generating the
|
|
185
|
+
* file on open is safe because codegen is deterministic and the studio already
|
|
186
|
+
* regenerates it on every canvas save, so this writes exactly the bytes the
|
|
187
|
+
* first save would have written.
|
|
188
|
+
*
|
|
189
|
+
* It goes through writePair, which also stamps the document with the
|
|
190
|
+
* meta.sourceHash of the source just generated. Without that stamp the watcher
|
|
191
|
+
* would meet a pair it has never seen in sync and report a conflict on the
|
|
192
|
+
* first edit, which is the state a hand-assembled pair lands in. The bytes
|
|
193
|
+
* come back so the caller can hand them to the watcher's noteWrite as well:
|
|
194
|
+
* the stamp alone leaves the outcome resting on the initial scan winning a
|
|
195
|
+
* race against the user's first edit.
|
|
196
|
+
*
|
|
197
|
+
* A document that cannot be generated from (invalid, or holding something
|
|
198
|
+
* codegen cannot express) is reported and skipped, not thrown: the rest of the
|
|
199
|
+
* directory still opens.
|
|
200
|
+
*/
|
|
201
|
+
export async function ensureBuilderFiles(dir) {
|
|
202
|
+
const generated = [];
|
|
203
|
+
const problems = [];
|
|
204
|
+
for (const name of await listDocNames(dir)) {
|
|
205
|
+
const { tsPath } = pairPaths(dir, name);
|
|
206
|
+
if (existsSync(tsPath))
|
|
207
|
+
continue;
|
|
208
|
+
try {
|
|
209
|
+
const { doc } = await readPair(dir, name);
|
|
210
|
+
const written = await writePair(dir, name, doc);
|
|
211
|
+
generated.push({ name, tsPath, tsText: written.tsText, docText: written.text });
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
problems.push({ name, message: err instanceof Error ? err.message : String(err) });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { generated, problems };
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=pair.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pair.js","sourceRoot":"","sources":["../../src/bridge/pair.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { FlowDoc } from "@flow-as-code/core";
|
|
2
|
+
/** Bumped when a change to this file is not backwards compatible. */
|
|
3
|
+
export declare const BRIDGE_PROTOCOL = 1;
|
|
4
|
+
/** Every bridge route lives under this prefix; everything else is an asset. */
|
|
5
|
+
export declare const BRIDGE_PREFIX = "/bridge";
|
|
6
|
+
/**
|
|
7
|
+
* The global the server injects into the served index.html. Its presence is
|
|
8
|
+
* how the studio knows it is being served by the bridge rather than opened as
|
|
9
|
+
* a static build, so the app never has to speculatively probe for a server
|
|
10
|
+
* that is not there.
|
|
11
|
+
*/
|
|
12
|
+
export declare const BRIDGE_GLOBAL = "__FLOW_STUDIO_BRIDGE__";
|
|
13
|
+
/**
|
|
14
|
+
* Per-session secret proving a request came from the page flow-cli opened.
|
|
15
|
+
*
|
|
16
|
+
* Binding to loopback keeps the network out but not the developer's own
|
|
17
|
+
* browser: any site they visit can send this server a CORS-simple POST. A page
|
|
18
|
+
* cannot read the URL of a document it did not open, so it cannot learn this,
|
|
19
|
+
* which is what separates the studio from any other tab.
|
|
20
|
+
*/
|
|
21
|
+
export declare const TOKEN_PARAM = "token";
|
|
22
|
+
export declare const TOKEN_HEADER = "x-flow-studio-token";
|
|
23
|
+
/** The file suffixes the bridge pairs, matching the A04 watch engine. */
|
|
24
|
+
export declare const DOC_SUFFIX = ".flowdoc.json";
|
|
25
|
+
export declare const TS_SUFFIX = ".flow.ts";
|
|
26
|
+
/** What the bridge says about itself. Injected, and served at /bridge/info. */
|
|
27
|
+
export interface BridgeInfo {
|
|
28
|
+
protocol: number;
|
|
29
|
+
/** Absolute path of the served directory, for the toolbar. */
|
|
30
|
+
dir: string;
|
|
31
|
+
/** Short label for the toolbar (the directory's base name). */
|
|
32
|
+
label: string;
|
|
33
|
+
/** Session token; every bridge API request must present it. */
|
|
34
|
+
token: string;
|
|
35
|
+
}
|
|
36
|
+
export interface BridgeDocRef {
|
|
37
|
+
name: string;
|
|
38
|
+
}
|
|
39
|
+
export interface BridgeDocList {
|
|
40
|
+
docs: BridgeDocRef[];
|
|
41
|
+
}
|
|
42
|
+
/** A document as it exists on disk right now. `text` is the exact file bytes. */
|
|
43
|
+
export interface BridgeDocPayload {
|
|
44
|
+
name: string;
|
|
45
|
+
doc: FlowDoc;
|
|
46
|
+
text: string;
|
|
47
|
+
}
|
|
48
|
+
/** PUT /bridge/docs/<name> */
|
|
49
|
+
export interface BridgeWriteRequest {
|
|
50
|
+
doc: FlowDoc;
|
|
51
|
+
/**
|
|
52
|
+
* Write even though the builder file no longer matches the doc's
|
|
53
|
+
* meta.sourceHash. This is the user answering the conflict dialog with "keep
|
|
54
|
+
* the canvas version", and it is the ONLY thing that may set it: a write
|
|
55
|
+
* that sets it by default would be the silent overwrite the dirty guard
|
|
56
|
+
* exists to prevent.
|
|
57
|
+
*/
|
|
58
|
+
force?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** What a successful write wrote, both halves of the pair. */
|
|
61
|
+
export interface BridgeWriteResult extends BridgeDocPayload {
|
|
62
|
+
docPath: string;
|
|
63
|
+
tsPath: string;
|
|
64
|
+
/** The regenerated builder source. */
|
|
65
|
+
tsText: string;
|
|
66
|
+
}
|
|
67
|
+
/** Which side of a conflict the user chose: the FlowDoc or the builder code. */
|
|
68
|
+
export type ConflictSide = "doc" | "code";
|
|
69
|
+
/** POST /bridge/docs/<name>/resolve */
|
|
70
|
+
export interface BridgeResolveRequest {
|
|
71
|
+
side: ConflictSide;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Both sides of a dirty-both pair, so the studio can show the diff and ask.
|
|
75
|
+
* Either side can be null when it cannot be read (unparseable JSON, a builder
|
|
76
|
+
* file that throws); the matching *Error says why, and the UI must then offer
|
|
77
|
+
* only the side it has.
|
|
78
|
+
*/
|
|
79
|
+
export interface BridgeConflict {
|
|
80
|
+
name: string;
|
|
81
|
+
/** The sentence describing what diverged, from whichever side noticed. */
|
|
82
|
+
reason: string;
|
|
83
|
+
/**
|
|
84
|
+
* Where the canvas side lives, which decides how the choice is applied.
|
|
85
|
+
*
|
|
86
|
+
* "disk" both sides are files: the watcher found the pair diverged.
|
|
87
|
+
* Either choice is a POST to /resolve.
|
|
88
|
+
* "canvas" the canvas side is the unsaved document in the studio (a write
|
|
89
|
+
* the bridge refused, or a builder-file edit arriving while the
|
|
90
|
+
* canvas had unsaved changes). Keeping it is a forced write of
|
|
91
|
+
* that document, which only the studio holds.
|
|
92
|
+
*/
|
|
93
|
+
origin: "disk" | "canvas";
|
|
94
|
+
/** Absolute paths, when the side that raised the conflict knows them. */
|
|
95
|
+
docPath?: string;
|
|
96
|
+
tsPath?: string;
|
|
97
|
+
/** The canvas side: the FlowDoc on disk, or the one the studio holds. */
|
|
98
|
+
docSide: FlowDoc | null;
|
|
99
|
+
/** The FlowDoc the builder file synths to right now. */
|
|
100
|
+
codeSide: FlowDoc | null;
|
|
101
|
+
docError?: string;
|
|
102
|
+
codeError?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The A04 watch engine's three events, enriched with the payload the studio
|
|
106
|
+
* needs: "synced" carries the new document so the canvas can hot-reload
|
|
107
|
+
* without a second request, and "conflict" carries both sides.
|
|
108
|
+
*/
|
|
109
|
+
export type BridgeEvent = ({
|
|
110
|
+
seq: number;
|
|
111
|
+
kind: "synced";
|
|
112
|
+
} & BridgeDocPayload) | ({
|
|
113
|
+
seq: number;
|
|
114
|
+
kind: "conflict";
|
|
115
|
+
} & BridgeConflict) | {
|
|
116
|
+
seq: number;
|
|
117
|
+
kind: "error";
|
|
118
|
+
name?: string;
|
|
119
|
+
path: string;
|
|
120
|
+
message: string;
|
|
121
|
+
};
|
|
122
|
+
export interface BridgeEventBatch {
|
|
123
|
+
/** Highest seq in this batch, or the cursor unchanged when it is empty. */
|
|
124
|
+
cursor: number;
|
|
125
|
+
events: BridgeEvent[];
|
|
126
|
+
}
|
|
127
|
+
/** Every non-2xx response body. */
|
|
128
|
+
export interface BridgeErrorBody {
|
|
129
|
+
error: string;
|
|
130
|
+
/** Set on the 409 a write gets while the pair is in conflict. */
|
|
131
|
+
conflict?: BridgeConflict;
|
|
132
|
+
}
|
|
133
|
+
/** The export targets, the same three `flow-cli emit` and `render` produce. */
|
|
134
|
+
export declare const EXPORT_TARGETS: readonly ["cdk", "raw", "tf"];
|
|
135
|
+
export type ExportTarget = (typeof EXPORT_TARGETS)[number];
|
|
136
|
+
/**
|
|
137
|
+
* POST /bridge/export: the studio has emitted a file map and the CLI writes
|
|
138
|
+
* it, because the browser cannot and the bridge is the only thing here that
|
|
139
|
+
* touches disk. The studio computes the bytes (it holds the documents and the
|
|
140
|
+
* address or resource map); the server only checks and writes them.
|
|
141
|
+
*
|
|
142
|
+
* Deliberately not a document write: nothing is paired, nothing is synced, no
|
|
143
|
+
* event is published, and existing files are overwritten in place exactly as
|
|
144
|
+
* `flow-cli emit` overwrites them.
|
|
145
|
+
*/
|
|
146
|
+
export interface BridgeExportRequest {
|
|
147
|
+
target: ExportTarget;
|
|
148
|
+
/**
|
|
149
|
+
* Relative POSIX path to file content, each path satisfying isExportPath and
|
|
150
|
+
* taken relative to `subdir`.
|
|
151
|
+
*/
|
|
152
|
+
files: Record<string, string>;
|
|
153
|
+
/**
|
|
154
|
+
* Subdirectory of the served directory to write into. Absent or "" writes to
|
|
155
|
+
* its root, which is where `flow-cli emit` writes with no --out.
|
|
156
|
+
*/
|
|
157
|
+
subdir?: string;
|
|
158
|
+
}
|
|
159
|
+
export interface BridgeExportResult {
|
|
160
|
+
target: ExportTarget;
|
|
161
|
+
/** Absolute paths written, sorted. */
|
|
162
|
+
paths: string[];
|
|
163
|
+
}
|
|
164
|
+
/** Most files an export may carry, and the most bytes across all of them. */
|
|
165
|
+
export declare const EXPORT_MAX_FILES = 200;
|
|
166
|
+
export declare const EXPORT_MAX_BYTES: number;
|
|
167
|
+
/** `flows.tf`, `flows/support-line.flow.tftpl`: relative, POSIX, no traversal. */
|
|
168
|
+
export declare function isExportPath(path: string): boolean;
|
|
169
|
+
/** A destination subdirectory: the same rule, and "" for the served root. */
|
|
170
|
+
export declare function isExportSubdir(subdir: string): boolean;
|
|
171
|
+
/** Documents are addressed by @flow-as-code/core slug, which is also the path guard. */
|
|
172
|
+
export declare function isBridgeDocName(name: string): boolean;
|
|
173
|
+
export declare function infoUrl(base: string): string;
|
|
174
|
+
export declare function docsUrl(base: string): string;
|
|
175
|
+
export declare function docUrl(base: string, name: string): string;
|
|
176
|
+
export declare function resolveUrl(base: string, name: string): string;
|
|
177
|
+
export declare function eventsUrl(base: string, cursor: number): string;
|
|
178
|
+
export declare function exportUrl(base: string): string;
|
|
179
|
+
/** Type guard for the injected global, used by the studio at boot. */
|
|
180
|
+
export declare function readBridgeInfo(scope: unknown): BridgeInfo | undefined;
|
|
181
|
+
/**
|
|
182
|
+
* The script tag the server injects into index.html. "<" is escaped so a
|
|
183
|
+
* directory name containing "</script>" cannot close the tag it sits in;
|
|
184
|
+
* JSON.stringify handles the rest, and the result is still valid JSON.
|
|
185
|
+
*/
|
|
186
|
+
export declare function bridgeBootScript(info: BridgeInfo): string;
|
|
187
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/bridge/protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 The flow-as-code Authors
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
// The `flow-cli studio` bridge protocol: the whole contract between the local
|
|
6
|
+
// server (packages/cli/src/bridge/server.ts) and the studio's BridgeStore
|
|
7
|
+
// (packages/studio/src/store/bridgeStore.ts).
|
|
8
|
+
//
|
|
9
|
+
// This file exists twice, byte for byte, once in each package. The studio does
|
|
10
|
+
// not depend on @flow-as-code/cli (which depends on the studio, for its built
|
|
11
|
+
// assets), so importing across would make the dependency circular. The copy is
|
|
12
|
+
// only safe while it is provably a copy: packages/cli/src/bridge/
|
|
13
|
+
// protocol.test.ts fails if the two files differ by a single byte, the same
|
|
14
|
+
// arrangement src/schema.test.ts uses for the packaged FlowDoc schema.
|
|
15
|
+
//
|
|
16
|
+
// Transport notes that belong to the contract, not to either implementation:
|
|
17
|
+
//
|
|
18
|
+
// - Everything is JSON over plain HTTP on 127.0.0.1. No websocket library
|
|
19
|
+
// and no EventSource: the event stream is a long poll, which needs nothing
|
|
20
|
+
// but fetch, behaves identically in a browser and in a node test, and
|
|
21
|
+
// cannot half-close the way a streamed response can.
|
|
22
|
+
// - The client holds one GET /bridge/events?cursor=<seq> open at a time. The
|
|
23
|
+
// server answers immediately when it already has events past the cursor,
|
|
24
|
+
// and otherwise parks the request until one arrives or the poll times out
|
|
25
|
+
// with an empty batch. The client then re-polls from the returned cursor,
|
|
26
|
+
// so no event can be missed between two polls.
|
|
27
|
+
// - Every document is addressed by NAME, never by path. A name is a
|
|
28
|
+
// @flow-as-code/core slug; the server refuses anything else, which is also what
|
|
29
|
+
// keeps ".." out of the file paths it builds.
|
|
30
|
+
import { SLUG_PATTERN } from "@flow-as-code/core";
|
|
31
|
+
/** Bumped when a change to this file is not backwards compatible. */
|
|
32
|
+
export const BRIDGE_PROTOCOL = 1;
|
|
33
|
+
/** Every bridge route lives under this prefix; everything else is an asset. */
|
|
34
|
+
export const BRIDGE_PREFIX = "/bridge";
|
|
35
|
+
/**
|
|
36
|
+
* The global the server injects into the served index.html. Its presence is
|
|
37
|
+
* how the studio knows it is being served by the bridge rather than opened as
|
|
38
|
+
* a static build, so the app never has to speculatively probe for a server
|
|
39
|
+
* that is not there.
|
|
40
|
+
*/
|
|
41
|
+
export const BRIDGE_GLOBAL = "__FLOW_STUDIO_BRIDGE__";
|
|
42
|
+
/**
|
|
43
|
+
* Per-session secret proving a request came from the page flow-cli opened.
|
|
44
|
+
*
|
|
45
|
+
* Binding to loopback keeps the network out but not the developer's own
|
|
46
|
+
* browser: any site they visit can send this server a CORS-simple POST. A page
|
|
47
|
+
* cannot read the URL of a document it did not open, so it cannot learn this,
|
|
48
|
+
* which is what separates the studio from any other tab.
|
|
49
|
+
*/
|
|
50
|
+
export const TOKEN_PARAM = "token";
|
|
51
|
+
export const TOKEN_HEADER = "x-flow-studio-token";
|
|
52
|
+
/** The file suffixes the bridge pairs, matching the A04 watch engine. */
|
|
53
|
+
export const DOC_SUFFIX = ".flowdoc.json";
|
|
54
|
+
export const TS_SUFFIX = ".flow.ts";
|
|
55
|
+
/** The export targets, the same three `flow-cli emit` and `render` produce. */
|
|
56
|
+
export const EXPORT_TARGETS = ["cdk", "raw", "tf"];
|
|
57
|
+
/** Most files an export may carry, and the most bytes across all of them. */
|
|
58
|
+
export const EXPORT_MAX_FILES = 200;
|
|
59
|
+
export const EXPORT_MAX_BYTES = 4 * 1024 * 1024;
|
|
60
|
+
/**
|
|
61
|
+
* One path segment of an export path: printable, no separator, no traversal,
|
|
62
|
+
* and never a dotfile. Anything else is refused rather than sanitized, because
|
|
63
|
+
* a sanitizer is a guess about intent and this writes files the user owns.
|
|
64
|
+
*/
|
|
65
|
+
const EXPORT_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
|
|
66
|
+
/** Deepest an export path may nest, counting the file itself. */
|
|
67
|
+
const EXPORT_MAX_DEPTH = 4;
|
|
68
|
+
function exportSegments(path, maxDepth) {
|
|
69
|
+
if (path === "" || path.length > 200 || path.includes("\0") || path.includes("\\")) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const segments = path.split("/");
|
|
73
|
+
if (segments.length > maxDepth)
|
|
74
|
+
return undefined;
|
|
75
|
+
return segments.every((s) => EXPORT_SEGMENT.test(s)) ? segments : undefined;
|
|
76
|
+
}
|
|
77
|
+
/** `flows.tf`, `flows/support-line.flow.tftpl`: relative, POSIX, no traversal. */
|
|
78
|
+
export function isExportPath(path) {
|
|
79
|
+
return exportSegments(path, EXPORT_MAX_DEPTH) !== undefined;
|
|
80
|
+
}
|
|
81
|
+
/** A destination subdirectory: the same rule, and "" for the served root. */
|
|
82
|
+
export function isExportSubdir(subdir) {
|
|
83
|
+
return subdir === "" || exportSegments(subdir, EXPORT_MAX_DEPTH - 1) !== undefined;
|
|
84
|
+
}
|
|
85
|
+
/** Documents are addressed by @flow-as-code/core slug, which is also the path guard. */
|
|
86
|
+
export function isBridgeDocName(name) {
|
|
87
|
+
return SLUG_PATTERN.test(name);
|
|
88
|
+
}
|
|
89
|
+
export function infoUrl(base) {
|
|
90
|
+
return `${base}${BRIDGE_PREFIX}/info`;
|
|
91
|
+
}
|
|
92
|
+
export function docsUrl(base) {
|
|
93
|
+
return `${base}${BRIDGE_PREFIX}/docs`;
|
|
94
|
+
}
|
|
95
|
+
export function docUrl(base, name) {
|
|
96
|
+
return `${docsUrl(base)}/${encodeURIComponent(name)}`;
|
|
97
|
+
}
|
|
98
|
+
export function resolveUrl(base, name) {
|
|
99
|
+
return `${docUrl(base, name)}/resolve`;
|
|
100
|
+
}
|
|
101
|
+
export function eventsUrl(base, cursor) {
|
|
102
|
+
return `${base}${BRIDGE_PREFIX}/events?cursor=${String(cursor)}`;
|
|
103
|
+
}
|
|
104
|
+
export function exportUrl(base) {
|
|
105
|
+
return `${base}${BRIDGE_PREFIX}/export`;
|
|
106
|
+
}
|
|
107
|
+
/** Type guard for the injected global, used by the studio at boot. */
|
|
108
|
+
export function readBridgeInfo(scope) {
|
|
109
|
+
if (scope === null || typeof scope !== "object")
|
|
110
|
+
return undefined;
|
|
111
|
+
const value = scope[BRIDGE_GLOBAL];
|
|
112
|
+
if (value === null || typeof value !== "object")
|
|
113
|
+
return undefined;
|
|
114
|
+
const info = value;
|
|
115
|
+
if (info.protocol !== BRIDGE_PROTOCOL)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (typeof info.token !== "string" || info.token === "")
|
|
118
|
+
return undefined;
|
|
119
|
+
if (typeof info.dir !== "string" || typeof info.label !== "string")
|
|
120
|
+
return undefined;
|
|
121
|
+
return { protocol: info.protocol, dir: info.dir, label: info.label, token: info.token };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The script tag the server injects into index.html. "<" is escaped so a
|
|
125
|
+
* directory name containing "</script>" cannot close the tag it sits in;
|
|
126
|
+
* JSON.stringify handles the rest, and the result is still valid JSON.
|
|
127
|
+
*/
|
|
128
|
+
export function bridgeBootScript(info) {
|
|
129
|
+
const json = JSON.stringify(info).replace(/</g, "\\u003c");
|
|
130
|
+
return `<script>window.${BRIDGE_GLOBAL} = ${json};</script>`;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/bridge/protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import { type BridgeEvent, type BridgeInfo } from "./protocol.js";
|
|
3
|
+
import { type EnsureBuilderFilesResult } from "./pair.js";
|
|
4
|
+
/**
|
|
5
|
+
* A bridge event before it is published. Distributive by hand: `Omit` over a
|
|
6
|
+
* union keeps only the keys every member shares, which would erase the payload
|
|
7
|
+
* of all three event kinds.
|
|
8
|
+
*/
|
|
9
|
+
type Unsequenced<T> = T extends {
|
|
10
|
+
seq: number;
|
|
11
|
+
} ? Omit<T, "seq"> : never;
|
|
12
|
+
export type PendingEvent = Unsequenced<BridgeEvent>;
|
|
13
|
+
/** The one address this server may bind. See the note at the top of the file. */
|
|
14
|
+
export declare const BRIDGE_HOST = "127.0.0.1";
|
|
15
|
+
export interface StudioServerOptions {
|
|
16
|
+
/** Directory of FlowDocs and builder files to serve. */
|
|
17
|
+
dir: string;
|
|
18
|
+
/**
|
|
19
|
+
* Directory of built studio assets. Omitted, the bridge serves the API only,
|
|
20
|
+
* which is what the tests use and what a `vite dev` session would proxy to.
|
|
21
|
+
*/
|
|
22
|
+
assetsDir?: string;
|
|
23
|
+
/** 0 (the default) picks a free port. */
|
|
24
|
+
port?: number;
|
|
25
|
+
/** Watch `dir` for builder-file edits. Default true. */
|
|
26
|
+
watch?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Write the missing `<name>.flow.ts` for every document in `dir` before
|
|
29
|
+
* serving it, so a directory of FlowDocs alone can start the edit-the-code
|
|
30
|
+
* loop. Default false; `flow-cli studio` turns it on. See ensureBuilderFiles.
|
|
31
|
+
*/
|
|
32
|
+
ensurePairs?: boolean;
|
|
33
|
+
/** Shortened in tests; the default is a normal long-poll timeout. */
|
|
34
|
+
pollTimeoutMs?: number;
|
|
35
|
+
/** Fixed session token. Tests pin it; production generates one per run. */
|
|
36
|
+
token?: string;
|
|
37
|
+
/** Called for every published event, so the command can log a line. */
|
|
38
|
+
onEvent?: (event: BridgeEvent) => void;
|
|
39
|
+
}
|
|
40
|
+
export interface StudioServer {
|
|
41
|
+
readonly url: string;
|
|
42
|
+
/** Session token embedded in `url`; every bridge API call must present it. */
|
|
43
|
+
readonly token: string;
|
|
44
|
+
readonly host: string;
|
|
45
|
+
readonly port: number;
|
|
46
|
+
readonly dir: string;
|
|
47
|
+
/** What `ensurePairs` wrote and what it could not write. Empty when off. */
|
|
48
|
+
readonly prepared: EnsureBuilderFilesResult;
|
|
49
|
+
/** The underlying server, for tests that assert the bound address. */
|
|
50
|
+
readonly server: Server;
|
|
51
|
+
close(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
/** 256 bits from the CSPRNG. Never logged except as part of the URL. */
|
|
54
|
+
export declare function newSessionToken(): string;
|
|
55
|
+
export declare function hostHeaderAllowed(host: string | undefined): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Where an asset request points: the absolute path, or the status the refusal
|
|
58
|
+
* deserves. The two failures are not the same thing and must not answer the
|
|
59
|
+
* same way. A request the client mis-encoded is a 400 (it can be fixed by
|
|
60
|
+
* asking differently); a well-formed path that leaves the asset root is a 403
|
|
61
|
+
* (it cannot). Collapsing them was a real bug: decodeURIComponent throws a
|
|
62
|
+
* URIError on a truncated escape such as "/%E0%A4%A", which left this function
|
|
63
|
+
* uncaught and surfaced as 500 "URI malformed", reporting our own fault for
|
|
64
|
+
* the client's mistake.
|
|
65
|
+
*/
|
|
66
|
+
export type AssetTarget = {
|
|
67
|
+
ok: true;
|
|
68
|
+
path: string;
|
|
69
|
+
} | {
|
|
70
|
+
ok: false;
|
|
71
|
+
status: 400 | 403;
|
|
72
|
+
message: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The URL parser has already collapsed "." and ".." segments, and the
|
|
76
|
+
* containment check below catches whatever survives that (an encoded
|
|
77
|
+
* separator, a symlinked name, a Windows drive letter).
|
|
78
|
+
*/
|
|
79
|
+
export declare function assetPathFor(root: string, pathname: string): AssetTarget;
|
|
80
|
+
/**
|
|
81
|
+
* Percent-decodes one piece of a request path, or undefined when the escaping
|
|
82
|
+
* is malformed. Every decodeURIComponent in this file goes through here:
|
|
83
|
+
* unguarded it throws a URIError, and a URIError reaching the router is a 500
|
|
84
|
+
* for what is really a bad request.
|
|
85
|
+
*/
|
|
86
|
+
export declare function decodePathSegment(raw: string): string | undefined;
|
|
87
|
+
/** Puts the boot script first in <head>, before the app's module script. */
|
|
88
|
+
export declare function injectBoot(html: string, info: BridgeInfo): string;
|
|
89
|
+
/**
|
|
90
|
+
* Starts the bridge on 127.0.0.1. Resolves once it is listening, with the URL
|
|
91
|
+
* to open.
|
|
92
|
+
*/
|
|
93
|
+
export declare function startStudioServer(options: StudioServerOptions): Promise<StudioServer>;
|
|
94
|
+
export {};
|
|
95
|
+
//# sourceMappingURL=server.d.ts.map
|