handoff-app 1.2.2-7 → 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/cli.js +0 -0
- 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/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,45 @@
|
|
|
1
|
+
import { PageListObject } from '../../transformers/preview/types';
|
|
2
|
+
|
|
3
|
+
type RawPageFrontmatter = Record<string, any>;
|
|
4
|
+
|
|
5
|
+
type NormalizePageOptions = {
|
|
6
|
+
/** The page id — its slug path (e.g. `guides/setup`), derived from the file location. */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Logical route the page is served at (e.g. `/guides/setup`). */
|
|
9
|
+
routePath: string;
|
|
10
|
+
/** Absolute filesystem path of the source `.md` (workspace-only, stripped on publish). */
|
|
11
|
+
sourcePath: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Humanize a slug segment (`getting-started` → `Getting Started`) for a default title. */
|
|
15
|
+
const humanize = (segment: string): string =>
|
|
16
|
+
segment
|
|
17
|
+
.replace(/[-_]/g, ' ')
|
|
18
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
19
|
+
.trim();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize a page's parsed markdown frontmatter into a {@link PageListObject}. Pages have no
|
|
23
|
+
* required authored fields (a bare `.md` is valid); the title falls back to a humanized last slug
|
|
24
|
+
* segment. The id is always the slug path (a page is addressed by its URL, so id ≡ route): the file
|
|
25
|
+
* location is the single source of identity for both filesystem and registry serving.
|
|
26
|
+
*/
|
|
27
|
+
export const normalizePageDeclaration = (raw: RawPageFrontmatter, options: NormalizePageOptions): PageListObject => {
|
|
28
|
+
const lastSegment = options.id.split('/').pop() ?? options.id;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
id: options.id,
|
|
32
|
+
path: options.routePath,
|
|
33
|
+
sourcePath: options.sourcePath,
|
|
34
|
+
title: typeof raw.title === 'string' && raw.title.trim() ? raw.title : humanize(lastSegment),
|
|
35
|
+
description: typeof raw.description === 'string' ? raw.description : undefined,
|
|
36
|
+
group: typeof raw.group === 'string' ? raw.group : undefined,
|
|
37
|
+
weight: typeof raw.weight === 'number' ? raw.weight : undefined,
|
|
38
|
+
menuTitle: typeof raw.menuTitle === 'string' ? raw.menuTitle : undefined,
|
|
39
|
+
metaTitle: typeof raw.metaTitle === 'string' ? raw.metaTitle : undefined,
|
|
40
|
+
metaDescription: typeof raw.metaDescription === 'string' ? raw.metaDescription : undefined,
|
|
41
|
+
enabled: typeof raw.enabled === 'boolean' ? raw.enabled : undefined,
|
|
42
|
+
external: typeof raw.external === 'string' || typeof raw.external === 'boolean' ? raw.external : undefined,
|
|
43
|
+
menu: raw.menu && typeof raw.menu === 'object' ? raw.menu : undefined,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { PatternListObject } from '../../transformers/preview/types';
|
|
2
3
|
|
|
3
4
|
type RawPatternDeclaration = Record<string, any>;
|
|
@@ -51,6 +52,8 @@ export const normalizePatternDeclaration = (
|
|
|
51
52
|
group: typeof raw.group === 'string' ? raw.group : undefined,
|
|
52
53
|
tags: Array.isArray(raw.tags) ? raw.tags.filter((t: any) => typeof t === 'string') : undefined,
|
|
53
54
|
components,
|
|
54
|
-
|
|
55
|
+
// Source location of the pattern (declaration directory), retained so the store can expose a
|
|
56
|
+
// stable source reference for checkout/publish.
|
|
57
|
+
path: path.dirname(options.declarationPath),
|
|
55
58
|
};
|
|
56
59
|
};
|
package/src/config/runtime.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import esbuild from 'esbuild';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
+
import matter from 'gray-matter';
|
|
3
4
|
import { createRequire } from 'module';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import { ComponentListObject, PatternListObject } from '../transformers/preview/types';
|
|
@@ -9,6 +10,7 @@ import { Config, ConfigFileEntry, RuntimeConfig } from '../types/config';
|
|
|
9
10
|
import { Logger } from '../utils/logger';
|
|
10
11
|
import { normalizePathForCompare } from '../utils/path';
|
|
11
12
|
import { normalizeComponentDeclaration } from './normalizers/declaration';
|
|
13
|
+
import { normalizePageDeclaration } from './normalizers/page';
|
|
12
14
|
import { normalizePatternDeclaration } from './normalizers/pattern';
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -56,7 +58,7 @@ const findPreferredModernDeclaration = (componentDir: string, componentBaseName:
|
|
|
56
58
|
return modernFiles[0];
|
|
57
59
|
};
|
|
58
60
|
|
|
59
|
-
const resolveComponentDeclaration = (componentDir: string, componentBaseName: string): DeclarationResolution | null => {
|
|
61
|
+
export const resolveComponentDeclaration = (componentDir: string, componentBaseName: string): DeclarationResolution | null => {
|
|
60
62
|
const modernMatch = findPreferredModernDeclaration(componentDir, componentBaseName);
|
|
61
63
|
const legacyFiles = getLegacyDeclarationFiles(componentBaseName);
|
|
62
64
|
const legacyMatch = legacyFiles.find((candidate) => fs.existsSync(path.resolve(componentDir, candidate)));
|
|
@@ -171,6 +173,7 @@ export const initRuntimeConfig = (handoff: HandoffContext): [runtimeConfig: Runt
|
|
|
171
173
|
js: undefined,
|
|
172
174
|
components: {},
|
|
173
175
|
patterns: {},
|
|
176
|
+
pages: {},
|
|
174
177
|
},
|
|
175
178
|
};
|
|
176
179
|
|
|
@@ -284,7 +287,12 @@ export const initRuntimeConfig = (handoff: HandoffContext): [runtimeConfig: Runt
|
|
|
284
287
|
fallbackId: patternBaseName,
|
|
285
288
|
});
|
|
286
289
|
} catch (err) {
|
|
287
|
-
|
|
290
|
+
// Treat as a warning rather than an error: the file may be empty or
|
|
291
|
+
// mid-edit. It is already tracked in configFiles above, so
|
|
292
|
+
// watchRuntimeConfiguration will pick it up once the file is saved
|
|
293
|
+
// with valid content.
|
|
294
|
+
Logger.warn(`Pattern config skipped (incomplete or invalid) — will retry on next save: ${resolvedPatternConfigPath}`);
|
|
295
|
+
Logger.debug(`Pattern parse detail:`, err);
|
|
288
296
|
continue;
|
|
289
297
|
}
|
|
290
298
|
|
|
@@ -293,6 +301,26 @@ export const initRuntimeConfig = (handoff: HandoffContext): [runtimeConfig: Runt
|
|
|
293
301
|
}
|
|
294
302
|
}
|
|
295
303
|
|
|
304
|
+
// -------------------------------------------------------------------------
|
|
305
|
+
// Discover markdown pages
|
|
306
|
+
// -------------------------------------------------------------------------
|
|
307
|
+
// Working pages under `<workingPath>/pages/` are first-class registry entities (publish/checkout).
|
|
308
|
+
// Discovery mirrors the catch-all/static-path walk: every `.md` except `index.md` becomes a page
|
|
309
|
+
// keyed by its slug path. Package `config/docs` defaults are NOT entities (they ship with the tool).
|
|
310
|
+
const pagesRoot = path.resolve(handoff.workingPath, 'pages');
|
|
311
|
+
for (const segments of collectPageMarkdownPaths(pagesRoot)) {
|
|
312
|
+
const slug = segments.join('/');
|
|
313
|
+
const sourcePath = path.resolve(pagesRoot, `${slug}.md`);
|
|
314
|
+
try {
|
|
315
|
+
const { data: frontmatter } = matter(fs.readFileSync(sourcePath, 'utf-8'));
|
|
316
|
+
const page = normalizePageDeclaration(frontmatter, { id: slug, routePath: `/${slug}`, sourcePath });
|
|
317
|
+
result.entries.pages[page.id] = page;
|
|
318
|
+
} catch (err) {
|
|
319
|
+
Logger.warn(`Page skipped (unreadable or invalid frontmatter): ${sourcePath}`);
|
|
320
|
+
Logger.debug(`Page parse detail:`, err);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
296
324
|
// -------------------------------------------------------------------------
|
|
297
325
|
// Inject synthetic previews from patterns onto components
|
|
298
326
|
// -------------------------------------------------------------------------
|
|
@@ -301,6 +329,24 @@ export const initRuntimeConfig = (handoff: HandoffContext): [runtimeConfig: Runt
|
|
|
301
329
|
return [result, Array.from(configFiles), configFileIndex];
|
|
302
330
|
};
|
|
303
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Recursively collect `.md` files under a root, returning slug segments (relative path without the
|
|
334
|
+
* `.md`). `index.md` is excluded — section index content is served by dedicated/home routes.
|
|
335
|
+
*/
|
|
336
|
+
const collectPageMarkdownPaths = (rootDir: string, relativeParts: string[] = []): string[][] => {
|
|
337
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
338
|
+
const results: string[][] = [];
|
|
339
|
+
for (const entry of fs.readdirSync(rootDir)) {
|
|
340
|
+
const fullPath = path.join(rootDir, entry);
|
|
341
|
+
if (fs.lstatSync(fullPath).isDirectory()) {
|
|
342
|
+
results.push(...collectPageMarkdownPaths(fullPath, [...relativeParts, entry]));
|
|
343
|
+
} else if (entry.endsWith('.md') && entry !== 'index.md') {
|
|
344
|
+
results.push([...relativeParts, entry.replace(/\.md$/, '')]);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return results;
|
|
348
|
+
};
|
|
349
|
+
|
|
304
350
|
/**
|
|
305
351
|
* Returns a list of component directories for a given path.
|
|
306
352
|
*
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,12 @@ import 'dotenv/config';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { Types as CoreTypes, Handoff as HandoffRunner, Providers } from 'handoff-core';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import buildApp, { devApp, watchApp } from './app-builder';
|
|
5
|
+
import buildApp, { devApp, watchApp, type BuildPackage, type BuildTarget } from './app-builder';
|
|
6
6
|
import { ejectConfig, ejectPages, ejectTheme } from './cli/eject';
|
|
7
7
|
import { makeComponent, makePage, makeTemplate } from './cli/make';
|
|
8
8
|
import { initConfigWithMetadata, initRuntimeConfig, validateConfig } from './config';
|
|
9
9
|
import pipeline, { buildComponents, buildPatterns } from './pipeline';
|
|
10
|
+
import { createFilesystemStore, type HandoffStore } from './store';
|
|
10
11
|
import processComponents, { ComponentSegment } from './transformers/preview/component/builder';
|
|
11
12
|
import { Config, ConfigFileEntry, RuntimeConfig } from './types/config';
|
|
12
13
|
import { Logger } from './utils/logger';
|
|
@@ -28,6 +29,19 @@ class Handoff {
|
|
|
28
29
|
typography: {};
|
|
29
30
|
};
|
|
30
31
|
|
|
32
|
+
private _store?: HandoffStore;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalized store over the current workspace runtime for components, patterns, pages, tokens,
|
|
36
|
+
* and assets. It is created lazily and reads live from `runtimeConfig`.
|
|
37
|
+
*/
|
|
38
|
+
get store(): HandoffStore {
|
|
39
|
+
if (!this._store) {
|
|
40
|
+
this._store = createFilesystemStore(this);
|
|
41
|
+
}
|
|
42
|
+
return this._store;
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
private _initialArgs: { debug?: boolean; force?: boolean; config?: Partial<Config> } = {};
|
|
32
46
|
private _configFilePaths: string[] = [];
|
|
33
47
|
private _configFileIndex: Map<string, ConfigFileEntry> = new Map();
|
|
@@ -103,9 +117,101 @@ class Handoff {
|
|
|
103
117
|
return this;
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
async build(skipComponents?: boolean): Promise<Handoff> {
|
|
120
|
+
async build(target: BuildTarget = 'static', skipComponents?: boolean, buildPackage?: BuildPackage): Promise<Handoff> {
|
|
121
|
+
this.preRunner();
|
|
122
|
+
await buildApp(this, target, skipComponents, buildPackage);
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Runs the package-owned registry database migrations against the configured PostgreSQL/Neon
|
|
128
|
+
* database. Independent of {@link build} — it only reads config + DB env vars and applies the
|
|
129
|
+
* bundled migration set.
|
|
130
|
+
*/
|
|
131
|
+
async dbMigrate(): Promise<Handoff> {
|
|
132
|
+
if (!this.config) {
|
|
133
|
+
throw Error('Handoff not initialized');
|
|
134
|
+
}
|
|
135
|
+
const { runRegistryMigrations } = await import('./registry/db/migrate');
|
|
136
|
+
await runRegistryMigrations(this);
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Publish a single component, pattern, or page from this connected workspace to the configured
|
|
142
|
+
* remote registry. Components and patterns receive a fresh targeted build before upload. Pages
|
|
143
|
+
* upload their record and markdown source because they render at runtime. The publish module is
|
|
144
|
+
* loaded lazily so the registry client and build code never enter the docs app bundle.
|
|
145
|
+
*/
|
|
146
|
+
async publish(kind: 'component' | 'pattern' | 'page', id: string): Promise<Handoff> {
|
|
147
|
+
this.preRunner();
|
|
148
|
+
const { publishEntity } = await import('./registry/publish');
|
|
149
|
+
await publishEntity(this, kind, id);
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Checkout a single component, pattern, or page from the connected remote registry. The checkout
|
|
155
|
+
* module is loaded lazily so the registry client never enters the docs app bundle.
|
|
156
|
+
*/
|
|
157
|
+
async checkout(kind: 'component' | 'pattern' | 'page', id: string): Promise<Handoff> {
|
|
107
158
|
this.preRunner();
|
|
108
|
-
await
|
|
159
|
+
const { checkoutEntity } = await import('./registry/checkout');
|
|
160
|
+
await checkoutEntity(this, kind, id);
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Publish design token sets from this connected workspace to the configured remote registry. Runs a
|
|
166
|
+
* fresh token build (Figma extract + style transformers), discovers the logical sets, and uploads
|
|
167
|
+
* each changed set (its extracted record + generated artifacts). Publishes every set when `setId` is
|
|
168
|
+
* omitted, or only the named set (`foundation/colors`, `component/<id>`). Loaded lazily so the
|
|
169
|
+
* registry client/build code never enters the docs app bundle.
|
|
170
|
+
*/
|
|
171
|
+
async publishTokens(setId?: string): Promise<Handoff> {
|
|
172
|
+
this.preRunner();
|
|
173
|
+
const { publishTokens } = await import('./registry/publish/tokens');
|
|
174
|
+
await publishTokens(this, setId);
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Checkout design token sets from the connected remote registry into this workspace: reconstruct
|
|
180
|
+
* the canonical local `tokens.json` and restore the generated token files to their configured output
|
|
181
|
+
* paths. Checks out every published set when `setId` is omitted, or only the named set. Loaded lazily
|
|
182
|
+
* so the registry client never enters the docs app bundle.
|
|
183
|
+
*/
|
|
184
|
+
async checkoutTokens(setId?: string): Promise<Handoff> {
|
|
185
|
+
this.preRunner();
|
|
186
|
+
const { checkoutTokens } = await import('./registry/checkout/tokens');
|
|
187
|
+
await checkoutTokens(this, setId);
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Publish asset collections (icons/logos/fonts) from this connected workspace to the configured
|
|
193
|
+
* remote registry. Runs a fresh build (Figma extract + asset generation), discovers the collections,
|
|
194
|
+
* uploads only content hashes the registry lacks, and finalizes each collection manifest atomically.
|
|
195
|
+
* Publishes every collection when `collection` is omitted, or only the named one. Loaded lazily so
|
|
196
|
+
* the registry client/build code never enters the docs app bundle.
|
|
197
|
+
*/
|
|
198
|
+
async publishAssets(collection?: string): Promise<Handoff> {
|
|
199
|
+
this.preRunner();
|
|
200
|
+
const { publishAssets } = await import('./registry/publish/assets');
|
|
201
|
+
await publishAssets(this, collection);
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Checkout asset collections from the connected remote registry into this workspace: recreate the
|
|
207
|
+
* standard workspace asset files, collection JSON, icon sprite/manifest, and downloadable archives.
|
|
208
|
+
* Checks out every published collection when `collection` is omitted, or only the named one. Loaded
|
|
209
|
+
* lazily so the registry client never enters the docs app bundle.
|
|
210
|
+
*/
|
|
211
|
+
async checkoutAssets(collection?: string): Promise<Handoff> {
|
|
212
|
+
this.preRunner();
|
|
213
|
+
const { checkoutAssets } = await import('./registry/checkout/assets');
|
|
214
|
+
await checkoutAssets(this, collection);
|
|
109
215
|
return this;
|
|
110
216
|
}
|
|
111
217
|
|
|
@@ -261,6 +367,15 @@ class Handoff {
|
|
|
261
367
|
return path.join(this.getOutputPath(), 'tokens');
|
|
262
368
|
}
|
|
263
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Gets the generated docs API root (`<workingPath>/public/api`) — the tree holding the asset
|
|
372
|
+
* collection JSON, per-asset icon/logo bodies, and the icon sprite + manifest.
|
|
373
|
+
* @returns {string} The absolute path to the public docs API directory
|
|
374
|
+
*/
|
|
375
|
+
getAssetsApiPath(): string {
|
|
376
|
+
return path.resolve(this.workingPath, 'public', 'api');
|
|
377
|
+
}
|
|
378
|
+
|
|
264
379
|
/**
|
|
265
380
|
* Gets the path to the icons.zip file
|
|
266
381
|
* @returns {string} The absolute path to the icons.zip file
|
|
@@ -329,6 +444,14 @@ export {
|
|
|
329
444
|
definePattern,
|
|
330
445
|
defineReactComponent,
|
|
331
446
|
} from './declarations';
|
|
447
|
+
export { defineAssetStorage } from './registry/asset-storage/define';
|
|
448
|
+
export type {
|
|
449
|
+
AssetStorage,
|
|
450
|
+
AssetStorageContext,
|
|
451
|
+
AssetStorageFactory,
|
|
452
|
+
AssetStorageInput,
|
|
453
|
+
AssetStorageReadResult,
|
|
454
|
+
} from './registry/asset-storage/types';
|
|
332
455
|
export type {
|
|
333
456
|
CsfDeclarationConfig,
|
|
334
457
|
DeclarationPreview,
|
package/src/nav/index.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** A mode-aware placeholder retained in the serialized navigation shell. */
|
|
2
|
+
export interface NavDynamicSlot {
|
|
3
|
+
kind: 'components' | 'patterns' | 'token-components';
|
|
4
|
+
type?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** A link nested within a navigation subsection. */
|
|
8
|
+
export interface NavMenuItem {
|
|
9
|
+
title: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
image?: string;
|
|
12
|
+
menu?: NavMenuItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A group or direct link beneath a top-level navigation section. */
|
|
16
|
+
export interface NavSubSection {
|
|
17
|
+
title: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
image?: string;
|
|
20
|
+
menu?: NavMenuItem[];
|
|
21
|
+
dynamic?: NavDynamicSlot;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The canonical top-level navigation section shared by the build pipeline and docs app. */
|
|
25
|
+
export interface SectionLink {
|
|
26
|
+
title: string;
|
|
27
|
+
weight: number;
|
|
28
|
+
external?: string | boolean;
|
|
29
|
+
path: string;
|
|
30
|
+
subSections: NavSubSection[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Minimal navigation entity served by the docs navigation endpoint. */
|
|
34
|
+
export interface NavEntity {
|
|
35
|
+
id: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
group?: string;
|
|
38
|
+
type?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A navigation token-set record. Only component sets are rendered in the component-token slot. */
|
|
42
|
+
export interface NavTokenSet extends NavEntity {
|
|
43
|
+
kind: 'foundation' | 'component';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A declarative submenu entry carried by a page record. */
|
|
47
|
+
export interface NavPageMenuDeclaration {
|
|
48
|
+
title?: string;
|
|
49
|
+
enabled?: boolean;
|
|
50
|
+
components?: boolean | string;
|
|
51
|
+
patterns?: boolean;
|
|
52
|
+
tokens?: boolean;
|
|
53
|
+
path?: string;
|
|
54
|
+
image?: string;
|
|
55
|
+
menu?: NavMenuItem[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The page fields needed to compose page-authored navigation sections. */
|
|
59
|
+
export interface NavPageRecord {
|
|
60
|
+
id: string;
|
|
61
|
+
path?: string;
|
|
62
|
+
title?: string;
|
|
63
|
+
menuTitle?: string;
|
|
64
|
+
weight?: number;
|
|
65
|
+
enabled?: boolean;
|
|
66
|
+
external?: string | boolean;
|
|
67
|
+
menu?: Record<string, NavPageMenuDeclaration> | NavPageMenuDeclaration[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Eager, already-resolved inputs to the pure navigation resolver. */
|
|
71
|
+
export interface NavSources {
|
|
72
|
+
shell: SectionLink[];
|
|
73
|
+
components: NavEntity[];
|
|
74
|
+
patterns: NavEntity[];
|
|
75
|
+
pages: NavPageRecord[];
|
|
76
|
+
tokenSets: NavTokenSet[];
|
|
77
|
+
tokenFoundations: NavMenuItem[];
|
|
78
|
+
basePath?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type NavMode = 'workspace' | 'registry';
|
|
82
|
+
export type NavLoad = 'initial' | 'refresh';
|
|
83
|
+
|
|
84
|
+
/** The render-ready navigation payload. */
|
|
85
|
+
export interface NavData {
|
|
86
|
+
shell: SectionLink[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { getNavData } from './resolver';
|
|
90
|
+
export type { GetNavDataInput } from './resolver';
|
|
91
|
+
export { getWorkspaceNavData } from './workspace';
|
|
92
|
+
export type { WorkspaceNavAdapterOptions, WorkspaceNavLoaders } from './workspace';
|
|
93
|
+
export { getRegistryNavData } from './registry';
|
|
94
|
+
export type { RegistryNavAdapterOptions, RegistryNavRecords } from './registry';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { buildTokensFoundationsMenu } from '../utils/menu-shell';
|
|
2
|
+
import type { NavData, NavEntity, NavLoad, NavPageRecord, NavTokenSet, SectionLink } from '.';
|
|
3
|
+
import { getNavData } from './resolver';
|
|
4
|
+
|
|
5
|
+
export interface RegistryNavRecords {
|
|
6
|
+
components: NavEntity[];
|
|
7
|
+
patterns: NavEntity[];
|
|
8
|
+
pages: NavPageRecord[];
|
|
9
|
+
tokenSets: NavTokenSet[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RegistryNavAdapterOptions {
|
|
13
|
+
shell: SectionLink[];
|
|
14
|
+
load: NavLoad;
|
|
15
|
+
basePath?: string;
|
|
16
|
+
fetchRecords: () => Promise<RegistryNavRecords>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const emptyRecords = (): RegistryNavRecords => ({ components: [], patterns: [], pages: [], tokenSets: [] });
|
|
20
|
+
|
|
21
|
+
/** Registry adapter: shell-only initial load, or one best-effort runtime-record fetch on refresh. */
|
|
22
|
+
export const getRegistryNavData = async (options: RegistryNavAdapterOptions): Promise<NavData> => {
|
|
23
|
+
let records = emptyRecords();
|
|
24
|
+
if (options.load === 'refresh') {
|
|
25
|
+
try {
|
|
26
|
+
records = (await options.fetchRecords()) ?? records;
|
|
27
|
+
} catch {
|
|
28
|
+
// Fall back to the baked navigation shell when the registry backend is unavailable.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const componentById = new Map(records.components.map((component) => [component.id, component]));
|
|
32
|
+
const tokenSets = records.tokenSets.map((set) => {
|
|
33
|
+
const id = set.id.startsWith('component/') ? set.id.slice('component/'.length) : set.id;
|
|
34
|
+
const component = componentById.get(id);
|
|
35
|
+
return {
|
|
36
|
+
...set,
|
|
37
|
+
title: set.title || component?.title,
|
|
38
|
+
group: set.group ?? component?.group,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
const normalizedBasePath = options.basePath ? `${options.basePath.replace(/^\/+|\/+$/g, '')}/` : '';
|
|
42
|
+
|
|
43
|
+
return getNavData({
|
|
44
|
+
mode: 'registry',
|
|
45
|
+
load: options.load,
|
|
46
|
+
sources: {
|
|
47
|
+
shell: options.shell,
|
|
48
|
+
...records,
|
|
49
|
+
tokenSets,
|
|
50
|
+
tokenFoundations: buildTokensFoundationsMenu(normalizedBasePath),
|
|
51
|
+
basePath: options.basePath,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { startCase } from 'lodash';
|
|
2
|
+
import type {
|
|
3
|
+
NavData,
|
|
4
|
+
NavEntity,
|
|
5
|
+
NavLoad,
|
|
6
|
+
NavMenuItem,
|
|
7
|
+
NavMode,
|
|
8
|
+
NavPageMenuDeclaration,
|
|
9
|
+
NavPageRecord,
|
|
10
|
+
NavSources,
|
|
11
|
+
NavSubSection,
|
|
12
|
+
SectionLink,
|
|
13
|
+
} from '.';
|
|
14
|
+
|
|
15
|
+
export interface GetNavDataInput {
|
|
16
|
+
mode: NavMode;
|
|
17
|
+
load: NavLoad;
|
|
18
|
+
sources: NavSources;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalizeBasePath = (basePath?: string): string => {
|
|
22
|
+
if (!basePath) return '';
|
|
23
|
+
return `${basePath.replace(/^\/+|\/+$/g, '')}/`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const cloneMenu = (menu: NavMenuItem[] = []): NavMenuItem[] =>
|
|
27
|
+
menu.map((item) => ({ ...item, ...(item.menu ? { menu: cloneMenu(item.menu) } : {}) }));
|
|
28
|
+
|
|
29
|
+
const cloneSubSection = (section: NavSubSection): NavSubSection => ({
|
|
30
|
+
...section,
|
|
31
|
+
...(section.dynamic ? { dynamic: { ...section.dynamic } } : {}),
|
|
32
|
+
...(section.menu ? { menu: cloneMenu(section.menu) } : {}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const cloneSection = (section: SectionLink): SectionLink => ({
|
|
36
|
+
...section,
|
|
37
|
+
subSections: section.subSections.map(cloneSubSection),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const entityTitle = (entity: NavEntity): string => entity.title || startCase(entity.id);
|
|
41
|
+
|
|
42
|
+
const buildEntityMenu = (entities: NavEntity[], segment: 'component' | 'pattern', basePath: string): NavMenuItem[] => {
|
|
43
|
+
const grouped = new Map<string, NavEntity[]>();
|
|
44
|
+
for (const entity of entities) {
|
|
45
|
+
const group = entity.group ?? '';
|
|
46
|
+
grouped.set(group, [...(grouped.get(group) ?? []), entity]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return Array.from(grouped.entries())
|
|
50
|
+
.map(([group, records]) => ({
|
|
51
|
+
title: group || 'Uncategorized',
|
|
52
|
+
path: '',
|
|
53
|
+
menu: records
|
|
54
|
+
.map((record) => ({
|
|
55
|
+
title: entityTitle(record),
|
|
56
|
+
path: `${basePath}system/${segment}/${record.id}`,
|
|
57
|
+
}))
|
|
58
|
+
.sort((a, b) => a.title.localeCompare(b.title)),
|
|
59
|
+
}))
|
|
60
|
+
.map(({ path: _path, ...group }) => group)
|
|
61
|
+
.sort((a, b) => a.title.localeCompare(b.title));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const buildTokensMenu = (sources: NavSources, basePath: string): NavMenuItem[] => {
|
|
65
|
+
const foundations = cloneMenu(sources.tokenFoundations);
|
|
66
|
+
const componentSets = sources.tokenSets
|
|
67
|
+
.filter((set) => set.kind === 'component')
|
|
68
|
+
.map((set) => {
|
|
69
|
+
const id = set.id.startsWith('component/') ? set.id.slice('component/'.length) : set.id;
|
|
70
|
+
return {
|
|
71
|
+
title: set.title || startCase(id),
|
|
72
|
+
path: `${basePath}system/tokens/components/${id}`,
|
|
73
|
+
};
|
|
74
|
+
})
|
|
75
|
+
.sort((a, b) => a.title.localeCompare(b.title));
|
|
76
|
+
|
|
77
|
+
if (componentSets.length > 0) {
|
|
78
|
+
foundations.push({
|
|
79
|
+
title: 'Components',
|
|
80
|
+
path: `${basePath}system/tokens/components`,
|
|
81
|
+
menu: componentSets,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return foundations;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const fillSubSections = (subSections: NavSubSection[], sources: NavSources, keepEmpty: boolean): NavSubSection[] => {
|
|
88
|
+
const basePath = normalizeBasePath(sources.basePath);
|
|
89
|
+
const resolved: NavSubSection[] = [];
|
|
90
|
+
|
|
91
|
+
for (const original of subSections) {
|
|
92
|
+
const section = cloneSubSection(original);
|
|
93
|
+
if (section.dynamic?.kind === 'components') {
|
|
94
|
+
const entities = section.dynamic.type
|
|
95
|
+
? sources.components.filter((component) => component.type === section.dynamic?.type)
|
|
96
|
+
: sources.components;
|
|
97
|
+
section.menu = buildEntityMenu(entities, 'component', basePath);
|
|
98
|
+
} else if (section.dynamic?.kind === 'patterns') {
|
|
99
|
+
section.menu = buildEntityMenu(sources.patterns, 'pattern', basePath);
|
|
100
|
+
} else if (section.dynamic?.kind === 'token-components') {
|
|
101
|
+
section.menu = buildTokensMenu(sources, basePath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!section.dynamic || keepEmpty || (section.menu?.length ?? 0) > 0) {
|
|
105
|
+
resolved.push(section);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return resolved;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const pageMenuEntries = (menu: NavPageRecord['menu']): NavPageMenuDeclaration[] => (Array.isArray(menu) ? menu : Object.values(menu ?? {}));
|
|
112
|
+
|
|
113
|
+
const declarationToSubSection = (declaration: NavPageMenuDeclaration, sources: NavSources): NavSubSection | undefined => {
|
|
114
|
+
if (declaration.components) {
|
|
115
|
+
return {
|
|
116
|
+
title: declaration.title ?? 'Components',
|
|
117
|
+
menu: [],
|
|
118
|
+
dynamic: typeof declaration.components === 'string' ? { kind: 'components', type: declaration.components } : { kind: 'components' },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (declaration.patterns) {
|
|
122
|
+
return { title: declaration.title ?? 'Patterns', menu: [], dynamic: { kind: 'patterns' } };
|
|
123
|
+
}
|
|
124
|
+
if (declaration.tokens) {
|
|
125
|
+
return {
|
|
126
|
+
title: 'Tokens',
|
|
127
|
+
menu: cloneMenu(sources.tokenFoundations),
|
|
128
|
+
dynamic: { kind: 'token-components' },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (declaration.enabled === false) return undefined;
|
|
132
|
+
return {
|
|
133
|
+
title: declaration.title ?? '',
|
|
134
|
+
...(declaration.path !== undefined ? { path: declaration.path } : {}),
|
|
135
|
+
...(declaration.image !== undefined ? { image: declaration.image } : {}),
|
|
136
|
+
...(declaration.menu !== undefined ? { menu: cloneMenu(declaration.menu) } : {}),
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const buildChildPageItems = (parentId: string, pages: NavPageRecord[]): NavMenuItem[] => {
|
|
141
|
+
const prefix = `${parentId}/`;
|
|
142
|
+
const segments = Array.from(
|
|
143
|
+
new Set(pages.filter((page) => page.id.startsWith(prefix)).map((page) => page.id.slice(prefix.length).split('/')[0]))
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return segments
|
|
147
|
+
.map((segment) => {
|
|
148
|
+
const id = `${parentId}/${segment}`;
|
|
149
|
+
const record = pages.find((page) => page.id === id);
|
|
150
|
+
const nested = buildChildPageItems(id, pages);
|
|
151
|
+
const item: NavMenuItem & { weight: number } = {
|
|
152
|
+
title: record?.menuTitle || record?.title || startCase(segment),
|
|
153
|
+
path: record?.path ?? `/${id}`,
|
|
154
|
+
weight: record?.weight ?? 0,
|
|
155
|
+
};
|
|
156
|
+
if (nested.length > 0) item.menu = nested;
|
|
157
|
+
return item;
|
|
158
|
+
})
|
|
159
|
+
.sort((a, b) => a.weight - b.weight || a.title.localeCompare(b.title))
|
|
160
|
+
.map(({ weight: _weight, ...item }) => item);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const buildPageSections = (sources: NavSources, keepEmpty: boolean): SectionLink[] => {
|
|
164
|
+
const pages = sources.pages.filter((page) => page.enabled !== false);
|
|
165
|
+
return pages
|
|
166
|
+
.filter((page) => !page.id.includes('/'))
|
|
167
|
+
.map((page) => {
|
|
168
|
+
const declared = page.menu
|
|
169
|
+
? pageMenuEntries(page.menu)
|
|
170
|
+
.map((entry) => declarationToSubSection(entry, sources))
|
|
171
|
+
.filter((entry): entry is NavSubSection => entry !== undefined)
|
|
172
|
+
: [];
|
|
173
|
+
const children = page.menu ? [] : buildChildPageItems(page.id, pages);
|
|
174
|
+
const subSections = page.menu
|
|
175
|
+
? fillSubSections(declared, sources, keepEmpty)
|
|
176
|
+
: children.length > 0
|
|
177
|
+
? [{ title: page.menuTitle || page.title || startCase(page.id), menu: children }]
|
|
178
|
+
: [];
|
|
179
|
+
const external =
|
|
180
|
+
typeof page.external === 'string' &&
|
|
181
|
+
(page.external.startsWith('http://') || page.external.startsWith('https://') || page.external.startsWith('/'))
|
|
182
|
+
? page.external
|
|
183
|
+
: false;
|
|
184
|
+
return {
|
|
185
|
+
title: page.menuTitle || page.title || startCase(page.id),
|
|
186
|
+
external,
|
|
187
|
+
weight: page.weight ?? 0,
|
|
188
|
+
path: page.path ?? `/${page.id}`,
|
|
189
|
+
subSections,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const resolveCompleteTree = (sources: NavSources, keepEmpty: boolean): SectionLink[] => {
|
|
195
|
+
const byPath = new Map(
|
|
196
|
+
sources.shell.map((section) => [
|
|
197
|
+
section.path,
|
|
198
|
+
{ ...cloneSection(section), subSections: fillSubSections(section.subSections, sources, keepEmpty) },
|
|
199
|
+
])
|
|
200
|
+
);
|
|
201
|
+
for (const section of buildPageSections(sources, keepEmpty)) {
|
|
202
|
+
byPath.set(section.path, section);
|
|
203
|
+
}
|
|
204
|
+
return Array.from(byPath.values()).sort((a, b) => a.weight - b.weight);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** Resolve an eager navigation snapshot without fetching, mutation, or asynchronous work. */
|
|
208
|
+
export const getNavData = ({ mode, load, sources }: GetNavDataInput): NavData => {
|
|
209
|
+
if (mode === 'workspace') {
|
|
210
|
+
return { shell: resolveCompleteTree(sources, false) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (load === 'initial') {
|
|
214
|
+
return { shell: sources.shell.map(cloneSection) };
|
|
215
|
+
}
|
|
216
|
+
return { shell: resolveCompleteTree(sources, true) };
|
|
217
|
+
};
|