UNPKG

creevey

Version:

Cross-browser screenshot testing tool for Storybook with fancy UI Runner

606 lines (418 loc) 16.1 kB
# Dev-Only Client Statics Bootstrap Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use **superpowers:subagent-driven-development** (recommended) or **superpowers:executing-plans** to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Keep on-demand UI bundle builds for repo-local `yarn creevey report` and `yarn creevey test --ui`, while removing build-on-demand behavior from production server/runtime code. **Architecture:** Move the Vite build fallback out of `src/server/*` and into a dev-only bootstrap helper called from `src/creevey.ts` before UI entrypoints run. Server/runtime code will switch to a strict client-statics accessor that only reads `dist/client/web` and fails clearly when assets are missing. **Tech Stack:** TypeScript, Node.js `fs`/`path`, Vite, Vitest --- ## Files Changed | File | Action | Purpose | | --------------------------------------- | ------ | ------------------------------------------------------------------ | | `src/server/utils.ts` | Modify | Replace runtime build fallback with strict client statics accessor | | `src/server/report.ts` | Modify | Remove runtime call to build client statics | | `src/server/master/start.ts` | Modify | Remove runtime call to build client statics | | `src/creevey.ts` | Modify | Add CLI-side dev bootstrap for UI flows | | `src/dev/ensure-client-statics.ts` | Create | Hold dev-only Vite build-on-demand helper | | `tests/utils.test.ts` | Modify | Add tests for strict runtime accessor | | `tests/dev/ensureClientStatics.test.ts` | Create | Add tests for dev-only bootstrap helper | | `memories/memory.md` | Modify | Update project memory to reflect new local-dev/runtime split | --- ### Task 1: Add a strict runtime accessor for built client statics **Files:** - Modify: `src/server/utils.ts` - Modify: `tests/utils.test.ts` - [ ] **Step 1: Write the failing tests for strict runtime statics lookup** Add to `tests/utils.test.ts` near the top with the existing imports: ```ts import fs from 'fs'; import path from 'path'; ``` Update the existing utils import: ```ts import { getClientDir, getRequiredClientDir, shouldSkip } from '../src/server/utils.js'; ``` Append this new block to `tests/utils.test.ts` after the `shouldSkip` tests: ```ts describe('getRequiredClientDir', () => { const clientDir = getClientDir(); const indexHtml = path.join(clientDir, 'index.html'); const backupHtml = path.join(clientDir, 'index.html.test-backup'); const hadIndexHtml = fs.existsSync(indexHtml); const cleanupIndexHtml = (): void => { if (fs.existsSync(backupHtml)) { fs.renameSync(backupHtml, indexHtml); return; } if (!hadIndexHtml && fs.existsSync(indexHtml)) { fs.unlinkSync(indexHtml); } }; test('returns client dir when built statics exist', () => { if (!hadIndexHtml) { fs.mkdirSync(clientDir, { recursive: true }); fs.writeFileSync(indexHtml, '<!doctype html>'); } expect(getRequiredClientDir()).toBe(clientDir); cleanupIndexHtml(); }); test('throws a clear error when built statics are missing', () => { if (fs.existsSync(indexHtml)) { fs.renameSync(indexHtml, backupHtml); } try { expect(() => getRequiredClientDir()).toThrow( 'Creevey web UI assets are missing. Run `yarn build` or `yarn build:client` before starting UI mode.', ); } finally { cleanupIndexHtml(); } }); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: ```bash yarn test tests/utils.test.ts ``` Expected: FAIL because `getRequiredClientDir` is not exported yet. - [ ] **Step 3: Implement the strict runtime accessor in `src/server/utils.ts`** Replace the current client statics section in `src/server/utils.ts` with: ```ts export function getClientDir(): string { return path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../dist/client/web'); } export function getRequiredClientDir(): string { const clientDir = getClientDir(); const indexHtml = path.join(clientDir, 'index.html'); if (fs.existsSync(indexHtml)) return clientDir; throw new Error( 'Creevey web UI assets are missing. Run `yarn build` or `yarn build:client` before starting UI mode.', ); } ``` Delete the old `ensureClientStatics()` implementation entirely. - [ ] **Step 4: Run the test to verify it passes** Run: ```bash yarn test tests/utils.test.ts ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/server/utils.ts tests/utils.test.ts git commit -m "refactor: require built client statics at runtime" ``` --- ### Task 2: Add the dev-only on-demand client statics bootstrap helper **Files:** - Create: `src/dev/ensure-client-statics.ts` - Create: `tests/dev/ensureClientStatics.test.ts` - [ ] **Step 1: Write the failing tests for the dev helper** Create `tests/dev/ensureClientStatics.test.ts`: ```ts import fs from 'fs'; import path from 'path'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; const build = vi.fn(); vi.mock('vite', () => ({ build, })); import { ensureClientStaticsForLocalDev } from '../../src/dev/ensure-client-statics.js'; import { getClientDir } from '../../src/server/utils.js'; describe('ensureClientStaticsForLocalDev', () => { const clientDir = getClientDir(); const indexHtml = path.join(clientDir, 'index.html'); const backupHtml = path.join(clientDir, 'index.html.dev-test-backup'); const hadIndexHtml = fs.existsSync(indexHtml); const cleanupIndexHtml = (): void => { if (fs.existsSync(backupHtml)) { fs.renameSync(backupHtml, indexHtml); return; } if (!hadIndexHtml && fs.existsSync(indexHtml)) { fs.unlinkSync(indexHtml); } }; beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { cleanupIndexHtml(); }); test('returns immediately when built statics already exist', async () => { if (!hadIndexHtml) { fs.mkdirSync(clientDir, { recursive: true }); fs.writeFileSync(indexHtml, '<!doctype html>'); } await expect(ensureClientStaticsForLocalDev()).resolves.toBe(clientDir); expect(build).not.toHaveBeenCalled(); }); test('runs vite build when built statics are missing', async () => { if (fs.existsSync(indexHtml)) { fs.renameSync(indexHtml, backupHtml); } build.mockImplementation(async () => { fs.mkdirSync(clientDir, { recursive: true }); fs.writeFileSync(indexHtml, '<!doctype html>'); }); await expect(ensureClientStaticsForLocalDev()).resolves.toBe(clientDir); expect(build).toHaveBeenCalledTimes(1); }); test('throws a clear error when vite build does not produce index.html', async () => { if (fs.existsSync(indexHtml)) { fs.renameSync(indexHtml, backupHtml); } build.mockResolvedValue(undefined); await expect(ensureClientStaticsForLocalDev()).rejects.toThrow( /Failed to build Creevey web UI: .*index\.html was not created/, ); }); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: ```bash yarn test tests/dev/ensureClientStatics.test.ts ``` Expected: FAIL because `src/dev/ensure-client-statics.ts` does not exist yet. - [ ] **Step 3: Create `src/dev/ensure-client-statics.ts`** Write `src/dev/ensure-client-statics.ts`: ```ts import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { build } from 'vite'; import { logger } from '../server/logger.js'; import { getClientDir } from '../server/utils.js'; const importMetaUrl = import.meta.url; export async function ensureClientStaticsForLocalDev(): Promise<string> { const clientDir = getClientDir(); const indexHtml = path.join(clientDir, 'index.html'); if (fs.existsSync(indexHtml)) return clientDir; logger().info('Building Creevey web UI...'); try { await build({ configFile: path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../vite.config.mts'), logLevel: 'error', }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to build Creevey web UI: ${errorMessage}`); } if (!fs.existsSync(indexHtml)) { throw new Error(`Failed to build Creevey web UI: ${indexHtml} was not created`); } return clientDir; } ``` - [ ] **Step 4: Run the test to verify it passes** Run: ```bash yarn test tests/dev/ensureClientStatics.test.ts ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/dev/ensure-client-statics.ts tests/dev/ensureClientStatics.test.ts git commit -m "feat: add local dev client statics bootstrap" ``` --- ### Task 3: Move UI bootstrap to the CLI boundary **Files:** - Modify: `src/creevey.ts` - Test: `tests/dev/ensureClientStatics.test.ts` - [ ] **Step 1: Extend the test file with a pure command predicate test** Append this block to `tests/dev/ensureClientStatics.test.ts`: ```ts import { shouldEnsureClientStaticsForCommand } from '../../src/dev/ensure-client-statics.js'; describe('shouldEnsureClientStaticsForCommand', () => { test('returns true for report', () => { expect(shouldEnsureClientStaticsForCommand('report', { ui: true })).toBe(true); }); test('returns true for test ui mode', () => { expect(shouldEnsureClientStaticsForCommand('test', { ui: true })).toBe(true); }); test('returns false for non-ui test mode', () => { expect(shouldEnsureClientStaticsForCommand('test', { ui: false })).toBe(false); }); test('returns false for worker', () => { expect(shouldEnsureClientStaticsForCommand('worker', { ui: false })).toBe(false); }); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: ```bash yarn test tests/dev/ensureClientStatics.test.ts ``` Expected: FAIL because `shouldEnsureClientStaticsForCommand` does not exist yet. - [ ] **Step 3: Add the command predicate and wire it into `src/creevey.ts`** Update `src/dev/ensure-client-statics.ts` to export this function above `ensureClientStaticsForLocalDev`: ```ts export function shouldEnsureClientStaticsForCommand( command: 'report' | 'test' | 'worker', options: { readonly ui?: boolean }, ): boolean { return command === 'report' || (command === 'test' && options.ui === true); } ``` Update `src/creevey.ts` imports: ```ts import { ensureClientStaticsForLocalDev, shouldEnsureClientStaticsForCommand } from './dev/ensure-client-statics.js'; ``` Then replace the final line: ```ts void creevey(command, options); ``` with: ```ts const start = async (): Promise<void> => { if (cluster.isPrimary && v.is(OptionsSchema, options) && shouldEnsureClientStaticsForCommand(command, options)) { await ensureClientStaticsForLocalDev(); } await creevey(command, options); }; void start(); ``` - [ ] **Step 4: Run the tests to verify they pass** Run: ```bash yarn test tests/dev/ensureClientStatics.test.ts ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/creevey.ts src/dev/ensure-client-statics.ts tests/dev/ensureClientStatics.test.ts git commit -m "refactor: move client statics bootstrap to cli" ``` --- ### Task 4: Remove runtime build-on-demand calls from server flows **Files:** - Modify: `src/server/report.ts` - Modify: `src/server/master/start.ts` - Modify: `src/server/utils.ts` - [ ] **Step 1: Update runtime imports and calls** In `src/server/report.ts`, replace: ```ts import { ensureClientStatics, shutdownWorkers } from './utils.js'; ``` with: ```ts import { getRequiredClientDir, shutdownWorkers } from './utils.js'; ``` Replace: ```ts await ensureClientStatics(); ``` with: ```ts getRequiredClientDir(); ``` In `src/server/master/start.ts`, replace: ```ts import { shutdownWorkers, testsToImages, readDirRecursive, copyStatics, ensureClientStatics } from '../utils.js'; ``` with: ```ts import { shutdownWorkers, testsToImages, readDirRecursive, copyStatics, getRequiredClientDir } from '../utils.js'; ``` Replace: ```ts if (options.ui) await ensureClientStatics(); ``` with: ```ts if (options.ui) getRequiredClientDir(); ``` In `src/server/utils.ts`, update `copyStatics()` so the first line becomes: ```ts const clientDir = getRequiredClientDir(); ``` - [ ] **Step 2: Run targeted tests** Run: ```bash yarn test tests/utils.test.ts tests/dev/ensureClientStatics.test.ts ``` Expected: PASS. - [ ] **Step 3: Commit** ```bash git add src/server/report.ts src/server/master/start.ts src/server/utils.ts tests/utils.test.ts tests/dev/ensureClientStatics.test.ts git commit -m "refactor: remove runtime ui build fallback" ``` --- ### Task 5: Update memories for the new dev/runtime split **Files:** - Modify: `memories/memory.md` - [ ] **Step 1: Update the project memory** Replace the current note about server startup building missing UI assets with: ```md - The Creevey UI server serves the built Vite bundle from `dist/client/web`; published/runtime server flows require those assets to already exist and now fail clearly if they are missing - Repo-local CLI execution via `yarn creevey report` and `yarn creevey test --ui` keeps a dev-only bootstrap that builds the client bundle on demand before entering UI server flows ``` - [ ] **Step 2: Commit** ```bash git add memories/memory.md git commit -m "docs: update memory for client statics bootstrap split" ``` --- ## Verification Steps After all commits are complete: - [ ] Run the targeted unit tests: ```bash yarn test tests/utils.test.ts tests/dev/ensureClientStatics.test.ts ``` Expected: PASS. - [ ] Run the full test suite: ```bash yarn test ``` Expected: PASS. - [ ] Run lint: ```bash yarn lint ``` Expected: PASS. - [ ] Manual local-dev verification from a source checkout with missing client bundle: ```bash rm -rf dist/client/web yarn creevey report ``` Expected: logs `Building Creevey web UI...`, rebuilds `dist/client/web`, then starts report UI. - [ ] Manual local-dev UI test verification: ```bash rm -rf dist/client/web yarn creevey test --ui ``` Expected: logs `Building Creevey web UI...`, rebuilds `dist/client/web`, then starts UI mode. - [ ] Verify non-UI flow does not build the UI bundle: ```bash rm -rf dist/client/web yarn creevey test ``` Expected: no `Building Creevey web UI...` log line. Command behavior should proceed according to normal non-UI prerequisites. --- ## Spec Coverage Checklist | Spec Requirement | Task | | ------------------------------------------------------------------ | ---------------------------- | | Remove build-on-demand logic from runtime server utilities | Task 1, Task 4 | | Add dev-only bootstrap helper outside server runtime path | Task 2 | | Call the helper from `src/creevey.ts` for `report` and `test --ui` | Task 3 | | Preserve local-dev on-demand build behavior | Task 2, Task 3, Verification | | Make runtime fail clearly when client statics are missing | Task 1, Task 4 | | Update project memory for the new behavior | Task 5 | --- ## Placeholder Scan - No `TBD`, `TODO`, or deferred implementation text in tasks. - All file paths are exact. - Every code-changing step includes concrete code. - Every test/verification step includes exact commands and expected outcomes. --- ## Rollback Instructions If the split causes regressions: ```bash git revert <task-5-commit> git revert <task-4-commit> git revert <task-3-commit> git revert <task-2-commit> git revert <task-1-commit> ``` That restores the previous server-owned `ensureClientStatics()` flow.