@clearlydev/nx-archweave 0.3.2 → 0.4.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/bin/review-verdict.js +8 -249
- package/dist/eslint/index.js +28 -321
- package/dist/generators/_chunks/chunk-2WSX5YNB.js +274 -0
- package/dist/generators/_chunks/chunk-34V6HAJT.js +55 -0
- package/dist/generators/_chunks/chunk-3A6RZUCZ.js +114 -0
- package/dist/generators/_chunks/chunk-47T7NOPP.js +30 -0
- package/dist/generators/_chunks/chunk-4FJ7CLEU.js +189 -0
- package/dist/generators/_chunks/chunk-5MI3O4VZ.js +99 -0
- package/dist/generators/_chunks/chunk-6ZTIUBB2.js +139 -0
- package/dist/generators/_chunks/chunk-AIJCJ7JK.js +203 -0
- package/dist/generators/_chunks/chunk-ALSXKTLM.js +1098 -0
- package/dist/generators/_chunks/chunk-AQHV7DJS.js +656 -0
- package/dist/generators/_chunks/chunk-C2VVUWBA.js +386 -0
- package/dist/generators/_chunks/chunk-EXYZLL7A.js +234 -0
- package/dist/generators/_chunks/chunk-FQHFT5IH.js +50 -0
- package/dist/generators/_chunks/chunk-H222FPLK.js +80 -0
- package/dist/generators/_chunks/chunk-IIDOBI42.js +616 -0
- package/dist/generators/_chunks/chunk-L2RZ2LBO.js +132 -0
- package/dist/generators/_chunks/chunk-MQNQJOWA.js +217 -0
- package/dist/generators/_chunks/chunk-QQNDYJTR.js +243 -0
- package/dist/generators/_chunks/chunk-SFXU6IUS.js +0 -0
- package/dist/generators/_chunks/chunk-SNBIVIMI.js +89 -0
- package/dist/generators/_chunks/chunk-TUN34LAQ.js +8 -0
- package/dist/generators/_chunks/chunk-TXAQ2GJC.js +20 -0
- package/dist/generators/_chunks/chunk-U6LAFHSF.js +49 -0
- package/dist/generators/_chunks/chunk-URV6FLUH.js +73 -0
- package/dist/generators/_chunks/chunk-YNMLB4DL.js +25 -0
- package/dist/generators/_chunks/chunk-YY2UESLM.js +282 -0
- package/dist/generators/add-agent-rules/generator.js +11 -842
- package/dist/generators/add-backend/generator.js +11 -906
- package/dist/generators/add-fsd-lite/generator.js +12 -801
- package/dist/generators/add-github-templates/generator.js +13 -116
- package/dist/generators/add-integration/generator.js +15 -743
- package/dist/generators/add-review/generator.js +12 -1231
- package/dist/generators/allow-builds/generator.js +7 -71
- package/dist/generators/doctor/generator.js +106 -2044
- package/dist/generators/entity/generator.js +15 -604
- package/dist/generators/feature/generator.js +15 -604
- package/dist/generators/init/generator.js +68 -3376
- package/dist/generators/install-integration/generator.js +35 -871
- package/dist/generators/page/generator.js +15 -604
- package/dist/generators/record-integration/generator.js +12 -778
- package/dist/generators/shared/generator.js +15 -604
- package/dist/generators/widget/generator.js +15 -604
- package/dist/index.js +5 -663
- package/package.json +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// ../shared/dist/lib/spawnable-command.js
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import { win32 } from "node:path";
|
|
4
|
+
function isFile(path) {
|
|
5
|
+
try {
|
|
6
|
+
return statSync(path).isFile();
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
var DEFAULT_ENV = { platform: process.platform, env: process.env, isFile };
|
|
12
|
+
function envValue(env, name) {
|
|
13
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
14
|
+
return key === void 0 ? void 0 : env[key];
|
|
15
|
+
}
|
|
16
|
+
function resolveWindowsCommand(command, environment) {
|
|
17
|
+
const exts = (envValue(environment.env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
18
|
+
const candidates = (base) => win32.extname(base) !== "" ? [base] : exts.map((ext) => base + ext);
|
|
19
|
+
if (win32.isAbsolute(command) || /[\\/]/.test(command)) {
|
|
20
|
+
return candidates(command).find(environment.isFile);
|
|
21
|
+
}
|
|
22
|
+
const dirs = (envValue(environment.env, "PATH") ?? "").split(";").filter(Boolean);
|
|
23
|
+
for (const dir of dirs) {
|
|
24
|
+
const found = candidates(win32.join(dir, command)).find(environment.isFile);
|
|
25
|
+
if (found !== void 0)
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
var META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
31
|
+
var BIN_SHIM = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i;
|
|
32
|
+
function escapeCmdArgument(arg, doubleEscape = false) {
|
|
33
|
+
let quoted = arg.replace(/(\\*)"/g, '$1$1\\"');
|
|
34
|
+
quoted = quoted.replace(/(\\*)$/, "$1$1");
|
|
35
|
+
const escaped = `"${quoted}"`.replace(META_CHARS, "^$1");
|
|
36
|
+
return doubleEscape ? escaped.replace(META_CHARS, "^$1") : escaped;
|
|
37
|
+
}
|
|
38
|
+
function spawnableCommand(command, args, environment = DEFAULT_ENV) {
|
|
39
|
+
if (environment.platform !== "win32")
|
|
40
|
+
return { command, args: [...args] };
|
|
41
|
+
const resolved = resolveWindowsCommand(command, environment);
|
|
42
|
+
if (resolved === void 0 || !/\.(cmd|bat)$/i.test(resolved))
|
|
43
|
+
return { command, args: [...args] };
|
|
44
|
+
const doubleEscape = BIN_SHIM.test(resolved);
|
|
45
|
+
const line = [resolved.replace(META_CHARS, "^$1"), ...args.map((arg) => escapeCmdArgument(arg, doubleEscape))].join(" ");
|
|
46
|
+
return {
|
|
47
|
+
command: envValue(environment.env, "COMSPEC") ?? "cmd.exe",
|
|
48
|
+
args: ["/d", "/s", "/c", `"${line}"`],
|
|
49
|
+
windowsVerbatimArguments: true
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
spawnableCommand
|
|
55
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/generators/add-review/render.ts
|
|
2
|
+
var DEPENDABOT_PATH = ".github/dependabot.yml";
|
|
3
|
+
var PROVIDER_STATUS_URL = "https://github.com/clearlydev/nx-archweave/blob/main/docs/pr-review.md#provider-status";
|
|
4
|
+
function renderLineDiff(existing, generated) {
|
|
5
|
+
const a = existing.split("\n");
|
|
6
|
+
const b = generated.split("\n");
|
|
7
|
+
const m = a.length;
|
|
8
|
+
const n = b.length;
|
|
9
|
+
const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
10
|
+
for (let i2 = m - 1; i2 >= 0; i2--) {
|
|
11
|
+
for (let j2 = n - 1; j2 >= 0; j2--) {
|
|
12
|
+
lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const out = ["--- existing", "+++ generated"];
|
|
16
|
+
let i = 0;
|
|
17
|
+
let j = 0;
|
|
18
|
+
while (i < m && j < n) {
|
|
19
|
+
if (a[i] === b[j]) {
|
|
20
|
+
out.push(` ${a[i]}`);
|
|
21
|
+
i++;
|
|
22
|
+
j++;
|
|
23
|
+
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
24
|
+
out.push(`- ${a[i]}`);
|
|
25
|
+
i++;
|
|
26
|
+
} else {
|
|
27
|
+
out.push(`+ ${b[j]}`);
|
|
28
|
+
j++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
while (i < m) out.push(`- ${a[i++]}`);
|
|
32
|
+
while (j < n) out.push(`+ ${b[j++]}`);
|
|
33
|
+
return out.join("\n");
|
|
34
|
+
}
|
|
35
|
+
var WORKFLOW_ACTION_LINES = {
|
|
36
|
+
created: "created",
|
|
37
|
+
overwritten: "overwritten with the generated version (per --onConflict=overwrite)",
|
|
38
|
+
unchanged: "unchanged (already matches the generated version)",
|
|
39
|
+
proposed: "NOT written \u2014 differs from the generated version (merge proposal below)",
|
|
40
|
+
skipped: "left as-is (per --onConflict=skip)",
|
|
41
|
+
alternate: "generated alongside the existing file",
|
|
42
|
+
aborted: "NOT written"
|
|
43
|
+
};
|
|
44
|
+
var CONFIG_ACTION_LINES = {
|
|
45
|
+
created: "created with the review field",
|
|
46
|
+
updated: "review field updated",
|
|
47
|
+
unchanged: "review field unchanged",
|
|
48
|
+
"not-updated": "NOT updated \u2014 kept consistent with the workflow file that is actually in effect",
|
|
49
|
+
aborted: "NOT updated"
|
|
50
|
+
};
|
|
51
|
+
function renderAddReviewReport(report) {
|
|
52
|
+
const lines = [];
|
|
53
|
+
if (report.packageManagerNote !== void 0) {
|
|
54
|
+
lines.push(`Note: ${report.packageManagerNote}`);
|
|
55
|
+
}
|
|
56
|
+
if (report.workflowAction === "aborted") {
|
|
57
|
+
lines.push("add-review aborted \u2014 nothing was written.");
|
|
58
|
+
lines.push(` toolkit.config.json: ${report.configError ?? "unusable"}`);
|
|
59
|
+
lines.push(" Fix the config file manually, then rerun the generator.");
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
if (report.dryRun) {
|
|
63
|
+
lines.push("add-review dry-run \u2014 no files were written.");
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push("Generated workflow YAML:");
|
|
66
|
+
lines.push(report.workflowYaml ?? "");
|
|
67
|
+
lines.push("");
|
|
68
|
+
lines.push("toolkit.config.json review field:");
|
|
69
|
+
lines.push(report.configSnippetJson ?? "");
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push("Effective decision:");
|
|
72
|
+
}
|
|
73
|
+
lines.push(` ${report.workflowPath}: ${WORKFLOW_ACTION_LINES[report.workflowAction]}`);
|
|
74
|
+
lines.push(` toolkit.config.json: ${CONFIG_ACTION_LINES[report.configAction]}`);
|
|
75
|
+
if (report.workflowAction === "proposed" && report.diff) {
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push(report.diff);
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push("To resolve, rerun with one of:");
|
|
80
|
+
lines.push(" --onConflict=overwrite replace the existing file with the generated version");
|
|
81
|
+
lines.push(" --onConflict=skip keep the existing file");
|
|
82
|
+
lines.push(" --onConflict=alternate-filename write the generated version alongside it");
|
|
83
|
+
}
|
|
84
|
+
if (report.dependabot === "created") {
|
|
85
|
+
lines.push(` ${DEPENDABOT_PATH}: created \u2014 Dependabot proposes newer pins for the workflow's actions`);
|
|
86
|
+
} else if (report.dependabot === "present") {
|
|
87
|
+
lines.push(` ${DEPENDABOT_PATH}: already updates github-actions`);
|
|
88
|
+
} else if (report.dependabot === "proposed") {
|
|
89
|
+
lines.push(` ${DEPENDABOT_PATH}: NOT changed \u2014 it has no github-actions entry. The workflow pins each action`);
|
|
90
|
+
lines.push(" to a commit SHA; add this entry under `updates:` so those pins get update pull requests:");
|
|
91
|
+
lines.push("");
|
|
92
|
+
lines.push(report.dependabotProposal ?? "");
|
|
93
|
+
}
|
|
94
|
+
if (report.workflowAction === "alternate") {
|
|
95
|
+
lines.push("");
|
|
96
|
+
lines.push(" Review the generated file, then replace the main workflow file with it yourself;");
|
|
97
|
+
lines.push(" the config stays pointing at the workflow that is actually in effect until you do.");
|
|
98
|
+
}
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push(`Note: the review workflow stays green-skipped until you register the ${report.requiredSecretName} secret.`);
|
|
101
|
+
if (report.betaProvider !== void 0) {
|
|
102
|
+
lines.push(`Note: the ${report.betaProvider} provider is in beta \u2014 its workflow hasn't finished a review end to end on`);
|
|
103
|
+
lines.push(`GitHub Actions yet. Status, costs and known issues: ${PROVIDER_STATUS_URL}`);
|
|
104
|
+
}
|
|
105
|
+
lines.push("Note: this workflow runs with elevated permissions only after it is merged to the default branch \u2014");
|
|
106
|
+
lines.push("merging is the first real validation.");
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
DEPENDABOT_PATH,
|
|
112
|
+
renderLineDiff,
|
|
113
|
+
renderAddReviewReport
|
|
114
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TOOLKIT_CONFIG_PATH,
|
|
3
|
+
workspacePackageManager
|
|
4
|
+
} from "./chunk-EXYZLL7A.js";
|
|
5
|
+
import {
|
|
6
|
+
packageManagerCommands
|
|
7
|
+
} from "./chunk-FQHFT5IH.js";
|
|
8
|
+
|
|
9
|
+
// src/generators/shared/run-nx.ts
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
function runNxFor(exists, read) {
|
|
13
|
+
let config = null;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(read(TOOLKIT_CONFIG_PATH) ?? "null");
|
|
16
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) config = parsed;
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
return packageManagerCommands(workspacePackageManager(exists, read, config) ?? "pnpm").runNx;
|
|
20
|
+
}
|
|
21
|
+
function diskRunNx(root) {
|
|
22
|
+
return runNxFor(
|
|
23
|
+
(path) => existsSync(join(root, path)),
|
|
24
|
+
(path) => existsSync(join(root, path)) ? readFileSync(join(root, path), "utf-8") : void 0
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
diskRunNx
|
|
30
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
composeReviewSetup,
|
|
3
|
+
judgeReviewFileConflict
|
|
4
|
+
} from "./chunk-ALSXKTLM.js";
|
|
5
|
+
import {
|
|
6
|
+
DEPENDABOT_PATH,
|
|
7
|
+
renderAddReviewReport,
|
|
8
|
+
renderLineDiff
|
|
9
|
+
} from "./chunk-3A6RZUCZ.js";
|
|
10
|
+
import {
|
|
11
|
+
generatorFilesDir,
|
|
12
|
+
renderTemplateFiles
|
|
13
|
+
} from "./chunk-YNMLB4DL.js";
|
|
14
|
+
import {
|
|
15
|
+
resolveDryRun
|
|
16
|
+
} from "./chunk-TUN34LAQ.js";
|
|
17
|
+
import {
|
|
18
|
+
applyJsonEdits,
|
|
19
|
+
treePackageManager
|
|
20
|
+
} from "./chunk-EXYZLL7A.js";
|
|
21
|
+
|
|
22
|
+
// src/generators/add-review/generator.ts
|
|
23
|
+
var CONFIG_PATH = "toolkit.config.json";
|
|
24
|
+
function deriveAltWorkflowPath(mainWorkflowPath) {
|
|
25
|
+
return mainWorkflowPath.replace(/\.yml$/, ".archweave.yml");
|
|
26
|
+
}
|
|
27
|
+
function readConfig(tree) {
|
|
28
|
+
const raw = tree.read(CONFIG_PATH, "utf-8");
|
|
29
|
+
if (raw === null) return { raw: null, parsed: null, error: null };
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
33
|
+
return { raw, parsed: null, error: `${CONFIG_PATH} must contain a top-level JSON object` };
|
|
34
|
+
}
|
|
35
|
+
return { raw, parsed, error: null };
|
|
36
|
+
} catch {
|
|
37
|
+
return { raw, parsed: null, error: `${CONFIG_PATH} is not valid JSON` };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function resolveLocale(option, config) {
|
|
41
|
+
if (option) return option;
|
|
42
|
+
const fromConfig = config.parsed?.["locale"];
|
|
43
|
+
return fromConfig === "ko" || fromConfig === "en" ? fromConfig : "en";
|
|
44
|
+
}
|
|
45
|
+
function deepEqual(a, b) {
|
|
46
|
+
if (a === b) return true;
|
|
47
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
48
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
49
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, index) => deepEqual(value, b[index]));
|
|
50
|
+
}
|
|
51
|
+
const aKeys = Object.keys(a).sort();
|
|
52
|
+
const bKeys = Object.keys(b).sort();
|
|
53
|
+
if (aKeys.length !== bKeys.length || aKeys.some((key, index) => key !== bKeys[index])) return false;
|
|
54
|
+
return aKeys.every((key) => deepEqual(a[key], b[key]));
|
|
55
|
+
}
|
|
56
|
+
var PROVIDERS = ["claude", "gemini", "gpt", "cursor"];
|
|
57
|
+
var BETA_PROVIDERS = ["gemini", "gpt", "cursor"];
|
|
58
|
+
var MODES = ["fast", "standard", "thorough"];
|
|
59
|
+
function recordedReview(config) {
|
|
60
|
+
const review = config?.["review"];
|
|
61
|
+
if (typeof review !== "object" || review === null || Array.isArray(review)) return {};
|
|
62
|
+
const record = review;
|
|
63
|
+
const pick = (value, allowed) => typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
64
|
+
const rounds = record["maxRounds"];
|
|
65
|
+
return {
|
|
66
|
+
provider: pick(record["provider"], PROVIDERS),
|
|
67
|
+
mode: pick(record["mode"], MODES),
|
|
68
|
+
maxRounds: rounds === 1 || rounds === 2 || rounds === 3 || rounds === 4 || rounds === 5 ? rounds : void 0
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function runAddReview(tree, options) {
|
|
72
|
+
const config = readConfig(tree);
|
|
73
|
+
const detectedPm = treePackageManager(tree, config.parsed);
|
|
74
|
+
const packageManager = options.packageManager ?? detectedPm ?? "pnpm";
|
|
75
|
+
const packageManagerNote = options.packageManager === void 0 && detectedPm === null ? "the package manager could not be detected (no pnpm, yarn, bun or npm lockfile) \u2014 the workflow was rendered for pnpm; re-run with --packageManager=npm|yarn|bun if that is wrong." : void 0;
|
|
76
|
+
const recorded = recordedReview(config.parsed);
|
|
77
|
+
const provider = options.provider ?? recorded.provider ?? "claude";
|
|
78
|
+
const plan = composeReviewSetup({
|
|
79
|
+
enabled: true,
|
|
80
|
+
provider,
|
|
81
|
+
mode: options.mode ?? recorded.mode ?? "standard",
|
|
82
|
+
locale: resolveLocale(options.locale, config),
|
|
83
|
+
maxRounds: options.maxRounds ?? recorded.maxRounds ?? 3,
|
|
84
|
+
packageManager
|
|
85
|
+
});
|
|
86
|
+
if (plan === null) throw new Error("unreachable: add-review always composes with enabled: true");
|
|
87
|
+
if (config.error !== null) {
|
|
88
|
+
return {
|
|
89
|
+
dryRun: options.dryRun ?? false,
|
|
90
|
+
workflowPath: plan.workflowFilePath,
|
|
91
|
+
workflowAction: "aborted",
|
|
92
|
+
configAction: "aborted",
|
|
93
|
+
requiredSecretName: plan.requiredSecretName,
|
|
94
|
+
diff: null,
|
|
95
|
+
workflowYaml: null,
|
|
96
|
+
configSnippetJson: null,
|
|
97
|
+
configError: config.error,
|
|
98
|
+
dependabot: "not-applied",
|
|
99
|
+
dependabotProposal: null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const existing = tree.read(plan.workflowFilePath, "utf-8");
|
|
103
|
+
const verdict = judgeReviewFileConflict(existing, plan.workflowYaml);
|
|
104
|
+
let workflowAction;
|
|
105
|
+
let workflowPath = plan.workflowFilePath;
|
|
106
|
+
let writeWorkflowTo = null;
|
|
107
|
+
let diff = null;
|
|
108
|
+
let mainFileMatchesOptions;
|
|
109
|
+
if (verdict.kind === "create") {
|
|
110
|
+
workflowAction = "created";
|
|
111
|
+
writeWorkflowTo = plan.workflowFilePath;
|
|
112
|
+
mainFileMatchesOptions = true;
|
|
113
|
+
} else if (verdict.kind === "unchanged") {
|
|
114
|
+
workflowAction = "unchanged";
|
|
115
|
+
mainFileMatchesOptions = true;
|
|
116
|
+
} else {
|
|
117
|
+
switch (options.onConflict) {
|
|
118
|
+
case "overwrite":
|
|
119
|
+
workflowAction = "overwritten";
|
|
120
|
+
writeWorkflowTo = plan.workflowFilePath;
|
|
121
|
+
mainFileMatchesOptions = true;
|
|
122
|
+
break;
|
|
123
|
+
case "skip":
|
|
124
|
+
workflowAction = "skipped";
|
|
125
|
+
mainFileMatchesOptions = false;
|
|
126
|
+
break;
|
|
127
|
+
case "alternate-filename": {
|
|
128
|
+
const altWorkflowPath = deriveAltWorkflowPath(plan.workflowFilePath);
|
|
129
|
+
workflowAction = "alternate";
|
|
130
|
+
workflowPath = altWorkflowPath;
|
|
131
|
+
writeWorkflowTo = altWorkflowPath;
|
|
132
|
+
mainFileMatchesOptions = false;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
default:
|
|
136
|
+
workflowAction = "proposed";
|
|
137
|
+
diff = renderLineDiff(verdict.existing, verdict.generated);
|
|
138
|
+
mainFileMatchesOptions = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let configAction;
|
|
142
|
+
let newConfigContent = null;
|
|
143
|
+
if (!mainFileMatchesOptions) {
|
|
144
|
+
configAction = "not-updated";
|
|
145
|
+
} else {
|
|
146
|
+
const existingReview = config.parsed?.["review"];
|
|
147
|
+
if (deepEqual(existingReview, plan.configSnippet)) {
|
|
148
|
+
configAction = "unchanged";
|
|
149
|
+
} else {
|
|
150
|
+
configAction = config.raw === null ? "created" : "updated";
|
|
151
|
+
newConfigContent = applyJsonEdits(config.raw, [[["review"], plan.configSnippet]]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const [dependabotFile] = renderTemplateFiles(generatorFilesDir("add-review", import.meta.url), ".github", {});
|
|
155
|
+
const existingDependabot = tree.read(DEPENDABOT_PATH, "utf-8");
|
|
156
|
+
const dependabot = existingDependabot === null ? "created" : /github-actions/.test(existingDependabot) ? "present" : "proposed";
|
|
157
|
+
const dryRun = options.dryRun ?? false;
|
|
158
|
+
if (!dryRun) {
|
|
159
|
+
if (writeWorkflowTo !== null) tree.write(writeWorkflowTo, plan.workflowYaml);
|
|
160
|
+
if (newConfigContent !== null) tree.write(CONFIG_PATH, newConfigContent);
|
|
161
|
+
if (dependabot === "created") tree.write(DEPENDABOT_PATH, dependabotFile.content);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
dryRun,
|
|
165
|
+
workflowPath,
|
|
166
|
+
workflowAction,
|
|
167
|
+
configAction,
|
|
168
|
+
requiredSecretName: plan.requiredSecretName,
|
|
169
|
+
diff,
|
|
170
|
+
workflowYaml: dryRun ? plan.workflowYaml : null,
|
|
171
|
+
configSnippetJson: dryRun ? JSON.stringify(plan.configSnippet, null, 2) : null,
|
|
172
|
+
configError: null,
|
|
173
|
+
dependabot,
|
|
174
|
+
dependabotProposal: dependabot === "proposed" ? dependabotFile.content : null,
|
|
175
|
+
...packageManagerNote !== void 0 ? { packageManagerNote } : {},
|
|
176
|
+
...BETA_PROVIDERS.includes(provider) ? { betaProvider: provider } : {}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async function addReviewGenerator(tree, options) {
|
|
180
|
+
console.log(renderAddReviewReport(runAddReview(tree, { ...options, dryRun: resolveDryRun(options) })));
|
|
181
|
+
}
|
|
182
|
+
var generator_default = addReviewGenerator;
|
|
183
|
+
|
|
184
|
+
export {
|
|
185
|
+
recordedReview,
|
|
186
|
+
runAddReview,
|
|
187
|
+
addReviewGenerator,
|
|
188
|
+
generator_default
|
|
189
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/eslint/fsd-policy.ts
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
var PRESETS = {
|
|
5
|
+
basic: {
|
|
6
|
+
sliceGroups: { auto: true, include: [], exclude: [] },
|
|
7
|
+
sharedPublicApi: "segment",
|
|
8
|
+
testFiles: false,
|
|
9
|
+
segments: [],
|
|
10
|
+
platformSuffixes: [],
|
|
11
|
+
routerDirs: []
|
|
12
|
+
},
|
|
13
|
+
practical: {
|
|
14
|
+
sliceGroups: { auto: true, include: [], exclude: [] },
|
|
15
|
+
sharedPublicApi: "file",
|
|
16
|
+
testFiles: ["**/*.{spec,test}.*", "**/__tests__/**"],
|
|
17
|
+
segments: [],
|
|
18
|
+
platformSuffixes: [],
|
|
19
|
+
routerDirs: []
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
23
|
+
function resolveFsdPolicy(recorded, overrides = {}) {
|
|
24
|
+
const fsd = typeof recorded === "object" && recorded !== null ? recorded : {};
|
|
25
|
+
const preset = fsd["preset"] === "practical" ? "practical" : "basic";
|
|
26
|
+
const policy = { ...PRESETS[preset] };
|
|
27
|
+
const groups = fsd["sliceGroups"];
|
|
28
|
+
if (groups === false) policy.sliceGroups = false;
|
|
29
|
+
else if (groups === "auto") policy.sliceGroups = { auto: true, include: [], exclude: [] };
|
|
30
|
+
else if (typeof groups === "object" && groups !== null) {
|
|
31
|
+
const value = groups;
|
|
32
|
+
policy.sliceGroups = {
|
|
33
|
+
auto: value["auto"] !== false,
|
|
34
|
+
include: isStringArray(value["include"]) ? value["include"] : [],
|
|
35
|
+
exclude: isStringArray(value["exclude"]) ? value["exclude"] : []
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const shared = fsd["sharedPublicApi"];
|
|
39
|
+
if (shared === "segment" || shared === "component" || shared === "file") policy.sharedPublicApi = shared;
|
|
40
|
+
const tests = fsd["testFiles"];
|
|
41
|
+
if (tests === false || isStringArray(tests)) policy.testFiles = tests;
|
|
42
|
+
if (isStringArray(fsd["segments"])) policy.segments = fsd["segments"];
|
|
43
|
+
if (isStringArray(fsd["platformSuffixes"])) policy.platformSuffixes = fsd["platformSuffixes"];
|
|
44
|
+
if (isStringArray(fsd["routerDirs"])) policy.routerDirs = fsd["routerDirs"];
|
|
45
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
46
|
+
if (value !== void 0) policy[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return policy;
|
|
49
|
+
}
|
|
50
|
+
function readRecordedFsd(from) {
|
|
51
|
+
for (let dir = from; ; dir = dirname(dir)) {
|
|
52
|
+
const file = join(dir, "toolkit.config.json");
|
|
53
|
+
if (existsSync(file)) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(file, "utf-8"))["fsd"];
|
|
56
|
+
} catch {
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (dirname(dir) === dir) return void 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
var escapeLiteral = (text) => text.replace(/[.+^$()|[\]\\{}]/g, "\\$&");
|
|
64
|
+
function wildcardsToRegExp(glob) {
|
|
65
|
+
let source = "";
|
|
66
|
+
for (let i = 0; i < glob.length; i++) {
|
|
67
|
+
const ch = glob[i];
|
|
68
|
+
if (ch === "*" && glob[i + 1] === "*") {
|
|
69
|
+
const slash = glob[i + 2] === "/";
|
|
70
|
+
source += slash ? "(?:.*/)?" : ".*";
|
|
71
|
+
i += slash ? 2 : 1;
|
|
72
|
+
} else if (ch === "*") source += "[^/]*";
|
|
73
|
+
else if (ch === "?") source += "[^/]";
|
|
74
|
+
else source += escapeLiteral(ch);
|
|
75
|
+
}
|
|
76
|
+
return source;
|
|
77
|
+
}
|
|
78
|
+
function globToRegExp(glob) {
|
|
79
|
+
let source = "";
|
|
80
|
+
let rest = glob;
|
|
81
|
+
for (; ; ) {
|
|
82
|
+
const open = rest.indexOf("{");
|
|
83
|
+
const close = open === -1 ? -1 : rest.indexOf("}", open);
|
|
84
|
+
if (open === -1 || close === -1) {
|
|
85
|
+
source += wildcardsToRegExp(rest);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
source += wildcardsToRegExp(rest.slice(0, open));
|
|
89
|
+
source += `(?:${rest.slice(open + 1, close).split(",").map(wildcardsToRegExp).join("|")})`;
|
|
90
|
+
rest = rest.slice(close + 1);
|
|
91
|
+
}
|
|
92
|
+
return new RegExp(`^${source}$`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
resolveFsdPolicy,
|
|
97
|
+
readRecordedFsd,
|
|
98
|
+
globToRegExp
|
|
99
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// ../template-engine/dist/backend/eslint-boundaries.js
|
|
2
|
+
var DEFAULT_BOUNDARY_TAGS = { frontend: "scope:frontend", backend: "scope:backend", shared: "scope:shared" };
|
|
3
|
+
function readBoundaryTags(config) {
|
|
4
|
+
const boundaries = config?.["boundaries"];
|
|
5
|
+
const tags = typeof boundaries === "object" && boundaries !== null ? boundaries["tags"] : void 0;
|
|
6
|
+
const pick = (role) => {
|
|
7
|
+
const value = typeof tags === "object" && tags !== null ? tags[role] : void 0;
|
|
8
|
+
return typeof value === "string" && value.trim() !== "" ? value : DEFAULT_BOUNDARY_TAGS[role];
|
|
9
|
+
};
|
|
10
|
+
return { frontend: pick("frontend"), backend: pick("backend"), shared: pick("shared") };
|
|
11
|
+
}
|
|
12
|
+
function scopeConstraints(tags = DEFAULT_BOUNDARY_TAGS) {
|
|
13
|
+
return [
|
|
14
|
+
{ sourceTag: tags.frontend, notDependOnLibsWithTags: [tags.backend] },
|
|
15
|
+
{ sourceTag: tags.backend, notDependOnLibsWithTags: [tags.frontend] },
|
|
16
|
+
{ sourceTag: tags.shared, notDependOnLibsWithTags: [tags.frontend, tags.backend] }
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
var SCOPE_CONSTRAINTS = scopeConstraints();
|
|
20
|
+
var ESLINT_CONFIG_CANDIDATES = ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"];
|
|
21
|
+
var appliedMarker = (tags) => `sourceTag: '${tags.frontend}'`;
|
|
22
|
+
var ALLOW_ALL_BLOCK = /depConstraints:\s*\[\s*\{\s*sourceTag:\s*'\*',\s*onlyDependOnLibsWithTags:\s*\[\s*'\*'\s*\],?\s*\},?\s*\]/g;
|
|
23
|
+
function maskComments(text) {
|
|
24
|
+
const out = text.split("");
|
|
25
|
+
let mode = "code";
|
|
26
|
+
let quote = "";
|
|
27
|
+
for (let i = 0; i < text.length; i++) {
|
|
28
|
+
const c = text[i];
|
|
29
|
+
const d = text[i + 1];
|
|
30
|
+
if (mode === "code") {
|
|
31
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
32
|
+
mode = "string";
|
|
33
|
+
quote = c;
|
|
34
|
+
} else if (c === "/" && d === "/") {
|
|
35
|
+
mode = "line";
|
|
36
|
+
out[i] = " ";
|
|
37
|
+
} else if (c === "/" && d === "*") {
|
|
38
|
+
mode = "block";
|
|
39
|
+
out[i] = " ";
|
|
40
|
+
}
|
|
41
|
+
} else if (mode === "string") {
|
|
42
|
+
if (c === "\\") {
|
|
43
|
+
i++;
|
|
44
|
+
} else if (c === quote) {
|
|
45
|
+
mode = "code";
|
|
46
|
+
}
|
|
47
|
+
} else if (mode === "line") {
|
|
48
|
+
if (c === "\n") {
|
|
49
|
+
mode = "code";
|
|
50
|
+
} else {
|
|
51
|
+
out[i] = " ";
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
if (c === "*" && d === "/") {
|
|
55
|
+
out[i] = " ";
|
|
56
|
+
out[i + 1] = " ";
|
|
57
|
+
i++;
|
|
58
|
+
mode = "code";
|
|
59
|
+
} else if (c !== "\n") {
|
|
60
|
+
out[i] = " ";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out.join("");
|
|
65
|
+
}
|
|
66
|
+
function findAllowAllMatches(masked) {
|
|
67
|
+
const matches = [];
|
|
68
|
+
ALLOW_ALL_BLOCK.lastIndex = 0;
|
|
69
|
+
for (let m = ALLOW_ALL_BLOCK.exec(masked); m !== null; m = ALLOW_ALL_BLOCK.exec(masked)) {
|
|
70
|
+
matches.push({ index: m.index, length: m[0].length });
|
|
71
|
+
}
|
|
72
|
+
return matches;
|
|
73
|
+
}
|
|
74
|
+
function classifyBoundaryConstraints(configText, tags = DEFAULT_BOUNDARY_TAGS) {
|
|
75
|
+
const masked = maskComments(configText);
|
|
76
|
+
if (masked.includes(appliedMarker(tags)))
|
|
77
|
+
return "enforced";
|
|
78
|
+
if (findAllowAllMatches(masked).length > 0)
|
|
79
|
+
return "allow-all";
|
|
80
|
+
return "unknown";
|
|
81
|
+
}
|
|
82
|
+
function renderConstraintsBlock(indent, eol, tags) {
|
|
83
|
+
const renderList = (list) => list.map((t) => `'${t}'`).join(", ");
|
|
84
|
+
const inner = scopeConstraints(tags).map((c) => {
|
|
85
|
+
const field = c.notDependOnLibsWithTags ? `notDependOnLibsWithTags: [${renderList(c.notDependOnLibsWithTags)}]` : `onlyDependOnLibsWithTags: [${renderList(c.onlyDependOnLibsWithTags ?? [])}]`;
|
|
86
|
+
return [`${indent} {`, `${indent} sourceTag: '${c.sourceTag}',`, `${indent} ${field},`, `${indent} },`].join(eol);
|
|
87
|
+
}).join(eol);
|
|
88
|
+
return [
|
|
89
|
+
`// nx-archweave boundaries \u2014 the frontend and the backend may never import each other`,
|
|
90
|
+
`${indent}depConstraints: [`,
|
|
91
|
+
inner,
|
|
92
|
+
[
|
|
93
|
+
`${indent} // baseline: everything else stays allowed (constraints apply conjunctively)`,
|
|
94
|
+
`${indent} {`,
|
|
95
|
+
`${indent} sourceTag: '*',`,
|
|
96
|
+
`${indent} onlyDependOnLibsWithTags: ['*'],`,
|
|
97
|
+
`${indent} },`
|
|
98
|
+
].join(eol),
|
|
99
|
+
`${indent}]`
|
|
100
|
+
].join(eol);
|
|
101
|
+
}
|
|
102
|
+
function applyBoundaryConstraints(configText, tags = DEFAULT_BOUNDARY_TAGS) {
|
|
103
|
+
const classification = classifyBoundaryConstraints(configText, tags);
|
|
104
|
+
if (classification === "enforced")
|
|
105
|
+
return { status: "already-applied" };
|
|
106
|
+
if (classification === "unknown")
|
|
107
|
+
return { status: "unrecognized" };
|
|
108
|
+
const masked = maskComments(configText);
|
|
109
|
+
const matches = findAllowAllMatches(masked);
|
|
110
|
+
if (matches.length !== 1) {
|
|
111
|
+
return { status: "unrecognized" };
|
|
112
|
+
}
|
|
113
|
+
const [match] = matches;
|
|
114
|
+
const eol = configText.includes("\r\n") ? "\r\n" : "\n";
|
|
115
|
+
const lineStart = configText.lastIndexOf("\n", match.index) + 1;
|
|
116
|
+
const indent = /^[ \t]*/.exec(configText.slice(lineStart, match.index))?.[0] ?? "";
|
|
117
|
+
const replacement = renderConstraintsBlock(indent, eol, tags);
|
|
118
|
+
const text = configText.slice(0, match.index) + replacement + configText.slice(match.index + match.length);
|
|
119
|
+
return { status: "applied", text };
|
|
120
|
+
}
|
|
121
|
+
function buildBoundarySnippet(tags = DEFAULT_BOUNDARY_TAGS) {
|
|
122
|
+
return [
|
|
123
|
+
"// nx-archweave project boundaries \u2014 merge these entries into the depConstraints",
|
|
124
|
+
"// array of the '@nx/enforce-module-boundaries' rule in your root ESLint config.",
|
|
125
|
+
"// They ride on top of your existing entries: constraints apply conjunctively,",
|
|
126
|
+
"// so the allow-all baseline can stay.",
|
|
127
|
+
renderConstraintsBlock("", "\n", tags)
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
DEFAULT_BOUNDARY_TAGS,
|
|
133
|
+
readBoundaryTags,
|
|
134
|
+
ESLINT_CONFIG_CANDIDATES,
|
|
135
|
+
maskComments,
|
|
136
|
+
classifyBoundaryConstraints,
|
|
137
|
+
applyBoundaryConstraints,
|
|
138
|
+
buildBoundarySnippet
|
|
139
|
+
};
|