@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
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"properties": {
|
|
8
8
|
"name": {
|
|
9
9
|
"type": "string",
|
|
10
|
-
"description": "Slice name in kebab-case (e.g. \"home\") — becomes <app>/src/pages/<name> (a screen composed from the layers below). Invalid values are refused with a report, not thrown"
|
|
10
|
+
"description": "Slice name in kebab-case (e.g. \"home\") — becomes <app>/src/pages/<name> (a screen composed from the layers below). Invalid values are refused with a report, not thrown",
|
|
11
|
+
"$default": { "$source": "argv", "index": 0 }
|
|
11
12
|
},
|
|
12
13
|
"publicApiName": {
|
|
13
14
|
"type": "string",
|
|
@@ -20,6 +21,15 @@
|
|
|
20
21
|
},
|
|
21
22
|
"description": "Extra segments to start the slice with, each as <segment>/index.ts (e.g. ui,api). model always exists — it holds the stub. Names are by purpose: ui, api, lib, config … (components/hooks/utils/types are refused)"
|
|
22
23
|
},
|
|
24
|
+
"group": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "A slice group folder to put the slice in: <layer>/<group>/<name>/ (e.g. --group=mypage). The group folder itself gets no files — FSD keeps each slice in it separate"
|
|
27
|
+
},
|
|
28
|
+
"unitTestRunner": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"enum": ["auto", "jest", "vitest", "none"],
|
|
31
|
+
"description": "Whether the stub gets a .spec.ts. auto (default): when the app or workspace has jest or vitest (a test target, a runner config, or the dependency); toolkit.config.json's fsd.unitTestRunner comes before detection"
|
|
32
|
+
},
|
|
23
33
|
"project": {
|
|
24
34
|
"type": "string",
|
|
25
35
|
"description": "The frontend app whose source root holds the FSD layers. Omitted: the workspace's one frontend application; with more than one, the run is refused with the candidates listed"
|
|
@@ -28,9 +38,11 @@
|
|
|
28
38
|
"type": "string",
|
|
29
39
|
"enum": [
|
|
30
40
|
"react-vite",
|
|
31
|
-
"next"
|
|
41
|
+
"next",
|
|
42
|
+
"expo",
|
|
43
|
+
"generic"
|
|
32
44
|
],
|
|
33
|
-
"description": "Decides the layer directory: pages/ on react-vite, _pages/ on next (Next.js reads src/pages as its Pages Router). Omitted: toolkit.config.json's frontend (init records it), else detected from
|
|
45
|
+
"description": "Decides the layer directory: pages/ on react-vite, _pages/ on next (Next.js reads src/pages as its Pages Router); 'generic' keeps every layer name. Omitted: toolkit.config.json's frontend (init records it), else detected from the app (its next.config / vite config, executors and dependencies), else generic"
|
|
34
46
|
},
|
|
35
47
|
"locale": {
|
|
36
48
|
"type": "string",
|
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
"properties": {
|
|
8
8
|
"name": {
|
|
9
9
|
"type": "string",
|
|
10
|
-
"description": "Slice name in kebab-case (e.g. \"date-format\") — becomes <app>/src/shared/<name>. Invalid values are refused with a report, not thrown"
|
|
10
|
+
"description": "Slice name in kebab-case (e.g. \"date-format\") — becomes <app>/src/shared/<name>. Invalid values are refused with a report, not thrown",
|
|
11
|
+
"$default": { "$source": "argv", "index": 0 }
|
|
11
12
|
},
|
|
12
13
|
"publicApiName": {
|
|
13
14
|
"type": "string",
|
|
14
15
|
"description": "Exported stub function name. Defaults to the camelCase form of name"
|
|
15
16
|
},
|
|
17
|
+
"unitTestRunner": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"enum": ["auto", "jest", "vitest", "none"],
|
|
20
|
+
"description": "Whether the stub gets a .spec.ts. auto (default): when the app or workspace has jest or vitest (a test target, a runner config, or the dependency); toolkit.config.json's fsd.unitTestRunner comes before detection"
|
|
21
|
+
},
|
|
16
22
|
"project": {
|
|
17
23
|
"type": "string",
|
|
18
24
|
"description": "The frontend app whose source root holds the FSD layers (run add-fsd-lite first). Omitted: the workspace's one frontend application; with more than one, the run is refused with the candidates listed"
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"properties": {
|
|
8
8
|
"name": {
|
|
9
9
|
"type": "string",
|
|
10
|
-
"description": "Slice name in kebab-case (e.g. \"header\") — becomes <app>/src/widgets/<name> (a self-contained block of UI composed from features and entities). Invalid values are refused with a report, not thrown"
|
|
10
|
+
"description": "Slice name in kebab-case (e.g. \"header\") — becomes <app>/src/widgets/<name> (a self-contained block of UI composed from features and entities). Invalid values are refused with a report, not thrown",
|
|
11
|
+
"$default": { "$source": "argv", "index": 0 }
|
|
11
12
|
},
|
|
12
13
|
"publicApiName": {
|
|
13
14
|
"type": "string",
|
|
@@ -20,6 +21,15 @@
|
|
|
20
21
|
},
|
|
21
22
|
"description": "Extra segments to start the slice with, each as <segment>/index.ts (e.g. ui,api). model always exists — it holds the stub. Names are by purpose: ui, api, lib, config … (components/hooks/utils/types are refused)"
|
|
22
23
|
},
|
|
24
|
+
"group": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "A slice group folder to put the slice in: <layer>/<group>/<name>/ (e.g. --group=mypage). The group folder itself gets no files — FSD keeps each slice in it separate"
|
|
27
|
+
},
|
|
28
|
+
"unitTestRunner": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"enum": ["auto", "jest", "vitest", "none"],
|
|
31
|
+
"description": "Whether the stub gets a .spec.ts. auto (default): when the app or workspace has jest or vitest (a test target, a runner config, or the dependency); toolkit.config.json's fsd.unitTestRunner comes before detection"
|
|
32
|
+
},
|
|
23
33
|
"project": {
|
|
24
34
|
"type": "string",
|
|
25
35
|
"description": "The frontend app whose source root holds the FSD layers. Omitted: the workspace's one frontend application; with more than one, the run is refused with the candidates listed"
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { posix, relative, resolve, sep } from 'node:path';
|
|
2
|
-
const LAYER_ORDER = ['app', 'pages', 'widgets', 'features', 'entities', 'shared'];
|
|
3
|
-
/** Directory name → layer. `_app`/`_pages` are FSD's own Next.js naming (its routers own
|
|
4
|
-
* `app/` and `pages/`). */
|
|
5
|
-
const LAYER_BY_DIR = {
|
|
6
|
-
app: 'app',
|
|
7
|
-
_app: 'app',
|
|
8
|
-
pages: 'pages',
|
|
9
|
-
_pages: 'pages',
|
|
10
|
-
widgets: 'widgets',
|
|
11
|
-
features: 'features',
|
|
12
|
-
entities: 'entities',
|
|
13
|
-
shared: 'shared',
|
|
14
|
-
};
|
|
15
|
-
const SLICED_LAYERS = new Set(['pages', 'widgets', 'features', 'entities']);
|
|
16
|
-
const INDEX_FILE = /^index(\.[cm]?[jt]sx?)?$/;
|
|
17
|
-
export function locate(rootRelativePath) {
|
|
18
|
-
const segments = rootRelativePath.split('/').filter((s) => s.length > 0 && s !== '.');
|
|
19
|
-
if (segments.length === 0)
|
|
20
|
-
return null;
|
|
21
|
-
const layer = LAYER_BY_DIR[segments[0]];
|
|
22
|
-
if (layer === undefined)
|
|
23
|
-
return null;
|
|
24
|
-
const hasUnit = layer !== 'app';
|
|
25
|
-
const unit = hasUnit ? (segments[1] ?? null) : null;
|
|
26
|
-
const rest = segments.slice(hasUnit ? 2 : 1);
|
|
27
|
-
if (rest.length > 0 && INDEX_FILE.test(rest[rest.length - 1]))
|
|
28
|
-
rest.pop();
|
|
29
|
-
return { layer, unit, rest };
|
|
30
|
-
}
|
|
31
|
-
/** Pure core: the violation (if any) of `from` importing `to`. */
|
|
32
|
-
export function judge(from, to) {
|
|
33
|
-
if (LAYER_ORDER.indexOf(to.layer) < LAYER_ORDER.indexOf(from.layer))
|
|
34
|
-
return { kind: 'upward', from, to };
|
|
35
|
-
if (to.unit === null)
|
|
36
|
-
return null; // the app layer, or a bare layer directory — nothing finer to guard
|
|
37
|
-
const sameUnit = from.layer === to.layer && from.unit === to.unit;
|
|
38
|
-
if (sameUnit)
|
|
39
|
-
return null; // inside one slice/segment anything goes
|
|
40
|
-
const viaCrossImportApi = to.rest[0] === '@x' && to.rest[1] === from.unit && from.layer === to.layer;
|
|
41
|
-
if (viaCrossImportApi)
|
|
42
|
-
return null;
|
|
43
|
-
if (from.layer === to.layer && SLICED_LAYERS.has(to.layer))
|
|
44
|
-
return { kind: 'cross-slice', from, to };
|
|
45
|
-
if (to.rest.length > 0)
|
|
46
|
-
return { kind: 'sidestep', from, to };
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
export function describeLocation(location) {
|
|
50
|
-
return location.unit === null ? location.layer : `${location.layer}/${location.unit}`;
|
|
51
|
-
}
|
|
52
|
-
/** Resolves an import specifier to a path relative to the source root, or null when it is
|
|
53
|
-
* a package specifier or lands outside the root. */
|
|
54
|
-
export function resolveSpecifier(specifier, importerRootRelative, options) {
|
|
55
|
-
let rootRelative = null;
|
|
56
|
-
if (specifier.startsWith('./') || specifier.startsWith('../') || specifier === '.' || specifier === '..') {
|
|
57
|
-
rootRelative = posix.normalize(posix.join(posix.dirname(importerRootRelative), specifier));
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
for (const [prefix, target] of Object.entries(options.aliases)) {
|
|
61
|
-
if (!specifier.startsWith(prefix))
|
|
62
|
-
continue;
|
|
63
|
-
const targetAbsolute = resolve(options.cwd, target);
|
|
64
|
-
const targetRootRelative = toPosix(relative(options.rootAbsolute, targetAbsolute));
|
|
65
|
-
rootRelative = posix.normalize(posix.join(targetRootRelative === '' ? '.' : targetRootRelative, specifier.slice(prefix.length)));
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (rootRelative === null)
|
|
70
|
-
return null;
|
|
71
|
-
if (rootRelative === '..' || rootRelative.startsWith('../'))
|
|
72
|
-
return null;
|
|
73
|
-
return rootRelative;
|
|
74
|
-
}
|
|
75
|
-
function toPosix(path) {
|
|
76
|
-
return sep === '/' ? path : path.split(sep).join('/');
|
|
77
|
-
}
|
|
78
|
-
export const fsdBoundariesRule = {
|
|
79
|
-
meta: {
|
|
80
|
-
type: 'problem',
|
|
81
|
-
docs: {
|
|
82
|
-
description: 'Enforce canonical Feature-Sliced Design import rules inside a frontend app',
|
|
83
|
-
url: 'https://github.com/clearlydev/nx-archweave#fsd-lite',
|
|
84
|
-
},
|
|
85
|
-
schema: [
|
|
86
|
-
{
|
|
87
|
-
type: 'object',
|
|
88
|
-
properties: {
|
|
89
|
-
root: { type: 'string' },
|
|
90
|
-
aliases: { type: 'object', additionalProperties: { type: 'string' } },
|
|
91
|
-
},
|
|
92
|
-
additionalProperties: false,
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
messages: {
|
|
96
|
-
upward: 'FSD: "{{from}}" may not import from "{{to}}" (a higher layer).',
|
|
97
|
-
crossSlice: 'FSD: "{{from}}" may not import a sibling slice "{{to}}" — move the shared part down a layer, or expose it through "{{to}}/@x/{{fromUnit}}".',
|
|
98
|
-
sidestep: 'FSD: import "{{to}}" through its public API (index.ts), not its internal file "{{path}}".',
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
create(context) {
|
|
102
|
-
const options = (context.options[0] ?? {});
|
|
103
|
-
const root = options.root ?? 'src';
|
|
104
|
-
const aliases = options.aliases ?? { '@/': root };
|
|
105
|
-
const cwd = context.cwd;
|
|
106
|
-
const rootAbsolute = resolve(cwd, root);
|
|
107
|
-
const importerRootRelative = toPosix(relative(rootAbsolute, context.filename));
|
|
108
|
-
if (importerRootRelative === '' || importerRootRelative.startsWith('..'))
|
|
109
|
-
return {};
|
|
110
|
-
const from = locate(importerRootRelative);
|
|
111
|
-
if (from === null)
|
|
112
|
-
return {};
|
|
113
|
-
const check = (node, specifier) => {
|
|
114
|
-
if (typeof specifier !== 'string')
|
|
115
|
-
return;
|
|
116
|
-
const target = resolveSpecifier(specifier, importerRootRelative, { rootAbsolute, cwd, aliases });
|
|
117
|
-
if (target === null)
|
|
118
|
-
return;
|
|
119
|
-
const to = locate(target);
|
|
120
|
-
if (to === null)
|
|
121
|
-
return;
|
|
122
|
-
const violation = judge(from, to);
|
|
123
|
-
if (violation === null)
|
|
124
|
-
return;
|
|
125
|
-
const data = { from: describeLocation(from), to: describeLocation(to), fromUnit: from.unit ?? '', path: target };
|
|
126
|
-
context.report({
|
|
127
|
-
node,
|
|
128
|
-
messageId: violation.kind === 'upward' ? 'upward' : violation.kind === 'cross-slice' ? 'crossSlice' : 'sidestep',
|
|
129
|
-
data,
|
|
130
|
-
});
|
|
131
|
-
};
|
|
132
|
-
const fromSource = (node) => check(node, node.source?.value);
|
|
133
|
-
return {
|
|
134
|
-
ImportDeclaration: fromSource,
|
|
135
|
-
ExportNamedDeclaration: fromSource,
|
|
136
|
-
ExportAllDeclaration: fromSource,
|
|
137
|
-
ImportExpression: (node) => {
|
|
138
|
-
const argument = node.source;
|
|
139
|
-
if (argument && argument.type === 'Literal')
|
|
140
|
-
check(node, argument.value);
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
},
|
|
144
|
-
};
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
-
/**
|
|
4
|
-
* Import aliases from a tsconfig's `compilerOptions.paths`: every
|
|
5
|
-
* `"<prefix>*": ["<dir>/*"]` entry becomes `{ "<prefix>": "<dir>" }`, the directory relative
|
|
6
|
-
* to `cwd` (what the rule's `aliases` option takes). The `extends` chain is followed
|
|
7
|
-
* (relative paths only — a package preset is skipped), the nearest config winning per key;
|
|
8
|
-
* `baseUrl` of the config that declares the paths resolves the targets. Comments and
|
|
9
|
-
* trailing commas are tolerated, like tsc. Anything unreadable yields nothing: the rule
|
|
10
|
-
* then falls back to its default alias — a wrong alias map costs a missed report, never a
|
|
11
|
-
* false one, so this stays best-effort.
|
|
12
|
-
*/
|
|
13
|
-
export function aliasesFromTsconfig({ cwd, tsconfig = 'tsconfig.json', maxDepth = 8 }) {
|
|
14
|
-
const aliases = {};
|
|
15
|
-
let current = resolve(cwd, tsconfig);
|
|
16
|
-
const seen = new Set();
|
|
17
|
-
for (let depth = 0; current !== null && depth < maxDepth && !seen.has(current); depth++) {
|
|
18
|
-
seen.add(current);
|
|
19
|
-
const config = readJsonc(current);
|
|
20
|
-
if (config === null)
|
|
21
|
-
break;
|
|
22
|
-
const compilerOptions = asRecord(config['compilerOptions']);
|
|
23
|
-
const paths = asRecord(compilerOptions?.['paths']);
|
|
24
|
-
if (paths) {
|
|
25
|
-
const baseUrl = typeof compilerOptions?.['baseUrl'] === 'string' ? resolve(dirname(current), compilerOptions['baseUrl']) : dirname(current);
|
|
26
|
-
for (const [pattern, targets] of Object.entries(paths)) {
|
|
27
|
-
if (!pattern.endsWith('*'))
|
|
28
|
-
continue;
|
|
29
|
-
const prefix = pattern.slice(0, -1);
|
|
30
|
-
if (prefix in aliases)
|
|
31
|
-
continue; // a nearer config already decided this prefix
|
|
32
|
-
const target = Array.isArray(targets) ? targets.find((t) => typeof t === 'string') : undefined;
|
|
33
|
-
if (target === undefined || !target.endsWith('/*'))
|
|
34
|
-
continue;
|
|
35
|
-
const dir = toPosix(relative(cwd, resolve(baseUrl, target.slice(0, -'/*'.length))));
|
|
36
|
-
aliases[prefix] = dir === '' ? '.' : dir;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const parent = config['extends'];
|
|
40
|
-
const parentPath = typeof parent === 'string' ? parent : Array.isArray(parent) ? parent.find((p) => typeof p === 'string') : undefined;
|
|
41
|
-
current = parentPath !== undefined && (parentPath.startsWith('.') || isAbsolute(parentPath)) ? resolveExtends(dirname(current), parentPath) : null;
|
|
42
|
-
}
|
|
43
|
-
return aliases;
|
|
44
|
-
}
|
|
45
|
-
function resolveExtends(fromDir, target) {
|
|
46
|
-
const candidates = [resolve(fromDir, target), resolve(fromDir, `${target}.json`)];
|
|
47
|
-
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
48
|
-
}
|
|
49
|
-
function asRecord(value) {
|
|
50
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null;
|
|
51
|
-
}
|
|
52
|
-
function toPosix(path) {
|
|
53
|
-
return sep === '/' ? path : path.split(sep).join('/');
|
|
54
|
-
}
|
|
55
|
-
/** tsconfig is JSON with comments and trailing commas; strip both outside strings. */
|
|
56
|
-
export function readJsonc(path) {
|
|
57
|
-
let text;
|
|
58
|
-
try {
|
|
59
|
-
text = readFileSync(path, 'utf-8');
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
let out = '';
|
|
65
|
-
let mode = 'code';
|
|
66
|
-
for (let i = 0; i < text.length; i++) {
|
|
67
|
-
const c = text[i];
|
|
68
|
-
const d = text[i + 1];
|
|
69
|
-
if (mode === 'code') {
|
|
70
|
-
if (c === '"') {
|
|
71
|
-
mode = 'string';
|
|
72
|
-
out += c;
|
|
73
|
-
}
|
|
74
|
-
else if (c === '/' && d === '/') {
|
|
75
|
-
mode = 'line';
|
|
76
|
-
i++;
|
|
77
|
-
}
|
|
78
|
-
else if (c === '/' && d === '*') {
|
|
79
|
-
mode = 'block';
|
|
80
|
-
i++;
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
out += c;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
else if (mode === 'string') {
|
|
87
|
-
out += c;
|
|
88
|
-
if (c === '\\') {
|
|
89
|
-
out += d ?? '';
|
|
90
|
-
i++;
|
|
91
|
-
}
|
|
92
|
-
else if (c === '"') {
|
|
93
|
-
mode = 'code';
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
else if (mode === 'line') {
|
|
97
|
-
if (c === '\n') {
|
|
98
|
-
mode = 'code';
|
|
99
|
-
out += c;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
else if (c === '*' && d === '/') {
|
|
103
|
-
mode = 'code';
|
|
104
|
-
i++;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
const parsed = JSON.parse(out.replace(/,(\s*[}\]])/g, '$1'));
|
|
109
|
-
return asRecord(parsed);
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
const ACTION_LINE = {
|
|
2
|
-
created: (result) => `${result.filePath}: created`,
|
|
3
|
-
appended: (result) => `${result.filePath}: nx-archweave block added after the existing content (nothing else changed)`,
|
|
4
|
-
updated: (result) => `${result.filePath}: nx-archweave block updated (nothing outside it changed)`,
|
|
5
|
-
unchanged: (result) => `${result.filePath}: unchanged`,
|
|
6
|
-
unrecognized: (result) => `${result.filePath}: not written — ${result.reason ?? 'the nx-archweave markers could not be placed'}`,
|
|
7
|
-
};
|
|
8
|
-
export function renderAddAgentRulesReport(report) {
|
|
9
|
-
const lines = [];
|
|
10
|
-
lines.push(report.dryRun ? 'add-agent-rules dry-run — no files were written.' : 'add-agent-rules:');
|
|
11
|
-
lines.push(` hosts: ${report.hosts.join(', ')}`);
|
|
12
|
-
lines.push(` locale: ${report.locale}`);
|
|
13
|
-
lines.push(` teamRules: ${report.teamRules}`);
|
|
14
|
-
lines.push('');
|
|
15
|
-
for (const file of report.files) {
|
|
16
|
-
lines.push(` ${ACTION_LINE[file.action](file)}`);
|
|
17
|
-
}
|
|
18
|
-
const RECORD_LINE = {
|
|
19
|
-
created: 'toolkit.config.json: aiHosts recorded (file created)',
|
|
20
|
-
updated: 'toolkit.config.json: aiHosts recorded',
|
|
21
|
-
unchanged: 'toolkit.config.json: aiHosts already recorded',
|
|
22
|
-
unrecognized: 'toolkit.config.json: not a JSON object — aiHosts not recorded (fix the file, then rerun)',
|
|
23
|
-
'not-applied': 'toolkit.config.json: aiHosts not recorded — a file above was not written',
|
|
24
|
-
'not-requested': null,
|
|
25
|
-
};
|
|
26
|
-
const recordLine = RECORD_LINE[report.hostsRecord];
|
|
27
|
-
if (recordLine !== null)
|
|
28
|
-
lines.push(` ${recordLine}`);
|
|
29
|
-
if (report.files.some((file) => file.action === 'unrecognized')) {
|
|
30
|
-
lines.push('');
|
|
31
|
-
lines.push('Manual fix needed: keep exactly one "<!-- nx-archweave:agent-rules start -->" … "<!-- nx-archweave:agent-rules end -->"');
|
|
32
|
-
lines.push('pair in the file (or remove both markers), then rerun add-agent-rules.');
|
|
33
|
-
}
|
|
34
|
-
if (report.dryRun) {
|
|
35
|
-
lines.push('');
|
|
36
|
-
lines.push('Generated content preview:');
|
|
37
|
-
for (const file of report.files) {
|
|
38
|
-
if (file.content === null)
|
|
39
|
-
continue;
|
|
40
|
-
lines.push('');
|
|
41
|
-
lines.push(`--- ${file.filePath} ---`);
|
|
42
|
-
lines.push(file.content);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return lines.join('\n');
|
|
46
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
export function renderAddFsdLiteReport(report) {
|
|
2
|
-
const lines = [];
|
|
3
|
-
if (report.aborted) {
|
|
4
|
-
lines.push('add-fsd-lite aborted — nothing was written.');
|
|
5
|
-
const refusal = report.appRefusal;
|
|
6
|
-
if (refusal !== null) {
|
|
7
|
-
lines.push('');
|
|
8
|
-
lines.push(" FSD layers live inside the frontend app's source root (e.g. apps/web/src), and no app could be picked:");
|
|
9
|
-
if (refusal.reason === 'project-not-found') {
|
|
10
|
-
lines.push(` --project=${refusal.project} names no project in this workspace.`);
|
|
11
|
-
}
|
|
12
|
-
else if (refusal.reason === 'ambiguous-frontend') {
|
|
13
|
-
lines.push(' more than one frontend app was found — rerun with --project=<one of them>:');
|
|
14
|
-
for (const name of refusal.candidates)
|
|
15
|
-
lines.push(` - ${name}`);
|
|
16
|
-
}
|
|
17
|
-
else if (refusal.reason === 'invalid-project-json') {
|
|
18
|
-
lines.push(` a project.json could not be parsed (${refusal.detail}) — fix it, then rerun.`);
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
lines.push(' the workspace has no frontend application — create one (e.g. nx g @nx/react:app) or pass --project.');
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return lines.join('\n');
|
|
25
|
-
}
|
|
26
|
-
if (report.dryRun) {
|
|
27
|
-
lines.push('add-fsd-lite dry-run — no files were written.');
|
|
28
|
-
lines.push('');
|
|
29
|
-
}
|
|
30
|
-
lines.push(`FSD layers (in ${report.project}, ${report.sourceRoot}):`);
|
|
31
|
-
if (report.layerGuide !== null)
|
|
32
|
-
lines.push(report.layerGuide);
|
|
33
|
-
lines.push('');
|
|
34
|
-
const where = report.eslintPath ?? `${report.project}'s eslint.config.mjs`;
|
|
35
|
-
switch (report.eslint) {
|
|
36
|
-
case 'applied':
|
|
37
|
-
lines.push(`ESLint: archweave/fsd-boundaries (${report.enforcement}) written to ${where}`);
|
|
38
|
-
break;
|
|
39
|
-
case 'already-applied':
|
|
40
|
-
lines.push(`ESLint: archweave/fsd-boundaries already wired in ${where} — no change`);
|
|
41
|
-
break;
|
|
42
|
-
case 'degraded-unrecognized':
|
|
43
|
-
lines.push(`ESLint: ${report.eslintPath === null ? `no ESLint config found for ${report.project}` : `${where} was NOT recognized — left untouched`}.`);
|
|
44
|
-
lines.push(' Add the rule yourself (then the layer rules are actually enforced):');
|
|
45
|
-
break;
|
|
46
|
-
case 'snippet-only':
|
|
47
|
-
lines.push(`ESLint: report-only (pass --applyEslint to write the rule into ${where}).`);
|
|
48
|
-
break;
|
|
49
|
-
case 'not-applied':
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
if (report.snippet !== null)
|
|
53
|
-
lines.push(report.snippet);
|
|
54
|
-
if (report.steiger !== null) {
|
|
55
|
-
const st = report.steiger;
|
|
56
|
-
lines.push('');
|
|
57
|
-
lines.push('steiger (official FSD linter, opt-in):');
|
|
58
|
-
lines.push(` ${st.configPath}: ${st.config === 'created' ? 'created' : st.config === 'unchanged' ? 'unchanged' : 'kept — an existing config with other content is never replaced'}`);
|
|
59
|
-
lines.push(st.target === 'added'
|
|
60
|
-
? ` target ${'fsd-check'}: added to ${st.targetPath}`
|
|
61
|
-
: st.target === 'present'
|
|
62
|
-
? ` target ${'fsd-check'}: already present in ${st.targetPath}`
|
|
63
|
-
: ' target fsd-check: NOT added — no parseable project.json / package.json for the app');
|
|
64
|
-
lines.push(st.dependencies === 'added'
|
|
65
|
-
? ` devDependencies: steiger + @feature-sliced/steiger-plugin added to package.json — run \`${st.installCommand}\``
|
|
66
|
-
: st.dependencies === 'present'
|
|
67
|
-
? ' devDependencies: already present'
|
|
68
|
-
: ' devDependencies: NOT added — the root package.json could not be parsed');
|
|
69
|
-
lines.push(` run it with: ${st.runCommand} (steiger needs Node >= 22.18)`);
|
|
70
|
-
}
|
|
71
|
-
return lines.join('\n');
|
|
72
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const ACTION_LINES = {
|
|
2
|
-
created: 'created',
|
|
3
|
-
unchanged: 'unchanged (already matches the generated version)',
|
|
4
|
-
skipped: 'left as-is (a different file already exists there — never overwritten)',
|
|
5
|
-
};
|
|
6
|
-
export function renderAddGithubTemplatesReport(report) {
|
|
7
|
-
const lines = [];
|
|
8
|
-
if (report.files.length === 0) {
|
|
9
|
-
lines.push('add-github-templates: nothing selected (issue/pr/codeowners all false) — no files to install.');
|
|
10
|
-
return lines.join('\n');
|
|
11
|
-
}
|
|
12
|
-
if (report.dryRun) {
|
|
13
|
-
lines.push('add-github-templates dry-run — no files were written.');
|
|
14
|
-
lines.push('');
|
|
15
|
-
for (const file of report.files) {
|
|
16
|
-
lines.push(`${file.filePath}:`);
|
|
17
|
-
lines.push(file.content);
|
|
18
|
-
lines.push('');
|
|
19
|
-
}
|
|
20
|
-
lines.push('Effective decision:');
|
|
21
|
-
}
|
|
22
|
-
for (const file of report.files) {
|
|
23
|
-
lines.push(` ${file.filePath}: ${ACTION_LINES[file.action]}`);
|
|
24
|
-
}
|
|
25
|
-
return lines.join('\n');
|
|
26
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { renderUserCommand } from '@nx-archweave/integrations';
|
|
2
|
-
export function renderIntegrationPlan(view) {
|
|
3
|
-
const command = view.plan.officialCommand;
|
|
4
|
-
const scopeFlag = view.scopeExplicit ? ` --scope=${view.scope}` : '';
|
|
5
|
-
const lines = [
|
|
6
|
-
`Integration: ${view.tool}`,
|
|
7
|
-
`Source repository: ${view.plan.sourceRepository}`,
|
|
8
|
-
`Official homepage: ${view.plan.officialHomepage}`,
|
|
9
|
-
`License: ${view.plan.license}`,
|
|
10
|
-
`Resolved version: ${view.plan.resolvedVersion}`,
|
|
11
|
-
`Official install method: ${command === null ? 'manual (no scriptable command for this target)' : renderUserCommand(command)}`,
|
|
12
|
-
...(command !== null && view.teamMarketplaceCommand !== null
|
|
13
|
-
? [`For teammates (project scope), install-integration first declares the marketplace in .claude/settings.json: ${view.teamMarketplaceCommand}`]
|
|
14
|
-
: []),
|
|
15
|
-
...(command === null
|
|
16
|
-
? []
|
|
17
|
-
: [
|
|
18
|
-
`Run it for you (verifies and records afterwards): ${view.runNx} g @clearlydev/nx-archweave:install-integration --tool=${view.tool} --apply${scopeFlag}`,
|
|
19
|
-
]),
|
|
20
|
-
'',
|
|
21
|
-
command === null ? 'Manual steps:' : 'Manual alternative (inside a Claude Code session):',
|
|
22
|
-
...view.manualSteps.map((step) => ` - ${step}`),
|
|
23
|
-
'',
|
|
24
|
-
'Uninstall instructions:',
|
|
25
|
-
...view.uninstallSteps.map((step) => ` - ${step}`),
|
|
26
|
-
'',
|
|
27
|
-
`Current detection: ${view.detection.installed ? 'installed' : 'not installed'}`,
|
|
28
|
-
`Current verification: ${view.verification.verified ? 'passed' : 'not verified'}`,
|
|
29
|
-
...view.verification.problems.map((problem) => ` ! ${problem}`),
|
|
30
|
-
'',
|
|
31
|
-
'Manifest write: none here — install-integration records it after installing; record-integration records an install done elsewhere.',
|
|
32
|
-
];
|
|
33
|
-
return lines.join('\n');
|
|
34
|
-
}
|