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 +90 -1
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +92 -1
- package/README.md +5 -0
- package/dist/cli.js +1 -1
- package/dist/registry/publish/errors.js +4 -0
- package/dist/registry/publish/index.js +2 -0
- package/dist/registry/publish/package.d.ts +6 -0
- package/dist/registry/publish/package.js +21 -1
- package/dist/transformers/utils/build.d.ts +4 -0
- package/dist/transformers/utils/build.js +6 -1
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/registry/publish/errors.ts +4 -0
- package/src/registry/publish/index.ts +3 -1
- package/src/registry/publish/package.ts +24 -0
- package/src/transformers/utils/build.ts +6 -1
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,96 @@
|
|
|
1
|
-
#
|
|
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
|
-
#
|
|
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
|
@@ -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:
|
|
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
package/src/cli.ts
CHANGED
|
@@ -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:
|
|
46
|
+
minify: true,
|
|
47
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
43
48
|
loader: STYLE_IMPORT_LOADERS,
|
|
44
49
|
};
|
|
45
50
|
|