zephyr-xpack-internal
Version:
Xpack internals for Zephyr
408 lines (407 loc) • 20.1 kB
JavaScript
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