diff --git a/node-src/__mocks__/iframe.html b/node-src/__mocks__/iframe.html new file mode 100644 index 000000000..ffa0ec4e0 --- /dev/null +++ b/node-src/__mocks__/iframe.html @@ -0,0 +1,364 @@ +Webpack App

No Preview

Sorry, but you either have no stories or none are selected somehow.

If the problem persists, check the browser console, or the terminal you've run Storybook from.

\ No newline at end of file diff --git a/node-src/__mocks__/index.html b/node-src/__mocks__/index.html new file mode 100644 index 000000000..661e2680e --- /dev/null +++ b/node-src/__mocks__/index.html @@ -0,0 +1,59 @@ +Webpack App
\ No newline at end of file diff --git a/node-src/index.test.ts b/node-src/index.test.ts index ece046695..6698b0778 100644 --- a/node-src/index.test.ts +++ b/node-src/index.test.ts @@ -295,6 +295,11 @@ const getCommit = vi.mocked(git.getCommit); vi.mock('./lib/emailHash'); +vi.mock('./lib/getFileHashes', () => ({ + getFileHashes: (files: string[]) => + Promise.resolve(Object.fromEntries(files.map((f) => [f, 'hash']))), +})); + vi.mock('./lib/getPackageManager', () => ({ getPackageManagerName: () => Promise.resolve('pnpm'), getPackageManagerRunCommand: (args) => Promise.resolve(`pnpm run ${args.join(' ')}`), @@ -457,6 +462,7 @@ it('calls out to npm build script passed and uploads files', async () => { expect.any(Object), [ { + contentHash: 'hash', contentLength: 42, contentType: 'text/html', localPath: expect.stringMatching(/\/iframe\.html$/), @@ -464,6 +470,7 @@ it('calls out to npm build script passed and uploads files', async () => { targetUrl: 'https://cdn.example.com/iframe.html', }, { + contentHash: 'hash', contentLength: 42, contentType: 'text/html', localPath: expect.stringMatching(/\/index\.html$/), @@ -484,6 +491,7 @@ it('skips building and uploads directly with storybook-build-dir', async () => { expect.any(Object), [ { + contentHash: 'hash', contentLength: 42, contentType: 'text/html', localPath: expect.stringMatching(/\/iframe\.html$/), @@ -491,6 +499,7 @@ it('skips building and uploads directly with storybook-build-dir', async () => { targetUrl: 'https://cdn.example.com/iframe.html', }, { + contentHash: 'hash', contentLength: 42, contentType: 'text/html', localPath: expect.stringMatching(/\/index\.html$/), diff --git a/node-src/lib/getFileHashes.test.ts b/node-src/lib/getFileHashes.test.ts new file mode 100644 index 000000000..9cc4580ee --- /dev/null +++ b/node-src/lib/getFileHashes.test.ts @@ -0,0 +1,14 @@ +import { describe } from 'node:test'; +import { expect, it } from 'vitest'; +import { getFileHashes } from './getFileHashes'; + +describe('getFileHashes', () => { + it('should return a map of file paths to hashes', async () => { + const hashes = await getFileHashes(['iframe.html', 'index.html'], 'node-src/__mocks__'); + + expect(hashes).toEqual({ + 'iframe.html': '80b7ac41594507e8', + 'index.html': '0e98fd69b0b01605', + }); + }); +}); diff --git a/node-src/lib/getFileHashes.ts b/node-src/lib/getFileHashes.ts index 24ee246a7..369d20c26 100644 --- a/node-src/lib/getFileHashes.ts +++ b/node-src/lib/getFileHashes.ts @@ -55,14 +55,14 @@ const hashFile = (buffer: Buffer, path: string, xxhash: XXHashAPI): Promise { const limit = pLimit(48); + const xxhash = await xxHashWasm(); // Pre-allocate a 64K buffer for each file, matching WASM memory page size. const buffers = files.map((file) => [Buffer.allocUnsafe(64 * 1024), file] as const); - const api = await xxHashWasm(); const hashes = await Promise.all( buffers.map(([buffer, file]) => - limit(async () => [file, await hashFile(buffer, join(dir, file), api)] as const) + limit(async () => [file, await hashFile(buffer, join(dir, file), xxhash)] as const) ) ); diff --git a/node-src/tasks/upload.test.ts b/node-src/tasks/upload.test.ts index 038b1d2d6..8835964ea 100644 --- a/node-src/tasks/upload.test.ts +++ b/node-src/tasks/upload.test.ts @@ -6,7 +6,7 @@ import { default as compress } from '../lib/compress'; import { getDependentStoryFiles as getDepStoryFiles } from '../lib/getDependentStoryFiles'; import { findChangedDependencies as findChangedDep } from '../lib/findChangedDependencies'; import { findChangedPackageFiles as findChangedPkg } from '../lib/findChangedPackageFiles'; -import { validateFiles, traceChangedFiles, uploadStorybook } from './upload'; +import { calculateFileHashes, validateFiles, traceChangedFiles, uploadStorybook } from './upload'; vi.mock('fs'); vi.mock('progress-stream'); @@ -16,6 +16,11 @@ vi.mock('../lib/findChangedDependencies'); vi.mock('../lib/findChangedPackageFiles'); vi.mock('./read-stats-file'); +vi.mock('../lib/getFileHashes', () => ({ + getFileHashes: (files: string[]) => + Promise.resolve(Object.fromEntries(files.map((f) => [f, 'hash']))), +})); + const makeZipFile = vi.mocked(compress); const findChangedDependencies = vi.mocked(findChangedDep); const findChangedPackageFiles = vi.mocked(findChangedPkg); @@ -219,6 +224,35 @@ describe('traceChangedFiles', () => { }); }); +describe('calculateFileHashes', () => { + it('sets hashes on context.fileInfo', async () => { + const fileInfo = { + lengths: [ + { knownAs: 'iframe.html', contentLength: 42 }, + { knownAs: 'index.html', contentLength: 42 }, + ], + paths: ['iframe.html', 'index.html'], + total: 84, + }; + const ctx = { + env, + log, + http, + sourceDir: '/static/', + options: { fileHashing: true }, + fileInfo, + announcedBuild: { id: '1' }, + } as any; + + await calculateFileHashes(ctx, {} as any); + + expect(ctx.fileInfo.hashes).toMatchObject({ + 'iframe.html': 'hash', + 'index.html': 'hash', + }); + }); +}); + describe('uploadStorybook', () => { it('retrieves the upload locations, puts the files there and sets the isolatorUrl on context', async () => { const client = { runQuery: vi.fn() }; diff --git a/node-src/tasks/upload.ts b/node-src/tasks/upload.ts index 132597513..a27251b70 100644 --- a/node-src/tasks/upload.ts +++ b/node-src/tasks/upload.ts @@ -185,9 +185,15 @@ export const traceChangedFiles = async (ctx: Context, task: Task) => { export const calculateFileHashes = async (ctx: Context, task: Task) => { if (ctx.skip || !ctx.options.fileHashing) return; transitionTo(hashing)(ctx, task); - const start = Date.now(); - ctx.fileInfo.hashes = await getFileHashes(ctx.fileInfo.paths, ctx.sourceDir); - ctx.log.debug(`Calculated file hashes in ${Date.now() - start}ms`); + + try { + const start = Date.now(); + ctx.fileInfo.hashes = await getFileHashes(ctx.fileInfo.paths, ctx.sourceDir); + ctx.log.debug(`Calculated file hashes in ${Date.now() - start}ms`); + } catch (err) { + ctx.log.warn('Failed to calculate file hashes'); + ctx.log.debug(err); + } }; export const uploadStorybook = async (ctx: Context, task: Task) => {