handoff-app 2.0.0-9 → 2.0.1
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/AGENTS.md +90 -1
- package/{Changelog.md → CHANGELOG.md} +105 -1
- package/CLAUDE.md +92 -1
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +818 -406
- 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/cli/create.js +1 -2
- package/dist/cli.js +1 -1
- 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 +26 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +1 -1
- 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/errors.js +4 -0
- package/dist/registry/publish/index.js +2 -0
- package/dist/registry/publish/package.d.ts +6 -0
- package/dist/registry/publish/package.js +29 -1
- 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/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +2 -1
- package/dist/transformers/preview/component/css.js +8 -5
- 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/utils/build.d.ts +9 -0
- package/dist/transformers/utils/build.js +27 -5
- package/dist/types/config.d.ts +129 -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/Preview.tsx +5 -2
- 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/cli/create.ts +1 -2
- package/src/cli.ts +1 -1
- 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 +25 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +1 -1
- 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/errors.ts +4 -0
- package/src/registry/publish/index.ts +3 -1
- package/src/registry/publish/package.ts +33 -0
- package/src/registry/transfer.ts +4 -0
- package/src/store/registry.ts +7 -7
- package/src/transformers/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +9 -4
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/utils/build.ts +22 -4
- package/src/types/config.ts +142 -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
|
@@ -36,7 +36,7 @@ export interface ResolvedRegistryDatabase {
|
|
|
36
36
|
|
|
37
37
|
/** Resolve the env-var name holding the database URL (defaults to `DATABASE_URL`). */
|
|
38
38
|
export const resolveDatabaseUrlEnv = (config: ResolvedConfig | null | undefined): string => {
|
|
39
|
-
const configured = config?.runtime?.registry?.
|
|
39
|
+
const configured = config?.runtime?.registry?.database?.url?.$env?.trim();
|
|
40
40
|
return configured || DEFAULT_DATABASE_URL_ENV;
|
|
41
41
|
};
|
|
42
42
|
|
|
@@ -58,7 +58,7 @@ export const resolveRegistryDatabase = (config: ResolvedConfig | null | undefine
|
|
|
58
58
|
if (!connectionString) {
|
|
59
59
|
throw new Error(
|
|
60
60
|
`Registry database URL is not configured. Set the "${databaseUrlEnv}" environment variable to a ` +
|
|
61
|
-
`PostgreSQL connection string (or change "runtime.registry.
|
|
61
|
+
`PostgreSQL connection string (or change "runtime.registry.database.url" to point at the env var you use).`
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -30,7 +30,7 @@ import type { TokenSetKind } from '../tokens/sets';
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* PostgreSQL `bytea` column. Drizzle pg-core has no built-in binary type, so declare it once here.
|
|
33
|
-
* Reads/writes are Node `Buffer`s. The default asset storage
|
|
33
|
+
* Reads/writes are Node `Buffer`s. The default asset storage provider keeps small/ordinary asset
|
|
34
34
|
* bytes inline in this column; larger assets are offloaded to an object-storage `storageRef` instead.
|
|
35
35
|
*/
|
|
36
36
|
const bytea = customType<{ data: Buffer; driverData: Buffer }>({
|
|
@@ -39,8 +39,8 @@ const bytea = customType<{ data: Buffer; driverData: Buffer }>({
|
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
/** Where an asset blob's bytes
|
|
43
|
-
export type AssetStorageProvider = 'database' | '
|
|
42
|
+
/** Where an asset blob's bytes live: inline in the database (`database`) or in a custom provider (`custom`). */
|
|
43
|
+
export type AssetStorageProvider = 'database' | 'custom';
|
|
44
44
|
import type { ComponentListObject, PageListObject, PatternComponentEntry, PatternListObject } from '../../transformers/preview/types';
|
|
45
45
|
|
|
46
46
|
/** Registry account role. Administrators may issue write-scoped credentials. */
|
|
@@ -112,6 +112,8 @@ export const components = pgTable(
|
|
|
112
112
|
sourceFormat: text('source_format'),
|
|
113
113
|
tags: jsonb('tags').$type<string[]>(),
|
|
114
114
|
categories: jsonb('categories').$type<string[]>(),
|
|
115
|
+
/** Workspace-relative directory the component was published from. */
|
|
116
|
+
sourceDir: text('source_dir'),
|
|
115
117
|
/** Full normalized component record. */
|
|
116
118
|
record: jsonb('record').$type<ComponentListObject>().notNull(),
|
|
117
119
|
/** Registry-only review/catalog metadata (management-API allowlist; never a render input). */
|
|
@@ -165,6 +167,8 @@ export const patterns = pgTable(
|
|
|
165
167
|
tags: jsonb('tags').$type<string[]>(),
|
|
166
168
|
/** Pattern component references. */
|
|
167
169
|
components: jsonb('components').$type<PatternComponentEntry[]>(),
|
|
170
|
+
/** Workspace-relative directory the pattern was published from. */
|
|
171
|
+
sourceDir: text('source_dir'),
|
|
168
172
|
/** Full normalized pattern record. */
|
|
169
173
|
record: jsonb('record').$type<PatternListObject>().notNull(),
|
|
170
174
|
/** Registry-only review/catalog metadata (management-API allowlist; never a render input). */
|
|
@@ -378,23 +382,20 @@ export const tokenArtifacts = pgTable(
|
|
|
378
382
|
* collection is its current manifest. Publish replaces those rows atomically, so readers always see a
|
|
379
383
|
* complete manifest.
|
|
380
384
|
*/
|
|
381
|
-
export const assetCollections = pgTable(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
396
|
-
}
|
|
397
|
-
);
|
|
385
|
+
export const assetCollections = pgTable('asset_collections', {
|
|
386
|
+
/** Stable collection id (`icons`|`logos`|`fonts`). Join key across stores. */
|
|
387
|
+
collection: text('collection').primaryKey(),
|
|
388
|
+
/** Deterministic content hash over the collection's `(path, contentHash)` manifest (skip-unchanged). */
|
|
389
|
+
sourceHash: text('source_hash'),
|
|
390
|
+
/** Build/publish status for the collection. */
|
|
391
|
+
status: text('status').$type<RegistryBuildStatus>(),
|
|
392
|
+
builtAt: timestamp('built_at', { withTimezone: true }),
|
|
393
|
+
builderVersion: text('builder_version'),
|
|
394
|
+
/** Registry-only review/catalog metadata (parity with other entities; management-API only). */
|
|
395
|
+
metadata: jsonb('metadata').$type<RegistryReviewMetadata>(),
|
|
396
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
397
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
398
|
+
});
|
|
398
399
|
|
|
399
400
|
/**
|
|
400
401
|
* Logical asset metadata - the collection manifest. One row per asset (icon/logo SVG, sprite,
|
|
@@ -431,13 +432,13 @@ export const assets = pgTable(
|
|
|
431
432
|
/**
|
|
432
433
|
* Content-addressed binary blob store, shared across assets/collections (the `hash` is the blob
|
|
433
434
|
* identity, so an unchanged file re-references the same row). Exactly one location is active per row:
|
|
434
|
-
* inline `content` (bytea, default
|
|
435
|
+
* inline `content` (bytea, default `database` provider) or an external `storageRef` resolved through the
|
|
435
436
|
* recorded `storageProvider`. Both nullable so DB- and object-backed blobs coexist.
|
|
436
437
|
*/
|
|
437
438
|
export const assetBlobs = pgTable('asset_blobs', {
|
|
438
439
|
/** SHA-256 of the blob bytes (hex). */
|
|
439
440
|
hash: text('hash').primaryKey(),
|
|
440
|
-
/** Which storage backed this blob when it was written (`database
|
|
441
|
+
/** Which storage backed this blob when it was written (`database`|custom id). */
|
|
441
442
|
storageProvider: text('storage_provider').$type<AssetStorageProvider>().notNull(),
|
|
442
443
|
/** Inline bytes; null when stored externally via `storageRef`. */
|
|
443
444
|
content: bytea('content'),
|
|
@@ -575,6 +576,31 @@ export const registryDeviceAuthorizations = pgTable(
|
|
|
575
576
|
]
|
|
576
577
|
);
|
|
577
578
|
|
|
579
|
+
/**
|
|
580
|
+
* One reader's key for one declared AI connection. Unlike every other credential here the value
|
|
581
|
+
* must be recoverable, because it is sent to the provider on that reader's behalf, so it is
|
|
582
|
+
* encrypted rather than hashed (see `../auth/crypto`). A row whose `connectionId` no longer appears
|
|
583
|
+
* in the config is ignored, not deleted: the connection may come back on the next deploy.
|
|
584
|
+
*/
|
|
585
|
+
export const registryAiKeys = pgTable(
|
|
586
|
+
'registry_ai_keys',
|
|
587
|
+
{
|
|
588
|
+
userId: text('user_id')
|
|
589
|
+
.notNull()
|
|
590
|
+
.references(() => registryUsers.id, { onDelete: 'cascade' }),
|
|
591
|
+
/** Id of the connection the config declares. Never a reader-supplied endpoint. */
|
|
592
|
+
connectionId: text('connection_id').notNull(),
|
|
593
|
+
/** Self-describing AES-256-GCM payload; the deployment secret it is keyed from is never stored. */
|
|
594
|
+
encryptedKey: text('encrypted_key').notNull(),
|
|
595
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
596
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
597
|
+
},
|
|
598
|
+
(table) => [
|
|
599
|
+
primaryKey({ columns: [table.userId, table.connectionId] }),
|
|
600
|
+
check('registry_ai_keys_connection_not_blank', sql`length(btrim(${table.connectionId})) > 0`),
|
|
601
|
+
]
|
|
602
|
+
);
|
|
603
|
+
|
|
578
604
|
/**
|
|
579
605
|
* Fixed-window throttling counters. Callers pass a hash of the identifying value so raw emails and
|
|
580
606
|
* IP addresses are not retained.
|
|
@@ -616,6 +642,7 @@ export const registrySchema = {
|
|
|
616
642
|
registryInstallations,
|
|
617
643
|
registryAuthActionTokens,
|
|
618
644
|
registryAccessTokens,
|
|
645
|
+
registryAiKeys,
|
|
619
646
|
registryDeviceAuthorizations,
|
|
620
647
|
registryAuthRateLimits,
|
|
621
648
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed identity helper for custom email providers (mirrors `defineStorageProvider`): a consumer
|
|
3
|
+
* default-exports its result from a **server-only** module and points `runtime.registry.email.module`
|
|
4
|
+
* at it. It must be its own module (not an inline `defineConfig` function) so the build can trace it
|
|
5
|
+
* into the deployed registry. The default export may be the provider object, or a factory
|
|
6
|
+
* `(context) => EmailProvider` when it needs its options or env.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { EmailProvider, EmailProviderFactory } from './types';
|
|
10
|
+
|
|
11
|
+
export function defineEmailProvider(provider: EmailProvider): EmailProvider;
|
|
12
|
+
export function defineEmailProvider(factory: EmailProviderFactory): EmailProviderFactory;
|
|
13
|
+
export function defineEmailProvider(input: EmailProvider | EmailProviderFactory): EmailProvider | EmailProviderFactory {
|
|
14
|
+
return input;
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in SMTP provider, selected with `runtime.registry.email.provider: "smtp"`.
|
|
3
|
+
*
|
|
4
|
+
* `nodemailer` is loaded lazily so it is only pulled into memory (and traced into the registry
|
|
5
|
+
* bundle) when this provider is selected. Options may come from environment variables, so numbers
|
|
6
|
+
* and booleans can arrive as strings.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { EmailProvider } from '../types';
|
|
10
|
+
|
|
11
|
+
const toNumber = (value: unknown): number | undefined => {
|
|
12
|
+
const number = typeof value === 'number' ? value : typeof value === 'string' && value.trim() ? Number(value) : NaN;
|
|
13
|
+
return Number.isFinite(number) ? number : undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const toBoolean = (value: unknown): boolean | undefined =>
|
|
17
|
+
typeof value === 'boolean' ? value : typeof value === 'string' && value.trim() ? value.trim() === 'true' : undefined;
|
|
18
|
+
|
|
19
|
+
/** Build an SMTP provider from options whose references are already resolved. */
|
|
20
|
+
export const createSmtpProvider = (options: Record<string, unknown>): EmailProvider => {
|
|
21
|
+
const host = typeof options.host === 'string' ? options.host : '';
|
|
22
|
+
const configuredPort = toNumber(options.port);
|
|
23
|
+
const secure = toBoolean(options.secure) ?? (configuredPort === undefined || configuredPort === 465);
|
|
24
|
+
const port = configuredPort ?? (secure ? 465 : 587);
|
|
25
|
+
const user = typeof options.user === 'string' ? options.user : undefined;
|
|
26
|
+
const password = typeof options.password === 'string' ? options.password : undefined;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
async send({ from, to, subject, html, text }) {
|
|
30
|
+
if (!host) throw new Error('The SMTP host is not set.');
|
|
31
|
+
const { createTransport } = await import('nodemailer');
|
|
32
|
+
const transport = createTransport({ host, port, secure, auth: user ? { user, pass: password } : undefined });
|
|
33
|
+
const info = await transport.sendMail({ from, to, subject, html, text });
|
|
34
|
+
if (info.rejected?.length) {
|
|
35
|
+
throw new Error(`The SMTP server rejected the recipient: ${info.response}`);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry email settings resolution.
|
|
3
|
+
*
|
|
4
|
+
* `runtime.registry.email.provider` selects the built-in `smtp` provider or a `custom` module. Without
|
|
5
|
+
* an email block, email delivery is off. The build bakes the provider, the module location, and the
|
|
6
|
+
* options. An option literal is baked as it is. An environment reference stays `{ $env }` and is
|
|
7
|
+
* resolved at request time.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { HandoffConfigError } from '../../config/errors';
|
|
11
|
+
import type { ResolvedConfig } from '../../types/config';
|
|
12
|
+
|
|
13
|
+
export type EmailProviderKind = 'smtp' | 'custom';
|
|
14
|
+
|
|
15
|
+
const PROVIDERS: readonly EmailProviderKind[] = ['smtp', 'custom'];
|
|
16
|
+
|
|
17
|
+
/** The options each built-in provider accepts. A custom provider accepts any. */
|
|
18
|
+
const BUILT_IN_OPTIONS: Record<Exclude<EmailProviderKind, 'custom'>, readonly string[]> = {
|
|
19
|
+
smtp: ['host', 'port', 'secure', 'user', 'password'],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Email settings as they are baked: literals and `{ $env }` references, never secret values. */
|
|
23
|
+
export interface EmailSettings {
|
|
24
|
+
from?: string;
|
|
25
|
+
/** Unset when the config has no email block. */
|
|
26
|
+
provider?: EmailProviderKind;
|
|
27
|
+
/** For `custom`: the server-only provider module path. */
|
|
28
|
+
module?: string;
|
|
29
|
+
options: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
|
|
33
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
34
|
+
|
|
35
|
+
/** Resolve and validate the email block from a loaded config (CLI/build side). An unusable block fails the build. */
|
|
36
|
+
export const resolveEmailFromConfig = (config: ResolvedConfig | null | undefined): EmailSettings => {
|
|
37
|
+
const email = config?.runtime?.registry?.email as unknown as Record<string, unknown> | undefined;
|
|
38
|
+
const fail = (problem: string): never => {
|
|
39
|
+
throw new HandoffConfigError(`Config "runtime.registry.email": ${problem}`);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (email === undefined) return { options: {} };
|
|
43
|
+
const provider = email.provider as EmailProviderKind;
|
|
44
|
+
if (!PROVIDERS.includes(provider)) {
|
|
45
|
+
fail(
|
|
46
|
+
email.provider === undefined
|
|
47
|
+
? '"provider" is required. Use "smtp" or "custom".'
|
|
48
|
+
: `unknown provider ${JSON.stringify(provider)}. Use "smtp" or "custom".`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (email.options !== undefined && !isPlainRecord(email.options)) {
|
|
52
|
+
fail('"options" must be an object.');
|
|
53
|
+
}
|
|
54
|
+
const options = { ...((email.options as Record<string, unknown> | undefined) ?? {}) };
|
|
55
|
+
const module = typeof email.module === 'string' ? email.module.trim() : undefined;
|
|
56
|
+
|
|
57
|
+
if (provider === 'custom') {
|
|
58
|
+
if (!module) fail('provider "custom" needs a "module" that default-exports a defineEmailProvider() result.');
|
|
59
|
+
} else {
|
|
60
|
+
if (email.module !== undefined) fail(`"module" is used only by provider "custom", not "${provider}".`);
|
|
61
|
+
const allowed = BUILT_IN_OPTIONS[provider];
|
|
62
|
+
const unknown = Object.keys(options).filter((key) => !allowed.includes(key));
|
|
63
|
+
if (unknown.length > 0) {
|
|
64
|
+
fail(
|
|
65
|
+
`provider "${provider}" does not support ${unknown.map((key) => `"options.${key}"`).join(', ')}. ` +
|
|
66
|
+
`Use ${allowed.map((key) => `"${key}"`).join(', ')}.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (provider === 'smtp' && options.host === undefined) fail('provider "smtp" needs "options.host".');
|
|
71
|
+
|
|
72
|
+
const from = typeof email.from === 'string' ? email.from.trim() || undefined : undefined;
|
|
73
|
+
return { from, provider, ...(module ? { module } : {}), options };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Read baked settings back leniently (server side): a malformed value must not take the server down. */
|
|
77
|
+
export const parseEmailSettings = (raw: { from?: unknown; provider?: unknown; module?: unknown; options?: unknown }): EmailSettings => {
|
|
78
|
+
let options: unknown = raw.options;
|
|
79
|
+
if (typeof options === 'string') {
|
|
80
|
+
try {
|
|
81
|
+
options = options.trim() ? JSON.parse(options) : {};
|
|
82
|
+
} catch {
|
|
83
|
+
options = {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const provider = PROVIDERS.find((kind) => kind === raw.provider);
|
|
87
|
+
const module = typeof raw.module === 'string' ? raw.module.trim() : '';
|
|
88
|
+
return {
|
|
89
|
+
from: typeof raw.from === 'string' ? raw.from.trim() || undefined : undefined,
|
|
90
|
+
...(provider ? { provider } : {}),
|
|
91
|
+
...(module ? { module } : {}),
|
|
92
|
+
options: isPlainRecord(options) ? options : {},
|
|
93
|
+
};
|
|
94
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-only email provider contract. Handoff renders every message, and a provider only delivers it.
|
|
3
|
+
* SMTP is built in. Anything else is a {@link defineEmailProvider} module.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** One rendered message. */
|
|
7
|
+
export interface EmailMessage {
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
subject: string;
|
|
11
|
+
html: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The pluggable provider contract. `send` throws when the message is not accepted for delivery. */
|
|
16
|
+
export interface EmailProvider {
|
|
17
|
+
send(message: EmailMessage): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Context passed to a provider factory. */
|
|
21
|
+
export interface EmailProviderContext {
|
|
22
|
+
/** `runtime.registry.email.options`, with environment references resolved to their values. */
|
|
23
|
+
options: Record<string, unknown>;
|
|
24
|
+
env: NodeJS.ProcessEnv;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A custom provider may export the provider directly or a factory that builds it from its context. */
|
|
28
|
+
export type EmailProviderFactory = (context: EmailProviderContext) => EmailProvider | Promise<EmailProvider>;
|
package/src/registry/path.ts
CHANGED
|
@@ -13,6 +13,17 @@ export const isSafeRelativePath = (value: string): boolean => {
|
|
|
13
13
|
|
|
14
14
|
export const normalizeRelativePath = (value: string): string => value.replace(/\\/g, '/');
|
|
15
15
|
|
|
16
|
+
/** Unlike {@link isSafeRelativePath}, it can start with `../`. */
|
|
17
|
+
export const isWorkspaceRelativePath = (value: unknown): value is string =>
|
|
18
|
+
typeof value === 'string' &&
|
|
19
|
+
value !== '' &&
|
|
20
|
+
value !== '.' &&
|
|
21
|
+
!value.includes('\0') &&
|
|
22
|
+
!value.includes('\\') &&
|
|
23
|
+
!value.startsWith('/') &&
|
|
24
|
+
!/^[a-zA-Z]:/.test(value) &&
|
|
25
|
+
path.posix.normalize(value) === value;
|
|
26
|
+
|
|
16
27
|
export const isSafePathSegment = (value: string): boolean =>
|
|
17
28
|
typeof value === 'string' && value === value.trim() && isSafeRelativePath(value) && !normalizeRelativePath(value).includes('/');
|
|
18
29
|
|
|
@@ -8,6 +8,10 @@ export const describeUploadFailure = (
|
|
|
8
8
|
registryUrl: string,
|
|
9
9
|
rejectedPayload: RejectedPublishPayload
|
|
10
10
|
): string => {
|
|
11
|
+
// An oversized body is rejected by the platform or body parser, so the response has no error envelope.
|
|
12
|
+
if (error.status === 413) {
|
|
13
|
+
return `The registry at ${registryUrl} rejected the ${rejectedPayload} as too large (413). Reduce the size of its largest files and publish again.`;
|
|
14
|
+
}
|
|
11
15
|
switch (error.code) {
|
|
12
16
|
case 'runtime_mode_conflict':
|
|
13
17
|
return `The registry at ${registryUrl} is not running in registry mode, so it cannot accept publishes: ${error.message}`;
|
|
@@ -21,7 +21,7 @@ import { loginCommandHint, resolveAuthenticatedRegistryConnection, unusedLoginNo
|
|
|
21
21
|
import type { EntitySummary, TransferEntityKind } from '../transfer';
|
|
22
22
|
import { quoteIds, selectIds, splitCatalogIds } from '../selection';
|
|
23
23
|
import { describePublishError, describeUploadFailure } from './errors';
|
|
24
|
-
import { assertRequiredArtifactsPresent, buildPublishPackage, PublishPackageError } from './package';
|
|
24
|
+
import { assertPackageWithinUploadLimit, assertRequiredArtifactsPresent, buildPublishPackage, PublishPackageError } from './package';
|
|
25
25
|
|
|
26
26
|
/** A connected-workspace configuration or precondition failure surfaced to the CLI. */
|
|
27
27
|
export class PublishError extends Error {
|
|
@@ -173,6 +173,7 @@ export const publishEntity = async (handoff: Handoff, kind: TransferEntityKind,
|
|
|
173
173
|
|
|
174
174
|
const pkg = await buildPublishPackage(handoff, kind, id);
|
|
175
175
|
assertRequiredArtifactsPresent(pkg);
|
|
176
|
+
assertPackageWithinUploadLimit(kind, id, pkg);
|
|
176
177
|
|
|
177
178
|
const contents = `${pkg.artifacts.length} artifact(s), ${pkg.files.length} source file(s)`;
|
|
178
179
|
if (!client) {
|
|
@@ -274,6 +275,7 @@ const uploadEntities = async (handoff: Handoff, transport: PublishTransport, kin
|
|
|
274
275
|
try {
|
|
275
276
|
const pkg = await buildPublishPackage(handoff, kind, id);
|
|
276
277
|
assertRequiredArtifactsPresent(pkg);
|
|
278
|
+
assertPackageWithinUploadLimit(kind, id, pkg);
|
|
277
279
|
|
|
278
280
|
const remoteHash = remoteHashById.get(id);
|
|
279
281
|
const localHash = entityHash(pkg.build);
|
|
@@ -88,6 +88,13 @@ const enrichItem = async (
|
|
|
88
88
|
return item;
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
const findSourceDir = async (handoff: Handoff, kind: RenderedEntityKind, id: string): Promise<string | undefined> => {
|
|
92
|
+
const runtime = kind === 'component' ? await handoff.store.components.get(id) : await handoff.store.patterns.get(id);
|
|
93
|
+
const sourceDir = (runtime as { path?: string } | null)?.path;
|
|
94
|
+
const relativeDir = sourceDir ? path.relative(handoff.workingPath, sourceDir) : '';
|
|
95
|
+
return relativeDir && !path.isAbsolute(relativeDir) ? relativeDir.split(path.sep).join('/') : undefined;
|
|
96
|
+
};
|
|
97
|
+
|
|
91
98
|
/** Map an entity's related source files to transfer files, dropping workspace-only declarations. */
|
|
92
99
|
const collectSourceFiles = async (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<TransferFile[]> => {
|
|
93
100
|
const store = kind === 'component' ? handoff.store.components : kind === 'pattern' ? handoff.store.patterns : handoff.store.pages;
|
|
@@ -164,6 +171,7 @@ const buildComponentPackage = async (handoff: Handoff, id: string): Promise<Tran
|
|
|
164
171
|
const artifacts = Array.from(byPath.values());
|
|
165
172
|
return {
|
|
166
173
|
item: await enrichItem(handoff, 'component', id, summary as unknown as Record<string, unknown>),
|
|
174
|
+
sourceDir: await findSourceDir(handoff, 'component', id),
|
|
167
175
|
files: await collectSourceFiles(handoff, 'component', id),
|
|
168
176
|
artifacts,
|
|
169
177
|
build: createArtifactBuild(artifacts),
|
|
@@ -214,6 +222,7 @@ const buildPatternPackage = async (handoff: Handoff, id: string): Promise<Transf
|
|
|
214
222
|
const artifacts = Array.from(byPath.values());
|
|
215
223
|
return {
|
|
216
224
|
item: await enrichItem(handoff, 'pattern', id, summary as unknown as Record<string, unknown>),
|
|
225
|
+
sourceDir: await findSourceDir(handoff, 'pattern', id),
|
|
217
226
|
files: await collectSourceFiles(handoff, 'pattern', id),
|
|
218
227
|
artifacts,
|
|
219
228
|
build: createArtifactBuild(artifacts),
|
|
@@ -276,6 +285,30 @@ export const assertRequiredArtifactsPresent = (pkg: TransferPackage): void => {
|
|
|
276
285
|
}
|
|
277
286
|
};
|
|
278
287
|
|
|
288
|
+
/**
|
|
289
|
+
* The largest publish request body the registry accepts. Vercel rejects a larger function body with
|
|
290
|
+
* `413 FUNCTION_PAYLOAD_TOO_LARGE`, and the transfer route's `bodyParser.sizeLimit` matches it.
|
|
291
|
+
*/
|
|
292
|
+
export const MAX_PUBLISH_BODY_BYTES = 4_500_000;
|
|
293
|
+
|
|
294
|
+
const formatMegabytes = (bytes: number): string => `${(bytes / 1_000_000).toFixed(1)} MB`;
|
|
295
|
+
|
|
296
|
+
export const assertPackageWithinUploadLimit = (kind: TransferEntityKind, id: string, pkg: TransferPackage): void => {
|
|
297
|
+
const size = Buffer.byteLength(JSON.stringify(pkg));
|
|
298
|
+
if (size <= MAX_PUBLISH_BODY_BYTES) return;
|
|
299
|
+
|
|
300
|
+
const largest = [...pkg.artifacts, ...pkg.files]
|
|
301
|
+
.map((entry) => ({ path: entry.path, size: Buffer.byteLength(entry.content) }))
|
|
302
|
+
.sort((a, b) => b.size - a.size)
|
|
303
|
+
.slice(0, 3)
|
|
304
|
+
.map((entry) => ` - ${entry.path} (${formatMegabytes(entry.size)})`)
|
|
305
|
+
.join('\n');
|
|
306
|
+
throw new PublishPackageError(
|
|
307
|
+
`The ${kind} "${id}" package is ${formatMegabytes(size)}; the registry accepts at most ` +
|
|
308
|
+
`${formatMegabytes(MAX_PUBLISH_BODY_BYTES)} per publish. Largest entries:\n${largest}`
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
279
312
|
/** Assemble the publish package for an entity from the generated `public/api` artifacts. */
|
|
280
313
|
export const buildPublishPackage = (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<TransferPackage> =>
|
|
281
314
|
kind === 'component'
|
package/src/registry/transfer.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface TransferBuild {
|
|
|
65
65
|
export interface TransferPackage {
|
|
66
66
|
/** Normalized component, pattern, or page record stored as the served record. */
|
|
67
67
|
item: Record<string, unknown>;
|
|
68
|
+
/** Workspace-relative entity directory, for components and patterns. It can start with `../`. */
|
|
69
|
+
sourceDir?: string;
|
|
68
70
|
files: TransferFile[];
|
|
69
71
|
artifacts: TransferArtifact[];
|
|
70
72
|
build: TransferBuild;
|
|
@@ -78,6 +80,8 @@ export interface CheckoutPayload {
|
|
|
78
80
|
kind: TransferEntityKind;
|
|
79
81
|
/** Normalized record used to reconstruct the entity. */
|
|
80
82
|
item: Record<string, unknown>;
|
|
83
|
+
/** The published {@link TransferPackage.sourceDir}. */
|
|
84
|
+
sourceDir?: string;
|
|
81
85
|
files: TransferFile[];
|
|
82
86
|
}
|
|
83
87
|
|
package/src/store/registry.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { orderPreviews } from '../catalog/previews';
|
|
18
18
|
import { and, eq, inArray, or, sql, type SQLWrapper } from 'drizzle-orm';
|
|
19
|
-
import type {
|
|
19
|
+
import type { StorageProvider, StorageReadResult } from '../registry/asset-storage/types';
|
|
20
20
|
import type { RegistryDatabase } from '../registry/db/client';
|
|
21
21
|
import {
|
|
22
22
|
assetBlobs,
|
|
@@ -53,11 +53,11 @@ import type {
|
|
|
53
53
|
export interface RegistryStoreContext {
|
|
54
54
|
db: RegistryDatabase;
|
|
55
55
|
/**
|
|
56
|
-
* Resolve the {@link
|
|
56
|
+
* Resolve the {@link StorageProvider} for a blob's recorded provider (or `null` for the inline
|
|
57
57
|
* `database` provider). Injected by the app runtime so `src/store` stays free of app-side imports;
|
|
58
58
|
* when omitted, only inline DB-backed asset content is resolvable.
|
|
59
59
|
*/
|
|
60
|
-
|
|
60
|
+
resolveStorageProvider?: (provider: AssetStorageProvider) => Promise<StorageProvider | null>;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** A persisted text-file row, as selected from `component_files`/`pattern_files`. */
|
|
@@ -422,14 +422,14 @@ export class RegistryAssetStore implements AssetStore {
|
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
// Object-backed - resolve through the provider that stored it.
|
|
425
|
-
if (!row.storageRef || !this.context.
|
|
425
|
+
if (!row.storageRef || !this.context.resolveStorageProvider) {
|
|
426
426
|
return null;
|
|
427
427
|
}
|
|
428
|
-
const
|
|
429
|
-
if (!
|
|
428
|
+
const storage = await this.context.resolveStorageProvider(row.storageProvider);
|
|
429
|
+
if (!storage) {
|
|
430
430
|
return null;
|
|
431
431
|
}
|
|
432
|
-
const result:
|
|
432
|
+
const result: StorageReadResult = await storage.get(row.storageRef);
|
|
433
433
|
if (result.kind === 'redirect') {
|
|
434
434
|
return { ...base, redirectUrl: result.url };
|
|
435
435
|
}
|
|
@@ -5,8 +5,10 @@ import type { SourceFormat } from '../../../catalog/renderers';
|
|
|
5
5
|
import {
|
|
6
6
|
BuildCache,
|
|
7
7
|
checkOutputExists,
|
|
8
|
+
computeBuildInputStates,
|
|
8
9
|
computeComponentFileStates,
|
|
9
10
|
computeGlobalDepsState,
|
|
11
|
+
createBuildInputs,
|
|
10
12
|
createEmptyCache,
|
|
11
13
|
hasComponentChanged,
|
|
12
14
|
haveGlobalDepsChanged,
|
|
@@ -162,27 +164,17 @@ const createComponentBuildPlan = (
|
|
|
162
164
|
};
|
|
163
165
|
};
|
|
164
166
|
|
|
165
|
-
/**
|
|
166
|
-
* Options for processing components
|
|
167
|
-
*/
|
|
168
|
-
export interface ProcessComponentsOptions {
|
|
169
|
-
/** Enable caching to skip unchanged components */
|
|
170
|
-
useCache?: boolean;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
167
|
/**
|
|
174
168
|
* Process components and generate their code, styles, and previews
|
|
175
169
|
* @param handoff - The Handoff instance containing configuration and state
|
|
176
170
|
* @param id - Optional component ID to process a specific component
|
|
177
171
|
* @param segmentToProcess - Optional segment to update
|
|
178
|
-
* @param options - Optional processing options including cache settings
|
|
179
172
|
* @returns Promise resolving to an array of processed components
|
|
180
173
|
*/
|
|
181
174
|
export async function processComponents(
|
|
182
175
|
handoff: Handoff,
|
|
183
176
|
id?: string,
|
|
184
|
-
segmentToProcess?: ComponentSegment
|
|
185
|
-
options?: ProcessComponentsOptions
|
|
177
|
+
segmentToProcess?: ComponentSegment
|
|
186
178
|
): Promise<ComponentListObject[]> {
|
|
187
179
|
const result: ComponentListObject[] = [];
|
|
188
180
|
|
|
@@ -204,18 +196,17 @@ export async function processComponents(
|
|
|
204
196
|
return [];
|
|
205
197
|
}
|
|
206
198
|
|
|
207
|
-
// Determine which components need building based on cache
|
|
199
|
+
// Determine which components need building based on cache
|
|
208
200
|
let componentsToBuild: Set<string>;
|
|
209
201
|
let cache: BuildCache | null = null;
|
|
210
202
|
let currentGlobalDeps = {};
|
|
211
203
|
const componentFileStatesMap: Map<string, Awaited<ReturnType<typeof computeComponentFileStates>>> = new Map();
|
|
212
204
|
|
|
213
205
|
// Only use caching when:
|
|
214
|
-
// - useCache option is enabled
|
|
215
206
|
// - No specific component ID is requested (full build scenario)
|
|
216
207
|
// - No specific segment is requested (full build scenario)
|
|
217
208
|
// - Force flag is not set
|
|
218
|
-
const shouldUseCache =
|
|
209
|
+
const shouldUseCache = !id && !segmentToProcess && !handoff.force;
|
|
219
210
|
|
|
220
211
|
if (shouldUseCache) {
|
|
221
212
|
Logger.debug('Loading build cache...');
|
|
@@ -240,7 +231,7 @@ export async function processComponents(
|
|
|
240
231
|
if (!cachedEntry) {
|
|
241
232
|
Logger.info(`Component '${componentId}': new component, will build`);
|
|
242
233
|
componentsToBuild.add(componentId);
|
|
243
|
-
} else if (hasComponentChanged(cachedEntry, currentFileStates)) {
|
|
234
|
+
} else if (await hasComponentChanged(cachedEntry, currentFileStates)) {
|
|
244
235
|
Logger.info(`Component '${componentId}': source files changed, will rebuild`);
|
|
245
236
|
componentsToBuild.add(componentId);
|
|
246
237
|
} else if (!(await checkOutputExists(handoff, componentId))) {
|
|
@@ -372,6 +363,7 @@ export async function processComponents(
|
|
|
372
363
|
// Components should always have at least one preview variation.
|
|
373
364
|
ensureDefaultPreview(data);
|
|
374
365
|
|
|
366
|
+
const inputs = shouldUseCache ? createBuildInputs() : undefined;
|
|
375
367
|
const runsViteBuildSteps = buildPlan.js || buildPlan.css || buildPlan.previews;
|
|
376
368
|
let viteBuildStartedAtMs = 0;
|
|
377
369
|
if (runsViteBuildSteps) {
|
|
@@ -381,11 +373,11 @@ export async function processComponents(
|
|
|
381
373
|
|
|
382
374
|
// Build JS if needed (new build, validation missing, or explicit segment request).
|
|
383
375
|
if (buildPlan.js) {
|
|
384
|
-
data = await buildComponentJs(data, handoff);
|
|
376
|
+
data = await buildComponentJs(data, handoff, inputs);
|
|
385
377
|
}
|
|
386
378
|
// Build CSS if needed.
|
|
387
379
|
if (buildPlan.css) {
|
|
388
|
-
data = await buildComponentCss(data, handoff);
|
|
380
|
+
data = await buildComponentCss(data, handoff, inputs);
|
|
389
381
|
}
|
|
390
382
|
// Build previews (HTML, snapshots, etc) if needed.
|
|
391
383
|
if (buildPlan.previews) {
|
|
@@ -395,7 +387,8 @@ export async function processComponents(
|
|
|
395
387
|
previews: getBuildPreviews(data),
|
|
396
388
|
},
|
|
397
389
|
handoff,
|
|
398
|
-
components
|
|
390
|
+
components,
|
|
391
|
+
inputs
|
|
399
392
|
);
|
|
400
393
|
}
|
|
401
394
|
|
|
@@ -428,17 +421,18 @@ export async function processComponents(
|
|
|
428
421
|
result.push(summary);
|
|
429
422
|
|
|
430
423
|
// Update cache entry for this component after successful build
|
|
431
|
-
if (shouldUseCache) {
|
|
424
|
+
if (shouldUseCache && inputs) {
|
|
432
425
|
if (!cache) {
|
|
433
426
|
cache = createEmptyCache();
|
|
434
427
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
428
|
+
// Compute file states if not already computed (e.g., when global deps changed)
|
|
429
|
+
const fileStates = componentFileStatesMap.get(runtimeComponentId) ?? (await computeComponentFileStates(handoff, runtimeComponentId));
|
|
430
|
+
const inputStates = await computeBuildInputStates(handoff, runtimeComponentId, inputs);
|
|
431
|
+
if (inputStates) {
|
|
432
|
+
updateComponentCacheEntry(cache, runtimeComponentId, fileStates, inputStates);
|
|
438
433
|
} else {
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
updateComponentCacheEntry(cache, runtimeComponentId, computedFileStates);
|
|
434
|
+
// An incomplete list can miss a changed file, so the next run builds this component again.
|
|
435
|
+
delete cache.components[runtimeComponentId];
|
|
442
436
|
}
|
|
443
437
|
}
|
|
444
438
|
}
|