@craft-ts/dev-tools 0.7.0-beta.13 → 0.7.0-beta.16
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 +60 -10
- package/package.json +8 -2
- package/src/bin/agent-selector.d.ts +22 -0
- package/src/bin/agent-selector.js +100 -0
- package/src/bin/agent-selector.js.map +1 -0
- package/src/bin/craft-graph.js +74 -8
- package/src/bin/craft-graph.js.map +1 -1
- package/src/bin/craft.d.ts +22 -0
- package/src/bin/craft.js +231 -28
- package/src/bin/craft.js.map +1 -1
- package/src/eslint-rules/anchor-has-href.cjs +37 -3
- package/src/eslint-rules/craft-computed-name-match.cjs +26 -5
- package/src/eslint-rules/craft-name-match-utils.cjs +18 -9
- package/src/eslint-rules/hyperscript-walk.cjs +3 -3
- package/src/eslint-rules/index.cjs +46 -1
- package/src/eslint-rules/no-auth-token-in-local-storage.cjs +17 -0
- package/src/eslint-rules/no-craft-computed-side-effects.cjs +12 -5
- package/src/eslint-rules/no-craft-service-component-same-file.cjs +138 -0
- package/src/eslint-rules/no-effect-import-in-frontend.cjs +52 -0
- package/src/eslint-rules/no-effect-in-params.cjs +31 -1
- package/src/eslint-rules/no-explicit-effect-type.cjs +137 -0
- package/src/eslint-rules/no-free-has.cjs +47 -0
- package/src/eslint-rules/no-heading-level-skip.cjs +6 -6
- package/src/eslint-rules/no-raw-class.cjs +76 -0
- package/src/eslint-rules/no-raw-css-value.cjs +163 -0
- package/src/eslint-rules/no-raw-user-url.cjs +114 -0
- package/src/eslint-rules/no-trust-forwarded-headers.cjs +43 -0
- package/src/eslint-rules/no-unsafe-html.cjs +93 -0
- package/src/eslint-rules/no-unsafe-transfer-state.cjs +54 -0
- package/src/eslint-rules/prefer-craft-template-blocks.cjs +8 -8
- package/src/eslint-rules/prefer-direct-yieldable-callback.cjs +57 -14
- package/src/eslint-rules/prefer-inline-effect-insertion.cjs +263 -0
- package/src/eslint-rules/prefer-inline-route-providers.cjs +138 -0
- package/src/eslint-rules/recommended-config.cjs +6 -0
- package/src/eslint-rules/require-reactive-template-bindings.cjs +8 -8
- package/src/eslint-rules/require-route-security-policy.cjs +87 -0
- package/src/eslint-rules/require-server-function-timeout.cjs +39 -0
- package/src/eslint-rules/security-config.cjs +14 -0
- package/src/eslint-rules/security-rule-utils.cjs +21 -0
- package/src/eslint-rules/style-file-boundary.cjs +75 -0
- package/src/eslint-rules/sync-effect-body.cjs +343 -0
- package/src/index.d.ts +4 -0
- package/src/index.js +4 -0
- package/src/index.js.map +1 -1
- package/src/scripts/architecture-graph.js +8 -2
- package/src/scripts/architecture-graph.js.map +1 -1
- package/src/scripts/create/create-project.d.ts +60 -1
- package/src/scripts/create/create-project.js +2082 -184
- package/src/scripts/create/create-project.js.map +1 -1
- package/src/scripts/data-flow-graph.d.ts +1 -1
- package/src/scripts/effect-dependency-graph.d.ts +1 -1
- package/src/scripts/security-check.d.ts +30 -0
- package/src/scripts/security-check.js +223 -0
- package/src/scripts/security-check.js.map +1 -0
- package/src/scripts/style-architecture.d.ts +56 -0
- package/src/scripts/style-architecture.js +142 -0
- package/src/scripts/style-architecture.js.map +1 -0
- package/src/scripts/style-graph.d.ts +99 -0
- package/src/scripts/style-graph.js +179 -0
- package/src/scripts/style-graph.js.map +1 -0
- package/src/scripts/style-report.d.ts +55 -0
- package/src/scripts/style-report.js +67 -0
- package/src/scripts/style-report.js.map +1 -0
- package/src/scripts/template/migrate-template.js +1 -1
- package/src/scripts/template/migrate-template.js.map +1 -1
- package/src/scripts/angular-brand-codemod.ts +0 -3985
- package/src/scripts/architecture/migrate-architecture.ts +0 -653
- package/src/scripts/architecture-graph.ts +0 -3136
- package/src/scripts/components/migrate-components.ts +0 -227
- package/src/scripts/components/migration-diagnostic.ts +0 -8
- package/src/scripts/create/create-project.ts +0 -849
- package/src/scripts/data-flow-graph.ts +0 -240
- package/src/scripts/dependency-graph.ts +0 -5908
- package/src/scripts/effect-dependency-graph.ts +0 -630
- package/src/scripts/migrate.ts +0 -162
- package/src/scripts/migration-workspace.ts +0 -93
- package/src/scripts/primitives/migrate-named-primitives.ts +0 -627
- package/src/scripts/primitives/migrate-primitive-generators.ts +0 -410
- package/src/scripts/primitives/migrate-primitives.ts +0 -655
- package/src/scripts/primitives/migrate-yieldable-reactive-reads.ts +0 -499
- package/src/scripts/primitives/migration-diagnostic.ts +0 -15
- package/src/scripts/routes/migrate-routes.ts +0 -895
- package/src/scripts/routes/migration-diagnostic.ts +0 -25
- package/src/scripts/routes/route-command.ts +0 -1807
- package/src/scripts/routes/verify-routes.ts +0 -1511
- package/src/scripts/services/config.ts +0 -60
- package/src/scripts/services/migrate-services.ts +0 -1348
- package/src/scripts/services/migration-diagnostic.ts +0 -28
- package/src/scripts/template/migrate-template.ts +0 -418
|
@@ -1,653 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
architectureCatalogToTypeScript,
|
|
6
|
-
buildArchitectureCatalog,
|
|
7
|
-
} from '../architecture-graph.js';
|
|
8
|
-
import {
|
|
9
|
-
analyzeDependencyGraph,
|
|
10
|
-
type DependencyGraph,
|
|
11
|
-
} from '../dependency-graph.js';
|
|
12
|
-
|
|
13
|
-
export type MigrateArchitectureOptions = {
|
|
14
|
-
rootDir?: string;
|
|
15
|
-
tsConfigFilePath?: string;
|
|
16
|
-
write?: boolean;
|
|
17
|
-
check?: boolean;
|
|
18
|
-
json?: boolean;
|
|
19
|
-
jsonFilePath?: string;
|
|
20
|
-
log?: (message: string) => void;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type MigrateArchitectureResult = {
|
|
24
|
-
changedFiles: string[];
|
|
25
|
-
exitCode: number;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type ArchitectureContext = {
|
|
29
|
-
appRoot: string;
|
|
30
|
-
workspaceRoot: string;
|
|
31
|
-
sourceGlob: string;
|
|
32
|
-
projectName: string;
|
|
33
|
-
nx: boolean;
|
|
34
|
-
graphTsConfigRel: string;
|
|
35
|
-
workspaceFromArchitecture: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const RULES = [
|
|
39
|
-
{
|
|
40
|
-
file: 'craft-unique.spec.ts',
|
|
41
|
-
helper: 'assertCraftUnique',
|
|
42
|
-
describe: 'assertCraftUnique',
|
|
43
|
-
it: 'requires craftUnique identities to appear once',
|
|
44
|
-
call: 'assertCraftUnique(graph.graph)',
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
file: 'http-endpoint-unique.spec.ts',
|
|
48
|
-
helper: 'assertHttpEndpointUnique',
|
|
49
|
-
describe: 'assertHttpEndpointUnique',
|
|
50
|
-
it: 'owns each HTTP endpoint once',
|
|
51
|
-
call: 'assertHttpEndpointUnique(graph.graph)',
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
file: 'craft-computed-pure.spec.ts',
|
|
55
|
-
helper: 'assertCraftComputedPure',
|
|
56
|
-
describe: 'assertCraftComputedPure',
|
|
57
|
-
it: 'keeps craftComputed free of methods and source$ writes',
|
|
58
|
-
call: 'assertCraftComputedPure(graph.graph)',
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
file: 'no-dependency-cycles.spec.ts',
|
|
62
|
-
helper: 'assertNoDependencyCycles',
|
|
63
|
-
describe: 'assertNoDependencyCycles',
|
|
64
|
-
it: 'forbids depends-on cycles',
|
|
65
|
-
call: 'assertNoDependencyCycles(graph.graph)',
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
file: 'app-config-route-cycles.spec.ts',
|
|
69
|
-
helper: 'assertNoAppConfigRouteCycles',
|
|
70
|
-
describe: 'assertNoAppConfigRouteCycles',
|
|
71
|
-
it: 'keeps app configuration and route modules acyclic',
|
|
72
|
-
call: 'assertNoAppConfigRouteCycles(graph.graph)',
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
file: 'route-di-proofs.spec.ts',
|
|
76
|
-
helper: 'assertRouteDiProofs',
|
|
77
|
-
describe: 'assertRouteDiProofs',
|
|
78
|
-
it: 'requires a DI proof on every routed component and app-config error screen',
|
|
79
|
-
call: 'assertRouteDiProofs(graph.graph)',
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
file: 'mutation-react-on.spec.ts',
|
|
83
|
-
helper: 'assertMutationHasReactOn',
|
|
84
|
-
describe: 'assertMutationHasReactOn',
|
|
85
|
-
it: 'requires a query to react to each mutation',
|
|
86
|
-
call: 'assertMutationHasReactOn(graph.graph)',
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
file: 'persisted-primitive-unique.spec.ts',
|
|
90
|
-
helper: 'assertPersistedPrimitiveHasUnique',
|
|
91
|
-
describe: 'assertPersistedPrimitiveHasUnique',
|
|
92
|
-
it: 'requires craftUnique on every persisted primitive',
|
|
93
|
-
call: 'assertPersistedPrimitiveHasUnique(graph.graph)',
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
file: 'insert-select-unique.spec.ts',
|
|
97
|
-
helper: 'assertInsertSelectUnique',
|
|
98
|
-
describe: 'assertInsertSelectUnique',
|
|
99
|
-
it: 'keeps insertSelect keys unique on each host',
|
|
100
|
-
call: 'assertInsertSelectUnique(graph.graph)',
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
file: 'craft-effect-no-network.spec.ts',
|
|
104
|
-
helper: 'assertCraftEffectNoNetwork',
|
|
105
|
-
describe: 'assertCraftEffectNoNetwork',
|
|
106
|
-
it: 'keeps craftEffect off HTTP and mutations',
|
|
107
|
-
call: 'assertCraftEffectNoNetwork(graph.graph)',
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
file: 'craft-effect-no-imperative-sync.spec.ts',
|
|
111
|
-
helper: 'assertCraftEffectNoImperativeSync',
|
|
112
|
-
describe: 'assertCraftEffectNoImperativeSync',
|
|
113
|
-
it: 'keeps craftEffect from pushing into state, sources, queries or mutations',
|
|
114
|
-
call: 'assertCraftEffectNoImperativeSync(graph.graph)',
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
file: 'interactive-element-named.spec.ts',
|
|
118
|
-
helper: 'assertInteractiveElementNamed',
|
|
119
|
-
describe: 'assertInteractiveElementNamed',
|
|
120
|
-
it: 'requires a unique literal data-craft-name on every interactive element',
|
|
121
|
-
call: 'assertInteractiveElementNamed(graph.graph)',
|
|
122
|
-
},
|
|
123
|
-
] as const;
|
|
124
|
-
|
|
125
|
-
export async function runArchitectureMigration(
|
|
126
|
-
options: MigrateArchitectureOptions = {},
|
|
127
|
-
): Promise<MigrateArchitectureResult> {
|
|
128
|
-
const context = await resolveContext(options);
|
|
129
|
-
const files = buildScaffoldFiles(context);
|
|
130
|
-
const changedFiles = Object.keys(files).map((relativePath) =>
|
|
131
|
-
join(context.appRoot, relativePath),
|
|
132
|
-
);
|
|
133
|
-
changedFiles.push(join(context.appRoot, 'architecture/catalog.ts'));
|
|
134
|
-
const packagePath = findPackageJson(context.appRoot, context.workspaceRoot);
|
|
135
|
-
const projectPath = join(context.appRoot, 'project.json');
|
|
136
|
-
if (!context.nx && packagePath) {
|
|
137
|
-
changedFiles.push(packagePath);
|
|
138
|
-
}
|
|
139
|
-
if (context.nx && existsSync(projectPath)) {
|
|
140
|
-
changedFiles.push(projectPath);
|
|
141
|
-
}
|
|
142
|
-
const tsconfigPath = join(context.appRoot, 'tsconfig.json');
|
|
143
|
-
if (existsSync(tsconfigPath)) changedFiles.push(tsconfigPath);
|
|
144
|
-
const eslintPath = findEslintConfig(context.appRoot, context.workspaceRoot);
|
|
145
|
-
if (eslintPath) changedFiles.push(eslintPath);
|
|
146
|
-
|
|
147
|
-
if (options.write) {
|
|
148
|
-
const graphConfig = files['tsconfig.graph.json'];
|
|
149
|
-
if (graphConfig) {
|
|
150
|
-
await mkdir(context.appRoot, { recursive: true });
|
|
151
|
-
await writeFile(join(context.appRoot, 'tsconfig.graph.json'), graphConfig, 'utf8');
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
files['architecture/catalog.ts'] = await generateCatalog(context);
|
|
156
|
-
|
|
157
|
-
if (options.write) {
|
|
158
|
-
for (const [relativePath, contents] of Object.entries(files)) {
|
|
159
|
-
const fullPath = join(context.appRoot, relativePath);
|
|
160
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
161
|
-
await writeFile(fullPath, contents, 'utf8');
|
|
162
|
-
}
|
|
163
|
-
if (!context.nx && packagePath) {
|
|
164
|
-
await patchPackageJson(packagePath, context);
|
|
165
|
-
}
|
|
166
|
-
if (context.nx) {
|
|
167
|
-
await patchNxProject(context);
|
|
168
|
-
}
|
|
169
|
-
await patchTsconfigReferences(context.appRoot);
|
|
170
|
-
await patchEslintIgnore(context.appRoot, context.workspaceRoot);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const exitCode =
|
|
174
|
-
options.check && !(await scaffoldMatches(context, files)) ? 1 : 0;
|
|
175
|
-
|
|
176
|
-
const result: MigrateArchitectureResult = {
|
|
177
|
-
changedFiles,
|
|
178
|
-
exitCode,
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
if (options.jsonFilePath) {
|
|
182
|
-
const path = resolve(options.jsonFilePath);
|
|
183
|
-
await mkdir(dirname(path), { recursive: true });
|
|
184
|
-
await writeFile(path, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
185
|
-
}
|
|
186
|
-
const log = options.log ?? console.log;
|
|
187
|
-
if (options.json) log(JSON.stringify(result, null, 2));
|
|
188
|
-
else {
|
|
189
|
-
log(
|
|
190
|
-
`${options.write ? 'Wrote' : 'Would write'} architecture suite: ${result.changedFiles.length} file(s).`,
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return result;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function resolveContext(
|
|
198
|
-
options: MigrateArchitectureOptions,
|
|
199
|
-
): Promise<ArchitectureContext> {
|
|
200
|
-
const rootDir = resolve(options.rootDir ?? process.cwd());
|
|
201
|
-
const tsConfigFilePath = options.tsConfigFilePath
|
|
202
|
-
? resolve(options.tsConfigFilePath)
|
|
203
|
-
: defaultTsConfig(rootDir);
|
|
204
|
-
const appRoot = dirname(tsConfigFilePath);
|
|
205
|
-
const workspaceRoot = findWorkspaceRoot(appRoot);
|
|
206
|
-
const nx = existsSync(join(appRoot, 'project.json'));
|
|
207
|
-
const sourceRel = posixRelative(appRoot, rootDir);
|
|
208
|
-
const sourceGlob =
|
|
209
|
-
!sourceRel || sourceRel === '.' ? 'src' : sourceRel.replace(/\\/g, '/');
|
|
210
|
-
const projectName = await readProjectName(appRoot, nx);
|
|
211
|
-
return {
|
|
212
|
-
appRoot,
|
|
213
|
-
workspaceRoot,
|
|
214
|
-
sourceGlob,
|
|
215
|
-
projectName,
|
|
216
|
-
nx,
|
|
217
|
-
graphTsConfigRel: posixRelative(
|
|
218
|
-
workspaceRoot,
|
|
219
|
-
join(appRoot, 'tsconfig.graph.json'),
|
|
220
|
-
),
|
|
221
|
-
workspaceFromArchitecture: posixRelative(
|
|
222
|
-
join(appRoot, 'architecture'),
|
|
223
|
-
workspaceRoot,
|
|
224
|
-
),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function buildScaffoldFiles(
|
|
229
|
-
context: ArchitectureContext,
|
|
230
|
-
): Record<string, string> {
|
|
231
|
-
return {
|
|
232
|
-
'tsconfig.graph.json': `${JSON.stringify(
|
|
233
|
-
{
|
|
234
|
-
extends: './tsconfig.json',
|
|
235
|
-
compilerOptions: { skipLibCheck: true },
|
|
236
|
-
include: [`${context.sourceGlob}/**/*.ts`],
|
|
237
|
-
exclude: [
|
|
238
|
-
`${context.sourceGlob}/**/*.spec.ts`,
|
|
239
|
-
`${context.sourceGlob}/**/*.test.ts`,
|
|
240
|
-
],
|
|
241
|
-
},
|
|
242
|
-
null,
|
|
243
|
-
2,
|
|
244
|
-
)}\n`,
|
|
245
|
-
'tsconfig.architecture.json': `${JSON.stringify(
|
|
246
|
-
{
|
|
247
|
-
extends: './tsconfig.json',
|
|
248
|
-
compilerOptions: {
|
|
249
|
-
types: ['node', 'vitest/globals'],
|
|
250
|
-
module: 'esnext',
|
|
251
|
-
moduleResolution: 'bundler',
|
|
252
|
-
},
|
|
253
|
-
include: ['architecture/**/*.ts'],
|
|
254
|
-
},
|
|
255
|
-
null,
|
|
256
|
-
2,
|
|
257
|
-
)}\n`,
|
|
258
|
-
'vitest.architecture.config.ts': vitestConfig(context),
|
|
259
|
-
'architecture/load-graph.ts': loadGraphSource(context),
|
|
260
|
-
'architecture/architecture.spec.ts': architectureSpecSource(),
|
|
261
|
-
...Object.fromEntries(
|
|
262
|
-
RULES.map((rule) => [
|
|
263
|
-
`architecture/rules/${rule.file}`,
|
|
264
|
-
ruleSpecSource(rule),
|
|
265
|
-
]),
|
|
266
|
-
),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async function generateCatalog(context: ArchitectureContext): Promise<string> {
|
|
271
|
-
const header = '// Generated. Do not edit.\n';
|
|
272
|
-
try {
|
|
273
|
-
const graph = analyzeDependencyGraph({
|
|
274
|
-
rootDir: context.workspaceRoot,
|
|
275
|
-
tsConfigFilePath: context.graphTsConfigRel,
|
|
276
|
-
});
|
|
277
|
-
return `${header}${architectureCatalogToTypeScript(buildArchitectureCatalog(graph))}`;
|
|
278
|
-
} catch {
|
|
279
|
-
const empty: DependencyGraph = {
|
|
280
|
-
version: 1,
|
|
281
|
-
rootDir: context.workspaceRoot,
|
|
282
|
-
tsConfigFilePath: join(context.workspaceRoot, context.graphTsConfigRel),
|
|
283
|
-
nodes: [],
|
|
284
|
-
edges: [],
|
|
285
|
-
};
|
|
286
|
-
return `${header}${architectureCatalogToTypeScript(buildArchitectureCatalog(empty))}`;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function loadGraphSource(context: ArchitectureContext): string {
|
|
291
|
-
return `import { writeFileSync } from 'node:fs';
|
|
292
|
-
import { join, resolve } from 'node:path';
|
|
293
|
-
import {
|
|
294
|
-
analyzeDependencyGraph,
|
|
295
|
-
architectureCatalogToTypeScript,
|
|
296
|
-
buildArchitectureCatalog,
|
|
297
|
-
createArchitectureGraph,
|
|
298
|
-
} from '@craft-ts/dev-tools';
|
|
299
|
-
import { architectureCatalog } from './catalog';
|
|
300
|
-
|
|
301
|
-
const workspaceRoot = resolve(import.meta.dirname, '${context.workspaceFromArchitecture}');
|
|
302
|
-
const catalogPath = join(import.meta.dirname, 'catalog.ts');
|
|
303
|
-
|
|
304
|
-
let cached: ReturnType<typeof createArchitectureGraph> | undefined;
|
|
305
|
-
|
|
306
|
-
export function loadArchitectureGraph() {
|
|
307
|
-
if (cached) return cached;
|
|
308
|
-
const graph = analyzeDependencyGraph({
|
|
309
|
-
rootDir: workspaceRoot,
|
|
310
|
-
tsConfigFilePath: '${context.graphTsConfigRel}',
|
|
311
|
-
});
|
|
312
|
-
writeFileSync(
|
|
313
|
-
catalogPath,
|
|
314
|
-
\`// Generated. Do not edit.\\n\${architectureCatalogToTypeScript(buildArchitectureCatalog(graph))}\`,
|
|
315
|
-
);
|
|
316
|
-
cached = createArchitectureGraph(graph, architectureCatalog);
|
|
317
|
-
return cached;
|
|
318
|
-
}
|
|
319
|
-
`;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function architectureSpecSource(): string {
|
|
323
|
-
return `import { beforeAll, describe, expect, it } from 'vitest';
|
|
324
|
-
import { loadArchitectureGraph } from './load-graph';
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* App-specific lookups. Common architecture rules live in \`rules/\`.
|
|
328
|
-
*/
|
|
329
|
-
describe('architecture', () => {
|
|
330
|
-
let graph: ReturnType<typeof loadArchitectureGraph>;
|
|
331
|
-
|
|
332
|
-
beforeAll(() => {
|
|
333
|
-
graph = loadArchitectureGraph();
|
|
334
|
-
}, 180_000);
|
|
335
|
-
|
|
336
|
-
it('loads the architecture graph', () => {
|
|
337
|
-
expect(graph.graph.version).toBe(1);
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
`;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function ruleSpecSource(rule: (typeof RULES)[number]): string {
|
|
344
|
-
return `import { beforeAll, describe, it } from 'vitest';
|
|
345
|
-
import { ${rule.helper} } from '@craft-ts/dev-tools';
|
|
346
|
-
import { loadArchitectureGraph } from '../load-graph';
|
|
347
|
-
|
|
348
|
-
describe('${rule.describe}', () => {
|
|
349
|
-
let graph: ReturnType<typeof loadArchitectureGraph>;
|
|
350
|
-
|
|
351
|
-
beforeAll(() => {
|
|
352
|
-
graph = loadArchitectureGraph();
|
|
353
|
-
}, 180_000);
|
|
354
|
-
|
|
355
|
-
it('${rule.it}', () => {
|
|
356
|
-
${rule.call};
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
`;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function vitestConfig(context: ArchitectureContext): string {
|
|
363
|
-
if (context.nx) {
|
|
364
|
-
const cacheRel = posixRelative(
|
|
365
|
-
context.appRoot,
|
|
366
|
-
join(
|
|
367
|
-
context.workspaceRoot,
|
|
368
|
-
'node_modules',
|
|
369
|
-
'.vite',
|
|
370
|
-
`${context.projectName}-architecture`,
|
|
371
|
-
),
|
|
372
|
-
);
|
|
373
|
-
return `/// <reference types="vitest" />
|
|
374
|
-
import { defineConfig } from 'vite';
|
|
375
|
-
|
|
376
|
-
export default defineConfig(() => ({
|
|
377
|
-
root: import.meta.dirname,
|
|
378
|
-
cacheDir: ${JSON.stringify(cacheRel)},
|
|
379
|
-
plugins: [],
|
|
380
|
-
resolve: {
|
|
381
|
-
tsconfigPaths: true,
|
|
382
|
-
},
|
|
383
|
-
test: {
|
|
384
|
-
name: ${JSON.stringify(`${context.projectName}-architecture`)},
|
|
385
|
-
watch: false,
|
|
386
|
-
globals: true,
|
|
387
|
-
environment: 'node',
|
|
388
|
-
testTimeout: 180_000,
|
|
389
|
-
hookTimeout: 180_000,
|
|
390
|
-
include: ['architecture/**/*.spec.ts'],
|
|
391
|
-
},
|
|
392
|
-
}));
|
|
393
|
-
`;
|
|
394
|
-
}
|
|
395
|
-
return `/// <reference types="vitest" />
|
|
396
|
-
import { defineConfig } from 'vite';
|
|
397
|
-
|
|
398
|
-
export default defineConfig(() => ({
|
|
399
|
-
root: import.meta.dirname,
|
|
400
|
-
cacheDir: ${JSON.stringify(`node_modules/.vite/${context.projectName}-architecture`)},
|
|
401
|
-
test: {
|
|
402
|
-
name: ${JSON.stringify(`${context.projectName}-architecture`)},
|
|
403
|
-
watch: false,
|
|
404
|
-
globals: true,
|
|
405
|
-
environment: 'node',
|
|
406
|
-
testTimeout: 180_000,
|
|
407
|
-
hookTimeout: 180_000,
|
|
408
|
-
include: ['architecture/**/*.spec.ts'],
|
|
409
|
-
},
|
|
410
|
-
}));
|
|
411
|
-
`;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function defaultTsConfig(rootDir: string): string {
|
|
415
|
-
for (const name of ['tsconfig.app.json', 'tsconfig.json']) {
|
|
416
|
-
const candidate = join(rootDir, name);
|
|
417
|
-
if (existsSync(candidate)) return candidate;
|
|
418
|
-
const parent = join(dirname(rootDir), name);
|
|
419
|
-
if (existsSync(parent)) return parent;
|
|
420
|
-
}
|
|
421
|
-
return join(rootDir, 'tsconfig.json');
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function findWorkspaceRoot(appRoot: string): string {
|
|
425
|
-
let current = appRoot;
|
|
426
|
-
while (true) {
|
|
427
|
-
if (
|
|
428
|
-
existsSync(join(current, 'nx.json')) ||
|
|
429
|
-
existsSync(join(current, 'angular.json'))
|
|
430
|
-
) {
|
|
431
|
-
return current;
|
|
432
|
-
}
|
|
433
|
-
const parent = dirname(current);
|
|
434
|
-
if (parent === current) return appRoot;
|
|
435
|
-
current = parent;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async function readProjectName(appRoot: string, nx: boolean): Promise<string> {
|
|
440
|
-
if (nx) {
|
|
441
|
-
try {
|
|
442
|
-
const project = JSON.parse(
|
|
443
|
-
await readFile(join(appRoot, 'project.json'), 'utf8'),
|
|
444
|
-
) as { name?: string };
|
|
445
|
-
if (project.name) return sanitizeName(project.name);
|
|
446
|
-
} catch {
|
|
447
|
-
/* fall through */
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
try {
|
|
451
|
-
const pkg = JSON.parse(
|
|
452
|
-
await readFile(join(appRoot, 'package.json'), 'utf8'),
|
|
453
|
-
) as { name?: string };
|
|
454
|
-
if (pkg.name) return sanitizeName(pkg.name);
|
|
455
|
-
} catch {
|
|
456
|
-
/* fall through */
|
|
457
|
-
}
|
|
458
|
-
return sanitizeName(appRoot.split(/[\\/]/).at(-1) ?? 'app');
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function sanitizeName(name: string): string {
|
|
462
|
-
return name.replace(/^@.*\//, '').replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function posixRelative(from: string, to: string): string {
|
|
466
|
-
const value = relative(from, to).split('\\').join('/');
|
|
467
|
-
return value === '' ? '.' : value;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
async function patchNxProject(context: ArchitectureContext): Promise<void> {
|
|
471
|
-
const projectPath = join(context.appRoot, 'project.json');
|
|
472
|
-
if (!existsSync(projectPath)) return;
|
|
473
|
-
const project = JSON.parse(await readFile(projectPath, 'utf8')) as {
|
|
474
|
-
targets?: Record<string, unknown>;
|
|
475
|
-
};
|
|
476
|
-
const cwd = posixRelative(context.workspaceRoot, context.appRoot);
|
|
477
|
-
project.targets = {
|
|
478
|
-
...project.targets,
|
|
479
|
-
architecture: {
|
|
480
|
-
executor: 'nx:run-commands',
|
|
481
|
-
options: {
|
|
482
|
-
command: 'npx vitest run --config vitest.architecture.config.ts',
|
|
483
|
-
cwd,
|
|
484
|
-
},
|
|
485
|
-
inputs: [
|
|
486
|
-
'{projectRoot}/src/**/*.ts',
|
|
487
|
-
'{projectRoot}/architecture/**/*.ts',
|
|
488
|
-
'{projectRoot}/tsconfig.graph.json',
|
|
489
|
-
'{projectRoot}/tsconfig.architecture.json',
|
|
490
|
-
],
|
|
491
|
-
cache: true,
|
|
492
|
-
},
|
|
493
|
-
'typecheck-architecture': {
|
|
494
|
-
executor: 'nx:run-commands',
|
|
495
|
-
options: {
|
|
496
|
-
cwd,
|
|
497
|
-
command: 'tsc -p tsconfig.architecture.json --noEmit --pretty false',
|
|
498
|
-
},
|
|
499
|
-
inputs: [
|
|
500
|
-
'{projectRoot}/tsconfig.json',
|
|
501
|
-
'{projectRoot}/tsconfig.architecture.json',
|
|
502
|
-
'{projectRoot}/architecture/**/*.ts',
|
|
503
|
-
],
|
|
504
|
-
cache: true,
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
await writeFile(projectPath, `${JSON.stringify(project, null, 2)}\n`, 'utf8');
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
async function patchPackageJson(
|
|
511
|
-
packagePath: string,
|
|
512
|
-
context: ArchitectureContext,
|
|
513
|
-
): Promise<void> {
|
|
514
|
-
const pkg = JSON.parse(await readFile(packagePath, 'utf8')) as {
|
|
515
|
-
scripts?: Record<string, string>;
|
|
516
|
-
};
|
|
517
|
-
const fromPkg = dirname(packagePath);
|
|
518
|
-
const vitestConfig = posixRelative(
|
|
519
|
-
fromPkg,
|
|
520
|
-
join(context.appRoot, 'vitest.architecture.config.ts'),
|
|
521
|
-
);
|
|
522
|
-
const architectureTsconfig = posixRelative(
|
|
523
|
-
fromPkg,
|
|
524
|
-
join(context.appRoot, 'tsconfig.architecture.json'),
|
|
525
|
-
);
|
|
526
|
-
pkg.scripts = {
|
|
527
|
-
...pkg.scripts,
|
|
528
|
-
architecture: `vitest run --config ${vitestConfig}`,
|
|
529
|
-
'typecheck-architecture': `tsc -p ${architectureTsconfig} --noEmit --pretty false`,
|
|
530
|
-
};
|
|
531
|
-
await writeFile(packagePath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
async function scaffoldMatches(
|
|
535
|
-
context: ArchitectureContext,
|
|
536
|
-
files: Record<string, string>,
|
|
537
|
-
): Promise<boolean> {
|
|
538
|
-
for (const [relativePath, contents] of Object.entries(files)) {
|
|
539
|
-
const fullPath = join(context.appRoot, relativePath);
|
|
540
|
-
if (!existsSync(fullPath)) return false;
|
|
541
|
-
if (
|
|
542
|
-
relativePath === 'architecture/catalog.ts' ||
|
|
543
|
-
relativePath === 'architecture/architecture.spec.ts'
|
|
544
|
-
) {
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
const actual = await readFile(fullPath, 'utf8');
|
|
548
|
-
if (normalize(actual) !== normalize(contents)) return false;
|
|
549
|
-
}
|
|
550
|
-
if (context.nx) {
|
|
551
|
-
const projectPath = join(context.appRoot, 'project.json');
|
|
552
|
-
if (!existsSync(projectPath)) return false;
|
|
553
|
-
const project = JSON.parse(await readFile(projectPath, 'utf8')) as {
|
|
554
|
-
targets?: Record<string, { options?: { command?: string } }>;
|
|
555
|
-
};
|
|
556
|
-
return Boolean(project.targets?.['architecture']?.options?.command);
|
|
557
|
-
}
|
|
558
|
-
const packagePath = findPackageJson(context.appRoot, context.workspaceRoot);
|
|
559
|
-
if (!packagePath) return false;
|
|
560
|
-
const pkg = JSON.parse(await readFile(packagePath, 'utf8')) as {
|
|
561
|
-
scripts?: Record<string, string>;
|
|
562
|
-
};
|
|
563
|
-
return pkg.scripts?.['architecture']?.includes('vitest.architecture.config.ts') === true;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function normalize(value: string): string {
|
|
567
|
-
return value.replace(/\r\n/g, '\n');
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const ESLINT_CONFIG_NAMES = [
|
|
571
|
-
'eslint.config.js',
|
|
572
|
-
'eslint.config.mjs',
|
|
573
|
-
'eslint.config.cjs',
|
|
574
|
-
'eslint.config.ts',
|
|
575
|
-
'eslint.config.mts',
|
|
576
|
-
'eslint.config.cts',
|
|
577
|
-
];
|
|
578
|
-
|
|
579
|
-
async function patchTsconfigReferences(appRoot: string): Promise<void> {
|
|
580
|
-
const tsconfigPath = join(appRoot, 'tsconfig.json');
|
|
581
|
-
if (!existsSync(tsconfigPath)) return;
|
|
582
|
-
const text = await readFile(tsconfigPath, 'utf8');
|
|
583
|
-
let parsed: { references?: { path: string }[] };
|
|
584
|
-
try {
|
|
585
|
-
parsed = JSON.parse(text) as { references?: { path: string }[] };
|
|
586
|
-
} catch {
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
const references = parsed.references ?? [];
|
|
590
|
-
if (references.some((ref) => ref.path === './tsconfig.architecture.json')) {
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
parsed.references = [...references, { path: './tsconfig.architecture.json' }];
|
|
594
|
-
await writeFile(tsconfigPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
async function patchEslintIgnore(
|
|
598
|
-
appRoot: string,
|
|
599
|
-
workspaceRoot: string,
|
|
600
|
-
): Promise<void> {
|
|
601
|
-
const eslintPath = findEslintConfig(appRoot, workspaceRoot);
|
|
602
|
-
if (!eslintPath) return;
|
|
603
|
-
const text = await readFile(eslintPath, 'utf8');
|
|
604
|
-
if (text.includes('architecture/catalog.ts')) return;
|
|
605
|
-
let next = text;
|
|
606
|
-
if (/ignores:\s*\[/.test(next)) {
|
|
607
|
-
next = next.replace(
|
|
608
|
-
/ignores:\s*\[/,
|
|
609
|
-
"ignores: ['**/architecture/catalog.ts', ",
|
|
610
|
-
);
|
|
611
|
-
} else if (/export default\s*(?:defineConfig\s*\()?\[/.test(next)) {
|
|
612
|
-
next = next.replace(
|
|
613
|
-
/export default\s*(?:defineConfig\s*\()?\[/,
|
|
614
|
-
(match) => `${match}\n { ignores: ['**/architecture/catalog.ts'] },`,
|
|
615
|
-
);
|
|
616
|
-
} else {
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
await writeFile(eslintPath, next, 'utf8');
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function findEslintConfig(
|
|
623
|
-
startDir: string,
|
|
624
|
-
stopDir: string,
|
|
625
|
-
): string | undefined {
|
|
626
|
-
return findUpFile(startDir, ESLINT_CONFIG_NAMES, stopDir);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function findPackageJson(
|
|
630
|
-
startDir: string,
|
|
631
|
-
stopDir: string,
|
|
632
|
-
): string | undefined {
|
|
633
|
-
return findUpFile(startDir, ['package.json'], stopDir);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function findUpFile(
|
|
637
|
-
startDir: string,
|
|
638
|
-
names: readonly string[],
|
|
639
|
-
stopDir: string,
|
|
640
|
-
): string | undefined {
|
|
641
|
-
const stop = resolve(stopDir);
|
|
642
|
-
let current = resolve(startDir);
|
|
643
|
-
while (true) {
|
|
644
|
-
for (const name of names) {
|
|
645
|
-
const candidate = join(current, name);
|
|
646
|
-
if (existsSync(candidate)) return candidate;
|
|
647
|
-
}
|
|
648
|
-
if (current === stop) return undefined;
|
|
649
|
-
const parent = dirname(current);
|
|
650
|
-
if (parent === current) return undefined;
|
|
651
|
-
current = parent;
|
|
652
|
-
}
|
|
653
|
-
}
|