@starci/hfs 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +116 -12
- package/bin/hfs.mjs +119 -15
- package/emit/compiler.mjs +35 -0
- package/emit/contracts.mjs +97 -0
- package/emit/operations-worker.mjs +24 -0
- package/emit/operations.mjs +126 -0
- package/emit/schema-worker.mjs +117 -0
- package/emit/static-graph.mjs +670 -0
- package/emit/type-schema.mjs +145 -0
- package/package.json +10 -2
- package/report/sonar.mjs +180 -0
- package/runtime/engine/admission.mjs +284 -0
- package/runtime/engine/digest.mjs +10 -0
- package/runtime/engine/ledger-db.mjs +1245 -0
- package/runtime/engine/machine-db.mjs +1484 -0
- package/runtime/engine/migrations/machine/0001-init.sql +887 -0
- package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
- package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
- package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
- package/runtime/engine/plain-object.mjs +5 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
- package/runtime/knowledge/hfs/slots.yaml +409 -140
- package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
- package/runtime/knowledge/sonar-gate.yaml +85 -0
- package/runtime/modules/kernel/failure-codes.yaml +1480 -16
- package/runtime/scripts/checks/architecture/backend.mjs +350 -0
- package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
- package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
- package/runtime/scripts/checks/architecture/clones.mjs +200 -0
- package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
- package/runtime/scripts/checks/architecture/config.mjs +310 -0
- package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
- package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
- package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
- package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
- package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
- package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
- package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
- package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
- package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
- package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
- package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
- package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
- package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
- package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
- package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
- package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
- package/runtime/scripts/checks/architecture/index.mjs +316 -0
- package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
- package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
- package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
- package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
- package/runtime/scripts/checks/architecture/owners.mjs +89 -0
- package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
- package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
- package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
- package/runtime/scripts/checks/architecture/registration.mjs +319 -0
- package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
- package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
- package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
- package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
- package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
- package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
- package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
- package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
- package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
- package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
- package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
- package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
- package/runtime/scripts/checks/architecture.mjs +41 -0
- package/runtime/scripts/checks/common.mjs +37 -0
- package/runtime/scripts/checks/typescript-programs.mjs +82 -0
- package/runtime/scripts/lib/artifact-hold.mjs +89 -0
- package/runtime/scripts/lib/artifact-store.mjs +103 -0
- package/runtime/scripts/lib/fs-kind.mjs +10 -0
- package/runtime/scripts/lib/git.mjs +53 -0
- package/runtime/scripts/lib/hfs-allows.mjs +57 -0
- package/runtime/scripts/lib/hfs-check.mjs +254 -28
- package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
- package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
- package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
- package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
- package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
- package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
- package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
- package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
- package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
- package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
- package/runtime/scripts/lib/hfs-slots.mjs +95 -41
- package/runtime/scripts/lib/hfs-tree.mjs +80 -0
- package/runtime/scripts/lib/hfs-view.mjs +68 -0
- package/runtime/scripts/lib/json.mjs +22 -0
- package/runtime/scripts/lib/language.mjs +107 -0
- package/runtime/scripts/lib/path-key.mjs +2 -0
- package/runtime/scripts/lib/redact.mjs +148 -0
- package/runtime/scripts/lib/repo-identity.mjs +50 -0
- package/runtime/scripts/lib/safe-remove.mjs +179 -0
- package/runtime/scripts/lib/secret-patterns.mjs +44 -0
- package/runtime/scripts/lib/sleep-sync.mjs +17 -0
- package/runtime/scripts/lib/stack-declaration.mjs +52 -0
- package/runtime/scripts/lib/stack-services.mjs +217 -0
- package/runtime/scripts/lib/test-secrets.mjs +120 -0
- package/scaffold/service.mjs +333 -0
- package/sync/format.mjs +46 -0
- package/sync/hygiene.mjs +56 -24
- package/sync/index.mjs +126 -41
- package/sync/managed.mjs +170 -0
- package/sync/skeleton.mjs +32 -10
- package/sync/sonar-key.mjs +13 -0
- package/sync/ts-strict.mjs +48 -0
- package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
- package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
- package/templates/be/hooks/husky/pre-commit +13 -0
- package/templates/be/hooks/husky/pre-push +7 -0
- package/templates/be/package-scripts/package.json +21 -0
- package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
- package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
- package/templates/be/skeleton/scripts/.gitkeep +0 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
- package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
- package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
- package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
- package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
- package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
- package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
- package/templates/be/tool-config/eslint.config.mjs +3 -0
- package/templates/be/tool-config/jest.config.js +1 -0
- package/templates/be/tool-config/prettierignore +8 -0
- package/templates/be/tool-config/prettierrc +1 -0
- package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
- package/templates/be/tool-config/tsconfig.build.json +5 -0
- package/templates/be/tool-config/tsconfig.json +11 -0
- package/templates/common/gitignore.base +1 -1
- package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
- package/templates/fe/hooks/husky/pre-commit +16 -0
- package/templates/fe/hooks/husky/pre-push +6 -0
- package/templates/fe/package-scripts/package.json +17 -0
- package/templates/fe/parts/api-client.ts +44 -0
- package/templates/fe/parts/api-outcome.ts +7 -0
- package/templates/fe/quality-config/sonar-project.properties +8 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
- package/templates/fe/skeleton/scripts/.gitkeep +0 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
- package/templates/fe/tool-config/eslint.config.mjs +3 -0
- package/templates/fe/tool-config/prettierignore +10 -0
- package/templates/fe/tool-config/prettierrc +1 -0
- package/templates/fe/tool-config/stylelint.config.mjs +3 -0
- package/templates/fe/tool-config/tsconfig.json +4 -0
- package/templates/be/pre-commit +0 -8
- package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
- package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
- package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
- package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
- package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
- package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
- package/templates/common/codecov.yml +0 -13
- package/templates/common/pre-push +0 -5
- package/templates/fe/e2e.yml +0 -22
- package/templates/fe/pre-commit +0 -7
- package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
- package/templates/fe/sonar-project.properties +0 -11
- /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
- /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Nest application serves over GraphQL, read from its SOURCE: the module graph an app root composes and the resolver
|
|
3
|
+
* classes Nest GraphQL's explorers would collect from it. Nothing is executed and no configuration is needed: the app root and
|
|
4
|
+
* every module it reaches are parsed with the TypeScript compiler API and their `@Module` metadata and static registration
|
|
5
|
+
* methods (`register`, `forRoot`, ...) are interpreted symbolically. The interpretation is lazy and only follows what decides
|
|
6
|
+
* the graph: module imports, providers, `forwardRef`, spreads, conditionals (both branches are taken: the contract is the
|
|
7
|
+
* superset a deployment can serve), static methods called with the options of a deployment (unknown values), the options of
|
|
8
|
+
* `GraphQLModule.forRoot` / `forRootAsync`.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors Nest: a module's providers are the `@Module({ providers })` of its class plus the `providers` of a dynamic module;
|
|
11
|
+
* likewise its imports. A GraphQL server reads the providers of EVERY module of the application unless its options carry
|
|
12
|
+
* `include`, which whitelists module classes and their transitive imports (Nest's `BaseExplorerService.getModules`).
|
|
13
|
+
*
|
|
14
|
+
* Anything the interpreter cannot decide in a position that decides the graph (a module import or a provider list) is an
|
|
15
|
+
* error naming the file and line: an answer is exact or it is not given.
|
|
16
|
+
*
|
|
17
|
+
* Injected: `ts` (the TypeScript module), `host.read(file)` (text or null) and `host.resolve(fromFile, specifier)` (an
|
|
18
|
+
* absolute file of the repository, or null for an external package). Pure of the file system otherwise.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const NEST_COMMON = '@nestjs/common';
|
|
22
|
+
const NEST_GRAPHQL = '@nestjs/graphql';
|
|
23
|
+
|
|
24
|
+
/** GraphQL server options that change the printed schema and that this reader does not model: presence is an error. */
|
|
25
|
+
const SCHEMA_OPTIONS_NOT_MODELED = ['buildSchemaOptions', 'typeDefs', 'schema', 'transformSchema', 'transformAutoSchemaFile'];
|
|
26
|
+
|
|
27
|
+
const undef = { t: 'undef' };
|
|
28
|
+
const nul = { t: 'nul' };
|
|
29
|
+
const free = (why) => ({ t: 'free', why });
|
|
30
|
+
|
|
31
|
+
export function createGraphReader({ ts, host }) {
|
|
32
|
+
const files = new Map();
|
|
33
|
+
const classes = new Map();
|
|
34
|
+
const thunks = new WeakMap();
|
|
35
|
+
|
|
36
|
+
const where = (node) => {
|
|
37
|
+
const sf = node.getSourceFile();
|
|
38
|
+
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
39
|
+
return `${sf.fileName}:${line + 1}`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ---- files: imports, declarations, exports ----
|
|
43
|
+
function info(file) {
|
|
44
|
+
if (files.has(file)) return files.get(file);
|
|
45
|
+
const text = host.read(file);
|
|
46
|
+
if (text === null || text === undefined) throw new Error(`cannot read ${file}`);
|
|
47
|
+
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
48
|
+
const record = { sourceFile, imports: new Map(), decls: new Map(), exports: new Map(), stars: [] };
|
|
49
|
+
files.set(file, record);
|
|
50
|
+
const exported = (node) => (ts.getSyntacticModifierFlags(node) & ts.ModifierFlags.Export) !== 0;
|
|
51
|
+
const isDefault = (node) => (ts.getSyntacticModifierFlags(node) & ts.ModifierFlags.Default) !== 0;
|
|
52
|
+
for (const statement of sourceFile.statements) {
|
|
53
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
54
|
+
const spec = statement.moduleSpecifier.text;
|
|
55
|
+
const clause = statement.importClause;
|
|
56
|
+
if (!clause) continue;
|
|
57
|
+
if (clause.name) record.imports.set(clause.name.text, { spec, name: 'default' });
|
|
58
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
59
|
+
for (const element of clause.namedBindings.elements) record.imports.set(element.name.text, { spec, name: (element.propertyName ?? element.name).text });
|
|
60
|
+
} else if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) record.imports.set(clause.namedBindings.name.text, { spec, name: '*' });
|
|
61
|
+
} else if ((ts.isClassDeclaration(statement) || ts.isFunctionDeclaration(statement)) && statement.name) {
|
|
62
|
+
record.decls.set(statement.name.text, { kind: ts.isClassDeclaration(statement) ? 'class' : 'function', node: statement });
|
|
63
|
+
if (exported(statement)) record.exports.set(isDefault(statement) ? 'default' : statement.name.text, { local: statement.name.text });
|
|
64
|
+
} else if (ts.isVariableStatement(statement)) {
|
|
65
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
66
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
67
|
+
record.decls.set(declaration.name.text, { kind: 'var', node: declaration });
|
|
68
|
+
if (exported(statement)) record.exports.set(declaration.name.text, { local: declaration.name.text });
|
|
69
|
+
} else {
|
|
70
|
+
for (const element of declaration.name.elements ?? []) {
|
|
71
|
+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
72
|
+
record.decls.set(element.name.text, { kind: 'opaque', node: element });
|
|
73
|
+
if (exported(statement)) record.exports.set(element.name.text, { local: element.name.text });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else if (ts.isEnumDeclaration(statement)) {
|
|
79
|
+
record.decls.set(statement.name.text, { kind: 'opaque', node: statement });
|
|
80
|
+
if (exported(statement)) record.exports.set(statement.name.text, { local: statement.name.text });
|
|
81
|
+
} else if (ts.isExportDeclaration(statement) && !statement.isTypeOnly) {
|
|
82
|
+
const spec = statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : null;
|
|
83
|
+
if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
84
|
+
for (const element of statement.exportClause.elements) {
|
|
85
|
+
if (element.isTypeOnly) continue;
|
|
86
|
+
record.exports.set(element.name.text, spec ? { spec, name: (element.propertyName ?? element.name).text } : { local: (element.propertyName ?? element.name).text });
|
|
87
|
+
}
|
|
88
|
+
} else if (!statement.exportClause && spec) record.stars.push(spec);
|
|
89
|
+
} else if (ts.isExportAssignment(statement)) record.exports.set('default', { expression: statement.expression });
|
|
90
|
+
}
|
|
91
|
+
return record;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const target = (file, spec) => host.resolve(file, spec);
|
|
95
|
+
|
|
96
|
+
/** A top-level name of a file: its own declaration, or what an import binding leads to. */
|
|
97
|
+
function resolveName(file, name, seen = new Set()) {
|
|
98
|
+
const key = `${file}#${name}`;
|
|
99
|
+
if (seen.has(key)) return null;
|
|
100
|
+
seen.add(key);
|
|
101
|
+
const record = info(file);
|
|
102
|
+
const decl = record.decls.get(name);
|
|
103
|
+
if (decl) return { file, name, ...decl };
|
|
104
|
+
const binding = record.imports.get(name);
|
|
105
|
+
if (!binding) return null;
|
|
106
|
+
const to = target(file, binding.spec);
|
|
107
|
+
if (to === null) return { ext: binding.spec, name: binding.name };
|
|
108
|
+
if (binding.name === '*') return { opaque: true };
|
|
109
|
+
return resolveExport(to, binding.name, seen);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A name a file exports, followed through re-exports and `export *`. */
|
|
113
|
+
function resolveExport(file, name, seen = new Set()) {
|
|
114
|
+
const record = info(file);
|
|
115
|
+
const entry = record.exports.get(name);
|
|
116
|
+
if (entry?.local) return resolveName(file, entry.local, seen);
|
|
117
|
+
if (entry?.spec) {
|
|
118
|
+
const to = target(file, entry.spec);
|
|
119
|
+
return to === null ? { ext: entry.spec, name: entry.name } : resolveExport(to, entry.name, seen);
|
|
120
|
+
}
|
|
121
|
+
if (entry?.expression) return { file, name, kind: 'expression', node: entry.expression };
|
|
122
|
+
for (const spec of record.stars) {
|
|
123
|
+
const to = target(file, spec);
|
|
124
|
+
if (to === null) continue;
|
|
125
|
+
const found = resolveExport(to, name, seen);
|
|
126
|
+
if (found) return found;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const classRef = (found) => {
|
|
132
|
+
const key = `${found.file}#${found.name}`;
|
|
133
|
+
if (!classes.has(key)) classes.set(key, { t: 'class', file: found.file, name: found.name, node: found.node });
|
|
134
|
+
return classes.get(key);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ---- values ----
|
|
138
|
+
const thunk = (node, env) => {
|
|
139
|
+
const key = env.cache;
|
|
140
|
+
if (!thunks.has(key)) thunks.set(key, new Map());
|
|
141
|
+
const memo = thunks.get(key);
|
|
142
|
+
if (!memo.has(node)) memo.set(node, { t: 'thunk', node, env, done: false, value: undefined });
|
|
143
|
+
return memo.get(node);
|
|
144
|
+
};
|
|
145
|
+
const lazy = (compute) => ({ t: 'thunk', compute, done: false, value: undefined });
|
|
146
|
+
const force = (value) => {
|
|
147
|
+
let current = value;
|
|
148
|
+
while (current && current.t === 'thunk') {
|
|
149
|
+
if (!current.done) {
|
|
150
|
+
if (current.forcing) return free('circular value');
|
|
151
|
+
current.forcing = true;
|
|
152
|
+
current.value = current.compute ? current.compute() : ev(current.node, current.env);
|
|
153
|
+
current.forcing = false;
|
|
154
|
+
current.done = true;
|
|
155
|
+
}
|
|
156
|
+
current = current.value;
|
|
157
|
+
}
|
|
158
|
+
return current;
|
|
159
|
+
};
|
|
160
|
+
const newEnv = (file, extra = {}) => ({ file, vars: new Map(), cache: {}, depth: 0, ...extra });
|
|
161
|
+
|
|
162
|
+
const TRANSPARENT = new Set([ts.SyntaxKind.ParenthesizedExpression, ts.SyntaxKind.AsExpression, ts.SyntaxKind.SatisfiesExpression, ts.SyntaxKind.NonNullExpression, ts.SyntaxKind.TypeAssertionExpression, ts.SyntaxKind.AwaitExpression]);
|
|
163
|
+
const unwrapped = (node) => {
|
|
164
|
+
let current = node;
|
|
165
|
+
while (TRANSPARENT.has(current.kind)) current = current.expression;
|
|
166
|
+
return current;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// ---- conditions: only what literals decide is folded; the rest is recorded and judged at the end ----
|
|
170
|
+
const undecidedRecords = [];
|
|
171
|
+
/** A choice the literals do not decide: both outcomes stay, and it is an error at the end unless they serve the same resolvers. */
|
|
172
|
+
function undecided(conditionNode, alts) {
|
|
173
|
+
undecidedRecords.push({ node: conditionNode, alts });
|
|
174
|
+
return { t: 'alt', alts };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The truthiness of a value: true, false, or null when it depends on something unknown. */
|
|
178
|
+
function truthiness(value) {
|
|
179
|
+
switch (value.t) {
|
|
180
|
+
case 'lit': return Boolean(value.v);
|
|
181
|
+
case 'undef': case 'nul': return false;
|
|
182
|
+
case 'obj': case 'arr': case 'class': case 'fn': case 'dyn': case 'ext': return true;
|
|
183
|
+
case 'alt': {
|
|
184
|
+
const all = value.alts.map((alt) => truthiness(force(alt)));
|
|
185
|
+
return all.every((item) => item === all[0]) ? all[0] : null;
|
|
186
|
+
}
|
|
187
|
+
default: return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Strict/loose equality of two forced values, or null when unknown. */
|
|
192
|
+
function equality(a, b, loose) {
|
|
193
|
+
if (a.t === 'free' || b.t === 'free' || a.t === 'alt' || b.t === 'alt') return null;
|
|
194
|
+
const nullish = (v) => v.t === 'undef' || v.t === 'nul';
|
|
195
|
+
if (nullish(a) && nullish(b)) return loose ? true : a.t === b.t;
|
|
196
|
+
if (nullish(a) || nullish(b)) return ['lit', 'class', 'obj', 'arr'].includes(nullish(a) ? b.t : a.t) ? false : null;
|
|
197
|
+
if (a.t === 'lit' && b.t === 'lit') return loose ? a.v == b.v : a.v === b.v; // eslint-disable-line eqeqeq
|
|
198
|
+
if (a.t === 'class' && b.t === 'class') return a === b;
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The value of a condition expression: true, false, or null when literals do not decide it. */
|
|
203
|
+
function condition(rawNode, env) {
|
|
204
|
+
const node = unwrapped(rawNode);
|
|
205
|
+
const K = ts.SyntaxKind;
|
|
206
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === K.ExclamationToken) {
|
|
207
|
+
const inner = condition(node.operand, env);
|
|
208
|
+
return inner === null ? null : !inner;
|
|
209
|
+
}
|
|
210
|
+
if (ts.isBinaryExpression(node)) {
|
|
211
|
+
const op = node.operatorToken.kind;
|
|
212
|
+
if (op === K.AmpersandAmpersandToken || op === K.BarBarToken) {
|
|
213
|
+
const left = condition(node.left, env);
|
|
214
|
+
const right = condition(node.right, env);
|
|
215
|
+
if (op === K.AmpersandAmpersandToken) return left === false || right === false ? false : left === true && right === true ? true : null;
|
|
216
|
+
return left === true || right === true ? true : left === false && right === false ? false : null;
|
|
217
|
+
}
|
|
218
|
+
if (op === K.EqualsEqualsEqualsToken || op === K.ExclamationEqualsEqualsToken || op === K.EqualsEqualsToken || op === K.ExclamationEqualsToken) {
|
|
219
|
+
const equal = equality(force(thunk(node.left, env)), force(thunk(node.right, env)), op === K.EqualsEqualsToken || op === K.ExclamationEqualsToken);
|
|
220
|
+
return equal === null ? null : op === K.EqualsEqualsEqualsToken || op === K.EqualsEqualsToken ? equal : !equal;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return truthiness(force(thunk(node, env)));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function ev(rawNode, env) {
|
|
227
|
+
const node = unwrapped(rawNode);
|
|
228
|
+
if (env.depth > 60) return free('evaluation too deep');
|
|
229
|
+
if (ts.isIdentifier(node)) return identifier(node.text, node, env);
|
|
230
|
+
if (ts.isArrayLiteralExpression(node)) return { t: 'arr', items: node.elements.map((element) => (ts.isSpreadElement(element) ? { t: 'spread', v: thunk(element.expression, env) } : thunk(element, env))) };
|
|
231
|
+
if (ts.isObjectLiteralExpression(node)) return objectLiteral(node, env);
|
|
232
|
+
if (ts.isConditionalExpression(node)) {
|
|
233
|
+
const decided = condition(node.condition, env);
|
|
234
|
+
if (decided === true) return thunk(node.whenTrue, env);
|
|
235
|
+
if (decided === false) return thunk(node.whenFalse, env);
|
|
236
|
+
return undecided(node.condition, [thunk(node.whenTrue, env), thunk(node.whenFalse, env)]);
|
|
237
|
+
}
|
|
238
|
+
if (ts.isBinaryExpression(node)) {
|
|
239
|
+
const op = node.operatorToken.kind;
|
|
240
|
+
if (op === ts.SyntaxKind.QuestionQuestionToken) {
|
|
241
|
+
const left = force(thunk(node.left, env));
|
|
242
|
+
if (left.t === 'undef' || left.t === 'nul') return thunk(node.right, env);
|
|
243
|
+
return left.t === 'free' ? undecided(node.left, [left, thunk(node.right, env)]) : left;
|
|
244
|
+
}
|
|
245
|
+
if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
246
|
+
const decided = condition(node.left, env);
|
|
247
|
+
const orElse = op === ts.SyntaxKind.BarBarToken;
|
|
248
|
+
if (decided === null) return undecided(node.left, [thunk(node.right, env), orElse ? thunk(node.left, env) : undef]);
|
|
249
|
+
return decided === orElse ? thunk(node.left, env) : thunk(node.right, env);
|
|
250
|
+
}
|
|
251
|
+
if (op === ts.SyntaxKind.EqualsToken) return thunk(node.right, env);
|
|
252
|
+
return free('binary expression');
|
|
253
|
+
}
|
|
254
|
+
if (ts.isPropertyAccessExpression(node)) return member(thunk(node.expression, env), node.name.text);
|
|
255
|
+
if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) return member(thunk(node.expression, env), node.argumentExpression.text);
|
|
256
|
+
if (ts.isCallExpression(node)) return call(node, env);
|
|
257
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return { t: 'fn', node, env };
|
|
258
|
+
if (ts.isStringLiteralLike(node)) return { t: 'lit', v: node.text };
|
|
259
|
+
if (ts.isNumericLiteral(node)) return { t: 'lit', v: Number(node.text) };
|
|
260
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return { t: 'lit', v: node.kind === ts.SyntaxKind.TrueKeyword };
|
|
261
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return nul;
|
|
262
|
+
return free(`unmodelled expression ${ts.SyntaxKind[node.kind]}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function identifier(name, node, env) {
|
|
266
|
+
if (env.vars.has(name)) return env.vars.get(name);
|
|
267
|
+
if (name === 'undefined') return undef;
|
|
268
|
+
const found = resolveName(env.file, name);
|
|
269
|
+
if (!found) return free(`unresolved name ${name}`);
|
|
270
|
+
return declared(found);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function declared(found) {
|
|
274
|
+
if (found.ext) return { t: 'ext', spec: found.ext, name: found.name };
|
|
275
|
+
if (found.opaque || found.kind === 'opaque') return free('opaque declaration');
|
|
276
|
+
if (found.kind === 'class') return classRef(found);
|
|
277
|
+
const env = newEnv(found.file);
|
|
278
|
+
if (found.kind === 'function') return { t: 'fn', node: found.node, env };
|
|
279
|
+
if (found.kind === 'expression') return thunk(found.node, env);
|
|
280
|
+
const initializer = found.node.initializer;
|
|
281
|
+
return initializer ? thunk(initializer, newEnv(found.file, { cache: found.node })) : free('declaration without a value');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function objectLiteral(node, env) {
|
|
285
|
+
const props = new Map();
|
|
286
|
+
let open = false;
|
|
287
|
+
for (const property of node.properties) {
|
|
288
|
+
if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name))) props.set(property.name.text, thunk(property.initializer, env));
|
|
289
|
+
else if (ts.isShorthandPropertyAssignment(property)) props.set(property.name.text, thunk(property.name, env));
|
|
290
|
+
else if (ts.isSpreadAssignment(property)) {
|
|
291
|
+
const spread = force(thunk(property.expression, env));
|
|
292
|
+
if (spread.t === 'obj') {
|
|
293
|
+
for (const [key, value] of spread.props) props.set(key, value);
|
|
294
|
+
open = open || spread.open;
|
|
295
|
+
} else if (spread.t === 'dyn') {
|
|
296
|
+
props.set('module', spread.module);
|
|
297
|
+
props.set('imports', { t: 'arr', items: spread.imports });
|
|
298
|
+
props.set('providers', { t: 'arr', items: spread.providers });
|
|
299
|
+
} else open = true;
|
|
300
|
+
} else if (ts.isMethodDeclaration(property) && ts.isIdentifier(property.name)) props.set(property.name.text, { t: 'fn', node: property, env });
|
|
301
|
+
else open = true;
|
|
302
|
+
}
|
|
303
|
+
return { t: 'obj', props, open };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function member(baseThunk, key) {
|
|
307
|
+
return lazy(() => {
|
|
308
|
+
const base = force(baseThunk);
|
|
309
|
+
switch (base.t) {
|
|
310
|
+
case 'obj':
|
|
311
|
+
return base.props.get(key) ?? (base.open ? free(`property ${key} of an open object`) : undef);
|
|
312
|
+
case 'dyn':
|
|
313
|
+
return key === 'module' ? base.module : key === 'imports' || key === 'providers' ? { t: 'arr', items: base[key] } : undef;
|
|
314
|
+
case 'alt':
|
|
315
|
+
return { t: 'alt', alts: base.alts.map((alt) => member(alt, key)) };
|
|
316
|
+
case 'class': {
|
|
317
|
+
const property = base.node.members.find((item) => ts.isPropertyDeclaration(item) && item.name && ts.isIdentifier(item.name) && item.name.text === key && item.initializer);
|
|
318
|
+
return property ? thunk(property.initializer, newEnv(base.file, { cache: property })) : free(`static ${key} of ${base.name}`);
|
|
319
|
+
}
|
|
320
|
+
case 'undef':
|
|
321
|
+
case 'nul':
|
|
322
|
+
return undef;
|
|
323
|
+
default:
|
|
324
|
+
return free(`property ${key} of ${base.t}`);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---- calls ----
|
|
330
|
+
function call(node, env) {
|
|
331
|
+
const callee = unwrapped(node.expression);
|
|
332
|
+
const args = node.arguments.map((argument) => thunk(argument, env));
|
|
333
|
+
if (callee.kind === ts.SyntaxKind.SuperKeyword) return free('super call');
|
|
334
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
335
|
+
if (callee.expression.kind === ts.SyntaxKind.SuperKeyword) return superCall(env);
|
|
336
|
+
const base = force(thunk(callee.expression, env));
|
|
337
|
+
return methodCall(base, callee.name.text, args, node);
|
|
338
|
+
}
|
|
339
|
+
const target = force(thunk(callee, env));
|
|
340
|
+
if (target.t === 'ext' && target.spec === NEST_COMMON && target.name === 'forwardRef') return args[0] ? lazy(() => callFunction(force(args[0]), [])) : free('forwardRef without a value');
|
|
341
|
+
if (target.t === 'fn') return callFunction(target, args);
|
|
342
|
+
return free(`call of ${target.t}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** `super.register(...)` of a class built on Nest's ConfigurableModuleBuilder: the class itself, no user additions. */
|
|
346
|
+
const superCall = (env) => ({ t: 'dyn', module: env.thisClass ?? free('super outside a class'), imports: [], providers: [], args: [] });
|
|
347
|
+
|
|
348
|
+
/** Array methods over a list whose items are known (`arr`) or unknown (`free`: one representative item). */
|
|
349
|
+
function arrayMethod(base, name, args) {
|
|
350
|
+
const items = base.t === 'arr' ? base.items : [free('an item of a list from the options of a deployment')];
|
|
351
|
+
if (name === 'map' || name === 'flatMap') {
|
|
352
|
+
const fn = args[0] ? force(args[0]) : null;
|
|
353
|
+
if (fn?.t !== 'fn') return free(`${name} without a function`);
|
|
354
|
+
const mapped = { t: 'arr', items: items.map((item) => callFunction(fn, [item, undef])) };
|
|
355
|
+
return base.t === 'arr' ? mapped : undecided(fn.node, [mapped, { t: 'arr', items: [] }]);
|
|
356
|
+
}
|
|
357
|
+
if (name === 'filter' || name === 'slice' || name === 'sort' || name === 'reverse') return base;
|
|
358
|
+
if (name === 'concat') return { t: 'arr', items: [{ t: 'spread', v: base }, ...args.map((argument) => ({ t: 'spread', v: argument }))] };
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function methodCall(base, name, args, node) {
|
|
363
|
+
if ((base.t === 'arr' || base.t === 'free') && ['map', 'flatMap', 'filter', 'slice', 'sort', 'reverse', 'concat'].includes(name)) {
|
|
364
|
+
const result = arrayMethod(base, name, args);
|
|
365
|
+
if (result) return result;
|
|
366
|
+
}
|
|
367
|
+
if (base.t === 'alt') return { t: 'alt', alts: base.alts.map((alt) => lazy(() => methodCall(force(alt), name, args, node))) };
|
|
368
|
+
if (base.t === 'ext') return { t: 'dyn', module: base, imports: listOf(args[0], 'imports'), providers: [], args, ext: name };
|
|
369
|
+
if (base.t === 'class') {
|
|
370
|
+
const found = staticMethod(base, name);
|
|
371
|
+
if (found) return callFunction({ t: 'fn', node: found.method, env: newEnv(found.owner.file, { thisClass: base }) }, args);
|
|
372
|
+
return { t: 'dyn', module: base, imports: [], providers: [], args };
|
|
373
|
+
}
|
|
374
|
+
return free(`method ${name} of ${base.t} at ${where(node)}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The static method of a class or of its in-repository base classes; null when none (the base is external). */
|
|
378
|
+
function staticMethod(cls, name) {
|
|
379
|
+
const method = cls.node.members.find((item) => ts.isMethodDeclaration(item) && item.name && ts.isIdentifier(item.name) && item.name.text === name && (ts.getSyntacticModifierFlags(item) & ts.ModifierFlags.Static) !== 0 && item.body);
|
|
380
|
+
if (method) return { method, owner: cls };
|
|
381
|
+
const heritage = cls.node.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types[0];
|
|
382
|
+
if (!heritage) return null;
|
|
383
|
+
const base = force(thunk(heritage.expression, newEnv(cls.file)));
|
|
384
|
+
return base.t === 'class' ? staticMethod(base, name) : null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The `imports` array a call's first argument carries (forRootAsync-style options), as a list of thunks. */
|
|
388
|
+
function listOf(argument, key) {
|
|
389
|
+
if (!argument) return [];
|
|
390
|
+
const options = force(argument);
|
|
391
|
+
if (options.t !== 'obj' || !options.props.has(key)) return [];
|
|
392
|
+
return [options.props.get(key)];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function callFunction(fn, args) {
|
|
396
|
+
if (fn.t !== 'fn') return free(`call of ${fn.t}`);
|
|
397
|
+
const env = newEnv(fn.env.file, { ...fn.env, vars: new Map(fn.env.vars), depth: fn.env.depth + 1, cache: {} });
|
|
398
|
+
fn.node.parameters.forEach((parameter, index) => bindParameter(parameter.name, args[index] ?? undef, env));
|
|
399
|
+
const body = fn.node.body;
|
|
400
|
+
if (!body) return free('function without a body');
|
|
401
|
+
if (!ts.isBlock(body)) return thunk(body, env);
|
|
402
|
+
const outcome = runStatements(body.statements, env);
|
|
403
|
+
return outcome.returned ? outcome.value : undef;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function bindParameter(name, value, env) {
|
|
407
|
+
if (ts.isIdentifier(name)) env.vars.set(name.text, value);
|
|
408
|
+
else if (ts.isObjectBindingPattern(name)) {
|
|
409
|
+
for (const element of name.elements) {
|
|
410
|
+
const key = (element.propertyName ?? element.name).text;
|
|
411
|
+
if (ts.isIdentifier(element.name)) env.vars.set(element.name.text, member(value, key));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** True when a statement returns or changes a list a later `return` may hand back: what an undecided branch must not hide. */
|
|
417
|
+
function hasEffect(node) {
|
|
418
|
+
let found = false;
|
|
419
|
+
const visit = (child) => {
|
|
420
|
+
if (found) return;
|
|
421
|
+
if (ts.isReturnStatement(child) || (ts.isBinaryExpression(child) && child.operatorToken.kind === ts.SyntaxKind.EqualsToken)) found = true;
|
|
422
|
+
else if (ts.isCallExpression(child) && ts.isPropertyAccessExpression(child.expression) && ['push', 'unshift', 'splice'].includes(child.expression.name.text)) found = true;
|
|
423
|
+
else if (!ts.isFunctionLike(child)) ts.forEachChild(child, visit);
|
|
424
|
+
};
|
|
425
|
+
visit(node);
|
|
426
|
+
return found;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const SILENT_STATEMENTS = new Set([ts.SyntaxKind.EmptyStatement, ts.SyntaxKind.ThrowStatement, ts.SyntaxKind.FunctionDeclaration, ts.SyntaxKind.ClassDeclaration, ts.SyntaxKind.InterfaceDeclaration, ts.SyntaxKind.TypeAliasDeclaration]);
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Runs statements in order against an environment: declarations bind lazily, `if` follows the branch its condition decides
|
|
433
|
+
* (an undecidable condition over statements that return or change a list is an error), `push` appends to a local list and
|
|
434
|
+
* `return` ends the run. Answers `{ returned, value }`. A loop, switch or try that returns or changes a list is an error.
|
|
435
|
+
*/
|
|
436
|
+
function runStatements(statements, env) {
|
|
437
|
+
for (const statement of statements) {
|
|
438
|
+
if (ts.isVariableStatement(statement)) {
|
|
439
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
440
|
+
if (declaration.initializer) bindParameter(declaration.name, thunk(declaration.initializer, env), env);
|
|
441
|
+
}
|
|
442
|
+
} else if (ts.isReturnStatement(statement)) return { returned: true, value: statement.expression ? thunk(statement.expression, env) : undef };
|
|
443
|
+
else if (ts.isIfStatement(statement)) {
|
|
444
|
+
const decided = condition(statement.expression, env);
|
|
445
|
+
if (decided === null && hasEffect(statement)) throw new Error(`${where(statement)}: the condition ${statement.expression.getText()} cannot be decided from literals and a branch returns or adds providers; the schema is not known`);
|
|
446
|
+
const branch = decided === false ? statement.elseStatement : decided === true ? statement.thenStatement : null;
|
|
447
|
+
if (branch) {
|
|
448
|
+
const outcome = runStatements(ts.isBlock(branch) ? branch.statements : [branch], env);
|
|
449
|
+
if (outcome.returned) return outcome;
|
|
450
|
+
}
|
|
451
|
+
} else if (ts.isBlock(statement)) {
|
|
452
|
+
const outcome = runStatements(statement.statements, env);
|
|
453
|
+
if (outcome.returned) return outcome;
|
|
454
|
+
} else if (ts.isExpressionStatement(statement)) pushStatement(statement, env);
|
|
455
|
+
else if (!SILENT_STATEMENTS.has(statement.kind) && hasEffect(statement)) throw new Error(`${where(statement)}: a ${ts.SyntaxKind[statement.kind]} that returns or changes a list is not modeled by this reader`);
|
|
456
|
+
}
|
|
457
|
+
return { returned: false, value: undef };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** `list.push(...)` on a local list and `name = value` on a local name; any other expression statement has no effect here. */
|
|
461
|
+
function pushStatement(statement, env) {
|
|
462
|
+
const expression = unwrapped(statement.expression);
|
|
463
|
+
if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(expression.left) && env.vars.has(expression.left.text)) {
|
|
464
|
+
env.vars.set(expression.left.text, thunk(expression.right, env));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!ts.isCallExpression(expression) || !ts.isPropertyAccessExpression(expression.expression)) return;
|
|
468
|
+
const name = expression.expression.name.text;
|
|
469
|
+
if (name !== 'push' && name !== 'unshift') return;
|
|
470
|
+
const list = force(thunk(expression.expression.expression, env));
|
|
471
|
+
if (list.t !== 'arr') throw new Error(`${where(statement)}: ${name} on something that is not a local list`);
|
|
472
|
+
const added = expression.arguments.map((argument) => (ts.isSpreadElement(argument) ? { t: 'spread', v: thunk(argument.expression, env) } : thunk(argument, env)));
|
|
473
|
+
if (name === 'push') list.items.push(...added); else list.items.unshift(...added);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ---- modules ----
|
|
477
|
+
const decoratorsOf = (node) => (ts.canHaveDecorators?.(node) ? ts.getDecorators(node) ?? [] : node.decorators ?? []);
|
|
478
|
+
|
|
479
|
+
/** The `@Module({...})` metadata object of a class (an obj value), or null. */
|
|
480
|
+
function moduleMetadata(cls) {
|
|
481
|
+
for (const decorator of decoratorsOf(cls.node)) {
|
|
482
|
+
const expression = decorator.expression;
|
|
483
|
+
if (!ts.isCallExpression(expression)) continue;
|
|
484
|
+
const callee = force(thunk(expression.expression, newEnv(cls.file)));
|
|
485
|
+
if (callee.t === 'ext' && callee.spec === NEST_COMMON && callee.name === 'Module') {
|
|
486
|
+
const argument = expression.arguments[0];
|
|
487
|
+
const metadata = argument ? force(thunk(argument, newEnv(cls.file, { thisClass: cls }))) : { t: 'obj', props: new Map(), open: false };
|
|
488
|
+
if (metadata.t !== 'obj' || metadata.open) throw new Error(`${where(expression)}: the @Module metadata of ${cls.name} cannot be decided`);
|
|
489
|
+
return metadata;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Flattens a value into its module/provider entries: arrays, spreads and both branches of a conditional. */
|
|
496
|
+
function entries(value, out = []) {
|
|
497
|
+
const v = force(value);
|
|
498
|
+
if (v.t === 'arr') for (const item of v.items) entries(item.t === 'spread' ? item.v : item, out);
|
|
499
|
+
else if (v.t === 'alt') for (const alt of v.alts) entries(alt, out);
|
|
500
|
+
else if (v.t !== 'undef' && v.t !== 'nul' && !(v.t === 'lit' && !v.v)) out.push(v);
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const isGraphqlServer = (module) => module.t === 'ext' && module.spec === NEST_GRAPHQL && module.name === 'GraphQLModule';
|
|
505
|
+
|
|
506
|
+
/** A module entry as `{ module, dyn }`: a class, an external module, a dynamic module, or a `{ module, ... }` literal. */
|
|
507
|
+
function normalize(value, origin) {
|
|
508
|
+
if (value.t === 'class' || value.t === 'ext') return { module: value, dyn: null, key: value.t === 'class' ? value : `${value.spec}#${value.name}` };
|
|
509
|
+
if (value.t === 'dyn') return { module: force(value.module), dyn: value, key: value };
|
|
510
|
+
if (value.t === 'obj' && value.props.has('module')) {
|
|
511
|
+
if (value.open && !(value.props.has('imports') && value.props.has('providers'))) throw new Error(`${origin}: a module object with an unknown spread may add imports or providers`);
|
|
512
|
+
const dyn = { t: 'dyn', module: value.props.get('module'), imports: value.props.has('imports') ? [value.props.get('imports')] : [], providers: value.props.has('providers') ? [value.props.get('providers')] : [], args: [] };
|
|
513
|
+
return { module: force(dyn.module), dyn, key: value };
|
|
514
|
+
}
|
|
515
|
+
throw new Error(`${origin}: an import cannot be decided (${value.why ?? value.t})`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Walks the module graph from a root entry. Answers the module instances `{ module, providers, imports, server }` in
|
|
520
|
+
* discovery order; `imports` are instances of the same list. A static class is one instance; every dynamic module value is
|
|
521
|
+
* its own instance (Nest keys them by their metadata).
|
|
522
|
+
*/
|
|
523
|
+
function walk(root) {
|
|
524
|
+
const order = [];
|
|
525
|
+
const seen = new Map();
|
|
526
|
+
function visit(entry, origin) {
|
|
527
|
+
const nodes = [];
|
|
528
|
+
for (const value of entries(entry)) {
|
|
529
|
+
const { module, dyn, key } = normalize(value, origin);
|
|
530
|
+
if (module.t === 'free') throw new Error(`${origin}: a dynamic module's class cannot be decided (${module.why})`);
|
|
531
|
+
if (seen.has(key)) {
|
|
532
|
+
nodes.push(seen.get(key));
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const node = { module, dynamic: dyn, providers: [], imports: [], server: null };
|
|
536
|
+
seen.set(key, node);
|
|
537
|
+
order.push(node);
|
|
538
|
+
nodes.push(node);
|
|
539
|
+
const children = [];
|
|
540
|
+
const statics = module.t === 'class' ? moduleMetadata(module) : null;
|
|
541
|
+
if (statics?.props.has('imports')) children.push(statics.props.get('imports'));
|
|
542
|
+
if (statics?.props.has('providers')) node.providers.push(statics.props.get('providers'));
|
|
543
|
+
if (dyn) {
|
|
544
|
+
children.push(...dyn.imports);
|
|
545
|
+
node.providers.push(...dyn.providers);
|
|
546
|
+
if (isGraphqlServer(module)) node.server = dyn;
|
|
547
|
+
}
|
|
548
|
+
const name = module.t === 'class' ? `${module.name} (${module.file})` : `${module.name}`;
|
|
549
|
+
for (const child of children) node.imports.push(...visit(child, name));
|
|
550
|
+
}
|
|
551
|
+
return nodes;
|
|
552
|
+
}
|
|
553
|
+
visit(root, 'the application root');
|
|
554
|
+
return order;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** The instances of the modules a GraphQL server reads: all of them, or the `include` whitelist and its transitive imports. */
|
|
558
|
+
function servedNodes(nodes, include) {
|
|
559
|
+
if (include.length === 0) return nodes;
|
|
560
|
+
const served = new Set();
|
|
561
|
+
const pending = nodes.filter((node) => include.includes(node.module));
|
|
562
|
+
while (pending.length) {
|
|
563
|
+
const node = pending.pop();
|
|
564
|
+
if (served.has(node)) continue;
|
|
565
|
+
served.add(node);
|
|
566
|
+
pending.push(...node.imports);
|
|
567
|
+
}
|
|
568
|
+
return nodes.filter((node) => served.has(node));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** The provider classes of instances: plain class providers and `useClass` providers, unique, in order. */
|
|
572
|
+
function providerClasses(nodes) {
|
|
573
|
+
const found = [];
|
|
574
|
+
for (const node of nodes) {
|
|
575
|
+
for (const list of node.providers) {
|
|
576
|
+
for (const provider of entries(list)) {
|
|
577
|
+
const cls = provider.t === 'class' ? provider : provider.t === 'obj' && provider.props.has('useClass') ? force(provider.props.get('useClass')) : null;
|
|
578
|
+
if (provider.t === 'free') throw new Error(`${node.module.name ?? 'a module'}: a provider list cannot be decided (${provider.why})`);
|
|
579
|
+
if (cls && cls.t === 'class' && !found.includes(cls)) found.push(cls);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return found;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** True when a class carries `@Resolver` or `@Scalar` of @nestjs/graphql (by where the decorator was imported from). */
|
|
587
|
+
function graphqlKind(cls) {
|
|
588
|
+
for (const decorator of decoratorsOf(cls.node)) {
|
|
589
|
+
const expression = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
590
|
+
const callee = force(thunk(expression, newEnv(cls.file)));
|
|
591
|
+
if (callee.t === 'ext' && callee.spec === NEST_GRAPHQL && (callee.name === 'Resolver' || callee.name === 'Scalar')) return callee.name;
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** The options of a GraphQL server: its literal, or what the factory of `forRootAsync` returns. */
|
|
597
|
+
function serverOptions(server, origin) {
|
|
598
|
+
const argument = server.args[0];
|
|
599
|
+
if (!argument) return { include: [] };
|
|
600
|
+
let options = force(argument);
|
|
601
|
+
if (options.t === 'obj' && options.props.has('useFactory')) {
|
|
602
|
+
const result = callFunction(force(options.props.get('useFactory')), (options.props.has('inject') ? entries(options.props.get('inject')) : []).map(() => free('injected value')));
|
|
603
|
+
options = force(result);
|
|
604
|
+
}
|
|
605
|
+
const objects = options.t === 'alt' ? options.alts.map(force) : [options];
|
|
606
|
+
const props = new Map();
|
|
607
|
+
for (const object of objects) {
|
|
608
|
+
if (object.t !== 'obj' || object.open) throw new Error(`${origin}: the GraphQL server options cannot be decided (${object.why ?? (object.open ? 'an unknown spread' : object.t)})`);
|
|
609
|
+
for (const [key, value] of object.props) props.set(key, value);
|
|
610
|
+
}
|
|
611
|
+
for (const key of SCHEMA_OPTIONS_NOT_MODELED) {
|
|
612
|
+
if (props.has(key)) throw new Error(`${origin}: GraphQL server option ${key} changes the schema and is not modeled by this reader`);
|
|
613
|
+
}
|
|
614
|
+
const auto = props.has('autoSchemaFile') ? force(props.get('autoSchemaFile')) : undef;
|
|
615
|
+
if (auto.t === 'undef' || (auto.t === 'lit' && auto.v === false)) throw new Error(`${origin}: the GraphQL server is not code-first (no autoSchemaFile)`);
|
|
616
|
+
const include = props.has('include') ? entries(props.get('include')) : [];
|
|
617
|
+
if (include.some((item) => item.t !== 'class')) throw new Error(`${origin}: the GraphQL include list cannot be decided`);
|
|
618
|
+
return { include };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** The keys of the resolver and scalar classes one branch of an undecided choice would serve. */
|
|
622
|
+
function graphqlClassKeys(alt) {
|
|
623
|
+
const keys = new Set();
|
|
624
|
+
for (const value of entries(alt)) {
|
|
625
|
+
if (value.t === 'class' && graphqlKind(value)) keys.add(`${value.file}#${value.name}`);
|
|
626
|
+
const moduleLike = value.t === 'dyn' || (value.t === 'obj' && value.props.has('module')) || (value.t === 'class' && moduleMetadata(value) !== null);
|
|
627
|
+
if (!moduleLike) continue;
|
|
628
|
+
for (const cls of providerClasses(walk(value))) if (graphqlKind(cls)) keys.add(`${cls.file}#${cls.name}`);
|
|
629
|
+
}
|
|
630
|
+
return keys;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Every choice the literals did not decide (a condition on the options of a deployment, an unknown list) must serve the same
|
|
635
|
+
* resolvers whichever way it goes; when the branches differ the schema is not known and that is an error, never a guess.
|
|
636
|
+
*/
|
|
637
|
+
function assertUndecidedHarmless() {
|
|
638
|
+
for (let at = 0; at < undecidedRecords.length; at += 1) {
|
|
639
|
+
const { node, alts } = undecidedRecords[at];
|
|
640
|
+
const [first, ...rest] = alts.map(graphqlClassKeys);
|
|
641
|
+
for (const other of rest) {
|
|
642
|
+
const differing = [...first].filter((key) => !other.has(key)).concat([...other].filter((key) => !first.has(key)));
|
|
643
|
+
if (differing.length) throw new Error(`${where(node)}: ${node.getText()} cannot be decided from literals and its branches serve different resolvers (${differing.map((key) => key.split('#')[1]).join(', ')})`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Composes an app root file. Answers null when the file exports no `AppModule` class or its module graph holds no GraphQL server, else
|
|
650
|
+
* `{ resolvers: [{ file, name }], scalars: [{ file, name }], include: [{ file, name }] }`.
|
|
651
|
+
*/
|
|
652
|
+
function compose(appFile, exportName = 'AppModule') {
|
|
653
|
+
const found = resolveExport(appFile, exportName);
|
|
654
|
+
if (!found || found.kind !== 'class') return null;
|
|
655
|
+
const root = classRef(found);
|
|
656
|
+
const register = staticMethod(root, 'register');
|
|
657
|
+
const rootValue = register ? { t: 'arr', items: [root, callFunction({ t: 'fn', node: register.method, env: newEnv(register.owner.file, { thisClass: root }) }, [free('the options of a deployment')])] } : root;
|
|
658
|
+
const nodes = walk(rootValue);
|
|
659
|
+
const servers = nodes.filter((node) => node.server);
|
|
660
|
+
if (servers.length === 0) return null;
|
|
661
|
+
if (servers.length > 1) throw new Error(`${appFile} composes ${servers.length} GraphQL servers; a contract snapshot describes exactly one`);
|
|
662
|
+
const { include } = serverOptions(servers[0].server, `${exportName} of ${appFile}`);
|
|
663
|
+
const classesOf = providerClasses(servedNodes(nodes, include));
|
|
664
|
+
assertUndecidedHarmless();
|
|
665
|
+
const pick = (kind) => classesOf.filter((cls) => graphqlKind(cls) === kind).map((cls) => ({ file: cls.file, name: cls.name }));
|
|
666
|
+
return { resolvers: pick('Resolver'), scalars: pick('Scalar'), include: include.map((cls) => ({ file: cls.file, name: cls.name })) };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return { compose };
|
|
670
|
+
}
|