Files
apparel-designer/MIGRATION_RUNBOOK.md
2026-05-24 08:44:29 -05:00

11 KiB

Migration runbook — apparel-designer → goods-editor module

Step-by-step instructions for executing the module extraction. This document is intentionally minimal-fluff: do each step in order, verify the marker at each checkpoint, move to the next.

Prerequisites

Verify both repos exist as siblings:

/Users/khalid/Documents/Claude-Desktop/
├── apparel-designer/        (the host app, in working state)
└── goods-editor-module/     (the empty module repo we just scaffolded)

Phase 1 — Module scaffolding (already done)

These were created during the planning session and are ready to use:

goods-editor-module/
├── package.json
├── vite.config.js              (library mode)
├── vite.server.config.js       (Node server entry)
├── vitest.config.js
├── .gitignore
├── README.md
├── src/
│   ├── ApparelDesigner.jsx     (~30KB orchestration absorbed from App.jsx)
│   ├── index.js                (public API barrel)
│   └── server/
│       ├── index.js
│       └── mountEditorApi.js   (Express middleware factory)
├── examples/dev-host/          (standalone dev playground)
└── scripts/
    ├── release.sh
    └── post-migrate-patches.mjs

Verification: ls goods-editor-module/src/index.js should succeed.

Phase 2 — File migration

Copy all the editor source from the host into the module.

cd /Users/khalid/Documents/Claude-Desktop
bash apparel-designer/scripts/migrate-to-module.sh

What this does: Copies utils, hooks, constants, components, styles, fonts, stickers, tests from apparel-designer/src/ (and related directories) into goods-editor-module/src/. Source is left intact in apparel-designer/ — nothing is deleted yet.

Verification: ls goods-editor-module/src/components/canvas/ should show DesignCanvas.jsx, ElementToolbar.jsx, etc.

Phase 3 — Post-copy patches

The copied files import constants like SHIRT_COLORS directly. Inside the module, those need to come from props. Run the patch script:

cd /Users/khalid/Documents/Claude-Desktop/goods-editor-module
node scripts/post-migrate-patches.mjs

What this does: Modifies a handful of files (ShirtOptionsPanel, Sidebar, UploadTab) to accept their data as props instead of importing constants.

Verification: Search the module source for SHIRT_COLORS — should return 0 hits in component files (still present as a constants/shirt.js default that's no longer imported by components).

Phase 4 — Module install + build

cd /Users/khalid/Documents/Claude-Desktop/goods-editor-module
npm install
npm run build

Verification: ls dist/ should show:

  • goods-editor.es.js
  • goods-editor.cjs.js
  • style.css
  • server/index.js
  • server/index.cjs

If the build fails:

  • "Cannot find module 'X'" — the migration may have missed a file. Check goods-editor-module/src/ for the missing path and copy it from apparel-designer/src/ manually.
  • "SHIRT_COLORS is not defined" — patch script didn't run or didn't fully match. Manually search for the symbol in the offending file and replace with the prop.

Verify the module works in isolation before wiring it into the host:

cd /Users/khalid/Documents/Claude-Desktop/goods-editor-module
npm run dev
# open http://localhost:3000 in a browser

You should see the editor with the dev host's minimal top bar. If something's broken it's a module issue; fix here before touching the host.

The dev host doesn't have an /api/upload endpoint, so uploads will fail — that's expected. Text, stickers, emoji, and templates work without a server.

Phase 6 — Host cutover

Replace the host's monolithic App.jsx + server.js with the new module-consuming versions:

cd /Users/khalid/Documents/Claude-Desktop/apparel-designer
bash scripts/cutover-host.sh

What this does:

  1. Renames the monolithic files to *.monolith.* (kept for reference)
  2. Promotes App.host.jsxApp.jsx, server.host.jsserver.js, package.host.jsonpackage.json
  3. Deletes the module-owned source from src/

Verification: ls apparel-designer/src/ should show only:

  • App.jsx
  • App.monolith.jsx (backup)
  • main.jsx
  • index.css
  • components/Header.jsx, components/PWAInstall.jsx, components/OfflineIndicator.jsx
  • styles/Header.css, styles/PWAInstall.css (or however they organize after cleanup)

(The host's src/styles/ and src/components/ may still have host-specific CSS that wasn't deleted. That's expected — only module-owned files were removed.)

Phase 7 — Host install + run

cd /Users/khalid/Documents/Claude-Desktop/apparel-designer
rm -rf node_modules package-lock.json
npm install
npm run dev

The file:../goods-editor-module reference in package.json points at the local module directory. npm install will copy it into node_modules/goods-editor/.

Important: host-owned constants survive cutover

The cutover-host.sh script preserves three files in src/constants/:

  • shirt.js — host's color / size / price catalog
  • templates.js — host's template definitions
  • products.js — host's product configuration

These are imported by the new App.jsx and passed into <ApparelDesigner> as props. Other constants (fonts, stickers, elements, etc.) are module-owned and get deleted.

If you customize the host's catalog (different shirt colors, new templates), you edit those files — not the module.

Important: host-required native deps for server-side export

goods-editor/server's mountEditorApi() accepts the host's canvas and sharp modules via options:

import canvas from 'canvas';
import sharp from 'sharp';
import { mountEditorApi } from 'goods-editor/server';

mountEditorApi(app, { canvas, sharp, exportsDir, fontsDir, ... });

This injection pattern means:

  • The module declares both as optional peer dependencies — listed for tooling visibility but not auto-installed.
  • Hosts that mount the server install canvas + sharp in their OWN package.json (as regular deps) and import them at the top of their server.js.
  • Hosts that DON'T mount the server (editor-only, preview-only) don't install them at all. The React side of goods-editor doesn't need them.

Why injection rather than require() inside the module: when the module is consumed via file: (local dev) or git+ssh:// (release), the module's built server bundle lives in the consuming app's node_modules. A require('canvas') from inside that bundle would resolve from the MODULE'S directory chain, not the host's. The injection pattern sidesteps this entirely — the host imports canvas from its own node_modules and passes the imported object in.

System libs: canvas builds native bindings against cairo/pango. On macOS:

brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman

If npm install fails on the canvas build step, those libs are usually the missing piece.

If the host doesn't need server-side export (e.g. editor-only flows), skip the mountEditorApi(app, ...) call in server.js entirely and don't install canvas/sharp — the editor still works as a pure React component.

Important: the host's virtual:sticker-manifest setup stays in place

The module's src/constants/stickers.js imports from virtual:sticker-manifest — a Vite virtual module that the HOST'S vite.config.js provides via stickerManifestPlugin(). This is intentional: it makes the host the source-of-truth for which stickers are available.

Don't delete stickerManifestPlugin() from the host's vite.config.js, and don't delete public/stickers/. The cutover script leaves both alone. If you accidentally remove the plugin, you will see this error when running the editor:

Failed to resolve import "virtual:sticker-manifest" from
"node_modules/goods-editor/dist/goods-editor.es.js".

The fix is to restore the plugin (it's the stickerManifestPlugin() function at the top of vite.config.js, plus its inclusion in the plugins: array).

If you want to ship the editor on a host that doesn't use Vite, or that has a fundamentally different sticker layout, you can pass stickers in via props on <ApparelDesigner> instead and provide a no-op virtual module that exports an empty STICKER_FILES array. (That extension point doesn't exist yet — file an issue / add a prop when the second host needs it.)

Verification: http://localhost:3000 should show the editor with the host's full Header and the editor mounted below it. Functionality should match the pre-migration version exactly.

Phase 8 — Final checks

Things to verify in the running app:

  • Can add text from the Text tab
  • Can upload a photo and see it on the canvas
  • Can add a sticker / emoji
  • Cmd+Z undoes; Cmd+Shift+Z redoes
  • Crop an image, Cmd+Z restores pre-crop (the May 22 regression)
  • Save button triggers an export (download via the export pipeline)
  • Reload the page — designs persist via localStorage
  • Production build: npm run build && NODE_ENV=production npm start

If everything works:

  1. git add -A && git commit -m "Migrate to goods-editor module" in both repos
  2. Cut a release of the module:
    cd goods-editor-module
    ./scripts/release.sh --version=0.1.0-alpha.0
    
  3. In the host, swap the dep from file: to git+ssh:// once you've pushed to Gitea.

Rollback

If anything goes sideways and you need to undo the cutover:

cd apparel-designer
mv src/App.jsx src/App.host.jsx
mv server.js server.host.js
mv package.json package.host.json
mv src/App.monolith.jsx src/App.jsx
mv server.monolith.js server.js
mv package.monolith.json package.json
# Restore deleted module-owned files from git:
git checkout -- src/ fonts/ e2e/ playwright.config.js vitest.config.js
rm -rf node_modules package-lock.json
npm install
npm run dev

The goods-editor-module/ repo stays put — rollback only affects the host. You can iterate on the module separately and try the cutover again later.

Done

After successful cutover, future development looks like:

  • Editor changes (canvas behavior, sidebar UX, hooks, utilities): edit in goods-editor-module/. Test via npm run dev (dev host). When happy, ./scripts/release.sh --version=X.Y.Z and bump the host's package.json.

  • Host changes (homepage, marketing, cart, top bar): edit in apparel-designer/. AI tools pointed at this directory only see the host's ~10 files plus the editor's public API surface, not the editor's ~100 internal files. That's the win.