@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
@@ -1,7 +1,13 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
- import { describe, it, beforeEach } from 'node:test';
3
+ import { describe, it, before, beforeEach } from 'node:test';
4
4
  import assert from 'node:assert';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { buildSync } from 'esbuild';
5
11
  import { compilePath, registerRoute, matchRoute, getRegisteredRoutes, clearRouteRegistry, lockRouteRegistry, unlockRouteRegistry, RawRouteErrors, derivePathFromScope, resolveRoutePath, } from './raw-route.js';
6
12
  const noop = async (_ctx) => { };
7
13
  /** Create a null-prototype object for comparing with params from matchRoute. */
@@ -595,3 +601,328 @@ describe('resolveRoutePath', () => {
595
601
  assert.strictEqual(path, '/v1/users');
596
602
  });
597
603
  });
604
+ // ── Cross-copy registry sharing ─────────────────────────────────────────────
605
+ //
606
+ // A bundle can contain more than one physical copy of @aws-blocks/core (an
607
+ // unlucky dependency tree, no dedupe). With module-local registry state, every
608
+ // copy gets its own route table: Building Blocks register into theirs, the
609
+ // dispatcher reads its own, and the routes 404 silently.
610
+ //
611
+ // These tests stand in for a second copy: they poke the shared state directly
612
+ // through globalThis, exactly as a second copy of the module would see it.
613
+ // The key is spelled out as a literal on purpose — it IS the cross-copy
614
+ // contract, so changing it must break a test.
615
+ const REGISTRY_KEY = '__AWS_BLOCKS_RAW_ROUTE_REGISTRY_V1__';
616
+ /** Read the registry state that a second core copy would share with us. */
617
+ function sharedState() {
618
+ const state = globalThis[REGISTRY_KEY];
619
+ assert.ok(state, `route registry state must live on globalThis['${REGISTRY_KEY}']`);
620
+ return state;
621
+ }
622
+ describe('registry state is shared across core copies', () => {
623
+ it('matchRoute() resolves a route registered by another core copy', () => {
624
+ const { pattern, paramNames } = compilePath('/from-other-copy');
625
+ sharedState().routes.push({
626
+ method: 'GET',
627
+ path: '/from-other-copy',
628
+ pattern,
629
+ paramNames,
630
+ handler: noop,
631
+ });
632
+ const result = matchRoute('GET', '/from-other-copy');
633
+ assert.ok(result, 'route registered by another copy must be dispatchable');
634
+ assert.strictEqual(result.route.path, '/from-other-copy');
635
+ });
636
+ it('getRegisteredRoutes() reports routes registered by another core copy', () => {
637
+ const { pattern, paramNames } = compilePath('/synth-visible');
638
+ sharedState().routes.push({
639
+ method: 'GET',
640
+ path: '/synth-visible',
641
+ pattern,
642
+ paramNames,
643
+ handler: noop,
644
+ });
645
+ // Synth-time consumers (CloudFront behaviors in hosting.ts) iterate this.
646
+ assert.ok(getRegisteredRoutes().some((r) => r.path === '/synth-visible'));
647
+ });
648
+ it('registerRoute() publishes the route into the shared state', () => {
649
+ registerRoute({ method: 'POST', path: '/published', handler: noop });
650
+ assert.ok(sharedState().routes.some((r) => r.method === 'POST' && r.path === '/published'), 'another copy must be able to see routes we registered');
651
+ });
652
+ it('registerRoute() honours a lock set by another core copy', () => {
653
+ sharedState().locked = true;
654
+ assert.throws(() => registerRoute({ method: 'GET', path: '/after-foreign-lock', handler: noop }), (err) => {
655
+ assert.ok(err.message.includes('Cannot register routes after handler creation'));
656
+ return true;
657
+ });
658
+ });
659
+ it('lockRouteRegistry() locks the shared state, not a module-local flag', () => {
660
+ lockRouteRegistry();
661
+ assert.strictEqual(sharedState().locked, true);
662
+ });
663
+ it('clearRouteRegistry() clears the shared state and releases the shared lock', () => {
664
+ registerRoute({ method: 'GET', path: '/leftover', handler: noop });
665
+ lockRouteRegistry();
666
+ clearRouteRegistry();
667
+ assert.deepStrictEqual(sharedState().routes, []);
668
+ assert.strictEqual(sharedState().locked, false);
669
+ });
670
+ it('clearRouteRegistry() does not reset the copy counter', () => {
671
+ const before = sharedState().copies;
672
+ clearRouteRegistry();
673
+ assert.strictEqual(sharedState().copies, before, 'copies counts module loads, not routes');
674
+ });
675
+ // Behavior change, deliberately kept: with a shared registry, duplicate
676
+ // detection reaches across copies for the first time. An app with two core
677
+ // copies AND two instances of the same Building Block used to split the
678
+ // routes silently (half of them 404ing); it now fails loudly at startup.
679
+ // The app was already broken — this only makes it say so.
680
+ it('rejects a duplicate of a route another core copy registered', () => {
681
+ const { pattern, paramNames } = compilePath('/shared-path');
682
+ sharedState().routes.push({
683
+ method: 'GET',
684
+ path: '/shared-path',
685
+ pattern,
686
+ paramNames,
687
+ handler: noop,
688
+ });
689
+ assert.throws(() => registerRoute({ method: 'GET', path: '/shared-path', handler: noop }), (err) => {
690
+ assert.strictEqual(err.name, RawRouteErrors.DuplicateRoute);
691
+ return true;
692
+ });
693
+ });
694
+ });
695
+ describe('duplicate core copies are announced', () => {
696
+ /**
697
+ * Use raw-route.js in a fresh process, optionally pre-seeding the shared
698
+ * state as an earlier copy would have left it, and return stderr.
699
+ *
700
+ * The copy must be *used*, not merely imported: the counter increments on
701
+ * first registry access, so a copy that never touches the registry is never
702
+ * counted — deliberately, since only routing copies matter here.
703
+ */
704
+ function useInFreshProcess(preSeededCopies) {
705
+ const moduleUrl = new URL('./raw-route.js', import.meta.url).href;
706
+ const seed = preSeededCopies === null
707
+ ? ''
708
+ : `globalThis[${JSON.stringify(REGISTRY_KEY)}] = { routes: [], locked: false, copies: ${preSeededCopies} };`;
709
+ const script = `${seed}const m = await import(${JSON.stringify(moduleUrl)}); m.registerRoute({ method: 'GET', path: '/probe', handler: async () => {} });`;
710
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], { encoding: 'utf8' });
711
+ assert.strictEqual(result.status, 0, `using raw-route.js failed: ${result.stderr}`);
712
+ return result.stderr;
713
+ }
714
+ it('warns when a second copy of @aws-blocks/core joins the shared registry', () => {
715
+ const stderr = useInFreshProcess(1);
716
+ assert.match(stderr, /2 copies of @aws-blocks\/core/);
717
+ });
718
+ it('stays silent for a healthy single-copy install', () => {
719
+ const stderr = useInFreshProcess(null);
720
+ assert.doesNotMatch(stderr, /copies of @aws-blocks\/core/);
721
+ });
722
+ it('warns once per copy, not once per registry access', () => {
723
+ const moduleUrl = new URL('./raw-route.js', import.meta.url).href;
724
+ const script = `globalThis[${JSON.stringify(REGISTRY_KEY)}] = { routes: [], locked: false, copies: 1 };
725
+ const m = await import(${JSON.stringify(moduleUrl)});
726
+ m.registerRoute({ method: 'GET', path: '/a', handler: async () => {} });
727
+ m.registerRoute({ method: 'GET', path: '/b', handler: async () => {} });
728
+ m.matchRoute('GET', '/a');`;
729
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], { encoding: 'utf8' });
730
+ assert.strictEqual(result.status, 0, result.stderr);
731
+ const warnings = result.stderr.split('\n').filter((line) => line.includes('copies of @aws-blocks/core'));
732
+ assert.strictEqual(warnings.length, 1, `expected exactly one warning, got: ${JSON.stringify(warnings)}`);
733
+ });
734
+ });
735
+ // ── A peer copy's state is adopted, not trusted ─────────────────────────────
736
+ //
737
+ // The registry key is a permanent contract: copies sharing a process are
738
+ // usually different *versions* of @aws-blocks/core, so the shape may only grow
739
+ // — a copy that renamed the key would re-split the registry silently, each key
740
+ // holding its own copy counter. The other half of that contract is this side:
741
+ // a copy must cope with a state a peer left without the fields it expects.
742
+ describe('registry state from a peer copy is adopted, not trusted', () => {
743
+ /**
744
+ * Import raw-route.js in a fresh process on top of a pre-seeded shared state
745
+ * and report what the copy made of it.
746
+ *
747
+ * `seed` stands in for a copy whose registry shape differs from this one's.
748
+ * Fields are omitted, never renamed — that is exactly the evolution the key's
749
+ * contract permits.
750
+ */
751
+ function useOnTopOf(seed) {
752
+ const moduleUrl = new URL('./raw-route.js', import.meta.url).href;
753
+ const script = `globalThis[${JSON.stringify(REGISTRY_KEY)}] = ${seed};
754
+ const m = await import(${JSON.stringify(moduleUrl)});
755
+ m.registerRoute({ method: 'GET', path: '/probe', handler: async () => {} });
756
+ console.log(JSON.stringify({
757
+ matched: m.matchRoute('GET', '/probe') !== null,
758
+ copies: m.getLoadedCoreCopies(),
759
+ }));`;
760
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], { encoding: 'utf8' });
761
+ assert.strictEqual(result.status, 0, `using raw-route.js failed: ${result.stderr}`);
762
+ // NaN serializes to null — which is the point of reporting it this way.
763
+ return JSON.parse(result.stdout.trim());
764
+ }
765
+ it('registers into a state that arrived without a route table', () => {
766
+ const { matched } = useOnTopOf('{ locked: false, copies: 1 }');
767
+ assert.strictEqual(matched, true, 'a missing route table must be created, not thrown on');
768
+ });
769
+ it('keeps the copy count a number when the state arrived without one', () => {
770
+ const { copies } = useOnTopOf('{ routes: [], locked: false }');
771
+ assert.strictEqual(copies, 1, 'a missing counter must restart at a number — NaN would mute the warning');
772
+ });
773
+ });
774
+ /**
775
+ * The probe itself: a Building Block registers its route through one copy of
776
+ * the package, the dispatcher looks it up through the other.
777
+ *
778
+ * Kept as a standalone ES module with static imports so the very same source
779
+ * can be run as-is and, in the test below, sent through a bundler first.
780
+ */
781
+ const TWO_COPY_ENTRY = `
782
+ import * as a from './copy-a/dist/raw-route.js';
783
+ import * as b from './copy-b/dist/raw-route.js';
784
+
785
+ a.registerRoute({ method: 'GET', path: '/aws-blocks/auth/signin/google', handler: async () => {} });
786
+ const matched = b.matchRoute('GET', '/aws-blocks/auth/signin/google');
787
+
788
+ const observed = {
789
+ distinctInstances: a !== b,
790
+ matchedPath: matched ? matched.route.path : null,
791
+ routesSeenByDispatcher: b.getRegisteredRoutes().length,
792
+ copiesReported: null,
793
+ };
794
+ // Report the routing facts even if the diagnostics helper is missing or
795
+ // throws — whether the route resolves is what this probe is about, and a
796
+ // build that lost the helper must not hide that behind a crashed child.
797
+ try {
798
+ observed.copiesReported = b.getLoadedCoreCopies();
799
+ } catch {}
800
+ console.log(JSON.stringify(observed));
801
+ `;
802
+ /**
803
+ * Install two physical copies of this package and route a request across them.
804
+ *
805
+ * Runs in a child process so the copies start from a clean `globalThis` and
806
+ * cannot leak registry state into this test run. The whole directory is copied
807
+ * rather than the two files currently needed, so the probe keeps working when
808
+ * `raw-route.js` grows an import.
809
+ *
810
+ * With `bundle`, the probe is first bundled the way a deployed handler is —
811
+ * `NodejsFunction` runs esbuild with `--conditions=aws-runtime` — before it is
812
+ * executed.
813
+ */
814
+ function runTwoCopyProbe({ bundle }) {
815
+ const distDir = dirname(fileURLToPath(import.meta.url));
816
+ const packageDir = dirname(distDir);
817
+ const root = mkdtempSync(join(tmpdir(), 'aws-blocks-two-copies-'));
818
+ try {
819
+ // Copy A belongs to the Building Block, copy B to the dispatcher. Each is
820
+ // laid out as an installed package — `package.json` beside `dist/`, the
821
+ // shape npm produces for a nested `node_modules/@aws-blocks/core`. The
822
+ // manifest has to come along: a real install has one, and it is what a
823
+ // bundler reads for `type` and `sideEffects` — without it the bundle here
824
+ // would be built under different rules than the deployed one.
825
+ for (const copy of ['copy-a', 'copy-b']) {
826
+ cpSync(distDir, join(root, copy, 'dist'), { recursive: true });
827
+ cpSync(join(packageDir, 'package.json'), join(root, copy, 'package.json'));
828
+ }
829
+ const entry = join(root, 'entry.mjs');
830
+ writeFileSync(entry, TWO_COPY_ENTRY);
831
+ let script = entry;
832
+ let bundleText = null;
833
+ if (bundle) {
834
+ const outfile = join(root, 'bundle.mjs');
835
+ buildSync({
836
+ entryPoints: [entry],
837
+ outfile,
838
+ bundle: true,
839
+ format: 'esm',
840
+ platform: 'node',
841
+ conditions: ['aws-runtime'],
842
+ minify: true,
843
+ logLevel: 'silent',
844
+ });
845
+ bundleText = readFileSync(outfile, 'utf8');
846
+ script = outfile;
847
+ }
848
+ const result = spawnSync(process.execPath, [script], { encoding: 'utf8' });
849
+ assert.strictEqual(result.status, 0, `two-copy probe failed: ${result.stderr}`);
850
+ return {
851
+ ...JSON.parse(result.stdout),
852
+ stderr: result.stderr,
853
+ bundleText,
854
+ };
855
+ }
856
+ finally {
857
+ rmSync(root, { recursive: true, force: true });
858
+ }
859
+ }
860
+ describe('two physical core copies share one registry', () => {
861
+ let probe;
862
+ before(() => {
863
+ probe = runTwoCopyProbe({ bundle: false });
864
+ });
865
+ it('loads two separate module instances', () => {
866
+ // Guards the probe itself: if this ever collapses to one instance, the
867
+ // tests below would pass without exercising anything.
868
+ assert.strictEqual(probe.distinctInstances, true);
869
+ });
870
+ it('dispatches a route the other copy registered', () => {
871
+ assert.strictEqual(probe.matchedPath, '/aws-blocks/auth/signin/google', 'the dispatcher copy must resolve a route registered through the other copy');
872
+ });
873
+ it('shows the route in the table the dispatcher consults', () => {
874
+ // A zero here is the fingerprint of the original defect: registration
875
+ // succeeded, and the dispatcher looked at an empty table.
876
+ assert.strictEqual(probe.routesSeenByDispatcher, 1);
877
+ });
878
+ it('counts and announces both copies', () => {
879
+ assert.strictEqual(probe.copiesReported, 2);
880
+ assert.match(probe.stderr, /2 copies of @aws-blocks\/core/);
881
+ });
882
+ });
883
+ // ── The same thing, but bundled ─────────────────────────────────────────────
884
+ //
885
+ // Duplicate copies are a bundling phenomenon: the reported defect was found in
886
+ // a deployed bundle, not in a node_modules tree at rest. A deployed handler is
887
+ // built by `NodejsFunction`, which runs esbuild with `--conditions=aws-runtime`
888
+ // (see cdk/blocks-backend.ts), so this repeats that step and then runs the
889
+ // result. Minified on purpose — it is the harsher case, and passing it implies
890
+ // the unminified one.
891
+ //
892
+ // What this pins down that the unbundled probe cannot:
893
+ //
894
+ // - that a bundler keeps two copies of the same file at different paths
895
+ // rather than collapsing them, i.e. that the failure mode reaches the
896
+ // deployed artifact at all;
897
+ // - that reaching the registry through `globalThis[<string key>]` survives
898
+ // minification, so the two copies still meet;
899
+ // - that the key stays a greppable literal, which is the documented way to
900
+ // diagnose a duplicate install in a deployed bundle (and the reason it is
901
+ // a plain string rather than a `Symbol.for()` key).
902
+ //
903
+ // What it deliberately does *not* claim: that counting copies on first use
904
+ // rather than at module load is required to survive tree-shaking. That was
905
+ // measured — with `sideEffects` honoured and `--minify` on, esbuild retains a
906
+ // top-level counter here, because a module whose exports are used is kept
907
+ // whole. Counting on first use stands on its other stated reason: it counts the
908
+ // copies that take part in routing, not the ones merely imported.
909
+ describe('the shared registry survives bundling', () => {
910
+ let probe;
911
+ before(() => {
912
+ probe = runTwoCopyProbe({ bundle: true });
913
+ });
914
+ it('keeps two participating copies in one bundle', () => {
915
+ // Also guards the test: were the two copies collapsed into one module
916
+ // scope, the assertions below would pass without ever crossing a copy
917
+ // boundary — and there would be no defect left to protect against.
918
+ assert.strictEqual(probe.copiesReported, 2, 'the bundle must still contain two participating copies');
919
+ assert.strictEqual(probe.distinctInstances, true);
920
+ });
921
+ it('dispatches a route the other copy registered', () => {
922
+ assert.strictEqual(probe.matchedPath, '/aws-blocks/auth/signin/google');
923
+ assert.strictEqual(probe.routesSeenByDispatcher, 1);
924
+ });
925
+ it('keeps the registry key greppable in the bundle', () => {
926
+ assert.ok(probe.bundleText?.includes('__AWS_BLOCKS_RAW_ROUTE_REGISTRY_V1__'));
927
+ });
928
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../src/scripts/dev-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../src/scripts/dev-server.ts"],"names":[],"mappings":"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"}
@@ -784,6 +784,10 @@ export async function startDevServer(options) {
784
784
  });
785
785
  // WebSocket upgrade — route to frontend (HMR) or dev attachments
786
786
  server.on('upgrade', (req, socket, head) => {
787
+ // A stale client can reset the connection mid-upgrade (ECONNRESET). The raw
788
+ // socket has no 'error' listener at this point, so Node's default handler
789
+ // would kill the dev server. Attach one before any parsing/routing.
790
+ socket.on('error', () => socket.destroy());
787
791
  const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
788
792
  if (url.pathname === '/realtime')
789
793
  return; // handled by dev attachment (noServer mode)
@@ -869,6 +873,13 @@ export async function startDevServer(options) {
869
873
  onExhausted: () => process.exit(1),
870
874
  warn: (msg) => console.error(msg),
871
875
  });
876
+ // Malformed/aborted requests must not become unhandled socket errors.
877
+ server.on('clientError', (_err, socket) => {
878
+ if (socket.writable)
879
+ socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
880
+ else
881
+ socket.destroy();
882
+ });
872
883
  server.on('error', (err) => {
873
884
  if (err.code === 'EADDRINUSE') {
874
885
  // Keep the telemetry signal (unchanged) …
@@ -1 +1 @@
1
- {"version":3,"file":"extract-ts-types.d.ts","sourceRoot":"","sources":["../../src/scripts/extract-ts-types.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,gBAAgB;IAChC,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,+GAA+G;IAC/G,QAAQ,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,UAAU,EAAE,UAAU,CAAC;IACvB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAS3D;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA4DzE;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,KAAK,CAAC,EAAE,UAAU,GAAG,UAAU,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;IAC5C,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,oBAAoB,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAID;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAqElF"}
1
+ {"version":3,"file":"extract-ts-types.d.ts","sourceRoot":"","sources":["../../src/scripts/extract-ts-types.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,gBAAgB;IAChC,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,+GAA+G;IAC/G,QAAQ,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,UAAU,EAAE,UAAU,CAAC;IACvB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAS3D;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAgEzE;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,KAAK,CAAC,EAAE,UAAU,GAAG,UAAU,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;IAC5C,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,oBAAoB,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAID;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAoHlF"}
@@ -46,13 +46,16 @@ export function extractSkipCodegenMethods(sourcePath) {
46
46
  return result;
47
47
  const sourceFile = ts.createSourceFile(absPath, fileText, ts.ScriptTarget.ESNext,
48
48
  /*setParentNodes*/ true, ts.ScriptKind.TS);
49
- function visitObjectLiteral(obj) {
49
+ function visitObjectLiteral(obj, namespaceName) {
50
50
  for (const prop of obj.properties) {
51
51
  if (ts.isMethodDeclaration(prop) &&
52
52
  prop.name &&
53
53
  ts.isIdentifier(prop.name) &&
54
54
  hasBlocksSkipCodegenTag(prop)) {
55
- result.add(prop.name.text);
55
+ // Qualify by namespace (like extractMethodTypes) so a tag on one
56
+ // namespace's `create` doesn't cause another namespace's `create` to be
57
+ // dropped from the spec. Bare key only when the namespace is unknown.
58
+ result.add(namespaceName ? `${namespaceName}.${prop.name.text}` : prop.name.text);
56
59
  }
57
60
  }
58
61
  }
@@ -61,23 +64,24 @@ export function extractSkipCodegenMethods(sourcePath) {
61
64
  const callee = node.expression;
62
65
  if (ts.isIdentifier(callee) && callee.text === 'ApiNamespace' && node.arguments && node.arguments.length >= 2) {
63
66
  const handler = node.arguments[node.arguments.length - 1];
67
+ const nsName = enclosingBindingName(node);
64
68
  if (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) {
65
69
  const body = handler.body;
66
70
  if (ts.isParenthesizedExpression(body) && ts.isObjectLiteralExpression(body.expression)) {
67
- visitObjectLiteral(body.expression);
71
+ visitObjectLiteral(body.expression, nsName);
68
72
  }
69
73
  else if (ts.isObjectLiteralExpression(body)) {
70
- visitObjectLiteral(body);
74
+ visitObjectLiteral(body, nsName);
71
75
  }
72
76
  else if (ts.isBlock(body)) {
73
77
  for (const stmt of body.statements) {
74
78
  if (ts.isReturnStatement(stmt) && stmt.expression) {
75
79
  if (ts.isObjectLiteralExpression(stmt.expression)) {
76
- visitObjectLiteral(stmt.expression);
80
+ visitObjectLiteral(stmt.expression, nsName);
77
81
  }
78
82
  else if (ts.isParenthesizedExpression(stmt.expression) &&
79
83
  ts.isObjectLiteralExpression(stmt.expression.expression)) {
80
- visitObjectLiteral(stmt.expression.expression);
84
+ visitObjectLiteral(stmt.expression.expression, nsName);
81
85
  }
82
86
  }
83
87
  }
@@ -135,7 +139,7 @@ export function extractMethodTypes(sourcePath) {
135
139
  const fnName = node.expression;
136
140
  if (ts.isIdentifier(fnName) && fnName.text === 'ApiNamespace' && node.arguments && node.arguments.length >= 2) {
137
141
  const handlerArg = node.arguments[node.arguments.length - 1];
138
- extractMethodsFromHandler(handlerArg, checker, sourceFile, result);
142
+ extractMethodsFromHandler(handlerArg, checker, sourceFile, result, enclosingBindingName(node));
139
143
  }
140
144
  }
141
145
  ts.forEachChild(node, visit);
@@ -148,19 +152,67 @@ export function extractMethodTypes(sourcePath) {
148
152
  if (!ts.isVariableStatement(node))
149
153
  return;
150
154
  for (const decl of node.declarationList.declarations) {
151
- if (!decl.initializer || !ts.isIdentifier(decl.name))
155
+ if (!decl.initializer)
156
+ continue;
157
+ // (a) Simple binding: `const api = auth.createApi()` /
158
+ // `export const api = backend.namespace`.
159
+ if (ts.isIdentifier(decl.name)) {
160
+ // Skip if we already extracted methods for this via the AST walk
161
+ if (ts.isNewExpression(decl.initializer)) {
162
+ const callee = decl.initializer.expression;
163
+ if (ts.isIdentifier(callee) && callee.text === 'ApiNamespace')
164
+ continue;
165
+ }
166
+ // Use the type checker to get the type of the initializer expression.
167
+ // For `auth.createApi()`, this resolves through the method's return type
168
+ // to the `AsyncAPI<T>` type that ApiNamespace returns.
169
+ const initType = checker.getTypeAtLocation(decl.initializer);
170
+ extractMethodsFromResolvedType(initType, checker, result, decl.name.text);
152
171
  continue;
153
- // Skip if we already extracted methods for this via the AST walk
154
- if (ts.isNewExpression(decl.initializer)) {
155
- const callee = decl.initializer.expression;
156
- if (ts.isIdentifier(callee) && callee.text === 'ApiNamespace')
157
- continue;
158
172
  }
159
- // Use the type checker to get the type of the initializer expression.
160
- // For `auth.createApi()`, this resolves through the method's return type
161
- // to the `AsyncAPI<T>` type that ApiNamespace returns.
162
- const initType = checker.getTypeAtLocation(decl.initializer);
163
- extractMethodsFromResolvedType(initType, checker, result);
173
+ // (b) Object binding pattern: `const { indirectNamespace } = factory()`.
174
+ // Each destructured binding is a separate namespace whose name is the
175
+ // LOCAL binding name (the name it's exported under). Resolve the property
176
+ // type off the initializer and extract methods keyed by that name, so a
177
+ // factory-returned namespace keys qualified (`indirectNamespace.method`)
178
+ // like a directly-constructed one — instead of landing on a bare key that
179
+ // collides with same-named methods in other namespaces (#444, #445).
180
+ //
181
+ // This adds the qualified key ALONGSIDE the bare key the first-pass AST
182
+ // walk already emitted for the same `new ApiNamespace(...)` (its
183
+ // `enclosingBindingName` returns undefined behind the factory, so it keys
184
+ // bare). Leaving the bare key is harmless: `generate-spec` looks up
185
+ // `tsTypes.get(qualified) ?? tsTypes.get(bare)`, so the qualified key
186
+ // added here always wins for an attributable namespace, and the bare
187
+ // entry never influences the emitted schema (a regression test asserts
188
+ // this under a name collision). The #498 bare-name fallback is retained
189
+ // on purpose — it's the last resort for namespaces this pass still can't
190
+ // attribute (default exports, deeper indirection, the patterns below), so
191
+ // it is NOT redundant.
192
+ //
193
+ // Boundary: only a top-level identifier (a) and a shallow object binding
194
+ // pattern (b) are attributed. Array patterns (`const [ns] = factory()`)
195
+ // and nested patterns (`const { a: { b } } = …`) fall through to the
196
+ // bare-key path, where the #498 fallback still resolves the schema when
197
+ // there's no collision. These shapes are rare for namespace exports and
198
+ // fail soft; widening the traversal can come later if a real case appears.
199
+ if (ts.isObjectBindingPattern(decl.name)) {
200
+ const initType = checker.getTypeAtLocation(decl.initializer);
201
+ for (const element of decl.name.elements) {
202
+ if (!ts.isIdentifier(element.name))
203
+ continue;
204
+ const localName = element.name.text;
205
+ // `const { foo: bar } = …` binds `bar` but reads property `foo`.
206
+ const propKey = element.propertyName && ts.isIdentifier(element.propertyName)
207
+ ? element.propertyName.text
208
+ : localName;
209
+ const propSymbol = initType.getProperty(propKey);
210
+ if (!propSymbol)
211
+ continue;
212
+ const propType = checker.getTypeOfSymbol(propSymbol);
213
+ extractMethodsFromResolvedType(propType, checker, result, localName);
214
+ }
215
+ }
164
216
  }
165
217
  });
166
218
  return result;
@@ -180,7 +232,7 @@ function findTsConfig(dir) {
180
232
  return undefined;
181
233
  }
182
234
  // ── Handler extraction ──────────────────────────────────────────────────────
183
- function extractMethodsFromHandler(handlerNode, checker, sourceFile, result) {
235
+ function extractMethodsFromHandler(handlerNode, checker, sourceFile, result, namespaceName) {
184
236
  let objectLiteral;
185
237
  if (ts.isArrowFunction(handlerNode) || ts.isFunctionExpression(handlerNode)) {
186
238
  const body = handlerNode.body;
@@ -212,11 +264,42 @@ function extractMethodsFromHandler(handlerNode, checker, sourceFile, result) {
212
264
  if (ts.isMethodDeclaration(prop) && prop.name && ts.isIdentifier(prop.name)) {
213
265
  const methodName = prop.name.text;
214
266
  const info = extractMethodTypeInfo(prop, checker, sourceFile);
267
+ // Key by the namespace-qualified name when the namespace is known, so two
268
+ // namespaces sharing a method name (e.g. widgets.create / subscriptions.create)
269
+ // don't collide and cross-assign schemas. Fall back to the bare name when the
270
+ // namespace can't be determined (consumers try qualified, then bare).
215
271
  if (info)
216
- result.set(methodName, info);
272
+ result.set(namespaceName ? `${namespaceName}.${methodName}` : methodName, info);
217
273
  }
218
274
  }
219
275
  }
276
+ /**
277
+ * Walk up from a `new ApiNamespace(...)` node to the name it's bound to
278
+ * (`export const widgets = new ApiNamespace(...)` → `"widgets"`). This is the
279
+ * name the dev server / Lambda handler route by, and the one `generate-spec`
280
+ * uses to qualify method names. Returns `undefined` when the namespace is
281
+ * created behind indirection (factory return, etc.), in which case methods key
282
+ * by their bare name.
283
+ *
284
+ * Limitation: this is the local binding name, not the export name. An export
285
+ * rename — `const widgets = new ApiNamespace(...); export { widgets as gadgets }`
286
+ * — keys `widgets.*` while `generate-spec` looks up `gadgets.*`, so the qualified
287
+ * lookup misses and falls back to the bare key (schemas resolve if there's no
288
+ * collision, else `unknown`). Rare; resolving the export alias would remove it.
289
+ */
290
+ function enclosingBindingName(node) {
291
+ let current = node.parent;
292
+ while (current) {
293
+ if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name))
294
+ return current.name.text;
295
+ // Don't cross a function/block boundary — beyond it the binding no longer
296
+ // corresponds to a directly-exported namespace.
297
+ if (ts.isSourceFile(current) || ts.isBlock(current) || ts.isFunctionLike(current))
298
+ return undefined;
299
+ current = current.parent;
300
+ }
301
+ return undefined;
302
+ }
220
303
  function extractMethodTypeInfo(method, checker, sourceFile) {
221
304
  const params = [];
222
305
  for (const param of method.parameters) {
@@ -284,7 +367,7 @@ function unwrapPromiseTypeNode(node) {
284
367
  * We also handle the case where the type is callable (the raw handler function
285
368
  * before `AsyncAPI` wrapping) — we call-resolve it and inspect the result.
286
369
  */
287
- function extractMethodsFromResolvedType(type, checker, result) {
370
+ function extractMethodsFromResolvedType(type, checker, result, namespaceName) {
288
371
  // The runtime value of an ApiNamespace export is the handler function itself
289
372
  // (with the marker symbol attached). Its TS type is `AsyncAPI<T>`, which is
290
373
  // an object type with method-like properties. But it could also be typed as
@@ -309,8 +392,9 @@ function extractMethodsFromResolvedType(type, checker, result) {
309
392
  // Skip internal symbols and the marker
310
393
  if (propName.startsWith('_') || propName === 'Symbol(blocks:ApiNamespace)')
311
394
  continue;
395
+ const key = namespaceName ? `${namespaceName}.${propName}` : propName;
312
396
  // Don't overwrite methods already found by the AST walk
313
- if (result.has(propName))
397
+ if (result.has(key))
314
398
  continue;
315
399
  const propType = checker.getTypeOfSymbol(prop);
316
400
  const methodSigs = propType.getCallSignatures();
@@ -346,7 +430,7 @@ function extractMethodsFromResolvedType(type, checker, result) {
346
430
  if (declaration && hasBlocksSkipCodegenTag(declaration)) {
347
431
  skipCodegen = true;
348
432
  }
349
- result.set(propName, { params, returnType, transferable, skipCodegen });
433
+ result.set(key, { params, returnType, transferable, skipCodegen });
350
434
  }
351
435
  }
352
436
  // ── Transferable detection ───────────────────────────────────────────────────