@echojs-ecosystem/architect 0.1.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 +89 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +791 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +711 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs2 from 'fs';
|
|
3
|
+
import { parse, sep, join, extname, resolve, dirname, relative, basename } from 'path';
|
|
4
|
+
import { minimatch } from 'minimatch';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import { loadConfig, watchConfig as watchConfig$1 } from 'c12';
|
|
7
|
+
import { from, map, switchMap, Observable, filter, debounceTime } from 'rxjs';
|
|
8
|
+
import * as process from 'process';
|
|
9
|
+
import { fromError } from 'zod-validation-error';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import precinct from 'precinct';
|
|
12
|
+
import { parse as parse$1 } from 'tsconfck';
|
|
13
|
+
import chokidar from 'chokidar';
|
|
14
|
+
import { isGitIgnored } from 'globby';
|
|
15
|
+
import { produce } from 'immer';
|
|
16
|
+
import isGlob from 'is-glob';
|
|
17
|
+
|
|
18
|
+
var normalizeFileTemplate = (fileTemplate, fileTemplateUrl) => {
|
|
19
|
+
if (fileTemplateUrl) {
|
|
20
|
+
return () => fs2.readFileSync(fileTemplateUrl, "utf-8");
|
|
21
|
+
}
|
|
22
|
+
if (!fileTemplate) {
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
if (typeof fileTemplate === "string") {
|
|
26
|
+
return () => fileTemplate;
|
|
27
|
+
}
|
|
28
|
+
return fileTemplate;
|
|
29
|
+
};
|
|
30
|
+
var abstraction = (optionsOrName, optionalConfig) => {
|
|
31
|
+
if (typeof optionsOrName === "string") {
|
|
32
|
+
return {
|
|
33
|
+
name: optionsOrName,
|
|
34
|
+
children: optionalConfig?.children ?? {},
|
|
35
|
+
rules: optionalConfig?.rules ?? [],
|
|
36
|
+
fractal: optionalConfig?.fractal,
|
|
37
|
+
fileTemplate: normalizeFileTemplate(
|
|
38
|
+
optionalConfig?.fileTemplate,
|
|
39
|
+
optionalConfig?.fileTemplateUrl
|
|
40
|
+
)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
name: optionsOrName.name,
|
|
45
|
+
children: optionsOrName.children ?? {},
|
|
46
|
+
rules: optionsOrName.rules ?? [],
|
|
47
|
+
fractal: optionsOrName.fractal,
|
|
48
|
+
fileTemplate: normalizeFileTemplate(
|
|
49
|
+
optionsOrName.fileTemplate,
|
|
50
|
+
optionsOrName?.fileTemplateUrl
|
|
51
|
+
)
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
var getAbstractionInstanceLabel = (instance) => {
|
|
55
|
+
const { name } = parse(instance.path);
|
|
56
|
+
if (name === instance.abstraction.name) {
|
|
57
|
+
return name;
|
|
58
|
+
}
|
|
59
|
+
return `${name} (${instance.abstraction.name})`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/shared/memoize.ts
|
|
63
|
+
var memoize = (fn) => {
|
|
64
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
65
|
+
return function(arg) {
|
|
66
|
+
if (cache.has(arg)) {
|
|
67
|
+
return cache.get(arg);
|
|
68
|
+
}
|
|
69
|
+
const result = fn.call(this, arg);
|
|
70
|
+
cache.set(arg, result);
|
|
71
|
+
return result;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
var resolveImport = (importedPath, importerPath, tsCompilerOptions, fileExists, directoryExists) => {
|
|
75
|
+
return ts.resolveModuleName(
|
|
76
|
+
importedPath,
|
|
77
|
+
importerPath,
|
|
78
|
+
normalizeCompilerOptions(tsCompilerOptions),
|
|
79
|
+
{
|
|
80
|
+
...ts.sys,
|
|
81
|
+
fileExists,
|
|
82
|
+
directoryExists
|
|
83
|
+
}
|
|
84
|
+
).resolvedModule?.resolvedFileName?.replaceAll("/", sep) ?? null;
|
|
85
|
+
};
|
|
86
|
+
var imperfectKeys = {
|
|
87
|
+
module: ts.ModuleKind,
|
|
88
|
+
moduleResolution: {
|
|
89
|
+
...ts.ModuleResolutionKind,
|
|
90
|
+
node: ts.ModuleResolutionKind.Node10
|
|
91
|
+
},
|
|
92
|
+
moduleDetection: ts.ModuleDetectionKind,
|
|
93
|
+
newLine: ts.NewLineKind,
|
|
94
|
+
target: ts.ScriptTarget
|
|
95
|
+
};
|
|
96
|
+
var normalizeCompilerOptions = (compilerOptions) => {
|
|
97
|
+
return Object.fromEntries(
|
|
98
|
+
Object.entries(compilerOptions).map(([key, value]) => {
|
|
99
|
+
if (Object.keys(imperfectKeys).includes(key) && typeof value === "string") {
|
|
100
|
+
for (const [enumKey, enumValue] of Object.entries(
|
|
101
|
+
imperfectKeys[key]
|
|
102
|
+
)) {
|
|
103
|
+
if (enumKey.toLowerCase() === value.toLowerCase()) {
|
|
104
|
+
return [key, enumValue];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [key, value];
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/abstraction/instance/parse.ts
|
|
114
|
+
var parseAbstractionInstance = memoize(
|
|
115
|
+
(abstraction2) => memoize((node) => {
|
|
116
|
+
if (node.type === "file") {
|
|
117
|
+
return {
|
|
118
|
+
abstraction: abstraction2,
|
|
119
|
+
path: node.path,
|
|
120
|
+
children: [],
|
|
121
|
+
childNodes: []
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const children = {};
|
|
125
|
+
for (const [pattern, childAbstraction] of Object.entries(
|
|
126
|
+
abstraction2.children
|
|
127
|
+
)) {
|
|
128
|
+
const nodeAbstractionInstance = parseAbstractionInstance(childAbstraction);
|
|
129
|
+
const nodesStack = [node];
|
|
130
|
+
while (nodesStack.length) {
|
|
131
|
+
const currentNode = nodesStack.pop();
|
|
132
|
+
if (minimatch(relative(node.path, currentNode.path), pattern)) {
|
|
133
|
+
children[currentNode.path] = nodeAbstractionInstance(currentNode);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (children[currentNode.path]) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (currentNode.type === "folder") {
|
|
140
|
+
nodesStack.push(...currentNode.children);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const childNodes = [];
|
|
145
|
+
const childrenNodesStack = [node];
|
|
146
|
+
while (childrenNodesStack.length) {
|
|
147
|
+
const currentNode = childrenNodesStack.pop();
|
|
148
|
+
if (children[currentNode.path]) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (currentNode.path !== node.path) {
|
|
152
|
+
childNodes.push(currentNode.path);
|
|
153
|
+
}
|
|
154
|
+
if (currentNode.type === "folder") {
|
|
155
|
+
childrenNodesStack.push(...currentNode.children);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
abstraction: abstraction2,
|
|
160
|
+
path: node.path,
|
|
161
|
+
childNodes: Object.values(childNodes),
|
|
162
|
+
children: Object.values(children)
|
|
163
|
+
};
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// src/config/define-config.ts
|
|
168
|
+
var defineConfig = (config) => config;
|
|
169
|
+
var ConfigurationNotFoundError = class extends Error {
|
|
170
|
+
constructor() {
|
|
171
|
+
super(`Configuration not found in ${process.cwd()}`);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var ConfigurationInvalidError = class extends Error {
|
|
175
|
+
constructor(error, filepath) {
|
|
176
|
+
super(
|
|
177
|
+
fromError(error, {
|
|
178
|
+
prefix: `Invalid configuration in ${relative(process.cwd(), filepath)}`
|
|
179
|
+
}).toString()
|
|
180
|
+
);
|
|
181
|
+
this.error = error;
|
|
182
|
+
}
|
|
183
|
+
error;
|
|
184
|
+
};
|
|
185
|
+
var RuleSchema = z.object({
|
|
186
|
+
name: z.string(),
|
|
187
|
+
check: z.custom(),
|
|
188
|
+
severity: z.enum(["off", "warn", "error"]),
|
|
189
|
+
descriptionUrl: z.string().optional()
|
|
190
|
+
});
|
|
191
|
+
var AbstractionSchema = z.object({
|
|
192
|
+
name: z.string(),
|
|
193
|
+
children: z.record(z.lazy(() => AbstractionSchema)),
|
|
194
|
+
rules: z.array(RuleSchema),
|
|
195
|
+
fractal: z.string().optional(),
|
|
196
|
+
fileTemplate: z.custom()
|
|
197
|
+
});
|
|
198
|
+
var EvolutionConfigSchema = z.object({
|
|
199
|
+
root: AbstractionSchema,
|
|
200
|
+
baseUrl: z.string().optional(),
|
|
201
|
+
files: z.array(z.string()).optional(),
|
|
202
|
+
ignores: z.array(z.string()).optional()
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// src/config/load.ts
|
|
206
|
+
var CONFIG_NAME = "architect";
|
|
207
|
+
var parseConfigResult = (filepath, data) => {
|
|
208
|
+
const parseResult = EvolutionConfigSchema.safeParse(data);
|
|
209
|
+
if (!parseResult.success) {
|
|
210
|
+
throw new ConfigurationInvalidError(parseResult.error, filepath);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
config: parseResult.data,
|
|
214
|
+
configPath: filepath
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
var watchConfig = ({
|
|
218
|
+
cwd: cwd2,
|
|
219
|
+
onlyOne
|
|
220
|
+
}) => {
|
|
221
|
+
const config$ = from(
|
|
222
|
+
loadConfig({
|
|
223
|
+
cwd: cwd2,
|
|
224
|
+
name: CONFIG_NAME
|
|
225
|
+
})
|
|
226
|
+
).pipe(
|
|
227
|
+
map(({ configFile, config }) => {
|
|
228
|
+
if (!configFile) {
|
|
229
|
+
throw new ConfigurationNotFoundError();
|
|
230
|
+
}
|
|
231
|
+
return parseConfigResult(configFile, config);
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
if (onlyOne) {
|
|
235
|
+
return config$;
|
|
236
|
+
}
|
|
237
|
+
return config$.pipe(
|
|
238
|
+
switchMap(
|
|
239
|
+
({ configPath, config }) => new Observable((subscriber) => {
|
|
240
|
+
subscriber.next({ configPath, config });
|
|
241
|
+
let unwatchCallback = () => {
|
|
242
|
+
};
|
|
243
|
+
watchConfig$1({
|
|
244
|
+
cwd: cwd2,
|
|
245
|
+
name: CONFIG_NAME,
|
|
246
|
+
onUpdate: (config2) => {
|
|
247
|
+
subscriber.next(
|
|
248
|
+
parseConfigResult(configPath, config2.newConfig.config)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}).then(({ unwatch }) => {
|
|
252
|
+
unwatchCallback = unwatch;
|
|
253
|
+
});
|
|
254
|
+
return () => unwatchCallback();
|
|
255
|
+
})
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// src/vfs/get-flatten-files.ts
|
|
261
|
+
var getFlattenFiles = memoize((node) => {
|
|
262
|
+
if (node.type === "file") {
|
|
263
|
+
return [node];
|
|
264
|
+
}
|
|
265
|
+
return node.children.reduce((acc, child) => {
|
|
266
|
+
if (child.type === "file") {
|
|
267
|
+
return [...acc, child];
|
|
268
|
+
}
|
|
269
|
+
return [...acc, ...getFlattenFiles(child)];
|
|
270
|
+
}, []);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// src/dependencies/parse.ts
|
|
274
|
+
var { paperwork } = precinct;
|
|
275
|
+
var parseDependenciesMap = async (vfs) => {
|
|
276
|
+
const dependenciesMap = {
|
|
277
|
+
dependencies: {},
|
|
278
|
+
dependencyFor: {}
|
|
279
|
+
};
|
|
280
|
+
const basePath = vfs.path;
|
|
281
|
+
const { tsconfig } = await parse$1(basePath);
|
|
282
|
+
const files = getFlattenFiles(vfs);
|
|
283
|
+
for (const file of files) {
|
|
284
|
+
const dependencies = paperwork(file.path, {
|
|
285
|
+
includeCore: false,
|
|
286
|
+
fileSystem: fs2
|
|
287
|
+
});
|
|
288
|
+
const resolvedDependencies = dependencies.map(
|
|
289
|
+
(dependency) => resolveImport(
|
|
290
|
+
dependency,
|
|
291
|
+
file.path,
|
|
292
|
+
tsconfig?.compilerOptions ?? {},
|
|
293
|
+
fs2.existsSync,
|
|
294
|
+
fs2.existsSync
|
|
295
|
+
)
|
|
296
|
+
).filter((dependency) => dependency !== null);
|
|
297
|
+
dependenciesMap.dependencies[file.path] = new Set(resolvedDependencies);
|
|
298
|
+
for (const dependency of resolvedDependencies) {
|
|
299
|
+
if (!dependenciesMap.dependencyFor[dependency]) {
|
|
300
|
+
dependenciesMap.dependencyFor[dependency] = /* @__PURE__ */ new Set();
|
|
301
|
+
}
|
|
302
|
+
dependenciesMap.dependencyFor[dependency].add(file.path);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return dependenciesMap;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// src/rule/rule.ts
|
|
309
|
+
var rule = (config) => {
|
|
310
|
+
const defaultCheck = () => ({ diagnostics: [] });
|
|
311
|
+
if (typeof config === "string") {
|
|
312
|
+
return {
|
|
313
|
+
name: config,
|
|
314
|
+
severity: "error",
|
|
315
|
+
descriptionUrl: void 0,
|
|
316
|
+
check: defaultCheck
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
name: config.name,
|
|
321
|
+
severity: config.severity ?? "error",
|
|
322
|
+
descriptionUrl: config.descriptionUrl,
|
|
323
|
+
check: config.check ?? defaultCheck
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// src/vfs/get-nodes-record.ts
|
|
328
|
+
var getNodesRecord = memoize(
|
|
329
|
+
(node) => {
|
|
330
|
+
if (node.type === "file") {
|
|
331
|
+
return { [node.path]: node };
|
|
332
|
+
}
|
|
333
|
+
return node.children.reduce(
|
|
334
|
+
(acc, child) => {
|
|
335
|
+
if (child.type === "file") {
|
|
336
|
+
return { ...acc, [child.path]: child };
|
|
337
|
+
}
|
|
338
|
+
return { ...acc, [child.path]: child, ...getNodesRecord(child) };
|
|
339
|
+
},
|
|
340
|
+
{}
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
var addDirectory = (tree, newDirectoryPath) => {
|
|
345
|
+
const rootPath = tree.path;
|
|
346
|
+
return produce(tree, (draft) => {
|
|
347
|
+
const pathSegments = relative(rootPath, newDirectoryPath).split(sep);
|
|
348
|
+
let currentFolder = draft;
|
|
349
|
+
for (const pathSegment of pathSegments.slice(0, -1)) {
|
|
350
|
+
const existingChild = currentFolder.children.find(
|
|
351
|
+
(child) => child.type === "folder" && basename(child.path) === pathSegment
|
|
352
|
+
);
|
|
353
|
+
if (existingChild === void 0) {
|
|
354
|
+
currentFolder.children.push({
|
|
355
|
+
type: "folder",
|
|
356
|
+
path: join(currentFolder.path, pathSegment),
|
|
357
|
+
children: []
|
|
358
|
+
});
|
|
359
|
+
currentFolder = currentFolder.children[currentFolder.children.length - 1];
|
|
360
|
+
} else {
|
|
361
|
+
currentFolder = existingChild;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
currentFolder.children.push({
|
|
365
|
+
type: "folder",
|
|
366
|
+
path: newDirectoryPath,
|
|
367
|
+
children: []
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
var addFile = (tree, newFilePath) => {
|
|
372
|
+
const rootPath = tree.path;
|
|
373
|
+
return produce(tree, (draft) => {
|
|
374
|
+
const pathSegments = relative(rootPath, newFilePath).split(sep);
|
|
375
|
+
let currentFolder = draft;
|
|
376
|
+
for (const pathSegment of pathSegments.slice(0, -1)) {
|
|
377
|
+
const existingChild = currentFolder.children.find(
|
|
378
|
+
(child) => child.type === "folder" && basename(child.path) === pathSegment
|
|
379
|
+
);
|
|
380
|
+
if (existingChild === void 0) {
|
|
381
|
+
currentFolder.children.push({
|
|
382
|
+
type: "folder",
|
|
383
|
+
path: join(currentFolder.path, pathSegment),
|
|
384
|
+
children: []
|
|
385
|
+
});
|
|
386
|
+
currentFolder = currentFolder.children[currentFolder.children.length - 1];
|
|
387
|
+
} else {
|
|
388
|
+
currentFolder = existingChild;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
currentFolder.children.push({ type: "file", path: newFilePath });
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/vfs/create-root.ts
|
|
396
|
+
var createVfsRoot = (path) => ({
|
|
397
|
+
type: "folder",
|
|
398
|
+
path,
|
|
399
|
+
children: []
|
|
400
|
+
});
|
|
401
|
+
var removeNode = (tree, removedNodePath) => {
|
|
402
|
+
const rootPath = tree.path;
|
|
403
|
+
return produce(tree, (draft) => {
|
|
404
|
+
const pathSegments = relative(rootPath, removedNodePath).split(sep);
|
|
405
|
+
let currentFolder = draft;
|
|
406
|
+
for (const pathSegment of pathSegments.slice(0, -1)) {
|
|
407
|
+
const existingChild = currentFolder.children.find(
|
|
408
|
+
(child) => child.type === "folder" && basename(child.path) === pathSegment
|
|
409
|
+
);
|
|
410
|
+
if (existingChild === void 0) {
|
|
411
|
+
return tree;
|
|
412
|
+
} else {
|
|
413
|
+
currentFolder = existingChild;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const removedNodeIndex = currentFolder.children.findIndex(
|
|
417
|
+
(child) => child.path === removedNodePath
|
|
418
|
+
);
|
|
419
|
+
if (removedNodeIndex === -1) {
|
|
420
|
+
return tree;
|
|
421
|
+
}
|
|
422
|
+
currentFolder.children.splice(removedNodeIndex, 1);
|
|
423
|
+
});
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// src/vfs/watch-fs.ts
|
|
427
|
+
var watchFs = (path, { onlyReady } = {}) => {
|
|
428
|
+
const isIgnored$ = from(isGitIgnored({ cwd: path }));
|
|
429
|
+
let vfs$ = isIgnored$.pipe(
|
|
430
|
+
switchMap((isIgnored) => createWatcherObservable({ path, isIgnored }))
|
|
431
|
+
);
|
|
432
|
+
if (onlyReady) {
|
|
433
|
+
vfs$ = vfs$.pipe(filter((e) => e.type === "ready"));
|
|
434
|
+
}
|
|
435
|
+
return vfs$;
|
|
436
|
+
};
|
|
437
|
+
var createWatcherObservable = ({
|
|
438
|
+
path,
|
|
439
|
+
isIgnored
|
|
440
|
+
}) => {
|
|
441
|
+
return new Observable((observer) => {
|
|
442
|
+
let vfs = createVfsRoot(path);
|
|
443
|
+
const watcher = chokidar.watch(path, {
|
|
444
|
+
ignored: (path2) => path2.split(sep).includes("node_modules") || isIgnored(path2),
|
|
445
|
+
ignoreInitial: false,
|
|
446
|
+
alwaysStat: true,
|
|
447
|
+
awaitWriteFinish: true,
|
|
448
|
+
disableGlobbing: true,
|
|
449
|
+
cwd: path
|
|
450
|
+
});
|
|
451
|
+
watcher.on("add", async (relativePath) => {
|
|
452
|
+
vfs = addFile(vfs, join(path, relativePath));
|
|
453
|
+
observer.next({ type: "add", vfs });
|
|
454
|
+
});
|
|
455
|
+
watcher.on("addDir", async (relativePath) => {
|
|
456
|
+
vfs = addDirectory(vfs, join(path, relativePath));
|
|
457
|
+
observer.next({ type: "addDir", vfs });
|
|
458
|
+
});
|
|
459
|
+
watcher.on("change", async () => {
|
|
460
|
+
observer.next({ type: "change", vfs });
|
|
461
|
+
});
|
|
462
|
+
watcher.on("unlink", async (relativePath) => {
|
|
463
|
+
vfs = removeNode(vfs, join(path, relativePath));
|
|
464
|
+
observer.next({ type: "unlink", vfs });
|
|
465
|
+
});
|
|
466
|
+
watcher.on("unlinkDir", async (relativePath) => {
|
|
467
|
+
vfs = removeNode(vfs, join(path, relativePath));
|
|
468
|
+
observer.next({ type: "unlinkDir", vfs });
|
|
469
|
+
});
|
|
470
|
+
watcher.on("ready", () => {
|
|
471
|
+
observer.next({ type: "ready", vfs });
|
|
472
|
+
});
|
|
473
|
+
return () => {
|
|
474
|
+
watcher.close();
|
|
475
|
+
};
|
|
476
|
+
});
|
|
477
|
+
};
|
|
478
|
+
var matchesAllowDownward = (dependencyPath, patterns) => {
|
|
479
|
+
const normalized = dependencyPath.replace(/\\/g, "/");
|
|
480
|
+
return patterns.some(
|
|
481
|
+
(pattern) => minimatch(normalized, pattern) || minimatch(normalized, `**/${pattern}`)
|
|
482
|
+
);
|
|
483
|
+
};
|
|
484
|
+
var off = (rule2) => {
|
|
485
|
+
if (Array.isArray(rule2)) {
|
|
486
|
+
return rule2.map((r) => ({ ...r, severity: "off" }));
|
|
487
|
+
}
|
|
488
|
+
return { ...rule2, severity: "off" };
|
|
489
|
+
};
|
|
490
|
+
var warn = (rule2) => {
|
|
491
|
+
if (Array.isArray(rule2)) {
|
|
492
|
+
return rule2.map((r) => ({ ...r, severity: "warn" }));
|
|
493
|
+
}
|
|
494
|
+
return { ...rule2, severity: "warn" };
|
|
495
|
+
};
|
|
496
|
+
var requiredChildren = (abstractions) => rule({
|
|
497
|
+
name: "default/required-children",
|
|
498
|
+
severity: "error",
|
|
499
|
+
check: ({ instance, root }) => {
|
|
500
|
+
const diagnostics = [];
|
|
501
|
+
const nodesRecord = getNodesRecord(root);
|
|
502
|
+
const reuqiredAbstractions = Object.entries(instance.abstraction.children).filter(([ext]) => !isGlob(ext)).filter(
|
|
503
|
+
([, abstraction2]) => !abstractions || abstractions.includes(abstraction2.name)
|
|
504
|
+
);
|
|
505
|
+
for (const [ext, abstraction2] of reuqiredAbstractions) {
|
|
506
|
+
const path = join(instance.path, ext);
|
|
507
|
+
const instanceNode = nodesRecord[path];
|
|
508
|
+
if (instanceNode !== void 0) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const message = `Required abstraction "${abstraction2.name}" in "${getAbstractionInstanceLabel(instance)}"`;
|
|
512
|
+
if (extname(path) === "") {
|
|
513
|
+
diagnostics.push({
|
|
514
|
+
message,
|
|
515
|
+
location: { path },
|
|
516
|
+
fixes: [
|
|
517
|
+
{
|
|
518
|
+
type: "create-folder",
|
|
519
|
+
path
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
});
|
|
523
|
+
} else {
|
|
524
|
+
diagnostics.push({
|
|
525
|
+
message,
|
|
526
|
+
location: { path },
|
|
527
|
+
fixes: [
|
|
528
|
+
{
|
|
529
|
+
type: "create-file",
|
|
530
|
+
path,
|
|
531
|
+
content: abstraction2.fileTemplate?.(path) ?? ""
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
diagnostics
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
var noUnabstractionFiles = () => rule({
|
|
543
|
+
name: "default/no-unabstraction-files",
|
|
544
|
+
severity: "error",
|
|
545
|
+
check: ({ instance, root }) => {
|
|
546
|
+
const record = getNodesRecord(root);
|
|
547
|
+
const files = instance.childNodes.filter(
|
|
548
|
+
(node) => record[node]?.type === "file"
|
|
549
|
+
);
|
|
550
|
+
if (files.length > 0) {
|
|
551
|
+
return {
|
|
552
|
+
diagnostics: files.map((node) => ({
|
|
553
|
+
message: ` 'Unabstraction files are not allowed in ${instance.abstraction.name}'`,
|
|
554
|
+
location: { path: node }
|
|
555
|
+
}))
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
diagnostics: []
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
var publicAbstraction = (name) => ({
|
|
564
|
+
name: "default/public-abstraction",
|
|
565
|
+
severity: "error",
|
|
566
|
+
check: ({ instance, dependenciesMap, root }) => {
|
|
567
|
+
const diagnostics = [];
|
|
568
|
+
const nodesRecord = getNodesRecord(root);
|
|
569
|
+
const childFilesEntires = instance.children.flatMap((childInstance) => {
|
|
570
|
+
const instanceNode = nodesRecord[childInstance.path];
|
|
571
|
+
const files = getFlattenFiles(instanceNode);
|
|
572
|
+
return files.map((file) => [file.path, childInstance]);
|
|
573
|
+
});
|
|
574
|
+
const childFilesIndex = Object.fromEntries(childFilesEntires);
|
|
575
|
+
for (const [path, childInstance] of childFilesEntires) {
|
|
576
|
+
const importers = dependenciesMap.dependencyFor[path];
|
|
577
|
+
if (!importers) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (childInstance.abstraction.name === name) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
for (const importer of importers) {
|
|
584
|
+
const dependencyInstance = childFilesIndex[importer];
|
|
585
|
+
if (dependencyInstance === void 0) {
|
|
586
|
+
diagnostics.push({
|
|
587
|
+
message: `Imports of "${getAbstractionInstanceLabel(instance)}" bypassing the public api are forbidden`,
|
|
588
|
+
location: { path }
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return { diagnostics };
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
var restrictCrossImports = () => rule({
|
|
597
|
+
name: "default/restrict-cross-imports",
|
|
598
|
+
severity: "error",
|
|
599
|
+
check: async ({ root, instance, dependenciesMap }) => {
|
|
600
|
+
const diagnostics = [];
|
|
601
|
+
const nodesRecord = getNodesRecord(root);
|
|
602
|
+
const childFilesEntires = instance.children.flatMap((childInstance) => {
|
|
603
|
+
const instanceNode = nodesRecord[childInstance.path];
|
|
604
|
+
const files = getFlattenFiles(instanceNode);
|
|
605
|
+
return files.map((file) => [file.path, childInstance]);
|
|
606
|
+
});
|
|
607
|
+
const childFilesIndex = Object.fromEntries(childFilesEntires);
|
|
608
|
+
for (const [path, instance2] of childFilesEntires) {
|
|
609
|
+
const dependencies = dependenciesMap.dependencies[path];
|
|
610
|
+
for (const dependency of dependencies) {
|
|
611
|
+
const dependencyInstance = childFilesIndex[dependency];
|
|
612
|
+
if (dependencyInstance === void 0) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (dependencyInstance.path !== instance2.path) {
|
|
616
|
+
diagnostics.push({
|
|
617
|
+
message: `Forbidden dependency "${getAbstractionInstanceLabel(instance2)}" <= "${getAbstractionInstanceLabel(dependencyInstance)}".
|
|
618
|
+
cross imports are not allowed!`,
|
|
619
|
+
location: { path }
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return { diagnostics };
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
var dependenciesDirection = (order, options) => rule({
|
|
628
|
+
name: `default/dependencies-direction`,
|
|
629
|
+
severity: "error",
|
|
630
|
+
check: async ({ root, instance, dependenciesMap }) => {
|
|
631
|
+
const diagnostics = [];
|
|
632
|
+
const nodesRecord = getNodesRecord(root);
|
|
633
|
+
const allowDownward = options?.allowDownward ?? [];
|
|
634
|
+
const childFilesEntires = instance.children.flatMap((childInstance) => {
|
|
635
|
+
const instanceNode = nodesRecord[childInstance.path];
|
|
636
|
+
const files = getFlattenFiles(instanceNode);
|
|
637
|
+
return files.map((file) => [file.path, childInstance]);
|
|
638
|
+
});
|
|
639
|
+
const childFilesIndex = Object.fromEntries(childFilesEntires);
|
|
640
|
+
for (const [path, instance2] of childFilesEntires) {
|
|
641
|
+
const dependencies = dependenciesMap.dependencies[path];
|
|
642
|
+
const instanceNameIndex = order.indexOf(instance2.abstraction.name);
|
|
643
|
+
for (const dependency of dependencies) {
|
|
644
|
+
if (allowDownward.length > 0 && matchesAllowDownward(dependency, allowDownward)) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const dependencyInstance = childFilesIndex[dependency];
|
|
648
|
+
if (dependencyInstance === void 0) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const dependencyInstanceNameIndex = order.indexOf(
|
|
652
|
+
dependencyInstance.abstraction.name
|
|
653
|
+
);
|
|
654
|
+
if (dependencyInstanceNameIndex < instanceNameIndex) {
|
|
655
|
+
diagnostics.push({
|
|
656
|
+
message: `Forbidden dependency "${instance2.abstraction.name}" <= "${dependencyInstance.abstraction.name}".
|
|
657
|
+
allowed dependencies order: ${order.join(" <= ")}`,
|
|
658
|
+
location: { path }
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return { diagnostics };
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// src/linter/run-rules.ts
|
|
668
|
+
var runRules = async ({
|
|
669
|
+
root,
|
|
670
|
+
instance,
|
|
671
|
+
dependenciesMap
|
|
672
|
+
}) => {
|
|
673
|
+
const ruleDiagnostics = (currentInstance) => async (rule2) => {
|
|
674
|
+
if (rule2.severity === "off") {
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
const { diagnostics } = await rule2.check({
|
|
678
|
+
root,
|
|
679
|
+
instance: currentInstance,
|
|
680
|
+
dependenciesMap
|
|
681
|
+
});
|
|
682
|
+
return diagnostics.map((d) => ({ ...d, rule: rule2 }));
|
|
683
|
+
};
|
|
684
|
+
const runAbstractionRules = (currentInstance) => {
|
|
685
|
+
return currentInstance.abstraction.rules.map(ruleDiagnostics(currentInstance)).concat(...currentInstance.children.flatMap(runAbstractionRules));
|
|
686
|
+
};
|
|
687
|
+
return await Promise.all(runAbstractionRules(instance)).then((r) => r.flat());
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// src/linter/lint.ts
|
|
691
|
+
var lint = ({
|
|
692
|
+
watch,
|
|
693
|
+
config,
|
|
694
|
+
configPath
|
|
695
|
+
}) => {
|
|
696
|
+
const rootPath = resolve(dirname(configPath), config.baseUrl ?? "./");
|
|
697
|
+
const parseNode = parseAbstractionInstance(config.root);
|
|
698
|
+
return watchFs(rootPath, { onlyReady: !watch }).pipe(
|
|
699
|
+
debounceTime(500),
|
|
700
|
+
switchMap(async ({ vfs }) => ({
|
|
701
|
+
root: vfs,
|
|
702
|
+
instance: parseNode(vfs),
|
|
703
|
+
dependenciesMap: await parseDependenciesMap(vfs)
|
|
704
|
+
})),
|
|
705
|
+
switchMap(runRules)
|
|
706
|
+
);
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
export { abstraction, defineConfig, dependenciesDirection, getAbstractionInstanceLabel, getFlattenFiles, getNodesRecord, lint, noUnabstractionFiles, off, parseAbstractionInstance, parseDependenciesMap, publicAbstraction, requiredChildren, restrictCrossImports, rule, warn, watchConfig, watchFs };
|
|
710
|
+
//# sourceMappingURL=index.js.map
|
|
711
|
+
//# sourceMappingURL=index.js.map
|