@starci/hfs 4.0.10 → 4.2.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 +5 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +65 -24
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +194 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +222 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/release-config.mjs +28 -0
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +161 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +126 -265
- package/runtime/modules/models/tiers.yaml +66 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +164 -104
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/catalog-once.mjs +15 -0
- package/runtime/scripts/hfs/check.mjs +63 -71
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +21 -4
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +59 -48
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +74 -695
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/runtime/scripts/lib/yaml-cached.mjs +14 -0
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/runtime/scripts/lib/sqlite.mjs +0 -21
|
@@ -2,9 +2,10 @@ import path from 'node:path';
|
|
|
2
2
|
import { canonical, isInside } from './config.mjs';
|
|
3
3
|
import { parseContract, installedSWR } from './next-data-contract.mjs';
|
|
4
4
|
import { relativePath, unwrapExpression } from './typescript.mjs';
|
|
5
|
-
import { anyDescendant, normalizedSymbol, normalizedSymbolValue,
|
|
6
|
-
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
7
|
-
|
|
5
|
+
import { anyDescendant, normalizedSymbol, normalizedSymbolValue, valueSymbol as sharedValueSymbol } from './ast-walks.mjs';
|
|
6
|
+
import { isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
|
|
7
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
8
|
+
import { createNextDataKeyInspector } from './next-data-key.mjs';
|
|
8
9
|
export const SWR_KEY_RULE_ID = 'FE_SWR_KEY_IDENTITY';
|
|
9
10
|
export const SWR_MUTATION_RULE_ID = 'FE_SWR_MUTATION_RESOURCE_IDENTITY';
|
|
10
11
|
export const SWR_DATA_RULE_IDS = [SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID];
|
|
@@ -20,6 +21,47 @@ function declarationsInside(symbol, root) {
|
|
|
20
21
|
return declarations.length > 0 && declarations.every(declaration => isInside(root, canonical(declaration.getSourceFile().fileName)));
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function selectedImportBindings(ts, statement, selected) {
|
|
25
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause) return 0;
|
|
26
|
+
const clause = statement.importClause;
|
|
27
|
+
let count = clause.name && selected.has('default') ? 1 : 0;
|
|
28
|
+
const bindings = clause.namedBindings;
|
|
29
|
+
if (bindings && ts.isNamespaceImport(bindings)) count += 1;
|
|
30
|
+
if (bindings && ts.isNamedImports(bindings)) count += bindings.elements
|
|
31
|
+
.filter(element => selected.has(element.propertyName?.text ?? element.name.text)).length;
|
|
32
|
+
return count;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function addOfficialExports(official, checker, statement, selected, targets) {
|
|
36
|
+
const { config, context, installed, reasons } = official;
|
|
37
|
+
const moduleSymbol = checker.getSymbolAtLocation(statement.moduleSpecifier);
|
|
38
|
+
if (!moduleSymbol) {
|
|
39
|
+
reasons.push(`${relativePath(config.root, statement.getSourceFile().fileName)} cannot resolve ${statement.moduleSpecifier.text} through the target TypeScript program`);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(), normalizedSymbolValue(context.ts, checker, symbol)]));
|
|
43
|
+
for (const [name, kind] of selected) {
|
|
44
|
+
const target = exports.get(name);
|
|
45
|
+
if (!target) continue;
|
|
46
|
+
if (!declarationsInside(target, installed.root)) {
|
|
47
|
+
reasons.push(`${relativePath(config.root, statement.getSourceFile().fileName)} resolves ${statement.moduleSpecifier.text}#${name} outside installed swr ${installed.version}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
targets.set(target, kind);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function addOfficialStatement(official, wanted, checker, statement, targets) {
|
|
56
|
+
const ts = official.context.ts;
|
|
57
|
+
if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) return 0;
|
|
58
|
+
if (!statement.moduleSpecifier || !ts.isStringLiteralLike(statement.moduleSpecifier)) return 0;
|
|
59
|
+
const selected = wanted.get(statement.moduleSpecifier.text);
|
|
60
|
+
if (!selected) return 0;
|
|
61
|
+
if (!addOfficialExports(official, checker, statement, selected, targets)) return 0;
|
|
62
|
+
return selectedImportBindings(ts, statement, selected);
|
|
63
|
+
}
|
|
64
|
+
|
|
23
65
|
function officialTargets(config, context, installed, reasons) {
|
|
24
66
|
const wanted = new Map([
|
|
25
67
|
['swr', new Map([['default', 'query'], ['mutate', 'global-mutate'], ['useSWRConfig', 'config']])],
|
|
@@ -27,38 +69,11 @@ function officialTargets(config, context, installed, reasons) {
|
|
|
27
69
|
['swr/mutation', new Map([['default', 'mutation'], ['useSWRMutation', 'mutation']])],
|
|
28
70
|
]);
|
|
29
71
|
const targets = new Map();
|
|
72
|
+
const official = { config, context, installed, reasons };
|
|
30
73
|
let selectedBindings = 0;
|
|
31
74
|
for (const source of context.files) {
|
|
32
75
|
const checker = context.checkerFor(source.fileName);
|
|
33
|
-
for (const statement of source.statements)
|
|
34
|
-
if (!context.ts.isImportDeclaration(statement) && !context.ts.isExportDeclaration(statement)) continue;
|
|
35
|
-
if (!statement.moduleSpecifier || !context.ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
|
|
36
|
-
const selected = wanted.get(statement.moduleSpecifier.text);
|
|
37
|
-
if (!selected) continue;
|
|
38
|
-
const moduleSymbol = checker.getSymbolAtLocation(statement.moduleSpecifier);
|
|
39
|
-
if (!moduleSymbol) {
|
|
40
|
-
reasons.push(`${relativePath(config.root, source.fileName)} cannot resolve ${statement.moduleSpecifier.text} through the target TypeScript program`);
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(), normalizedSymbolValue(context.ts, checker, symbol)]));
|
|
44
|
-
for (const [name, kind] of selected) {
|
|
45
|
-
const target = exports.get(name);
|
|
46
|
-
if (!target) continue;
|
|
47
|
-
if (!declarationsInside(target, installed.root)) {
|
|
48
|
-
reasons.push(`${relativePath(config.root, source.fileName)} resolves ${statement.moduleSpecifier.text}#${name} outside installed swr ${installed.version}`);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
targets.set(target, kind);
|
|
52
|
-
}
|
|
53
|
-
if (context.ts.isImportDeclaration(statement) && statement.importClause) {
|
|
54
|
-
const clause = statement.importClause;
|
|
55
|
-
if (clause.name && selected.has('default')) selectedBindings += 1;
|
|
56
|
-
const bindings = clause.namedBindings;
|
|
57
|
-
if (bindings && context.ts.isNamespaceImport(bindings)) selectedBindings += 1;
|
|
58
|
-
if (bindings && context.ts.isNamedImports(bindings)) selectedBindings += bindings.elements
|
|
59
|
-
.filter(element => selected.has(element.propertyName?.text ?? element.name.text)).length;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
76
|
+
for (const statement of source.statements) selectedBindings += addOfficialStatement(official, wanted, checker, statement, targets);
|
|
62
77
|
}
|
|
63
78
|
return { targets, selectedBindings };
|
|
64
79
|
}
|
|
@@ -95,21 +110,28 @@ function nearestFunction(ts, node) {
|
|
|
95
110
|
return null;
|
|
96
111
|
}
|
|
97
112
|
|
|
113
|
+
function configElementProperty(ts, element) {
|
|
114
|
+
return element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text
|
|
115
|
+
: (ts.isIdentifier(element.name) && element.name.text) || null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function addConfigMutateSymbols(ts, checker, node, targets, symbols) {
|
|
119
|
+
if (!(ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && node.initializer
|
|
120
|
+
&& ts.isCallExpression(unwrapExpression(ts, node.initializer))
|
|
121
|
+
&& callKind(ts, checker, unwrapExpression(ts, node.initializer), targets) === 'config')) return;
|
|
122
|
+
for (const element of node.name.elements) {
|
|
123
|
+
if (configElementProperty(ts, element) === 'mutate' && ts.isIdentifier(element.name)) symbols.add(normalizedSymbol(ts, checker, element.name));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
98
127
|
function configMutateSymbols(config, context, targets) {
|
|
128
|
+
const { ts } = context;
|
|
99
129
|
const symbols = new Set();
|
|
100
130
|
for (const source of context.files) {
|
|
101
131
|
const checker = context.checkerFor(source.fileName);
|
|
102
132
|
const visit = node => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
&& callKind(context.ts, checker, unwrapExpression(context.ts, node.initializer), targets) === 'config') {
|
|
106
|
-
for (const element of node.name.elements) {
|
|
107
|
-
const property = element.propertyName && context.ts.isIdentifier(element.propertyName) ? element.propertyName.text
|
|
108
|
-
: context.ts.isIdentifier(element.name) ? element.name.text : null;
|
|
109
|
-
if (property === 'mutate' && context.ts.isIdentifier(element.name)) symbols.add(normalizedSymbol(context.ts, checker, element.name));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
context.ts.forEachChild(node, visit);
|
|
133
|
+
addConfigMutateSymbols(ts, checker, node, targets, symbols);
|
|
134
|
+
ts.forEachChild(node, visit);
|
|
113
135
|
};
|
|
114
136
|
visit(source);
|
|
115
137
|
}
|
|
@@ -161,25 +183,25 @@ function exportedHook(config, context, entry, reasons) {
|
|
|
161
183
|
return { source, checker, fn: functions[0] };
|
|
162
184
|
}
|
|
163
185
|
|
|
186
|
+
function addBindingName(ts, checker, found, name) {
|
|
187
|
+
if (ts.isIdentifier(name)) {
|
|
188
|
+
const symbol = normalizedSymbol(ts, checker, name);
|
|
189
|
+
if (!symbol) return;
|
|
190
|
+
if (!found.has(name.text)) found.set(name.text, new Set());
|
|
191
|
+
found.get(name.text).add(symbol);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) for (const element of name.elements) {
|
|
195
|
+
if (element && !ts.isOmittedExpression(element)) addBindingName(ts, checker, found, element.name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
164
199
|
function bindingSymbols(ts, checker, fn) {
|
|
165
200
|
const found = new Map();
|
|
166
|
-
const
|
|
167
|
-
if (ts.isIdentifier(name)) {
|
|
168
|
-
const symbol = normalizedSymbol(ts, checker, name);
|
|
169
|
-
if (symbol) {
|
|
170
|
-
if (!found.has(name.text)) found.set(name.text, new Set());
|
|
171
|
-
found.get(name.text).add(symbol);
|
|
172
|
-
}
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) for (const element of name.elements) {
|
|
176
|
-
if (element && !ts.isOmittedExpression(element)) addName(element.name);
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
for (const parameter of fn.parameters) addName(parameter.name);
|
|
201
|
+
for (const parameter of fn.parameters) addBindingName(ts, checker, found, parameter.name);
|
|
180
202
|
const visit = node => {
|
|
181
203
|
if (node !== fn && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
|
|
182
|
-
if (ts.isVariableDeclaration(node))
|
|
204
|
+
if (ts.isVariableDeclaration(node)) addBindingName(ts, checker, found, node.name);
|
|
183
205
|
ts.forEachChild(node, visit);
|
|
184
206
|
};
|
|
185
207
|
if (fn.body) visit(fn.body);
|
|
@@ -201,249 +223,26 @@ function resolvedIdentities(ts, checker, fn, entry, reasons) {
|
|
|
201
223
|
return identities.length === entry.identities.length ? identities : null;
|
|
202
224
|
}
|
|
203
225
|
|
|
204
|
-
function accessPath(ts, checker, node) {
|
|
205
|
-
node = unwrapExpression(ts, node);
|
|
206
|
-
if (ts.isIdentifier(node)) return { symbol: normalizedSymbol(ts, checker, node), parts: [node.text] };
|
|
207
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
208
|
-
const base = accessPath(ts, checker, node.expression);
|
|
209
|
-
return base ? { symbol: base.symbol, parts: [...base.parts, node.name.text] } : null;
|
|
210
|
-
}
|
|
211
|
-
if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) {
|
|
212
|
-
const base = accessPath(ts, checker, node.expression);
|
|
213
|
-
return base ? { symbol: base.symbol, parts: [...base.parts, node.argumentExpression.text] } : null;
|
|
214
|
-
}
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function expandedAccessPath(ts, checker, node, seen = new Set(), depth = 0) {
|
|
219
|
-
if (!node || depth > 10) return null;
|
|
220
|
-
node = unwrapExpression(ts, node);
|
|
221
|
-
if (ts.isIdentifier(node)) {
|
|
222
|
-
const symbol = normalizedSymbol(ts, checker, node);
|
|
223
|
-
if (!symbol || seen.has(symbol)) return symbol ? { symbol, parts: [node.text] } : null;
|
|
224
|
-
const initializer = constInitializer(ts, symbol);
|
|
225
|
-
return initializer ? expandedAccessPath(ts, checker, initializer, new Set(seen).add(symbol), depth + 1)
|
|
226
|
-
?? { symbol, parts: [node.text] } : { symbol, parts: [node.text] };
|
|
227
|
-
}
|
|
228
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
229
|
-
const base = expandedAccessPath(ts, checker, node.expression, seen, depth + 1);
|
|
230
|
-
return base ? { symbol: base.symbol, parts: [...base.parts, node.name.text] } : null;
|
|
231
|
-
}
|
|
232
|
-
if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) {
|
|
233
|
-
const base = expandedAccessPath(ts, checker, node.expression, seen, depth + 1);
|
|
234
|
-
return base ? { symbol: base.symbol, parts: [...base.parts, node.argumentExpression.text] } : null;
|
|
235
|
-
}
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function matchesIdentityAccess(ts, checker, node, identity) {
|
|
240
|
-
const matches = access => access?.symbol === identity.symbol && access.parts.length >= identity.parts.length
|
|
241
|
-
&& identity.parts.every((part, index) => access.parts[index] === part);
|
|
242
|
-
return matches(accessPath(ts, checker, node)) || matches(expandedAccessPath(ts, checker, node));
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const expressionReferences = (ts, checker, expression, identity) =>
|
|
246
|
-
anyDescendant(ts, expression, (node) => matchesIdentityAccess(ts, checker, node, identity));
|
|
247
|
-
|
|
248
226
|
function constInitializer(ts, symbol) {
|
|
249
227
|
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
250
228
|
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
|
|
251
|
-
|| !(ts
|
|
229
|
+
|| !isConstVariable(ts, declarations[0])) return null;
|
|
252
230
|
return declarations[0].initializer;
|
|
253
231
|
}
|
|
254
232
|
|
|
255
|
-
function propertyKey(ts, name) {
|
|
256
|
-
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) return name.text;
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function effectiveObjectValues(ts, expression) {
|
|
261
|
-
const selected = new Map();
|
|
262
|
-
for (let index = expression.properties.length - 1; index >= 0; index -= 1) {
|
|
263
|
-
const property = expression.properties[index];
|
|
264
|
-
if (ts.isSpreadAssignment(property) || ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property)
|
|
265
|
-
|| ts.isSetAccessorDeclaration(property)) return null;
|
|
266
|
-
const key = propertyKey(ts, property.name);
|
|
267
|
-
if (key === null) return null;
|
|
268
|
-
if (!selected.has(key)) selected.set(key, ts.isShorthandPropertyAssignment(property) ? property.name : property.initializer);
|
|
269
|
-
}
|
|
270
|
-
return [...selected.entries()].sort(([left], [right]) => left.localeCompare(right));
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function identityContribution(ts, checker, expression, identity, seen = new Set(), depth = 0) {
|
|
274
|
-
if (!expression || depth > 12) return 'unproven';
|
|
275
|
-
expression = unwrapExpression(ts, expression);
|
|
276
|
-
if (matchesIdentityAccess(ts, checker, expression, identity)) return 'yes';
|
|
277
|
-
if (ts.isIdentifier(expression)) {
|
|
278
|
-
const symbol = normalizedSymbol(ts, checker, expression);
|
|
279
|
-
if (!symbol || seen.has(symbol)) return 'no';
|
|
280
|
-
const initializer = constInitializer(ts, symbol);
|
|
281
|
-
return initializer ? identityContribution(ts, checker, initializer, identity, new Set(seen).add(symbol), depth + 1) : 'no';
|
|
282
|
-
}
|
|
283
|
-
if (ts.isConditionalExpression(expression)) {
|
|
284
|
-
const whenTrue = identityContribution(ts, checker, expression.whenTrue, identity, seen, depth + 1);
|
|
285
|
-
const whenFalse = identityContribution(ts, checker, expression.whenFalse, identity, seen, depth + 1);
|
|
286
|
-
if (whenTrue === 'yes' && whenFalse === 'yes') return 'yes';
|
|
287
|
-
if (whenTrue === 'unproven' || whenFalse === 'unproven') return 'unproven';
|
|
288
|
-
return 'no';
|
|
289
|
-
}
|
|
290
|
-
if (ts.isBinaryExpression(expression)) {
|
|
291
|
-
if (expression.operatorToken.kind === ts.SyntaxKind.CommaToken) return identityContribution(ts, checker, expression.right, identity, seen, depth + 1);
|
|
292
|
-
if (expression.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionToken) {
|
|
293
|
-
return expressionReferences(ts, checker, expression, identity) ? 'unproven' : 'no';
|
|
294
|
-
}
|
|
295
|
-
const left = identityContribution(ts, checker, expression.left, identity, seen, depth + 1);
|
|
296
|
-
const right = identityContribution(ts, checker, expression.right, identity, seen, depth + 1);
|
|
297
|
-
return left === 'yes' || right === 'yes' ? 'yes' : left === 'unproven' || right === 'unproven' ? 'unproven' : 'no';
|
|
298
|
-
}
|
|
299
|
-
if (ts.isPrefixUnaryExpression(expression)) return expressionReferences(ts, checker, expression, identity) ? 'unproven' : 'no';
|
|
300
|
-
if (ts.isTemplateExpression(expression)) {
|
|
301
|
-
const values = expression.templateSpans.map(span => identityContribution(ts, checker, span.expression, identity, seen, depth + 1));
|
|
302
|
-
return values.includes('yes') ? 'yes' : values.includes('unproven') ? 'unproven' : 'no';
|
|
303
|
-
}
|
|
304
|
-
if (ts.isArrayLiteralExpression(expression)) {
|
|
305
|
-
if (expression.elements.some(element => ts.isSpreadElement(element))) return 'unproven';
|
|
306
|
-
const values = expression.elements.map(element => identityContribution(ts, checker, element, identity, seen, depth + 1));
|
|
307
|
-
return values.includes('yes') ? 'yes' : values.includes('unproven') ? 'unproven' : 'no';
|
|
308
|
-
}
|
|
309
|
-
if (ts.isObjectLiteralExpression(expression)) {
|
|
310
|
-
const values = effectiveObjectValues(ts, expression);
|
|
311
|
-
if (!values || values.length !== expression.properties.length) return 'unproven';
|
|
312
|
-
const states = values.map(([, value]) => identityContribution(ts, checker, value, identity, seen, depth + 1));
|
|
313
|
-
return states.includes('yes') ? 'yes' : states.includes('unproven') ? 'unproven' : 'no';
|
|
314
|
-
}
|
|
315
|
-
if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) return 'no';
|
|
316
|
-
if (ts.isCallExpression(expression) || ts.isNewExpression(expression) || ts.isAwaitExpression(expression)) return 'unproven';
|
|
317
|
-
return 'no';
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function returnedExpression(ts, fn) {
|
|
321
|
-
if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body)) return fn.body;
|
|
322
|
-
if (!fn.body || !ts.isBlock(fn.body)) return null;
|
|
323
|
-
const returns = [];
|
|
324
|
-
const visit = node => {
|
|
325
|
-
if (node !== fn.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
|
|
326
|
-
if (ts.isReturnStatement(node) && node.expression) returns.push(node.expression);
|
|
327
|
-
else ts.forEachChild(node, visit);
|
|
328
|
-
};
|
|
329
|
-
visit(fn.body);
|
|
330
|
-
return returns.length === 1 ? returns[0] : null;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function rootKeyExpression(ts, checker, expression, identities, seen = new Set(), depth = 0) {
|
|
334
|
-
if (!expression || depth > 10) return null;
|
|
335
|
-
expression = unwrapExpression(ts, expression);
|
|
336
|
-
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
|
|
337
|
-
const returned = returnedExpression(ts, expression);
|
|
338
|
-
return returned ? rootKeyExpression(ts, checker, returned, identities, seen, depth + 1) : null;
|
|
339
|
-
}
|
|
340
|
-
if (ts.isIdentifier(expression) && !identities.some(identity => identity.symbol === normalizedSymbol(ts, checker, expression))) {
|
|
341
|
-
const symbol = normalizedSymbol(ts, checker, expression);
|
|
342
|
-
if (symbol && !seen.has(symbol)) {
|
|
343
|
-
const declarations = symbol.getDeclarations?.() ?? [];
|
|
344
|
-
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
345
|
-
&& (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
|
|
346
|
-
return rootKeyExpression(ts, checker, declarations[0].initializer, identities, new Set(seen).add(symbol), depth + 1);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return expression;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function staticKeyExpression(ts, checker, expression, identities, seen = new Set(), depth = 0) {
|
|
354
|
-
if (!expression || depth > 12) return false;
|
|
355
|
-
expression = unwrapExpression(ts, expression);
|
|
356
|
-
if (expression.kind === ts.SyntaxKind.NullKeyword || expression.kind === ts.SyntaxKind.TrueKeyword
|
|
357
|
-
|| expression.kind === ts.SyntaxKind.FalseKeyword || ts.isStringLiteralLike(expression)
|
|
358
|
-
|| ts.isNumericLiteral(expression) || ts.isBigIntLiteral(expression)) return true;
|
|
359
|
-
if (ts.isNoSubstitutionTemplateLiteral(expression)) return true;
|
|
360
|
-
if (ts.isTemplateExpression(expression)) return expression.templateSpans.every(span => staticKeyExpression(ts, checker, span.expression, identities, seen, depth + 1));
|
|
361
|
-
if (ts.isArrayLiteralExpression(expression)) return expression.elements.every(element => !ts.isSpreadElement(element)
|
|
362
|
-
&& staticKeyExpression(ts, checker, element, identities, seen, depth + 1));
|
|
363
|
-
if (ts.isObjectLiteralExpression(expression)) {
|
|
364
|
-
const values = effectiveObjectValues(ts, expression);
|
|
365
|
-
return values !== null && values.length === expression.properties.length
|
|
366
|
-
&& values.every(([, value]) => staticKeyExpression(ts, checker, value, identities, seen, depth + 1));
|
|
367
|
-
}
|
|
368
|
-
if (ts.isConditionalExpression(expression)) return staticKeyExpression(ts, checker, expression.condition, identities, seen, depth + 1)
|
|
369
|
-
&& staticKeyExpression(ts, checker, expression.whenTrue, identities, seen, depth + 1)
|
|
370
|
-
&& staticKeyExpression(ts, checker, expression.whenFalse, identities, seen, depth + 1);
|
|
371
|
-
if (ts.isBinaryExpression(expression)) return staticKeyExpression(ts, checker, expression.left, identities, seen, depth + 1)
|
|
372
|
-
&& staticKeyExpression(ts, checker, expression.right, identities, seen, depth + 1);
|
|
373
|
-
if (ts.isPrefixUnaryExpression(expression)) return staticKeyExpression(ts, checker, expression.operand, identities, seen, depth + 1);
|
|
374
|
-
if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) {
|
|
375
|
-
if (identities.some(identity => expressionReferences(ts, checker, expression, identity))) return true;
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
if (ts.isIdentifier(expression)) {
|
|
379
|
-
if (expression.text === 'undefined' || identities.some(identity => identity.symbol === normalizedSymbol(ts, checker, expression))) return true;
|
|
380
|
-
const symbol = normalizedSymbol(ts, checker, expression);
|
|
381
|
-
if (!symbol || seen.has(symbol)) return false;
|
|
382
|
-
const declarations = symbol.getDeclarations?.() ?? [];
|
|
383
|
-
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
|
|
384
|
-
|| !(ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) return false;
|
|
385
|
-
return staticKeyExpression(ts, checker, declarations[0].initializer, identities, new Set(seen).add(symbol), depth + 1);
|
|
386
|
-
}
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function keyLeaves(ts, checker, expression, identities, decisions = [], depth = 0) {
|
|
391
|
-
if (depth > 12) return null;
|
|
392
|
-
expression = rootKeyExpression(ts, checker, expression, identities);
|
|
393
|
-
if (!expression) return null;
|
|
394
|
-
if (ts.isConditionalExpression(expression)) {
|
|
395
|
-
if (!staticKeyExpression(ts, checker, expression.condition, identities)) return null;
|
|
396
|
-
const whenTrue = keyLeaves(ts, checker, expression.whenTrue, identities, [...decisions, { condition: expression.condition, branch: true }], depth + 1);
|
|
397
|
-
const whenFalse = keyLeaves(ts, checker, expression.whenFalse, identities, [...decisions, { condition: expression.condition, branch: false }], depth + 1);
|
|
398
|
-
return whenTrue && whenFalse ? [...whenTrue, ...whenFalse] : null;
|
|
399
|
-
}
|
|
400
|
-
if (expression.kind === ts.SyntaxKind.NullKeyword) return [{ kind: 'null', expression, decisions }];
|
|
401
|
-
return staticKeyExpression(ts, checker, expression, identities) ? [{ kind: 'active', expression, decisions }] : null;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function nullish(ts, checker, expression) {
|
|
405
|
-
expression = unwrapExpression(ts, expression);
|
|
406
|
-
if (expression.kind === ts.SyntaxKind.NullKeyword) return true;
|
|
407
|
-
if (!ts.isIdentifier(expression) || expression.text !== 'undefined') return false;
|
|
408
|
-
const symbol = checker.getSymbolAtLocation(expression);
|
|
409
|
-
return !symbol || (symbol.getDeclarations?.() ?? []).every(declaration => declaration.getSourceFile().isDeclarationFile);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/** True when taking `branch` of `condition` proves the declared identity is available. */
|
|
413
|
-
function branchMeansAvailable(ts, checker, condition, identity, branch) {
|
|
414
|
-
condition = unwrapExpression(ts, condition);
|
|
415
|
-
if (expressionReferences(ts, checker, condition, identity) && (ts.isIdentifier(condition)
|
|
416
|
-
|| ts.isPropertyAccessExpression(condition) || ts.isElementAccessExpression(condition))) return branch;
|
|
417
|
-
if (ts.isPrefixUnaryExpression(condition) && condition.operator === ts.SyntaxKind.ExclamationToken) {
|
|
418
|
-
return branchMeansAvailable(ts, checker, condition.operand, identity, !branch);
|
|
419
|
-
}
|
|
420
|
-
if (ts.isBinaryExpression(condition)) {
|
|
421
|
-
const operator = condition.operatorToken.kind;
|
|
422
|
-
if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken) {
|
|
423
|
-
// `a && b` is true only when both sides are true; `a || b` is false only when both sides are false.
|
|
424
|
-
const decisive = operator === ts.SyntaxKind.AmpersandAmpersandToken ? branch : !branch;
|
|
425
|
-
return decisive && [condition.left, condition.right]
|
|
426
|
-
.some(side => branchMeansAvailable(ts, checker, side, identity, branch));
|
|
427
|
-
}
|
|
428
|
-
const leftIdentity = expressionReferences(ts, checker, condition.left, identity) && nullish(ts, checker, condition.right);
|
|
429
|
-
const rightIdentity = expressionReferences(ts, checker, condition.right, identity) && nullish(ts, checker, condition.left);
|
|
430
|
-
if (!leftIdentity && !rightIdentity) return false;
|
|
431
|
-
if ([ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(operator)) return branch;
|
|
432
|
-
if ([ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(operator)) return !branch;
|
|
433
|
-
}
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
233
|
function violation(config, call, ruleId, message, extra = {}) {
|
|
438
234
|
return { ruleId, path: relativePath(config.root, call.source.fileName), ...sourceLocation(call.source, call.node), message, ...extra };
|
|
439
235
|
}
|
|
440
236
|
|
|
237
|
+
const inspectKey = createNextDataKeyInspector({ anyDescendant, normalizedSymbol, unwrapExpression, constInitializer, sourceLocation, violation,
|
|
238
|
+
SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID });
|
|
239
|
+
|
|
441
240
|
function isUnshadowedCommonJsRequire(ts, checker, expression, seen = new Set(), depth = 0) {
|
|
442
241
|
expression = unwrapExpression(ts, expression);
|
|
443
242
|
if (!ts.isIdentifier(expression) || depth > 10) return false;
|
|
444
243
|
const symbol = checker.getSymbolAtLocation(expression);
|
|
445
244
|
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
446
|
-
if (expression.text === 'require' && (!symbol
|
|
245
|
+
if (expression.text === 'require' && (!symbol
|
|
447
246
|
|| declarations.every(declaration => declaration.getSourceFile().isDeclarationFile))) return true;
|
|
448
247
|
const normalized = symbolOfRequireAlias(ts, checker, expression);
|
|
449
248
|
if (!normalized || seen.has(normalized)) return false;
|
|
@@ -456,145 +255,59 @@ function symbolOfRequireAlias(ts, checker, expression) {
|
|
|
456
255
|
return normalizedSymbolValue(ts, checker, symbol);
|
|
457
256
|
}
|
|
458
257
|
|
|
258
|
+
function isSwrModuleDeclaration(ts, node) {
|
|
259
|
+
return (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier
|
|
260
|
+
&& ts.isStringLiteralLike(node.moduleSpecifier) && SWR_SPECIFIERS.has(node.moduleSpecifier.text);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The kind of unsupported SWR binding the node declares, else null. */
|
|
264
|
+
function unsupportedSwrBinding(ts, checker, node) {
|
|
265
|
+
if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)
|
|
266
|
+
&& node.moduleReference.expression && ts.isStringLiteralLike(node.moduleReference.expression)
|
|
267
|
+
&& SWR_SPECIFIERS.has(node.moduleReference.expression.text)) return 'import-equals binding';
|
|
268
|
+
if (ts.isCallExpression(node) && node.arguments.length >= 1 && ts.isStringLiteralLike(node.arguments[0])
|
|
269
|
+
&& SWR_SPECIFIERS.has(node.arguments[0].text)) {
|
|
270
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) return 'dynamic import binding';
|
|
271
|
+
if (isUnshadowedCommonJsRequire(ts, checker, node.expression)) return 'CommonJS require binding';
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
459
276
|
function swrReferences(config, context) {
|
|
277
|
+
const { ts } = context;
|
|
460
278
|
let found = false;
|
|
461
279
|
const unsupported = [];
|
|
462
280
|
for (const source of context.files) {
|
|
463
281
|
const checker = context.checkerFor(source.fileName);
|
|
464
|
-
const report = (node, kind) => {
|
|
465
|
-
found = true;
|
|
466
|
-
unsupported.push(`${relativePath(config.root, source.fileName)}:${sourceLocation(source, node).line} uses unsupported ${kind} for SWR`);
|
|
467
|
-
};
|
|
468
282
|
const visit = node => {
|
|
469
|
-
if ((
|
|
470
|
-
|
|
471
|
-
if (
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (context.ts.isCallExpression(node) && node.arguments.length >= 1 && context.ts.isStringLiteralLike(node.arguments[0])
|
|
475
|
-
&& SWR_SPECIFIERS.has(node.arguments[0].text)) {
|
|
476
|
-
if (node.expression.kind === context.ts.SyntaxKind.ImportKeyword) report(node, 'dynamic import binding');
|
|
477
|
-
else if (isUnshadowedCommonJsRequire(context.ts, checker, node.expression)) report(node, 'CommonJS require binding');
|
|
283
|
+
if (isSwrModuleDeclaration(ts, node)) found = true;
|
|
284
|
+
const kind = unsupportedSwrBinding(ts, checker, node);
|
|
285
|
+
if (kind) {
|
|
286
|
+
found = true;
|
|
287
|
+
unsupported.push(`${relativePath(config.root, source.fileName)}:${sourceLocation(source, node).line} uses unsupported ${kind} for SWR`);
|
|
478
288
|
}
|
|
479
|
-
|
|
289
|
+
ts.forEachChild(node, visit);
|
|
480
290
|
};
|
|
481
291
|
visit(source);
|
|
482
292
|
}
|
|
483
293
|
return { found, unsupported };
|
|
484
294
|
}
|
|
485
295
|
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
return expression.kind === ts.SyntaxKind.FalseKeyword || ts.isStringLiteralLike(expression) && expression.text === ''
|
|
489
|
-
|| ts.isNumericLiteral(expression) && Number(expression.text) === 0
|
|
490
|
-
|| ts.isIdentifier(expression) && expression.text === 'undefined';
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function containerSymbolsFromKey(ts, checker, expression, found = new Set(), seen = new Set(), depth = 0) {
|
|
494
|
-
if (!expression || depth > 12) return found;
|
|
495
|
-
expression = unwrapExpression(ts, expression);
|
|
496
|
-
if (ts.isIdentifier(expression)) {
|
|
497
|
-
const symbol = normalizedSymbol(ts, checker, expression);
|
|
498
|
-
if (!symbol || seen.has(symbol)) return found;
|
|
499
|
-
const initializer = constInitializer(ts, symbol);
|
|
500
|
-
if (!initializer) return found;
|
|
501
|
-
const value = unwrapExpression(ts, initializer);
|
|
502
|
-
if (ts.isArrayLiteralExpression(value) || ts.isObjectLiteralExpression(value)) found.add(symbol);
|
|
503
|
-
containerSymbolsFromKey(ts, checker, initializer, found, new Set(seen).add(symbol), depth + 1);
|
|
504
|
-
return found;
|
|
505
|
-
}
|
|
506
|
-
ts.forEachChild(expression, child => { containerSymbolsFromKey(ts, checker, child, found, seen, depth + 1); });
|
|
507
|
-
return found;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function keyContainerRisks(ts, checker, key, owner) {
|
|
511
|
-
const symbols = containerSymbolsFromKey(ts, checker, key);
|
|
512
|
-
if (!symbols.size || !owner?.body) return [];
|
|
513
|
-
const declarationNames = new Set([...symbols].flatMap(symbol => (symbol.getDeclarations?.() ?? [])
|
|
514
|
-
.filter(ts.isVariableDeclaration).map(declaration => declaration.name)));
|
|
515
|
-
const risks = [];
|
|
516
|
-
const visit = node => {
|
|
517
|
-
if (ts.isIdentifier(node) && symbols.has(normalizedSymbol(ts, checker, node)) && !declarationNames.has(node)
|
|
518
|
-
&& !(node.pos >= key.pos && node.end <= key.end)) risks.push(node);
|
|
519
|
-
ts.forEachChild(node, visit);
|
|
520
|
-
};
|
|
521
|
-
visit(owner.body);
|
|
522
|
-
return risks;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function inspectKey(config, context, entry, call, identities, violations, reasons) {
|
|
526
|
-
const key = call.node.arguments[0];
|
|
527
|
-
if (!key) {
|
|
528
|
-
reasons.push(`${entry.path}#${entry.export} has an SWR call without a key`);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
const containerRisks = keyContainerRisks(context.ts, call.checker, key, call.owner);
|
|
532
|
-
if (containerRisks.length) {
|
|
533
|
-
const locations = containerRisks.map(node => sourceLocation(call.source, node).line).filter((line, index, all) => all.indexOf(line) === index);
|
|
534
|
-
reasons.push(`${entry.path}#${entry.export} key container is referenced outside its declaration and selected SWR key${locations.length ? ` (line${locations.length === 1 ? '' : 's'} ${locations.join(', ')})` : ''}`);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
const leaves = keyLeaves(context.ts, call.checker, key, identities);
|
|
538
|
-
if (!leaves) {
|
|
539
|
-
reasons.push(`${entry.path}#${entry.export} uses a dynamic SWR key whose identity cannot be proved`);
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
const active = leaves.filter(leaf => leaf.kind === 'active');
|
|
543
|
-
if (active.some(leaf => nonNullFalsy(context.ts, leaf.expression))) violations.push(violation(config, call, SWR_KEY_RULE_ID,
|
|
544
|
-
`${entry.id} uses explicit null, rather than another falsy value, for a disabled SWR key.`, { lifecycle: entry.id }));
|
|
545
|
-
for (const identity of identities) {
|
|
546
|
-
if (!active.length) {
|
|
547
|
-
violations.push(violation(config, call, entry.kind === 'mutation' && identity.resource ? SWR_MUTATION_RULE_ID : SWR_KEY_RULE_ID,
|
|
548
|
-
`${entry.id} has no active key path carrying declared identity ${identity.id} (${identity.binding}).`,
|
|
549
|
-
{ lifecycle: entry.id, identity: identity.id }));
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
const contributions = active.map(leaf => identityContribution(context.ts, call.checker, leaf.expression, identity));
|
|
553
|
-
if (contributions.includes('unproven')) {
|
|
554
|
-
reasons.push(`${entry.path}#${entry.export} key value contribution for identity ${identity.id} (${identity.binding}) cannot be proved on every active path`);
|
|
555
|
-
} else if (contributions.includes('no')) {
|
|
556
|
-
const ruleId = entry.kind === 'mutation' && identity.resource ? SWR_MUTATION_RULE_ID : SWR_KEY_RULE_ID;
|
|
557
|
-
violations.push(violation(config, call, ruleId,
|
|
558
|
-
`${entry.id} key must include declared ${identity.resource ? 'resource ' : ''}identity ${identity.id} (${identity.binding}) on every active key path.`,
|
|
559
|
-
{ lifecycle: entry.id, identity: identity.id }));
|
|
560
|
-
}
|
|
561
|
-
if (identity.gatesRequest) {
|
|
562
|
-
const gated = active.every(leaf => leaf.decisions.some(decision => {
|
|
563
|
-
return branchMeansAvailable(context.ts, call.checker, decision.condition, identity, decision.branch);
|
|
564
|
-
}));
|
|
565
|
-
if (!gated) violations.push(violation(config, call, SWR_KEY_RULE_ID,
|
|
566
|
-
`${entry.id} must produce an explicit null key when ${identity.id} (${identity.binding}) is unavailable.`,
|
|
567
|
-
{ lifecycle: entry.id, identity: identity.id }));
|
|
568
|
-
}
|
|
569
|
-
}
|
|
296
|
+
function unavailableDataLifecycle(ruleIds, details) {
|
|
297
|
+
return { violations: [], coverage: { status: 'unavailable', ruleIds, details } };
|
|
570
298
|
}
|
|
571
299
|
|
|
572
|
-
|
|
573
|
-
export function checkFrontendDataLifecycle(config, context) {
|
|
574
|
-
const ruleIds = [...SWR_DATA_RULE_IDS];
|
|
575
|
-
const reasons = [];
|
|
576
|
-
const violations = [];
|
|
577
|
-
const references = swrReferences(config, context);
|
|
578
|
-
let contract;
|
|
579
|
-
try { contract = parseContract(config); } catch (error) {
|
|
580
|
-
return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
|
|
581
|
-
}
|
|
582
|
-
if (!contract && !references.found) return { violations, coverage: { status: 'not-applicable', ruleIds } };
|
|
583
|
-
if (!contract) return { violations, coverage: { status: 'unavailable', ruleIds,
|
|
584
|
-
details: ['package.json#starci.codePatterns.next.dataLifecycle is required when production source selects SWR', ...references.unsupported] } };
|
|
585
|
-
reasons.push(...references.unsupported);
|
|
586
|
-
let installed;
|
|
587
|
-
try { installed = installedSWR(config.packageRoot ?? config.root); } catch (error) {
|
|
588
|
-
return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
|
|
589
|
-
}
|
|
590
|
-
if (installed.major !== contract.swr.major) reasons.push(`installed swr ${installed.version} does not match declared major ${contract.swr.major}`);
|
|
300
|
+
function lifecycleCallSets(config, context, installed, reasons) {
|
|
591
301
|
const official = officialTargets(config, context, installed, reasons);
|
|
592
302
|
const calls = discoverCalls(config, context, official.targets);
|
|
593
303
|
const lifecycleCalls = calls.filter(call => call.kind === 'query' || call.kind === 'mutation');
|
|
594
304
|
const globalMutates = calls.filter(call => call.kind === 'global-mutate');
|
|
595
305
|
if (official.selectedBindings && lifecycleCalls.length === 0) reasons.push('selected SWR hook imports have no statically resolved lifecycle call');
|
|
596
306
|
for (const call of globalMutates) reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} uses global mutate; resource matching or filter semantics are not statically declared`);
|
|
307
|
+
return { lifecycleCalls };
|
|
308
|
+
}
|
|
597
309
|
|
|
310
|
+
function hookEntries(config, context, contract, reasons) {
|
|
598
311
|
const hookCache = new Map();
|
|
599
312
|
const entriesByHook = new Map();
|
|
600
313
|
for (const entry of contract.hooks) {
|
|
@@ -603,7 +316,32 @@ export function checkFrontendDataLifecycle(config, context) {
|
|
|
603
316
|
entriesByHook.get(key).push(entry);
|
|
604
317
|
if (!hookCache.has(key)) hookCache.set(key, exportedHook(config, context, entry, reasons));
|
|
605
318
|
}
|
|
319
|
+
return { hookCache, entriesByHook };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function inspectLifecycleEntry(scope, entry, hookCalls, mapped) {
|
|
323
|
+
const { config, context, violations, reasons } = scope;
|
|
324
|
+
const { entries, owned, hook } = hookCalls;
|
|
325
|
+
let candidates;
|
|
326
|
+
if (entry.resultBinding !== undefined) candidates = owned.filter(call => call.bindings.includes(entry.resultBinding));
|
|
327
|
+
else candidates = owned.length === 1 && entries.length === 1 ? owned : [];
|
|
328
|
+
if (candidates.length !== 1) {
|
|
329
|
+
reasons.push(`${entry.path}#${entry.export} lifecycle ${entry.id} must select exactly one SWR call${owned.length > 1 ? ' with resultBinding' : ''}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const call = candidates[0];
|
|
333
|
+
mapped.set(call, (mapped.get(call) ?? 0) + 1);
|
|
334
|
+
if (call.kind !== entry.kind) {
|
|
335
|
+
reasons.push(`${entry.path}#${entry.export} lifecycle ${entry.id} declares ${entry.kind} but resolves ${call.kind}`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const identities = resolvedIdentities(context.ts, hook.checker, hook.fn, entry, reasons);
|
|
339
|
+
if (identities) inspectKey(config, context, entry, call, identities, violations, reasons);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function inspectHookLifecycle(config, context, lifecycleCalls, hookCache, entriesByHook, violations, reasons) {
|
|
606
343
|
const mapped = new Map();
|
|
344
|
+
const scope = { config, context, violations, reasons };
|
|
607
345
|
for (const [key, entries] of entriesByHook) {
|
|
608
346
|
const hook = hookCache.get(key);
|
|
609
347
|
if (!hook) continue;
|
|
@@ -612,37 +350,51 @@ export function checkFrontendDataLifecycle(config, context) {
|
|
|
612
350
|
reasons.push(`${entries[0].path}#${entries[0].export} contains no statically resolved SWR lifecycle call`);
|
|
613
351
|
continue;
|
|
614
352
|
}
|
|
615
|
-
for (const entry of entries) {
|
|
616
|
-
let candidates;
|
|
617
|
-
if (entry.resultBinding !== undefined) candidates = owned.filter(call => call.bindings.includes(entry.resultBinding));
|
|
618
|
-
else candidates = owned.length === 1 && entries.length === 1 ? owned : [];
|
|
619
|
-
if (candidates.length !== 1) {
|
|
620
|
-
reasons.push(`${entry.path}#${entry.export} lifecycle ${entry.id} must select exactly one SWR call${owned.length > 1 ? ' with resultBinding' : ''}`);
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
const call = candidates[0];
|
|
624
|
-
mapped.set(call, (mapped.get(call) ?? 0) + 1);
|
|
625
|
-
if (call.kind !== entry.kind) {
|
|
626
|
-
reasons.push(`${entry.path}#${entry.export} lifecycle ${entry.id} declares ${entry.kind} but resolves ${call.kind}`);
|
|
627
|
-
continue;
|
|
628
|
-
}
|
|
629
|
-
const identities = resolvedIdentities(context.ts, hook.checker, hook.fn, entry, reasons);
|
|
630
|
-
if (identities) inspectKey(config, context, entry, call, identities, violations, reasons);
|
|
631
|
-
}
|
|
353
|
+
for (const entry of entries) inspectLifecycleEntry(scope, entry, { entries, owned, hook }, mapped);
|
|
632
354
|
for (const call of owned) if ((mapped.get(call) ?? 0) !== 1) {
|
|
633
355
|
reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} SWR call is not matched by exactly one declared lifecycle`);
|
|
634
356
|
}
|
|
635
357
|
}
|
|
358
|
+
return mapped;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function addUndeclaredLifecycleReasons(config, lifecycleCalls, hookCache, reasons) {
|
|
636
362
|
const declaredFunctions = new Set([...hookCache.values()].filter(Boolean).map(item => item.fn));
|
|
637
363
|
for (const call of lifecycleCalls) if (!declaredFunctions.has(call.owner)) {
|
|
638
364
|
reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} SWR call is outside every declared lifecycle hook`);
|
|
639
365
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function lifecycleCoverage(ruleIds, contract, installed, calls, details) {
|
|
369
|
+
return details.length ? { status: 'unavailable', ruleIds, hooks: contract.hooks.length, calls,
|
|
370
|
+
swr: { name: installed.name, version: installed.version, major: installed.major }, details }
|
|
371
|
+
: { status: 'checked', ruleIds, hooks: contract.hooks.length, calls,
|
|
372
|
+
swr: { name: installed.name, version: installed.version, major: installed.major } };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Check explicitly declared SWR lifecycle keys against the target program and installed SWR identity. */
|
|
376
|
+
export function checkFrontendDataLifecycle(config, context) {
|
|
377
|
+
const ruleIds = [...SWR_DATA_RULE_IDS];
|
|
378
|
+
const references = swrReferences(config, context);
|
|
379
|
+
let contract;
|
|
380
|
+
try { contract = parseContract(config); } catch (error) {
|
|
381
|
+
return unavailableDataLifecycle(ruleIds, [String(error.message ?? error)]);
|
|
382
|
+
}
|
|
383
|
+
if (!contract && !references.found) return { violations: [], coverage: { status: 'not-applicable', ruleIds } };
|
|
384
|
+
if (!contract) return { violations: [], coverage: { status: 'unavailable', ruleIds,
|
|
385
|
+
details: ['package.json#starci.codePatterns.next.dataLifecycle is required when production source selects SWR', ...references.unsupported] } };
|
|
386
|
+
const reasons = [];
|
|
387
|
+
const violations = [];
|
|
388
|
+
reasons.push(...references.unsupported);
|
|
389
|
+
let installed;
|
|
390
|
+
try { installed = installedSWR(config.packageRoot ?? config.root); } catch (error) {
|
|
391
|
+
return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
|
|
392
|
+
}
|
|
393
|
+
if (installed.major !== contract.swr.major) reasons.push(`installed swr ${installed.version} does not match declared major ${contract.swr.major}`);
|
|
394
|
+
const { lifecycleCalls } = lifecycleCallSets(config, context, installed, reasons);
|
|
395
|
+
const { hookCache, entriesByHook } = hookEntries(config, context, contract, reasons);
|
|
396
|
+
inspectHookLifecycle(config, context, lifecycleCalls, hookCache, entriesByHook, violations, reasons);
|
|
397
|
+
addUndeclaredLifecycleReasons(config, lifecycleCalls, hookCache, reasons);
|
|
398
|
+
const details = [...new Set(reasons)].sort(byCodeUnit);
|
|
399
|
+
return { violations, coverage: lifecycleCoverage(ruleIds, contract, installed, lifecycleCalls.length, details) };
|
|
648
400
|
}
|