@ankhorage/doctor 0.14.11 → 0.14.13

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.
@@ -1,22 +1,35 @@
1
1
  import { promises as fs } from 'node:fs';
2
- import path from 'node:path';
3
2
 
4
3
  import * as ts from 'typescript';
5
4
 
6
- import { resolveContractsCatalogImportAsync } from './capabilityCatalogContractsImport.js';
7
-
8
- type StaticValue = string | number | boolean | null | readonly StaticValue[] | StaticRecord;
9
- interface StaticRecord {
10
- readonly [key: string]: StaticValue;
11
- }
5
+ import {
6
+ findImportBinding,
7
+ findStaticDeclaration,
8
+ hasExportModifier,
9
+ isConstDeclaration,
10
+ unwrapExpression,
11
+ } from './capabilityCatalogAstSource.js';
12
+ import { resolveImportPathAsync, resolveLocalImportPathAsync } from './capabilityCatalogImport.js';
13
+ import {
14
+ isStaticArray,
15
+ isStaticRecord,
16
+ readStaticArray,
17
+ readStaticProperty,
18
+ type StaticRecord,
19
+ type StaticValue,
20
+ } from './capabilityCatalogStaticValue.js';
12
21
 
13
22
  /*** Evaluate one named catalog export from the supported static TypeScript subset. */
14
23
  export async function capabilityCatalogAstAsync(
15
24
  packageRoot: string,
16
25
  modulePath: string,
17
26
  exportName: string,
27
+ materializedCatalog?: unknown,
18
28
  ): Promise<unknown> {
19
- return new StaticCatalogResolver(packageRoot).resolveAsync(modulePath, exportName);
29
+ return new StaticCatalogResolver(packageRoot, materializedCatalog).resolveAsync(
30
+ modulePath,
31
+ exportName,
32
+ );
20
33
  }
21
34
 
22
35
  /*** Resolve static local imports and expressions without loading target modules. */
@@ -24,7 +37,14 @@ class StaticCatalogResolver {
24
37
  readonly #symbols = new Map<string, StaticValue>();
25
38
  readonly #activeSymbols: string[] = [];
26
39
 
27
- constructor(private readonly packageRoot: string) {}
40
+ private readonly materializedCatalog: readonly StaticValue[] | null;
41
+
42
+ constructor(
43
+ private readonly packageRoot: string,
44
+ materializedCatalog?: unknown,
45
+ ) {
46
+ this.materializedCatalog = readStaticArray(materializedCatalog);
47
+ }
28
48
 
29
49
  /*** Resolve a named local export without importing or running its module. */
30
50
  async resolveAsync(
@@ -98,6 +118,7 @@ class StaticCatalogResolver {
98
118
  if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
99
119
  if (node.kind === ts.SyntaxKind.NullKeyword) return null;
100
120
  if (ts.isIdentifier(node)) return environment.resolveAsync(node.text);
121
+ if (ts.isPropertyAccessExpression(node)) return this.propertyAccessAsync(node, environment);
101
122
  if (ts.isArrayLiteralExpression(node)) return this.arrayAsync(node, environment);
102
123
  if (ts.isObjectLiteralExpression(node)) return this.objectAsync(node, environment);
103
124
  if (ts.isTemplateExpression(node)) return this.templateAsync(node, environment);
@@ -105,6 +126,29 @@ class StaticCatalogResolver {
105
126
  throw new Error(`Unsupported static capability expression: ${node.getText()}.`);
106
127
  }
107
128
 
129
+ /*** Evaluate one static object property while preserving dynamic-expression diagnostics. */
130
+ async propertyAccessAsync(
131
+ node: ts.PropertyAccessExpression,
132
+ environment: StaticCatalogScope,
133
+ ): Promise<StaticValue> {
134
+ try {
135
+ return readStaticProperty(
136
+ await this.evaluateAsync(node.expression, environment),
137
+ node.name.text,
138
+ node.getText(),
139
+ );
140
+ } catch (error) {
141
+ if (
142
+ error instanceof Error &&
143
+ error.message.startsWith('Unsupported dynamic capability value:')
144
+ )
145
+ throw new Error(`Unsupported static capability expression: ${node.getText()}.`, {
146
+ cause: error,
147
+ });
148
+ throw error;
149
+ }
150
+ }
151
+
108
152
  /*** Evaluate a static array literal. */
109
153
  arrayAsync(
110
154
  node: ts.ArrayLiteralExpression,
@@ -180,9 +224,21 @@ class StaticCatalogResolver {
180
224
  environment: StaticCatalogScope,
181
225
  ): Promise<readonly StaticValue[]> {
182
226
  const result: StaticValue[] = [];
183
- for (const element of elements) {
184
- if (ts.isSpreadElement(element)) throw new Error('Catalog array spreads are not supported.');
185
- result.push(await this.evaluateAsync(element, environment));
227
+ for (const [index, element] of elements.entries()) {
228
+ if (ts.isSpreadElement(element)) {
229
+ if (await environment.isPackageLocalDerivedCallAsync(element.expression)) {
230
+ if (index !== elements.length - 1 || this.materializedCatalog === null)
231
+ throw new Error(
232
+ 'Derived catalog function spreads must be trailing and have materialized package metadata.',
233
+ );
234
+ result.push(...this.materializedCatalog.slice(result.length));
235
+ continue;
236
+ }
237
+ const spread = await this.evaluateAsync(element.expression, environment);
238
+ if (!isStaticArray(spread))
239
+ throw new Error('Catalog array spreads must resolve to static arrays.');
240
+ result.push(...spread);
241
+ } else result.push(await this.evaluateAsync(element, environment));
186
242
  }
187
243
  return result;
188
244
  }
@@ -233,6 +289,30 @@ class StaticCatalogScope {
233
289
  );
234
290
  }
235
291
 
292
+ /*** Detect one zero-argument function imported from a package-local source module. */
293
+ async isPackageLocalDerivedCallAsync(expression: ts.Expression): Promise<boolean> {
294
+ const node = unwrapExpression(expression);
295
+ if (
296
+ !ts.isCallExpression(node) ||
297
+ node.arguments.length !== 0 ||
298
+ !ts.isIdentifier(node.expression)
299
+ )
300
+ return false;
301
+ const imported = findImportBinding(this.source, node.expression.text);
302
+ if (
303
+ imported === undefined ||
304
+ !ts.isStringLiteral(imported.moduleSpecifier) ||
305
+ !imported.moduleSpecifier.text.startsWith('.')
306
+ )
307
+ return false;
308
+ await resolveLocalImportPathAsync(
309
+ this.packageRoot,
310
+ this.modulePath,
311
+ imported.moduleSpecifier.text,
312
+ );
313
+ return true;
314
+ }
315
+
236
316
  /*** Add a static callback parameter without evaluating unrelated module symbols. */
237
317
  withValue(name: string, value: StaticValue): StaticCatalogScope {
238
318
  return new StaticCatalogScope(
@@ -244,107 +324,3 @@ class StaticCatalogScope {
244
324
  );
245
325
  }
246
326
  }
247
-
248
- /*** Find the static top-level declaration that owns a local identifier. */
249
- function findStaticDeclaration(
250
- source: ts.SourceFile,
251
- name: string,
252
- ): ts.VariableDeclaration | undefined {
253
- return source.statements
254
- .filter((statement): statement is ts.VariableStatement => ts.isVariableStatement(statement))
255
- .filter((statement) => isConstDeclaration(statement.declarationList))
256
- .flatMap((statement) => statement.declarationList.declarations)
257
- .find((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === name);
258
- }
259
-
260
- /*** Find the import declaration that introduces a referenced local identifier. */
261
- function findImportBinding(source: ts.SourceFile, name: string): ts.ImportDeclaration | undefined {
262
- return source.statements
263
- .filter((statement): statement is ts.ImportDeclaration => ts.isImportDeclaration(statement))
264
- .find((statement) => {
265
- const clause = statement.importClause;
266
- return (
267
- clause?.name?.text === name ||
268
- (clause?.namedBindings !== undefined &&
269
- ts.isNamedImports(clause.namedBindings) &&
270
- clause.namedBindings.elements.some((element) => element.name.text === name))
271
- );
272
- });
273
- }
274
-
275
- /*** Determine whether a declaration list is immutable static catalog input. */
276
- function isConstDeclaration(list: ts.VariableDeclarationList): boolean {
277
- return (list.flags & ts.NodeFlags.Const) !== 0;
278
- }
279
-
280
- interface ResolvedStaticImport {
281
- readonly packageRoot: string;
282
- readonly path: string;
283
- }
284
-
285
- /*** Resolve an allowed static import without executing its target package. */
286
- async function resolveImportPathAsync(
287
- packageRoot: string,
288
- fromPath: string,
289
- specifier: string,
290
- ): Promise<ResolvedStaticImport> {
291
- if (specifier.startsWith('.'))
292
- return resolveLocalImportPathAsync(packageRoot, fromPath, specifier);
293
- return resolveContractsCatalogImportAsync(packageRoot, fromPath, specifier);
294
- }
295
-
296
- /*** Resolve a relative static import that remains within its owning package. */
297
- async function resolveLocalImportPathAsync(
298
- packageRoot: string,
299
- fromPath: string,
300
- specifier: string,
301
- ): Promise<ResolvedStaticImport> {
302
- const candidate = path.resolve(path.dirname(fromPath), specifier);
303
- if (!isInsidePackage(packageRoot, candidate) && candidate !== packageRoot)
304
- throw new Error('Capability catalogs may not import outside their package.');
305
- for (const extension of ['', '.ts', '.tsx', '.js', '.mjs', '/index.ts'])
306
- if (await isFileAsync(`${candidate}${extension}`))
307
- return { packageRoot, path: `${candidate}${extension}` };
308
- throw new Error(`Unable to resolve static capability import ${specifier}.`);
309
- }
310
-
311
- /*** Unwrap TypeScript syntax which does not alter a runtime value. */
312
- function unwrapExpression(expression: ts.Expression): ts.Expression {
313
- return ts.isAsExpression(expression) ||
314
- ts.isSatisfiesExpression(expression) ||
315
- ts.isParenthesizedExpression(expression)
316
- ? unwrapExpression(expression.expression)
317
- : expression;
318
- }
319
-
320
- /*** Narrow a static value to a descriptor-shaped record. */
321
- function isStaticRecord(value: StaticValue): value is StaticRecord {
322
- return typeof value === 'object' && value !== null && !Array.isArray(value);
323
- }
324
-
325
- /*** Narrow a static value to an array. */
326
- function isStaticArray(value: StaticValue): value is readonly StaticValue[] {
327
- return Array.isArray(value);
328
- }
329
-
330
- /*** Detect an exported variable statement. */
331
- function hasExportModifier(statement: ts.VariableStatement): boolean {
332
- return (
333
- statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false
334
- );
335
- }
336
-
337
- /*** Check whether a resolved path remains within the inspected package root. */
338
- function isInsidePackage(packageRoot: string, candidate: string): boolean {
339
- const relativePath = path.relative(packageRoot, candidate);
340
- return relativePath !== '' && !relativePath.startsWith(`..${path.sep}`) && relativePath !== '..';
341
- }
342
-
343
- /*** Check whether one candidate is a regular file. */
344
- async function isFileAsync(candidate: string): Promise<boolean> {
345
- try {
346
- return (await fs.stat(candidate)).isFile();
347
- } catch {
348
- return false;
349
- }
350
- }
@@ -0,0 +1,52 @@
1
+ import * as ts from 'typescript';
2
+
3
+ /*** Find the static top-level declaration that owns a local identifier. */
4
+ export function findStaticDeclaration(
5
+ source: ts.SourceFile,
6
+ name: string,
7
+ ): ts.VariableDeclaration | undefined {
8
+ return source.statements
9
+ .filter((statement): statement is ts.VariableStatement => ts.isVariableStatement(statement))
10
+ .filter((statement) => isConstDeclaration(statement.declarationList))
11
+ .flatMap((statement) => statement.declarationList.declarations)
12
+ .find((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === name);
13
+ }
14
+
15
+ /*** Find the import declaration that introduces a referenced local identifier. */
16
+ export function findImportBinding(
17
+ source: ts.SourceFile,
18
+ name: string,
19
+ ): ts.ImportDeclaration | undefined {
20
+ return source.statements
21
+ .filter((statement): statement is ts.ImportDeclaration => ts.isImportDeclaration(statement))
22
+ .find((statement) => {
23
+ const clause = statement.importClause;
24
+ return (
25
+ clause?.name?.text === name ||
26
+ (clause?.namedBindings !== undefined &&
27
+ ts.isNamedImports(clause.namedBindings) &&
28
+ clause.namedBindings.elements.some((element) => element.name.text === name))
29
+ );
30
+ });
31
+ }
32
+
33
+ /*** Determine whether a declaration list is immutable static catalog input. */
34
+ export function isConstDeclaration(list: ts.VariableDeclarationList): boolean {
35
+ return (list.flags & ts.NodeFlags.Const) !== 0;
36
+ }
37
+
38
+ /*** Unwrap TypeScript syntax which does not alter a runtime value. */
39
+ export function unwrapExpression(expression: ts.Expression): ts.Expression {
40
+ return ts.isAsExpression(expression) ||
41
+ ts.isSatisfiesExpression(expression) ||
42
+ ts.isParenthesizedExpression(expression)
43
+ ? unwrapExpression(expression.expression)
44
+ : expression;
45
+ }
46
+
47
+ /*** Detect an exported variable statement. */
48
+ export function hasExportModifier(statement: ts.VariableStatement): boolean {
49
+ return (
50
+ statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false
51
+ );
52
+ }
@@ -0,0 +1,69 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { resolveContractsCatalogImportAsync } from './capabilityCatalogContractsImport.js';
5
+
6
+ export interface ResolvedStaticImport {
7
+ readonly packageRoot: string;
8
+ readonly path: string;
9
+ }
10
+
11
+ /*** Resolve an allowed static import without executing its target package. */
12
+ export async function resolveImportPathAsync(
13
+ packageRoot: string,
14
+ fromPath: string,
15
+ specifier: string,
16
+ ): Promise<ResolvedStaticImport> {
17
+ if (specifier.startsWith('.'))
18
+ return resolveLocalImportPathAsync(packageRoot, fromPath, specifier);
19
+ return resolveContractsCatalogImportAsync(packageRoot, fromPath, specifier);
20
+ }
21
+
22
+ /*** Resolve a relative static import that remains within its owning package. */
23
+ export async function resolveLocalImportPathAsync(
24
+ packageRoot: string,
25
+ fromPath: string,
26
+ specifier: string,
27
+ ): Promise<ResolvedStaticImport> {
28
+ const candidate = path.resolve(path.dirname(fromPath), specifier);
29
+ if (!isInsidePackage(packageRoot, candidate) && candidate !== packageRoot)
30
+ throw new Error('Capability catalogs may not import outside their package.');
31
+ for (const sourcePath of localSourceCandidates(candidate))
32
+ if (await isFileAsync(sourcePath)) return { packageRoot, path: sourcePath };
33
+ throw new Error(`Unable to resolve static capability import ${specifier}.`);
34
+ }
35
+
36
+ /*** Enumerate source files represented by one package-local ESM import specifier. */
37
+ function localSourceCandidates(candidate: string): readonly string[] {
38
+ const extension = path.extname(candidate);
39
+ if (extension === '.js')
40
+ return [candidate.replace(/\.js$/u, '.ts'), candidate.replace(/\.js$/u, '.tsx'), candidate];
41
+ if (extension === '.mjs') return [candidate.replace(/\.mjs$/u, '.mts'), candidate];
42
+ if (extension === '.cjs') return [candidate.replace(/\.cjs$/u, '.cts'), candidate];
43
+ return [
44
+ candidate,
45
+ `${candidate}.ts`,
46
+ `${candidate}.tsx`,
47
+ `${candidate}.js`,
48
+ `${candidate}.mjs`,
49
+ `${candidate}.cjs`,
50
+ `${candidate}/index.ts`,
51
+ `${candidate}/index.tsx`,
52
+ `${candidate}/index.js`,
53
+ ];
54
+ }
55
+
56
+ /*** Check whether a resolved path remains within the inspected package root. */
57
+ function isInsidePackage(packageRoot: string, candidate: string): boolean {
58
+ const relativePath = path.relative(packageRoot, candidate);
59
+ return relativePath !== '' && !relativePath.startsWith(`..${path.sep}`) && relativePath !== '..';
60
+ }
61
+
62
+ /*** Check whether one candidate is a regular file. */
63
+ async function isFileAsync(candidate: string): Promise<boolean> {
64
+ try {
65
+ return (await fs.stat(candidate)).isFile();
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
@@ -0,0 +1,48 @@
1
+ export type StaticValue = string | number | boolean | null | readonly StaticValue[] | StaticRecord;
2
+
3
+ export interface StaticRecord {
4
+ readonly [key: string]: StaticValue;
5
+ }
6
+
7
+ /*** Narrow a static value to a descriptor-shaped record. */
8
+ export function isStaticRecord(value: StaticValue): value is StaticRecord {
9
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
10
+ }
11
+
12
+ /*** Narrow a static value to an array. */
13
+ export function isStaticArray(value: StaticValue): value is readonly StaticValue[] {
14
+ return Array.isArray(value);
15
+ }
16
+
17
+ /*** Read one property from a statically resolved object. */
18
+ export function readStaticProperty(
19
+ value: StaticValue,
20
+ key: string,
21
+ expression: string,
22
+ ): StaticValue {
23
+ const property = isStaticRecord(value)
24
+ ? Object.entries(value).find(([propertyKey]) => propertyKey === key)?.[1]
25
+ : undefined;
26
+ if (property === undefined)
27
+ throw new Error(`Unsupported static capability property access: ${expression}.`);
28
+ return property;
29
+ }
30
+
31
+ /*** Read one JSON-compatible array as static catalog materialization input. */
32
+ export function readStaticArray(value: unknown): readonly StaticValue[] | null {
33
+ return Array.isArray(value) && value.every(isStaticValue) ? value : null;
34
+ }
35
+
36
+ /*** Narrow untrusted JSON-compatible input to the static evaluator value model. */
37
+ function isStaticValue(value: unknown): value is StaticValue {
38
+ if (
39
+ value === null ||
40
+ typeof value === 'string' ||
41
+ typeof value === 'number' ||
42
+ typeof value === 'boolean'
43
+ )
44
+ return true;
45
+ if (Array.isArray(value)) return value.every(isStaticValue);
46
+ if (typeof value !== 'object') return false;
47
+ return Object.values(value).every(isStaticValue);
48
+ }