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
|
@@ -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
|
|
|
@@ -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),
|
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
|
}
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type SharedArtifactPresence,
|
|
26
26
|
} from '../preview/component/shared-artifacts';
|
|
27
27
|
import { TransformComponentTokensResult } from '../preview/types';
|
|
28
|
-
import { DEFAULT_CLIENT_BUILD_CONFIG, createReactResolvePlugin } from '../utils/build';
|
|
28
|
+
import { DEFAULT_CLIENT_BUILD_CONFIG, createReactResolvePlugin, withStyleLoaders } from '../utils/build';
|
|
29
29
|
import { formatHtml, trimPreview } from '../utils/html';
|
|
30
30
|
import { buildAndEvaluateModule } from '../utils/module';
|
|
31
31
|
import { loadSchemaFromComponent, loadSchemaFromFile } from '../utils/schema-loader';
|
|
@@ -138,13 +138,31 @@ async function loadComponentSchemaAndModule(
|
|
|
138
138
|
* @param componentExport - Export holding the implementation; the default export when absent
|
|
139
139
|
* @returns Client-side hydration source code
|
|
140
140
|
*/
|
|
141
|
-
function generateClientHydrationSource(
|
|
141
|
+
function generateClientHydrationSource(
|
|
142
|
+
componentId: string,
|
|
143
|
+
componentPath: string,
|
|
144
|
+
componentExport?: string,
|
|
145
|
+
declarationPath?: string
|
|
146
|
+
): string {
|
|
142
147
|
const importClause = componentExport && componentExport !== 'default' ? `{ ${componentExport} as Component }` : 'Component';
|
|
148
|
+
// A function cannot travel in the JSON props, so the client re-reads `render` from the module that
|
|
149
|
+
// defines it. Conditional, so an unaffected component does not ship its declaration's imports.
|
|
150
|
+
const declarationImport = declarationPath ? `import * as previewDeclaration from '${normalizePath(declarationPath)}';` : '';
|
|
151
|
+
const resolveComponent = declarationPath
|
|
152
|
+
? `(mount) => {
|
|
153
|
+
const previewKey = mount.getAttribute('data-handoff-preview');
|
|
154
|
+
const render = previewKey ? previewDeclaration[previewKey]?.render : undefined;
|
|
155
|
+
return typeof render === 'function' ? render : Component;
|
|
156
|
+
}`
|
|
157
|
+
: '() => Component';
|
|
143
158
|
|
|
144
159
|
return `
|
|
145
160
|
import React from 'react';
|
|
146
161
|
import { hydrateRoot } from 'react-dom/client';
|
|
147
162
|
import ${importClause} from '${normalizePath(componentPath)}';
|
|
163
|
+
${declarationImport}
|
|
164
|
+
|
|
165
|
+
const resolveComponent = ${resolveComponent};
|
|
148
166
|
|
|
149
167
|
const parseProps = (propsId) => {
|
|
150
168
|
const raw = propsId ? document.getElementById(propsId)?.textContent : '{}';
|
|
@@ -157,12 +175,14 @@ function generateClientHydrationSource(componentId: string, componentPath: strin
|
|
|
157
175
|
|
|
158
176
|
const standaloneRoot = document.getElementById('${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}');
|
|
159
177
|
if (standaloneRoot && !standaloneRoot.hasAttribute('data-handoff-component')) {
|
|
160
|
-
|
|
178
|
+
const StandaloneComponent = resolveComponent(standaloneRoot);
|
|
179
|
+
hydrateRoot(standaloneRoot, <StandaloneComponent {...parseProps('${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}')} />);
|
|
161
180
|
}
|
|
162
181
|
|
|
163
182
|
const patternMounts = document.querySelectorAll('[data-handoff-component=' + JSON.stringify('${componentId}') + ']');
|
|
164
183
|
patternMounts.forEach((mount) => {
|
|
165
|
-
|
|
184
|
+
const MountComponent = resolveComponent(mount);
|
|
185
|
+
hydrateRoot(mount, <MountComponent {...parseProps(mount.getAttribute('data-handoff-props'))} />);
|
|
166
186
|
});
|
|
167
187
|
`;
|
|
168
188
|
}
|
|
@@ -175,12 +195,13 @@ function generateClientHydrationSource(componentId: string, componentPath: strin
|
|
|
175
195
|
* artifact cannot be loaded.
|
|
176
196
|
* @param componentId - Component identifier
|
|
177
197
|
* @param previewTitle - Title for the preview
|
|
178
|
-
* @param renderedHtml - Server-rendered HTML
|
|
198
|
+
* @param renderedHtml - Server-rendered HTML, embedded verbatim so the client can hydrate it
|
|
179
199
|
* @param props - Component props as JSON
|
|
180
200
|
* @returns Complete HTML document
|
|
181
201
|
*/
|
|
182
202
|
function generateHtmlDocument(
|
|
183
203
|
componentId: string,
|
|
204
|
+
previewKey: string,
|
|
184
205
|
previewTitle: string,
|
|
185
206
|
renderedHtml: string,
|
|
186
207
|
props: any,
|
|
@@ -220,7 +241,7 @@ function generateHtmlDocument(
|
|
|
220
241
|
<title>${previewTitle}</title>
|
|
221
242
|
</head>
|
|
222
243
|
<body>
|
|
223
|
-
<div id="${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}">${renderedHtml}</div>
|
|
244
|
+
<div id="${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}" data-handoff-preview="${previewKey}">${renderedHtml}</div>
|
|
224
245
|
</body>
|
|
225
246
|
</html>`;
|
|
226
247
|
}
|
|
@@ -314,7 +335,15 @@ export function ssrRenderPlugin(
|
|
|
314
335
|
// `component/<id>.client.js` artifact. The hydration source only imports the component and
|
|
315
336
|
// reads props from the in-document `__APP_PROPS__` element, so it is identical across every
|
|
316
337
|
// preview of this component.
|
|
317
|
-
const
|
|
338
|
+
const declarationPath = Object.values(componentData.previews ?? {}).some((preview) => typeof preview.render === 'function')
|
|
339
|
+
? componentData.entries?.declaration
|
|
340
|
+
: undefined;
|
|
341
|
+
const clientHydrationSource = generateClientHydrationSource(
|
|
342
|
+
componentId,
|
|
343
|
+
componentPath,
|
|
344
|
+
componentData.componentExport,
|
|
345
|
+
declarationPath
|
|
346
|
+
);
|
|
318
347
|
const clientBuildConfig = {
|
|
319
348
|
...DEFAULT_CLIENT_BUILD_CONFIG,
|
|
320
349
|
logLevel: 'silent' as const,
|
|
@@ -326,9 +355,10 @@ export function ssrRenderPlugin(
|
|
|
326
355
|
plugins: [createReactResolvePlugin(handoff.workingPath, handoff.modulePath)],
|
|
327
356
|
};
|
|
328
357
|
|
|
329
|
-
const
|
|
358
|
+
const hookedClientBuildConfig = handoff.config?.hooks?.clientBuildConfig
|
|
330
359
|
? handoff.config.hooks.clientBuildConfig(clientBuildConfig)
|
|
331
360
|
: clientBuildConfig;
|
|
361
|
+
const finalClientBuildConfig = withStyleLoaders(hookedClientBuildConfig);
|
|
332
362
|
|
|
333
363
|
let clientBundleJs: string;
|
|
334
364
|
try {
|
|
@@ -364,16 +394,23 @@ export function ssrRenderPlugin(
|
|
|
364
394
|
|
|
365
395
|
// Generate previews for each variation
|
|
366
396
|
for (const previewKey in componentData.previews) {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
const
|
|
397
|
+
const preview = componentData.previews[previewKey];
|
|
398
|
+
const previewProps = preview.values;
|
|
399
|
+
// A synthetic pattern preview is not a named export, so the client looks up its
|
|
400
|
+
// `sourcePreview` instead. A preview without one has no `render` either.
|
|
401
|
+
const hydrationPreviewKey = preview.sourcePreview ?? previewKey;
|
|
402
|
+
// Rendered as a component rather than called, so a preview can use hooks and own state.
|
|
403
|
+
const PreviewComponent = (typeof preview.render === 'function' ? preview.render : ReactComponent) as ReactComponent;
|
|
404
|
+
|
|
405
|
+
// The markup must reach the document exactly as rendered. Formatting it adds text nodes React
|
|
406
|
+
// does not expect, so it discards the server markup and re-renders the preview on the client.
|
|
407
|
+
const serverRenderedHtml = ReactDOMServer.renderToString(React.createElement(PreviewComponent, previewProps));
|
|
372
408
|
|
|
373
409
|
finalHtml = generateHtmlDocument(
|
|
374
410
|
componentId,
|
|
375
|
-
|
|
376
|
-
|
|
411
|
+
hydrationPreviewKey,
|
|
412
|
+
preview.title,
|
|
413
|
+
serverRenderedHtml,
|
|
377
414
|
previewProps,
|
|
378
415
|
sharedArtifacts,
|
|
379
416
|
componentArtifacts
|
|
@@ -402,7 +439,7 @@ export function ssrRenderPlugin(
|
|
|
402
439
|
});
|
|
403
440
|
}
|
|
404
441
|
|
|
405
|
-
//
|
|
442
|
+
// The previews are already emitted, so this formats only the docs code view.
|
|
406
443
|
finalHtml = await formatHtml(finalHtml);
|
|
407
444
|
componentData.format = 'react';
|
|
408
445
|
componentData.preview = '';
|
|
@@ -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
|
}
|