@cosmicdrift/kumiko-framework 0.199.1 → 0.199.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/package.json +3 -3
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-name-const/constants.ts +1 -0
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-name-const/feature.ts +8 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +32 -1
- package/src/engine/feature-ast/extractors/events.ts +13 -12
- package/src/engine/feature-ast/extractors/handlers.ts +5 -4
- package/src/engine/feature-ast/extractors/hooks.ts +4 -4
- package/src/engine/feature-ast/extractors/jobs-routes.ts +5 -4
- package/src/engine/feature-ast/extractors/round2.ts +10 -8
- package/src/engine/feature-ast/extractors/round3.ts +9 -8
- package/src/engine/feature-ast/extractors/round5.ts +16 -14
- package/src/engine/feature-ast/extractors/shared.ts +57 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.199.
|
|
3
|
+
"version": "0.199.2",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
"./package.json": "./package.json"
|
|
187
187
|
},
|
|
188
188
|
"dependencies": {
|
|
189
|
-
"@cosmicdrift/kumiko-types": "0.199.
|
|
189
|
+
"@cosmicdrift/kumiko-types": "0.199.2",
|
|
190
190
|
"bullmq": "^5.76.7",
|
|
191
191
|
"bun-types": "^1.3.13",
|
|
192
192
|
"hono": "^4.13.1",
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
"zod": "^4.4.3"
|
|
203
203
|
},
|
|
204
204
|
"devDependencies": {
|
|
205
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.199.
|
|
205
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.199.2",
|
|
206
206
|
"bun-types": "^1.3.13",
|
|
207
207
|
"pino-pretty": "^13.1.3"
|
|
208
208
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const EXT_AUDIT = "audit" as const;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { EXT_AUDIT } from "./constants";
|
|
2
|
+
|
|
3
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural parser test fixture, never executed or type-checked at runtime
|
|
4
|
+
declare function defineFeature(name: string, setup: (r: any) => void): void;
|
|
5
|
+
|
|
6
|
+
defineFeature("cross-file-name-const", (r) => {
|
|
7
|
+
r.extendsRegistrar(EXT_AUDIT, {});
|
|
8
|
+
});
|
|
@@ -473,7 +473,7 @@ defineFeature("f", (r) => {
|
|
|
473
473
|
expect(result.errors[0]?.methodName).toBe("entity");
|
|
474
474
|
});
|
|
475
475
|
|
|
476
|
-
test("
|
|
476
|
+
test("resolves the name when it's an identifier initialized to a string literal", () => {
|
|
477
477
|
const result = parseInline(`
|
|
478
478
|
const ENTITY = "task";
|
|
479
479
|
defineFeature("f", (r) => {
|
|
@@ -481,6 +481,22 @@ defineFeature("f", (r) => {
|
|
|
481
481
|
});
|
|
482
482
|
`);
|
|
483
483
|
|
|
484
|
+
expect(result.errors).toEqual([]);
|
|
485
|
+
expect(result.patterns[0]).toMatchObject({
|
|
486
|
+
kind: "entity",
|
|
487
|
+
entityName: "task",
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("emits a ParseError when the name identifier does not resolve to a string literal", () => {
|
|
492
|
+
const result = parseInline(`
|
|
493
|
+
const ENTITY = computeEntityName();
|
|
494
|
+
defineFeature("f", (r) => {
|
|
495
|
+
r.entity(ENTITY, { fields: {} });
|
|
496
|
+
});
|
|
497
|
+
`);
|
|
498
|
+
|
|
499
|
+
expect(result.patterns).toEqual([]);
|
|
484
500
|
expect(result.errors[0]?.methodName).toBe("entity");
|
|
485
501
|
});
|
|
486
502
|
|
|
@@ -2366,3 +2382,18 @@ describe("cross-file registrar-wrapper resolution against a real filesystem Proj
|
|
|
2366
2382
|
expect(navPattern?.source.file).not.toBe(fixture);
|
|
2367
2383
|
});
|
|
2368
2384
|
});
|
|
2385
|
+
|
|
2386
|
+
// #1746 — registrar-call name args authored as an imported constant
|
|
2387
|
+
// (`r.useExtension(EXT_TENANT_DATA, ...)`) instead of a string literal.
|
|
2388
|
+
// This is the dominant real-world style across the framework's own
|
|
2389
|
+
// bundled-features (see extension-names.ts), and previously ParseErrored
|
|
2390
|
+
// on every such call.
|
|
2391
|
+
describe("cross-file imported-constant name resolution against a real filesystem Project (#1746)", () => {
|
|
2392
|
+
const fixture = resolve(__dirname, "fixtures/cross-file-name-const/feature.ts");
|
|
2393
|
+
const result = parseFeatureFile(fixture);
|
|
2394
|
+
|
|
2395
|
+
test("resolves the identifier to the imported const's string value", () => {
|
|
2396
|
+
expect(result.errors).toEqual([]);
|
|
2397
|
+
expect(result.patterns).toMatchObject([{ kind: "extendsRegistrar", extensionName: "audit" }]);
|
|
2398
|
+
});
|
|
2399
|
+
});
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
findFunctionLiteral,
|
|
10
10
|
ok,
|
|
11
11
|
readDataLiteralNode,
|
|
12
|
+
readNameLiteral,
|
|
12
13
|
readNameOrRef,
|
|
13
14
|
readPropertyKey,
|
|
14
15
|
} from "./shared";
|
|
@@ -146,12 +147,12 @@ export function extractDefineEvent(
|
|
|
146
147
|
});
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
150
|
+
const eventName = readNameLiteral(first);
|
|
151
|
+
if (eventName === undefined) {
|
|
151
152
|
return fail(
|
|
152
153
|
"defineEvent",
|
|
153
154
|
sourceLocationFromNode(call, sourceFile),
|
|
154
|
-
"first argument must be a string literal event name (or use the object form)",
|
|
155
|
+
"first argument must be a string literal event name, or an identifier resolving to one (or use the object form)",
|
|
155
156
|
);
|
|
156
157
|
}
|
|
157
158
|
const schemaArg = args[1];
|
|
@@ -186,7 +187,7 @@ export function extractDefineEvent(
|
|
|
186
187
|
return ok({
|
|
187
188
|
kind: "defineEvent",
|
|
188
189
|
source: sourceLocationFromNode(call, sourceFile),
|
|
189
|
-
eventName
|
|
190
|
+
eventName,
|
|
190
191
|
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
191
192
|
...(version !== undefined && { version }),
|
|
192
193
|
...(migrations !== undefined && { migrations }),
|
|
@@ -207,31 +208,32 @@ export function extractNotification(
|
|
|
207
208
|
);
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
let
|
|
211
|
+
let notificationName: string | undefined;
|
|
211
212
|
let defObj: ReturnType<typeof first.asKind<SyntaxKind.ObjectLiteralExpression>>;
|
|
212
213
|
|
|
213
214
|
const firstObj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
214
215
|
if (firstObj && args.length === 1) {
|
|
215
|
-
|
|
216
|
+
const nameInit = firstObj
|
|
216
217
|
.getProperty("name")
|
|
217
218
|
?.asKind(SyntaxKind.PropertyAssignment)
|
|
218
219
|
?.getInitializer()
|
|
219
220
|
?.asKind(SyntaxKind.StringLiteral);
|
|
220
|
-
if (!
|
|
221
|
+
if (!nameInit) {
|
|
221
222
|
return fail(
|
|
222
223
|
"notification",
|
|
223
224
|
sourceLocationFromNode(call, sourceFile),
|
|
224
225
|
"object form requires a string-literal `name` property",
|
|
225
226
|
);
|
|
226
227
|
}
|
|
228
|
+
notificationName = nameInit.getLiteralValue();
|
|
227
229
|
defObj = firstObj;
|
|
228
230
|
} else {
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
+
notificationName = readNameLiteral(first);
|
|
232
|
+
if (notificationName === undefined) {
|
|
231
233
|
return fail(
|
|
232
234
|
"notification",
|
|
233
235
|
sourceLocationFromNode(call, sourceFile),
|
|
234
|
-
"first argument must be a string literal notification name (or use the object form)",
|
|
236
|
+
"first argument must be a string literal notification name, or an identifier resolving to one (or use the object form)",
|
|
235
237
|
);
|
|
236
238
|
}
|
|
237
239
|
defObj = args[1]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
@@ -243,7 +245,6 @@ export function extractNotification(
|
|
|
243
245
|
);
|
|
244
246
|
}
|
|
245
247
|
}
|
|
246
|
-
const nameArg = nameLiteral;
|
|
247
248
|
const triggerObj = defObj
|
|
248
249
|
.getProperty("trigger")
|
|
249
250
|
?.asKind(SyntaxKind.PropertyAssignment)
|
|
@@ -313,7 +314,7 @@ export function extractNotification(
|
|
|
313
314
|
return ok({
|
|
314
315
|
kind: "notification",
|
|
315
316
|
source: sourceLocationFromNode(call, sourceFile),
|
|
316
|
-
notificationName
|
|
317
|
+
notificationName,
|
|
317
318
|
trigger: { on: onName },
|
|
318
319
|
recipientBody: sourceLocationFromNode(recipientFn, sourceFile),
|
|
319
320
|
dataBody: sourceLocationFromNode(dataFn, sourceFile),
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ok,
|
|
15
15
|
readBooleanProperty,
|
|
16
16
|
readDataLiteralNode,
|
|
17
|
+
readNameLiteral,
|
|
17
18
|
} from "./shared";
|
|
18
19
|
|
|
19
20
|
export type ParsedHandlerCall = {
|
|
@@ -134,12 +135,12 @@ export function parseHandlerCall(
|
|
|
134
135
|
if (args.length === 1 && isRawRefSentinel(readDataLiteralNode(first))) {
|
|
135
136
|
return ok({ source: sourceLocationFromNode(call, sourceFile) });
|
|
136
137
|
}
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
138
|
+
const handlerName = readNameLiteral(first);
|
|
139
|
+
if (handlerName === undefined) {
|
|
139
140
|
return fail(
|
|
140
141
|
methodName,
|
|
141
142
|
sourceLocationFromNode(call, sourceFile),
|
|
142
|
-
"first argument must be a string literal handler name (or use the object form)",
|
|
143
|
+
"first argument must be a string literal handler name, or an identifier resolving to one (or use the object form)",
|
|
143
144
|
);
|
|
144
145
|
}
|
|
145
146
|
const schemaArg = args[1];
|
|
@@ -178,7 +179,7 @@ export function parseHandlerCall(
|
|
|
178
179
|
}
|
|
179
180
|
return ok({
|
|
180
181
|
source: sourceLocationFromNode(call, sourceFile),
|
|
181
|
-
handlerName
|
|
182
|
+
handlerName,
|
|
182
183
|
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
183
184
|
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
184
185
|
...(access !== undefined && { access }),
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isPlainObject,
|
|
13
13
|
ok,
|
|
14
14
|
readDataLiteralNode,
|
|
15
|
+
readNameLiteral,
|
|
15
16
|
readNameOrRef,
|
|
16
17
|
readNameOrRefOrList,
|
|
17
18
|
} from "./shared";
|
|
@@ -155,15 +156,14 @@ export function extractHook(
|
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
159
|
+
const hookType = readNameLiteral(first);
|
|
160
|
+
if (hookType === undefined) {
|
|
160
161
|
return fail(
|
|
161
162
|
"hook",
|
|
162
163
|
sourceLocationFromNode(call, sourceFile),
|
|
163
|
-
"first argument must be a string literal hook type (or use the object form)",
|
|
164
|
+
"first argument must be a string literal hook type, or an identifier resolving to one (or use the object form)",
|
|
164
165
|
);
|
|
165
166
|
}
|
|
166
|
-
const hookType = typeArg.getLiteralValue();
|
|
167
167
|
if (!isHookType(hookType)) {
|
|
168
168
|
return fail(
|
|
169
169
|
"hook",
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ok,
|
|
13
13
|
readBooleanProperty,
|
|
14
14
|
readDataLiteralNode,
|
|
15
|
+
readNameLiteral,
|
|
15
16
|
readPropertyKey,
|
|
16
17
|
} from "./shared";
|
|
17
18
|
|
|
@@ -97,12 +98,12 @@ export function extractJob(
|
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
101
|
+
const jobName = readNameLiteral(first);
|
|
102
|
+
if (jobName === undefined) {
|
|
102
103
|
return fail(
|
|
103
104
|
"job",
|
|
104
105
|
sourceLocationFromNode(call, sourceFile),
|
|
105
|
-
"first argument must be a string literal job name (or use the object form)",
|
|
106
|
+
"first argument must be a string literal job name, or an identifier resolving to one (or use the object form)",
|
|
106
107
|
);
|
|
107
108
|
}
|
|
108
109
|
const optionsArg = args[1];
|
|
@@ -140,7 +141,7 @@ export function extractJob(
|
|
|
140
141
|
return ok({
|
|
141
142
|
kind: "job",
|
|
142
143
|
source: sourceLocationFromNode(call, sourceFile),
|
|
143
|
-
jobName
|
|
144
|
+
jobName,
|
|
144
145
|
options: options as Omit<JobDefinition, "name" | "handler">,
|
|
145
146
|
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
146
147
|
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isPlainObject,
|
|
13
13
|
ok,
|
|
14
14
|
readDataLiteralNode,
|
|
15
|
+
readNameLiteral,
|
|
15
16
|
readNameOrRef,
|
|
16
17
|
} from "./shared";
|
|
17
18
|
|
|
@@ -60,12 +61,12 @@ export function extractEntity(
|
|
|
60
61
|
});
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
64
|
+
const entityName = readNameLiteral(first);
|
|
65
|
+
if (entityName === undefined) {
|
|
65
66
|
return fail(
|
|
66
67
|
"entity",
|
|
67
68
|
sourceLocationFromNode(call, sourceFile),
|
|
68
|
-
"first argument must be a string literal name (or use the object form)",
|
|
69
|
+
"first argument must be a string literal name, or an identifier resolving to one (or use the object form)",
|
|
69
70
|
);
|
|
70
71
|
}
|
|
71
72
|
const defArg = args[1];
|
|
@@ -87,7 +88,7 @@ export function extractEntity(
|
|
|
87
88
|
return ok({
|
|
88
89
|
kind: "entity",
|
|
89
90
|
source: sourceLocationFromNode(call, sourceFile),
|
|
90
|
-
entityName
|
|
91
|
+
entityName,
|
|
91
92
|
definition: definition as EntityDefinition,
|
|
92
93
|
});
|
|
93
94
|
}
|
|
@@ -165,12 +166,13 @@ export function extractRelation(
|
|
|
165
166
|
'first argument must be a string literal or an inline { name: "..." } object (or use the object form)',
|
|
166
167
|
);
|
|
167
168
|
}
|
|
168
|
-
const
|
|
169
|
-
|
|
169
|
+
const relationNameArg = args[1];
|
|
170
|
+
const relationName = relationNameArg && readNameLiteral(relationNameArg);
|
|
171
|
+
if (!relationName) {
|
|
170
172
|
return fail(
|
|
171
173
|
"relation",
|
|
172
174
|
sourceLocationFromNode(call, sourceFile),
|
|
173
|
-
"second argument must be a string literal relation name",
|
|
175
|
+
"second argument must be a string literal relation name, or an identifier resolving to one",
|
|
174
176
|
);
|
|
175
177
|
}
|
|
176
178
|
const defArg = args[2];
|
|
@@ -193,7 +195,7 @@ export function extractRelation(
|
|
|
193
195
|
kind: "relation",
|
|
194
196
|
source: sourceLocationFromNode(call, sourceFile),
|
|
195
197
|
entityName,
|
|
196
|
-
relationName
|
|
198
|
+
relationName,
|
|
197
199
|
definition: definition as RelationDefinition,
|
|
198
200
|
});
|
|
199
201
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isPlainObject,
|
|
21
21
|
ok,
|
|
22
22
|
readDataLiteralNode,
|
|
23
|
+
readNameLiteral,
|
|
23
24
|
readNameOrRef,
|
|
24
25
|
} from "./shared";
|
|
25
26
|
|
|
@@ -68,12 +69,12 @@ export function readNamedOptions(
|
|
|
68
69
|
return { kind: "ok", name: nameInit.getLiteralValue(), options: optionsWithoutName };
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
72
|
+
const name = readNameLiteral(first);
|
|
73
|
+
if (name === undefined) {
|
|
73
74
|
return fail(
|
|
74
75
|
methodName,
|
|
75
76
|
sourceLocationFromNode(call, sourceFile),
|
|
76
|
-
"first argument must be a string literal name (or use the object form)",
|
|
77
|
+
"first argument must be a string literal name, or an identifier resolving to one (or use the object form)",
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
80
|
const optionsArg = args[1];
|
|
@@ -92,7 +93,7 @@ export function readNamedOptions(
|
|
|
92
93
|
"options could not be read as a plain object",
|
|
93
94
|
);
|
|
94
95
|
}
|
|
95
|
-
return { kind: "ok", name
|
|
96
|
+
return { kind: "ok", name, options };
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
export function extractConfig(
|
|
@@ -424,12 +425,12 @@ export function extractUseExtension(
|
|
|
424
425
|
});
|
|
425
426
|
}
|
|
426
427
|
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
428
|
+
const extensionName = readNameLiteral(first);
|
|
429
|
+
if (extensionName === undefined) {
|
|
429
430
|
return fail(
|
|
430
431
|
"useExtension",
|
|
431
432
|
sourceLocationFromNode(call, sourceFile),
|
|
432
|
-
"first argument must be a string literal extension name (or use the object form)",
|
|
433
|
+
"first argument must be a string literal extension name, or an identifier resolving to one (or use the object form)",
|
|
433
434
|
);
|
|
434
435
|
}
|
|
435
436
|
const entityRefArg = args[1];
|
|
@@ -464,7 +465,7 @@ export function extractUseExtension(
|
|
|
464
465
|
return ok({
|
|
465
466
|
kind: "useExtension",
|
|
466
467
|
source: sourceLocationFromNode(call, sourceFile),
|
|
467
|
-
extensionName
|
|
468
|
+
extensionName,
|
|
468
469
|
entityName,
|
|
469
470
|
...(options !== undefined && { options }),
|
|
470
471
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { CallExpression, SourceFile } from "ts-morph";
|
|
2
|
-
import { SyntaxKind } from "ts-morph";
|
|
3
2
|
import type {
|
|
4
3
|
EnvSchemaPattern,
|
|
5
4
|
ExposesApiPattern,
|
|
@@ -7,7 +6,7 @@ import type {
|
|
|
7
6
|
UsesApiPattern,
|
|
8
7
|
} from "../patterns";
|
|
9
8
|
import { sourceLocationFromNode } from "../source-location";
|
|
10
|
-
import { type ExtractOutput, fail, ok } from "./shared";
|
|
9
|
+
import { type ExtractOutput, fail, ok, readNameLiteral } from "./shared";
|
|
11
10
|
|
|
12
11
|
export function extractEnvSchema(
|
|
13
12
|
call: CallExpression,
|
|
@@ -33,12 +32,13 @@ export function extractExtendsRegistrar(
|
|
|
33
32
|
sourceFile: SourceFile,
|
|
34
33
|
): ExtractOutput<ExtendsRegistrarPattern> {
|
|
35
34
|
const args = call.getArguments();
|
|
36
|
-
const
|
|
37
|
-
|
|
35
|
+
const first = args[0];
|
|
36
|
+
const extensionName = first && readNameLiteral(first);
|
|
37
|
+
if (!extensionName) {
|
|
38
38
|
return fail(
|
|
39
39
|
"extendsRegistrar",
|
|
40
40
|
sourceLocationFromNode(call, sourceFile),
|
|
41
|
-
"first argument must be a string literal extension name",
|
|
41
|
+
"first argument must be a string literal extension name, or an identifier resolving to one",
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
const defArg = args[1];
|
|
@@ -52,7 +52,7 @@ export function extractExtendsRegistrar(
|
|
|
52
52
|
return ok({
|
|
53
53
|
kind: "extendsRegistrar",
|
|
54
54
|
source: sourceLocationFromNode(call, sourceFile),
|
|
55
|
-
extensionName
|
|
55
|
+
extensionName,
|
|
56
56
|
defBody: sourceLocationFromNode(defArg, sourceFile),
|
|
57
57
|
});
|
|
58
58
|
}
|
|
@@ -61,18 +61,19 @@ export function extractUsesApi(
|
|
|
61
61
|
call: CallExpression,
|
|
62
62
|
sourceFile: SourceFile,
|
|
63
63
|
): ExtractOutput<UsesApiPattern> {
|
|
64
|
-
const arg = call.getArguments()[0]
|
|
65
|
-
|
|
64
|
+
const arg = call.getArguments()[0];
|
|
65
|
+
const apiName = arg && readNameLiteral(arg);
|
|
66
|
+
if (!apiName) {
|
|
66
67
|
return fail(
|
|
67
68
|
"usesApi",
|
|
68
69
|
sourceLocationFromNode(call, sourceFile),
|
|
69
|
-
'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
|
|
70
|
+
'expected a single string-literal API name (e.g. "sessions.revokeAllForUser"), or an identifier resolving to one',
|
|
70
71
|
);
|
|
71
72
|
}
|
|
72
73
|
return ok({
|
|
73
74
|
kind: "usesApi",
|
|
74
75
|
source: sourceLocationFromNode(call, sourceFile),
|
|
75
|
-
apiName
|
|
76
|
+
apiName,
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -80,18 +81,19 @@ export function extractExposesApi(
|
|
|
80
81
|
call: CallExpression,
|
|
81
82
|
sourceFile: SourceFile,
|
|
82
83
|
): ExtractOutput<ExposesApiPattern> {
|
|
83
|
-
const arg = call.getArguments()[0]
|
|
84
|
-
|
|
84
|
+
const arg = call.getArguments()[0];
|
|
85
|
+
const apiName = arg && readNameLiteral(arg);
|
|
86
|
+
if (!apiName) {
|
|
85
87
|
return fail(
|
|
86
88
|
"exposesApi",
|
|
87
89
|
sourceLocationFromNode(call, sourceFile),
|
|
88
|
-
'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
|
|
90
|
+
'expected a single string-literal API name (e.g. "sessions.revokeAllForUser"), or an identifier resolving to one',
|
|
89
91
|
);
|
|
90
92
|
}
|
|
91
93
|
return ok({
|
|
92
94
|
kind: "exposesApi",
|
|
93
95
|
source: sourceLocationFromNode(call, sourceFile),
|
|
94
|
-
apiName
|
|
96
|
+
apiName,
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
99
|
|
|
@@ -175,9 +175,65 @@ export function readPropertyKey(propAssign: import("ts-morph").PropertyAssignmen
|
|
|
175
175
|
return propAssign.getName();
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
function unwrapLiteralInitializer(node: Node): string | undefined {
|
|
179
|
+
const literal =
|
|
180
|
+
node.asKind(SyntaxKind.StringLiteral) ?? node.asKind(SyntaxKind.NoSubstitutionTemplateLiteral);
|
|
181
|
+
if (literal) return literal.getLiteralValue();
|
|
182
|
+
const asExpr = node.asKind(SyntaxKind.AsExpression);
|
|
183
|
+
if (asExpr) return unwrapLiteralInitializer(asExpr.getExpression());
|
|
184
|
+
const satisfiesExpr = node.asKind(SyntaxKind.SatisfiesExpression);
|
|
185
|
+
if (satisfiesExpr) return unwrapLiteralInitializer(satisfiesExpr.getExpression());
|
|
186
|
+
const paren = node.asKind(SyntaxKind.ParenthesizedExpression);
|
|
187
|
+
if (paren) return unwrapLiteralInitializer(paren.getExpression());
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolves a bare Identifier to the string value of its declaration's
|
|
193
|
+
* initializer (`export const EXT_TENANT_DATA = "tenant-data" as const`),
|
|
194
|
+
* following imports via ts-morph's definition lookup — works across files
|
|
195
|
+
* and packages against a real-filesystem Project (see #1008 precedent in
|
|
196
|
+
* parse.ts). Only descends into VariableDeclaration initializers; a
|
|
197
|
+
* function/class/type definition or an unresolvable import (external
|
|
198
|
+
* package, ambient declaration) yields undefined, never a throw.
|
|
199
|
+
*/
|
|
200
|
+
function resolveIdentifierToStringLiteral(identifier: Node): string | undefined {
|
|
201
|
+
const id = identifier.asKind(SyntaxKind.Identifier);
|
|
202
|
+
if (!id) return undefined;
|
|
203
|
+
let defs: readonly Node[];
|
|
204
|
+
try {
|
|
205
|
+
defs = id.getDefinitionNodes();
|
|
206
|
+
} catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
for (const def of defs) {
|
|
210
|
+
const init = def.asKind(SyntaxKind.VariableDeclaration)?.getInitializer();
|
|
211
|
+
if (!init) continue;
|
|
212
|
+
const value = unwrapLiteralInitializer(init);
|
|
213
|
+
if (value !== undefined) return value;
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A node's string value when it's a string literal, or when it's a bare
|
|
220
|
+
* Identifier that resolves to one via a `const X = "..."` declaration
|
|
221
|
+
* (same-file or imported) — the pattern used throughout the framework's
|
|
222
|
+
* own bundled-features for registrar-call names (`EXT_TENANT_DATA`,
|
|
223
|
+
* `TENANT_SECRET_READ_EVENT`, ...) instead of repeating string literals.
|
|
224
|
+
* undefined for anything unresolvable (factory call, member access,
|
|
225
|
+
* external/ambient identifier) — callers keep their existing ParseError
|
|
226
|
+
* fallback, no crash.
|
|
227
|
+
*/
|
|
228
|
+
export function readNameLiteral(node: Node): string | undefined {
|
|
179
229
|
const literal = node.asKind(SyntaxKind.StringLiteral);
|
|
180
230
|
if (literal) return literal.getLiteralValue();
|
|
231
|
+
return resolveIdentifierToStringLiteral(node);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function readNameOrRef(node: Node): string | undefined {
|
|
235
|
+
const literal = readNameLiteral(node);
|
|
236
|
+
if (literal !== undefined) return literal;
|
|
181
237
|
const obj = readDataLiteralNode(node);
|
|
182
238
|
if (isPlainObject(obj) && typeof obj["name"] === "string") return obj["name"];
|
|
183
239
|
return undefined;
|