@syntax-syllogism/flow-delta 0.5.1 → 0.6.1

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.
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/ci/flexipage-github-report.ts
4
+ import { readFileSync as readFileSync2 } from "node:fs";
5
+ import { parseArgs } from "node:util";
6
+
7
+ // src/util/is-main-module.ts
8
+ import { realpathSync } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+ function isMainModule(metaUrl, argvPath = process.argv[1]) {
11
+ if (!argvPath) {
12
+ return false;
13
+ }
14
+ try {
15
+ return realpathSync(fileURLToPath(metaUrl)) === realpathSync(argvPath);
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ // src/ci/report-core.ts
22
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
23
+ import { join } from "node:path";
24
+
25
+ // src/render/snapshot-panel.ts
26
+ var COLLECTION_KEY_NAMES = [
27
+ "assignmentItems",
28
+ "choiceReferences",
29
+ "choices",
30
+ "conditions",
31
+ "customErrorMessages",
32
+ "dataTypeMappings",
33
+ "fields",
34
+ "filters",
35
+ "inputAssignments",
36
+ "inputParameters",
37
+ "outputParameters",
38
+ "outputAssignments",
39
+ "processMetadataValues",
40
+ "rules",
41
+ "scheduledPaths",
42
+ "sortOptions",
43
+ "stageSteps",
44
+ "steps",
45
+ "storeOutputParameters",
46
+ "transformValues",
47
+ "valueMappings",
48
+ "waitEvents"
49
+ ];
50
+ var COLLECTION_KEYS = new Set(COLLECTION_KEY_NAMES);
51
+
52
+ // src/util/file-name.ts
53
+ function safeFileName(value) {
54
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
55
+ }
56
+
57
+ // src/ci/report-core.ts
58
+ var FLEXIPAGE_MARKER = "<!-- FlexiPageDelta:report -->";
59
+ function loadArtifactUrls(path) {
60
+ if (!path || !existsSync(path)) {
61
+ return {};
62
+ }
63
+ const raw = readFileSync(path, "utf8").trim();
64
+ if (!raw) {
65
+ return {};
66
+ }
67
+ try {
68
+ const parsed = JSON.parse(raw);
69
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
70
+ return {};
71
+ }
72
+ return Object.fromEntries(
73
+ Object.entries(parsed).filter(
74
+ (entry) => typeof entry[1] === "string" && (entry[1].startsWith("https://") || entry[1].startsWith("http://"))
75
+ )
76
+ );
77
+ } catch {
78
+ return {};
79
+ }
80
+ }
81
+ function resolveArtifactUrl(stem, urls, fallback) {
82
+ return urls[`${stem}.html`] ?? fallback;
83
+ }
84
+ function buildProductComment(results, vocabulary, opts = {}) {
85
+ const sorted = [...results].sort((left, right) => vocabulary.sortName(left).localeCompare(vocabulary.sortName(right)));
86
+ const lines = [opts.marker ?? vocabulary.marker, "", vocabulary.heading(sorted.length), ""];
87
+ if (sorted.length === 0) lines.push(vocabulary.empty);
88
+ else lines.push(vocabulary.columns, vocabulary.separator, ...sorted.map(vocabulary.row));
89
+ if (opts.commitSha) lines.push("", `Commit: \`${opts.commitSha.slice(0, 7)}\``);
90
+ return lines.join("\n");
91
+ }
92
+ function isZeroPageSummary(summary) {
93
+ return summary.addedComponents === 0 && summary.removedComponents === 0 && summary.modifiedComponents === 0 && summary.addedRegions === 0 && summary.removedRegions === 0 && summary.modifiedRegions === 0 && summary.changedPageAttributes === 0;
94
+ }
95
+ function readFlexiPageResults(inputDir) {
96
+ return readdirSync(inputDir).filter((file) => file.endsWith(".diff.json")).map((file) => {
97
+ const diff = JSON.parse(readFileSync(join(inputDir, file), "utf8"));
98
+ if (isZeroPageSummary(diff.summary)) return null;
99
+ return { pageName: diff.pageName, summary: diff.summary, pageChanges: diff.pageChanges, stem: safeFileName(diff.pageName) };
100
+ }).filter((value) => value !== null);
101
+ }
102
+ function buildFlexiPageComment(results, opts = {}) {
103
+ return buildProductComment(results, {
104
+ marker: FLEXIPAGE_MARKER,
105
+ heading: (count) => `## \u{1F50D} FlexiPageDelta \u2014 ${count} page(s) changed`,
106
+ empty: "_No FlexiPage changes in this MR._",
107
+ columns: "| Page | Components (+/\u2013/~) | Regions (+/\u2013/~) | Page attributes | Diff |",
108
+ separator: "| --- | ---: | ---: | --- | --- |",
109
+ sortName: (result) => result.pageName,
110
+ row: (result) => {
111
+ const callout = result.pageChanges?.find((change) => change.path === "template");
112
+ const attributes = callout ? `Template: ${formatValue(callout.before)} \u2192 ${formatValue(callout.after)}` : String(result.summary.changedPageAttributes);
113
+ return `| ${escapeTableCell(result.pageName)} | +${result.summary.addedComponents} / \u2212${result.summary.removedComponents} / ~${result.summary.modifiedComponents} | +${result.summary.addedRegions} / \u2212${result.summary.removedRegions} / ~${result.summary.modifiedRegions} | ${escapeTableCell(attributes)} | [Open interactive diff](${result.artifactUrl}) |`;
114
+ }
115
+ }, opts);
116
+ }
117
+ function formatValue(value) {
118
+ return value === void 0 ? "(missing)" : String(value);
119
+ }
120
+ function findStickyNote(notes, marker, authorId) {
121
+ return notes.find((note) => note.body.includes(marker) && (authorId === void 0 || note.author?.id === authorId));
122
+ }
123
+ function escapeTableCell(value) {
124
+ return value.replaceAll("|", "\\|").replaceAll("`", "\\`").replace(/\r?\n/g, "<br>");
125
+ }
126
+
127
+ // src/ci/flexipage-github-report.ts
128
+ var buildComment = buildFlexiPageComment;
129
+ var isZeroSummary = isZeroPageSummary;
130
+ function artifactUrl(env) {
131
+ return `${env.serverUrl.replace(/\/+$/, "")}/${env.owner}/${env.repo}/actions/runs/${env.runId}`;
132
+ }
133
+ async function upsertComment(env, body, fetchImpl = fetch) {
134
+ const headers = { Authorization: `Bearer ${env.token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", "Content-Type": "application/json" };
135
+ const comments = await requestJson(fetchImpl, `${env.apiUrl}/repos/${env.owner}/${env.repo}/issues/${env.prNumber}/comments?per_page=100`, { headers });
136
+ const sticky = findStickyNote(comments.map((comment) => ({ id: comment.id, body: comment.body })), env.marker ?? FLEXIPAGE_MARKER);
137
+ await requestJson(fetchImpl, `${env.apiUrl}/repos/${env.owner}/${env.repo}/issues/${env.prNumber}/comments${sticky ? `/${sticky.id}` : ""}`, { method: sticky ? "PATCH" : "POST", headers, body: JSON.stringify({ body }) });
138
+ }
139
+ async function main(argv = process.argv.slice(2)) {
140
+ try {
141
+ const values = parseArgs({ args: argv, options: { in: { type: "string" }, "api-url": { type: "string" }, repo: { type: "string" }, pr: { type: "string" }, "server-url": { type: "string" }, "run-id": { type: "string" }, token: { type: "string" }, "commit-sha": { type: "string" }, "artifact-urls": { type: "string" }, marker: { type: "string" }, help: { type: "boolean", short: "h" } }, allowPositionals: false }).values;
142
+ if (values.help) {
143
+ console.log("Usage: flexipage-delta-github --in <flexipage-delta-out> [...GitHub options]");
144
+ return;
145
+ }
146
+ const inputDir = values.in ?? process.env.FLOW_LENS_OUT_DIR ?? "./flexipage-delta-out";
147
+ const records = readFlexiPageResults(inputDir);
148
+ if (!records.length) return;
149
+ const pr = typeof values.pr === "string" ? values.pr : resolvePrNumber();
150
+ if (!pr) {
151
+ console.error("No pull request number resolved; skipping GitHub comment.");
152
+ return;
153
+ }
154
+ const env = resolveEnv(values, pr);
155
+ const urls = loadArtifactUrls(typeof values["artifact-urls"] === "string" ? values["artifact-urls"] : void 0);
156
+ const fallback = artifactUrl(env);
157
+ const results = records.map((record) => ({ pageName: record.pageName, summary: record.summary, pageChanges: record.pageChanges, artifactUrl: resolveArtifactUrl(record.stem, urls, fallback) }));
158
+ await upsertComment(env, buildFlexiPageComment(results, { marker: env.marker, commitSha: env.commitSha }));
159
+ } catch (error) {
160
+ console.error(error.message);
161
+ process.exitCode = 1;
162
+ }
163
+ }
164
+ function resolvePrNumber() {
165
+ const path = process.env.GITHUB_EVENT_PATH;
166
+ if (!path) return void 0;
167
+ try {
168
+ const event = JSON.parse(readFileSync2(path, "utf8"));
169
+ return typeof event.pull_request?.number === "number" ? String(event.pull_request.number) : void 0;
170
+ } catch {
171
+ return void 0;
172
+ }
173
+ }
174
+ function resolveEnv(values, prNumber) {
175
+ const required = (value, name) => {
176
+ if (typeof value !== "string" || !value) throw new Error(`Missing required value: ${name}`);
177
+ return value;
178
+ };
179
+ const full = required(values.repo ?? process.env.GITHUB_REPOSITORY, "GITHUB_REPOSITORY").split("/");
180
+ if (full.length !== 2) throw new Error("Invalid repo (expected owner/repo)");
181
+ return { apiUrl: typeof (values["api-url"] ?? process.env.GITHUB_API_URL) === "string" ? values["api-url"] ?? process.env.GITHUB_API_URL : "https://api.github.com", owner: full[0], repo: full[1], prNumber, serverUrl: typeof (values["server-url"] ?? process.env.GITHUB_SERVER_URL) === "string" ? values["server-url"] ?? process.env.GITHUB_SERVER_URL : "https://github.com", runId: required(values["run-id"] ?? process.env.GITHUB_RUN_ID, "GITHUB_RUN_ID"), token: required(values.token ?? process.env.GITHUB_TOKEN, "GITHUB_TOKEN"), commitSha: typeof (values["commit-sha"] ?? process.env.GITHUB_SHA) === "string" ? values["commit-sha"] ?? process.env.GITHUB_SHA : void 0, marker: typeof values.marker === "string" ? values.marker : FLEXIPAGE_MARKER };
182
+ }
183
+ async function requestJson(fetchImpl, url, init) {
184
+ const response = await fetchImpl(url, init);
185
+ const body = await response.text();
186
+ if (!response.ok) throw new Error(`${url} failed: ${response.status} ${body}`);
187
+ return body ? JSON.parse(body) : void 0;
188
+ }
189
+ if (isMainModule(import.meta.url)) void main();
190
+ export {
191
+ artifactUrl,
192
+ buildComment,
193
+ isZeroSummary,
194
+ main,
195
+ upsertComment
196
+ };
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/ci/flexipage-gitlab-report.ts
4
+ import { parseArgs } from "node:util";
5
+
6
+ // src/util/is-main-module.ts
7
+ import { realpathSync } from "node:fs";
8
+ import { fileURLToPath } from "node:url";
9
+ function isMainModule(metaUrl, argvPath = process.argv[1]) {
10
+ if (!argvPath) {
11
+ return false;
12
+ }
13
+ try {
14
+ return realpathSync(fileURLToPath(metaUrl)) === realpathSync(argvPath);
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ // src/ci/report-core.ts
21
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
22
+ import { join } from "node:path";
23
+
24
+ // src/render/snapshot-panel.ts
25
+ var COLLECTION_KEY_NAMES = [
26
+ "assignmentItems",
27
+ "choiceReferences",
28
+ "choices",
29
+ "conditions",
30
+ "customErrorMessages",
31
+ "dataTypeMappings",
32
+ "fields",
33
+ "filters",
34
+ "inputAssignments",
35
+ "inputParameters",
36
+ "outputParameters",
37
+ "outputAssignments",
38
+ "processMetadataValues",
39
+ "rules",
40
+ "scheduledPaths",
41
+ "sortOptions",
42
+ "stageSteps",
43
+ "steps",
44
+ "storeOutputParameters",
45
+ "transformValues",
46
+ "valueMappings",
47
+ "waitEvents"
48
+ ];
49
+ var COLLECTION_KEYS = new Set(COLLECTION_KEY_NAMES);
50
+
51
+ // src/util/file-name.ts
52
+ function safeFileName(value) {
53
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
54
+ }
55
+
56
+ // src/ci/report-core.ts
57
+ var FLEXIPAGE_MARKER = "<!-- FlexiPageDelta:report -->";
58
+ function buildProductComment(results, vocabulary, opts = {}) {
59
+ const sorted = [...results].sort((left, right) => vocabulary.sortName(left).localeCompare(vocabulary.sortName(right)));
60
+ const lines = [opts.marker ?? vocabulary.marker, "", vocabulary.heading(sorted.length), ""];
61
+ if (sorted.length === 0) lines.push(vocabulary.empty);
62
+ else lines.push(vocabulary.columns, vocabulary.separator, ...sorted.map(vocabulary.row));
63
+ if (opts.commitSha) lines.push("", `Commit: \`${opts.commitSha.slice(0, 7)}\``);
64
+ return lines.join("\n");
65
+ }
66
+ function isZeroPageSummary(summary) {
67
+ return summary.addedComponents === 0 && summary.removedComponents === 0 && summary.modifiedComponents === 0 && summary.addedRegions === 0 && summary.removedRegions === 0 && summary.modifiedRegions === 0 && summary.changedPageAttributes === 0;
68
+ }
69
+ function readFlexiPageResults(inputDir) {
70
+ return readdirSync(inputDir).filter((file) => file.endsWith(".diff.json")).map((file) => {
71
+ const diff = JSON.parse(readFileSync(join(inputDir, file), "utf8"));
72
+ if (isZeroPageSummary(diff.summary)) return null;
73
+ return { pageName: diff.pageName, summary: diff.summary, pageChanges: diff.pageChanges, stem: safeFileName(diff.pageName) };
74
+ }).filter((value) => value !== null);
75
+ }
76
+ function buildFlexiPageComment(results, opts = {}) {
77
+ return buildProductComment(results, {
78
+ marker: FLEXIPAGE_MARKER,
79
+ heading: (count) => `## \u{1F50D} FlexiPageDelta \u2014 ${count} page(s) changed`,
80
+ empty: "_No FlexiPage changes in this MR._",
81
+ columns: "| Page | Components (+/\u2013/~) | Regions (+/\u2013/~) | Page attributes | Diff |",
82
+ separator: "| --- | ---: | ---: | --- | --- |",
83
+ sortName: (result) => result.pageName,
84
+ row: (result) => {
85
+ const callout = result.pageChanges?.find((change) => change.path === "template");
86
+ const attributes = callout ? `Template: ${formatValue(callout.before)} \u2192 ${formatValue(callout.after)}` : String(result.summary.changedPageAttributes);
87
+ return `| ${escapeTableCell(result.pageName)} | +${result.summary.addedComponents} / \u2212${result.summary.removedComponents} / ~${result.summary.modifiedComponents} | +${result.summary.addedRegions} / \u2212${result.summary.removedRegions} / ~${result.summary.modifiedRegions} | ${escapeTableCell(attributes)} | [Open interactive diff](${result.artifactUrl}) |`;
88
+ }
89
+ }, opts);
90
+ }
91
+ function formatValue(value) {
92
+ return value === void 0 ? "(missing)" : String(value);
93
+ }
94
+ function findStickyNote(notes, marker, authorId) {
95
+ return notes.find((note) => note.body.includes(marker) && (authorId === void 0 || note.author?.id === authorId));
96
+ }
97
+ function normalizePath(value) {
98
+ return value.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/^\/+/, "").replace(/\/+$/, "");
99
+ }
100
+ function escapeTableCell(value) {
101
+ return value.replaceAll("|", "\\|").replaceAll("`", "\\`").replace(/\r?\n/g, "<br>");
102
+ }
103
+
104
+ // src/ci/flexipage-gitlab-report.ts
105
+ var buildComment = buildFlexiPageComment;
106
+ var isZeroSummary = isZeroPageSummary;
107
+ function artifactUrl(env, relPath, stem) {
108
+ const base = env.projectUrl.replace(/\/+$/, "");
109
+ const path = [normalizePath(relPath), `${stem}.html`].filter(Boolean).flatMap((part) => part.split("/").filter(Boolean)).map(encodeURIComponent).join("/");
110
+ return `${base}/-/jobs/${env.jobId}/artifacts/file/${path}`;
111
+ }
112
+ async function upsertComment(env, body, fetchImpl = fetch) {
113
+ const headers = { "PRIVATE-TOKEN": env.token, "Content-Type": "application/json" };
114
+ const user = await requestJson(fetchImpl, `${env.apiUrl}/user`, { headers });
115
+ const notes = await requestJson(fetchImpl, `${env.apiUrl}/projects/${env.projectId}/merge_requests/${env.mergeRequestIid}/notes?per_page=100`, { headers });
116
+ const marker = env.marker ?? FLEXIPAGE_MARKER;
117
+ const sticky = findStickyNote(notes, marker, user.id);
118
+ await requestJson(fetchImpl, `${env.apiUrl}/projects/${env.projectId}/merge_requests/${env.mergeRequestIid}${sticky ? `/notes/${sticky.id}` : "/notes"}`, { method: sticky ? "PUT" : "POST", headers, body: JSON.stringify({ body }) });
119
+ }
120
+ async function main(argv = process.argv.slice(2)) {
121
+ try {
122
+ const values = parseArgs({ args: argv, options: { in: { type: "string" }, "api-url": { type: "string" }, "project-id": { type: "string" }, "mr-iid": { type: "string" }, "project-url": { type: "string" }, "job-id": { type: "string" }, token: { type: "string" }, "commit-sha": { type: "string" }, marker: { type: "string" }, help: { type: "boolean", short: "h" } }, allowPositionals: false }).values;
123
+ if (values.help) {
124
+ console.log("Usage: flexipage-delta-gitlab --in <flexipage-delta-out> [...GitLab options]");
125
+ return;
126
+ }
127
+ const inputDir = values.in ?? process.env.FLOW_LENS_OUT_DIR ?? "./flexipage-delta-out";
128
+ const records = readFlexiPageResults(inputDir);
129
+ if (!records.length) return;
130
+ const env = resolveEnv(values);
131
+ const results = records.map((record) => ({ pageName: record.pageName, summary: record.summary, pageChanges: record.pageChanges, artifactUrl: artifactUrl(env, inputDir, record.stem) }));
132
+ await upsertComment(env, buildFlexiPageComment(results, { marker: env.marker, commitSha: env.commitSha }));
133
+ } catch (error) {
134
+ console.error(error.message);
135
+ process.exitCode = 1;
136
+ }
137
+ }
138
+ function resolveEnv(values) {
139
+ const required = (value, name) => {
140
+ if (typeof value !== "string" || !value) throw new Error(`Missing required value: ${name}`);
141
+ return value;
142
+ };
143
+ const commitSha = values["commit-sha"] ?? process.env.CI_COMMIT_SHA;
144
+ return { apiUrl: required(values["api-url"] ?? process.env.CI_API_V4_URL, "CI_API_V4_URL"), projectId: required(values["project-id"] ?? process.env.CI_PROJECT_ID, "CI_PROJECT_ID"), mergeRequestIid: required(values["mr-iid"] ?? process.env.CI_MERGE_REQUEST_IID, "CI_MERGE_REQUEST_IID"), projectUrl: required(values["project-url"] ?? process.env.CI_PROJECT_URL, "CI_PROJECT_URL"), jobId: required(values["job-id"] ?? process.env.CI_JOB_ID, "CI_JOB_ID"), token: required(values.token ?? process.env.FlowDelta_GITLAB_TOKEN, "FlowDelta_GITLAB_TOKEN"), commitSha: typeof commitSha === "string" ? commitSha : void 0, marker: typeof values.marker === "string" ? values.marker : FLEXIPAGE_MARKER };
145
+ }
146
+ async function requestJson(fetchImpl, url, init) {
147
+ const response = await fetchImpl(url, init);
148
+ const body = await response.text();
149
+ if (!response.ok) throw new Error(`${url} failed: ${response.status} ${body}`);
150
+ return body ? JSON.parse(body) : void 0;
151
+ }
152
+ if (isMainModule(import.meta.url)) void main();
153
+ export {
154
+ artifactUrl,
155
+ buildComment,
156
+ isZeroSummary,
157
+ main,
158
+ upsertComment
159
+ };
@@ -85,25 +85,18 @@ function safeFileName(value) {
85
85
  // src/ci/report-core.ts
86
86
  var DEFAULT_MARKER = "<!-- FlowDelta:report -->";
87
87
  function buildComment(results, opts = {}) {
88
- const marker = opts.marker ?? DEFAULT_MARKER;
89
- const sorted = [...results].sort((left, right) => left.flowName.localeCompare(right.flowName));
90
- const lines = [marker, "", `## \u{1F50D} FlowDelta \u2014 ${sorted.length} flow(s) changed`, ""];
91
- if (sorted.length === 0) {
92
- lines.push("_No Flow changes in this MR._");
93
- } else {
94
- lines.push("| Flow | +nodes | -nodes | ~nodes | +/-edges | Flow | Diff |");
95
- lines.push("| --- | ---: | ---: | ---: | ---: | --- | --- |");
96
- for (const result of sorted) {
88
+ return buildProductComment(results, {
89
+ marker: DEFAULT_MARKER,
90
+ heading: (count) => `## \u{1F50D} FlowDelta \u2014 ${count} flow(s) changed`,
91
+ empty: "_No Flow changes in this MR._",
92
+ columns: "| Flow | +nodes | -nodes | ~nodes | +/-edges | Flow | Diff |",
93
+ separator: "| --- | ---: | ---: | ---: | ---: | --- | --- |",
94
+ sortName: (result) => result.flowName,
95
+ row: (result) => {
97
96
  const flowSummary = escapeTableCell(formatFlowChanges(result.flowChanges));
98
- lines.push(
99
- `| ${escapeTableCell(result.flowName)} | ${result.summary.addedNodes} | ${result.summary.removedNodes} | ${result.summary.modifiedNodes} | ${result.summary.addedEdges} / ${result.summary.removedEdges} | ${flowSummary} | [Open interactive diff](${result.artifactUrl}) |`
100
- );
97
+ return `| ${escapeTableCell(result.flowName)} | ${result.summary.addedNodes} | ${result.summary.removedNodes} | ${result.summary.modifiedNodes} | ${result.summary.addedEdges} / ${result.summary.removedEdges} | ${flowSummary} | [Open interactive diff](${result.artifactUrl}) |`;
101
98
  }
102
- }
103
- if (opts.commitSha) {
104
- lines.push("", `Commit: \`${opts.commitSha.slice(0, 7)}\``);
105
- }
106
- return lines.join("\n");
99
+ }, opts);
107
100
  }
108
101
  function readResults(inputDir) {
109
102
  return readdirSync(inputDir).filter((file) => file.endsWith(".diff.json")).map((file) => {
@@ -147,6 +140,14 @@ function resolveArtifactUrl(stem, urls, fallback) {
147
140
  function isZeroSummary(summary) {
148
141
  return summary.addedNodes === 0 && summary.removedNodes === 0 && summary.modifiedNodes === 0 && summary.addedEdges === 0 && summary.removedEdges === 0 && summary.changedFlowAttributes === 0;
149
142
  }
143
+ function buildProductComment(results, vocabulary, opts = {}) {
144
+ const sorted = [...results].sort((left, right) => vocabulary.sortName(left).localeCompare(vocabulary.sortName(right)));
145
+ const lines = [opts.marker ?? vocabulary.marker, "", vocabulary.heading(sorted.length), ""];
146
+ if (sorted.length === 0) lines.push(vocabulary.empty);
147
+ else lines.push(vocabulary.columns, vocabulary.separator, ...sorted.map(vocabulary.row));
148
+ if (opts.commitSha) lines.push("", `Commit: \`${opts.commitSha.slice(0, 7)}\``);
149
+ return lines.join("\n");
150
+ }
150
151
  function findStickyNote(notes, marker, authorId) {
151
152
  return notes.find((note) => note.body.includes(marker) && (authorId === void 0 || note.author?.id === authorId));
152
153
  }
@@ -84,25 +84,18 @@ function safeFileName(value) {
84
84
  // src/ci/report-core.ts
85
85
  var DEFAULT_MARKER = "<!-- FlowDelta:report -->";
86
86
  function buildComment(results, opts = {}) {
87
- const marker = opts.marker ?? DEFAULT_MARKER;
88
- const sorted = [...results].sort((left, right) => left.flowName.localeCompare(right.flowName));
89
- const lines = [marker, "", `## \u{1F50D} FlowDelta \u2014 ${sorted.length} flow(s) changed`, ""];
90
- if (sorted.length === 0) {
91
- lines.push("_No Flow changes in this MR._");
92
- } else {
93
- lines.push("| Flow | +nodes | -nodes | ~nodes | +/-edges | Flow | Diff |");
94
- lines.push("| --- | ---: | ---: | ---: | ---: | --- | --- |");
95
- for (const result of sorted) {
87
+ return buildProductComment(results, {
88
+ marker: DEFAULT_MARKER,
89
+ heading: (count) => `## \u{1F50D} FlowDelta \u2014 ${count} flow(s) changed`,
90
+ empty: "_No Flow changes in this MR._",
91
+ columns: "| Flow | +nodes | -nodes | ~nodes | +/-edges | Flow | Diff |",
92
+ separator: "| --- | ---: | ---: | ---: | ---: | --- | --- |",
93
+ sortName: (result) => result.flowName,
94
+ row: (result) => {
96
95
  const flowSummary = escapeTableCell(formatFlowChanges(result.flowChanges));
97
- lines.push(
98
- `| ${escapeTableCell(result.flowName)} | ${result.summary.addedNodes} | ${result.summary.removedNodes} | ${result.summary.modifiedNodes} | ${result.summary.addedEdges} / ${result.summary.removedEdges} | ${flowSummary} | [Open interactive diff](${result.artifactUrl}) |`
99
- );
96
+ return `| ${escapeTableCell(result.flowName)} | ${result.summary.addedNodes} | ${result.summary.removedNodes} | ${result.summary.modifiedNodes} | ${result.summary.addedEdges} / ${result.summary.removedEdges} | ${flowSummary} | [Open interactive diff](${result.artifactUrl}) |`;
100
97
  }
101
- }
102
- if (opts.commitSha) {
103
- lines.push("", `Commit: \`${opts.commitSha.slice(0, 7)}\``);
104
- }
105
- return lines.join("\n");
98
+ }, opts);
106
99
  }
107
100
  function readResults(inputDir) {
108
101
  return readdirSync(inputDir).filter((file) => file.endsWith(".diff.json")).map((file) => {
@@ -121,6 +114,14 @@ function readResults(inputDir) {
121
114
  function isZeroSummary(summary) {
122
115
  return summary.addedNodes === 0 && summary.removedNodes === 0 && summary.modifiedNodes === 0 && summary.addedEdges === 0 && summary.removedEdges === 0 && summary.changedFlowAttributes === 0;
123
116
  }
117
+ function buildProductComment(results, vocabulary, opts = {}) {
118
+ const sorted = [...results].sort((left, right) => vocabulary.sortName(left).localeCompare(vocabulary.sortName(right)));
119
+ const lines = [opts.marker ?? vocabulary.marker, "", vocabulary.heading(sorted.length), ""];
120
+ if (sorted.length === 0) lines.push(vocabulary.empty);
121
+ else lines.push(vocabulary.columns, vocabulary.separator, ...sorted.map(vocabulary.row));
122
+ if (opts.commitSha) lines.push("", `Commit: \`${opts.commitSha.slice(0, 7)}\``);
123
+ return lines.join("\n");
124
+ }
124
125
  function findStickyNote(notes, marker, authorId) {
125
126
  return notes.find((note) => note.body.includes(marker) && (authorId === void 0 || note.author?.id === authorId));
126
127
  }
@@ -1,3 +1,12 @@
1
+ interface R2Object {
2
+ body: string;
3
+ writeHttpMetadata(headers: Headers): void;
4
+ }
5
+
6
+ interface R2Bucket {
7
+ get(key: string): Promise<R2Object | null>;
8
+ }
9
+
1
10
  export interface Env {
2
11
  BUCKET: R2Bucket;
3
12
  ARTIFACT_HMAC_KEY?: string;
@@ -29,6 +29,18 @@ jobs:
29
29
  with:
30
30
  name: flow-delta-out
31
31
  path: flow-delta-out
32
+ - run: |
33
+ ./node_modules/.bin/flexipage-delta \
34
+ --repo . \
35
+ --from "${{ github.event.pull_request.base.sha }}" \
36
+ --to "${{ github.sha }}" \
37
+ --path 'force-app/**/*.flexipage-meta.xml' \
38
+ --changed-only \
39
+ --out flexipage-delta-out --json
40
+ - uses: actions/upload-artifact@v4
41
+ with:
42
+ name: flexipage-delta-out
43
+ path: flexipage-delta-out
32
44
  # Optional live-render path for private repos using your own R2 bucket and Worker.
33
45
  # See docs/ci.md for the required secrets, Worker template, and presigned alternative.
34
46
  - name: Publish to R2 + sign
@@ -40,17 +52,23 @@ jobs:
40
52
  R2_BUCKET: ${{ secrets.R2_BUCKET }}
41
53
  ARTIFACT_BASE_URL: ${{ vars.ARTIFACT_BASE_URL }}
42
54
  ARTIFACT_HMAC_KEY: ${{ secrets.ARTIFACT_HMAC_KEY }}
43
- run: node ./node_modules/@syntax-syllogism/flow-delta/scripts/r2-publish.mjs flow-delta-out "$GITHUB_REPOSITORY/${{ github.event.number }}/$GITHUB_SHA" > flow-delta-out/urls.json
55
+ run: |
56
+ node ./node_modules/@syntax-syllogism/flow-delta/scripts/r2-publish.mjs flow-delta-out "$GITHUB_REPOSITORY/${{ github.event.number }}/$GITHUB_SHA" > flow-delta-out/urls.json
57
+ node ./node_modules/@syntax-syllogism/flow-delta/scripts/r2-publish.mjs flexipage-delta-out "$GITHUB_REPOSITORY/${{ github.event.number }}/$GITHUB_SHA" > flexipage-delta-out/urls.json
44
58
  continue-on-error: true
45
59
  - name: Report with live-render links
46
60
  if: ${{ vars.ARTIFACT_BASE_URL != '' }}
47
61
  env:
48
62
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49
- run: ./node_modules/.bin/flow-delta-github --in flow-delta-out --artifact-urls flow-delta-out/urls.json
63
+ run: |
64
+ ./node_modules/.bin/flow-delta-github --in flow-delta-out --artifact-urls flow-delta-out/urls.json
65
+ ./node_modules/.bin/flexipage-delta-github --in flexipage-delta-out --artifact-urls flexipage-delta-out/urls.json
50
66
  continue-on-error: true
51
67
  - name: Report with baseline artifact link
52
68
  if: ${{ vars.ARTIFACT_BASE_URL == '' }}
53
69
  env:
54
70
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55
- run: ./node_modules/.bin/flow-delta-github --in flow-delta-out
71
+ run: |
72
+ ./node_modules/.bin/flow-delta-github --in flow-delta-out
73
+ ./node_modules/.bin/flexipage-delta-github --in flexipage-delta-out
56
74
  continue-on-error: true
@@ -17,7 +17,16 @@ FlowDelta:
17
17
  --changed-only \
18
18
  --out flow-delta-out --json
19
19
  - npx --yes -p @syntax-syllogism/flow-delta@latest flow-delta-gitlab --in flow-delta-out
20
+ - |
21
+ npx --yes -p @syntax-syllogism/flow-delta@latest flexipage-delta \
22
+ --repo . \
23
+ --from "$CI_MERGE_REQUEST_DIFF_BASE_SHA" \
24
+ --to "${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA:-$CI_COMMIT_SHA}" \
25
+ --path 'force-app/**/*.flexipage-meta.xml' \
26
+ --changed-only \
27
+ --out flexipage-delta-out --json
28
+ - npx --yes -p @syntax-syllogism/flow-delta@latest flexipage-delta-gitlab --in flexipage-delta-out
20
29
  artifacts:
21
- paths: [flow-delta-out]
30
+ paths: [flow-delta-out, flexipage-delta-out]
22
31
  expire_in: 30 days
23
32
  allow_failure: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syntax-syllogism/flow-delta",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "Semantic visual diff for Salesforce Flows",
6
6
  "license": "MIT",
@@ -26,18 +26,22 @@
26
26
  },
27
27
  "scripts": {
28
28
  "build": "node scripts/build.mjs",
29
+ "typecheck": "tsc --noEmit && tsc --project tsconfig.test.json --noEmit && tsc --project tsconfig.worker.json --noEmit",
29
30
  "smoke:gitlab": "node --import tsx scripts/smoke-gitlab.ts",
30
31
  "smoke:github": "node --import tsx scripts/smoke-github.ts",
31
- "test": "node --import tsx --test test/parser.test.ts test/semantic-diff.test.ts test/report-core.test.ts test/gitlab-report.test.ts test/github-report.test.ts test/smoke-gitlab.test.ts test/r2-publish.test.ts test/cloudflare-worker.test.ts",
32
+ "test": "node --import tsx --test test/parser.test.ts test/semantic-diff.test.ts test/report-core.test.ts test/gitlab-report.test.ts test/github-report.test.ts test/smoke-gitlab.test.ts test/smoke-github.test.ts test/r2-publish.test.ts test/cloudflare-worker.test.ts test/flexipage-delta.test.ts",
32
33
  "test:parser": "node --import tsx --test test/parser.test.ts",
33
- "prepublishOnly": "npm run build && npm test",
34
+ "prepublishOnly": "npm run typecheck && npm run build && npm test",
34
35
  "render:fixtures": "bash bin/render-fixtures.sh"
35
36
  },
36
37
  "devDependencies": {
37
- "esbuild": "^0.28.0",
38
+ "@cloudflare/workers-types": "^4.20260702.1",
38
39
  "@types/node": "^26.1.0",
39
40
  "@types/xml2js": "^0.4.14",
40
- "tsx": "^4.19.0"
41
+ "esbuild": "^0.28.0",
42
+ "happy-dom": "^20.10.6",
43
+ "tsx": "^4.19.0",
44
+ "typescript": "^5.9.3"
41
45
  },
42
46
  "dependencies": {
43
47
  "elkjs": "^0.11.1",
@@ -46,7 +50,10 @@
46
50
  "bin": {
47
51
  "flow-delta": "dist/cli.js",
48
52
  "flow-delta-gitlab": "dist/gitlab-report.js",
49
- "flow-delta-github": "dist/github-report.js"
53
+ "flow-delta-github": "dist/github-report.js",
54
+ "flexipage-delta": "dist/flexipage-cli.js",
55
+ "flexipage-delta-gitlab": "dist/flexipage-gitlab-report.js",
56
+ "flexipage-delta-github": "dist/flexipage-github-report.js"
50
57
  },
51
58
  "files": [
52
59
  "dist",