@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
|
@@ -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' };
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-empty-buckets.test.d.ts","sourceRoot":"","sources":["../../src/scripts/sandbox-empty-buckets.test.ts"],"names":[],"mappings":""}
|