@starci/hfs 2.0.2 → 4.0.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/CHANGELOG.md +25 -0
- package/README.md +33 -33
- package/bin/hfs.mjs +94 -68
- package/lint/run.mjs +161 -0
- package/package.json +3 -2
- package/report/sonar.mjs +14 -29
- package/runtime/engine/admission.mjs +3 -3
- package/runtime/engine/ledger-db.mjs +2 -2
- package/runtime/engine/machine-db.mjs +90 -9
- package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +13 -0
- package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +93 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +32 -13
- package/runtime/knowledge/hfs/peer-integrations.yaml +18 -0
- package/runtime/knowledge/hfs/slots.yaml +193 -128
- package/runtime/knowledge/patterns/fe/folder.yaml +36 -36
- package/runtime/knowledge/sonar-gate.yaml +8 -7
- package/runtime/modules/kernel/failure-codes.yaml +31 -52
- package/runtime/scripts/checks/architecture/backend.mjs +1 -1
- package/runtime/scripts/checks/architecture/config.mjs +31 -11
- package/runtime/scripts/checks/architecture/contracts.mjs +4 -4
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +7 -3
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +5 -47
- package/runtime/scripts/checks/architecture/frontend.mjs +6 -4
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/checks/architecture/hfs.mjs +105 -67
- package/runtime/scripts/checks/architecture/index.mjs +18 -13
- package/runtime/scripts/checks/architecture/next-data.mjs +3 -2
- package/runtime/scripts/checks/architecture/owners.mjs +12 -6
- package/runtime/scripts/checks/architecture/registration.mjs +1 -1
- package/runtime/scripts/checks/architecture/surface.mjs +91 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +13 -2
- package/runtime/scripts/checks/architecture/test-world-files.mjs +83 -45
- package/runtime/scripts/checks/architecture/typescript.mjs +127 -29
- package/runtime/scripts/checks/typescript-programs.mjs +2 -2
- package/runtime/scripts/lib/hfs-check.mjs +160 -209
- package/runtime/scripts/lib/hfs-path-findings.mjs +95 -0
- package/runtime/scripts/lib/hfs-rules/contract.mjs +15 -42
- package/runtime/scripts/lib/hfs-rules/frontend.mjs +37 -36
- package/runtime/scripts/lib/hfs-rules/peer-integrations.mjs +44 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +12 -13
- package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +11 -8
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +1 -3
- package/runtime/scripts/lib/hfs-slots.mjs +244 -61
- package/runtime/scripts/lib/hfs-view.mjs +9 -7
- package/runtime/scripts/lib/language.mjs +11 -1
- package/runtime/scripts/lib/safe-remove.mjs +95 -10
- package/scaffold/app.mjs +179 -0
- package/scaffold/service.mjs +26 -16
- package/sync/cli.mjs +1 -1
- package/sync/hygiene.mjs +11 -8
- package/sync/index.mjs +109 -111
- package/sync/managed.mjs +9 -8
- package/sync/sonar-key.mjs +20 -22
- package/templates/{be → app}/ci-workflows/github/workflows/ci.yml +5 -11
- package/templates/app/gitignore +6 -0
- package/templates/app/hooks/husky/pre-commit +25 -0
- package/templates/app/hooks/husky/pre-push +7 -0
- package/templates/app/package-scripts/package.json +22 -0
- package/templates/{be → app}/quality-config/sonar-project.properties +4 -3
- package/templates/app/skeleton/.editorconfig +15 -0
- package/templates/app/skeleton/.gitattributes +2 -0
- package/templates/app/skeleton/.nvmrc +1 -0
- package/templates/app/skeleton/.starciwork/features/index.yaml +7 -0
- package/templates/app/skeleton/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton/README.md +36 -0
- package/templates/app/skeleton/scripts/codegen.mjs +4 -0
- package/templates/{fe → app}/tool-config/prettierignore +4 -1
- package/templates/be/skeleton/.sops.yaml +2 -0
- package/templates/be/skeleton/.starcistacks/application-stacks.yaml +10 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +27 -6
- package/templates/be/skeleton/apps/__app__/src/main.ts +4 -1
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +11 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +25 -0
- package/templates/be/skeleton/src/modules/domain/identity/errors/identity.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +11 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +14 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +71 -23
- package/templates/be/skeleton/src/modules/platform/config/errors/config.error.ts +14 -16
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +2 -12
- package/templates/be/skeleton/src/modules/platform/errors/domain.error.ts +16 -7
- package/templates/be/skeleton/src/modules/platform/errors/errors/errors.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.contracts.ts +33 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.decorators.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.options.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.spec.ts +94 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/errors/http-status.policy.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +4 -1
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +31 -0
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +68 -0
- package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.spec.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.decorators.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module.ts +24 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.options.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.port.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.spec.ts +45 -0
- package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +85 -68
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +14 -10
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +0 -1
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.contracts.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.ts +24 -0
- package/templates/fe/skeleton/apps/__app__/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +5 -17
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +11 -22
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +3 -12
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +6 -24
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +4 -18
- package/templates/fe/skeleton/apps/__app__/src/app/globals.css +5 -0
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +28 -0
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +35 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +21 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +17 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +26 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +25 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +27 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +32 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/request.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +4 -0
- package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/proxy.ts +1 -1
- package/runtime/scripts/checks/architecture/size-growth.mjs +0 -73
- package/sync/skeleton.mjs +0 -76
- package/templates/be/gitignore +0 -2
- package/templates/be/hooks/husky/pre-commit +0 -13
- package/templates/be/hooks/husky/pre-push +0 -7
- package/templates/be/package-scripts/package.json +0 -22
- package/templates/be/skeleton/scripts/.gitkeep +0 -0
- package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +0 -9
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +0 -20
- package/templates/be/tool-config/prettierignore +0 -8
- package/templates/fe/ci-workflows/github/workflows/ci.yml +0 -54
- package/templates/fe/gitignore +0 -3
- package/templates/fe/hooks/husky/pre-commit +0 -16
- package/templates/fe/hooks/husky/pre-push +0 -6
- package/templates/fe/package-scripts/package.json +0 -17
- package/templates/fe/parts/api-client.ts +0 -44
- package/templates/fe/parts/api-outcome.ts +0 -7
- package/templates/fe/quality-config/sonar-project.properties +0 -8
- package/templates/fe/skeleton/scripts/.gitkeep +0 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +0 -1
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +0 -3
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +0 -1
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +0 -4
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/navigation.ts +0 -5
- package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +0 -12
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +0 -2
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +0 -9
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +0 -5
- package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +0 -5
- package/templates/fe/skeleton-shared/packages/__family__-api/package.json +0 -12
- package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +0 -1
- package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +0 -3
- package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +0 -1
- package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +0 -5
- package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +0 -18
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +0 -19
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +0 -2
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +0 -12
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +0 -15
- package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +0 -5
- package/templates/fe/tool-config/prettierrc +0 -1
- /package/templates/{be → app}/ci-workflows/github/workflows/e2e.yml +0 -0
- /package/templates/{be → app}/starciwork.gitignore +0 -0
- /package/templates/{be → app}/tool-config/prettierrc +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/next.config.ts +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/config.ts +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/routing.ts +0 -0
|
@@ -41,10 +41,6 @@ export const HFS_RULE_IDS = [
|
|
|
41
41
|
'HFS_WORK_IN_FE',
|
|
42
42
|
];
|
|
43
43
|
|
|
44
|
-
const REQUIRED_COMMON = ['.gitattributes', '.github', '.gitignore', '.husky', 'hfs.json',
|
|
45
|
-
'eslint.config.mjs', 'package-lock.json', 'package.json', 'README.md',
|
|
46
|
-
'sonar-project.properties', 'tsconfig.json'];
|
|
47
|
-
const REQUIRED_BACKEND = ['.sops.yaml', '.starcistacks', '.starciwork', 'jest.config.js', 'nest-cli.json', 'src'];
|
|
48
44
|
const NON_NPM_ENTRIES = new Set(['pnpm-lock.yaml', 'pnpm-workspace.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb']);
|
|
49
45
|
const RUNTIME_ROOT_MARKDOWN = new Set(['README.md', 'CONTEXT.md', 'CONTRIBUTING.md', 'CHANGELOG.md', 'THIRD_PARTY_NOTICES.md']);
|
|
50
46
|
const PRODUCT_ROOT_MARKDOWN = new Set(['README.md']);
|
|
@@ -57,18 +53,21 @@ const RETIRED_TEST_FOLDER = /^src\/tests\/(?:harness|live|e2e\/live)(?:\/|$)/u;
|
|
|
57
53
|
const EXTRA_TEST_CONFIG = /(?:^|\/)(?:jest[.-][^/]*(?:config\.[cm]?[jt]s|\.json)|jest-(?:e2e|int|integration|harness)[^/]*)$/u;
|
|
58
54
|
const NODE_ENTRIES_SKIPPED = new Set(['node_modules', '.git']);
|
|
59
55
|
|
|
60
|
-
/** The slot resolver of a
|
|
56
|
+
/** The slot resolver of a side folder: the caller's, else the side view its app's hfs.json declares, else the profile's slots with no app declared. */
|
|
61
57
|
function resolverOf(root, profile, given) {
|
|
62
58
|
if (given) return given;
|
|
63
59
|
try { return openHfs({ repoRoot: root }); } catch { /* an invalid hfs.json is HFS_DECLARATION_INVALID's finding, judged elsewhere */ }
|
|
64
|
-
return createSlotResolver(loadSlotManifest(), { profile, apps: [], optionalSlots: [], connections: [] });
|
|
60
|
+
return createSlotResolver(loadSlotManifest(), { profile, side: profile, apps: [], optionalSlots: [], connections: [], reads: [] });
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The first path segment of every slot of the scope that may exist: the root entries of a side (contracts/, docs/, src/ ...) or of
|
|
65
|
+
* the app (be/, fe/, .github/ ...). `scope` is the profile whose slots count (the app resolver answers for the sides too).
|
|
66
|
+
*/
|
|
67
|
+
function slotRootEntries(resolver, scope) {
|
|
68
|
+
const roots = new Set(scope === 'app' ? [] : ['apps']);
|
|
70
69
|
for (const slot of resolver.slots()) {
|
|
71
|
-
if (slot.presence === 'forbidden') continue;
|
|
70
|
+
if (slot.presence === 'forbidden' || !slot.profiles.includes(scope)) continue;
|
|
72
71
|
for (const variant of braceVariants(slot.path)) {
|
|
73
72
|
const first = variant.split('/')[0];
|
|
74
73
|
if (first && !/[*?<%]/u.test(first)) roots.add(first);
|
|
@@ -77,6 +76,11 @@ function slotRootEntries(resolver) {
|
|
|
77
76
|
return roots;
|
|
78
77
|
}
|
|
79
78
|
|
|
79
|
+
/** The root entries the scope's required slots name (the first segment of every required path of the scope): hfs.json, be/, .starcistacks/ ... */
|
|
80
|
+
function requiredRootEntries(resolver) {
|
|
81
|
+
return new Set(resolver.requiredPaths().paths.filter((entry) => !entry.side).map((entry) => entry.path.split('/')[0]));
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
/** The folders directly below src/tests/ that the be.tests.* slots declare (world, fixtures, integration, e2e, contract). */
|
|
81
85
|
function slotTestChildren(resolver) {
|
|
82
86
|
const children = new Set();
|
|
@@ -145,25 +149,28 @@ function fsFiles(root, relative = '') {
|
|
|
145
149
|
return out;
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
/** The tree view of a list of tracked paths (posix, root-relative): what the git view answers, for a list already in hand. */
|
|
153
|
+
export function trackedTreeView(tracked) {
|
|
154
|
+
const files = new Set(tracked);
|
|
155
|
+
return {
|
|
156
|
+
source: 'git',
|
|
157
|
+
top: [...new Set(tracked.map(file => file.split('/')[0]))],
|
|
158
|
+
children: dir => {
|
|
159
|
+
const prefix = `${dir}/`;
|
|
160
|
+
const names = new Set();
|
|
161
|
+
for (const file of tracked) if (file.startsWith(prefix)) names.add(file.slice(prefix.length).split('/')[0]);
|
|
162
|
+
return [...names];
|
|
163
|
+
},
|
|
164
|
+
hasDir: dir => tracked.some(file => file.startsWith(`${dir}/`)),
|
|
165
|
+
hasFile: file => files.has(file),
|
|
166
|
+
files: () => tracked,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
/** Uniform tree view: top entries plus children(dir)/hasDir/hasFile answers over posix relatives. */
|
|
149
171
|
function treeView(root) {
|
|
150
172
|
const tracked = gitPaths(root);
|
|
151
|
-
if (tracked !== null)
|
|
152
|
-
const files = new Set(tracked);
|
|
153
|
-
return {
|
|
154
|
-
source: 'git',
|
|
155
|
-
top: [...new Set(tracked.map(file => file.split('/')[0]))],
|
|
156
|
-
children: dir => {
|
|
157
|
-
const prefix = `${dir}/`;
|
|
158
|
-
const names = new Set();
|
|
159
|
-
for (const file of tracked) if (file.startsWith(prefix)) names.add(file.slice(prefix.length).split('/')[0]);
|
|
160
|
-
return [...names];
|
|
161
|
-
},
|
|
162
|
-
hasDir: dir => tracked.some(file => file.startsWith(`${dir}/`)),
|
|
163
|
-
hasFile: file => files.has(file),
|
|
164
|
-
files: () => tracked,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
173
|
+
if (tracked !== null) return trackedTreeView(tracked);
|
|
167
174
|
return {
|
|
168
175
|
source: 'fs',
|
|
169
176
|
top: fsChildren(root, '.'),
|
|
@@ -189,7 +196,7 @@ function privateHost(hostname) {
|
|
|
189
196
|
|
|
190
197
|
// The scripts the README Development section shows; each is required only when the managed package-scripts template of the
|
|
191
198
|
// profile (packages/hfs/templates/<profile>/package-scripts/package.json, the one source of the managed script names) has it.
|
|
192
|
-
const DEVELOPMENT_SCRIPTS = ['typecheck', 'lint
|
|
199
|
+
const DEVELOPMENT_SCRIPTS = ['typecheck', 'lint', 'build', 'test'];
|
|
193
200
|
// The templates sit beside the runtime in a checkout (packages/hfs/templates) and one level above the bundled runtime of @starci/hfs.
|
|
194
201
|
const TEMPLATE_ROOTS = [path.resolve(import.meta.dirname, '..', '..', '..', 'packages', 'hfs', 'templates'), path.resolve(import.meta.dirname, '..', '..', '..', '..', 'templates')];
|
|
195
202
|
const managedScriptCache = new Map();
|
|
@@ -211,7 +218,7 @@ function scriptCommand(name) {
|
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
/** Presentation checks shared by the product HFS gate and this runtime's own standalone gate. */
|
|
214
|
-
export function checkRepoPresentation({ root, runtime = false, tree = treeView(root), profile = '
|
|
221
|
+
export function checkRepoPresentation({ root, runtime = false, tree = treeView(root), profile = 'app' }) {
|
|
215
222
|
const violations = [];
|
|
216
223
|
const finding = (ruleId, entry, message, line = 1) => violations.push({ ruleId, path: entry, line, column: 1, message });
|
|
217
224
|
for (const entry of tree.top) {
|
|
@@ -333,7 +340,11 @@ function hooksPathNotRedirected({ root, finding }) {
|
|
|
333
340
|
return { status: 'checked' };
|
|
334
341
|
}
|
|
335
342
|
|
|
336
|
-
|
|
343
|
+
/**
|
|
344
|
+
* The app root's automatic gates never run integration, e2e or contract: the husky hooks and the root scripts they call, the unit
|
|
345
|
+
* scripts over the be side's jest configuration (`jestConfig`, app-relative), and the workflows that start on push or pull_request.
|
|
346
|
+
*/
|
|
347
|
+
function e2eInAutomaticGate({ root, tree, jestConfig, finding }) {
|
|
337
348
|
const rule = 'HFS_E2E_IN_AUTOMATIC_GATE';
|
|
338
349
|
let pkg = null;
|
|
339
350
|
try { pkg = JSON.parse(readText(root, 'package.json') ?? ''); } catch { /* the package checks own invalid JSON */ }
|
|
@@ -358,18 +369,31 @@ function e2eInAutomaticGate({ root, tree, backend, finding }) {
|
|
|
358
369
|
if (runsE2e(command)) finding(rule, 'package.json', `Script ${name} is run by a husky hook and touches integration, e2e or contract. They are manual only.`);
|
|
359
370
|
for (const match of command.matchAll(/npm\s+run\s+([\w:.-]+)/gu)) pending.push(match[1]);
|
|
360
371
|
}
|
|
361
|
-
// 2. Unit and coverage scripts
|
|
362
|
-
if (
|
|
372
|
+
// 2. Unit and coverage scripts over the be side's jest configuration select the unit project only and exclude src/tests.
|
|
373
|
+
if (tree.hasFile(jestConfig)) {
|
|
363
374
|
for (const name of UNIT_RUN_SCRIPTS) {
|
|
364
375
|
const command = scripts[name];
|
|
365
376
|
if (typeof command === 'string' && /\bjest\b/u.test(command) && !/--selectProjects\s+unit\b/u.test(command))
|
|
366
377
|
finding(rule, 'package.json', `Script ${name} runs jest without --selectProjects unit and would run the integration, e2e or contract project.`);
|
|
367
378
|
}
|
|
368
|
-
const
|
|
369
|
-
if (/collectCoverageFrom/u.test(
|
|
370
|
-
finding(rule,
|
|
379
|
+
const jestText = readText(root, jestConfig) ?? '';
|
|
380
|
+
if (/collectCoverageFrom/u.test(jestText) && !/!src\/tests\/(?:\*\*|e2e)/u.test(jestText))
|
|
381
|
+
finding(rule, jestConfig, 'collectCoverageFrom must exclude src/tests/** so no integration, e2e or contract file counts toward coverage.');
|
|
382
|
+
}
|
|
383
|
+
// 3. A workflow that starts on push or pull_request never runs e2e.
|
|
384
|
+
for (const file of tree.files().filter(entry => /^\.github\/workflows\/[^/]+\.ya?ml$/u.test(entry))) {
|
|
385
|
+
const text = readText(root, file);
|
|
386
|
+
if (text === null) continue;
|
|
387
|
+
const trigger = /^on:.*(?:\n(?:[ \t]+.*|)$)*/mu.exec(text)?.[0] ?? '';
|
|
388
|
+
if (!/\b(?:push|pull_request)\b/u.test(trigger)) continue;
|
|
389
|
+
if (runsE2e(withoutComments(text)))
|
|
390
|
+
finding(rule, file, `${file} runs e2e on push or pull_request. Move the e2e job to its own workflow with on: workflow_dispatch only.`);
|
|
371
391
|
}
|
|
372
|
-
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** The be side's default tsconfig excludes the world, integration, e2e and contract trees (they are checked by src/tests/tsconfig.json). */
|
|
395
|
+
function testTreesOutOfDefaultProgram({ root, tree, finding }) {
|
|
396
|
+
const rule = 'HFS_E2E_IN_AUTOMATIC_GATE';
|
|
373
397
|
const tsconfigText = readText(root, 'tsconfig.json');
|
|
374
398
|
let tsconfig = null;
|
|
375
399
|
try { tsconfig = tsconfigText === null ? null : JSON.parse(tsconfigText); } catch { /* the typecheck itself owns parsing */ }
|
|
@@ -378,19 +402,10 @@ function e2eInAutomaticGate({ root, tree, backend, finding }) {
|
|
|
378
402
|
const excludesTestTrees = ['world', 'integration', 'e2e', 'contract'].every(tree => excludedText.includes(`src/tests/${tree}`));
|
|
379
403
|
const defaultAll = tsconfig.include === undefined && tsconfig.files === undefined;
|
|
380
404
|
const files = tree.files();
|
|
381
|
-
if (
|
|
405
|
+
if (files.some(file => /^src\/tests\/(?:world|integration|e2e|contract)\/.+\.[cm]?tsx?$/u.test(file)) && !excludesTestTrees &&
|
|
382
406
|
(defaultAll || JSON.stringify(tsconfig.include ?? []).includes('src')))
|
|
383
407
|
finding(rule, 'tsconfig.json', 'The default tsconfig includes src/tests/{world,integration,e2e,contract}/**. Exclude those trees and check them with src/tests/tsconfig.json (typecheck:tests).');
|
|
384
408
|
}
|
|
385
|
-
// 4. A workflow that starts on push or pull_request never runs e2e.
|
|
386
|
-
for (const file of tree.files().filter(entry => /^\.github\/workflows\/[^/]+\.ya?ml$/u.test(entry))) {
|
|
387
|
-
const text = readText(root, file);
|
|
388
|
-
if (text === null) continue;
|
|
389
|
-
const trigger = /^on:.*(?:\n(?:[ \t]+.*|)$)*/mu.exec(text)?.[0] ?? '';
|
|
390
|
-
if (!/\b(?:push|pull_request)\b/u.test(trigger)) continue;
|
|
391
|
-
if (runsE2e(withoutComments(text)))
|
|
392
|
-
finding(rule, file, `${file} runs e2e on push or pull_request. Move the e2e job to its own workflow with on: workflow_dispatch only.`);
|
|
393
|
-
}
|
|
394
409
|
}
|
|
395
410
|
|
|
396
411
|
export function checkHfs(config) {
|
|
@@ -401,29 +416,27 @@ export function checkHfs(config) {
|
|
|
401
416
|
const tree = treeView(config.root);
|
|
402
417
|
const violations = [];
|
|
403
418
|
const finding = (ruleId, entry, message) => violations.push({ ruleId, path: entry, line: 1, column: 1, message });
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const allowed = slotRootEntries(resolver);
|
|
419
|
+
const profile = backend ? 'be' : 'fe';
|
|
420
|
+
const resolver = resolverOf(config.root, profile, config.hfs);
|
|
421
|
+
// The README, the hooks, the workflows and the scripts are the app root's (checkAppRoot); a side folder is judged as the old
|
|
422
|
+
// repository root it stands for, less those.
|
|
423
|
+
const allowed = slotRootEntries(resolver, profile);
|
|
409
424
|
for (const entry of [...tree.top].sort()) {
|
|
410
425
|
if (NON_NPM_ENTRIES.has(entry) || /\.md$/iu.test(entry)) continue;
|
|
411
426
|
if (frontend && !backend) {
|
|
412
|
-
if (entry === '.starciwork') { finding('HFS_WORK_IN_FE', entry, '
|
|
413
|
-
if (entry === '.starcistacks') { finding('HFS_STACKS_IN_FE', entry, '
|
|
414
|
-
if (entry === 'src') { finding('HFS_ROOT_SRC_FORBIDDEN_FE', entry, '
|
|
427
|
+
if (entry === '.starciwork') { finding('HFS_WORK_IN_FE', entry, 'The fe side must not hold a .starciwork tree; Work records live in the app root .starciwork.'); continue; }
|
|
428
|
+
if (entry === '.starcistacks') { finding('HFS_STACKS_IN_FE', entry, 'The fe side must not hold .starcistacks; stack declarations live in be/.starcistacks.'); continue; }
|
|
429
|
+
if (entry === 'src') { finding('HFS_ROOT_SRC_FORBIDDEN_FE', entry, 'The fe side keeps source only under apps/<app>/src; the fe/src/ tree must move.'); continue; }
|
|
415
430
|
}
|
|
416
431
|
if (frontend && !backend && (isFeTestPath(entry) || isFeTestPath(`${entry}/x`))) continue; // a test entry of a front end is FE_NO_TESTS's, the one finding of that path
|
|
417
432
|
if (!allowed.has(entry)) {
|
|
418
|
-
finding('HFS_ROOT_ENTRY_FORBIDDEN', entry, `Root entry ${entry} is not in the
|
|
433
|
+
finding('HFS_ROOT_ENTRY_FORBIDDEN', entry, `Root entry ${entry} of the ${profile} side is not in the slots of the ${profile} side.`);
|
|
419
434
|
}
|
|
420
435
|
}
|
|
421
436
|
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (entry === 'apps' || entry === 'README.md' || entry === '.gitattributes') continue;
|
|
426
|
-
if (!tree.top.includes(entry)) finding('HFS_ROOT_ENTRY_MISSING', entry, `The ${backend ? 'backend' : 'frontend'} HFS tree requires root entry ${entry}.`);
|
|
437
|
+
for (const entry of [...requiredRootEntries(resolver)].sort()) {
|
|
438
|
+
if (entry === 'apps') continue;
|
|
439
|
+
if (!tree.top.includes(entry)) finding('HFS_ROOT_ENTRY_MISSING', entry, `The ${profile} side requires root entry ${entry}.`);
|
|
427
440
|
}
|
|
428
441
|
|
|
429
442
|
const apps = tree.children('apps').sort();
|
|
@@ -444,8 +457,9 @@ export function checkHfs(config) {
|
|
|
444
457
|
}
|
|
445
458
|
// next-env.d.ts is generated by Next and commonly ignored by Git; it is not
|
|
446
459
|
// a reliable tracked-tree input.
|
|
447
|
-
|
|
448
|
-
|
|
460
|
+
// A front-end app holds the files its slot requires at its own root (next.config.ts, tsconfig.json, ...; it has no package.json).
|
|
461
|
+
if (frontend) for (const required of resolver.requiredFiles(`apps/${app}/next.config.ts`).filter(entry => !entry.slice(`apps/${app}/`.length).includes('/'))) {
|
|
462
|
+
if (!tree.hasFile(required)) missing.push(required.slice(`apps/${app}/`.length));
|
|
449
463
|
}
|
|
450
464
|
if (missing.length) finding('HFS_APP_LAYOUT_INVALID', `apps/${app}`, `Application apps/${app} lacks ${missing.join(', ')} required by the HFS app layout.`);
|
|
451
465
|
}
|
|
@@ -481,15 +495,13 @@ export function checkHfs(config) {
|
|
|
481
495
|
finding('HFS_TEST_KIND_RETIRED', file, `${file} is a per-lane test config. One root jest.config.js declares exactly the unit, integration, e2e and contract projects.`);
|
|
482
496
|
}
|
|
483
497
|
|
|
484
|
-
|
|
485
|
-
const hooksPath = hooksPathNotRedirected({ root: config.root, finding });
|
|
498
|
+
if (backend) testTreesOutOfDefaultProgram({ root: config.root, tree, finding });
|
|
486
499
|
|
|
487
500
|
return {
|
|
488
501
|
violations,
|
|
489
502
|
coverage: {
|
|
490
503
|
status: 'checked',
|
|
491
504
|
source: tree.source,
|
|
492
|
-
hooksPath,
|
|
493
505
|
rootEntries: tree.top.length,
|
|
494
506
|
apps,
|
|
495
507
|
ruleIds: [...HFS_RULE_IDS],
|
|
@@ -497,6 +509,30 @@ export function checkHfs(config) {
|
|
|
497
509
|
};
|
|
498
510
|
}
|
|
499
511
|
|
|
512
|
+
/**
|
|
513
|
+
* The HFS tree check of the app root (`resolver` is the app's): the README, the root entries the app-root slots allow and require,
|
|
514
|
+
* the automatic gates (hooks, the root scripts they call, the workflows) and the hooks path. `hfs check` runs it once per app; the
|
|
515
|
+
* machine runs checkHfs once per side folder.
|
|
516
|
+
*/
|
|
517
|
+
export function checkAppRoot({ root, resolver, tree = treeView(root) }) {
|
|
518
|
+
const violations = [];
|
|
519
|
+
const finding = (ruleId, entry, message) => violations.push({ ruleId, path: entry, line: 1, column: 1, message });
|
|
520
|
+
violations.push(...checkRepoPresentation({ root, tree, profile: 'app' }).violations);
|
|
521
|
+
const allowed = slotRootEntries(resolver, 'app');
|
|
522
|
+
for (const entry of [...tree.top].sort()) {
|
|
523
|
+
if (NON_NPM_ENTRIES.has(entry) || /\.md$/iu.test(entry)) continue;
|
|
524
|
+
if (!allowed.has(entry)) finding('HFS_ROOT_ENTRY_FORBIDDEN', entry, `Root entry ${entry} is not in the slots of the app root.`);
|
|
525
|
+
}
|
|
526
|
+
for (const entry of [...requiredRootEntries(resolver)].sort()) {
|
|
527
|
+
if (entry === 'README.md' || entry === '.gitattributes') continue; // checkRepoPresentation reports these two
|
|
528
|
+
if (!tree.top.includes(entry)) finding('HFS_ROOT_ENTRY_MISSING', entry, `The app root requires root entry ${entry}.`);
|
|
529
|
+
}
|
|
530
|
+
const jestConfig = resolver.sides ? `be/${[...braceVariants(resolver.slot('be.tool-config').path)].find(file => file.startsWith('jest.config.'))}` : null;
|
|
531
|
+
e2eInAutomaticGate({ root, tree, jestConfig, finding });
|
|
532
|
+
const hooksPath = hooksPathNotRedirected({ root, finding });
|
|
533
|
+
return { violations, coverage: { status: 'checked', source: tree.source, hooksPath, rootEntries: tree.top.length } };
|
|
534
|
+
}
|
|
535
|
+
|
|
500
536
|
/** Keep tree findings visible even when the TypeScript architecture config is invalid. */
|
|
501
537
|
export function checkHfsWithoutConfig(repositoryRoot) {
|
|
502
538
|
let root;
|
|
@@ -505,9 +541,11 @@ export function checkHfsWithoutConfig(repositoryRoot) {
|
|
|
505
541
|
}
|
|
506
542
|
let kinds = [];
|
|
507
543
|
try {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
544
|
+
const opened = openHfs({ repoRoot: root });
|
|
545
|
+
// The app root is judged by checkAppRoot; a side folder by its side's tree check.
|
|
546
|
+
if (opened.repo.profile === 'app') return checkAppRoot({ root, resolver: opened });
|
|
547
|
+
if (opened.repo.profile === 'be') kinds = ['backend'];
|
|
548
|
+
else if (opened.repo.profile === 'fe') kinds = ['frontend'];
|
|
511
549
|
} catch { /* A malformed hfs.json remains an HFS_DECLARATION_INVALID error. */ }
|
|
512
550
|
if (!kinds.length) {
|
|
513
551
|
try {
|
|
@@ -15,7 +15,6 @@ import { checkTiers, TIER_RULE_IDS } from './tiers.mjs';
|
|
|
15
15
|
import { checkReachability, REACHABILITY_RULE_IDS } from './reachability.mjs';
|
|
16
16
|
import { checkDeadExports, DEAD_EXPORT_RULE_IDS } from './dead-exports.mjs';
|
|
17
17
|
import { checkRequiredFiles, REQUIRED_FILE_RULE_IDS } from './required-files.mjs';
|
|
18
|
-
import { checkSizeGrowth, SIZE_GROWTH_RULE_IDS } from './size-growth.mjs';
|
|
19
18
|
import { checkClones, CLONE_RULE_IDS } from './clones.mjs';
|
|
20
19
|
import { checkSymbols, SYMBOL_RULE_IDS } from './symbols.mjs';
|
|
21
20
|
import { checkConnectionMap, CONNECTION_RULE_IDS } from './connection-map.mjs';
|
|
@@ -43,6 +42,7 @@ import { checkPackageShape, PACKAGE_SHAPE_RULE_IDS } from './package-shape.mjs';
|
|
|
43
42
|
import { checkFeSlotAllows, FE_SLOT_ALLOWS_RULE_IDS } from './fe-slot-allows.mjs';
|
|
44
43
|
import { checkI18nKeys, I18N_KEYS_RULE_IDS } from './i18n-keys.mjs';
|
|
45
44
|
import { checkDocLanguage, DOC_LANGUAGE_RULE_IDS } from './doc-language.mjs';
|
|
45
|
+
import { LINT_CODES, onLintSurface } from './surface.mjs';
|
|
46
46
|
|
|
47
47
|
export { REGISTRATION_RULE_IDS, SWR_DATA_RULE_IDS };
|
|
48
48
|
|
|
@@ -138,13 +138,12 @@ export const ERROR_RULE_IDS = Object.freeze([
|
|
|
138
138
|
]);
|
|
139
139
|
|
|
140
140
|
const machineIds = () => [
|
|
141
|
-
...TIER_RULE_IDS, ...REACHABILITY_RULE_IDS, ...DEAD_EXPORT_RULE_IDS, ...REQUIRED_FILE_RULE_IDS, ...
|
|
141
|
+
...TIER_RULE_IDS, ...REACHABILITY_RULE_IDS, ...DEAD_EXPORT_RULE_IDS, ...REQUIRED_FILE_RULE_IDS, ...CLONE_RULE_IDS, ...SYMBOL_RULE_IDS,
|
|
142
142
|
...Object.values(BACKEND_MACHINE).flatMap(([, ids]) => ids), ...Object.values(FRONTEND_MACHINE).flatMap(([, ids]) => ids),
|
|
143
143
|
];
|
|
144
144
|
/**
|
|
145
|
-
* The rules only the HFS machine emits: the tier, reachability, dead-export, required-file,
|
|
146
|
-
* backend composition and data machine and frontend repository machine. A
|
|
147
|
-
* spec and the app-composition check both report BE_APP_COMPOSITION_ONLY) is not here. A spec about another rule judges its own
|
|
145
|
+
* The rules only the HFS machine emits: the tier, reachability, dead-export, required-file, clone and symbol checks and the
|
|
146
|
+
* backend composition and data machine and frontend repository machine. A spec about another rule judges its own
|
|
148
147
|
* report without these (they have their own specs); every one of them is in ARCHITECTURE_RULE_IDS.
|
|
149
148
|
*/
|
|
150
149
|
export const HFS_MACHINE_RULE_IDS = Object.freeze((() => {
|
|
@@ -156,7 +155,7 @@ export const HFS_MACHINE_RULE_IDS = Object.freeze((() => {
|
|
|
156
155
|
/** Every code the machine can emit, derived from the rule id lists of its checks (`hfs check` ships exactly these why entries). */
|
|
157
156
|
export const ARCHITECTURE_RULE_IDS = Object.freeze([...new Set([
|
|
158
157
|
...COMMON_RULE_IDS, ...BACKEND_RULE_IDS, ...FRONTEND_RULE_IDS, ...HFS_RULE_IDS, ...TIER_RULE_IDS, ...REACHABILITY_RULE_IDS,
|
|
159
|
-
...DEAD_EXPORT_RULE_IDS, ...DOC_LANGUAGE_RULE_IDS, ...REQUIRED_FILE_RULE_IDS, ...
|
|
158
|
+
...DEAD_EXPORT_RULE_IDS, ...DOC_LANGUAGE_RULE_IDS, ...REQUIRED_FILE_RULE_IDS, ...CLONE_RULE_IDS, ...OWNER_RULE_IDS, ...GRAMMAR_RULE_IDS,
|
|
160
159
|
...REGISTRATION_RULE_IDS, ...SWR_DATA_RULE_IDS, ...SYMBOL_RULE_IDS, SOURCE_LAYOUT_RULE_ID, SOURCE_NAME_RULE_ID, PUBLIC_CONTRACT_RULE_ID,
|
|
161
160
|
READONLY_BOUNDARY_RULE_ID, ...ERROR_RULE_IDS, ...CONFIG_UNREAD_RULE_IDS, ...Object.values(BACKEND_MACHINE).flatMap(([, ids]) => ids), ...Object.values(FRONTEND_MACHINE).flatMap(([, ids]) => ids),
|
|
162
161
|
])].sort());
|
|
@@ -169,7 +168,8 @@ function stable(items) {
|
|
|
169
168
|
* Check a target repository. injectedTypeScript exists only for hermetic rule fixtures. `fast` leaves out the checks
|
|
170
169
|
* that read the whole repository to answer (clones, dead exports, repository-wide symbols); the pre-push check of the changed owners uses it.
|
|
171
170
|
*/
|
|
172
|
-
export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths = [], base, fast = false, hfs: openedHfs } = {}) {
|
|
171
|
+
export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths = [], base, fast = false, hfs: openedHfs, surface = 'all' } = {}) {
|
|
172
|
+
if (!['all', 'lint', 'check'].includes(surface)) throw new Error(`unknown surface ${surface}`);
|
|
173
173
|
let config;
|
|
174
174
|
try {
|
|
175
175
|
config = loadArchitectureConfig(repositoryRoot, { hfs: openedHfs });
|
|
@@ -233,24 +233,30 @@ export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths =
|
|
|
233
233
|
}
|
|
234
234
|
// HFS machine: the slot-driven graph checks read the whole program, also when the caller asked for one path.
|
|
235
235
|
const hfsContext = paths.length ? buildTypeScriptContext(config, injectedTypeScript) : context;
|
|
236
|
-
const hfsChecks = { tiers: null, reachability: null, deadExports: null, requiredFiles: null,
|
|
236
|
+
const hfsChecks = { tiers: null, reachability: null, deadExports: null, requiredFiles: null, clones: null, symbols: null, docLanguage: null };
|
|
237
237
|
if (hfsContext.program) {
|
|
238
238
|
const graph = buildHfsGraph(config, hfsContext);
|
|
239
239
|
const input = { config, context: hfsContext, graph, base };
|
|
240
240
|
const runs = { tiers: () => checkTiers(graph), reachability: () => checkReachability(input), deadExports: () => checkDeadExports(input),
|
|
241
|
-
requiredFiles: () => checkRequiredFiles(input),
|
|
241
|
+
requiredFiles: () => checkRequiredFiles(input), clones: () => checkClones(input), symbols: () => checkSymbols(input), docLanguage: () => checkDocLanguage(input) };
|
|
242
242
|
if (graph.profile === 'be') for (const [name, [check]] of Object.entries(BACKEND_MACHINE)) runs[name] = () => check(input);
|
|
243
243
|
if (graph.profile === 'fe') for (const [name, [check]] of Object.entries(FRONTEND_MACHINE)) runs[name] = () => check(input);
|
|
244
244
|
if (fast) { delete runs.deadExports; delete runs.clones; delete runs.symbols; }
|
|
245
245
|
for (const [name, run] of Object.entries(runs)) {
|
|
246
246
|
const result = run();
|
|
247
|
-
violations.push(...result.violations);
|
|
247
|
+
violations.push(...result.violations.map(violation => ({ ...violation, check: name })));
|
|
248
248
|
hfsChecks[name] = result.coverage;
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
const inScope = item => !paths.length || (item.path && paths.some(prefix => sameOrUnder(item.path, prefix.replace(/\/$/, ''))));
|
|
252
|
-
|
|
253
|
-
const
|
|
252
|
+
// Two findings travel as context errors (package boundary edges); they are obligations of the lint surface like any other finding.
|
|
253
|
+
const asViolation = item => LINT_CODES.has(item.ruleId);
|
|
254
|
+
const allErrors = context.errors.filter(item => item.ruleId.startsWith('ARCH_TSCONFIG_') || !item.path || inScope(item));
|
|
255
|
+
const obligations = surface === 'all' ? violations : [...violations, ...context.errors.filter(asViolation).filter(inScope)];
|
|
256
|
+
// The lint surface is what an editor can show on a line of a TypeScript file; `hfs check` keeps every other finding of the machine.
|
|
257
|
+
const onSurface = item => surface === 'all' || onLintSurface(config.root, item) === (surface === 'lint');
|
|
258
|
+
const errors = stable(surface === 'all' ? allErrors : allErrors.filter(item => !asViolation(item)));
|
|
259
|
+
const scopedViolations = stable(obligations.filter(inScope).filter(onSurface));
|
|
254
260
|
const sourceFiles = new Set(context.files.map(file => canonical(file.fileName)));
|
|
255
261
|
const missingOwnerEntries = config.owners?.filter(owner => !sourceFiles.has(canonical(path.resolve(config.root, ...owner.entry.split('/'))))) ?? [];
|
|
256
262
|
const coverage = {
|
|
@@ -282,7 +288,6 @@ export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths =
|
|
|
282
288
|
...(hfsChecks.reachability?.status === 'checked' ? REACHABILITY_RULE_IDS : []),
|
|
283
289
|
...(hfsChecks.deadExports?.status === 'checked' ? DEAD_EXPORT_RULE_IDS : []),
|
|
284
290
|
...(hfsChecks.requiredFiles?.status === 'checked' ? REQUIRED_FILE_RULE_IDS : []),
|
|
285
|
-
...(hfsChecks.sizeGrowth?.status === 'checked' ? SIZE_GROWTH_RULE_IDS : []),
|
|
286
291
|
...(hfsChecks.clones?.status === 'checked' ? CLONE_RULE_IDS : []),
|
|
287
292
|
...(hfsChecks.symbols?.status === 'checked' ? SYMBOL_RULE_IDS : []),
|
|
288
293
|
...(hfsChecks.docLanguage?.status === 'checked' ? DOC_LANGUAGE_RULE_IDS : []),
|
|
@@ -37,7 +37,8 @@ function relativeSource(repository, value, label) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function parseContract(config) {
|
|
40
|
-
|
|
40
|
+
// The contract is declared in the app's one package.json (the app root; config.root is the fe side folder).
|
|
41
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(config.packageRoot ?? config.root, 'package.json'), 'utf8'));
|
|
41
42
|
const next = manifest?.starci?.codePatterns?.next;
|
|
42
43
|
const value = next?.dataLifecycle;
|
|
43
44
|
if (value === undefined) return null;
|
|
@@ -711,7 +712,7 @@ export function checkFrontendDataLifecycle(config, context) {
|
|
|
711
712
|
details: ['package.json#starci.codePatterns.next.dataLifecycle is required when production source selects SWR', ...references.unsupported] } };
|
|
712
713
|
reasons.push(...references.unsupported);
|
|
713
714
|
let installed;
|
|
714
|
-
try { installed = installedSWR(config.root); } catch (error) {
|
|
715
|
+
try { installed = installedSWR(config.packageRoot ?? config.root); } catch (error) {
|
|
715
716
|
return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
|
|
716
717
|
}
|
|
717
718
|
if (installed.major !== contract.swr.major) reasons.push(`installed swr ${installed.version} does not match declared major ${contract.swr.major}`);
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { canonical, isInside } from './config.mjs';
|
|
3
|
-
import { relativePath, sourceLocation } from './typescript.mjs';
|
|
3
|
+
import { relativePath, sourceLocation, workspaceExportSources } from './typescript.mjs';
|
|
4
4
|
|
|
5
5
|
function absolute(root, relative) {
|
|
6
6
|
return canonical(path.resolve(root, ...relative.split('/')));
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/** The owner's public entries: its declared entry, plus every source file its package.json `exports` maps when the owner is a workspace package. */
|
|
10
|
+
function ownerDeclarations(config, context) {
|
|
11
|
+
return config.owners.map(owner => {
|
|
12
|
+
const root = absolute(config.root, owner.root);
|
|
13
|
+
const entry = absolute(config.root, owner.entry);
|
|
14
|
+
const workspace = context.workspaces?.find(item => item.root === root);
|
|
15
|
+
const entries = new Set([entry, ...(workspace ? workspaceExportSources(context.ts, workspace) : [])]);
|
|
16
|
+
return { ...owner, root, entry, entries };
|
|
17
|
+
}).sort((a, b) => b.root.length - a.root.length);
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
function ownerOf(owners, file) {
|
|
@@ -24,7 +30,7 @@ function privateOwnerChain(context, owners, edge) {
|
|
|
24
30
|
if (visited.has(current.file)) continue;
|
|
25
31
|
visited.add(current.file);
|
|
26
32
|
const owner = ownerOf(owners, current.file);
|
|
27
|
-
if (owner && sourceOwner?.id !== owner.id) return current.file
|
|
33
|
+
if (owner && sourceOwner?.id !== owner.id) return owner.entries.has(current.file) ? null : { owner, chain: current.chain };
|
|
28
34
|
for (const candidate of context.edges.get(current.file) ?? []) if (candidate.reexport) {
|
|
29
35
|
queue.push({ file: candidate.to, chain: [...current.chain, candidate.to] });
|
|
30
36
|
}
|
|
@@ -46,7 +52,7 @@ function arrangesSchema(config, from, to) {
|
|
|
46
52
|
/** Enforce explicit same-source owner entries without constraining imports inside one owner. */
|
|
47
53
|
export function checkOwners(config, context) {
|
|
48
54
|
if (!config.owners?.length) return [];
|
|
49
|
-
const owners = ownerDeclarations(config);
|
|
55
|
+
const owners = ownerDeclarations(config, context);
|
|
50
56
|
const violations = [];
|
|
51
57
|
const sourceFiles = new Map(context.files.map(file => [canonical(file.fileName), file]));
|
|
52
58
|
for (const owner of owners) {
|
|
@@ -115,7 +115,7 @@ function frameworkTargets(config, context, checker, localFiles) {
|
|
|
115
115
|
|
|
116
116
|
function classToken(ts, checker, node, localFiles) {
|
|
117
117
|
const symbol = valueSymbol(ts, checker, node);
|
|
118
|
-
const declarations = symbol?.getDeclarations?.().filter(declaration => ts.isClassDeclaration(declaration)) ?? [];
|
|
118
|
+
const declarations = (symbol?.getDeclarations?.() ?? []).filter(declaration => ts.isClassDeclaration(declaration)) ?? [];
|
|
119
119
|
const local = declarations.filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName)));
|
|
120
120
|
const keys = [...new Set(local.map(declaration => `${canonical(declaration.getSourceFile().fileName)}#${declaration.getStart(declaration.getSourceFile())}`))];
|
|
121
121
|
return { key: keys.length === 1 ? keys[0] : null, external: declarations.length > 0 && local.length === 0 };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lint surface: which architecture-machine findings an ESLint rule judges instead of `hfs check`.
|
|
3
|
+
* Single source of truth. Each enforcer id is the rules.yaml enforcer; `codes` are the exact violation.ruleId strings its
|
|
4
|
+
* check file emits on source files. plugins: 'be' = back-end profile repos, 'fe' = front-end profile, graph checks run for both.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
const BE = ['be'];
|
|
10
|
+
const FE = ['fe'];
|
|
11
|
+
const BOTH = ['be', 'fe'];
|
|
12
|
+
|
|
13
|
+
// `via` names the machine check (the key of its run in index.mjs) when two enforcers share a code; `origin: 'repo'` marks a finding of the
|
|
14
|
+
// slot manifest's per-path judgement (hfs-path-findings.mjs) instead of the machine. A finding reaches exactly one rule.
|
|
15
|
+
const enforcer = (id, plugins, codes, description, extra = {}) => Object.freeze({ id, plugins: Object.freeze(plugins), codes: Object.freeze(codes), description, ...extra });
|
|
16
|
+
|
|
17
|
+
export const LINT_ENFORCERS = Object.freeze([
|
|
18
|
+
enforcer('duplicate-code', BOTH, ['HFS_DUPLICATE_CODE'], 'A block of production code never repeats elsewhere in the owner graph.'),
|
|
19
|
+
enforcer('duplicate-symbol', BOTH, ['HFS_DUPLICATE_SYMBOL'], 'A symbol name is declared once across the production source.'),
|
|
20
|
+
enforcer('alias-reexport', BOTH, ['HFS_ALIAS_REEXPORT'], 'An owner entry re-exports a symbol under its own name, never through a const alias.'),
|
|
21
|
+
// cross-app-duplicate emits only FE_CROSS_APP_DUPLICATE today; HFS_DUPLICATE_CODE is named in its comment only.
|
|
22
|
+
enforcer('cross-app-duplicate', FE, ['FE_CROSS_APP_DUPLICATE'], 'A file is never copied between apps; shared code lives in a package.'),
|
|
23
|
+
enforcer('dead-exports', BOTH, ['HFS_UNUSED_EXPORT', 'HFS_UNUSED_FILE'], 'Every owner export and every production file is reached by a root.'),
|
|
24
|
+
enforcer('tier-direction', BOTH, ['BE_TIER_DIRECTION', 'FE_TIER_DIRECTION'], 'An import goes only to a tier the slot matrix allows.'),
|
|
25
|
+
enforcer('owner-cycle', BOTH, ['ARCH_OWNER_CYCLE'], 'Owners never import each other in a cycle.'),
|
|
26
|
+
enforcer('feature-imports-feature', BE, ['BE_FEATURE_IMPORTS_FEATURE'], 'A feature never imports another feature.'),
|
|
27
|
+
enforcer('app-isolation', FE, ['FE_APP_ISOLATION'], 'An app never imports another app.'),
|
|
28
|
+
enforcer('feature-layout', BE, ['BE_FEATURE_LAYOUT_INVALID'], 'A feature file sits in the folder its role names.'),
|
|
29
|
+
enforcer('application-never-imports-transport', BE, ['BE_APPLICATION_IMPORTS_TRANSPORT', 'BE_APPLICATION_TRANSPORT_FRAMEWORK'], 'Application code never imports transport code, directly or through a chain.'),
|
|
30
|
+
enforcer('owner-export-bypass', BOTH, ['ARCH_OWNER_EXPORT_BYPASS', 'ARCH_OWNER_EXPORT_STAR'], 'Another owner is imported only through its index entry, and an entry never uses export star.'),
|
|
31
|
+
enforcer('feature-not-composed', BE, ['BE_FEATURE_NOT_COMPOSED'], 'Every feature owner is composed into an app.'),
|
|
32
|
+
enforcer('owner-reachable', FE, ['FE_OWNER_REACHABLE', 'FE_HREF_RESOLVES'], 'Every owner is mounted by a route and every href resolves to a route.'),
|
|
33
|
+
enforcer('app-composition-only', BE, ['BE_APP_COMPOSITION_ONLY', 'BE_APP_BUSINESS_ROLE'], 'An app only composes modules and holds no business role.'),
|
|
34
|
+
enforcer('entrypoint-only-in-apps', BE, ['BE_ENTRYPOINT_ONLY_IN_APPS'], 'NestFactory and bootstrap live only in an app main file.'),
|
|
35
|
+
enforcer('schema-owner', BE, ['BE_SCHEMA_OWNER'], 'Each entity array is registered by one owner across all apps.'),
|
|
36
|
+
enforcer('error-code-unique', BE, ['BE_ERROR_HOME'], 'An error code is declared once in the repository.'),
|
|
37
|
+
enforcer('error-masked', BE, ['BE_ERROR_MASKED'], 'Every app wires the error filter that masks internal errors.'),
|
|
38
|
+
enforcer('default-deny-app-guard', BE, ['BE_DEFAULT_DENY'], 'Every app registers the default-deny guard before any other guard.'),
|
|
39
|
+
// BE_MODULE_SHAPE is emitted by both register-once and module-per-transport.
|
|
40
|
+
enforcer('register-once', BE, ['BE_MODULE_SHAPE'], 'A module is registered once and imported by one module.', { via: 'registerOnce' }),
|
|
41
|
+
enforcer('module-per-transport', BE, ['BE_MODULE_SHAPE'], 'An app imports transport modules only.', { via: 'modulePerTransport' }),
|
|
42
|
+
enforcer('module-registration', BE, ['BE_MODULE_HANDLER_REGISTRATION', 'BE_MODULE_PROVIDER_REREGISTRATION'], 'Every handler is registered by a module and no provider is registered twice.'),
|
|
43
|
+
enforcer('background-unowned', BE, ['BE_BACKGROUND_UNOWNED'], 'Every background job is reachable from a worker app.'),
|
|
44
|
+
enforcer('test-world-files', BE, ['BE_TEST_TOPOLOGY'], 'The test world files match the stack declaration.'),
|
|
45
|
+
enforcer('unit-spec-providers', BE, ['BE_SPEC_QUALITY'], 'A unit spec provides exactly the dependencies its service constructor takes.'),
|
|
46
|
+
enforcer('transport-owner', FE, ['FE_TRANSPORT_OWNER'], 'One client and one outcome type own the transport of the repository.'),
|
|
47
|
+
enforcer('swr-data-lifecycle', FE, ['FE_SWR_KEY_IDENTITY', 'FE_SWR_MUTATION_RESOURCE_IDENTITY'], 'SWR keys and mutations share one resource identity.'),
|
|
48
|
+
enforcer('route-files-thin', FE, ['FE_ROUTE_FILES_THIN'], 'A route file only mounts one feature owner.'),
|
|
49
|
+
enforcer('route-adapter', FE, ['FE_ROUTE_ONE_PAGE', 'FE_ROUTE_DRAWING_DECISION', 'FE_ROUTE_CLIENT_BOUNDARY', 'FE_ROUTE_CLIENT_HOOK', 'FE_ROUTE_DEFAULT_EXPORT'], 'A page route is a server adapter that mounts one pages-tier component.'),
|
|
50
|
+
enforcer('client-reaches-server', FE, ['FE_CLIENT_REACHES_SERVER'], 'Client code never reaches server-only code through any import chain.'),
|
|
51
|
+
enforcer('hooks-are-hooks', FE, ['FE_HOOKS_ARE_HOOKS'], 'A hooks folder holds hooks only, one shared file per domain.'),
|
|
52
|
+
enforcer('hook-location', FE, ['FE_CUSTOM_HOOK_LOCATION', 'FE_BLOCK_PRODUCT_HOOK_DEFINITION', 'FE_COMPONENT_DEEP_HOOK_IMPORT'], 'A custom hook is defined in the hooks folder and imported through its entry.'),
|
|
53
|
+
enforcer('connection-map', BE, ['BE_CONNECTION_DUPLICATE'], 'Each database connection is declared once and matches the connection map.'),
|
|
54
|
+
enforcer('injection-token-exported', BE, ['BE_RAW_INJECT'], 'An injection token is exported by its owner and never injected raw.'),
|
|
55
|
+
enforcer('sql-owner', BE, ['BE_SQL_TABLE_OWNER'], 'A SQL table is touched only by the owner of its entity.'),
|
|
56
|
+
enforcer('readonly-boundary', BE, ['BE_READONLY_BOUNDARY'], 'Messages and injected classes crossing a boundary are readonly.'),
|
|
57
|
+
enforcer('source-names', BE, ['BE_SOURCE_FORM'], 'Names and forms inside a source file follow its role.'),
|
|
58
|
+
enforcer('public-contract-form', BE, ['BE_PUBLIC_CONTRACT_FORM'], 'A public signature is typed by a named contract, never inline or untyped.'),
|
|
59
|
+
enforcer('frontend-source-layout', FE, ['FE_SOURCE_LAYOUT_INVALID'], 'A front-end source file sits in a tier folder.'),
|
|
60
|
+
enforcer('component-purity', FE, ['FE_COMPONENT_WORLD_OWNERSHIP', 'FE_WORLD_OWNER_RENDER_BOUNDARY', 'FE_CONNECTED_BLOCK_RENDER_PAIR', 'FE_PURE_REACHES_DATA', 'FE_PURE_WORLD_HOOK', 'FE_PURE_WORLD_IMPORT'], 'A pure component reaches no data or world state; its connected entry owns them.'),
|
|
61
|
+
enforcer('grammar-entry', FE, ['ARCH_GRAMMAR_CONTRACT_INVALID', 'ARCH_GRAMMAR_EXPORT_BYPASS'], 'Vendor grammar is reached only through its owner entry.'),
|
|
62
|
+
enforcer('i18n-keys', FE, ['FE_I18N_KEYS'], 'Translation keys read by source and keys held by catalogs agree both ways.'),
|
|
63
|
+
// The per-path judgements of the slot manifest (scripts/lib/hfs-path-findings.mjs), on a tracked TypeScript file.
|
|
64
|
+
enforcer('slot-undeclared', BOTH, ['HFS_SLOT_UNDECLARED', 'HFS_SLOT_AMBIGUOUS', 'HFS_SLOT_NOT_ENABLED'], 'A TypeScript file is owned by exactly one slot of the repository.', { origin: 'repo' }),
|
|
65
|
+
enforcer('source-suffix', BE, ['BE_SOURCE_FORM'], 'A source file name is index.ts, main.ts, a migration or <kebab-name>.<suffix>.ts with a suffix of the closed list.', { origin: 'repo' }),
|
|
66
|
+
enforcer('spec-placement', BE, ['BE_SPEC_PLACEMENT'], 'A spec file lives in one of the four test layers and nowhere else.', { origin: 'repo' }),
|
|
67
|
+
// The two below are emitted into context.errors today, not into violations.
|
|
68
|
+
enforcer('package-imports-app', BOTH, ['ARCH_PACKAGE_IMPORTS_APP'], 'A package never imports an app.'),
|
|
69
|
+
enforcer('package-export-bypass', BOTH, ['ARCH_PACKAGE_EXPORT_BYPASS'], 'A package is imported only through its declared exports.'),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
export const LINT_CODES = new Set(LINT_ENFORCERS.flatMap(e => e.codes));
|
|
73
|
+
|
|
74
|
+
export const TS_SOURCE = /\.(?:[cm]?tsx?)$/;
|
|
75
|
+
|
|
76
|
+
/** True when a finding sits on an existing TypeScript file of the repository: the only findings an editor can show on a line. */
|
|
77
|
+
export function attachesToSource(repositoryRoot, violation) {
|
|
78
|
+
const file = violation?.path;
|
|
79
|
+
if (typeof file !== 'string' || !TS_SOURCE.test(file)) return false;
|
|
80
|
+
return fs.existsSync(path.join(repositoryRoot, ...file.split('/')));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The finding's code: a machine violation spells it `ruleId`, a repository finding `code`. */
|
|
84
|
+
export const codeOf = (finding) => finding?.ruleId ?? finding?.code;
|
|
85
|
+
|
|
86
|
+
/** Whether `finding` (with its origin and check tags) belongs to `enforcer`. */
|
|
87
|
+
export const belongsTo = (enforcer, finding) => enforcer.codes.includes(codeOf(finding))
|
|
88
|
+
&& (finding.origin ?? 'machine') === (enforcer.origin ?? 'machine') && (enforcer.via === undefined || finding.check === enforcer.via);
|
|
89
|
+
|
|
90
|
+
/** True when some lint rule owns the finding and it sits on an existing TypeScript file: it is an ESLint report, not a `hfs check` finding. */
|
|
91
|
+
export const onLintSurface = (repositoryRoot, finding) => LINT_ENFORCERS.some(enforcer => belongsTo(enforcer, finding)) && attachesToSource(repositoryRoot, finding);
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* exported `const Y = X` / `const Y = X.y` (a bare identifier or member that resolves to a function,
|
|
9
9
|
* class, const or enum of the repository), `type Y = X` and `interface Y extends X {}` (no body, no
|
|
10
10
|
* type arguments, no type parameters): a second name for one declaration. Rename the declaration,
|
|
11
|
-
* or import it by its name.
|
|
11
|
+
* or import it by its name. A route file (slot fe.route) binding a declaration to a name Next.js
|
|
12
|
+
* requires of a route segment (generateMetadata, generateStaticParams, ...) is not a second name: the
|
|
13
|
+
* framework fixes that name, so no rename can remove it.
|
|
12
14
|
* Specs and tests are not in the graph. The public entry of an owner is found the way dead-exports.mjs finds it.
|
|
13
15
|
*/
|
|
14
16
|
import { canonical } from './config.mjs';
|
|
@@ -145,14 +147,23 @@ function declarationAliases(ts, checker, graph, statement) {
|
|
|
145
147
|
return found;
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
/** The names Next.js reads from a route segment file (layout, page, route, ...): the framework fixes them. */
|
|
151
|
+
const NEXT_SEGMENT_EXPORTS = new Set([
|
|
152
|
+
'generateMetadata', 'metadata', 'generateViewport', 'viewport', 'generateStaticParams', 'generateImageMetadata', 'generateSitemaps',
|
|
153
|
+
'dynamic', 'dynamicParams', 'revalidate', 'fetchCache', 'runtime', 'preferredRegion', 'maxDuration',
|
|
154
|
+
]);
|
|
155
|
+
const ROUTE_SLOT = 'fe.route';
|
|
156
|
+
|
|
148
157
|
function aliasReexports({ context, graph }) {
|
|
149
158
|
const kind = context.ts.SyntaxKind;
|
|
150
159
|
const violations = [];
|
|
151
160
|
for (const [rel, node] of graph.files) {
|
|
161
|
+
const frameworkName = name => node.slot === ROUTE_SLOT && NEXT_SEGMENT_EXPORTS.has(name);
|
|
152
162
|
const point = target => node.sourceFile.getLineAndCharacterOfPosition(target.getStart(node.sourceFile));
|
|
153
163
|
const checker = context.checkerFor(node.abs);
|
|
154
164
|
for (const statement of node.sourceFile.statements) {
|
|
155
165
|
for (const alias of declarationAliases(context.ts, checker, graph, statement)) {
|
|
166
|
+
if (frameworkName(alias.name)) continue;
|
|
156
167
|
const at = point(alias.node);
|
|
157
168
|
violations.push({
|
|
158
169
|
ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1, name: alias.name, aliasOf: alias.of,
|
|
@@ -170,7 +181,7 @@ function aliasReexports({ context, graph }) {
|
|
|
170
181
|
continue;
|
|
171
182
|
}
|
|
172
183
|
for (const element of clause.elements) {
|
|
173
|
-
if (!element.propertyName || element.propertyName.text === element.name.text) continue;
|
|
184
|
+
if (!element.propertyName || element.propertyName.text === element.name.text || frameworkName(element.name.text)) continue;
|
|
174
185
|
const at = point(element);
|
|
175
186
|
violations.push({
|
|
176
187
|
ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1,
|