@clearlydev/nx-archweave 0.1.0 → 0.2.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 +2 -2
- package/dist/eslint/fsd-boundaries.d.ts +27 -10
- package/dist/eslint/fsd-policy.d.ts +38 -0
- package/dist/eslint/index.d.ts +10 -9
- package/dist/eslint/index.js +227 -34
- package/dist/eslint/slice-groups.d.ts +12 -0
- package/dist/generators/add-agent-rules/generator.js +1175 -23
- package/dist/generators/add-backend/generator.js +1127 -75
- package/dist/generators/add-fsd-lite/generator.js +1274 -96
- package/dist/generators/add-integration/generator.js +4 -35
- package/dist/generators/add-review/files/dependabot.yml +7 -0
- package/dist/generators/add-review/generator.js +1100 -46
- package/dist/generators/doctor/generator.js +784 -131
- package/dist/generators/entity/generator.js +251 -32
- package/dist/generators/feature/generator.js +251 -32
- package/dist/generators/init/generator.js +1955 -549
- package/dist/generators/install-integration/generator.js +5 -37
- package/dist/generators/page/generator.js +251 -32
- package/dist/generators/record-integration/generator.js +5 -36
- package/dist/generators/shared/generator.js +251 -32
- package/dist/generators/widget/generator.js +251 -32
- package/dist/index.js +4 -35
- package/package.json +5 -3
- package/src/generators/add-agent-rules/schema.json +5 -0
- package/src/generators/add-fsd-lite/schema.json +2 -2
- package/src/generators/add-integration/schema.json +1 -1
- package/src/generators/entity/schema.json +11 -1
- package/src/generators/feature/schema.json +11 -1
- package/src/generators/init/schema.json +13 -2
- package/src/generators/page/schema.json +15 -3
- package/src/generators/shared/schema.json +7 -1
- package/src/generators/widget/schema.json +11 -1
- package/dist/eslint/fsd-boundaries.js +0 -144
- package/dist/eslint/tsconfig-aliases.js +0 -114
- package/dist/generators/add-agent-rules/render.js +0 -46
- package/dist/generators/add-backend/render.js +0 -102
- package/dist/generators/add-fsd-lite/render.js +0 -72
- package/dist/generators/add-github-templates/render.js +0 -26
- package/dist/generators/add-integration/render.js +0 -34
- package/dist/generators/add-review/render.js +0 -96
- package/dist/generators/allow-builds/render.js +0 -82
- package/dist/generators/doctor/integration-report.js +0 -70
- package/dist/generators/doctor/render.js +0 -10
- package/dist/generators/init/render.js +0 -125
- package/dist/generators/init/validation.js +0 -74
- package/dist/generators/install-integration/render.js +0 -128
- package/dist/generators/record-integration/render.js +0 -31
- package/dist/generators/shared/dry-run.js +0 -12
- package/dist/generators/shared/frontend-app.js +0 -57
- package/dist/generators/shared/project-type.js +0 -39
- package/dist/generators/shared/record-pipeline.js +0 -34
- package/dist/generators/shared/run-nx.js +0 -24
- package/dist/generators/shared/template-files.js +0 -38
- package/dist/generators/shared/toolkit-config-file.js +0 -110
- package/dist/generators/slice/core.js +0 -185
- package/dist/generators/slice/render.js +0 -70
- package/dist/lib/nx-plugin.js +0 -7
- package/dist/runtime/integrations-composition-root.js +0 -20
- package/dist/runtime/node-command-runner.js +0 -81
|
@@ -169,7 +169,7 @@ import { existsSync as existsSync6 } from "node:fs";
|
|
|
169
169
|
import { join as join6 } from "node:path";
|
|
170
170
|
|
|
171
171
|
// ../template-engine/dist/doctor/review-checks.js
|
|
172
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
172
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
173
173
|
import { join as join2 } from "node:path";
|
|
174
174
|
|
|
175
175
|
// ../review-policy/dist/modes/review-modes.js
|
|
@@ -291,6 +291,20 @@ function buildOverEngineeringPrompt(locale) {
|
|
|
291
291
|
// ../review-policy/dist/verdict-gate/verdict-gate.js
|
|
292
292
|
var ROUND_MARKER = "<!-- archweave-review-round -->";
|
|
293
293
|
|
|
294
|
+
// ../template-engine/dist/review/action-pins.js
|
|
295
|
+
var ACTION_PINS = {
|
|
296
|
+
"actions/checkout": "actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0",
|
|
297
|
+
"actions/setup-node": "actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0",
|
|
298
|
+
"pnpm/action-setup": "pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0",
|
|
299
|
+
"oven-sh/setup-bun": "oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0",
|
|
300
|
+
"anthropics/claude-code-action": "anthropics/claude-code-action@6b6870e649b5ff408753befeaee5fc7989d4e6a9 # v1.0.258",
|
|
301
|
+
"google-github-actions/run-gemini-cli": "google-github-actions/run-gemini-cli@f77273f4c914e4bf38440cf36a0369cb64a37489 # v0.1.22",
|
|
302
|
+
"openai/codex-action": "openai/codex-action@bdf19a4a223ec2549a3e2274a0cf61556bc07675 # v1.13"
|
|
303
|
+
};
|
|
304
|
+
function pinnedUses(action) {
|
|
305
|
+
return ACTION_PINS[action];
|
|
306
|
+
}
|
|
307
|
+
|
|
294
308
|
// ../template-engine/dist/review/workflow-skeleton.js
|
|
295
309
|
function indentBlock(text, spaces) {
|
|
296
310
|
const pad = " ".repeat(spaces);
|
|
@@ -309,6 +323,17 @@ function renderConcurrencyBlock() {
|
|
|
309
323
|
function renderPermissionsBlock() {
|
|
310
324
|
return ["permissions:", " contents: read", " pull-requests: write", " id-token: write"].join("\n");
|
|
311
325
|
}
|
|
326
|
+
function renderCheckoutStep(gateCondition, extraWith = []) {
|
|
327
|
+
return [
|
|
328
|
+
"- name: Checkout repository",
|
|
329
|
+
` if: ${gateCondition}`,
|
|
330
|
+
` uses: ${pinnedUses("actions/checkout")}`,
|
|
331
|
+
" with:",
|
|
332
|
+
...extraWith.map((line) => ` ${line}`),
|
|
333
|
+
" fetch-depth: 1",
|
|
334
|
+
" persist-credentials: false"
|
|
335
|
+
].join("\n");
|
|
336
|
+
}
|
|
312
337
|
function renderSecretGateStep(secretName) {
|
|
313
338
|
return [
|
|
314
339
|
"- name: Check review secret",
|
|
@@ -332,7 +357,7 @@ function renderRoundGateStep(maxRounds) {
|
|
|
332
357
|
" env:",
|
|
333
358
|
" GH_TOKEN: ${{ github.token }}",
|
|
334
359
|
" run: |",
|
|
335
|
-
` counts=$(gh api "repos/\${{ github.repository }}/issues/\${{ github.event.pull_request.number }}/comments" --paginate --jq '[([.[] | select(.body | contains("${ROUND_MARKER}"))] | length), ([.[].id] | max // 0)]' | jq -s -r '[(map(.[0]) | add // 0), (map(.[1]) | max // 0)] | @tsv')`,
|
|
360
|
+
` counts=$(gh api "repos/\${{ github.repository }}/issues/\${{ github.event.pull_request.number }}/comments" --paginate --jq '[([.[] | select(.user.type == "Bot") | select(.body | contains("${ROUND_MARKER}"))] | length), ([.[].id] | max // 0)]' | jq -s -r '[(map(.[0]) | add // 0), (map(.[1]) | max // 0)] | @tsv')`,
|
|
336
361
|
` rounds=$(printf '%s' "$counts" | cut -f1)`,
|
|
337
362
|
` echo "last-comment-id=$(printf '%s' "$counts" | cut -f2)" >> "$GITHUB_OUTPUT"`,
|
|
338
363
|
` if [ "$rounds" -ge ${maxRounds} ]; then`,
|
|
@@ -364,8 +389,7 @@ function renderVerdictGateStep(maxRounds, locale) {
|
|
|
364
389
|
].join("\n");
|
|
365
390
|
}
|
|
366
391
|
var SETUP_NODE_VERSION = "22";
|
|
367
|
-
var
|
|
368
|
-
var SETUP_BUN_ACTION = "oven-sh/setup-bun@v2";
|
|
392
|
+
var FALLBACK_PNPM_VERSION = "10";
|
|
369
393
|
var INSTALL_LINES = {
|
|
370
394
|
pnpm: [" pnpm install --frozen-lockfile"],
|
|
371
395
|
npm: [" npm ci"],
|
|
@@ -375,19 +399,39 @@ var INSTALL_LINES = {
|
|
|
375
399
|
],
|
|
376
400
|
bun: [" bun install --frozen-lockfile"]
|
|
377
401
|
};
|
|
402
|
+
function renderPnpmVersionStep(gate) {
|
|
403
|
+
return [
|
|
404
|
+
"- name: Detect pnpm version",
|
|
405
|
+
" id: pnpm-version",
|
|
406
|
+
gate,
|
|
407
|
+
" run: |",
|
|
408
|
+
` if [ -n "$(jq -r '.packageManager // empty' package.json)" ]; then`,
|
|
409
|
+
' echo "version=" >> "$GITHUB_OUTPUT"',
|
|
410
|
+
" else",
|
|
411
|
+
` echo "version=${FALLBACK_PNPM_VERSION}" >> "$GITHUB_OUTPUT"`,
|
|
412
|
+
" fi"
|
|
413
|
+
].join("\n");
|
|
414
|
+
}
|
|
378
415
|
function renderWorkspaceSetupSteps(packageManager) {
|
|
379
416
|
const gate = " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
|
|
380
417
|
const steps = [];
|
|
381
418
|
if (packageManager === "pnpm") {
|
|
382
|
-
steps.push(
|
|
419
|
+
steps.push(renderPnpmVersionStep(gate));
|
|
420
|
+
steps.push([
|
|
421
|
+
"- name: Set up pnpm",
|
|
422
|
+
gate,
|
|
423
|
+
` uses: ${pinnedUses("pnpm/action-setup")}`,
|
|
424
|
+
" with:",
|
|
425
|
+
" version: ${{ steps.pnpm-version.outputs.version }}"
|
|
426
|
+
].join("\n"));
|
|
383
427
|
}
|
|
384
428
|
if (packageManager === "bun") {
|
|
385
|
-
steps.push(["- name: Set up bun", gate, ` uses: ${
|
|
429
|
+
steps.push(["- name: Set up bun", gate, ` uses: ${pinnedUses("oven-sh/setup-bun")}`].join("\n"));
|
|
386
430
|
}
|
|
387
431
|
steps.push([
|
|
388
432
|
"- name: Set up Node",
|
|
389
433
|
gate,
|
|
390
|
-
|
|
434
|
+
` uses: ${pinnedUses("actions/setup-node")}`,
|
|
391
435
|
" with:",
|
|
392
436
|
` node-version: ${SETUP_NODE_VERSION}`,
|
|
393
437
|
// yarn: no setup-node cache — it would query the runner's global yarn 1 even in a
|
|
@@ -439,17 +483,11 @@ function buildWorkflowYaml(input) {
|
|
|
439
483
|
const steps = [
|
|
440
484
|
renderSecretGateStep(SECRET_NAME),
|
|
441
485
|
renderRoundGateStep(input.maxRounds),
|
|
442
|
-
|
|
443
|
-
"- name: Checkout repository",
|
|
444
|
-
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
445
|
-
" uses: actions/checkout@v4",
|
|
446
|
-
" with:",
|
|
447
|
-
" fetch-depth: 1"
|
|
448
|
-
].join("\n"),
|
|
486
|
+
renderCheckoutStep("steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'"),
|
|
449
487
|
[
|
|
450
488
|
"- name: Run Claude review",
|
|
451
489
|
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
452
|
-
|
|
490
|
+
` uses: ${pinnedUses("anthropics/claude-code-action")}`,
|
|
453
491
|
" with:",
|
|
454
492
|
` claude_code_oauth_token: \${{ secrets.${SECRET_NAME} }}`,
|
|
455
493
|
" prompt: |",
|
|
@@ -519,13 +557,7 @@ function buildWorkflowYaml2(input) {
|
|
|
519
557
|
const steps = [
|
|
520
558
|
renderSecretGateStep(SECRET_NAME2),
|
|
521
559
|
renderRoundGateStep(input.maxRounds),
|
|
522
|
-
|
|
523
|
-
"- name: Checkout repository",
|
|
524
|
-
` if: ${gateCondition}`,
|
|
525
|
-
" uses: actions/checkout@v4",
|
|
526
|
-
" with:",
|
|
527
|
-
" fetch-depth: 1"
|
|
528
|
-
].join("\n"),
|
|
560
|
+
renderCheckoutStep(gateCondition),
|
|
529
561
|
[
|
|
530
562
|
"- name: Install cursor-agent CLI",
|
|
531
563
|
` if: ${gateCondition}`,
|
|
@@ -605,17 +637,11 @@ function buildWorkflowYaml3(input) {
|
|
|
605
637
|
const steps = [
|
|
606
638
|
renderSecretGateStep(SECRET_NAME3),
|
|
607
639
|
renderRoundGateStep(input.maxRounds),
|
|
608
|
-
|
|
609
|
-
"- name: Checkout repository",
|
|
610
|
-
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
611
|
-
" uses: actions/checkout@v4",
|
|
612
|
-
" with:",
|
|
613
|
-
" fetch-depth: 1"
|
|
614
|
-
].join("\n"),
|
|
640
|
+
renderCheckoutStep("steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'"),
|
|
615
641
|
[
|
|
616
642
|
"- name: Run Gemini review",
|
|
617
643
|
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
618
|
-
|
|
644
|
+
` uses: ${pinnedUses("google-github-actions/run-gemini-cli")}`,
|
|
619
645
|
" env:",
|
|
620
646
|
" GITHUB_TOKEN: ${{ github.token }}",
|
|
621
647
|
" with:",
|
|
@@ -684,15 +710,7 @@ function buildWorkflowYaml4(input) {
|
|
|
684
710
|
const steps = [
|
|
685
711
|
renderSecretGateStep(SECRET_NAME4),
|
|
686
712
|
renderRoundGateStep(input.maxRounds),
|
|
687
|
-
[
|
|
688
|
-
"- name: Checkout repository",
|
|
689
|
-
` if: ${gateCondition}`,
|
|
690
|
-
" uses: actions/checkout@v4",
|
|
691
|
-
" with:",
|
|
692
|
-
" ref: refs/pull/${{ github.event.pull_request.number }}/merge",
|
|
693
|
-
" fetch-depth: 1",
|
|
694
|
-
" persist-credentials: false"
|
|
695
|
-
].join("\n"),
|
|
713
|
+
renderCheckoutStep(gateCondition, ["ref: refs/pull/${{ github.event.pull_request.number }}/merge"]),
|
|
696
714
|
[
|
|
697
715
|
"- name: Fetch base and head refs for the PR",
|
|
698
716
|
` if: ${gateCondition}`,
|
|
@@ -710,7 +728,7 @@ function buildWorkflowYaml4(input) {
|
|
|
710
728
|
"- name: Run Codex review",
|
|
711
729
|
" id: codex-review",
|
|
712
730
|
` if: ${gateCondition}`,
|
|
713
|
-
|
|
731
|
+
` uses: ${pinnedUses("openai/codex-action")}`,
|
|
714
732
|
" with:",
|
|
715
733
|
` openai-api-key: \${{ secrets.${SECRET_NAME4} }}`,
|
|
716
734
|
' permission-profile: ":read-only"',
|
|
@@ -830,27 +848,88 @@ function validateReviewConfig(review) {
|
|
|
830
848
|
var REVIEW_CHECK_IDS = [
|
|
831
849
|
"review-workflow-present",
|
|
832
850
|
"review-alt-file-coexistence",
|
|
833
|
-
"review-config-consistency"
|
|
851
|
+
"review-config-consistency",
|
|
852
|
+
"review-workflow-providers",
|
|
853
|
+
"review-workflow-pnpm"
|
|
834
854
|
];
|
|
855
|
+
var WORKFLOWS_DIR = ".github/workflows";
|
|
856
|
+
function archweaveWorkflows(root) {
|
|
857
|
+
let names;
|
|
858
|
+
try {
|
|
859
|
+
names = readdirSync(join2(root, WORKFLOWS_DIR));
|
|
860
|
+
} catch {
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
return names.filter((name) => /\.ya?ml$/.test(name)).map((name) => `${WORKFLOWS_DIR}/${name}`).filter((path) => readFileSync2(join2(root, path), "utf-8").includes(ROUND_MARKER)).sort();
|
|
864
|
+
}
|
|
865
|
+
function providerOfWorkflow(path) {
|
|
866
|
+
const main = path.replace(/\.archweave\.yml$/, ".yml");
|
|
867
|
+
return Object.values(REVIEW_PROVIDERS).find((provider) => provider.workflowFileName === main)?.id ?? null;
|
|
868
|
+
}
|
|
869
|
+
function buildReviewProvidersCheck(workflows) {
|
|
870
|
+
const id = "review-workflow-providers";
|
|
871
|
+
const label = "One review provider";
|
|
872
|
+
const providers = new Set(workflows.map(providerOfWorkflow).filter((provider) => provider !== null));
|
|
873
|
+
if (workflows.length === 0)
|
|
874
|
+
return createDoctorCheck({ id, label, status: "not-applicable", message: "no nx-archweave review workflow", evidence: [] });
|
|
875
|
+
if (providers.size <= 1)
|
|
876
|
+
return createDoctorCheck({ id, label, status: "pass", message: "one provider's review workflow", evidence: workflows });
|
|
877
|
+
return createDoctorCheck({
|
|
878
|
+
id,
|
|
879
|
+
label,
|
|
880
|
+
status: "warning",
|
|
881
|
+
message: `review workflows of ${providers.size} providers (${[...providers].join(", ")}) \u2014 each runs on every pull request`,
|
|
882
|
+
evidence: workflows,
|
|
883
|
+
suggestedAction: "Delete the workflow of the provider you no longer use."
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
function buildReviewPnpmCheck(root, workflows) {
|
|
887
|
+
const id = "review-workflow-pnpm";
|
|
888
|
+
const label = "Review workflow pnpm setup";
|
|
889
|
+
const fixed = workflows.filter((path) => /pnpm\/action-setup@[^\n]*\n\s*with:\s*\n\s*version:\s*['"]?\d/.test(readFileSync2(join2(root, path), "utf-8")));
|
|
890
|
+
if (fixed.length === 0)
|
|
891
|
+
return createDoctorCheck({ id, label, status: "not-applicable", message: "no review workflow pins a pnpm version", evidence: [] });
|
|
892
|
+
let packageManager;
|
|
893
|
+
try {
|
|
894
|
+
packageManager = JSON.parse(readFileSync2(join2(root, "package.json"), "utf-8"))["packageManager"];
|
|
895
|
+
} catch {
|
|
896
|
+
packageManager = void 0;
|
|
897
|
+
}
|
|
898
|
+
if (typeof packageManager !== "string") {
|
|
899
|
+
return createDoctorCheck({ id, label, status: "pass", message: "the workflow pins pnpm and package.json has no packageManager field", evidence: fixed });
|
|
900
|
+
}
|
|
901
|
+
return createDoctorCheck({
|
|
902
|
+
id,
|
|
903
|
+
label,
|
|
904
|
+
status: "warning",
|
|
905
|
+
message: `the review workflow pins a pnpm version while package.json says "${packageManager}" \u2014 pnpm/action-setup fails on the mismatch`,
|
|
906
|
+
evidence: fixed,
|
|
907
|
+
suggestedAction: "Regenerate the workflow: nx g @clearlydev/nx-archweave:add-review --onConflict=overwrite."
|
|
908
|
+
});
|
|
909
|
+
}
|
|
835
910
|
function buildReviewChecks(root) {
|
|
836
911
|
const config = readToolkitConfigState(root);
|
|
837
912
|
const mainWorkflowPath = REVIEW_PROVIDERS[resolveProviderId(config)].workflowFileName;
|
|
838
913
|
const altWorkflowPath = deriveAltWorkflowPath(mainWorkflowPath);
|
|
839
914
|
const mainExists = existsSync2(join2(root, mainWorkflowPath));
|
|
840
915
|
const altExists = existsSync2(join2(root, altWorkflowPath));
|
|
916
|
+
const workflows = archweaveWorkflows(root);
|
|
917
|
+
const mainIsOurs = workflows.includes(mainWorkflowPath);
|
|
841
918
|
return [
|
|
842
|
-
buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, config),
|
|
919
|
+
buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, mainIsOurs, config),
|
|
843
920
|
buildReviewAltFileCoexistenceCheck(mainWorkflowPath, altWorkflowPath, mainExists, altExists),
|
|
844
|
-
buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, config)
|
|
921
|
+
buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, config),
|
|
922
|
+
buildReviewProvidersCheck(workflows),
|
|
923
|
+
buildReviewPnpmCheck(root, workflows)
|
|
845
924
|
];
|
|
846
925
|
}
|
|
847
|
-
function buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, config) {
|
|
926
|
+
function buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, mainIsOurs, config) {
|
|
848
927
|
if (mainExists) {
|
|
849
928
|
return createDoctorCheck({
|
|
850
929
|
id: "review-workflow-present",
|
|
851
930
|
label: "Review workflow installed",
|
|
852
931
|
status: "pass",
|
|
853
|
-
message: `${mainWorkflowPath} found`,
|
|
932
|
+
message: mainIsOurs ? `${mainWorkflowPath} found (generated by nx-archweave)` : `${mainWorkflowPath} found, but not generated by nx-archweave (it has no review round marker) \u2014 doctor reports on it, add-review proposes changes to it but never overwrites it`,
|
|
854
933
|
evidence: [mainWorkflowPath]
|
|
855
934
|
});
|
|
856
935
|
}
|
|
@@ -970,12 +1049,30 @@ function buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, c
|
|
|
970
1049
|
}
|
|
971
1050
|
|
|
972
1051
|
// ../template-engine/dist/doctor/backend-checks.js
|
|
973
|
-
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
1052
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
974
1053
|
import { join as join3 } from "node:path";
|
|
975
1054
|
|
|
976
1055
|
// ../template-engine/dist/backend/eslint-boundaries.js
|
|
1056
|
+
var DEFAULT_BOUNDARY_TAGS = { frontend: "scope:frontend", backend: "scope:backend", shared: "scope:shared" };
|
|
1057
|
+
function readBoundaryTags(config) {
|
|
1058
|
+
const boundaries = config?.["boundaries"];
|
|
1059
|
+
const tags = typeof boundaries === "object" && boundaries !== null ? boundaries["tags"] : void 0;
|
|
1060
|
+
const pick = (role) => {
|
|
1061
|
+
const value = typeof tags === "object" && tags !== null ? tags[role] : void 0;
|
|
1062
|
+
return typeof value === "string" && value.trim() !== "" ? value : DEFAULT_BOUNDARY_TAGS[role];
|
|
1063
|
+
};
|
|
1064
|
+
return { frontend: pick("frontend"), backend: pick("backend"), shared: pick("shared") };
|
|
1065
|
+
}
|
|
1066
|
+
function scopeConstraints(tags = DEFAULT_BOUNDARY_TAGS) {
|
|
1067
|
+
return [
|
|
1068
|
+
{ sourceTag: tags.frontend, notDependOnLibsWithTags: [tags.backend] },
|
|
1069
|
+
{ sourceTag: tags.backend, notDependOnLibsWithTags: [tags.frontend] },
|
|
1070
|
+
{ sourceTag: tags.shared, notDependOnLibsWithTags: [tags.frontend, tags.backend] }
|
|
1071
|
+
];
|
|
1072
|
+
}
|
|
1073
|
+
var SCOPE_CONSTRAINTS = scopeConstraints();
|
|
977
1074
|
var ESLINT_CONFIG_CANDIDATES = ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"];
|
|
978
|
-
var
|
|
1075
|
+
var appliedMarker = (tags) => `sourceTag: '${tags.frontend}'`;
|
|
979
1076
|
var ALLOW_ALL_BLOCK = /depConstraints:\s*\[\s*\{\s*sourceTag:\s*'\*',\s*onlyDependOnLibsWithTags:\s*\[\s*'\*'\s*\],?\s*\},?\s*\]/g;
|
|
980
1077
|
function maskComments(text) {
|
|
981
1078
|
const out = text.split("");
|
|
@@ -1028,19 +1125,34 @@ function findAllowAllMatches(masked) {
|
|
|
1028
1125
|
}
|
|
1029
1126
|
return matches;
|
|
1030
1127
|
}
|
|
1031
|
-
function classifyBoundaryConstraints(configText) {
|
|
1128
|
+
function classifyBoundaryConstraints(configText, tags = DEFAULT_BOUNDARY_TAGS) {
|
|
1032
1129
|
const masked = maskComments(configText);
|
|
1033
|
-
if (masked.includes(
|
|
1130
|
+
if (masked.includes(appliedMarker(tags)))
|
|
1034
1131
|
return "enforced";
|
|
1035
1132
|
if (findAllowAllMatches(masked).length > 0)
|
|
1036
1133
|
return "allow-all";
|
|
1037
1134
|
return "unknown";
|
|
1038
1135
|
}
|
|
1039
1136
|
|
|
1137
|
+
// ../templates/dist/fsd/fsd-eslint.js
|
|
1138
|
+
var FSD_ESLINT_MODULE = "@clearlydev/nx-archweave/eslint";
|
|
1139
|
+
|
|
1140
|
+
// ../template-engine/dist/backend/compose-backend-wiring.js
|
|
1141
|
+
var DEFAULT_SHARED_TYPES_ROOT = "libs/shared-types";
|
|
1142
|
+
function readSharedTypesConfig(config) {
|
|
1143
|
+
const value = config?.["sharedTypes"];
|
|
1144
|
+
if (value === false)
|
|
1145
|
+
return false;
|
|
1146
|
+
if (typeof value !== "object" || value === null)
|
|
1147
|
+
return { path: DEFAULT_SHARED_TYPES_ROOT };
|
|
1148
|
+
const record = value;
|
|
1149
|
+
const path = typeof record["path"] === "string" && record["path"].trim() !== "" ? record["path"].replace(/\/+$/, "") : DEFAULT_SHARED_TYPES_ROOT;
|
|
1150
|
+
const importPath = typeof record["importPath"] === "string" && record["importPath"].trim() !== "" ? record["importPath"] : void 0;
|
|
1151
|
+
return importPath === void 0 ? { path } : { path, importPath };
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1040
1154
|
// ../template-engine/dist/doctor/backend-checks.js
|
|
1041
1155
|
var CONFIG_PATH2 = "toolkit.config.json";
|
|
1042
|
-
var SHARED_TYPES_PACKAGE = "libs/shared-types/package.json";
|
|
1043
|
-
var SHARED_TYPES_LEGACY = "libs/shared-types/project.json";
|
|
1044
1156
|
var BACKEND_CHECK_IDS = [
|
|
1045
1157
|
"backend-framework",
|
|
1046
1158
|
"backend-config-consistency",
|
|
@@ -1055,11 +1167,13 @@ var CHECK_LABELS = {
|
|
|
1055
1167
|
"boundary-constraints-applied": "Module-boundary constraints",
|
|
1056
1168
|
"boundary-tags-exist": "Boundary tags exist"
|
|
1057
1169
|
};
|
|
1058
|
-
function collectScopeTags(root) {
|
|
1170
|
+
function collectScopeTags(root, projects) {
|
|
1171
|
+
if (projects !== void 0)
|
|
1172
|
+
return new Set(projects.flatMap((project) => project.tags));
|
|
1059
1173
|
const tags = /* @__PURE__ */ new Set();
|
|
1060
1174
|
const dirsIn = (rel) => {
|
|
1061
1175
|
try {
|
|
1062
|
-
return
|
|
1176
|
+
return readdirSync2(join3(root, rel), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => `${rel}/${entry.name}`);
|
|
1063
1177
|
} catch {
|
|
1064
1178
|
return [];
|
|
1065
1179
|
}
|
|
@@ -1098,6 +1212,14 @@ function collectScopeTags(root) {
|
|
|
1098
1212
|
function notApplicable(message) {
|
|
1099
1213
|
return BACKEND_CHECK_IDS.map((id) => createDoctorCheck({ id, label: CHECK_LABELS[id], status: "not-applicable", message, evidence: [] }));
|
|
1100
1214
|
}
|
|
1215
|
+
function readConfigObject(root) {
|
|
1216
|
+
try {
|
|
1217
|
+
const parsed = JSON.parse(readFileSync3(join3(root, CONFIG_PATH2), "utf-8"));
|
|
1218
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
1219
|
+
} catch {
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1101
1223
|
function readsBackendOnly(root) {
|
|
1102
1224
|
try {
|
|
1103
1225
|
return JSON.parse(readFileSync3(join3(root, CONFIG_PATH2), "utf-8")).frontend === "none";
|
|
@@ -1121,7 +1243,7 @@ function readConfiguredBackend(root) {
|
|
|
1121
1243
|
return null;
|
|
1122
1244
|
}
|
|
1123
1245
|
}
|
|
1124
|
-
function buildBackendChecks(root) {
|
|
1246
|
+
function buildBackendChecks(root, projects) {
|
|
1125
1247
|
const backend = readConfiguredBackend(root);
|
|
1126
1248
|
if (backend === null) {
|
|
1127
1249
|
return notApplicable("Skipped \u2014 no backend configured in toolkit.config.json");
|
|
@@ -1143,21 +1265,21 @@ function buildBackendChecks(root) {
|
|
|
1143
1265
|
suggestedAction: "Fix the backend field, then re-run doctor."
|
|
1144
1266
|
}));
|
|
1145
1267
|
} else {
|
|
1146
|
-
let
|
|
1268
|
+
let hasDependency2 = false;
|
|
1147
1269
|
try {
|
|
1148
1270
|
const pkg = JSON.parse(readFileSync3(join3(root, "package.json"), "utf-8"));
|
|
1149
1271
|
const packageName = backend === "nest" ? "@nestjs/core" : "express";
|
|
1150
|
-
|
|
1272
|
+
hasDependency2 = Boolean(pkg.dependencies?.[packageName] ?? pkg.devDependencies?.[packageName]);
|
|
1151
1273
|
} catch {
|
|
1152
|
-
|
|
1274
|
+
hasDependency2 = false;
|
|
1153
1275
|
}
|
|
1154
1276
|
checks.push(createDoctorCheck({
|
|
1155
1277
|
id: "backend-framework",
|
|
1156
1278
|
label: CHECK_LABELS["backend-framework"],
|
|
1157
|
-
status:
|
|
1158
|
-
message:
|
|
1159
|
-
evidence:
|
|
1160
|
-
suggestedAction:
|
|
1279
|
+
status: hasDependency2 ? "pass" : "warning",
|
|
1280
|
+
message: hasDependency2 ? `${backend} dependency detected in the root package.json` : `backend is configured as ${backend} but its dependency was not found in the root package.json`,
|
|
1281
|
+
evidence: hasDependency2 ? ["package.json"] : [],
|
|
1282
|
+
suggestedAction: hasDependency2 ? void 0 : "Run the official generator commands shown by add-backend."
|
|
1161
1283
|
}), createDoctorCheck({
|
|
1162
1284
|
id: "backend-config-consistency",
|
|
1163
1285
|
label: CHECK_LABELS["backend-config-consistency"],
|
|
@@ -1166,23 +1288,37 @@ function buildBackendChecks(root) {
|
|
|
1166
1288
|
evidence: [CONFIG_PATH2]
|
|
1167
1289
|
}));
|
|
1168
1290
|
}
|
|
1169
|
-
const
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1291
|
+
const config = readConfigObject(root);
|
|
1292
|
+
const sharedTypesConfig = readSharedTypesConfig(config);
|
|
1293
|
+
if (sharedTypesConfig === false) {
|
|
1294
|
+
checks.push(createDoctorCheck({
|
|
1295
|
+
id: "shared-types-lib",
|
|
1296
|
+
label: CHECK_LABELS["shared-types-lib"],
|
|
1297
|
+
status: "not-applicable",
|
|
1298
|
+
message: "no shared-types lib (sharedTypes: false in toolkit.config.json)",
|
|
1299
|
+
evidence: [CONFIG_PATH2]
|
|
1300
|
+
}));
|
|
1301
|
+
} else {
|
|
1302
|
+
const current = `${sharedTypesConfig.path}/package.json`;
|
|
1303
|
+
const legacy = `${sharedTypesConfig.path}/project.json`;
|
|
1304
|
+
const sharedTypesCurrent = existsSync3(join3(root, current));
|
|
1305
|
+
const sharedTypesLegacy = !sharedTypesCurrent && existsSync3(join3(root, legacy));
|
|
1306
|
+
checks.push(createDoctorCheck({
|
|
1307
|
+
id: "shared-types-lib",
|
|
1308
|
+
label: CHECK_LABELS["shared-types-lib"],
|
|
1309
|
+
status: sharedTypesCurrent || sharedTypesLegacy ? "pass" : "warning",
|
|
1310
|
+
message: sharedTypesCurrent ? `${sharedTypesConfig.path} present` : sharedTypesLegacy ? `${sharedTypesConfig.path} present (legacy project.json shape)` : `${sharedTypesConfig.path} not found \u2014 run add-backend to wire it`,
|
|
1311
|
+
evidence: sharedTypesCurrent ? [current] : sharedTypesLegacy ? [legacy] : []
|
|
1312
|
+
}));
|
|
1313
|
+
}
|
|
1314
|
+
const boundaryTags = readBoundaryTags(config);
|
|
1179
1315
|
const eslintPath = ESLINT_CONFIG_CANDIDATES.find((candidate) => existsSync3(join3(root, candidate)));
|
|
1180
|
-
const classification = eslintPath ? classifyBoundaryConstraints(readFileSync3(join3(root, eslintPath), "utf-8")) : "unknown";
|
|
1316
|
+
const classification = eslintPath ? classifyBoundaryConstraints(readFileSync3(join3(root, eslintPath), "utf-8"), boundaryTags) : "unknown";
|
|
1181
1317
|
checks.push(createDoctorCheck({
|
|
1182
1318
|
id: "boundary-constraints-applied",
|
|
1183
1319
|
label: CHECK_LABELS["boundary-constraints-applied"],
|
|
1184
1320
|
status: classification === "enforced" ? "pass" : "warning",
|
|
1185
|
-
message: classification === "enforced" ?
|
|
1321
|
+
message: classification === "enforced" ? `${boundaryTags.frontend} / ${boundaryTags.backend} depConstraints are enforced` : classification === "allow-all" ? "ESLint still has the allow-all default \u2014 boundaries are NOT enforced" : "could not verify the ESLint config (missing or unrecognized shape)",
|
|
1186
1322
|
evidence: eslintPath ? [eslintPath] : [],
|
|
1187
1323
|
suggestedAction: classification === "enforced" ? void 0 : "Run add-backend with --applyEslint, or merge its snippet yourself."
|
|
1188
1324
|
}));
|
|
@@ -1195,41 +1331,46 @@ function buildBackendChecks(root) {
|
|
|
1195
1331
|
evidence: []
|
|
1196
1332
|
}));
|
|
1197
1333
|
} else {
|
|
1198
|
-
const tags = collectScopeTags(root);
|
|
1199
|
-
const required = readsBackendOnly(root) ? [
|
|
1334
|
+
const tags = collectScopeTags(root, projects);
|
|
1335
|
+
const required = readsBackendOnly(root) ? [boundaryTags.backend] : [boundaryTags.frontend, boundaryTags.backend];
|
|
1200
1336
|
const missing = required.filter((tag) => !tags.has(tag));
|
|
1201
1337
|
checks.push(createDoctorCheck({
|
|
1202
1338
|
id: "boundary-tags-exist",
|
|
1203
1339
|
label: CHECK_LABELS["boundary-tags-exist"],
|
|
1204
1340
|
status: missing.length === 0 ? "pass" : "warning",
|
|
1205
|
-
message: missing.length === 0 ? required.length === 1 ?
|
|
1341
|
+
message: missing.length === 0 ? required.length === 1 ? `${boundaryTags.backend} is carried by at least one project (backend-only workspace \u2014 no frontend to tag)` : `both ${boundaryTags.frontend} and ${boundaryTags.backend} are carried by at least one project` : `the toolkit's enforced deny rules reference ${missing.join(" and ")}, but no project ${projects === void 0 ? "in the scanned directories (apps/, libs/, packages/) " : ""}carries ${missing.length === 1 ? "it" : "them"} \u2014 the boundary is a dead letter`,
|
|
1206
1342
|
evidence: missing.length === 0 ? [] : missing,
|
|
1207
|
-
suggestedAction: missing.length === 0 ? void 0 : "Add the missing
|
|
1343
|
+
suggestedAction: missing.length === 0 ? void 0 : "Add the missing tag(s) to the app project.json tags or package.json nx.tags (add-backend does this for the two apps)."
|
|
1208
1344
|
}));
|
|
1209
1345
|
}
|
|
1210
1346
|
return checks;
|
|
1211
1347
|
}
|
|
1212
1348
|
|
|
1213
1349
|
// ../template-engine/dist/doctor/fsd-like-check.js
|
|
1214
|
-
import { existsSync as existsSync4, readdirSync as
|
|
1350
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
1215
1351
|
import { join as join4 } from "node:path";
|
|
1216
1352
|
var FSD_LAYERS = ["app", "_app", "pages", "_pages", "widgets", "features", "entities", "shared"];
|
|
1217
|
-
function
|
|
1353
|
+
function conventionalSourceRoots(root) {
|
|
1218
1354
|
const scanRoots = ["src"];
|
|
1219
1355
|
try {
|
|
1220
|
-
for (const entry of
|
|
1356
|
+
for (const entry of readdirSync3(join4(root, "apps"), { withFileTypes: true })) {
|
|
1221
1357
|
if (entry.isDirectory())
|
|
1222
1358
|
scanRoots.push(`apps/${entry.name}/src`);
|
|
1223
1359
|
}
|
|
1224
1360
|
} catch {
|
|
1225
1361
|
}
|
|
1226
|
-
|
|
1362
|
+
return scanRoots;
|
|
1363
|
+
}
|
|
1364
|
+
function buildFsdLikeCheck(root, projects) {
|
|
1365
|
+
const scanRoots = projects === void 0 ? conventionalSourceRoots(root) : [...new Set(projects.map((project) => project.sourceRoot))].sort();
|
|
1366
|
+
let found = [];
|
|
1227
1367
|
for (const scanRoot of scanRoots) {
|
|
1228
1368
|
for (const layer of FSD_LAYERS) {
|
|
1229
1369
|
if (existsSync4(join4(root, scanRoot, layer)))
|
|
1230
|
-
found.push(`${scanRoot}/${layer}`);
|
|
1370
|
+
found.push(scanRoot === "." || scanRoot === "" ? layer : `${scanRoot}/${layer}`);
|
|
1231
1371
|
}
|
|
1232
1372
|
}
|
|
1373
|
+
found = found.sort();
|
|
1233
1374
|
if (existsSync4(join4(root, "toolkit.config.json"))) {
|
|
1234
1375
|
return createDoctorCheck({
|
|
1235
1376
|
id: "fsd-like-structure",
|
|
@@ -1259,17 +1400,102 @@ function buildFsdLikeCheck(root) {
|
|
|
1259
1400
|
}
|
|
1260
1401
|
|
|
1261
1402
|
// ../template-engine/dist/doctor/fsd-enforcement-check.js
|
|
1262
|
-
import { existsSync as existsSync5, readdirSync as
|
|
1403
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
1263
1404
|
import { join as join5 } from "node:path";
|
|
1264
1405
|
|
|
1265
|
-
// ../
|
|
1266
|
-
var
|
|
1406
|
+
// ../template-engine/dist/frameworks/profiles.js
|
|
1407
|
+
var ALL_LAYERS = ["app", "pages", "features", "entities", "shared"];
|
|
1408
|
+
var NEXT = {
|
|
1409
|
+
id: "next",
|
|
1410
|
+
recognised: true,
|
|
1411
|
+
// Next.js's own app/ (App Router) directory is the composition root; FSD's Next.js guide
|
|
1412
|
+
// names the FSD app and pages layers _app and _pages, since Next.js reads src/app and
|
|
1413
|
+
// src/pages as its routers.
|
|
1414
|
+
layers: ["pages", "features", "entities", "shared"],
|
|
1415
|
+
layerDirs: { app: "_app", pages: "_pages" },
|
|
1416
|
+
buildOutputDirs: [".next", "out"],
|
|
1417
|
+
platformSuffixes: [],
|
|
1418
|
+
routerDirs: ["pages"]
|
|
1419
|
+
};
|
|
1420
|
+
var REACT_VITE = {
|
|
1421
|
+
id: "react-vite",
|
|
1422
|
+
recognised: true,
|
|
1423
|
+
layers: ALL_LAYERS,
|
|
1424
|
+
layerDirs: {},
|
|
1425
|
+
buildOutputDirs: ["dist"],
|
|
1426
|
+
platformSuffixes: [],
|
|
1427
|
+
routerDirs: []
|
|
1428
|
+
};
|
|
1429
|
+
var EXPO_ROUTER = {
|
|
1430
|
+
id: "expo",
|
|
1431
|
+
recognised: true,
|
|
1432
|
+
layers: ALL_LAYERS,
|
|
1433
|
+
layerDirs: { app: "_app" },
|
|
1434
|
+
buildOutputDirs: [".expo", "dist"],
|
|
1435
|
+
platformSuffixes: ["ios", "android", "native", "web"],
|
|
1436
|
+
routerDirs: []
|
|
1437
|
+
};
|
|
1438
|
+
var EXPO = { ...EXPO_ROUTER, layerDirs: {} };
|
|
1439
|
+
var GENERIC = {
|
|
1440
|
+
id: "generic",
|
|
1441
|
+
recognised: false,
|
|
1442
|
+
layers: ALL_LAYERS,
|
|
1443
|
+
layerDirs: {},
|
|
1444
|
+
buildOutputDirs: ["dist", "build"],
|
|
1445
|
+
platformSuffixes: [],
|
|
1446
|
+
routerDirs: []
|
|
1447
|
+
};
|
|
1448
|
+
var NEXT_CONFIGS = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"];
|
|
1449
|
+
var VITE_CONFIGS = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
|
|
1450
|
+
var EXPO_CONFIGS = ["app.config.ts", "app.config.js", "app.config.mjs"];
|
|
1451
|
+
function hasExpoKey(input, file) {
|
|
1452
|
+
const text = input.read(file);
|
|
1453
|
+
if (typeof text !== "string")
|
|
1454
|
+
return false;
|
|
1455
|
+
try {
|
|
1456
|
+
return typeof JSON.parse(text)["expo"] === "object";
|
|
1457
|
+
} catch {
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
function dependsOn(input, manifest, name) {
|
|
1462
|
+
const text = input.read(manifest);
|
|
1463
|
+
if (typeof text !== "string")
|
|
1464
|
+
return false;
|
|
1465
|
+
try {
|
|
1466
|
+
const parsed = JSON.parse(text);
|
|
1467
|
+
return ["dependencies", "devDependencies", "peerDependencies"].some((field) => parsed[field]?.[name] !== void 0);
|
|
1468
|
+
} catch {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function detectFramework(input) {
|
|
1473
|
+
const prefix = input.projectRoot === "" || input.projectRoot === "." ? "" : `${input.projectRoot}/`;
|
|
1474
|
+
const executors = input.executors ?? [];
|
|
1475
|
+
if (NEXT_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/next:"))) {
|
|
1476
|
+
return NEXT;
|
|
1477
|
+
}
|
|
1478
|
+
const dependsAnywhere = (name) => dependsOn(input, `${prefix}package.json`, name) || dependsOn(input, "package.json", name);
|
|
1479
|
+
const usesExpo = executors.some((executor) => executor.startsWith("@nx/expo:")) || hasExpoKey(input, `${prefix}app.json`) || EXPO_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || dependsOn(input, `${prefix}package.json`, "expo");
|
|
1480
|
+
if (usesExpo)
|
|
1481
|
+
return dependsAnywhere("expo-router") ? EXPO_ROUTER : EXPO;
|
|
1482
|
+
const usesReact = dependsAnywhere("react");
|
|
1483
|
+
const usesVite = VITE_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/vite:"));
|
|
1484
|
+
if (usesVite && usesReact)
|
|
1485
|
+
return REACT_VITE;
|
|
1486
|
+
return GENERIC;
|
|
1487
|
+
}
|
|
1267
1488
|
|
|
1268
1489
|
// ../template-engine/dist/fsd/fsd-eslint-config.js
|
|
1269
1490
|
function classifyFsdEslintConfig(configText) {
|
|
1270
1491
|
return maskComments(configText).includes(FSD_ESLINT_MODULE) ? "applied" : "absent";
|
|
1271
1492
|
}
|
|
1272
1493
|
|
|
1494
|
+
// ../template-engine/dist/doctor/doctor-projects.js
|
|
1495
|
+
function projectPrefix(root) {
|
|
1496
|
+
return root === "" || root === "." ? "" : `${root}/`;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1273
1499
|
// ../template-engine/dist/doctor/fsd-enforcement-check.js
|
|
1274
1500
|
function fsdEnabledPerConfig(root) {
|
|
1275
1501
|
const path = join5(root, "toolkit.config.json");
|
|
@@ -1286,10 +1512,14 @@ function fsdEnabledPerConfig(root) {
|
|
|
1286
1512
|
return null;
|
|
1287
1513
|
}
|
|
1288
1514
|
}
|
|
1289
|
-
function eslintConfigPaths(root) {
|
|
1515
|
+
function eslintConfigPaths(root, projects) {
|
|
1516
|
+
if (projects !== void 0) {
|
|
1517
|
+
const dirs2 = ["", ...projects.map((project) => projectPrefix(project.root))];
|
|
1518
|
+
return [...new Set(dirs2.flatMap((dir) => ESLINT_CONFIG_CANDIDATES.map((name) => `${dir}${name}`)))].filter((rel) => existsSync5(join5(root, rel)));
|
|
1519
|
+
}
|
|
1290
1520
|
const dirsIn = (rel) => {
|
|
1291
1521
|
try {
|
|
1292
|
-
return
|
|
1522
|
+
return readdirSync4(join5(root, rel), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => rel === "" ? entry.name : `${rel}/${entry.name}`);
|
|
1293
1523
|
} catch {
|
|
1294
1524
|
return [];
|
|
1295
1525
|
}
|
|
@@ -1298,7 +1528,7 @@ function eslintConfigPaths(root) {
|
|
|
1298
1528
|
const dirs = ["", ...apps, ...apps.flatMap((app) => dirsIn(app))];
|
|
1299
1529
|
return dirs.flatMap((dir) => ESLINT_CONFIG_CANDIDATES.map((name) => dir === "" ? name : `${dir}/${name}`)).filter((rel) => existsSync5(join5(root, rel)));
|
|
1300
1530
|
}
|
|
1301
|
-
function buildFsdEnforcementCheck(root) {
|
|
1531
|
+
function buildFsdEnforcementCheck(root, projects) {
|
|
1302
1532
|
const id = "fsd-enforcement-applied";
|
|
1303
1533
|
const label = "FSD layer rule applied";
|
|
1304
1534
|
const enabled = fsdEnabledPerConfig(root);
|
|
@@ -1311,7 +1541,7 @@ function buildFsdEnforcementCheck(root) {
|
|
|
1311
1541
|
evidence: []
|
|
1312
1542
|
});
|
|
1313
1543
|
}
|
|
1314
|
-
const applied =
|
|
1544
|
+
const applied = appliedFsdConfigs(root, projects);
|
|
1315
1545
|
if (applied.length > 0) {
|
|
1316
1546
|
return createDoctorCheck({ id, label, status: "pass", message: "the archweave/fsd-boundaries rule is wired into an ESLint config", evidence: applied });
|
|
1317
1547
|
}
|
|
@@ -1324,11 +1554,35 @@ function buildFsdEnforcementCheck(root) {
|
|
|
1324
1554
|
suggestedAction: "Run add-fsd-lite --applyEslint (or paste the snippet it prints into the frontend app's eslint.config.mjs)."
|
|
1325
1555
|
});
|
|
1326
1556
|
}
|
|
1557
|
+
function appliedFsdConfigs(root, projects) {
|
|
1558
|
+
return eslintConfigPaths(root, projects).filter((rel) => classifyFsdEslintConfig(readFileSync4(join5(root, rel), "utf-8")) === "applied");
|
|
1559
|
+
}
|
|
1560
|
+
function buildFsdRuleAnchoredCheck(root, projects) {
|
|
1561
|
+
const id = "fsd-rule-anchored";
|
|
1562
|
+
const label = "FSD rule anchored to its config";
|
|
1563
|
+
const applied = appliedFsdConfigs(root, projects);
|
|
1564
|
+
if (applied.length === 0) {
|
|
1565
|
+
return createDoctorCheck({ id, label, status: "not-applicable", message: "no ESLint config wires the FSD rule", evidence: [] });
|
|
1566
|
+
}
|
|
1567
|
+
const unanchored = applied.filter((rel) => !/fsdBoundaries\(\s*\{[^}]*\bcwd\s*:/s.test(maskComments(readFileSync4(join5(root, rel), "utf-8"))));
|
|
1568
|
+
if (unanchored.length === 0) {
|
|
1569
|
+
return createDoctorCheck({ id, label, status: "pass", message: "every FSD rule config passes cwd", evidence: applied });
|
|
1570
|
+
}
|
|
1571
|
+
return createDoctorCheck({
|
|
1572
|
+
id,
|
|
1573
|
+
label,
|
|
1574
|
+
status: "warning",
|
|
1575
|
+
message: "fsdBoundaries() is called without cwd \u2014 run from the workspace root or an editor, the rule checks no file",
|
|
1576
|
+
evidence: unanchored,
|
|
1577
|
+
suggestedAction: "Pass the config's own directory: fsdBoundaries({ severity, cwd: import.meta.dirname, root: '<source root>' })."
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1327
1580
|
|
|
1328
1581
|
// ../template-engine/dist/doctor/run-doctor-checks.js
|
|
1329
1582
|
var NOT_APPLICABLE_IDS = [
|
|
1330
1583
|
"fsd-like-structure",
|
|
1331
1584
|
"fsd-enforcement-applied",
|
|
1585
|
+
"fsd-rule-anchored",
|
|
1332
1586
|
"nx-version",
|
|
1333
1587
|
"package-manager",
|
|
1334
1588
|
"workspace-root",
|
|
@@ -1341,7 +1595,7 @@ var NOT_APPLICABLE_IDS = [
|
|
|
1341
1595
|
...REVIEW_CHECK_IDS,
|
|
1342
1596
|
...BACKEND_CHECK_IDS
|
|
1343
1597
|
];
|
|
1344
|
-
function runDoctorChecks(root) {
|
|
1598
|
+
function runDoctorChecks(root, projects) {
|
|
1345
1599
|
const env = detectEnvironment(root);
|
|
1346
1600
|
if (!env.hasNxWorkspace) {
|
|
1347
1601
|
const checks2 = [
|
|
@@ -1372,8 +1626,9 @@ function runDoctorChecks(root) {
|
|
|
1372
1626
|
evidence: ["nx.json"]
|
|
1373
1627
|
}),
|
|
1374
1628
|
buildNxVersionCheck(env),
|
|
1375
|
-
buildFsdLikeCheck(root),
|
|
1376
|
-
buildFsdEnforcementCheck(root),
|
|
1629
|
+
buildFsdLikeCheck(root, projects),
|
|
1630
|
+
buildFsdEnforcementCheck(root, projects),
|
|
1631
|
+
buildFsdRuleAnchoredCheck(root, projects),
|
|
1377
1632
|
createDoctorCheck({
|
|
1378
1633
|
id: "package-manager",
|
|
1379
1634
|
label: "Package manager",
|
|
@@ -1394,7 +1649,7 @@ function runDoctorChecks(root) {
|
|
|
1394
1649
|
buildGithubTemplatesCheck(root),
|
|
1395
1650
|
buildIntegrationManifestCheck(root),
|
|
1396
1651
|
...buildReviewChecks(root),
|
|
1397
|
-
...buildBackendChecks(root)
|
|
1652
|
+
...buildBackendChecks(root, projects)
|
|
1398
1653
|
];
|
|
1399
1654
|
const overallStatus = computeOverallStatus(checks);
|
|
1400
1655
|
return { checks, overallStatus };
|
|
@@ -1770,33 +2025,6 @@ var PLUGIN_CATALOG = [
|
|
|
1770
2025
|
];
|
|
1771
2026
|
var PLUGIN_IDS = PLUGIN_CATALOG.map((entry) => entry.id);
|
|
1772
2027
|
|
|
1773
|
-
// ../integrations/dist/catalog/resolve-version.js
|
|
1774
|
-
import { get } from "node:https";
|
|
1775
|
-
function resolveVersionFromNpmRegistry(npmPackage) {
|
|
1776
|
-
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(npmPackage)}`;
|
|
1777
|
-
return new Promise((resolve2, reject) => {
|
|
1778
|
-
get(registryUrl, (response) => {
|
|
1779
|
-
let body = "";
|
|
1780
|
-
response.on("data", (chunk) => {
|
|
1781
|
-
body += chunk;
|
|
1782
|
-
});
|
|
1783
|
-
response.on("end", () => {
|
|
1784
|
-
try {
|
|
1785
|
-
const parsed = JSON.parse(body);
|
|
1786
|
-
const latest = parsed?.["dist-tags"]?.["latest"];
|
|
1787
|
-
if (typeof latest !== "string") {
|
|
1788
|
-
reject(new Error(`Could not resolve the latest version of ${npmPackage} from the npm registry.`));
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
resolve2(latest);
|
|
1792
|
-
} catch (error) {
|
|
1793
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1794
|
-
}
|
|
1795
|
-
});
|
|
1796
|
-
}).on("error", reject);
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
2028
|
// ../integrations/dist/hosts/claude-code/detect.js
|
|
1801
2029
|
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
1802
2030
|
import { join as join9 } from "node:path";
|
|
@@ -1837,14 +2065,14 @@ var WORKSPACE_FILES_BY_SCOPE = {
|
|
|
1837
2065
|
local: [".claude/settings.local.json"]
|
|
1838
2066
|
};
|
|
1839
2067
|
var DEFAULT_PLUGIN_SCOPE = "project";
|
|
1840
|
-
function createClaudeMarketplacePlan(entry
|
|
2068
|
+
function createClaudeMarketplacePlan(entry) {
|
|
1841
2069
|
return async (options) => {
|
|
1842
2070
|
const distribution = entry.hosts["claude-code"];
|
|
1843
2071
|
if (options.targetEnvironment !== "claude-code" || distribution === void 0) {
|
|
1844
2072
|
const supported = Object.entries(entry.hosts).filter(([, value]) => value !== void 0).map(([host]) => host).join(", ");
|
|
1845
2073
|
throw new Error(`Target environment "${options.targetEnvironment}" is not supported by this integration. Supported: ${supported}.`);
|
|
1846
2074
|
}
|
|
1847
|
-
const resolvedVersion = options.versionRange ??
|
|
2075
|
+
const resolvedVersion = options.versionRange ?? VERSION_DECIDED_BY_MARKETPLACE;
|
|
1848
2076
|
const scope = options.scope ?? DEFAULT_PLUGIN_SCOPE;
|
|
1849
2077
|
return {
|
|
1850
2078
|
officialCommand: {
|
|
@@ -2262,10 +2490,6 @@ function createClaudeMarketplaceVerifier(entry, deps) {
|
|
|
2262
2490
|
}
|
|
2263
2491
|
|
|
2264
2492
|
// ../integrations/dist/catalog/create-catalog-integration.js
|
|
2265
|
-
function defaultResolveVersion(entry) {
|
|
2266
|
-
const npmPackage = entry.npmPackage;
|
|
2267
|
-
return npmPackage === void 0 ? async () => VERSION_DECIDED_BY_MARKETPLACE : () => resolveVersionFromNpmRegistry(npmPackage);
|
|
2268
|
-
}
|
|
2269
2493
|
function createCatalogIntegration(entry, deps) {
|
|
2270
2494
|
return {
|
|
2271
2495
|
id: entry.id,
|
|
@@ -2275,7 +2499,7 @@ function createCatalogIntegration(entry, deps) {
|
|
|
2275
2499
|
license: entry.license,
|
|
2276
2500
|
supportedEnvironments: PLUGIN_HOSTS.filter((host) => entry.hosts[host] !== void 0),
|
|
2277
2501
|
detect: (context) => detectWorkspacePackage(entry, context),
|
|
2278
|
-
plan: createClaudeMarketplacePlan(entry
|
|
2502
|
+
plan: createClaudeMarketplacePlan(entry),
|
|
2279
2503
|
install: createClaudeMarketplaceInstall(entry, { runner: deps.runner }),
|
|
2280
2504
|
verify: createClaudeMarketplaceVerifier(entry, { runner: deps.runner }),
|
|
2281
2505
|
uninstallInstructions: async () => [...entry.uninstallSteps]
|
|
@@ -2352,7 +2576,7 @@ var NodeCommandRunner = class {
|
|
|
2352
2576
|
// src/runtime/integrations-composition-root.ts
|
|
2353
2577
|
function createIntegrationsCompositionRoot(deps = {}) {
|
|
2354
2578
|
const runner = deps.runner ?? new NodeCommandRunner();
|
|
2355
|
-
const registry = createIntegrationRegistry(createCatalogIntegrations({ runner
|
|
2579
|
+
const registry = createIntegrationRegistry(createCatalogIntegrations({ runner }));
|
|
2356
2580
|
return {
|
|
2357
2581
|
registry,
|
|
2358
2582
|
runner,
|
|
@@ -2367,6 +2591,22 @@ import { join as join10 } from "node:path";
|
|
|
2367
2591
|
|
|
2368
2592
|
// src/generators/shared/toolkit-config-file.ts
|
|
2369
2593
|
var TOOLKIT_CONFIG_PATH = "toolkit.config.json";
|
|
2594
|
+
function readToolkitConfigFile(tree) {
|
|
2595
|
+
const raw = tree.read(TOOLKIT_CONFIG_PATH, "utf-8");
|
|
2596
|
+
if (raw === null) return { raw: null, parsed: null };
|
|
2597
|
+
try {
|
|
2598
|
+
const parsed = JSON.parse(raw);
|
|
2599
|
+
return {
|
|
2600
|
+
raw,
|
|
2601
|
+
parsed: typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null
|
|
2602
|
+
};
|
|
2603
|
+
} catch {
|
|
2604
|
+
return { raw, parsed: null };
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
function readToolkitConfig(tree) {
|
|
2608
|
+
return readToolkitConfigFile(tree).parsed;
|
|
2609
|
+
}
|
|
2370
2610
|
function recordedChoice(config, key, allowed) {
|
|
2371
2611
|
const value = config?.[key];
|
|
2372
2612
|
return typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
@@ -2375,6 +2615,13 @@ var RECORDED_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
|
2375
2615
|
function workspacePackageManager(exists, read, config) {
|
|
2376
2616
|
return detectWorkspacePackageManager(exists, read) ?? recordedChoice(config, "packageManager", RECORDED_PACKAGE_MANAGERS) ?? null;
|
|
2377
2617
|
}
|
|
2618
|
+
function recordedFrontends(config) {
|
|
2619
|
+
const frontends = config?.["frontends"];
|
|
2620
|
+
if (!Array.isArray(frontends)) return [];
|
|
2621
|
+
return frontends.flatMap(
|
|
2622
|
+
(entry) => typeof entry === "object" && entry !== null && typeof entry["project"] === "string" ? [entry.project] : []
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2378
2625
|
|
|
2379
2626
|
// src/generators/shared/run-nx.ts
|
|
2380
2627
|
function runNxFor(exists, read) {
|
|
@@ -2397,7 +2644,7 @@ function diskRunNx(root) {
|
|
|
2397
2644
|
var DOCTOR_TARGET_ENVIRONMENT = "claude-code";
|
|
2398
2645
|
async function buildDoctorReportWithIntegrations(root, options = {}, deps = {}) {
|
|
2399
2646
|
const integrationsRoot = deps.integrationsRoot ?? createIntegrationsCompositionRoot();
|
|
2400
|
-
const baseReport = runDoctorChecks(root);
|
|
2647
|
+
const baseReport = runDoctorChecks(root, options.projects);
|
|
2401
2648
|
const declaredIds = readEnabledIntegrationIds(root);
|
|
2402
2649
|
const runNx = diskRunNx(root);
|
|
2403
2650
|
const driftResults = await detectIntegrationsDrift({
|
|
@@ -2443,11 +2690,417 @@ async function applyLiveVerification(driftResults, ctx) {
|
|
|
2443
2690
|
return driftResults.map((result) => live.get(result.id) ?? result);
|
|
2444
2691
|
}
|
|
2445
2692
|
|
|
2693
|
+
// src/generators/doctor/workspace-checks.ts
|
|
2694
|
+
import { getProjects as getProjects2, joinPathFragments } from "@nx/devkit";
|
|
2695
|
+
|
|
2696
|
+
// src/eslint/fsd-policy.ts
|
|
2697
|
+
var PRESETS = {
|
|
2698
|
+
basic: {
|
|
2699
|
+
sliceGroups: { auto: true, include: [], exclude: [] },
|
|
2700
|
+
sharedPublicApi: "segment",
|
|
2701
|
+
testFiles: false,
|
|
2702
|
+
segments: [],
|
|
2703
|
+
platformSuffixes: [],
|
|
2704
|
+
routerDirs: []
|
|
2705
|
+
},
|
|
2706
|
+
practical: {
|
|
2707
|
+
sliceGroups: { auto: true, include: [], exclude: [] },
|
|
2708
|
+
sharedPublicApi: "file",
|
|
2709
|
+
testFiles: ["**/*.{spec,test}.*", "**/__tests__/**"],
|
|
2710
|
+
segments: [],
|
|
2711
|
+
platformSuffixes: [],
|
|
2712
|
+
routerDirs: []
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2716
|
+
function resolveFsdPolicy(recorded, overrides = {}) {
|
|
2717
|
+
const fsd = typeof recorded === "object" && recorded !== null ? recorded : {};
|
|
2718
|
+
const preset = fsd["preset"] === "practical" ? "practical" : "basic";
|
|
2719
|
+
const policy = { ...PRESETS[preset] };
|
|
2720
|
+
const groups = fsd["sliceGroups"];
|
|
2721
|
+
if (groups === false) policy.sliceGroups = false;
|
|
2722
|
+
else if (groups === "auto") policy.sliceGroups = { auto: true, include: [], exclude: [] };
|
|
2723
|
+
else if (typeof groups === "object" && groups !== null) {
|
|
2724
|
+
const value = groups;
|
|
2725
|
+
policy.sliceGroups = {
|
|
2726
|
+
auto: value["auto"] !== false,
|
|
2727
|
+
include: isStringArray(value["include"]) ? value["include"] : [],
|
|
2728
|
+
exclude: isStringArray(value["exclude"]) ? value["exclude"] : []
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
const shared = fsd["sharedPublicApi"];
|
|
2732
|
+
if (shared === "segment" || shared === "component" || shared === "file") policy.sharedPublicApi = shared;
|
|
2733
|
+
const tests = fsd["testFiles"];
|
|
2734
|
+
if (tests === false || isStringArray(tests)) policy.testFiles = tests;
|
|
2735
|
+
if (isStringArray(fsd["segments"])) policy.segments = fsd["segments"];
|
|
2736
|
+
if (isStringArray(fsd["platformSuffixes"])) policy.platformSuffixes = fsd["platformSuffixes"];
|
|
2737
|
+
if (isStringArray(fsd["routerDirs"])) policy.routerDirs = fsd["routerDirs"];
|
|
2738
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
2739
|
+
if (value !== void 0) policy[key] = value;
|
|
2740
|
+
}
|
|
2741
|
+
return policy;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// src/eslint/slice-groups.ts
|
|
2745
|
+
import { readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
2746
|
+
import { join as join11 } from "node:path";
|
|
2747
|
+
var SEGMENTS = [
|
|
2748
|
+
"ui",
|
|
2749
|
+
"model",
|
|
2750
|
+
"api",
|
|
2751
|
+
"lib",
|
|
2752
|
+
"config",
|
|
2753
|
+
"components",
|
|
2754
|
+
"hooks",
|
|
2755
|
+
"types",
|
|
2756
|
+
"utils",
|
|
2757
|
+
"helpers",
|
|
2758
|
+
"constants",
|
|
2759
|
+
"consts",
|
|
2760
|
+
"store",
|
|
2761
|
+
"stores",
|
|
2762
|
+
"styles",
|
|
2763
|
+
"assets",
|
|
2764
|
+
"services",
|
|
2765
|
+
"context"
|
|
2766
|
+
];
|
|
2767
|
+
var IGNORED = /^(\.|readme(\.|$))/i;
|
|
2768
|
+
var listings = /* @__PURE__ */ new Map();
|
|
2769
|
+
function list(dir) {
|
|
2770
|
+
let mtimeMs;
|
|
2771
|
+
try {
|
|
2772
|
+
mtimeMs = statSync2(dir).mtimeMs;
|
|
2773
|
+
} catch {
|
|
2774
|
+
listings.delete(dir);
|
|
2775
|
+
return null;
|
|
2776
|
+
}
|
|
2777
|
+
const cached = listings.get(dir);
|
|
2778
|
+
if (cached !== void 0 && cached.mtimeMs === mtimeMs) return cached.entries;
|
|
2779
|
+
try {
|
|
2780
|
+
const entries = readdirSync5(dir, { withFileTypes: true }).map((entry) => ({ name: entry.name, directory: entry.isDirectory() }));
|
|
2781
|
+
listings.set(dir, { mtimeMs, entries });
|
|
2782
|
+
return entries;
|
|
2783
|
+
} catch {
|
|
2784
|
+
return null;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
function createGroupDetector(rootAbsolute, policy, isIndexFile, listDirectory = list) {
|
|
2788
|
+
const groups = policy.sliceGroups;
|
|
2789
|
+
if (groups === false) return () => false;
|
|
2790
|
+
const segments = /* @__PURE__ */ new Set([...SEGMENTS, ...policy.segments]);
|
|
2791
|
+
return (layerDir, name) => {
|
|
2792
|
+
const key = `${layerDir}/${name}`;
|
|
2793
|
+
if (groups.include.includes(key)) return true;
|
|
2794
|
+
if (groups.exclude.includes(key) || !groups.auto) return false;
|
|
2795
|
+
const entries = listDirectory(join11(rootAbsolute, layerDir, name))?.filter((entry) => !IGNORED.test(entry.name)) ?? null;
|
|
2796
|
+
if (entries === null || entries.length === 0) return false;
|
|
2797
|
+
if (entries.some((entry) => !entry.directory || segments.has(entry.name))) return false;
|
|
2798
|
+
return entries.some((entry) => listDirectory(join11(rootAbsolute, layerDir, name, entry.name))?.some((child) => !child.directory && isIndexFile(child.name)) ?? false);
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// src/eslint/fsd-boundaries.ts
|
|
2803
|
+
function indexFilePattern(platformSuffixes) {
|
|
2804
|
+
const platforms = platformSuffixes.length === 0 ? "" : `(\\.(${platformSuffixes.map((suffix) => suffix.replace(/[^a-z0-9-]/gi, "")).join("|")}))?`;
|
|
2805
|
+
return new RegExp(`^index${platforms}(\\.[cm]?[jt]sx?)?$`);
|
|
2806
|
+
}
|
|
2807
|
+
var DEFAULT_POLICY = resolveFsdPolicy(void 0);
|
|
2808
|
+
|
|
2809
|
+
// src/generators/shared/frontend-app.ts
|
|
2810
|
+
import { getProjects } from "@nx/devkit";
|
|
2811
|
+
|
|
2812
|
+
// src/generators/shared/project-type.ts
|
|
2813
|
+
import { parseJson } from "@nx/devkit";
|
|
2814
|
+
function resolveProjectType(tree, root, declared) {
|
|
2815
|
+
if (declared) return declared === "library" ? "library" : "application";
|
|
2816
|
+
const prefix = root === "" || root === "." ? "" : `${root}/`;
|
|
2817
|
+
if (tree.exists(`${prefix}tsconfig.lib.json`)) return "library";
|
|
2818
|
+
if (tree.exists(`${prefix}tsconfig.app.json`)) return "application";
|
|
2819
|
+
const raw = tree.read(`${prefix}package.json`, "utf-8");
|
|
2820
|
+
if (raw === null) return "library";
|
|
2821
|
+
try {
|
|
2822
|
+
const manifest = parseJson(raw);
|
|
2823
|
+
if (manifest && !["exports", "main", "module", "bin"].some((key) => Boolean(manifest[key]))) return "application";
|
|
2824
|
+
} catch {
|
|
2825
|
+
}
|
|
2826
|
+
return "library";
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// src/generators/shared/frontend-app.ts
|
|
2830
|
+
var baseNameOf = (root) => root.split("/").pop() ?? root;
|
|
2831
|
+
function isEndToEnd(root) {
|
|
2832
|
+
const dir = baseNameOf(root);
|
|
2833
|
+
return dir === "e2e" || dir.endsWith("-e2e");
|
|
2834
|
+
}
|
|
2835
|
+
function isBackend(root, config, backendTag) {
|
|
2836
|
+
return baseNameOf(root) === "api" || (config.tags ?? []).includes(backendTag);
|
|
2837
|
+
}
|
|
2838
|
+
function prefixOf(root) {
|
|
2839
|
+
return root === "" || root === "." ? "" : `${root}/`;
|
|
2840
|
+
}
|
|
2841
|
+
function sourceRootOf(tree, config) {
|
|
2842
|
+
const prefix = prefixOf(config.root);
|
|
2843
|
+
const declared = config.sourceRoot?.replace(/\/$/, "");
|
|
2844
|
+
const atRoot = declared !== void 0 && (declared === config.root || prefix === "" && (declared === "." || declared === ""));
|
|
2845
|
+
if (declared && !atRoot) return declared;
|
|
2846
|
+
if (atRoot && !tree.exists(`${prefix}src`)) return prefix === "" ? "." : config.root;
|
|
2847
|
+
return `${prefix}src`;
|
|
2848
|
+
}
|
|
2849
|
+
function frameworkOf(tree, config) {
|
|
2850
|
+
return detectFramework({
|
|
2851
|
+
exists: (path) => tree.exists(path),
|
|
2852
|
+
read: (path) => tree.read(path, "utf-8"),
|
|
2853
|
+
projectRoot: config.root,
|
|
2854
|
+
executors: Object.values(config.targets ?? {}).flatMap((target) => target.executor ? [target.executor] : [])
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
function resolveFrontendApp(tree, explicitProject) {
|
|
2858
|
+
let projects;
|
|
2859
|
+
try {
|
|
2860
|
+
projects = getProjects(tree);
|
|
2861
|
+
} catch (error) {
|
|
2862
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2863
|
+
const pathMatch = /Cannot parse ([^:]+):/.exec(message);
|
|
2864
|
+
return { kind: "invalid-project-json", detail: pathMatch ? pathMatch[1] : message };
|
|
2865
|
+
}
|
|
2866
|
+
const found = (project2, config2) => {
|
|
2867
|
+
const profile = frameworkOf(tree, config2);
|
|
2868
|
+
return { kind: "found", project: project2, root: config2.root, sourceRoot: sourceRootOf(tree, config2), framework: profile.id, profile };
|
|
2869
|
+
};
|
|
2870
|
+
if (explicitProject !== void 0) {
|
|
2871
|
+
const config2 = projects.get(explicitProject);
|
|
2872
|
+
if (config2) return found(explicitProject, config2);
|
|
2873
|
+
const rootApp = inferredRootApp(tree, projects);
|
|
2874
|
+
return rootApp?.kind === "found" && rootApp.project === explicitProject ? rootApp : { kind: "not-found", project: explicitProject };
|
|
2875
|
+
}
|
|
2876
|
+
const backendTag = readBoundaryTags(readToolkitConfig(tree)).backend;
|
|
2877
|
+
const eligible = [...projects.entries()].filter(([, config2]) => !isEndToEnd(config2.root) && !isBackend(config2.root, config2, backendTag));
|
|
2878
|
+
const declared = eligible.filter(([, config2]) => config2.projectType === "application");
|
|
2879
|
+
const inferred = eligible.filter(([, config2]) => {
|
|
2880
|
+
if (config2.projectType !== void 0) return false;
|
|
2881
|
+
if (config2.root === "" || config2.root === ".") return frameworkOf(tree, config2).recognised;
|
|
2882
|
+
return resolveProjectType(tree, config2.root, void 0) === "application";
|
|
2883
|
+
});
|
|
2884
|
+
const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
|
|
2885
|
+
if (candidates.length === 0) {
|
|
2886
|
+
const rootApp = inferredRootApp(tree, projects);
|
|
2887
|
+
if (rootApp !== null) return rootApp;
|
|
2888
|
+
const otherProjects = [...projects.keys()].filter((name) => !isEndToEnd(projects.get(name)?.root ?? "")).sort();
|
|
2889
|
+
return { kind: "none", otherProjects };
|
|
2890
|
+
}
|
|
2891
|
+
if (candidates.length > 1) return { kind: "ambiguous", candidates: candidates.map(([name]) => name) };
|
|
2892
|
+
const [project, config] = candidates[0];
|
|
2893
|
+
return found(project, config);
|
|
2894
|
+
}
|
|
2895
|
+
function inferredRootApp(tree, projects) {
|
|
2896
|
+
if ([...projects.values()].some((config2) => config2.root === "" || config2.root === ".")) return null;
|
|
2897
|
+
const raw = tree.read("package.json", "utf-8");
|
|
2898
|
+
if (raw === null) return null;
|
|
2899
|
+
let name;
|
|
2900
|
+
try {
|
|
2901
|
+
name = JSON.parse(raw).name;
|
|
2902
|
+
} catch {
|
|
2903
|
+
return null;
|
|
2904
|
+
}
|
|
2905
|
+
if (typeof name !== "string" || name === "") return null;
|
|
2906
|
+
const config = { name, root: "." };
|
|
2907
|
+
const framework = frameworkOf(tree, config);
|
|
2908
|
+
if (!framework.recognised) return null;
|
|
2909
|
+
return { kind: "found", project: name, root: ".", sourceRoot: sourceRootOf(tree, config), framework: framework.id, profile: framework, inferredRoot: true };
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
// src/generators/slice/core.ts
|
|
2913
|
+
function hasDependency(tree, manifest, name) {
|
|
2914
|
+
const raw = tree.read(manifest, "utf-8");
|
|
2915
|
+
if (raw === null) return false;
|
|
2916
|
+
try {
|
|
2917
|
+
const parsed = JSON.parse(raw);
|
|
2918
|
+
return parsed["dependencies"]?.[name] !== void 0 || parsed["devDependencies"]?.[name] !== void 0;
|
|
2919
|
+
} catch {
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
function detectUnitTestRunner(tree, projectRoot) {
|
|
2924
|
+
const prefix = projectRoot === "" || projectRoot === "." ? "" : `${projectRoot}/`;
|
|
2925
|
+
const project = tree.read(`${prefix}project.json`, "utf-8");
|
|
2926
|
+
if (project !== null) {
|
|
2927
|
+
try {
|
|
2928
|
+
const targets = JSON.parse(project).targets ?? {};
|
|
2929
|
+
for (const target of Object.values(targets)) {
|
|
2930
|
+
if (target?.executor?.startsWith("@nx/jest:")) return "jest";
|
|
2931
|
+
if (target?.executor?.startsWith("@nx/vite:test") || target?.executor?.startsWith("@nx/vitest:")) return "vitest";
|
|
2932
|
+
}
|
|
2933
|
+
} catch {
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
for (const dir of prefix === "" ? [""] : [prefix, ""]) {
|
|
2937
|
+
if (["ts", "mts", "cts", "js", "mjs", "cjs"].some((ext) => tree.exists(`${dir}jest.config.${ext}`))) return "jest";
|
|
2938
|
+
if (["ts", "mts", "js", "mjs"].some((ext) => tree.exists(`${dir}vitest.config.${ext}`) || tree.exists(`${dir}vitest.workspace.${ext}`))) return "vitest";
|
|
2939
|
+
for (const ext of ["ts", "mts", "js", "mjs"]) {
|
|
2940
|
+
const vite = tree.read(`${dir}vite.config.${ext}`, "utf-8");
|
|
2941
|
+
if (vite !== null && /\btest\s*:/.test(vite)) return "vitest";
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
for (const manifest of prefix === "" ? ["package.json"] : [`${prefix}package.json`, "package.json"]) {
|
|
2945
|
+
if (hasDependency(tree, manifest, "vitest")) return "vitest";
|
|
2946
|
+
if (hasDependency(tree, manifest, "jest")) return "jest";
|
|
2947
|
+
}
|
|
2948
|
+
return "none";
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
// src/generators/doctor/workspace-checks.ts
|
|
2952
|
+
function doctorProjects(tree) {
|
|
2953
|
+
try {
|
|
2954
|
+
return [...getProjects2(tree).entries()].map(([name, config]) => ({ name, root: config.root, sourceRoot: sourceRootOf(tree, config), tags: config.tags ?? [] })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2955
|
+
} catch {
|
|
2956
|
+
return void 0;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
var SLICED_LAYER_DIRS = ["pages", "_pages", "widgets", "features", "entities"];
|
|
2960
|
+
var SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/;
|
|
2961
|
+
var SCAN_LIMIT = 2e3;
|
|
2962
|
+
function sourceFiles(tree, dir, out = []) {
|
|
2963
|
+
for (const name of tree.children(dir)) {
|
|
2964
|
+
if (out.length >= SCAN_LIMIT) break;
|
|
2965
|
+
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
2966
|
+
const path = joinPathFragments(dir, name);
|
|
2967
|
+
if (tree.isFile(path)) {
|
|
2968
|
+
if (SOURCE_FILE.test(name)) out.push(path);
|
|
2969
|
+
} else {
|
|
2970
|
+
sourceFiles(tree, path, out);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
return out;
|
|
2974
|
+
}
|
|
2975
|
+
function buildWorkspaceChecks(tree) {
|
|
2976
|
+
const config = readToolkitConfig(tree);
|
|
2977
|
+
if (config?.["frontend"] === "none") return [];
|
|
2978
|
+
const app = resolveFrontendApp(tree, void 0);
|
|
2979
|
+
const recorded = recordedFrontends(config);
|
|
2980
|
+
if (app.kind === "ambiguous" && app.candidates.every((name) => recorded.includes(name))) {
|
|
2981
|
+
return [
|
|
2982
|
+
createDoctorCheck({
|
|
2983
|
+
id: "frontend-resolution",
|
|
2984
|
+
label: "Frontend app",
|
|
2985
|
+
status: "pass",
|
|
2986
|
+
message: `${app.candidates.length} frontend apps, recorded in toolkit.config.json's frontends (pass --project to the slice generators)`,
|
|
2987
|
+
evidence: app.candidates
|
|
2988
|
+
})
|
|
2989
|
+
];
|
|
2990
|
+
}
|
|
2991
|
+
if (app.kind !== "found") {
|
|
2992
|
+
const resolution = app.kind === "ambiguous" ? createDoctorCheck({
|
|
2993
|
+
id: "frontend-resolution",
|
|
2994
|
+
label: "Frontend app",
|
|
2995
|
+
status: "warning",
|
|
2996
|
+
message: `${app.candidates.length} frontend apps \u2014 the FSD generators and init need to be told which one`,
|
|
2997
|
+
evidence: app.candidates,
|
|
2998
|
+
suggestedAction: `Pass --project=<name> to add-fsd-lite and the slice generators (e.g. --project=${app.candidates[0]}).`
|
|
2999
|
+
}) : app.kind === "none" ? createDoctorCheck({
|
|
3000
|
+
id: "frontend-resolution",
|
|
3001
|
+
label: "Frontend app",
|
|
3002
|
+
status: "warning",
|
|
3003
|
+
message: "no project was recognised as a frontend app",
|
|
3004
|
+
evidence: app.otherProjects,
|
|
3005
|
+
suggestedAction: app.otherProjects.length > 0 ? "If one of the listed projects is the app, pass --project=<name>." : "Create a frontend app (nx g @nx/react:app or @nx/next:app)."
|
|
3006
|
+
}) : createDoctorCheck({
|
|
3007
|
+
id: "frontend-resolution",
|
|
3008
|
+
label: "Frontend app",
|
|
3009
|
+
status: "error",
|
|
3010
|
+
message: app.kind === "invalid-project-json" ? `a project file could not be parsed (${app.detail})` : `--project ${app.project} not found`,
|
|
3011
|
+
evidence: []
|
|
3012
|
+
});
|
|
3013
|
+
return [resolution];
|
|
3014
|
+
}
|
|
3015
|
+
const checks = [
|
|
3016
|
+
createDoctorCheck({
|
|
3017
|
+
id: "frontend-resolution",
|
|
3018
|
+
label: "Frontend app",
|
|
3019
|
+
status: "pass",
|
|
3020
|
+
message: `${app.project}${app.inferredRoot ? " (the workspace root package)" : ""} \u2014 FSD layers live in ${app.sourceRoot}`,
|
|
3021
|
+
evidence: [app.project]
|
|
3022
|
+
}),
|
|
3023
|
+
app.framework === "generic" ? createDoctorCheck({
|
|
3024
|
+
id: "framework-profile",
|
|
3025
|
+
label: "Frontend framework",
|
|
3026
|
+
status: "warning",
|
|
3027
|
+
message: "framework not recognised \u2014 the common FSD rules apply, with no framework-specific layer names",
|
|
3028
|
+
evidence: [app.project],
|
|
3029
|
+
suggestedAction: "If it is React on Vite or Next.js, pass --frontend=react-vite|next (init records it)."
|
|
3030
|
+
}) : createDoctorCheck({ id: "framework-profile", label: "Frontend framework", status: "pass", message: app.framework, evidence: [app.project] })
|
|
3031
|
+
];
|
|
3032
|
+
const policy = resolveFsdPolicy(config?.["fsd"]);
|
|
3033
|
+
if (policy.sliceGroups !== false) {
|
|
3034
|
+
const lister = (dir) => tree.exists(dir) ? tree.children(dir).map((name) => ({ name, directory: !tree.isFile(joinPathFragments(dir, name)) })) : null;
|
|
3035
|
+
const isGroup = createGroupDetector(app.sourceRoot, policy, (name) => indexFilePattern(policy.platformSuffixes).test(name), lister);
|
|
3036
|
+
const groups = SLICED_LAYER_DIRS.flatMap(
|
|
3037
|
+
(layer) => tree.exists(joinPathFragments(app.sourceRoot, layer)) ? tree.children(joinPathFragments(app.sourceRoot, layer)).filter((name) => isGroup(layer, name)).map((name) => `${layer}/${name}`) : []
|
|
3038
|
+
);
|
|
3039
|
+
checks.push(
|
|
3040
|
+
createDoctorCheck({
|
|
3041
|
+
id: "slice-groups",
|
|
3042
|
+
label: "Slice groups",
|
|
3043
|
+
status: "pass",
|
|
3044
|
+
message: groups.length === 0 ? "no slice groups found" : `the FSD rule treats ${groups.length === 1 ? "this folder" : "these folders"} as groups of separate slices`,
|
|
3045
|
+
evidence: groups
|
|
3046
|
+
})
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
3049
|
+
const runner = detectUnitTestRunner(tree, app.root);
|
|
3050
|
+
checks.push(
|
|
3051
|
+
runner === "none" ? createDoctorCheck({
|
|
3052
|
+
id: "test-runner",
|
|
3053
|
+
label: "Test runner",
|
|
3054
|
+
status: "warning",
|
|
3055
|
+
message: "no jest or vitest found \u2014 the slice generators write no .spec.ts",
|
|
3056
|
+
evidence: [],
|
|
3057
|
+
suggestedAction: "Add a test runner, or pass --unitTestRunner=jest|vitest to the slice generators."
|
|
3058
|
+
}) : createDoctorCheck({ id: "test-runner", label: "Test runner", status: "pass", message: `${runner} \u2014 the slice generators write a .spec.ts`, evidence: [] })
|
|
3059
|
+
);
|
|
3060
|
+
checks.push(buildPolicySuggestion(tree, app.sourceRoot, policy.sharedPublicApi));
|
|
3061
|
+
return checks;
|
|
3062
|
+
}
|
|
3063
|
+
function buildPolicySuggestion(tree, sourceRoot, current) {
|
|
3064
|
+
const id = "fsd-policy-suggestion";
|
|
3065
|
+
const label = "Shared public API";
|
|
3066
|
+
let atSegment = 0;
|
|
3067
|
+
let deeper = 0;
|
|
3068
|
+
let examples = [];
|
|
3069
|
+
for (const file of sourceFiles(tree, sourceRoot)) {
|
|
3070
|
+
if (/(^|\/)shared\//.test(file.slice(sourceRoot.length))) continue;
|
|
3071
|
+
const text = tree.read(file, "utf-8") ?? "";
|
|
3072
|
+
for (const [, specifier] of text.matchAll(/\bfrom\s+['"]([^'"]+)['"]/g)) {
|
|
3073
|
+
const match = /(?:^|\/|@\/)shared\/(.+)$/.exec(specifier);
|
|
3074
|
+
if (match === null) continue;
|
|
3075
|
+
const parts = match[1].split("/").filter((part) => part !== "" && !/^index(\.[\w.]+)?$/.test(part));
|
|
3076
|
+
if (parts.length <= 1) atSegment++;
|
|
3077
|
+
else {
|
|
3078
|
+
deeper++;
|
|
3079
|
+
if (examples.length < 3) examples = [...examples, specifier];
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
if (current !== "segment" || deeper === 0 || deeper < atSegment) {
|
|
3084
|
+
return createDoctorCheck({ id, label, status: "pass", message: `sharedPublicApi: ${current} fits the imports found`, evidence: [] });
|
|
3085
|
+
}
|
|
3086
|
+
return createDoctorCheck({
|
|
3087
|
+
id,
|
|
3088
|
+
label,
|
|
3089
|
+
status: "warning",
|
|
3090
|
+
message: `${deeper} of ${deeper + atSegment} imports into shared reach past a segment's index \u2014 with sharedPublicApi: segment the FSD rule reports each`,
|
|
3091
|
+
evidence: examples,
|
|
3092
|
+
suggestedAction: 'If that is how this code is meant to be organised, set "fsd": { "sharedPublicApi": "file" } (or "component") in toolkit.config.json.'
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
|
|
2446
3096
|
// src/generators/doctor/generator.ts
|
|
2447
3097
|
async function doctorGenerator(tree, options) {
|
|
2448
|
-
const
|
|
2449
|
-
liveIntegrations: options.liveIntegrations ?? false
|
|
3098
|
+
const base = await buildDoctorReportWithIntegrations(tree.root, {
|
|
3099
|
+
liveIntegrations: options.liveIntegrations ?? false,
|
|
3100
|
+
projects: doctorProjects(tree)
|
|
2450
3101
|
});
|
|
3102
|
+
const checks = base.overallStatus === "unsupported" ? base.checks : [...base.checks, ...buildWorkspaceChecks(tree)];
|
|
3103
|
+
const report = { checks, overallStatus: base.overallStatus === "unsupported" ? base.overallStatus : computeOverallStatus(checks) };
|
|
2451
3104
|
if (options.json ?? false) {
|
|
2452
3105
|
console.log(serializeDoctorReport(report).trimEnd());
|
|
2453
3106
|
return;
|