handoff-app 2.0.0-0 → 2.0.0-2

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.
Files changed (91) hide show
  1. package/README.md +39 -17
  2. package/config/docs/foundations/colors/guidelines.md +9 -0
  3. package/config/docs/foundations/effects/guidelines.md +9 -0
  4. package/config/docs/foundations/effects/resources.md +9 -0
  5. package/config/docs/foundations/grid.md +10 -0
  6. package/config/docs/foundations/icons/guidelines.md +9 -0
  7. package/config/docs/foundations/images/guidelines.md +9 -0
  8. package/config/docs/foundations/images/resources.md +9 -0
  9. package/config/docs/foundations/images.md +10 -0
  10. package/config/docs/foundations/logo/resources.md +9 -0
  11. package/config/docs/foundations/typography/guidelines.md +9 -0
  12. package/config/docs/foundations/typography/resources.md +9 -0
  13. package/config/docs/foundations.md +5 -3
  14. package/dist/app-builder/build.js +89 -46
  15. package/dist/app-builder/client-config.js +5 -5
  16. package/dist/app-builder/paths.js +23 -1
  17. package/dist/app-builder/static-export.js +25 -0
  18. package/dist/app-builder/vercel-output.js +2 -2
  19. package/dist/artifacts/index.d.ts +1 -1
  20. package/dist/artifacts/index.js +4 -1
  21. package/dist/artifacts/url.d.ts +18 -0
  22. package/dist/artifacts/url.js +31 -1
  23. package/dist/cli/create.js +48 -3
  24. package/dist/commands/checkout/index.d.ts +6 -6
  25. package/dist/commands/checkout/index.js +8 -12
  26. package/dist/commands/publish/index.d.ts +6 -5
  27. package/dist/commands/publish/index.js +8 -11
  28. package/dist/commands/utils.d.ts +6 -0
  29. package/dist/commands/utils.js +9 -1
  30. package/dist/index.d.ts +12 -8
  31. package/dist/index.js +26 -10
  32. package/dist/registry/asset-storage/resolve.d.ts +1 -1
  33. package/dist/registry/asset-storage/resolve.js +1 -1
  34. package/dist/registry/checkout/index.d.ts +10 -5
  35. package/dist/registry/checkout/index.js +62 -11
  36. package/dist/registry/client.d.ts +2 -1
  37. package/dist/registry/client.js +8 -0
  38. package/dist/registry/db/client.d.ts +9 -9
  39. package/dist/registry/db/client.js +7 -7
  40. package/dist/registry/db/{adapter.d.ts → driver.d.ts} +16 -14
  41. package/dist/registry/db/{adapter.js → driver.js} +17 -15
  42. package/dist/registry/db/index.d.ts +1 -1
  43. package/dist/registry/db/index.js +9 -9
  44. package/dist/registry/db/migrate.d.ts +1 -1
  45. package/dist/registry/db/migrate.js +5 -5
  46. package/dist/registry/publish/index.d.ts +8 -1
  47. package/dist/registry/publish/index.js +88 -2
  48. package/dist/registry/transfer.d.ts +13 -0
  49. package/dist/types/config.d.ts +12 -8
  50. package/drizzle.config.ts +1 -1
  51. package/package.json +1 -1
  52. package/src/app/components/Foundations/DisplayLogo.tsx +8 -2
  53. package/src/app/lib/docs-api/runtime-config.ts +10 -10
  54. package/src/app/lib/registry-api/transfer.ts +29 -0
  55. package/src/app/lib/registry-connection.ts +2 -2
  56. package/src/app/next.config.mjs +1 -1
  57. package/src/app/pages/api/registry/transfer/[kind]/index.ts +21 -0
  58. package/src/app/pages/assets/logos.tsx +4 -1
  59. package/src/app/pages/foundations/icons/index.tsx +7 -5
  60. package/src/app/pages/foundations/icons/{[name] → library/[name]}/index.tsx +8 -8
  61. package/src/app/pages/foundations/logo.tsx +5 -4
  62. package/src/app-builder/build.ts +75 -55
  63. package/src/app-builder/client-config.ts +3 -3
  64. package/src/app-builder/paths.ts +25 -1
  65. package/src/app-builder/static-export.ts +27 -0
  66. package/src/app-builder/vercel-output.ts +2 -2
  67. package/src/artifacts/index.ts +8 -1
  68. package/src/artifacts/url.ts +32 -0
  69. package/src/cli/create.ts +58 -3
  70. package/src/commands/checkout/index.ts +10 -15
  71. package/src/commands/publish/index.ts +10 -14
  72. package/src/commands/utils.ts +12 -0
  73. package/src/index.ts +26 -12
  74. package/src/registry/asset-storage/resolve.ts +1 -1
  75. package/src/registry/checkout/index.ts +73 -11
  76. package/src/registry/client.ts +7 -1
  77. package/src/registry/db/client.ts +12 -12
  78. package/src/registry/db/{adapter.ts → driver.ts} +19 -17
  79. package/src/registry/db/index.ts +4 -4
  80. package/src/registry/db/migrate.ts +5 -5
  81. package/src/registry/publish/index.ts +100 -2
  82. package/src/registry/transfer.ts +14 -0
  83. package/src/types/config.ts +12 -8
  84. package/templates/default/README.md.tpl +40 -1
  85. package/templates/default/gitignore.tpl +18 -3
  86. package/templates/default/handoff.config.js.tpl +14 -0
  87. package/templates/default/handoff.config.ts.tpl +14 -0
  88. package/templates/default/handoff.config.with-examples.js.tpl +14 -0
  89. package/templates/default/handoff.config.with-examples.ts.tpl +14 -0
  90. package/templates/default/package.json.tpl +2 -1
  91. package/templates/default/vercel.json.tpl +4 -0
@@ -2,7 +2,7 @@
2
2
  * Registry migration runner.
3
3
  *
4
4
  * `handoff-app db:migrate` reads the consuming project's config + DB env vars, resolves the same
5
- * `runtime.registry.database.adapter` used at runtime/build, and applies the package-owned
5
+ * `runtime.registry.database.driver` used at runtime/build, and applies the package-owned
6
6
  * migration set with the bundled Drizzle migrator. Migrations are resolved relative to the
7
7
  * installed package (not the consuming project), so the schema is versioned with the tool. This
8
8
  * runs independently of `build`.
@@ -12,7 +12,7 @@ import fs from 'fs-extra';
12
12
  import path from 'path';
13
13
  import type Handoff from '../../';
14
14
  import { Logger } from '../../utils/logger';
15
- import { resolveRegistryDatabase } from './adapter';
15
+ import { resolveRegistryDatabase } from './driver';
16
16
  import { createRegistryDbConnection } from './client';
17
17
 
18
18
  /**
@@ -33,7 +33,7 @@ export const runRegistryMigrations = async (handoff: Handoff): Promise<void> =>
33
33
  throw new Error('Handoff is not initialized; cannot resolve registry database configuration.');
34
34
  }
35
35
 
36
- const { adapter, databaseUrlEnv, connectionString } = resolveRegistryDatabase(handoff.config);
36
+ const { driver, databaseUrlEnv, connectionString } = resolveRegistryDatabase(handoff.config);
37
37
 
38
38
  const migrationsFolder = getMigrationsFolder(handoff);
39
39
  const journalPath = path.join(migrationsFolder, 'meta', '_journal.json');
@@ -44,9 +44,9 @@ export const runRegistryMigrations = async (handoff: Handoff): Promise<void> =>
44
44
  );
45
45
  }
46
46
 
47
- Logger.info(`Running registry migrations (adapter: "${adapter}", database url from "${databaseUrlEnv}").`);
47
+ Logger.info(`Running registry migrations (driver: "${driver}", database url from "${databaseUrlEnv}").`);
48
48
 
49
- const connection = await createRegistryDbConnection({ adapter, connectionString });
49
+ const connection = await createRegistryDbConnection({ driver, connectionString });
50
50
  try {
51
51
  await connection.migrate(migrationsFolder);
52
52
  Logger.success('Registry migrations applied successfully.');
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Connected-workspace publish orchestration.
3
3
  *
4
- * `publish <component|pattern|page> <id>` prepares one entity and uploads its record, source files,
4
+ * `publish <components|patterns|pages> <id>` prepares one entity and uploads its record, source files,
5
5
  * applicable rendered artifacts, and build metadata through the shared registry client.
6
6
  *
7
7
  * The build is targeted: a component builds the global artifacts + that component; a pattern builds
@@ -17,7 +17,7 @@ import { processPatterns } from '../../transformers/preview/pattern/builder';
17
17
  import { Logger } from '../../utils/logger';
18
18
  import { createRegistryClient, RegistryClientError } from '../client';
19
19
  import { resolveRegistryConnection } from '../connection';
20
- import type { TransferEntityKind } from '../transfer';
20
+ import type { EntitySummary, TransferEntityKind } from '../transfer';
21
21
  import { describeUploadFailure } from './errors';
22
22
  import { assertRequiredArtifactsPresent, buildPublishPackage, PublishPackageError } from './package';
23
23
 
@@ -126,3 +126,101 @@ export const publishEntity = async (handoff: Handoff, kind: TransferEntityKind,
126
126
 
127
127
  Logger.success(`Published ${kind} "${id}" to the registry.`);
128
128
  };
129
+
130
+ /** The published-content hash a bulk publish compares for skip-unchanged (artifact for rendered kinds, source for pages). */
131
+ const entityHash = (value: { artifactHash?: string; sourceHash?: string }): string | undefined => value.artifactHash ?? value.sourceHash;
132
+
133
+ /**
134
+ * Run one build covering every entity of the kind, so a bulk publish builds once instead of per id.
135
+ * Pages carry no rendered artifacts. Components need the global artifacts + all components; patterns
136
+ * additionally need all patterns composed (each references pre-built component HTML).
137
+ */
138
+ const runBulkBuild = async (handoff: Handoff, kind: TransferEntityKind): Promise<void> => {
139
+ if (kind === 'page') {
140
+ return;
141
+ }
142
+ await buildMainJS(handoff);
143
+ await buildMainCss(handoff);
144
+ await processComponents(handoff);
145
+ if (kind === 'pattern') {
146
+ await processPatterns(handoff);
147
+ }
148
+ };
149
+
150
+ /** List every declared entity id of the kind from the workspace store. */
151
+ const listWorkspaceEntityIds = async (handoff: Handoff, kind: TransferEntityKind): Promise<string[]> => {
152
+ const store = kind === 'component' ? handoff.store.components : kind === 'pattern' ? handoff.store.patterns : handoff.store.pages;
153
+ return (await store.list()).map((entity) => entity.id);
154
+ };
155
+
156
+ /**
157
+ * Publish every component, pattern, or page declared in this connected workspace. Builds the kind
158
+ * once, assembles each entity's package, skips entities whose content hash already matches the
159
+ * registry (unless `--force`), and reports published/unchanged/failed counts. A per-entity failure is
160
+ * collected and never aborts the rest; the run throws at the end if any entity failed.
161
+ */
162
+ export const publishEntities = async (handoff: Handoff, kind: TransferEntityKind): Promise<void> => {
163
+ const connection = resolveConnectionOrThrow(handoff);
164
+
165
+ Logger.info(`Building ${kind}s for publish…`);
166
+ await runBulkBuild(handoff, kind);
167
+
168
+ const ids = await listWorkspaceEntityIds(handoff, kind);
169
+ if (ids.length === 0) {
170
+ Logger.success(`No ${kind}s are declared in this workspace; nothing to publish.`);
171
+ return;
172
+ }
173
+
174
+ const client = createRegistryClient({ baseUrl: connection.url, accessToken: connection.accessToken });
175
+ let remote: EntitySummary[] = [];
176
+ if (!handoff.force) {
177
+ try {
178
+ remote = await client.listEntities(kind);
179
+ } catch {
180
+ // If the summary listing fails we simply publish everything (the server still overwrites in place).
181
+ Logger.info(`Could not read current registry ${kind}s; publishing all.`);
182
+ }
183
+ }
184
+ const remoteHashById = new Map(remote.map((entry) => [entry.id, entityHash(entry)]));
185
+
186
+ let published = 0;
187
+ let unchanged = 0;
188
+ const failed: { id: string; message: string }[] = [];
189
+
190
+ for (const id of ids) {
191
+ try {
192
+ const pkg = await buildPublishPackage(handoff, kind, id);
193
+ assertRequiredArtifactsPresent(pkg);
194
+
195
+ const remoteHash = remoteHashById.get(id);
196
+ const localHash = entityHash(pkg.build);
197
+ if (!handoff.force && remoteHash && localHash && remoteHash === localHash) {
198
+ unchanged += 1;
199
+ Logger.info(`Unchanged: ${id}`);
200
+ continue;
201
+ }
202
+
203
+ await client.publish(kind, id, pkg);
204
+ published += 1;
205
+ Logger.info(`Published: ${id} (${pkg.artifacts.length} artifact(s), ${pkg.files.length} source file(s))`);
206
+ } catch (error) {
207
+ const message =
208
+ error instanceof RegistryClientError
209
+ ? describeUploadFailure(error, connection.url, 'package')
210
+ : error instanceof Error
211
+ ? error.message
212
+ : String(error);
213
+ failed.push({ id, message });
214
+ }
215
+ }
216
+
217
+ Logger.success(
218
+ `${kind[0].toUpperCase()}${kind.slice(1)}s publish complete — ${published} published, ${unchanged} unchanged${failed.length ? `, ${failed.length} failed` : ''}.`
219
+ );
220
+ if (failed.length > 0) {
221
+ for (const failure of failed) {
222
+ Logger.error(` - ${failure.id}: ${failure.message}`);
223
+ }
224
+ throw new PublishError(`${failed.length} ${kind}(s) failed to publish.`);
225
+ }
226
+ };
@@ -80,3 +80,17 @@ export interface CheckoutPayload {
80
80
  item: Record<string, unknown>;
81
81
  files: TransferFile[];
82
82
  }
83
+
84
+ /**
85
+ * A published entity's summary from `GET /api/registry/transfer/{component|pattern|page}`. Carries
86
+ * the build hashes so a connected workspace can skip unchanged entities on a bulk publish and
87
+ * enumerate published ids for a bulk checkout. Components/patterns key on `artifactHash`; pages key
88
+ * on `sourceHash`.
89
+ */
90
+ export interface EntitySummary {
91
+ id: string;
92
+ kind: TransferEntityKind;
93
+ status?: string;
94
+ artifactHash?: string;
95
+ sourceHash?: string;
96
+ }
@@ -138,8 +138,12 @@ export interface HandoffRuntimeConfig {
138
138
  /** Name of the env var holding the registry API token. @default "HANDOFF_REGISTRY_API_TOKEN" */
139
139
  apiTokenEnv?: string;
140
140
  database?: {
141
- /** @default "pg" */
142
- adapter?: 'pg' | 'neon';
141
+ /**
142
+ * PostgreSQL is the supported database. `driver` selects the built-in connection driver — how
143
+ * to connect, not which engine — over the same package-owned Postgres schema and migrations.
144
+ * @default "pg"
145
+ */
146
+ driver?: 'pg' | 'neon';
143
147
  };
144
148
  /**
145
149
  * Where published asset blobs (icons/logos/fonts/sprites/archives) are stored. Defaults to the
@@ -214,18 +218,18 @@ export interface Config {
214
218
  */
215
219
  entries?: ConfigEntries;
216
220
  /**
217
- * Configuration for asset zip file download links
218
- * @default { icons: "/icons.zip", logos: "/logos.zip" }
221
+ * Override URLs for the asset zip download links. When unset, each link defaults to the
222
+ * basePath-aware asset route `{basePath}/api/docs/assets/{collection}/{collection}.zip`, served by
223
+ * the docs read API in every runtime mode (from the DB in registry mode, from statically
224
+ * materialized files in a static export).
219
225
  */
220
226
  assets_zip_links?: {
221
227
  /**
222
- * Path to the icons zip file
223
- * @default "/icons.zip"
228
+ * Override URL for the icons zip. Defaults to `/api/docs/assets/icons/icons.zip` (basePath-aware).
224
229
  */
225
230
  icons?: string;
226
231
  /**
227
- * Path to the logos zip file
228
- * @default "/logos.zip"
232
+ * Override URL for the logos zip. Defaults to `/api/docs/assets/logos/logos.zip` (basePath-aware).
229
233
  */
230
234
  logos?: string;
231
235
  };
@@ -12,4 +12,43 @@ A design system documentation site powered by Handoff App.
12
12
 
13
13
  - `npm run fetch` - Fetch design data from Figma
14
14
  - `npm run start` - Start the documentation server
15
- - `npm run build` - Build for production
15
+ - `npm run dev` - Start the server in watch mode
16
+ - `npm run build` - Build the documentation site
17
+ - `npm run db:migrate` - Apply registry database migrations
18
+
19
+ ## Deploying
20
+
21
+ Handoff builds two deployment shapes:
22
+
23
+ - **Static** — plain files served by any static file server or CDN. Build with
24
+ `handoff-app build --target static`. The simplest to deploy and run.
25
+ - **Registry** — a live, database-backed Node app that serves a shared catalog.
26
+ Build with `handoff-app build --target registry`, run it on any Node host, and
27
+ provide a Postgres `DATABASE_URL`. The more capable, actively evolving option,
28
+ and the recommended path for a design system you plan to grow.
29
+
30
+ Add `--package vercel` to either build to produce a `.vercel/output` bundle. If
31
+ you scaffolded a `vercel.json`, it runs the build for the deployment you chose;
32
+ edit its `buildCommand` to switch.
33
+
34
+ ## Registry mode
35
+
36
+ New projects run in **workspace mode**, which needs no database and is the default
37
+ for building and iterating locally. The database-backed **registry mode** lets you
38
+ publish and share a design system across projects and is where Handoff's
39
+ capabilities are expanding, so it's the recommended path as your design system
40
+ grows. See the Handoff docs for setting up the database and publishing.
41
+
42
+ ### Database migrations
43
+
44
+ A registry deployment stores its catalog in Postgres, so its schema has to be
45
+ migrated before you deploy new code. Run this against the target database:
46
+
47
+ ```bash
48
+ npm run db:migrate
49
+ ```
50
+
51
+ It reads `DATABASE_URL` from the environment and applies the migrations bundled
52
+ with Handoff. It runs on its own, separate from build and start, so use it as a
53
+ release step (for example in CI/CD). Static deployments don't use a database and
54
+ can skip it.
@@ -1,7 +1,22 @@
1
+ # Dependencies
1
2
  node_modules/
2
- .env
3
+
4
+ # Build output
3
5
  dist/
4
- .next/
5
6
  out/
6
- .DS_Store
7
+ .next/
8
+
9
+ # Generated build artifacts
10
+ public/api
11
+
12
+ # Environment
13
+ .env
14
+
15
+ # Deployment
16
+ .vercel
17
+
18
+ # Logs
7
19
  *.log
20
+
21
+ # OS files
22
+ .DS_Store
@@ -61,6 +61,20 @@ const config = {
61
61
  // patterns: ["patterns"],
62
62
  // },
63
63
 
64
+ // Registry deployment settings, used by `build --target registry` and `db:migrate`.
65
+ // These are only needed when publishing to a hosted registry. The default workspace mode
66
+ // doesn't use a database, so these should stay commented out unless deploying one.
67
+ // Handoff runs on PostgreSQL, and `driver` just picks the connection driver to use, not
68
+ // the database engine. Secrets are referenced by env var name and never written here.
69
+ // runtime: {
70
+ // mode: "registry",
71
+ // registry: {
72
+ // databaseUrlEnv: "DATABASE_URL",
73
+ // apiTokenEnv: "HANDOFF_REGISTRY_API_TOKEN",
74
+ // database: { driver: "pg" }, // 'pg' | 'neon'
75
+ // },
76
+ // },
77
+
64
78
  // Optional handoff-docgen settings used by component docs generation
65
79
  // reactDocgen: {
66
80
  // maxDepth: 7,
@@ -59,6 +59,20 @@ export default defineConfig({
59
59
  // patterns: ["patterns"],
60
60
  // },
61
61
 
62
+ // Registry deployment settings, used by `build --target registry` and `db:migrate`.
63
+ // These are only needed when publishing to a hosted registry. The default workspace mode
64
+ // doesn't use a database, so these should stay commented out unless deploying one.
65
+ // Handoff runs on PostgreSQL, and `driver` just picks the connection driver to use, not
66
+ // the database engine. Secrets are referenced by env var name and never written here.
67
+ // runtime: {
68
+ // mode: "registry",
69
+ // registry: {
70
+ // databaseUrlEnv: "DATABASE_URL",
71
+ // apiTokenEnv: "HANDOFF_REGISTRY_API_TOKEN",
72
+ // database: { driver: "pg" }, // 'pg' | 'neon'
73
+ // },
74
+ // },
75
+
62
76
  // Optional handoff-docgen settings used by component docs generation
63
77
  // reactDocgen: {
64
78
  // maxDepth: 7,
@@ -60,6 +60,20 @@ const config = {
60
60
  // patterns: [],
61
61
  },
62
62
 
63
+ // Registry deployment settings, used by `build --target registry` and `db:migrate`.
64
+ // These are only needed when publishing to a hosted registry. The default workspace mode
65
+ // doesn't use a database, so these should stay commented out unless deploying one.
66
+ // Handoff runs on PostgreSQL, and `driver` just picks the connection driver to use, not
67
+ // the database engine. Secrets are referenced by env var name and never written here.
68
+ // runtime: {
69
+ // mode: "registry",
70
+ // registry: {
71
+ // databaseUrlEnv: "DATABASE_URL",
72
+ // apiTokenEnv: "HANDOFF_REGISTRY_API_TOKEN",
73
+ // database: { driver: "pg" }, // 'pg' | 'neon'
74
+ // },
75
+ // },
76
+
63
77
  // Optional handoff-docgen settings used by component docs generation
64
78
  // reactDocgen: {
65
79
  // maxDepth: 7,
@@ -58,6 +58,20 @@ export default defineConfig({
58
58
  // patterns: [],
59
59
  },
60
60
 
61
+ // Registry deployment settings, used by `build --target registry` and `db:migrate`.
62
+ // These are only needed when publishing to a hosted registry. The default workspace mode
63
+ // doesn't use a database, so these should stay commented out unless deploying one.
64
+ // Handoff runs on PostgreSQL, and `driver` just picks the connection driver to use, not
65
+ // the database engine. Secrets are referenced by env var name and never written here.
66
+ // runtime: {
67
+ // mode: "registry",
68
+ // registry: {
69
+ // databaseUrlEnv: "DATABASE_URL",
70
+ // apiTokenEnv: "HANDOFF_REGISTRY_API_TOKEN",
71
+ // database: { driver: "pg" }, // 'pg' | 'neon'
72
+ // },
73
+ // },
74
+
61
75
  // Optional handoff-docgen settings used by component docs generation
62
76
  // reactDocgen: {
63
77
  // maxDepth: 7,
@@ -6,8 +6,9 @@
6
6
  "scripts": {
7
7
  "start": "handoff-app start",
8
8
  "dev": "handoff-app dev",
9
+ "fetch": "handoff-app fetch",
9
10
  "build": "handoff-app build",
10
- "fetch": "handoff-app fetch"
11
+ "db:migrate": "handoff-app db:migrate"
11
12
  },
12
13
  "dependencies": {},
13
14
  "devDependencies": {},
@@ -0,0 +1,4 @@
1
+ {
2
+ "framework": null,
3
+ "buildCommand": "{{vercelBuildCommand}}"
4
+ }