handoff-app 1.2.2-6 → 2.0.0-0
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/Changelog.md +37 -15
- package/README.md +372 -118
- package/dist/app-builder/build.d.ts +41 -2
- package/dist/app-builder/build.js +455 -18
- package/dist/app-builder/client-config.d.ts +15 -2
- package/dist/app-builder/client-config.js +43 -4
- package/dist/app-builder/config-diff/snapshot.d.ts +1 -6
- package/dist/app-builder/config-diff/snapshot.js +2 -9
- package/dist/app-builder/index.d.ts +1 -1
- package/dist/app-builder/index.js +3 -1
- package/dist/app-builder/nav-shell.d.ts +23 -0
- package/dist/app-builder/nav-shell.js +80 -0
- package/dist/app-builder/static-export.d.ts +17 -0
- package/dist/app-builder/static-export.js +181 -0
- package/dist/app-builder/vercel-output.d.ts +39 -0
- package/dist/app-builder/vercel-output.js +259 -0
- package/dist/app-builder/watchers/utils.d.ts +2 -0
- package/dist/app-builder/watchers.d.ts +14 -4
- package/dist/app-builder/watchers.js +123 -28
- package/dist/app-builder/websocket.js +1 -1
- package/dist/artifacts/index.d.ts +2 -0
- package/dist/artifacts/index.js +13 -0
- package/dist/artifacts/types.d.ts +136 -0
- package/dist/artifacts/types.js +24 -0
- package/dist/artifacts/url.d.ts +49 -0
- package/dist/artifacts/url.js +86 -0
- package/dist/cache/build-cache.js +48 -4
- package/dist/commands/build/app.js +1 -2
- package/dist/commands/build/index.d.ts +17 -0
- package/dist/commands/build/index.js +66 -0
- package/dist/commands/checkout/index.d.ts +24 -0
- package/dist/commands/checkout/index.js +71 -0
- package/dist/commands/db/migrate.d.ts +11 -0
- package/dist/commands/db/migrate.js +33 -0
- package/dist/commands/index.js +8 -0
- package/dist/commands/publish/index.d.ts +21 -0
- package/dist/commands/publish/index.js +68 -0
- package/dist/config/defaults.js +15 -1
- package/dist/config/loader.js +6 -1
- package/dist/config/normalizers/declaration.js +4 -1
- package/dist/config/normalizers/page.d.ts +18 -0
- package/dist/config/normalizers/page.js +34 -0
- package/dist/config/normalizers/pattern.js +7 -1
- package/dist/config/runtime.d.ts +4 -0
- package/dist/config/runtime.js +53 -5
- package/dist/index.d.ts +65 -1
- package/dist/index.js +123 -3
- package/dist/nav/index.d.ts +83 -0
- package/dist/nav/index.js +9 -0
- package/dist/nav/registry.d.ts +15 -0
- package/dist/nav/registry.js +42 -0
- package/dist/nav/resolver.d.ts +8 -0
- package/dist/nav/resolver.js +192 -0
- package/dist/nav/workspace.d.ts +18 -0
- package/dist/nav/workspace.js +103 -0
- package/dist/pipeline/styles.js +7 -35
- package/dist/pipeline/token-transformers.d.ts +45 -0
- package/dist/pipeline/token-transformers.js +71 -0
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
- package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
- package/dist/registry/asset-storage/define.d.ts +10 -0
- package/dist/registry/asset-storage/define.js +13 -0
- package/dist/registry/asset-storage/resolve.d.ts +45 -0
- package/dist/registry/asset-storage/resolve.js +36 -0
- package/dist/registry/asset-storage/types.d.ts +51 -0
- package/dist/registry/asset-storage/types.js +9 -0
- package/dist/registry/assets/layout.d.ts +22 -0
- package/dist/registry/assets/layout.js +30 -0
- package/dist/registry/assets/sets.d.ts +18 -0
- package/dist/registry/assets/sets.js +44 -0
- package/dist/registry/assets/transfer.d.ts +45 -0
- package/dist/registry/assets/transfer.js +11 -0
- package/dist/registry/checkout/assets.d.ts +16 -0
- package/dist/registry/checkout/assets.js +150 -0
- package/dist/registry/checkout/index.d.ts +33 -0
- package/dist/registry/checkout/index.js +610 -0
- package/dist/registry/checkout/tokens.d.ts +15 -0
- package/dist/registry/checkout/tokens.js +171 -0
- package/dist/registry/client.d.ts +61 -0
- package/dist/registry/client.js +211 -0
- package/dist/registry/connection.d.ts +30 -0
- package/dist/registry/connection.js +36 -0
- package/dist/registry/db/adapter.d.ts +43 -0
- package/dist/registry/db/adapter.js +59 -0
- package/dist/registry/db/client.d.ts +35 -0
- package/dist/registry/db/client.js +94 -0
- package/dist/registry/db/index.d.ts +5 -0
- package/dist/registry/db/index.js +24 -0
- package/dist/registry/db/migrate.d.ts +23 -0
- package/dist/registry/db/migrate.js +64 -0
- package/dist/registry/db/schema.d.ts +4869 -0
- package/dist/registry/db/schema.js +347 -0
- package/dist/registry/path.d.ts +4 -0
- package/dist/registry/path.js +31 -0
- package/dist/registry/publish/artifacts.d.ts +16 -0
- package/dist/registry/publish/artifacts.js +132 -0
- package/dist/registry/publish/assets.d.ts +16 -0
- package/dist/registry/publish/assets.js +129 -0
- package/dist/registry/publish/errors.d.ts +5 -0
- package/dist/registry/publish/errors.js +26 -0
- package/dist/registry/publish/index.d.ts +27 -0
- package/dist/registry/publish/index.js +126 -0
- package/dist/registry/publish/package.d.ts +27 -0
- package/dist/registry/publish/package.js +257 -0
- package/dist/registry/publish/publish-build.d.ts +12 -0
- package/dist/registry/publish/publish-build.js +43 -0
- package/dist/registry/publish/tokens.d.ts +18 -0
- package/dist/registry/publish/tokens.js +112 -0
- package/dist/registry/tokens/sets.d.ts +59 -0
- package/dist/registry/tokens/sets.js +109 -0
- package/dist/registry/tokens/transfer.d.ts +48 -0
- package/dist/registry/tokens/transfer.js +10 -0
- package/dist/registry/transfer.d.ts +75 -0
- package/dist/registry/transfer.js +11 -0
- package/dist/store/filesystem-assets.d.ts +17 -0
- package/dist/store/filesystem-assets.js +147 -0
- package/dist/store/filesystem.d.ts +76 -0
- package/dist/store/filesystem.js +208 -0
- package/dist/store/index.d.ts +20 -0
- package/dist/store/index.js +38 -0
- package/dist/store/registry.d.ts +77 -0
- package/dist/store/registry.js +350 -0
- package/dist/store/source-files.d.ts +24 -0
- package/dist/store/source-files.js +147 -0
- package/dist/store/types.d.ts +185 -0
- package/dist/store/types.js +12 -0
- package/dist/transformers/plugins/csf-render.js +17 -8
- package/dist/transformers/plugins/handlebars-previews.js +10 -3
- package/dist/transformers/plugins/ssr-render.js +116 -49
- package/dist/transformers/preview/component/api.js +3 -1
- package/dist/transformers/preview/component/artifacts.js +8 -1
- package/dist/transformers/preview/component/builder.js +13 -5
- package/dist/transformers/preview/component/css.js +9 -1
- package/dist/transformers/preview/component/javascript.d.ts +9 -3
- package/dist/transformers/preview/component/javascript.js +45 -17
- package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
- package/dist/transformers/preview/component/shared-artifacts.js +92 -0
- package/dist/transformers/preview/component.js +5 -2
- package/dist/transformers/preview/pattern/builder.js +28 -9
- package/dist/transformers/preview/pattern/html.d.ts +13 -10
- package/dist/transformers/preview/pattern/html.js +62 -27
- package/dist/transformers/preview/types.d.ts +38 -0
- package/dist/transformers/utils/handlebars.d.ts +3 -0
- package/dist/transformers/utils/handlebars.js +19 -5
- package/dist/types/config.d.ts +118 -25
- package/dist/utils/menu-shell.d.ts +60 -0
- package/dist/utils/menu-shell.js +221 -0
- package/dist/utils/stable-stringify.d.ts +2 -0
- package/dist/utils/stable-stringify.js +8 -0
- package/docs/cli.md +4 -2
- package/drizzle/0000_init_registry_storage.sql +95 -0
- package/drizzle/0001_add_review_metadata.sql +2 -0
- package/drizzle/0002_add_pages.sql +28 -0
- package/drizzle/0003_add_tokens.sql +29 -0
- package/drizzle/0004_add_assets.sql +37 -0
- package/drizzle/meta/0000_snapshot.json +642 -0
- package/drizzle/meta/0001_snapshot.json +654 -0
- package/drizzle/meta/0002_snapshot.json +835 -0
- package/drizzle/meta/0003_snapshot.json +1023 -0
- package/drizzle/meta/0004_snapshot.json +1252 -0
- package/drizzle/meta/_journal.json +41 -0
- package/drizzle.config.ts +16 -0
- package/package.json +10 -1
- package/src/app/components/Component/ComponentLists.tsx +102 -102
- package/src/app/components/Component/Preview.tsx +3 -2
- package/src/app/components/Component/RegistryActions.tsx +150 -0
- package/src/app/components/Foundations/DisplayLogo.tsx +5 -3
- package/src/app/components/Layout/Header.tsx +21 -3
- package/src/app/components/Layout/Main.tsx +11 -3
- package/src/app/components/Navigation/MainNav.tsx +4 -2
- package/src/app/components/Navigation/MobileNav.tsx +4 -2
- package/src/app/components/Navigation/SideNav.tsx +46 -11
- package/src/app/components/NotFound.tsx +24 -0
- package/src/app/components/context/NavProvider.tsx +71 -0
- package/src/app/components/old/Header.tsx +3 -3
- package/src/app/components/util/index.ts +203 -380
- package/src/app/components/util/useCollectionAssets.ts +125 -0
- package/src/app/components/util/useFoundationTokens.ts +67 -0
- package/src/app/generated/default-pages.json +1 -0
- package/src/app/generated/nav-shell.json +1 -0
- package/src/app/lib/api/query.ts +6 -0
- package/src/app/lib/api/redact.ts +46 -0
- package/src/app/lib/asset-storage.ts +81 -0
- package/src/app/lib/docs-api/artifacts.ts +116 -0
- package/src/app/lib/docs-api/backend.ts +151 -0
- package/src/app/lib/docs-api/errors.ts +52 -0
- package/src/app/lib/docs-api/handler.ts +34 -0
- package/src/app/lib/docs-api/index.ts +7 -0
- package/src/app/lib/docs-api/records.ts +189 -0
- package/src/app/lib/docs-api/registry.ts +130 -0
- package/src/app/lib/docs-api/runtime-config.ts +173 -0
- package/src/app/lib/docs-api/serve.ts +37 -0
- package/src/app/lib/docs-api/token-detail.ts +19 -0
- package/src/app/lib/registry-api/allowlist.ts +163 -0
- package/src/app/lib/registry-api/asset-transfer.ts +363 -0
- package/src/app/lib/registry-api/auth.ts +68 -0
- package/src/app/lib/registry-api/entity-routes.ts +197 -0
- package/src/app/lib/registry-api/errors.ts +51 -0
- package/src/app/lib/registry-api/files.ts +135 -0
- package/src/app/lib/registry-api/handler.ts +96 -0
- package/src/app/lib/registry-api/index.ts +4 -0
- package/src/app/lib/registry-api/meta.ts +87 -0
- package/src/app/lib/registry-api/revalidate.ts +36 -0
- package/src/app/lib/registry-api/store.ts +348 -0
- package/src/app/lib/registry-api/token-transfer.ts +231 -0
- package/src/app/lib/registry-api/transfer.ts +513 -0
- package/src/app/lib/registry-api/validation.ts +74 -0
- package/src/app/lib/registry-connection.ts +58 -0
- package/src/app/lib/utils.ts +14 -0
- package/src/app/next.config.mjs +72 -13
- package/src/app/pages/[...slug]/index.tsx +74 -20
- package/src/app/pages/_app.tsx +5 -2
- package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
- package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
- package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
- package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
- package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
- package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
- package/src/app/pages/api/docs/components/[id].ts +24 -0
- package/src/app/pages/api/docs/components.json.ts +12 -0
- package/src/app/pages/api/docs/nav.json.ts +48 -0
- package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
- package/src/app/pages/api/docs/patterns/[id].ts +24 -0
- package/src/app/pages/api/docs/patterns.json.ts +12 -0
- package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
- package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
- package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
- package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
- package/src/app/pages/api/registry/components/[id].ts +20 -0
- package/src/app/pages/api/registry/components/index.ts +19 -0
- package/src/app/pages/api/registry/health.ts +20 -0
- package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
- package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
- package/src/app/pages/api/registry/patterns/[id].ts +20 -0
- package/src/app/pages/api/registry/patterns/index.ts +19 -0
- package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
- package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
- package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
- package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
- package/src/app/pages/assets/fonts.tsx +41 -17
- package/src/app/pages/assets/index.tsx +2 -2
- package/src/app/pages/assets/logos.tsx +2 -2
- package/src/app/pages/foundations/colors.tsx +18 -8
- package/src/app/pages/foundations/effects.tsx +18 -8
- package/src/app/pages/foundations/icons/[name]/index.tsx +26 -14
- package/src/app/pages/foundations/icons/index.tsx +13 -9
- package/src/app/pages/foundations/index.tsx +1 -1
- package/src/app/pages/foundations/logo.tsx +7 -4
- package/src/app/pages/foundations/typography.tsx +19 -9
- package/src/app/pages/index.tsx +1 -1
- package/src/app/pages/system/component/[component]/index.tsx +87 -17
- package/src/app/pages/system/index.tsx +13 -4
- package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
- package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
- package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
- package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
- package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
- package/src/app/tsconfig.json +2 -3
- package/src/app-builder/build.ts +555 -11
- package/src/app-builder/client-config.ts +61 -4
- package/src/app-builder/config-diff/snapshot.ts +1 -11
- package/src/app-builder/index.ts +1 -1
- package/src/app-builder/nav-shell.ts +74 -0
- package/src/app-builder/static-export.ts +215 -0
- package/src/app-builder/vercel-output.ts +297 -0
- package/src/app-builder/watchers/utils.ts +2 -0
- package/src/app-builder/watchers.ts +149 -25
- package/src/app-builder/websocket.ts +2 -2
- package/src/artifacts/README.md +14 -0
- package/src/artifacts/index.ts +17 -0
- package/src/artifacts/types.ts +146 -0
- package/src/artifacts/url.ts +90 -0
- package/src/cache/build-cache.ts +47 -2
- package/src/commands/build/app.ts +1 -2
- package/src/commands/build/index.ts +61 -0
- package/src/commands/checkout/index.ts +66 -0
- package/src/commands/db/migrate.ts +25 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/publish/index.ts +63 -0
- package/src/config/defaults.ts +15 -0
- package/src/config/loader.ts +10 -1
- package/src/config/normalizers/declaration.ts +3 -0
- package/src/config/normalizers/page.ts +45 -0
- package/src/config/normalizers/pattern.ts +4 -1
- package/src/config/runtime.ts +48 -2
- package/src/index.ts +126 -3
- package/src/nav/index.ts +94 -0
- package/src/nav/registry.ts +54 -0
- package/src/nav/resolver.ts +217 -0
- package/src/nav/workspace.ts +87 -0
- package/src/pipeline/styles.ts +4 -35
- package/src/pipeline/token-transformers.ts +97 -0
- package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
- package/src/registry/asset-storage/define.ts +15 -0
- package/src/registry/asset-storage/resolve.ts +72 -0
- package/src/registry/asset-storage/types.ts +46 -0
- package/src/registry/assets/layout.ts +38 -0
- package/src/registry/assets/sets.ts +48 -0
- package/src/registry/assets/transfer.ts +50 -0
- package/src/registry/checkout/assets.ts +126 -0
- package/src/registry/checkout/index.ts +596 -0
- package/src/registry/checkout/tokens.ts +152 -0
- package/src/registry/client.ts +234 -0
- package/src/registry/connection.ts +50 -0
- package/src/registry/db/adapter.ts +74 -0
- package/src/registry/db/client.ts +88 -0
- package/src/registry/db/index.ts +35 -0
- package/src/registry/db/migrate.ts +56 -0
- package/src/registry/db/schema.ts +444 -0
- package/src/registry/path.ts +26 -0
- package/src/registry/publish/artifacts.ts +147 -0
- package/src/registry/publish/assets.ts +129 -0
- package/src/registry/publish/errors.ts +34 -0
- package/src/registry/publish/index.ts +128 -0
- package/src/registry/publish/package.ts +272 -0
- package/src/registry/publish/publish-build.ts +51 -0
- package/src/registry/publish/tokens.ts +119 -0
- package/src/registry/tokens/sets.ts +132 -0
- package/src/registry/tokens/transfer.ts +53 -0
- package/src/registry/transfer.ts +82 -0
- package/src/store/filesystem-assets.ts +152 -0
- package/src/store/filesystem.ts +237 -0
- package/src/store/index.ts +62 -0
- package/src/store/registry.ts +334 -0
- package/src/store/source-files.ts +155 -0
- package/src/store/types.ts +210 -0
- package/src/transformers/plugins/csf-render.ts +31 -7
- package/src/transformers/plugins/handlebars-previews.ts +22 -3
- package/src/transformers/plugins/ssr-render.ts +145 -57
- package/src/transformers/preview/component/api.ts +2 -1
- package/src/transformers/preview/component/artifacts.ts +10 -1
- package/src/transformers/preview/component/builder.ts +9 -2
- package/src/transformers/preview/component/css.ts +10 -1
- package/src/transformers/preview/component/javascript.ts +55 -20
- package/src/transformers/preview/component/shared-artifacts.ts +146 -0
- package/src/transformers/preview/component.ts +5 -2
- package/src/transformers/preview/pattern/builder.ts +33 -7
- package/src/transformers/preview/pattern/html.ts +71 -27
- package/src/transformers/preview/types.ts +42 -0
- package/src/transformers/utils/handlebars.ts +31 -5
- package/src/types/config.ts +123 -25
- package/src/utils/menu-shell.ts +241 -0
- package/src/utils/stable-stringify.ts +7 -0
- package/templates/default/package.json.tpl +1 -1
- package/handoff.config.js +0 -90
- package/src/app/components/ComponentSearch.tsx +0 -93
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connected-workspace checkout orchestration.
|
|
3
|
+
*
|
|
4
|
+
* `checkout <component|pattern> <id>` reads a normalized registry record and its registry-safe
|
|
5
|
+
* source files from the connected registry through the shared registry client, then writes them
|
|
6
|
+
* into the local workspace in standard authoring form. The local **declaration is synthesized
|
|
7
|
+
* locally** in the configured `runtime.workspace.declarationFormat` — declarations are never read
|
|
8
|
+
* from the registry (they are workspace-only). Identity is matched by stable `id`, so a checked-out
|
|
9
|
+
* entity maps to the correct logical component/pattern and can be re-authored and re-published.
|
|
10
|
+
*
|
|
11
|
+
* Available only from a connected workspace (`runtime.mode: workspace` + a configured
|
|
12
|
+
* `registryConnection`). Overwriting existing local files is explicit: a `--force` flag or an
|
|
13
|
+
* interactive confirmation.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as p from '@clack/prompts';
|
|
17
|
+
import fs from 'fs-extra';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { resolveComponentDeclaration } from '../../config/runtime';
|
|
20
|
+
import Handoff from '../../index';
|
|
21
|
+
import type { DeclarationFormat } from '../../types/config';
|
|
22
|
+
import { Logger } from '../../utils/logger';
|
|
23
|
+
import { createRegistryClient, RegistryClientError } from '../client';
|
|
24
|
+
import { resolveRegistryConnection } from '../connection';
|
|
25
|
+
import { isSafePathSegment, isSafeRelativePath, resolvePathWithin } from '../path';
|
|
26
|
+
import type { CheckoutPayload, TransferEntityKind, TransferFile } from '../transfer';
|
|
27
|
+
|
|
28
|
+
/** A connected-workspace configuration or precondition failure surfaced to the CLI. */
|
|
29
|
+
export class CheckoutError extends Error {
|
|
30
|
+
constructor(message: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'CheckoutError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Default authoring format synthesized when no existing file or configured preference applies. */
|
|
37
|
+
const DEFAULT_DECLARATION_FORMAT: DeclarationFormat = 'ts';
|
|
38
|
+
|
|
39
|
+
/** Default workspace subdirectory an entity is written into when none is configured. */
|
|
40
|
+
const DEFAULT_ENTITY_DIR: Record<TransferEntityKind, string> = {
|
|
41
|
+
component: 'components',
|
|
42
|
+
pattern: 'patterns',
|
|
43
|
+
page: 'pages',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Source-file extensions stripped to form a module specifier for a React component import. */
|
|
47
|
+
const COMPONENT_EXTENSION = /\.(tsx|jsx|ts|js|cjs|mjs)$/i;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ensure the workspace is a connected workspace able to checkout: workspace runtime mode (a registry
|
|
51
|
+
* host has no local workspace to write into) and a resolved registry URL + access token. Throws an
|
|
52
|
+
* actionable {@link CheckoutError} naming the exact misconfiguration.
|
|
53
|
+
*/
|
|
54
|
+
export const resolveConnectionOrThrow = (handoff: Handoff) => {
|
|
55
|
+
const mode = handoff.config?.runtime?.mode ?? 'workspace';
|
|
56
|
+
if (mode !== 'workspace') {
|
|
57
|
+
throw new CheckoutError(
|
|
58
|
+
`checkout is only available from a connected workspace (runtime.mode: "workspace"); this project is "${mode}". ` +
|
|
59
|
+
'A registry host stores and serves entities; it has no local workspace to check them out into.'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const connection = resolveRegistryConnection(handoff.config);
|
|
64
|
+
if (!connection.url) {
|
|
65
|
+
throw new CheckoutError(
|
|
66
|
+
`No registry URL is configured. Set runtime.registryConnection.url, or the "${connection.urlEnv}" environment variable, ` +
|
|
67
|
+
'to the base URL of the registry to checkout from.'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (!connection.accessToken) {
|
|
71
|
+
throw new CheckoutError(
|
|
72
|
+
`No registry access token is configured. Set the "${connection.accessTokenEnv}" environment variable to the ` +
|
|
73
|
+
"registry's bearer token to authorize checkout."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return connection;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Map a registry client error to an actionable checkout message. */
|
|
80
|
+
const describeFetchFailure = (error: RegistryClientError, kind: TransferEntityKind, id: string, registryUrl: string): string => {
|
|
81
|
+
switch (error.code) {
|
|
82
|
+
case 'not_found':
|
|
83
|
+
return `No ${kind} "${id}" exists in the registry at ${registryUrl}.`;
|
|
84
|
+
case 'runtime_mode_conflict':
|
|
85
|
+
return `The registry at ${registryUrl} is not running in registry mode, so it cannot serve a checkout: ${error.message}`;
|
|
86
|
+
case 'unauthorized':
|
|
87
|
+
return `The registry rejected the access token (401). Check the configured access token matches the registry's token.`;
|
|
88
|
+
default:
|
|
89
|
+
return error.message;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the local directory the entity is written into: an already-checked-out / locally-declared
|
|
95
|
+
* entity keeps its existing source directory (matched by stable `id`); otherwise it lands under the
|
|
96
|
+
* first configured `entries.{components|patterns}` root (falling back to `components`/`patterns`).
|
|
97
|
+
*/
|
|
98
|
+
const resolveTargetDir = async (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<string> => {
|
|
99
|
+
const store = kind === 'component' ? handoff.store.components : handoff.store.patterns;
|
|
100
|
+
const existing = await store.get(id);
|
|
101
|
+
const existingDir = (existing as { path?: string } | null)?.path;
|
|
102
|
+
if (existingDir) {
|
|
103
|
+
return existingDir;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const configuredRoots = kind === 'component' ? handoff.config?.entries?.components : handoff.config?.entries?.patterns;
|
|
107
|
+
const root = configuredRoots?.length
|
|
108
|
+
? path.resolve(handoff.workingPath, configuredRoots[0])
|
|
109
|
+
: path.resolve(handoff.workingPath, DEFAULT_ENTITY_DIR[kind]);
|
|
110
|
+
const targetDir = resolvePathWithin(root, id);
|
|
111
|
+
if (!targetDir) {
|
|
112
|
+
throw new CheckoutError(`Cannot checkout ${kind} with unsafe id "${id}".`);
|
|
113
|
+
}
|
|
114
|
+
return targetDir;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Read the extension of an existing local declaration in `dir`, if any maps to a known format. */
|
|
118
|
+
const existingDeclarationFormat = (dir: string): DeclarationFormat | undefined => {
|
|
119
|
+
if (!fs.existsSync(dir)) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const declaration = resolveComponentDeclaration(dir, path.basename(dir));
|
|
123
|
+
if (!declaration) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const ext = path.extname(declaration.fileName).slice(1).toLowerCase();
|
|
127
|
+
return (['ts', 'js', 'cjs', 'json'] as const).includes(ext as DeclarationFormat) ? (ext as DeclarationFormat) : undefined;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve the declaration format: an existing local declaration's extension wins, else the
|
|
132
|
+
* configured `runtime.workspace.declarationFormat`, else {@link DEFAULT_DECLARATION_FORMAT}.
|
|
133
|
+
*/
|
|
134
|
+
const resolveDeclarationFormat = (handoff: Handoff, dir: string): DeclarationFormat => {
|
|
135
|
+
const configured = handoff.config?.runtime?.workspace?.declarationFormat;
|
|
136
|
+
return existingDeclarationFormat(dir) ?? configured ?? DEFAULT_DECLARATION_FORMAT;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** The declaration filename to write: reuse an existing local declaration, else `<id>.handoff.<format>`. */
|
|
140
|
+
const resolveDeclarationFileName = (dir: string, id: string, format: DeclarationFormat): string => {
|
|
141
|
+
if (fs.existsSync(dir)) {
|
|
142
|
+
const existing = resolveComponentDeclaration(dir, path.basename(dir));
|
|
143
|
+
if (existing) {
|
|
144
|
+
return existing.fileName;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return `${id}.handoff.${format}`;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
151
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The single authored "primary" entry key per renderer. The normalizer derives the others (e.g. it
|
|
155
|
+
* mirrors a React `component` into `template`, and a CSF `story` into `template`), so the stored
|
|
156
|
+
* record carries duplicates that must not be re-authored. React authors `component`; handlebars and
|
|
157
|
+
* CSF author `template` (a `.stories.*` template is what marks a CSF component).
|
|
158
|
+
*/
|
|
159
|
+
const PRIMARY_ENTRY_BY_RENDERER: Record<string, string> = {
|
|
160
|
+
react: 'component',
|
|
161
|
+
handlebars: 'template',
|
|
162
|
+
csf: 'template',
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Primary entry keys, in preference order, for an unknown renderer. */
|
|
166
|
+
const PRIMARY_ENTRY_KEYS = ['component', 'template', 'story'] as const;
|
|
167
|
+
|
|
168
|
+
/** Supporting entry keys kept verbatim regardless of renderer. */
|
|
169
|
+
const SUPPORTING_ENTRY_KEYS = ['js', 'scss', 'schema', 'templates'] as const;
|
|
170
|
+
|
|
171
|
+
const asString = (value: unknown): string | undefined => (typeof value === 'string' && value ? value : undefined);
|
|
172
|
+
const asStringArray = (value: unknown): string[] | undefined =>
|
|
173
|
+
Array.isArray(value) && value.every((item) => typeof item === 'string') ? (value as string[]) : undefined;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Rebuild the authored `entries` for a renderer: keep only the renderer's primary entry key (so the
|
|
177
|
+
* normalizer-duplicated keys are dropped) plus the supporting source entries (`js`/`scss`/`schema`/
|
|
178
|
+
* `templates`). An unknown renderer keeps the first available primary key by preference order.
|
|
179
|
+
*/
|
|
180
|
+
const buildEntries = (entries: unknown, renderer: string | undefined): Record<string, unknown> | undefined => {
|
|
181
|
+
if (!isPlainObject(entries)) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const result: Record<string, unknown> = {};
|
|
185
|
+
|
|
186
|
+
const primaryKey = renderer ? PRIMARY_ENTRY_BY_RENDERER[renderer] : undefined;
|
|
187
|
+
if (primaryKey && typeof entries[primaryKey] === 'string') {
|
|
188
|
+
result[primaryKey] = entries[primaryKey];
|
|
189
|
+
} else if (!primaryKey) {
|
|
190
|
+
const fallback = PRIMARY_ENTRY_KEYS.find((key) => typeof entries[key] === 'string');
|
|
191
|
+
if (fallback) {
|
|
192
|
+
result[fallback] = entries[fallback];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const key of SUPPORTING_ENTRY_KEYS) {
|
|
197
|
+
if (entries[key] !== undefined) {
|
|
198
|
+
result[key] = entries[key];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Rebuild the authored `previews` map (`DeclarationPreview`) from the stored previews, keeping only
|
|
207
|
+
* authored fields (`title`, `args`) and dropping build-derived ones (rendered `url`, `usage`,
|
|
208
|
+
* `sourcePreview`). The component normalizer maps authored `args` to `values`, so the stored record
|
|
209
|
+
* carries `values`; checkout re-emits it as `args` to match the authoring convention (the form
|
|
210
|
+
* `make`/`scaffold` generate). Both rebuild identically.
|
|
211
|
+
*/
|
|
212
|
+
const buildPreviews = (previews: unknown): Record<string, unknown> | undefined => {
|
|
213
|
+
if (!isPlainObject(previews)) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
const result: Record<string, unknown> = {};
|
|
217
|
+
for (const [key, raw] of Object.entries(previews)) {
|
|
218
|
+
if (!isPlainObject(raw)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const preview: Record<string, unknown> = {};
|
|
222
|
+
if (typeof raw.title === 'string') preview.title = raw.title;
|
|
223
|
+
const args = raw.args !== undefined ? raw.args : raw.values;
|
|
224
|
+
if (args !== undefined) preview.args = args;
|
|
225
|
+
result[key] = preview;
|
|
226
|
+
}
|
|
227
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Synthesize an authored component declaration (`GenericDeclarationConfig`) from the normalized
|
|
232
|
+
* record. Only authored fields are kept; registry-/build-derived data (`path`, `image`,
|
|
233
|
+
* `properties`/docgen, rendered preview URLs, `internalPatternPreviews`, `validations`, `variant`,
|
|
234
|
+
* review `metadata`) is dropped so the local build regenerates it. `name` carries the record title
|
|
235
|
+
* (the authored contract uses `name`; normalization maps it back to `title`); `shouldDo`/
|
|
236
|
+
* `shouldNotDo` are emitted in their camelCase authored form.
|
|
237
|
+
*/
|
|
238
|
+
const buildComponentDeclaration = (item: Record<string, unknown>): Record<string, unknown> => {
|
|
239
|
+
const declaration: Record<string, unknown> = {};
|
|
240
|
+
const id = asString(item.id);
|
|
241
|
+
if (id) declaration.id = id;
|
|
242
|
+
declaration.name = asString(item.title) ?? asString(item.name) ?? id ?? '';
|
|
243
|
+
|
|
244
|
+
const description = asString(item.description);
|
|
245
|
+
if (description) declaration.description = description;
|
|
246
|
+
const group = asString(item.group);
|
|
247
|
+
if (group) declaration.group = group;
|
|
248
|
+
const type = asString(item.type);
|
|
249
|
+
if (type) declaration.type = type;
|
|
250
|
+
const renderer = asString(item.renderer);
|
|
251
|
+
if (renderer) declaration.renderer = renderer;
|
|
252
|
+
const entries = buildEntries(item.entries, renderer);
|
|
253
|
+
if (entries) declaration.entries = entries;
|
|
254
|
+
|
|
255
|
+
const previews = buildPreviews(item.previews);
|
|
256
|
+
if (previews) declaration.previews = previews;
|
|
257
|
+
|
|
258
|
+
const categories = asStringArray(item.categories);
|
|
259
|
+
if (categories) declaration.categories = categories;
|
|
260
|
+
const tags = asStringArray(item.tags);
|
|
261
|
+
if (tags) declaration.tags = tags;
|
|
262
|
+
|
|
263
|
+
const shouldDo = asStringArray(item.shouldDo) ?? asStringArray(item.should_do);
|
|
264
|
+
if (shouldDo) declaration.shouldDo = shouldDo;
|
|
265
|
+
const shouldNotDo = asStringArray(item.shouldNotDo) ?? asStringArray(item.should_not_do);
|
|
266
|
+
if (shouldNotDo) declaration.shouldNotDo = shouldNotDo;
|
|
267
|
+
|
|
268
|
+
const figma = asString(item.figma);
|
|
269
|
+
if (figma) declaration.figma = figma;
|
|
270
|
+
const figmaComponentId = asString(item.figmaComponentId);
|
|
271
|
+
if (figmaComponentId) declaration.figmaComponentId = figmaComponentId;
|
|
272
|
+
if (isPlainObject(item.page)) declaration.page = item.page;
|
|
273
|
+
if (isPlainObject(item.options)) declaration.options = item.options;
|
|
274
|
+
|
|
275
|
+
return declaration;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Rebuild the authored pattern component refs (`PatternComponentRef`) from the stored entries,
|
|
280
|
+
* keeping only `id`/`preview`/`args` and dropping build-time resolution fields (`resolvedPreview`,
|
|
281
|
+
* `resolved`).
|
|
282
|
+
*/
|
|
283
|
+
const buildPatternComponents = (components: unknown): Record<string, unknown>[] => {
|
|
284
|
+
if (!Array.isArray(components)) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
return components.filter(isPlainObject).map((entry) => {
|
|
288
|
+
const ref: Record<string, unknown> = {};
|
|
289
|
+
const id = asString(entry.id);
|
|
290
|
+
if (id) ref.id = id;
|
|
291
|
+
const preview = asString(entry.preview);
|
|
292
|
+
if (preview) ref.preview = preview;
|
|
293
|
+
if (isPlainObject(entry.args)) ref.args = entry.args;
|
|
294
|
+
return ref;
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Synthesize an authored pattern declaration (`GenericPatternDeclarationConfig`) from the normalized
|
|
300
|
+
* record. Only the authored fields (`id`, `name`, `description`, `group`, `tags`, `components`) are
|
|
301
|
+
* kept; derived fields (`path`, rendered `url`, review `metadata`) are dropped.
|
|
302
|
+
*/
|
|
303
|
+
const buildPatternDeclaration = (item: Record<string, unknown>): Record<string, unknown> => {
|
|
304
|
+
const declaration: Record<string, unknown> = {};
|
|
305
|
+
const id = asString(item.id);
|
|
306
|
+
if (id) declaration.id = id;
|
|
307
|
+
declaration.name = asString(item.title) ?? asString(item.name) ?? id ?? '';
|
|
308
|
+
|
|
309
|
+
const description = asString(item.description);
|
|
310
|
+
if (description) declaration.description = description;
|
|
311
|
+
const group = asString(item.group);
|
|
312
|
+
if (group) declaration.group = group;
|
|
313
|
+
const tags = asStringArray(item.tags);
|
|
314
|
+
if (tags) declaration.tags = tags;
|
|
315
|
+
|
|
316
|
+
declaration.components = buildPatternComponents(item.components);
|
|
317
|
+
return declaration;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Pick the renderer-specific `handoff-app` component factory from the record's renderer so the
|
|
322
|
+
* synthesized declaration matches the authored contract for that renderer. `react`/`handlebars`/`csf`
|
|
323
|
+
* each have a dedicated factory that stamps the renderer itself (so the renderer is dropped from the
|
|
324
|
+
* authored config); an unknown/absent renderer falls back to the generic `defineComponent`, which
|
|
325
|
+
* keeps `renderer` in the config.
|
|
326
|
+
*/
|
|
327
|
+
const resolveComponentFactory = (renderer: string | undefined): { name: string; stampsRenderer: boolean } => {
|
|
328
|
+
switch (renderer) {
|
|
329
|
+
case 'react':
|
|
330
|
+
return { name: 'defineReactComponent', stampsRenderer: true };
|
|
331
|
+
case 'handlebars':
|
|
332
|
+
return { name: 'defineHandlebarsComponent', stampsRenderer: true };
|
|
333
|
+
case 'csf':
|
|
334
|
+
return { name: 'defineCsfComponent', stampsRenderer: true };
|
|
335
|
+
default:
|
|
336
|
+
return { name: 'defineComponent', stampsRenderer: false };
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/** Drop a key from an object without mutating it. */
|
|
341
|
+
const omit = (object: Record<string, unknown>, key: string): Record<string, unknown> => {
|
|
342
|
+
const { [key]: _removed, ...rest } = object;
|
|
343
|
+
return rest;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/** Turn a relative entry path into a module specifier (extension stripped, `./`-prefixed). */
|
|
347
|
+
const toImportSpecifier = (entryPath: string): string => {
|
|
348
|
+
const withoutExt = entryPath.replace(COMPONENT_EXTENSION, '').split(path.sep).join('/');
|
|
349
|
+
return withoutExt.startsWith('.') ? withoutExt : `./${withoutExt}`;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/** Derive a safe PascalCase identifier for the imported React component. */
|
|
353
|
+
const toComponentIdentifier = (entryPath: string, fallback: string): string => {
|
|
354
|
+
const base = path.basename(entryPath).replace(COMPONENT_EXTENSION, '');
|
|
355
|
+
const pascal = (base || fallback)
|
|
356
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
357
|
+
.trim()
|
|
358
|
+
.split(/\s+/)
|
|
359
|
+
.filter(Boolean)
|
|
360
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
361
|
+
.join('');
|
|
362
|
+
return /^[A-Za-z_$]/.test(pascal) ? pascal : `Component${pascal}`;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/** Keys matching this are emitted unquoted in JS/TS object literals; others are single-quoted. */
|
|
366
|
+
const JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
367
|
+
|
|
368
|
+
/** Serialize a string as a single-quoted JS literal (repo style), escaping safely. */
|
|
369
|
+
const singleQuote = (value: string): string => {
|
|
370
|
+
let out = "'";
|
|
371
|
+
for (const char of value) {
|
|
372
|
+
const code = char.codePointAt(0) ?? 0;
|
|
373
|
+
if (char === '\\') out += '\\\\';
|
|
374
|
+
else if (char === "'") out += "\\'";
|
|
375
|
+
else if (char === '\n') out += '\\n';
|
|
376
|
+
else if (char === '\r') out += '\\r';
|
|
377
|
+
else if (char === '\t') out += '\\t';
|
|
378
|
+
else if (code < 0x20) out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
379
|
+
else out += char;
|
|
380
|
+
}
|
|
381
|
+
return `${out}'`;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Serialize a value as a JS/TS object literal mirroring `JSON.stringify(value, null, 2)` formatting,
|
|
386
|
+
* except identifier-safe object keys are emitted unquoted (`id: 'button'` rather than
|
|
387
|
+
* `"id": "button"`) and strings are single-quoted to match the repo's authoring style. `undefined`
|
|
388
|
+
* entries are dropped.
|
|
389
|
+
*/
|
|
390
|
+
const toJsLiteral = (value: unknown, indentLevel = 0): string => {
|
|
391
|
+
const pad = ' '.repeat(indentLevel);
|
|
392
|
+
const padInner = ' '.repeat(indentLevel + 1);
|
|
393
|
+
|
|
394
|
+
if (value === null) {
|
|
395
|
+
return 'null';
|
|
396
|
+
}
|
|
397
|
+
if (typeof value === 'string') {
|
|
398
|
+
return singleQuote(value);
|
|
399
|
+
}
|
|
400
|
+
if (typeof value !== 'object') {
|
|
401
|
+
return JSON.stringify(value);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (Array.isArray(value)) {
|
|
405
|
+
if (value.length === 0) {
|
|
406
|
+
return '[]';
|
|
407
|
+
}
|
|
408
|
+
const items = value.map((item) => `${padInner}${toJsLiteral(item, indentLevel + 1)}`);
|
|
409
|
+
return `[\n${items.join(',\n')}\n${pad}]`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const entries = Object.entries(value as Record<string, unknown>).filter(([, v]) => v !== undefined);
|
|
413
|
+
if (entries.length === 0) {
|
|
414
|
+
return '{}';
|
|
415
|
+
}
|
|
416
|
+
const lines = entries.map(([key, v]) => {
|
|
417
|
+
const renderedKey = JS_IDENTIFIER.test(key) ? key : singleQuote(key);
|
|
418
|
+
return `${padInner}${renderedKey}: ${toJsLiteral(v, indentLevel + 1)}`;
|
|
419
|
+
});
|
|
420
|
+
return `{\n${lines.join(',\n')}\n${pad}}`;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
/** Wrap a config object in a factory call for the given code format (unquoted identifier keys). */
|
|
424
|
+
const wrapFactory = (factory: string, config: Record<string, unknown>, format: DeclarationFormat): string => {
|
|
425
|
+
const literal = toJsLiteral(config);
|
|
426
|
+
return format === 'ts'
|
|
427
|
+
? `import { ${factory} } from 'handoff-app';\n\nexport default ${factory}(${literal});\n`
|
|
428
|
+
: `const { ${factory} } = require('handoff-app');\n\nmodule.exports = ${factory}(${literal});\n`;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Synthesize the component declaration file. JSON declarations are the bare object (carrying
|
|
433
|
+
* `renderer`/`entries`, which the loader reads directly). Code declarations use the renderer-specific
|
|
434
|
+
* factory; React additionally imports the component from its `entries.component` source and passes it
|
|
435
|
+
* as the factory's first argument (matching the authored `defineReactComponent` contract). When the
|
|
436
|
+
* renderer is `react` but no component entry is available, it falls back to the generic factory.
|
|
437
|
+
*/
|
|
438
|
+
const renderComponentDeclaration = (declaration: Record<string, unknown>, format: DeclarationFormat): string => {
|
|
439
|
+
if (format === 'json') {
|
|
440
|
+
return `${JSON.stringify(declaration, null, 2)}\n`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const renderer = asString(declaration.renderer);
|
|
444
|
+
const factory = resolveComponentFactory(renderer);
|
|
445
|
+
const componentEntry = isPlainObject(declaration.entries) ? asString(declaration.entries.component) : undefined;
|
|
446
|
+
|
|
447
|
+
if (factory.name === 'defineReactComponent' && componentEntry) {
|
|
448
|
+
const specifier = toImportSpecifier(componentEntry);
|
|
449
|
+
const identifier = toComponentIdentifier(componentEntry, asString(declaration.id) ?? 'Component');
|
|
450
|
+
const literal = toJsLiteral(omit(declaration, 'renderer'));
|
|
451
|
+
return format === 'ts'
|
|
452
|
+
? `import { defineReactComponent } from 'handoff-app';\nimport ${identifier} from '${specifier}';\n\nexport default defineReactComponent(${identifier}, ${literal});\n`
|
|
453
|
+
: `const { defineReactComponent } = require('handoff-app');\nconst ${identifier} = require('${specifier}').default;\n\nmodule.exports = defineReactComponent(${identifier}, ${literal});\n`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// React without a resolvable component entry can't use defineReactComponent; keep renderer and use
|
|
457
|
+
// the generic factory so the build still infers the renderer.
|
|
458
|
+
const effective = factory.name === 'defineReactComponent' ? { name: 'defineComponent', stampsRenderer: false } : factory;
|
|
459
|
+
const config = effective.stampsRenderer ? omit(declaration, 'renderer') : declaration;
|
|
460
|
+
return wrapFactory(effective.name, config, format);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/** Synthesize the pattern declaration file via `definePattern` (or a bare object for JSON). */
|
|
464
|
+
const renderPatternDeclaration = (declaration: Record<string, unknown>, format: DeclarationFormat): string => {
|
|
465
|
+
if (format === 'json') {
|
|
466
|
+
return `${JSON.stringify(declaration, null, 2)}\n`;
|
|
467
|
+
}
|
|
468
|
+
return wrapFactory('definePattern', declaration, format);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/** Synthesize the declaration file contents for an entity, faithful to its renderer/kind contract. */
|
|
472
|
+
const synthesizeDeclaration = (kind: TransferEntityKind, item: Record<string, unknown>, format: DeclarationFormat): string =>
|
|
473
|
+
kind === 'component'
|
|
474
|
+
? renderComponentDeclaration(buildComponentDeclaration(item), format)
|
|
475
|
+
: renderPatternDeclaration(buildPatternDeclaration(item), format);
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Confirm overwriting any local files the checkout would replace. Returns `true` to proceed. With
|
|
479
|
+
* `--force` (or when nothing would be overwritten) it proceeds without prompting; otherwise it asks
|
|
480
|
+
* for explicit confirmation and treats a cancel/decline as an abort.
|
|
481
|
+
*/
|
|
482
|
+
const confirmOverwrite = async (handoff: Handoff, conflicts: string[]): Promise<boolean> => {
|
|
483
|
+
if (conflicts.length === 0 || handoff.force) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
const relative = conflicts.map((file) => ` - ${path.relative(handoff.workingPath, file)}`).join('\n');
|
|
487
|
+
const proceed = await p.confirm({
|
|
488
|
+
message: `Checkout will overwrite ${conflicts.length} existing local file(s):\n${relative}\nOverwrite?`,
|
|
489
|
+
initialValue: false,
|
|
490
|
+
});
|
|
491
|
+
return !p.isCancel(proceed) && proceed === true;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/** Write a registry source file at its relative path under the target directory. */
|
|
495
|
+
const writeSourceFile = async (targetDir: string, file: TransferFile): Promise<string> => {
|
|
496
|
+
const absolutePath = resolvePathWithin(targetDir, file.path);
|
|
497
|
+
if (!absolutePath) {
|
|
498
|
+
throw new CheckoutError(`Registry returned an unsafe source file path "${file.path}".`);
|
|
499
|
+
}
|
|
500
|
+
await fs.ensureDir(path.dirname(absolutePath));
|
|
501
|
+
await fs.writeFile(absolutePath, file.content, 'utf8');
|
|
502
|
+
return absolutePath;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Checkout a page: write its single verbatim `.md` (transfer path `<id>.md`) back under
|
|
507
|
+
* `<workingPath>/pages/`. Unlike components/patterns there is no declaration to synthesize — the
|
|
508
|
+
* markdown file is itself the authored source — so the page round-trips byte-for-byte.
|
|
509
|
+
*/
|
|
510
|
+
const checkoutPage = async (handoff: Handoff, id: string, payload: CheckoutPayload): Promise<void> => {
|
|
511
|
+
const pagesRoot = path.resolve(handoff.workingPath, DEFAULT_ENTITY_DIR.page);
|
|
512
|
+
const targets = payload.files.map((file) => {
|
|
513
|
+
const target = resolvePathWithin(pagesRoot, file.path);
|
|
514
|
+
if (!target) {
|
|
515
|
+
throw new CheckoutError(`Registry returned an unsafe page source path "${file.path}".`);
|
|
516
|
+
}
|
|
517
|
+
return target;
|
|
518
|
+
});
|
|
519
|
+
const conflicts = targets.filter((file) => fs.existsSync(file));
|
|
520
|
+
if (!(await confirmOverwrite(handoff, conflicts))) {
|
|
521
|
+
Logger.warn(`Checkout of page "${id}" cancelled; no files were written.`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
for (const file of payload.files) {
|
|
525
|
+
await writeSourceFile(pagesRoot, file);
|
|
526
|
+
}
|
|
527
|
+
Logger.success(
|
|
528
|
+
`Checked out page "${id}" into ${path.relative(handoff.workingPath, pagesRoot) || '.'} ` + `(${payload.files.length} markdown file(s)).`
|
|
529
|
+
);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Checkout a single component or pattern from a connected workspace: precondition checks → fetch the
|
|
534
|
+
* normalized record + source files → resolve the local target → explicit-overwrite guard → write
|
|
535
|
+
* source files and synthesize the local declaration. Throws {@link CheckoutError} with actionable
|
|
536
|
+
* messaging on any precondition or fetch failure.
|
|
537
|
+
*/
|
|
538
|
+
export const checkoutEntity = async (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<void> => {
|
|
539
|
+
const validId = kind === 'page' ? isSafeRelativePath(id) : isSafePathSegment(id);
|
|
540
|
+
if (!validId) {
|
|
541
|
+
throw new CheckoutError(`Cannot checkout ${kind} with unsafe id "${id}".`);
|
|
542
|
+
}
|
|
543
|
+
const connection = resolveConnectionOrThrow(handoff);
|
|
544
|
+
|
|
545
|
+
Logger.info(`Fetching ${kind} "${id}" from ${connection.url}…`);
|
|
546
|
+
const client = createRegistryClient({ baseUrl: connection.url, accessToken: connection.accessToken });
|
|
547
|
+
let payload: CheckoutPayload;
|
|
548
|
+
try {
|
|
549
|
+
payload = await client.checkout(kind, id);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
if (error instanceof RegistryClientError) {
|
|
552
|
+
throw new CheckoutError(describeFetchFailure(error, kind, id, connection.url));
|
|
553
|
+
}
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Pages round-trip as a single verbatim `.md` with no declaration synthesis.
|
|
558
|
+
if (kind === 'page') {
|
|
559
|
+
await checkoutPage(handoff, id, payload);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const targetDir = await resolveTargetDir(handoff, kind, id);
|
|
564
|
+
const format = resolveDeclarationFormat(handoff, targetDir);
|
|
565
|
+
const declarationFileName = resolveDeclarationFileName(targetDir, id, format);
|
|
566
|
+
const declarationPath = path.join(targetDir, declarationFileName);
|
|
567
|
+
|
|
568
|
+
// Compute everything that would be written, then gate overwrite of any pre-existing file.
|
|
569
|
+
const sourceTargets = payload.files.map((file) => {
|
|
570
|
+
const absolutePath = resolvePathWithin(targetDir, file.path);
|
|
571
|
+
if (!absolutePath) {
|
|
572
|
+
throw new CheckoutError(`Registry returned an unsafe source file path "${file.path}".`);
|
|
573
|
+
}
|
|
574
|
+
return { file, absolutePath };
|
|
575
|
+
});
|
|
576
|
+
const conflicts = [...sourceTargets.map((entry) => entry.absolutePath), declarationPath].filter((file) => fs.existsSync(file));
|
|
577
|
+
if (!(await confirmOverwrite(handoff, conflicts))) {
|
|
578
|
+
Logger.warn(`Checkout of ${kind} "${id}" cancelled; no files were written.`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
await fs.ensureDir(targetDir);
|
|
583
|
+
const written: string[] = [];
|
|
584
|
+
for (const { file } of sourceTargets) {
|
|
585
|
+
written.push(await writeSourceFile(targetDir, file));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const declaration = synthesizeDeclaration(kind, payload.item, format);
|
|
589
|
+
await fs.writeFile(declarationPath, declaration, 'utf8');
|
|
590
|
+
written.push(declarationPath);
|
|
591
|
+
|
|
592
|
+
Logger.success(
|
|
593
|
+
`Checked out ${kind} "${id}" into ${path.relative(handoff.workingPath, targetDir) || '.'} ` +
|
|
594
|
+
`(${payload.files.length} source file(s) + ${declarationFileName}).`
|
|
595
|
+
);
|
|
596
|
+
};
|