mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
perf(bundle): stop three routes shipping the block and tool registries (#6138)
The public /integrations page downloaded, parsed and executed 19.54 MB of JavaScript, 15.52 MB of which was the tool registry — 4,351 tool configs, to render a catalog of names and icons. It arrived as `<script async>` in the initial HTML, so the browser fetched and ran it on load. Measured on a production build, chunks referenced by the page's own HTML: /integrations 19.54 MB -> 3.55 MB across 41 chunks (-82%) No chunk over 1 MB remains on that route. credit-usage also loses its copy. Three separate import edges, all the same shape: a module mixing pure helpers with registry-backed ones, so importing the pure half dragged in all 282 block configs and the tool registry behind them. - `lib/integrations/index.ts` built POPULAR_WORKFLOWS at module scope via `getAllBlockMeta()` and re-exported registry functions. The landing grid is a client component, so importing anything from that barrel inherited the whole graph. POPULAR_WORKFLOWS moves to its own module — its one real consumer is a server page — and the re-export goes, which .claude/rules/sim-imports.md already forbids and which nothing imported through the barrel. - `blocks/icon-color.ts` mixed pure contrast maths (`getTileIconColorClass`, `isLightTileColor`, used by the landing page) with `getBareIconStyle`, which reads `getAllBlocks()`. Split by dependency into `blocks/brand-icon-style.ts`; all six of its consumers are under app/workspace/** where the registry is already legitimately present. - `credit-usage-view.tsx` imported a date formatter from the logs feature's `utils.ts`, which also exports registry-backed badge components. `formatDateShort` has three consumers across three features, so it moves to `lib/core/utils/date-display.ts` per the repo's utils-location rule. Not addressed: account/settings/[section] still reaches the registry through workspace-permissions-provider -> socket-provider, one of the four known client edges from the module-graph audit rather than a stray import. It is also loaded via `dynamic()`, so it is likely lazy rather than on first paint. That belongs with the registry split, not here. workspace/* keeps its copy, which is correct — the editor needs tool params and outputs.
This commit is contained in:
@@ -6,9 +6,9 @@ import {
|
||||
type FAQItem,
|
||||
INTEGRATIONS,
|
||||
type Integration,
|
||||
POPULAR_WORKFLOWS,
|
||||
toIntegrationSummary,
|
||||
} from '@/lib/integrations'
|
||||
import { POPULAR_WORKFLOWS } from '@/lib/integrations/popular-workflows'
|
||||
import { withFilteredNoindex } from '@/lib/landing/seo'
|
||||
import { JsonLd } from '@/app/(landing)/components/json-ld'
|
||||
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { cn } from '@sim/emcn'
|
||||
import type { ReactNodeViewProps } from '@tiptap/react'
|
||||
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color'
|
||||
import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style'
|
||||
import { mentionIcon } from './mention-icon'
|
||||
import { MarkdownMention, type MentionAttrs } from './mention-node'
|
||||
import { simLinkPath } from './sim-link'
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {
|
||||
import { AgentSkillsIcon, McpIcon } from '@/components/icons'
|
||||
import { getDocumentIcon } from '@/components/icons/document-icons'
|
||||
import type { ChatContextKind, ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { getBareIconStyle } from '@/blocks/icon-color'
|
||||
import { getBareIconStyle } from '@/blocks/brand-icon-style'
|
||||
import { getBlockRegistry } from '@/blocks/registry'
|
||||
|
||||
interface RenderIconArgs {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import {
|
||||
import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block'
|
||||
import { extractStreamingStringArgument } from '@/lib/copilot/tools/streaming-args'
|
||||
import { getToolStatusDisplayTitle, getWaitCountdownTitle } from '@/lib/copilot/tools/tool-display'
|
||||
import { getBareIconStyle } from '@/blocks/icon-color'
|
||||
import { getBareIconStyle } from '@/blocks/brand-icon-style'
|
||||
import { getBlockByToolName } from '@/blocks/registry'
|
||||
import type { ToolCallStatus } from '../../../../types'
|
||||
import { resolveToolDisplayState } from '../../utils'
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import type {
|
||||
MothershipResource,
|
||||
MothershipResourceType,
|
||||
} from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color'
|
||||
import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style'
|
||||
import { logKeys } from '@/hooks/queries/logs'
|
||||
import { mothershipChatKeys } from '@/hooks/queries/mothership-chats'
|
||||
import { scheduleKeys } from '@/hooks/queries/schedules'
|
||||
|
||||
+2
-2
@@ -9,7 +9,6 @@ import { useParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { GmailIcon, SlackIcon } from '@/components/icons'
|
||||
import {
|
||||
getAllBlockMeta,
|
||||
INTEGRATIONS,
|
||||
type OAuthServiceMatch,
|
||||
resolveOAuthServiceForIntegration,
|
||||
@@ -17,7 +16,8 @@ import {
|
||||
} from '@/lib/integrations'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
|
||||
import { getBareIconStyle } from '@/blocks/icon-color'
|
||||
import { getBareIconStyle } from '@/blocks/brand-icon-style'
|
||||
import { getAllBlockMeta } from '@/blocks/registry'
|
||||
import type { ModuleTag } from '@/blocks/types'
|
||||
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
|
||||
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
|
||||
|
||||
@@ -33,6 +33,7 @@ import type {
|
||||
WorkflowLogSummary,
|
||||
} from '@/lib/api/contracts/logs'
|
||||
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
|
||||
import { formatDateShort } from '@/lib/core/utils/date-display'
|
||||
import {
|
||||
getEndDateFromTimeRange,
|
||||
getStartDateFromTimeRange,
|
||||
@@ -91,7 +92,6 @@ import {
|
||||
DELETED_WORKFLOW_LABEL,
|
||||
extractRetryInput,
|
||||
formatDate,
|
||||
formatDateShort,
|
||||
getDisplayStatus,
|
||||
type LogStatus,
|
||||
parseDuration,
|
||||
|
||||
@@ -188,31 +188,6 @@ export function formatLatency(ms: number): string {
|
||||
return formatDuration(ms, { precision: 2 }) ?? '—'
|
||||
}
|
||||
|
||||
export function formatDateShort(dateStr: string): string {
|
||||
const hasTime = dateStr.includes('T')
|
||||
const [datePart, timePart] = dateStr.split('T')
|
||||
const [, month, day] = datePart.split('-').map(Number)
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
]
|
||||
const dateLabel = `${months[month - 1]} ${day}`
|
||||
if (hasTime && timePart) {
|
||||
return `${dateLabel} ${timePart.slice(0, 5)}`
|
||||
}
|
||||
return dateLabel
|
||||
}
|
||||
|
||||
export const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return {
|
||||
|
||||
+1
-1
@@ -18,8 +18,8 @@ import { useRouter } from 'next/navigation'
|
||||
import { useQueryStates } from 'nuqs'
|
||||
import type { UsageLogEntry, UsageLogPeriod } from '@/lib/api/contracts/user'
|
||||
import { formatApportionedCreditCost, formatCreditsLabel } from '@/lib/billing/credits/conversion'
|
||||
import { formatDateShort } from '@/lib/core/utils/date-display'
|
||||
import { USAGE_LOG_SOURCE_LABELS } from '@/app/api/users/me/usage-logs/source-labels'
|
||||
import { formatDateShort } from '@/app/workspace/[workspaceId]/logs/utils'
|
||||
import {
|
||||
creditUsageParsers,
|
||||
creditUsageUrlKeys,
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
|
||||
import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color'
|
||||
import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useWorkspaceCredential, useWorkspaceCredentials } from '@/hooks/queries/credentials'
|
||||
import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials'
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Brand-icon styling that needs the block registry, kept apart from the pure
|
||||
* contrast helpers in `@/blocks/icon-color`.
|
||||
*
|
||||
* The split is by dependency, not by topic: this module reads `getAllBlocks()`,
|
||||
* so importing it pulls `blocks/registry-maps` → all 282 block configs → the
|
||||
* tool registry. `icon-color.ts` needs none of that, and it is imported by the
|
||||
* public landing `/integrations` page — which was inheriting the whole registry
|
||||
* purely because the two lived in one file.
|
||||
*
|
||||
* Every consumer here is under `app/workspace/**`, where the registry is
|
||||
* already legitimately present.
|
||||
*/
|
||||
|
||||
import type { ComponentType, CSSProperties } from 'react'
|
||||
import { getAllBlocks } from '@/blocks/registry'
|
||||
|
||||
/** A brand icon component that accepts standard styling props. */
|
||||
export type StyleableIcon = ComponentType<{ className?: string; style?: CSSProperties }>
|
||||
|
||||
/**
|
||||
* Lazily-built lookup from a block's icon component to its theme-safe brand
|
||||
* {@link BlockConfig.iconColor}. Keyed by component reference so callers that
|
||||
* already hold the icon (suggested actions, credential pickers, …) never need
|
||||
* to thread a block type or hand-pick a color. Built once on first read since
|
||||
* the block registry is static for the app's lifetime.
|
||||
*/
|
||||
let iconColorByComponent: Map<StyleableIcon, string> | null = null
|
||||
|
||||
function getIconColorMap(): Map<StyleableIcon, string> {
|
||||
if (iconColorByComponent) return iconColorByComponent
|
||||
const map = new Map<StyleableIcon, string>()
|
||||
for (const block of getAllBlocks()) {
|
||||
if (block.iconColor) map.set(block.icon, block.iconColor)
|
||||
}
|
||||
iconColorByComponent = map
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline `style` for rendering a brand icon bare (without its colored tile
|
||||
* background): the block's theme-safe {@link BlockConfig.iconColor} as `color`,
|
||||
* or `undefined` when none is defined so the caller keeps its own default
|
||||
* icon styling.
|
||||
*
|
||||
* Single-fill icons drawn with `fill='currentColor'` (e.g. HubSpot) adopt the
|
||||
* color; multi-color brand icons that hardcode their own fills (Slack, Gmail,
|
||||
* Jira, Salesforce, Google Calendar) ignore it and keep their own colors.
|
||||
*/
|
||||
export function getBareIconStyle(icon: StyleableIcon): CSSProperties | undefined {
|
||||
const color = getIconColorMap().get(icon)
|
||||
return color ? { color } : undefined
|
||||
}
|
||||
@@ -1,43 +1,10 @@
|
||||
import type { ComponentType, CSSProperties } from 'react'
|
||||
/**
|
||||
* Contrast helpers for brand tiles. Pure colour maths — deliberately free of any
|
||||
* `@/blocks/registry` import so the public landing `/integrations` page can use
|
||||
* these without pulling 282 block configs and the tool registry into its bundle.
|
||||
* Registry-backed icon styling lives in `@/blocks/brand-icon-style`.
|
||||
*/
|
||||
import { isLightColor } from '@/lib/colors'
|
||||
import { getAllBlocks } from '@/blocks/registry'
|
||||
|
||||
/** A brand icon component that accepts standard styling props. */
|
||||
export type StyleableIcon = ComponentType<{ className?: string; style?: CSSProperties }>
|
||||
|
||||
/**
|
||||
* Lazily-built lookup from a block's icon component to its theme-safe brand
|
||||
* {@link BlockConfig.iconColor}. Keyed by component reference so callers that
|
||||
* already hold the icon (suggested actions, credential pickers, …) never need
|
||||
* to thread a block type or hand-pick a color. Built once on first read since
|
||||
* the block registry is static for the app's lifetime.
|
||||
*/
|
||||
let iconColorByComponent: Map<StyleableIcon, string> | null = null
|
||||
|
||||
function getIconColorMap(): Map<StyleableIcon, string> {
|
||||
if (iconColorByComponent) return iconColorByComponent
|
||||
const map = new Map<StyleableIcon, string>()
|
||||
for (const block of getAllBlocks()) {
|
||||
if (block.iconColor) map.set(block.icon, block.iconColor)
|
||||
}
|
||||
iconColorByComponent = map
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline `style` for rendering a brand icon bare (without its colored tile
|
||||
* background): the block's theme-safe {@link BlockConfig.iconColor} as `color`,
|
||||
* or `undefined` when none is defined so the caller keeps its own default
|
||||
* icon styling.
|
||||
*
|
||||
* Single-fill icons drawn with `fill='currentColor'` (e.g. HubSpot) adopt the
|
||||
* color; multi-color brand icons that hardcode their own fills (Slack, Gmail,
|
||||
* Jira, Salesforce, Google Calendar) ignore it and keep their own colors.
|
||||
*/
|
||||
export function getBareIconStyle(icon: StyleableIcon): CSSProperties | undefined {
|
||||
const color = getIconColorMap().get(icon)
|
||||
return color ? { color } : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Brightness above which a brand tile is "clearly light" and a white foreground
|
||||
|
||||
@@ -21,10 +21,10 @@ import { createLogger } from '@sim/logger'
|
||||
import { formatDateTime } from '@sim/utils/formatting'
|
||||
import { isRecordLike } from '@sim/utils/object'
|
||||
import { useQueryStates } from 'nuqs'
|
||||
import { formatDateShort } from '@/lib/core/utils/date-display'
|
||||
import { getEndDateFromTimeRange, getStartDateFromTimeRange } from '@/lib/logs/filters'
|
||||
import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state'
|
||||
import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format'
|
||||
import { formatDateShort } from '@/app/workspace/[workspaceId]/logs/utils'
|
||||
import {
|
||||
ActivityLog,
|
||||
type ActivityLogEntry,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Compact date labels for dense table rows (`Jul 31`, `Jul 31 14:22`).
|
||||
*
|
||||
* Lives in `lib/` rather than beside the logs table because three unrelated
|
||||
* features render it — logs, credit usage, and audit logs — and the previous
|
||||
* home (`app/workspace/[workspaceId]/logs/utils.ts`) also exports registry-backed
|
||||
* badge components. Importing a date formatter from there pulled
|
||||
* `@/blocks/registry` → all 282 block configs → the tool registry into every
|
||||
* consumer's bundle.
|
||||
*/
|
||||
|
||||
const MONTHS = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Formats an ISO-ish date string as `MMM D`, appending `HH:mm` when the input
|
||||
* carries a time component. Parsed by string split rather than `Date` so the
|
||||
* label reflects the timestamp exactly as stored, with no timezone shifting.
|
||||
*/
|
||||
export function formatDateShort(dateStr: string): string {
|
||||
const hasTime = dateStr.includes('T')
|
||||
const [datePart, timePart] = dateStr.split('T')
|
||||
const [, month, day] = datePart.split('-').map(Number)
|
||||
const dateLabel = `${MONTHS[month - 1]} ${day}`
|
||||
if (hasTime && timePart) {
|
||||
return `${dateLabel} ${timePart.slice(0, 5)}`
|
||||
}
|
||||
return dateLabel
|
||||
}
|
||||
@@ -6,16 +6,16 @@
|
||||
* `INTEGRATIONS` is the serialized projection of `BlockConfig` written by
|
||||
* `scripts/generate-docs.ts` whenever a block changes.
|
||||
*
|
||||
* `POPULAR_WORKFLOWS` is derived from each block's `*BlockMeta` export (see
|
||||
* `apps/sim/blocks/registry.ts`, which now hosts both the execution
|
||||
* `BlockConfig` lookups and the presentation `BlockMeta` lookups). Block
|
||||
* files are the source of truth for both surfaces.
|
||||
* Deliberately free of `@/blocks` value imports: the landing `/integrations`
|
||||
* grid is a client component, so anything imported here that reaches
|
||||
* `blocks/registry-maps` ships all 282 block configs and the tool registry to a
|
||||
* public marketing page. `POPULAR_WORKFLOWS` lives in
|
||||
* `@/lib/integrations/popular-workflows` for exactly that reason. Type-only
|
||||
* imports are fine — they erase.
|
||||
*/
|
||||
|
||||
import { stripVersionSuffix } from '@sim/utils/string'
|
||||
import integrationsJson from '@/lib/integrations/integrations.json'
|
||||
import type { Integration, IntegrationSummary } from '@/lib/integrations/types'
|
||||
import { getAllBlockMeta } from '@/blocks/registry'
|
||||
|
||||
/** All integrations surfaced in the catalog, ordered by `scripts/generate-docs.ts`. */
|
||||
export const INTEGRATIONS: readonly Integration[] =
|
||||
@@ -28,45 +28,6 @@ export const INTEGRATIONS: readonly Integration[] =
|
||||
*/
|
||||
export const INTEGRATIONS_UPDATED_AT: string = integrationsJson.updatedAt
|
||||
|
||||
/** A curated `from → to` block-pair workflow surfaced on the landing page. */
|
||||
export interface PopularWorkflow {
|
||||
/** Integration display name (matches `Integration.name`). */
|
||||
from: string
|
||||
/** Integration display name. */
|
||||
to: string
|
||||
headline: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const TYPE_TO_NAME = new Map<string, string>()
|
||||
for (const integration of INTEGRATIONS) {
|
||||
TYPE_TO_NAME.set(integration.type, integration.name)
|
||||
TYPE_TO_NAME.set(stripVersionSuffix(integration.type), integration.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Curated popular workflow pairs (templates flagged `featured: true` that
|
||||
* reference at least one other integration). Derived from per-block meta —
|
||||
* each entry's `from` is the owner block, `to` is the first
|
||||
* `alsoIntegrations` entry, and `headline`/`description` come from the
|
||||
* template title and prompt.
|
||||
*/
|
||||
export const POPULAR_WORKFLOWS: readonly PopularWorkflow[] = (() => {
|
||||
const pairs: PopularWorkflow[] = []
|
||||
for (const [ownerType, meta] of Object.entries(getAllBlockMeta())) {
|
||||
for (const template of meta.templates ?? []) {
|
||||
if (!template.featured) continue
|
||||
const toType = template.alsoIntegrations?.[0]
|
||||
if (!toType) continue
|
||||
const from = TYPE_TO_NAME.get(ownerType) ?? TYPE_TO_NAME.get(stripVersionSuffix(ownerType))
|
||||
const to = TYPE_TO_NAME.get(toType) ?? TYPE_TO_NAME.get(stripVersionSuffix(toType))
|
||||
if (!from || !to) continue
|
||||
pairs.push({ from, to, headline: template.title, description: template.prompt })
|
||||
}
|
||||
}
|
||||
return pairs
|
||||
})()
|
||||
|
||||
/**
|
||||
* Projects a full `Integration` down to the fields the `/integrations`
|
||||
* catalog grid renders and searches by, replacing the full `operations`/
|
||||
@@ -109,6 +70,5 @@ export {
|
||||
resolveOAuthServiceForSlug,
|
||||
} from '@/lib/integrations/oauth-service'
|
||||
export type { AuthType, FAQItem, Integration, IntegrationSummary } from '@/lib/integrations/types'
|
||||
export { getAllBlockMeta, getBlockMeta, getTemplatesForBlock } from '@/blocks/registry'
|
||||
export type { BlockMeta, BlockTemplate } from '@/blocks/types'
|
||||
export { formatIntegrationType } from '@/blocks/types'
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Curated `from → to` block-pair workflows for the landing page, derived from
|
||||
* each block's `*BlockMeta` export.
|
||||
*
|
||||
* Split out of the `@/lib/integrations` barrel rather than living beside the
|
||||
* rest of the catalog data, because building this list calls `getAllBlockMeta()`
|
||||
* at module scope. That single call pulls `@/blocks/registry` →
|
||||
* `blocks/registry-maps` → all 282 block configs → the tool registry, and any
|
||||
* client component importing *anything* from the barrel inherited the whole
|
||||
* graph. The landing `/integrations` grid is a client component, so a public
|
||||
* marketing page was shipping the full registry to render a catalog of names
|
||||
* and icons.
|
||||
*
|
||||
* Keeping it in its own module means the one server component that needs it
|
||||
* imports it directly, and the barrel stays free of `@/blocks` value imports.
|
||||
*/
|
||||
|
||||
import { stripVersionSuffix } from '@sim/utils/string'
|
||||
import { INTEGRATIONS } from '@/lib/integrations'
|
||||
import { getAllBlockMeta } from '@/blocks/registry'
|
||||
|
||||
/** A curated `from → to` block-pair workflow surfaced on the landing page. */
|
||||
export interface PopularWorkflow {
|
||||
/** Integration display name (matches `Integration.name`). */
|
||||
from: string
|
||||
/** Integration display name. */
|
||||
to: string
|
||||
headline: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const TYPE_TO_NAME = new Map<string, string>()
|
||||
for (const integration of INTEGRATIONS) {
|
||||
TYPE_TO_NAME.set(integration.type, integration.name)
|
||||
TYPE_TO_NAME.set(stripVersionSuffix(integration.type), integration.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Templates flagged `featured: true` that reference at least one other
|
||||
* integration. Each entry's `from` is the owner block, `to` is the first
|
||||
* `alsoIntegrations` entry, and `headline`/`description` come from the template
|
||||
* title and prompt.
|
||||
*/
|
||||
export const POPULAR_WORKFLOWS: readonly PopularWorkflow[] = (() => {
|
||||
const pairs: PopularWorkflow[] = []
|
||||
for (const [ownerType, meta] of Object.entries(getAllBlockMeta())) {
|
||||
for (const template of meta.templates ?? []) {
|
||||
if (!template.featured) continue
|
||||
const toType = template.alsoIntegrations?.[0]
|
||||
if (!toType) continue
|
||||
const from = TYPE_TO_NAME.get(ownerType) ?? TYPE_TO_NAME.get(stripVersionSuffix(ownerType))
|
||||
const to = TYPE_TO_NAME.get(toType) ?? TYPE_TO_NAME.get(stripVersionSuffix(toType))
|
||||
if (!from || !to) continue
|
||||
pairs.push({ from, to, headline: template.title, description: template.prompt })
|
||||
}
|
||||
}
|
||||
return pairs
|
||||
})()
|
||||
Reference in New Issue
Block a user