@syntax-syllogism/flow-delta 0.5.1 → 0.6.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 +15 -1
- package/dist/cli.js +8 -5
- package/dist/flexipage-cli.js +704 -0
- package/dist/flexipage-github-report.js +196 -0
- package/dist/flexipage-gitlab-report.js +159 -0
- package/dist/github-report.js +18 -17
- package/dist/gitlab-report.js +18 -17
- package/examples/cloudflare-worker/src/index.ts +9 -0
- package/package.json +13 -6
|
@@ -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
|
+
};
|
package/dist/github-report.js
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/gitlab-report.js
CHANGED
|
@@ -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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syntax-syllogism/flow-delta",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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
|
-
"
|
|
38
|
+
"@cloudflare/workers-types": "^4.20260702.1",
|
|
38
39
|
"@types/node": "^26.1.0",
|
|
39
40
|
"@types/xml2js": "^0.4.14",
|
|
40
|
-
"
|
|
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",
|