handoff-app 2.0.0-8 → 2.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 → CHANGELOG.md} +85 -1
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +816 -393
- package/RECIPES.md +975 -0
- package/UPGRADE.md +57 -9
- 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/connections.d.ts +66 -0
- package/dist/ai/connections.js +149 -0
- package/dist/ai/define.d.ts +10 -0
- package/dist/ai/define.js +13 -0
- package/dist/ai/types.d.ts +26 -0
- package/dist/ai/types.js +10 -0
- package/dist/app-builder/build.d.ts +14 -7
- package/dist/app-builder/build.js +195 -66
- package/dist/app-builder/client-config.js +3 -2
- package/dist/app-builder/paths.d.ts +0 -12
- package/dist/app-builder/paths.js +3 -20
- package/dist/app-builder/static-export.js +1 -1
- package/dist/app-builder/vercel-output.d.ts +1 -1
- package/dist/app-builder/vercel-output.js +5 -4
- package/dist/cache/build-cache.d.ts +27 -4
- package/dist/cache/build-cache.js +114 -47
- package/dist/cache/build-inputs.d.ts +13 -0
- package/dist/cache/build-inputs.js +19 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +5 -1
- package/dist/catalog/normalize.js +3 -0
- package/dist/catalog/previews.js +5 -4
- package/dist/catalog/types.d.ts +8 -0
- package/dist/cli/create.js +1 -2
- package/dist/commands/build/index.d.ts +1 -1
- package/dist/commands/build/index.js +1 -1
- package/dist/commands/utils.d.ts +3 -3
- package/dist/config/defaults.js +7 -4
- package/dist/config/from-env.d.ts +4 -0
- package/dist/config/from-env.js +38 -1
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +0 -1
- package/dist/config/index.js +1 -4
- package/dist/config/loader.js +28 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +9 -11
- package/dist/handlebars.d.ts +10 -2
- package/dist/index.d.ts +12 -7
- package/dist/index.js +18 -22
- 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/pipeline/figma.d.ts +13 -2
- package/dist/pipeline/figma.js +86 -46
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/validation.d.ts +1 -1
- package/dist/pipeline/validation.js +3 -3
- package/dist/providers.d.ts +10 -0
- package/dist/providers.js +13 -0
- 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 +9 -19
- package/dist/registry/asset-storage/resolve.js +33 -21
- package/dist/registry/asset-storage/types.d.ts +37 -17
- package/dist/registry/asset-storage/types.js +5 -5
- package/dist/registry/auth/ai-keys.d.ts +32 -0
- package/dist/registry/auth/ai-keys.js +69 -0
- package/dist/registry/auth/crypto.d.ts +17 -0
- package/dist/registry/auth/crypto.js +61 -1
- package/dist/registry/auth/index.d.ts +2 -1
- package/dist/registry/auth/index.js +11 -2
- package/dist/registry/checkout/index.d.ts +2 -1
- package/dist/registry/checkout/index.js +57 -127
- package/dist/registry/client.js +53 -17
- package/dist/registry/db/driver.js +3 -3
- package/dist/registry/db/schema.d.ts +261 -3
- package/dist/registry/db/schema.js +29 -4
- package/dist/registry/email/define.d.ts +10 -0
- package/dist/registry/email/define.js +13 -0
- package/dist/registry/email/providers/smtp.d.ts +10 -0
- package/dist/registry/email/providers/smtp.js +73 -0
- package/dist/registry/email/resolve.d.ts +28 -0
- package/dist/registry/email/resolve.js +74 -0
- package/dist/registry/email/types.d.ts +24 -0
- package/dist/registry/email/types.js +6 -0
- package/dist/registry/path.d.ts +2 -0
- package/dist/registry/path.js +11 -1
- package/dist/registry/publish/package.js +8 -0
- package/dist/registry/transfer.d.ts +4 -0
- package/dist/store/registry.d.ts +3 -3
- package/dist/store/registry.js +4 -4
- package/dist/transformers/plugins/ssr-render.js +40 -15
- package/dist/transformers/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +3 -1
- package/dist/transformers/preview/component/css.js +92 -51
- package/dist/transformers/preview/component/html.d.ts +3 -1
- package/dist/transformers/preview/component/html.js +6 -2
- package/dist/transformers/preview/component/javascript.d.ts +2 -1
- package/dist/transformers/preview/component/javascript.js +10 -4
- package/dist/transformers/preview/pattern/html.js +3 -1
- package/dist/transformers/preview/types.d.ts +13 -0
- package/dist/transformers/utils/build.d.ts +24 -0
- package/dist/transformers/utils/build.js +100 -1
- package/dist/transformers/utils/module.js +2 -1
- package/dist/types/config.d.ts +135 -18
- package/dist/utils/path.d.ts +1 -5
- package/dist/utils/path.js +6 -23
- package/docs/api.md +6 -4
- package/docs/cli.md +275 -54
- package/drizzle/0007_add_ai_keys.sql +11 -0
- package/drizzle/0008_add_source_dir.sql +2 -0
- package/drizzle/meta/0007_snapshot.json +2013 -0
- package/drizzle/meta/0008_snapshot.json +2025 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +19 -8
- package/src/ai/README.md +42 -0
- package/src/ai/connections.ts +189 -0
- package/src/ai/define.ts +15 -0
- package/src/ai/types.ts +30 -0
- package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
- package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
- package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
- package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
- package/src/app/components/AiAssistant/dockState.ts +86 -0
- package/src/app/components/AiAssistant/toolLabel.ts +24 -0
- package/src/app/components/AiAssistant/useAiChat.ts +147 -0
- package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
- package/src/app/components/AiAssistant/usePageContext.ts +63 -0
- package/src/app/components/Auth/AccountLayout.tsx +58 -19
- package/src/app/components/Auth/AuthShell.tsx +27 -33
- package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
- package/src/app/components/Auth/installStatus.ts +29 -0
- package/src/app/components/Component/PageSliceResolver.tsx +1 -0
- package/src/app/components/Component/Preview.tsx +383 -189
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
- package/src/app/components/Foundations/ColorGrid.tsx +13 -14
- package/src/app/components/Layout/Header.tsx +21 -15
- package/src/app/components/Layout/Main.tsx +2 -2
- package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
- package/src/app/components/Navigation/MobileNav.tsx +2 -2
- package/src/app/components/Navigation/SideNav.tsx +1 -15
- package/src/app/components/context/NavProvider.tsx +7 -3
- package/src/app/components/ui/attachment.tsx +50 -0
- package/src/app/components/ui/bubble.tsx +57 -0
- package/src/app/components/ui/button.tsx +3 -1
- package/src/app/components/ui/confirm-dialog.tsx +87 -0
- package/src/app/components/ui/dialog.tsx +7 -2
- package/src/app/components/ui/marker.tsx +56 -0
- package/src/app/components/ui/sidebar.tsx +3 -1
- package/src/app/components/util/index.ts +3 -12
- package/src/app/css/index.css +64 -0
- package/src/app/lib/ai/auth.ts +33 -0
- package/src/app/lib/ai/context.ts +99 -0
- package/src/app/lib/ai/model.ts +79 -0
- package/src/app/lib/ai/prompt.ts +36 -0
- package/src/app/lib/ai/resolve.ts +97 -0
- package/src/app/lib/ai/tools.ts +120 -0
- package/src/app/lib/api/methods.ts +13 -0
- package/src/app/lib/asset-storage.ts +35 -36
- package/src/app/lib/auth/api.ts +4 -12
- package/src/app/lib/auth/email.ts +96 -23
- 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 +47 -21
- package/src/app/lib/mcp/components.ts +523 -0
- package/src/app/lib/mcp/page-url.ts +24 -0
- package/src/app/lib/mcp/pages.ts +84 -0
- package/src/app/lib/mcp/result.ts +33 -0
- package/src/app/lib/mcp/server.ts +39 -318
- package/src/app/lib/mcp/tokens.ts +334 -0
- package/src/app/lib/mcp/tool.ts +19 -0
- package/src/app/lib/registry-api/asset-transfer.ts +119 -11
- package/src/app/lib/registry-api/errors.ts +2 -0
- package/src/app/lib/registry-api/handler.ts +7 -1
- package/src/app/lib/registry-api/transfer.ts +28 -6
- package/src/app/lib/server-module.ts +34 -0
- package/src/app/lib/utils.ts +20 -0
- package/src/app/next.config.mjs +26 -24
- package/src/app/pages/[...slug]/index.tsx +2 -1
- package/src/app/pages/_app.tsx +6 -2
- package/src/app/pages/_document.tsx +6 -0
- package/src/app/pages/account/ai.tsx +183 -0
- package/src/app/pages/account/index.tsx +68 -68
- package/src/app/pages/account/tokens.tsx +37 -7
- package/src/app/pages/account/users.tsx +75 -15
- package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
- package/src/app/pages/api/account/ai/keys/index.ts +20 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
- package/src/app/pages/api/admin/users/index.ts +2 -2
- package/src/app/pages/api/ai/chat.ts +115 -0
- package/src/app/pages/api/ai/connections.ts +23 -0
- package/src/app/pages/api/auth/request-reset.ts +14 -16
- package/src/app/pages/api/docs/nav.json.ts +1 -1
- package/src/app/pages/api/install.ts +3 -3
- package/src/app/pages/api/mcp.ts +6 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
- package/src/app/pages/cli/device.tsx +10 -5
- 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/install.tsx +7 -2
- package/src/app/pages/login.tsx +32 -27
- package/src/app/pages/reset-password.tsx +21 -20
- 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/README.md +2 -1
- package/src/app-builder/build.ts +218 -73
- package/src/app-builder/client-config.ts +3 -2
- package/src/app-builder/paths.ts +3 -22
- package/src/app-builder/static-export.ts +1 -1
- package/src/app-builder/vercel-output.ts +5 -4
- package/src/cache/README.md +4 -1
- package/src/cache/build-cache.ts +128 -43
- package/src/cache/build-inputs.ts +28 -0
- package/src/cache/index.ts +4 -0
- package/src/catalog/README.md +3 -0
- package/src/catalog/normalize.ts +2 -0
- package/src/catalog/previews.ts +6 -3
- package/src/catalog/types.ts +8 -0
- package/src/cli/create.ts +1 -2
- package/src/commands/build/index.ts +1 -1
- package/src/config/README.md +10 -9
- package/src/config/defaults.ts +7 -4
- package/src/config/from-env.ts +33 -0
- package/src/config/helpers.ts +1 -3
- package/src/config/index.ts +0 -3
- package/src/config/loader.ts +27 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +10 -5
- package/src/handlebars.ts +8 -2
- package/src/index.ts +33 -29
- package/src/nav/index.ts +2 -2
- package/src/nav/registry.ts +3 -3
- package/src/nav/workspace.ts +3 -3
- package/src/pipeline/README.md +1 -1
- package/src/pipeline/figma.ts +98 -49
- package/src/pipeline/styles.ts +1 -1
- package/src/pipeline/validation.ts +3 -3
- package/src/providers.ts +18 -0
- package/src/registry/asset-storage/define.ts +6 -6
- package/src/registry/asset-storage/resolve.ts +38 -37
- package/src/registry/asset-storage/types.ts +39 -17
- package/src/registry/auth/ai-keys.ts +62 -0
- package/src/registry/auth/crypto.ts +63 -1
- package/src/registry/auth/index.ts +11 -1
- package/src/registry/checkout/index.ts +60 -138
- package/src/registry/client.ts +60 -20
- package/src/registry/db/driver.ts +2 -2
- package/src/registry/db/schema.ts +49 -22
- package/src/registry/email/define.ts +15 -0
- package/src/registry/email/providers/smtp.ts +39 -0
- package/src/registry/email/resolve.ts +94 -0
- package/src/registry/email/types.ts +28 -0
- package/src/registry/path.ts +11 -0
- package/src/registry/publish/package.ts +9 -0
- package/src/registry/transfer.ts +4 -0
- package/src/store/registry.ts +7 -7
- package/src/transformers/plugins/ssr-render.ts +53 -16
- package/src/transformers/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +101 -53
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/preview/pattern/html.ts +3 -1
- package/src/transformers/preview/types.ts +14 -0
- package/src/transformers/utils/build.ts +93 -0
- package/src/transformers/utils/module.ts +4 -2
- package/src/types/config.ts +148 -20
- package/src/utils/README.md +1 -1
- package/src/utils/path.ts +6 -29
- package/templates/default/handoff.config.js.tpl +10 -2
- package/templates/default/handoff.config.ts.tpl +10 -2
- package/templates/default/handoff.config.with-examples.js.tpl +10 -2
- package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
- package/templates/default/package.json.tpl +2 -2
- 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/config/validator.d.ts +0 -9
- package/dist/config/validator.js +0 -24
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
- package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
- package/src/app/components/old/Header.tsx +0 -89
- package/src/app/lib/mcp/shape.ts +0 -596
- 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/config/validator.ts +0 -22
- package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
|
@@ -4,11 +4,32 @@ import { Logger } from '../utils/logger';
|
|
|
4
4
|
import { isEnvReference } from './from-env';
|
|
5
5
|
import { HandoffConfigError } from './errors';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
7
|
+
/**
|
|
8
|
+
* Match a dot path against a pattern. A `*` segment matches exactly one segment, so one pattern
|
|
9
|
+
* covers every element of a list. With `prefix`, the pattern also matches anything nested under it.
|
|
10
|
+
*/
|
|
11
|
+
const pathMatches = (pattern: string, path: string, prefix = false): boolean => {
|
|
12
|
+
const expected = pattern.split('.');
|
|
13
|
+
const actual = path.split('.');
|
|
14
|
+
if (prefix ? expected.length > actual.length : expected.length !== actual.length) return false;
|
|
15
|
+
return expected.every((segment, index) => segment === '*' || segment === actual[index]);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Paths whose value the deployed app reads at request time, so only the variable name is stored. */
|
|
19
|
+
export const DEFERRED_PATHS = [
|
|
20
|
+
'runtime.registry.database.url',
|
|
21
|
+
'runtime.registry.email.options.apiKey',
|
|
22
|
+
'runtime.registry.email.options.password',
|
|
23
|
+
'runtime.ai.connections.*.apiKey',
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Path prefixes whose nested references the deployed app reads at request time. Literals below them
|
|
27
|
+
* are baked. `DEFERRED_PATHS` entries below them still reject literals.
|
|
28
|
+
*/
|
|
29
|
+
const DEFERRED_PREFIXES = ['runtime.registry.email.options', 'runtime.registry.assetStorage.options', 'runtime.ai.connections.*.options'];
|
|
30
|
+
const SECRET_PATHS = new Set(['integrations.figma.accessToken', 'runtime.registryConnection.accessToken']);
|
|
31
|
+
const isDeferred = (path: string): boolean => DEFERRED_PATHS.some((deferred) => pathMatches(deferred, path));
|
|
32
|
+
const isUnderDeferredPrefix = (path: string): boolean => DEFERRED_PREFIXES.some((prefix) => pathMatches(`${prefix}.*`, path, true));
|
|
12
33
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
13
34
|
const ALIASES: Record<string, string> = {
|
|
14
35
|
HANDOFF_REGISTRY_URL: 'HANDOFF_CLOUD_URL',
|
|
@@ -40,20 +61,12 @@ export const resolveConfigEnv = (config: Config, profile?: string, defaults?: Co
|
|
|
40
61
|
throw new HandoffConfigError(`Config "${path}" (profile "${profile ?? 'default'}"): ${message}`);
|
|
41
62
|
};
|
|
42
63
|
const walk = (value: unknown, path: string, seed?: unknown): unknown => {
|
|
43
|
-
const deferred =
|
|
64
|
+
const deferred = isDeferred(path);
|
|
44
65
|
const secret = SECRET_PATHS.has(path);
|
|
45
66
|
if ((secret || deferred) && value !== undefined && !isEnvReference(value)) {
|
|
46
67
|
fail(path, 'Expected an environment reference, such as { $env: "VARIABLE_NAME" }. Literal values are not allowed.');
|
|
47
68
|
}
|
|
48
69
|
if (isEnvReference(value)) {
|
|
49
|
-
// Checked before the name, so the message names the supported pattern instead of the reference shape.
|
|
50
|
-
if (isBaked(path)) {
|
|
51
|
-
fail(
|
|
52
|
-
path,
|
|
53
|
-
'Environment references are not allowed here because the resolved value is baked into the build. ' +
|
|
54
|
-
"Put the variable name in options and read it through the adapter factory's `env` argument."
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
70
|
if (typeof value.$env !== 'string' || !ENV_NAME.test(value.$env)) {
|
|
58
71
|
fail(path, 'Invalid environment variable name. Use letters, digits, and underscores, starting with a letter or underscore.');
|
|
59
72
|
}
|
|
@@ -62,7 +75,7 @@ export const resolveConfigEnv = (config: Config, profile?: string, defaults?: Co
|
|
|
62
75
|
if (Object.keys(value).some((key) => key !== '$env' && key !== 'default')) {
|
|
63
76
|
fail(path, `Environment variable "${name}" has an invalid reference shape.`);
|
|
64
77
|
}
|
|
65
|
-
if (deferred) {
|
|
78
|
+
if (deferred || isUnderDeferredPrefix(path)) {
|
|
66
79
|
if ('default' in value) fail(path, `Environment variable "${name}" cannot have a default because it is read by the deployed app.`);
|
|
67
80
|
return { $env: name };
|
|
68
81
|
}
|
package/src/config/runtime.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import esbuild from 'esbuild';
|
|
2
|
+
import { STYLE_IMPORT_LOADERS } from '../transformers/utils/build';
|
|
2
3
|
import fs from 'fs-extra';
|
|
3
4
|
import { createRequire } from 'module';
|
|
4
5
|
import path from 'path';
|
|
@@ -35,7 +36,8 @@ type DeclarationResolution = {
|
|
|
35
36
|
fileName: string;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
/** `.tsx`/`.jsx` are accepted so a preview can write its `render` as JSX. */
|
|
40
|
+
const DECLARATION_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'cjs'] as const;
|
|
39
41
|
|
|
40
42
|
const getDeclarationFiles = (componentBaseName: string): string[] =>
|
|
41
43
|
DECLARATION_EXTENSIONS.map((ext) => `${componentBaseName}.handoff.${ext}`);
|
|
@@ -46,7 +48,7 @@ const findPreferredDeclaration = (componentDir: string, componentBaseName: strin
|
|
|
46
48
|
if (exactMatch) return exactMatch;
|
|
47
49
|
|
|
48
50
|
const allFiles = fs.existsSync(componentDir) ? fs.readdirSync(componentDir) : [];
|
|
49
|
-
const declarationFiles = allFiles.filter((file) => /\.handoff\.(ts|js|cjs)$/.test(file)).sort((a, b) => a.localeCompare(b));
|
|
51
|
+
const declarationFiles = allFiles.filter((file) => /\.handoff\.(tsx|ts|jsx|js|cjs)$/.test(file)).sort((a, b) => a.localeCompare(b));
|
|
50
52
|
|
|
51
53
|
if (!declarationFiles.length) return undefined;
|
|
52
54
|
|
|
@@ -70,10 +72,11 @@ const evaluateDeclaration = (filePath: string, handoffModulePath: string): any =
|
|
|
70
72
|
write: false,
|
|
71
73
|
platform: 'node',
|
|
72
74
|
format: 'cjs',
|
|
73
|
-
target: '
|
|
75
|
+
target: 'node22',
|
|
74
76
|
logLevel: 'silent',
|
|
75
77
|
jsx: 'automatic',
|
|
76
78
|
external: ['react', 'react-dom', 'handoff-app'],
|
|
79
|
+
loader: STYLE_IMPORT_LOADERS,
|
|
77
80
|
});
|
|
78
81
|
|
|
79
82
|
const code = buildResult.outputFiles?.[0]?.text;
|
|
@@ -219,7 +222,7 @@ export const initRuntimeConfig = (
|
|
|
219
222
|
if (!declaration) {
|
|
220
223
|
const declarationFiles = getDeclarationFiles(itemBaseName);
|
|
221
224
|
Logger.warn(
|
|
222
|
-
`Missing catalog declaration (use .handoff.ts, .handoff.js, or .handoff.cjs; see UPGRADE.md#catalog-items): ${path.resolve(itemPath, declarationFiles.join(' or '))}`
|
|
225
|
+
`Missing catalog declaration (use .handoff.tsx, .handoff.ts, .handoff.jsx, .handoff.js, or .handoff.cjs; see UPGRADE.md#catalog-items): ${path.resolve(itemPath, declarationFiles.join(' or '))}`
|
|
223
226
|
);
|
|
224
227
|
continue;
|
|
225
228
|
}
|
|
@@ -433,9 +436,9 @@ const injectPatternPreviews = (result: RuntimeConfig): void => {
|
|
|
433
436
|
|
|
434
437
|
// Case 3: args present (with or without preview base) -> create synthetic preview
|
|
435
438
|
let resolvedValues: Record<string, any> = {};
|
|
439
|
+
const basePreview = ref.preview ? component.previews?.[ref.preview] : undefined;
|
|
436
440
|
|
|
437
441
|
if (ref.preview) {
|
|
438
|
-
const basePreview = component.previews?.[ref.preview];
|
|
439
442
|
if (basePreview) {
|
|
440
443
|
resolvedValues = { ...basePreview.values };
|
|
441
444
|
} else {
|
|
@@ -454,6 +457,8 @@ const injectPatternPreviews = (result: RuntimeConfig): void => {
|
|
|
454
457
|
values: resolvedValues,
|
|
455
458
|
url: '',
|
|
456
459
|
sourcePreview: ref.preview,
|
|
460
|
+
// The ref overrides arguments, not how the preview renders, so a base `render` still applies.
|
|
461
|
+
...(basePreview?.render ? { render: basePreview.render } : {}),
|
|
457
462
|
};
|
|
458
463
|
|
|
459
464
|
ref.resolvedPreview = syntheticKey;
|
package/src/handlebars.ts
CHANGED
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
* an argument type to check previews: `defineCatalogItem<BadgeArgs>`.
|
|
6
6
|
*/
|
|
7
7
|
import { createCatalogItem } from './catalog/define';
|
|
8
|
-
import type { CatalogItem, CatalogItemMeta } from './catalog/types';
|
|
8
|
+
import type { CatalogItem, CatalogItemMeta, CatalogPreview as RendererPreview } from './catalog/types';
|
|
9
9
|
|
|
10
|
-
export type { CatalogItem, CatalogItemMeta,
|
|
10
|
+
export type { CatalogItem, CatalogItemMeta, CompositionRef, SourceDescriptor } from './catalog/types';
|
|
11
|
+
|
|
12
|
+
/** A template renderer never calls a preview `render`, so declaring one is an error, not a no-op. */
|
|
13
|
+
export type CatalogPreview<TArgs = Record<string, unknown>> = Omit<RendererPreview<TArgs>, 'render'> & { render?: never };
|
|
14
|
+
|
|
15
|
+
/** Preview type for a Handlebars catalog item, with its argument type applied. */
|
|
16
|
+
export type Preview<TItem> = TItem extends { readonly __args?: infer TArgs } ? CatalogPreview<Partial<NonNullable<TArgs>>> : never;
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Declares a Handlebars catalog item. `implementation` is the path to its `.hbs` template. The
|
package/src/index.ts
CHANGED
|
@@ -12,10 +12,16 @@ import {
|
|
|
12
12
|
initRuntimeConfig,
|
|
13
13
|
loadProfileEnv,
|
|
14
14
|
resolveProfileSelection,
|
|
15
|
-
validateConfig,
|
|
16
15
|
} from './config';
|
|
17
16
|
import pipeline, { buildComponents, buildPatterns } from './pipeline';
|
|
18
|
-
import {
|
|
17
|
+
import { resolveFigmaConnection } from './pipeline/figma';
|
|
18
|
+
import {
|
|
19
|
+
ALL_KIND_ORDER,
|
|
20
|
+
ENTITY_WIRE_KIND,
|
|
21
|
+
isRegistryEntityKind,
|
|
22
|
+
REGISTRY_ENTITY_KINDS,
|
|
23
|
+
type RegistryEntityKind,
|
|
24
|
+
} from './registry/content-kinds';
|
|
19
25
|
import type { TransferEntityKind } from './registry/transfer';
|
|
20
26
|
import { createFilesystemStore, type HandoffStore } from './store';
|
|
21
27
|
import processComponents, { ComponentSegment } from './transformers/preview/component/builder';
|
|
@@ -231,7 +237,7 @@ class Handoff {
|
|
|
231
237
|
this.exportsDirectory = config.exportsOutputDirectory ?? this.exportsDirectory;
|
|
232
238
|
this.sitesDirectory = config.sitesOutputDirectory ?? this.exportsDirectory;
|
|
233
239
|
[this.runtimeConfig, this._configFilePaths, this._configFileIndex] = initRuntimeConfig(this);
|
|
234
|
-
if(this.config.app.base_path && !process.env.HANDOFF_APP_BASE_PATH) {
|
|
240
|
+
if (this.config.app.base_path && !process.env.HANDOFF_APP_BASE_PATH) {
|
|
235
241
|
process.env.HANDOFF_APP_BASE_PATH = this.config.app.base_path ?? '';
|
|
236
242
|
}
|
|
237
243
|
return this;
|
|
@@ -242,13 +248,10 @@ class Handoff {
|
|
|
242
248
|
return this;
|
|
243
249
|
}
|
|
244
250
|
|
|
245
|
-
preRunner(
|
|
251
|
+
preRunner(): Handoff {
|
|
246
252
|
if (!this.config) {
|
|
247
253
|
throw Error('Handoff not initialized');
|
|
248
254
|
}
|
|
249
|
-
if (validate) {
|
|
250
|
-
this.config = validateConfig(this.config);
|
|
251
|
-
}
|
|
252
255
|
return this;
|
|
253
256
|
}
|
|
254
257
|
|
|
@@ -523,13 +526,8 @@ class Handoff {
|
|
|
523
526
|
return this._handoffRunner;
|
|
524
527
|
}
|
|
525
528
|
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
accessToken: this.config.dev_access_token,
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
// Initialize the provider
|
|
532
|
-
const provider = Providers.RestApiProvider(apiCredentials);
|
|
529
|
+
const { projectId, accessToken } = resolveFigmaConnection(this.config);
|
|
530
|
+
const provider = Providers.RestApiProvider({ projectId, accessToken });
|
|
533
531
|
|
|
534
532
|
this._handoffRunner = HandoffRunner(
|
|
535
533
|
provider,
|
|
@@ -557,15 +555,8 @@ class Handoff {
|
|
|
557
555
|
return this._handoffRunner;
|
|
558
556
|
}
|
|
559
557
|
|
|
560
|
-
/**
|
|
561
|
-
* Gets the project ID, falling back to filesystem-safe working path if figma_project_id is missing
|
|
562
|
-
* @returns {string} The project ID to use for path construction
|
|
563
|
-
*/
|
|
558
|
+
/** Keys `.handoff/<projectId>`, so workspaces that share one installed package stage apart. */
|
|
564
559
|
getProjectId(): string {
|
|
565
|
-
if (this.config?.figma_project_id) {
|
|
566
|
-
return this.config.figma_project_id;
|
|
567
|
-
}
|
|
568
|
-
// Fallback to filesystem-safe transformation of working path
|
|
569
560
|
return generateFilesystemSafeId(this.workingPath);
|
|
570
561
|
}
|
|
571
562
|
|
|
@@ -574,7 +565,7 @@ class Handoff {
|
|
|
574
565
|
* @returns {string} The absolute path to the output directory
|
|
575
566
|
*/
|
|
576
567
|
getOutputPath(): string {
|
|
577
|
-
return path.resolve(this.workingPath, this.exportsDirectory
|
|
568
|
+
return path.resolve(this.workingPath, this.exportsDirectory);
|
|
578
569
|
}
|
|
579
570
|
|
|
580
571
|
/**
|
|
@@ -642,6 +633,13 @@ class Handoff {
|
|
|
642
633
|
return this._mainConfigFilePath;
|
|
643
634
|
}
|
|
644
635
|
|
|
636
|
+
/**
|
|
637
|
+
* Gets the selected profile config file path if one exists.
|
|
638
|
+
*/
|
|
639
|
+
getProfileConfigFilePath(): string | undefined {
|
|
640
|
+
return this._profileConfigFilePath;
|
|
641
|
+
}
|
|
642
|
+
|
|
645
643
|
getProfile(): string | undefined {
|
|
646
644
|
return this._profile;
|
|
647
645
|
}
|
|
@@ -699,14 +697,20 @@ export type {
|
|
|
699
697
|
SourceFormat,
|
|
700
698
|
RendererKind,
|
|
701
699
|
} from './catalog';
|
|
702
|
-
export {
|
|
700
|
+
export { defineStorageProvider } from './registry/asset-storage/define';
|
|
703
701
|
export type {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
702
|
+
StorageProvider,
|
|
703
|
+
StorageProviderContext,
|
|
704
|
+
StorageProviderFactory,
|
|
705
|
+
StorageInput,
|
|
706
|
+
StorageReadResult,
|
|
707
|
+
AssetUpload,
|
|
708
|
+
AssetUploadInput,
|
|
709
709
|
} from './registry/asset-storage/types';
|
|
710
|
+
export { defineEmailProvider } from './registry/email/define';
|
|
711
|
+
export type { EmailMessage, EmailProvider, EmailProviderContext, EmailProviderFactory } from './registry/email/types';
|
|
712
|
+
export { defineAiProvider } from './ai/define';
|
|
713
|
+
export type { AiProvider, AiProviderContext, AiProviderFactory } from './ai/types';
|
|
710
714
|
|
|
711
715
|
// Export transformers and types from handoff-core
|
|
712
716
|
export { Transformers as CoreTransformers, TransformerUtils as CoreTransformerUtils, Types as CoreTypes } from 'handoff-core';
|
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),
|
package/src/pipeline/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Handles the end-to-end Figma data pipeline: authentication, extraction, token ge
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|------|---------|
|
|
9
9
|
| `index.ts` | Main `pipeline()` orchestrator and barrel re-exports |
|
|
10
|
-
| `figma.ts` | `
|
|
10
|
+
| `figma.ts` | `resolveFigmaConnection()` — Figma values from `integrations.figma` and their variable names; `validateFigmaAuth()` — in a terminal, asks for an empty value and can save it to the profile env file, otherwise throws `HandoffConfigError`; `figmaExtract()` — Figma data extraction |
|
|
11
11
|
| `styles.ts` | `buildStyles()` — design token transformers; `buildCustomFonts()` — font zipping |
|
|
12
12
|
| `components.ts` | `buildComponents()` — component preview generation |
|
|
13
13
|
| `documentation.ts` | `createDocumentationObject()` — Figma data extraction into a documentation object with assets and SVG sprites |
|
package/src/pipeline/figma.ts
CHANGED
|
@@ -4,29 +4,74 @@ import { Types as HandoffTypes } from 'handoff-core';
|
|
|
4
4
|
import * as stream from 'node:stream';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import Handoff from '..';
|
|
7
|
+
import { HandoffConfigError } from '../config/errors';
|
|
8
|
+
import { configEnvName } from '../config/resolve-env';
|
|
9
|
+
import type { ResolvedConfig } from '../types/config';
|
|
7
10
|
import { Logger } from '../utils/logger';
|
|
8
11
|
import { zipAssets } from './archive';
|
|
9
12
|
import { createDocumentationObject } from './documentation';
|
|
10
13
|
|
|
14
|
+
/** Figma values resolved at config load, with the variable names they come from. */
|
|
15
|
+
export type FigmaConnection = {
|
|
16
|
+
projectId: string;
|
|
17
|
+
accessToken: string;
|
|
18
|
+
/** Unset when the config gives a literal file ID. */
|
|
19
|
+
projectIdEnv?: string;
|
|
20
|
+
accessTokenEnv?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const resolveFigmaConnection = (config: ResolvedConfig | null | undefined): FigmaConnection => {
|
|
24
|
+
const figma = config?.integrations?.figma;
|
|
25
|
+
return {
|
|
26
|
+
projectId: figma?.projectId?.trim() || '',
|
|
27
|
+
accessToken: figma?.accessToken?.trim() || '',
|
|
28
|
+
projectIdEnv: configEnvName(config, 'integrations.figma.projectId'),
|
|
29
|
+
accessTokenEnv: configEnvName(config, 'integrations.figma.accessToken'),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const describeSource = (envName: string | undefined, configPath: string): string =>
|
|
34
|
+
envName ? `${envName} (${configPath})` : configPath;
|
|
35
|
+
|
|
36
|
+
/** Replaces each variable that the file already sets and appends the others. */
|
|
37
|
+
const saveEnvValues = async (filePath: string, values: Record<string, string>): Promise<void> => {
|
|
38
|
+
const lines = fs.existsSync(filePath) ? (await fs.readFile(filePath, 'utf8')).split(/\r?\n/) : [];
|
|
39
|
+
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
|
|
40
|
+
for (const [name, value] of Object.entries(values)) {
|
|
41
|
+
const line = `${name}="${value}"`;
|
|
42
|
+
const index = lines.findIndex((existing) => new RegExp(`^\\s*(export\\s+)?${name}\\s*=`).test(existing));
|
|
43
|
+
if (index === -1) lines.push(line);
|
|
44
|
+
else lines[index] = line;
|
|
45
|
+
}
|
|
46
|
+
await fs.writeFile(filePath, `${lines.join('\n')}\n`);
|
|
47
|
+
};
|
|
48
|
+
|
|
11
49
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
50
|
+
* Makes sure that `fetch` has a Figma file ID and access token. In a terminal, it asks for an empty
|
|
51
|
+
* value and can save it to the env file of the selected profile. Without a terminal, it throws
|
|
52
|
+
* `HandoffConfigError`.
|
|
14
53
|
*/
|
|
15
54
|
export const validateFigmaAuth = async (handoff: Handoff): Promise<void> => {
|
|
16
|
-
|
|
17
|
-
let
|
|
55
|
+
const connection = resolveFigmaConnection(handoff.config);
|
|
56
|
+
let { projectId, accessToken } = connection;
|
|
18
57
|
|
|
19
|
-
if (
|
|
58
|
+
if (projectId && accessToken) {
|
|
20
59
|
return;
|
|
21
60
|
}
|
|
22
61
|
|
|
23
|
-
|
|
62
|
+
const profile = handoff.getProfile();
|
|
63
|
+
if (!process.stdin.isTTY) {
|
|
64
|
+
const missing = [
|
|
65
|
+
!accessToken && describeSource(connection.accessTokenEnv, 'integrations.figma.accessToken'),
|
|
66
|
+
!projectId && describeSource(connection.projectIdEnv, 'integrations.figma.projectId'),
|
|
67
|
+
].filter(Boolean);
|
|
68
|
+
throw new HandoffConfigError(`Figma is not configured (profile "${profile ?? 'default'}"). Set ${missing.join(' and ')}.`);
|
|
69
|
+
}
|
|
24
70
|
|
|
25
|
-
if (!
|
|
26
|
-
missingEnvVars = true;
|
|
71
|
+
if (!accessToken) {
|
|
27
72
|
p.log.warn(
|
|
28
|
-
`Figma
|
|
29
|
-
|
|
73
|
+
`Figma access token not found. Set ${describeSource(connection.accessTokenEnv, 'integrations.figma.accessToken')}.\n` +
|
|
74
|
+
`Use these instructions to generate one: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens`
|
|
30
75
|
);
|
|
31
76
|
const token = await p.password({
|
|
32
77
|
message: 'Figma Developer Key:',
|
|
@@ -35,68 +80,55 @@ export const validateFigmaAuth = async (handoff: Handoff): Promise<void> => {
|
|
|
35
80
|
p.cancel('Authentication cancelled.');
|
|
36
81
|
process.exit(0);
|
|
37
82
|
}
|
|
38
|
-
|
|
83
|
+
accessToken = (token as string).trim();
|
|
39
84
|
}
|
|
40
85
|
|
|
41
|
-
if (!
|
|
42
|
-
missingEnvVars = true;
|
|
86
|
+
if (!projectId) {
|
|
43
87
|
p.log.warn(
|
|
44
|
-
`Figma project ID not found.
|
|
45
|
-
|
|
88
|
+
`Figma project ID not found. Set ${describeSource(connection.projectIdEnv, 'integrations.figma.projectId')}.\n` +
|
|
89
|
+
`Find it in your Figma file URL (e.g., figma.com/file/{PROJECT_ID}/...).`
|
|
46
90
|
);
|
|
47
|
-
const
|
|
91
|
+
const value = await p.text({
|
|
48
92
|
message: 'Figma Project Id:',
|
|
49
93
|
validate: (value) => {
|
|
50
94
|
if (!value.trim()) return 'Project ID is required';
|
|
51
95
|
},
|
|
52
96
|
});
|
|
53
|
-
if (p.isCancel(
|
|
97
|
+
if (p.isCancel(value)) {
|
|
54
98
|
p.cancel('Authentication cancelled.');
|
|
55
99
|
process.exit(0);
|
|
56
100
|
}
|
|
57
|
-
|
|
101
|
+
projectId = (value as string).trim();
|
|
58
102
|
}
|
|
59
103
|
|
|
60
|
-
|
|
61
|
-
|
|
104
|
+
const values: Record<string, string> = {};
|
|
105
|
+
if (!connection.accessToken && connection.accessTokenEnv) values[connection.accessTokenEnv] = accessToken;
|
|
106
|
+
if (!connection.projectId && connection.projectIdEnv) values[connection.projectIdEnv] = projectId;
|
|
62
107
|
|
|
63
|
-
|
|
64
|
-
|
|
108
|
+
const envFileName = profile ? `.env.${profile}` : '.env';
|
|
109
|
+
if (Object.keys(values).length) {
|
|
110
|
+
const save = await p.confirm({
|
|
111
|
+
message: `Save ${Object.keys(values).join(' and ')} to ${envFileName} for future runs?`,
|
|
65
112
|
initialValue: true,
|
|
66
113
|
});
|
|
67
114
|
|
|
68
|
-
if (p.isCancel(
|
|
69
|
-
p.log.info(`Skipped .
|
|
115
|
+
if (p.isCancel(save) || save === false) {
|
|
116
|
+
p.log.info(`Skipped saving. Set these variables before the next run.`);
|
|
70
117
|
} else {
|
|
71
|
-
const envFilePath = path.resolve(handoff.workingPath, '.env');
|
|
72
|
-
const envFileContent = `
|
|
73
|
-
HANDOFF_DEV_ACCESS_TOKEN="${DEV_ACCESS_TOKEN}"
|
|
74
|
-
HANDOFF_FIGMA_PROJECT_ID="${FIGMA_PROJECT_ID}"
|
|
75
|
-
`;
|
|
76
|
-
|
|
77
118
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.catch(() => false);
|
|
82
|
-
|
|
83
|
-
if (fileExists) {
|
|
84
|
-
await fs.appendFile(envFilePath, envFileContent);
|
|
85
|
-
p.log.success(
|
|
86
|
-
`The .env file was found and updated with new content. Since these are sensitive variables, please do not commit this file.`
|
|
87
|
-
);
|
|
88
|
-
} else {
|
|
89
|
-
await fs.writeFile(envFilePath, envFileContent.replace(/^\s*[\r\n]/gm, ''));
|
|
90
|
-
p.log.success(`Created .env file. Do not commit sensitive variables.`);
|
|
91
|
-
}
|
|
119
|
+
await saveEnvValues(path.resolve(handoff.workingPath, envFileName), values);
|
|
120
|
+
const secretNote = connection.accessTokenEnv && values[connection.accessTokenEnv] ? ' It contains a secret, so do not commit it.' : '';
|
|
121
|
+
p.log.success(`Saved to ${envFileName}.${secretNote}`);
|
|
92
122
|
} catch (error) {
|
|
93
|
-
Logger.error(
|
|
123
|
+
Logger.error(`Could not save ${envFileName}:`, error);
|
|
94
124
|
}
|
|
95
125
|
}
|
|
96
126
|
}
|
|
97
127
|
|
|
98
|
-
handoff.config.
|
|
99
|
-
|
|
128
|
+
handoff.config.integrations = {
|
|
129
|
+
...handoff.config.integrations,
|
|
130
|
+
figma: { ...handoff.config.integrations?.figma, projectId, accessToken },
|
|
131
|
+
};
|
|
100
132
|
};
|
|
101
133
|
|
|
102
134
|
/**
|
|
@@ -105,13 +137,26 @@ HANDOFF_FIGMA_PROJECT_ID="${FIGMA_PROJECT_ID}"
|
|
|
105
137
|
export const figmaExtract = async (handoff: Handoff): Promise<HandoffTypes.IDocumentationObject> => {
|
|
106
138
|
Logger.success(`Starting Figma data extraction.`);
|
|
107
139
|
|
|
108
|
-
|
|
140
|
+
// The export directory can hold other files, so remove only what fetch writes.
|
|
141
|
+
const outputPath = handoff.getOutputPath();
|
|
142
|
+
await Promise.all(
|
|
143
|
+
[
|
|
144
|
+
handoff.getTokensFilePath(),
|
|
145
|
+
handoff.getVariablesFilePath(),
|
|
146
|
+
handoff.getIconsZipFilePath(),
|
|
147
|
+
handoff.getLogosZipFilePath(),
|
|
148
|
+
path.join(outputPath, 'tokens-map.json'),
|
|
149
|
+
path.join(outputPath, 'fonts'),
|
|
150
|
+
].map((entry) => fs.remove(entry))
|
|
151
|
+
);
|
|
152
|
+
await fs.ensureDir(outputPath);
|
|
109
153
|
|
|
110
154
|
const documentationObject = await createDocumentationObject(handoff);
|
|
155
|
+
const createZipFiles = process.env.HANDOFF_CREATE_ASSETS_ZIP_FILES !== 'false';
|
|
111
156
|
|
|
112
157
|
await Promise.all([
|
|
113
158
|
fs.writeJSON(handoff.getTokensFilePath(), documentationObject, { spaces: 2 }),
|
|
114
|
-
...(
|
|
159
|
+
...(createZipFiles
|
|
115
160
|
? [
|
|
116
161
|
zipAssets(documentationObject.assets.icons, fs.createWriteStream(handoff.getIconsZipFilePath())).then((writeStream) =>
|
|
117
162
|
stream.promises.finished(writeStream)
|
|
@@ -123,6 +168,10 @@ export const figmaExtract = async (handoff: Handoff): Promise<HandoffTypes.IDocu
|
|
|
123
168
|
: []),
|
|
124
169
|
]);
|
|
125
170
|
|
|
171
|
+
if (!createZipFiles) {
|
|
172
|
+
return documentationObject;
|
|
173
|
+
}
|
|
174
|
+
|
|
126
175
|
// define the output folder
|
|
127
176
|
const outputFolder = path.resolve(handoff.modulePath, '.handoff', `${handoff.getProjectId()}`, 'public');
|
|
128
177
|
|
package/src/pipeline/styles.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const buildCustomFonts = async (handoff: Handoff, documentationObject: Ha
|
|
|
29
29
|
if (fs.existsSync(fontDirName)) {
|
|
30
30
|
const stream = fs.createWriteStream(path.join(fontLocation, `${name}.zip`));
|
|
31
31
|
await zip(fontDirName, stream);
|
|
32
|
-
const fontsFolder = path.
|
|
32
|
+
const fontsFolder = path.join(handoff.getOutputPath(), 'fonts');
|
|
33
33
|
if (!fs.existsSync(fontsFolder)) {
|
|
34
34
|
fs.mkdirSync(fontsFolder);
|
|
35
35
|
}
|
|
@@ -3,13 +3,13 @@ import { Logger } from '../utils/logger';
|
|
|
3
3
|
/**
|
|
4
4
|
* Validates that the Node.js runtime meets the minimum version requirements.
|
|
5
5
|
*
|
|
6
|
-
* @throws {Error} If Node.js version is below
|
|
6
|
+
* @throws {Error} If Node.js version is below 22.
|
|
7
7
|
*/
|
|
8
8
|
export const validateHandoffRequirements = async () => {
|
|
9
9
|
let requirements = false;
|
|
10
10
|
const result = process.versions;
|
|
11
11
|
if (result && result.node) {
|
|
12
|
-
if (parseInt(result.node) >=
|
|
12
|
+
if (parseInt(result.node) >= 22) {
|
|
13
13
|
requirements = true;
|
|
14
14
|
}
|
|
15
15
|
} else {
|
|
@@ -18,7 +18,7 @@ export const validateHandoffRequirements = async () => {
|
|
|
18
18
|
if (!requirements) {
|
|
19
19
|
Logger.error('Handoff installation failed.');
|
|
20
20
|
Logger.warn(
|
|
21
|
-
'- Please update node to at least Node
|
|
21
|
+
'- Please update node to at least Node 22 https://nodejs.org/en/download. \n- You can read more about installing handoff at https://www.handoff.com/docs/'
|
|
22
22
|
);
|
|
23
23
|
throw new Error('Could not run handoff');
|
|
24
24
|
}
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
5
|
+
export { defineAiProvider } from './ai/define';
|
|
6
|
+
export type { AiProvider, AiProviderContext, AiProviderFactory } from './ai/types';
|
|
7
|
+
export { defineStorageProvider } from './registry/asset-storage/define';
|
|
8
|
+
export type {
|
|
9
|
+
StorageProvider,
|
|
10
|
+
StorageProviderContext,
|
|
11
|
+
StorageProviderFactory,
|
|
12
|
+
StorageInput,
|
|
13
|
+
StorageReadResult,
|
|
14
|
+
AssetUpload,
|
|
15
|
+
AssetUploadInput,
|
|
16
|
+
} from './registry/asset-storage/types';
|
|
17
|
+
export { defineEmailProvider } from './registry/email/define';
|
|
18
|
+
export type { EmailMessage, EmailProvider, EmailProviderContext, EmailProviderFactory } from './registry/email/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
|
}
|