@mondaydotcomorg/z2h-cli 0.30.0 → 0.31.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/dist/backend/handlers.d.ts +3 -0
- package/dist/backend/handlers.d.ts.map +1 -1
- package/dist/backend/handlers.js +7 -4
- package/dist/backend/parser/__tests__/ast.test.d.ts.map +1 -0
- package/dist/backend/parser/__tests__/parse.test.d.ts.map +1 -0
- package/dist/backend/parser/api-signatures.d.ts +4 -0
- package/dist/backend/parser/api-signatures.d.ts.map +1 -0
- package/dist/backend/parser/api-signatures.js +31 -0
- package/dist/backend/parser/api-usage.d.ts +5 -0
- package/dist/backend/parser/api-usage.d.ts.map +1 -0
- package/dist/backend/parser/api-usage.js +61 -0
- package/dist/backend/parser/bindings.d.ts +5 -0
- package/dist/backend/parser/bindings.d.ts.map +1 -0
- package/dist/backend/parser/bindings.js +84 -0
- package/dist/backend/parser/handler-declaration.d.ts.map +1 -0
- package/dist/backend/parser/member-path.d.ts +3 -0
- package/dist/backend/parser/member-path.d.ts.map +1 -0
- package/dist/backend/parser/member-path.js +31 -0
- package/dist/backend/parser/messages.d.ts.map +1 -0
- package/dist/backend/parser/parse.d.ts.map +1 -0
- package/dist/backend/{parse.js → parser/parse.js} +9 -9
- package/dist/backend/parser/static-value.d.ts +6 -0
- package/dist/backend/parser/static-value.d.ts.map +1 -0
- package/dist/backend/parser/static-value.js +117 -0
- package/dist/backend/parser/types.d.ts +53 -0
- package/dist/backend/parser/types.d.ts.map +1 -0
- package/dist/backend/parser/types.js +1 -0
- package/dist/backend/parser/utils.d.ts +19 -0
- package/dist/backend/parser/utils.d.ts.map +1 -0
- package/dist/backend/parser/utils.js +97 -0
- package/dist/backend/validate.d.ts +3 -3
- package/dist/backend/validate.d.ts.map +1 -1
- package/dist/backend/validate.js +18 -18
- package/dist/commands/backend.d.ts +2 -13
- package/dist/commands/backend.d.ts.map +1 -1
- package/dist/commands/backend.js +30 -21
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +1 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +2 -8
- package/dist/esm/backend/handlers.d.ts +3 -0
- package/dist/esm/backend/handlers.d.ts.map +1 -1
- package/dist/esm/backend/handlers.mjs +7 -4
- package/dist/esm/backend/parser/__tests__/ast.test.d.ts.map +1 -0
- package/dist/esm/backend/parser/__tests__/parse.test.d.ts.map +1 -0
- package/dist/esm/backend/parser/api-signatures.d.ts +4 -0
- package/dist/esm/backend/parser/api-signatures.d.ts.map +1 -0
- package/dist/esm/backend/parser/api-signatures.mjs +24 -0
- package/dist/esm/backend/parser/api-usage.d.ts +5 -0
- package/dist/esm/backend/parser/api-usage.d.ts.map +1 -0
- package/dist/esm/backend/parser/api-usage.mjs +58 -0
- package/dist/esm/backend/parser/bindings.d.ts +5 -0
- package/dist/esm/backend/parser/bindings.d.ts.map +1 -0
- package/dist/esm/backend/parser/bindings.mjs +81 -0
- package/dist/esm/backend/parser/handler-declaration.d.ts.map +1 -0
- package/dist/esm/backend/parser/member-path.d.ts +3 -0
- package/dist/esm/backend/parser/member-path.d.ts.map +1 -0
- package/dist/esm/backend/parser/member-path.mjs +29 -0
- package/dist/esm/backend/parser/messages.d.ts.map +1 -0
- package/dist/esm/backend/parser/parse.d.ts.map +1 -0
- package/dist/esm/backend/{parse.mjs → parser/parse.mjs} +1 -1
- package/dist/esm/backend/parser/static-value.d.ts +6 -0
- package/dist/esm/backend/parser/static-value.d.ts.map +1 -0
- package/dist/esm/backend/parser/static-value.mjs +114 -0
- package/dist/esm/backend/parser/types.d.ts +53 -0
- package/dist/esm/backend/parser/types.d.ts.map +1 -0
- package/dist/esm/backend/parser/types.mjs +1 -0
- package/dist/esm/backend/parser/utils.d.ts +19 -0
- package/dist/esm/backend/parser/utils.d.ts.map +1 -0
- package/dist/esm/backend/parser/utils.mjs +88 -0
- package/dist/esm/backend/validate.d.ts +3 -3
- package/dist/esm/backend/validate.d.ts.map +1 -1
- package/dist/esm/backend/validate.mjs +8 -8
- package/dist/esm/commands/backend.d.ts +2 -13
- package/dist/esm/commands/backend.d.ts.map +1 -1
- package/dist/esm/commands/backend.mjs +31 -22
- package/dist/esm/commands/create.d.ts.map +1 -1
- package/dist/esm/commands/create.mjs +1 -0
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +3 -8
- package/dist/esm/templates/app/usage.mjs +5 -0
- package/dist/esm/util/machine-identity.d.ts +2 -3
- package/dist/esm/util/machine-identity.d.ts.map +1 -1
- package/dist/esm/util/machine-identity.mjs +7 -4
- package/dist/templates/app/usage.js +7 -0
- package/dist/util/machine-identity.d.ts +2 -3
- package/dist/util/machine-identity.d.ts.map +1 -1
- package/dist/util/machine-identity.js +11 -3
- package/handler-api/signatures.json +26 -0
- package/package.json +1 -1
- package/src/backend/__tests__/handlers.test.ts +13 -2
- package/src/backend/__tests__/validate.test.ts +54 -0
- package/src/backend/handlers.ts +10 -5
- package/src/backend/parser/__tests__/ast.test.ts +252 -0
- package/src/backend/{__tests__ → parser/__tests__}/parse.test.ts +2 -2
- package/src/backend/parser/api-signatures.ts +23 -0
- package/src/backend/parser/api-usage.ts +73 -0
- package/src/backend/parser/bindings.ts +87 -0
- package/src/backend/parser/member-path.ts +31 -0
- package/src/backend/{parse.ts → parser/parse.ts} +1 -1
- package/src/backend/parser/static-value.ts +145 -0
- package/src/backend/parser/types.ts +66 -0
- package/src/backend/parser/utils.ts +106 -0
- package/src/backend/validate.ts +12 -8
- package/src/commands/__tests__/backend-deploy.test.ts +70 -5
- package/src/commands/__tests__/backend-validate.test.ts +11 -1
- package/src/commands/__tests__/create.test.ts +1 -0
- package/src/commands/backend.ts +31 -20
- package/src/commands/create.ts +1 -0
- package/src/commands/deploy.ts +3 -9
- package/src/templates/app/usage.json +1 -0
- package/src/util/machine-identity.ts +7 -3
- package/dist/backend/__tests__/ast.test.d.ts.map +0 -1
- package/dist/backend/__tests__/parse.test.d.ts.map +0 -1
- package/dist/backend/ast.d.ts +0 -39
- package/dist/backend/ast.d.ts.map +0 -1
- package/dist/backend/ast.js +0 -149
- package/dist/backend/handler-declaration.d.ts.map +0 -1
- package/dist/backend/messages.d.ts.map +0 -1
- package/dist/backend/parse.d.ts.map +0 -1
- package/dist/esm/backend/__tests__/ast.test.d.ts.map +0 -1
- package/dist/esm/backend/__tests__/parse.test.d.ts.map +0 -1
- package/dist/esm/backend/ast.d.ts +0 -39
- package/dist/esm/backend/ast.d.ts.map +0 -1
- package/dist/esm/backend/ast.mjs +0 -144
- package/dist/esm/backend/handler-declaration.d.ts.map +0 -1
- package/dist/esm/backend/messages.d.ts.map +0 -1
- package/dist/esm/backend/parse.d.ts.map +0 -1
- package/src/backend/__tests__/ast.test.ts +0 -53
- package/src/backend/ast.ts +0 -201
- /package/dist/backend/{__tests__ → parser/__tests__}/ast.test.d.ts +0 -0
- /package/dist/backend/{__tests__ → parser/__tests__}/parse.test.d.ts +0 -0
- /package/dist/backend/{handler-declaration.d.ts → parser/handler-declaration.d.ts} +0 -0
- /package/dist/backend/{handler-declaration.js → parser/handler-declaration.js} +0 -0
- /package/dist/backend/{messages.d.ts → parser/messages.d.ts} +0 -0
- /package/dist/backend/{messages.js → parser/messages.js} +0 -0
- /package/dist/backend/{parse.d.ts → parser/parse.d.ts} +0 -0
- /package/dist/esm/backend/{__tests__ → parser/__tests__}/ast.test.d.ts +0 -0
- /package/dist/esm/backend/{__tests__ → parser/__tests__}/parse.test.d.ts +0 -0
- /package/dist/esm/backend/{handler-declaration.d.ts → parser/handler-declaration.d.ts} +0 -0
- /package/dist/esm/backend/{handler-declaration.mjs → parser/handler-declaration.mjs} +0 -0
- /package/dist/esm/backend/{messages.d.ts → parser/messages.d.ts} +0 -0
- /package/dist/esm/backend/{messages.mjs → parser/messages.mjs} +0 -0
- /package/dist/esm/backend/{parse.d.ts → parser/parse.d.ts} +0 -0
- /package/src/backend/{handler-declaration.ts → parser/handler-declaration.ts} +0 -0
- /package/src/backend/{messages.ts → parser/messages.ts} +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { AnyNode, Expression, Node, VariableDeclaration } from 'acorn';
|
|
2
|
+
|
|
3
|
+
/** `"<domain>.<method>"` → parameter names by position, as documented for handler authors. */
|
|
4
|
+
export type ApiSignatures = Record<string, readonly string[]>;
|
|
5
|
+
|
|
6
|
+
// ─────────── Bindings ───────────
|
|
7
|
+
|
|
8
|
+
export interface Declaration {
|
|
9
|
+
kind: VariableDeclaration['kind'] | 'param';
|
|
10
|
+
init: Expression | null | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Property path from `init` to this binding: `[]` for `const x = init`,
|
|
13
|
+
* `['a', 'b']` for `const { a: { b: x } } = init`, `undefined` when the value
|
|
14
|
+
* is not a plain property read (array/rest/default patterns).
|
|
15
|
+
*/
|
|
16
|
+
path: string[] | undefined;
|
|
17
|
+
scope: Node;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Bindings {
|
|
21
|
+
declarations: Map<string, Declaration[]>;
|
|
22
|
+
/** Names reassigned or updated after declaration — never followed as a stable alias. */
|
|
23
|
+
unpinnable: Set<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PatternBinding {
|
|
27
|
+
name: string;
|
|
28
|
+
path: string[] | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─────────── Static values ───────────
|
|
32
|
+
|
|
33
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
34
|
+
|
|
35
|
+
/** Wraps a resolved value so "resolved to null" is distinguishable from "unresolved" (`undefined`). */
|
|
36
|
+
export type Resolved<T> = { readonly value: T };
|
|
37
|
+
|
|
38
|
+
// ─────────── Member paths ───────────
|
|
39
|
+
|
|
40
|
+
export interface MemberChain {
|
|
41
|
+
root: string;
|
|
42
|
+
parts: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PathResolver {
|
|
46
|
+
/**
|
|
47
|
+
* Dotted path of a member chain, following `const` aliases back to their
|
|
48
|
+
* source: with `const sf = ctx.api.v1.snowflake`, `sf.query` resolves to
|
|
49
|
+
* `ctx.api.v1.snowflake.query`. Destructuring counts too
|
|
50
|
+
* (`const { snowflake: sf } = ctx.api.v1`). `undefined` if any link is dynamic.
|
|
51
|
+
*
|
|
52
|
+
* Only single, never-reassigned, never-shadowed `const` bindings are followed —
|
|
53
|
+
* anything else is left as written. The handler's first parameter is treated as
|
|
54
|
+
* `ctx` even when renamed (`handler(context)` → `context.api.v1.db` resolves as
|
|
55
|
+
* `ctx.api.v1.db`).
|
|
56
|
+
*/
|
|
57
|
+
pathOf(node: AnyNode): string | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─────────── API usage ───────────
|
|
61
|
+
|
|
62
|
+
/** Undocumented arguments or methods fall back to `arg<index>` keys. */
|
|
63
|
+
export type ApiCall = Record<string, JsonValue>;
|
|
64
|
+
|
|
65
|
+
/** `domain -> method -> calls`, later written into `usage.json` by deploy. */
|
|
66
|
+
export type ApiUsage = Record<string, Record<string, ApiCall[]>>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { AnyNode, Node, Pattern } from 'acorn';
|
|
2
|
+
import type { JsonValue, MemberChain, PatternBinding, Resolved } from './types';
|
|
3
|
+
|
|
4
|
+
// ─────────── Source text ───────────
|
|
5
|
+
|
|
6
|
+
export const sourceTextOf = (node: AnyNode, source: string): string => source.slice(node.start, node.end);
|
|
7
|
+
|
|
8
|
+
export function displayText(value: JsonValue): string {
|
|
9
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveOrSourceText(
|
|
13
|
+
node: AnyNode,
|
|
14
|
+
source: string,
|
|
15
|
+
resolve: (node: AnyNode) => Resolved<JsonValue> | undefined,
|
|
16
|
+
fallbackText: (node: AnyNode) => string = n => sourceTextOf(n, source)
|
|
17
|
+
): JsonValue {
|
|
18
|
+
const resolved = resolve(node);
|
|
19
|
+
return resolved !== undefined ? resolved.value : fallbackText(node);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─────────── Keys and member chains ───────────
|
|
23
|
+
|
|
24
|
+
/** Static property key of `obj.key` / `obj['key']` / `{ key: … }`; `undefined` when computed from a non-literal. */
|
|
25
|
+
export function propertyKey(key: AnyNode, computed: boolean): string | undefined {
|
|
26
|
+
if (key.type === 'Literal' && typeof key.value === 'string') {
|
|
27
|
+
return key.value;
|
|
28
|
+
}
|
|
29
|
+
if (!computed && key.type === 'Identifier') {
|
|
30
|
+
return key.name;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `a.b.c` / `a['b'].c` → root `a`, parts `[b, c]`; `undefined` if any link is dynamic or the root is not an identifier. */
|
|
36
|
+
export function memberChain(node: AnyNode): MemberChain | undefined {
|
|
37
|
+
if (node.type === 'ChainExpression') {
|
|
38
|
+
return memberChain(node.expression);
|
|
39
|
+
}
|
|
40
|
+
const parts: string[] = [];
|
|
41
|
+
let cur: AnyNode = node;
|
|
42
|
+
while (cur.type === 'MemberExpression') {
|
|
43
|
+
const key = propertyKey(cur.property, cur.computed);
|
|
44
|
+
if (key === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
parts.unshift(key);
|
|
48
|
+
cur = cur.object;
|
|
49
|
+
}
|
|
50
|
+
return cur.type === 'Identifier' ? { root: cur.name, parts } : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─────────── Binding patterns ───────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Every identifier a binding pattern introduces, with its property path from the
|
|
57
|
+
* pattern's value. `path` is deliberately not defaulted: callers pass `[]` for
|
|
58
|
+
* the root and `undefined` once the pattern stops being a plain property read.
|
|
59
|
+
*/
|
|
60
|
+
export function patternBindings(pattern: Pattern, path: string[] | undefined): PatternBinding[] {
|
|
61
|
+
switch (pattern.type) {
|
|
62
|
+
case 'Identifier':
|
|
63
|
+
return [{ name: pattern.name, path }];
|
|
64
|
+
case 'ObjectPattern':
|
|
65
|
+
return pattern.properties.flatMap(p => {
|
|
66
|
+
if (p.type === 'RestElement') {
|
|
67
|
+
return patternBindings(p.argument, undefined);
|
|
68
|
+
}
|
|
69
|
+
const key = propertyKey(p.key, p.computed);
|
|
70
|
+
return patternBindings(p.value, path && key !== undefined ? [...path, key] : undefined);
|
|
71
|
+
});
|
|
72
|
+
case 'ArrayPattern':
|
|
73
|
+
return pattern.elements.flatMap(e => (e ? patternBindings(e, undefined) : []));
|
|
74
|
+
case 'AssignmentPattern':
|
|
75
|
+
return patternBindings(pattern.left, undefined);
|
|
76
|
+
case 'RestElement':
|
|
77
|
+
return patternBindings(pattern.argument, undefined);
|
|
78
|
+
default:
|
|
79
|
+
// MemberExpression targets (`obj.x = …`) bind no names.
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const patternNames = (pattern: Pattern): string[] => patternBindings(pattern, []).map(b => b.name);
|
|
85
|
+
|
|
86
|
+
// ─────────── Scopes ───────────
|
|
87
|
+
|
|
88
|
+
const SCOPE_TYPES: ReadonlySet<string> = new Set([
|
|
89
|
+
'Program',
|
|
90
|
+
'BlockStatement',
|
|
91
|
+
'StaticBlock',
|
|
92
|
+
'SwitchStatement',
|
|
93
|
+
'ForStatement',
|
|
94
|
+
'ForInStatement',
|
|
95
|
+
'ForOfStatement',
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
/** Nearest scope enclosing the last node of `ancestors` (which `fullAncestor` includes in the list). */
|
|
99
|
+
export function enclosingScope(ancestors: AnyNode[]): Node {
|
|
100
|
+
for (let i = ancestors.length - 2; i > 0; i--) {
|
|
101
|
+
if (SCOPE_TYPES.has(ancestors[i].type)) {
|
|
102
|
+
return ancestors[i];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return ancestors[0];
|
|
106
|
+
}
|
package/src/backend/validate.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { simple as walkSimple } from 'acorn-walk';
|
|
2
2
|
import { ALWAYS_ON_CHANNELS, DECLARED_INTEGRATIONS } from '@mondaydotcomorg/z2h-shared-utils';
|
|
3
3
|
import { warn } from '../util/logger';
|
|
4
|
-
import { collectApiDomains,
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { collectApiDomains, collectApiUsages } from './parser/api-usage';
|
|
5
|
+
import { collectBindings } from './parser/bindings';
|
|
6
|
+
import { findHandlerDeclaration, handlerContextParamName, NEVER_A_FUNCTION } from './parser/handler-declaration';
|
|
7
|
+
import { createPathResolver } from './parser/member-path';
|
|
8
|
+
import { messages } from './parser/messages';
|
|
9
|
+
import { parseOrThrow } from './parser/parse';
|
|
10
|
+
import type { ApiUsage } from './parser/types';
|
|
11
|
+
|
|
12
|
+
export type { ApiCall, ApiUsage } from './parser/types';
|
|
8
13
|
|
|
9
14
|
const KNOWN_API_DOMAINS: readonly string[] = [...ALWAYS_ON_CHANNELS, ...DECLARED_INTEGRATIONS];
|
|
10
15
|
|
|
11
16
|
export interface HandlerValidation {
|
|
12
17
|
/** Every `ctx.api.v1.<domain>` the handler touches (known or not), sorted and unique. */
|
|
13
18
|
apiDomains: string[];
|
|
19
|
+
apiUsages: ApiUsage;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
/**
|
|
@@ -23,9 +29,6 @@ export interface HandlerValidation {
|
|
|
23
29
|
* Unknown `ctx.api.v1.<domain>` usage is a warning, not a failure: this pass is
|
|
24
30
|
* build-time DX feedback for agent-generated code, never a security boundary —
|
|
25
31
|
* the isolate has no ambient capabilities regardless of what a handler contains.
|
|
26
|
-
*
|
|
27
|
-
* Returns the `ctx.api.v1.*` domains the handler touches so callers (integration
|
|
28
|
-
* detection) can reuse the parse instead of re-scanning the source.
|
|
29
32
|
*/
|
|
30
33
|
export function validateHandlerSource(file: string, source: string): HandlerValidation {
|
|
31
34
|
const program = parseOrThrow(file, source);
|
|
@@ -53,6 +56,7 @@ export function validateHandlerSource(file: string, source: string): HandlerVali
|
|
|
53
56
|
|
|
54
57
|
const paths = createPathResolver(bindings, handlerContextParamName(declaration));
|
|
55
58
|
const apiDomains = collectApiDomains(program, paths);
|
|
59
|
+
const apiUsages = collectApiUsages(program, bindings, paths, source);
|
|
56
60
|
|
|
57
61
|
for (const domain of apiDomains) {
|
|
58
62
|
if (!KNOWN_API_DOMAINS.includes(domain)) {
|
|
@@ -60,5 +64,5 @@ export function validateHandlerSource(file: string, source: string): HandlerVali
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
return { apiDomains: [...apiDomains].sort() };
|
|
67
|
+
return { apiDomains: [...apiDomains].sort(), apiUsages };
|
|
64
68
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
-
import { mkdtemp, rm, ensureDir, writeFile } from 'fs-extra';
|
|
3
|
+
import { mkdtemp, rm, ensureDir, writeFile, writeJson, readJson, pathExists } from 'fs-extra';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
6
|
import { deployBackend, hasHandlers } from '../backend';
|
|
@@ -23,7 +23,11 @@ const readHandlersMock = vi.mocked(readHandlers);
|
|
|
23
23
|
// handlers directory is known to exist.
|
|
24
24
|
beforeEach(() => {
|
|
25
25
|
readHandlersMock.mockReset();
|
|
26
|
-
readHandlersMock.mockResolvedValue({
|
|
26
|
+
readHandlersMock.mockResolvedValue({
|
|
27
|
+
handlers: { call_snowflake: '// code' },
|
|
28
|
+
apiDomains: [],
|
|
29
|
+
apiUsages: { call_snowflake: {} },
|
|
30
|
+
});
|
|
27
31
|
});
|
|
28
32
|
|
|
29
33
|
describe('hasHandlers', () => {
|
|
@@ -60,7 +64,7 @@ describe('deployBackend (live)', () => {
|
|
|
60
64
|
});
|
|
61
65
|
|
|
62
66
|
it('skips upload when the handlers directory holds nothing', async () => {
|
|
63
|
-
readHandlersMock.mockResolvedValue({ handlers: {}, apiDomains: [] });
|
|
67
|
+
readHandlersMock.mockResolvedValue({ handlers: {}, apiDomains: [], apiUsages: {} });
|
|
64
68
|
|
|
65
69
|
await expect(
|
|
66
70
|
deployBackend({
|
|
@@ -79,8 +83,9 @@ describe('deployBackend (live)', () => {
|
|
|
79
83
|
await ensureDir(path.join(tmp, 'backend', 'handlers'));
|
|
80
84
|
await writeFile(path.join(tmp, 'backend', 'handlers', 'call_snowflake.ts'), '// placeholder');
|
|
81
85
|
readHandlersMock.mockResolvedValue({
|
|
82
|
-
handlers: { call_snowflake: 'async function handler(ctx) { return ctx.api.v1.snowflake.
|
|
86
|
+
handlers: { call_snowflake: 'async function handler(ctx) { return ctx.api.v1.snowflake.query("x"); }' },
|
|
83
87
|
apiDomains: ['snowflake'],
|
|
88
|
+
apiUsages: { call_snowflake: { snowflake: { query: [{ sql: 'x' }] } } },
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
// snowflake is always-on — not a declared integration
|
|
@@ -97,6 +102,63 @@ describe('deployBackend (live)', () => {
|
|
|
97
102
|
expect(mockSend).toHaveBeenCalled();
|
|
98
103
|
const keys: string[] = mockSend.mock.calls.map((c: unknown[]) => (c[0] as { input: { Key: string } }).input.Key);
|
|
99
104
|
expect(keys.some(k => k.includes('/backend-1/'))).toBe(true);
|
|
105
|
+
// usage.json stays local-only — the runner's S3 discovery still comes from meta.json.
|
|
106
|
+
expect(keys.some(k => k.endsWith('/usage.json'))).toBe(false);
|
|
107
|
+
expect(keys.some(k => k.endsWith('/meta.json'))).toBe(true);
|
|
108
|
+
|
|
109
|
+
const metaCall = mockSend.mock.calls.find((c: unknown[]) =>
|
|
110
|
+
(c[0] as { input: { Key: string } }).input.Key.endsWith('/meta.json')
|
|
111
|
+
);
|
|
112
|
+
const meta = JSON.parse((metaCall![0] as { input: { Body: string } }).input.Body);
|
|
113
|
+
expect(meta).toEqual({ version: 1, deployedBy: 'test-user', handlers: ['call_snowflake'] });
|
|
114
|
+
|
|
115
|
+
await expect(readJson(path.join(tmp, 'usage.json'))).resolves.toEqual({
|
|
116
|
+
appName: 'mf-my-app',
|
|
117
|
+
deployedBy: 'test-user',
|
|
118
|
+
apiUsages: { call_snowflake: { snowflake: { query: [{ sql: 'x' }] } } },
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('writes usage.json locally even for apps that had none yet', async () => {
|
|
123
|
+
await ensureDir(path.join(tmp, 'backend', 'handlers'));
|
|
124
|
+
await expect(pathExists(path.join(tmp, 'usage.json'))).resolves.toBe(false);
|
|
125
|
+
|
|
126
|
+
await deployBackend({
|
|
127
|
+
appDir: tmp,
|
|
128
|
+
appName: 'mf-my-app',
|
|
129
|
+
mfKeyPrefix: 'mf-my-app/1',
|
|
130
|
+
version: 1,
|
|
131
|
+
deployedBy: 'test-user',
|
|
132
|
+
isPreview: false,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(mockSend).toHaveBeenCalled();
|
|
136
|
+
await expect(readJson(path.join(tmp, 'usage.json'))).resolves.toEqual({
|
|
137
|
+
appName: 'mf-my-app',
|
|
138
|
+
deployedBy: 'test-user',
|
|
139
|
+
apiUsages: { call_snowflake: {} },
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('merges the local usage.json instead of overwriting it, and writes the merged result back locally', async () => {
|
|
144
|
+
await ensureDir(path.join(tmp, 'backend', 'handlers'));
|
|
145
|
+
await writeJson(path.join(tmp, 'usage.json'), { someOtherFeatureField: 'keep-me' });
|
|
146
|
+
|
|
147
|
+
await deployBackend({
|
|
148
|
+
appDir: tmp,
|
|
149
|
+
appName: 'mf-my-app',
|
|
150
|
+
mfKeyPrefix: 'mf-my-app/1',
|
|
151
|
+
version: 1,
|
|
152
|
+
deployedBy: 'test-user',
|
|
153
|
+
isPreview: false,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await expect(readJson(path.join(tmp, 'usage.json'))).resolves.toEqual({
|
|
157
|
+
someOtherFeatureField: 'keep-me',
|
|
158
|
+
appName: 'mf-my-app',
|
|
159
|
+
deployedBy: 'test-user',
|
|
160
|
+
apiUsages: { call_snowflake: {} },
|
|
161
|
+
});
|
|
100
162
|
});
|
|
101
163
|
|
|
102
164
|
it('tags a handler upload failure as deploy_backend_error, not register_error', async () => {
|
|
@@ -130,7 +192,7 @@ describe('deployBackend (preview)', () => {
|
|
|
130
192
|
});
|
|
131
193
|
|
|
132
194
|
it('skips upload when the handlers directory holds nothing', async () => {
|
|
133
|
-
readHandlersMock.mockResolvedValue({ handlers: {}, apiDomains: [] });
|
|
195
|
+
readHandlersMock.mockResolvedValue({ handlers: {}, apiDomains: [], apiUsages: {} });
|
|
134
196
|
|
|
135
197
|
await expect(
|
|
136
198
|
deployBackend({
|
|
@@ -151,6 +213,7 @@ describe('deployBackend (preview)', () => {
|
|
|
151
213
|
readHandlersMock.mockResolvedValue({
|
|
152
214
|
handlers: { call_snowflake: 'async function handler(ctx) { return ctx.api.v1.monday.query("x"); }' },
|
|
153
215
|
apiDomains: ['monday'],
|
|
216
|
+
apiUsages: { call_snowflake: { monday: { query: [{ query: 'x' }] } } },
|
|
154
217
|
});
|
|
155
218
|
|
|
156
219
|
await expect(
|
|
@@ -166,5 +229,7 @@ describe('deployBackend (preview)', () => {
|
|
|
166
229
|
expect(mockSend).toHaveBeenCalled();
|
|
167
230
|
const keys: string[] = mockSend.mock.calls.map((c: unknown[]) => (c[0] as { input: { Key: string } }).input.Key);
|
|
168
231
|
expect(keys.some(k => k.includes('/MS/'))).toBe(true);
|
|
232
|
+
expect(keys.some(k => k.endsWith('/usage.json'))).toBe(false);
|
|
233
|
+
expect(keys.some(k => k.endsWith('/meta.json'))).toBe(true);
|
|
169
234
|
});
|
|
170
235
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
-
import { mkdtemp, rm, ensureDir, writeFile } from 'fs-extra';
|
|
3
|
+
import { mkdtemp, rm, ensureDir, writeFile, writeJson, readJson } from 'fs-extra';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
6
|
import { backendValidateCommand } from '../backend';
|
|
@@ -14,6 +14,9 @@ vi.mock('../../util/tracker', () => ({
|
|
|
14
14
|
trackEvent: vi.fn().mockResolvedValue(undefined),
|
|
15
15
|
trackDuration: vi.fn(),
|
|
16
16
|
}));
|
|
17
|
+
vi.mock('../../util/machine-identity', () => ({
|
|
18
|
+
whoAmI: () => 'test-user',
|
|
19
|
+
}));
|
|
17
20
|
|
|
18
21
|
const mockSend = vi.fn();
|
|
19
22
|
vi.mock('../../util/s3/client', () => ({
|
|
@@ -44,6 +47,7 @@ describe('backendValidateCommand', () => {
|
|
|
44
47
|
});
|
|
45
48
|
|
|
46
49
|
it('reports valid handlers and detected integrations without uploading', async () => {
|
|
50
|
+
await writeJson(path.join(tmp, 'package.json'), { name: 'my-app' });
|
|
47
51
|
const dir = path.join(tmp, 'backend', 'handlers');
|
|
48
52
|
await ensureDir(dir);
|
|
49
53
|
await writeFile(
|
|
@@ -58,6 +62,12 @@ describe('backendValidateCommand', () => {
|
|
|
58
62
|
integrations: ['monday'],
|
|
59
63
|
});
|
|
60
64
|
expect(mockSend).not.toHaveBeenCalled();
|
|
65
|
+
|
|
66
|
+
await expect(readJson(path.join(tmp, 'usage.json'))).resolves.toEqual({
|
|
67
|
+
appName: 'mf-my-app',
|
|
68
|
+
deployedBy: 'test-user',
|
|
69
|
+
apiUsages: { list_items: { monday: { query: [{ query: 'query { me { id } }' }] } } },
|
|
70
|
+
});
|
|
61
71
|
});
|
|
62
72
|
|
|
63
73
|
it('throws the same validation error deploy would', async () => {
|
|
@@ -95,6 +95,7 @@ describe('create', () => {
|
|
|
95
95
|
expect(await pathExists(path.join(appDir, '.gitignore'))).toBe(true);
|
|
96
96
|
expect(await pathExists(path.join(appDir, '.claude', 'settings.json'))).toBe(true);
|
|
97
97
|
expect(await pathExists(path.join(appDir, 'CLAUDE.md'))).toBe(true);
|
|
98
|
+
await expect(readJson(path.join(appDir, 'usage.json'))).resolves.toEqual({});
|
|
98
99
|
|
|
99
100
|
const pkg = await readJson(path.join(appDir, 'package.json'));
|
|
100
101
|
expect(pkg.name).toBe('demo-app');
|
package/src/commands/backend.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readJson, writeJson } from 'fs-extra';
|
|
3
4
|
import { PutObjectCommand } from '@aws-sdk/client-s3';
|
|
4
5
|
import { info, output } from '../util/logger';
|
|
5
6
|
import { mfAppName, getCwdAppName } from '../util/app-name';
|
|
7
|
+
import { whoAmI } from '../util/machine-identity';
|
|
6
8
|
import { readHandlers } from '../backend/handlers';
|
|
7
9
|
import { detectIntegrations } from '../backend/detect-integrations';
|
|
8
10
|
import { getS3Client } from '../util/s3/client';
|
|
@@ -18,24 +20,36 @@ function currentAppName(): string {
|
|
|
18
20
|
return mfAppName(raw);
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
/**
|
|
22
|
-
* True if the app ships an app-level backend — a `backend/handlers/` directory.
|
|
23
|
-
* Callers gate on this so the deploy path is explicit about whether a backend is in
|
|
24
|
-
* play.
|
|
25
|
-
*/
|
|
26
23
|
export function hasHandlers(appDir: string): boolean {
|
|
27
24
|
return existsSync(path.join(appDir, 'backend', 'handlers'));
|
|
28
25
|
}
|
|
29
26
|
|
|
27
|
+
/** Other features may add top-level fields, so writes below merge into this copy instead of replacing it. */
|
|
28
|
+
async function readExistingUsage(appDir: string): Promise<Record<string, unknown>> {
|
|
29
|
+
try {
|
|
30
|
+
return (await readJson(path.join(appDir, 'usage.json'))) as Record<string, unknown>;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if ((err as { code?: string }).code === 'ENOENT') {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Keeps the app's git-tracked `usage.json` current with every handler parse (validate or deploy). */
|
|
40
|
+
async function writeUsageJson(appDir: string, patch: Record<string, unknown>): Promise<void> {
|
|
41
|
+
const existing = await readExistingUsage(appDir);
|
|
42
|
+
await writeJson(path.join(appDir, 'usage.json'), { ...existing, ...patch }, { spaces: 2 });
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* (or it is empty of sources) so callers can always pass the result to
|
|
34
|
-
* `registerOrUpdateApp` without a separate gate.
|
|
46
|
+
* Returns `[]` when there is no `backend/handlers/` dir (or it is empty of sources)
|
|
47
|
+
* so callers can always pass the result to `registerOrUpdateApp` without a separate gate.
|
|
35
48
|
*/
|
|
36
49
|
async function readDetectAndUpload(
|
|
37
50
|
appDir: string,
|
|
38
51
|
prefix: string,
|
|
52
|
+
appName: string,
|
|
39
53
|
version: number,
|
|
40
54
|
deployedBy: string,
|
|
41
55
|
logLabel: string
|
|
@@ -44,11 +58,12 @@ async function readDetectAndUpload(
|
|
|
44
58
|
return [];
|
|
45
59
|
}
|
|
46
60
|
info(`reading backend handlers ${logLabel}`);
|
|
47
|
-
const { handlers, apiDomains } = await readHandlers(appDir);
|
|
61
|
+
const { handlers, apiDomains, apiUsages } = await readHandlers(appDir);
|
|
48
62
|
const integrations = detectIntegrations(apiDomains);
|
|
49
63
|
if (Object.keys(handlers).length === 0) {
|
|
50
64
|
return integrations;
|
|
51
65
|
}
|
|
66
|
+
await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
52
67
|
const s3 = getS3Client();
|
|
53
68
|
const meta = JSON.stringify({ version, deployedBy, handlers: Object.keys(handlers) });
|
|
54
69
|
const put = (key: string, body: string, contentType: string) =>
|
|
@@ -65,11 +80,6 @@ async function readDetectAndUpload(
|
|
|
65
80
|
* from their sources. Each `.js` handler is uploaded as its own flat file — no
|
|
66
81
|
* compilation, no bundling, no npm deps.
|
|
67
82
|
*
|
|
68
|
-
* Live and preview differ only in where the handlers land, so that is the only
|
|
69
|
-
* thing `isPreview` switches:
|
|
70
|
-
* - live: `<appName>/backend-<version>` — the app's immutable per-version slot.
|
|
71
|
-
* - preview: `<mfKeyPrefix without /MF>/MS` — beside the preview's `/MF` frontend.
|
|
72
|
-
*
|
|
73
83
|
* Integration sync note: the returned set is re-derived from the CURRENT backend
|
|
74
84
|
* sources on every deploy, and the platform set is reconciled to exactly match
|
|
75
85
|
* it (there is no stored declaration to fall back on). A deploy therefore always
|
|
@@ -88,25 +98,26 @@ export async function deployBackend(opts: {
|
|
|
88
98
|
const prefix = isPreview ? `${mfKeyPrefix.replace(/\/MF$/, '')}/MS` : `${appName}/backend-${version}`;
|
|
89
99
|
const logLabel = isPreview ? 'for preview' : `for "${appName}"`;
|
|
90
100
|
try {
|
|
91
|
-
return await readDetectAndUpload(appDir, prefix, version, deployedBy, logLabel);
|
|
101
|
+
return await readDetectAndUpload(appDir, prefix, appName, version, deployedBy, logLabel);
|
|
92
102
|
} catch (err) {
|
|
93
103
|
throw tagDeployError('deploy_backend_error', err);
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* uploading anything. Throws on the first invalid handler.
|
|
108
|
+
* Runs the same checks deploy does against the working tree, refreshing the
|
|
109
|
+
* local `usage.json` — no upload to S3. Throws on the first invalid handler.
|
|
101
110
|
*/
|
|
102
111
|
export async function backendValidateCommand(appDir: string = process.cwd()): Promise<void> {
|
|
103
112
|
if (!hasHandlers(appDir)) {
|
|
104
113
|
output('[z2h-cli] no backend/handlers/ directory — nothing to validate', { handlers: [], integrations: [] });
|
|
105
114
|
return;
|
|
106
115
|
}
|
|
107
|
-
const { handlers, apiDomains } = await readHandlers(appDir);
|
|
116
|
+
const { handlers, apiDomains, apiUsages } = await readHandlers(appDir);
|
|
108
117
|
const names = Object.keys(handlers).sort();
|
|
109
118
|
const integrations = detectIntegrations(apiDomains);
|
|
119
|
+
const { name } = (await readJson(path.join(appDir, 'package.json'))) as { name?: string };
|
|
120
|
+
await writeUsageJson(appDir, { appName: name ? mfAppName(name) : undefined, deployedBy: whoAmI(), apiUsages });
|
|
110
121
|
const list = (xs: string[]) => (xs.length ? xs.join(', ') : 'none');
|
|
111
122
|
output(`[z2h-cli] ${names.length} handler(s) valid: ${list(names)}\n[z2h-cli] integrations: ${list(integrations)}`, {
|
|
112
123
|
handlers: names,
|
package/src/commands/create.ts
CHANGED
|
@@ -108,6 +108,7 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
|
|
|
108
108
|
await copy(path.join(appTemplatesDir, 'gitignore'), path.join(appDir, '.gitignore'));
|
|
109
109
|
await copy(path.join(appTemplatesDir, 'claude-settings.json'), path.join(appDir, '.claude', 'settings.json'));
|
|
110
110
|
await copy(path.join(appTemplatesDir, 'CLAUDE.md'), path.join(appDir, 'CLAUDE.md'));
|
|
111
|
+
await copy(path.join(appTemplatesDir, 'usage.json'), path.join(appDir, 'usage.json'));
|
|
111
112
|
|
|
112
113
|
// html-embed: also copy the source HTML.
|
|
113
114
|
if (template === 'html-embed' && opts.htmlFile) {
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { copy, ensureDir, readJson } from 'fs-extra';
|
|
4
3
|
import { info, warn, output, getOutputMode } from '../util/logger';
|
|
@@ -19,7 +18,7 @@ import { getRemoteUrl, guardCleanTree, pullAndResolve, pushToRemote, initRepo }
|
|
|
19
18
|
import { resolveGitEnv } from '../util/git/git-env';
|
|
20
19
|
import { type RemoteType, validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options';
|
|
21
20
|
import { setupS3Client } from '../util/s3/client';
|
|
22
|
-
import {
|
|
21
|
+
import { whoAmI } from '../util/machine-identity';
|
|
23
22
|
import { fetchApp, registerOrUpdateApp } from '../util/broker/app';
|
|
24
23
|
import { openInBrowser } from '../util/open-browser';
|
|
25
24
|
import { resolvePortAndMetadata } from '../shadow/prepare';
|
|
@@ -30,11 +29,6 @@ import { promptForTags } from './tag';
|
|
|
30
29
|
import type { ConsumerPaths, DeployFailureReason } from '../types';
|
|
31
30
|
import type { ManifestEntry, AssetManifestFile } from '../util/manifest';
|
|
32
31
|
|
|
33
|
-
/** Who is publishing this version — the machine identity, else the OS user. */
|
|
34
|
-
function whoDeployed(): string {
|
|
35
|
-
return getMachineAppName() ?? os.userInfo().username;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
32
|
export interface DeployOptions {
|
|
39
33
|
dryRun?: boolean;
|
|
40
34
|
remote?: RemoteType;
|
|
@@ -106,7 +100,7 @@ async function buildAndStamp(opts: {
|
|
|
106
100
|
} else {
|
|
107
101
|
({ assetManifest } = await buildCommand({ publicUrl: opts.publicUrl }));
|
|
108
102
|
}
|
|
109
|
-
const deployedBy =
|
|
103
|
+
const deployedBy = whoAmI();
|
|
110
104
|
|
|
111
105
|
return {
|
|
112
106
|
...assetManifest,
|
|
@@ -148,7 +142,7 @@ async function uploadAndRegister(opts: {
|
|
|
148
142
|
appName,
|
|
149
143
|
mfKeyPrefix: keyPrefix,
|
|
150
144
|
version: nextVersion,
|
|
151
|
-
deployedBy:
|
|
145
|
+
deployedBy: whoAmI(),
|
|
152
146
|
isPreview,
|
|
153
147
|
});
|
|
154
148
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Returns the machine app name when running as a service (e.g. agent-runner).
|
|
3
5
|
* Available when the process is a monday.com microservice with `APP_NAME` set
|
|
4
|
-
* by the platform runtime.
|
|
5
|
-
*
|
|
6
|
-
* Returns `undefined` for interactive human users (fallback to os.userInfo).
|
|
6
|
+
* by the platform runtime. Returns `undefined` for interactive human users.
|
|
7
7
|
*/
|
|
8
8
|
export function getMachineAppName(): string | undefined {
|
|
9
9
|
return process.env.APP_NAME || undefined;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
export function whoAmI(): string {
|
|
13
|
+
return getMachineAppName() ?? os.userInfo().username;
|
|
14
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ast.test.d.ts","sourceRoot":"","sources":["../../../src/backend/__tests__/ast.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"parse.test.d.ts","sourceRoot":"","sources":["../../../src/backend/__tests__/parse.test.ts"],"names":[],"mappings":""}
|
package/dist/backend/ast.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { AnyNode, Expression, Pattern, Program, VariableDeclaration } from 'acorn';
|
|
2
|
-
export interface Declaration {
|
|
3
|
-
kind: VariableDeclaration['kind'];
|
|
4
|
-
init: Expression | null | undefined;
|
|
5
|
-
/**
|
|
6
|
-
* Property path from `init` to this binding: `[]` for `const x = init`,
|
|
7
|
-
* `['a', 'b']` for `const { a: { b: x } } = init`, `undefined` when the value
|
|
8
|
-
* is not a plain property read (array/rest/default patterns).
|
|
9
|
-
*/
|
|
10
|
-
path: string[] | undefined;
|
|
11
|
-
}
|
|
12
|
-
export interface Bindings {
|
|
13
|
-
/** Every `var|let|const <name> = …` in the file, by name. */
|
|
14
|
-
declarations: Map<string, Declaration[]>;
|
|
15
|
-
/** Names whose value cannot be pinned: reassigned, updated, or bound as parameters. */
|
|
16
|
-
unpinnable: Set<string>;
|
|
17
|
-
}
|
|
18
|
-
/** Every identifier a binding pattern introduces (`{ a, b: [c] } = …` → a, c). */
|
|
19
|
-
export declare const patternNames: (pattern: Pattern) => string[];
|
|
20
|
-
export declare function collectBindings(program: Program): Bindings;
|
|
21
|
-
export interface PathResolver {
|
|
22
|
-
/**
|
|
23
|
-
* Dotted path of a member chain, following `const` aliases back to their
|
|
24
|
-
* source: with `const sf = ctx.api.v1.snowflake`, `sf.query` resolves to
|
|
25
|
-
* `ctx.api.v1.snowflake.query`. Destructuring counts too
|
|
26
|
-
* (`const { snowflake: sf } = ctx.api.v1`). `undefined` if any link is dynamic.
|
|
27
|
-
*
|
|
28
|
-
* Only single, never-reassigned, never-shadowed `const` bindings are followed —
|
|
29
|
-
* anything else is left as written, so a `let` alias or a parameter of the same
|
|
30
|
-
* name is never mistaken for the channel. The handler's first parameter is
|
|
31
|
-
* treated as `ctx` even when renamed (`handler(context)` → `context.api.v1.db`
|
|
32
|
-
* resolves as `ctx.api.v1.db`).
|
|
33
|
-
*/
|
|
34
|
-
pathOf(node: AnyNode): string | undefined;
|
|
35
|
-
}
|
|
36
|
-
export declare function createPathResolver(bindings: Bindings, contextParam?: string): PathResolver;
|
|
37
|
-
/** Every `ctx.api.v1.<domain>` the program touches (known or not), through `paths`. */
|
|
38
|
-
export declare function collectApiDomains(program: Program, paths: PathResolver): Set<string>;
|
|
39
|
-
//# sourceMappingURL=ast.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/backend/ast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAKxF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC;;;;OAIG;IACH,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,QAAQ;IACvB,6DAA6D;IAC7D,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACzC,uFAAuF;IACvF,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACzB;AA8CD,kFAAkF;AAClF,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,MAAM,EAAmD,CAAC;AAE1G,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CA4C1D;AA2BD,MAAM,WAAW,YAAY;IAC3B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,YAAY,CA6B1F;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,CAWpF"}
|