UNPKG

zephyr-xpack-internal

Version:

Xpack internals for Zephyr

408 lines (407 loc) • 20.1 kB
import "node:module"; import { ApplicationContext, ZeErrors, ZephyrError, normalizeBasePath } from "zephyr-agent"; import { getLegacyModuleFederationConfig, mergeModuleFederationBuildMetadata, mergeModuleFederationConfigs } from "./federation-config-metadata.mjs"; import * as __rspack_external_node_path_806ed179 from "node:path"; function xpackError(message) { return new ZephyrError(ZeErrors.ERR_DEPLOY_LOCAL_BUILD, { message }); } function normalizedDependencyPath(value) { return __rspack_external_node_path_806ed179.resolve(value); } function isWithinDirectory(filePath, directory) { const relative = __rspack_external_node_path_806ed179.relative(directory, filePath); return '' === relative || '..' !== relative && !relative.startsWith(`..${__rspack_external_node_path_806ed179.sep}`) && !__rspack_external_node_path_806ed179.isAbsolute(relative); } function dependencyPathsInclude(dependencies, invalidatedPath) { const exactDependencies = [ ...dependencies.fileDependencies, ...dependencies.missingDependencies, ...dependencies.buildDependencies ]; if (exactDependencies.some((dependency)=>normalizedDependencyPath(dependency) === invalidatedPath)) return true; return dependencies.contextDependencies.some((dependency)=>isWithinDirectory(invalidatedPath, normalizedDependencyPath(dependency))); } function mergeUnique(values) { const merged = []; const seen = new Set(); for (const value of values)for (const item of value ?? []){ const key = JSON.stringify(item); if (!seen.has(key)) { seen.add(key); merged.push(item); } } return merged; } function mergeBuildStats(stats) { const [base, ...rest] = stats; if (!base) throw xpackError('Cannot publish an xpack build without build statistics'); if (0 === rest.length) return base; return { ...base, modules: mergeUnique(stats.map((item)=>item.modules)), consumes: mergeUnique(stats.map((item)=>item.consumes)), overrides: mergeUnique(stats.map((item)=>item.overrides)), remotes: [ ...new Set(stats.flatMap((item)=>item.remotes ?? [])) ], federation: mergeModuleFederationBuildMetadata(stats.map((item)=>item.federation)) }; } function refreshBuildStatsIdentity(stats, current) { return { ...stats, version: current.version, app: { ...stats.app, buildId: current.app.buildId } }; } function mergeMfConfigs(configs) { const merged = mergeModuleFederationConfigs(configs); return merged.length > 0 ? merged : void 0; } function resolveServerEntrypoint(assetsMap, configuredEntrypoint) { if (configuredEntrypoint) { const normalized = configuredEntrypoint.replace(/\\/g, '/').replace(/^\.?\/+/, ''); const segments = normalized.split('/').filter((segment)=>segment && '.' !== segment); if (!normalized || 0 === segments.length || segments.includes('..')) throw xpackError(`XPack server entrypoint must be inside the snapshot: "${configuredEntrypoint}"`); return segments.join('/'); } const paths = new Set(Object.values(assetsMap).map((asset)=>asset.path)); const preferred = [ 'server/index.js', 'server/index.mjs', 'server/index.cjs', 'ssr/index.js', 'ssr/index.mjs', 'ssr/index.cjs', 'index.js', 'index.mjs', 'index.cjs' ]; return preferred.find((candidate)=>paths.has(candidate)) ?? [ ...paths ].find((candidate)=>/(^|\/)(server|index)\.(mjs|cjs|js)$/.test(candidate)); } class XPackBuildCoordinator { engine; context; participants; participantNames; dependents = new Map(); lastSuccessful = new Map(); localGenerations = new Map(); rejectedLocalGenerations = new Map(); pendingRecoveryParticipants = new Set(); participantBaseHrefs = new Map(); preparationPromises = new Map(); applicationBaseHref; activeBuild; nextLogicalGeneration = 0; constructor(engine, participants, options = {}){ this.engine = engine; this.participants = participants; this.participantNames = new Set(participants.map(({ name })=>name)); for (const { name } of participants)this.dependents.set(name, new Set()); for (const participant of participants)for (const dependency of participant.dependencies ?? []){ this.assertParticipant(dependency); this.dependents.get(dependency)?.add(participant.name); } this.context = new ApplicationContext({ applicationUid: engine.application_uid, prepare: ({ generation })=>this.prepareLogicalGeneration(generation), publish: async (publication)=>{ const metadata = publication.contributions.map((contribution)=>contribution.data).filter((item)=>!!item); const entrypoint = 'ssr' === options.snapshotType ? resolveServerEntrypoint(publication.assetsMap, options.entrypoint) : options.entrypoint; if ('ssr' === options.snapshotType && !entrypoint) throw xpackError('Could not infer the server entrypoint for a coordinated xpack build'); if ('ssr' === options.snapshotType && entrypoint && !Object.values(publication.assetsMap).some((asset)=>asset.path === entrypoint)) throw xpackError(`Coordinated xpack server entrypoint "${entrypoint}" was not emitted`); const mfConfigs = mergeMfConfigs(metadata.map((item)=>item.mfConfigs)); const buildStats = mergeBuildStats(metadata.map((item)=>item.buildStats)); await engine.upload_assets({ assetsMap: publication.assetsMap, buildStats, mfConfig: getLegacyModuleFederationConfig(mfConfigs), mfConfigs, hooks: metadata.find((item)=>item.hooks)?.hooks, snapshotType: options.snapshotType, entrypoint }); }, finish: async ({ identity })=>{ try { await engine.build_finished(); } finally{ this.preparationPromises.delete(identity.generation); } }, onFailure: (identity)=>{ this.preparationPromises.delete(identity.generation); engine.build_failed(); } }); } registerParticipantBaseHref(participant, baseHref) { this.assertParticipant(participant); if (this.engine.env?.target === 'tap-app') { this.participantBaseHrefs.set(participant, ''); this.applicationBaseHref = ''; this.engine.buildProperties.baseHref = ''; return; } const normalizedBaseHref = normalizeBasePath(baseHref); const existingParticipantBase = this.participantBaseHrefs.get(participant); if (this.participantBaseHrefs.has(participant) && existingParticipantBase !== normalizedBaseHref) throw xpackError(`Coordinated xpack participant "${participant}" registered conflicting application bases "${existingParticipantBase}" and "${normalizedBaseHref}"`); if (void 0 !== this.applicationBaseHref && this.applicationBaseHref !== normalizedBaseHref) { const [existingParticipant] = this.participantBaseHrefs.entries().next().value ?? [ 'another participant' ]; throw xpackError(`Coordinated xpack participants "${existingParticipant}" and "${participant}" use conflicting application bases "${this.applicationBaseHref}" and "${normalizedBaseHref}"`); } if (void 0 === this.applicationBaseHref) { this.applicationBaseHref = normalizedBaseHref; this.engine.buildProperties.baseHref = normalizedBaseHref; } this.participantBaseHrefs.set(participant, normalizedBaseHref); } beginBatch(participants) { if (0 === participants.length) return; for (const participant of participants)this.assertParticipant(participant); this.openInvalidationBatch(participants[0], participants); } invalidateParticipant(participant, filename) { this.assertParticipant(participant); const affected = this.resolveAffectedParticipants(participant, filename); const invalidationKey = filename?.trim() ? `path:${normalizedDependencyPath(filename)}` : 'unknown'; this.openInvalidationBatch(participant, affected, invalidationKey); } beginParticipant(participant, localGeneration) { this.assertParticipant(participant); if (!Number.isSafeInteger(localGeneration) || localGeneration < 0) throw xpackError(`XPack local generation must be a non-negative safe integer, received ${localGeneration}`); const rejected = this.rejectedLocalGenerations.get(participant); if (rejected?.has(localGeneration)) { rejected.delete(localGeneration); if (this.activeBuild && !this.activeBuild.session.isTerminal) this.activeBuild.session.abort(`Rejected failed xpack generation ${localGeneration} for "${participant}"`); throw xpackError(`XPack generation ${localGeneration} for "${participant}" belongs to a failed logical build`); } for (const rejectedGeneration of rejected ?? [])if (rejectedGeneration < localGeneration) rejected?.delete(rejectedGeneration); const previous = this.localGenerations.get(participant); if (previous && localGeneration < previous.localGeneration) throw xpackError(`Stale xpack generation ${localGeneration} for "${participant}"; latest is ${previous.localGeneration}`); if (previous && localGeneration === previous.localGeneration) { if (this.activeBuild?.logicalGeneration === previous.logicalGeneration && !this.activeBuild.session.isTerminal) { if (!this.activeBuild.contributions.has(participant)) this.activeBuild.dirtyParticipants.add(participant); return; } throw xpackError(`Stale xpack generation ${localGeneration} for "${participant}" has already completed`); } let active = this.activeBuild; if (!active || active.session.isTerminal) active = this.createBuild(); else if (active.startedParticipants.has(participant)) { const pendingParticipants = [ ...active.dirtyParticipants ]; active = this.createBuild(); for (const pending of pendingParticipants)active.dirtyParticipants.add(pending); } this.activeBuild = active; active.dirtyParticipants.add(participant); active.startedParticipants.add(participant); this.localGenerations.set(participant, { localGeneration, logicalGeneration: active.logicalGeneration }); } async prepareParticipant(participant, localGeneration) { const active = this.resolveParticipantBuild(participant, localGeneration); await this.prepareLogicalGeneration(active.logicalGeneration); if (active !== this.activeBuild || active.session.isTerminal) throw xpackError(`Stale xpack generation ${localGeneration ?? 'unknown'} for "${participant}"`); } async contribute(contribution) { const active = this.resolveBuild(contribution); const { session } = active; try { session.contribute({ participant: contribution.participant, key: contribution.participant, assetsMap: contribution.assetsMap, data: { buildStats: contribution.buildStats, mfConfigs: contribution.mfConfigs, hooks: contribution.hooks, dependencyPaths: contribution.dependencyPaths } }); session.completeParticipant(contribution.participant); active.contributions.set(contribution.participant, contribution); active.dirtyParticipants.delete(contribution.participant); return await this.publishIfReady(active); } catch (error) { this.rememberFailedBuild(active); if (!session.isTerminal) session.fail(contribution.participant, error); throw error; } } failParticipant(participant, error) { this.assertParticipant(participant); if (this.activeBuild?.session.status === 'failed') return; const active = !this.activeBuild || this.activeBuild.session.isTerminal ? this.createBuild() : this.activeBuild; active.dirtyParticipants.add(participant); this.rememberFailedBuild(active); active.session.fail(participant, error); } createBuild() { const logicalGeneration = this.nextLogicalGeneration++; const session = this.context.beginBuild({ invocationId: 'xpack-multi-compiler', generation: logicalGeneration, participants: this.participants }); const active = { logicalGeneration, session, dirtyParticipants: new Set(this.pendingRecoveryParticipants), startedParticipants: new Set(), contributions: new Map(), invalidationKeys: new Set() }; this.activeBuild = active; return active; } resolveBuild(contribution) { return this.resolveParticipantBuild(contribution.participant, contribution.generation); } resolveParticipantBuild(participant, localGeneration) { this.assertParticipant(participant); if (void 0 !== localGeneration) { let localState = this.localGenerations.get(participant); if (!localState || localState.localGeneration !== localGeneration) { this.beginParticipant(participant, localGeneration); localState = this.localGenerations.get(participant); } const active = this.activeBuild; if (!active || localState?.logicalGeneration !== active.logicalGeneration) throw xpackError(`Stale xpack generation ${localGeneration} for "${participant}"`); return active; } let active = this.activeBuild; if (!active || active.session.isTerminal || active.contributions.has(participant)) active = this.createBuild(); active.dirtyParticipants.add(participant); return active; } prepareLogicalGeneration(logicalGeneration) { if (0 === logicalGeneration) return Promise.resolve(); let preparation = this.preparationPromises.get(logicalGeneration); if (!preparation) { preparation = this.engine.start_new_build(); this.preparationPromises.set(logicalGeneration, preparation); } return preparation; } async publishIfReady(active) { if (active !== this.activeBuild || active.dirtyParticipants.size > 0) return false; const currentBuildStats = active.contributions.values().next().value?.buildStats; if (!currentBuildStats) return false; for (const { name } of this.participants){ if (active.contributions.has(name)) continue; const previous = this.lastSuccessful.get(name); if (!previous) return false; const carried = { ...previous, buildStats: refreshBuildStatsIdentity(previous.buildStats, currentBuildStats) }; active.session.contribute({ participant: name, key: name, assetsMap: carried.assetsMap, data: { buildStats: carried.buildStats, mfConfigs: carried.mfConfigs, hooks: carried.hooks, dependencyPaths: carried.dependencyPaths } }); active.session.completeParticipant(name); active.contributions.set(name, carried); } if (!active.session.readiness.ready) return false; await active.session.publish(); for (const contribution of active.session.seal().contributions)if (contribution.data) this.lastSuccessful.set(contribution.participant, { participant: contribution.participant, assetsMap: contribution.assetsMap, buildStats: contribution.data.buildStats, mfConfigs: contribution.data.mfConfigs, hooks: contribution.data.hooks, dependencyPaths: contribution.data.dependencyPaths }); this.pendingRecoveryParticipants.clear(); return true; } rememberFailedBuild(active) { const affected = new Set([ ...active.dirtyParticipants, ...active.startedParticipants ]); for (const participant of affected){ this.pendingRecoveryParticipants.add(participant); const state = this.localGenerations.get(participant); const rejectedGeneration = active.startedParticipants.has(participant) ? state?.localGeneration : (state?.localGeneration ?? -1) + 1; if (void 0 === rejectedGeneration) continue; let rejected = this.rejectedLocalGenerations.get(participant); if (!rejected) { rejected = new Set(); this.rejectedLocalGenerations.set(participant, rejected); } rejected.add(rejectedGeneration); } } openInvalidationBatch(originParticipant, affectedParticipants, invalidationKey) { let active = this.activeBuild; const affected = [ ...affectedParticipants ]; const joinsExistingInvalidation = !!active && !!invalidationKey && active.invalidationKeys.has(invalidationKey); if (!active || active.session.isTerminal) active = this.createBuild(); else if (active.startedParticipants.has(originParticipant)) active = this.createBuild(); else if (!joinsExistingInvalidation && affected.some((participant)=>active?.contributions.has(participant))) active = this.createBuild(); if (invalidationKey) active.invalidationKeys.add(invalidationKey); for (const participant of affected){ this.assertParticipant(participant); if (!(joinsExistingInvalidation && active.contributions.has(participant))) active.dirtyParticipants.add(participant); } this.activeBuild = active; } resolveAffectedParticipants(originParticipant, filename) { const direct = new Set([ originParticipant ]); if (!filename?.trim()) return new Set(this.participantNames); if (this.lastSuccessful.size !== this.participants.length || [ ...this.participantNames ].some((participant)=>!this.lastSuccessful.get(participant)?.dependencyPaths)) return new Set(this.participantNames); const invalidatedPath = normalizedDependencyPath(filename); for (const participant of this.participantNames){ const dependencies = this.lastSuccessful.get(participant)?.dependencyPaths; if (dependencies && dependencyPathsInclude(dependencies, invalidatedPath)) direct.add(participant); } const affected = new Set(direct); const queue = [ ...direct ]; while(queue.length > 0){ const dependency = queue.shift(); if (!dependency) continue; for (const dependent of this.dependents.get(dependency) ?? [])if (!affected.has(dependent)) { affected.add(dependent); queue.push(dependent); } } return affected; } assertParticipant(participant) { if (!this.participantNames.has(participant)) throw xpackError(`Unknown coordinated xpack participant "${participant}"`); } } export { XPackBuildCoordinator }; //# sourceMappingURL=xpack-build-coordinator.mjs.map