@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.
Files changed (147) hide show
  1. package/README.md +25 -0
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +32 -0
  4. package/dist/api.test.js +22 -0
  5. package/dist/bb-utils.d.ts +1 -0
  6. package/dist/bb-utils.d.ts.map +1 -1
  7. package/dist/bb-utils.js +3 -0
  8. package/dist/cdk/apigateway-account.d.ts +33 -0
  9. package/dist/cdk/apigateway-account.d.ts.map +1 -0
  10. package/dist/cdk/apigateway-account.js +60 -0
  11. package/dist/cdk/blocks-backend.d.ts +11 -0
  12. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  13. package/dist/cdk/blocks-backend.js +72 -13
  14. package/dist/cdk/blocks-backend.test.js +88 -1
  15. package/dist/cdk/blocks-defaults.d.ts +76 -1
  16. package/dist/cdk/blocks-defaults.d.ts.map +1 -1
  17. package/dist/cdk/blocks-defaults.js +17 -1
  18. package/dist/cdk/blocks-defaults.test.js +17 -0
  19. package/dist/cdk/blocks-stack.test.js +44 -3
  20. package/dist/cdk/compute/compute-registry.d.ts +19 -0
  21. package/dist/cdk/compute/compute-registry.d.ts.map +1 -0
  22. package/dist/cdk/compute/compute-registry.js +38 -0
  23. package/dist/cdk/compute/compute.d.ts +82 -2
  24. package/dist/cdk/compute/compute.d.ts.map +1 -1
  25. package/dist/cdk/compute/compute.js +64 -2
  26. package/dist/cdk/compute/default-compute-factory.d.ts +1 -0
  27. package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -1
  28. package/dist/cdk/config-registry.d.ts +34 -4
  29. package/dist/cdk/config-registry.d.ts.map +1 -1
  30. package/dist/cdk/config-registry.js +83 -25
  31. package/dist/cdk/config-registry.test.d.ts +2 -0
  32. package/dist/cdk/config-registry.test.d.ts.map +1 -0
  33. package/dist/cdk/config-registry.test.js +127 -0
  34. package/dist/cdk/dashboard-registry.d.ts +41 -0
  35. package/dist/cdk/dashboard-registry.d.ts.map +1 -0
  36. package/dist/cdk/dashboard-registry.js +61 -0
  37. package/dist/cdk/index.d.ts +90 -6
  38. package/dist/cdk/index.d.ts.map +1 -1
  39. package/dist/cdk/index.js +135 -13
  40. package/dist/cdk/internal.d.ts +3 -1
  41. package/dist/cdk/internal.d.ts.map +1 -1
  42. package/dist/cdk/internal.js +4 -1
  43. package/dist/cdk/tracer-registry.d.ts +31 -0
  44. package/dist/cdk/tracer-registry.d.ts.map +1 -0
  45. package/dist/cdk/tracer-registry.js +49 -0
  46. package/dist/cdk/vpc-requirements-registry.d.ts +33 -0
  47. package/dist/cdk/vpc-requirements-registry.d.ts.map +1 -0
  48. package/dist/cdk/vpc-requirements-registry.js +46 -0
  49. package/dist/cdk/vpc-types.d.ts +151 -0
  50. package/dist/cdk/vpc-types.d.ts.map +1 -0
  51. package/dist/cdk/vpc-types.js +3 -0
  52. package/dist/cdk/vpc.d.ts +59 -0
  53. package/dist/cdk/vpc.d.ts.map +1 -0
  54. package/dist/cdk/vpc.js +298 -0
  55. package/dist/cdk/vpc.test.d.ts +2 -0
  56. package/dist/cdk/vpc.test.d.ts.map +1 -0
  57. package/dist/cdk/vpc.test.js +285 -0
  58. package/dist/client/index.d.ts +1 -1
  59. package/dist/client/index.d.ts.map +1 -1
  60. package/dist/client/index.js +1 -1
  61. package/dist/common/config.d.ts +34 -0
  62. package/dist/common/config.d.ts.map +1 -1
  63. package/dist/common/config.js +45 -3
  64. package/dist/common/config.test.js +19 -0
  65. package/dist/common/index.d.ts +8 -0
  66. package/dist/common/index.d.ts.map +1 -1
  67. package/dist/errors.d.ts +21 -0
  68. package/dist/errors.d.ts.map +1 -1
  69. package/dist/errors.js +25 -0
  70. package/dist/hosting.d.ts +9 -0
  71. package/dist/hosting.d.ts.map +1 -1
  72. package/dist/hosting.js +12 -1
  73. package/dist/hosting.test.js +63 -1
  74. package/dist/index.cdk.d.ts +3 -2
  75. package/dist/index.cdk.d.ts.map +1 -1
  76. package/dist/index.cdk.js +2 -2
  77. package/dist/index.d.ts +1 -1
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/lambda-handler.d.ts +17 -0
  81. package/dist/lambda-handler.d.ts.map +1 -1
  82. package/dist/lambda-handler.js +61 -5
  83. package/dist/lambda-handler.test.js +191 -2
  84. package/dist/raw-route.d.ts +15 -1
  85. package/dist/raw-route.d.ts.map +1 -1
  86. package/dist/raw-route.js +96 -12
  87. package/dist/raw-route.test.js +332 -1
  88. package/dist/scripts/dev-server.d.ts.map +1 -1
  89. package/dist/scripts/dev-server.js +11 -0
  90. package/dist/scripts/extract-ts-types.d.ts.map +1 -1
  91. package/dist/scripts/extract-ts-types.js +107 -23
  92. package/dist/scripts/extract-ts-types.test.js +225 -26
  93. package/dist/scripts/generate-spec.d.ts.map +1 -1
  94. package/dist/scripts/generate-spec.js +14 -5
  95. package/dist/scripts/generate-spec.test.js +93 -0
  96. package/dist/scripts/sandbox-empty-buckets.test.d.ts +2 -0
  97. package/dist/scripts/sandbox-empty-buckets.test.d.ts.map +1 -0
  98. package/dist/scripts/sandbox-empty-buckets.test.js +171 -0
  99. package/dist/scripts/sandbox.d.ts +54 -0
  100. package/dist/scripts/sandbox.d.ts.map +1 -1
  101. package/dist/scripts/sandbox.js +163 -25
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/package.json +10 -1
  105. package/src/api.test.ts +25 -0
  106. package/src/api.ts +39 -0
  107. package/src/bb-utils.ts +3 -0
  108. package/src/cdk/apigateway-account.ts +66 -0
  109. package/src/cdk/blocks-backend.test.ts +168 -60
  110. package/src/cdk/blocks-backend.ts +299 -227
  111. package/src/cdk/blocks-defaults.test.ts +21 -0
  112. package/src/cdk/blocks-defaults.ts +79 -1
  113. package/src/cdk/blocks-stack.test.ts +57 -13
  114. package/src/cdk/compute/compute-registry.ts +45 -0
  115. package/src/cdk/compute/compute.ts +114 -2
  116. package/src/cdk/compute/default-compute-factory.ts +1 -0
  117. package/src/cdk/config-registry.test.ts +149 -0
  118. package/src/cdk/config-registry.ts +92 -34
  119. package/src/cdk/dashboard-registry.ts +68 -0
  120. package/src/cdk/index.ts +427 -256
  121. package/src/cdk/internal.ts +6 -2
  122. package/src/cdk/tracer-registry.ts +54 -0
  123. package/src/cdk/vpc-requirements-registry.ts +63 -0
  124. package/src/cdk/vpc-types.ts +158 -0
  125. package/src/cdk/vpc.test.ts +348 -0
  126. package/src/cdk/vpc.ts +336 -0
  127. package/src/client/index.ts +1 -1
  128. package/src/common/config.test.ts +21 -0
  129. package/src/common/config.ts +47 -3
  130. package/src/common/index.ts +8 -0
  131. package/src/errors.ts +26 -0
  132. package/src/hosting.test.ts +87 -1
  133. package/src/hosting.ts +24 -1
  134. package/src/index.cdk.ts +11 -1
  135. package/src/index.ts +1 -1
  136. package/src/lambda-handler.test.ts +220 -2
  137. package/src/lambda-handler.ts +65 -4
  138. package/src/raw-route.test.ts +427 -1
  139. package/src/raw-route.ts +125 -12
  140. package/src/scripts/dev-server.ts +12 -1
  141. package/src/scripts/extract-ts-types.test.ts +228 -26
  142. package/src/scripts/extract-ts-types.ts +104 -20
  143. package/src/scripts/generate-spec.test.ts +101 -0
  144. package/src/scripts/generate-spec.ts +15 -5
  145. package/src/scripts/sandbox-empty-buckets.test.ts +191 -0
  146. package/src/scripts/sandbox.ts +185 -24
  147. package/src/version.ts +1 -1
@@ -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,CAsQ1B;AAyQD;;;GAGG;AACH,wBAAsB,SAAS,CAC9B,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,gBAAgB,GACvB,OAAO,CAAC,IAAI,CAAC,CA0Bf"}
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
- if (tsTypes.get(methodName)?.skipCodegen || skipCodegenNames.has(methodName))
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' };
@@ -15,6 +15,7 @@ import { join, dirname } from 'path';
15
15
  import { tmpdir } from 'os';
16
16
  import { fileURLToPath, pathToFileURL } from 'url';
17
17
  import { generateSpec } from './generate-spec.js';
18
+ import { ApiNamespace } from '../api.js';
18
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
20
  // `import.meta.url` lives in dist/scripts/, so `..` is dist/ and the built
20
21
  // `api.js` is next to it. Re-using the real ApiNamespace ensures the marker
@@ -86,3 +87,95 @@ describe('generateSpec — @blocksSkipCodegen', () => {
86
87
  }
87
88
  });
88
89
  });
90
+ describe('generateSpec — namespaces sharing a method name (regression: #445)', () => {
91
+ it('emits each namespace its OWN schema for a same-named method', async () => {
92
+ const dir = join(tmpdir(), `blocks-spec-test-${Date.now()}-collision`);
93
+ mkdirSync(dir, { recursive: true });
94
+ writeTsconfig(dir);
95
+ // Typed foundation for the TS type extractor. `create` has DIFFERENT params
96
+ // in each namespace; binding names (widgets/subscriptions) match the runtime
97
+ // module's export names below.
98
+ writeFileSync(join(dir, 'index.ts'), `
99
+ interface ApiNamespaceConstructor { new <T>(scope: any, name: string, handler: (ctx: any) => T): T; }
100
+ const ApiNamespace: ApiNamespaceConstructor = class {} as any;
101
+
102
+ export const widgets = new ApiNamespace(null, 'widgets', () => ({
103
+ async create(label: string): Promise<{ widgetId: string }> { return { widgetId: 'w' }; },
104
+ }));
105
+ export const subscriptions = new ApiNamespace(null, 'subscriptions', () => ({
106
+ async create(topicCount: number): Promise<{ subId: number }> { return { subId: 1 }; },
107
+ }));
108
+ `);
109
+ // Runtime module for namespace discovery (types come from the .ts above via
110
+ // the extractor). Use the real ApiNamespace so the marker symbol matches.
111
+ const widgets = new ApiNamespace(null, 'widgets', () => ({ async create(_label) { return { widgetId: 'w' }; } }));
112
+ const subscriptions = new ApiNamespace(null, 'subscriptions', () => ({ async create(_topicCount) { return { subId: 1 }; } }));
113
+ const loader = async () => ({ widgets, subscriptions });
114
+ try {
115
+ const doc = await generateSpec(join(dir, 'index.ts'), loader);
116
+ const byName = new Map(doc.methods.map((m) => [m.name, m]));
117
+ const w = byName.get('widgets.create');
118
+ const s = byName.get('subscriptions.create');
119
+ assert.ok(w && s, 'both namespace-qualified methods should be present');
120
+ // Each keeps its OWN param — no cross-assignment.
121
+ assert.strictEqual(w.params[0]?.name, 'label');
122
+ assert.strictEqual(w.params[0]?.schema?.type, 'string');
123
+ assert.strictEqual(s.params[0]?.name, 'topicCount');
124
+ assert.strictEqual(s.params[0]?.schema?.type, 'number');
125
+ }
126
+ finally {
127
+ rmSync(dir, { recursive: true, force: true });
128
+ }
129
+ });
130
+ });
131
+ describe('generateSpec — namespace returned by a factory, then destructured (regression: #444)', () => {
132
+ it('emits real schemas (not unknown) for a factory/destructured namespace', async () => {
133
+ const dir = join(tmpdir(), `blocks-spec-test-${Date.now()}-factory`);
134
+ mkdirSync(dir, { recursive: true });
135
+ writeTsconfig(dir);
136
+ // Typed foundation: one directly-exported namespace and one built inside a
137
+ // factory, returned as a property, destructured, and re-exported. Both use
138
+ // `create` with DIFFERENT params so a regression would cross-assign or emit
139
+ // unknown.
140
+ writeFileSync(join(dir, 'index.ts'), `
141
+ interface ApiNamespaceConstructor { new <T>(scope: any, name: string, handler: (ctx: any) => T): T; }
142
+ const ApiNamespace: ApiNamespaceConstructor = class {} as any;
143
+ class Scope { constructor(id: string) {} }
144
+
145
+ class Factory {
146
+ constructor(private readonly scope: Scope) {}
147
+ build() {
148
+ return {
149
+ subscriptions: new ApiNamespace(this.scope, 'subscriptions', () => ({
150
+ async create(topicCount: number): Promise<{ subId: number }> { return { subId: 1 }; },
151
+ })),
152
+ };
153
+ }
154
+ }
155
+
156
+ const scope = new Scope('app');
157
+ export const widgets = new ApiNamespace(scope, 'widgets', () => ({
158
+ async create(label: string): Promise<{ widgetId: string }> { return { widgetId: 'w' }; },
159
+ }));
160
+ const { subscriptions } = new Factory(scope).build();
161
+ export { subscriptions };
162
+ `);
163
+ const widgets = new ApiNamespace(null, 'widgets', () => ({ async create(_label) { return { widgetId: 'w' }; } }));
164
+ const subscriptions = new ApiNamespace(null, 'subscriptions', () => ({ async create(_topicCount) { return { subId: 1 }; } }));
165
+ const loader = async () => ({ widgets, subscriptions });
166
+ try {
167
+ const doc = await generateSpec(join(dir, 'index.ts'), loader);
168
+ const byName = new Map(doc.methods.map((m) => [m.name, m]));
169
+ const w = byName.get('widgets.create');
170
+ const s = byName.get('subscriptions.create');
171
+ assert.ok(w && s, 'both qualified methods present');
172
+ // The factory-returned namespace keeps its own param, not `unknown` and
173
+ // not the direct namespace's `string`.
174
+ assert.strictEqual(s.params[0]?.schema?.type, 'number');
175
+ assert.strictEqual(w.params[0]?.schema?.type, 'string');
176
+ }
177
+ finally {
178
+ rmSync(dir, { recursive: true, force: true });
179
+ }
180
+ });
181
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sandbox-empty-buckets.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-empty-buckets.test.d.ts","sourceRoot":"","sources":["../../src/scripts/sandbox-empty-buckets.test.ts"],"names":[],"mappings":""}