UNPKG

@embeddable.com/sdk-core

Version:

Core Embeddable SDK module responsible for web-components bundling and publishing.

349 lines (294 loc) 9.42 kB
import * as fs from "node:fs/promises"; import * as fsSync from "node:fs"; import archiver from "archiver"; import axios from "axios"; import ora, { Ora } from "ora"; import { initLogger, logError } from "./logger"; import * as path from "path"; import provideConfig from "./provideConfig"; // @ts-ignore import reportErrorToRollbar from "./rollbar.mjs"; import { findFiles } from "@embeddable.com/sdk-utils"; import { getToken } from "./login"; import { checkBuildSuccess, checkNodeVersion, getArgumentByKey } from "./utils"; import { selectWorkspace } from "./workspaceUtils"; import { ResolvedEmbeddableConfig } from "./defineConfig"; // grab cube files export const CUBE_FILES = /^(.*)\.cube\.(ya?ml|js)$/; export const CLIENT_CONTEXT_FILES = /^(.*)\.cc\.ya?ml$/; export const SECURITY_CONTEXT_FILES = /^(.*)\.sc\.ya?ml$/; export default async () => { await initLogger("push"); const breadcrumbs: string[] = []; let spinnerPushing; try { checkNodeVersion(); breadcrumbs.push("checkNodeVersion"); const isBuildSuccess = await checkBuildSuccess(); const config = await provideConfig(); const cubeVersion = getArgumentByKey(["--cube-version"]); if (!isBuildSuccess && config.pushComponents) { console.error( "Build failed or not completed. Please run `embeddable:build` first.", ); process.exit(1); } if (process.argv.includes("--api-key") || process.argv.includes("-k")) { spinnerPushing = ora("Using API key...").start(); breadcrumbs.push("push by api key"); try { await pushByApiKey(config, spinnerPushing, cubeVersion); } catch (error: any) { if (error.response?.data?.errorCode === "BUILDER-998") { spinnerPushing.fail( `Authentication failure. Server responded with: "${error.response?.data?.errorMessage}". Ensure that your API key is valid for the region specified in the embeddable.config.ts|js file. You are using the following region: ${config.region.replace("legacy-", "")} (${config.previewBaseUrl.replace("https://", "")} via ${config.pushBaseUrl}) Read more about deployment regions at https://docs.embeddable.com/deployment/deployment-regions`, ); process.exit(1); } spinnerPushing.fail("Publishing failed"); console.log(error.response?.data || error); process.exit(1); } publishedSectionFeedback(config, spinnerPushing); spinnerPushing.succeed("Published using API key"); return; } breadcrumbs.push("push by standard login"); const token = await verify(config); spinnerPushing = ora() .start() .info("No API Key provided. Standard login will be used."); breadcrumbs.push("select workspace"); const { workspaceId, name: workspaceName } = await selectWorkspace( ora, config, token, ); const workspacePreviewUrl = `${config.previewBaseUrl}/workspace/${workspaceId}`; const message = getArgumentByKey(["--message", "-m"]); breadcrumbs.push("build archive"); await buildArchive(config); spinnerPushing.info( `Publishing to ${workspaceName} using ${workspacePreviewUrl}...`, ); breadcrumbs.push("send build"); await sendBuild(config, { workspaceId, token, message, cubeVersion }); publishedSectionFeedback(config, spinnerPushing); spinnerPushing.succeed( `Published to ${workspaceName} using ${workspacePreviewUrl}`, ); } catch (error: any) { spinnerPushing?.fail("Publishing failed"); await logError({ command: "push", breadcrumbs, error }); await reportErrorToRollbar(error); console.log(error.response?.data || error); process.exit(1); } }; const publishedSectionFeedback = ( config: ResolvedEmbeddableConfig, spinnerPushing: Ora, ) => { config.pushModels && spinnerPushing.succeed("Models published"); config.pushComponents && spinnerPushing.succeed("Components published"); }; async function pushByApiKey( config: ResolvedEmbeddableConfig, spinner: any, cubeVersion?: string, ) { const apiKey = getArgumentByKey(["--api-key", "-k"]); if (!apiKey) { spinner.fail("No API key provided"); process.exit(1); } const email = getArgumentByKey(["--email", "-e"]); if (!email || !/\S+@\S+\.\S+/.test(email)) { spinner.fail( "Invalid email provided. Please provide a valid email using --email (-e) flag", ); process.exit(1); } // message is optional const message = getArgumentByKey(["--message", "-m"]); await buildArchive(config); return sendBuildByApiKey(config, { apiKey, email, message, cubeVersion, }); } async function verify(ctx: ResolvedEmbeddableConfig) { if (ctx.pushComponents) { try { await fs.access(ctx.client.buildDir); } catch (_e) { console.error("No embeddable build was produced."); process.exit(1); } } // TODO: initiate login if no/invalid token. const token = await getToken(); if (!token) { console.error("Expired token. Please login again."); process.exit(1); } return token; } export async function buildArchive(config: ResolvedEmbeddableConfig) { const spinnerArchive = ora("Building...").start(); if (!config.pushModels && !config.pushComponents) { spinnerArchive.fail( "Cannot push: both pushModels and pushComponents are disabled", ); process.exit(1); } const filesList: [string, string][] = []; if (config.pushModels) { const cubeFilesList = await findFiles( config.client.modelsSrc || config.client.srcDir, CUBE_FILES, ); const securityContextFilesList = await findFiles( config.client.presetsSrc || config.client.srcDir, SECURITY_CONTEXT_FILES, ); filesList.push( ...cubeFilesList.map((entry): [string, string] => [ path.basename(entry[1]), entry[1], ]), ...securityContextFilesList.map((entry): [string, string] => [ path.basename(entry[1]), entry[1], ]), ); } if (config.pushComponents) { const clientContextFilesList = await findFiles( config.client.presetsSrc || config.client.srcDir, CLIENT_CONTEXT_FILES, ); filesList.push( ...clientContextFilesList.map((entry): [string, string] => [ path.basename(entry[1]), entry[1], ]), ); } await archive({ ctx: config, filesList, isDev: false, }); return spinnerArchive.succeed("Bundling completed"); } export async function archive(args: { ctx: ResolvedEmbeddableConfig; filesList: [string, string][]; isDev: boolean; }) { const { ctx, filesList, isDev } = args; const output = fsSync.createWriteStream(ctx.client.archiveFile); const archive = archiver.create("zip", { zlib: { level: 9 }, }); archive.pipe(output); if (!isDev) { archive.directory(ctx.client.buildDir, false); // NOTE: for backward compatibility, keep the file name as global.css archive.file(ctx.client.customCanvasCss, { name: "global.css", }); } for (const fileData of filesList) { archive.file(fileData[1], { name: fileData[0], }); } await archive.finalize(); return new Promise<void>((resolve: any, _reject) => { output.on("close", () => resolve()); }); } export async function createFormData( filePath: string, metadata: Record<string, any>, ) { const { FormData, Blob } = await import("formdata-node"); const { fileFromPath } = await import("formdata-node/file-from-path"); const file = await fileFromPath(filePath, "embeddable-build.zip"); const form = new FormData(); form.set("file", file, "embeddable-build.zip"); const metadataBlob = new Blob([JSON.stringify(metadata)], { type: "application/json", }); form.set("request", metadataBlob, "request.json"); return form; } export async function sendBuildByApiKey( ctx: ResolvedEmbeddableConfig, { apiKey, email, message, cubeVersion, }: { apiKey: string; email: string; message?: string; cubeVersion?: string }, ) { const form = await createFormData(ctx.client.archiveFile, { pushModels: ctx.pushModels, pushComponents: ctx.pushComponents, authorEmail: email, description: message, ...(cubeVersion ? { cubeVersion } : {}), }); const response = await uploadFile( form, `${ctx.pushBaseUrl}/api/v1/bundle/upload`, apiKey, ); await fs.rm(ctx.client.archiveFile); return { ...response.data, message, cubeVersion }; } export async function sendBuild( ctx: ResolvedEmbeddableConfig, { workspaceId, token, message, cubeVersion, }: { workspaceId: string; token: string; message?: string; cubeVersion?: string; }, ) { const form = await createFormData(ctx.client.archiveFile, { pushModels: ctx.pushModels, pushComponents: ctx.pushComponents, authorEmail: "", description: message, ...(cubeVersion ? { cubeVersion } : {}), }); await uploadFile( form, `${ctx.pushBaseUrl}/bundle/${workspaceId}/upload`, token, ); await fs.rm(ctx.client.archiveFile); } async function uploadFile(formData: any, url: string, token: string) { return axios.post(url, formData, { headers: { "Content-Type": "multipart/form-data", Authorization: `Bearer ${token}`, }, maxContentLength: Infinity, maxBodyLength: Infinity, }); }