handoff-app 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,7 +1,96 @@
1
- # Instructions
1
+ # AGENTS.md
2
+
3
+ ## Local instructions
2
4
 
3
5
  Before starting work, check whether `AGENTS.local.md` exists in the repository root.
4
6
 
5
7
  If it exists, read and follow its instructions in addition to this file. Treat `AGENTS.local.md` as developer-specific local guidance.
6
8
 
7
9
  If it does not exist, proceed normally.
10
+
11
+ ## Project
12
+
13
+ `handoff-app` is an npm package (CLI + library) that turns design tokens (fetched from Figma), components, patterns, and Markdown pages into a Next.js documentation app and distributable artifacts. User-facing docs: [README.md](README.md), [RECIPES.md](RECIPES.md), [docs/cli.md](docs/cli.md), [docs/api.md](docs/api.md), [UPGRADE.md](UPGRADE.md).
14
+
15
+ Requires Node.js >= 22 and npm >= 10.
16
+
17
+ ## Commands
18
+
19
+ ```bash
20
+ npm run build # tsc: compiles src/ (except src/app) to dist/
21
+ npm run lint # next lint over src/app
22
+ npm test # alias for lint; there is no unit test suite
23
+ npx tsc -p src/app/tsconfig.json --noEmit # typecheck the Next.js app (root tsc excludes src/app);
24
+ # errors under a stale src/app/.next/types are not real
25
+ npx drizzle-kit generate # generate a registry migration after editing src/registry/db/schema.ts
26
+ ```
27
+
28
+ The CLI (`dist/cli.js`, bin `handoff-app`) always runs against a **working directory**: the directory that holds `handoff.config.ts` (or `HANDOFF_WORKING_PATH`). This repo is not a Handoff project, so to exercise a change, build here and run the CLI from a consumer project that uses this build (`start`, `dev`, `build`, `fetch`, `publish`, `checkout`, ...). Verify behavior there; `npm run lint` and the two `tsc` runs are the only checks in this repo.
29
+
30
+ Formatting: Prettier (config in `package.json`: 140 cols, single quotes, `es5` trailing commas, Tailwind class sorting) and `.editorconfig` (2 spaces, LF).
31
+
32
+ ## Architecture
33
+
34
+ ### Runtime modes
35
+
36
+ - **Workspace** (default): local files are the source of truth; no database. It is the **only** place that builds content.
37
+ - **Registry**: a deployed catalog backed by PostgreSQL (Drizzle). It serves only what a workspace publishes. It never builds and never reads workspace source.
38
+ - A **connected workspace** publishes to / checks out from a registry; it is not a third mode.
39
+
40
+ `runtime.mode` selects the mode; `build --target registry` sets registry mode in the generated app. Keep registry-only dependencies (Drizzle/Postgres) out of the workspace and static paths — e.g. `src/store/index.ts` deliberately does not re-export the registry store; registry consumers import `@handoff/store/registry` lazily.
41
+
42
+ ### Two TypeScript programs
43
+
44
+ - **`src/` (minus `src/app`)** — the Node CLI/library, CommonJS, compiled by root `tsconfig.json` to `dist/`. `src/index.ts` holds the `Handoff` class (`fetch`, `build`, `publish`, `checkout`, `start`, `dev`); `src/cli.ts` registers the yargs commands in `src/commands/`.
45
+ - **`src/app/`** — the Next.js (pages router) documentation app. It is not compiled by `tsc`; at build/start time `src/app-builder` stages it into `.handoff/<projectId>/` under the package and runs `next build` / `next dev`. It imports the core via the `@handoff/*` alias (→ `src/*`). It also hosts the APIs: docs API, registry API, MCP endpoint (`/api/mcp`), auth, and the AI assistant (`src/app/lib/*`, `src/app/pages/api/*`).
46
+
47
+ ### Main flows
48
+
49
+ - **fetch**: `src/pipeline` — Figma auth → extract → fonts → token transformers; output goes to the working directory's `exported/`.
50
+ - **build/start**: `src/config` loads `handoff.config.*` + profile sidecar + env, then `initRuntimeConfig()` normalizes catalog declarations (`src/catalog`) into component/pattern records; `src/transformers` builds previews with Vite/esbuild per renderer (React, Handlebars, CSF); `src/cache` skips unchanged components (`.handoff/.cache/build-cache.json`, bypass with `--force`); `src/app-builder` builds the app (static → `out/static`, registry → `out/registry`, `--package vercel` → `.vercel/output`). Watch mode uses chokidar + a WebSocket reload server with diff-aware rebuilds.
51
+ - **publish/checkout**: `src/registry` (`publish/`, `checkout/`, `transfer.ts`, `tokens/`, `assets/`, `auth/`) against the registry API in `src/app/lib/registry-api`. Storage is abstracted by `src/store` (filesystem vs. registry implementations of the same interfaces). Asset blobs go through a pluggable storage provider; email and AI providers are also pluggable custom server modules (`handoff-app/providers`).
52
+ - **Migrations**: SQL in `drizzle/` ships with the package and is applied by `handoff-app db:migrate`.
53
+
54
+ ### Config and server-module rules
55
+
56
+ - **Baked vs. request-time values.** For a `fromEnv('NAME')` value, the build keeps only the variable name, and a deployed registry reads the value at request time. Secrets must use `fromEnv()`. These settings change the shape of the build and are baked, so a change needs a rebuild: `runtime.mode`, `runtime.mcp`, `runtime.ai.enabled`, the database driver, the asset storage and email providers, and every custom server module. A new or changed setting must fit one of these two categories.
57
+ - **Custom server modules** (asset storage, email, AI providers) are `.js`/`.mjs` files, relative to the working directory, that import their define helper from `handoff-app/providers`. The registry build stops when a module imports `handoff-app`. The build copies each module and the packages it imports into the registry bundle.
58
+
59
+ Package entry points (`handoff-app`, `/react`, `/handlebars`, `/pattern`, `/providers`) map to `src/index.ts`, `src/react.ts`, `src/handlebars.ts`, `src/pattern.ts`, `src/providers.ts`. Adding a renderer touches several `Record<RendererKind, …>` maps — see [src/catalog/README.md](src/catalog/README.md).
60
+
61
+ Most `src/<module>/` directories have a `README.md` describing their files and flow. Read it before changing a module, and update it when the module's contract changes.
62
+
63
+ ## Conventions
64
+
65
+ - **Breaking changes** get migration steps in [UPGRADE.md](UPGRADE.md), per major version.
66
+ - **Changelog**: [CHANGELOG.md](CHANGELOG.md) follows Keep a Changelog + SemVer. Entries are written during release prep on the release branch. Do not add or edit entries in feature or fix work unless asked.
67
+ - **Docs style**: README/RECIPES/CHANGELOG use short, plain, imperative sentences (Simplified Technical English style).
68
+
69
+ ### Branches and releases
70
+
71
+ - **Branch names**: `<category>/<kebab-case-topic>`, e.g. `feature/config-profiles`, `bugfix/code-toc-md`, `docs/readme-rewrite`, `chore/remove-mdx`. Categories in use: `feature/`, `bugfix/`, `docs/`, `chore/`, `release/`.
72
+ - **Release branches**: each release has a `release/X.Y.Z` branch. Feature and fix branches start from it and merge back into it through a PR, not into `main`. A major release can first collect work in a long-running `integration/X.Y.Z` branch, from which `release/X.Y.Z` is cut (as for 2.0.0).
73
+ - **Prereleases and final version**: `np` (`npm run release`) runs on the release branch. It publishes prereleases (`X.Y.Z-N`) and then the final `X.Y.Z`; each creates a commit whose message is the bare version and a `vX.Y.Z[-N]` tag. Do not write these commits by hand. Release prep (changelog entries and date, last fixes) is committed directly on the release branch.
74
+ - **`main`** receives only release merges: a PR from `release/X.Y.Z` into `main` ships the release.
75
+
76
+ ### Commit messages
77
+
78
+ Use a Conventional Commit subject with no scope and, normally, no body:
79
+
80
+ ```
81
+ <type>: <description>
82
+ ```
83
+
84
+ - Types: `feat`, `fix`, `chore`, `refactor`, `docs`, `test`, `build`, `ci`, `perf`, `style`.
85
+ - Description: lowercase, imperative, no trailing period. State what the change achieves for the user, not how the code does it; omit file names and implementation details unless needed for clarity.
86
+ - Several distinct changes in one commit: one imperative clause each, separated by `; `. Supporting changes belong to the change they support and are not listed separately.
87
+ - No fixed length limit; the purpose sets the length.
88
+
89
+ Examples from history:
90
+
91
+ ```
92
+ fix: use plain JSON Schema for MCP tool inputs
93
+ feat: move Figma settings to integrations.figma
94
+ refactor: rename define entry point and storage adapter to providers
95
+ feat: accept fromEnv() references in asset storage options and resolve them at request time; remove providerId and record blobs as database or custom
96
+ ```
package/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to
7
7
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## \[2.0.1] - 2026-11-01
10
+
11
+ This release makes catalog items smaller to publish and reports packages that
12
+ are too large for the registry.
13
+
14
+ ### Changed
15
+
16
+ * Client bundles of interactive React previews use the minified production
17
+ build of React. This reduces each bundle by about 1 MB, so components and
18
+ patterns publish within the registry request limit more often. In the local
19
+ workspace, previews no longer show React development warnings. To get them
20
+ back, set `minify: false` and a development `process.env.NODE_ENV` in
21
+ `hooks.clientBuildConfig`, then rebuild with `--force`.
22
+
23
+ ### Fixed
24
+
25
+ * `publish` stops before the upload when a package is larger than the 4.5 MB
26
+ registry limit and names the largest files.
27
+ * A `413` response from the registry now shows a clear "too large" message.
28
+
9
29
  ## \[2.0.0] - 2026-10-16
10
30
 
11
31
  Handoff 2.0 adds a shared registry to the local workspace. You write content in
package/CLAUDE.md CHANGED
@@ -1,7 +1,98 @@
1
- # Instructions
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Local instructions
2
6
 
3
7
  Before starting work, check whether `AGENTS.local.md` exists in the repository root.
4
8
 
5
9
  If it exists, read and follow its instructions in addition to this file. Treat `AGENTS.local.md` as developer-specific local guidance.
6
10
 
7
11
  If it does not exist, proceed normally.
12
+
13
+ ## Project
14
+
15
+ `handoff-app` is an npm package (CLI + library) that turns design tokens (fetched from Figma), components, patterns, and Markdown pages into a Next.js documentation app and distributable artifacts. User-facing docs: [README.md](README.md), [RECIPES.md](RECIPES.md), [docs/cli.md](docs/cli.md), [docs/api.md](docs/api.md), [UPGRADE.md](UPGRADE.md).
16
+
17
+ Requires Node.js >= 22 and npm >= 10.
18
+
19
+ ## Commands
20
+
21
+ ```bash
22
+ npm run build # tsc: compiles src/ (except src/app) to dist/
23
+ npm run lint # next lint over src/app
24
+ npm test # alias for lint; there is no unit test suite
25
+ npx tsc -p src/app/tsconfig.json --noEmit # typecheck the Next.js app (root tsc excludes src/app);
26
+ # errors under a stale src/app/.next/types are not real
27
+ npx drizzle-kit generate # generate a registry migration after editing src/registry/db/schema.ts
28
+ ```
29
+
30
+ The CLI (`dist/cli.js`, bin `handoff-app`) always runs against a **working directory**: the directory that holds `handoff.config.ts` (or `HANDOFF_WORKING_PATH`). This repo is not a Handoff project, so to exercise a change, build here and run the CLI from a consumer project that uses this build (`start`, `dev`, `build`, `fetch`, `publish`, `checkout`, ...). Verify behavior there; `npm run lint` and the two `tsc` runs are the only checks in this repo.
31
+
32
+ Formatting: Prettier (config in `package.json`: 140 cols, single quotes, `es5` trailing commas, Tailwind class sorting) and `.editorconfig` (2 spaces, LF).
33
+
34
+ ## Architecture
35
+
36
+ ### Runtime modes
37
+
38
+ - **Workspace** (default): local files are the source of truth; no database. It is the **only** place that builds content.
39
+ - **Registry**: a deployed catalog backed by PostgreSQL (Drizzle). It serves only what a workspace publishes. It never builds and never reads workspace source.
40
+ - A **connected workspace** publishes to / checks out from a registry; it is not a third mode.
41
+
42
+ `runtime.mode` selects the mode; `build --target registry` sets registry mode in the generated app. Keep registry-only dependencies (Drizzle/Postgres) out of the workspace and static paths — e.g. `src/store/index.ts` deliberately does not re-export the registry store; registry consumers import `@handoff/store/registry` lazily.
43
+
44
+ ### Two TypeScript programs
45
+
46
+ - **`src/` (minus `src/app`)** — the Node CLI/library, CommonJS, compiled by root `tsconfig.json` to `dist/`. `src/index.ts` holds the `Handoff` class (`fetch`, `build`, `publish`, `checkout`, `start`, `dev`); `src/cli.ts` registers the yargs commands in `src/commands/`.
47
+ - **`src/app/`** — the Next.js (pages router) documentation app. It is not compiled by `tsc`; at build/start time `src/app-builder` stages it into `.handoff/<projectId>/` under the package and runs `next build` / `next dev`. It imports the core via the `@handoff/*` alias (→ `src/*`). It also hosts the APIs: docs API, registry API, MCP endpoint (`/api/mcp`), auth, and the AI assistant (`src/app/lib/*`, `src/app/pages/api/*`).
48
+
49
+ ### Main flows
50
+
51
+ - **fetch**: `src/pipeline` — Figma auth → extract → fonts → token transformers; output goes to the working directory's `exported/`.
52
+ - **build/start**: `src/config` loads `handoff.config.*` + profile sidecar + env, then `initRuntimeConfig()` normalizes catalog declarations (`src/catalog`) into component/pattern records; `src/transformers` builds previews with Vite/esbuild per renderer (React, Handlebars, CSF); `src/cache` skips unchanged components (`.handoff/.cache/build-cache.json`, bypass with `--force`); `src/app-builder` builds the app (static → `out/static`, registry → `out/registry`, `--package vercel` → `.vercel/output`). Watch mode uses chokidar + a WebSocket reload server with diff-aware rebuilds.
53
+ - **publish/checkout**: `src/registry` (`publish/`, `checkout/`, `transfer.ts`, `tokens/`, `assets/`, `auth/`) against the registry API in `src/app/lib/registry-api`. Storage is abstracted by `src/store` (filesystem vs. registry implementations of the same interfaces). Asset blobs go through a pluggable storage provider; email and AI providers are also pluggable custom server modules (`handoff-app/providers`).
54
+ - **Migrations**: SQL in `drizzle/` ships with the package and is applied by `handoff-app db:migrate`.
55
+
56
+ ### Config and server-module rules
57
+
58
+ - **Baked vs. request-time values.** For a `fromEnv('NAME')` value, the build keeps only the variable name, and a deployed registry reads the value at request time. Secrets must use `fromEnv()`. These settings change the shape of the build and are baked, so a change needs a rebuild: `runtime.mode`, `runtime.mcp`, `runtime.ai.enabled`, the database driver, the asset storage and email providers, and every custom server module. A new or changed setting must fit one of these two categories.
59
+ - **Custom server modules** (asset storage, email, AI providers) are `.js`/`.mjs` files, relative to the working directory, that import their define helper from `handoff-app/providers`. The registry build stops when a module imports `handoff-app`. The build copies each module and the packages it imports into the registry bundle.
60
+
61
+ Package entry points (`handoff-app`, `/react`, `/handlebars`, `/pattern`, `/providers`) map to `src/index.ts`, `src/react.ts`, `src/handlebars.ts`, `src/pattern.ts`, `src/providers.ts`. Adding a renderer touches several `Record<RendererKind, …>` maps — see [src/catalog/README.md](src/catalog/README.md).
62
+
63
+ Most `src/<module>/` directories have a `README.md` describing their files and flow. Read it before changing a module, and update it when the module's contract changes.
64
+
65
+ ## Conventions
66
+
67
+ - **Breaking changes** get migration steps in [UPGRADE.md](UPGRADE.md), per major version.
68
+ - **Changelog**: [CHANGELOG.md](CHANGELOG.md) follows Keep a Changelog + SemVer. Entries are written during release prep on the release branch. Do not add or edit entries in feature or fix work unless asked.
69
+ - **Docs style**: README/RECIPES/CHANGELOG use short, plain, imperative sentences (Simplified Technical English style).
70
+
71
+ ### Branches and releases
72
+
73
+ - **Branch names**: `<category>/<kebab-case-topic>`, e.g. `feature/config-profiles`, `bugfix/code-toc-md`, `docs/readme-rewrite`, `chore/remove-mdx`. Categories in use: `feature/`, `bugfix/`, `docs/`, `chore/`, `release/`.
74
+ - **Release branches**: each release has a `release/X.Y.Z` branch. Feature and fix branches start from it and merge back into it through a PR, not into `main`. A major release can first collect work in a long-running `integration/X.Y.Z` branch, from which `release/X.Y.Z` is cut (as for 2.0.0).
75
+ - **Prereleases and final version**: `np` (`npm run release`) runs on the release branch. It publishes prereleases (`X.Y.Z-N`) and then the final `X.Y.Z`; each creates a commit whose message is the bare version and a `vX.Y.Z[-N]` tag. Do not write these commits by hand. Release prep (changelog entries and date, last fixes) is committed directly on the release branch.
76
+ - **`main`** receives only release merges: a PR from `release/X.Y.Z` into `main` ships the release.
77
+
78
+ ### Commit messages
79
+
80
+ Use a Conventional Commit subject with no scope and, normally, no body:
81
+
82
+ ```
83
+ <type>: <description>
84
+ ```
85
+
86
+ - Types: `feat`, `fix`, `chore`, `refactor`, `docs`, `test`, `build`, `ci`, `perf`, `style`.
87
+ - Description: lowercase, imperative, no trailing period. State what the change achieves for the user, not how the code does it; omit file names and implementation details unless needed for clarity.
88
+ - Several distinct changes in one commit: one imperative clause each, separated by `; `. Supporting changes belong to the change they support and are not listed separately.
89
+ - No fixed length limit; the purpose sets the length.
90
+
91
+ Examples from history:
92
+
93
+ ```
94
+ fix: use plain JSON Schema for MCP tool inputs
95
+ feat: move Figma settings to integrations.figma
96
+ refactor: rename define entry point and storage adapter to providers
97
+ feat: accept fromEnv() references in asset storage options and resolve them at request time; remove providerId and record blobs as database or custom
98
+ ```
package/README.md CHANGED
@@ -938,6 +938,11 @@ npm run publish -- all --dry-run
938
938
  npm run publish -- catalog --no-build
939
939
  ```
940
940
 
941
+ A publish sends each catalog item or page in one request of at most 4.5 MB.
942
+ An item's package contains its rendered previews, its client bundle, and the
943
+ shared stylesheet and script. A pattern also contains the artifacts of the
944
+ components it uses.
945
+
941
946
  After a publish, reload the registry to see the published items and
942
947
  foundations.
943
948
 
package/dist/cli.js CHANGED
@@ -30,7 +30,7 @@ class HandoffCliError extends Error {
30
30
  * Show the help message
31
31
  */
32
32
  const showVersion = () => {
33
- return 'Handoff App - 2.0.0';
33
+ return 'Handoff App - 2.0.1';
34
34
  };
35
35
  /**
36
36
  * Define a CLI error
@@ -4,6 +4,10 @@ exports.describePublishError = exports.describeUploadFailure = void 0;
4
4
  const client_1 = require("../client");
5
5
  /** Map registry error codes to the actionable messages used by publish commands. */
6
6
  const describeUploadFailure = (error, registryUrl, rejectedPayload) => {
7
+ // An oversized body is rejected by the platform or body parser, so the response has no error envelope.
8
+ if (error.status === 413) {
9
+ return `The registry at ${registryUrl} rejected the ${rejectedPayload} as too large (413). Reduce the size of its largest files and publish again.`;
10
+ }
7
11
  switch (error.code) {
8
12
  case 'runtime_mode_conflict':
9
13
  return `The registry at ${registryUrl} is not running in registry mode, so it cannot accept publishes: ${error.message}`;
@@ -160,6 +160,7 @@ const publishEntity = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, f
160
160
  }
161
161
  const pkg = yield (0, package_1.buildPublishPackage)(handoff, kind, id);
162
162
  (0, package_1.assertRequiredArtifactsPresent)(pkg);
163
+ (0, package_1.assertPackageWithinUploadLimit)(kind, id, pkg);
163
164
  const contents = `${pkg.artifacts.length} artifact(s), ${pkg.files.length} source file(s)`;
164
165
  if (!client) {
165
166
  logger_1.Logger.info(`Would publish ${kind} "${id}" (${contents}).`);
@@ -251,6 +252,7 @@ const uploadEntities = (handoff, transport, kind, ids) => __awaiter(void 0, void
251
252
  try {
252
253
  const pkg = yield (0, package_1.buildPublishPackage)(handoff, kind, id);
253
254
  (0, package_1.assertRequiredArtifactsPresent)(pkg);
255
+ (0, package_1.assertPackageWithinUploadLimit)(kind, id, pkg);
254
256
  const remoteHash = remoteHashById.get(id);
255
257
  const localHash = entityHash(pkg.build);
256
258
  if (!handoff.force && remoteHash && localHash && remoteHash === localHash) {
@@ -23,5 +23,11 @@ export declare class PublishPackageError extends Error {
23
23
  * upload, so a publish fails locally (no network call) on a missing required artifact.
24
24
  */
25
25
  export declare const assertRequiredArtifactsPresent: (pkg: TransferPackage) => void;
26
+ /**
27
+ * The largest publish request body the registry accepts. Vercel rejects a larger function body with
28
+ * `413 FUNCTION_PAYLOAD_TOO_LARGE`, and the transfer route's `bodyParser.sizeLimit` matches it.
29
+ */
30
+ export declare const MAX_PUBLISH_BODY_BYTES = 4500000;
31
+ export declare const assertPackageWithinUploadLimit: (kind: TransferEntityKind, id: string, pkg: TransferPackage) => void;
26
32
  /** Assemble the publish package for an entity from the generated `public/api` artifacts. */
27
33
  export declare const buildPublishPackage: (handoff: Handoff, kind: TransferEntityKind, id: string) => Promise<TransferPackage>;
@@ -37,7 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
- exports.buildPublishPackage = exports.assertRequiredArtifactsPresent = exports.PublishPackageError = void 0;
40
+ exports.buildPublishPackage = exports.assertPackageWithinUploadLimit = exports.MAX_PUBLISH_BODY_BYTES = exports.assertRequiredArtifactsPresent = exports.PublishPackageError = void 0;
41
41
  const fs_extra_1 = __importDefault(require("fs-extra"));
42
42
  const path_1 = __importDefault(require("path"));
43
43
  const source_files_1 = require("../../store/source-files");
@@ -269,6 +269,26 @@ const assertRequiredArtifactsPresent = (pkg) => {
269
269
  }
270
270
  };
271
271
  exports.assertRequiredArtifactsPresent = assertRequiredArtifactsPresent;
272
+ /**
273
+ * The largest publish request body the registry accepts. Vercel rejects a larger function body with
274
+ * `413 FUNCTION_PAYLOAD_TOO_LARGE`, and the transfer route's `bodyParser.sizeLimit` matches it.
275
+ */
276
+ exports.MAX_PUBLISH_BODY_BYTES = 4500000;
277
+ const formatMegabytes = (bytes) => `${(bytes / 1000000).toFixed(1)} MB`;
278
+ const assertPackageWithinUploadLimit = (kind, id, pkg) => {
279
+ const size = Buffer.byteLength(JSON.stringify(pkg));
280
+ if (size <= exports.MAX_PUBLISH_BODY_BYTES)
281
+ return;
282
+ const largest = [...pkg.artifacts, ...pkg.files]
283
+ .map((entry) => ({ path: entry.path, size: Buffer.byteLength(entry.content) }))
284
+ .sort((a, b) => b.size - a.size)
285
+ .slice(0, 3)
286
+ .map((entry) => ` - ${entry.path} (${formatMegabytes(entry.size)})`)
287
+ .join('\n');
288
+ throw new PublishPackageError(`The ${kind} "${id}" package is ${formatMegabytes(size)}; the registry accepts at most ` +
289
+ `${formatMegabytes(exports.MAX_PUBLISH_BODY_BYTES)} per publish. Largest entries:\n${largest}`);
290
+ };
291
+ exports.assertPackageWithinUploadLimit = assertPackageWithinUploadLimit;
272
292
  /** Assemble the publish package for an entity from the generated `public/api` artifacts. */
273
293
  const buildPublishPackage = (handoff, kind, id) => kind === 'component'
274
294
  ? buildComponentPackage(handoff, id)
@@ -15,6 +15,10 @@ export declare const STYLE_IMPORT_LOADERS: Record<string, esbuild.Loader>;
15
15
  export declare const DEFAULT_SSR_BUILD_CONFIG: esbuild.BuildOptions;
16
16
  /**
17
17
  * Default esbuild configuration for client builds
18
+ *
19
+ * Production React, minified: each client bundle carries its own React copy, and a publish uploads it
20
+ * with the component and with every pattern that uses it. A development build quickly pushes a pattern
21
+ * past serverless request body limits.
18
22
  */
19
23
  export declare const DEFAULT_CLIENT_BUILD_CONFIG: esbuild.BuildOptions;
20
24
  /** Apply after a consumer hook: a hook that replaces `loader` would otherwise drop these. */
@@ -45,6 +45,10 @@ exports.DEFAULT_SSR_BUILD_CONFIG = {
45
45
  };
46
46
  /**
47
47
  * Default esbuild configuration for client builds
48
+ *
49
+ * Production React, minified: each client bundle carries its own React copy, and a publish uploads it
50
+ * with the component and with every pattern that uses it. A development build quickly pushes a pattern
51
+ * past serverless request body limits.
48
52
  */
49
53
  exports.DEFAULT_CLIENT_BUILD_CONFIG = {
50
54
  bundle: true,
@@ -53,7 +57,8 @@ exports.DEFAULT_CLIENT_BUILD_CONFIG = {
53
57
  platform: 'browser',
54
58
  jsx: 'automatic',
55
59
  sourcemap: false,
56
- minify: false,
60
+ minify: true,
61
+ define: { 'process.env.NODE_ENV': '"production"' },
57
62
  loader: exports.STYLE_IMPORT_LOADERS,
58
63
  };
59
64
  /** Apply after a consumer hook: a hook that replaces `loader` would otherwise drop these. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handoff-app",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Automated documentation toolchain for building client side documentation from figma",
5
5
  "author ": {
6
6
  "name": "Convertiv",
package/src/cli.ts CHANGED
@@ -21,7 +21,7 @@ class HandoffCliError extends Error {
21
21
  * Show the help message
22
22
  */
23
23
  const showVersion = () => {
24
- return 'Handoff App - 2.0.0';
24
+ return 'Handoff App - 2.0.1';
25
25
  };
26
26
 
27
27
  /**
@@ -8,6 +8,10 @@ export const describeUploadFailure = (
8
8
  registryUrl: string,
9
9
  rejectedPayload: RejectedPublishPayload
10
10
  ): string => {
11
+ // An oversized body is rejected by the platform or body parser, so the response has no error envelope.
12
+ if (error.status === 413) {
13
+ return `The registry at ${registryUrl} rejected the ${rejectedPayload} as too large (413). Reduce the size of its largest files and publish again.`;
14
+ }
11
15
  switch (error.code) {
12
16
  case 'runtime_mode_conflict':
13
17
  return `The registry at ${registryUrl} is not running in registry mode, so it cannot accept publishes: ${error.message}`;
@@ -21,7 +21,7 @@ import { loginCommandHint, resolveAuthenticatedRegistryConnection, unusedLoginNo
21
21
  import type { EntitySummary, TransferEntityKind } from '../transfer';
22
22
  import { quoteIds, selectIds, splitCatalogIds } from '../selection';
23
23
  import { describePublishError, describeUploadFailure } from './errors';
24
- import { assertRequiredArtifactsPresent, buildPublishPackage, PublishPackageError } from './package';
24
+ import { assertPackageWithinUploadLimit, assertRequiredArtifactsPresent, buildPublishPackage, PublishPackageError } from './package';
25
25
 
26
26
  /** A connected-workspace configuration or precondition failure surfaced to the CLI. */
27
27
  export class PublishError extends Error {
@@ -173,6 +173,7 @@ export const publishEntity = async (handoff: Handoff, kind: TransferEntityKind,
173
173
 
174
174
  const pkg = await buildPublishPackage(handoff, kind, id);
175
175
  assertRequiredArtifactsPresent(pkg);
176
+ assertPackageWithinUploadLimit(kind, id, pkg);
176
177
 
177
178
  const contents = `${pkg.artifacts.length} artifact(s), ${pkg.files.length} source file(s)`;
178
179
  if (!client) {
@@ -274,6 +275,7 @@ const uploadEntities = async (handoff: Handoff, transport: PublishTransport, kin
274
275
  try {
275
276
  const pkg = await buildPublishPackage(handoff, kind, id);
276
277
  assertRequiredArtifactsPresent(pkg);
278
+ assertPackageWithinUploadLimit(kind, id, pkg);
277
279
 
278
280
  const remoteHash = remoteHashById.get(id);
279
281
  const localHash = entityHash(pkg.build);
@@ -285,6 +285,30 @@ export const assertRequiredArtifactsPresent = (pkg: TransferPackage): void => {
285
285
  }
286
286
  };
287
287
 
288
+ /**
289
+ * The largest publish request body the registry accepts. Vercel rejects a larger function body with
290
+ * `413 FUNCTION_PAYLOAD_TOO_LARGE`, and the transfer route's `bodyParser.sizeLimit` matches it.
291
+ */
292
+ export const MAX_PUBLISH_BODY_BYTES = 4_500_000;
293
+
294
+ const formatMegabytes = (bytes: number): string => `${(bytes / 1_000_000).toFixed(1)} MB`;
295
+
296
+ export const assertPackageWithinUploadLimit = (kind: TransferEntityKind, id: string, pkg: TransferPackage): void => {
297
+ const size = Buffer.byteLength(JSON.stringify(pkg));
298
+ if (size <= MAX_PUBLISH_BODY_BYTES) return;
299
+
300
+ const largest = [...pkg.artifacts, ...pkg.files]
301
+ .map((entry) => ({ path: entry.path, size: Buffer.byteLength(entry.content) }))
302
+ .sort((a, b) => b.size - a.size)
303
+ .slice(0, 3)
304
+ .map((entry) => ` - ${entry.path} (${formatMegabytes(entry.size)})`)
305
+ .join('\n');
306
+ throw new PublishPackageError(
307
+ `The ${kind} "${id}" package is ${formatMegabytes(size)}; the registry accepts at most ` +
308
+ `${formatMegabytes(MAX_PUBLISH_BODY_BYTES)} per publish. Largest entries:\n${largest}`
309
+ );
310
+ };
311
+
288
312
  /** Assemble the publish package for an entity from the generated `public/api` artifacts. */
289
313
  export const buildPublishPackage = (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<TransferPackage> =>
290
314
  kind === 'component'
@@ -31,6 +31,10 @@ export const DEFAULT_SSR_BUILD_CONFIG: esbuild.BuildOptions = {
31
31
 
32
32
  /**
33
33
  * Default esbuild configuration for client builds
34
+ *
35
+ * Production React, minified: each client bundle carries its own React copy, and a publish uploads it
36
+ * with the component and with every pattern that uses it. A development build quickly pushes a pattern
37
+ * past serverless request body limits.
34
38
  */
35
39
  export const DEFAULT_CLIENT_BUILD_CONFIG: esbuild.BuildOptions = {
36
40
  bundle: true,
@@ -39,7 +43,8 @@ export const DEFAULT_CLIENT_BUILD_CONFIG: esbuild.BuildOptions = {
39
43
  platform: 'browser',
40
44
  jsx: 'automatic',
41
45
  sourcemap: false,
42
- minify: false,
46
+ minify: true,
47
+ define: { 'process.env.NODE_ENV': '"production"' },
43
48
  loader: STYLE_IMPORT_LOADERS,
44
49
  };
45
50