@aws-blocks/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +12 -0
  2. package/dist/cdk/blocks-backend.d.ts +4 -1
  3. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  4. package/dist/cdk/blocks-backend.js +56 -10
  5. package/dist/cdk/blocks-backend.test.js +71 -1
  6. package/dist/cdk/blocks-defaults.d.ts +11 -0
  7. package/dist/cdk/blocks-defaults.d.ts.map +1 -1
  8. package/dist/cdk/blocks-stack.test.js +23 -3
  9. package/dist/cdk/compute/compute.d.ts +80 -2
  10. package/dist/cdk/compute/compute.d.ts.map +1 -1
  11. package/dist/cdk/compute/compute.js +57 -3
  12. package/dist/cdk/config-registry.test.js +12 -0
  13. package/dist/cdk/dashboard-registry.d.ts +41 -0
  14. package/dist/cdk/dashboard-registry.d.ts.map +1 -0
  15. package/dist/cdk/dashboard-registry.js +61 -0
  16. package/dist/cdk/index.d.ts +67 -11
  17. package/dist/cdk/index.d.ts.map +1 -1
  18. package/dist/cdk/index.js +100 -18
  19. package/dist/cdk/internal.d.ts +3 -1
  20. package/dist/cdk/internal.d.ts.map +1 -1
  21. package/dist/cdk/internal.js +4 -1
  22. package/dist/cdk/tracer-registry.d.ts +31 -0
  23. package/dist/cdk/tracer-registry.d.ts.map +1 -0
  24. package/dist/cdk/tracer-registry.js +49 -0
  25. package/dist/cdk/vpc-requirements-registry.d.ts +33 -0
  26. package/dist/cdk/vpc-requirements-registry.d.ts.map +1 -0
  27. package/dist/cdk/vpc-requirements-registry.js +46 -0
  28. package/dist/cdk/vpc-types.d.ts +151 -0
  29. package/dist/cdk/vpc-types.d.ts.map +1 -0
  30. package/dist/cdk/vpc-types.js +3 -0
  31. package/dist/cdk/vpc.d.ts +59 -0
  32. package/dist/cdk/vpc.d.ts.map +1 -0
  33. package/dist/cdk/vpc.js +298 -0
  34. package/dist/cdk/vpc.test.d.ts +2 -0
  35. package/dist/cdk/vpc.test.d.ts.map +1 -0
  36. package/dist/cdk/vpc.test.js +285 -0
  37. package/dist/errors.d.ts +5 -0
  38. package/dist/errors.d.ts.map +1 -1
  39. package/dist/errors.js +5 -0
  40. package/dist/hosting.d.ts.map +1 -1
  41. package/dist/hosting.js +2 -0
  42. package/dist/hosting.test.js +39 -1
  43. package/dist/index.cdk.d.ts +2 -1
  44. package/dist/index.cdk.d.ts.map +1 -1
  45. package/dist/index.cdk.js +1 -1
  46. package/dist/lambda-handler.js +9 -2
  47. package/dist/lambda-handler.test.js +61 -1
  48. package/dist/raw-route.d.ts +15 -1
  49. package/dist/raw-route.d.ts.map +1 -1
  50. package/dist/raw-route.js +96 -12
  51. package/dist/raw-route.test.js +332 -1
  52. package/dist/scripts/dev-server.d.ts.map +1 -1
  53. package/dist/scripts/dev-server.js +11 -0
  54. package/dist/scripts/extract-ts-types.d.ts.map +1 -1
  55. package/dist/scripts/extract-ts-types.js +107 -23
  56. package/dist/scripts/extract-ts-types.test.js +225 -26
  57. package/dist/scripts/generate-spec.d.ts.map +1 -1
  58. package/dist/scripts/generate-spec.js +14 -5
  59. package/dist/scripts/generate-spec.test.js +93 -0
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +8 -1
  63. package/src/cdk/blocks-backend.test.ts +144 -60
  64. package/src/cdk/blocks-backend.ts +298 -239
  65. package/src/cdk/blocks-defaults.ts +12 -0
  66. package/src/cdk/blocks-stack.test.ts +32 -13
  67. package/src/cdk/compute/compute.ts +105 -3
  68. package/src/cdk/config-registry.test.ts +14 -0
  69. package/src/cdk/dashboard-registry.ts +68 -0
  70. package/src/cdk/index.ts +426 -298
  71. package/src/cdk/internal.ts +6 -2
  72. package/src/cdk/tracer-registry.ts +54 -0
  73. package/src/cdk/vpc-requirements-registry.ts +63 -0
  74. package/src/cdk/vpc-types.ts +158 -0
  75. package/src/cdk/vpc.test.ts +348 -0
  76. package/src/cdk/vpc.ts +336 -0
  77. package/src/errors.ts +5 -0
  78. package/src/hosting.test.ts +59 -1
  79. package/src/hosting.ts +3 -0
  80. package/src/index.cdk.ts +7 -0
  81. package/src/lambda-handler.test.ts +79 -1
  82. package/src/lambda-handler.ts +11 -2
  83. package/src/raw-route.test.ts +427 -1
  84. package/src/raw-route.ts +125 -12
  85. package/src/scripts/dev-server.ts +12 -1
  86. package/src/scripts/extract-ts-types.test.ts +228 -26
  87. package/src/scripts/extract-ts-types.ts +104 -20
  88. package/src/scripts/generate-spec.test.ts +101 -0
  89. package/src/scripts/generate-spec.ts +15 -5
  90. package/src/version.ts +1 -1
package/dist/raw-route.js CHANGED
@@ -60,16 +60,94 @@ export function derivePathFromScope(scope, id) {
60
60
  export function resolveRoutePath(scope, id, options) {
61
61
  return options.path ?? derivePathFromScope(scope, id);
62
62
  }
63
- const routeRegistry = [];
64
- let registrationLocked = false;
63
+ /**
64
+ * Well-known `globalThis` key holding the shared registry state.
65
+ *
66
+ * A plain string rather than a `Symbol.for()` key, deliberately: duplicate
67
+ * copies are diagnosed by counting strings in the deployed bundle, and a
68
+ * symbol would be invisible to that. It also matches the existing
69
+ * `__BLOCKS_LAMBDA_EVENT_HANDLERS__` precedent.
70
+ *
71
+ * **The key is a permanent contract — do not bump it.** `_V1_` names the shape
72
+ * this file introduced; it is not a license to mint `_V2_`. The copies that
73
+ * share a process are usually *different versions* of `@aws-blocks/core` — that
74
+ * is how a tree ends up with duplicates in the first place — so a copy reading
75
+ * a different key does not get a private namespace, it re-splits the registry:
76
+ * routes registered through one key, dispatched through the other, 404. And
77
+ * silently, because each key carries its own `copies` counter, so both sides
78
+ * report `copies: 1` and the duplicate warning never fires. That is the bug
79
+ * this module exists to fix, reintroduced with its own diagnostic switched off.
80
+ *
81
+ * Evolve the shape **additively** instead: add optional fields, and tolerate
82
+ * fields a peer copy did not write (`getState()` does). If a `_V2_` key ever
83
+ * proves unavoidable, it has to adopt the V1 state — read it, and keep writing
84
+ * it — so that a mixed tree still routes through one table.
85
+ */
86
+ const REGISTRY_KEY = '__AWS_BLOCKS_RAW_ROUTE_REGISTRY_V1__';
87
+ /** Whether *this* module copy has already counted itself. Intentionally module-local. */
88
+ let thisCopyCounted = false;
89
+ /**
90
+ * Resolve the shared registry state, creating it on first access.
91
+ *
92
+ * The copy count is incremented here rather than at module load so that it
93
+ * counts the copies actually taking part in routing, not every copy a bundle
94
+ * happens to carry. Bundled code is where duplicate copies occur, and a copy
95
+ * that is imported but never routes is not what the diagnostic is about.
96
+ *
97
+ * Tree-shaking is not the reason. `sideEffects` in package.json lets a bundler
98
+ * drop this module whole when nothing it exports is used, but a module that
99
+ * *is* used keeps its top-level statements — measured with esbuild under
100
+ * `--bundle --minify`, and covered by the bundling test in `raw-route.test.ts`.
101
+ */
102
+ function getState() {
103
+ const g = globalThis;
104
+ let state = g[REGISTRY_KEY];
105
+ if (!state) {
106
+ state = { routes: [], locked: false, copies: 0 };
107
+ g[REGISTRY_KEY] = state;
108
+ }
109
+ // The state may have been left by a copy that evolved the shape (see the
110
+ // contract on REGISTRY_KEY), so fill in what this copy needs rather than
111
+ // assume it is there. A missing `routes` would throw on the first
112
+ // registration, and a missing `copies` would go NaN on the increment below —
113
+ // silencing the duplicate warning, which is the failure this module exists to
114
+ // report. `locked` needs no default: absent reads as unlocked, which is what
115
+ // a registry still accepting registrations means.
116
+ if (!Array.isArray(state.routes))
117
+ state.routes = [];
118
+ if (typeof state.copies !== 'number')
119
+ state.copies = 0;
120
+ if (!thisCopyCounted) {
121
+ thisCopyCounted = true;
122
+ state.copies += 1;
123
+ if (state.copies > 1) {
124
+ console.warn(`AWS Blocks: ${state.copies} copies of @aws-blocks/core loaded in this process. ` +
125
+ 'They now share one RawRoute registry, but duplicate copies indicate a ' +
126
+ 'dependency-tree problem — deduplicate @aws-blocks/core.');
127
+ }
128
+ }
129
+ return state;
130
+ }
131
+ /**
132
+ * Number of `@aws-blocks/core` copies taking part in the shared route registry.
133
+ *
134
+ * `1` for a healthy install. A higher number means the dependency tree carries
135
+ * duplicates — routes still work (the registry is shared), but it is worth
136
+ * reporting in diagnostics.
137
+ *
138
+ * **Internal.** Not re-exported from the package entry point.
139
+ */
140
+ export function getLoadedCoreCopies() {
141
+ return getState().copies;
142
+ }
65
143
  const VALID_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']);
66
144
  /** Prevent further route registration. Call after initialization is complete. */
67
145
  export function lockRouteRegistry() {
68
- registrationLocked = true;
146
+ getState().locked = true;
69
147
  }
70
148
  /** Re-allow route registration (useful in tests after lock). */
71
149
  export function unlockRouteRegistry() {
72
- registrationLocked = false;
150
+ getState().locked = false;
73
151
  }
74
152
  /**
75
153
  * Register a raw HTTP route for runtime dispatch.
@@ -83,7 +161,8 @@ export function unlockRouteRegistry() {
83
161
  * @throws {RawRouteErrors.DuplicateRoute} If the same method+path is registered twice.
84
162
  */
85
163
  export function registerRoute(options) {
86
- if (registrationLocked) {
164
+ const state = getState();
165
+ if (state.locked) {
87
166
  throw new Error('Routes must be registered during initialization. Cannot register routes after handler creation.');
88
167
  }
89
168
  if (!VALID_METHODS.has(options.method)) {
@@ -97,14 +176,14 @@ export function registerRoute(options) {
97
176
  if (lowerPath === BLOCKS_NAMESPACE || lowerPath === BLOCKS_RPC_PREFIX || lowerPath.startsWith(`${BLOCKS_RPC_PREFIX}/`)) {
98
177
  throw new Error(`Cannot register RawRoute at ${BLOCKS_NAMESPACE} or ${BLOCKS_RPC_PREFIX}/* — these paths are reserved for RPC dispatch`);
99
178
  }
100
- const existing = routeRegistry.find((r) => r.method === options.method && r.path === normalizedPath);
179
+ const existing = state.routes.find((r) => r.method === options.method && r.path === normalizedPath);
101
180
  if (existing) {
102
181
  const err = new Error(`${RawRouteErrors.DuplicateRoute}: ${options.method} ${normalizedPath} is already registered`);
103
182
  err.name = RawRouteErrors.DuplicateRoute;
104
183
  throw err;
105
184
  }
106
185
  const { pattern, paramNames } = compilePath(normalizedPath);
107
- routeRegistry.push({
186
+ state.routes.push({
108
187
  method: options.method,
109
188
  path: normalizedPath,
110
189
  pattern,
@@ -114,12 +193,17 @@ export function registerRoute(options) {
114
193
  }
115
194
  /** Return a read-only snapshot of all registered routes. */
116
195
  export function getRegisteredRoutes() {
117
- return routeRegistry;
196
+ return getState().routes;
118
197
  }
119
- /** Remove all registered routes and unlock registration (used by tests). */
198
+ /**
199
+ * Remove all registered routes and unlock registration (used by tests).
200
+ *
201
+ * Leaves the copy counter alone — it tracks module copies, not routes.
202
+ */
120
203
  export function clearRouteRegistry() {
121
- routeRegistry.length = 0;
122
- registrationLocked = false;
204
+ const state = getState();
205
+ state.routes.length = 0;
206
+ state.locked = false;
123
207
  }
124
208
  /**
125
209
  * Match an incoming HTTP request against registered routes.
@@ -129,7 +213,7 @@ export function clearRouteRegistry() {
129
213
  export function matchRoute(method, path) {
130
214
  // Normalize incoming path: collapse double slashes
131
215
  path = path.replace(/\/+/g, '/');
132
- for (const route of routeRegistry) {
216
+ for (const route of getState().routes) {
133
217
  if (route.method !== method)
134
218
  continue;
135
219
  const match = route.pattern.exec(path);
@@ -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":"AAuCA,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAEhF;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASjF;AAED,sFAAsF;AACtF,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,OAAO,GAAG,SAAS,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,mBAAmB,CAKvF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE/E;AAED,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA0CD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa/F;AAYD,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,uFAAuF;AACvF,eAAO,MAAM,+BAA+B,EAAE,qBAK7C,CAAC;AAEF,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,OAAO,EAAE,OAAO,CAAC;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,EAAE,EACxB,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,qBAAuD,GAC9D,eAAe,CAOjB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAOnF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,EAC5E,OAAO,EAAE,OAAO,GACf,OAAO,CAOT;AAID,sCAAsC;AACtC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,oFAAoF;IACpF,SAAS,EAAE,OAAO,CAAC;IACnB,yGAAyG;IACzG,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,gDAAgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IACrC,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC;IACxD,kDAAkD;IAClD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,OAAO,CAAC,eAAe,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAwB3G;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAW3F;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,oFAAoF;AACpF,eAAO,MAAM,8BAA8B,EAAE,mBAG5C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,mBAAoD,GAC3D;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAGrC;AAED,4GAA4G;AAC5G,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IAClD,wEAAwE;IACxE,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,wEAAwE;IACxE,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,+EAA+E;IAC/E,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,oDAAoD;IACpD,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,aAAa,EACnB,MAAM,GAAE,mBAAoD,GAC3D,MAAM,IAAI,CAwBZ;AAID,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,mGAAmG;AACnG,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAUtE;AAED,wGAAwG;AACxG,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAmC,GAAG,OAAO,CAQ3H;AAED,8GAA8G;AAC9G,MAAM,MAAM,iBAAiB,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3F;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,EACnC,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACnC,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GAChC,iBAAiB,CASnB;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,gBAAgB,iBAojB7D"}
@@ -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"}