@clearlydev/nx-archweave 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +265 -2
- package/dist/bin/review-verdict.js +330 -0
- package/dist/eslint/fsd-boundaries.d.ts +119 -0
- package/dist/eslint/fsd-boundaries.js +144 -0
- package/dist/eslint/index.d.ts +78 -0
- package/dist/eslint/index.js +256 -0
- package/dist/eslint/tsconfig-aliases.d.ts +18 -0
- package/dist/eslint/tsconfig-aliases.js +114 -0
- package/dist/generators/add-agent-rules/generator.js +550 -0
- package/dist/generators/add-agent-rules/render.js +46 -0
- package/dist/generators/add-backend/generator.js +784 -0
- package/dist/generators/add-backend/render.js +102 -0
- package/dist/generators/add-fsd-lite/generator.js +553 -0
- package/dist/generators/add-fsd-lite/render.js +72 -0
- package/dist/generators/add-github-templates/files/en/codeowners/.github/CODEOWNERS.template +4 -0
- package/dist/generators/add-github-templates/files/en/issue/.github/ISSUE_TEMPLATE/bug-report.yml.template +28 -0
- package/dist/generators/add-github-templates/files/en/issue/.github/ISSUE_TEMPLATE/feature-request.yml.template +17 -0
- package/dist/generators/add-github-templates/files/en/pr/.github/PULL_REQUEST_TEMPLATE.md.template +12 -0
- package/dist/generators/add-github-templates/files/ko/codeowners/.github/CODEOWNERS.template +4 -0
- package/dist/generators/add-github-templates/files/ko/issue/.github/ISSUE_TEMPLATE/bug-report.yml.template +28 -0
- package/dist/generators/add-github-templates/files/ko/issue/.github/ISSUE_TEMPLATE/feature-request.yml.template +17 -0
- package/dist/generators/add-github-templates/files/ko/pr/.github/PULL_REQUEST_TEMPLATE.md.template +12 -0
- package/dist/generators/add-github-templates/generator.js +125 -0
- package/dist/generators/add-github-templates/render.js +26 -0
- package/dist/generators/add-integration/generator.js +818 -0
- package/dist/generators/add-integration/render.js +34 -0
- package/dist/generators/add-review/generator.js +921 -0
- package/dist/generators/add-review/render.js +96 -0
- package/dist/generators/allow-builds/generator.js +436 -0
- package/dist/generators/allow-builds/render.js +82 -0
- package/dist/generators/doctor/generator.js +2431 -0
- package/dist/generators/doctor/integration-report.js +70 -0
- package/dist/generators/doctor/render.js +10 -0
- package/dist/generators/entity/generator.js +395 -0
- package/dist/generators/feature/generator.js +395 -0
- package/dist/generators/init/generator.js +3211 -0
- package/dist/generators/init/render.js +125 -0
- package/dist/generators/init/validation.js +74 -0
- package/dist/generators/install-integration/generator.js +1233 -0
- package/dist/generators/install-integration/render.js +128 -0
- package/dist/generators/page/generator.js +395 -0
- package/dist/generators/record-integration/generator.js +852 -0
- package/dist/generators/record-integration/render.js +31 -0
- package/dist/generators/shared/dry-run.js +12 -0
- package/dist/generators/shared/frontend-app.js +57 -0
- package/dist/generators/shared/generator.js +395 -0
- package/dist/generators/shared/project-type.js +39 -0
- package/dist/generators/shared/record-pipeline.js +34 -0
- package/dist/generators/shared/run-nx.js +24 -0
- package/dist/generators/shared/template-files.js +38 -0
- package/dist/generators/shared/toolkit-config-file.js +110 -0
- package/dist/generators/slice/core.js +185 -0
- package/dist/generators/slice/files/index/index.ts.template +14 -0
- package/dist/generators/slice/files/module/__name__.spec.ts.template +11 -0
- package/dist/generators/slice/files/module/__name__.ts.template +11 -0
- package/dist/generators/slice/files/segment/index.ts.template +20 -0
- package/dist/generators/slice/render.js +70 -0
- package/dist/generators/widget/generator.js +395 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +680 -0
- package/dist/lib/nx-plugin.d.ts +2 -0
- package/dist/lib/nx-plugin.js +7 -0
- package/dist/runtime/integrations-composition-root.js +20 -0
- package/dist/runtime/node-command-runner.js +81 -0
- package/generators.json +85 -0
- package/package.json +95 -4
- package/src/generators/add-agent-rules/schema.json +43 -0
- package/src/generators/add-backend/schema.json +39 -0
- package/src/generators/add-fsd-lite/schema.json +43 -0
- package/src/generators/add-github-templates/schema.json +35 -0
- package/src/generators/add-integration/schema.json +35 -0
- package/src/generators/add-review/schema.json +72 -0
- package/src/generators/allow-builds/schema.json +20 -0
- package/src/generators/doctor/schema.json +20 -0
- package/src/generators/entity/schema.json +37 -0
- package/src/generators/feature/schema.json +37 -0
- package/src/generators/init/schema.json +98 -0
- package/src/generators/install-integration/schema.json +35 -0
- package/src/generators/page/schema.json +53 -0
- package/src/generators/record-integration/schema.json +26 -0
- package/src/generators/shared/schema.json +32 -0
- package/src/generators/widget/schema.json +45 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { packageManagerCommands } from '@nx-archweave/template-engine';
|
|
2
|
+
/** Section body, no header — init prints its own `## add-backend` title and
|
|
3
|
+
* dry-run marker, so the body must not repeat them. */
|
|
4
|
+
export function renderAddBackendBody(report) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
if (report.refused === 'no-frontend') {
|
|
7
|
+
lines.push(`Refused: no frontend application and no "${report.apiName}" app found in this workspace.`);
|
|
8
|
+
lines.push(' add-backend wires a backend app — beside a frontend, or on its own in a backend-only');
|
|
9
|
+
lines.push(` workspace (create one with: npx create-nx-workspace --preset=${report.backend} --appName=${report.apiName}).`);
|
|
10
|
+
lines.push(' If the backend app has another name, pass --apiName=<its directory name>.');
|
|
11
|
+
return lines.join('\n');
|
|
12
|
+
}
|
|
13
|
+
if (report.refused === 'frontend-not-found') {
|
|
14
|
+
lines.push(`Refused: --frontendProject "${report.frontendProject}" does not match any project.`);
|
|
15
|
+
lines.push(' Nothing was written. Check the name with `nx show projects`.');
|
|
16
|
+
return lines.join('\n');
|
|
17
|
+
}
|
|
18
|
+
if (report.refused === 'ambiguous-frontend') {
|
|
19
|
+
lines.push('Refused: multiple frontend applications found — add-backend will not guess:');
|
|
20
|
+
for (const name of report.frontendCandidates ?? [])
|
|
21
|
+
lines.push(` - ${name}`);
|
|
22
|
+
lines.push(' Nothing was written. Re-run with --frontendProject=<name> to pick one.');
|
|
23
|
+
return lines.join('\n');
|
|
24
|
+
}
|
|
25
|
+
if (report.refused === 'invalid-project-json') {
|
|
26
|
+
lines.push('Refused: a project.json this run must update could not be parsed:');
|
|
27
|
+
for (const path of report.invalidProjects ?? [])
|
|
28
|
+
lines.push(` - ${path}`);
|
|
29
|
+
lines.push(' Nothing was written — fix the JSON, then re-run add-backend.');
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
32
|
+
if (report.refused === 'missing-api-project') {
|
|
33
|
+
lines.push(`Refused: no "${report.apiName}" application project found.`);
|
|
34
|
+
lines.push(' Create it with the official commands first (copy-paste, names already filled in),');
|
|
35
|
+
lines.push(' then re-run add-backend to wire it up:');
|
|
36
|
+
lines.push('');
|
|
37
|
+
for (const command of report.commands ?? [])
|
|
38
|
+
lines.push(` ${command}`);
|
|
39
|
+
if (report.expressRange !== undefined) {
|
|
40
|
+
lines.push('');
|
|
41
|
+
lines.push(` The express line declares express@${report.expressRange}, the range this workspace's @nx/react supports —`);
|
|
42
|
+
lines.push(' the official generator then keeps it instead of adding express 5, which npm rejects as a peer conflict.');
|
|
43
|
+
}
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
if (report.packageManagerNote !== undefined)
|
|
47
|
+
lines.push(` note: ${report.packageManagerNote}`);
|
|
48
|
+
lines.push(` frontend project: ${report.frontendProject ?? 'none (backend-only workspace)'}`);
|
|
49
|
+
lines.push(report.sharedTypes === 'created'
|
|
50
|
+
? ` libs/shared-types: created (import it as ${report.sharedTypesPackage})`
|
|
51
|
+
: ' libs/shared-types: already exists — skipped');
|
|
52
|
+
if (report.config !== undefined && report.config !== 'unchanged') {
|
|
53
|
+
lines.push(` toolkit.config.json: backend "${report.backend}" recorded (${report.config}) — doctor's backend checks now run`);
|
|
54
|
+
}
|
|
55
|
+
if (report.workspaceGlob !== undefined) {
|
|
56
|
+
const pmName = report.packageManager ?? 'pnpm';
|
|
57
|
+
const registry = packageManagerCommands(pmName).workspaceRegistration;
|
|
58
|
+
lines.push({
|
|
59
|
+
added: ` ${registry}: 'libs/shared-types' registered — run \`${pmName} install\` once to link it`,
|
|
60
|
+
present: ` ${registry}: already covers libs/shared-types`,
|
|
61
|
+
unrecognized:
|
|
62
|
+
// an integrated layout lists no workspace packages: the paths alias below links it
|
|
63
|
+
report.tsconfigPaths === 'added' || report.tsconfigPaths === 'present'
|
|
64
|
+
? ` ${registry}: no workspace package list (integrated layout) — the tsconfig paths alias links libs/shared-types`
|
|
65
|
+
: ` ${registry}: shape not recognized — add 'libs/shared-types' to its workspace list yourself`,
|
|
66
|
+
}[report.workspaceGlob]);
|
|
67
|
+
}
|
|
68
|
+
for (const tag of report.tags) {
|
|
69
|
+
lines.push(` tag ${tag.project}: ${tag.action === 'added' ? 'added' : 'already present'}`);
|
|
70
|
+
}
|
|
71
|
+
lines.push({
|
|
72
|
+
added: ` tsconfig.base.json paths: ${report.sharedTypesPackage} added (a classic or integrated setup — libs resolve through paths)`,
|
|
73
|
+
present: ' tsconfig.base.json paths: already wired',
|
|
74
|
+
'workspace-link': ' tsconfig paths: not needed — the workspace package link is the wiring',
|
|
75
|
+
unrecognized: ' tsconfig.base.json: could not be parsed — left untouched',
|
|
76
|
+
}[report.tsconfigPaths]);
|
|
77
|
+
switch (report.eslint) {
|
|
78
|
+
case 'applied':
|
|
79
|
+
lines.push(' ESLint boundaries: cross-side deny rules written (existing imports stay allowed)');
|
|
80
|
+
break;
|
|
81
|
+
case 'already-applied':
|
|
82
|
+
lines.push(' ESLint boundaries: already enforced — no change');
|
|
83
|
+
break;
|
|
84
|
+
case 'degraded-unrecognized':
|
|
85
|
+
lines.push(' ESLint boundaries: your config was NOT recognized — left untouched.');
|
|
86
|
+
lines.push(' Merge this yourself (then boundaries are actually enforced):');
|
|
87
|
+
break;
|
|
88
|
+
case 'snippet-only':
|
|
89
|
+
lines.push(' ESLint boundaries: report-only (pass --applyEslint to write them).');
|
|
90
|
+
lines.push(' Paste-ready snippet:');
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (report.snippet) {
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push(report.snippet);
|
|
96
|
+
}
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
/** Standalone rendering (the add-backend generator's own console output). */
|
|
100
|
+
export function renderAddBackendReport(report, dryRun) {
|
|
101
|
+
return [`add-backend (${report.backend})${dryRun ? ' — dry run' : ''}`, '', renderAddBackendBody(report)].join('\n');
|
|
102
|
+
}
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// ../shared/dist/lib/toolkit-config.js
|
|
2
|
+
function readFsdEnforcement(fsd) {
|
|
3
|
+
if (typeof fsd !== "object" || fsd === null)
|
|
4
|
+
return void 0;
|
|
5
|
+
const enforcement = fsd["enforcement"];
|
|
6
|
+
return enforcement === "error" || enforcement === "warn" ? enforcement : void 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// ../template-engine/dist/environment/package-manager.js
|
|
10
|
+
var LOCKFILES = [
|
|
11
|
+
["yarn", ["yarn.lock"]],
|
|
12
|
+
["bun", ["bun.lock", "bun.lockb"]],
|
|
13
|
+
["npm", ["package-lock.json"]]
|
|
14
|
+
];
|
|
15
|
+
function detectWorkspacePackageManager(exists, read = () => void 0) {
|
|
16
|
+
if (exists("pnpm-lock.yaml") || exists("pnpm-workspace.yaml"))
|
|
17
|
+
return "pnpm";
|
|
18
|
+
const found = LOCKFILES.filter(([, files]) => files.some((file) => exists(file))).map(([pm]) => pm);
|
|
19
|
+
if (found.length <= 1)
|
|
20
|
+
return found[0] ?? null;
|
|
21
|
+
const declared = /"packageManager"\s*:\s*"(npm|yarn|bun)@/.exec(read("package.json") ?? "")?.[1];
|
|
22
|
+
const named = found.find((pm) => pm === declared);
|
|
23
|
+
if (named)
|
|
24
|
+
return named;
|
|
25
|
+
return found.includes("npm") ? "npm" : found[0];
|
|
26
|
+
}
|
|
27
|
+
function detectYarnFlavor(exists, read) {
|
|
28
|
+
if (exists(".yarnrc.yml"))
|
|
29
|
+
return "berry";
|
|
30
|
+
if (/^__metadata:/m.test(read("yarn.lock") ?? ""))
|
|
31
|
+
return "berry";
|
|
32
|
+
const major = /"packageManager"\s*:\s*"yarn@(\d+)\./.exec(read("package.json") ?? "")?.[1];
|
|
33
|
+
return major !== void 0 && Number(major) >= 2 ? "berry" : "classic";
|
|
34
|
+
}
|
|
35
|
+
function packageManagerCommands(pm, flavor = "classic") {
|
|
36
|
+
switch (pm) {
|
|
37
|
+
case "pnpm":
|
|
38
|
+
return { addDevAtRoot: (spec) => `pnpm add -D -w ${spec}`, addAtRoot: (spec) => `pnpm add -w ${spec}`, runNx: "pnpm nx", install: "pnpm install", workspaceRegistration: "pnpm-workspace.yaml" };
|
|
39
|
+
case "yarn":
|
|
40
|
+
return {
|
|
41
|
+
addDevAtRoot: (spec) => flavor === "berry" ? `yarn add -D ${spec}` : `yarn add -D -W ${spec}`,
|
|
42
|
+
addAtRoot: (spec) => flavor === "berry" ? `yarn add ${spec}` : `yarn add -W ${spec}`,
|
|
43
|
+
runNx: "yarn nx",
|
|
44
|
+
install: "yarn install",
|
|
45
|
+
workspaceRegistration: "package.json workspaces"
|
|
46
|
+
};
|
|
47
|
+
case "bun":
|
|
48
|
+
return { addDevAtRoot: (spec) => `bun add -d ${spec}`, addAtRoot: (spec) => `bun add ${spec}`, runNx: "bun nx", install: "bun install", workspaceRegistration: "package.json workspaces" };
|
|
49
|
+
case "npm":
|
|
50
|
+
return { addDevAtRoot: (spec) => `npm install -D ${spec}`, addAtRoot: (spec) => `npm install ${spec}`, runNx: "npm exec nx --", install: "npm install", workspaceRegistration: "package.json workspaces" };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ../template-engine/dist/backend/eslint-boundaries.js
|
|
55
|
+
var ESLINT_CONFIG_CANDIDATES = ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"];
|
|
56
|
+
function maskComments(text) {
|
|
57
|
+
const out = text.split("");
|
|
58
|
+
let mode = "code";
|
|
59
|
+
let quote = "";
|
|
60
|
+
for (let i = 0; i < text.length; i++) {
|
|
61
|
+
const c = text[i];
|
|
62
|
+
const d = text[i + 1];
|
|
63
|
+
if (mode === "code") {
|
|
64
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
65
|
+
mode = "string";
|
|
66
|
+
quote = c;
|
|
67
|
+
} else if (c === "/" && d === "/") {
|
|
68
|
+
mode = "line";
|
|
69
|
+
out[i] = " ";
|
|
70
|
+
} else if (c === "/" && d === "*") {
|
|
71
|
+
mode = "block";
|
|
72
|
+
out[i] = " ";
|
|
73
|
+
}
|
|
74
|
+
} else if (mode === "string") {
|
|
75
|
+
if (c === "\\") {
|
|
76
|
+
i++;
|
|
77
|
+
} else if (c === quote) {
|
|
78
|
+
mode = "code";
|
|
79
|
+
}
|
|
80
|
+
} else if (mode === "line") {
|
|
81
|
+
if (c === "\n") {
|
|
82
|
+
mode = "code";
|
|
83
|
+
} else {
|
|
84
|
+
out[i] = " ";
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
if (c === "*" && d === "/") {
|
|
88
|
+
out[i] = " ";
|
|
89
|
+
out[i + 1] = " ";
|
|
90
|
+
i++;
|
|
91
|
+
mode = "code";
|
|
92
|
+
} else if (c !== "\n") {
|
|
93
|
+
out[i] = " ";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out.join("");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ../templates/dist/fsd/fsd-eslint.js
|
|
101
|
+
var FSD_ESLINT_MODULE = "@clearlydev/nx-archweave/eslint";
|
|
102
|
+
function buildFsdEslintImportLine() {
|
|
103
|
+
return `import { fsdBoundaries } from '${FSD_ESLINT_MODULE}';`;
|
|
104
|
+
}
|
|
105
|
+
function buildFsdEslintEntry(severity) {
|
|
106
|
+
return `...fsdBoundaries({ severity: '${severity}' }),`;
|
|
107
|
+
}
|
|
108
|
+
function buildFsdEslintSnippet({ severity, locale }) {
|
|
109
|
+
const lines = locale === "ko" ? [
|
|
110
|
+
"// nx-archweave FSD \uB808\uC774\uC5B4 \uADDC\uCE59 \u2014 \uD504\uB860\uD2B8\uC5D4\uB4DC \uC571\uC758 eslint.config.mjs\uC5D0 \uC9C1\uC811 \uB123\uC73C\uC138\uC694:",
|
|
111
|
+
"// 1) \uB9C8\uC9C0\uB9C9 import \uB4A4\uC5D0 \uC544\uB798 import\uB97C,",
|
|
112
|
+
"// 2) export default [...] \uBC30\uC5F4\uC758 \uB9C8\uC9C0\uB9C9 \uD56D\uBAA9\uC73C\uB85C \uC544\uB798 spread\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4."
|
|
113
|
+
] : [
|
|
114
|
+
"// nx-archweave FSD layer rules \u2014 add these to the frontend app's eslint.config.mjs yourself:",
|
|
115
|
+
"// 1) the import after the last import,",
|
|
116
|
+
"// 2) the spread as the last element of the export default [...] array."
|
|
117
|
+
];
|
|
118
|
+
return [...lines, buildFsdEslintImportLine(), buildFsdEslintEntry(severity)].join("\n");
|
|
119
|
+
}
|
|
120
|
+
var LAYER_GUIDE_EN = {
|
|
121
|
+
app: "composition root: providers, routing, global styles (the framework entry point lives here)",
|
|
122
|
+
pages: "one slice per screen, composed from the layers below",
|
|
123
|
+
features: "one slice per user-facing capability (add-to-cart, sign-in)",
|
|
124
|
+
entities: "one slice per business object (user, product)",
|
|
125
|
+
shared: "segments, not slices: ui, api, lib, model, config \u2014 named by purpose"
|
|
126
|
+
};
|
|
127
|
+
var LAYER_GUIDE_KO = {
|
|
128
|
+
app: "\uC870\uD569 \uB8E8\uD2B8: providers, \uB77C\uC6B0\uD305, \uC804\uC5ED \uC2A4\uD0C0\uC77C (\uD504\uB808\uC784\uC6CC\uD06C \uC9C4\uC785\uC810\uC774 \uC5EC\uAE30 \uC788\uC74C)",
|
|
129
|
+
pages: "\uD654\uBA74 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 \u2014 \uC544\uB798 \uB808\uC774\uC5B4\uB4E4\uC744 \uC870\uD569",
|
|
130
|
+
features: "\uC0AC\uC6A9\uC790 \uAE30\uB2A5 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 (add-to-cart, sign-in)",
|
|
131
|
+
entities: "\uBE44\uC988\uB2C8\uC2A4 \uAC1D\uCCB4 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 (user, product)",
|
|
132
|
+
shared: "\uC2AC\uB77C\uC774\uC2A4\uAC00 \uC544\uB2C8\uB77C \uC138\uADF8\uBA3C\uD2B8: ui, api, lib, model, config \u2014 \uBAA9\uC801\uC73C\uB85C \uC774\uB984 \uC9D3\uAE30"
|
|
133
|
+
};
|
|
134
|
+
function buildFsdLayerGuide({ layers, directories, sourceRoot, locale }) {
|
|
135
|
+
const guide = locale === "ko" ? LAYER_GUIDE_KO : LAYER_GUIDE_EN;
|
|
136
|
+
const width = Math.max(...directories.map((d) => d.length));
|
|
137
|
+
const rows = layers.map((layer, index) => ` ${sourceRoot}/${directories[index].padEnd(width)} ${guide[layer] ?? ""}`);
|
|
138
|
+
const footer = locale === "ko" ? [
|
|
139
|
+
"\uB808\uC774\uC5B4 \uD3F4\uB354\uB294 \uCCAB \uC2AC\uB77C\uC774\uC2A4\uAC00 \uC0DD\uAE38 \uB54C \uB9CC\uB4E4\uC5B4\uC9D1\uB2C8\uB2E4 (\uC815\uC2DD FSD):",
|
|
140
|
+
" nx g @clearlydev/nx-archweave:feature --name=<name> (page, entity, shared\uB3C4 \uAC19\uC740 \uBC29\uC2DD; --segments=ui,api)",
|
|
141
|
+
" widgets/ \uB808\uC774\uC5B4\uB294 \uAE30\uBCF8 \uBC94\uC704 \uBC16\uC774\uC9C0\uB9CC nx g @clearlydev/nx-archweave:widget \uC73C\uB85C \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
142
|
+
"\uC704\uC5D0\uC11C \uC544\uB798\uB85C\uB9CC import\uD560 \uC218 \uC788\uACE0, \uAC19\uC740 \uB808\uC774\uC5B4\uC758 \uC2AC\uB77C\uC774\uC2A4\uB07C\uB9AC\uB294 import\uD558\uC9C0 \uC54A\uC73C\uBA70, \uC2AC\uB77C\uC774\uC2A4\uB294",
|
|
143
|
+
"index.ts\uB97C \uD1B5\uD574\uC11C\uB9CC import\uD569\uB2C8\uB2E4 \u2014 archweave/fsd-boundaries ESLint \uADDC\uCE59\uC774 \uC704\uBC18\uC744 \uC54C\uB824 \uC90D\uB2C8\uB2E4."
|
|
144
|
+
] : [
|
|
145
|
+
"Layer directories appear with their first slice (canonical FSD):",
|
|
146
|
+
" nx g @clearlydev/nx-archweave:feature --name=<name> (page, entity and shared work the same way; --segments=ui,api)",
|
|
147
|
+
" widgets/ is outside the default scope but one command away: nx g @clearlydev/nx-archweave:widget",
|
|
148
|
+
"Imports go downward only, slices on one layer never import each other, and a slice is",
|
|
149
|
+
"imported through its index.ts \u2014 the archweave/fsd-boundaries ESLint rule reports violations."
|
|
150
|
+
];
|
|
151
|
+
return [...rows, ...footer].join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ../templates/dist/fsd/fsd-steiger.js
|
|
155
|
+
var STEIGER_DEPENDENCIES = {
|
|
156
|
+
steiger: "^0.7.0",
|
|
157
|
+
"@feature-sliced/steiger-plugin": "^0.8.0"
|
|
158
|
+
};
|
|
159
|
+
var STEIGER_CONFIG_FILE = "steiger.config.mjs";
|
|
160
|
+
var FSD_CHECK_TARGET = "fsd-check";
|
|
161
|
+
function buildFsdCheckTarget(sourceRootName = "src") {
|
|
162
|
+
return { command: `steiger ${sourceRootName}`, options: { cwd: "{projectRoot}" } };
|
|
163
|
+
}
|
|
164
|
+
function buildSteigerConfig({ frontend, locale }) {
|
|
165
|
+
const ko = locale === "ko";
|
|
166
|
+
const lines = [
|
|
167
|
+
"import { defineConfig } from 'steiger';",
|
|
168
|
+
"import fsd from '@feature-sliced/steiger-plugin';",
|
|
169
|
+
"",
|
|
170
|
+
ko ? "// nx-archweave: \uC815\uC2DD FSD \uADDC\uCE59 \uC804\uCCB4(recommended). \uAD6C\uC870\uB294 generator\uAC00 \uC815\uC2DD\uB300\uB85C \uB9CC\uB4E4\uBBC0\uB85C \uADF8\uB300\uB85C \uB9DE\uBB3C\uB9BD\uB2C8\uB2E4." : "// nx-archweave: the canonical rule set (recommended). The generators scaffold canonical FSD, so it fits as is.",
|
|
171
|
+
"export default defineConfig([",
|
|
172
|
+
" ...fsd.configs.recommended,",
|
|
173
|
+
" {",
|
|
174
|
+
" rules: {",
|
|
175
|
+
ko ? " // \uC544\uBB34\uB3C4 \uCC38\uC870\uD558\uC9C0 \uC54A\uB294 \uC2AC\uB77C\uC774\uC2A4 = \uBC29\uAE08 \uC0DD\uC131\uD55C \uC2AC\uB77C\uC774\uC2A4. \uACBD\uACE0\uB85C \uBCF4\uC774\uB418 \uAC80\uC0AC\uB97C \uB9C9\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." : " // a slice nothing references yet is every freshly generated slice: visible, not blocking",
|
|
176
|
+
" 'fsd/insignificant-slice': 'warn',",
|
|
177
|
+
...frontend === "next" ? [
|
|
178
|
+
ko ? " // Next.js: pages \uB808\uC774\uC5B4\uB294 _pages/ (FSD \uACF5\uC2DD Next.js \uAC00\uC774\uB4DC) \u2014 steiger 0.7\uC740 \uC544\uC9C1 \uC624\uD0C0\uB85C \uBD05\uB2C8\uB2E4." : " // Next.js: the pages layer is _pages/ (FSD's own Next.js guide) \u2014 steiger 0.7 still reads it as a typo",
|
|
179
|
+
" 'fsd/typo-in-layer-name': 'off',"
|
|
180
|
+
] : [],
|
|
181
|
+
" },",
|
|
182
|
+
" },",
|
|
183
|
+
"]);",
|
|
184
|
+
""
|
|
185
|
+
];
|
|
186
|
+
return lines.join("\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ../template-engine/dist/fsd/fsd-eslint-config.js
|
|
190
|
+
function classifyFsdEslintConfig(configText) {
|
|
191
|
+
return maskComments(configText).includes(FSD_ESLINT_MODULE) ? "applied" : "absent";
|
|
192
|
+
}
|
|
193
|
+
var EXPORT_DEFAULT_ARRAY = /export\s+default\s+\[/g;
|
|
194
|
+
var IMPORT_STATEMENT = /^import\b[^;]*?\bfrom\s+['"][^'"]+['"]\s*;?[ \t]*$/gm;
|
|
195
|
+
var CLOSING_BRACKET = /\]\s*;?\s*$/;
|
|
196
|
+
function applyFsdEslintConfig(configText, severity) {
|
|
197
|
+
if (classifyFsdEslintConfig(configText) === "applied")
|
|
198
|
+
return { status: "already-applied" };
|
|
199
|
+
const masked = maskComments(configText);
|
|
200
|
+
EXPORT_DEFAULT_ARRAY.lastIndex = 0;
|
|
201
|
+
const exportMatches = masked.match(EXPORT_DEFAULT_ARRAY) ?? [];
|
|
202
|
+
const closing = CLOSING_BRACKET.exec(masked);
|
|
203
|
+
if (exportMatches.length !== 1 || closing === null)
|
|
204
|
+
return { status: "unrecognized" };
|
|
205
|
+
const eol = configText.includes("\r\n") ? "\r\n" : "\n";
|
|
206
|
+
const closingIndex = closing.index;
|
|
207
|
+
const before = configText.slice(0, closingIndex);
|
|
208
|
+
const lastNonSpace = before.replace(/\s+$/, "");
|
|
209
|
+
const needsComma = !lastNonSpace.endsWith(",") && !lastNonSpace.endsWith("[");
|
|
210
|
+
const entryText = `${needsComma ? "," : ""}${eol} ${buildFsdEslintEntry(severity)}${eol}`;
|
|
211
|
+
const withEntry = `${lastNonSpace}${entryText}${configText.slice(closingIndex)}`;
|
|
212
|
+
IMPORT_STATEMENT.lastIndex = 0;
|
|
213
|
+
let lastImportEnd = -1;
|
|
214
|
+
const maskedWithEntry = maskComments(withEntry);
|
|
215
|
+
for (let m = IMPORT_STATEMENT.exec(maskedWithEntry); m !== null; m = IMPORT_STATEMENT.exec(maskedWithEntry)) {
|
|
216
|
+
lastImportEnd = m.index + m[0].length;
|
|
217
|
+
}
|
|
218
|
+
const importLine = buildFsdEslintImportLine();
|
|
219
|
+
const text = lastImportEnd === -1 ? `${importLine}${eol}${eol}${withEntry}` : `${withEntry.slice(0, lastImportEnd)}${eol}${importLine}${withEntry.slice(lastImportEnd)}`;
|
|
220
|
+
return { status: "applied", text };
|
|
221
|
+
}
|
|
222
|
+
function buildFsdEslintPasteSnippet(severity, locale) {
|
|
223
|
+
return buildFsdEslintSnippet({ severity, locale });
|
|
224
|
+
}
|
|
225
|
+
var REACT_VITE_LAYERS = ["app", "pages", "features", "entities", "shared"];
|
|
226
|
+
var NEXT_LAYERS = ["pages", "features", "entities", "shared"];
|
|
227
|
+
function fsdLayerDirName(frontend, layer) {
|
|
228
|
+
return frontend === "next" && (layer === "pages" || layer === "app") ? `_${layer}` : layer;
|
|
229
|
+
}
|
|
230
|
+
function fsdLayersFor(frontend) {
|
|
231
|
+
return frontend === "next" ? [...NEXT_LAYERS] : [...REACT_VITE_LAYERS];
|
|
232
|
+
}
|
|
233
|
+
function composeFsdLayerGuide({ frontend, sourceRoot, locale }) {
|
|
234
|
+
const layers = fsdLayersFor(frontend);
|
|
235
|
+
return buildFsdLayerGuide({ layers, directories: layers.map((layer) => fsdLayerDirName(frontend, layer)), sourceRoot, locale });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/generators/shared/dry-run.ts
|
|
239
|
+
function resolveDryRun(options) {
|
|
240
|
+
return process.env["NX_DRY_RUN"] === "true" || (options.dryRun ?? false);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/generators/shared/frontend-app.ts
|
|
244
|
+
import { getProjects } from "@nx/devkit";
|
|
245
|
+
|
|
246
|
+
// src/generators/shared/project-type.ts
|
|
247
|
+
import { parseJson } from "@nx/devkit";
|
|
248
|
+
function resolveProjectType(tree, root, declared) {
|
|
249
|
+
if (declared) return declared === "library" ? "library" : "application";
|
|
250
|
+
const prefix = root === "" || root === "." ? "" : `${root}/`;
|
|
251
|
+
if (tree.exists(`${prefix}tsconfig.lib.json`)) return "library";
|
|
252
|
+
if (tree.exists(`${prefix}tsconfig.app.json`)) return "application";
|
|
253
|
+
const raw = tree.read(`${prefix}package.json`, "utf-8");
|
|
254
|
+
if (raw === null) return "library";
|
|
255
|
+
try {
|
|
256
|
+
const manifest = parseJson(raw);
|
|
257
|
+
if (manifest && !["exports", "main", "module", "bin"].some((key) => Boolean(manifest[key]))) return "application";
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
return "library";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/generators/shared/frontend-app.ts
|
|
264
|
+
var baseNameOf = (root) => root.split("/").pop() ?? root;
|
|
265
|
+
function isEndToEnd(root) {
|
|
266
|
+
const dir = baseNameOf(root);
|
|
267
|
+
return dir === "e2e" || dir.endsWith("-e2e");
|
|
268
|
+
}
|
|
269
|
+
function isBackend(root, config) {
|
|
270
|
+
return baseNameOf(root) === "api" || (config.tags ?? []).includes("scope:backend");
|
|
271
|
+
}
|
|
272
|
+
function sourceRootOf(config) {
|
|
273
|
+
if (config.sourceRoot) return config.sourceRoot;
|
|
274
|
+
return config.root === "" || config.root === "." ? "src" : `${config.root}/src`;
|
|
275
|
+
}
|
|
276
|
+
function resolveFrontendApp(tree, explicitProject) {
|
|
277
|
+
let projects;
|
|
278
|
+
try {
|
|
279
|
+
projects = getProjects(tree);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
282
|
+
const pathMatch = /Cannot parse ([^:]+):/.exec(message);
|
|
283
|
+
return { kind: "invalid-project-json", detail: pathMatch ? pathMatch[1] : message };
|
|
284
|
+
}
|
|
285
|
+
const found = (project2, config2) => ({
|
|
286
|
+
kind: "found",
|
|
287
|
+
project: project2,
|
|
288
|
+
root: config2.root,
|
|
289
|
+
sourceRoot: sourceRootOf(config2)
|
|
290
|
+
});
|
|
291
|
+
if (explicitProject !== void 0) {
|
|
292
|
+
const config2 = projects.get(explicitProject);
|
|
293
|
+
return config2 ? found(explicitProject, config2) : { kind: "not-found", project: explicitProject };
|
|
294
|
+
}
|
|
295
|
+
const eligible = [...projects.entries()].filter(([, config2]) => !isEndToEnd(config2.root) && !isBackend(config2.root, config2));
|
|
296
|
+
const declared = eligible.filter(([, config2]) => config2.projectType === "application");
|
|
297
|
+
const inferred = eligible.filter(
|
|
298
|
+
([, config2]) => config2.projectType === void 0 && config2.root !== "" && config2.root !== "." && resolveProjectType(tree, config2.root, void 0) === "application"
|
|
299
|
+
);
|
|
300
|
+
const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
|
|
301
|
+
if (candidates.length === 0) return { kind: "none" };
|
|
302
|
+
if (candidates.length > 1) return { kind: "ambiguous", candidates: candidates.map(([name]) => name) };
|
|
303
|
+
const [project, config] = candidates[0];
|
|
304
|
+
return found(project, config);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/generators/shared/toolkit-config-file.ts
|
|
308
|
+
var TOOLKIT_CONFIG_PATH = "toolkit.config.json";
|
|
309
|
+
function detectIndent(raw) {
|
|
310
|
+
for (const line of raw.split("\n")) {
|
|
311
|
+
const match = /^(\t+| +)/.exec(line);
|
|
312
|
+
if (match) return match[1];
|
|
313
|
+
}
|
|
314
|
+
return " ";
|
|
315
|
+
}
|
|
316
|
+
function readToolkitConfigFile(tree) {
|
|
317
|
+
const raw = tree.read(TOOLKIT_CONFIG_PATH, "utf-8");
|
|
318
|
+
if (raw === null) return { raw: null, parsed: null };
|
|
319
|
+
try {
|
|
320
|
+
const parsed = JSON.parse(raw);
|
|
321
|
+
return {
|
|
322
|
+
raw,
|
|
323
|
+
parsed: typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null
|
|
324
|
+
};
|
|
325
|
+
} catch {
|
|
326
|
+
return { raw, parsed: null };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function readToolkitConfig(tree) {
|
|
330
|
+
return readToolkitConfigFile(tree).parsed;
|
|
331
|
+
}
|
|
332
|
+
function resolveLocaleOption(option, config) {
|
|
333
|
+
if (option) return option;
|
|
334
|
+
const locale = config?.["locale"];
|
|
335
|
+
return locale === "ko" || locale === "en" ? locale : "en";
|
|
336
|
+
}
|
|
337
|
+
function recordedChoice(config, key, allowed) {
|
|
338
|
+
const value = config?.[key];
|
|
339
|
+
return typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
340
|
+
}
|
|
341
|
+
var RECORDED_FRONTENDS = ["react-vite", "next", "none"];
|
|
342
|
+
var RECORDED_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
343
|
+
function resolveFrontendOption(option, config, generator) {
|
|
344
|
+
const frontend = option ?? recordedChoice(config, "frontend", RECORDED_FRONTENDS);
|
|
345
|
+
if (frontend === void 0) {
|
|
346
|
+
throw new Error(`${generator}: pass --frontend=react-vite|next (toolkit.config.json records none \u2014 init writes it).`);
|
|
347
|
+
}
|
|
348
|
+
if (frontend === "none") {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`${generator}: this is a backend-only workspace (toolkit.config.json frontend: none) \u2014 FSD structures a frontend app. Pass --frontend=react-vite|next if you added one.`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return frontend;
|
|
354
|
+
}
|
|
355
|
+
function workspacePackageManager(exists, read, config) {
|
|
356
|
+
return detectWorkspacePackageManager(exists, read) ?? recordedChoice(config, "packageManager", RECORDED_PACKAGE_MANAGERS) ?? null;
|
|
357
|
+
}
|
|
358
|
+
function treePackageManager(tree, config) {
|
|
359
|
+
return workspacePackageManager((p) => tree.exists(p), (p) => tree.read(p, "utf-8"), config);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/generators/add-fsd-lite/render.ts
|
|
363
|
+
function renderAddFsdLiteReport(report) {
|
|
364
|
+
const lines = [];
|
|
365
|
+
if (report.aborted) {
|
|
366
|
+
lines.push("add-fsd-lite aborted \u2014 nothing was written.");
|
|
367
|
+
const refusal = report.appRefusal;
|
|
368
|
+
if (refusal !== null) {
|
|
369
|
+
lines.push("");
|
|
370
|
+
lines.push(" FSD layers live inside the frontend app's source root (e.g. apps/web/src), and no app could be picked:");
|
|
371
|
+
if (refusal.reason === "project-not-found") {
|
|
372
|
+
lines.push(` --project=${refusal.project} names no project in this workspace.`);
|
|
373
|
+
} else if (refusal.reason === "ambiguous-frontend") {
|
|
374
|
+
lines.push(" more than one frontend app was found \u2014 rerun with --project=<one of them>:");
|
|
375
|
+
for (const name of refusal.candidates) lines.push(` - ${name}`);
|
|
376
|
+
} else if (refusal.reason === "invalid-project-json") {
|
|
377
|
+
lines.push(` a project.json could not be parsed (${refusal.detail}) \u2014 fix it, then rerun.`);
|
|
378
|
+
} else {
|
|
379
|
+
lines.push(" the workspace has no frontend application \u2014 create one (e.g. nx g @nx/react:app) or pass --project.");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return lines.join("\n");
|
|
383
|
+
}
|
|
384
|
+
if (report.dryRun) {
|
|
385
|
+
lines.push("add-fsd-lite dry-run \u2014 no files were written.");
|
|
386
|
+
lines.push("");
|
|
387
|
+
}
|
|
388
|
+
lines.push(`FSD layers (in ${report.project}, ${report.sourceRoot}):`);
|
|
389
|
+
if (report.layerGuide !== null) lines.push(report.layerGuide);
|
|
390
|
+
lines.push("");
|
|
391
|
+
const where = report.eslintPath ?? `${report.project}'s eslint.config.mjs`;
|
|
392
|
+
switch (report.eslint) {
|
|
393
|
+
case "applied":
|
|
394
|
+
lines.push(`ESLint: archweave/fsd-boundaries (${report.enforcement}) written to ${where}`);
|
|
395
|
+
break;
|
|
396
|
+
case "already-applied":
|
|
397
|
+
lines.push(`ESLint: archweave/fsd-boundaries already wired in ${where} \u2014 no change`);
|
|
398
|
+
break;
|
|
399
|
+
case "degraded-unrecognized":
|
|
400
|
+
lines.push(`ESLint: ${report.eslintPath === null ? `no ESLint config found for ${report.project}` : `${where} was NOT recognized \u2014 left untouched`}.`);
|
|
401
|
+
lines.push(" Add the rule yourself (then the layer rules are actually enforced):");
|
|
402
|
+
break;
|
|
403
|
+
case "snippet-only":
|
|
404
|
+
lines.push(`ESLint: report-only (pass --applyEslint to write the rule into ${where}).`);
|
|
405
|
+
break;
|
|
406
|
+
case "not-applied":
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
if (report.snippet !== null) lines.push(report.snippet);
|
|
410
|
+
if (report.steiger !== null) {
|
|
411
|
+
const st = report.steiger;
|
|
412
|
+
lines.push("");
|
|
413
|
+
lines.push("steiger (official FSD linter, opt-in):");
|
|
414
|
+
lines.push(
|
|
415
|
+
` ${st.configPath}: ${st.config === "created" ? "created" : st.config === "unchanged" ? "unchanged" : "kept \u2014 an existing config with other content is never replaced"}`
|
|
416
|
+
);
|
|
417
|
+
lines.push(
|
|
418
|
+
st.target === "added" ? ` target ${"fsd-check"}: added to ${st.targetPath}` : st.target === "present" ? ` target ${"fsd-check"}: already present in ${st.targetPath}` : " target fsd-check: NOT added \u2014 no parseable project.json / package.json for the app"
|
|
419
|
+
);
|
|
420
|
+
lines.push(
|
|
421
|
+
st.dependencies === "added" ? ` devDependencies: steiger + @feature-sliced/steiger-plugin added to package.json \u2014 run \`${st.installCommand}\`` : st.dependencies === "present" ? " devDependencies: already present" : " devDependencies: NOT added \u2014 the root package.json could not be parsed"
|
|
422
|
+
);
|
|
423
|
+
lines.push(` run it with: ${st.runCommand} (steiger needs Node >= 22.18)`);
|
|
424
|
+
}
|
|
425
|
+
return lines.join("\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/generators/add-fsd-lite/generator.ts
|
|
429
|
+
function resolveEnforcement(option, config) {
|
|
430
|
+
if (option === "error" || option === "warn") return option;
|
|
431
|
+
return readFsdEnforcement(config?.["fsd"]) ?? "warn";
|
|
432
|
+
}
|
|
433
|
+
function runAddFsdLite(tree, options) {
|
|
434
|
+
const config = readToolkitConfig(tree);
|
|
435
|
+
const locale = resolveLocaleOption(options.locale, config);
|
|
436
|
+
const enforcement = resolveEnforcement(options.enforcement, config);
|
|
437
|
+
const frontend = resolveFrontendOption(options.frontend, config, "add-fsd-lite");
|
|
438
|
+
const dryRun = options.dryRun ?? false;
|
|
439
|
+
const applyEslint = options.applyEslint ?? false;
|
|
440
|
+
const snippet = buildFsdEslintPasteSnippet(enforcement, locale);
|
|
441
|
+
const app = resolveFrontendApp(tree, options.project);
|
|
442
|
+
if (app.kind !== "found") {
|
|
443
|
+
const appRefusal = app.kind === "not-found" ? { reason: "project-not-found", project: app.project } : app.kind === "ambiguous" ? { reason: "ambiguous-frontend", candidates: app.candidates } : app.kind === "invalid-project-json" ? { reason: "invalid-project-json", detail: app.detail } : { reason: "no-frontend-app" };
|
|
444
|
+
return { dryRun, aborted: true, appRefusal, project: null, sourceRoot: null, enforcement, eslint: "not-applied", eslintPath: null, snippet, layerGuide: null, steiger: null };
|
|
445
|
+
}
|
|
446
|
+
const appDir = app.root === "" || app.root === "." ? "" : `${app.root}/`;
|
|
447
|
+
const eslintPath = ESLINT_CONFIG_CANDIDATES.map((name) => `${appDir}${name}`).find((path) => tree.exists(path)) ?? null;
|
|
448
|
+
const configText = eslintPath === null ? null : tree.read(eslintPath, "utf-8");
|
|
449
|
+
let eslint;
|
|
450
|
+
if (!applyEslint) {
|
|
451
|
+
eslint = configText !== null && applyFsdEslintConfig(configText, enforcement).status === "already-applied" ? "already-applied" : "snippet-only";
|
|
452
|
+
} else if (configText === null) {
|
|
453
|
+
eslint = "degraded-unrecognized";
|
|
454
|
+
} else {
|
|
455
|
+
const result = applyFsdEslintConfig(configText, enforcement);
|
|
456
|
+
eslint = result.status === "applied" ? "applied" : result.status === "already-applied" ? "already-applied" : "degraded-unrecognized";
|
|
457
|
+
if (result.status === "applied" && !dryRun && eslintPath !== null) tree.write(eslintPath, result.text);
|
|
458
|
+
}
|
|
459
|
+
const steiger = options.steiger ? addSteiger(tree, { appDir, appRoot: app.root, sourceRoot: app.sourceRoot, frontend, locale, write: !dryRun, config }) : null;
|
|
460
|
+
return {
|
|
461
|
+
dryRun,
|
|
462
|
+
aborted: false,
|
|
463
|
+
appRefusal: null,
|
|
464
|
+
project: app.project,
|
|
465
|
+
sourceRoot: app.sourceRoot,
|
|
466
|
+
steiger,
|
|
467
|
+
enforcement,
|
|
468
|
+
eslint,
|
|
469
|
+
eslintPath,
|
|
470
|
+
snippet: eslint === "applied" || eslint === "already-applied" ? null : snippet,
|
|
471
|
+
layerGuide: composeFsdLayerGuide({ frontend, sourceRoot: app.sourceRoot, locale })
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function readJsonObject(tree, path) {
|
|
475
|
+
const raw = tree.read(path, "utf-8");
|
|
476
|
+
if (raw === null) return null;
|
|
477
|
+
try {
|
|
478
|
+
const parsed = JSON.parse(raw);
|
|
479
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { raw, parsed } : null;
|
|
480
|
+
} catch {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function addSteiger(tree, input) {
|
|
485
|
+
const configPath = `${input.appDir}${STEIGER_CONFIG_FILE}`;
|
|
486
|
+
const configText = buildSteigerConfig({ frontend: input.frontend, locale: input.locale });
|
|
487
|
+
const existing = tree.read(configPath, "utf-8");
|
|
488
|
+
let configAction;
|
|
489
|
+
if (existing === null) {
|
|
490
|
+
configAction = "created";
|
|
491
|
+
if (input.write) tree.write(configPath, configText);
|
|
492
|
+
} else {
|
|
493
|
+
configAction = existing === configText ? "unchanged" : "kept";
|
|
494
|
+
}
|
|
495
|
+
const rootPrefix = input.appRoot === "" || input.appRoot === "." ? "" : `${input.appRoot}/`;
|
|
496
|
+
const sourceRootName = input.sourceRoot.startsWith(rootPrefix) ? input.sourceRoot.slice(rootPrefix.length) : "src";
|
|
497
|
+
const target = buildFsdCheckTarget(sourceRootName);
|
|
498
|
+
let targetAction = "unrecognized";
|
|
499
|
+
let targetPath = null;
|
|
500
|
+
for (const candidate of [`${input.appDir}project.json`, `${input.appDir}package.json`]) {
|
|
501
|
+
const file = readJsonObject(tree, candidate);
|
|
502
|
+
if (file === null) continue;
|
|
503
|
+
targetPath = candidate;
|
|
504
|
+
const holder = candidate.endsWith("project.json") ? file.parsed : file.parsed["nx"] ??= {};
|
|
505
|
+
if (typeof holder !== "object" || holder === null) break;
|
|
506
|
+
const targets = holder["targets"] ??= {};
|
|
507
|
+
if (typeof targets !== "object" || targets === null) break;
|
|
508
|
+
if (targets[FSD_CHECK_TARGET] !== void 0) {
|
|
509
|
+
targetAction = "present";
|
|
510
|
+
} else {
|
|
511
|
+
targets[FSD_CHECK_TARGET] = target;
|
|
512
|
+
targetAction = "added";
|
|
513
|
+
if (input.write) tree.write(candidate, `${JSON.stringify(file.parsed, null, detectIndent(file.raw))}
|
|
514
|
+
`);
|
|
515
|
+
}
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
let dependenciesAction = "unrecognized";
|
|
519
|
+
const rootPackage = readJsonObject(tree, "package.json");
|
|
520
|
+
if (rootPackage !== null) {
|
|
521
|
+
const devDependencies = rootPackage.parsed["devDependencies"] ??= {};
|
|
522
|
+
const dependencies = rootPackage.parsed["dependencies"] ?? {};
|
|
523
|
+
const missing = Object.entries(STEIGER_DEPENDENCIES).filter(([name]) => devDependencies[name] === void 0 && dependencies[name] === void 0);
|
|
524
|
+
if (missing.length === 0) {
|
|
525
|
+
dependenciesAction = "present";
|
|
526
|
+
} else {
|
|
527
|
+
for (const [name, range] of missing) devDependencies[name] = range;
|
|
528
|
+
dependenciesAction = "added";
|
|
529
|
+
if (input.write) tree.write("package.json", `${JSON.stringify(rootPackage.parsed, null, detectIndent(rootPackage.raw))}
|
|
530
|
+
`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const pm = treePackageManager(tree, input.config) ?? "pnpm";
|
|
534
|
+
const commands = packageManagerCommands(pm, pm === "yarn" ? detectYarnFlavor((p) => tree.exists(p), (p) => tree.read(p, "utf-8")) : void 0);
|
|
535
|
+
return {
|
|
536
|
+
configPath,
|
|
537
|
+
config: configAction,
|
|
538
|
+
targetPath,
|
|
539
|
+
target: targetAction,
|
|
540
|
+
dependencies: dependenciesAction,
|
|
541
|
+
installCommand: commands.install,
|
|
542
|
+
runCommand: `${commands.runNx} run <project>:${FSD_CHECK_TARGET}`
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async function addFsdLiteGenerator(tree, options) {
|
|
546
|
+
console.log(renderAddFsdLiteReport(runAddFsdLite(tree, { ...options, dryRun: resolveDryRun(options) })));
|
|
547
|
+
}
|
|
548
|
+
var generator_default = addFsdLiteGenerator;
|
|
549
|
+
export {
|
|
550
|
+
addFsdLiteGenerator,
|
|
551
|
+
generator_default as default,
|
|
552
|
+
runAddFsdLite
|
|
553
|
+
};
|