@bleedingdev/modern-js-server-runtime-extensions 0.0.0-trusted-publisher-bootstrap

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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/cjs/contractGateAutopilot.js +162 -0
  4. package/dist/cjs/contractGateSnapshotStore.js +253 -0
  5. package/dist/cjs/env.js +58 -0
  6. package/dist/cjs/index.js +162 -0
  7. package/dist/cjs/mfCache.js +106 -0
  8. package/dist/cjs/moduleFederationCss.js +285 -0
  9. package/dist/cjs/runtimeFallbackSignal.js +311 -0
  10. package/dist/cjs/telemetry.js +373 -0
  11. package/dist/cjs/telemetryCore.js +819 -0
  12. package/dist/esm/contractGateAutopilot.mjs +124 -0
  13. package/dist/esm/contractGateSnapshotStore.mjs +190 -0
  14. package/dist/esm/env.mjs +17 -0
  15. package/dist/esm/index.mjs +6 -0
  16. package/dist/esm/mfCache.mjs +55 -0
  17. package/dist/esm/moduleFederationCss.mjs +225 -0
  18. package/dist/esm/runtimeFallbackSignal.mjs +222 -0
  19. package/dist/esm/telemetry.mjs +275 -0
  20. package/dist/esm/telemetryCore.mjs +759 -0
  21. package/dist/esm-node/contractGateAutopilot.mjs +125 -0
  22. package/dist/esm-node/contractGateSnapshotStore.mjs +192 -0
  23. package/dist/esm-node/env.mjs +18 -0
  24. package/dist/esm-node/index.mjs +7 -0
  25. package/dist/esm-node/mfCache.mjs +56 -0
  26. package/dist/esm-node/moduleFederationCss.mjs +226 -0
  27. package/dist/esm-node/runtimeFallbackSignal.mjs +223 -0
  28. package/dist/esm-node/telemetry.mjs +276 -0
  29. package/dist/esm-node/telemetryCore.mjs +760 -0
  30. package/dist/types/contractGateAutopilot.d.ts +35 -0
  31. package/dist/types/contractGateSnapshotStore.d.ts +57 -0
  32. package/dist/types/env.d.ts +40 -0
  33. package/dist/types/index.d.ts +6 -0
  34. package/dist/types/mfCache.d.ts +27 -0
  35. package/dist/types/moduleFederationCss.d.ts +87 -0
  36. package/dist/types/runtimeFallbackSignal.d.ts +94 -0
  37. package/dist/types/telemetry.d.ts +12 -0
  38. package/dist/types/telemetryCore.d.ts +257 -0
  39. package/package.json +69 -0
  40. package/rslib.config.mts +4 -0
  41. package/rstest.config.mts +7 -0
  42. package/src/contractGateAutopilot.ts +247 -0
  43. package/src/contractGateSnapshotStore.ts +420 -0
  44. package/src/env.ts +63 -0
  45. package/src/index.ts +84 -0
  46. package/src/mfCache.ts +119 -0
  47. package/src/moduleFederationCss.ts +473 -0
  48. package/src/runtimeFallbackSignal.ts +584 -0
  49. package/src/telemetry.ts +554 -0
  50. package/src/telemetryCore.ts +1332 -0
  51. package/tests/contractGateAutopilot.test.ts +203 -0
  52. package/tests/contractGateSnapshotStore.test.ts +223 -0
  53. package/tests/env.test.ts +73 -0
  54. package/tests/helpers.ts +19 -0
  55. package/tests/mfCache.test.ts +150 -0
  56. package/tests/moduleFederationCss.test.ts +392 -0
  57. package/tests/registration.test.ts +112 -0
  58. package/tests/telemetry.test.ts +360 -0
  59. package/tests/telemetryAutopilot.test.ts +993 -0
  60. package/tests/telemetryCanaryOrchestrator.test.ts +140 -0
  61. package/tests/telemetryLifecycle.test.ts +168 -0
  62. package/tests/telemetryTraceparent.test.ts +167 -0
  63. package/tests/tsconfig.json +11 -0
  64. package/tsconfig.json +10 -0
@@ -0,0 +1,473 @@
1
+ import { fileReader } from '@modern-js/runtime-utils/fileReader';
2
+ import type {
3
+ Middleware,
4
+ ServerEnv,
5
+ ServerPlugin,
6
+ } from '@modern-js/server-core';
7
+ import type { Monitors } from '@modern-js/types';
8
+ import { fs, isProd } from '@modern-js/utils';
9
+ import path from 'path';
10
+
11
+ const MODULE_FEDERATION_MANIFEST_FILE = 'mf-manifest.json';
12
+ const DEFAULT_REMOTE_MANIFEST_TIMEOUT = 1500;
13
+
14
+ type ModuleFederationAssets = {
15
+ css?: {
16
+ sync?: string[];
17
+ async?: string[];
18
+ };
19
+ };
20
+
21
+ export type ModuleFederationManifest = {
22
+ metaData?: {
23
+ publicPath?: string;
24
+ };
25
+ shared?: Array<{
26
+ assets?: ModuleFederationAssets;
27
+ }>;
28
+ remotes?: Array<{
29
+ entry?: string;
30
+ assets?: ModuleFederationAssets;
31
+ }>;
32
+ exposes?: Array<{
33
+ assets?: ModuleFederationAssets;
34
+ }>;
35
+ };
36
+
37
+ type FetchLike = (
38
+ input: string,
39
+ init?: {
40
+ signal?: AbortSignal;
41
+ },
42
+ ) => Promise<Response>;
43
+
44
+ export type CollectDirectRemoteModuleFederationCssOptions = {
45
+ fetcher?: FetchLike;
46
+ monitors?: Monitors;
47
+ timeout?: number;
48
+ };
49
+
50
+ const warn = (
51
+ monitors: Monitors | undefined,
52
+ message: string,
53
+ ...args: unknown[]
54
+ ) => {
55
+ if (monitors) {
56
+ monitors.warn(message, ...args);
57
+ return;
58
+ }
59
+
60
+ console.warn(message, ...args);
61
+ };
62
+
63
+ const ensureTrailingSlash = (value: string) =>
64
+ value.endsWith('/') ? value : `${value}/`;
65
+
66
+ const tryResolveUrl = (value: string, base: string) => {
67
+ try {
68
+ return new URL(value, base).toString();
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ };
73
+
74
+ const normalizeRemoteEntry = (entry: string) => {
75
+ const value = entry.trim();
76
+ if (!value) {
77
+ return undefined;
78
+ }
79
+
80
+ const atIndex = value.lastIndexOf('@');
81
+ return atIndex >= 0 ? value.slice(atIndex + 1) : value;
82
+ };
83
+
84
+ const getCssAssets = (assets?: ModuleFederationAssets) => [
85
+ ...(assets?.css?.sync || []),
86
+ ...(assets?.css?.async || []),
87
+ ];
88
+
89
+ const getManifestFallbackBase = (manifestUrl: string) => {
90
+ try {
91
+ return new URL('.', manifestUrl).toString();
92
+ } catch {
93
+ return ensureTrailingSlash(manifestUrl);
94
+ }
95
+ };
96
+
97
+ const getManifestPublicPathBase = (
98
+ publicPath: string | undefined,
99
+ manifestUrl: string,
100
+ ) => {
101
+ if (!publicPath || publicPath === 'auto') {
102
+ return getManifestFallbackBase(manifestUrl);
103
+ }
104
+
105
+ const base = tryResolveUrl(ensureTrailingSlash(publicPath), manifestUrl);
106
+ return base || getManifestFallbackBase(manifestUrl);
107
+ };
108
+
109
+ const appendResolvedCssAssets = (
110
+ result: string[],
111
+ seen: Set<string>,
112
+ assets: string[],
113
+ base: string,
114
+ ) => {
115
+ for (const asset of assets) {
116
+ if (!asset) {
117
+ continue;
118
+ }
119
+
120
+ const resolved = tryResolveUrl(asset, base);
121
+
122
+ if (resolved && !seen.has(resolved)) {
123
+ seen.add(resolved);
124
+ result.push(resolved);
125
+ }
126
+ }
127
+ };
128
+
129
+ export const collectModuleFederationManifestCss = (
130
+ manifest: ModuleFederationManifest,
131
+ manifestUrl: string,
132
+ ) => {
133
+ const base = getManifestPublicPathBase(
134
+ manifest.metaData?.publicPath,
135
+ manifestUrl,
136
+ );
137
+ const result: string[] = [];
138
+ const seen = new Set<string>();
139
+
140
+ for (const item of manifest.shared || []) {
141
+ appendResolvedCssAssets(result, seen, getCssAssets(item.assets), base);
142
+ }
143
+
144
+ for (const item of manifest.exposes || []) {
145
+ appendResolvedCssAssets(result, seen, getCssAssets(item.assets), base);
146
+ }
147
+
148
+ return result;
149
+ };
150
+
151
+ const fetchJsonWithTimeout = async (
152
+ url: string,
153
+ fetcher: FetchLike,
154
+ timeout: number,
155
+ ) => {
156
+ const controller = new AbortController();
157
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
158
+
159
+ try {
160
+ const response = await Promise.race([
161
+ fetcher(url, { signal: controller.signal }),
162
+ new Promise<Response>((_, reject) => {
163
+ timeoutId = setTimeout(() => {
164
+ controller.abort();
165
+ reject(new Error(`Request timed out after ${timeout}ms`));
166
+ }, timeout);
167
+ }),
168
+ ]);
169
+
170
+ if (!response.ok) {
171
+ throw new Error(`Unexpected status ${response.status}`);
172
+ }
173
+
174
+ return (await response.json()) as ModuleFederationManifest;
175
+ } finally {
176
+ if (timeoutId) {
177
+ clearTimeout(timeoutId);
178
+ }
179
+ }
180
+ };
181
+
182
+ const getHostManifest = async (
183
+ pwd: string,
184
+ monitors?: Monitors,
185
+ ): Promise<ModuleFederationManifest | undefined> => {
186
+ const manifestPath = path.join(pwd, MODULE_FEDERATION_MANIFEST_FILE);
187
+
188
+ if (!(await fs.pathExists(manifestPath))) {
189
+ return undefined;
190
+ }
191
+
192
+ const manifestBuffer = await fileReader.readFileFromSystem(
193
+ manifestPath,
194
+ 'buffer',
195
+ );
196
+
197
+ if (manifestBuffer === null) {
198
+ return undefined;
199
+ }
200
+
201
+ try {
202
+ return JSON.parse(
203
+ manifestBuffer.toString('utf-8'),
204
+ ) as ModuleFederationManifest;
205
+ } catch (error) {
206
+ warn(
207
+ monitors,
208
+ 'Parse module federation manifest failed, error = %s',
209
+ error instanceof Error ? error.message : error,
210
+ );
211
+ return undefined;
212
+ }
213
+ };
214
+
215
+ export type RemoteModuleFederationCssCollection = {
216
+ assets: string[];
217
+ /**
218
+ * True when at least one remote manifest fetch failed, i.e. `assets` may be
219
+ * incomplete and should not be cached long-term.
220
+ */
221
+ errored: boolean;
222
+ };
223
+
224
+ export const collectDirectRemoteModuleFederationCssWithMeta = async (
225
+ pwd: string,
226
+ options: CollectDirectRemoteModuleFederationCssOptions = {},
227
+ ): Promise<RemoteModuleFederationCssCollection> => {
228
+ const hostManifest = await getHostManifest(pwd, options.monitors);
229
+
230
+ if (!hostManifest) {
231
+ return { assets: [], errored: false };
232
+ }
233
+
234
+ const fetcher = options.fetcher || globalThis.fetch?.bind(globalThis);
235
+ if (!fetcher) {
236
+ warn(
237
+ options.monitors,
238
+ 'Skip module federation remote CSS collection because fetch is unavailable.',
239
+ );
240
+ return { assets: [], errored: false };
241
+ }
242
+
243
+ const timeout = options.timeout ?? DEFAULT_REMOTE_MANIFEST_TIMEOUT;
244
+
245
+ // Fetch all remote manifests in parallel so SSR latency is bounded by the
246
+ // slowest remote rather than the sum of all remotes.
247
+ const remoteResults = await Promise.all(
248
+ (hostManifest.remotes || []).map(
249
+ async (remote): Promise<{ assets: string[]; errored: boolean }> => {
250
+ if (!remote.entry) {
251
+ return { assets: [], errored: false };
252
+ }
253
+
254
+ const remoteEntry = normalizeRemoteEntry(remote.entry);
255
+ if (!remoteEntry) {
256
+ return { assets: [], errored: false };
257
+ }
258
+
259
+ let remoteManifestUrl: string;
260
+ try {
261
+ remoteManifestUrl = new URL(remoteEntry).toString();
262
+ } catch {
263
+ warn(
264
+ options.monitors,
265
+ 'Skip module federation remote CSS collection for non-absolute manifest URL %s',
266
+ remoteEntry,
267
+ );
268
+ return { assets: [], errored: false };
269
+ }
270
+
271
+ try {
272
+ const remoteManifest = await fetchJsonWithTimeout(
273
+ remoteManifestUrl,
274
+ fetcher,
275
+ timeout,
276
+ );
277
+ return {
278
+ assets: collectModuleFederationManifestCss(
279
+ remoteManifest,
280
+ remoteManifestUrl,
281
+ ),
282
+ errored: false,
283
+ };
284
+ } catch (error) {
285
+ warn(
286
+ options.monitors,
287
+ 'Load module federation remote manifest %s failed, error = %s',
288
+ remoteManifestUrl,
289
+ error instanceof Error ? error.message : error,
290
+ );
291
+ return { assets: [], errored: true };
292
+ }
293
+ },
294
+ ),
295
+ );
296
+
297
+ const cssAssets: string[] = [];
298
+ const seen = new Set<string>();
299
+ let errored = false;
300
+ for (const result of remoteResults) {
301
+ errored = errored || result.errored;
302
+ for (const asset of result.assets) {
303
+ if (!seen.has(asset)) {
304
+ seen.add(asset);
305
+ cssAssets.push(asset);
306
+ }
307
+ }
308
+ }
309
+
310
+ return { assets: cssAssets, errored };
311
+ };
312
+
313
+ export const collectDirectRemoteModuleFederationCss = async (
314
+ pwd: string,
315
+ options: CollectDirectRemoteModuleFederationCssOptions = {},
316
+ ) => {
317
+ const { assets } = await collectDirectRemoteModuleFederationCssWithMeta(
318
+ pwd,
319
+ options,
320
+ );
321
+ return assets;
322
+ };
323
+
324
+ export type ModuleFederationCssCollectorOptions =
325
+ CollectDirectRemoteModuleFederationCssOptions & {
326
+ /**
327
+ * How long a successful remote CSS collection may be served from cache.
328
+ * `0` disables caching (beyond in-flight request coalescing).
329
+ */
330
+ ttlMs?: number;
331
+ /** Clock override for tests. */
332
+ now?: () => number;
333
+ };
334
+
335
+ /**
336
+ * TTL cache around remote MF CSS collection. Remote manifests mutate
337
+ * independently of the host (that is the point of module federation), so the
338
+ * collection must expire: a boot-time-forever cache serves stale or broken
339
+ * CSS links after any remote redeploy.
340
+ *
341
+ * Error handling: a collection where any remote fetch failed is never cached.
342
+ * The last known-good asset list is served instead (stale-on-error) and the
343
+ * next request retries the collection.
344
+ */
345
+ export const createModuleFederationCssCollector = (
346
+ pwd: string,
347
+ options: ModuleFederationCssCollectorOptions = {},
348
+ ) => {
349
+ const { ttlMs = 0, now = Date.now, ...collectOptions } = options;
350
+ const normalizedTtlMs = Math.max(0, ttlMs);
351
+
352
+ let cached: { assets: string[]; expiresAt: number } | undefined;
353
+ let lastGoodAssets: string[] | undefined;
354
+ let inflight: Promise<string[]> | undefined;
355
+
356
+ const refresh = (monitors?: Monitors) => {
357
+ const promise = collectDirectRemoteModuleFederationCssWithMeta(pwd, {
358
+ ...collectOptions,
359
+ monitors: monitors ?? collectOptions.monitors,
360
+ })
361
+ .then(result => {
362
+ if (!result.errored) {
363
+ lastGoodAssets = result.assets;
364
+ cached = {
365
+ assets: result.assets,
366
+ expiresAt: now() + normalizedTtlMs,
367
+ };
368
+ return result.assets;
369
+ }
370
+
371
+ // Error path: invalidate so the next request retries, and serve the
372
+ // last known-good list when one exists.
373
+ cached = undefined;
374
+ return lastGoodAssets ?? result.assets;
375
+ })
376
+ .finally(() => {
377
+ if (inflight === promise) {
378
+ inflight = undefined;
379
+ }
380
+ });
381
+
382
+ inflight = promise;
383
+ return promise;
384
+ };
385
+
386
+ return {
387
+ collect(monitors?: Monitors): Promise<string[]> {
388
+ if (cached && now() < cached.expiresAt) {
389
+ return Promise.resolve(cached.assets);
390
+ }
391
+ if (inflight) {
392
+ return inflight;
393
+ }
394
+ return refresh(monitors);
395
+ },
396
+ };
397
+ };
398
+
399
+ const DEFAULT_REMOTE_CSS_CACHE_TTL_MS = 30_000;
400
+
401
+ export type ModuleFederationCssPluginOptions = {
402
+ /**
403
+ * TTL for the cached remote CSS collection in production.
404
+ *
405
+ * @default 30000 in production, 0 (no caching) otherwise
406
+ */
407
+ remoteCssCacheTtlMs?: number;
408
+ };
409
+
410
+ /**
411
+ * Enriches the request-scoped server manifest with CSS assets collected from
412
+ * direct module federation remotes, so SSR/CSR-RSC rendering can inline
413
+ * `<link>` tags for remote CSS.
414
+ *
415
+ * In production the collection is cached with a TTL (default 30s) instead of
416
+ * being pinned at boot: remote manifests change on every remote redeploy, and
417
+ * fetch failures must not pin an empty/partial list for the process lifetime.
418
+ *
419
+ * This plugin must be registered after `injectResourcePlugin()` (which sets
420
+ * `serverManifest` on the request context). @modern-js/prod-server wires it
421
+ * into its plugin assembly for both production and dev servers.
422
+ */
423
+ export const injectModuleFederationCssPlugin = (
424
+ options: ModuleFederationCssPluginOptions = {},
425
+ ): ServerPlugin => ({
426
+ name: '@modern-js/inject-module-federation-css',
427
+
428
+ setup(api) {
429
+ api.onPrepare(() => {
430
+ const { middlewares, distDirectory: pwd } = api.getServerContext();
431
+
432
+ if (!pwd) {
433
+ return;
434
+ }
435
+
436
+ const ttlMs = Math.max(
437
+ 0,
438
+ options.remoteCssCacheTtlMs ??
439
+ (isProd() ? DEFAULT_REMOTE_CSS_CACHE_TTL_MS : 0),
440
+ );
441
+ const collector = createModuleFederationCssCollector(pwd, { ttlMs });
442
+
443
+ if (isProd()) {
444
+ // Warm up the remote manifest fetch at prepare time, mirroring the
445
+ // server manifest warmup of injectResourcePlugin, so the first
446
+ // request does not pay the collection latency. Failures are retried
447
+ // on the next request instead of failing startup.
448
+ collector.collect().catch(() => {});
449
+ }
450
+
451
+ const handler: Middleware<ServerEnv> = async (c, next) => {
452
+ const serverManifest = c.get('serverManifest');
453
+
454
+ if (serverManifest && !serverManifest.moduleFederationCssAssets) {
455
+ const monitors = c.get('monitors');
456
+ const moduleFederationCssAssets = await collector.collect(monitors);
457
+
458
+ c.set('serverManifest', {
459
+ ...serverManifest,
460
+ moduleFederationCssAssets,
461
+ });
462
+ }
463
+
464
+ await next();
465
+ };
466
+
467
+ middlewares.push({
468
+ name: 'inject-module-federation-css',
469
+ handler,
470
+ });
471
+ });
472
+ },
473
+ });