UNPKG

zephyr-agent

Version:
472 lines (471 loc) • 21.9 kB
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