handoff-app 2.0.0-12 → 2.0.0-13
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 → CHANGELOG.md} +84 -0
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +760 -801
- package/RECIPES.md +975 -0
- package/components.json +20 -20
- package/config/docs/foundations/colors/guidelines.md +0 -1
- package/config/docs/foundations/colors.md +0 -1
- package/config/docs/foundations/effects/guidelines.md +0 -1
- package/config/docs/foundations/effects/resources.md +0 -1
- package/config/docs/foundations/effects.md +0 -1
- package/config/docs/foundations/fonts.md +0 -1
- package/config/docs/foundations/grid.md +0 -1
- package/config/docs/foundations/icon.md +0 -1
- package/config/docs/foundations/icons/guidelines.md +0 -1
- package/config/docs/foundations/icons.md +0 -1
- package/config/docs/foundations/images/guidelines.md +0 -1
- package/config/docs/foundations/images/resources.md +0 -1
- package/config/docs/foundations/images.md +0 -1
- package/config/docs/foundations/logo/resources.md +0 -1
- package/config/docs/foundations/logo.md +0 -1
- package/config/docs/foundations/typography/guidelines.md +0 -1
- package/config/docs/foundations/typography/resources.md +0 -1
- package/config/docs/foundations/typography.md +0 -1
- package/config/docs/foundations.md +0 -1
- package/config/docs/guidelines.md +0 -1
- package/config/docs/index.md +0 -1
- package/config/docs/system/tokens/foundations/colors.md +0 -4
- package/config/docs/system/tokens/foundations/effects.md +0 -4
- package/config/docs/system/tokens/foundations/typography.md +0 -2
- package/config/docs/system.md +0 -2
- package/config/templates/page.md +2 -1
- package/config/templates/page.mdx +2 -1
- package/dist/ai/define.d.ts +1 -1
- package/dist/ai/define.js +1 -1
- package/dist/ai/types.d.ts +1 -1
- package/dist/ai/types.js +1 -1
- package/dist/app-builder/build.js +14 -14
- package/dist/app-builder/client-config.js +1 -1
- package/dist/config/loader.js +1 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/nav/index.d.ts +2 -2
- package/dist/nav/registry.d.ts +3 -3
- package/dist/nav/registry.js +1 -1
- package/dist/nav/workspace.d.ts +3 -3
- package/dist/nav/workspace.js +1 -1
- package/dist/providers.d.ts +10 -0
- package/dist/{define.js → providers.js} +4 -4
- package/dist/registry/asset-storage/define.d.ts +5 -5
- package/dist/registry/asset-storage/define.js +4 -4
- package/dist/registry/asset-storage/resolve.d.ts +8 -11
- package/dist/registry/asset-storage/resolve.js +12 -13
- package/dist/registry/asset-storage/types.d.ts +12 -12
- package/dist/registry/asset-storage/types.js +2 -2
- package/dist/registry/db/schema.d.ts +2 -2
- package/dist/registry/db/schema.js +2 -2
- package/dist/registry/email/define.d.ts +1 -1
- package/dist/registry/email/define.js +1 -1
- package/dist/registry/email/resolve.d.ts +7 -8
- package/dist/registry/email/resolve.js +21 -23
- package/dist/registry/email/types.d.ts +1 -1
- package/dist/registry/email/types.js +1 -1
- package/dist/store/registry.d.ts +3 -3
- package/dist/store/registry.js +4 -4
- package/dist/types/config.d.ts +4 -13
- package/docs/cli.md +275 -54
- package/package.json +7 -7
- package/src/ai/define.ts +1 -1
- package/src/ai/types.ts +1 -1
- package/src/app/components/Auth/AccountLayout.tsx +1 -1
- package/src/app/components/Auth/AuthShell.tsx +10 -4
- package/src/app/components/Foundations/ColorGrid.tsx +13 -14
- package/src/app/components/Layout/Main.tsx +2 -2
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
- package/src/app/components/Navigation/MobileNav.tsx +0 -2
- package/src/app/components/util/index.ts +0 -11
- package/src/app/lib/asset-storage.ts +25 -25
- package/src/app/lib/auth/email.ts +4 -5
- package/src/app/lib/docs-api/page-rendering.ts +1 -1
- package/src/app/lib/docs-api/registry.ts +2 -3
- package/src/app/lib/docs-api/runtime-config.ts +8 -8
- package/src/app/lib/registry-api/asset-transfer.ts +25 -25
- package/src/app/lib/server-module.ts +1 -1
- package/src/app/lib/utils.ts +7 -0
- package/src/app/next.config.mjs +2 -2
- package/src/app/pages/[...slug]/index.tsx +2 -1
- package/src/app/pages/account/ai.tsx +1 -1
- package/src/app/pages/account/index.tsx +1 -1
- package/src/app/pages/account/tokens.tsx +1 -1
- package/src/app/pages/account/users.tsx +1 -1
- package/src/app/pages/api/docs/nav.json.ts +1 -1
- package/src/app/pages/api/mcp.ts +6 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
- package/src/app/pages/cli/device.tsx +1 -1
- package/src/app/pages/foundations/colors.tsx +9 -8
- package/src/app/pages/foundations/index.tsx +3 -3
- package/src/app/pages/index.tsx +2 -2
- package/src/app/pages/login.tsx +10 -5
- package/src/app/pages/reset-password.tsx +7 -2
- package/src/app/pages/system/component/[component]/index.tsx +2 -5
- package/src/app/pages/system/index.tsx +11 -11
- package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
- package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
- package/src/app-builder/build.ts +10 -10
- package/src/app-builder/client-config.ts +1 -1
- package/src/config/loader.ts +1 -2
- package/src/index.ts +6 -6
- package/src/nav/index.ts +2 -2
- package/src/nav/registry.ts +3 -3
- package/src/nav/workspace.ts +3 -3
- package/src/{define.ts → providers.ts} +8 -8
- package/src/registry/asset-storage/define.ts +6 -6
- package/src/registry/asset-storage/resolve.ts +16 -20
- package/src/registry/asset-storage/types.ts +12 -12
- package/src/registry/db/schema.ts +3 -3
- package/src/registry/email/define.ts +1 -1
- package/src/registry/email/resolve.ts +23 -23
- package/src/registry/email/types.ts +1 -1
- package/src/store/registry.ts +7 -7
- package/src/types/config.ts +4 -14
- package/.editorconfig +0 -19
- package/.eslintrc.json +0 -4
- package/.prettierignore +0 -0
- package/.prettierrc.json +0 -8
- package/.vscode/settings.json +0 -17
- package/dist/define.d.ts +0 -10
- package/dist/registry/email/adapters/resend.d.ts +0 -7
- package/dist/registry/email/adapters/resend.js +0 -35
- package/src/app/components/old/Header.tsx +0 -89
- package/src/app/pages/assets/fonts.tsx +0 -129
- package/src/app/pages/assets/index.tsx +0 -91
- package/src/app/pages/assets/logos.tsx +0 -75
- package/src/registry/email/adapters/resend.ts +0 -22
- /package/dist/registry/email/{adapters → providers}/smtp.d.ts +0 -0
- /package/dist/registry/email/{adapters → providers}/smtp.js +0 -0
- /package/src/registry/email/{adapters → providers}/smtp.ts +0 -0
|
@@ -20,7 +20,7 @@ export const getStaticProps: GetStaticProps = async () => ({ props: { config: ge
|
|
|
20
20
|
|
|
21
21
|
export default function DeviceApprovalPage({ config }: { config: ClientConfig }) {
|
|
22
22
|
if (process.env.HANDOFF_RUNTIME_MODE !== 'registry') {
|
|
23
|
-
return <AuthShell title="Authorize CLI">{null}</AuthShell>;
|
|
23
|
+
return <AuthShell config={config} title="Authorize CLI">{null}</AuthShell>;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
return <RegistryDeviceApprovalPage config={config} />;
|
|
@@ -33,6 +33,10 @@ export const getStaticProps: GetStaticProps = async () => {
|
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
// Tokens without a Figma group land under an empty key.
|
|
37
|
+
const groupTitle = (group: string) => upperFirst(group) || 'Other';
|
|
38
|
+
const groupId = (group: string) => group || 'other';
|
|
39
|
+
|
|
36
40
|
const ColorsPage = ({
|
|
37
41
|
content,
|
|
38
42
|
menu,
|
|
@@ -82,20 +86,17 @@ const ColorsPage = ({
|
|
|
82
86
|
<div className="lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_280px]">
|
|
83
87
|
<div className="flex flex-col gap-0">
|
|
84
88
|
{Object.keys(colorGroups).map((group) => (
|
|
85
|
-
<ColorGrid
|
|
86
|
-
title={upperFirst(group)}
|
|
87
|
-
group={group}
|
|
88
|
-
description="Colors that are used most frequently across all pages and components."
|
|
89
|
-
colors={colorGroups[group]}
|
|
90
|
-
key={group}
|
|
91
|
-
/>
|
|
89
|
+
<ColorGrid title={groupTitle(group)} group={groupId(group)} colors={colorGroups[group]} key={group} />
|
|
92
90
|
))}
|
|
93
91
|
<PrevNextNav previous={null} next={{ title: 'Typography', href: '/foundations/typography' }} />
|
|
94
92
|
</div>
|
|
95
93
|
|
|
96
94
|
<AnchorNav
|
|
97
95
|
groups={[
|
|
98
|
-
Object.assign(
|
|
96
|
+
Object.assign(
|
|
97
|
+
{},
|
|
98
|
+
...[...Object.keys(colorGroups).map((group) => ({ [`${groupId(group)}-colors`]: `${groupTitle(group)} Colors` }))]
|
|
99
|
+
),
|
|
99
100
|
]}
|
|
100
101
|
/>
|
|
101
102
|
|
|
@@ -50,7 +50,7 @@ const DesignPage = ({ content, menu, metadata, current, config }: DocumentationP
|
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
title: 'Colors',
|
|
53
|
-
description: 'Official
|
|
53
|
+
description: 'Official color palette used for all digital products.',
|
|
54
54
|
icon: Palette,
|
|
55
55
|
link: '/foundations/colors',
|
|
56
56
|
cta: 'Explore Colors',
|
|
@@ -74,14 +74,14 @@ const DesignPage = ({ content, menu, metadata, current, config }: DocumentationP
|
|
|
74
74
|
description: 'Downloadable icon set for use in digital and offline materials.',
|
|
75
75
|
icon: Shapes,
|
|
76
76
|
link: '/foundations/icons',
|
|
77
|
-
cta: '
|
|
77
|
+
cta: 'Explore Icons',
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
title: 'Effects',
|
|
81
81
|
description: 'Shadows, blurs, and other effects used in the design system.',
|
|
82
82
|
icon: Sun,
|
|
83
83
|
link: '/foundations/effects',
|
|
84
|
-
cta: '
|
|
84
|
+
cta: 'Explore Effects',
|
|
85
85
|
},
|
|
86
86
|
]}
|
|
87
87
|
/>
|
package/src/app/pages/index.tsx
CHANGED
|
@@ -57,11 +57,11 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
|
|
57
57
|
|
|
58
58
|
export const Home = ({ content, menu, metadata, config, current }: DocumentationProps) => {
|
|
59
59
|
return (
|
|
60
|
-
<Layout config={config} menu={menu} current={current} metadata={metadata} fullWidthHero={true}>
|
|
60
|
+
<Layout config={config} menu={menu} current={current} metadata={{ ...metadata, metaTitle: metadata.metaTitle || config?.app?.title }} fullWidthHero={true}>
|
|
61
61
|
<div className="w-full bg-linear-to-r py-12 dark:from-gray-900 dark:to-gray-800 sm:py-20">
|
|
62
62
|
<div className="container mx-auto px-8">
|
|
63
63
|
<HeadersType.H1 className="max-w-4xl text-3xl font-semibold leading-[-0.05px] sm:text-4xl">
|
|
64
|
-
{config?.app?.
|
|
64
|
+
{config?.app?.title}
|
|
65
65
|
</HeadersType.H1>
|
|
66
66
|
{content && (
|
|
67
67
|
<div className="text-lg max-w-4xl empty:hidden [&>*]:mt-6 [&>p]:font-light [&>h2:first-child]:mt-0 [&>h2:first-child]:font-normal sm:text-xl">
|
package/src/app/pages/login.tsx
CHANGED
|
@@ -2,29 +2,34 @@ import { Button } from '../components/ui/button';
|
|
|
2
2
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../components/ui/card';
|
|
3
3
|
import { Input } from '../components/ui/input';
|
|
4
4
|
import { Label } from '../components/ui/label';
|
|
5
|
+
import type { ClientConfig } from '@handoff/types/config';
|
|
6
|
+
import type { GetStaticProps } from 'next';
|
|
5
7
|
import Link from 'next/link';
|
|
6
8
|
import { signIn, useSession } from 'next-auth/react';
|
|
7
9
|
import { useRouter } from 'next/router';
|
|
8
10
|
import { FormEvent, useEffect, useState } from 'react';
|
|
9
11
|
import { AuthShell } from '../components/Auth/AuthShell';
|
|
10
12
|
import { loadInstallStatus } from '../components/Auth/installStatus';
|
|
13
|
+
import { getClientRuntimeConfig } from '../components/util';
|
|
11
14
|
|
|
12
15
|
const safeCallbackUrl = (value: unknown): string =>
|
|
13
16
|
typeof value === 'string' && value.startsWith('/') && !value.startsWith('//') && !value.startsWith('/\\') ? value : '/';
|
|
14
17
|
|
|
15
|
-
export
|
|
18
|
+
export const getStaticProps: GetStaticProps = async () => ({ props: { config: getClientRuntimeConfig() } });
|
|
19
|
+
|
|
20
|
+
export default function LoginPage({ config }: { config: ClientConfig }) {
|
|
16
21
|
if (process.env.HANDOFF_RUNTIME_MODE !== 'registry') {
|
|
17
22
|
return (
|
|
18
|
-
<AuthShell title="Sign in" centered>
|
|
23
|
+
<AuthShell config={config} title="Sign in" centered>
|
|
19
24
|
{null}
|
|
20
25
|
</AuthShell>
|
|
21
26
|
);
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
return <RegistryLoginPage />;
|
|
29
|
+
return <RegistryLoginPage config={config} />;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
function RegistryLoginPage() {
|
|
32
|
+
function RegistryLoginPage({ config }: { config: ClientConfig }) {
|
|
28
33
|
const router = useRouter();
|
|
29
34
|
const { data: session, status } = useSession();
|
|
30
35
|
const [pending, setPending] = useState(false);
|
|
@@ -77,7 +82,7 @@ function RegistryLoginPage() {
|
|
|
77
82
|
};
|
|
78
83
|
|
|
79
84
|
return (
|
|
80
|
-
<AuthShell title="Sign in" centered>
|
|
85
|
+
<AuthShell config={config} title="Sign in" centered>
|
|
81
86
|
<Card>
|
|
82
87
|
<CardHeader>
|
|
83
88
|
<CardTitle>Sign in</CardTitle>
|
|
@@ -2,14 +2,19 @@ import { Button } from '../components/ui/button';
|
|
|
2
2
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../components/ui/card';
|
|
3
3
|
import { Input } from '../components/ui/input';
|
|
4
4
|
import { Label } from '../components/ui/label';
|
|
5
|
+
import type { ClientConfig } from '@handoff/types/config';
|
|
6
|
+
import type { GetStaticProps } from 'next';
|
|
5
7
|
import Head from 'next/head';
|
|
6
8
|
import Link from 'next/link';
|
|
7
9
|
import { useRouter } from 'next/router';
|
|
8
10
|
import { FormEvent, useEffect, useState } from 'react';
|
|
9
11
|
import { AuthShell } from '../components/Auth/AuthShell';
|
|
10
12
|
import { authApiUrl, readApiError } from '../components/Auth/api';
|
|
13
|
+
import { getClientRuntimeConfig } from '../components/util';
|
|
11
14
|
|
|
12
|
-
export
|
|
15
|
+
export const getStaticProps: GetStaticProps = async () => ({ props: { config: getClientRuntimeConfig() } });
|
|
16
|
+
|
|
17
|
+
export default function ResetPasswordPage({ config }: { config: ClientConfig }) {
|
|
13
18
|
const router = useRouter();
|
|
14
19
|
const [token, setToken] = useState('');
|
|
15
20
|
const [purpose, setPurpose] = useState<'invite' | 'reset'>('reset');
|
|
@@ -87,7 +92,7 @@ export default function ResetPasswordPage() {
|
|
|
87
92
|
};
|
|
88
93
|
|
|
89
94
|
return (
|
|
90
|
-
<AuthShell title={token ? 'Set a new password' : 'Reset password'} centered>
|
|
95
|
+
<AuthShell config={config} title={token ? 'Set a new password' : 'Reset password'} centered>
|
|
91
96
|
<Head>
|
|
92
97
|
<meta name="referrer" content="no-referrer" />
|
|
93
98
|
</Head>
|
|
@@ -96,9 +96,6 @@ export const getStaticProps = async (context) => {
|
|
|
96
96
|
const previousComponent = sameGroupComponents[groupIndex - 1] ?? null;
|
|
97
97
|
const nextComponent = sameGroupComponents[groupIndex + 1] ?? null;
|
|
98
98
|
|
|
99
|
-
const fallbackTitle = componentData.title || startCase(component as string);
|
|
100
|
-
const fallbackMetaTitle = `${fallbackTitle}${config?.app?.client ? ` | ${config.app.client} Design System` : ''}`;
|
|
101
|
-
|
|
102
99
|
return {
|
|
103
100
|
props: {
|
|
104
101
|
id: component,
|
|
@@ -110,7 +107,7 @@ export const getStaticProps = async (context) => {
|
|
|
110
107
|
...componentData,
|
|
111
108
|
title: componentData.title || docs.metadata.title || startCase(component as string),
|
|
112
109
|
description: componentData.description,
|
|
113
|
-
metaTitle: docs.metadata.metaTitle
|
|
110
|
+
metaTitle: docs.metadata.metaTitle ?? '',
|
|
114
111
|
metaDescription: docs.metadata.metaDescription || componentData.description,
|
|
115
112
|
image: docs.metadata.image || 'hero-brand-assets',
|
|
116
113
|
},
|
|
@@ -256,7 +253,7 @@ const GenericComponentPage = ({ menu, metadata, current, id, config, componentHo
|
|
|
256
253
|
</Button>
|
|
257
254
|
)}
|
|
258
255
|
<Drawer direction="right">
|
|
259
|
-
<DrawerTrigger>
|
|
256
|
+
<DrawerTrigger asChild>
|
|
260
257
|
<Button variant="outline" size={'sm'} className="font-normal [&_svg]:size-3!">
|
|
261
258
|
API Reference
|
|
262
259
|
</Button>
|
|
@@ -107,20 +107,20 @@ const ComponentsPage = ({ content, menu, metadata, current, config }: ComponentP
|
|
|
107
107
|
<div className="mt-3 flex flex-row justify-between gap-3">
|
|
108
108
|
<p className="text-lg leading-relaxed text-gray-600 dark:text-gray-300">{metadata.description}</p>
|
|
109
109
|
<Drawer direction="right">
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
<
|
|
110
|
+
<TooltipProvider>
|
|
111
|
+
<Tooltip>
|
|
112
|
+
<TooltipTrigger asChild>
|
|
113
|
+
<DrawerTrigger asChild>
|
|
114
114
|
<Button variant="outline">
|
|
115
115
|
API <Webhook strokeWidth={1.5} />
|
|
116
116
|
</Button>
|
|
117
|
-
</
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
</
|
|
121
|
-
</
|
|
122
|
-
</
|
|
123
|
-
</
|
|
117
|
+
</DrawerTrigger>
|
|
118
|
+
</TooltipTrigger>
|
|
119
|
+
<TooltipContent side="left">
|
|
120
|
+
<Badge>{apiUrl}</Badge>
|
|
121
|
+
</TooltipContent>
|
|
122
|
+
</Tooltip>
|
|
123
|
+
</TooltipProvider>
|
|
124
124
|
<DrawerContent>
|
|
125
125
|
<div className="mx-5 w-full max-w-lg">
|
|
126
126
|
<DrawerHeader>
|
|
@@ -130,7 +130,7 @@ export const getStaticProps = async (context: { params: IParams }) => {
|
|
|
130
130
|
metadata: {
|
|
131
131
|
title: fallbackTitle,
|
|
132
132
|
description: patternData?.description || '',
|
|
133
|
-
metaTitle:
|
|
133
|
+
metaTitle: '',
|
|
134
134
|
metaDescription: patternData?.description || '',
|
|
135
135
|
},
|
|
136
136
|
previousPattern,
|
|
@@ -189,7 +189,7 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex
|
|
|
189
189
|
|
|
190
190
|
if (patternNotFound) {
|
|
191
191
|
return (
|
|
192
|
-
<Layout config={config} menu={menu} current={current} metadata={{ ...metadata, title: '
|
|
192
|
+
<Layout config={config} menu={menu} current={current} metadata={{ ...metadata, title: 'Page Not Found', metaTitle: '' }}>
|
|
193
193
|
<div className="flex min-h-[60vh] flex-col items-center justify-center">
|
|
194
194
|
<NotFound />
|
|
195
195
|
</div>
|
|
@@ -70,14 +70,10 @@ export const getStaticProps = async (context) => {
|
|
|
70
70
|
description = componentData?.description;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const fallbackTitle = displayName || startCase(componentSlug);
|
|
74
|
-
const fallbackMetaTitle = `${fallbackTitle}${config?.app?.client ? ` | ${config.app.client} Design System` : ''}`;
|
|
75
|
-
|
|
76
73
|
markdownProps.metadata = {
|
|
77
74
|
...markdownProps.metadata,
|
|
78
75
|
title: displayName || markdownProps.metadata.title || startCase(componentSlug),
|
|
79
76
|
description: markdownProps.metadata.description || description || '',
|
|
80
|
-
metaTitle: markdownProps.metadata.metaTitle || fallbackMetaTitle,
|
|
81
77
|
metaDescription: markdownProps.metadata.metaDescription || description || '',
|
|
82
78
|
};
|
|
83
79
|
|
package/src/app-builder/build.ts
CHANGED
|
@@ -300,14 +300,14 @@ const initializeProjectApp = async (handoff: Handoff, options: InitializeProject
|
|
|
300
300
|
const escapedMcpEnabled = escapeForSingleQuotedJsString(String(isMcpEnabled(handoff.config)));
|
|
301
301
|
const escapedRegistryDriver = escapeForSingleQuotedJsString(resolveRegistryDriver(handoff.config));
|
|
302
302
|
const escapedDatabaseUrlEnv = escapeForSingleQuotedJsString(resolveDatabaseUrlEnv(handoff.config));
|
|
303
|
-
// Asset storage selection baked (
|
|
303
|
+
// Asset storage selection baked (provider + module + options JSON with `{ $env }` references).
|
|
304
304
|
const assetStorage = resolveAssetStorageFromConfig(handoff.config);
|
|
305
|
-
const
|
|
305
|
+
const escapedAssetStorageProvider = escapeForSingleQuotedJsString(assetStorage.provider);
|
|
306
306
|
const escapedAssetStorageModule = escapeForSingleQuotedJsString(assetStorage.module ?? '');
|
|
307
307
|
const escapedAssetStorageMaxInline = escapeForSingleQuotedJsString(String(assetStorage.maxInlineBytes));
|
|
308
308
|
const escapedAssetStorageOptions = escapeForSingleQuotedJsString(JSON.stringify(assetStorage.options ?? {}));
|
|
309
309
|
const email = resolveEmailFromConfig(handoff.config);
|
|
310
|
-
const escapedEmailProvider = escapeForSingleQuotedJsString(email.provider);
|
|
310
|
+
const escapedEmailProvider = escapeForSingleQuotedJsString(email.provider ?? '');
|
|
311
311
|
const escapedEmailFrom = escapeForSingleQuotedJsString(email.from ?? '');
|
|
312
312
|
const escapedEmailModule = escapeForSingleQuotedJsString(email.module ?? '');
|
|
313
313
|
const escapedEmailOptions = escapeForSingleQuotedJsString(JSON.stringify(email.options));
|
|
@@ -330,7 +330,7 @@ const initializeProjectApp = async (handoff: Handoff, options: InitializeProject
|
|
|
330
330
|
'%HANDOFF_MCP_ENABLED%': escapedMcpEnabled,
|
|
331
331
|
'%HANDOFF_REGISTRY_DRIVER%': escapedRegistryDriver,
|
|
332
332
|
'%HANDOFF_REGISTRY_DATABASE_URL_ENV%': escapedDatabaseUrlEnv,
|
|
333
|
-
'%
|
|
333
|
+
'%HANDOFF_ASSET_STORAGE_PROVIDER%': escapedAssetStorageProvider,
|
|
334
334
|
'%HANDOFF_ASSET_STORAGE_MODULE%': escapedAssetStorageModule,
|
|
335
335
|
'%HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES%': escapedAssetStorageMaxInline,
|
|
336
336
|
'%HANDOFF_ASSET_STORAGE_OPTIONS%': escapedAssetStorageOptions,
|
|
@@ -474,7 +474,7 @@ merged over the list this build baked in, keyed by \`id\`. Use it to add or repo
|
|
|
474
474
|
without rebuilding.
|
|
475
475
|
`;
|
|
476
476
|
|
|
477
|
-
const assetStorageEnvs = assetStorage.
|
|
477
|
+
const assetStorageEnvs = assetStorage.provider === 'custom' ? envReferenceNames(assetStorage.options) : [];
|
|
478
478
|
const emailEnvs = envReferenceNames(email.options);
|
|
479
479
|
const emailSection = !email.from
|
|
480
480
|
? 'Email delivery is off because `runtime.registry.email.from` is not set. Invitation links are shown once to an administrator for manual delivery.'
|
|
@@ -498,7 +498,7 @@ and \`.next/static/\` already copied alongside so the server serves them.
|
|
|
498
498
|
- \`${databaseUrlEnv}\` — PostgreSQL/Neon connection string, read at request time.
|
|
499
499
|
- \`AUTH_SECRET\` — a long, random secret used to sign browser sessions.
|
|
500
500
|
- \`AUTH_URL\` — the canonical public registry URL, including the configured base path.
|
|
501
|
-
${assetStorageEnvs.map((name) => `- \`${name}\` — read by the custom asset storage
|
|
501
|
+
${assetStorageEnvs.map((name) => `- \`${name}\` — read by the custom asset storage provider at request time.\n`).join('')}
|
|
502
502
|
${emailSection}
|
|
503
503
|
${aiSection}
|
|
504
504
|
## Database migrations
|
|
@@ -569,8 +569,8 @@ For multi-instance self-hosting, front \`/_next/static\` with a CDN and configur
|
|
|
569
569
|
|
|
570
570
|
## Deploying to Vercel
|
|
571
571
|
|
|
572
|
-
Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and
|
|
573
|
-
|
|
572
|
+
Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and runs its
|
|
573
|
+
own build, so this pre-built bundle is **not** the artifact you deploy there. For Vercel, deploy the
|
|
574
574
|
registry app via Vercel's Git/CLI source build (it produces and serves the dynamic app itself) and
|
|
575
575
|
supply the same environment variables above as project env vars. Use this standalone bundle for
|
|
576
576
|
containers, custom Node servers, and other non-Vercel hosts.
|
|
@@ -635,7 +635,7 @@ const getCustomServerModules = (handoff: Handoff): string[] => {
|
|
|
635
635
|
const assetStorage = resolveAssetStorageFromConfig(handoff.config);
|
|
636
636
|
const email = resolveEmailFromConfig(handoff.config);
|
|
637
637
|
const modules = [
|
|
638
|
-
assetStorage.
|
|
638
|
+
assetStorage.provider === 'custom' ? assetStorage.module : undefined,
|
|
639
639
|
email.provider === 'custom' ? email.module : undefined,
|
|
640
640
|
...resolveAiFromConfig(handoff.config).connections.map((connection) => connection.module),
|
|
641
641
|
];
|
|
@@ -667,7 +667,7 @@ const copyCustomServerModules = async (handoff: Handoff, tracingRoot: string, en
|
|
|
667
667
|
}
|
|
668
668
|
if ([...fileList].some((file) => file.split(path.sep).join('/').endsWith('node_modules/handoff-app/dist/index.js'))) {
|
|
669
669
|
throw new HandoffBuildError(
|
|
670
|
-
`The custom server module "${module}" imports "handoff-app", which loads the whole CLI. Import the define helpers from "handoff-app/
|
|
670
|
+
`The custom server module "${module}" imports "handoff-app", which loads the whole CLI. Import the define helpers from "handoff-app/providers".`
|
|
671
671
|
);
|
|
672
672
|
}
|
|
673
673
|
fileList.forEach((file) => files.add(file));
|
|
@@ -64,7 +64,7 @@ const buildServerRuntimeConfig = (config: ResolvedConfig, modeOverride?: Runtime
|
|
|
64
64
|
databaseUrlEnv: resolveDatabaseUrlEnv(config),
|
|
65
65
|
},
|
|
66
66
|
assetStorage: {
|
|
67
|
-
|
|
67
|
+
provider: assetStorage.provider,
|
|
68
68
|
module: assetStorage.module,
|
|
69
69
|
maxInlineBytes: assetStorage.maxInlineBytes,
|
|
70
70
|
options: assetStorage.options,
|
package/src/config/loader.ts
CHANGED
|
@@ -5,7 +5,6 @@ import mergeWith from 'lodash/mergeWith';
|
|
|
5
5
|
import omit from 'lodash/omit';
|
|
6
6
|
import { createRequire } from 'module';
|
|
7
7
|
import path from 'path';
|
|
8
|
-
import { DEFAULT_EMAIL_PROVIDER } from '../registry/email/resolve';
|
|
9
8
|
import { Config, HandoffEmailConfig, ResolvedConfig } from '../types/config';
|
|
10
9
|
import { Logger } from '../utils/logger';
|
|
11
10
|
import { resolveWorkingPath } from '../utils/path';
|
|
@@ -240,7 +239,7 @@ const replaceValues = (destination: unknown, source: unknown) => {
|
|
|
240
239
|
* the deep merge never mixes the settings of two providers.
|
|
241
240
|
*/
|
|
242
241
|
const resetEmailOnProviderChange = (layers: Partial<Config>[]): Partial<Config>[] => {
|
|
243
|
-
let provider: string
|
|
242
|
+
let provider: string | undefined;
|
|
244
243
|
let start = 0;
|
|
245
244
|
layers.forEach((layer, index) => {
|
|
246
245
|
const next = layer.runtime?.registry?.email?.provider;
|
package/src/index.ts
CHANGED
|
@@ -697,13 +697,13 @@ export type {
|
|
|
697
697
|
SourceFormat,
|
|
698
698
|
RendererKind,
|
|
699
699
|
} from './catalog';
|
|
700
|
-
export {
|
|
700
|
+
export { defineStorageProvider } from './registry/asset-storage/define';
|
|
701
701
|
export type {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
702
|
+
StorageProvider,
|
|
703
|
+
StorageProviderContext,
|
|
704
|
+
StorageProviderFactory,
|
|
705
|
+
StorageInput,
|
|
706
|
+
StorageReadResult,
|
|
707
707
|
AssetUpload,
|
|
708
708
|
AssetUploadInput,
|
|
709
709
|
} from './registry/asset-storage/types';
|
package/src/nav/index.ts
CHANGED
|
@@ -89,6 +89,6 @@ export interface NavData {
|
|
|
89
89
|
export { getNavData } from './resolver';
|
|
90
90
|
export type { GetNavDataInput } from './resolver';
|
|
91
91
|
export { getWorkspaceNavData } from './workspace';
|
|
92
|
-
export type {
|
|
92
|
+
export type { WorkspaceNavOptions, WorkspaceNavLoaders } from './workspace';
|
|
93
93
|
export { getRegistryNavData } from './registry';
|
|
94
|
-
export type {
|
|
94
|
+
export type { RegistryNavOptions, RegistryNavRecords } from './registry';
|
package/src/nav/registry.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface RegistryNavRecords {
|
|
|
9
9
|
tokenSets: NavTokenSet[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export interface
|
|
12
|
+
export interface RegistryNavOptions {
|
|
13
13
|
shell: SectionLink[];
|
|
14
14
|
load: NavLoad;
|
|
15
15
|
basePath?: string;
|
|
@@ -18,8 +18,8 @@ export interface RegistryNavAdapterOptions {
|
|
|
18
18
|
|
|
19
19
|
const emptyRecords = (): RegistryNavRecords => ({ components: [], patterns: [], pages: [], tokenSets: [] });
|
|
20
20
|
|
|
21
|
-
/** Registry
|
|
22
|
-
export const getRegistryNavData = async (options:
|
|
21
|
+
/** Registry nav data: shell-only initial load, or one best-effort runtime-record fetch on refresh. */
|
|
22
|
+
export const getRegistryNavData = async (options: RegistryNavOptions): Promise<NavData> => {
|
|
23
23
|
let records = emptyRecords();
|
|
24
24
|
if (options.load === 'refresh') {
|
|
25
25
|
try {
|
package/src/nav/workspace.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface WorkspaceNavLoaders {
|
|
|
14
14
|
tokenSets: () => Awaitable<NavTokenSet[]>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export interface
|
|
17
|
+
export interface WorkspaceNavOptions {
|
|
18
18
|
docRoot: string;
|
|
19
19
|
workingPagesDir?: string;
|
|
20
20
|
basePath?: string;
|
|
@@ -43,8 +43,8 @@ const markdownTitle = (id: string, docRoot: string, workingPagesDir?: string): s
|
|
|
43
43
|
return undefined;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
/** Workspace/static
|
|
47
|
-
export const getWorkspaceNavData = async (options:
|
|
46
|
+
/** Workspace/static nav data: live filesystem shell plus best-effort local snapshot loaders. */
|
|
47
|
+
export const getWorkspaceNavData = async (options: WorkspaceNavOptions): Promise<NavData> => {
|
|
48
48
|
const [loadedComponents, patterns, pages, loadedTokenSets] = await Promise.all([
|
|
49
49
|
safeLoad(options.loaders.components),
|
|
50
50
|
safeLoad(options.loaders.patterns),
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Define helpers for custom server modules. This entry point has no
|
|
3
|
-
* that imports it does not pull the CLI into the registry bundle.
|
|
2
|
+
* Define helpers for custom server modules: storage, email and AI providers. This entry point has no
|
|
3
|
+
* runtime dependencies, so a module that imports it does not pull the CLI into the registry bundle.
|
|
4
4
|
*/
|
|
5
5
|
export { defineAiProvider } from './ai/define';
|
|
6
6
|
export type { AiProvider, AiProviderContext, AiProviderFactory } from './ai/types';
|
|
7
|
-
export {
|
|
7
|
+
export { defineStorageProvider } from './registry/asset-storage/define';
|
|
8
8
|
export type {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
StorageProvider,
|
|
10
|
+
StorageProviderContext,
|
|
11
|
+
StorageProviderFactory,
|
|
12
|
+
StorageInput,
|
|
13
|
+
StorageReadResult,
|
|
14
14
|
AssetUpload,
|
|
15
15
|
AssetUploadInput,
|
|
16
16
|
} from './registry/asset-storage/types';
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Typed identity helper for custom asset storage
|
|
2
|
+
* Typed identity helper for custom asset storage providers (mirrors `defineCatalogItem`): a consumer
|
|
3
3
|
* default-exports its result from a **server-only** module and points
|
|
4
4
|
* `runtime.registry.assetStorage.module` at it. It must be its own module (not an inline
|
|
5
5
|
* `defineConfig` function) so the build can trace it into the deployed registry. The default export
|
|
6
|
-
* may be the
|
|
6
|
+
* may be the provider object, or a factory `(context) => StorageProvider` when it needs its options/env.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { StorageProvider, StorageProviderFactory } from './types';
|
|
10
10
|
|
|
11
|
-
export function
|
|
12
|
-
export function
|
|
13
|
-
export function
|
|
11
|
+
export function defineStorageProvider(provider: StorageProvider): StorageProvider;
|
|
12
|
+
export function defineStorageProvider(factory: StorageProviderFactory): StorageProviderFactory;
|
|
13
|
+
export function defineStorageProvider(input: StorageProvider | StorageProviderFactory): StorageProvider | StorageProviderFactory {
|
|
14
14
|
return input;
|
|
15
15
|
}
|
|
@@ -2,54 +2,50 @@
|
|
|
2
2
|
* Asset storage settings resolution.
|
|
3
3
|
*
|
|
4
4
|
* `runtime.registry.assetStorage` selects the active provider for **new** uploads: the built-in
|
|
5
|
-
* `database` inline default or a `custom` module. The build bakes the
|
|
5
|
+
* `database` inline default or a `custom` module. The build bakes the provider, the module location,
|
|
6
6
|
* and the options. An option literal is baked as it is. An environment reference stays `{ $env }`
|
|
7
|
-
* and is resolved at request time, before the custom
|
|
7
|
+
* and is resolved at request time, before the custom provider factory runs.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { HandoffConfigError } from '../../config/errors';
|
|
11
11
|
import type { ResolvedConfig } from '../../types/config';
|
|
12
12
|
import type { AssetStorageProvider } from '../db/schema';
|
|
13
13
|
|
|
14
|
-
/** Which adapter implementation is active. Recorded on each blob row as its storage provider. */
|
|
15
|
-
export type AssetStorageAdapterKind = AssetStorageProvider;
|
|
16
|
-
|
|
17
14
|
/** The raw, authored settings block (a structural copy of the config type, resolvable from bake). */
|
|
18
15
|
export interface AssetStorageSettings {
|
|
19
|
-
|
|
16
|
+
provider?: AssetStorageProvider;
|
|
20
17
|
module?: string;
|
|
21
18
|
maxInlineBytes?: number;
|
|
22
19
|
options?: Record<string, unknown>;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
const
|
|
22
|
+
const PROVIDERS: readonly AssetStorageProvider[] = ['database', 'custom'];
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
export const DEFAULT_ASSET_STORAGE_ADAPTER: AssetStorageAdapterKind = 'database';
|
|
24
|
+
export const DEFAULT_ASSET_STORAGE_PROVIDER: AssetStorageProvider = 'database';
|
|
29
25
|
|
|
30
26
|
/** Default max bytes kept inline in Postgres `bytea` (4 MB, under Vercel's ~4.5 MB function limit). */
|
|
31
27
|
export const DEFAULT_MAX_INLINE_BYTES = 4 * 1024 * 1024;
|
|
32
28
|
|
|
33
29
|
/** Fully resolved asset storage settings. */
|
|
34
30
|
export interface ResolvedAssetStorage {
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/** For `custom`: the server-only
|
|
31
|
+
/** Recorded on each new blob row as its storage provider. */
|
|
32
|
+
provider: AssetStorageProvider;
|
|
33
|
+
/** For `custom`: the server-only provider module path. */
|
|
38
34
|
module?: string;
|
|
39
35
|
/** For `database`: the inline-content size ceiling. */
|
|
40
36
|
maxInlineBytes: number;
|
|
41
|
-
/**
|
|
37
|
+
/** Provider options: literals and `{ $env }` references, never secret values. */
|
|
42
38
|
options: Record<string, unknown>;
|
|
43
39
|
}
|
|
44
40
|
|
|
45
41
|
/** Resolve raw asset storage settings into a fully-defaulted shape. */
|
|
46
42
|
export const resolveAssetStorageSettings = (settings: AssetStorageSettings | null | undefined): ResolvedAssetStorage => {
|
|
47
|
-
const
|
|
43
|
+
const provider = settings?.provider ?? DEFAULT_ASSET_STORAGE_PROVIDER;
|
|
48
44
|
const options = settings?.options ?? {};
|
|
49
45
|
const maxInlineBytes =
|
|
50
46
|
typeof settings?.maxInlineBytes === 'number' && settings.maxInlineBytes > 0 ? settings.maxInlineBytes : DEFAULT_MAX_INLINE_BYTES;
|
|
51
47
|
|
|
52
|
-
return {
|
|
48
|
+
return { provider, module: settings?.module?.trim() || undefined, maxInlineBytes, options };
|
|
53
49
|
};
|
|
54
50
|
|
|
55
51
|
/** Resolve and validate the asset storage block from a loaded config (CLI/build side). An unusable block fails the build. */
|
|
@@ -59,17 +55,17 @@ export const resolveAssetStorageFromConfig = (config: ResolvedConfig | null | un
|
|
|
59
55
|
throw new HandoffConfigError(`Config "runtime.registry.assetStorage": ${problem}`);
|
|
60
56
|
};
|
|
61
57
|
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
64
|
-
fail(`unknown
|
|
58
|
+
const provider = settings?.provider ?? DEFAULT_ASSET_STORAGE_PROVIDER;
|
|
59
|
+
if (!PROVIDERS.includes(provider as AssetStorageProvider)) {
|
|
60
|
+
fail(`unknown provider ${JSON.stringify(provider)}. Use "database" or "custom".`);
|
|
65
61
|
}
|
|
66
62
|
const options = settings?.options;
|
|
67
63
|
if (options !== undefined && (options === null || typeof options !== 'object' || Array.isArray(options))) {
|
|
68
64
|
fail('"options" must be an object.');
|
|
69
65
|
}
|
|
70
66
|
const module = settings?.module;
|
|
71
|
-
if (
|
|
72
|
-
fail('
|
|
67
|
+
if (provider === 'custom' && (typeof module !== 'string' || !module.trim())) {
|
|
68
|
+
fail('provider "custom" needs a "module" that default-exports a defineStorageProvider() result.');
|
|
73
69
|
}
|
|
74
70
|
const maxInlineBytes = settings?.maxInlineBytes;
|
|
75
71
|
if (maxInlineBytes !== undefined && (typeof maxInlineBytes !== 'number' || !Number.isInteger(maxInlineBytes) || maxInlineBytes <= 0)) {
|