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
package/src/cache/build-cache.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import Handoff from '../index';
|
|
4
|
+
import { collectModuleInputs } from '../transformers/utils/build';
|
|
4
5
|
import { Logger } from '../utils/logger';
|
|
5
6
|
import { normalizePathForCompare } from '../utils/path';
|
|
7
|
+
import { BuildInputs } from './build-inputs';
|
|
6
8
|
import { computeDirectoryState, computeFileState, directoryStatesMatch, FileState, statesMatch } from './file-state';
|
|
7
9
|
|
|
8
10
|
/** Current cache format version - bump when structure changes */
|
|
9
|
-
const CACHE_VERSION = '
|
|
11
|
+
const CACHE_VERSION = '2.0.0';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Cache entry for a single component
|
|
13
15
|
*/
|
|
14
16
|
export interface ComponentCacheEntry {
|
|
15
|
-
/** File states for
|
|
17
|
+
/** File states for the declared source files of this component and every file its last build read */
|
|
16
18
|
files: Record<string, FileState>;
|
|
17
19
|
/** States for template directory files (if templates is a directory) */
|
|
18
20
|
templateDirFiles?: Record<string, FileState>;
|
|
@@ -30,8 +32,25 @@ export interface GlobalDepsState {
|
|
|
30
32
|
globalScss?: FileState;
|
|
31
33
|
/** Global JS entry file state */
|
|
32
34
|
globalJs?: FileState;
|
|
35
|
+
/** Main config file state. The build hooks are declared there */
|
|
36
|
+
mainConfig?: FileState;
|
|
37
|
+
/** Profile config file state */
|
|
38
|
+
profileConfig?: FileState;
|
|
39
|
+
/** Selected profile. All profiles write to the same output, so a switch invalidates all of it */
|
|
40
|
+
profile?: string;
|
|
41
|
+
/** Lockfile state. It stands in for the files under `node_modules`, which the cache does not track */
|
|
42
|
+
lockfile?: FileState;
|
|
43
|
+
/** handoff-app version that built the output */
|
|
44
|
+
version?: string;
|
|
45
|
+
/** Base path written into the generated HTML */
|
|
46
|
+
basePath?: string;
|
|
33
47
|
}
|
|
34
48
|
|
|
49
|
+
const GLOBAL_FILE_DEPS = ['tokens', 'globalScss', 'globalJs', 'mainConfig', 'profileConfig', 'lockfile'] as const;
|
|
50
|
+
const GLOBAL_VALUE_DEPS = ['profile', 'version', 'basePath'] as const;
|
|
51
|
+
|
|
52
|
+
const LOCKFILE_NAMES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'];
|
|
53
|
+
|
|
35
54
|
/**
|
|
36
55
|
* Complete build cache structure
|
|
37
56
|
*/
|
|
@@ -48,11 +67,9 @@ export interface BuildCache {
|
|
|
48
67
|
* Gets the path to the build cache file
|
|
49
68
|
*/
|
|
50
69
|
export function getCachePath(handoff: Handoff): string {
|
|
51
|
-
//
|
|
52
|
-
// the
|
|
53
|
-
|
|
54
|
-
const fileName = profile ? `build-cache.${profile}.json` : 'build-cache.json';
|
|
55
|
-
return path.resolve(handoff.modulePath, '.handoff', handoff.getProjectId(), '.cache', fileName);
|
|
70
|
+
// The cache describes the output in the working directory, so it lives there too. A build deletes
|
|
71
|
+
// the staged app under the package, so a cache there would not survive.
|
|
72
|
+
return path.resolve(handoff.workingPath, '.handoff', '.cache', 'build-cache.json');
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
/**
|
|
@@ -128,29 +145,64 @@ export async function computeGlobalDepsState(handoff: Handoff): Promise<GlobalDe
|
|
|
128
145
|
result.globalJs = (await computeFileState(handoff.runtimeConfig.entries.js)) ?? undefined;
|
|
129
146
|
}
|
|
130
147
|
|
|
148
|
+
const mainConfigPath = handoff.getMainConfigFilePath();
|
|
149
|
+
if (mainConfigPath) {
|
|
150
|
+
result.mainConfig = (await computeFileState(mainConfigPath)) ?? undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const profileConfigPath = handoff.getProfileConfigFilePath();
|
|
154
|
+
if (profileConfigPath) {
|
|
155
|
+
result.profileConfig = (await computeFileState(profileConfigPath)) ?? undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
result.profile = handoff.getProfile();
|
|
159
|
+
|
|
160
|
+
const lockfilePath = findLockfile(handoff.workingPath);
|
|
161
|
+
if (lockfilePath) {
|
|
162
|
+
result.lockfile = (await computeFileState(lockfilePath)) ?? undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
result.version = await readPackageVersion(handoff);
|
|
166
|
+
result.basePath = process.env.HANDOFF_APP_BASE_PATH ?? '';
|
|
167
|
+
|
|
131
168
|
return result;
|
|
132
169
|
}
|
|
133
170
|
|
|
171
|
+
/** The nearest lockfile, searched up from the working directory so that a monorepo root lockfile is found. */
|
|
172
|
+
function findLockfile(fromDirectory: string): string | undefined {
|
|
173
|
+
for (let directory = path.resolve(fromDirectory); ; directory = path.dirname(directory)) {
|
|
174
|
+
const lockfile = LOCKFILE_NAMES.map((name) => path.join(directory, name)).find((file) => fs.existsSync(file));
|
|
175
|
+
if (lockfile) return lockfile;
|
|
176
|
+
if (path.dirname(directory) === directory) return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function readPackageVersion(handoff: Handoff): Promise<string | undefined> {
|
|
181
|
+
try {
|
|
182
|
+
return (await fs.readJson(path.resolve(handoff.modulePath, 'package.json'))).version;
|
|
183
|
+
} catch {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
134
188
|
/**
|
|
135
189
|
* Checks if global dependencies have changed
|
|
136
190
|
*/
|
|
137
191
|
export function haveGlobalDepsChanged(cached: GlobalDepsState | null | undefined, current: GlobalDepsState): boolean {
|
|
138
192
|
if (!cached) return true;
|
|
139
193
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!statesMatch(cached.globalScss, current.globalScss)) {
|
|
147
|
-
Logger.debug('Global dependency changed: global SCSS entry');
|
|
148
|
-
return true;
|
|
194
|
+
for (const key of GLOBAL_FILE_DEPS) {
|
|
195
|
+
if (!statesMatch(cached[key], current[key])) {
|
|
196
|
+
Logger.debug(`Global dependency changed: ${key}`);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
149
199
|
}
|
|
150
200
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
201
|
+
for (const key of GLOBAL_VALUE_DEPS) {
|
|
202
|
+
if (cached[key] !== current[key]) {
|
|
203
|
+
Logger.debug(`Global dependency changed: ${key}`);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
154
206
|
}
|
|
155
207
|
|
|
156
208
|
return false;
|
|
@@ -274,46 +326,78 @@ export async function computeComponentFileStates(
|
|
|
274
326
|
}
|
|
275
327
|
|
|
276
328
|
/**
|
|
277
|
-
* Checks if a component needs to be rebuilt based on file states
|
|
329
|
+
* Checks if a component needs to be rebuilt based on file states. This also compares the files that
|
|
330
|
+
* the last build read with their current state on disk.
|
|
278
331
|
*/
|
|
279
|
-
export function hasComponentChanged(
|
|
332
|
+
export async function hasComponentChanged(
|
|
280
333
|
cached: ComponentCacheEntry | null | undefined,
|
|
281
334
|
current: { files: Record<string, FileState>; templateDirFiles?: Record<string, FileState> }
|
|
282
|
-
): boolean {
|
|
335
|
+
): Promise<boolean> {
|
|
283
336
|
if (!cached) {
|
|
284
337
|
return true; // No cache entry means new component
|
|
285
338
|
}
|
|
286
339
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// Check if file count changed
|
|
292
|
-
if (cachedFiles.length !== currentFiles.length) {
|
|
293
|
-
return true;
|
|
340
|
+
for (const [file, state] of Object.entries(current.files)) {
|
|
341
|
+
if (!statesMatch(cached.files[file], state)) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
294
344
|
}
|
|
295
345
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
for (const file of currentFiles) {
|
|
299
|
-
if (!cachedSet.has(file)) {
|
|
346
|
+
for (const [file, state] of Object.entries(cached.files)) {
|
|
347
|
+
if (!(file in current.files) && !statesMatch(state, await computeFileState(file))) {
|
|
300
348
|
return true;
|
|
301
349
|
}
|
|
302
350
|
}
|
|
303
351
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
352
|
+
return !directoryStatesMatch(cached.templateDirFiles, current.templateDirFiles);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const SCRIPT_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts']);
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* File states for every file that the builds of a component read, or `null` when that list is
|
|
359
|
+
* incomplete and the component must not be cached.
|
|
360
|
+
*
|
|
361
|
+
* Previews compile their sources with esbuild inside a Vite plugin, so Rollup does not see those
|
|
362
|
+
* imports. The esbuild graph of each script entry supplies them. The lockfile covers the files under
|
|
363
|
+
* `node_modules`, so they are skipped, together with the generated files in the output directory.
|
|
364
|
+
*/
|
|
365
|
+
export async function computeBuildInputStates(
|
|
366
|
+
handoff: Handoff,
|
|
367
|
+
componentId: string,
|
|
368
|
+
inputs: BuildInputs
|
|
369
|
+
): Promise<Record<string, FileState> | null> {
|
|
370
|
+
if (inputs.failed) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const files = new Set(inputs.files);
|
|
375
|
+
const entries = handoff.runtimeConfig?.entities.components[componentId]?.entries as Record<string, string | undefined> | undefined;
|
|
376
|
+
const scriptEntries = new Set(Object.values(entries ?? {}).filter((entry): entry is string => !!entry && SCRIPT_EXTENSIONS.has(path.extname(entry))));
|
|
377
|
+
|
|
378
|
+
for (const entry of scriptEntries) {
|
|
379
|
+
const moduleInputs = await collectModuleInputs(entry, handoff);
|
|
380
|
+
if (!moduleInputs) {
|
|
381
|
+
return null;
|
|
308
382
|
}
|
|
383
|
+
for (const file of moduleInputs) files.add(file);
|
|
309
384
|
}
|
|
310
385
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
386
|
+
const outputDirectory = path.resolve(handoff.workingPath, 'public/api/component');
|
|
387
|
+
const states: Record<string, FileState> = {};
|
|
388
|
+
|
|
389
|
+
for (const id of files) {
|
|
390
|
+
const file = id.split('?')[0];
|
|
391
|
+
if (!path.isAbsolute(file) || file.split(path.sep).includes('node_modules') || file.startsWith(outputDirectory + path.sep)) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const state = await computeFileState(file);
|
|
395
|
+
if (state) {
|
|
396
|
+
states[path.resolve(file)] = state;
|
|
397
|
+
}
|
|
314
398
|
}
|
|
315
399
|
|
|
316
|
-
return
|
|
400
|
+
return states;
|
|
317
401
|
}
|
|
318
402
|
|
|
319
403
|
/**
|
|
@@ -369,10 +453,11 @@ export function createEmptyCache(): BuildCache {
|
|
|
369
453
|
export function updateComponentCacheEntry(
|
|
370
454
|
cache: BuildCache,
|
|
371
455
|
componentId: string,
|
|
372
|
-
fileStates: { files: Record<string, FileState>; templateDirFiles?: Record<string, FileState> }
|
|
456
|
+
fileStates: { files: Record<string, FileState>; templateDirFiles?: Record<string, FileState> },
|
|
457
|
+
inputStates: Record<string, FileState>
|
|
373
458
|
): void {
|
|
374
459
|
cache.components[componentId] = {
|
|
375
|
-
files: fileStates.files,
|
|
460
|
+
files: { ...inputStates, ...fileStates.files },
|
|
376
461
|
templateDirFiles: fileStates.templateDirFiles,
|
|
377
462
|
buildTimestamp: Date.now(),
|
|
378
463
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { InlineConfig, Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The files that the builds of one component read, so the build cache can tell when an imported
|
|
5
|
+
* file changes. A failed build can stop before it reads the file that broke it, so its list is
|
|
6
|
+
* incomplete and must not be cached.
|
|
7
|
+
*/
|
|
8
|
+
export interface BuildInputs {
|
|
9
|
+
files: Set<string>;
|
|
10
|
+
failed: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const createBuildInputs = (): BuildInputs => ({ files: new Set(), failed: false });
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Records every file that a Vite build loaded, Sass partials included. The Vite CSS plugin registers
|
|
17
|
+
* partials with `addWatchFile`, and Rollup keeps this list in every build, not only in watch mode.
|
|
18
|
+
*/
|
|
19
|
+
const collectBuildInputs = (inputs: BuildInputs): Plugin => ({
|
|
20
|
+
name: 'handoff:collect-build-inputs',
|
|
21
|
+
buildEnd() {
|
|
22
|
+
for (const file of this.getWatchFiles()) inputs.files.add(file);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** Call after the consumer hooks, so that a hook that replaces `plugins` cannot drop the collector. */
|
|
27
|
+
export const withBuildInputs = (config: InlineConfig, inputs?: BuildInputs): InlineConfig =>
|
|
28
|
+
inputs ? { ...config, plugins: [...(config.plugins ?? []), collectBuildInputs(inputs)] } : config;
|
package/src/cache/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { computeDirectoryState, computeFileState, directoryStatesMatch, statesMa
|
|
|
4
4
|
// Build cache utilities
|
|
5
5
|
export {
|
|
6
6
|
checkOutputExists,
|
|
7
|
+
computeBuildInputStates,
|
|
7
8
|
computeComponentFileStates,
|
|
8
9
|
computeGlobalDepsState,
|
|
9
10
|
createEmptyCache,
|
|
@@ -18,3 +19,6 @@ export {
|
|
|
18
19
|
type ComponentCacheEntry,
|
|
19
20
|
type GlobalDepsState,
|
|
20
21
|
} from './build-cache';
|
|
22
|
+
|
|
23
|
+
// Build input collection
|
|
24
|
+
export { createBuildInputs, type BuildInputs } from './build-inputs';
|
package/src/catalog/README.md
CHANGED
|
@@ -30,6 +30,9 @@ owns `fromCSF`, because CSF is a source format of React rather than a framework.
|
|
|
30
30
|
The renderer entry points call `createCatalogItem` with their renderer. The pattern entry point calls it without a renderer.
|
|
31
31
|
`define.ts` handles the shared `implementation` forms: a helper's `{ format, file }`, a file path, or an imported component.
|
|
32
32
|
|
|
33
|
+
An entry point re-exports only the preview shape its renderer honors: `react.ts` exports `CatalogPreview` with `render`,
|
|
34
|
+
`handlebars.ts` narrows it to reject `render`, and `pattern.ts` exports neither.
|
|
35
|
+
|
|
33
36
|
A new renderer needs:
|
|
34
37
|
|
|
35
38
|
- A `RENDERERS` row in `renderers.ts`
|
package/src/catalog/normalize.ts
CHANGED
|
@@ -101,6 +101,8 @@ export const normalizeCatalogItem = (moduleExports: Record<string, unknown>, opt
|
|
|
101
101
|
entries[entryKey] = path.resolve(directory, source.file);
|
|
102
102
|
// Existing builders consume template as the primary build input for every renderer.
|
|
103
103
|
entries.template = entries[entryKey];
|
|
104
|
+
// The renderer and the hydration bundle both read a preview `render` from the declaration.
|
|
105
|
+
if (options.declarationPath) entries.declaration = options.declarationPath;
|
|
104
106
|
// A wrong renderer builds a broken preview without failing on its own, so a mismatch is fatal.
|
|
105
107
|
// `config/runtime.ts` turns the throw into a skipped item that retries on the next save.
|
|
106
108
|
const claim = sourceForFile(source.file);
|
package/src/catalog/previews.ts
CHANGED
|
@@ -5,15 +5,17 @@ import type { CatalogPreview } from './types';
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* A declaration module exports more than previews: the item itself, the implementation it
|
|
8
|
-
* re-exports, helper values. Accept only a plain object that owns
|
|
9
|
-
* component or helper is never mistaken for a preview.
|
|
8
|
+
* re-exports, helper values. Accept only a plain object that owns one of the preview fields, so an
|
|
9
|
+
* exported component or helper is never mistaken for a preview.
|
|
10
10
|
*
|
|
11
11
|
* `getCsfStoryEntries` does not fit here: on a CSF file every named export is a story, and on a
|
|
12
12
|
* declaration file it is not.
|
|
13
13
|
*/
|
|
14
|
+
const PREVIEW_KEYS = ['args', 'name', 'render'] as const;
|
|
15
|
+
|
|
14
16
|
const isPreviewDeclaration = (value: unknown): boolean => {
|
|
15
17
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
16
|
-
return
|
|
18
|
+
return PREVIEW_KEYS.some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
/** Matches an exported binding, an export list, or a CommonJS export assignment. */
|
|
@@ -104,6 +106,7 @@ export const createCatalogPreviews = (
|
|
|
104
106
|
values: { ...(preview.args ?? {}) },
|
|
105
107
|
url: preview.url ?? '',
|
|
106
108
|
...(preview.usage ? { usage: preview.usage } : {}),
|
|
109
|
+
...(typeof preview.render === 'function' ? { render: preview.render as OptionalPreviewRender['render'] } : {}),
|
|
107
110
|
};
|
|
108
111
|
}
|
|
109
112
|
|
package/src/catalog/types.ts
CHANGED
|
@@ -53,6 +53,14 @@ export type CatalogPreview<TArgs = Record<string, unknown>> = {
|
|
|
53
53
|
args?: TArgs;
|
|
54
54
|
usage?: string;
|
|
55
55
|
url?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Renders this preview in place of the implementation, for props it cannot carry as data: an icon
|
|
58
|
+
* component, a callback, JSX children, or state the preview owns.
|
|
59
|
+
*
|
|
60
|
+
* Rendered as a component with `args`, so it can use hooks. The item still documents the
|
|
61
|
+
* implementation, so a wrapper never becomes the published API.
|
|
62
|
+
*/
|
|
63
|
+
render?: (args: TArgs) => unknown;
|
|
56
64
|
};
|
|
57
65
|
|
|
58
66
|
/**
|
package/src/cli/create.ts
CHANGED
|
@@ -125,8 +125,7 @@ const renderTemplate = (content: string, data: CreateOptions): string => {
|
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
const getDependenciesToInstall = (options: CreateOptions): { dependencies: string[]; devDependencies: string[] } => {
|
|
128
|
-
|
|
129
|
-
const dependencies = ['handoff-app@next'];
|
|
128
|
+
const dependencies = ['handoff-app@latest'];
|
|
130
129
|
const devDependencies: string[] = [];
|
|
131
130
|
|
|
132
131
|
if (options.includeExamples) {
|
|
@@ -17,7 +17,7 @@ export interface BuildArgs extends SharedArgs {
|
|
|
17
17
|
* `handoff-app build [--target static|registry] [--package standalone|vercel]` — builds the
|
|
18
18
|
* documentation site for the resolved target and packages it for the resolved deliverable.
|
|
19
19
|
* `--target` selects *what* is built; the optional, additive `--package` selects *how* it is
|
|
20
|
-
* packaged. Bare `build` resolves to the static target with the `out
|
|
20
|
+
* packaged. Bare `build` resolves to the static target with the `out/static` export;
|
|
21
21
|
* `--package` never implies a `--target`.
|
|
22
22
|
*/
|
|
23
23
|
const command: CommandModule<{}, BuildArgs> = {
|
package/src/config/README.md
CHANGED
|
@@ -8,24 +8,22 @@ Handles loading, validation, and resolution of handoff configuration.
|
|
|
8
8
|
|------|---------|
|
|
9
9
|
| `defaults.ts` | Default config values and `getClientConfig()` for the Next.js app |
|
|
10
10
|
| `from-env.ts` | JSON-compatible environment markers and public types |
|
|
11
|
-
| `resolve-env.ts` | Resolves merged environment references and guards reference-only properties |
|
|
11
|
+
| `resolve-env.ts` | Resolves merged environment references and guards reference-only properties, whose paths accept a `*` segment to cover list elements |
|
|
12
12
|
| `errors.ts` | Shared `HandoffConfigError` |
|
|
13
13
|
| `env.ts` | Reads `.env` at load, then `loadProfileEnv()` applies `.env.<profile>` on top |
|
|
14
14
|
| `loader.ts` | `initConfig()` — reads `handoff.config.ts/.js/.cjs/.json` (in that order) plus the selected profile sidecar, warns on conflicts, and merges every layer |
|
|
15
15
|
| `runtime.ts` | `initRuntimeConfig()` — one pass over every registered catalog directory: loads each declaration, normalizes it into a component or pattern record, skips a declaration with a taken id, resolves SCSS/JS paths and transformer options |
|
|
16
16
|
| `catalog-include.ts` | `isIncluded()` / `addToCatalog()` — registers a directory under `catalog.include` |
|
|
17
|
-
| `validator.ts` | `validateConfig()` — ensures required Figma credentials are present |
|
|
18
17
|
| `helpers.ts` | `defineConfig()` — typed authoring; maps camelCase keys to the runtime keys |
|
|
19
18
|
| `index.ts` | Barrel re-exports |
|
|
20
19
|
|
|
21
20
|
## Usage
|
|
22
21
|
|
|
23
22
|
```ts
|
|
24
|
-
import { initConfig, initRuntimeConfig
|
|
23
|
+
import { initConfig, initRuntimeConfig } from './config';
|
|
25
24
|
|
|
26
25
|
const config = initConfig(overrides);
|
|
27
26
|
const [runtimeConfig, configPaths] = initRuntimeConfig({ config, workingPath });
|
|
28
|
-
const validated = validateConfig(config);
|
|
29
27
|
```
|
|
30
28
|
|
|
31
29
|
## Main Config Precedence
|
|
@@ -70,7 +68,7 @@ property path, variable name, and active profile. Invalid names and rejected lit
|
|
|
70
68
|
`EnvValue<T>` accepts a literal or reference. `EnvSecret<T>` accepts only a reference and resolves
|
|
71
69
|
at config load. Secret defaults can only be empty or null, for optional credentials.
|
|
72
70
|
`RuntimeEnvReference<T>` accepts only a reference with no default. Its name survives config loading and
|
|
73
|
-
is read in the deployed process. Database URLs
|
|
71
|
+
is read in the deployed process. Database URLs, asset-storage tokens, email secrets, and AI keys are deferred.
|
|
74
72
|
|
|
75
73
|
The loader resolves references once after all layers merge, before other config guards.
|
|
76
74
|
References replace whole values, including earlier references and their defaults. A profile can
|
|
@@ -83,10 +81,13 @@ numeric and boolean properties also retain their default's conversion when a ref
|
|
|
83
81
|
fallback. Booleans are true only for `true`. Numbers must be finite. A lower-layer fallback never
|
|
84
82
|
becomes the fallback for a replacement reference.
|
|
85
83
|
|
|
86
|
-
`runtime.registry.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
`runtime.registry.email.options`, `runtime.registry.assetStorage.options`, and
|
|
85
|
+
`runtime.ai.connections.*.options` are deferred prefixes (`DEFERRED_PREFIXES`). A reference anywhere
|
|
86
|
+
inside them keeps its name, and the deployed app resolves it with `resolveEnvReferences()` before it
|
|
87
|
+
calls the module factory. Literals are baked. `email.options.apiKey` and `email.options.password` are
|
|
88
|
+
also `DEFERRED_PATHS`, so a literal secret is rejected for every email provider. Before the merge, a
|
|
89
|
+
layer that changes `email.provider` drops `options` and `module` from the lower layers, so the
|
|
90
|
+
settings of two providers never mix.
|
|
90
91
|
|
|
91
92
|
`ResolvedConfig` narrows eager properties by hand. Adding an eager property requires its `Config`
|
|
92
93
|
member and resolved narrowing. A secret also needs a `SECRET_PATHS` entry for untyped configs.
|
package/src/config/defaults.ts
CHANGED
|
@@ -15,8 +15,12 @@ export const isMcpEnabled = (config: Pick<Config, 'runtime'> | Pick<ResolvedConf
|
|
|
15
15
|
config?.runtime?.mcp !== false;
|
|
16
16
|
|
|
17
17
|
export const defaultConfig = (): Config => ({
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
integrations: {
|
|
19
|
+
figma: {
|
|
20
|
+
projectId: fromEnv('HANDOFF_FIGMA_PROJECT_ID', { default: null }),
|
|
21
|
+
accessToken: fromEnv('HANDOFF_DEV_ACCESS_TOKEN', { default: null }),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
20
24
|
runtime: {
|
|
21
25
|
mode: 'workspace',
|
|
22
26
|
registryConnection: {
|
|
@@ -24,8 +28,7 @@ export const defaultConfig = (): Config => ({
|
|
|
24
28
|
accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN', { default: '' }),
|
|
25
29
|
},
|
|
26
30
|
registry: {
|
|
27
|
-
|
|
28
|
-
assetStorage: { token: fromEnv('BLOB_READ_WRITE_TOKEN') },
|
|
31
|
+
database: { url: fromEnv('DATABASE_URL') },
|
|
29
32
|
},
|
|
30
33
|
},
|
|
31
34
|
exportsOutputDirectory: fromEnv('HANDOFF_OUTPUT_DIR', { default: 'exported' }),
|
package/src/config/from-env.ts
CHANGED
|
@@ -24,3 +24,36 @@ export const fromEnv = <T = string>(name: string, options?: { default: T }): Env
|
|
|
24
24
|
|
|
25
25
|
export const isEnvReference = (value: unknown): value is EnvReference<unknown> =>
|
|
26
26
|
value !== null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, '$env');
|
|
27
|
+
|
|
28
|
+
const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
|
|
29
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
|
|
31
|
+
/** Replace each reference nested in `options` with its value from `env`. An unset or empty variable becomes `undefined`. */
|
|
32
|
+
export const resolveEnvReferences = (options: Record<string, unknown>, env: NodeJS.ProcessEnv): Record<string, unknown> => {
|
|
33
|
+
const resolve = (value: unknown): unknown => {
|
|
34
|
+
if (isEnvReference(value)) {
|
|
35
|
+
const resolved = typeof value.$env === 'string' ? env[value.$env]?.trim() : undefined;
|
|
36
|
+
return resolved || undefined;
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(value)) return value.map(resolve);
|
|
39
|
+
if (isPlainRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolve(entry)]));
|
|
40
|
+
return value;
|
|
41
|
+
};
|
|
42
|
+
return resolve(options) as Record<string, unknown>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The variable names that the references nested in `options` use, for deployment instructions. */
|
|
46
|
+
export const envReferenceNames = (options: Record<string, unknown>): string[] => {
|
|
47
|
+
const names = new Set<string>();
|
|
48
|
+
const collect = (value: unknown): void => {
|
|
49
|
+
if (isEnvReference(value)) {
|
|
50
|
+
if (typeof value.$env === 'string') names.add(value.$env);
|
|
51
|
+
} else if (Array.isArray(value)) {
|
|
52
|
+
value.forEach(collect);
|
|
53
|
+
} else if (isPlainRecord(value)) {
|
|
54
|
+
Object.values(value).forEach(collect);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
collect(options);
|
|
58
|
+
return [...names];
|
|
59
|
+
};
|
package/src/config/helpers.ts
CHANGED
|
@@ -25,12 +25,10 @@ const normalizeAppConfig = (app?: Config['app']): Config['app'] => {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
export const normalizeConfig = (config: Config): Config => {
|
|
28
|
-
const {
|
|
28
|
+
const { assetsZipLinks, app, ...rest } = config;
|
|
29
29
|
|
|
30
30
|
return {
|
|
31
31
|
...(rest as Config),
|
|
32
|
-
dev_access_token: devAccessToken !== undefined ? devAccessToken : config.dev_access_token,
|
|
33
|
-
figma_project_id: figmaProjectId !== undefined ? figmaProjectId : config.figma_project_id,
|
|
34
32
|
assets_zip_links: assetsZipLinks ?? config.assets_zip_links,
|
|
35
33
|
app: normalizeAppConfig(app),
|
|
36
34
|
};
|
package/src/config/index.ts
CHANGED
|
@@ -15,9 +15,6 @@ export { defineConfig } from './helpers';
|
|
|
15
15
|
// Runtime config resolution
|
|
16
16
|
export { initRuntimeConfig } from './runtime';
|
|
17
17
|
|
|
18
|
-
// Config validation
|
|
19
|
-
export { validateConfig } from './validator';
|
|
20
|
-
|
|
21
18
|
// Registering catalog item directories under `catalog.include`
|
|
22
19
|
export { addToCatalog, isIncluded } from './catalog-include';
|
|
23
20
|
export type { AddToCatalogResult } from './catalog-include';
|
package/src/config/loader.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
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 mergeWith from 'lodash/mergeWith';
|
|
5
|
+
import omit from 'lodash/omit';
|
|
4
6
|
import { createRequire } from 'module';
|
|
5
7
|
import path from 'path';
|
|
6
|
-
import { Config, ResolvedConfig } from '../types/config';
|
|
8
|
+
import { Config, HandoffEmailConfig, ResolvedConfig } from '../types/config';
|
|
7
9
|
import { Logger } from '../utils/logger';
|
|
8
10
|
import { resolveWorkingPath } from '../utils/path';
|
|
9
11
|
import { defaultConfig } from './defaults';
|
|
@@ -64,9 +66,10 @@ const evaluateTypeScriptConfig = (filePath: string, handoffModulePath: string):
|
|
|
64
66
|
write: false,
|
|
65
67
|
platform: 'node',
|
|
66
68
|
format: 'cjs',
|
|
67
|
-
target: '
|
|
69
|
+
target: 'node22',
|
|
68
70
|
logLevel: 'silent',
|
|
69
71
|
external: ['handoff-app'],
|
|
72
|
+
loader: STYLE_IMPORT_LOADERS,
|
|
70
73
|
});
|
|
71
74
|
|
|
72
75
|
const code = buildResult.outputFiles?.[0]?.text;
|
|
@@ -231,6 +234,27 @@ const replaceValues = (destination: unknown, source: unknown) => {
|
|
|
231
234
|
return Array.isArray(source) || isEnvReference(source) || isEnvReference(destination) ? source : undefined;
|
|
232
235
|
};
|
|
233
236
|
|
|
237
|
+
/**
|
|
238
|
+
* A layer that changes `runtime.registry.email.provider` starts from fresh `options` and `module`, so
|
|
239
|
+
* the deep merge never mixes the settings of two providers.
|
|
240
|
+
*/
|
|
241
|
+
const resetEmailOnProviderChange = (layers: Partial<Config>[]): Partial<Config>[] => {
|
|
242
|
+
let provider: string | undefined;
|
|
243
|
+
let start = 0;
|
|
244
|
+
layers.forEach((layer, index) => {
|
|
245
|
+
const next = layer.runtime?.registry?.email?.provider;
|
|
246
|
+
if (next !== undefined && next !== provider) {
|
|
247
|
+
provider = next;
|
|
248
|
+
start = index;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
return layers.map((layer, index) => {
|
|
252
|
+
const email = layer.runtime?.registry?.email;
|
|
253
|
+
if (index >= start || !email) return layer;
|
|
254
|
+
return { ...layer, runtime: { ...layer.runtime, registry: { ...layer.runtime!.registry, email: omit(email, ['options', 'module']) as HandoffEmailConfig } } };
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
234
258
|
/**
|
|
235
259
|
* Loads the handoff configuration for the given working directory and returns metadata.
|
|
236
260
|
*/
|
|
@@ -262,7 +286,7 @@ export const initConfigWithMetadata = (configOverride?: Partial<Config>, context
|
|
|
262
286
|
Logger.debug(`Config profile: ${profileLabel}`);
|
|
263
287
|
Logger.debug(`Config files loaded: ${[configPath, profileConfigPath].filter(Boolean).join(', ') || 'none, using defaults'}`);
|
|
264
288
|
|
|
265
|
-
const merged = mergeWith({}, ...layers, replaceValues) as Config;
|
|
289
|
+
const merged = mergeWith({}, ...resetEmailOnProviderChange(layers), replaceValues) as Config;
|
|
266
290
|
const config = resolveConfigEnv(merged, profileSelection?.name, layers[0]);
|
|
267
291
|
|
|
268
292
|
// Guards run on the resolved config so a profile cannot slip a removed setting past them.
|