@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,420 @@
1
+ import { createRequire } from 'node:module';
2
+ import { fs } from '@modern-js/utils';
3
+ import { promises as nodeFs } from 'fs';
4
+ import path from 'path';
5
+ import { parseServerRuntimeExtensionsEnv } from './env';
6
+
7
+ export const CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION = 1;
8
+ export const DEFAULT_CONTRACT_GATE_SNAPSHOT_PATH =
9
+ '.modern/contract-gates.json';
10
+
11
+ export type GateSnapshotGateValue =
12
+ | boolean
13
+ | {
14
+ passed?: boolean;
15
+ reason?: string;
16
+ updatedAt?: number;
17
+ expiresAt?: number;
18
+ [key: string]: unknown;
19
+ };
20
+
21
+ export type GateSnapshot = {
22
+ schemaVersion?: number;
23
+ updatedAt?: number;
24
+ gates?: Record<string, GateSnapshotGateValue>;
25
+ };
26
+
27
+ type LoggerLike = {
28
+ info?: (message: string) => void;
29
+ warn?: (message: string) => void;
30
+ };
31
+
32
+ export type ContractGateSnapshotStore = {
33
+ name: string;
34
+ readSnapshot: () => Promise<GateSnapshot | undefined>;
35
+ writeSnapshot: (snapshot: GateSnapshot) => Promise<void>;
36
+ };
37
+
38
+ export type ContractGateSnapshotStoreFactoryContext = {
39
+ appDirectory: string;
40
+ gateSnapshotPath: string;
41
+ options?: Record<string, unknown>;
42
+ logger?: LoggerLike;
43
+ };
44
+
45
+ export type ContractGateSnapshotStoreFactory = (
46
+ context: ContractGateSnapshotStoreFactoryContext,
47
+ ) => Promise<ContractGateSnapshotStore> | ContractGateSnapshotStore;
48
+
49
+ export type ContractGateSnapshotStoreModule = {
50
+ createContractGateSnapshotStore?: ContractGateSnapshotStoreFactory;
51
+ default?:
52
+ | ContractGateSnapshotStoreFactory
53
+ | {
54
+ createContractGateSnapshotStore?: ContractGateSnapshotStoreFactory;
55
+ };
56
+ };
57
+
58
+ export type ContractGateSnapshotStoreUserConfig = {
59
+ module: string;
60
+ options?: Record<string, unknown>;
61
+ };
62
+
63
+ export type ContractGateSnapshotHttpStoreOptions = {
64
+ endpoint: string;
65
+ readMethod?: string;
66
+ writeMethod?: string;
67
+ headers?: Record<string, string>;
68
+ timeoutMs?: number;
69
+ };
70
+
71
+ const DEFAULT_HTTP_STORE_TIMEOUT_MS = 5_000;
72
+ const BUILTIN_HTTP_STATE_STORE_MODULES = new Set([
73
+ 'http',
74
+ // historical aliases kept for config compatibility with the era when these
75
+ // modules lived inside @modern-js/server-core.
76
+ '@modern-js/server-core/http',
77
+ '@modern-js/server-core/contract-gate-http-store',
78
+ '@modern-js/server-runtime-extensions/http',
79
+ '@modern-js/server-runtime-extensions/contract-gate-http-store',
80
+ ]);
81
+
82
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
83
+ typeof value === 'object' && value !== null && !Array.isArray(value);
84
+
85
+ const normalizeSnapshot = (snapshot: unknown): GateSnapshot | undefined => {
86
+ if (!isRecord(snapshot)) {
87
+ return undefined;
88
+ }
89
+
90
+ const schemaVersion =
91
+ typeof snapshot.schemaVersion === 'number'
92
+ ? snapshot.schemaVersion
93
+ : CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION;
94
+ const updatedAt =
95
+ typeof snapshot.updatedAt === 'number' ? snapshot.updatedAt : Date.now();
96
+ const gates = isRecord(snapshot.gates)
97
+ ? (snapshot.gates as Record<string, GateSnapshotGateValue>)
98
+ : {};
99
+
100
+ return {
101
+ schemaVersion,
102
+ updatedAt,
103
+ gates,
104
+ };
105
+ };
106
+
107
+ const normalizeHttpStoreOptions = (
108
+ options: Record<string, unknown> | undefined,
109
+ ): ContractGateSnapshotHttpStoreOptions => {
110
+ const endpoint =
111
+ typeof options?.endpoint === 'string' ? options.endpoint.trim() : '';
112
+ if (!endpoint) {
113
+ throw new Error(
114
+ '[telemetry.canary.autopilot] HTTP stateStore requires options.endpoint',
115
+ );
116
+ }
117
+
118
+ const readMethod =
119
+ typeof options?.readMethod === 'string' && options.readMethod.trim()
120
+ ? options.readMethod.trim().toUpperCase()
121
+ : 'GET';
122
+ const writeMethod =
123
+ typeof options?.writeMethod === 'string' && options.writeMethod.trim()
124
+ ? options.writeMethod.trim().toUpperCase()
125
+ : 'PUT';
126
+
127
+ const timeoutMsRaw = Number(options?.timeoutMs);
128
+ const timeoutMs =
129
+ Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0
130
+ ? Math.floor(timeoutMsRaw)
131
+ : DEFAULT_HTTP_STORE_TIMEOUT_MS;
132
+
133
+ const headersRaw = options?.headers;
134
+ const headers: Record<string, string> = {};
135
+ if (
136
+ headersRaw &&
137
+ typeof headersRaw === 'object' &&
138
+ !Array.isArray(headersRaw)
139
+ ) {
140
+ Object.entries(headersRaw).forEach(([key, value]) => {
141
+ if (typeof key === 'string' && key.trim().length > 0 && value != null) {
142
+ headers[key] = String(value);
143
+ }
144
+ });
145
+ }
146
+
147
+ return {
148
+ endpoint,
149
+ readMethod,
150
+ writeMethod,
151
+ headers,
152
+ timeoutMs,
153
+ };
154
+ };
155
+
156
+ const withTimeoutAbort = (timeoutMs: number) => {
157
+ const controller = new AbortController();
158
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
159
+ return {
160
+ signal: controller.signal,
161
+ clear: () => clearTimeout(timer),
162
+ };
163
+ };
164
+
165
+ export const createHttpContractGateSnapshotStore = (
166
+ options: ContractGateSnapshotHttpStoreOptions,
167
+ ): ContractGateSnapshotStore => {
168
+ const normalized = normalizeHttpStoreOptions(
169
+ options as unknown as Record<string, unknown>,
170
+ );
171
+ const endpoint = normalized.endpoint;
172
+
173
+ return {
174
+ name: `http:${endpoint}`,
175
+ async readSnapshot() {
176
+ const { signal, clear } = withTimeoutAbort(normalized.timeoutMs || 5_000);
177
+ try {
178
+ const response = await fetch(endpoint, {
179
+ method: normalized.readMethod || 'GET',
180
+ headers: {
181
+ accept: 'application/json',
182
+ ...(normalized.headers || {}),
183
+ },
184
+ signal,
185
+ });
186
+
187
+ if (response.status === 404) {
188
+ return undefined;
189
+ }
190
+
191
+ if (!response.ok) {
192
+ throw new Error(
193
+ `HTTP stateStore read failed with status ${String(response.status)}`,
194
+ );
195
+ }
196
+
197
+ const payload = await response.json();
198
+ return normalizeSnapshot(payload);
199
+ } finally {
200
+ clear();
201
+ }
202
+ },
203
+ async writeSnapshot(snapshot) {
204
+ const body = JSON.stringify(
205
+ normalizeSnapshot(snapshot) || {
206
+ schemaVersion: CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION,
207
+ updatedAt: Date.now(),
208
+ gates: {},
209
+ },
210
+ );
211
+ const { signal, clear } = withTimeoutAbort(normalized.timeoutMs || 5_000);
212
+ try {
213
+ const response = await fetch(endpoint, {
214
+ method: normalized.writeMethod || 'PUT',
215
+ headers: {
216
+ 'content-type': 'application/json',
217
+ ...(normalized.headers || {}),
218
+ },
219
+ body,
220
+ signal,
221
+ });
222
+ if (!response.ok) {
223
+ throw new Error(
224
+ `HTTP stateStore write failed with status ${String(response.status)}`,
225
+ );
226
+ }
227
+ } finally {
228
+ clear();
229
+ }
230
+ },
231
+ };
232
+ };
233
+
234
+ const tryResolveBuiltinSnapshotStore = (input: {
235
+ stateStore: ContractGateSnapshotStoreUserConfig;
236
+ }): ContractGateSnapshotStore | undefined => {
237
+ const moduleName = input.stateStore.module.trim();
238
+ if (!BUILTIN_HTTP_STATE_STORE_MODULES.has(moduleName)) {
239
+ return undefined;
240
+ }
241
+
242
+ return createHttpContractGateSnapshotStore(
243
+ (input.stateStore.options || {}) as ContractGateSnapshotHttpStoreOptions,
244
+ );
245
+ };
246
+
247
+ const pickStoreFactory = (
248
+ mod: ContractGateSnapshotStoreModule,
249
+ ): ContractGateSnapshotStoreFactory | undefined => {
250
+ if (typeof mod === 'function') {
251
+ return mod as unknown as ContractGateSnapshotStoreFactory;
252
+ }
253
+
254
+ if (typeof mod.createContractGateSnapshotStore === 'function') {
255
+ return mod.createContractGateSnapshotStore;
256
+ }
257
+
258
+ if (typeof mod.default === 'function') {
259
+ return mod.default;
260
+ }
261
+
262
+ if (
263
+ mod.default &&
264
+ typeof mod.default === 'object' &&
265
+ typeof mod.default.createContractGateSnapshotStore === 'function'
266
+ ) {
267
+ return mod.default.createContractGateSnapshotStore;
268
+ }
269
+
270
+ return undefined;
271
+ };
272
+
273
+ const ensureStoreShape = (
274
+ store: ContractGateSnapshotStore,
275
+ modulePath: string,
276
+ ) => {
277
+ if (
278
+ !store ||
279
+ typeof store !== 'object' ||
280
+ typeof store.readSnapshot !== 'function' ||
281
+ typeof store.writeSnapshot !== 'function'
282
+ ) {
283
+ throw new Error(
284
+ `Invalid contract gate snapshot store from "${modulePath}". Expected { readSnapshot(), writeSnapshot() }.`,
285
+ );
286
+ }
287
+ };
288
+
289
+ /**
290
+ * Resolves a user-configured stateStore module specifier from the app, not
291
+ * from this framework package: relative paths resolve against appDirectory,
292
+ * and bare package specifiers resolve through the app's own module graph
293
+ * (`createRequire` anchored at the app package.json). Resolving from the
294
+ * framework package breaks app-installed stores under pnpm's strict
295
+ * node_modules layout. Bare specifiers fall back to framework-local
296
+ * resolution for stores installed alongside the framework.
297
+ */
298
+ const resolveStoreModulePath = (appDirectory: string, modulePath: string) => {
299
+ const normalized = modulePath.trim();
300
+ if (!normalized) {
301
+ throw new Error(
302
+ 'Contract gate snapshot stateStore.module must be non-empty',
303
+ );
304
+ }
305
+
306
+ if (path.isAbsolute(normalized)) {
307
+ return normalized;
308
+ }
309
+
310
+ if (normalized.startsWith('.')) {
311
+ return path.resolve(appDirectory, normalized);
312
+ }
313
+
314
+ const appRequire = createRequire(path.join(appDirectory, 'package.json'));
315
+ try {
316
+ return appRequire.resolve(normalized);
317
+ } catch (_error) {
318
+ // Fall back to this package's own resolution so stores installed next to
319
+ // the framework keep working.
320
+ return normalized;
321
+ }
322
+ };
323
+
324
+ export const resolveContractGateSnapshotPath = (
325
+ appDirectory: string,
326
+ configuredPath: string | undefined,
327
+ ) => {
328
+ const rawPath =
329
+ configuredPath ||
330
+ parseServerRuntimeExtensionsEnv().contractGatesFile ||
331
+ DEFAULT_CONTRACT_GATE_SNAPSHOT_PATH;
332
+ if (path.isAbsolute(rawPath)) {
333
+ return rawPath;
334
+ }
335
+ return path.resolve(appDirectory, rawPath);
336
+ };
337
+
338
+ export const createFileContractGateSnapshotStore = (
339
+ gateSnapshotPath: string,
340
+ ): ContractGateSnapshotStore => {
341
+ const resolvedPath = path.resolve(gateSnapshotPath);
342
+ return {
343
+ name: `file:${resolvedPath}`,
344
+ async readSnapshot() {
345
+ if (!(await fs.pathExists(resolvedPath))) {
346
+ return undefined;
347
+ }
348
+
349
+ try {
350
+ const raw = await nodeFs.readFile(resolvedPath, 'utf8');
351
+ return normalizeSnapshot(JSON.parse(raw));
352
+ } catch (_error) {
353
+ return undefined;
354
+ }
355
+ },
356
+ async writeSnapshot(snapshot) {
357
+ const normalized = normalizeSnapshot(snapshot) || {
358
+ schemaVersion: CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION,
359
+ updatedAt: Date.now(),
360
+ gates: {},
361
+ };
362
+ await nodeFs.mkdir(path.dirname(resolvedPath), { recursive: true });
363
+ await nodeFs.writeFile(
364
+ resolvedPath,
365
+ `${JSON.stringify(normalized, null, 2)}\n`,
366
+ );
367
+ },
368
+ };
369
+ };
370
+
371
+ export const resolveContractGateSnapshotStore = async (input: {
372
+ appDirectory: string;
373
+ gateSnapshotPath: string;
374
+ stateStore?: ContractGateSnapshotStoreUserConfig;
375
+ logger?: LoggerLike;
376
+ }): Promise<ContractGateSnapshotStore> => {
377
+ const { appDirectory, gateSnapshotPath, stateStore, logger } = input;
378
+
379
+ if (!stateStore?.module) {
380
+ return createFileContractGateSnapshotStore(gateSnapshotPath);
381
+ }
382
+
383
+ const builtinStore = tryResolveBuiltinSnapshotStore({ stateStore });
384
+ if (builtinStore) {
385
+ logger?.info?.(
386
+ `[telemetry.canary.autopilot] using built-in contract gate snapshot store "${builtinStore.name}"`,
387
+ );
388
+ return builtinStore;
389
+ }
390
+
391
+ const modulePath = resolveStoreModulePath(appDirectory, stateStore.module);
392
+ let mod: ContractGateSnapshotStoreModule;
393
+ try {
394
+ // eslint-disable-next-line import/no-dynamic-require,global-require
395
+ mod = require(modulePath) as ContractGateSnapshotStoreModule;
396
+ } catch (error) {
397
+ throw new Error(
398
+ `[telemetry.canary.autopilot] Failed to load stateStore.module "${stateStore.module}" (${modulePath}): ${error instanceof Error ? error.message : String(error)}`,
399
+ );
400
+ }
401
+
402
+ const factory = pickStoreFactory(mod);
403
+ if (!factory) {
404
+ throw new Error(
405
+ `[telemetry.canary.autopilot] stateStore.module "${stateStore.module}" does not export createContractGateSnapshotStore()`,
406
+ );
407
+ }
408
+
409
+ const store = await factory({
410
+ appDirectory,
411
+ gateSnapshotPath,
412
+ options: stateStore.options,
413
+ logger,
414
+ });
415
+ ensureStoreShape(store, modulePath);
416
+ logger?.info?.(
417
+ `[telemetry.canary.autopilot] using contract gate snapshot store "${store.name || modulePath}"`,
418
+ );
419
+ return store;
420
+ };
package/src/env.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Single typed pass over the statically-known environment variables consumed
3
+ * by the ultramodern.js server runtime extensions (telemetry pipeline and
4
+ * contract-gate canary autopilot).
5
+ *
6
+ * Modules in this package read this environment state through
7
+ * `parseServerRuntimeExtensionsEnv()` so the surface is documented and
8
+ * testable in one place, with one documented exception: the telemetry
9
+ * runtime-fallback-signal auth config can name an arbitrary token variable
10
+ * via `auth.expectedValueEnv`, which is necessarily read dynamically from
11
+ * `process.env` when that config is normalized. See the package README for
12
+ * per-variable docs.
13
+ */
14
+
15
+ export const DEFAULT_ENVIRONMENT_NAME = 'development';
16
+
17
+ export interface ServerRuntimeExtensionsEnv {
18
+ /**
19
+ * `MODERN_ENV` — deployment environment name loaded by the Modern.js env
20
+ * bootstrap (`.env.{MODERN_ENV}`). Undefined when unset or blank.
21
+ */
22
+ modernEnv?: string;
23
+ /** `NODE_ENV` — standard Node.js environment name. Undefined when unset or blank. */
24
+ nodeEnv?: string;
25
+ /**
26
+ * Effective telemetry environment label:
27
+ * `MODERN_ENV` || `NODE_ENV` || `"development"`.
28
+ */
29
+ environmentName: string;
30
+ /**
31
+ * `MODERN_CONTRACT_GATES_FILE` — path of the contract-gate snapshot file
32
+ * (absolute, or relative to the app directory). Undefined when unset.
33
+ */
34
+ contractGatesFile?: string;
35
+ }
36
+
37
+ const readString = (value: string | undefined): string | undefined => {
38
+ if (typeof value !== 'string') {
39
+ return undefined;
40
+ }
41
+ const trimmed = value.trim();
42
+ return trimmed.length > 0 ? trimmed : undefined;
43
+ };
44
+
45
+ /**
46
+ * Parse the statically-known environment variables consumed by this package
47
+ * in one typed, documented pass. Defaults are applied here so consumers never
48
+ * touch `process.env` directly (except the documented `expectedValueEnv`
49
+ * indirection above).
50
+ */
51
+ export const parseServerRuntimeExtensionsEnv = (
52
+ env: Record<string, string | undefined> = process.env,
53
+ ): ServerRuntimeExtensionsEnv => {
54
+ const modernEnv = readString(env.MODERN_ENV);
55
+ const nodeEnv = readString(env.NODE_ENV);
56
+
57
+ return {
58
+ modernEnv,
59
+ nodeEnv,
60
+ environmentName: modernEnv || nodeEnv || DEFAULT_ENVIRONMENT_NAME,
61
+ contractGatesFile: readString(env.MODERN_CONTRACT_GATES_FILE),
62
+ };
63
+ };
package/src/index.ts ADDED
@@ -0,0 +1,84 @@
1
+ export {
2
+ ContractGateAutopilot,
3
+ type ContractGateAutopilotOptions,
4
+ } from './contractGateAutopilot';
5
+ export {
6
+ CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION,
7
+ type ContractGateSnapshotHttpStoreOptions,
8
+ type ContractGateSnapshotStore,
9
+ type ContractGateSnapshotStoreFactory,
10
+ type ContractGateSnapshotStoreFactoryContext,
11
+ type ContractGateSnapshotStoreModule,
12
+ type ContractGateSnapshotStoreUserConfig,
13
+ createFileContractGateSnapshotStore,
14
+ createHttpContractGateSnapshotStore,
15
+ DEFAULT_CONTRACT_GATE_SNAPSHOT_PATH,
16
+ type GateSnapshot,
17
+ type GateSnapshotGateValue,
18
+ resolveContractGateSnapshotPath,
19
+ resolveContractGateSnapshotStore,
20
+ } from './contractGateSnapshotStore';
21
+ export {
22
+ DEFAULT_ENVIRONMENT_NAME,
23
+ parseServerRuntimeExtensionsEnv,
24
+ type ServerRuntimeExtensionsEnv,
25
+ } from './env';
26
+ export {
27
+ getRequestPathname,
28
+ injectMfAssetCacheHeadersPlugin,
29
+ isMfManifestAsset,
30
+ isMfRemoteEntryAsset,
31
+ resolveMfAssetCacheHeaders,
32
+ } from './mfCache';
33
+ export {
34
+ type CollectDirectRemoteModuleFederationCssOptions,
35
+ collectDirectRemoteModuleFederationCss,
36
+ collectDirectRemoteModuleFederationCssWithMeta,
37
+ collectModuleFederationManifestCss,
38
+ createModuleFederationCssCollector,
39
+ injectModuleFederationCssPlugin,
40
+ type ModuleFederationCssCollectorOptions,
41
+ type ModuleFederationCssPluginOptions,
42
+ type ModuleFederationManifest,
43
+ type RemoteModuleFederationCssCollection,
44
+ } from './moduleFederationCss';
45
+ export {
46
+ createOtlpTelemetryExporter,
47
+ createRuntimeFallbackSignalRuntimeState,
48
+ createRuntimeSignalError,
49
+ createTelemetryAwareMetrics,
50
+ createVictoriaMetricsTelemetryExporter,
51
+ DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT,
52
+ DEFAULT_RUNTIME_STATUS_ENDPOINT,
53
+ enforceRuntimeFallbackSignalAuthToken,
54
+ enforceRuntimeFallbackSignalTrustPolicy,
55
+ getRuntimeSignalErrorStatusCode,
56
+ hasEnabledTelemetryExporters,
57
+ injectTelemetryPlugin,
58
+ normalizeRequiredRuntimeFallbackSignalAuthConfig,
59
+ normalizeRuntimeFallbackSignalAuthConfig,
60
+ normalizeRuntimeFallbackTrustPolicy,
61
+ type OtlpExporterOptions,
62
+ parseRuntimeFallbackSignalPayloadFromRawBody,
63
+ type RuntimeFallbackSignalAuthConfig,
64
+ type RuntimeFallbackSignalRuntimeState,
65
+ type RuntimeFallbackSignalSource,
66
+ type RuntimeFallbackSignalTrustContext,
67
+ type RuntimeFallbackSignalTrustPolicy,
68
+ type RuntimeSignalError,
69
+ type RuntimeSignalErrorCode,
70
+ resolveRuntimeFallbackSignalEndpoint,
71
+ resolveTelemetrySloOptions,
72
+ type TelemetryCanaryDecision,
73
+ TelemetryCanaryOrchestrator,
74
+ type TelemetryCanaryStatusSnapshot,
75
+ type TelemetryEnvelope,
76
+ type TelemetryExporter,
77
+ type TelemetryQueueStats,
78
+ TelemetryRegistry,
79
+ type TelemetryRegistryOptions,
80
+ type TelemetrySignalType,
81
+ type TelemetrySloAlert,
82
+ TelemetryStartupHealthError,
83
+ type VictoriaMetricsExporterOptions,
84
+ } from './telemetry';
package/src/mfCache.ts ADDED
@@ -0,0 +1,119 @@
1
+ import type {
2
+ Middleware,
3
+ ServerEnv,
4
+ ServerPlugin,
5
+ } from '@modern-js/server-core';
6
+
7
+ const REMOTE_ENTRY_REGEXP = /(^|\/)remoteEntry(?:\.[a-zA-Z0-9_-]+)?\.js$/;
8
+
9
+ function firstQueryValue(value: unknown): string | undefined {
10
+ if (Array.isArray(value)) {
11
+ return value.find(item => typeof item === 'string' && item.length > 0);
12
+ }
13
+ if (typeof value === 'string' && value.length > 0) {
14
+ return value;
15
+ }
16
+ return undefined;
17
+ }
18
+
19
+ export function getRequestPathname(url: string) {
20
+ try {
21
+ return new URL(url, 'http://modernjs.local').pathname;
22
+ } catch (_error) {
23
+ return url.split('?')[0] || '/';
24
+ }
25
+ }
26
+
27
+ export function isMfManifestAsset(pathname: string) {
28
+ return (
29
+ pathname.endsWith('/mf-manifest.json') ||
30
+ pathname.endsWith('/mf-stats.json')
31
+ );
32
+ }
33
+
34
+ export function isMfRemoteEntryAsset(pathname: string) {
35
+ return REMOTE_ENTRY_REGEXP.test(pathname);
36
+ }
37
+
38
+ function hasRemoteVersionPin(query: Record<string, unknown> = {}) {
39
+ return Boolean(
40
+ firstQueryValue(query.mfv) ||
41
+ firstQueryValue(query.v) ||
42
+ firstQueryValue(query.version),
43
+ );
44
+ }
45
+
46
+ export function resolveMfAssetCacheHeaders(
47
+ url: string,
48
+ query: Record<string, unknown> = {},
49
+ ) {
50
+ const pathname = getRequestPathname(url);
51
+
52
+ if (isMfManifestAsset(pathname)) {
53
+ return {
54
+ 'cache-control': 'no-cache, no-store, must-revalidate',
55
+ pragma: 'no-cache',
56
+ expires: '0',
57
+ };
58
+ }
59
+
60
+ if (isMfRemoteEntryAsset(pathname)) {
61
+ return hasRemoteVersionPin(query)
62
+ ? {
63
+ 'cache-control': 'public, max-age=31536000, immutable',
64
+ }
65
+ : {
66
+ 'cache-control': 'public, max-age=0, must-revalidate',
67
+ };
68
+ }
69
+
70
+ return undefined;
71
+ }
72
+
73
+ /**
74
+ * Applies the documented MF asset cache-header policy (ADR-0002) to responses
75
+ * for module federation manifest/remoteEntry endpoints:
76
+ *
77
+ * - `mf-manifest.json` / `mf-stats.json` are never cached (`no-store`), so
78
+ * hosts always observe remote redeploys;
79
+ * - `remoteEntry*.js` revalidates unless explicitly version-pinned via a
80
+ * `mfv`/`v`/`version` query parameter, in which case it is immutable.
81
+ *
82
+ * Registered by @modern-js/prod-server next to the other fork plugins; the
83
+ * middleware runs in the `pre` phase so it wraps the static-file middleware
84
+ * that actually serves these assets.
85
+ */
86
+ export const injectMfAssetCacheHeadersPlugin = (): ServerPlugin => ({
87
+ name: '@modern-js/inject-mf-asset-cache-headers',
88
+
89
+ setup(api) {
90
+ api.onPrepare(() => {
91
+ const { middlewares } = api.getServerContext();
92
+
93
+ const handler: Middleware<ServerEnv> = async (c, next) => {
94
+ await next();
95
+
96
+ // Never attach cache policies to error responses: an `immutable`
97
+ // 404 remoteEntry could otherwise be cached for a year.
98
+ if (!c.res || c.res.status >= 400) {
99
+ return;
100
+ }
101
+
102
+ const headers = resolveMfAssetCacheHeaders(c.req.path, c.req.query());
103
+ if (!headers) {
104
+ return;
105
+ }
106
+
107
+ for (const [name, value] of Object.entries(headers)) {
108
+ c.res.headers.set(name, value);
109
+ }
110
+ };
111
+
112
+ middlewares.push({
113
+ name: 'mf-asset-cache-headers',
114
+ handler,
115
+ order: 'pre',
116
+ });
117
+ });
118
+ },
119
+ });