@aws-blocks/core 0.3.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 +25 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +32 -0
- package/dist/api.test.js +22 -0
- package/dist/bb-utils.d.ts +1 -0
- package/dist/bb-utils.d.ts.map +1 -1
- package/dist/bb-utils.js +3 -0
- package/dist/cdk/apigateway-account.d.ts +33 -0
- package/dist/cdk/apigateway-account.d.ts.map +1 -0
- package/dist/cdk/apigateway-account.js +60 -0
- package/dist/cdk/blocks-backend.d.ts +11 -0
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +72 -13
- package/dist/cdk/blocks-backend.test.js +88 -1
- package/dist/cdk/blocks-defaults.d.ts +76 -1
- package/dist/cdk/blocks-defaults.d.ts.map +1 -1
- package/dist/cdk/blocks-defaults.js +17 -1
- package/dist/cdk/blocks-defaults.test.js +17 -0
- package/dist/cdk/blocks-stack.test.js +44 -3
- package/dist/cdk/compute/compute-registry.d.ts +19 -0
- package/dist/cdk/compute/compute-registry.d.ts.map +1 -0
- package/dist/cdk/compute/compute-registry.js +38 -0
- package/dist/cdk/compute/compute.d.ts +82 -2
- package/dist/cdk/compute/compute.d.ts.map +1 -1
- package/dist/cdk/compute/compute.js +64 -2
- package/dist/cdk/compute/default-compute-factory.d.ts +1 -0
- package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -1
- package/dist/cdk/config-registry.d.ts +34 -4
- package/dist/cdk/config-registry.d.ts.map +1 -1
- package/dist/cdk/config-registry.js +83 -25
- package/dist/cdk/config-registry.test.d.ts +2 -0
- package/dist/cdk/config-registry.test.d.ts.map +1 -0
- package/dist/cdk/config-registry.test.js +127 -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 +90 -6
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +135 -13
- 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/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/common/config.d.ts +34 -0
- package/dist/common/config.d.ts.map +1 -1
- package/dist/common/config.js +45 -3
- package/dist/common/config.test.js +19 -0
- package/dist/common/index.d.ts +8 -0
- package/dist/common/index.d.ts.map +1 -1
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +25 -0
- package/dist/hosting.d.ts +9 -0
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +12 -1
- package/dist/hosting.test.js +63 -1
- package/dist/index.cdk.d.ts +3 -2
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lambda-handler.d.ts +17 -0
- package/dist/lambda-handler.d.ts.map +1 -1
- package/dist/lambda-handler.js +61 -5
- package/dist/lambda-handler.test.js +191 -2
- 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/scripts/sandbox-empty-buckets.test.d.ts +2 -0
- package/dist/scripts/sandbox-empty-buckets.test.d.ts.map +1 -0
- package/dist/scripts/sandbox-empty-buckets.test.js +171 -0
- package/dist/scripts/sandbox.d.ts +54 -0
- package/dist/scripts/sandbox.d.ts.map +1 -1
- package/dist/scripts/sandbox.js +163 -25
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -1
- package/src/api.test.ts +25 -0
- package/src/api.ts +39 -0
- package/src/bb-utils.ts +3 -0
- package/src/cdk/apigateway-account.ts +66 -0
- package/src/cdk/blocks-backend.test.ts +168 -60
- package/src/cdk/blocks-backend.ts +299 -227
- package/src/cdk/blocks-defaults.test.ts +21 -0
- package/src/cdk/blocks-defaults.ts +79 -1
- package/src/cdk/blocks-stack.test.ts +57 -13
- package/src/cdk/compute/compute-registry.ts +45 -0
- package/src/cdk/compute/compute.ts +114 -2
- package/src/cdk/compute/default-compute-factory.ts +1 -0
- package/src/cdk/config-registry.test.ts +149 -0
- package/src/cdk/config-registry.ts +92 -34
- package/src/cdk/dashboard-registry.ts +68 -0
- package/src/cdk/index.ts +427 -256
- 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/client/index.ts +1 -1
- package/src/common/config.test.ts +21 -0
- package/src/common/config.ts +47 -3
- package/src/common/index.ts +8 -0
- package/src/errors.ts +26 -0
- package/src/hosting.test.ts +87 -1
- package/src/hosting.ts +24 -1
- package/src/index.cdk.ts +11 -1
- package/src/index.ts +1 -1
- package/src/lambda-handler.test.ts +220 -2
- package/src/lambda-handler.ts +65 -4
- 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/scripts/sandbox-empty-buckets.test.ts +191 -0
- package/src/scripts/sandbox.ts +185 -24
- package/src/version.ts +1 -1
|
@@ -29,6 +29,47 @@ const API_NS_MOCK = `
|
|
|
29
29
|
const ApiNamespace: ApiNamespaceConstructor = class {} as any;
|
|
30
30
|
`;
|
|
31
31
|
|
|
32
|
+
describe('extractMethodTypes — namespaces sharing a method name (regression: #445)', () => {
|
|
33
|
+
it('keys methods by namespace, so a shared method name does not cross-assign schemas', () => {
|
|
34
|
+
const dir = createTempProject({
|
|
35
|
+
'tsconfig.json': JSON.stringify({
|
|
36
|
+
compilerOptions: { target: 'ESNext', module: 'ESNext', moduleResolution: 'bundler', strict: true },
|
|
37
|
+
}),
|
|
38
|
+
'index.ts': `
|
|
39
|
+
${API_NS_MOCK}
|
|
40
|
+
|
|
41
|
+
// Two namespaces both expose \`create\`, with deliberately incompatible contracts.
|
|
42
|
+
export const widgets = new ApiNamespace(null, 'widgets', (context) => ({
|
|
43
|
+
async create(label: string): Promise<{ widgetId: string }> {
|
|
44
|
+
return { widgetId: 'w' };
|
|
45
|
+
},
|
|
46
|
+
}));
|
|
47
|
+
export const subscriptions = new ApiNamespace(null, 'subscriptions', (context) => ({
|
|
48
|
+
async create(topicCount: number): Promise<{ subId: number }> {
|
|
49
|
+
return { subId: 1 };
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
`,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
57
|
+
// Distinct qualified keys — no collision.
|
|
58
|
+
assert.ok(types.has('widgets.create'), 'widgets.create should be keyed by namespace');
|
|
59
|
+
assert.ok(types.has('subscriptions.create'), 'subscriptions.create should be keyed by namespace');
|
|
60
|
+
assert.ok(!types.has('create'), 'no bare-name entry that could cross-assign');
|
|
61
|
+
|
|
62
|
+
// Each keeps its OWN parameter type.
|
|
63
|
+
assert.strictEqual(types.get('widgets.create')!.params[0].name, 'label');
|
|
64
|
+
assert.strictEqual(types.get('widgets.create')!.params[0].schema.type, 'string');
|
|
65
|
+
assert.strictEqual(types.get('subscriptions.create')!.params[0].name, 'topicCount');
|
|
66
|
+
assert.strictEqual(types.get('subscriptions.create')!.params[0].schema.type, 'number');
|
|
67
|
+
} finally {
|
|
68
|
+
rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
32
73
|
describe('extractMethodTypes — direct ApiNamespace calls', () => {
|
|
33
74
|
it('extracts param and return types from inline ApiNamespace', () => {
|
|
34
75
|
const dir = createTempProject({
|
|
@@ -58,15 +99,15 @@ describe('extractMethodTypes — direct ApiNamespace calls', () => {
|
|
|
58
99
|
|
|
59
100
|
try {
|
|
60
101
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
61
|
-
assert.ok(types.has('getUser'), 'should find getUser');
|
|
62
|
-
assert.ok(types.has('createUser'), 'should find createUser');
|
|
102
|
+
assert.ok(types.has('api.getUser'), 'should find getUser');
|
|
103
|
+
assert.ok(types.has('api.createUser'), 'should find createUser');
|
|
63
104
|
|
|
64
|
-
const getUser = types.get('getUser')!;
|
|
105
|
+
const getUser = types.get('api.getUser')!;
|
|
65
106
|
assert.strictEqual(getUser.params.length, 1);
|
|
66
107
|
assert.strictEqual(getUser.params[0].name, 'id');
|
|
67
108
|
assert.strictEqual(getUser.params[0].schema.type, 'string');
|
|
68
109
|
|
|
69
|
-
const createUser = types.get('createUser')!;
|
|
110
|
+
const createUser = types.get('api.createUser')!;
|
|
70
111
|
assert.strictEqual(createUser.params.length, 2);
|
|
71
112
|
assert.strictEqual(createUser.params[0].schema.type, 'string');
|
|
72
113
|
assert.strictEqual(createUser.params[1].schema.type, 'number');
|
|
@@ -102,9 +143,34 @@ describe('extractSkipCodegenMethods — pure-AST scan', () => {
|
|
|
102
143
|
try {
|
|
103
144
|
const skips = extractSkipCodegenMethods(join(dir, 'index.ts'));
|
|
104
145
|
assert.strictEqual(skips.size, 1);
|
|
105
|
-
assert.ok(skips.has('devOnly'));
|
|
106
|
-
assert.ok(!skips.has('normalMethod'));
|
|
107
|
-
assert.ok(!skips.has('alsoNormal'));
|
|
146
|
+
assert.ok(skips.has('api.devOnly'), 'skip set is namespace-qualified');
|
|
147
|
+
assert.ok(!skips.has('api.normalMethod'));
|
|
148
|
+
assert.ok(!skips.has('api.alsoNormal'));
|
|
149
|
+
} finally {
|
|
150
|
+
rmSync(dir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('qualifies skip names by namespace so a tag on one namespace does not drop another (regression: #445)', () => {
|
|
155
|
+
const dir = createTempProject({
|
|
156
|
+
'index.ts': `
|
|
157
|
+
class ApiNamespace { constructor(scope: any, name: any, handler: any) {} }
|
|
158
|
+
|
|
159
|
+
export const widgets = new ApiNamespace(null, 'widgets', () => ({
|
|
160
|
+
/** @blocksSkipCodegen */
|
|
161
|
+
async create() { return null; },
|
|
162
|
+
}));
|
|
163
|
+
// subscriptions.create is NOT tagged and must survive.
|
|
164
|
+
export const subscriptions = new ApiNamespace(null, 'subscriptions', () => ({
|
|
165
|
+
async create() { return null; },
|
|
166
|
+
}));
|
|
167
|
+
`,
|
|
168
|
+
});
|
|
169
|
+
try {
|
|
170
|
+
const skips = extractSkipCodegenMethods(join(dir, 'index.ts'));
|
|
171
|
+
assert.ok(skips.has('widgets.create'), 'tagged method is skipped, qualified');
|
|
172
|
+
assert.ok(!skips.has('subscriptions.create'), 'untagged same-named method must NOT be skipped');
|
|
173
|
+
assert.ok(!skips.has('create'), 'no bare key that would drop both');
|
|
108
174
|
} finally {
|
|
109
175
|
rmSync(dir, { recursive: true, force: true });
|
|
110
176
|
}
|
|
@@ -155,8 +221,8 @@ describe('extractMethodTypes — @blocksSkipCodegen JSDoc tag', () => {
|
|
|
155
221
|
|
|
156
222
|
try {
|
|
157
223
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
158
|
-
assert.strictEqual(types.get('normalMethod')?.skipCodegen, undefined);
|
|
159
|
-
assert.strictEqual(types.get('getLastCode')?.skipCodegen, true);
|
|
224
|
+
assert.strictEqual(types.get('api.normalMethod')?.skipCodegen, undefined);
|
|
225
|
+
assert.strictEqual(types.get('api.getLastCode')?.skipCodegen, true);
|
|
160
226
|
} finally {
|
|
161
227
|
rmSync(dir, { recursive: true, force: true });
|
|
162
228
|
}
|
|
@@ -217,23 +283,23 @@ describe('extractMethodTypes — indirect ApiNamespace calls (e.g., auth.createA
|
|
|
217
283
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
218
284
|
|
|
219
285
|
// Direct ApiNamespace methods should still work
|
|
220
|
-
assert.ok(types.has('greet'), 'should find greet from direct ApiNamespace');
|
|
221
|
-
const greet = types.get('greet')!;
|
|
286
|
+
assert.ok(types.has('api.greet'), 'should find greet from direct ApiNamespace');
|
|
287
|
+
const greet = types.get('api.greet')!;
|
|
222
288
|
assert.strictEqual(greet.params[0].name, 'name');
|
|
223
289
|
assert.strictEqual(greet.params[0].schema.type, 'string');
|
|
224
290
|
|
|
225
291
|
// Indirect methods from auth.createApi() should now be resolved
|
|
226
|
-
assert.ok(types.has('getAuthState'), 'should find getAuthState from auth.createApi()');
|
|
227
|
-
assert.ok(types.has('setAuthState'), 'should find setAuthState from auth.createApi()');
|
|
292
|
+
assert.ok(types.has('authApi.getAuthState'), 'should find getAuthState from auth.createApi()');
|
|
293
|
+
assert.ok(types.has('authApi.setAuthState'), 'should find setAuthState from auth.createApi()');
|
|
228
294
|
|
|
229
|
-
const getAuthState = types.get('getAuthState')!;
|
|
295
|
+
const getAuthState = types.get('authApi.getAuthState')!;
|
|
230
296
|
assert.strictEqual(getAuthState.params.length, 0, 'getAuthState has no params');
|
|
231
297
|
assert.strictEqual(getAuthState.returnType.type, 'object', 'return type should be object');
|
|
232
298
|
assert.ok(getAuthState.returnType.properties, 'return type should have properties');
|
|
233
299
|
assert.ok('isSignedIn' in getAuthState.returnType.properties!, 'should have isSignedIn');
|
|
234
300
|
assert.ok('username' in getAuthState.returnType.properties!, 'should have username');
|
|
235
301
|
|
|
236
|
-
const setAuthState = types.get('setAuthState')!;
|
|
302
|
+
const setAuthState = types.get('authApi.setAuthState')!;
|
|
237
303
|
assert.strictEqual(setAuthState.params.length, 2, 'setAuthState has 2 params');
|
|
238
304
|
assert.strictEqual(setAuthState.params[0].name, 'action');
|
|
239
305
|
assert.strictEqual(setAuthState.params[0].schema.type, 'string');
|
|
@@ -271,9 +337,9 @@ describe('extractMethodTypes — indirect ApiNamespace calls (e.g., auth.createA
|
|
|
271
337
|
|
|
272
338
|
try {
|
|
273
339
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
274
|
-
assert.ok(types.has('greet'), 'should find greet');
|
|
340
|
+
assert.ok(types.has('api.greet'), 'should find greet');
|
|
275
341
|
// The AST-walk version should be preserved (it has richer info from the declaration)
|
|
276
|
-
assert.strictEqual(types.get('greet')!.params[0].name, 'name');
|
|
342
|
+
assert.strictEqual(types.get('api.greet')!.params[0].name, 'name');
|
|
277
343
|
} finally {
|
|
278
344
|
rmSync(dir, { recursive: true, force: true });
|
|
279
345
|
}
|
|
@@ -335,7 +401,7 @@ describe('extractMethodTypes — transferable detection', () => {
|
|
|
335
401
|
`,
|
|
336
402
|
});
|
|
337
403
|
try {
|
|
338
|
-
const info = extractMethodTypes(join(dir, 'index.ts')).get('getCursorChannel');
|
|
404
|
+
const info = extractMethodTypes(join(dir, 'index.ts')).get('api.getCursorChannel');
|
|
339
405
|
assert.ok(info, 'method should be discovered');
|
|
340
406
|
assert.ok(info.transferable, 'should be detected as transferable');
|
|
341
407
|
assert.strictEqual(info.transferable.blocksTag, 'realtime/channel');
|
|
@@ -360,7 +426,7 @@ describe('extractMethodTypes — transferable detection', () => {
|
|
|
360
426
|
`,
|
|
361
427
|
});
|
|
362
428
|
try {
|
|
363
|
-
const info = extractMethodTypes(join(dir, 'index.ts')).get('getDownload');
|
|
429
|
+
const info = extractMethodTypes(join(dir, 'index.ts')).get('api.getDownload');
|
|
364
430
|
assert.ok(info?.transferable);
|
|
365
431
|
assert.strictEqual(info.transferable.blocksTag, 'file-bucket/download');
|
|
366
432
|
assert.deepStrictEqual(info.transferable.typeArgs, []);
|
|
@@ -382,8 +448,8 @@ describe('extractMethodTypes — transferable detection', () => {
|
|
|
382
448
|
});
|
|
383
449
|
try {
|
|
384
450
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
385
|
-
assert.deepStrictEqual(types.get('defaulted')?.transferable?.typeArgs, []);
|
|
386
|
-
assert.deepStrictEqual(types.get('explicit')?.transferable?.typeArgs, [{}]);
|
|
451
|
+
assert.deepStrictEqual(types.get('api.defaulted')?.transferable?.typeArgs, []);
|
|
452
|
+
assert.deepStrictEqual(types.get('api.explicit')?.transferable?.typeArgs, [{}]);
|
|
387
453
|
} finally {
|
|
388
454
|
rmSync(dir, { recursive: true, force: true });
|
|
389
455
|
}
|
|
@@ -404,8 +470,8 @@ describe('extractMethodTypes — transferable detection', () => {
|
|
|
404
470
|
});
|
|
405
471
|
try {
|
|
406
472
|
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
407
|
-
assert.strictEqual(types.get('plain')?.transferable, undefined);
|
|
408
|
-
assert.strictEqual(types.get('loose')?.transferable, undefined);
|
|
473
|
+
assert.strictEqual(types.get('api.plain')?.transferable, undefined);
|
|
474
|
+
assert.strictEqual(types.get('api.loose')?.transferable, undefined);
|
|
409
475
|
} finally {
|
|
410
476
|
rmSync(dir, { recursive: true, force: true });
|
|
411
477
|
}
|
|
@@ -431,7 +497,7 @@ describe('extractMethodTypes — transferable detection', () => {
|
|
|
431
497
|
`,
|
|
432
498
|
});
|
|
433
499
|
try {
|
|
434
|
-
const info = extractMethodTypes(join(dir, 'index.ts')).get('getNotifications');
|
|
500
|
+
const info = extractMethodTypes(join(dir, 'index.ts')).get('api.getNotifications');
|
|
435
501
|
assert.ok(info?.transferable);
|
|
436
502
|
assert.strictEqual(info.transferable.blocksTag, 'realtime/channel');
|
|
437
503
|
assert.strictEqual(info.transferable.typeArgs[0]?.title, 'Notification');
|
|
@@ -465,7 +531,7 @@ describe('extractMethodTypes — open-shape intersections', () => {
|
|
|
465
531
|
`,
|
|
466
532
|
});
|
|
467
533
|
try {
|
|
468
|
-
const info = extractMethodTypes(join(dir, 'index.ts')).get('signUp');
|
|
534
|
+
const info = extractMethodTypes(join(dir, 'index.ts')).get('api.signUp');
|
|
469
535
|
assert.ok(info, 'should find signUp');
|
|
470
536
|
const inputSchema = info.params[0].schema as any;
|
|
471
537
|
assert.strictEqual(inputSchema.type, 'object');
|
|
@@ -502,7 +568,7 @@ describe('extractMethodTypes — open-shape intersections', () => {
|
|
|
502
568
|
`,
|
|
503
569
|
});
|
|
504
570
|
try {
|
|
505
|
-
const info = extractMethodTypes(join(dir, 'index.ts')).get('tag');
|
|
571
|
+
const info = extractMethodTypes(join(dir, 'index.ts')).get('api.tag');
|
|
506
572
|
assert.ok(info, 'should find tag');
|
|
507
573
|
const inputSchema = info.params[0].schema as any;
|
|
508
574
|
assert.strictEqual(inputSchema.type, 'object');
|
|
@@ -514,3 +580,139 @@ describe('extractMethodTypes — open-shape intersections', () => {
|
|
|
514
580
|
}
|
|
515
581
|
});
|
|
516
582
|
});
|
|
583
|
+
|
|
584
|
+
describe('extractMethodTypes — namespace returned by a factory, then destructured (regression: #444)', () => {
|
|
585
|
+
it('keys a destructured factory namespace qualified, with real schemas', () => {
|
|
586
|
+
const dir = createTempProject({
|
|
587
|
+
'tsconfig.json': JSON.stringify({
|
|
588
|
+
compilerOptions: { target: 'ESNext', module: 'ESNext', moduleResolution: 'bundler', strict: true },
|
|
589
|
+
}),
|
|
590
|
+
'index.ts': `
|
|
591
|
+
${API_NS_MOCK}
|
|
592
|
+
class Scope { constructor(id: string) {} }
|
|
593
|
+
|
|
594
|
+
class NamespaceFactory {
|
|
595
|
+
constructor(private readonly scope: Scope) {}
|
|
596
|
+
createNamespaces() {
|
|
597
|
+
return {
|
|
598
|
+
indirectNamespace: new ApiNamespace(this.scope, 'indirectNamespace', () => ({
|
|
599
|
+
async getIndirectGreeting(times: number): Promise<string> { return 'hi'; },
|
|
600
|
+
})),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const scope = new Scope('repro');
|
|
606
|
+
export const directNamespace = new ApiNamespace(scope, 'directNamespace', () => ({
|
|
607
|
+
async getDirectGreeting(name: string): Promise<number> { return name.length; },
|
|
608
|
+
}));
|
|
609
|
+
const { indirectNamespace } = new NamespaceFactory(scope).createNamespaces();
|
|
610
|
+
export { indirectNamespace };
|
|
611
|
+
`,
|
|
612
|
+
});
|
|
613
|
+
try {
|
|
614
|
+
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
615
|
+
// The factory-returned namespace is now attributed to its binding name,
|
|
616
|
+
// exactly like the directly-constructed one.
|
|
617
|
+
const indirect = types.get('indirectNamespace.getIndirectGreeting');
|
|
618
|
+
assert.ok(indirect, 'indirectNamespace.getIndirectGreeting should be keyed by namespace');
|
|
619
|
+
assert.strictEqual((indirect.params[0].schema as any).type, 'number');
|
|
620
|
+
assert.strictEqual((indirect.returnType as any).type, 'string');
|
|
621
|
+
|
|
622
|
+
const direct = types.get('directNamespace.getDirectGreeting');
|
|
623
|
+
assert.ok(direct, 'directNamespace.getDirectGreeting should be present');
|
|
624
|
+
assert.strictEqual((direct.params[0].schema as any).type, 'string');
|
|
625
|
+
assert.strictEqual((direct.returnType as any).type, 'number');
|
|
626
|
+
} finally {
|
|
627
|
+
rmSync(dir, { recursive: true, force: true });
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('a factory namespace sharing a method name with a direct one does NOT cross-assign', () => {
|
|
632
|
+
const dir = createTempProject({
|
|
633
|
+
'tsconfig.json': JSON.stringify({
|
|
634
|
+
compilerOptions: { target: 'ESNext', module: 'ESNext', moduleResolution: 'bundler', strict: true },
|
|
635
|
+
}),
|
|
636
|
+
'index.ts': `
|
|
637
|
+
${API_NS_MOCK}
|
|
638
|
+
class Scope { constructor(id: string) {} }
|
|
639
|
+
|
|
640
|
+
class Factory {
|
|
641
|
+
constructor(private readonly scope: Scope) {}
|
|
642
|
+
build() {
|
|
643
|
+
return {
|
|
644
|
+
subscriptions: new ApiNamespace(this.scope, 'subscriptions', () => ({
|
|
645
|
+
async create(topicCount: number): Promise<{ subId: number }> { return { subId: 1 }; },
|
|
646
|
+
})),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const scope = new Scope('app');
|
|
652
|
+
export const widgets = new ApiNamespace(scope, 'widgets', () => ({
|
|
653
|
+
async create(label: string): Promise<{ widgetId: string }> { return { widgetId: 'w' }; },
|
|
654
|
+
}));
|
|
655
|
+
const { subscriptions } = new Factory(scope).build();
|
|
656
|
+
export { subscriptions };
|
|
657
|
+
`,
|
|
658
|
+
});
|
|
659
|
+
try {
|
|
660
|
+
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
661
|
+
// Each namespace keeps its OWN param type under a distinct qualified key.
|
|
662
|
+
assert.strictEqual((types.get('widgets.create')?.params[0].schema as any)?.type, 'string');
|
|
663
|
+
assert.strictEqual((types.get('subscriptions.create')?.params[0].schema as any)?.type, 'number');
|
|
664
|
+
// The first-pass AST walk still emits a lingering bare `create` for the
|
|
665
|
+
// factory namespace, but it never overrides a qualified key: the
|
|
666
|
+
// direct namespace's qualified `widgets.create` is unaffected above, and
|
|
667
|
+
// `generate-spec` prefers the qualified key (see the collision test in
|
|
668
|
+
// generate-spec.test.ts, which asserts this end-to-end in the output).
|
|
669
|
+
// So the bare entry can't reintroduce the #445 cross-assignment here.
|
|
670
|
+
assert.strictEqual((types.get('widgets.create')?.params[0].schema as any)?.type, 'string');
|
|
671
|
+
} finally {
|
|
672
|
+
rmSync(dir, { recursive: true, force: true });
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('array/nested destructuring falls through to the bare key (documented boundary)', () => {
|
|
677
|
+
// Only a top-level identifier and a shallow object binding pattern are
|
|
678
|
+
// attributed to a namespace. An array pattern (`const [ns] = factory()`)
|
|
679
|
+
// falls through to the bare-key path — the schema is still recovered (the
|
|
680
|
+
// AST walk found `new ApiNamespace(...)`), just under the bare method name,
|
|
681
|
+
// which generate-spec's #498 fallback resolves when there's no collision.
|
|
682
|
+
const dir = createTempProject({
|
|
683
|
+
'tsconfig.json': JSON.stringify({
|
|
684
|
+
compilerOptions: { target: 'ESNext', module: 'ESNext', moduleResolution: 'bundler', strict: true },
|
|
685
|
+
}),
|
|
686
|
+
'index.ts': `
|
|
687
|
+
${API_NS_MOCK}
|
|
688
|
+
class Scope { constructor(id: string) {} }
|
|
689
|
+
|
|
690
|
+
class Factory {
|
|
691
|
+
constructor(private readonly scope: Scope) {}
|
|
692
|
+
build() {
|
|
693
|
+
return [
|
|
694
|
+
new ApiNamespace(this.scope, 'tupleNs', () => ({
|
|
695
|
+
async ping(count: number): Promise<string> { return 'p'; },
|
|
696
|
+
})),
|
|
697
|
+
] as const;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const scope = new Scope('app');
|
|
701
|
+
const [tupleNs] = new Factory(scope).build();
|
|
702
|
+
export { tupleNs };
|
|
703
|
+
`,
|
|
704
|
+
});
|
|
705
|
+
try {
|
|
706
|
+
const types = extractMethodTypes(join(dir, 'index.ts'));
|
|
707
|
+
// Not attributed to the binding (array pattern), so no qualified key…
|
|
708
|
+
assert.ok(!types.has('tupleNs.ping'), 'array pattern is not attributed (documented boundary)');
|
|
709
|
+
// …but the schema is still present under the bare name (soft fallback).
|
|
710
|
+
const bare = types.get('ping');
|
|
711
|
+
assert.ok(bare, 'method schema is still recovered under the bare key');
|
|
712
|
+
assert.strictEqual((bare.params[0].schema as any)?.type, 'number');
|
|
713
|
+
assert.strictEqual((bare.returnType as any)?.type, 'string');
|
|
714
|
+
} finally {
|
|
715
|
+
rmSync(dir, { recursive: true, force: true });
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
});
|
|
@@ -80,7 +80,7 @@ export function extractSkipCodegenMethods(sourcePath: string): Set<string> {
|
|
|
80
80
|
ts.ScriptKind.TS,
|
|
81
81
|
);
|
|
82
82
|
|
|
83
|
-
function visitObjectLiteral(obj: ts.ObjectLiteralExpression) {
|
|
83
|
+
function visitObjectLiteral(obj: ts.ObjectLiteralExpression, namespaceName?: string) {
|
|
84
84
|
for (const prop of obj.properties) {
|
|
85
85
|
if (
|
|
86
86
|
ts.isMethodDeclaration(prop) &&
|
|
@@ -88,7 +88,10 @@ export function extractSkipCodegenMethods(sourcePath: string): Set<string> {
|
|
|
88
88
|
ts.isIdentifier(prop.name) &&
|
|
89
89
|
hasBlocksSkipCodegenTag(prop)
|
|
90
90
|
) {
|
|
91
|
-
|
|
91
|
+
// Qualify by namespace (like extractMethodTypes) so a tag on one
|
|
92
|
+
// namespace's `create` doesn't cause another namespace's `create` to be
|
|
93
|
+
// dropped from the spec. Bare key only when the namespace is unknown.
|
|
94
|
+
result.add(namespaceName ? `${namespaceName}.${prop.name.text}` : prop.name.text);
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
}
|
|
@@ -98,22 +101,23 @@ export function extractSkipCodegenMethods(sourcePath: string): Set<string> {
|
|
|
98
101
|
const callee = node.expression;
|
|
99
102
|
if (ts.isIdentifier(callee) && callee.text === 'ApiNamespace' && node.arguments && node.arguments.length >= 2) {
|
|
100
103
|
const handler = node.arguments[node.arguments.length - 1];
|
|
104
|
+
const nsName = enclosingBindingName(node);
|
|
101
105
|
if (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) {
|
|
102
106
|
const body = handler.body;
|
|
103
107
|
if (ts.isParenthesizedExpression(body) && ts.isObjectLiteralExpression(body.expression)) {
|
|
104
|
-
visitObjectLiteral(body.expression);
|
|
108
|
+
visitObjectLiteral(body.expression, nsName);
|
|
105
109
|
} else if (ts.isObjectLiteralExpression(body)) {
|
|
106
|
-
visitObjectLiteral(body);
|
|
110
|
+
visitObjectLiteral(body, nsName);
|
|
107
111
|
} else if (ts.isBlock(body)) {
|
|
108
112
|
for (const stmt of body.statements) {
|
|
109
113
|
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
110
114
|
if (ts.isObjectLiteralExpression(stmt.expression)) {
|
|
111
|
-
visitObjectLiteral(stmt.expression);
|
|
115
|
+
visitObjectLiteral(stmt.expression, nsName);
|
|
112
116
|
} else if (
|
|
113
117
|
ts.isParenthesizedExpression(stmt.expression) &&
|
|
114
118
|
ts.isObjectLiteralExpression(stmt.expression.expression)
|
|
115
119
|
) {
|
|
116
|
-
visitObjectLiteral(stmt.expression.expression);
|
|
120
|
+
visitObjectLiteral(stmt.expression.expression, nsName);
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
}
|
|
@@ -197,7 +201,7 @@ export function extractMethodTypes(sourcePath: string): Map<string, MethodTypeIn
|
|
|
197
201
|
const fnName = node.expression;
|
|
198
202
|
if (ts.isIdentifier(fnName) && fnName.text === 'ApiNamespace' && node.arguments && node.arguments.length >= 2) {
|
|
199
203
|
const handlerArg = node.arguments[node.arguments.length - 1];
|
|
200
|
-
extractMethodsFromHandler(handlerArg, checker, sourceFile!, result);
|
|
204
|
+
extractMethodsFromHandler(handlerArg, checker, sourceFile!, result, enclosingBindingName(node));
|
|
201
205
|
}
|
|
202
206
|
}
|
|
203
207
|
ts.forEachChild(node, visit);
|
|
@@ -210,18 +214,65 @@ export function extractMethodTypes(sourcePath: string): Map<string, MethodTypeIn
|
|
|
210
214
|
ts.forEachChild(sourceFile, (node) => {
|
|
211
215
|
if (!ts.isVariableStatement(node)) return;
|
|
212
216
|
for (const decl of node.declarationList.declarations) {
|
|
213
|
-
if (!decl.initializer
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
if (!decl.initializer) continue;
|
|
218
|
+
|
|
219
|
+
// (a) Simple binding: `const api = auth.createApi()` /
|
|
220
|
+
// `export const api = backend.namespace`.
|
|
221
|
+
if (ts.isIdentifier(decl.name)) {
|
|
222
|
+
// Skip if we already extracted methods for this via the AST walk
|
|
223
|
+
if (ts.isNewExpression(decl.initializer)) {
|
|
224
|
+
const callee = decl.initializer.expression;
|
|
225
|
+
if (ts.isIdentifier(callee) && callee.text === 'ApiNamespace') continue;
|
|
226
|
+
}
|
|
227
|
+
// Use the type checker to get the type of the initializer expression.
|
|
228
|
+
// For `auth.createApi()`, this resolves through the method's return type
|
|
229
|
+
// to the `AsyncAPI<T>` type that ApiNamespace returns.
|
|
230
|
+
const initType = checker.getTypeAtLocation(decl.initializer);
|
|
231
|
+
extractMethodsFromResolvedType(initType, checker, result, decl.name.text);
|
|
232
|
+
continue;
|
|
218
233
|
}
|
|
219
234
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
235
|
+
// (b) Object binding pattern: `const { indirectNamespace } = factory()`.
|
|
236
|
+
// Each destructured binding is a separate namespace whose name is the
|
|
237
|
+
// LOCAL binding name (the name it's exported under). Resolve the property
|
|
238
|
+
// type off the initializer and extract methods keyed by that name, so a
|
|
239
|
+
// factory-returned namespace keys qualified (`indirectNamespace.method`)
|
|
240
|
+
// like a directly-constructed one — instead of landing on a bare key that
|
|
241
|
+
// collides with same-named methods in other namespaces (#444, #445).
|
|
242
|
+
//
|
|
243
|
+
// This adds the qualified key ALONGSIDE the bare key the first-pass AST
|
|
244
|
+
// walk already emitted for the same `new ApiNamespace(...)` (its
|
|
245
|
+
// `enclosingBindingName` returns undefined behind the factory, so it keys
|
|
246
|
+
// bare). Leaving the bare key is harmless: `generate-spec` looks up
|
|
247
|
+
// `tsTypes.get(qualified) ?? tsTypes.get(bare)`, so the qualified key
|
|
248
|
+
// added here always wins for an attributable namespace, and the bare
|
|
249
|
+
// entry never influences the emitted schema (a regression test asserts
|
|
250
|
+
// this under a name collision). The #498 bare-name fallback is retained
|
|
251
|
+
// on purpose — it's the last resort for namespaces this pass still can't
|
|
252
|
+
// attribute (default exports, deeper indirection, the patterns below), so
|
|
253
|
+
// it is NOT redundant.
|
|
254
|
+
//
|
|
255
|
+
// Boundary: only a top-level identifier (a) and a shallow object binding
|
|
256
|
+
// pattern (b) are attributed. Array patterns (`const [ns] = factory()`)
|
|
257
|
+
// and nested patterns (`const { a: { b } } = …`) fall through to the
|
|
258
|
+
// bare-key path, where the #498 fallback still resolves the schema when
|
|
259
|
+
// there's no collision. These shapes are rare for namespace exports and
|
|
260
|
+
// fail soft; widening the traversal can come later if a real case appears.
|
|
261
|
+
if (ts.isObjectBindingPattern(decl.name)) {
|
|
262
|
+
const initType = checker.getTypeAtLocation(decl.initializer);
|
|
263
|
+
for (const element of decl.name.elements) {
|
|
264
|
+
if (!ts.isIdentifier(element.name)) continue;
|
|
265
|
+
const localName = element.name.text;
|
|
266
|
+
// `const { foo: bar } = …` binds `bar` but reads property `foo`.
|
|
267
|
+
const propKey = element.propertyName && ts.isIdentifier(element.propertyName)
|
|
268
|
+
? element.propertyName.text
|
|
269
|
+
: localName;
|
|
270
|
+
const propSymbol = initType.getProperty(propKey);
|
|
271
|
+
if (!propSymbol) continue;
|
|
272
|
+
const propType = checker.getTypeOfSymbol(propSymbol);
|
|
273
|
+
extractMethodsFromResolvedType(propType, checker, result, localName);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
225
276
|
}
|
|
226
277
|
});
|
|
227
278
|
|
|
@@ -249,6 +300,7 @@ function extractMethodsFromHandler(
|
|
|
249
300
|
checker: ts.TypeChecker,
|
|
250
301
|
sourceFile: ts.SourceFile,
|
|
251
302
|
result: Map<string, MethodTypeInfo>,
|
|
303
|
+
namespaceName?: string,
|
|
252
304
|
): void {
|
|
253
305
|
let objectLiteral: ts.ObjectLiteralExpression | undefined;
|
|
254
306
|
|
|
@@ -280,11 +332,41 @@ function extractMethodsFromHandler(
|
|
|
280
332
|
if (ts.isMethodDeclaration(prop) && prop.name && ts.isIdentifier(prop.name)) {
|
|
281
333
|
const methodName = prop.name.text;
|
|
282
334
|
const info = extractMethodTypeInfo(prop, checker, sourceFile);
|
|
283
|
-
|
|
335
|
+
// Key by the namespace-qualified name when the namespace is known, so two
|
|
336
|
+
// namespaces sharing a method name (e.g. widgets.create / subscriptions.create)
|
|
337
|
+
// don't collide and cross-assign schemas. Fall back to the bare name when the
|
|
338
|
+
// namespace can't be determined (consumers try qualified, then bare).
|
|
339
|
+
if (info) result.set(namespaceName ? `${namespaceName}.${methodName}` : methodName, info);
|
|
284
340
|
}
|
|
285
341
|
}
|
|
286
342
|
}
|
|
287
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Walk up from a `new ApiNamespace(...)` node to the name it's bound to
|
|
346
|
+
* (`export const widgets = new ApiNamespace(...)` → `"widgets"`). This is the
|
|
347
|
+
* name the dev server / Lambda handler route by, and the one `generate-spec`
|
|
348
|
+
* uses to qualify method names. Returns `undefined` when the namespace is
|
|
349
|
+
* created behind indirection (factory return, etc.), in which case methods key
|
|
350
|
+
* by their bare name.
|
|
351
|
+
*
|
|
352
|
+
* Limitation: this is the local binding name, not the export name. An export
|
|
353
|
+
* rename — `const widgets = new ApiNamespace(...); export { widgets as gadgets }`
|
|
354
|
+
* — keys `widgets.*` while `generate-spec` looks up `gadgets.*`, so the qualified
|
|
355
|
+
* lookup misses and falls back to the bare key (schemas resolve if there's no
|
|
356
|
+
* collision, else `unknown`). Rare; resolving the export alias would remove it.
|
|
357
|
+
*/
|
|
358
|
+
function enclosingBindingName(node: ts.Node): string | undefined {
|
|
359
|
+
let current: ts.Node | undefined = node.parent;
|
|
360
|
+
while (current) {
|
|
361
|
+
if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) return current.name.text;
|
|
362
|
+
// Don't cross a function/block boundary — beyond it the binding no longer
|
|
363
|
+
// corresponds to a directly-exported namespace.
|
|
364
|
+
if (ts.isSourceFile(current) || ts.isBlock(current) || ts.isFunctionLike(current)) return undefined;
|
|
365
|
+
current = current.parent;
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
288
370
|
function extractMethodTypeInfo(
|
|
289
371
|
method: ts.MethodDeclaration,
|
|
290
372
|
checker: ts.TypeChecker,
|
|
@@ -365,6 +447,7 @@ function extractMethodsFromResolvedType(
|
|
|
365
447
|
type: ts.Type,
|
|
366
448
|
checker: ts.TypeChecker,
|
|
367
449
|
result: Map<string, MethodTypeInfo>,
|
|
450
|
+
namespaceName?: string,
|
|
368
451
|
): void {
|
|
369
452
|
// The runtime value of an ApiNamespace export is the handler function itself
|
|
370
453
|
// (with the marker symbol attached). Its TS type is `AsyncAPI<T>`, which is
|
|
@@ -392,8 +475,9 @@ function extractMethodsFromResolvedType(
|
|
|
392
475
|
const propName = prop.getName();
|
|
393
476
|
// Skip internal symbols and the marker
|
|
394
477
|
if (propName.startsWith('_') || propName === 'Symbol(blocks:ApiNamespace)') continue;
|
|
478
|
+
const key = namespaceName ? `${namespaceName}.${propName}` : propName;
|
|
395
479
|
// Don't overwrite methods already found by the AST walk
|
|
396
|
-
if (result.has(
|
|
480
|
+
if (result.has(key)) continue;
|
|
397
481
|
|
|
398
482
|
const propType = checker.getTypeOfSymbol(prop);
|
|
399
483
|
const methodSigs = propType.getCallSignatures();
|
|
@@ -433,7 +517,7 @@ function extractMethodsFromResolvedType(
|
|
|
433
517
|
skipCodegen = true;
|
|
434
518
|
}
|
|
435
519
|
|
|
436
|
-
result.set(
|
|
520
|
+
result.set(key, { params, returnType, transferable, skipCodegen });
|
|
437
521
|
}
|
|
438
522
|
}
|
|
439
523
|
|