@aws-blocks/core 0.4.0 → 0.5.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/README.md +12 -0
- package/dist/cdk/blocks-backend.d.ts +4 -1
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +56 -10
- package/dist/cdk/blocks-backend.test.js +71 -1
- package/dist/cdk/blocks-defaults.d.ts +11 -0
- package/dist/cdk/blocks-defaults.d.ts.map +1 -1
- package/dist/cdk/blocks-stack.test.js +23 -3
- package/dist/cdk/compute/compute.d.ts +80 -2
- package/dist/cdk/compute/compute.d.ts.map +1 -1
- package/dist/cdk/compute/compute.js +57 -3
- package/dist/cdk/config-registry.test.js +12 -0
- package/dist/cdk/dashboard-registry.d.ts +41 -0
- package/dist/cdk/dashboard-registry.d.ts.map +1 -0
- package/dist/cdk/dashboard-registry.js +61 -0
- package/dist/cdk/index.d.ts +67 -11
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +100 -18
- package/dist/cdk/internal.d.ts +3 -1
- package/dist/cdk/internal.d.ts.map +1 -1
- package/dist/cdk/internal.js +4 -1
- package/dist/cdk/tracer-registry.d.ts +31 -0
- package/dist/cdk/tracer-registry.d.ts.map +1 -0
- package/dist/cdk/tracer-registry.js +49 -0
- package/dist/cdk/vpc-requirements-registry.d.ts +33 -0
- package/dist/cdk/vpc-requirements-registry.d.ts.map +1 -0
- package/dist/cdk/vpc-requirements-registry.js +46 -0
- package/dist/cdk/vpc-types.d.ts +151 -0
- package/dist/cdk/vpc-types.d.ts.map +1 -0
- package/dist/cdk/vpc-types.js +3 -0
- package/dist/cdk/vpc.d.ts +59 -0
- package/dist/cdk/vpc.d.ts.map +1 -0
- package/dist/cdk/vpc.js +298 -0
- package/dist/cdk/vpc.test.d.ts +2 -0
- package/dist/cdk/vpc.test.d.ts.map +1 -0
- package/dist/cdk/vpc.test.js +285 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -0
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +2 -0
- package/dist/hosting.test.js +39 -1
- package/dist/index.cdk.d.ts +2 -1
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +1 -1
- package/dist/lambda-handler.js +9 -2
- package/dist/lambda-handler.test.js +61 -1
- package/dist/raw-route.d.ts +15 -1
- package/dist/raw-route.d.ts.map +1 -1
- package/dist/raw-route.js +96 -12
- package/dist/raw-route.test.js +332 -1
- package/dist/scripts/dev-server.d.ts.map +1 -1
- package/dist/scripts/dev-server.js +11 -0
- package/dist/scripts/extract-ts-types.d.ts.map +1 -1
- package/dist/scripts/extract-ts-types.js +107 -23
- package/dist/scripts/extract-ts-types.test.js +225 -26
- package/dist/scripts/generate-spec.d.ts.map +1 -1
- package/dist/scripts/generate-spec.js +14 -5
- package/dist/scripts/generate-spec.test.js +93 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -1
- package/src/cdk/blocks-backend.test.ts +144 -60
- package/src/cdk/blocks-backend.ts +298 -239
- package/src/cdk/blocks-defaults.ts +12 -0
- package/src/cdk/blocks-stack.test.ts +32 -13
- package/src/cdk/compute/compute.ts +105 -3
- package/src/cdk/config-registry.test.ts +14 -0
- package/src/cdk/dashboard-registry.ts +68 -0
- package/src/cdk/index.ts +426 -298
- package/src/cdk/internal.ts +6 -2
- package/src/cdk/tracer-registry.ts +54 -0
- package/src/cdk/vpc-requirements-registry.ts +63 -0
- package/src/cdk/vpc-types.ts +158 -0
- package/src/cdk/vpc.test.ts +348 -0
- package/src/cdk/vpc.ts +336 -0
- package/src/errors.ts +5 -0
- package/src/hosting.test.ts +59 -1
- package/src/hosting.ts +3 -0
- package/src/index.cdk.ts +7 -0
- package/src/lambda-handler.test.ts +79 -1
- package/src/lambda-handler.ts +11 -2
- package/src/raw-route.test.ts +427 -1
- package/src/raw-route.ts +125 -12
- package/src/scripts/dev-server.ts +12 -1
- package/src/scripts/extract-ts-types.test.ts +228 -26
- package/src/scripts/extract-ts-types.ts +104 -20
- package/src/scripts/generate-spec.test.ts +101 -0
- package/src/scripts/generate-spec.ts +15 -5
- package/src/version.ts +1 -1
|
@@ -46,13 +46,16 @@ export function extractSkipCodegenMethods(sourcePath) {
|
|
|
46
46
|
return result;
|
|
47
47
|
const sourceFile = ts.createSourceFile(absPath, fileText, ts.ScriptTarget.ESNext,
|
|
48
48
|
/*setParentNodes*/ true, ts.ScriptKind.TS);
|
|
49
|
-
function visitObjectLiteral(obj) {
|
|
49
|
+
function visitObjectLiteral(obj, namespaceName) {
|
|
50
50
|
for (const prop of obj.properties) {
|
|
51
51
|
if (ts.isMethodDeclaration(prop) &&
|
|
52
52
|
prop.name &&
|
|
53
53
|
ts.isIdentifier(prop.name) &&
|
|
54
54
|
hasBlocksSkipCodegenTag(prop)) {
|
|
55
|
-
|
|
55
|
+
// Qualify by namespace (like extractMethodTypes) so a tag on one
|
|
56
|
+
// namespace's `create` doesn't cause another namespace's `create` to be
|
|
57
|
+
// dropped from the spec. Bare key only when the namespace is unknown.
|
|
58
|
+
result.add(namespaceName ? `${namespaceName}.${prop.name.text}` : prop.name.text);
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -61,23 +64,24 @@ export function extractSkipCodegenMethods(sourcePath) {
|
|
|
61
64
|
const callee = node.expression;
|
|
62
65
|
if (ts.isIdentifier(callee) && callee.text === 'ApiNamespace' && node.arguments && node.arguments.length >= 2) {
|
|
63
66
|
const handler = node.arguments[node.arguments.length - 1];
|
|
67
|
+
const nsName = enclosingBindingName(node);
|
|
64
68
|
if (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) {
|
|
65
69
|
const body = handler.body;
|
|
66
70
|
if (ts.isParenthesizedExpression(body) && ts.isObjectLiteralExpression(body.expression)) {
|
|
67
|
-
visitObjectLiteral(body.expression);
|
|
71
|
+
visitObjectLiteral(body.expression, nsName);
|
|
68
72
|
}
|
|
69
73
|
else if (ts.isObjectLiteralExpression(body)) {
|
|
70
|
-
visitObjectLiteral(body);
|
|
74
|
+
visitObjectLiteral(body, nsName);
|
|
71
75
|
}
|
|
72
76
|
else if (ts.isBlock(body)) {
|
|
73
77
|
for (const stmt of body.statements) {
|
|
74
78
|
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
75
79
|
if (ts.isObjectLiteralExpression(stmt.expression)) {
|
|
76
|
-
visitObjectLiteral(stmt.expression);
|
|
80
|
+
visitObjectLiteral(stmt.expression, nsName);
|
|
77
81
|
}
|
|
78
82
|
else if (ts.isParenthesizedExpression(stmt.expression) &&
|
|
79
83
|
ts.isObjectLiteralExpression(stmt.expression.expression)) {
|
|
80
|
-
visitObjectLiteral(stmt.expression.expression);
|
|
84
|
+
visitObjectLiteral(stmt.expression.expression, nsName);
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
}
|
|
@@ -135,7 +139,7 @@ export function extractMethodTypes(sourcePath) {
|
|
|
135
139
|
const fnName = node.expression;
|
|
136
140
|
if (ts.isIdentifier(fnName) && fnName.text === 'ApiNamespace' && node.arguments && node.arguments.length >= 2) {
|
|
137
141
|
const handlerArg = node.arguments[node.arguments.length - 1];
|
|
138
|
-
extractMethodsFromHandler(handlerArg, checker, sourceFile, result);
|
|
142
|
+
extractMethodsFromHandler(handlerArg, checker, sourceFile, result, enclosingBindingName(node));
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
145
|
ts.forEachChild(node, visit);
|
|
@@ -148,19 +152,67 @@ export function extractMethodTypes(sourcePath) {
|
|
|
148
152
|
if (!ts.isVariableStatement(node))
|
|
149
153
|
return;
|
|
150
154
|
for (const decl of node.declarationList.declarations) {
|
|
151
|
-
if (!decl.initializer
|
|
155
|
+
if (!decl.initializer)
|
|
156
|
+
continue;
|
|
157
|
+
// (a) Simple binding: `const api = auth.createApi()` /
|
|
158
|
+
// `export const api = backend.namespace`.
|
|
159
|
+
if (ts.isIdentifier(decl.name)) {
|
|
160
|
+
// Skip if we already extracted methods for this via the AST walk
|
|
161
|
+
if (ts.isNewExpression(decl.initializer)) {
|
|
162
|
+
const callee = decl.initializer.expression;
|
|
163
|
+
if (ts.isIdentifier(callee) && callee.text === 'ApiNamespace')
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Use the type checker to get the type of the initializer expression.
|
|
167
|
+
// For `auth.createApi()`, this resolves through the method's return type
|
|
168
|
+
// to the `AsyncAPI<T>` type that ApiNamespace returns.
|
|
169
|
+
const initType = checker.getTypeAtLocation(decl.initializer);
|
|
170
|
+
extractMethodsFromResolvedType(initType, checker, result, decl.name.text);
|
|
152
171
|
continue;
|
|
153
|
-
// Skip if we already extracted methods for this via the AST walk
|
|
154
|
-
if (ts.isNewExpression(decl.initializer)) {
|
|
155
|
-
const callee = decl.initializer.expression;
|
|
156
|
-
if (ts.isIdentifier(callee) && callee.text === 'ApiNamespace')
|
|
157
|
-
continue;
|
|
158
172
|
}
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
// (b) Object binding pattern: `const { indirectNamespace } = factory()`.
|
|
174
|
+
// Each destructured binding is a separate namespace whose name is the
|
|
175
|
+
// LOCAL binding name (the name it's exported under). Resolve the property
|
|
176
|
+
// type off the initializer and extract methods keyed by that name, so a
|
|
177
|
+
// factory-returned namespace keys qualified (`indirectNamespace.method`)
|
|
178
|
+
// like a directly-constructed one — instead of landing on a bare key that
|
|
179
|
+
// collides with same-named methods in other namespaces (#444, #445).
|
|
180
|
+
//
|
|
181
|
+
// This adds the qualified key ALONGSIDE the bare key the first-pass AST
|
|
182
|
+
// walk already emitted for the same `new ApiNamespace(...)` (its
|
|
183
|
+
// `enclosingBindingName` returns undefined behind the factory, so it keys
|
|
184
|
+
// bare). Leaving the bare key is harmless: `generate-spec` looks up
|
|
185
|
+
// `tsTypes.get(qualified) ?? tsTypes.get(bare)`, so the qualified key
|
|
186
|
+
// added here always wins for an attributable namespace, and the bare
|
|
187
|
+
// entry never influences the emitted schema (a regression test asserts
|
|
188
|
+
// this under a name collision). The #498 bare-name fallback is retained
|
|
189
|
+
// on purpose — it's the last resort for namespaces this pass still can't
|
|
190
|
+
// attribute (default exports, deeper indirection, the patterns below), so
|
|
191
|
+
// it is NOT redundant.
|
|
192
|
+
//
|
|
193
|
+
// Boundary: only a top-level identifier (a) and a shallow object binding
|
|
194
|
+
// pattern (b) are attributed. Array patterns (`const [ns] = factory()`)
|
|
195
|
+
// and nested patterns (`const { a: { b } } = …`) fall through to the
|
|
196
|
+
// bare-key path, where the #498 fallback still resolves the schema when
|
|
197
|
+
// there's no collision. These shapes are rare for namespace exports and
|
|
198
|
+
// fail soft; widening the traversal can come later if a real case appears.
|
|
199
|
+
if (ts.isObjectBindingPattern(decl.name)) {
|
|
200
|
+
const initType = checker.getTypeAtLocation(decl.initializer);
|
|
201
|
+
for (const element of decl.name.elements) {
|
|
202
|
+
if (!ts.isIdentifier(element.name))
|
|
203
|
+
continue;
|
|
204
|
+
const localName = element.name.text;
|
|
205
|
+
// `const { foo: bar } = …` binds `bar` but reads property `foo`.
|
|
206
|
+
const propKey = element.propertyName && ts.isIdentifier(element.propertyName)
|
|
207
|
+
? element.propertyName.text
|
|
208
|
+
: localName;
|
|
209
|
+
const propSymbol = initType.getProperty(propKey);
|
|
210
|
+
if (!propSymbol)
|
|
211
|
+
continue;
|
|
212
|
+
const propType = checker.getTypeOfSymbol(propSymbol);
|
|
213
|
+
extractMethodsFromResolvedType(propType, checker, result, localName);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
164
216
|
}
|
|
165
217
|
});
|
|
166
218
|
return result;
|
|
@@ -180,7 +232,7 @@ function findTsConfig(dir) {
|
|
|
180
232
|
return undefined;
|
|
181
233
|
}
|
|
182
234
|
// ── Handler extraction ──────────────────────────────────────────────────────
|
|
183
|
-
function extractMethodsFromHandler(handlerNode, checker, sourceFile, result) {
|
|
235
|
+
function extractMethodsFromHandler(handlerNode, checker, sourceFile, result, namespaceName) {
|
|
184
236
|
let objectLiteral;
|
|
185
237
|
if (ts.isArrowFunction(handlerNode) || ts.isFunctionExpression(handlerNode)) {
|
|
186
238
|
const body = handlerNode.body;
|
|
@@ -212,11 +264,42 @@ function extractMethodsFromHandler(handlerNode, checker, sourceFile, result) {
|
|
|
212
264
|
if (ts.isMethodDeclaration(prop) && prop.name && ts.isIdentifier(prop.name)) {
|
|
213
265
|
const methodName = prop.name.text;
|
|
214
266
|
const info = extractMethodTypeInfo(prop, checker, sourceFile);
|
|
267
|
+
// Key by the namespace-qualified name when the namespace is known, so two
|
|
268
|
+
// namespaces sharing a method name (e.g. widgets.create / subscriptions.create)
|
|
269
|
+
// don't collide and cross-assign schemas. Fall back to the bare name when the
|
|
270
|
+
// namespace can't be determined (consumers try qualified, then bare).
|
|
215
271
|
if (info)
|
|
216
|
-
result.set(methodName, info);
|
|
272
|
+
result.set(namespaceName ? `${namespaceName}.${methodName}` : methodName, info);
|
|
217
273
|
}
|
|
218
274
|
}
|
|
219
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Walk up from a `new ApiNamespace(...)` node to the name it's bound to
|
|
278
|
+
* (`export const widgets = new ApiNamespace(...)` → `"widgets"`). This is the
|
|
279
|
+
* name the dev server / Lambda handler route by, and the one `generate-spec`
|
|
280
|
+
* uses to qualify method names. Returns `undefined` when the namespace is
|
|
281
|
+
* created behind indirection (factory return, etc.), in which case methods key
|
|
282
|
+
* by their bare name.
|
|
283
|
+
*
|
|
284
|
+
* Limitation: this is the local binding name, not the export name. An export
|
|
285
|
+
* rename — `const widgets = new ApiNamespace(...); export { widgets as gadgets }`
|
|
286
|
+
* — keys `widgets.*` while `generate-spec` looks up `gadgets.*`, so the qualified
|
|
287
|
+
* lookup misses and falls back to the bare key (schemas resolve if there's no
|
|
288
|
+
* collision, else `unknown`). Rare; resolving the export alias would remove it.
|
|
289
|
+
*/
|
|
290
|
+
function enclosingBindingName(node) {
|
|
291
|
+
let current = node.parent;
|
|
292
|
+
while (current) {
|
|
293
|
+
if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name))
|
|
294
|
+
return current.name.text;
|
|
295
|
+
// Don't cross a function/block boundary — beyond it the binding no longer
|
|
296
|
+
// corresponds to a directly-exported namespace.
|
|
297
|
+
if (ts.isSourceFile(current) || ts.isBlock(current) || ts.isFunctionLike(current))
|
|
298
|
+
return undefined;
|
|
299
|
+
current = current.parent;
|
|
300
|
+
}
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
220
303
|
function extractMethodTypeInfo(method, checker, sourceFile) {
|
|
221
304
|
const params = [];
|
|
222
305
|
for (const param of method.parameters) {
|
|
@@ -284,7 +367,7 @@ function unwrapPromiseTypeNode(node) {
|
|
|
284
367
|
* We also handle the case where the type is callable (the raw handler function
|
|
285
368
|
* before `AsyncAPI` wrapping) — we call-resolve it and inspect the result.
|
|
286
369
|
*/
|
|
287
|
-
function extractMethodsFromResolvedType(type, checker, result) {
|
|
370
|
+
function extractMethodsFromResolvedType(type, checker, result, namespaceName) {
|
|
288
371
|
// The runtime value of an ApiNamespace export is the handler function itself
|
|
289
372
|
// (with the marker symbol attached). Its TS type is `AsyncAPI<T>`, which is
|
|
290
373
|
// an object type with method-like properties. But it could also be typed as
|
|
@@ -309,8 +392,9 @@ function extractMethodsFromResolvedType(type, checker, result) {
|
|
|
309
392
|
// Skip internal symbols and the marker
|
|
310
393
|
if (propName.startsWith('_') || propName === 'Symbol(blocks:ApiNamespace)')
|
|
311
394
|
continue;
|
|
395
|
+
const key = namespaceName ? `${namespaceName}.${propName}` : propName;
|
|
312
396
|
// Don't overwrite methods already found by the AST walk
|
|
313
|
-
if (result.has(
|
|
397
|
+
if (result.has(key))
|
|
314
398
|
continue;
|
|
315
399
|
const propType = checker.getTypeOfSymbol(prop);
|
|
316
400
|
const methodSigs = propType.getCallSignatures();
|
|
@@ -346,7 +430,7 @@ function extractMethodsFromResolvedType(type, checker, result) {
|
|
|
346
430
|
if (declaration && hasBlocksSkipCodegenTag(declaration)) {
|
|
347
431
|
skipCodegen = true;
|
|
348
432
|
}
|
|
349
|
-
result.set(
|
|
433
|
+
result.set(key, { params, returnType, transferable, skipCodegen });
|
|
350
434
|
}
|
|
351
435
|
}
|
|
352
436
|
// ── Transferable detection ───────────────────────────────────────────────────
|
|
@@ -25,6 +25,45 @@ const API_NS_MOCK = `
|
|
|
25
25
|
interface ApiNamespaceConstructor { new <T>(scope: any, name: string, handler: (ctx: any) => T): T; }
|
|
26
26
|
const ApiNamespace: ApiNamespaceConstructor = class {} as any;
|
|
27
27
|
`;
|
|
28
|
+
describe('extractMethodTypes — namespaces sharing a method name (regression: #445)', () => {
|
|
29
|
+
it('keys methods by namespace, so a shared method name does not cross-assign schemas', () => {
|
|
30
|
+
const dir = createTempProject({
|
|
31
|
+
'tsconfig.json': JSON.stringify({
|
|
32
|
+
compilerOptions: { target: 'ESNext', module: 'ESNext', moduleResolution: 'bundler', strict: true },
|
|
33
|
+
}),
|
|
34
|
+
'index.ts': `
|
|
35
|
+
${API_NS_MOCK}
|
|
36
|
+
|
|
37
|
+
// Two namespaces both expose \`create\`, with deliberately incompatible contracts.
|
|
38
|
+
export const widgets = new ApiNamespace(null, 'widgets', (context) => ({
|
|
39
|
+
async create(label: string): Promise<{ widgetId: string }> {
|
|
40
|
+
return { widgetId: 'w' };
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
export const subscriptions = new ApiNamespace(null, 'subscriptions', (context) => ({
|
|
44
|
+
async create(topicCount: number): Promise<{ subId: number }> {
|
|
45
|
+
return { subId: 1 };
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
`,
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
52
|
+
// Distinct qualified keys — no collision.
|
|
53
|
+
assert.ok(types.has('widgets.create'), 'widgets.create should be keyed by namespace');
|
|
54
|
+
assert.ok(types.has('subscriptions.create'), 'subscriptions.create should be keyed by namespace');
|
|
55
|
+
assert.ok(!types.has('create'), 'no bare-name entry that could cross-assign');
|
|
56
|
+
// Each keeps its OWN parameter type.
|
|
57
|
+
assert.strictEqual(types.get('widgets.create').params[0].name, 'label');
|
|
58
|
+
assert.strictEqual(types.get('widgets.create').params[0].schema.type, 'string');
|
|
59
|
+
assert.strictEqual(types.get('subscriptions.create').params[0].name, 'topicCount');
|
|
60
|
+
assert.strictEqual(types.get('subscriptions.create').params[0].schema.type, 'number');
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
28
67
|
describe('extractMethodTypes — direct ApiNamespace calls', () => {
|
|
29
68
|
it('extracts param and return types from inline ApiNamespace', () => {
|
|
30
69
|
const dir = createTempProject({
|
|
@@ -53,13 +92,13 @@ describe('extractMethodTypes — direct ApiNamespace calls', () => {
|
|
|
53
92
|
});
|
|
54
93
|
try {
|
|
55
94
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
56
|
-
assert.ok(types.has('getUser'), 'should find getUser');
|
|
57
|
-
assert.ok(types.has('createUser'), 'should find createUser');
|
|
58
|
-
const getUser = types.get('getUser');
|
|
95
|
+
assert.ok(types.has('api.getUser'), 'should find getUser');
|
|
96
|
+
assert.ok(types.has('api.createUser'), 'should find createUser');
|
|
97
|
+
const getUser = types.get('api.getUser');
|
|
59
98
|
assert.strictEqual(getUser.params.length, 1);
|
|
60
99
|
assert.strictEqual(getUser.params[0].name, 'id');
|
|
61
100
|
assert.strictEqual(getUser.params[0].schema.type, 'string');
|
|
62
|
-
const createUser = types.get('createUser');
|
|
101
|
+
const createUser = types.get('api.createUser');
|
|
63
102
|
assert.strictEqual(createUser.params.length, 2);
|
|
64
103
|
assert.strictEqual(createUser.params[0].schema.type, 'string');
|
|
65
104
|
assert.strictEqual(createUser.params[1].schema.type, 'number');
|
|
@@ -94,9 +133,34 @@ describe('extractSkipCodegenMethods — pure-AST scan', () => {
|
|
|
94
133
|
try {
|
|
95
134
|
const skips = extractSkipCodegenMethods(join(dir, 'index.ts'));
|
|
96
135
|
assert.strictEqual(skips.size, 1);
|
|
97
|
-
assert.ok(skips.has('devOnly'));
|
|
98
|
-
assert.ok(!skips.has('normalMethod'));
|
|
99
|
-
assert.ok(!skips.has('alsoNormal'));
|
|
136
|
+
assert.ok(skips.has('api.devOnly'), 'skip set is namespace-qualified');
|
|
137
|
+
assert.ok(!skips.has('api.normalMethod'));
|
|
138
|
+
assert.ok(!skips.has('api.alsoNormal'));
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
rmSync(dir, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
it('qualifies skip names by namespace so a tag on one namespace does not drop another (regression: #445)', () => {
|
|
145
|
+
const dir = createTempProject({
|
|
146
|
+
'index.ts': `
|
|
147
|
+
class ApiNamespace { constructor(scope: any, name: any, handler: any) {} }
|
|
148
|
+
|
|
149
|
+
export const widgets = new ApiNamespace(null, 'widgets', () => ({
|
|
150
|
+
/** @blocksSkipCodegen */
|
|
151
|
+
async create() { return null; },
|
|
152
|
+
}));
|
|
153
|
+
// subscriptions.create is NOT tagged and must survive.
|
|
154
|
+
export const subscriptions = new ApiNamespace(null, 'subscriptions', () => ({
|
|
155
|
+
async create() { return null; },
|
|
156
|
+
}));
|
|
157
|
+
`,
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
const skips = extractSkipCodegenMethods(join(dir, 'index.ts'));
|
|
161
|
+
assert.ok(skips.has('widgets.create'), 'tagged method is skipped, qualified');
|
|
162
|
+
assert.ok(!skips.has('subscriptions.create'), 'untagged same-named method must NOT be skipped');
|
|
163
|
+
assert.ok(!skips.has('create'), 'no bare key that would drop both');
|
|
100
164
|
}
|
|
101
165
|
finally {
|
|
102
166
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -146,8 +210,8 @@ describe('extractMethodTypes — @blocksSkipCodegen JSDoc tag', () => {
|
|
|
146
210
|
});
|
|
147
211
|
try {
|
|
148
212
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
149
|
-
assert.strictEqual(types.get('normalMethod')?.skipCodegen, undefined);
|
|
150
|
-
assert.strictEqual(types.get('getLastCode')?.skipCodegen, true);
|
|
213
|
+
assert.strictEqual(types.get('api.normalMethod')?.skipCodegen, undefined);
|
|
214
|
+
assert.strictEqual(types.get('api.getLastCode')?.skipCodegen, true);
|
|
151
215
|
}
|
|
152
216
|
finally {
|
|
153
217
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -206,20 +270,20 @@ describe('extractMethodTypes — indirect ApiNamespace calls (e.g., auth.createA
|
|
|
206
270
|
try {
|
|
207
271
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
208
272
|
// Direct ApiNamespace methods should still work
|
|
209
|
-
assert.ok(types.has('greet'), 'should find greet from direct ApiNamespace');
|
|
210
|
-
const greet = types.get('greet');
|
|
273
|
+
assert.ok(types.has('api.greet'), 'should find greet from direct ApiNamespace');
|
|
274
|
+
const greet = types.get('api.greet');
|
|
211
275
|
assert.strictEqual(greet.params[0].name, 'name');
|
|
212
276
|
assert.strictEqual(greet.params[0].schema.type, 'string');
|
|
213
277
|
// Indirect methods from auth.createApi() should now be resolved
|
|
214
|
-
assert.ok(types.has('getAuthState'), 'should find getAuthState from auth.createApi()');
|
|
215
|
-
assert.ok(types.has('setAuthState'), 'should find setAuthState from auth.createApi()');
|
|
216
|
-
const getAuthState = types.get('getAuthState');
|
|
278
|
+
assert.ok(types.has('authApi.getAuthState'), 'should find getAuthState from auth.createApi()');
|
|
279
|
+
assert.ok(types.has('authApi.setAuthState'), 'should find setAuthState from auth.createApi()');
|
|
280
|
+
const getAuthState = types.get('authApi.getAuthState');
|
|
217
281
|
assert.strictEqual(getAuthState.params.length, 0, 'getAuthState has no params');
|
|
218
282
|
assert.strictEqual(getAuthState.returnType.type, 'object', 'return type should be object');
|
|
219
283
|
assert.ok(getAuthState.returnType.properties, 'return type should have properties');
|
|
220
284
|
assert.ok('isSignedIn' in getAuthState.returnType.properties, 'should have isSignedIn');
|
|
221
285
|
assert.ok('username' in getAuthState.returnType.properties, 'should have username');
|
|
222
|
-
const setAuthState = types.get('setAuthState');
|
|
286
|
+
const setAuthState = types.get('authApi.setAuthState');
|
|
223
287
|
assert.strictEqual(setAuthState.params.length, 2, 'setAuthState has 2 params');
|
|
224
288
|
assert.strictEqual(setAuthState.params[0].name, 'action');
|
|
225
289
|
assert.strictEqual(setAuthState.params[0].schema.type, 'string');
|
|
@@ -255,9 +319,9 @@ describe('extractMethodTypes — indirect ApiNamespace calls (e.g., auth.createA
|
|
|
255
319
|
});
|
|
256
320
|
try {
|
|
257
321
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
258
|
-
assert.ok(types.has('greet'), 'should find greet');
|
|
322
|
+
assert.ok(types.has('api.greet'), 'should find greet');
|
|
259
323
|
// The AST-walk version should be preserved (it has richer info from the declaration)
|
|
260
|
-
assert.strictEqual(types.get('greet').params[0].name, 'name');
|
|
324
|
+
assert.strictEqual(types.get('api.greet').params[0].name, 'name');
|
|
261
325
|
}
|
|
262
326
|
finally {
|
|
263
327
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -315,7 +379,7 @@ describe('extractMethodTypes — transferable detection', () => {
|
|
|
315
379
|
`,
|
|
316
380
|
});
|
|
317
381
|
try {
|
|
318
|
-
const info = extractMethodTypes(join(dir, 'index.ts')).get('getCursorChannel');
|
|
382
|
+
const info = extractMethodTypes(join(dir, 'index.ts')).get('api.getCursorChannel');
|
|
319
383
|
assert.ok(info, 'method should be discovered');
|
|
320
384
|
assert.ok(info.transferable, 'should be detected as transferable');
|
|
321
385
|
assert.strictEqual(info.transferable.blocksTag, 'realtime/channel');
|
|
@@ -340,7 +404,7 @@ describe('extractMethodTypes — transferable detection', () => {
|
|
|
340
404
|
`,
|
|
341
405
|
});
|
|
342
406
|
try {
|
|
343
|
-
const info = extractMethodTypes(join(dir, 'index.ts')).get('getDownload');
|
|
407
|
+
const info = extractMethodTypes(join(dir, 'index.ts')).get('api.getDownload');
|
|
344
408
|
assert.ok(info?.transferable);
|
|
345
409
|
assert.strictEqual(info.transferable.blocksTag, 'file-bucket/download');
|
|
346
410
|
assert.deepStrictEqual(info.transferable.typeArgs, []);
|
|
@@ -362,8 +426,8 @@ describe('extractMethodTypes — transferable detection', () => {
|
|
|
362
426
|
});
|
|
363
427
|
try {
|
|
364
428
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
365
|
-
assert.deepStrictEqual(types.get('defaulted')?.transferable?.typeArgs, []);
|
|
366
|
-
assert.deepStrictEqual(types.get('explicit')?.transferable?.typeArgs, [{}]);
|
|
429
|
+
assert.deepStrictEqual(types.get('api.defaulted')?.transferable?.typeArgs, []);
|
|
430
|
+
assert.deepStrictEqual(types.get('api.explicit')?.transferable?.typeArgs, [{}]);
|
|
367
431
|
}
|
|
368
432
|
finally {
|
|
369
433
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -384,8 +448,8 @@ describe('extractMethodTypes — transferable detection', () => {
|
|
|
384
448
|
});
|
|
385
449
|
try {
|
|
386
450
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
387
|
-
assert.strictEqual(types.get('plain')?.transferable, undefined);
|
|
388
|
-
assert.strictEqual(types.get('loose')?.transferable, undefined);
|
|
451
|
+
assert.strictEqual(types.get('api.plain')?.transferable, undefined);
|
|
452
|
+
assert.strictEqual(types.get('api.loose')?.transferable, undefined);
|
|
389
453
|
}
|
|
390
454
|
finally {
|
|
391
455
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -411,7 +475,7 @@ describe('extractMethodTypes — transferable detection', () => {
|
|
|
411
475
|
`,
|
|
412
476
|
});
|
|
413
477
|
try {
|
|
414
|
-
const info = extractMethodTypes(join(dir, 'index.ts')).get('getNotifications');
|
|
478
|
+
const info = extractMethodTypes(join(dir, 'index.ts')).get('api.getNotifications');
|
|
415
479
|
assert.ok(info?.transferable);
|
|
416
480
|
assert.strictEqual(info.transferable.blocksTag, 'realtime/channel');
|
|
417
481
|
assert.strictEqual(info.transferable.typeArgs[0]?.title, 'Notification');
|
|
@@ -445,7 +509,7 @@ describe('extractMethodTypes — open-shape intersections', () => {
|
|
|
445
509
|
`,
|
|
446
510
|
});
|
|
447
511
|
try {
|
|
448
|
-
const info = extractMethodTypes(join(dir, 'index.ts')).get('signUp');
|
|
512
|
+
const info = extractMethodTypes(join(dir, 'index.ts')).get('api.signUp');
|
|
449
513
|
assert.ok(info, 'should find signUp');
|
|
450
514
|
const inputSchema = info.params[0].schema;
|
|
451
515
|
assert.strictEqual(inputSchema.type, 'object');
|
|
@@ -482,7 +546,7 @@ describe('extractMethodTypes — open-shape intersections', () => {
|
|
|
482
546
|
`,
|
|
483
547
|
});
|
|
484
548
|
try {
|
|
485
|
-
const info = extractMethodTypes(join(dir, 'index.ts')).get('tag');
|
|
549
|
+
const info = extractMethodTypes(join(dir, 'index.ts')).get('api.tag');
|
|
486
550
|
assert.ok(info, 'should find tag');
|
|
487
551
|
const inputSchema = info.params[0].schema;
|
|
488
552
|
assert.strictEqual(inputSchema.type, 'object');
|
|
@@ -495,3 +559,138 @@ describe('extractMethodTypes — open-shape intersections', () => {
|
|
|
495
559
|
}
|
|
496
560
|
});
|
|
497
561
|
});
|
|
562
|
+
describe('extractMethodTypes — namespace returned by a factory, then destructured (regression: #444)', () => {
|
|
563
|
+
it('keys a destructured factory namespace qualified, with real schemas', () => {
|
|
564
|
+
const dir = createTempProject({
|
|
565
|
+
'tsconfig.json': JSON.stringify({
|
|
566
|
+
compilerOptions: { target: 'ESNext', module: 'ESNext', moduleResolution: 'bundler', strict: true },
|
|
567
|
+
}),
|
|
568
|
+
'index.ts': `
|
|
569
|
+
${API_NS_MOCK}
|
|
570
|
+
class Scope { constructor(id: string) {} }
|
|
571
|
+
|
|
572
|
+
class NamespaceFactory {
|
|
573
|
+
constructor(private readonly scope: Scope) {}
|
|
574
|
+
createNamespaces() {
|
|
575
|
+
return {
|
|
576
|
+
indirectNamespace: new ApiNamespace(this.scope, 'indirectNamespace', () => ({
|
|
577
|
+
async getIndirectGreeting(times: number): Promise<string> { return 'hi'; },
|
|
578
|
+
})),
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const scope = new Scope('repro');
|
|
584
|
+
export const directNamespace = new ApiNamespace(scope, 'directNamespace', () => ({
|
|
585
|
+
async getDirectGreeting(name: string): Promise<number> { return name.length; },
|
|
586
|
+
}));
|
|
587
|
+
const { indirectNamespace } = new NamespaceFactory(scope).createNamespaces();
|
|
588
|
+
export { indirectNamespace };
|
|
589
|
+
`,
|
|
590
|
+
});
|
|
591
|
+
try {
|
|
592
|
+
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
593
|
+
// The factory-returned namespace is now attributed to its binding name,
|
|
594
|
+
// exactly like the directly-constructed one.
|
|
595
|
+
const indirect = types.get('indirectNamespace.getIndirectGreeting');
|
|
596
|
+
assert.ok(indirect, 'indirectNamespace.getIndirectGreeting should be keyed by namespace');
|
|
597
|
+
assert.strictEqual(indirect.params[0].schema.type, 'number');
|
|
598
|
+
assert.strictEqual(indirect.returnType.type, 'string');
|
|
599
|
+
const direct = types.get('directNamespace.getDirectGreeting');
|
|
600
|
+
assert.ok(direct, 'directNamespace.getDirectGreeting should be present');
|
|
601
|
+
assert.strictEqual(direct.params[0].schema.type, 'string');
|
|
602
|
+
assert.strictEqual(direct.returnType.type, 'number');
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
rmSync(dir, { recursive: true, force: true });
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
it('a factory namespace sharing a method name with a direct one does NOT cross-assign', () => {
|
|
609
|
+
const dir = createTempProject({
|
|
610
|
+
'tsconfig.json': JSON.stringify({
|
|
611
|
+
compilerOptions: { target: 'ESNext', module: 'ESNext', moduleResolution: 'bundler', strict: true },
|
|
612
|
+
}),
|
|
613
|
+
'index.ts': `
|
|
614
|
+
${API_NS_MOCK}
|
|
615
|
+
class Scope { constructor(id: string) {} }
|
|
616
|
+
|
|
617
|
+
class Factory {
|
|
618
|
+
constructor(private readonly scope: Scope) {}
|
|
619
|
+
build() {
|
|
620
|
+
return {
|
|
621
|
+
subscriptions: new ApiNamespace(this.scope, 'subscriptions', () => ({
|
|
622
|
+
async create(topicCount: number): Promise<{ subId: number }> { return { subId: 1 }; },
|
|
623
|
+
})),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const scope = new Scope('app');
|
|
629
|
+
export const widgets = new ApiNamespace(scope, 'widgets', () => ({
|
|
630
|
+
async create(label: string): Promise<{ widgetId: string }> { return { widgetId: 'w' }; },
|
|
631
|
+
}));
|
|
632
|
+
const { subscriptions } = new Factory(scope).build();
|
|
633
|
+
export { subscriptions };
|
|
634
|
+
`,
|
|
635
|
+
});
|
|
636
|
+
try {
|
|
637
|
+
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
638
|
+
// Each namespace keeps its OWN param type under a distinct qualified key.
|
|
639
|
+
assert.strictEqual(types.get('widgets.create')?.params[0].schema?.type, 'string');
|
|
640
|
+
assert.strictEqual(types.get('subscriptions.create')?.params[0].schema?.type, 'number');
|
|
641
|
+
// The first-pass AST walk still emits a lingering bare `create` for the
|
|
642
|
+
// factory namespace, but it never overrides a qualified key: the
|
|
643
|
+
// direct namespace's qualified `widgets.create` is unaffected above, and
|
|
644
|
+
// `generate-spec` prefers the qualified key (see the collision test in
|
|
645
|
+
// generate-spec.test.ts, which asserts this end-to-end in the output).
|
|
646
|
+
// So the bare entry can't reintroduce the #445 cross-assignment here.
|
|
647
|
+
assert.strictEqual(types.get('widgets.create')?.params[0].schema?.type, 'string');
|
|
648
|
+
}
|
|
649
|
+
finally {
|
|
650
|
+
rmSync(dir, { recursive: true, force: true });
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
it('array/nested destructuring falls through to the bare key (documented boundary)', () => {
|
|
654
|
+
// Only a top-level identifier and a shallow object binding pattern are
|
|
655
|
+
// attributed to a namespace. An array pattern (`const [ns] = factory()`)
|
|
656
|
+
// falls through to the bare-key path — the schema is still recovered (the
|
|
657
|
+
// AST walk found `new ApiNamespace(...)`), just under the bare method name,
|
|
658
|
+
// which generate-spec's #498 fallback resolves when there's no collision.
|
|
659
|
+
const dir = createTempProject({
|
|
660
|
+
'tsconfig.json': JSON.stringify({
|
|
661
|
+
compilerOptions: { target: 'ESNext', module: 'ESNext', moduleResolution: 'bundler', strict: true },
|
|
662
|
+
}),
|
|
663
|
+
'index.ts': `
|
|
664
|
+
${API_NS_MOCK}
|
|
665
|
+
class Scope { constructor(id: string) {} }
|
|
666
|
+
|
|
667
|
+
class Factory {
|
|
668
|
+
constructor(private readonly scope: Scope) {}
|
|
669
|
+
build() {
|
|
670
|
+
return [
|
|
671
|
+
new ApiNamespace(this.scope, 'tupleNs', () => ({
|
|
672
|
+
async ping(count: number): Promise<string> { return 'p'; },
|
|
673
|
+
})),
|
|
674
|
+
] as const;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const scope = new Scope('app');
|
|
678
|
+
const [tupleNs] = new Factory(scope).build();
|
|
679
|
+
export { tupleNs };
|
|
680
|
+
`,
|
|
681
|
+
});
|
|
682
|
+
try {
|
|
683
|
+
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
684
|
+
// Not attributed to the binding (array pattern), so no qualified key…
|
|
685
|
+
assert.ok(!types.has('tupleNs.ping'), 'array pattern is not attributed (documented boundary)');
|
|
686
|
+
// …but the schema is still present under the bare name (soft fallback).
|
|
687
|
+
const bare = types.get('ping');
|
|
688
|
+
assert.ok(bare, 'method schema is still recovered under the bare key');
|
|
689
|
+
assert.strictEqual(bare.params[0].schema?.type, 'number');
|
|
690
|
+
assert.strictEqual(bare.returnType?.type, 'string');
|
|
691
|
+
}
|
|
692
|
+
finally {
|
|
693
|
+
rmSync(dir, { recursive: true, force: true });
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-spec.d.ts","sourceRoot":"","sources":["../../src/scripts/generate-spec.ts"],"names":[],"mappings":"AA2BA,UAAU,eAAe;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1C,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;KAAE,CAAC;CACtD;AAED,UAAU,aAAa;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,EAAE,aAAa,CAAC;CACtB;AAED,UAAU,YAAY;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,UAAU,aAAa;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,UAAU,UAAU;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAsKD;;;;;;;;;;;GAWG;AACH;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAIrF,wBAAsB,YAAY,CACjC,cAAc,EAAE,MAAM,EACtB,MAAM,GAAE,gBAAgC,GACtC,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"generate-spec.d.ts","sourceRoot":"","sources":["../../src/scripts/generate-spec.ts"],"names":[],"mappings":"AA2BA,UAAU,eAAe;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1C,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;KAAE,CAAC;CACtD;AAED,UAAU,aAAa;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,EAAE,aAAa,CAAC;CACtB;AAED,UAAU,YAAY;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,UAAU,aAAa;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,UAAU,UAAU;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAsKD;;;;;;;;;;;GAWG;AACH;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAIrF,wBAAsB,YAAY,CACjC,cAAc,EAAE,MAAM,EACtB,MAAM,GAAE,gBAAgC,GACtC,OAAO,CAAC,eAAe,CAAC,CAgR1B;AAyQD;;;GAGG;AACH,wBAAsB,SAAS,CAC9B,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,gBAAgB,GACvB,OAAO,CAAC,IAAI,CAAC,CA0Bf"}
|
|
@@ -257,6 +257,15 @@ export async function generateSpec(foundationPath, loader = defaultLoader) {
|
|
|
257
257
|
for (const [methodName, methodFn] of Object.entries(methodMap)) {
|
|
258
258
|
if (typeof methodFn !== 'function')
|
|
259
259
|
continue;
|
|
260
|
+
const qualifiedName = `${routingName}.${methodName}`;
|
|
261
|
+
// Prefer the namespace-qualified TS types; fall back to the bare method
|
|
262
|
+
// name for entries the extractor couldn't attribute to a namespace.
|
|
263
|
+
// Qualified keys stop two namespaces that share a method name (e.g.
|
|
264
|
+
// widgets.create / subscriptions.create) from cross-assigning schemas.
|
|
265
|
+
// Residual edge: namespaces the extractor can't attribute (default export,
|
|
266
|
+
// factory-wrapped, or an `export { x as y }` rename) still land on bare
|
|
267
|
+
// keys and could overwrite each other here — same class as #445, but rare.
|
|
268
|
+
const tsInfo = tsTypes.get(qualifiedName) ?? tsTypes.get(methodName);
|
|
260
269
|
// `@blocksSkipCodegen` is a JSDoc tag the customer puts on a method
|
|
261
270
|
// they want callable from the generated TS client (Proxy-based,
|
|
262
271
|
// doesn't enumerate) but absent from the OpenRPC spec — so native
|
|
@@ -267,9 +276,11 @@ export async function generateSpec(foundationPath, loader = defaultLoader) {
|
|
|
267
276
|
// type-resolution errors and silently empty its Map — we always
|
|
268
277
|
// want the JSDoc tag to be honored, so the AST-only scan acts as
|
|
269
278
|
// a safety net.
|
|
270
|
-
|
|
279
|
+
// The skip set is namespace-qualified too (with a bare fallback), so a
|
|
280
|
+
// `@blocksSkipCodegen` tag on one namespace's method doesn't drop another
|
|
281
|
+
// namespace's same-named method.
|
|
282
|
+
if (tsInfo?.skipCodegen || skipCodegenNames.has(qualifiedName) || skipCodegenNames.has(methodName))
|
|
271
283
|
continue;
|
|
272
|
-
const qualifiedName = `${routingName}.${methodName}`;
|
|
273
284
|
const resultName = capitalize(methodName) + 'Result';
|
|
274
285
|
const paramNames = extractParamNames(methodFn);
|
|
275
286
|
const fnSource = methodFn.toString();
|
|
@@ -305,7 +316,6 @@ export async function generateSpec(foundationPath, loader = defaultLoader) {
|
|
|
305
316
|
}
|
|
306
317
|
else {
|
|
307
318
|
// No Zod schema — fall back to TypeScript AST types
|
|
308
|
-
const tsInfo = tsTypes.get(methodName);
|
|
309
319
|
if (tsInfo) {
|
|
310
320
|
params = tsInfo.params.map((p) => ({
|
|
311
321
|
name: p.name,
|
|
@@ -322,8 +332,7 @@ export async function generateSpec(foundationPath, loader = defaultLoader) {
|
|
|
322
332
|
}));
|
|
323
333
|
}
|
|
324
334
|
}
|
|
325
|
-
// Return type: Zod doesn't cover this yet, so use TS types
|
|
326
|
-
const tsInfo = tsTypes.get(methodName);
|
|
335
|
+
// Return type: Zod doesn't cover this yet, so use TS types (tsInfo resolved above)
|
|
327
336
|
const rawResultSchema = tsInfo?.returnType && tsInfo.returnType.type !== 'unknown'
|
|
328
337
|
? tsInfo.returnType
|
|
329
338
|
: { type: 'unknown' };
|