@nanobpm/workflow 0.8.1 → 0.9.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 +1 -1
- package/dist/declarative.d.ts +172 -10
- package/dist/declarative.js +213 -355
- package/dist/nodes/branch.d.ts +11 -0
- package/dist/nodes/branch.js +40 -0
- package/dist/nodes/break.d.ts +8 -0
- package/dist/nodes/break.js +19 -0
- package/dist/nodes/continue.d.ts +8 -0
- package/dist/nodes/continue.js +18 -0
- package/dist/nodes/forEach.d.ts +24 -0
- package/dist/nodes/forEach.js +104 -0
- package/dist/nodes/index.d.ts +11 -0
- package/dist/nodes/index.js +18 -0
- package/dist/nodes/loop.d.ts +9 -0
- package/dist/nodes/loop.js +25 -0
- package/dist/nodes/parallel.d.ts +9 -0
- package/dist/nodes/parallel.js +40 -0
- package/dist/nodes/registry.d.ts +33 -0
- package/dist/nodes/registry.js +71 -0
- package/dist/nodes/run.d.ts +10 -0
- package/dist/nodes/run.js +21 -0
- package/dist/nodes/signal.d.ts +11 -0
- package/dist/nodes/signal.js +21 -0
- package/dist/nodes/switch.d.ts +11 -0
- package/dist/nodes/switch.js +51 -0
- package/dist/nodes/task.d.ts +11 -0
- package/dist/nodes/task.js +21 -0
- package/dist/nodes/timer.d.ts +9 -0
- package/dist/nodes/timer.js +34 -0
- package/dist/test-support/diff.d.ts +8 -0
- package/dist/test-support/diff.js +70 -0
- package/dist/test-support/index.d.ts +8 -0
- package/dist/test-support/index.js +49 -0
- package/dist/test-support/normalize.d.ts +19 -0
- package/dist/test-support/normalize.js +245 -0
- package/dist/test-support/parity.d.ts +28 -0
- package/dist/test-support/parity.js +61 -0
- package/dist/test-support/xml.d.ts +17 -0
- package/dist/test-support/xml.js +135 -0
- package/dist/types.d.ts +20 -56
- package/package.json +14 -6
- package/src/declarative.ts +327 -359
- package/src/nodes/README.md +123 -0
- package/src/nodes/branch.ts +48 -0
- package/src/nodes/break.ts +25 -0
- package/src/nodes/continue.ts +24 -0
- package/src/nodes/forEach.ts +137 -0
- package/src/nodes/index.ts +19 -0
- package/src/nodes/loop.ts +34 -0
- package/src/nodes/parallel.ts +48 -0
- package/src/nodes/registry.ts +105 -0
- package/src/nodes/run.ts +29 -0
- package/src/nodes/signal.ts +29 -0
- package/src/nodes/switch.ts +56 -0
- package/src/nodes/task.ts +29 -0
- package/src/nodes/timer.ts +42 -0
- package/src/test-support/diff.ts +69 -0
- package/src/test-support/index.ts +53 -0
- package/src/test-support/normalize.ts +274 -0
- package/src/test-support/parity.ts +85 -0
- package/src/test-support/xml.ts +140 -0
- package/src/types.ts +20 -30
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// A dependency-free XML reader, just enough to parse a BPMN 2.0 document into a
|
|
2
|
+
// tree the normalizer can canonicalize. BPMN files this package emits (and the
|
|
3
|
+
// hand-authored goldens) are plain XML — elements, attributes, text — with no
|
|
4
|
+
// DTDs, processing instructions beyond the `<?xml …?>` prolog, or mixed-content
|
|
5
|
+
// subtleties that would need a full parser. We keep prefixed names verbatim
|
|
6
|
+
// (`bpmn:serviceTask`, `zeebe:taskDefinition`) so the normalizer can reason about
|
|
7
|
+
// namespaces by local name.
|
|
8
|
+
|
|
9
|
+
/** A parsed XML element. `attrs` preserves declared attributes (entity-decoded);
|
|
10
|
+
* `children` are child elements in document order; `text` is the concatenated
|
|
11
|
+
* direct text content (trimmed), used for elements like `conditionExpression`. */
|
|
12
|
+
export interface XmlElement {
|
|
13
|
+
/** The qualified name as written, e.g. `bpmn:serviceTask` or `process`. */
|
|
14
|
+
name: string;
|
|
15
|
+
attrs: Record<string, string>;
|
|
16
|
+
children: XmlElement[];
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The local (un-prefixed) name of an element or attribute. */
|
|
21
|
+
export function localName(qualified: string): string {
|
|
22
|
+
const i = qualified.indexOf(":");
|
|
23
|
+
return i === -1 ? qualified : qualified.slice(i + 1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function decodeEntities(s: string): string {
|
|
27
|
+
return s.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (m, body: string) => {
|
|
28
|
+
switch (body) {
|
|
29
|
+
case "lt":
|
|
30
|
+
return "<";
|
|
31
|
+
case "gt":
|
|
32
|
+
return ">";
|
|
33
|
+
case "amp":
|
|
34
|
+
return "&";
|
|
35
|
+
case "quot":
|
|
36
|
+
return '"';
|
|
37
|
+
case "apos":
|
|
38
|
+
return "'";
|
|
39
|
+
}
|
|
40
|
+
if (body[0] === "#") {
|
|
41
|
+
const code =
|
|
42
|
+
body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
|
|
43
|
+
// Only decode a valid Unicode scalar value. `String.fromCodePoint` throws
|
|
44
|
+
// a `RangeError` for code points outside 0..0x10FFFF (e.g. `�`),
|
|
45
|
+
// and surrogate halves (0xD800..0xDFFF) are not valid characters — treat
|
|
46
|
+
// any such reference as malformed and leave it verbatim rather than
|
|
47
|
+
// throwing an opaque exception out of `parseXml`.
|
|
48
|
+
const isScalar = Number.isInteger(code) && code >= 0 && code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff);
|
|
49
|
+
return isScalar ? String.fromCodePoint(code) : m;
|
|
50
|
+
}
|
|
51
|
+
return m;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseAttrs(src: string): Record<string, string> {
|
|
56
|
+
const attrs: Record<string, string> = {};
|
|
57
|
+
const re = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
58
|
+
let m: RegExpExecArray | null = re.exec(src);
|
|
59
|
+
while (m !== null) {
|
|
60
|
+
const value = m[3] !== undefined ? m[3] : (m[4] ?? "");
|
|
61
|
+
attrs[m[1]] = decodeEntities(value);
|
|
62
|
+
m = re.exec(src);
|
|
63
|
+
}
|
|
64
|
+
return attrs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function appendText(el: XmlElement, chunk: string): void {
|
|
68
|
+
if (chunk) el.text = el.text ? `${el.text} ${chunk}` : chunk;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Parse an XML document into its root element. Throws on a malformed document
|
|
72
|
+
* (unbalanced tags, no root, or more than one top-level element). Comments,
|
|
73
|
+
* CDATA, the `<?xml?>` prolog and other processing instructions are handled;
|
|
74
|
+
* CDATA content contributes to `text`. */
|
|
75
|
+
export function parseXml(xml: string): XmlElement {
|
|
76
|
+
const root: XmlElement = { name: "#root", attrs: {}, children: [], text: "" };
|
|
77
|
+
const stack: XmlElement[] = [root];
|
|
78
|
+
let i = 0;
|
|
79
|
+
const n = xml.length;
|
|
80
|
+
while (i < n) {
|
|
81
|
+
const lt = xml.indexOf("<", i);
|
|
82
|
+
if (lt === -1) break;
|
|
83
|
+
if (lt > i) appendText(stack[stack.length - 1], decodeEntities(xml.slice(i, lt)).trim());
|
|
84
|
+
if (xml.startsWith("<!--", lt)) {
|
|
85
|
+
const end = xml.indexOf("-->", lt + 4);
|
|
86
|
+
if (end === -1) throw new Error("parseXml: unterminated comment");
|
|
87
|
+
i = end + 3;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (xml.startsWith("<![CDATA[", lt)) {
|
|
91
|
+
const end = xml.indexOf("]]>", lt + 9);
|
|
92
|
+
if (end === -1) throw new Error("parseXml: unterminated CDATA");
|
|
93
|
+
appendText(stack[stack.length - 1], xml.slice(lt + 9, end).trim());
|
|
94
|
+
i = end + 3;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (xml.startsWith("<?", lt) || xml.startsWith("<!", lt)) {
|
|
98
|
+
const end = xml.indexOf(">", lt + 2);
|
|
99
|
+
if (end === -1) throw new Error("parseXml: unterminated processing instruction / declaration");
|
|
100
|
+
i = end + 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const gt = xml.indexOf(">", lt);
|
|
104
|
+
if (gt === -1) throw new Error("parseXml: unterminated tag");
|
|
105
|
+
let inner = xml.slice(lt + 1, gt);
|
|
106
|
+
if (inner.startsWith("/")) {
|
|
107
|
+
const closeName = inner.slice(1).trim();
|
|
108
|
+
if (stack.length <= 1) throw new Error(`parseXml: unbalanced end tag </${closeName}>`);
|
|
109
|
+
const open = stack.pop();
|
|
110
|
+
if (open === undefined || open.name !== closeName) {
|
|
111
|
+
throw new Error(`parseXml: mismatched end tag </${closeName}> (expected </${open?.name ?? "#root"}>)`);
|
|
112
|
+
}
|
|
113
|
+
i = gt + 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const selfClosing = inner.endsWith("/");
|
|
117
|
+
if (selfClosing) inner = inner.slice(0, -1);
|
|
118
|
+
const nameMatch = /^\s*(\S+)/.exec(inner);
|
|
119
|
+
if (!nameMatch) throw new Error("parseXml: malformed start tag");
|
|
120
|
+
const name = nameMatch[1];
|
|
121
|
+
const attrs = parseAttrs(inner.slice(nameMatch[0].length));
|
|
122
|
+
const el: XmlElement = { name, attrs, children: [], text: "" };
|
|
123
|
+
stack[stack.length - 1].children.push(el);
|
|
124
|
+
if (!selfClosing) stack.push(el);
|
|
125
|
+
i = gt + 1;
|
|
126
|
+
}
|
|
127
|
+
if (stack.length !== 1) {
|
|
128
|
+
const unclosed = stack
|
|
129
|
+
.slice(1)
|
|
130
|
+
.map((el) => `<${el.name}>`)
|
|
131
|
+
.join(", ");
|
|
132
|
+
throw new Error(`parseXml: unclosed tag(s): ${unclosed}`);
|
|
133
|
+
}
|
|
134
|
+
if (root.children.length === 0) throw new Error("parseXml: no root element");
|
|
135
|
+
if (root.children.length > 1) {
|
|
136
|
+
const roots = root.children.map((el) => `<${el.name}>`).join(", ");
|
|
137
|
+
throw new Error(`parseXml: multiple root elements (${roots}) — a well-formed document has exactly one`);
|
|
138
|
+
}
|
|
139
|
+
return root.children[0];
|
|
140
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -52,36 +52,26 @@ export type StepHandler<
|
|
|
52
52
|
bivarianceHack(job: Job<V>): Promise<R | void> | R | void;
|
|
53
53
|
}["bivarianceHack"];
|
|
54
54
|
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
body: FlowNode[];
|
|
76
|
-
/** Run children one at a time (sequential MI) instead of all at once. */
|
|
77
|
-
sequential?: boolean;
|
|
78
|
-
/** List variable that each child's `outputElement` is collected into. */
|
|
79
|
-
outputCollection?: string;
|
|
80
|
-
/** FEEL expression producing each child's contribution to `outputCollection`. */
|
|
81
|
-
outputElement?: string;
|
|
82
|
-
/** FEEL boolean that, once true, completes the body early. */
|
|
83
|
-
completionCondition?: string;
|
|
84
|
-
};
|
|
55
|
+
/** The OPEN registry of flow-node kinds — the single extension seam for the
|
|
56
|
+
* `FlowNode` union (epic #314, S0/#315). Each kind module under `src/nodes/`
|
|
57
|
+
* declaration-merges ONE property into this interface (`kindName: NodeShape`),
|
|
58
|
+
* so `FlowNode` grows without editing a central union here. The built-in kinds
|
|
59
|
+
* register `run`/`task`/`signal`/`timer`/`switch`/`branch`/`loop`/`break`/
|
|
60
|
+
* `continue`/`parallel`/`forEach` from their own modules through this same
|
|
61
|
+
* mechanism — see `src/nodes/README.md`.
|
|
62
|
+
*
|
|
63
|
+
* Invariant: every value type MUST be an object with a `kind` discriminant
|
|
64
|
+
* equal to its key, so `FlowNode` stays a discriminated union.
|
|
65
|
+
*
|
|
66
|
+
* This interface is intentionally empty here: it is the augmentation target the
|
|
67
|
+
* per-kind modules declaration-merge their variant into. */
|
|
68
|
+
export interface FlowNodeRegistry {}
|
|
69
|
+
|
|
70
|
+
/** A node in a declarative flow tree, derived from the {@link FlowNodeRegistry}.
|
|
71
|
+
* Leaf activities carry optional data envelopes (lifted to `nano:shape` +
|
|
72
|
+
* `dataEnvelope` in the model); structural combinators carry nested
|
|
73
|
+
* `FlowNode[]` bodies. */
|
|
74
|
+
export type FlowNode = FlowNodeRegistry[keyof FlowNodeRegistry];
|
|
85
75
|
|
|
86
76
|
/** A timer intermediate-catch definition: exactly one of `after` (an ISO-8601
|
|
87
77
|
* delay or FEEL expression) or `at` (an absolute instant or FEEL expression).
|