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:
Waleed
2026-07-31 16:04:07 -07:00
committed by GitHub
parent c0e7ea9421
commit 0bcf64a505
16 changed files with 175 additions and 121 deletions
@@ -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'
@@ -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'
@@ -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 {
@@ -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'
@@ -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'
@@ -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 {
@@ -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,
@@ -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'
+53
View File
@@ -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
}
+6 -39
View File
@@ -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,
+41
View File
@@ -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 -46
View File
@@ -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
})()