UNPKG

@supernovaio/cli

Version:

Supernova.io Command Line Interface

368 lines (366 loc) 16.5 kB
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="478a26da-061b-5a2e-a411-cb308a54d3ca")}catch(e){}}(); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; import { Flags } from "@oclif/core"; import { action } from "@oclif/core/ux"; import { SentryTraced } from "@sentry/nestjs"; import slugify from "@sindresorhus/slugify"; import AdmZip from "adm-zip"; import axios from "axios"; import cliProgress from "cli-progress"; import inquirer from "inquirer"; import * as fs from "node:fs"; import path from "node:path"; import terminalLink from "terminal-link"; import { z } from "zod"; import { commonFlags, SentryCommand, storybookUrlForEnvironment } from "../types/index.js"; import { sleep } from "../utils/common.js"; const MAX_ZIP_SIZE_BYTES = 500 * 1024 * 1024; const bytesToKB = (bytes) => (bytes / 1024).toFixed(2); const bytesToMB = (bytes) => (bytes / (1024 * 1024)).toFixed(2); const ImportStorybookConfig = z.object({ brandId: z.string().optional(), designSystemId: z.string().optional(), from: z.string().optional(), name: z.string().optional(), sourceId: z.string().optional(), }); const storybookEndpoint = (environment, designSystemId, name, accessToken, format = "html") => { const host = storybookUrlForEnvironment(environment); const encodedAccessToken = encodeURIComponent(accessToken); return `${host}/design-systems/${designSystemId}/alias/${name}/index.${format}?authorization=${encodedAccessToken}`; }; export default class ImportStorybook extends SentryCommand { static args = {}; static description = "Import storybook static export to Supernova"; static examples = ["<%= config.bin %> <%= command.id %> import-storybook "]; static flags = { ...commonFlags, brandId: Flags.string({ char: "b", description: "Import storybooks to brand of" }), designSystemId: Flags.string({ char: "d", description: "Import storybooks to design system of" }), from: Flags.string({ char: "f", description: "Directory with storybook static export to import.", }), name: Flags.string({ char: "n", description: "Import storybooks with name of" }), sourceId: Flags.string({ char: "s", description: "Import storybooks to source of" }), }; get commandId() { return ImportStorybook.id; } get configSchema() { return ImportStorybookConfig; } async run() { const { flags } = await this.parse(); const config = this.configService.get(); const storybookConfig = config?.storybook ?? {}; const { sourceId } = { ...storybookConfig, ...flags }; const storybookDirectory = await this.getStorybookDirectory(flags, storybookConfig); const storybookName = await this.getStorybookName(flags, storybookConfig); const designSystemId = await this.getDesignSystemId(flags, storybookConfig); const brandPersistentId = await this.getBrandId(flags, storybookConfig, designSystemId); const apiClient = await this.apiClient(); const { designSystems } = apiClient; const { storybookHosting } = designSystems; await this.validateDatasource(designSystems.sources, sourceId, designSystemId); this.log(`Preparing Storybook files from ${storybookDirectory}...`); const { sizeBytes, zipPath } = await this.createZipFromDirectory(storybookDirectory); const sizeValidation = this.validateZipSize(zipPath, sizeBytes); if (!sizeValidation.isValid) { this.error(sizeValidation.error); } const { signedUrl, storybookUploadId } = await storybookHosting.getSignedUploadUrl(designSystemId, { name: storybookName, }); await this.uploadArchiveToSignedUrl({ designSystemId, storybookName, signedUrl, storybookUploadId, zipPath }); this.log("✅ Upload complete."); action.start("Deploying Storybook to private Supernova hosting service"); await this.waitForPublishing({ apiClient, designSystemId, storybookUploadId }); action.stop("\n✅ Private Storybook deployed successfully!"); action.start("Updating Storybook stories"); const { sourceId: finalSourceId } = await this.importStorybookStories({ apiClient, designSystemId, brandPersistentId, sourceId, storybookDirectory, storybookName, }); action.stop("\n✅ Storybook stories have been updated!"); this.configService.update({ storybook: { designSystemId, from: storybookDirectory, name: storybookName, brandId: brandPersistentId, sourceId: finalSourceId, }, }); } async getStorybookName(flags, config) { let result = flags.name ?? config.name; if (!result) { const choice = await inquirer.prompt([ { message: "Enter name of your storybook instance (it will be part of the URL)", name: "name", type: "input", }, ]); if (typeof choice.name === "string") { result = choice.name; } } if (!result) { this.error("Parameter `name` is required"); } return slugify(result, { lowercase: true }); } async getDesignSystemId(flags, config) { let designSystemId = flags.designSystemId ?? config.designSystemId; if (!designSystemId) { designSystemId = await this.promptDesignSystemId(); } if (!designSystemId) this.error("Parameter `designSystemId` is required"); return designSystemId; } async getBrandId(flags, config, designSystemId) { let brandId = flags.brandId ?? config.brandId; if (!brandId) { brandId = await this.promptBrandId(designSystemId); } if (!brandId) this.error("Parameter `brandId` is required"); return brandId; } async getStorybookDirectory(flags, config) { const from = flags.from ?? config.from; if (!from) this.error("Parameter `from` is required"); const directoryValidation = this.validateStorybookDirectory(from); if (!directoryValidation.isValid) { this.error(directoryValidation.error); } return from; } async createZipFromDirectory(directoryPath) { const zip = new AdmZip(); let totalFiles = 0; let processedFiles = 0; const countFiles = (currentPath) => { const files = fs.readdirSync(currentPath); for (const file of files) { const filePath = path.join(currentPath, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { countFiles(filePath); } else { totalFiles++; } } }; countFiles(directoryPath); const progressBar = new cliProgress.SingleBar({ barCompleteChar: "\u2588", barIncompleteChar: "\u2591", format: "Creating zip [{bar}] {percentage}% | {value}/{total} files | Current: {filename}", hideCursor: true, }, cliProgress.Presets.shades_classic); progressBar.start(totalFiles, 0, { filename: "Initializing...", }); const addFilesToZip = (currentPath, relativePath = "") => { const files = fs.readdirSync(currentPath); for (const file of files) { const filePath = path.join(currentPath, file); const stat = fs.statSync(filePath); const displayPath = path.join(relativePath, file); if (stat.isDirectory()) { addFilesToZip(filePath, displayPath); } else { processedFiles++; progressBar.update(processedFiles, { filename: displayPath.length > 40 ? "..." + displayPath.slice(Math.max(0, displayPath.length - 37)) : displayPath, }); zip.addLocalFile(filePath, relativePath); } } }; try { addFilesToZip(directoryPath); } finally { progressBar.stop(); } const zipPath = `${directoryPath}.zip`; await zip.writeZipPromise(zipPath); const sizeBytes = fs.statSync(zipPath).size; return { sizeBytes, zipPath }; } getIndexJson(directoryPath) { const indexJsonPath = path.join(directoryPath, "index.json"); if (!fs.existsSync(indexJsonPath)) { return "{}"; } return JSON.parse(fs.readFileSync(indexJsonPath, "utf8")); } async uploadArchiveToSignedUrl(input) { const { designSystemId, storybookName, signedUrl, storybookUploadId, zipPath } = input; this.log(`Securely uploading ${zipPath} to Supernova...`); const fileBuffer = fs.readFileSync(zipPath); const fileSize = fileBuffer.byteLength; const fileSizeKB = Number.parseFloat(bytesToKB(fileSize)); const progressBar = new cliProgress.SingleBar({ barCompleteChar: "\u2588", barIncompleteChar: "\u2591", format: "Uploading |{bar}| {percentage}% || {value}/{total} kB", hideCursor: true, }, cliProgress.Presets.shades_classic); progressBar.start(fileSizeKB, 0); try { await axios.put(signedUrl, fileBuffer, { headers: { "Content-Length": fileSize, "Content-Type": "application/zip", designSystemId, storybookUploadId, ...(storybookName && { name: storybookName }), }, onUploadProgress(progressEvent) { const loaded = progressEvent.loaded || 0; const loadedKB = Number.parseFloat(bytesToKB(loaded)); progressBar.update(loadedKB); }, }); progressBar.update(fileSizeKB); progressBar.stop(); } catch (error) { progressBar.stop(); throw error; } } async waitForPublishing(input) { const { apiClient, designSystemId, storybookUploadId } = input; const getStatus = async () => { const { status } = await apiClient.designSystems.storybookHosting.getUploadStatus(designSystemId, storybookUploadId); return status; }; let lastStatus = "Unknown"; let unknownStatusCount = 0; for (let i = 0; unknownStatusCount < 10 && i < 15 * 60; i++) { lastStatus = await getStatus(); if (lastStatus === "Unknown") unknownStatusCount++; else unknownStatusCount = 0; if (lastStatus === "Completed" || lastStatus === "Failed") break; await sleep(1000); } switch (lastStatus) { case "Unknown": return this.error("Storybook deployment initialization has timed out"); case "Failed": return this.error("Storybook deployment has failed"); case "InProgress": return this.error("Storybook deployment has timed out"); } } async importStorybookStories(input) { const { apiClient, brandPersistentId, designSystemId, storybookDirectory, storybookName } = input; let { sourceId } = input; const sourcesEndpoint = apiClient.designSystems.sources; const storybookHostingEndpoint = apiClient.designSystems.storybookHosting; const { accessToken } = await storybookHostingEndpoint.getAccessToken(designSystemId, storybookName); const storybookUrl = storybookEndpoint(this.env, designSystemId, storybookName, accessToken); try { let storiesCount = 0; if (sourceId) { const sourceUpdateResult = await sourcesEndpoint.updateStorybookImport(designSystemId, "head", { payload: this.getIndexJson(storybookDirectory), sourceId, }); storiesCount = sourceUpdateResult.storiesCount; } else { const { source } = await sourcesEndpoint.create(designSystemId, { brandPersistentId, description: "CLI", indexUrl: storybookEndpoint(this.env, designSystemId, storybookName, accessToken, "json"), payload: this.getIndexJson(storybookDirectory), type: "Storybook", userUrl: storybookUrl, fileName: storybookName, }); storiesCount = source.storybook.storiesCount; sourceId = source.id; } this.log(`✅ Imported ${storiesCount} component stories into Supernova!`); const link = terminalLink("here", storybookUrl, { fallback: (_, url) => url }); this.log(`🔒 Access your Storybook ${link}`); return { sourceId }; } catch (error) { this.error(`Failed to connect Storybook as data source: ${error instanceof Error ? error.message : String(error)}`); } } validateStorybookDirectory(directoryPath) { if (!fs.existsSync(directoryPath)) { return { error: `Directory not found: ${directoryPath}`, isValid: false }; } if (!fs.statSync(directoryPath).isDirectory()) { return { error: `Not a directory: ${directoryPath}`, isValid: false }; } const requiredFiles = ["index.html", "iframe.html"]; const missingFiles = requiredFiles.filter(file => !fs.existsSync(path.join(directoryPath, file))); if (missingFiles.length > 0) { return { error: `Directory does not appear to be a valid Storybook export. Missing files: ${missingFiles.join(", ")}`, isValid: false, }; } return { isValid: true }; } validateZipSize(zipPath, sizeBytes) { if (sizeBytes > MAX_ZIP_SIZE_BYTES) { try { fs.unlinkSync(zipPath); } catch { } return { error: `Zip file is too large: ${bytesToMB(sizeBytes)}MB. Maximum allowed size is ${bytesToMB(MAX_ZIP_SIZE_BYTES)}MB`, isValid: false, }; } return { isValid: true }; } async validateDatasource(sourcesEndpoint, sourceId, designSystemId) { if (sourceId) { const storybookDatasource = await sourcesEndpoint.get(designSystemId, sourceId).catch(() => null); if (!storybookDatasource) { this.error("This data source was deleted. Remove supernova.config.json and try again to create a new data source."); } } } } __decorate([ SentryTraced(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], ImportStorybook.prototype, "run", null); //# sourceMappingURL=storybook-import.js.map //# debugId=478a26da-061b-5a2e-a411-cb308a54d3ca