@starci/eslint-canon-fe 9.5.23 → 9.6.2
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/CHANGELOG.md +12 -0
- package/docs/button-intent.md +1 -1
- package/package.json +1 -1
- package/runtime/bundle.json +192 -0
- package/runtime/engine/by-code-unit.mjs +6 -6
- package/runtime/engine/model-config.mjs +98 -76
- package/runtime/engine/orca-config.mjs +22 -17
- package/runtime/engine/release-config.mjs +22 -18
- package/runtime/engine/removed-vocabulary.mjs +21 -20
- package/runtime/engine/resources-config.mjs +42 -35
- package/runtime/engine/runtime-root.mjs +11 -13
- package/runtime/engine/secrets.mjs +170 -102
- package/runtime/engine/sonar-config.mjs +16 -13
- package/runtime/engine/temp-root.mjs +3 -9
- package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
- package/runtime/knowledge/hfs/slots.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +1 -1
- package/runtime/scripts/api/git/cat-file.mjs +0 -1
- package/runtime/scripts/api/git/config-get.mjs +1 -2
- package/runtime/scripts/api/git/diff.mjs +0 -1
- package/runtime/scripts/api/git/lib.mjs +32 -32
- package/runtime/scripts/api/git/ls-files.mjs +0 -1
- package/runtime/scripts/api/git/ls-tree.mjs +0 -1
- package/runtime/scripts/api/git/merge-base.mjs +2 -3
- package/runtime/scripts/api/git/remote.mjs +0 -1
- package/runtime/scripts/api/git/rev-parse-query.mjs +0 -1
- package/runtime/scripts/api/git/show.mjs +0 -1
- package/runtime/scripts/hfs/allows.mjs +37 -38
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +180 -163
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +80 -73
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +88 -77
- package/runtime/scripts/hfs/architecture/backend.mjs +378 -357
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +201 -187
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +260 -209
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +149 -116
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +72 -68
- package/runtime/scripts/hfs/architecture/clones.mjs +212 -217
- package/runtime/scripts/hfs/architecture/config-unread.mjs +7 -9
- package/runtime/scripts/hfs/architecture/config.mjs +302 -268
- package/runtime/scripts/hfs/architecture/connection-map.mjs +258 -225
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +80 -71
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +82 -77
- package/runtime/scripts/hfs/architecture/context-map.mjs +267 -234
- package/runtime/scripts/hfs/architecture/context-owner.mjs +73 -61
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -43
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +54 -52
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +113 -104
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +288 -270
- package/runtime/scripts/hfs/architecture/contracts.mjs +408 -393
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +50 -51
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +143 -119
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +286 -228
- package/runtime/scripts/hfs/architecture/default-deny.mjs +129 -110
- package/runtime/scripts/hfs/architecture/doc-language.mjs +24 -19
- package/runtime/scripts/hfs/architecture/entity-registry.mjs +20 -22
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +47 -45
- package/runtime/scripts/hfs/architecture/error-codes.mjs +37 -35
- package/runtime/scripts/hfs/architecture/error-masked.mjs +55 -59
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +24 -23
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +40 -38
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +21 -21
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +106 -98
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +153 -150
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +161 -149
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +343 -305
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +340 -294
- package/runtime/scripts/hfs/architecture/frontend.mjs +174 -184
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +124 -106
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +52 -48
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +81 -71
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +155 -141
- package/runtime/scripts/hfs/architecture/identity.mjs +141 -125
- package/runtime/scripts/hfs/architecture/index.mjs +272 -272
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +45 -43
- package/runtime/scripts/hfs/architecture/jwt-ast.mjs +195 -176
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +163 -155
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +19 -20
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +177 -172
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +100 -78
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +170 -154
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +311 -275
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +17 -17
- package/runtime/scripts/hfs/architecture/next-data.mjs +351 -334
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +39 -37
- package/runtime/scripts/hfs/architecture/owners.mjs +97 -98
- package/runtime/scripts/hfs/architecture/package-shape.mjs +56 -55
- package/runtime/scripts/hfs/architecture/presentation.mjs +138 -130
- package/runtime/scripts/hfs/architecture/reachability.mjs +258 -241
- package/runtime/scripts/hfs/architecture/register-once.mjs +152 -139
- package/runtime/scripts/hfs/architecture/registration.mjs +301 -267
- package/runtime/scripts/hfs/architecture/required-files.mjs +172 -167
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +85 -77
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +206 -181
- package/runtime/scripts/hfs/architecture/server-action.mjs +48 -48
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +261 -259
- package/runtime/scripts/hfs/architecture/source-names.mjs +304 -278
- package/runtime/scripts/hfs/architecture/sql-keys.mjs +277 -194
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +236 -194
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +46 -45
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +86 -74
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +456 -341
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +35 -30
- package/runtime/scripts/hfs/architecture/surface.mjs +64 -74
- package/runtime/scripts/hfs/architecture/symbols.mjs +209 -216
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +165 -155
- package/runtime/scripts/hfs/architecture/tiers.mjs +161 -154
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +155 -149
- package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
- package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +110 -92
- package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
- package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
- package/runtime/scripts/hfs/edition-slots.mjs +43 -40
- package/runtime/scripts/hfs/linear-text.mjs +20 -20
- package/runtime/scripts/hfs/param-names.mjs +6 -11
- package/runtime/scripts/hfs/path-findings.mjs +120 -100
- package/runtime/scripts/hfs/repo-identity.mjs +55 -42
- package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
- package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
- package/runtime/scripts/hfs/rules/database-migrations.mjs +428 -388
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +37 -36
- package/runtime/scripts/hfs/rules/read.mjs +19 -15
- package/runtime/scripts/hfs/rules/secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/spec-placement.mjs +12 -13
- package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
- package/runtime/scripts/hfs/slot-classify.mjs +42 -38
- package/runtime/scripts/hfs/slot-errors.mjs +8 -7
- package/runtime/scripts/hfs/slot-imports.mjs +70 -63
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
- package/runtime/scripts/hfs/slot-match.mjs +122 -111
- package/runtime/scripts/hfs/slot-path.mjs +0 -2
- package/runtime/scripts/hfs/slot-required.mjs +65 -59
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
- package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
- package/runtime/scripts/hfs/slots.mjs +175 -152
- package/runtime/scripts/hfs/test-world-slot.mjs +0 -1
- package/runtime/scripts/hfs/trailing-slashes.mjs +4 -4
- package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
- package/runtime/scripts/hfs/view.mjs +38 -40
- package/runtime/scripts/lib/dockerfile.mjs +131 -114
- package/runtime/scripts/lib/env.mjs +3 -6
- package/runtime/scripts/lib/event-contract.mjs +73 -78
- package/runtime/scripts/lib/git.mjs +15 -16
- package/runtime/scripts/lib/glob.mjs +23 -14
- package/runtime/scripts/lib/grammar-lint.mjs +33 -37
- package/runtime/scripts/lib/i18n.mjs +32 -35
- package/runtime/scripts/lib/in-order.mjs +57 -38
- package/runtime/scripts/lib/json-clone.mjs +45 -36
- package/runtime/scripts/lib/json.mjs +26 -14
- package/runtime/scripts/lib/language.mjs +133 -129
- package/runtime/scripts/lib/list.mjs +9 -14
- package/runtime/scripts/lib/mutation-fence.mjs +10 -5
- package/runtime/scripts/lib/path-key.mjs +53 -38
- package/runtime/scripts/lib/read-text.mjs +30 -17
- package/runtime/scripts/lib/regex.mjs +1 -2
- package/runtime/scripts/lib/same-text.mjs +0 -1
- package/runtime/scripts/lib/sops-envelope.mjs +148 -120
- package/runtime/scripts/lib/stack-services.mjs +128 -129
- package/runtime/scripts/lib/ts-analysis.mjs +28 -26
- package/runtime/scripts/lib/ts-ast.mjs +36 -42
- package/runtime/scripts/lib/walk.mjs +33 -27
- package/runtime/scripts/lib/yaml-cached.mjs +4 -4
|
@@ -1,53 +1,55 @@
|
|
|
1
1
|
/** The app emitter consumes the API app's declared operation table without importing it from app.module.ts. */
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { hasAnyFlag } from '../../lib/ts-ast.mjs';
|
|
4
|
-
|
|
5
4
|
const actualSymbol = (ts, checker, symbol) => symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
|
|
6
|
-
|
|
7
5
|
/** Follow declaration aliases while retaining the actual expression rather than an asserted type. */
|
|
8
6
|
const initializerOf = (ts, checker, node, seen = new Set()) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
if (!node || seen.has(node))
|
|
8
|
+
return null;
|
|
9
|
+
seen.add(node);
|
|
10
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
|
|
11
|
+
|| ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))
|
|
12
|
+
return initializerOf(ts, checker, node.expression, seen);
|
|
13
|
+
if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node))
|
|
14
|
+
return node;
|
|
15
|
+
const declaration = actualSymbol(ts, checker, checker.getSymbolAtLocation(node))?.valueDeclaration; // reason: a value declaration may or may not be a variable declaration; `initializer` is read only when present
|
|
16
|
+
return declaration?.initializer ? initializerOf(ts, checker, declaration.initializer, seen) : node;
|
|
16
17
|
};
|
|
17
|
-
|
|
18
18
|
/** A same-named local function cannot declare the platform operation contract. */
|
|
19
19
|
const isOperationDefiner = (ts, checker, graph, config, expression, seen = new Set()) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
const symbol = actualSymbol(ts, checker, checker.getSymbolAtLocation(expression));
|
|
21
|
+
if (!symbol || seen.has(symbol))
|
|
22
|
+
return false;
|
|
23
|
+
seen.add(symbol);
|
|
24
|
+
return (symbol.declarations ?? []).some((declaration) => {
|
|
25
|
+
const relative = path.relative(config.root, declaration.getSourceFile().fileName).split(path.sep).join('/');
|
|
26
|
+
const owner = graph.resolver.ownerOf(relative);
|
|
27
|
+
if (declaration.name?.text === 'defineOperations' && owner?.slot === 'be.platform' && path.posix.basename(owner.root) === 'operations')
|
|
28
|
+
return true;
|
|
29
|
+
const alias = declaration.initializer;
|
|
30
|
+
return Boolean(alias && (ts.isIdentifier(alias) || ts.isPropertyAccessExpression(alias)) && isOperationDefiner(ts, checker, graph, config, alias, seen));
|
|
31
|
+
});
|
|
30
32
|
};
|
|
31
|
-
|
|
32
33
|
/** True only for an exported, nonempty registration by the canon platform capability. */
|
|
33
34
|
const declaresOperations = (context, graph, config, sourceFile) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
const { ts } = context;
|
|
36
|
+
const checker = context.checkerFor?.(sourceFile.fileName);
|
|
37
|
+
const moduleSymbol = checker?.getSymbolAtLocation(sourceFile);
|
|
38
|
+
if (!checker || !moduleSymbol)
|
|
39
|
+
return false;
|
|
40
|
+
const exported = checker.getExportsOfModule(moduleSymbol).find(symbol => symbol.getName() === 'OPERATIONS');
|
|
41
|
+
const declaration = actualSymbol(ts, checker, exported)?.valueDeclaration; // reason: see initializerOf
|
|
42
|
+
const registration = initializerOf(ts, checker, declaration?.initializer);
|
|
43
|
+
if (!registration || !ts.isCallExpression(registration) || !isOperationDefiner(ts, checker, graph, config, registration.expression))
|
|
44
|
+
return false;
|
|
45
|
+
const entries = initializerOf(ts, checker, registration.arguments[0]);
|
|
46
|
+
return entries !== null && ts.isObjectLiteralExpression(entries) && checker.getPropertiesOfType(checker.getTypeAtLocation(entries)).length > 0;
|
|
44
47
|
};
|
|
45
|
-
|
|
46
48
|
/** Only the emitter's exact entry of a declared API app is a contract root; workers and arbitrary app files remain judged. */
|
|
47
49
|
export function operationContractRoots(context, graph, config) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
return (config.apps ?? []).filter(app => app.kind === 'api').flatMap(app => {
|
|
51
|
+
const relative = `apps/${app.name}/src/operations.ts`;
|
|
52
|
+
const node = graph.files.get(relative);
|
|
53
|
+
return node?.tier === 'app' && node.slot === 'be.app.api' && declaresOperations(context, graph, config, node.sourceFile) ? [relative] : [];
|
|
54
|
+
});
|
|
53
55
|
}
|
|
@@ -5,139 +5,138 @@ import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
|
5
5
|
import { globExpression } from '../../lib/glob.mjs';
|
|
6
6
|
import { posixPath } from '../../lib/path-key.mjs';
|
|
7
7
|
import { registryImporter, registrySlot } from './entity-registry.mjs';
|
|
8
|
-
|
|
9
8
|
function absolute(root, relative) {
|
|
10
|
-
|
|
9
|
+
return canonical(path.resolve(root, ...relative.split('/')));
|
|
11
10
|
}
|
|
12
|
-
|
|
13
11
|
/**
|
|
14
12
|
* The extra public entries the slot manifest declares for the owner (slot field `entries`, on the slot that holds its entry file):
|
|
15
13
|
* exact files as absolute paths, and the entries holding a `*` (one file per operation, `graphql/queries/query-*.ts`) as patterns.
|
|
16
14
|
*/
|
|
17
15
|
function slotEntries(config, owner) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
const found = config.hfs?.classifyPath?.(owner.entry);
|
|
17
|
+
if (!found?.slot)
|
|
18
|
+
return { exact: [], patterns: [] };
|
|
19
|
+
const names = config.hfs.slot(found.slot)?.entries ?? [];
|
|
20
|
+
const exact = names.filter(name => !name.includes('*')).map(name => absolute(config.root, `${found.root}/${name}`));
|
|
21
|
+
const patterns = names.filter(name => name.includes('*')).map(name => globExpression(`${posixPath(absolute(config.root, found.root))}/${name}`));
|
|
22
|
+
return { exact, patterns };
|
|
23
23
|
}
|
|
24
|
-
|
|
25
24
|
/** The owner's public entries: its declared entry, the `entries` its slot declares, plus every source file its package.json `exports` maps when the owner is a workspace package. */
|
|
26
25
|
function ownerDeclarations(config, context) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
return config.owners.map((owner) => {
|
|
27
|
+
const root = absolute(config.root, owner.root);
|
|
28
|
+
const entry = absolute(config.root, owner.entry);
|
|
29
|
+
const workspace = context.workspaces?.find(item => item.root === root);
|
|
30
|
+
const { exact: declared, patterns } = slotEntries(config, owner);
|
|
31
|
+
const entries = new Set([entry, ...declared, ...(workspace ? workspaceExportSources(context.ts, workspace) : [])]);
|
|
32
|
+
const isEntry = (file) => entries.has(file) || patterns.some(pattern => pattern.test(posixPath(file)));
|
|
33
|
+
return { ...owner, root, entry, entries, isEntry, declared };
|
|
34
|
+
}).sort((a, b) => b.root.length - a.root.length);
|
|
36
35
|
}
|
|
37
|
-
|
|
38
36
|
function ownerOf(owners, file) {
|
|
39
|
-
|
|
37
|
+
return owners.find(owner => isInside(owner.root, file)) ?? null;
|
|
40
38
|
}
|
|
41
|
-
|
|
42
39
|
function reexportSteps(context, current) {
|
|
43
|
-
|
|
40
|
+
return (context.edges.get(current.file) ?? []).filter(candidate => candidate.reexport).map(candidate => ({ file: candidate.to, chain: [...current.chain, candidate.to] }));
|
|
44
41
|
}
|
|
45
|
-
|
|
46
42
|
function privateOwnerChain(context, owners, edge) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
43
|
+
const sourceOwner = ownerOf(owners, edge.from);
|
|
44
|
+
const queue = [{ file: edge.to, chain: [edge.from, edge.to] }];
|
|
45
|
+
const visited = new Set();
|
|
46
|
+
for (let current = queue.shift(); current; current = queue.shift()) {
|
|
47
|
+
if (visited.has(current.file))
|
|
48
|
+
continue;
|
|
49
|
+
visited.add(current.file);
|
|
50
|
+
const owner = ownerOf(owners, current.file);
|
|
51
|
+
if (owner && sourceOwner?.id !== owner.id)
|
|
52
|
+
return owner.isEntry(current.file) ? null : { owner, chain: current.chain };
|
|
53
|
+
queue.push(...reexportSteps(context, current));
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
59
56
|
}
|
|
60
|
-
|
|
61
57
|
/**
|
|
62
58
|
* A `*.builder.ts` of slot be.tests.fixtures.builders arranges data at the schema level: it may deep-import the persistence
|
|
63
59
|
* entity classes and connection entity lists (slot be.persistence) of any capability. No other file may.
|
|
64
60
|
*/
|
|
65
61
|
function arrangesSchema(config, from, to) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
const resolver = config.hfs;
|
|
63
|
+
if (!resolver?.classifyPath)
|
|
64
|
+
return false;
|
|
65
|
+
return resolver.classifyPath(relativePath(config.root, from)).slot === 'be.tests.fixtures.builders'
|
|
66
|
+
&& resolver.classifyPath(relativePath(config.root, to)).slot === 'be.persistence';
|
|
70
67
|
}
|
|
71
|
-
|
|
72
68
|
/**
|
|
73
69
|
* The entity registry (slot role entity-registry, entity-registry.mjs) is imported by deep path, and only from the files its slot's
|
|
74
70
|
* `importedBy` names: app composition and the test world. Any other importer goes through the owner's entry, which never exports it.
|
|
75
71
|
*/
|
|
76
72
|
function importsRegistry(config, from, to) {
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
const slot = registrySlot(config.hfs, relativePath(config.root, to));
|
|
74
|
+
return slot !== null && registryImporter(config.hfs, slot, relativePath(config.root, from));
|
|
79
75
|
}
|
|
80
|
-
|
|
81
76
|
function publicEntryViolations(config, context, owners, sourceFiles) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
77
|
+
const violations = [];
|
|
78
|
+
for (const owner of owners) {
|
|
79
|
+
const sourceFile = sourceFiles.get(owner.entry);
|
|
80
|
+
if (!sourceFile) {
|
|
81
|
+
violations.push({ ruleId: 'ARCH_OWNER_EXPORT_BYPASS', path: relativePath(config.root, owner.entry), line: 1, column: 1, owner: owner.id,
|
|
82
|
+
message: `Owner ${owner.id} public entry is outside the configured TypeScript programs, so its boundary cannot be checked.` });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const publicFiles = new Set([
|
|
86
|
+
sourceFile,
|
|
87
|
+
...owner.declared.map(file => sourceFiles.get(file)).filter((file) => Boolean(file)),
|
|
88
|
+
]);
|
|
89
|
+
for (const entryFile of publicFiles) {
|
|
90
|
+
for (const statement of entryFile.statements)
|
|
91
|
+
if (context.ts.isExportDeclaration(statement) && !statement.exportClause) {
|
|
92
|
+
violations.push({
|
|
93
|
+
ruleId: 'ARCH_OWNER_EXPORT_STAR',
|
|
94
|
+
path: relativePath(config.root, canonical(entryFile.fileName)),
|
|
95
|
+
...sourceLocation(entryFile, statement),
|
|
96
|
+
owner: owner.id,
|
|
97
|
+
message: `Owner ${owner.id} public entry must use explicit named exports rather than export *.`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
104
101
|
}
|
|
105
|
-
|
|
106
|
-
return violations;
|
|
102
|
+
return violations;
|
|
107
103
|
}
|
|
108
|
-
|
|
109
104
|
function ownerImportViolations(config, context, owners) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
105
|
+
const violations = [];
|
|
106
|
+
for (const sourceFile of context.files) {
|
|
107
|
+
const from = canonical(sourceFile.fileName);
|
|
108
|
+
for (const edge of context.edges.get(from) ?? []) {
|
|
109
|
+
const bypass = privateOwnerChain(context, owners, edge);
|
|
110
|
+
if (!bypass)
|
|
111
|
+
continue;
|
|
112
|
+
const target = bypass.chain.at(-1) ?? edge.to;
|
|
113
|
+
if (arrangesSchema(config, from, target))
|
|
114
|
+
continue;
|
|
115
|
+
if (bypass.chain.length === 2 && importsRegistry(config, from, target))
|
|
116
|
+
continue;
|
|
117
|
+
violations.push({
|
|
118
|
+
ruleId: 'ARCH_OWNER_EXPORT_BYPASS',
|
|
119
|
+
path: relativePath(config.root, from),
|
|
120
|
+
line: edge.line,
|
|
121
|
+
column: edge.column,
|
|
122
|
+
owner: bypass.owner.id,
|
|
123
|
+
specifier: edge.specifier,
|
|
124
|
+
resolvedPath: relativePath(config.root, target),
|
|
125
|
+
dependencyChain: bypass.chain.map(file => relativePath(config.root, file)),
|
|
126
|
+
message: `Cross-owner dependency must enter ${bypass.owner.id} through ${relativePath(config.root, bypass.owner.entry)}.`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
return violations;
|
|
130
|
+
return violations;
|
|
132
131
|
}
|
|
133
|
-
|
|
134
132
|
/** Enforce explicit same-source owner entries without constraining imports inside one owner. */
|
|
135
133
|
export function checkOwners(config, context) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
134
|
+
if (!config.owners?.length)
|
|
135
|
+
return [];
|
|
136
|
+
const owners = ownerDeclarations(config, context);
|
|
137
|
+
const sourceFiles = new Map(context.files.map((file) => [canonical(file.fileName), file]));
|
|
138
|
+
return [
|
|
139
|
+
...publicEntryViolations(config, context, owners, sourceFiles),
|
|
140
|
+
...ownerImportViolations(config, context, owners),
|
|
141
|
+
];
|
|
143
142
|
}
|
|
@@ -2,7 +2,6 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { treeOf } from './required-files.mjs';
|
|
4
4
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* R63 `package-shape` (FE_PACKAGE_SHAPE, knowledge/hfs/README.md 6.2). A shared package is built to `dist` and says what it
|
|
8
7
|
* exports. The package manifest of every package owner (slots repo.packages and fe.package.ui, packages/<pkg>/package.json)
|
|
@@ -17,75 +16,77 @@ import { byCodeUnit } from '../../lib/list.mjs';
|
|
|
17
16
|
* eslint rule `monorepo-tier-belongs-to-its-side`; none is judged a second time here.
|
|
18
17
|
*/
|
|
19
18
|
export const PACKAGE_SHAPE_RULE_IDS = ['FE_PACKAGE_SHAPE'];
|
|
20
|
-
|
|
21
19
|
const RULE = 'FE_PACKAGE_SHAPE';
|
|
22
20
|
const UNREADABLE = Symbol('unreadable');
|
|
23
|
-
|
|
24
21
|
/** Every string target of an exports value (a string, a condition map, an array of either), with the condition path. */
|
|
25
22
|
function targetsOf(value, trail = []) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
if (typeof value === 'string')
|
|
24
|
+
return [{ target: value, trail }];
|
|
25
|
+
if (Array.isArray(value))
|
|
26
|
+
return value.flatMap((item, index) => targetsOf(item, [...trail, String(index)]));
|
|
27
|
+
if (value && typeof value === 'object')
|
|
28
|
+
return Object.entries(value).flatMap(([key, item]) => targetsOf(item, [...trail, key]));
|
|
29
|
+
return [];
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
const insideDist = target => path.posix.normalize(target).replace(/^\.\//u, '').startsWith('dist/');
|
|
33
|
-
|
|
31
|
+
const insideDist = (target) => path.posix.normalize(target).replace(/^\.\//u, '').startsWith('dist/');
|
|
34
32
|
function packageRoots(config, resolver) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
const roots = new Set();
|
|
34
|
+
for (const rel of treeOf(config.root).files) {
|
|
35
|
+
const owner = resolver.ownerOf(rel);
|
|
36
|
+
if (owner && resolver.slot(owner.slot)?.tier === 'package')
|
|
37
|
+
roots.add(owner.root);
|
|
38
|
+
}
|
|
39
|
+
return [...roots].sort(byCodeUnit);
|
|
41
40
|
}
|
|
42
|
-
|
|
43
41
|
function readManifest(config, manifestRel) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(path.join(config.root, ...manifestRel.split('/')), 'utf8')); // reason: parsed from disk; the fields read are checked at use
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return UNREADABLE;
|
|
47
|
+
}
|
|
49
48
|
}
|
|
50
|
-
|
|
51
49
|
function reportExportIssues(manifest, manifestRel, report) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
if (manifest.exports === undefined) {
|
|
51
|
+
report(`${manifestRel} declares no exports map; a package lists what it exports, subpath by subpath, each pointing into ./dist/.`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (typeof manifest.exports === 'object' && !Array.isArray(manifest.exports)) {
|
|
55
|
+
for (const key of Object.keys(manifest.exports)) { // reason: typeof narrows to object | null; a null exports map throws here exactly as before
|
|
56
|
+
if (key.includes('*') || key.endsWith('/'))
|
|
57
|
+
report(`exports key "${key}" of ${manifestRel} is a pattern or folder mapping; list each exported subpath explicitly.`, { export: key });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const { target, trail } of targetsOf(manifest.exports)) {
|
|
61
|
+
if (!insideDist(target))
|
|
62
|
+
report(`exports target "${target}" (${trail.join(' > ') || '.'}) of ${manifestRel} is not inside ./dist/; a package exports what it built, never its source.`, { export: target });
|
|
59
63
|
}
|
|
60
|
-
}
|
|
61
|
-
for (const { target, trail } of targetsOf(manifest.exports)) {
|
|
62
|
-
if (!insideDist(target)) report(`exports target "${target}" (${trail.join(' > ') || '.'}) of ${manifestRel} is not inside ./dist/; a package exports what it built, never its source.`, { export: target });
|
|
63
|
-
}
|
|
64
64
|
}
|
|
65
|
-
|
|
66
65
|
function reportManifestIssues(manifest, manifestRel, report) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
if (typeof manifest.scripts?.build !== 'string' || !manifest.scripts.build.trim())
|
|
67
|
+
report(`${manifestRel} has no scripts.build; a shared package is built to dist before an app consumes it.`);
|
|
68
|
+
reportExportIssues(manifest, manifestRel, report);
|
|
69
|
+
for (const field of ['main', 'module', 'types', 'typings']) {
|
|
70
|
+
if (typeof manifest[field] === 'string' && !insideDist(manifest[field]))
|
|
71
|
+
report(`${field} "${manifest[field]}" of ${manifestRel} is not inside ./dist/; a package points at what it built.`, { field });
|
|
72
|
+
}
|
|
72
73
|
}
|
|
73
|
-
|
|
74
74
|
function inspectPackage(config, root, violations) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
const manifestRel = `${root}/package.json`;
|
|
76
|
+
const report = (message, extra = {}) => violations.push({ ruleId: RULE, path: manifestRel, line: 1, column: 1, package: root, message, ...extra });
|
|
77
|
+
const manifest = readManifest(config, manifestRel);
|
|
78
|
+
if (manifest === UNREADABLE)
|
|
79
|
+
return false;
|
|
80
|
+
reportManifestIssues(manifest, manifestRel, report);
|
|
81
|
+
return true;
|
|
81
82
|
}
|
|
82
|
-
|
|
83
83
|
export function checkPackageShape({ config, graph }) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
const roots = packageRoots(config, graph.resolver);
|
|
85
|
+
const violations = [];
|
|
86
|
+
let packages = 0;
|
|
87
|
+
for (const root of roots) {
|
|
88
|
+
if (inspectPackage(config, root, violations))
|
|
89
|
+
packages += 1;
|
|
90
|
+
}
|
|
91
|
+
return { violations, coverage: { status: 'checked', packages } };
|
|
91
92
|
}
|