@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,124 @@
1
+ import { createFileContractGateSnapshotStore } from "./contractGateSnapshotStore.mjs";
2
+ const DEFAULT_POLL_INTERVAL_MS = 15000;
3
+ const DEFAULT_GATE_STALE_AFTER_MS = 600000;
4
+ class ContractGateAutopilot {
5
+ async start() {
6
+ await this.syncOnce();
7
+ if (this.poller) return;
8
+ this.poller = setInterval(()=>{
9
+ this.syncOnce();
10
+ }, this.pollIntervalMs);
11
+ if ('function' == typeof this.poller.unref) this.poller.unref();
12
+ }
13
+ stop() {
14
+ if (this.poller) {
15
+ clearInterval(this.poller);
16
+ this.poller = void 0;
17
+ }
18
+ }
19
+ async syncOnce() {
20
+ const snapshot = await this.loadSnapshot();
21
+ if (!snapshot) return 0;
22
+ const gates = this.normalizeSnapshot(snapshot);
23
+ let updatedCount = 0;
24
+ for (const gate of gates){
25
+ this.orchestrator.addRequiredContractGate(gate.name);
26
+ const fingerprint = `${gate.passed ? '1' : '0'}:${gate.reason || ''}`;
27
+ if (this.appliedGateFingerprints.get(gate.name) !== fingerprint) {
28
+ this.orchestrator.setContractGate(gate.name, gate.passed, gate.reason);
29
+ this.appliedGateFingerprints.set(gate.name, fingerprint);
30
+ updatedCount += 1;
31
+ this.logger?.info?.(`[telemetry.canary.autopilot] gate=${gate.name} passed=${String(gate.passed)} reason=${gate.reason || 'none'}`);
32
+ }
33
+ }
34
+ return updatedCount;
35
+ }
36
+ async loadSnapshot() {
37
+ try {
38
+ const snapshot = await this.gateSnapshotStore.readSnapshot();
39
+ if (!snapshot) return;
40
+ const fingerprint = JSON.stringify(snapshot);
41
+ if (fingerprint === this.lastSnapshotFingerprint) return;
42
+ this.lastSnapshotFingerprint = fingerprint;
43
+ return snapshot;
44
+ } catch (error) {
45
+ const source = this.gateSnapshotPath || this.gateSnapshotStore.name || 'stateStore';
46
+ this.logger?.warn?.(`[telemetry.canary.autopilot] failed to load gate snapshot ${source}: ${error instanceof Error ? error.message : String(error)}`);
47
+ return;
48
+ }
49
+ }
50
+ normalizeSnapshot(snapshot) {
51
+ const now = Date.now();
52
+ const output = [];
53
+ const gates = snapshot.gates;
54
+ if (!gates || 'object' != typeof gates) return output;
55
+ for (const [name, value] of Object.entries(gates)){
56
+ const normalizedName = name.trim();
57
+ if (!normalizedName) continue;
58
+ const gate = this.normalizeGateValue(value, snapshot.updatedAt, now);
59
+ if (!gate) continue;
60
+ if ('number' == typeof gate.expiresAt && Number.isFinite(gate.expiresAt) && gate.expiresAt > 0 && now >= gate.expiresAt) {
61
+ output.push({
62
+ name: normalizedName,
63
+ passed: true,
64
+ reason: void 0,
65
+ updatedAt: gate.updatedAt,
66
+ expiresAt: gate.expiresAt
67
+ });
68
+ continue;
69
+ }
70
+ const isStale = this.gateStaleAfterMs > 0 && now - gate.updatedAt > this.gateStaleAfterMs;
71
+ if (isStale) {
72
+ output.push({
73
+ name: normalizedName,
74
+ passed: false,
75
+ reason: gate.reason || 'Gate snapshot is stale',
76
+ updatedAt: gate.updatedAt
77
+ });
78
+ continue;
79
+ }
80
+ output.push({
81
+ name: normalizedName,
82
+ passed: gate.passed,
83
+ reason: gate.reason,
84
+ updatedAt: gate.updatedAt
85
+ });
86
+ }
87
+ return output;
88
+ }
89
+ normalizeGateValue(value, snapshotUpdatedAt, now) {
90
+ if ('boolean' == typeof value) return {
91
+ passed: value,
92
+ updatedAt: this.normalizeUpdatedAt(snapshotUpdatedAt, now)
93
+ };
94
+ if (!value || 'object' != typeof value) return;
95
+ const hasPassed = 'boolean' == typeof value.passed;
96
+ const passed = true === value.passed;
97
+ let reason = 'string' == typeof value.reason && value.reason.trim().length > 0 ? value.reason : void 0;
98
+ if (!hasPassed) reason = reason || 'Gate snapshot record is missing "passed" boolean';
99
+ return {
100
+ passed,
101
+ reason,
102
+ updatedAt: this.normalizeUpdatedAt(value.updatedAt ?? snapshotUpdatedAt, now),
103
+ expiresAt: this.normalizeExpiresAt(value.expiresAt)
104
+ };
105
+ }
106
+ normalizeUpdatedAt(value, fallback) {
107
+ if ('number' == typeof value && Number.isFinite(value) && value > 0) return value;
108
+ return fallback;
109
+ }
110
+ normalizeExpiresAt(value) {
111
+ if ('number' == typeof value && Number.isFinite(value) && value > 0) return value;
112
+ }
113
+ constructor(options){
114
+ this.appliedGateFingerprints = new Map();
115
+ this.orchestrator = options.orchestrator;
116
+ if (!options.gateSnapshotStore && !options.gateSnapshotPath) throw new Error('ContractGateAutopilot requires gateSnapshotPath or gateSnapshotStore');
117
+ this.gateSnapshotPath = options.gateSnapshotPath;
118
+ this.gateSnapshotStore = options.gateSnapshotStore || createFileContractGateSnapshotStore(options.gateSnapshotPath);
119
+ this.pollIntervalMs = Math.max(250, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
120
+ this.gateStaleAfterMs = Math.max(0, options.gateStaleAfterMs ?? DEFAULT_GATE_STALE_AFTER_MS);
121
+ this.logger = options.logger;
122
+ }
123
+ }
124
+ export { ContractGateAutopilot };
@@ -0,0 +1,190 @@
1
+ import { createRequire } from "node:module";
2
+ import { fs } from "@modern-js/utils";
3
+ import { promises } from "fs";
4
+ import path from "path";
5
+ import { parseServerRuntimeExtensionsEnv } from "./env.mjs";
6
+ const CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION = 1;
7
+ const DEFAULT_CONTRACT_GATE_SNAPSHOT_PATH = '.modern/contract-gates.json';
8
+ const DEFAULT_HTTP_STORE_TIMEOUT_MS = 5000;
9
+ const BUILTIN_HTTP_STATE_STORE_MODULES = new Set([
10
+ 'http',
11
+ '@modern-js/server-core/http',
12
+ '@modern-js/server-core/contract-gate-http-store',
13
+ '@modern-js/server-runtime-extensions/http',
14
+ '@modern-js/server-runtime-extensions/contract-gate-http-store'
15
+ ]);
16
+ const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
17
+ const normalizeSnapshot = (snapshot)=>{
18
+ if (!isRecord(snapshot)) return;
19
+ const schemaVersion = 'number' == typeof snapshot.schemaVersion ? snapshot.schemaVersion : CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION;
20
+ const updatedAt = 'number' == typeof snapshot.updatedAt ? snapshot.updatedAt : Date.now();
21
+ const gates = isRecord(snapshot.gates) ? snapshot.gates : {};
22
+ return {
23
+ schemaVersion,
24
+ updatedAt,
25
+ gates
26
+ };
27
+ };
28
+ const normalizeHttpStoreOptions = (options)=>{
29
+ const endpoint = 'string' == typeof options?.endpoint ? options.endpoint.trim() : '';
30
+ if (!endpoint) throw new Error('[telemetry.canary.autopilot] HTTP stateStore requires options.endpoint');
31
+ const readMethod = 'string' == typeof options?.readMethod && options.readMethod.trim() ? options.readMethod.trim().toUpperCase() : 'GET';
32
+ const writeMethod = 'string' == typeof options?.writeMethod && options.writeMethod.trim() ? options.writeMethod.trim().toUpperCase() : 'PUT';
33
+ const timeoutMsRaw = Number(options?.timeoutMs);
34
+ const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? Math.floor(timeoutMsRaw) : DEFAULT_HTTP_STORE_TIMEOUT_MS;
35
+ const headersRaw = options?.headers;
36
+ const headers = {};
37
+ if (headersRaw && 'object' == typeof headersRaw && !Array.isArray(headersRaw)) Object.entries(headersRaw).forEach(([key, value])=>{
38
+ if ('string' == typeof key && key.trim().length > 0 && null != value) headers[key] = String(value);
39
+ });
40
+ return {
41
+ endpoint,
42
+ readMethod,
43
+ writeMethod,
44
+ headers,
45
+ timeoutMs
46
+ };
47
+ };
48
+ const withTimeoutAbort = (timeoutMs)=>{
49
+ const controller = new AbortController();
50
+ const timer = setTimeout(()=>controller.abort(), timeoutMs);
51
+ return {
52
+ signal: controller.signal,
53
+ clear: ()=>clearTimeout(timer)
54
+ };
55
+ };
56
+ const createHttpContractGateSnapshotStore = (options)=>{
57
+ const normalized = normalizeHttpStoreOptions(options);
58
+ const endpoint = normalized.endpoint;
59
+ return {
60
+ name: `http:${endpoint}`,
61
+ async readSnapshot () {
62
+ const { signal, clear } = withTimeoutAbort(normalized.timeoutMs || 5000);
63
+ try {
64
+ const response = await fetch(endpoint, {
65
+ method: normalized.readMethod || 'GET',
66
+ headers: {
67
+ accept: 'application/json',
68
+ ...normalized.headers || {}
69
+ },
70
+ signal
71
+ });
72
+ if (404 === response.status) return;
73
+ if (!response.ok) throw new Error(`HTTP stateStore read failed with status ${String(response.status)}`);
74
+ const payload = await response.json();
75
+ return normalizeSnapshot(payload);
76
+ } finally{
77
+ clear();
78
+ }
79
+ },
80
+ async writeSnapshot (snapshot) {
81
+ const body = JSON.stringify(normalizeSnapshot(snapshot) || {
82
+ schemaVersion: CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION,
83
+ updatedAt: Date.now(),
84
+ gates: {}
85
+ });
86
+ const { signal, clear } = withTimeoutAbort(normalized.timeoutMs || 5000);
87
+ try {
88
+ const response = await fetch(endpoint, {
89
+ method: normalized.writeMethod || 'PUT',
90
+ headers: {
91
+ 'content-type': 'application/json',
92
+ ...normalized.headers || {}
93
+ },
94
+ body,
95
+ signal
96
+ });
97
+ if (!response.ok) throw new Error(`HTTP stateStore write failed with status ${String(response.status)}`);
98
+ } finally{
99
+ clear();
100
+ }
101
+ }
102
+ };
103
+ };
104
+ const tryResolveBuiltinSnapshotStore = (input)=>{
105
+ const moduleName = input.stateStore.module.trim();
106
+ if (!BUILTIN_HTTP_STATE_STORE_MODULES.has(moduleName)) return;
107
+ return createHttpContractGateSnapshotStore(input.stateStore.options || {});
108
+ };
109
+ const pickStoreFactory = (mod)=>{
110
+ if ('function' == typeof mod) return mod;
111
+ if ('function' == typeof mod.createContractGateSnapshotStore) return mod.createContractGateSnapshotStore;
112
+ if ('function' == typeof mod.default) return mod.default;
113
+ if (mod.default && 'object' == typeof mod.default && 'function' == typeof mod.default.createContractGateSnapshotStore) return mod.default.createContractGateSnapshotStore;
114
+ };
115
+ const ensureStoreShape = (store, modulePath)=>{
116
+ if (!store || 'object' != typeof store || 'function' != typeof store.readSnapshot || 'function' != typeof store.writeSnapshot) throw new Error(`Invalid contract gate snapshot store from "${modulePath}". Expected { readSnapshot(), writeSnapshot() }.`);
117
+ };
118
+ const resolveStoreModulePath = (appDirectory, modulePath)=>{
119
+ const normalized = modulePath.trim();
120
+ if (!normalized) throw new Error('Contract gate snapshot stateStore.module must be non-empty');
121
+ if (path.isAbsolute(normalized)) return normalized;
122
+ if (normalized.startsWith('.')) return path.resolve(appDirectory, normalized);
123
+ const appRequire = createRequire(path.join(appDirectory, 'package.json'));
124
+ try {
125
+ return appRequire.resolve(normalized);
126
+ } catch (_error) {
127
+ return normalized;
128
+ }
129
+ };
130
+ const resolveContractGateSnapshotPath = (appDirectory, configuredPath)=>{
131
+ const rawPath = configuredPath || parseServerRuntimeExtensionsEnv().contractGatesFile || DEFAULT_CONTRACT_GATE_SNAPSHOT_PATH;
132
+ if (path.isAbsolute(rawPath)) return rawPath;
133
+ return path.resolve(appDirectory, rawPath);
134
+ };
135
+ const createFileContractGateSnapshotStore = (gateSnapshotPath)=>{
136
+ const resolvedPath = path.resolve(gateSnapshotPath);
137
+ return {
138
+ name: `file:${resolvedPath}`,
139
+ async readSnapshot () {
140
+ if (!await fs.pathExists(resolvedPath)) return;
141
+ try {
142
+ const raw = await promises.readFile(resolvedPath, 'utf8');
143
+ return normalizeSnapshot(JSON.parse(raw));
144
+ } catch (_error) {
145
+ return;
146
+ }
147
+ },
148
+ async writeSnapshot (snapshot) {
149
+ const normalized = normalizeSnapshot(snapshot) || {
150
+ schemaVersion: CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION,
151
+ updatedAt: Date.now(),
152
+ gates: {}
153
+ };
154
+ await promises.mkdir(path.dirname(resolvedPath), {
155
+ recursive: true
156
+ });
157
+ await promises.writeFile(resolvedPath, `${JSON.stringify(normalized, null, 2)}\n`);
158
+ }
159
+ };
160
+ };
161
+ const resolveContractGateSnapshotStore = async (input)=>{
162
+ const { appDirectory, gateSnapshotPath, stateStore, logger } = input;
163
+ if (!stateStore?.module) return createFileContractGateSnapshotStore(gateSnapshotPath);
164
+ const builtinStore = tryResolveBuiltinSnapshotStore({
165
+ stateStore
166
+ });
167
+ if (builtinStore) {
168
+ logger?.info?.(`[telemetry.canary.autopilot] using built-in contract gate snapshot store "${builtinStore.name}"`);
169
+ return builtinStore;
170
+ }
171
+ const modulePath = resolveStoreModulePath(appDirectory, stateStore.module);
172
+ let mod;
173
+ try {
174
+ mod = require(modulePath);
175
+ } catch (error) {
176
+ throw new Error(`[telemetry.canary.autopilot] Failed to load stateStore.module "${stateStore.module}" (${modulePath}): ${error instanceof Error ? error.message : String(error)}`);
177
+ }
178
+ const factory = pickStoreFactory(mod);
179
+ if (!factory) throw new Error(`[telemetry.canary.autopilot] stateStore.module "${stateStore.module}" does not export createContractGateSnapshotStore()`);
180
+ const store = await factory({
181
+ appDirectory,
182
+ gateSnapshotPath,
183
+ options: stateStore.options,
184
+ logger
185
+ });
186
+ ensureStoreShape(store, modulePath);
187
+ logger?.info?.(`[telemetry.canary.autopilot] using contract gate snapshot store "${store.name || modulePath}"`);
188
+ return store;
189
+ };
190
+ export { CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION, DEFAULT_CONTRACT_GATE_SNAPSHOT_PATH, createFileContractGateSnapshotStore, createHttpContractGateSnapshotStore, resolveContractGateSnapshotPath, resolveContractGateSnapshotStore };
@@ -0,0 +1,17 @@
1
+ const DEFAULT_ENVIRONMENT_NAME = 'development';
2
+ const readString = (value)=>{
3
+ if ('string' != typeof value) return;
4
+ const trimmed = value.trim();
5
+ return trimmed.length > 0 ? trimmed : void 0;
6
+ };
7
+ const parseServerRuntimeExtensionsEnv = (env = process.env)=>{
8
+ const modernEnv = readString(env.MODERN_ENV);
9
+ const nodeEnv = readString(env.NODE_ENV);
10
+ return {
11
+ modernEnv,
12
+ nodeEnv,
13
+ environmentName: modernEnv || nodeEnv || DEFAULT_ENVIRONMENT_NAME,
14
+ contractGatesFile: readString(env.MODERN_CONTRACT_GATES_FILE)
15
+ };
16
+ };
17
+ export { DEFAULT_ENVIRONMENT_NAME, parseServerRuntimeExtensionsEnv };
@@ -0,0 +1,6 @@
1
+ export { ContractGateAutopilot } from "./contractGateAutopilot.mjs";
2
+ export { CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION, DEFAULT_CONTRACT_GATE_SNAPSHOT_PATH, createFileContractGateSnapshotStore, createHttpContractGateSnapshotStore, resolveContractGateSnapshotPath, resolveContractGateSnapshotStore } from "./contractGateSnapshotStore.mjs";
3
+ export { DEFAULT_ENVIRONMENT_NAME, parseServerRuntimeExtensionsEnv } from "./env.mjs";
4
+ export { getRequestPathname, injectMfAssetCacheHeadersPlugin, isMfManifestAsset, isMfRemoteEntryAsset, resolveMfAssetCacheHeaders } from "./mfCache.mjs";
5
+ export { collectDirectRemoteModuleFederationCss, collectDirectRemoteModuleFederationCssWithMeta, collectModuleFederationManifestCss, createModuleFederationCssCollector, injectModuleFederationCssPlugin } from "./moduleFederationCss.mjs";
6
+ export { DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT, DEFAULT_RUNTIME_STATUS_ENDPOINT, TelemetryCanaryOrchestrator, TelemetryRegistry, TelemetryStartupHealthError, createOtlpTelemetryExporter, createRuntimeFallbackSignalRuntimeState, createRuntimeSignalError, createTelemetryAwareMetrics, createVictoriaMetricsTelemetryExporter, enforceRuntimeFallbackSignalAuthToken, enforceRuntimeFallbackSignalTrustPolicy, getRuntimeSignalErrorStatusCode, hasEnabledTelemetryExporters, injectTelemetryPlugin, normalizeRequiredRuntimeFallbackSignalAuthConfig, normalizeRuntimeFallbackSignalAuthConfig, normalizeRuntimeFallbackTrustPolicy, parseRuntimeFallbackSignalPayloadFromRawBody, resolveRuntimeFallbackSignalEndpoint, resolveTelemetrySloOptions } from "./telemetry.mjs";
@@ -0,0 +1,55 @@
1
+ const REMOTE_ENTRY_REGEXP = /(^|\/)remoteEntry(?:\.[a-zA-Z0-9_-]+)?\.js$/;
2
+ function firstQueryValue(value) {
3
+ if (Array.isArray(value)) return value.find((item)=>'string' == typeof item && item.length > 0);
4
+ if ('string' == typeof value && value.length > 0) return value;
5
+ }
6
+ function getRequestPathname(url) {
7
+ try {
8
+ return new URL(url, 'http://modernjs.local').pathname;
9
+ } catch (_error) {
10
+ return url.split('?')[0] || '/';
11
+ }
12
+ }
13
+ function isMfManifestAsset(pathname) {
14
+ return pathname.endsWith('/mf-manifest.json') || pathname.endsWith('/mf-stats.json');
15
+ }
16
+ function isMfRemoteEntryAsset(pathname) {
17
+ return REMOTE_ENTRY_REGEXP.test(pathname);
18
+ }
19
+ function hasRemoteVersionPin(query = {}) {
20
+ return Boolean(firstQueryValue(query.mfv) || firstQueryValue(query.v) || firstQueryValue(query.version));
21
+ }
22
+ function resolveMfAssetCacheHeaders(url, query = {}) {
23
+ const pathname = getRequestPathname(url);
24
+ if (isMfManifestAsset(pathname)) return {
25
+ 'cache-control': 'no-cache, no-store, must-revalidate',
26
+ pragma: 'no-cache',
27
+ expires: '0'
28
+ };
29
+ if (isMfRemoteEntryAsset(pathname)) return hasRemoteVersionPin(query) ? {
30
+ 'cache-control': 'public, max-age=31536000, immutable'
31
+ } : {
32
+ 'cache-control': 'public, max-age=0, must-revalidate'
33
+ };
34
+ }
35
+ const injectMfAssetCacheHeadersPlugin = ()=>({
36
+ name: '@modern-js/inject-mf-asset-cache-headers',
37
+ setup (api) {
38
+ api.onPrepare(()=>{
39
+ const { middlewares } = api.getServerContext();
40
+ const handler = async (c, next)=>{
41
+ await next();
42
+ if (!c.res || c.res.status >= 400) return;
43
+ const headers = resolveMfAssetCacheHeaders(c.req.path, c.req.query());
44
+ if (!headers) return;
45
+ for (const [name, value] of Object.entries(headers))c.res.headers.set(name, value);
46
+ };
47
+ middlewares.push({
48
+ name: 'mf-asset-cache-headers',
49
+ handler,
50
+ order: 'pre'
51
+ });
52
+ });
53
+ }
54
+ });
55
+ export { getRequestPathname, injectMfAssetCacheHeadersPlugin, isMfManifestAsset, isMfRemoteEntryAsset, resolveMfAssetCacheHeaders };
@@ -0,0 +1,225 @@
1
+ import { fileReader } from "@modern-js/runtime-utils/fileReader";
2
+ import { fs, isProd } from "@modern-js/utils";
3
+ import path from "path";
4
+ const MODULE_FEDERATION_MANIFEST_FILE = 'mf-manifest.json';
5
+ const DEFAULT_REMOTE_MANIFEST_TIMEOUT = 1500;
6
+ const warn = (monitors, message, ...args)=>{
7
+ if (monitors) return void monitors.warn(message, ...args);
8
+ console.warn(message, ...args);
9
+ };
10
+ const ensureTrailingSlash = (value)=>value.endsWith('/') ? value : `${value}/`;
11
+ const tryResolveUrl = (value, base)=>{
12
+ try {
13
+ return new URL(value, base).toString();
14
+ } catch {
15
+ return;
16
+ }
17
+ };
18
+ const normalizeRemoteEntry = (entry)=>{
19
+ const value = entry.trim();
20
+ if (!value) return;
21
+ const atIndex = value.lastIndexOf('@');
22
+ return atIndex >= 0 ? value.slice(atIndex + 1) : value;
23
+ };
24
+ const getCssAssets = (assets)=>[
25
+ ...assets?.css?.sync || [],
26
+ ...assets?.css?.async || []
27
+ ];
28
+ const getManifestFallbackBase = (manifestUrl)=>{
29
+ try {
30
+ return new URL('.', manifestUrl).toString();
31
+ } catch {
32
+ return ensureTrailingSlash(manifestUrl);
33
+ }
34
+ };
35
+ const getManifestPublicPathBase = (publicPath, manifestUrl)=>{
36
+ if (!publicPath || 'auto' === publicPath) return getManifestFallbackBase(manifestUrl);
37
+ const base = tryResolveUrl(ensureTrailingSlash(publicPath), manifestUrl);
38
+ return base || getManifestFallbackBase(manifestUrl);
39
+ };
40
+ const appendResolvedCssAssets = (result, seen, assets, base)=>{
41
+ for (const asset of assets){
42
+ if (!asset) continue;
43
+ const resolved = tryResolveUrl(asset, base);
44
+ if (resolved && !seen.has(resolved)) {
45
+ seen.add(resolved);
46
+ result.push(resolved);
47
+ }
48
+ }
49
+ };
50
+ const collectModuleFederationManifestCss = (manifest, manifestUrl)=>{
51
+ const base = getManifestPublicPathBase(manifest.metaData?.publicPath, manifestUrl);
52
+ const result = [];
53
+ const seen = new Set();
54
+ for (const item of manifest.shared || [])appendResolvedCssAssets(result, seen, getCssAssets(item.assets), base);
55
+ for (const item of manifest.exposes || [])appendResolvedCssAssets(result, seen, getCssAssets(item.assets), base);
56
+ return result;
57
+ };
58
+ const fetchJsonWithTimeout = async (url, fetcher, timeout)=>{
59
+ const controller = new AbortController();
60
+ let timeoutId;
61
+ try {
62
+ const response = await Promise.race([
63
+ fetcher(url, {
64
+ signal: controller.signal
65
+ }),
66
+ new Promise((_, reject)=>{
67
+ timeoutId = setTimeout(()=>{
68
+ controller.abort();
69
+ reject(new Error(`Request timed out after ${timeout}ms`));
70
+ }, timeout);
71
+ })
72
+ ]);
73
+ if (!response.ok) throw new Error(`Unexpected status ${response.status}`);
74
+ return await response.json();
75
+ } finally{
76
+ if (timeoutId) clearTimeout(timeoutId);
77
+ }
78
+ };
79
+ const getHostManifest = async (pwd, monitors)=>{
80
+ const manifestPath = path.join(pwd, MODULE_FEDERATION_MANIFEST_FILE);
81
+ if (!await fs.pathExists(manifestPath)) return;
82
+ const manifestBuffer = await fileReader.readFileFromSystem(manifestPath, 'buffer');
83
+ if (null === manifestBuffer) return;
84
+ try {
85
+ return JSON.parse(manifestBuffer.toString('utf-8'));
86
+ } catch (error) {
87
+ warn(monitors, 'Parse module federation manifest failed, error = %s', error instanceof Error ? error.message : error);
88
+ return;
89
+ }
90
+ };
91
+ const collectDirectRemoteModuleFederationCssWithMeta = async (pwd, options = {})=>{
92
+ const hostManifest = await getHostManifest(pwd, options.monitors);
93
+ if (!hostManifest) return {
94
+ assets: [],
95
+ errored: false
96
+ };
97
+ const fetcher = options.fetcher || globalThis.fetch?.bind(globalThis);
98
+ if (!fetcher) {
99
+ warn(options.monitors, 'Skip module federation remote CSS collection because fetch is unavailable.');
100
+ return {
101
+ assets: [],
102
+ errored: false
103
+ };
104
+ }
105
+ const timeout = options.timeout ?? DEFAULT_REMOTE_MANIFEST_TIMEOUT;
106
+ const remoteResults = await Promise.all((hostManifest.remotes || []).map(async (remote)=>{
107
+ if (!remote.entry) return {
108
+ assets: [],
109
+ errored: false
110
+ };
111
+ const remoteEntry = normalizeRemoteEntry(remote.entry);
112
+ if (!remoteEntry) return {
113
+ assets: [],
114
+ errored: false
115
+ };
116
+ let remoteManifestUrl;
117
+ try {
118
+ remoteManifestUrl = new URL(remoteEntry).toString();
119
+ } catch {
120
+ warn(options.monitors, 'Skip module federation remote CSS collection for non-absolute manifest URL %s', remoteEntry);
121
+ return {
122
+ assets: [],
123
+ errored: false
124
+ };
125
+ }
126
+ try {
127
+ const remoteManifest = await fetchJsonWithTimeout(remoteManifestUrl, fetcher, timeout);
128
+ return {
129
+ assets: collectModuleFederationManifestCss(remoteManifest, remoteManifestUrl),
130
+ errored: false
131
+ };
132
+ } catch (error) {
133
+ warn(options.monitors, 'Load module federation remote manifest %s failed, error = %s', remoteManifestUrl, error instanceof Error ? error.message : error);
134
+ return {
135
+ assets: [],
136
+ errored: true
137
+ };
138
+ }
139
+ }));
140
+ const cssAssets = [];
141
+ const seen = new Set();
142
+ let errored = false;
143
+ for (const result of remoteResults){
144
+ errored = errored || result.errored;
145
+ for (const asset of result.assets)if (!seen.has(asset)) {
146
+ seen.add(asset);
147
+ cssAssets.push(asset);
148
+ }
149
+ }
150
+ return {
151
+ assets: cssAssets,
152
+ errored
153
+ };
154
+ };
155
+ const collectDirectRemoteModuleFederationCss = async (pwd, options = {})=>{
156
+ const { assets } = await collectDirectRemoteModuleFederationCssWithMeta(pwd, options);
157
+ return assets;
158
+ };
159
+ const createModuleFederationCssCollector = (pwd, options = {})=>{
160
+ const { ttlMs = 0, now = Date.now, ...collectOptions } = options;
161
+ const normalizedTtlMs = Math.max(0, ttlMs);
162
+ let cached;
163
+ let lastGoodAssets;
164
+ let inflight;
165
+ const refresh = (monitors)=>{
166
+ const promise = collectDirectRemoteModuleFederationCssWithMeta(pwd, {
167
+ ...collectOptions,
168
+ monitors: monitors ?? collectOptions.monitors
169
+ }).then((result)=>{
170
+ if (!result.errored) {
171
+ lastGoodAssets = result.assets;
172
+ cached = {
173
+ assets: result.assets,
174
+ expiresAt: now() + normalizedTtlMs
175
+ };
176
+ return result.assets;
177
+ }
178
+ cached = void 0;
179
+ return lastGoodAssets ?? result.assets;
180
+ }).finally(()=>{
181
+ if (inflight === promise) inflight = void 0;
182
+ });
183
+ inflight = promise;
184
+ return promise;
185
+ };
186
+ return {
187
+ collect (monitors) {
188
+ if (cached && now() < cached.expiresAt) return Promise.resolve(cached.assets);
189
+ if (inflight) return inflight;
190
+ return refresh(monitors);
191
+ }
192
+ };
193
+ };
194
+ const DEFAULT_REMOTE_CSS_CACHE_TTL_MS = 30000;
195
+ const injectModuleFederationCssPlugin = (options = {})=>({
196
+ name: '@modern-js/inject-module-federation-css',
197
+ setup (api) {
198
+ api.onPrepare(()=>{
199
+ const { middlewares, distDirectory: pwd } = api.getServerContext();
200
+ if (!pwd) return;
201
+ const ttlMs = Math.max(0, options.remoteCssCacheTtlMs ?? (isProd() ? DEFAULT_REMOTE_CSS_CACHE_TTL_MS : 0));
202
+ const collector = createModuleFederationCssCollector(pwd, {
203
+ ttlMs
204
+ });
205
+ if (isProd()) collector.collect().catch(()=>{});
206
+ const handler = async (c, next)=>{
207
+ const serverManifest = c.get('serverManifest');
208
+ if (serverManifest && !serverManifest.moduleFederationCssAssets) {
209
+ const monitors = c.get('monitors');
210
+ const moduleFederationCssAssets = await collector.collect(monitors);
211
+ c.set('serverManifest', {
212
+ ...serverManifest,
213
+ moduleFederationCssAssets
214
+ });
215
+ }
216
+ await next();
217
+ };
218
+ middlewares.push({
219
+ name: 'inject-module-federation-css',
220
+ handler
221
+ });
222
+ });
223
+ }
224
+ });
225
+ export { collectDirectRemoteModuleFederationCss, collectDirectRemoteModuleFederationCssWithMeta, collectModuleFederationManifestCss, createModuleFederationCssCollector, injectModuleFederationCssPlugin };