diff --git a/node-src/index.test.ts b/node-src/index.test.ts index 6f29249a7..6698b0778 100644 --- a/node-src/index.test.ts +++ b/node-src/index.test.ts @@ -98,16 +98,14 @@ vi.mock('node-fetch', () => ({ } if (query?.match('PublishBuildMutation')) { - publishedBuild = { - id: variables.id, - ...variables.input, - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', - }; + if (variables.input.isolatorUrl.startsWith('http://throw-an-error')) { + throw new Error('fetch error'); + } + publishedBuild = { id: variables.id, ...variables.input }; return { data: { publishBuild: { status: 'PUBLISHED', - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', }, }, }; @@ -134,8 +132,8 @@ vi.mock('node-fetch', () => ({ status: 'IN_PROGRESS', specCount: 1, componentCount: 1, - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', webUrl: 'http://test.com', + cachedUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/iframe.html', ...mockBuildFeatures, app: { account: { @@ -195,8 +193,7 @@ vi.mock('node-fetch', () => ({ }; } - if (query?.match('UploadBuildMutation') || query?.match('UploadMetadataMutation')) { - const key = query?.match('UploadBuildMutation') ? 'uploadBuild' : 'uploadMetadata'; + if (query?.match('GetUploadUrlsMutation')) { const contentTypes = { html: 'text/html', js: 'text/javascript', @@ -205,17 +202,13 @@ vi.mock('node-fetch', () => ({ }; return { data: { - [key]: { - info: { - targets: variables.files.map(({ filePath }) => ({ - contentType: contentTypes[filePath.split('.').at(-1)], - fileKey: '', - filePath, - formAction: 'https://s3.amazonaws.com', - formFields: {}, - })), - }, - userErrors: [], + getUploadUrls: { + domain: 'https://chromatic.com', + urls: variables.paths.map((path: string) => ({ + path, + url: `https://cdn.example.com/${path}`, + contentType: contentTypes[path.split('.').at(-1)], + })), }, }, }; @@ -412,7 +405,7 @@ it('runs in simple situations', async () => { storybookViewLayer: 'viewLayer', committerEmail: 'test@test.com', committerName: 'tester', - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', + isolatorUrl: `https://chromatic.com/iframe.html`, }); }); @@ -469,24 +462,20 @@ it('calls out to npm build script passed and uploads files', async () => { expect.any(Object), [ { + contentHash: 'hash', contentLength: 42, contentType: 'text/html', - fileKey: '', - filePath: 'iframe.html', - formAction: 'https://s3.amazonaws.com', - formFields: {}, localPath: expect.stringMatching(/\/iframe\.html$/), targetPath: 'iframe.html', + targetUrl: 'https://cdn.example.com/iframe.html', }, { + contentHash: 'hash', contentLength: 42, contentType: 'text/html', - fileKey: '', - filePath: 'index.html', - formAction: 'https://s3.amazonaws.com', - formFields: {}, localPath: expect.stringMatching(/\/index\.html$/), targetPath: 'index.html', + targetUrl: 'https://cdn.example.com/index.html', }, ], expect.any(Function) @@ -502,24 +491,20 @@ it('skips building and uploads directly with storybook-build-dir', async () => { expect.any(Object), [ { + contentHash: 'hash', contentLength: 42, contentType: 'text/html', - fileKey: '', - filePath: 'iframe.html', - formAction: 'https://s3.amazonaws.com', - formFields: {}, localPath: expect.stringMatching(/\/iframe\.html$/), targetPath: 'iframe.html', + targetUrl: 'https://cdn.example.com/iframe.html', }, { + contentHash: 'hash', contentLength: 42, contentType: 'text/html', - fileKey: '', - filePath: 'index.html', - formAction: 'https://s3.amazonaws.com', - formFields: {}, localPath: expect.stringMatching(/\/index\.html$/), targetPath: 'index.html', + targetUrl: 'https://cdn.example.com/index.html', }, ], expect.any(Function) @@ -714,44 +699,32 @@ it('should upload metadata files if --upload-metadata is passed', async () => { expect(upload.mock.calls.at(-1)[1]).toEqual( expect.arrayContaining([ { - contentLength: 518, - contentType: 'text/javascript', - fileKey: '', - filePath: '.chromatic/main.js', - formAction: 'https://s3.amazonaws.com', - formFields: {}, localPath: '.storybook/main.js', targetPath: '.chromatic/main.js', + contentLength: 518, + contentType: 'text/javascript', + targetUrl: 'https://cdn.example.com/.chromatic/main.js', }, { - contentLength: 457, - contentType: 'application/json', - fileKey: '', - filePath: '.chromatic/preview-stats.trimmed.json', - formAction: 'https://s3.amazonaws.com', - formFields: {}, localPath: 'storybook-out/preview-stats.trimmed.json', targetPath: '.chromatic/preview-stats.trimmed.json', + contentLength: 457, + contentType: 'application/json', + targetUrl: 'https://cdn.example.com/.chromatic/preview-stats.trimmed.json', }, { - contentLength: 1338, - contentType: 'text/javascript', - fileKey: '', - filePath: '.chromatic/preview.js', - formAction: 'https://s3.amazonaws.com', - formFields: {}, localPath: '.storybook/preview.js', targetPath: '.chromatic/preview.js', + contentLength: 1338, + contentType: 'text/javascript', + targetUrl: 'https://cdn.example.com/.chromatic/preview.js', }, { - contentLength: expect.any(Number), - contentType: 'text/html', - fileKey: '', - filePath: '.chromatic/index.html', - formAction: 'https://s3.amazonaws.com', - formFields: {}, localPath: expect.any(String), targetPath: '.chromatic/index.html', + contentLength: expect.any(Number), + contentType: 'text/html', + targetUrl: 'https://cdn.example.com/.chromatic/index.html', }, ]) ); diff --git a/node-src/index.ts b/node-src/index.ts index 8b7e49193..86a24359f 100644 --- a/node-src/index.ts +++ b/node-src/index.ts @@ -120,7 +120,7 @@ export async function run({ code: ctx.exitCode, url: ctx.build?.webUrl, buildUrl: ctx.build?.webUrl, - storybookUrl: ctx.build?.storybookUrl, + storybookUrl: ctx.build?.cachedUrl?.replace(/iframe\.html.*$/, ''), specCount: ctx.build?.specCount, componentCount: ctx.build?.componentCount, testCount: ctx.build?.testCount, diff --git a/node-src/lib/compress.ts b/node-src/lib/compress.ts index 312560034..b19d00d5c 100644 --- a/node-src/lib/compress.ts +++ b/node-src/lib/compress.ts @@ -24,7 +24,7 @@ export default async function makeZipFile(ctx: Context, files: FileDesc[]) { archive.pipe(sink); files.forEach(({ localPath, targetPath: name }) => { - ctx.log.debug(`Adding to zip archive: ${name}`); + ctx.log.debug({ name }, 'Adding file to zip archive'); archive.append(createReadStream(localPath), { name }); }); diff --git a/node-src/lib/upload.ts b/node-src/lib/upload.ts index f5ad83571..c7e679e61 100644 --- a/node-src/lib/upload.ts +++ b/node-src/lib/upload.ts @@ -1,199 +1,112 @@ import makeZipFile from './compress'; -import { Context, FileDesc, TargetInfo } from '../types'; +import { Context, FileDesc, TargetedFile } from '../types'; import { uploadZip, waitForUnpack } from './uploadZip'; import { uploadFiles } from './uploadFiles'; -import { maxFileCountExceeded } from '../ui/messages/errors/maxFileCountExceeded'; -import { maxFileSizeExceeded } from '../ui/messages/errors/maxFileSizeExceeded'; -const UploadBuildMutation = ` - mutation UploadBuildMutation($buildId: ObjID!, $files: [FileUploadInput!]!, $zip: Boolean) { - uploadBuild(buildId: $buildId, files: $files, zip: $zip) { - info { - targets { - contentType - fileKey - filePath - formAction - formFields - } - zipTarget { - contentType - fileKey - filePath - formAction - formFields - sentinelUrl - } - } - userErrors { - __typename - ... on UserError { - message - } - ... on MaxFileCountExceededError { - maxFileCount - fileCount - } - ... on MaxFileSizeExceededError { - maxFileSize - filePaths - } +const GetUploadUrlsMutation = ` + mutation GetUploadUrlsMutation($buildId: ObjID, $paths: [String!]!) { + getUploadUrls(buildId: $buildId, paths: $paths) { + domain + urls { + path + url + contentType } } } `; +interface GetUploadUrlsMutationResult { + getUploadUrls: { + domain: string; + urls: { + path: string; + url: string; + contentType: string; + }[]; + }; +} -interface UploadBuildMutationResult { - uploadBuild: { - info?: { - targets: TargetInfo[]; - zipTarget?: TargetInfo & { sentinelUrl: string }; - }; - userErrors: ( - | { - __typename: 'UserError'; - message: string; - } - | { - __typename: 'MaxFileCountExceededError'; - message: string; - maxFileCount: number; - fileCount: number; - } - | { - __typename: 'MaxFileSizeExceededError'; - message: string; - maxFileSize: number; - filePaths: string[]; - } - )[]; +const GetZipUploadUrlMutation = ` + mutation GetZipUploadUrlMutation($buildId: ObjID) { + getZipUploadUrl(buildId: $buildId) { + domain + url + sentinelUrl + } + } +`; +interface GetZipUploadUrlMutationResult { + getZipUploadUrl: { + domain: string; + url: string; + sentinelUrl: string; }; } -export async function uploadBuild( +export async function uploadAsIndividualFiles( ctx: Context, files: FileDesc[], options: { onStart?: () => void; onProgress?: (progress: number, total: number) => void; - onComplete?: (uploadedBytes: number, uploadedFiles: number) => void; + onComplete?: (uploadedBytes: number, domain?: string) => void; onError?: (error: Error, path?: string) => void; } = {} ) { - const { uploadBuild } = await ctx.client.runQuery( - UploadBuildMutation, - { - buildId: ctx.announcedBuild.id, - files: files.map(({ contentLength, targetPath }) => ({ - contentLength, - filePath: targetPath, - })), - zip: ctx.options.zip, - } + const { getUploadUrls } = await ctx.client.runQuery( + GetUploadUrlsMutation, + { buildId: ctx.announcedBuild.id, paths: files.map(({ targetPath }) => targetPath) } ); - - if (uploadBuild.userErrors.length) { - uploadBuild.userErrors.forEach((e) => { - if (e.__typename === 'MaxFileCountExceededError') { - ctx.log.error(maxFileCountExceeded(e)); - } else if (e.__typename === 'MaxFileSizeExceededError') { - ctx.log.error(maxFileSizeExceeded(e)); - } else { - ctx.log.error(e.message); - } - }); - return options.onError?.(new Error('Upload rejected due to user error')); - } - - const targets = uploadBuild.info.targets.map((target) => { - const file = files.find((f) => f.targetPath === target.filePath); - return { ...file, ...target }; + const { domain, urls } = getUploadUrls; + const targets = urls.map(({ path, url, contentType }) => { + const file = files.find((f) => f.targetPath === path); + return { ...file, contentType, targetUrl: url }; }); - - if (!targets.length) { - ctx.log.debug('No new files to upload, continuing'); - return options.onComplete?.(0, 0); - } - - options.onStart?.(); - const total = targets.reduce((acc, { contentLength }) => acc + contentLength, 0); - if (uploadBuild.info.zipTarget) { - try { - const { path, size } = await makeZipFile(ctx, targets); - const compressionRate = (total - size) / total; - ctx.log.debug(`Compression reduced upload size by ${Math.round(compressionRate * 100)}%`); - const target = { ...uploadBuild.info.zipTarget, contentLength: size, localPath: path }; - await uploadZip(ctx, target, (progress) => options.onProgress?.(progress, size)); - await waitForUnpack(ctx, target.sentinelUrl); - return options.onComplete?.(size, targets.length); - } catch (err) { - ctx.log.debug({ err }, 'Error uploading zip, falling back to uploading individual files'); - } - } + options.onStart?.(); try { await uploadFiles(ctx, targets, (progress) => options.onProgress?.(progress, total)); - return options.onComplete?.(total, targets.length); } catch (e) { return options.onError?.(e, files.some((f) => f.localPath === e.message) && e.message); } + + options.onComplete?.(total, domain); } -const UploadMetadataMutation = ` - mutation UploadMetadataMutation($buildId: ObjID!, $files: [FileUploadInput!]!) { - uploadMetadata(buildId: $buildId, files: $files) { - info { - targets { - contentType - fileKey - filePath - formAction - formFields - } - } - userErrors { - ... on UserError { - message - } - } - } - } -`; +export async function uploadAsZipFile( + ctx: Context, + files: FileDesc[], + options: { + onStart?: () => void; + onProgress?: (progress: number, total: number) => void; + onComplete?: (uploadedBytes: number, domain?: string) => void; + onError?: (error: Error, path?: string) => void; + } = {} +) { + const originalSize = files.reduce((acc, { contentLength }) => acc + contentLength, 0); + const zipped = await makeZipFile(ctx, files); + const { path, size } = zipped; -interface UploadMetadataMutationResult { - uploadMetadata: { - info?: { - targets: TargetInfo[]; - }; - userErrors: { - message: string; - }[]; - }; -} + if (size > originalSize) throw new Error('Zip file is larger than individual files'); + ctx.log.debug(`Compression reduced upload size by ${originalSize - size} bytes`); -export async function uploadMetadata(ctx: Context, files: FileDesc[]) { - const { uploadMetadata } = await ctx.client.runQuery( - UploadMetadataMutation, - { - buildId: ctx.announcedBuild.id, - files: files.map(({ contentLength, targetPath }) => ({ - contentLength, - filePath: targetPath, - })), - } + const { getZipUploadUrl } = await ctx.client.runQuery( + GetZipUploadUrlMutation, + { buildId: ctx.announcedBuild.id } ); + const { domain, url, sentinelUrl } = getZipUploadUrl; - if (uploadMetadata.info) { - const targets = uploadMetadata.info.targets.map((target) => { - const file = files.find((f) => f.targetPath === target.filePath); - return { ...file, ...target }; - }); - await uploadFiles(ctx, targets); - } + options.onStart?.(); - if (uploadMetadata.userErrors.length) { - uploadMetadata.userErrors.forEach((e) => ctx.log.warn(e.message)); + try { + await uploadZip(ctx, path, url, size, (progress) => options.onProgress?.(progress, size)); + } catch (e) { + return options.onError?.(e, path); } + + await waitForUnpack(ctx, sentinelUrl); + + options.onComplete?.(size, domain); } diff --git a/node-src/lib/uploadFiles.ts b/node-src/lib/uploadFiles.ts index b0e41e465..24dc4ddd8 100644 --- a/node-src/lib/uploadFiles.ts +++ b/node-src/lib/uploadFiles.ts @@ -1,25 +1,25 @@ import retry from 'async-retry'; -import { filesize } from 'filesize'; -import FormData from 'form-data'; import { createReadStream } from 'fs'; import pLimit from 'p-limit'; import progress from 'progress-stream'; -import { Context, FileDesc, TargetInfo } from '../types'; +import { Context, TargetedFile } from '../types'; export async function uploadFiles( ctx: Context, - targets: (FileDesc & TargetInfo)[], - onProgress?: (progress: number) => void + files: TargetedFile[], + onProgress: (progress: number) => void ) { const { experimental_abortSignal: signal } = ctx.options; const limitConcurrency = pLimit(10); let totalProgress = 0; await Promise.all( - targets.map(({ contentLength, filePath, formAction, formFields, localPath }) => { + files.map(({ localPath, targetUrl, contentType, contentLength }) => { let fileProgress = 0; // The bytes uploaded for this this particular file - ctx.log.debug(`Uploading ${filePath} (${filesize(contentLength)})`); + ctx.log.debug( + `Uploading ${contentLength} bytes of ${contentType} for '${localPath}' to '${targetUrl}'` + ); return limitConcurrency(() => retry( @@ -33,34 +33,37 @@ export async function uploadFiles( progressStream.on('progress', ({ delta }) => { fileProgress += delta; // We upload multiple files so we only care about the delta totalProgress += delta; - onProgress?.(totalProgress); - }); - - const formData = new FormData(); - Object.entries(formFields).forEach(([k, v]) => formData.append(k, v)); - formData.append('file', createReadStream(localPath).pipe(progressStream), { - knownLength: contentLength, + onProgress(totalProgress); }); const res = await ctx.http.fetch( - formAction, - { body: formData, method: 'POST', signal }, + targetUrl, + { + method: 'PUT', + body: createReadStream(localPath).pipe(progressStream), + headers: { + 'content-type': contentType, + 'content-length': contentLength.toString(), + 'cache-control': 'max-age=31536000', + }, + signal, + }, { retries: 0 } // already retrying the whole operation ); if (!res.ok) { - ctx.log.debug(`Uploading ${localPath} failed: %O`, res); + ctx.log.debug(`Uploading '${localPath}' failed: %O`, res); throw new Error(localPath); } - ctx.log.debug(`Uploaded ${filePath} (${filesize(contentLength)})`); + ctx.log.debug(`Uploaded '${localPath}'.`); }, { retries: ctx.env.CHROMATIC_RETRIES, onRetry: (err: Error) => { totalProgress -= fileProgress; fileProgress = 0; - ctx.log.debug('Retrying upload for %s, %O', localPath, err); - onProgress?.(totalProgress); + ctx.log.debug('Retrying upload %s, %O', targetUrl, err); + onProgress(totalProgress); }, } ) diff --git a/node-src/lib/uploadMetadataFiles.ts b/node-src/lib/uploadMetadataFiles.ts index bbca940b3..a2ba5876a 100644 --- a/node-src/lib/uploadMetadataFiles.ts +++ b/node-src/lib/uploadMetadataFiles.ts @@ -7,7 +7,8 @@ import { Context, FileDesc } from '../types'; import metadataHtml from '../ui/html/metadata.html'; import uploadingMetadata from '../ui/messages/info/uploadingMetadata'; import { findStorybookConfigFile } from './getStorybookMetadata'; -import { uploadMetadata } from './upload'; +import { uploadAsIndividualFiles } from './upload'; +import { baseStorybookUrl } from './utils'; const fileSize = (path: string): Promise => new Promise((resolve) => stat(path, (err, stats) => resolve(err ? 0 : stats.size))); @@ -29,9 +30,9 @@ export async function uploadMetadataFiles(ctx: Context) { const files = await Promise.all( metadataFiles.map(async (localPath) => { - const contentLength = await fileSize(localPath); const targetPath = `.chromatic/${basename(localPath)}`; - return contentLength && { contentLength, localPath, targetPath }; + const contentLength = await fileSize(localPath); + return contentLength && { localPath, targetPath, contentLength }; }) ).then((files) => files @@ -48,14 +49,14 @@ export async function uploadMetadataFiles(ctx: Context) { const html = metadataHtml(ctx, files); writeFileSync(path, html); files.push({ - contentLength: html.length, localPath: path, targetPath: '.chromatic/index.html', + contentLength: html.length, }); - const directoryUrl = `${ctx.build.storybookUrl}.chromatic/`; + const directoryUrl = `${baseStorybookUrl(ctx.isolatorUrl)}/.chromatic/`; ctx.log.info(uploadingMetadata(directoryUrl, files)); - await uploadMetadata(ctx, files); + await uploadAsIndividualFiles(ctx, files); }); } diff --git a/node-src/lib/uploadZip.ts b/node-src/lib/uploadZip.ts index 9aff4b712..068a828d7 100644 --- a/node-src/lib/uploadZip.ts +++ b/node-src/lib/uploadZip.ts @@ -1,10 +1,8 @@ import retry from 'async-retry'; -import { filesize } from 'filesize'; -import FormData from 'form-data'; import { createReadStream } from 'fs'; import { Response } from 'node-fetch'; import progress from 'progress-stream'; -import { Context, TargetInfo } from '../types'; +import { Context } from '../types'; // A sentinel file is created by a zip-unpack lambda within the Chromatic infrastructure once the // uploaded zip is fully extracted. The contents of this file will consist of 'OK' if the process @@ -13,14 +11,15 @@ const SENTINEL_SUCCESS_VALUE = 'OK'; export async function uploadZip( ctx: Context, - target: TargetInfo & { contentLength: number; localPath: string; sentinelUrl: string }, + path: string, + url: string, + contentLength: number, onProgress: (progress: number) => void ) { const { experimental_abortSignal: signal } = ctx.options; - const { contentLength, filePath, formAction, formFields, localPath } = target; let totalProgress = 0; - ctx.log.debug(`Uploading ${filePath} (${filesize(contentLength)})`); + ctx.log.debug(`Uploading ${contentLength} bytes for '${path}' to '${url}'`); return retry( async (bail) => { @@ -35,29 +34,31 @@ export async function uploadZip( onProgress(totalProgress); }); - const formData = new FormData(); - Object.entries(formFields).forEach(([k, v]) => formData.append(k, v)); - formData.append('file', createReadStream(localPath).pipe(progressStream), { - knownLength: contentLength, - }); - const res = await ctx.http.fetch( - formAction, - { body: formData, method: 'POST', signal }, + url, + { + method: 'PUT', + body: createReadStream(path).pipe(progressStream), + headers: { + 'content-type': 'application/zip', + 'content-length': contentLength.toString(), + }, + signal, + }, { retries: 0 } // already retrying the whole operation ); if (!res.ok) { - ctx.log.debug(`Uploading ${localPath} failed: %O`, res); - throw new Error(localPath); + ctx.log.debug(`Uploading '${path}' failed: %O`, res); + throw new Error(path); } - ctx.log.debug(`Uploaded ${filePath} (${filesize(contentLength)})`); + ctx.log.debug(`Uploaded '${path}'.`); }, { retries: ctx.env.CHROMATIC_RETRIES, onRetry: (err: Error) => { totalProgress = 0; - ctx.log.debug('Retrying upload for %s, %O', localPath, err); + ctx.log.debug('Retrying upload %s, %O', url, err); onProgress(totalProgress); }, } diff --git a/node-src/tasks/report.ts b/node-src/tasks/report.ts index c36b9d453..299e5329f 100644 --- a/node-src/tasks/report.ts +++ b/node-src/tasks/report.ts @@ -2,6 +2,7 @@ import reportBuilder from 'junit-report-builder'; import path from 'path'; import { createTask, transitionTo } from '../lib/tasks'; +import { baseStorybookUrl } from '../lib/utils'; import { Context } from '../types'; import wroteReport from '../ui/messages/info/wroteReport'; import { initial, pending, success } from '../ui/tasks/report'; @@ -12,8 +13,8 @@ const ReportQuery = ` build(number: $buildNumber) { number status(legacy: false) - storybookUrl webUrl + cachedUrl createdAt completedAt tests { @@ -43,8 +44,8 @@ interface ReportQueryResult { build: { number: number; status: string; - storybookUrl: string; webUrl: string; + cachedUrl: string; createdAt: number; completedAt: number; tests: { @@ -93,7 +94,7 @@ export const generateReport = async (ctx: Context) => { .property('buildNumber', build.number) .property('buildStatus', build.status) .property('buildUrl', build.webUrl) - .property('storybookUrl', build.storybookUrl); + .property('storybookUrl', baseStorybookUrl(build.cachedUrl)); build.tests.forEach(({ status, result, spec, parameters, mode }) => { const testSuffixName = mode.name || `[${parameters.viewport}px]`; diff --git a/node-src/tasks/storybookInfo.test.ts b/node-src/tasks/storybookInfo.test.ts index b1bc59a14..0f00a09b2 100644 --- a/node-src/tasks/storybookInfo.test.ts +++ b/node-src/tasks/storybookInfo.test.ts @@ -7,8 +7,8 @@ vi.mock('../lib/getStorybookInfo'); const getStorybookInfo = vi.mocked(storybookInfo); -describe('storybookInfo', () => { - it('retrieves Storybook metadata and sets it on context', async () => { +describe('startStorybook', () => { + it('starts the app and sets the isolatorUrl on context', async () => { const storybook = { version: '1.0.0', viewLayer: 'react', addons: [] }; getStorybookInfo.mockResolvedValue(storybook); diff --git a/node-src/tasks/upload.test.ts b/node-src/tasks/upload.test.ts index 3bfb5817a..8835964ea 100644 --- a/node-src/tasks/upload.test.ts +++ b/node-src/tasks/upload.test.ts @@ -1,7 +1,6 @@ -import FormData from 'form-data'; import { createReadStream, readdirSync, readFileSync, statSync } from 'fs'; import progressStream from 'progress-stream'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { default as compress } from '../lib/compress'; import { getDependentStoryFiles as getDepStoryFiles } from '../lib/getDependentStoryFiles'; @@ -9,7 +8,6 @@ import { findChangedDependencies as findChangedDep } from '../lib/findChangedDep import { findChangedPackageFiles as findChangedPkg } from '../lib/findChangedPackageFiles'; import { calculateFileHashes, validateFiles, traceChangedFiles, uploadStorybook } from './upload'; -vi.mock('form-data'); vi.mock('fs'); vi.mock('progress-stream'); vi.mock('../lib/compress'); @@ -37,10 +35,6 @@ const env = { CHROMATIC_RETRIES: 2, CHROMATIC_OUTPUT_INTERVAL: 0 }; const log = { info: vi.fn(), warn: vi.fn(), debug: vi.fn() }; const http = { fetch: vi.fn() }; -afterEach(() => { - vi.restoreAllMocks(); -}); - describe('validateFiles', () => { it('sets fileInfo on context', async () => { readdirSyncMock.mockReturnValue(['iframe.html', 'index.html'] as any); @@ -260,27 +254,23 @@ describe('calculateFileHashes', () => { }); describe('uploadStorybook', () => { - it('retrieves the upload locations and uploads the files', async () => { + it('retrieves the upload locations, puts the files there and sets the isolatorUrl on context', async () => { const client = { runQuery: vi.fn() }; client.runQuery.mockReturnValue({ - uploadBuild: { - info: { - targets: [ - { - contentType: 'text/html', - filePath: 'iframe.html', - formAction: 'https://s3.amazonaws.com/presigned?iframe.html', - formFields: {}, - }, - { - contentType: 'text/html', - filePath: 'index.html', - formAction: 'https://s3.amazonaws.com/presigned?index.html', - formFields: {}, - }, - ], - }, - userErrors: [], + getUploadUrls: { + domain: 'https://asdqwe.chromatic.com', + urls: [ + { + path: 'iframe.html', + url: 'https://asdqwe.chromatic.com/iframe.html', + contentType: 'text/html', + }, + { + path: 'index.html', + url: 'https://asdqwe.chromatic.com/index.html', + contentType: 'text/html', + }, + ], }, }); @@ -308,48 +298,55 @@ describe('uploadStorybook', () => { } as any; await uploadStorybook(ctx, {} as any); - expect(client.runQuery).toHaveBeenCalledWith(expect.stringMatching(/UploadBuildMutation/), { + expect(client.runQuery).toHaveBeenCalledWith(expect.stringMatching(/GetUploadUrlsMutation/), { buildId: '1', - files: [ - { contentLength: 42, filePath: 'iframe.html' }, - { contentLength: 42, filePath: 'index.html' }, - ], + paths: ['iframe.html', 'index.html'], }); expect(http.fetch).toHaveBeenCalledWith( - 'https://s3.amazonaws.com/presigned?iframe.html', - expect.objectContaining({ body: expect.any(FormData), method: 'POST' }), + 'https://asdqwe.chromatic.com/iframe.html', + expect.objectContaining({ + method: 'PUT', + headers: { + 'content-type': 'text/html', + 'content-length': '42', + 'cache-control': 'max-age=31536000', + }, + }), expect.objectContaining({ retries: 0 }) ); expect(http.fetch).toHaveBeenCalledWith( - 'https://s3.amazonaws.com/presigned?index.html', - expect.objectContaining({ body: expect.any(FormData), method: 'POST' }), + 'https://asdqwe.chromatic.com/index.html', + expect.objectContaining({ + method: 'PUT', + headers: { + 'content-type': 'text/html', + 'content-length': '42', + 'cache-control': 'max-age=31536000', + }, + }), expect.objectContaining({ retries: 0 }) ); expect(ctx.uploadedBytes).toBe(84); - expect(ctx.uploadedFiles).toBe(2); + expect(ctx.isolatorUrl).toBe('https://asdqwe.chromatic.com/iframe.html'); }); - it.skip('calls experimental_onTaskProgress with progress', async () => { + it('calls experimental_onTaskProgress with progress', async () => { const client = { runQuery: vi.fn() }; client.runQuery.mockReturnValue({ - uploadBuild: { - info: { - targets: [ - { - contentType: 'text/html', - filePath: 'iframe.html', - formAction: 'https://s3.amazonaws.com/presigned?iframe.html', - formFields: {}, - }, - { - contentType: 'text/html', - filePath: 'index.html', - formAction: 'https://s3.amazonaws.com/presigned?index.html', - formFields: {}, - }, - ], - }, - userErrors: [], + getUploadUrls: { + domain: 'https://asdqwe.chromatic.com', + urls: [ + { + path: 'iframe.html', + url: 'https://asdqwe.chromatic.com/iframe.html', + contentType: 'text/html', + }, + { + path: 'index.html', + url: 'https://asdqwe.chromatic.com/index.html', + contentType: 'text/html', + }, + ], }, }); @@ -364,8 +361,9 @@ describe('uploadStorybook', () => { }; }) as any); http.fetch.mockReset().mockImplementation(async (url, { body }) => { - // How to update progress? - console.log(body); + // body is just the mocked progress stream, as pipe returns it + body.sendProgress(21); + body.sendProgress(21); return { ok: true }; }); @@ -416,31 +414,10 @@ describe('uploadStorybook', () => { it('retrieves the upload location, adds the files to an archive and uploads it', async () => { const client = { runQuery: vi.fn() }; client.runQuery.mockReturnValue({ - uploadBuild: { - info: { - targets: [ - { - contentType: 'text/html', - filePath: 'iframe.html', - formAction: 'https://s3.amazonaws.com/presigned?iframe.html', - formFields: {}, - }, - { - contentType: 'text/html', - filePath: 'index.html', - formAction: 'https://s3.amazonaws.com/presigned?index.html', - formFields: {}, - }, - ], - zipTarget: { - contentType: 'application/zip', - filePath: 'storybook.zip', - formAction: 'https://s3.amazonaws.com/presigned?storybook.zip', - formFields: {}, - sentinelUrl: 'https://asdqwe.chromatic.com/sentinel.txt', - }, - }, - userErrors: [], + getZipUploadUrl: { + domain: 'https://asdqwe.chromatic.com', + url: 'https://asdqwe.chromatic.com/storybook.zip', + sentinelUrl: 'https://asdqwe.chromatic.com/upload.txt', }, }); @@ -469,31 +446,22 @@ describe('uploadStorybook', () => { } as any; await uploadStorybook(ctx, {} as any); - expect(client.runQuery).toHaveBeenCalledWith(expect.stringMatching(/UploadBuildMutation/), { - buildId: '1', - files: [ - { contentLength: 42, filePath: 'iframe.html' }, - { contentLength: 42, filePath: 'index.html' }, - ], - zip: true, - }); + expect(client.runQuery).toHaveBeenCalledWith( + expect.stringMatching(/GetZipUploadUrlMutation/), + { buildId: '1' } + ); expect(http.fetch).toHaveBeenCalledWith( - 'https://s3.amazonaws.com/presigned?storybook.zip', - expect.objectContaining({ body: expect.any(FormData), method: 'POST' }), + 'https://asdqwe.chromatic.com/storybook.zip', + expect.objectContaining({ + method: 'PUT', + headers: { + 'content-type': 'application/zip', + 'content-length': '80', + }, + }), expect.objectContaining({ retries: 0 }) ); - expect(http.fetch).not.toHaveBeenCalledWith( - 'https://s3.amazonaws.com/presigned?iframe.html', - expect.anything(), - expect.anything() - ); - expect(http.fetch).not.toHaveBeenCalledWith( - 'https://s3.amazonaws.com/presigned?iframe.html', - expect.anything(), - expect.anything() - ); expect(ctx.uploadedBytes).toBe(80); - expect(ctx.uploadedFiles).toBe(2); }); }); }); diff --git a/node-src/tasks/upload.ts b/node-src/tasks/upload.ts index 93903c3be..565b4877a 100644 --- a/node-src/tasks/upload.ts +++ b/node-src/tasks/upload.ts @@ -27,7 +27,7 @@ import { readStatsFile } from './read-stats-file'; import bailFile from '../ui/messages/warnings/bailFile'; import { findChangedPackageFiles } from '../lib/findChangedPackageFiles'; import { findChangedDependencies } from '../lib/findChangedDependencies'; -import { uploadBuild } from '../lib/upload'; +import { uploadAsIndividualFiles, uploadAsZipFile } from '../lib/upload'; import { getFileHashes } from '../lib/getFileHashes'; interface PathSpec { @@ -204,15 +204,7 @@ export const uploadStorybook = async (ctx: Context, task: Task) => { if (ctx.skip) return; transitionTo(preparing)(ctx, task); - const files = ctx.fileInfo.paths - .map((path) => ({ - contentLength: ctx.fileInfo.lengths.find(({ knownAs }) => knownAs === path).contentLength, - localPath: join(ctx.sourceDir, path), - targetPath: path, - })) - .filter((f) => f.contentLength); - - await uploadBuild(ctx, files, { + const options = { onStart: () => (task.output = starting().output), onProgress: throttle( (progress, total) => { @@ -224,14 +216,35 @@ export const uploadStorybook = async (ctx: Context, task: Task) => { // Avoid spamming the logs with progress updates in non-interactive mode ctx.options.interactive ? 100 : ctx.env.CHROMATIC_OUTPUT_INTERVAL ), - onComplete: (uploadedBytes: number, uploadedFiles: number) => { + onComplete: (uploadedBytes: number, domain: string) => { ctx.uploadedBytes = uploadedBytes; - ctx.uploadedFiles = uploadedFiles; + ctx.isolatorUrl = new URL('/iframe.html', domain).toString(); }, onError: (error: Error, path?: string) => { throw path === error.message ? new Error(failed({ path }).output) : error; }, - }); + }; + + const files = ctx.fileInfo.paths.map((path) => ({ + localPath: join(ctx.sourceDir, path), + targetPath: path, + contentLength: ctx.fileInfo.lengths.find(({ knownAs }) => knownAs === path).contentLength, + ...(ctx.fileInfo.hashes && { contentHash: ctx.fileInfo.hashes[path] }), + })); + + if (ctx.options.zip) { + try { + await uploadAsZipFile(ctx, files, options); + } catch (err) { + ctx.log.debug( + { err }, + 'Error uploading zip file, falling back to uploading individual files' + ); + await uploadAsIndividualFiles(ctx, files, options); + } + } else { + await uploadAsIndividualFiles(ctx, files, options); + } }; export default createTask({ diff --git a/node-src/tasks/verify.test.ts b/node-src/tasks/verify.test.ts index fc0c34872..2e2f9c3f9 100644 --- a/node-src/tasks/verify.test.ts +++ b/node-src/tasks/verify.test.ts @@ -29,7 +29,13 @@ describe('publishBuild', () => { expect(client.runQuery).toHaveBeenCalledWith( expect.stringMatching(/PublishBuildMutation/), - { input: { turboSnapStatus: 'UNUSED' } }, + { + input: { + cachedUrl: ctx.cachedUrl, + isolatorUrl: ctx.isolatorUrl, + turboSnapStatus: 'UNUSED', + }, + }, { headers: { Authorization: `Bearer report-token` }, retries: 3 } ); expect(ctx.announcedBuild).toEqual({ ...announcedBuild, ...publishedBuild }); @@ -45,6 +51,7 @@ describe('verifyBuild', () => { git: { version: 'whatever', matchesBranch: () => false }, pkg: { version: '1.0.0' }, storybook: { version: '2.0.0', viewLayer: 'react', addons: [] }, + isolatorUrl: 'https://tunnel.chromaticqa.com/', announcedBuild: { number: 1, reportToken: 'report-token' }, }; @@ -102,7 +109,7 @@ describe('verifyBuild', () => { build: { number: 1, status: 'IN_PROGRESS', - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', + cachedUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/iframe.html', features: { uiTests: true, uiReview: false }, app: { account: {} }, wasLimited: true, @@ -123,7 +130,7 @@ describe('verifyBuild', () => { build: { number: 1, status: 'IN_PROGRESS', - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', + cachedUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/iframe.html', features: { uiTests: true, uiReview: false }, app: { account: { exceededThreshold: true } }, wasLimited: true, @@ -144,7 +151,7 @@ describe('verifyBuild', () => { build: { number: 1, status: 'IN_PROGRESS', - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', + cachedUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/iframe.html', features: { uiTests: true, uiReview: false }, app: { account: { paymentRequired: true } }, wasLimited: true, @@ -165,7 +172,7 @@ describe('verifyBuild', () => { build: { number: 1, status: 'IN_PROGRESS', - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', + cachedUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/iframe.html', features: { uiTests: false, uiReview: false }, app: { account: { paymentRequired: true } }, wasLimited: true, diff --git a/node-src/tasks/verify.ts b/node-src/tasks/verify.ts index 4bc4f4111..f52581713 100644 --- a/node-src/tasks/verify.ts +++ b/node-src/tasks/verify.ts @@ -26,19 +26,15 @@ const PublishBuildMutation = ` publishBuild(id: $id, input: $input) { # no need for legacy:false on PublishedBuild.status status - storybookUrl } } `; interface PublishBuildMutationResult { - publishBuild: { - status: string; - storybookUrl: string; - }; + publishBuild: Context['announcedBuild']; } export const publishBuild = async (ctx: Context) => { - const { turboSnap } = ctx; + const { cachedUrl, isolatorUrl, turboSnap } = ctx; const { id, reportToken } = ctx.announcedBuild; const { replacementBuildIds } = ctx.git; const { onlyStoryNames, onlyStoryFiles = ctx.onlyStoryFiles } = ctx.options; @@ -55,6 +51,8 @@ export const publishBuild = async (ctx: Context) => { { id, input: { + cachedUrl, + isolatorUrl, ...(onlyStoryFiles && { onlyStoryFiles }), ...(onlyStoryNames && { onlyStoryNames: [].concat(onlyStoryNames) }), ...(replacementBuildIds && { replacementBuildIds }), @@ -68,15 +66,12 @@ export const publishBuild = async (ctx: Context) => { ); ctx.announcedBuild = { ...ctx.announcedBuild, ...publishedBuild }; - ctx.storybookUrl = publishedBuild.storybookUrl; // Queueing the extract may have failed if (publishedBuild.status === 'FAILED') { setExitCode(ctx, exitCodes.BUILD_FAILED, false); throw new Error(publishFailed().output); } - - ctx.log.info(storybookPublished(ctx)); }; const StartedBuildQuery = ` @@ -116,6 +111,7 @@ const VerifyBuildQuery = ` inheritedCaptureCount interactionTestFailuresCount webUrl + cachedUrl browsers { browser } @@ -165,7 +161,7 @@ interface VerifyBuildQueryResult { } export const verifyBuild = async (ctx: Context, task: Task) => { - const { client } = ctx; + const { client, isolatorUrl } = ctx; const { list, onlyStoryNames, onlyStoryFiles = ctx.onlyStoryFiles } = ctx.options; const { matchesBranch } = ctx.git; @@ -179,7 +175,6 @@ export const verifyBuild = async (ctx: Context, task: Task) => { } const waitForBuildToStart = async () => { - const { storybookUrl } = ctx; const { number, reportToken } = ctx.announcedBuild; const variables = { number }; const options = { headers: { Authorization: `Bearer ${reportToken}` } }; @@ -188,7 +183,7 @@ export const verifyBuild = async (ctx: Context, task: Task) => { app: { build }, } = await client.runQuery(StartedBuildQuery, variables, options); if (build.failureReason) { - ctx.log.warn(brokenStorybook({ ...build, storybookUrl })); + ctx.log.warn(brokenStorybook({ ...build, isolatorUrl })); setExitCode(ctx, exitCodes.STORYBOOK_BROKEN, true); throw new Error(publishFailed().output); } diff --git a/node-src/types.ts b/node-src/types.ts index 5014f3205..41de7720e 100644 --- a/node-src/types.ts +++ b/node-src/types.ts @@ -223,7 +223,8 @@ export interface Context { }; mainConfigFilePath?: string; }; - storybookUrl?: string; + isolatorUrl: string; + cachedUrl: string; announcedBuild: { id: string; number: number; @@ -240,7 +241,7 @@ export interface Context { number: number; status: string; webUrl: string; - storybookUrl: string; + cachedUrl: string; reportToken?: string; inheritedCaptureCount: number; actualCaptureCount: number; @@ -293,7 +294,7 @@ export interface Context { buildLogFile?: string; fileInfo?: { paths: string[]; - hashes?: Record; + hashes: Record; statsPath: string; lengths: { knownAs: string; @@ -303,7 +304,6 @@ export interface Context { total: number; }; uploadedBytes?: number; - uploadedFiles?: number; turboSnap?: Partial<{ unavailable?: boolean; rootPath: string; @@ -354,15 +354,13 @@ export interface Stats { } export interface FileDesc { + contentHash?: string; contentLength: number; localPath: string; targetPath: string; } -export interface TargetInfo { +export interface TargetedFile extends FileDesc { contentType: string; - fileKey: string; - filePath: string; - formAction: string; - formFields: { [key: string]: string }; + targetUrl: string; } diff --git a/node-src/ui/messages/errors/brokenStorybook.stories.ts b/node-src/ui/messages/errors/brokenStorybook.stories.ts index 44ccf67a1..bfd65fa5c 100644 --- a/node-src/ui/messages/errors/brokenStorybook.stories.ts +++ b/node-src/ui/messages/errors/brokenStorybook.stories.ts @@ -15,6 +15,6 @@ ReferenceError: foo is not defined at https://61b0a4b8ebf0e344c2aa231c-nsoaxcirhi.capture.dev-chromatic.com/main.72ad6d7a.iframe.bundle.js:1:47 `; -const storybookUrl = 'https://61b0a4b8ebf0e344c2aa231c-wdooytetbw.dev-chromatic.com/'; +const isolatorUrl = 'https://61b0a4b8ebf0e344c2aa231c-wdooytetbw.dev-chromatic.com'; -export const BrokenStorybook = () => brokenStorybook({ failureReason, storybookUrl }); +export const BrokenStorybook = () => brokenStorybook({ failureReason, isolatorUrl }); diff --git a/node-src/ui/messages/errors/brokenStorybook.ts b/node-src/ui/messages/errors/brokenStorybook.ts index b0c1fd3f2..94c5e77e6 100644 --- a/node-src/ui/messages/errors/brokenStorybook.ts +++ b/node-src/ui/messages/errors/brokenStorybook.ts @@ -1,15 +1,16 @@ import chalk from 'chalk'; import { dedent } from 'ts-dedent'; +import { baseStorybookUrl } from '../../../lib/utils'; import { error } from '../../components/icons'; import link from '../../components/link'; -export default ({ failureReason, storybookUrl }) => +export default ({ failureReason, isolatorUrl }) => `${dedent(chalk` ${error} {bold Failed to extract stories from your Storybook} This is usually a problem with your published Storybook, not with Chromatic. Build and open your Storybook locally and check the browser console for errors. - Visit your published Storybook at ${link(storybookUrl)} + Visit your published Storybook at ${link(baseStorybookUrl(isolatorUrl))} The following error was encountered while running your Storybook: `)}\n\n${failureReason.trim()}`; diff --git a/node-src/ui/messages/errors/fatalError.stories.ts b/node-src/ui/messages/errors/fatalError.stories.ts index 77a8a4f6a..36faa7c4d 100644 --- a/node-src/ui/messages/errors/fatalError.stories.ts +++ b/node-src/ui/messages/errors/fatalError.stories.ts @@ -47,10 +47,10 @@ const context = { build: { id: '5ec5069ae0d35e0022b6a9cc', number: 42, - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', webUrl: 'https://www.chromatic.com/build?appId=5d67dc0374b2e300209c41e7&number=1400', }, - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', + isolatorUrl: 'https://pfkaemtlit.tunnel.chromaticqa.com/iframe.html', + cachedUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/iframe.html', }; const stack = `Error: Oh no! diff --git a/node-src/ui/messages/errors/fatalError.ts b/node-src/ui/messages/errors/fatalError.ts index 00c4fd109..218a14af1 100644 --- a/node-src/ui/messages/errors/fatalError.ts +++ b/node-src/ui/messages/errors/fatalError.ts @@ -7,12 +7,7 @@ import { Context, InitialContext } from '../../..'; import link from '../../components/link'; import { redact } from '../../../lib/utils'; -const buildFields = ({ id, number, storybookUrl = undefined, webUrl = undefined }) => ({ - id, - number, - ...(storybookUrl && { storybookUrl }), - ...(webUrl && { webUrl }), -}); +const buildFields = ({ id, number, webUrl }) => ({ id, number, webUrl }); export default function fatalError( ctx: Context | InitialContext, @@ -31,8 +26,9 @@ export default function fatalError( runtimeMetadata, exitCode, exitCodeKey, - announcedBuild, - build = announcedBuild, + isolatorUrl, + cachedUrl, + build, buildCommand, } = ctx; @@ -58,6 +54,8 @@ export default function fatalError( exitCodeKey, errorType: errors.map((err) => err.name).join('\n'), errorMessage: stripAnsi(errors[0].message.split('\n')[0].trim()), + ...(isolatorUrl ? { isolatorUrl } : {}), + ...(cachedUrl ? { cachedUrl } : {}), ...(build && { build: buildFields(build) }), }, 'projectToken', diff --git a/node-src/ui/messages/errors/maxFileCountExceeded.stories.ts b/node-src/ui/messages/errors/maxFileCountExceeded.stories.ts deleted file mode 100644 index 41b67cade..000000000 --- a/node-src/ui/messages/errors/maxFileCountExceeded.stories.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { maxFileCountExceeded } from './maxFileCountExceeded'; - -export default { - title: 'CLI/Messages/Errors', -}; - -export const MaxFileCountExceeded = () => - maxFileCountExceeded({ - fileCount: 54_321, - maxFileCount: 20_000, - }); diff --git a/node-src/ui/messages/errors/maxFileCountExceeded.ts b/node-src/ui/messages/errors/maxFileCountExceeded.ts deleted file mode 100644 index 6487599c8..000000000 --- a/node-src/ui/messages/errors/maxFileCountExceeded.ts +++ /dev/null @@ -1,19 +0,0 @@ -import chalk from 'chalk'; -import { dedent } from 'ts-dedent'; - -import { error } from '../../components/icons'; - -export const maxFileCountExceeded = ({ - fileCount, - maxFileCount, -}: { - fileCount: number; - maxFileCount: number; -}) => - dedent(chalk` - ${error} {bold Attempted to upload too many files} - You're not allowed to upload more than ${maxFileCount} files per build. - Your Storybook contains ${fileCount} files. This is a very high number. - Do you have files in a static/public directory that shouldn't be there? - Contact customer support if you need to increase this limit. - `); diff --git a/node-src/ui/messages/errors/maxFileSizeExceeded.stories.ts b/node-src/ui/messages/errors/maxFileSizeExceeded.stories.ts deleted file mode 100644 index a7cb75430..000000000 --- a/node-src/ui/messages/errors/maxFileSizeExceeded.stories.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { maxFileSizeExceeded } from './maxFileSizeExceeded'; - -export default { - title: 'CLI/Messages/Errors', -}; - -export const MaxFileSizeExceeded = () => - maxFileSizeExceeded({ filePaths: ['index.js', 'main.js'], maxFileSize: 12345 }); diff --git a/node-src/ui/messages/errors/maxFileSizeExceeded.ts b/node-src/ui/messages/errors/maxFileSizeExceeded.ts deleted file mode 100644 index ec2c274a9..000000000 --- a/node-src/ui/messages/errors/maxFileSizeExceeded.ts +++ /dev/null @@ -1,19 +0,0 @@ -import chalk from 'chalk'; -import { filesize } from 'filesize'; -import { dedent } from 'ts-dedent'; - -import { error } from '../../components/icons'; - -export const maxFileSizeExceeded = ({ - filePaths, - maxFileSize, -}: { - filePaths: string[]; - maxFileSize: number; -}) => - dedent(chalk` - ${error} {bold Attempted to exceed maximum file size} - You're attempting to upload files that exceed the maximum file size of ${filesize(maxFileSize)}. - Contact customer support if you need to increase this limit. - - ${filePaths.map((path) => path).join('\n- ')} - `); diff --git a/node-src/ui/messages/info/storybookPublished.stories.ts b/node-src/ui/messages/info/storybookPublished.stories.ts index 9dbee8d3a..55e017638 100644 --- a/node-src/ui/messages/info/storybookPublished.stories.ts +++ b/node-src/ui/messages/info/storybookPublished.stories.ts @@ -5,11 +5,6 @@ export default { }; export const StorybookPublished = () => - storybookPublished({ - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', - } as any); - -export const StorybookPrepared = () => storybookPublished({ build: { actualCaptureCount: undefined, @@ -19,6 +14,6 @@ export const StorybookPrepared = () => errorCount: undefined, componentCount: 5, specCount: 8, + cachedUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/iframe.html', }, - storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', } as any); diff --git a/node-src/ui/messages/info/storybookPublished.ts b/node-src/ui/messages/info/storybookPublished.ts index 084a663d1..8254904a5 100644 --- a/node-src/ui/messages/info/storybookPublished.ts +++ b/node-src/ui/messages/info/storybookPublished.ts @@ -1,23 +1,17 @@ import chalk from 'chalk'; import { dedent } from 'ts-dedent'; +import { baseStorybookUrl } from '../../../lib/utils'; import { Context } from '../../../types'; import { info, success } from '../../components/icons'; import link from '../../components/link'; import { stats } from '../../tasks/snapshot'; -export default ({ build, storybookUrl }: Pick) => { - if (build) { - const { components, stories } = stats({ build }); - return dedent(chalk` - ${success} {bold Storybook published} - We found ${components} with ${stories}. - ${info} View your Storybook at ${link(storybookUrl)} - `); - } - +export default ({ build }: Pick) => { + const { components, stories } = stats({ build }); return dedent(chalk` ${success} {bold Storybook published} - ${info} View your Storybook at ${link(storybookUrl)} + We found ${components} with ${stories}. + ${info} View your Storybook at ${link(baseStorybookUrl(build.cachedUrl))} `); }; diff --git a/node-src/ui/tasks/upload.stories.ts b/node-src/ui/tasks/upload.stories.ts index cf66e6368..c6691d394 100644 --- a/node-src/ui/tasks/upload.stories.ts +++ b/node-src/ui/tasks/upload.stories.ts @@ -20,6 +20,9 @@ export default { decorators: [(storyFn: any) => task(storyFn())], }; +const isolatorUrl = 'https://5eb48280e78a12aeeaea33cf-kdypokzbrs.chromatic.com/iframe.html'; +// const storybookUrl = 'https://self-hosted-storybook.netlify.app'; + export const Initial = () => initial; export const DryRun = () => dryRun(); @@ -57,14 +60,6 @@ export const Starting = () => starting(); export const Uploading = () => uploading({ percentage: 42 }); -export const Success = () => - success({ - now: 0, - startedAt: -54321, - uploadedBytes: 1234567, - uploadedFiles: 42, - } as any); - -export const SuccessNoFiles = () => success({} as any); +export const Success = () => success({ now: 0, startedAt: -54321, isolatorUrl } as any); export const Failed = () => failed({ path: 'main.9e3e453142da82719bf4.bundle.js' }); diff --git a/node-src/ui/tasks/upload.ts b/node-src/ui/tasks/upload.ts index c9f169e27..193162bfb 100644 --- a/node-src/ui/tasks/upload.ts +++ b/node-src/ui/tasks/upload.ts @@ -1,8 +1,7 @@ -import { filesize } from 'filesize'; import pluralize from 'pluralize'; import { getDuration } from '../../lib/tasks'; -import { isPackageManifestFile, progressBar } from '../../lib/utils'; +import { baseStorybookUrl, progressBar, isPackageManifestFile } from '../../lib/utils'; import { Context } from '../../types'; export const initial = { @@ -99,15 +98,11 @@ export const uploading = ({ percentage }: { percentage: number }) => ({ output: `${progressBar(percentage)} ${percentage}%`, }); -export const success = (ctx: Context) => { - const files = pluralize('file', ctx.uploadedFiles, true); - const bytes = filesize(ctx.uploadedBytes || 0); - return { - status: 'success', - title: ctx.uploadedBytes ? `Publish complete in ${getDuration(ctx)}` : `Publish complete`, - output: ctx.uploadedBytes ? `Uploaded ${files} (${bytes})` : 'No new files to upload', - }; -}; +export const success = (ctx: Context) => ({ + status: 'success', + title: `Publish complete in ${getDuration(ctx)}`, + output: `View your Storybook at ${baseStorybookUrl(ctx.isolatorUrl)}`, +}); export const failed = ({ path }: { path: string }) => ({ status: 'error', diff --git a/package.json b/package.json index f39cd56af..6dc3398e9 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,6 @@ "execa": "^7.2.0", "fake-tag": "^2.0.0", "filesize": "^10.1.0", - "form-data": "^4.0.0", "fs-extra": "^10.0.0", "https-proxy-agent": "^7.0.2", "husky": "^7.0.0", @@ -203,9 +202,6 @@ }, "auto": { "baseBranch": "main", - "canary": { - "force": true - }, "plugins": [ "npm", "released", diff --git a/yarn.lock b/yarn.lock index 29ad83171..ea2c2ff3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7905,15 +7905,6 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - format@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"