zephyr-agent
Version:
Zephyr plugin agent
472 lines (471 loc) • 21.9 kB
JavaScript
import "node:module";
import { isCI } from "ci-info";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { ZEPHYR_MANIFEST_FILENAME, ZE_ENV, ZeUtils, assertZephyrBuildTarget, createApplicationUid, flatCreateSnapshotId } from "zephyr-edge-contract";
import { checkAuth } from "../lib/auth/login.mjs";
import { getGitInfo } from "../lib/build-context/ze-util-get-git-info.mjs";
import { getPackageJson } from "../lib/build-context/ze-util-read-package-json.mjs";
import { getZephyrConfig, mergeRemoteDependencies } from "../lib/build-context/zephyr-config.mjs";
import { getUploadStrategy } from "../lib/deployment/get-upload-strategy.mjs";
import { get_hash_list } from "../lib/edge-hash-list/distributed-hash-control.mjs";
import { get_missing_assets } from "../lib/edge-hash-list/get-missing-assets.mjs";
import { getApplicationConfiguration } from "../lib/edge-requests/get-application-configuration.mjs";
import { getBuildId } from "../lib/edge-requests/get-build-id.mjs";
import { ZeErrors, ZephyrError } from "../lib/errors/index.mjs";
import { ze_log } from "../lib/logging/index.mjs";
import { cyanBright, greenBright, white, yellow } from "../lib/logging/picocolor.mjs";
import { logFn, logger as ze_log_event_mjs_logger } from "../lib/logging/ze-log-event.mjs";
import { setAppDeployResult } from "../lib/node-persist/app-deploy-result-cache.mjs";
import { zeBuildAssets } from "../lib/transformers/ze-build-assets.mjs";
import { createSnapshot } from "../lib/transformers/ze-build-snapshot.mjs";
import { convertResolvedDependencies, createManifestContent } from "../lib/transformers/ze-create-manifest.mjs";
import { maybeShowOutdatedPluginWarning } from "../lib/version/outdated-plugin-warning.mjs";
import { resolveZephyrPluginPackageName } from "../lib/version/plugin-package-name.mjs";
import { getZephyrAgentVersion } from "../lib/version/zephyr-agent-version.mjs";
import { resolve_remote_dependency } from "./resolve_remote_dependency.mjs";
function is_zephyr_dependency_pair(dep) {
return !!dep;
}
function is_zephyr_resolved_dependency(dep) {
return null !== dep;
}
class ZephyrEngine {
npmProperties;
gitProperties;
zephyrConfig;
application_uid;
application_configuration;
applicationProperties;
logger;
env = {
isCI: isCI,
target: 'web',
env: ZE_ENV(),
ssr: false
};
buildProperties = {
output: './dist'
};
builder;
federated_dependencies = null;
build_start_time = null;
build_id = null;
snapshotId = null;
hash_list = null;
resolved_hash_list = null;
version_url = null;
target_urls = null;
build_stats_time = null;
snapshot_with_envs = null;
ze_env_vars = null;
ze_env_vars_hash = null;
worker_version = null;
get hasActiveBuild() {
return null !== this.build_start_time || null !== this.build_id || null !== this.snapshotId;
}
get zephyr_dependencies() {
return convertResolvedDependencies(this.federated_dependencies ?? []);
}
constructor(options){
this.builder = options.builder;
}
static defer_create() {
let resolve;
let reject;
let creationStarted = false;
const zephyr_engine_defer = new Promise((res, rej)=>{
resolve = res;
reject = rej;
});
zephyr_engine_defer.catch(()=>void 0);
return {
zephyr_engine_defer,
zephyr_defer_create (options) {
if (creationStarted) return;
creationStarted = true;
ZephyrEngine.create(options).then(resolve, reject);
}
};
}
static async create(options) {
if (void 0 !== options.target) assertZephyrBuildTarget(options.target, 'ZephyrEngine.create({ target })');
const context = options.context || process.cwd();
const zephyrConfig = getZephyrConfig(context);
ze_log.init(`Initializing: Zephyr Engine for ${context}...`);
const ze = new ZephyrEngine({
context,
builder: options.builder
});
if (void 0 !== options.target) ze.env.target = options.target;
ze.zephyrConfig = zephyrConfig;
ze_log.init('Initializing: npm package info...');
ze.npmProperties = await getPackageJson(context, zephyrConfig);
const pluginPackageName = resolveZephyrPluginPackageName(ze.npmProperties, options.builder);
maybeShowOutdatedPluginWarning(pluginPackageName);
ze_log.init('Initializing: git info...');
ze.gitProperties = await getGitInfo(context, zephyrConfig);
mut_zephyr_app_uid(ze);
const application_uid = ze.application_uid;
ze_log.init('Initializing: checking authentication...');
await checkAuth(ze.gitProperties);
ze_log.init('Initialized: loading application configuration...');
ze.application_configuration = getApplicationConfiguration({
application_uid
});
ze.application_configuration.then((appConfig)=>{
const { username, email, EDGE_URL } = appConfig;
ze_log.init('Loaded: application configuration', {
username,
email,
EDGE_URL
});
}).catch((err)=>ze_log.init(`Failed to get application configuration: ${err}`));
try {
await ze.start_new_build();
const [initializedLogger, { username }, buildId] = await Promise.all([
ze.logger,
ze.application_configuration,
ze.build_id
]);
initializedLogger({
level: 'info',
action: 'build:info:user',
ignore: true,
message: `Hi ${cyanBright(username)}!\n${white(application_uid)}${yellow(`#${buildId}`)}\n`
});
return ze;
} catch (error) {
if (ze.hasActiveBuild) ze.build_failed();
throw error;
}
}
async resolve_remote_dependencies(deps, defaultLibraryType = 'var') {
if (!deps) return null;
const app_config = await this.application_configuration;
const ze_dependencies = this.npmProperties.zephyrDependencies;
const platform = this.env.target ?? 'web';
assertZephyrBuildTarget(platform, 'ZephyrEngine.env.target');
const build_context_json = {
target: platform,
isCI: isCI,
branch: this.gitProperties.git.branch,
username: app_config.username,
env: this.env.env
};
if (this.env.env) ze_log.config('Using environment:', this.env.env);
const build_context = Buffer.from(JSON.stringify(build_context_json)).toString('base64');
ze_log.remotes('resolve_remote_dependencies.deps', deps, 'platform', platform, 'ze_dependencies', ze_dependencies);
const resolution_errors = [];
const tasks = deps.map(async (dep)=>{
const [app_name, project_name, org_name] = dep.name.split('.', 3);
const ze_dependency = ze_dependencies?.[dep.name];
const [ze_app_name, ze_project_name, ze_org_name] = ze_dependency?.app_uid?.split('.') ?? [];
const dep_application_uid = createApplicationUid({
org: ze_org_name ?? org_name ?? this.gitProperties.app.org,
project: ze_project_name ?? project_name ?? this.gitProperties.app.project,
name: ze_app_name ?? app_name
});
const tuple = await ZeUtils.PromiseTuple(resolve_remote_dependency({
application_uid: dep_application_uid,
version: ze_dependency?.version ?? dep.version,
platform,
build_context,
dependencyUrlMode: this.zephyrConfig.dependencyUrlMode
}));
if (!ZeUtils.isSuccessTuple(tuple)) {
ze_log.remotes(`Failed to resolve remote dependency: ${dep.name}@${dep.version}`, 'skipping...');
resolution_errors.push({
dep,
error: tuple[0]
});
return null;
}
ze_log.remotes(`Resolved dependency: ${tuple[1].default_url}`);
if (dep.name === tuple[1].name) return tuple[1];
return Object.assign({
library_type: defaultLibraryType
}, tuple[1], {
name: dep.name,
version: dep.version
});
});
const resolution_results = await Promise.all(tasks);
if (resolution_errors.length > 0) {
const logger = await this.logger;
const errorSummary = resolution_errors.map(({ dep, error })=>{
const errorMessage = ZephyrError.is(error) ? `Error code: ${error.code}` : "Unknown error";
const version = ZephyrError.is(error) && error.template && 'version' in error.template ? error.template.version : dep.version;
return ` - ${dep.name}@${version} -> ${errorMessage}`;
}).join('\n');
logger({
level: 'warn',
action: 'build:error:dependency_resolution',
message: `Failed to resolve remote dependencies:
${errorSummary}\n
More information on remote dependency resolution please check:
https://docs.zephyr-cloud.io/features/remote-dependencies`
});
}
this.federated_dependencies = resolution_results.filter(is_zephyr_resolved_dependency);
if (this.federated_dependencies.length > 0) {
const remotesList = this.federated_dependencies.map((dep)=>` ${dep.name} → ${dep.remote_entry_url}`).join('\n');
logFn('info', `Resolved remotes:\n${remotesList}`);
}
return this.federated_dependencies;
}
async start_new_build() {
ze_log.init('Starting new build');
const ze = this;
ze.build_start_time = Date.now();
if (ze.build_id && ze.snapshotId && await ze.build_id && await ze.snapshotId) return void ze_log.init('Skip: creating new build because no assets was uploaded');
const application_uid = ze.application_uid;
ze_log.init('Initializing: loading of hash list');
const hashList = get_hash_list(application_uid);
ze.hash_list = hashList;
hashList.then((hash_set)=>{
if (ze.hash_list === hashList) {
ze.resolved_hash_list = hash_set;
ze_log.app(`Loaded: hash list with ${hash_set.hash_set.size} entries`);
}
}).catch((err)=>ze_log.app(`Failed to get hash list: ${err}`));
ze_log.init('Initializing: loading of build id');
const buildId = getBuildId(application_uid);
ze.build_id = buildId;
buildId.then((buildId)=>ze_log.app(`Loaded build id "${buildId}"`)).catch((err)=>ze_log.app(`Failed to get build id: ${err}`));
ze_log.init('Initializing: logger');
const initializedLogger = Promise.all([
ze.application_configuration,
buildId
]).then((record)=>{
const resolvedBuildId = record[1];
ze_log.init('Initialized: application configuration, build id and hash list');
return ze_log_event_mjs_logger({
application_uid,
buildId: resolvedBuildId,
git: ze.gitProperties.git
});
});
ze.logger = initializedLogger;
const snapshotId = Promise.all([
ze.application_configuration,
buildId
]).then((record)=>flatCreateSnapshotId({
...ze.applicationProperties,
buildId: record[1],
username: record[0].username
}));
ze.snapshotId = snapshotId;
try {
await Promise.all([
initializedLogger,
snapshotId
]);
} catch (error) {
if (ze.build_id === buildId) resetBuildState(ze);
throw error;
}
}
async build_finished() {
const zephyr_engine = this;
try {
const logger = await zephyr_engine.logger;
const zeStart = zephyr_engine.build_start_time;
const buildStatsTime = zephyr_engine.build_stats_time;
const versionUrl = zephyr_engine.version_url;
const targetUrls = zephyr_engine.target_urls;
const dependencies = zephyr_engine.federated_dependencies;
const if_target_is_react_native = 'ios' === zephyr_engine.env.target || 'android' === zephyr_engine.env.target;
const ze_env = ZE_ENV();
if (ze_env) {
logger({
level: 'info',
action: 'build:info:env',
ignore: true,
message: `Using environment: ${cyanBright(ze_env)}`
});
zephyr_engine.env.env = ze_env;
}
if (zeStart && versionUrl) {
if (dependencies && dependencies.length > 0) logger({
level: 'info',
action: 'build:info:user',
ignore: true,
message: if_target_is_react_native ? `Resolved zephyr dependencies: ${dependencies.map((dep)=>dep.name).join(', ')} for platform: ${zephyr_engine.env.target}` : `Resolved zephyr dependencies: ${dependencies.map((dep)=>dep.name).join(', ')}`
});
logger({
level: 'trace',
action: 'deploy:url',
message: `Deployed to ${cyanBright('Zephyr')}'s edge in ${yellow(`${Date.now() - zeStart}`)}ms.\n\n${cyanBright(versionUrl)}`
});
}
if (targetUrls?.length && buildStatsTime) logger({
level: 'trace',
action: 'deploy:url',
message: `\nUpdated ${greenBright(targetUrls.length.toString())} targets in ${yellow(buildStatsTime?.toString())}ms\n- ${targetUrls.join('\n- ')}`
});
} finally{
resetBuildState(this);
}
}
build_failed() {
resetBuildState(this);
}
async upload_assets(props) {
const zephyr_engine = this;
try {
ze_log.upload('Initializing: upload assets');
const { assetsMap: providedAssetsMap, buildStats, mfConfig, mfConfigs, snapshotType, entrypoint } = props;
const target = zephyr_engine.env?.target ?? 'web';
assertZephyrBuildTarget(target, 'ZephyrEngine.env.target');
const assetsMap = Object.isFrozen(providedAssetsMap) ? {
...providedAssetsMap
} : providedAssetsMap;
const assetsByPath = indexAssetsBySnapshotPath(assetsMap);
const emittedManifest = assetsByPath.get(ZEPHYR_MANIFEST_FILENAME);
if (!emittedManifest) {
const manifest = {
filepath: ZEPHYR_MANIFEST_FILENAME,
content: createManifestContent(zephyr_engine.federated_dependencies ?? [])
};
const manifestAsset = zeBuildAssets(manifest);
if (assetsMap[manifestAsset.hash]) throw new ZephyrError(ZeErrors.ERR_DEPLOY_LOCAL_BUILD, {
message: `Generated ${ZEPHYR_MANIFEST_FILENAME} hash ${manifestAsset.hash} collides with an existing asset map entry.`
});
assetsMap[manifestAsset.hash] = manifestAsset;
}
if (!zephyr_engine.application_uid || !zephyr_engine.build_id) throw new ZephyrError(ZeErrors.ERR_DEPLOY_LOCAL_BUILD, {
message: 'ZephyrEngine cannot upload before application_uid and build_id are initialized.'
});
await zephyr_engine.build_id;
let hash_set = zephyr_engine.resolved_hash_list;
if (!hash_set && zephyr_engine.hash_list) try {
hash_set = await zephyr_engine.hash_list;
zephyr_engine.resolved_hash_list = hash_set;
} catch (error) {
ze_log.upload(`Failed to load hash list; uploading all assets: ${error}`);
hash_set = {
hash_set: new Set()
};
}
const missingAssets = get_missing_assets({
assetsMap,
hash_set: hash_set ?? {
hash_set: new Set()
}
});
const snapshot = await createSnapshot(zephyr_engine, {
assets: assetsMap,
mfConfig,
mfConfigs,
snapshotType,
entrypoint
});
const waitForCompletion = !!process.env['ZE_WAIT_FOR_DEPLOYMENTS']?.trim();
if (waitForCompletion) {
const logger = await this.logger;
logger({
action: 'deploy:wait',
level: 'info',
message: "Waiting for deployment to complete..."
});
}
const upload_options = {
snapshot,
getDashData: (engine)=>{
const dash_data = buildStats.ze_envs || buildStats.ze_envs_hash ? buildStats : {
...buildStats,
ze_envs: (engine || zephyr_engine).ze_env_vars || void 0,
ze_envs_hash: (engine || zephyr_engine).ze_env_vars_hash || void 0
};
return {
...dash_data,
builder: dash_data.builder ?? zephyr_engine.builder,
plugin_version: dash_data.plugin_version ?? getZephyrAgentVersion(),
worker_version: dash_data.worker_version ?? zephyr_engine.worker_version ?? void 0,
waitForCompletion
};
},
assets: {
assetsMap,
missingAssets
}
};
const platform = (await zephyr_engine.application_configuration).PLATFORM;
const strategy = getUploadStrategy(platform);
zephyr_engine.version_url = await strategy(zephyr_engine, upload_options);
const application_uid = zephyr_engine.application_uid;
await setAppDeployResult(application_uid, {
urls: [
zephyr_engine.version_url
],
snapshot
});
if (props.hooks?.onDeployComplete && zephyr_engine.version_url) try {
const snapshotId = await zephyr_engine.snapshotId;
const deploymentInfo = {
url: zephyr_engine.version_url,
snapshotId,
snapshot,
federatedDependencies: zephyr_engine.federated_dependencies || [],
buildStats: upload_options.getDashData(zephyr_engine)
};
await props.hooks.onDeployComplete(deploymentInfo);
} catch (error) {
ze_log.upload('Warning: deployment hook failed', error);
}
} catch (error) {
zephyr_engine.build_failed();
throw error;
}
}
}
function indexAssetsBySnapshotPath(assetsMap) {
const assetsByPath = new Map();
for (const asset of Object.values(assetsMap)){
const canonicalPath = asset.path.replaceAll('\\', '/');
const existing = assetsByPath.get(canonicalPath);
if (existing) throw new ZephyrError(ZeErrors.ERR_DEPLOY_LOCAL_BUILD, {
message: `Ambiguous asset path "${canonicalPath}": ${existing.hash} and ${asset.hash} would publish to the same location.`
});
assetsByPath.set(canonicalPath, asset);
}
return assetsByPath;
}
function resetBuildState(zephyr_engine) {
zephyr_engine.build_id = null;
zephyr_engine.snapshotId = null;
zephyr_engine.hash_list = null;
zephyr_engine.resolved_hash_list = null;
zephyr_engine.version_url = null;
zephyr_engine.target_urls = null;
zephyr_engine.build_start_time = null;
zephyr_engine.build_stats_time = null;
zephyr_engine.snapshot_with_envs = null;
zephyr_engine.ze_env_vars = null;
zephyr_engine.ze_env_vars_hash = null;
zephyr_engine.worker_version = null;
}
function mut_zephyr_app_uid(ze) {
ze.applicationProperties = {
org: ze.gitProperties.app.org,
project: ze.gitProperties.app.project,
name: ze.zephyrConfig.appName ?? ze.npmProperties.name,
version: ze.npmProperties.version
};
ze.application_uid = createApplicationUid(ze.applicationProperties);
}
function readPackageJson(root, zephyrConfig = getZephyrConfig(root)) {
const packageJsonPath = join(root, 'package.json');
const packageJsonContent = existsSync(packageJsonPath) ? readFileSync(packageJsonPath, 'utf-8') : '{}';
const packageJson = JSON.parse(packageJsonContent);
const zephyrDependencies = mergeRemoteDependencies(packageJson.zephyrDependencies ?? packageJson['zephyr:dependencies'], zephyrConfig);
return zephyrDependencies ? {
...packageJson,
zephyrDependencies
} : packageJson;
}
export { ApplicationContext, ApplicationContextRegistry } from "./application-context.mjs";
export { BuildParticipantFailedError, BuildSession, BuildSessionAbortedError, BuildSessionAssetCollisionError, BuildSessionNotReadyError, BuildSessionRollbackError, BuildSessionStateError } from "./build-session.mjs";
export { ZephyrEngine, is_zephyr_dependency_pair, is_zephyr_resolved_dependency, readPackageJson };
//# sourceMappingURL=index.mjs.map