@supernovaio/cli
Version:
Supernova.io Command Line Interface
368 lines (366 loc) • 16.5 kB
JavaScript
!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