@astrale-os/cli 1.0.0-beta.0 → 1.0.0-beta.2
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/README.md +7 -11
- package/dist/astrale.js +3769 -3549
- package/dist/public/connect-core.js +396 -392
- package/dist/public/keys/index.js +371 -378
- package/dist/public/paths/index.js +371 -378
- package/dist/types/lib/idp.d.ts +6 -0
- package/dist/types/lib/meta.d.ts +2 -2
- package/package.json +6 -4
- package/src/admin/binding.ts +168 -0
- package/src/admin/catalog/__tests__/client.test.ts +2 -2
- package/src/admin/catalog/client.ts +4 -9
- package/src/admin/instance/__tests__/client.test.ts +5 -3
- package/src/admin/instance/client.ts +9 -27
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
- package/src/commands/__tests__/domain-list.test.ts +83 -4
- package/src/commands/__tests__/install-identity-override.test.ts +30 -19
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +73 -0
- package/src/commands/domain/install.ts +11 -15
- package/src/commands/domain/list.ts +17 -12
- package/src/commands/studio.ts +20 -0
- package/src/commands/update.ts +39 -13
- package/src/connection/__tests__/errors.test.ts +16 -12
- package/src/connection/__tests__/exchange.test.ts +6 -5
- package/src/connection/exchange.ts +5 -4
- package/src/lib/__tests__/domain-publication.test.ts +144 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +2 -0
- package/src/lib/__tests__/instance.test.ts +6 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +12 -0
- package/src/lib/domain-publication.ts +102 -0
- package/src/lib/idp.ts +5 -1
- package/src/lib/instance.ts +6 -1
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/update.ts +4 -4
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
- package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
- package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
- package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +3 -1
- package/studio/server/agent/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/system.ts +3 -2
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +44 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +111 -0
- package/studio/server/cache.ts +163 -9
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +30 -2
- package/studio/server/domain.ts +14 -2
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
- package/studio/server/introspect/anatomy/views/routes.ts +105 -0
- package/studio/server/introspect/anatomy/views.ts +58 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -4
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.ts +46 -19
- package/studio/server/introspect/bundle.ts +15 -6
- package/studio/server/introspect/canonical-schema.test.ts +79 -0
- package/studio/server/introspect/canonical-schema.ts +80 -18
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/diff.test.ts +7 -11
- package/studio/server/introspect/diff.ts +28 -37
- package/studio/server/introspect/extractor.ts +8 -6
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +63 -5
- package/studio/server/introspect/runtime.ts +49 -3
- package/studio/server/introspect/schema-ir-json.ts +181 -0
- package/studio/server/introspect/source-overlay/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +681 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +360 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +212 -1
- package/studio/server/state/baseline.ts +145 -29
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -7
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
- package/studio/server/views/target.ts +248 -0
- package/studio/server/watch.ts +1 -1
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +462 -0
- package/studio/shared/contracts/surface.test.ts +319 -0
- package/studio/shared/contracts/workspace.ts +274 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +58 -0
- package/studio/shared/schema/identity.ts +82 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +35 -35
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.ts +0 -535
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel-operation token scan for resolved handler source files.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
|
|
6
|
+
type KernelToken = { token: string; label?: string }
|
|
7
|
+
|
|
8
|
+
/** Kernel-op idioms surfaced as `kernelCalls`; entries are longest-first. */
|
|
9
|
+
const KERNEL_TOKENS: KernelToken[] = [
|
|
10
|
+
{ token: 'graph.createEdge' },
|
|
11
|
+
{ token: 'graph.removeEdge' },
|
|
12
|
+
{ token: 'function.mutate' },
|
|
13
|
+
{ token: 'graph.children' },
|
|
14
|
+
{ token: 'function.get' },
|
|
15
|
+
{ token: 'graph.create' },
|
|
16
|
+
{ token: 'graph.update' },
|
|
17
|
+
{ token: 'graph.remove' },
|
|
18
|
+
{ token: 'graph.mutate' },
|
|
19
|
+
{ token: 'auth.revoke' },
|
|
20
|
+
{ token: 'graph.links' },
|
|
21
|
+
{ token: 'auth.grant' },
|
|
22
|
+
{ token: 'auth.check' },
|
|
23
|
+
{ token: 'graph.tree' },
|
|
24
|
+
{ token: 'graph.node' },
|
|
25
|
+
{ token: 'revokePerm', label: 'revokePerm (legacy)' },
|
|
26
|
+
{ token: 'checkPerm', label: 'checkPerm (legacy)' },
|
|
27
|
+
{ token: 'graph.get' },
|
|
28
|
+
{ token: 'grantPerm', label: 'grantPerm (legacy)' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
/** Scan handler file text for kernel-op tokens. */
|
|
32
|
+
export function scanKernelCalls(file: string): string[] {
|
|
33
|
+
let text: string
|
|
34
|
+
try {
|
|
35
|
+
text = readFileSync(file, 'utf8')
|
|
36
|
+
} catch {
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
const found: string[] = []
|
|
40
|
+
let work = text
|
|
41
|
+
for (const entry of KERNEL_TOKENS) {
|
|
42
|
+
if (work.includes(entry.token)) {
|
|
43
|
+
found.push(entry.label ?? entry.token)
|
|
44
|
+
// Blank out matches so `::getLinks` doesn't also count as `::getLink`.
|
|
45
|
+
work = work.split(entry.token).join(' '.repeat(entry.token.length))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return found
|
|
49
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tolerant ts-morph project construction and authored-source resolution.
|
|
3
|
+
*
|
|
4
|
+
* This module owns syntax/value lookup only. It does not interpret handlers or
|
|
5
|
+
* schema members.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
8
|
+
import { dirname, isAbsolute, relative, resolve as resolvePath } from 'node:path'
|
|
9
|
+
import { Node, Project, SyntaxKind, type CallExpression, type SourceFile } from 'ts-morph'
|
|
10
|
+
|
|
11
|
+
/** A fresh, in-memory-ish ts-morph project: no tsconfig, tolerant of errors. */
|
|
12
|
+
export function newProject(): Project {
|
|
13
|
+
return new Project({
|
|
14
|
+
useInMemoryFileSystem: false,
|
|
15
|
+
skipFileDependencyResolution: true,
|
|
16
|
+
skipLoadingLibFiles: true,
|
|
17
|
+
compilerOptions: {
|
|
18
|
+
allowJs: true,
|
|
19
|
+
// `allowImportingTsExtensions` keeps `.ts`-suffixed imports from blowing up.
|
|
20
|
+
allowImportingTsExtensions: true,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Add a file to the project if it exists; returns undefined otherwise. */
|
|
26
|
+
export function tryAddFile(project: Project, file: string): SourceFile | undefined {
|
|
27
|
+
try {
|
|
28
|
+
if (!existsSync(file)) return undefined
|
|
29
|
+
return project.getSourceFile(file) ?? project.addSourceFileAtPath(file)
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Path relative to `root`, POSIX-style ('schema/monitor.ts'), never absolute. */
|
|
36
|
+
export function relToRoot(root: string, file: string): string {
|
|
37
|
+
const abs = isAbsolute(file) ? file : resolvePath(root, file)
|
|
38
|
+
const canonical = (value: string) => {
|
|
39
|
+
try {
|
|
40
|
+
return realpathSync(value)
|
|
41
|
+
} catch {
|
|
42
|
+
return value
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return relative(canonical(root), canonical(abs)).split('\\').join('/')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The call's callee identifier name, e.g. `method` / `classMethods` / `todo`. */
|
|
49
|
+
export function calleeName(call: CallExpression): string | undefined {
|
|
50
|
+
const expr = call.getExpression()
|
|
51
|
+
if (Node.isIdentifier(expr)) return expr.getText()
|
|
52
|
+
if (Node.isPropertyAccessExpression(expr)) return expr.getName()
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Remove syntax-only wrappers around a runtime value. */
|
|
57
|
+
export function unwrapExpression(node: Node): Node {
|
|
58
|
+
let current = node
|
|
59
|
+
while (
|
|
60
|
+
Node.isParenthesizedExpression(current) ||
|
|
61
|
+
Node.isAsExpression(current) ||
|
|
62
|
+
Node.isSatisfiesExpression(current) ||
|
|
63
|
+
Node.isTypeAssertion(current)
|
|
64
|
+
) {
|
|
65
|
+
current = current.getExpression()
|
|
66
|
+
}
|
|
67
|
+
return current
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Resolve a local or relatively imported identifier to its runtime value. */
|
|
71
|
+
export function valueOfIdentifier(id: Node): Node | undefined {
|
|
72
|
+
if (!Node.isIdentifier(id)) return undefined
|
|
73
|
+
const decl = firstValueDeclaration(id)
|
|
74
|
+
if (decl && Node.isVariableDeclaration(decl)) return decl.getInitializer()
|
|
75
|
+
return resolveImportedValue(id)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Follow a named relative import to the exported value that defines it. */
|
|
79
|
+
function resolveImportedValue(id: Node): Node | undefined {
|
|
80
|
+
if (!Node.isIdentifier(id)) return undefined
|
|
81
|
+
const localName = id.getText()
|
|
82
|
+
const sourceFile = id.getSourceFile()
|
|
83
|
+
|
|
84
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
85
|
+
for (const named of imp.getNamedImports()) {
|
|
86
|
+
const local = named.getAliasNode()?.getText() ?? named.getName()
|
|
87
|
+
if (local !== localName) continue
|
|
88
|
+
const target = resolveModuleFile(sourceFile, imp.getModuleSpecifierValue())
|
|
89
|
+
if (!target) return undefined
|
|
90
|
+
return findExportedValue(id.getProject(), target, named.getName(), new Set<string>())
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Find an exported variable/function value, following local and barrel exports. */
|
|
97
|
+
function findExportedValue(
|
|
98
|
+
project: Project,
|
|
99
|
+
file: string,
|
|
100
|
+
exportName: string,
|
|
101
|
+
seen: Set<string>,
|
|
102
|
+
): Node | undefined {
|
|
103
|
+
const key = `${file}:${exportName}`
|
|
104
|
+
if (seen.has(key)) return undefined
|
|
105
|
+
seen.add(key)
|
|
106
|
+
const sf = tryAddFile(project, file)
|
|
107
|
+
if (!sf) return undefined
|
|
108
|
+
|
|
109
|
+
const localValue = (name: string, exportedOnly: boolean): Node | undefined => {
|
|
110
|
+
const variable = sf
|
|
111
|
+
.getVariableDeclarations()
|
|
112
|
+
.find(
|
|
113
|
+
(candidate) => candidate.getName() === name && (!exportedOnly || candidate.isExported()),
|
|
114
|
+
)
|
|
115
|
+
if (variable) return variable.getInitializer()
|
|
116
|
+
return sf
|
|
117
|
+
.getFunctions()
|
|
118
|
+
.find(
|
|
119
|
+
(candidate) => candidate.getName() === name && (!exportedOnly || candidate.isExported()),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const direct = localValue(exportName, true)
|
|
124
|
+
if (direct) return direct
|
|
125
|
+
|
|
126
|
+
for (const ex of sf.getExportDeclarations()) {
|
|
127
|
+
const modSpec = ex.getModuleSpecifierValue()
|
|
128
|
+
const target = modSpec ? resolveModuleFile(sf, modSpec) : undefined
|
|
129
|
+
const named = ex.getNamedExports()
|
|
130
|
+
if (named.length > 0) {
|
|
131
|
+
for (const specifier of named) {
|
|
132
|
+
const exposed = specifier.getAliasNode()?.getText() ?? specifier.getName()
|
|
133
|
+
if (exposed !== exportName) continue
|
|
134
|
+
const original = specifier.getName()
|
|
135
|
+
if (target) return findExportedValue(project, target, original, seen)
|
|
136
|
+
return localValue(original, false)
|
|
137
|
+
}
|
|
138
|
+
} else if (target) {
|
|
139
|
+
const found = findExportedValue(project, target, exportName, seen)
|
|
140
|
+
if (found) return found
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Get the value node of an object-literal property (handles shorthand). */
|
|
147
|
+
export function getProp(obj: Node, name: string): Node | undefined {
|
|
148
|
+
if (!Node.isObjectLiteralExpression(obj)) return undefined
|
|
149
|
+
const prop = obj.getProperty(name)
|
|
150
|
+
if (!prop) return undefined
|
|
151
|
+
if (Node.isPropertyAssignment(prop)) return prop.getInitializer()
|
|
152
|
+
if (Node.isShorthandPropertyAssignment(prop)) return prop.getNameNode()
|
|
153
|
+
if (Node.isMethodDeclaration(prop)) return prop
|
|
154
|
+
return prop
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** First value/declaration node a name resolves to (definition, not reference). */
|
|
158
|
+
export function firstValueDeclaration(node: Node): Node | undefined {
|
|
159
|
+
const idNode = Node.isIdentifier(node)
|
|
160
|
+
? node
|
|
161
|
+
: node.getFirstDescendantByKind(SyntaxKind.Identifier)
|
|
162
|
+
if (!idNode || !Node.isIdentifier(idNode)) return undefined
|
|
163
|
+
const symbol = idNode.getSymbol()
|
|
164
|
+
if (!symbol) return undefined
|
|
165
|
+
const decls = symbol.getDeclarations()
|
|
166
|
+
return decls[0]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Resolve a relative module specifier to a concrete .ts file path on disk. */
|
|
170
|
+
export function resolveModuleFile(from: SourceFile, spec: string): string | undefined {
|
|
171
|
+
if (!spec.startsWith('.')) {
|
|
172
|
+
// Current SDK projects use package `imports` aliases such as
|
|
173
|
+
// `#actions/risk`. Resolve them from the authored file's directory through
|
|
174
|
+
// Bun so the overlay follows the same project-local map as runtime imports.
|
|
175
|
+
if (!spec.startsWith('#')) return undefined
|
|
176
|
+
try {
|
|
177
|
+
const resolved = Bun.resolveSync(spec, dirname(from.getFilePath()))
|
|
178
|
+
if (existsSync(resolved)) return resolved
|
|
179
|
+
} catch {
|
|
180
|
+
return undefined
|
|
181
|
+
}
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
const baseDir = dirname(from.getFilePath())
|
|
185
|
+
const base = resolvePath(baseDir, spec)
|
|
186
|
+
const sourceBase = base.replace(/\.(?:[cm]?js|jsx)$/, '')
|
|
187
|
+
const candidates = [
|
|
188
|
+
base,
|
|
189
|
+
sourceBase.endsWith('.ts') ? sourceBase : `${sourceBase}.ts`,
|
|
190
|
+
sourceBase.endsWith('.tsx') ? sourceBase : `${sourceBase}.tsx`,
|
|
191
|
+
`${base}/index.ts`,
|
|
192
|
+
`${base}/index.tsx`,
|
|
193
|
+
]
|
|
194
|
+
for (const c of candidates) if (existsSync(c)) return c
|
|
195
|
+
return undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Resolve a value to an object literal, following local/imported identifiers. */
|
|
199
|
+
export function resolveObjectLiteral(
|
|
200
|
+
node: Node,
|
|
201
|
+
seen = new Set<string>(),
|
|
202
|
+
): import('ts-morph').ObjectLiteralExpression | undefined {
|
|
203
|
+
const value = unwrapExpression(node)
|
|
204
|
+
const key = `${value.getSourceFile().getFilePath()}:${value.getStart()}`
|
|
205
|
+
if (seen.has(key)) return undefined
|
|
206
|
+
seen.add(key)
|
|
207
|
+
if (Node.isObjectLiteralExpression(value)) return value
|
|
208
|
+
if (Node.isIdentifier(value)) {
|
|
209
|
+
const resolved = valueOfIdentifier(value)
|
|
210
|
+
if (resolved) return resolveObjectLiteral(resolved, seen)
|
|
211
|
+
}
|
|
212
|
+
return undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Value carried by an object-literal property, including shorthand/method syntax. */
|
|
216
|
+
export function objectPropertyValue(prop: Node): Node | undefined {
|
|
217
|
+
if (Node.isPropertyAssignment(prop)) return prop.getInitializer()
|
|
218
|
+
if (Node.isShorthandPropertyAssignment(prop)) return prop.getNameNode()
|
|
219
|
+
if (Node.isMethodDeclaration(prop)) return prop
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** A string-literal property value, e.g. `as: 'page'` → 'page'. */
|
|
224
|
+
export function stringLiteralOfProp(obj: Node, name: string): string | undefined {
|
|
225
|
+
if (!Node.isObjectLiteralExpression(obj)) return undefined
|
|
226
|
+
const prop = obj.getProperty(name)
|
|
227
|
+
if (!prop || !Node.isPropertyAssignment(prop)) return undefined
|
|
228
|
+
const v = prop.getInitializer()
|
|
229
|
+
if (v && (Node.isStringLiteral(v) || Node.isNoSubstitutionTemplateLiteral(v)))
|
|
230
|
+
return v.getLiteralText()
|
|
231
|
+
return undefined
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The key name of an object-literal property (assignment / shorthand / method). */
|
|
235
|
+
export function propertyKey(prop: Node): string | undefined {
|
|
236
|
+
if (
|
|
237
|
+
Node.isPropertyAssignment(prop) ||
|
|
238
|
+
Node.isShorthandPropertyAssignment(prop) ||
|
|
239
|
+
Node.isMethodDeclaration(prop)
|
|
240
|
+
) {
|
|
241
|
+
const nameNode = prop.getNameNode()
|
|
242
|
+
if (Node.isStringLiteral(nameNode)) return nameNode.getLiteralText()
|
|
243
|
+
if (Node.isComputedPropertyName(nameNode)) return undefined // skip `[expr]: …`
|
|
244
|
+
return nameNode.getText()
|
|
245
|
+
}
|
|
246
|
+
// Spread (`...knobs`) — no single key.
|
|
247
|
+
return undefined
|
|
248
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source coordinates and leading documentation for authored schema members.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync } from 'node:fs'
|
|
5
|
+
import { dirname } from 'node:path'
|
|
6
|
+
import { Node, SyntaxKind, type CallExpression, type SourceFile } from 'ts-morph'
|
|
7
|
+
|
|
8
|
+
import type { SchemaIR, SourceSpan } from '../../../shared/types'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
calleeName,
|
|
12
|
+
newProject,
|
|
13
|
+
objectPropertyValue,
|
|
14
|
+
propertyKey,
|
|
15
|
+
relToRoot,
|
|
16
|
+
resolveObjectLiteral,
|
|
17
|
+
stringLiteralOfProp,
|
|
18
|
+
unwrapExpression,
|
|
19
|
+
valueOfIdentifier,
|
|
20
|
+
} from './project'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Harvest the leading JSDoc / line-comment block immediately above `node`,
|
|
24
|
+
* stripped of comment markers, collapsed to a single trimmed string.
|
|
25
|
+
*/
|
|
26
|
+
function leadingDoc(node: Node): string | undefined {
|
|
27
|
+
// Prefer real JSDoc nodes when present (ts-morph exposes them on many decls).
|
|
28
|
+
const anyNode = node as unknown as { getJsDocs?: () => Array<{ getText: () => string }> }
|
|
29
|
+
if (typeof anyNode.getJsDocs === 'function') {
|
|
30
|
+
const docs = anyNode.getJsDocs()
|
|
31
|
+
if (docs.length > 0) {
|
|
32
|
+
const text = docs.map((d) => d.getText()).join('\n')
|
|
33
|
+
const cleaned = cleanComment(text)
|
|
34
|
+
if (cleaned) return cleaned
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Fall back to raw leading comment ranges (covers `//` line comments too).
|
|
38
|
+
const ranges = node.getLeadingCommentRanges()
|
|
39
|
+
if (ranges.length === 0) return undefined
|
|
40
|
+
const raw = ranges.map((r) => r.getText()).join('\n')
|
|
41
|
+
const cleaned = cleanComment(raw)
|
|
42
|
+
return cleaned || undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Strip `/** */`, `//`, leading `*` gutters; collapse to a tidy single line. */
|
|
46
|
+
function cleanComment(raw: string): string {
|
|
47
|
+
const lines = raw
|
|
48
|
+
.replace(/\/\*\*?/g, '')
|
|
49
|
+
.replace(/\*\//g, '')
|
|
50
|
+
.split('\n')
|
|
51
|
+
.map((l) =>
|
|
52
|
+
l
|
|
53
|
+
.replace(/^\s*\*\s?/, '')
|
|
54
|
+
.replace(/^\s*\/\/\s?/, '')
|
|
55
|
+
.trim(),
|
|
56
|
+
)
|
|
57
|
+
.filter((l) => l.length > 0)
|
|
58
|
+
return lines.join(' ').replace(/\s+/g, ' ').trim()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const DECL_HELPERS: Record<string, 'node' | 'interface' | 'edge' | 'function'> = {
|
|
62
|
+
nodeClass: 'node',
|
|
63
|
+
nodeInterface: 'interface',
|
|
64
|
+
edgeClass: 'edge',
|
|
65
|
+
fn: 'function',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Recognize both legacy `edgeClass(...)` and current
|
|
69
|
+
* `edgeClass.directed/undirected({...})` authoring forms. */
|
|
70
|
+
function declarationHelper(
|
|
71
|
+
call: CallExpression,
|
|
72
|
+
): 'node' | 'interface' | 'edge' | 'function' | undefined {
|
|
73
|
+
const direct = calleeName(call)
|
|
74
|
+
if (direct && direct in DECL_HELPERS) return DECL_HELPERS[direct]
|
|
75
|
+
const expression = call.getExpression()
|
|
76
|
+
if (
|
|
77
|
+
Node.isPropertyAccessExpression(expression) &&
|
|
78
|
+
(expression.getName() === 'directed' || expression.getName() === 'undirected') &&
|
|
79
|
+
expression.getExpression().getText() === 'edgeClass'
|
|
80
|
+
) {
|
|
81
|
+
return 'edge'
|
|
82
|
+
}
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A schema member's true NAME + section, resolved from the `defineSchema` map.
|
|
88
|
+
* The declaration helpers (nodeClass/nodeInterface/edgeClass) carry no name — a
|
|
89
|
+
* member is named by the KEY it is registered under, not by its variable — and a
|
|
90
|
+
* domain may register a class and an interface under the SAME name (the
|
|
91
|
+
* intentional same-name pattern: the `iUser` interface alongside the `User`
|
|
92
|
+
* class). So the map is the sole authority for both the anchor name and the
|
|
93
|
+
* interface-vs-class distinction; the variable identifier alone tells us neither.
|
|
94
|
+
*/
|
|
95
|
+
interface MemberName {
|
|
96
|
+
schemaName: string
|
|
97
|
+
section: 'interface' | 'class' | 'function' // the `classes` map also holds edge classes
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface MemberNameMap {
|
|
101
|
+
/** Exact declaration initializer, robust to imported/local aliases. */
|
|
102
|
+
byValue: Map<string, MemberName>
|
|
103
|
+
/** Legacy fallback when a symbol cannot be resolved. */
|
|
104
|
+
byIdentifier: Map<string, MemberName>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Stable source coordinate for the value behind a local/imported alias. */
|
|
108
|
+
function memberValueKey(node: Node, seen = new Set<string>()): string | undefined {
|
|
109
|
+
const value = unwrapExpression(node)
|
|
110
|
+
const key = `${value.getSourceFile().getFilePath()}:${value.getStart()}`
|
|
111
|
+
if (seen.has(key)) return undefined
|
|
112
|
+
seen.add(key)
|
|
113
|
+
if (Node.isIdentifier(value)) {
|
|
114
|
+
const resolved = valueOfIdentifier(value)
|
|
115
|
+
if (resolved) return memberValueKey(resolved, seen)
|
|
116
|
+
}
|
|
117
|
+
return key
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Map each registered member VARIABLE (as referenced in `defineSchema`) to its
|
|
121
|
+
* schema name + section, from the domain's own schema files — never node_modules
|
|
122
|
+
* (a dependency's `defineSchema` is not this domain's). */
|
|
123
|
+
function buildMemberNameMap(files: SourceFile[]): MemberNameMap {
|
|
124
|
+
const map: MemberNameMap = {
|
|
125
|
+
byValue: new Map<string, MemberName>(),
|
|
126
|
+
byIdentifier: new Map<string, MemberName>(),
|
|
127
|
+
}
|
|
128
|
+
for (const sf of files) {
|
|
129
|
+
if (sf.getFilePath().includes('/node_modules/')) continue
|
|
130
|
+
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
131
|
+
if (calleeName(call) !== 'defineSchema') continue
|
|
132
|
+
const input = call.getArguments()[1]
|
|
133
|
+
const cfg = input ? resolveObjectLiteral(input) : undefined
|
|
134
|
+
if (!cfg) continue
|
|
135
|
+
collectSchemaSection(map, cfg, 'interfaces', 'interface')
|
|
136
|
+
collectSchemaSection(map, cfg, 'classes', 'class')
|
|
137
|
+
collectSchemaSection(map, cfg, 'functions', 'function')
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return map
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Record `variable → { schemaName, section }` for one `defineSchema` section,
|
|
144
|
+
* handling both `Key: alias` and shorthand `Key`. */
|
|
145
|
+
function collectSchemaSection(
|
|
146
|
+
map: MemberNameMap,
|
|
147
|
+
cfg: Node,
|
|
148
|
+
prop: 'interfaces' | 'classes' | 'functions',
|
|
149
|
+
section: 'interface' | 'class' | 'function',
|
|
150
|
+
): void {
|
|
151
|
+
const obj = getObjectProp(cfg, prop)
|
|
152
|
+
if (!obj) return
|
|
153
|
+
for (const p of obj.getProperties()) {
|
|
154
|
+
const schemaName = propertyKey(p)
|
|
155
|
+
const memberValue = objectPropertyValue(p)
|
|
156
|
+
if (!schemaName || !memberValue) continue
|
|
157
|
+
const member = { schemaName, section } satisfies MemberName
|
|
158
|
+
const valueKey = memberValueKey(memberValue)
|
|
159
|
+
if (valueKey) map.byValue.set(valueKey, member)
|
|
160
|
+
const unwrapped = unwrapExpression(memberValue)
|
|
161
|
+
if (Node.isIdentifier(unwrapped)) map.byIdentifier.set(unwrapped.getText(), member)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The anchor namespace ('class' | 'interface' | 'edge') for a declared member.
|
|
167
|
+
* The `defineSchema` SECTION is authoritative for interface-vs-class — a
|
|
168
|
+
* `nodeClass` whose name collides with a same-named interface must still anchor
|
|
169
|
+
* as a class. Within the class section, an `edgeClass` (or an IR edge type)
|
|
170
|
+
* anchors as 'edge'. Falls back to the declaration helper when the member isn't
|
|
171
|
+
* in a parseable `defineSchema` map (e.g. an imported kernel member).
|
|
172
|
+
*/
|
|
173
|
+
function resolveMemberKind(
|
|
174
|
+
ir: SchemaIR | null,
|
|
175
|
+
name: string,
|
|
176
|
+
section: 'interface' | 'class' | 'function' | undefined,
|
|
177
|
+
helperKind: 'node' | 'interface' | 'edge' | 'function',
|
|
178
|
+
): 'class' | 'interface' | 'edge' | 'function' {
|
|
179
|
+
if (section === 'function' || helperKind === 'function') return 'function'
|
|
180
|
+
if (section === 'interface') return 'interface'
|
|
181
|
+
const isEdge = helperKind === 'edge' || ir?.classes?.[name]?.type === 'edge'
|
|
182
|
+
if (section === 'class') return isEdge ? 'edge' : 'class'
|
|
183
|
+
if (helperKind === 'interface') return 'interface'
|
|
184
|
+
return isEdge ? 'edge' : 'class'
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function buildSourceSpans(args: {
|
|
188
|
+
ir: SchemaIR | null
|
|
189
|
+
schemaDir: string
|
|
190
|
+
}): Record<string, SourceSpan> {
|
|
191
|
+
const { ir, schemaDir } = args
|
|
192
|
+
if (!schemaDir || !existsSync(schemaDir)) return {}
|
|
193
|
+
|
|
194
|
+
// The domain root is the parent of the schema dir (spans are relative to it).
|
|
195
|
+
const domainRoot = dirname(schemaDir.replace(/\/$/, ''))
|
|
196
|
+
|
|
197
|
+
const project = newProject()
|
|
198
|
+
let files: string[] = []
|
|
199
|
+
try {
|
|
200
|
+
const added = project.addSourceFilesAtPaths(`${schemaDir.replace(/\/$/, '')}/**/*.ts`)
|
|
201
|
+
files = added.map((f) => f.getFilePath())
|
|
202
|
+
} catch {
|
|
203
|
+
return {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const spans: Record<string, SourceSpan> = {}
|
|
207
|
+
|
|
208
|
+
const sourceFiles = files.map((f) => project.getSourceFile(f)).filter((f): f is SourceFile => !!f)
|
|
209
|
+
// The domain's `defineSchema` map is authoritative for each member's name +
|
|
210
|
+
// interface-vs-class kind, so an aliased interface (`iUser`→`User`) and a class
|
|
211
|
+
// sharing its name (`User`) each anchor correctly instead of colliding.
|
|
212
|
+
const memberNames = buildMemberNameMap(sourceFiles)
|
|
213
|
+
|
|
214
|
+
for (const sf of sourceFiles) {
|
|
215
|
+
const fileRel = relToRoot(domainRoot, sf.getFilePath())
|
|
216
|
+
|
|
217
|
+
for (const v of sf.getVariableDeclarations()) {
|
|
218
|
+
if (!v.isExported()) continue
|
|
219
|
+
const init = v.getInitializer()
|
|
220
|
+
if (!init || !Node.isCallExpression(init)) continue
|
|
221
|
+
const helperKind = declarationHelper(init)
|
|
222
|
+
if (!helperKind) continue
|
|
223
|
+
const valueKey = memberValueKey(init)
|
|
224
|
+
const member =
|
|
225
|
+
(valueKey ? memberNames.byValue.get(valueKey) : undefined) ??
|
|
226
|
+
memberNames.byIdentifier.get(v.getName())
|
|
227
|
+
const name = member?.schemaName ?? v.getName()
|
|
228
|
+
const ns = resolveMemberKind(ir, name, member?.section, helperKind)
|
|
229
|
+
|
|
230
|
+
const stmt = v.getVariableStatement() ?? v
|
|
231
|
+
spans[`${ns}.${name}`] = makeSpan(domainRoot, fileRel, stmt, v)
|
|
232
|
+
|
|
233
|
+
// The single object-literal argument: nodeClass({ properties, methods })
|
|
234
|
+
// and current edgeClass.directed({ source, target, properties }).
|
|
235
|
+
const cfgArg = init.getArguments()[0]
|
|
236
|
+
if (ns !== 'function' && cfgArg && Node.isObjectLiteralExpression(cfgArg)) {
|
|
237
|
+
collectPropsAndMethods(spans, ns, name, cfgArg, domainRoot, fileRel)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Edges: endpoints are the first two args; props live in the third.
|
|
241
|
+
if (ns === 'edge') {
|
|
242
|
+
collectEdge(spans, name, init, domainRoot, fileRel)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return spans
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Build a SourceSpan, harvesting leading doc from the declaration `docNode`. */
|
|
251
|
+
function makeSpan(domainRoot: string, fileRel: string, spanNode: Node, docNode: Node): SourceSpan {
|
|
252
|
+
const span: SourceSpan = {
|
|
253
|
+
file: fileRel,
|
|
254
|
+
startLine: spanNode.getStartLineNumber(),
|
|
255
|
+
endLine: spanNode.getEndLineNumber(),
|
|
256
|
+
}
|
|
257
|
+
const doc = leadingDoc(docNode) ?? leadingDoc(spanNode)
|
|
258
|
+
if (doc) span.doc = doc
|
|
259
|
+
return span
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Record `<ns>.<Name>.property.<p>` and `.method.<m>` from a config object. */
|
|
263
|
+
function collectPropsAndMethods(
|
|
264
|
+
spans: Record<string, SourceSpan>,
|
|
265
|
+
ns: string,
|
|
266
|
+
name: string,
|
|
267
|
+
cfg: Node,
|
|
268
|
+
domainRoot: string,
|
|
269
|
+
fileRel: string,
|
|
270
|
+
): void {
|
|
271
|
+
if (!Node.isObjectLiteralExpression(cfg)) return
|
|
272
|
+
const propsObj = getObjectProp(cfg, 'properties') ?? getObjectProp(cfg, 'props')
|
|
273
|
+
if (propsObj) {
|
|
274
|
+
for (const p of propsObj.getProperties()) {
|
|
275
|
+
const pName = propertyKey(p)
|
|
276
|
+
if (!pName) continue
|
|
277
|
+
spans[`${ns}.${name}.property.${pName}`] = makeSpan(domainRoot, fileRel, p, p)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const methodsObj = getObjectProp(cfg, 'methods')
|
|
281
|
+
if (methodsObj) {
|
|
282
|
+
for (const m of methodsObj.getProperties()) {
|
|
283
|
+
const mName = propertyKey(m)
|
|
284
|
+
if (!mName) continue
|
|
285
|
+
spans[`${ns}.${name}.method.${mName}`] = makeSpan(domainRoot, fileRel, m, m)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Record edge endpoint spans `edge.<Name>.endpoint.<role>` from the two args. */
|
|
291
|
+
function collectEdge(
|
|
292
|
+
spans: Record<string, SourceSpan>,
|
|
293
|
+
name: string,
|
|
294
|
+
init: CallExpression,
|
|
295
|
+
domainRoot: string,
|
|
296
|
+
fileRel: string,
|
|
297
|
+
): void {
|
|
298
|
+
const argsList = init.getArguments()
|
|
299
|
+
const expression = init.getExpression()
|
|
300
|
+
const currentObjectForm =
|
|
301
|
+
Node.isPropertyAccessExpression(expression) &&
|
|
302
|
+
(expression.getName() === 'directed' || expression.getName() === 'undirected') &&
|
|
303
|
+
argsList[0] &&
|
|
304
|
+
Node.isObjectLiteralExpression(argsList[0])
|
|
305
|
+
|
|
306
|
+
if (currentObjectForm) {
|
|
307
|
+
const config = argsList[0]
|
|
308
|
+
if (Node.isObjectLiteralExpression(config)) {
|
|
309
|
+
for (const endpointName of ['source', 'target'] as const) {
|
|
310
|
+
const ep = getObjectProp(config, endpointName)
|
|
311
|
+
if (!ep) continue
|
|
312
|
+
const role = stringLiteralOfProp(ep, 'as') ?? stringLiteralOfProp(ep, 'role')
|
|
313
|
+
if (role) spans[`edge.${name}.endpoint.${role}`] = makeSpan(domainRoot, fileRel, ep, ep)
|
|
314
|
+
}
|
|
315
|
+
const properties = getObjectProp(config, 'properties') ?? getObjectProp(config, 'props')
|
|
316
|
+
if (properties) {
|
|
317
|
+
for (const p of properties.getProperties()) {
|
|
318
|
+
const pName = propertyKey(p)
|
|
319
|
+
if (!pName) continue
|
|
320
|
+
spans[`edge.${name}.property.${pName}`] = makeSpan(domainRoot, fileRel, p, p)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (let i = 0; i < Math.min(2, argsList.length); i++) {
|
|
328
|
+
const ep = argsList[i]
|
|
329
|
+
if (!Node.isObjectLiteralExpression(ep)) continue
|
|
330
|
+
const role = stringLiteralOfProp(ep, 'as')
|
|
331
|
+
if (!role) continue
|
|
332
|
+
spans[`edge.${name}.endpoint.${role}`] = makeSpan(domainRoot, fileRel, ep, ep)
|
|
333
|
+
}
|
|
334
|
+
// Legacy edge props live in the third (config) arg.
|
|
335
|
+
const cfgArg = argsList[2]
|
|
336
|
+
if (cfgArg && Node.isObjectLiteralExpression(cfgArg)) {
|
|
337
|
+
const propsObj = getObjectProp(cfgArg, 'properties') ?? getObjectProp(cfgArg, 'props')
|
|
338
|
+
if (propsObj) {
|
|
339
|
+
for (const p of propsObj.getProperties()) {
|
|
340
|
+
const pName = propertyKey(p)
|
|
341
|
+
if (!pName) continue
|
|
342
|
+
spans[`edge.${name}.property.${pName}`] = makeSpan(domainRoot, fileRel, p, p)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The object-literal value of a named property, if it is itself an object. */
|
|
349
|
+
function getObjectProp(
|
|
350
|
+
obj: Node,
|
|
351
|
+
name: string,
|
|
352
|
+
): import('ts-morph').ObjectLiteralExpression | undefined {
|
|
353
|
+
if (!Node.isObjectLiteralExpression(obj)) return undefined
|
|
354
|
+
const prop = obj.getProperty(name)
|
|
355
|
+
if (!prop) return undefined
|
|
356
|
+
let value: Node | undefined
|
|
357
|
+
if (Node.isPropertyAssignment(prop)) value = prop.getInitializer()
|
|
358
|
+
else if (Node.isShorthandPropertyAssignment(prop)) value = prop.getNameNode()
|
|
359
|
+
return value ? resolveObjectLiteral(value) : undefined
|
|
360
|
+
}
|