@starci/eslint-canon-be 3.0.7 → 3.0.9
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 +13 -1
- package/README.md +13 -7
- package/ambient-id.mjs +90 -0
- package/cli.mjs +181 -0
- package/comments.mjs +18 -21
- package/config-owner.mjs +20 -4
- package/connections.mjs +12 -12
- package/cookies.mjs +106 -0
- package/cqrs.mjs +22 -26
- package/data-access.mjs +9 -28
- package/default-deny.mjs +2 -1
- package/e2e-flow.mjs +4 -4
- package/event-bus.mjs +237 -0
- package/feature-thin.mjs +237 -0
- package/idempotency.mjs +3 -1
- package/index.mjs +22 -1
- package/infra-owner.mjs +1 -1
- package/injection.mjs +67 -22
- package/input-bounds.mjs +2 -1
- package/jobs.mjs +214 -0
- package/lib/ast.mjs +7 -0
- package/lib/config.mjs +1 -1
- package/lib/db-writes.mjs +1 -1
- package/lib/doors.mjs +81 -0
- package/lib/edition.mjs +45 -0
- package/lib/entity-writes.mjs +35 -0
- package/lib/hfs.mjs +3 -3
- package/lib/path.mjs +0 -6
- package/lib/persistence.mjs +11 -18
- package/lib/ports.mjs +5 -2
- package/lib/specifier.mjs +27 -0
- package/lib/transactions.mjs +37 -0
- package/lib/transport-slots.mjs +8 -3
- package/lib/unit-spec.mjs +63 -2
- package/module-layering.mjs +3 -24
- package/module-shape.mjs +2 -2
- package/naming.mjs +2 -2
- package/package.json +5 -3
- package/project-graph.mjs +8 -5
- package/projections.mjs +117 -0
- package/query-safety.mjs +1 -1
- package/realtime.mjs +181 -0
- package/runtime/engine/config.mjs +494 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/runtime-root.mjs +5 -0
- package/runtime/engine/yaml.mjs +3 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +163 -32
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +915 -123
- package/runtime/knowledge/patterns/fe/folder.yaml +133 -27
- package/runtime/knowledge/sonar-gate.yaml +53 -19
- package/runtime/modules/models/registry.yaml +591 -0
- package/runtime/modules/models/runtimes.yaml +701 -0
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +3 -10
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/lib.mjs +17 -27
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/recorded-lines.mjs +19 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +17 -24
- package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/config.mjs +11 -8
- package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +21 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +5 -11
- package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +33 -162
- package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +6 -3
- package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +17 -43
- package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +27 -42
- package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/index.mjs +17 -4
- package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
- package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +40 -168
- package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +35 -15
- package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +23 -81
- package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +21 -13
- package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +27 -147
- package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +10 -4
- package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +11 -21
- package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +47 -5
- package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +13 -27
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +247 -0
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +7 -7
- package/runtime/scripts/{lib → hfs}/project-graph.mjs +0 -0
- package/runtime/scripts/{lib → hfs}/project-rule.mjs +3 -3
- package/runtime/scripts/{lib → hfs}/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +6 -5
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +1 -1
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +4 -4
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +1 -1
- package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +114 -150
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -5
- package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +6 -5
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +26 -7
- package/runtime/scripts/lib/line-count.mjs +9 -0
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/path-key.mjs +59 -2
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sops-envelope.mjs +58 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +5 -15
- package/schema-authority.mjs +14 -12
- package/service-deps.mjs +7 -13
- package/service-isolation.mjs +72 -0
- package/size-budget.mjs +3 -2
- package/test-world.mjs +5 -4
- package/testing.mjs +27 -24
- package/transport.mjs +12 -9
- package/unit-spec.mjs +1 -1
- package/user-copy.mjs +5 -4
- package/webhooks.mjs +173 -0
- package/runtime/scripts/api/sops/decrypt.mjs +0 -16
- package/runtime/scripts/lib/recorded-lines.mjs +0 -27
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
- /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
|
@@ -2,21 +2,14 @@
|
|
|
2
2
|
"scripts": {
|
|
3
3
|
"prepare": "husky",
|
|
4
4
|
{{appScripts}}
|
|
5
|
-
|
|
6
|
-
"build:fe": "{{buildFe}}",
|
|
7
|
-
"codegen": "node scripts/codegen.mjs",
|
|
8
|
-
"typecheck": "{{typecheck}}",
|
|
5
|
+
{{> app/package-scripts/core.json}}
|
|
9
6
|
"typecheck:tests": "tsc -p be/src/tests/tsconfig.json",
|
|
10
|
-
"lint": "npm run codegen --silent && hfs lint",
|
|
11
|
-
"lint:fix": "npm run codegen --silent && hfs lint --fix",
|
|
12
|
-
"format": "prettier --write .",
|
|
13
|
-
"format:check": "prettier --check .",
|
|
14
7
|
"test": "cd be && jest --selectProjects unit --coverage",
|
|
15
8
|
"test:affected": "cd be && jest --selectProjects unit --passWithNoTests",
|
|
16
9
|
"test:integration": "npm run typecheck:tests && cd be && jest --selectProjects integration",
|
|
17
10
|
"test:e2e": "npm run typecheck:tests && cd be && jest --selectProjects e2e",
|
|
18
11
|
"test:contract": "npm run typecheck:tests && cd be && jest --selectProjects contract",
|
|
19
|
-
"test:stack": "
|
|
20
|
-
"contract:emit": "
|
|
12
|
+
"test:stack": "starci app stack",
|
|
13
|
+
"contract:emit": "starci app emit"
|
|
21
14
|
}
|
|
22
15
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// config-get.mjs — `git config [--local] --get <key>`: one configuration value of a checkout (empty when unset).
|
|
2
|
+
import { gitRunner } from './lib.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {ok, stdout, stderr}; `local` reads the repository's own config only; `env` replaces the process env (a caller drops
|
|
6
|
+
* git's repository-local variables). git: the caller's runner or null.
|
|
7
|
+
*/
|
|
8
|
+
export function configGet(cwd, key, { local = false, env = undefined, git = null } = {}) {
|
|
9
|
+
return gitRunner(git)(['config', ...(local ? ['--local'] : []), '--get', key], env ? { cwd, env } : { cwd });
|
|
10
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
// scripts/api/git/lib.mjs — the one place the runtime spawns git (scripts/
|
|
1
|
+
// scripts/api/git/lib.mjs — the one place the runtime spawns git (RT_EXTERNAL_OWNER of scripts/hfs/runtime-rules/external-owner.mjs enforces it).
|
|
2
2
|
//
|
|
3
3
|
// Every caller spelt the same options by hand - encoding:'utf8', windowsHide:true, sometimes a timeout - with two shapes:
|
|
4
|
-
// `git args` in a cwd and `git -C dir args`.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// `git args` in a cwd and `git -C dir args`. gitSpawn keeps the spawnSync(file, args, options) signature so an injected
|
|
5
|
+
// runner or a spec's fake takes the same three arguments; runGit is the `-C` / `-c` convenience; gitRunner folds any
|
|
6
|
+
// caller's runner into {ok, stdout, stderr}. Only the call files beside this one import it: each names one git verb
|
|
7
|
+
// (diff.mjs, ls-files.mjs, worktree-*.mjs, ...), and a caller folds the spawn result it gets back with the pure
|
|
8
|
+
// helpers of scripts/lib/git.mjs (gitResultOf, gitOutputOf).
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { withoutGitLocalEnv } from '../../lib/git.mjs';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Spawn `file` (the git binary) once with `args`: utf8 text, a hidden window, never a shell.
|
|
@@ -20,27 +21,16 @@ export const gitSpawn = (file, args, options = {}) => {
|
|
|
20
21
|
return spawnSync(file, args, spawn);
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** runGit's result as {ok, stdout, error}: ok only on a clean exit, error from stderr or the spawn. */
|
|
36
|
-
export function gitResult(args, options = {}) {
|
|
37
|
-
const r = runGit(args, options);
|
|
38
|
-
return {
|
|
39
|
-
ok: !r.error && r.status === 0,
|
|
40
|
-
stdout: r.stdout ?? '',
|
|
41
|
-
error: (r.stderr ?? '').trim() || String(r.error?.message ?? (r.status == null ? 'timed out' : `exit ${r.status}`)),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
24
|
+
/**
|
|
25
|
+
* `git args` in `cwd`, or `git -C dir args` when `dir` is given. `git` overrides the binary; `config` ({key: value})
|
|
26
|
+
* prefixes one `-c key=value` per entry (core.quotepath=off, a committer identity). Scoped calls discard ambient
|
|
27
|
+
* hook repository variables; an explicit env remains authoritative, including an intentional temporary index.
|
|
28
|
+
*/
|
|
29
|
+
export const runGit = (args, { cwd = null, dir = null, git = 'git', config = null, ...options } = {}) =>
|
|
30
|
+
gitSpawn(git, [...(dir ? ['-C', dir] : []), ...Object.entries(config ?? {}).flatMap(([key, value]) => ['-c', `${key}=${value}`]), ...args], {
|
|
31
|
+
...(cwd ? { cwd } : {}), ...options,
|
|
32
|
+
...((cwd || dir) && options.env === undefined ? { env: withoutGitLocalEnv(process.env) } : {}),
|
|
33
|
+
});
|
|
44
34
|
|
|
45
35
|
/**
|
|
46
36
|
* A caller's git runner folded to (args, opts) -> {ok, stdout, stderr}. `git`: the caller's runner (args, {cwd}) ->
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// ls-files.mjs — `git ls-files <args>`: the paths the index tracks (--cached), or the untracked / deleted / ignored ones
|
|
2
|
+
// the flags ask for (-z keeps any file name intact).
|
|
3
|
+
import { runGit } from './lib.mjs';
|
|
4
|
+
|
|
5
|
+
/** The spawn result {status, stdout, stderr, error} of `git ls-files ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
|
|
6
|
+
export const lsFiles = (args = [], options = {}) => runGit(['ls-files', ...args], options);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// recorded-lines.mjs - the line count a file had at a git revision (git show <ref>:<file>): the recorded size a
|
|
2
|
+
// size-growth rule compares the working file with.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { runGit } from './lib.mjs';
|
|
5
|
+
import { lineCount } from '../../lib/line-count.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The line count of a file at a git revision, or null when the file did not exist there or git is absent.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} filename - Absolute path of the file.
|
|
11
|
+
* @param {string} ref - The revision, `HEAD` by default (the parent of the commit being made).
|
|
12
|
+
* @returns {number | null} The recorded line count.
|
|
13
|
+
*/
|
|
14
|
+
export const recordedLines = (filename, ref = 'HEAD') => {
|
|
15
|
+
const r = runGit(['show', `${ref}:./${path.basename(filename)}`], { dir: path.dirname(filename), maxBuffer: 64 * 1024 * 1024 });
|
|
16
|
+
// no repository, no such revision, or a new file: all mean there is no recorded size to compare with
|
|
17
|
+
if (r.error || r.status !== 0) return null;
|
|
18
|
+
return lineCount(r.stdout);
|
|
19
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// remote.mjs — `git remote <args>`: the configured remotes (get-url <name>).
|
|
2
|
+
import { runGit } from './lib.mjs';
|
|
3
|
+
|
|
4
|
+
/** The spawn result {status, stdout, stderr, error} of `git remote ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
|
|
5
|
+
export const remote = (args = [], options = {}) => runGit(['remote', ...args], options);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// rev-parse-query.mjs — `git rev-parse <args>`: one rev-parse query (--show-toplevel, --git-common-dir, --git-path, a ref): the text it prints.
|
|
2
|
+
import { runGit } from './lib.mjs';
|
|
3
|
+
|
|
4
|
+
/** The spawn result {status, stdout, stderr, error} of `git rev-parse ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
|
|
5
|
+
export const revParseQuery = (args = [], options = {}) => runGit(['rev-parse', ...args], options);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { braceVariants, globExpression } from '
|
|
2
|
+
import { braceVariants, globExpression } from '../lib/glob.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* What a slot's `requires`, `allows` and `forbids` entries (knowledge/hfs/slots.yaml) say about a file, read through the
|
|
6
|
-
* resolver of scripts/
|
|
7
|
-
* (feature-shape, fe-slot-allows) and the front-end lint rules (through `hfs.allows(file)` of scripts/
|
|
6
|
+
* resolver of scripts/hfs/slots.mjs. The machine checks that judge "this folder holds only what its slot allows"
|
|
7
|
+
* (feature-shape, fe-slot-allows) and the front-end lint rules (through `hfs.allows(file)` of scripts/hfs/view.mjs) read it here so no check keeps a path list.
|
|
8
8
|
*
|
|
9
9
|
* An entry is a name relative to the slot instance root: `<var>` is the instance's bound variable when the slot binds it
|
|
10
10
|
* (`<feature>`, `<capability>`) and one path segment of any text otherwise (`<action>`), `*` and `?` stay inside a segment,
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// ast-walks.mjs - AST walks the architecture checkers share (moved out of typescript.mjs, which keeps the program loading and module resolution).
|
|
2
|
+
import { canonical, isInside } from './config.mjs';
|
|
3
|
+
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
4
|
+
import { UNPROVEN_FRAMEWORK, isUnshadowedCommonJsRequire, relativePath, unwrapExpression } from './typescript.mjs';
|
|
5
|
+
|
|
6
|
+
/** `expression` unwrapped by the caller's own predicate set (a predicate absent on an older ts reads as never). */
|
|
7
|
+
export function unwrapEach(ts, expression, predicates) {
|
|
8
|
+
while (expression && predicates.some((predicate) => predicate?.(expression))) expression = expression.expression;
|
|
9
|
+
return expression;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `value` with every TS alias hop resolved (a re-exported import reads as its target symbol). */
|
|
13
|
+
export function normalizedSymbolValue(ts, checker, value) {
|
|
14
|
+
let symbol = value ?? null;
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
while (symbol && (symbol.flags & ts.SymbolFlags.Alias) && !seen.has(symbol)) {
|
|
17
|
+
seen.add(symbol);
|
|
18
|
+
const target = checker.getAliasedSymbol(symbol);
|
|
19
|
+
if (!target || target === symbol) break;
|
|
20
|
+
symbol = target;
|
|
21
|
+
}
|
|
22
|
+
return symbol;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The symbol `node` names, aliases resolved. */
|
|
26
|
+
export function normalizedSymbol(ts, checker, node) {
|
|
27
|
+
return normalizedSymbolValue(ts, checker, checker?.getSymbolAtLocation(node) ?? null);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The identifier or member-name node an expression selects through unwrapExpression, or null. */
|
|
31
|
+
export function selectedNode(ts, expression) {
|
|
32
|
+
expression = unwrapExpression(ts, expression);
|
|
33
|
+
if (ts.isIdentifier(expression)) return expression;
|
|
34
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name;
|
|
35
|
+
if (ts.isElementAccessExpression(expression) && ts.isStringLiteralLike(expression.argumentExpression)) return expression.argumentExpression;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The symbol `node` ultimately evaluates to: its own symbol, followed through a single `const` variable
|
|
41
|
+
* initializer (and so on). `unselected: 'null'` answers null when `node` selects no name at all.
|
|
42
|
+
*/
|
|
43
|
+
export function valueSymbol(ts, checker, node, seen = new Set(), { unselected = 'node' } = {}) {
|
|
44
|
+
const selected = selectedNode(ts, node);
|
|
45
|
+
if (!selected && unselected === 'null') return null;
|
|
46
|
+
const symbol = normalizedSymbol(ts, checker, selected ?? node);
|
|
47
|
+
if (!symbol || seen.has(symbol)) return symbol;
|
|
48
|
+
seen.add(symbol);
|
|
49
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
50
|
+
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
51
|
+
&& (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
|
|
52
|
+
return valueSymbol(ts, checker, declarations[0].initializer, seen, { unselected });
|
|
53
|
+
}
|
|
54
|
+
return symbol;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The expressions `declaration` returns: its body when it is an expression-bodied arrow/function, else every
|
|
59
|
+
* `return`'s argument without descending into nested functions. `functionLike` overrides which node kinds bound
|
|
60
|
+
* that descent (default the four callable declaration kinds; pass `ts.isFunctionLike` for the wider set).
|
|
61
|
+
*/
|
|
62
|
+
export function returnedExpressions(ts, declaration, { functionLike = null } = {}) {
|
|
63
|
+
const boundary = functionLike ?? ((node) => ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)
|
|
64
|
+
|| ts.isArrowFunction(node) || ts.isFunctionExpression(node));
|
|
65
|
+
if ((ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration)) && !ts.isBlock(declaration.body)) return [declaration.body];
|
|
66
|
+
const body = boundary(declaration) ? declaration.body : null;
|
|
67
|
+
if (!body || !ts.isBlock(body)) return [];
|
|
68
|
+
const returned = [];
|
|
69
|
+
const visit = (node) => {
|
|
70
|
+
if (node !== body && boundary(node)) return;
|
|
71
|
+
if (ts.isReturnStatement(node) && node.expression) returned.push(node.expression);
|
|
72
|
+
else ts.forEachChild(node, visit);
|
|
73
|
+
};
|
|
74
|
+
visit(body);
|
|
75
|
+
return returned;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A node's decorators across TS versions (ts.getDecorators, else the legacy `decorators` property). */
|
|
79
|
+
export function nodeDecorators(ts, node) {
|
|
80
|
+
return ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : node.decorators ?? [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One finding row: rule id, root-relative path, node position and message, plus rule extras. */
|
|
84
|
+
export function violation(config, sourceFile, node, ruleId, message, extra = {}) {
|
|
85
|
+
return { ruleId, path: relativePath(config.root, sourceFile.fileName), ...sourceLocation(sourceFile, node), message, ...extra };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Whether `match` holds for `node` or a descendant (the walk stops at the first match). */
|
|
89
|
+
export function anyDescendant(ts, node, match) {
|
|
90
|
+
let found = false;
|
|
91
|
+
const visit = (current) => {
|
|
92
|
+
if (found) return;
|
|
93
|
+
if (match(current)) { found = true; return; }
|
|
94
|
+
ts.forEachChild(current, visit);
|
|
95
|
+
};
|
|
96
|
+
visit(node);
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The framework kinds `expression` can produce: `directOf(ts, checker, {symbol, selected, targets})` maps a
|
|
102
|
+
* selected node's symbols to a kind (default `targets.get(symbol)`); `shorthandOf` (when set) also resolves
|
|
103
|
+
* shorthand-property declarations and descends property assignments; `newExpression` descends `new X()` callees.
|
|
104
|
+
*/
|
|
105
|
+
export function tracedFrameworkKinds(ts, checker, expression, targets,
|
|
106
|
+
{ seen = new Set(), depth = 0, directOf = null, shorthandOf = null, newExpression = false } = {}) {
|
|
107
|
+
if (!expression) return new Set();
|
|
108
|
+
if (depth > 10) return new Set([UNPROVEN_FRAMEWORK]);
|
|
109
|
+
expression = unwrapExpression(ts, expression);
|
|
110
|
+
const selected = selectedNode(ts, expression);
|
|
111
|
+
const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
|
|
112
|
+
const direct = symbol ? (directOf ?? ((ts2, c, { symbol: s, targets: t }) => t.get(s) ?? null))(ts, checker, { symbol, selected, targets }) : null;
|
|
113
|
+
if (direct) return new Set([direct]);
|
|
114
|
+
const opts = { depth: depth + 1, directOf, shorthandOf, newExpression };
|
|
115
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
|
|
116
|
+
const kinds = new Set();
|
|
117
|
+
for (const returned of returnedExpressions(ts, expression)) {
|
|
118
|
+
for (const kind of tracedFrameworkKinds(ts, checker, returned, targets, { ...opts, seen })) kinds.add(kind);
|
|
119
|
+
}
|
|
120
|
+
return kinds;
|
|
121
|
+
}
|
|
122
|
+
if (ts.isCallExpression(expression)) {
|
|
123
|
+
const kinds = tracedFrameworkKinds(ts, checker, expression.expression, targets, { ...opts, seen });
|
|
124
|
+
if (kinds.size) return kinds;
|
|
125
|
+
}
|
|
126
|
+
if (newExpression && ts.isNewExpression(expression)) {
|
|
127
|
+
return tracedFrameworkKinds(ts, checker, expression.expression, targets, { ...opts, seen });
|
|
128
|
+
}
|
|
129
|
+
if (!symbol || seen.has(symbol)) return new Set();
|
|
130
|
+
const nextSeen = new Set(seen).add(symbol);
|
|
131
|
+
const kinds = new Set();
|
|
132
|
+
for (const declaration of symbol.getDeclarations?.() ?? []) {
|
|
133
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
134
|
+
for (const kind of tracedFrameworkKinds(ts, checker, declaration.initializer, targets, { ...opts, seen: nextSeen })) kinds.add(kind);
|
|
135
|
+
}
|
|
136
|
+
if (shorthandOf && ts.isShorthandPropertyAssignment(declaration)) {
|
|
137
|
+
const target = normalizedSymbolValue(ts, checker, checker.getShorthandAssignmentValueSymbol?.(declaration));
|
|
138
|
+
const kind = shorthandOf(ts, checker, target, targets);
|
|
139
|
+
if (kind) kinds.add(kind);
|
|
140
|
+
}
|
|
141
|
+
if (shorthandOf && ts.isPropertyAssignment(declaration)) {
|
|
142
|
+
for (const kind of tracedFrameworkKinds(ts, checker, declaration.initializer, targets, { ...opts, seen: nextSeen })) kinds.add(kind);
|
|
143
|
+
}
|
|
144
|
+
for (const returned of returnedExpressions(ts, declaration)) {
|
|
145
|
+
for (const kind of tracedFrameworkKinds(ts, checker, returned, targets, { ...opts, seen: nextSeen })) kinds.add(kind);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return kinds;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 'unproven framework' | 'multiple framework' | the one kind | null, for a decorator whose expression constructs. */
|
|
152
|
+
export function constructedDecoratorKind(ts, checker, decorator, targets, traceOpts = {}) {
|
|
153
|
+
const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
154
|
+
const kinds = tracedFrameworkKinds(ts, checker, callee, targets, traceOpts);
|
|
155
|
+
if (kinds.has(UNPROVEN_FRAMEWORK)) return 'unproven framework';
|
|
156
|
+
return kinds.size === 1 ? [...kinds][0] : kinds.size ? 'multiple framework' : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The target a decorator resolves to through a `let`/`var` alias's single initializer, or null. */
|
|
160
|
+
export function mutableDecoratorKind(ts, checker, decorator, targets) {
|
|
161
|
+
const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
162
|
+
const selected = selectedNode(ts, callee);
|
|
163
|
+
const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
|
|
164
|
+
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
165
|
+
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
|
|
166
|
+
|| (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) return null;
|
|
167
|
+
return targets.get(valueSymbol(ts, checker, declarations[0].initializer)) ?? null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The callee expression of a decorator (the called expression of `@x(...)`, else the expression itself). */
|
|
171
|
+
export function decoratorCallee(ts, decorator) {
|
|
172
|
+
return ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The module `statement` (import, export-from or import-equals) statically binds: its specifier text, the
|
|
177
|
+
* module's symbol and its exported symbols by name (aliases resolved); `symbol` is null when the module does
|
|
178
|
+
* not resolve. Null when the statement names no static specifier.
|
|
179
|
+
*/
|
|
180
|
+
export function moduleExportsOf(ts, checker, statement) {
|
|
181
|
+
const moduleSpecifier = (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement))
|
|
182
|
+
&& statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier) ? statement.moduleSpecifier : null;
|
|
183
|
+
const importEquals = ts.isImportEqualsDeclaration(statement) && ts.isExternalModuleReference(statement.moduleReference)
|
|
184
|
+
&& statement.moduleReference.expression && ts.isStringLiteralLike(statement.moduleReference.expression)
|
|
185
|
+
? statement.moduleReference.expression : null;
|
|
186
|
+
const specifierNode = moduleSpecifier ?? importEquals;
|
|
187
|
+
const specifier = specifierNode?.text;
|
|
188
|
+
if (specifier == null) return null;
|
|
189
|
+
const symbol = checker.getSymbolAtLocation(specifierNode) ?? null;
|
|
190
|
+
const exports = new Map((symbol ? checker.getExportsOfModule(symbol) : []).map(item => [item.getName(), normalizedSymbolValue(ts, checker, item)]));
|
|
191
|
+
return { specifierNode, specifier, symbol, exports };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The program source files a framework-target scan of `checker` judges: every file in `localFiles`, plus (when
|
|
196
|
+
* `root` is given) declaration files inside the root but outside node_modules. Null when no program of the
|
|
197
|
+
* context owns the checker.
|
|
198
|
+
*/
|
|
199
|
+
export function programSourcesOf(context, checker, localFiles, { root = null } = {}) {
|
|
200
|
+
const program = context.programs.find(candidate => candidate.getTypeChecker() === checker) ?? null;
|
|
201
|
+
if (!program) return null;
|
|
202
|
+
return program.getSourceFiles().filter(sourceFile => localFiles.has(canonical(sourceFile.fileName))
|
|
203
|
+
|| (root && sourceFile.isDeclarationFile && isInside(root, sourceFile.fileName)
|
|
204
|
+
&& !sourceFile.fileName.replaceAll('\\', '/').includes('/node_modules/')));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The CommonJS require() caveat every framework scan reports: each `require('<specifier>')` `accepted` pushes
|
|
209
|
+
* `${filePath} uses a CommonJS <label ?? specifier> <detail>` (the import-shape reasoning cannot prove that binding).
|
|
210
|
+
*/
|
|
211
|
+
export function commonJsRequireReasons(ts, checker, sourceFile, accepted, reasons, filePath, detail, label = null) {
|
|
212
|
+
const visit = node => {
|
|
213
|
+
if (ts.isCallExpression(node) && isUnshadowedCommonJsRequire(ts, checker, node.expression)
|
|
214
|
+
&& node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0]) && accepted(node.arguments[0].text)) {
|
|
215
|
+
reasons.push(`${filePath} uses a CommonJS ${label ?? node.arguments[0].text} ${detail}`);
|
|
216
|
+
}
|
|
217
|
+
ts.forEachChild(node, visit);
|
|
218
|
+
};
|
|
219
|
+
visit(sourceFile);
|
|
220
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { isInside } from './config.mjs';
|
|
3
|
-
import { slotAdmitsFile } from '
|
|
4
|
-
import { reachableViolation, relativePath
|
|
3
|
+
import { slotAdmitsFile } from '../allows.mjs';
|
|
4
|
+
import { reachableViolation, relativePath } from './typescript.mjs';
|
|
5
|
+
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
5
6
|
|
|
6
7
|
const FORBIDDEN_APP_ROLE = /(?:^|\.)(?:service|provider|providers|resolver|controller|handler|repository|entity|use-case|command|query|listener|consumer|processor)\.[cm]?[jt]sx?$/i;
|
|
7
8
|
const FORBIDDEN_DECLARATION = /(?:Service|Provider|Resolver|Controller|Handler|Repository|Entity|UseCase|Command|Query|Listener|Consumer|Processor)$/;
|
|
@@ -73,6 +74,18 @@ function roleEvidence(ts, sourceFile) {
|
|
|
73
74
|
|
|
74
75
|
function transportFrameworkEvidence(ts, sourceFile, checker) {
|
|
75
76
|
const found = [];
|
|
77
|
+
// The destructured binding elements naming a transport export (or a computed binding, which cannot be
|
|
78
|
+
// proved to exclude one): `const { Controller, [x]: y } = ...`. `rest` elements are always opaque.
|
|
79
|
+
const destructuredBindings = (elements, detail) => elements.flatMap(element => {
|
|
80
|
+
const selected = element.dotDotDotToken
|
|
81
|
+
? null
|
|
82
|
+
: ts.isIdentifier(element.propertyName ?? element.name)
|
|
83
|
+
? (element.propertyName ?? element.name).text
|
|
84
|
+
: ts.isStringLiteralLike(element.propertyName)
|
|
85
|
+
? element.propertyName.text
|
|
86
|
+
: null;
|
|
87
|
+
return selected === null || NEST_COMMON_TRANSPORT.has(selected) ? [{ node: element, detail: selected ?? detail }] : [];
|
|
88
|
+
});
|
|
76
89
|
const namespaceUsages = binding => {
|
|
77
90
|
const usages = [];
|
|
78
91
|
const alias = binding.text;
|
|
@@ -90,18 +103,7 @@ function transportFrameworkEvidence(ts, sourceFile, checker) {
|
|
|
90
103
|
if (!ts.isObjectBindingPattern(node.name)) {
|
|
91
104
|
usages.push({ node: node.name, detail: 'escaped @nestjs/common namespace access' });
|
|
92
105
|
} else {
|
|
93
|
-
|
|
94
|
-
const selected = element.dotDotDotToken
|
|
95
|
-
? null
|
|
96
|
-
: ts.isIdentifier(element.propertyName ?? element.name)
|
|
97
|
-
? (element.propertyName ?? element.name).text
|
|
98
|
-
: ts.isStringLiteralLike(element.propertyName)
|
|
99
|
-
? element.propertyName.text
|
|
100
|
-
: null;
|
|
101
|
-
if (selected === null || NEST_COMMON_TRANSPORT.has(selected)) {
|
|
102
|
-
usages.push({ node: element, detail: selected ?? 'computed @nestjs/common namespace destructuring' });
|
|
103
|
-
}
|
|
104
|
-
}
|
|
106
|
+
usages.push(...destructuredBindings(node.name.elements, 'computed @nestjs/common namespace destructuring'));
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
ts.forEachChild(node, visit);
|
|
@@ -168,16 +170,7 @@ function transportFrameworkEvidence(ts, sourceFile, checker) {
|
|
|
168
170
|
if (ts.isIdentifier(node.name)) {
|
|
169
171
|
found.push(...namespaceUsages(node.name).map(item => ({ ...item, specifier })));
|
|
170
172
|
} else if (ts.isObjectBindingPattern(node.name)) {
|
|
171
|
-
|
|
172
|
-
const selected = element.dotDotDotToken
|
|
173
|
-
? null
|
|
174
|
-
: ts.isIdentifier(element.propertyName ?? element.name)
|
|
175
|
-
? (element.propertyName ?? element.name).text
|
|
176
|
-
: ts.isStringLiteralLike(element.propertyName)
|
|
177
|
-
? element.propertyName.text
|
|
178
|
-
: null;
|
|
179
|
-
if (selected === null || NEST_COMMON_TRANSPORT.has(selected)) found.push({ node: element, specifier, detail: selected ?? 'computed @nestjs/common require destructuring' });
|
|
180
|
-
}
|
|
173
|
+
found.push(...destructuredBindings(node.name.elements, 'computed @nestjs/common require destructuring').map(item => ({ ...item, specifier })));
|
|
181
174
|
}
|
|
182
175
|
}
|
|
183
176
|
}
|
|
@@ -3,29 +3,26 @@ import { machineKit } from './machine-ast.mjs';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* R46 `background-unowned` (BE_BACKGROUND_UNOWNED, BE-CONVENTION 1.6 and 1.13). Background work is a transport that only
|
|
6
|
-
* a worker app runs:
|
|
6
|
+
* a worker or api app runs (an api app composes the message transport of its own service; a separate worker is opt-in):
|
|
7
7
|
*
|
|
8
|
-
* - every job (`*.
|
|
9
|
-
* through runtime imports, from the root module of some
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* - every job processor (`*.processor.ts`, the jobs kind) and consumer (`*.consumer.ts`) is composed by an app of kind `worker` or
|
|
9
|
+
* `api`: the file is reachable, through runtime imports, from the root module of some such app declared in hfs.json; one no app
|
|
10
|
+
* reaches never runs. A schedule is a BullMQ job scheduler of a queue, so no scheduler decorator or timer is judged here: `@nestjs/schedule`
|
|
11
|
+
* and the timers are owned by platform capabilities (R90 BE_INFRA_OWNER);
|
|
12
12
|
* - a method whose name is one of the words the law names (`sweep`, `deliver`, `reconcile`, `retry`, alone or as the
|
|
13
|
-
* first word of a camelCase name) in a domain, integrations or feature class is reachable from a composed
|
|
14
|
-
* consumer: from the
|
|
15
|
-
* (its handlers are registered with the command bus, not imported by the
|
|
13
|
+
* first word of a camelCase name) in a domain, integrations or feature class is reachable from a composed processor or
|
|
14
|
+
* consumer: from the processor or consumer files and everything they import, and from the files of the feature that holds them
|
|
15
|
+
* (its handlers are registered with the command bus, not imported by the processor). Mechanisms of platform are exempt.
|
|
16
16
|
*
|
|
17
|
-
* Reachability is static (imports, re-exports and the feature of the
|
|
17
|
+
* Reachability is static (imports, re-exports and the feature of the processor); it proves a processor or consumer could call the method,
|
|
18
18
|
* not that it does.
|
|
19
19
|
*/
|
|
20
20
|
export const BACKGROUND_UNOWNED_RULE_IDS = ['BE_BACKGROUND_UNOWNED'];
|
|
21
21
|
|
|
22
22
|
const RULE = 'BE_BACKGROUND_UNOWNED';
|
|
23
|
-
const BACKGROUND_ROLES = ['
|
|
23
|
+
const BACKGROUND_ROLES = ['processor', 'consumer'];
|
|
24
24
|
/** The words the law names for background methods (knowledge/hfs/README.md 5.7). */
|
|
25
25
|
const BACKGROUND_WORDS = ['sweep', 'deliver', 'reconcile', 'retry'];
|
|
26
|
-
const SCHEDULER_DECORATORS = ['Cron', 'Interval', 'Timeout'];
|
|
27
|
-
const SCHEDULER_PACKAGE = '@nestjs/schedule';
|
|
28
|
-
const SCHEDULING_CAPABILITY = 'scheduling';
|
|
29
26
|
|
|
30
27
|
const roleOf = rel => {
|
|
31
28
|
const parts = path.posix.basename(rel).split('.');
|
|
@@ -52,17 +49,18 @@ export function checkBackgroundUnowned(input) {
|
|
|
52
49
|
return seen;
|
|
53
50
|
};
|
|
54
51
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
52
|
+
const rootOf = app => `apps/${app.name}/src/app.module.ts`;
|
|
53
|
+
const workerRoots = resolver.repo.apps.filter(app => app.kind === 'worker').map(rootOf).filter(rel => graph.files.has(rel));
|
|
54
|
+
const composed = closure([...workerRoots, ...resolver.repo.apps.filter(app => app.kind === 'api').map(rootOf).filter(rel => graph.files.has(rel))]);
|
|
57
55
|
const background = [...graph.files.values()].filter(file => roleOf(file.rel) && file.tier === 'feature');
|
|
58
56
|
const running = background.filter(file => composed.has(file.rel));
|
|
59
57
|
for (const file of background) {
|
|
60
58
|
if (composed.has(file.rel)) continue;
|
|
61
59
|
const role = roleOf(file.rel);
|
|
62
|
-
report(file, file.sourceFile, `${file.rel} is a ${role} that no
|
|
60
|
+
report(file, file.sourceFile, `${file.rel} is a ${role} that no service app composes${workerRoots.length ? '' : ' (hfs.json declares no worker app)'}; a ${role} runs only when the ${role === 'processor' ? 'job' : 'message'} module of its feature is imported by the root module of an app of kind worker or api.`, { role });
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
// Everything a composed
|
|
63
|
+
// Everything a composed processor or consumer can reach: its imports, and the files of its own feature (command handlers).
|
|
66
64
|
const roots = new Set();
|
|
67
65
|
for (const file of running) {
|
|
68
66
|
roots.add(file.rel);
|
|
@@ -73,31 +71,15 @@ export function checkBackgroundUnowned(input) {
|
|
|
73
71
|
|
|
74
72
|
let methods = 0;
|
|
75
73
|
for (const file of graph.files.values()) {
|
|
76
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
77
|
-
const inScheduling = file.owner?.slot === 'be.platform' && path.posix.basename(file.owner.root) === SCHEDULING_CAPABILITY;
|
|
78
74
|
const mechanism = file.tier === 'platform';
|
|
79
75
|
kit.walk(file.sourceFile, node => {
|
|
80
|
-
if (!inScheduling && ts.isDecorator(node)) {
|
|
81
|
-
const call = ts.isCallExpression(node.expression) ? node.expression.expression : node.expression;
|
|
82
|
-
const binding = kit.importBinding(checker, call);
|
|
83
|
-
if (binding && binding.module === SCHEDULER_PACKAGE && SCHEDULER_DECORATORS.includes(binding.name)) {
|
|
84
|
-
report(file, node, `@${binding.name} schedules work inside a class outside platform/scheduling. Write a job in transport/schedule/<job>.job.ts that dispatches one command or query, and let platform/scheduling and a worker app run it.`, { scheduler: binding.name });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (!inScheduling && ts.isCallExpression(node)) {
|
|
88
|
-
const callee = node.expression;
|
|
89
|
-
const bare = ts.isIdentifier(callee) && callee.text === 'setInterval' && kit.declarationsOf(checker, callee).every(declaration => declaration.getSourceFile().isDeclarationFile);
|
|
90
|
-
const member = ts.isPropertyAccessExpression(callee) && callee.name.text === 'setInterval' && ts.isIdentifier(callee.expression) && ['globalThis', 'window', 'global'].includes(callee.expression.text);
|
|
91
|
-
const global = bare || member;
|
|
92
|
-
if (global) report(file, node, 'setInterval starts a scheduler outside platform/scheduling. Write a job in transport/schedule/<job>.job.ts and let platform/scheduling and a worker app run it.', { scheduler: 'setInterval' });
|
|
93
|
-
}
|
|
94
76
|
if (!mechanism && ts.isClassDeclaration(node)) {
|
|
95
77
|
for (const member of node.members) {
|
|
96
78
|
if (!ts.isMethodDeclaration(member) || !member.name) continue;
|
|
97
79
|
const name = kit.propertyNameText(member.name);
|
|
98
80
|
if (!name || !isBackgroundName(name)) continue;
|
|
99
81
|
methods += 1;
|
|
100
|
-
if (!reachable.has(file.rel)) report(file, member.name, `${name} is background work (a ${BACKGROUND_WORDS.join(', ')} method) that no
|
|
82
|
+
if (!reachable.has(file.rel)) report(file, member.name, `${name} is background work (a ${BACKGROUND_WORDS.join(', ')} method) that no processor or consumer composed by a worker or api app can reach. Add a processor in features/jobs/<job> or a consumer in transport/message of a feature that runs it, and compose its module in a worker or api app.`, { method: name });
|
|
101
83
|
}
|
|
102
84
|
}
|
|
103
85
|
return true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { builtinModules } from 'node:module';
|
|
2
|
-
import { sourceLocation } from '
|
|
2
|
+
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
3
|
+
import { isServerActionModule } from './server-action.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* R55 `client-reaches-server` (FE_CLIENT_REACHES_SERVER), the repository half of the eslint rule `client-no-server-import`,
|
|
@@ -75,6 +76,7 @@ export function checkClientReachesServer({ graph, context }) {
|
|
|
75
76
|
const queue = [entry.rel];
|
|
76
77
|
for (let head = 0; head < queue.length; head += 1) {
|
|
77
78
|
const rel = queue[head];
|
|
79
|
+
if (isServerActionModule(ts, graph.files.get(rel).sourceFile)) continue;
|
|
78
80
|
const state = seen.get(rel);
|
|
79
81
|
for (const hit of serverOf(rel)) {
|
|
80
82
|
const at = state.first ?? sourceLocation(graph.files.get(rel).sourceFile, hit.node);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Two kinds of file are uniform by design and are not compared, both found through the slot resolver: the entry main.ts of
|
|
18
18
|
* an app (slot appKind + its required main.ts) and the transport doors of a feature (a *.controller.ts, *.resolver.ts,
|
|
19
|
-
* *.gateway.ts, *.consumer.ts, *.
|
|
19
|
+
* *.gateway.ts, *.consumer.ts, *.processor.ts or *.cli.ts of a slot that a protocol app composes). This is not a loophole: logic
|
|
20
20
|
* cannot live in either. A door body is dispatch only (eslint transport-is-thin, R88), and main.ts cannot share a
|
|
21
21
|
* helper because NestFactory is allowed only in main.ts (BE_ENTRYPOINT_ONLY_IN_APPS), so what repeats there is the
|
|
22
22
|
* canonical shape of section 5 of BE-CONVENTION. A door or main.ts that grows logic is refused by those enforcers, not
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
export const CLONE_RULE_IDS = ['HFS_DUPLICATE_CODE'];
|
|
26
26
|
|
|
27
27
|
const MAX_VIOLATIONS = 200;
|
|
28
|
-
const DOOR_ROLES = new Set(['controller', 'resolver', 'gateway', 'consumer', '
|
|
28
|
+
const DOOR_ROLES = new Set(['controller', 'resolver', 'gateway', 'consumer', 'processor', 'cli']);
|
|
29
29
|
const APP_ENTRY = 'main.ts';
|
|
30
30
|
const ID = -1;
|
|
31
31
|
const LIT = -2;
|
|
32
32
|
|
|
33
|
-
function tokenize(ts, sourceFile) {
|
|
33
|
+
export function tokenize(ts, sourceFile) {
|
|
34
34
|
const { SyntaxKind } = ts;
|
|
35
35
|
const literalKinds = new Set([SyntaxKind.StringLiteral, SyntaxKind.NumericLiteral, SyntaxKind.BigIntLiteral, SyntaxKind.RegularExpressionLiteral,
|
|
36
36
|
SyntaxKind.NoSubstitutionTemplateLiteral, SyntaxKind.TemplateHead, SyntaxKind.TemplateMiddle, SyntaxKind.TemplateTail, SyntaxKind.JsxText]);
|
|
@@ -76,7 +76,7 @@ function uniformByDesign(resolver, rel) {
|
|
|
76
76
|
const base = rel.slice(rel.lastIndexOf('/') + 1);
|
|
77
77
|
if (slot.appKind !== undefined) return base === APP_ENTRY && (slot.requires ?? []).includes(APP_ENTRY);
|
|
78
78
|
const role = base.split('.').at(-2);
|
|
79
|
-
return (slot.composedBy?.length ?? 0) > 0 && DOOR_ROLES.has(role) && base.endsWith('.ts');
|
|
79
|
+
return !slot.owner && (slot.composedBy?.length ?? 0) > 0 && DOOR_ROLES.has(role) && base.endsWith('.ts');
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function homeText(profile, sameOwner, crossApp) {
|