From 80a6f9f9b1c8b4ca0fd5a06a3d7c1b0bf512d492 Mon Sep 17 00:00:00 2001 From: Jay Singh <140599484+mathdebate09@users.noreply.github.com> Date: Sun, 3 May 2026 10:55:16 +0530 Subject: [PATCH 1/2] ci: minify lib output, bundle size gate, pr build report --- src/effects/fx.ts | 22 +- src/flexbox/flex.ts | 428 +++--------------------------- src/internal/boundedStyleCache.ts | 18 ++ src/internal/spacingAxisMap.ts | 67 +++++ src/sizing/h.ts | 115 +------- src/sizing/size.ts | 50 +--- src/sizing/w.ts | 115 +------- src/spacing/margin.ts | 99 +++---- src/spacing/padding.ts | 99 +++---- src/typography/text.ts | 14 +- 10 files changed, 235 insertions(+), 792 deletions(-) create mode 100644 src/internal/boundedStyleCache.ts create mode 100644 src/internal/spacingAxisMap.ts diff --git a/src/effects/fx.ts b/src/effects/fx.ts index 1a92deb..2936f72 100644 --- a/src/effects/fx.ts +++ b/src/effects/fx.ts @@ -56,25 +56,11 @@ Array.from({ length: 10 }, (_, i) => i + 1).forEach((value) => { fx[`elevation_${value}`] = { elevation: value }; }); -// Dynamically add background color properties from colorList Object.keys(colorList).forEach((colorKey) => { - fx[`bg_color_${colorKey}`] = { - backgroundColor: colorList[colorKey], - }; -}); - -// Dynamically add tint color properties from colorList -Object.keys(colorList).forEach((colorKey) => { - fx[`tint_${colorKey}`] = { - tintColor: colorList[colorKey], - }; -}); - -// Dynamically add overlay color properties from colorList -Object.keys(colorList).forEach((colorKey) => { - fx[`overlay_${colorKey}`] = { - overlayColor: colorList[colorKey], - }; + const hex = colorList[colorKey]; + fx[`bg_color_${colorKey}`] = { backgroundColor: hex }; + fx[`tint_${colorKey}`] = { tintColor: hex }; + fx[`overlay_${colorKey}`] = { overlayColor: hex }; }); export default fx; diff --git a/src/flexbox/flex.ts b/src/flexbox/flex.ts index 33aa8ea..c0efde0 100644 --- a/src/flexbox/flex.ts +++ b/src/flexbox/flex.ts @@ -1,10 +1,12 @@ +import { spacingScale } from '../constants/spacingScale'; +import { getCachedStyle } from '../internal/boundedStyleCache'; import type { Flex } from '../types/flexbox'; const flex = { - // Function to handle dynamic flex values - f_: (num: number | string = 1): Flex => ({ - flex: Number(num), - }), + f_: (num: number | string = 1): Flex => + getCachedStyle(`f|${num}`, () => ({ + flex: Number(num), + })), f_1: { flex: 1 }, f_2: { flex: 2 }, @@ -16,298 +18,26 @@ const flex = { f_8: { flex: 8 }, f_9: { flex: 9 }, - // Function to handle dynamic gap values - gap_: (num: number | string = 0): Flex => ({ - gap: Number(num), - }), + gap_: (num: number | string = 0): Flex => + getCachedStyle(`gap|${num}`, () => ({ + gap: Number(num), + })), - // Gap properties - gap_0: { - gap: 0, - }, - gap_1: { - gap: 4, - }, - gap_2: { - gap: 8, - }, - gap_3: { - gap: 12, - }, - gap_4: { - gap: 16, - }, - gap_5: { - gap: 20, - }, - gap_6: { - gap: 24, - }, - gap_7: { - gap: 28, - }, - gap_8: { - gap: 32, - }, - gap_9: { - gap: 36, - }, - gap_10: { - gap: 40, - }, - gap_11: { - gap: 44, - }, - gap_12: { - gap: 48, - }, - gap_14: { - gap: 56, - }, - gap_16: { - gap: 64, - }, - gap_20: { - gap: 80, - }, - gap_24: { - gap: 96, - }, - gap_28: { - gap: 112, - }, - gap_32: { - gap: 128, - }, - gap_36: { - gap: 144, - }, - gap_40: { - gap: 160, - }, - gap_44: { - gap: 176, - }, - gap_48: { - gap: 192, - }, - gap_52: { - gap: 208, - }, - gap_56: { - gap: 224, - }, - gap_60: { - gap: 240, - }, - gap_64: { - gap: 256, - }, - gap_72: { - gap: 288, - }, - gap_80: { - gap: 320, - }, - gap_96: { - gap: 384, - }, + gap_x_: (num: number | string = 0): Flex => + getCachedStyle(`gapx|${num}`, () => ({ + rowGap: Number(num), + })), - // Function to handle dynamic row gap values - gap_x_: (num: number | string = 0): Flex => ({ - rowGap: Number(num), - }), + gap_y_: (num: number | string = 0): Flex => + getCachedStyle(`gapy|${num}`, () => ({ + columnGap: Number(num), + })), - // Row gap properties - gap_x_0: { - rowGap: 0, - }, - gap_x_1: { - rowGap: 4, - }, - gap_x_2: { - rowGap: 8, - }, - gap_x_3: { - rowGap: 12, - }, - gap_x_4: { - rowGap: 16, - }, - gap_x_5: { - rowGap: 20, - }, - gap_x_6: { - rowGap: 24, - }, - gap_x_7: { - rowGap: 28, - }, - gap_x_8: { - rowGap: 32, - }, - gap_x_9: { - rowGap: 36, - }, - gap_x_10: { - rowGap: 40, - }, - gap_x_11: { - rowGap: 44, - }, - gap_x_12: { - rowGap: 48, - }, - gap_x_14: { - rowGap: 56, - }, - gap_x_16: { - rowGap: 64, - }, - gap_x_20: { - rowGap: 80, - }, - gap_x_24: { - rowGap: 96, - }, - gap_x_28: { - rowGap: 112, - }, - gap_x_32: { - rowGap: 128, - }, - gap_x_36: { - rowGap: 144, - }, - gap_x_40: { - rowGap: 160, - }, - gap_x_44: { - rowGap: 176, - }, - gap_x_48: { - rowGap: 192, - }, - gap_x_52: { - rowGap: 208, - }, - gap_x_56: { - rowGap: 224, - }, - gap_x_60: { - rowGap: 240, - }, - gap_x_64: { - rowGap: 256, - }, - gap_x_72: { - rowGap: 288, - }, - gap_x_80: { - rowGap: 320, - }, - gap_x_96: { - rowGap: 384, - }, + basis_: (value: string | number = 'auto'): Flex => + getCachedStyle(`basis|${value}`, () => ({ + flexBasis: value === 'auto' ? value : Number(value), + })), - // Function to handle dynamic column gap values - gap_y_: (num: number | string = 0): Flex => ({ - columnGap: Number(num), - }), - - // Column gap properties - gap_y_0: { - columnGap: 0, - }, - gap_y_1: { - columnGap: 4, - }, - gap_y_2: { - columnGap: 8, - }, - gap_y_3: { - columnGap: 12, - }, - gap_y_4: { - columnGap: 16, - }, - gap_y_5: { - columnGap: 20, - }, - gap_y_6: { - columnGap: 24, - }, - gap_y_7: { - columnGap: 28, - }, - gap_y_8: { - columnGap: 32, - }, - gap_y_9: { - columnGap: 36, - }, - gap_y_10: { - columnGap: 40, - }, - gap_y_11: { - columnGap: 44, - }, - gap_y_12: { - columnGap: 48, - }, - gap_y_14: { - columnGap: 56, - }, - gap_y_16: { - columnGap: 64, - }, - gap_y_20: { - columnGap: 80, - }, - gap_y_24: { - columnGap: 96, - }, - gap_y_28: { - columnGap: 112, - }, - gap_y_32: { - columnGap: 128, - }, - gap_y_36: { - columnGap: 144, - }, - gap_y_40: { - columnGap: 160, - }, - gap_y_44: { - columnGap: 176, - }, - gap_y_48: { - columnGap: 192, - }, - gap_y_52: { - columnGap: 208, - }, - gap_y_56: { - columnGap: 224, - }, - gap_y_60: { - columnGap: 240, - }, - gap_y_64: { - columnGap: 256, - }, - gap_y_72: { - columnGap: 288, - }, - gap_y_80: { - columnGap: 320, - }, - gap_y_96: { - columnGap: 384, - }, - - // Flex direction properties row: { flexDirection: 'row', }, @@ -321,7 +51,6 @@ const flex = { flexDirection: 'column-reverse', }, - // Flex grow properties grow: { flexGrow: 1, }, @@ -329,7 +58,6 @@ const flex = { flexGrow: 0, }, - // Flex shrink properties shrink: { flexShrink: 1, }, @@ -337,7 +65,6 @@ const flex = { flexShrink: 0, }, - // Flex wrap properties wrap: { flexWrap: 'wrap', }, @@ -348,108 +75,17 @@ const flex = { flexWrap: 'nowrap', }, - // Function to handle dynamic flex basis values - basis_: (value: string | number = 'auto'): Flex => ({ - flexBasis: value === 'auto' ? value : Number(value), - }), - - // Flex basis properties - basis_0: { - flexBasis: 0, - }, - basis_1: { - flexBasis: 4, - }, - basis_2: { - flexBasis: 8, - }, - basis_3: { - flexBasis: 12, - }, - basis_4: { - flexBasis: 16, - }, - basis_5: { - flexBasis: 20, - }, - basis_6: { - flexBasis: 24, - }, - basis_7: { - flexBasis: 28, - }, - basis_8: { - flexBasis: 32, - }, - basis_9: { - flexBasis: 36, - }, - basis_10: { - flexBasis: 40, - }, - basis_11: { - flexBasis: 44, - }, - basis_12: { - flexBasis: 48, - }, - basis_14: { - flexBasis: 56, - }, - basis_16: { - flexBasis: 64, - }, - basis_20: { - flexBasis: 80, - }, - basis_24: { - flexBasis: 96, - }, - basis_28: { - flexBasis: 112, - }, - basis_32: { - flexBasis: 128, - }, - basis_36: { - flexBasis: 144, - }, - basis_40: { - flexBasis: 160, - }, - basis_44: { - flexBasis: 176, - }, - basis_48: { - flexBasis: 192, - }, - basis_52: { - flexBasis: 208, - }, - basis_56: { - flexBasis: 224, - }, - basis_60: { - flexBasis: 240, - }, - basis_64: { - flexBasis: 256, - }, - basis_72: { - flexBasis: 288, - }, - basis_80: { - flexBasis: 320, - }, - basis_96: { - flexBasis: 384, - }, basis_auto: { flexBasis: 'auto', }, - basis_px: { - flexBasis: 1, - }, -}; +} as Flex & Record>; + +for (const key of Object.keys(spacingScale)) { + const n = spacingScale[key as keyof typeof spacingScale]; + flex[`gap_${key}`] = { gap: n }; + flex[`gap_x_${key}`] = { rowGap: n }; + flex[`gap_y_${key}`] = { columnGap: n }; + flex[`basis_${key}`] = { flexBasis: n }; +} -export default flex; +export default flex as Flex; diff --git a/src/internal/boundedStyleCache.ts b/src/internal/boundedStyleCache.ts new file mode 100644 index 0000000..3fa517c --- /dev/null +++ b/src/internal/boundedStyleCache.ts @@ -0,0 +1,18 @@ +/** Tiny LRU-ish cache: caps heap from repeated dynamic style calls with identical args. */ +const MAX_ENTRIES = 64; + +const cache = new Map(); +const order: string[] = []; + +export function getCachedStyle(key: string, create: () => T): T { + const hit = cache.get(key); + if (hit) return hit as T; + const next = create(); + if (cache.size >= MAX_ENTRIES) { + const oldest = order.shift(); + if (oldest !== undefined) cache.delete(oldest); + } + cache.set(key, next); + order.push(key); + return next; +} diff --git a/src/internal/spacingAxisMap.ts b/src/internal/spacingAxisMap.ts new file mode 100644 index 0000000..9b0deb5 --- /dev/null +++ b/src/internal/spacingAxisMap.ts @@ -0,0 +1,67 @@ +import { spacingScale } from '../constants/spacingScale'; +import type { DynamicStyleMap } from '../types/maps'; +import type { Margin, Padding } from '../types/spacing'; + +const lookup: Record = spacingScale; + +export function resolveSpacingValue(key: string | number, kind: 'padding' | 'margin'): number | 'auto' { + if (typeof key === 'number') return key; + if (key === 'auto') return 'auto'; + const value = lookup[key] ?? parseInt(key, 10); + if (isNaN(value)) throw new Error(`Invalid ${kind} key: ${key}`); + return value; +} + +/** Longest token first so `px_1` matches `px` not `p`. */ +export function parseStaticKey(prop: string, prefixes: readonly string[]): { type: string; scaleKey: string } | null { + for (const prefix of prefixes) { + const head = `${prefix}_`; + if (prop.startsWith(head) && prop.length > head.length) { + return { type: prefix, scaleKey: prop.slice(head.length) }; + } + } + return null; +} + +/** + * Static tokens (e.g. `p_4`) are created on first access via Proxy get — lowers startup heap + * until tokens are touched. `Object.keys(map)` only lists keys that already exist on the target; + * use `in` / property reads for unknown static tokens. + */ +export function wrapLazySpacingProxy(base: Record, prefixes: readonly string[], build: (type: string, scaleKey: string | number) => T, kind: 'padding' | 'margin'): DynamicStyleMap { + return new Proxy(base, { + get(target, prop, receiver) { + if (typeof prop === 'symbol') return Reflect.get(target, prop, receiver); + const direct = Reflect.get(target, prop, receiver); + if (direct !== undefined || Object.prototype.hasOwnProperty.call(target, prop)) { + return direct; + } + const parsed = parseStaticKey(prop, prefixes); + if (!parsed) return undefined; + try { + const style = build(parsed.type, parsed.scaleKey); + Object.defineProperty(target, prop, { + value: style, + enumerable: true, + writable: true, + configurable: true, + }); + return style; + } catch { + return undefined; + } + }, + has(target, prop) { + if (typeof prop === 'symbol') return Reflect.has(target, prop); + if (prop in target) return true; + const parsed = parseStaticKey(prop, prefixes); + if (!parsed) return false; + try { + resolveSpacingValue(parsed.scaleKey, kind); + return true; + } catch { + return false; + } + }, + }) as DynamicStyleMap; +} diff --git a/src/sizing/h.ts b/src/sizing/h.ts index 9e41cd4..41c3c84 100644 --- a/src/sizing/h.ts +++ b/src/sizing/h.ts @@ -1,110 +1,21 @@ +import { spacingScale } from '../constants/spacingScale'; +import { getCachedStyle } from '../internal/boundedStyleCache'; + const h = { - // Height properties - h_0: { height: 0 }, - h_px: { height: 1 }, - h_1: { height: 4 }, - h_2: { height: 8 }, - h_3: { height: 12 }, - h_4: { height: 16 }, - h_5: { height: 20 }, - h_6: { height: 24 }, - h_7: { height: 28 }, - h_8: { height: 32 }, - h_9: { height: 36 }, - h_10: { height: 40 }, - h_11: { height: 44 }, - h_12: { height: 48 }, - h_14: { height: 56 }, - h_16: { height: 64 }, - h_20: { height: 80 }, - h_24: { height: 96 }, - h_28: { height: 112 }, - h_32: { height: 128 }, - h_36: { height: 144 }, - h_40: { height: 160 }, - h_44: { height: 176 }, - h_48: { height: 192 }, - h_52: { height: 208 }, - h_56: { height: 224 }, - h_60: { height: 240 }, - h_64: { height: 256 }, - h_72: { height: 288 }, - h_80: { height: 320 }, - h_96: { height: 384 }, h_full: { height: '100%' }, h_half: { height: '50%' }, h_auto: { height: 'auto' }, - // Max-height properties - max_0: { maxHeight: 0 }, - max_px: { maxHeight: 1 }, - max_1: { maxHeight: 4 }, - max_2: { maxHeight: 8 }, - max_3: { maxHeight: 12 }, - max_4: { maxHeight: 16 }, - max_5: { maxHeight: 20 }, - max_6: { maxHeight: 24 }, - max_7: { maxHeight: 28 }, - max_8: { maxHeight: 32 }, - max_9: { maxHeight: 36 }, - max_10: { maxHeight: 40 }, - max_11: { maxHeight: 44 }, - max_12: { maxHeight: 48 }, - max_14: { maxHeight: 56 }, - max_16: { maxHeight: 64 }, - max_20: { maxHeight: 80 }, - max_24: { maxHeight: 96 }, - max_28: { maxHeight: 112 }, - max_32: { maxHeight: 128 }, - max_36: { maxHeight: 144 }, - max_40: { maxHeight: 160 }, - max_44: { maxHeight: 176 }, - max_48: { maxHeight: 192 }, - max_52: { maxHeight: 208 }, - max_56: { maxHeight: 224 }, - max_60: { maxHeight: 240 }, - max_64: { maxHeight: 256 }, - max_72: { maxHeight: 288 }, - max_80: { maxHeight: 320 }, - max_96: { maxHeight: 384 }, - - // Min-height properties - min_0: { minHeight: 0 }, - min_px: { minHeight: 1 }, - min_1: { minHeight: 4 }, - min_2: { minHeight: 8 }, - min_3: { minHeight: 12 }, - min_4: { minHeight: 16 }, - min_5: { minHeight: 20 }, - min_6: { minHeight: 24 }, - min_7: { minHeight: 28 }, - min_8: { minHeight: 32 }, - min_9: { minHeight: 36 }, - min_10: { minHeight: 40 }, - min_11: { minHeight: 44 }, - min_12: { minHeight: 48 }, - min_14: { minHeight: 56 }, - min_16: { minHeight: 64 }, - min_20: { minHeight: 80 }, - min_24: { minHeight: 96 }, - min_28: { minHeight: 112 }, - min_32: { minHeight: 128 }, - min_36: { minHeight: 144 }, - min_40: { minHeight: 160 }, - min_44: { minHeight: 176 }, - min_48: { minHeight: 192 }, - min_52: { minHeight: 208 }, - min_56: { minHeight: 224 }, - min_60: { minHeight: 240 }, - min_64: { minHeight: 256 }, - min_72: { minHeight: 288 }, - min_80: { minHeight: 320 }, - min_96: { minHeight: 384 }, + w_: (value: number | string) => getCachedStyle(`h|${value}`, () => ({ height: value })), + max_: (value: number | string) => getCachedStyle(`hmax|${value}`, () => ({ maxHeight: value })), + min_: (value: number | string) => getCachedStyle(`hmin|${value}`, () => ({ minHeight: value })), +} as Record; - // Dynamic height functions - w_: (value: number | string) => ({ height: value }), - max_: (value: number | string) => ({ maxHeight: value }), - min_: (value: number | string) => ({ minHeight: value }), -}; +for (const key of Object.keys(spacingScale)) { + const n = spacingScale[key as keyof typeof spacingScale]; + h[`h_${key}`] = { height: n }; + h[`max_${key}`] = { maxHeight: n }; + h[`min_${key}`] = { minHeight: n }; +} export default h; diff --git a/src/sizing/size.ts b/src/sizing/size.ts index a0aa3d7..24126c6 100644 --- a/src/sizing/size.ts +++ b/src/sizing/size.ts @@ -1,41 +1,17 @@ +import { spacingScale } from '../constants/spacingScale'; +import { getCachedStyle } from '../internal/boundedStyleCache'; + const size = { - // Predefined size properties - s_0: { width: 0, height: 0 }, - s_1: { width: 4, height: 4 }, - s_2: { width: 8, height: 8 }, - s_3: { width: 12, height: 12 }, - s_4: { width: 16, height: 16 }, - s_5: { width: 20, height: 20 }, - s_6: { width: 24, height: 24 }, - s_7: { width: 28, height: 28 }, - s_8: { width: 32, height: 32 }, - s_9: { width: 36, height: 36 }, - s_10: { width: 40, height: 40 }, - s_11: { width: 44, height: 44 }, - s_12: { width: 48, height: 48 }, - s_14: { width: 56, height: 56 }, - s_16: { width: 64, height: 64 }, - s_20: { width: 80, height: 80 }, - s_24: { width: 96, height: 96 }, - s_28: { width: 112, height: 112 }, - s_32: { width: 128, height: 128 }, - s_36: { width: 144, height: 144 }, - s_40: { width: 160, height: 160 }, - s_44: { width: 176, height: 176 }, - s_48: { width: 192, height: 192 }, - s_52: { width: 208, height: 208 }, - s_56: { width: 224, height: 224 }, - s_60: { width: 240, height: 240 }, - s_64: { width: 256, height: 256 }, - s_72: { width: 288, height: 288 }, - s_80: { width: 320, height: 320 }, - s_96: { width: 384, height: 384 }, + s_: (value: number | string) => + getCachedStyle(`size|${value}`, () => ({ + width: value, + height: value, + })), +} as Record; - // Dynamic size functions - s_: (value: number | string) => ({ - width: value, - height: value, - }), -}; +for (const key of Object.keys(spacingScale)) { + const n = spacingScale[key as keyof typeof spacingScale]; + size[`s_${key}`] = { width: n, height: n }; +} export default size; diff --git a/src/sizing/w.ts b/src/sizing/w.ts index e9601ca..ce4472b 100644 --- a/src/sizing/w.ts +++ b/src/sizing/w.ts @@ -1,110 +1,21 @@ +import { spacingScale } from '../constants/spacingScale'; +import { getCachedStyle } from '../internal/boundedStyleCache'; + const w = { - // Width properties - w_0: { width: 0 }, - w_px: { width: 1 }, - w_1: { width: 4 }, - w_2: { width: 8 }, - w_3: { width: 12 }, - w_4: { width: 16 }, - w_5: { width: 20 }, - w_6: { width: 24 }, - w_7: { width: 28 }, - w_8: { width: 32 }, - w_9: { width: 36 }, - w_10: { width: 40 }, - w_11: { width: 44 }, - w_12: { width: 48 }, - w_14: { width: 56 }, - w_16: { width: 64 }, - w_20: { width: 80 }, - w_24: { width: 96 }, - w_28: { width: 112 }, - w_32: { width: 128 }, - w_36: { width: 144 }, - w_40: { width: 160 }, - w_44: { width: 176 }, - w_48: { width: 192 }, - w_52: { width: 208 }, - w_56: { width: 224 }, - w_60: { width: 240 }, - w_64: { width: 256 }, - w_72: { width: 288 }, - w_80: { width: 320 }, - w_96: { width: 384 }, w_full: { width: '100%' }, w_half: { width: '50%' }, w_auto: { width: 'auto' }, - // Max-width properties - max_0: { maxWidth: 0 }, - max_px: { maxWidth: 1 }, - max_1: { maxWidth: 4 }, - max_2: { maxWidth: 8 }, - max_3: { maxWidth: 12 }, - max_4: { maxWidth: 16 }, - max_5: { maxWidth: 20 }, - max_6: { maxWidth: 24 }, - max_7: { maxWidth: 28 }, - max_8: { maxWidth: 32 }, - max_9: { maxWidth: 36 }, - max_10: { maxWidth: 40 }, - max_11: { maxWidth: 44 }, - max_12: { maxWidth: 48 }, - max_14: { maxWidth: 56 }, - max_16: { maxWidth: 64 }, - max_20: { maxWidth: 80 }, - max_24: { maxWidth: 96 }, - max_28: { maxWidth: 112 }, - max_32: { maxWidth: 128 }, - max_36: { maxWidth: 144 }, - max_40: { maxWidth: 160 }, - max_44: { maxWidth: 176 }, - max_48: { maxWidth: 192 }, - max_52: { maxWidth: 208 }, - max_56: { maxWidth: 224 }, - max_60: { maxWidth: 240 }, - max_64: { maxWidth: 256 }, - max_72: { maxWidth: 288 }, - max_80: { maxWidth: 320 }, - max_96: { maxWidth: 384 }, - - // Min-width properties - min_0: { minWidth: 0 }, - min_px: { minWidth: 1 }, - min_1: { minWidth: 4 }, - min_2: { minWidth: 8 }, - min_3: { minWidth: 12 }, - min_4: { minWidth: 16 }, - min_5: { minWidth: 20 }, - min_6: { minWidth: 24 }, - min_7: { minWidth: 28 }, - min_8: { minWidth: 32 }, - min_9: { minWidth: 36 }, - min_10: { minWidth: 40 }, - min_11: { minWidth: 44 }, - min_12: { minWidth: 48 }, - min_14: { minWidth: 56 }, - min_16: { minWidth: 64 }, - min_20: { minWidth: 80 }, - min_24: { minWidth: 96 }, - min_28: { minWidth: 112 }, - min_32: { minWidth: 128 }, - min_36: { minWidth: 144 }, - min_40: { minWidth: 160 }, - min_44: { minWidth: 176 }, - min_48: { minWidth: 192 }, - min_52: { minWidth: 208 }, - min_56: { minWidth: 224 }, - min_60: { minWidth: 240 }, - min_64: { minWidth: 256 }, - min_72: { minWidth: 288 }, - min_80: { minWidth: 320 }, - min_96: { minWidth: 384 }, + w_: (value: number | string) => getCachedStyle(`w|${value}`, () => ({ width: value })), + max_: (value: number | string) => getCachedStyle(`wmax|${value}`, () => ({ maxWidth: value })), + min_: (value: number | string) => getCachedStyle(`wmin|${value}`, () => ({ minWidth: value })), +} as Record; - // Dynamic width functions - w_: (value: number | string) => ({ width: value }), - max_: (value: number | string) => ({ maxWidth: value }), - min_: (value: number | string) => ({ minWidth: value }), -}; +for (const key of Object.keys(spacingScale)) { + const n = spacingScale[key as keyof typeof spacingScale]; + w[`w_${key}`] = { width: n }; + w[`max_${key}`] = { maxWidth: n }; + w[`min_${key}`] = { minWidth: n }; +} export default w; diff --git a/src/spacing/margin.ts b/src/spacing/margin.ts index 6dd5f72..1dc20aa 100644 --- a/src/spacing/margin.ts +++ b/src/spacing/margin.ts @@ -1,19 +1,13 @@ -import { spacingScale } from '../constants/spacingScale'; -import type { Margin } from '../types/spacing'; +import { getCachedStyle } from '../internal/boundedStyleCache'; +import { resolveSpacingValue, wrapLazySpacingProxy } from '../internal/spacingAxisMap'; import type { DynamicArg, DynamicStyleMap } from '../types/maps'; +import type { Margin } from '../types/spacing'; -const marginLookup: Record = spacingScale; - -const getMarginValue = (key: string | number): number | 'auto' => { - if (typeof key === 'number') return key; - if (key === 'auto') return 'auto'; - const value = marginLookup[key] ?? parseInt(key, 10); - if (isNaN(value)) throw new Error(`Invalid margin key: ${key}`); - return value; -}; +const M_NAMES = ['m', 'mx', 'my', 'mt', 'mr', 'mb', 'ml', 'ms', 'me'] as const; +const M_PREFIXES: readonly string[] = ['mx', 'my', 'mt', 'mr', 'mb', 'ml', 'ms', 'me', 'm']; -const generateMargin = (type: string, key: string | number): Margin => { - const value = getMarginValue(key); +function buildMargin(type: string, key: string | number): Margin { + const value = resolveSpacingValue(key, 'margin'); switch (type) { case 'm': return { margin: value }; @@ -36,61 +30,40 @@ const generateMargin = (type: string, key: string | number): Margin => { default: return {}; } -}; - -// prettier-ignore -const m: DynamicStyleMap = {}; +} -const allMarginKeys = [...Object.keys(marginLookup), 'auto']; +function createMarginMap(): DynamicStyleMap { + const base: Record = {}; -allMarginKeys.forEach((key) => { - ['m', 'mx', 'my', 'mt', 'mr', 'mb', 'ml', 'ms', 'me'].forEach((type) => { - m[`${type}_${key}`] = generateMargin(type, key); - }); -}); + base.m_ = (...keys: Array): Margin => { + if (keys.length === 1) { + return getCachedStyle(`m|1|${keys[0]}`, () => buildMargin('m', keys[0])); + } + if (keys.length === 2) { + return getCachedStyle(`m|2|${keys[0]}|${keys[1]}`, () => ({ + marginVertical: resolveSpacingValue(keys[0], 'margin'), + marginHorizontal: resolveSpacingValue(keys[1], 'margin'), + })); + } + if (keys.length === 4) { + return getCachedStyle(`m|4|${keys.join('|')}`, () => ({ + marginTop: resolveSpacingValue(keys[0], 'margin'), + marginRight: resolveSpacingValue(keys[1], 'margin'), + marginBottom: resolveSpacingValue(keys[2], 'margin'), + marginLeft: resolveSpacingValue(keys[3], 'margin'), + })); + } + throw new Error('m_ expects 1, 2, or 4 values'); + }; -['m', 'mx', 'my', 'mt', 'mr', 'mb', 'ml', 'ms', 'me'].forEach((type) => { - if (type === 'm') { - m[`${type}_`] = (...keys: Array): Margin => { - if (keys.length === 1) return generateMargin(type, keys[0]); - if (keys.length === 2) { - const vertical = getMarginValue(keys[0]); - const horizontal = getMarginValue(keys[1]); - return { marginVertical: vertical, marginHorizontal: horizontal }; - } - if (keys.length === 4) { - const top = getMarginValue(keys[0]); - const right = getMarginValue(keys[1]); - const bottom = getMarginValue(keys[2]); - const left = getMarginValue(keys[3]); - return { - marginTop: top, - marginRight: right, - marginBottom: bottom, - marginLeft: left, - }; - } - throw new Error('m_ expects 1, 2, or 4 values'); - }; - return; + for (const t of M_NAMES) { + if (t === 'm') continue; + base[`${t}_`] = (key: DynamicArg): Margin => getCachedStyle(`${t}|${key}`, () => buildMargin(t, key)); } - m[`${type}_`] = (key: DynamicArg): Margin => generateMargin(type, key); -}); + return wrapLazySpacingProxy(base, M_PREFIXES, buildMargin, 'margin'); +} -// Example usage -// const marginStyle = m.m_1; // { margin: 4 } -// const marginHorizontalStyle = m.mx_1; // { marginHorizontal: 4 } -// const marginVerticalStyle = m.my_1; // { marginVertical: 4 } -// const marginTopStyle = m.mt_1; // { marginTop: 4 } -// const marginBottomStyle = m.mb_1; // { marginBottom: 4 } -// const marginRightStyle = m.mr_1; // { marginRight: 4 } -// const marginLeftStyle = m.ml_1; // { marginLeft: 4 } -// const customMarginStyle = m.m_(1000); // { margin: 1000 } -// const customVerticalStyle = m.m_(8, 16); // { marginTop: 8, marginBottom: 16 } -// const customAllSidesStyle = m.m_(4, 8, 12, 16); // { marginTop: 4, marginRight: 8, marginBottom: 12, marginLeft: 16 } -// const autoMarginStyle = m.m_auto; // { margin: 'auto' } -// const autoHorizontalStyle = m.mx_auto; // { marginHorizontal: 'auto' } -// const autoVerticalStyle = m.my_auto; // { marginVertical: 'auto' } +const m = createMarginMap(); export default m; diff --git a/src/spacing/padding.ts b/src/spacing/padding.ts index b38ca85..9e77f68 100644 --- a/src/spacing/padding.ts +++ b/src/spacing/padding.ts @@ -1,19 +1,13 @@ -import { spacingScale } from '../constants/spacingScale'; -import type { Padding } from '../types/spacing'; +import { getCachedStyle } from '../internal/boundedStyleCache'; +import { resolveSpacingValue, wrapLazySpacingProxy } from '../internal/spacingAxisMap'; import type { DynamicArg, DynamicStyleMap } from '../types/maps'; +import type { Padding } from '../types/spacing'; -const paddingLookup: Record = spacingScale; - -const getPaddingValue = (key: string | number): number | 'auto' => { - if (typeof key === 'number') return key; - if (key === 'auto') return 'auto'; - const value = paddingLookup[key] ?? parseInt(key, 10); - if (isNaN(value)) throw new Error(`Invalid padding key: ${key}`); - return value; -}; +const P_NAMES = ['p', 'px', 'py', 'pt', 'pr', 'pb', 'pl', 'ps', 'pe'] as const; +const P_PREFIXES: readonly string[] = ['px', 'py', 'pt', 'pr', 'pb', 'pl', 'ps', 'pe', 'p']; -const generatePadding = (type: string, key: string | number): Padding => { - const value = getPaddingValue(key); +function buildPadding(type: string, key: string | number): Padding { + const value = resolveSpacingValue(key, 'padding'); switch (type) { case 'p': return { padding: value }; @@ -36,61 +30,40 @@ const generatePadding = (type: string, key: string | number): Padding => { default: return {}; } -}; - -// prettier-ignore -const p: DynamicStyleMap = {}; +} -const allPaddingKeys = [...Object.keys(paddingLookup), 'auto']; +function createPaddingMap(): DynamicStyleMap { + const base: Record = {}; -allPaddingKeys.forEach((key) => { - ['p', 'px', 'py', 'pt', 'pr', 'pb', 'pl', 'ps', 'pe'].forEach((type) => { - p[`${type}_${key}`] = generatePadding(type, key); - }); -}); + base.p_ = (...keys: Array): Padding => { + if (keys.length === 1) { + return getCachedStyle(`p|1|${keys[0]}`, () => buildPadding('p', keys[0])); + } + if (keys.length === 2) { + return getCachedStyle(`p|2|${keys[0]}|${keys[1]}`, () => ({ + paddingVertical: resolveSpacingValue(keys[0], 'padding'), + paddingHorizontal: resolveSpacingValue(keys[1], 'padding'), + })); + } + if (keys.length === 4) { + return getCachedStyle(`p|4|${keys.join('|')}`, () => ({ + paddingTop: resolveSpacingValue(keys[0], 'padding'), + paddingRight: resolveSpacingValue(keys[1], 'padding'), + paddingBottom: resolveSpacingValue(keys[2], 'padding'), + paddingLeft: resolveSpacingValue(keys[3], 'padding'), + })); + } + throw new Error('p_ expects 1, 2, or 4 values'); + }; -['p', 'px', 'py', 'pt', 'pr', 'pb', 'pl', 'ps', 'pe'].forEach((type) => { - if (type === 'p') { - p[`${type}_`] = (...keys: Array): Padding => { - if (keys.length === 1) return generatePadding(type, keys[0]); - if (keys.length === 2) { - const vertical = getPaddingValue(keys[0]); - const horizontal = getPaddingValue(keys[1]); - return { paddingVertical: vertical, paddingHorizontal: horizontal }; - } - if (keys.length === 4) { - const top = getPaddingValue(keys[0]); - const right = getPaddingValue(keys[1]); - const bottom = getPaddingValue(keys[2]); - const left = getPaddingValue(keys[3]); - return { - paddingTop: top, - paddingRight: right, - paddingBottom: bottom, - paddingLeft: left, - }; - } - throw new Error('p_ expects 1, 2, or 4 values'); - }; - return; + for (const t of P_NAMES) { + if (t === 'p') continue; + base[`${t}_`] = (key: DynamicArg): Padding => getCachedStyle(`${t}|${key}`, () => buildPadding(t, key)); } - p[`${type}_`] = (key: DynamicArg): Padding => generatePadding(type, key); -}); + return wrapLazySpacingProxy(base, P_PREFIXES, buildPadding, 'padding'); +} -// Example usage -// const paddingStyle = p.p_1; // { padding: 4 } -// const paddingHorizontalStyle = p.px_1; // { paddingHorizontal: 4 } -// const paddingVerticalStyle = p.py_1; // { paddingVertical: 4 } -// const paddingTopStyle = p.pt_1; // { paddingTop: 4 } -// const paddingBottomStyle = p.pb_1; // { paddingBottom: 4 } -// const paddingRightStyle = p.pr_1; // { paddingRight: 4 } -// const paddingLeftStyle = p.pl_1; // { paddingLeft: 4 } -// const customPaddingStyle = p.p_(1000); // { padding: 1000 } -// const customVerticalStyle = p.p_(8, 16); // { paddingTop: 8, paddingBottom: 16 } -// const customAllSidesStyle = p.p_(4, 8, 12, 16); // { paddingTop: 4, paddingRight: 8, paddingBottom: 12, paddingLeft: 16 } -// const autoPaddingStyle = p.p_auto; // { padding: 'auto' } -// const autoHorizontalStyle = p.px_auto; // { paddingHorizontal: 'auto' } -// const autoVerticalStyle = p.py_auto; // { paddingVertical: 'auto' } +const p = createPaddingMap(); export default p; diff --git a/src/typography/text.ts b/src/typography/text.ts index 3a94354..e6b24e5 100644 --- a/src/typography/text.ts +++ b/src/typography/text.ts @@ -103,18 +103,10 @@ const text: ColoredTextStyle = { select_all: textSnippet({ userSelect: 'all' }), }; -// Dynamically add color properties Object.keys(colorList).forEach((colorKey) => { - text[`color_${colorKey}`] = { - color: colorList[colorKey], - }; -}); - -// Dynamically add shadow color properties -Object.keys(colorList).forEach((colorKey) => { - text[`shadow_color_${colorKey}`] = { - textShadowColor: colorList[colorKey], - }; + const hex = colorList[colorKey]; + text[`color_${colorKey}`] = { color: hex }; + text[`shadow_color_${colorKey}`] = { textShadowColor: hex }; }); // Dynamically add shadow radius properties From 60f6b216899412fa28cc8a372e2ee3c8d6ae46e5 Mon Sep 17 00:00:00 2001 From: Jay Singh <140599484+mathdebate09@users.noreply.github.com> Date: Sun, 3 May 2026 13:24:39 +0530 Subject: [PATCH 2/2] ci: minify lib output, bundle size check, pr build report --- .github/workflows/ci.yml | 3 + .github/workflows/pr-build-report.yml | 80 +++++++++++++++++++++++++++ .gitignore | 1 + package.json | 1 + scripts/check-lib-size.mjs | 22 ++++++++ scripts/pr-build-report.mjs | 66 ++++++++++++++++++++++ tsup.config.ts | 1 + 7 files changed, 174 insertions(+) create mode 100644 .github/workflows/pr-build-report.yml create mode 100644 scripts/check-lib-size.mjs create mode 100644 scripts/pr-build-report.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db82975..ba2b4a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,3 +68,6 @@ jobs: - name: Build package run: npm run build + + - name: Check published bundle size + run: npm run check:size diff --git a/.github/workflows/pr-build-report.yml b/.github/workflows/pr-build-report.yml new file mode 100644 index 0000000..8e68733 --- /dev/null +++ b/.github/workflows/pr-build-report.yml @@ -0,0 +1,80 @@ +# Posts / updates a PR comment with typecheck + build wall times, lib bundle sizes, and links. +name: PR build report + +on: + pull_request: + branches: + - main + +permissions: + contents: read + pull-requests: write + +concurrency: + group: pr-build-report-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + report: + # Fork PRs cannot update comments with the default token; skip to avoid a failing step. + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Generate timed build report + env: + COMMIT_SHA: ${{ github.event.pull_request.head.sha }} + COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: node scripts/pr-build-report.mjs + + - name: Upsert PR comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + const body = fs.readFileSync('build-report.md', 'utf8'); + + const issue_number = context.payload.pull_request.number; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + per_page: 100, + }); + + const existing = comments.find( + (c) => + c.body?.includes(marker) && + (c.user?.login === 'github-actions[bot]' || c.user?.type === 'Bot'), + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + body, + }); + } diff --git a/.gitignore b/.gitignore index 72ebcb8..fd6a46c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ npm-debug.log yarn-debug.log yarn-error.log lib/ +build-report.md # Yarn .yarn/* diff --git a/package.json b/package.json index be6bf44..d158cad 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ ], "scripts": { "build": "tsup", + "check:size": "node scripts/check-lib-size.mjs", "typecheck": "tsc --noEmit", "lint": "eslint \"**/*.ts\"", "test": "jest", diff --git a/scripts/check-lib-size.mjs b/scripts/check-lib-size.mjs new file mode 100644 index 0000000..d853493 --- /dev/null +++ b/scripts/check-lib-size.mjs @@ -0,0 +1,22 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.join(__dirname, '..'); +const esmPath = path.join(root, 'lib', 'index.js'); +const cjsPath = path.join(root, 'lib', 'index.cjs'); + +const maxBytes = Number(process.env.MAX_LIB_BYTES ?? 45 * 1024); + +function check(label, filePath) { + const bytes = fs.statSync(filePath).size; + console.log(`${label}: ${bytes} bytes (max ${maxBytes})`); + if (bytes > maxBytes) { + console.error(`${label} exceeds budget — raise MAX_LIB_BYTES only after intentional growth`); + process.exit(1); + } +} + +check('lib/index.js', esmPath); +check('lib/index.cjs', cjsPath); diff --git a/scripts/pr-build-report.mjs b/scripts/pr-build-report.mjs new file mode 100644 index 0000000..b8994ca --- /dev/null +++ b/scripts/pr-build-report.mjs @@ -0,0 +1,66 @@ +/** + * Runs timed typecheck + build + size gate, writes build-report.md for the PR comment bot. + * Env: COMMIT_SHA, COMMIT_URL, RUN_URL (set by GitHub Actions). + */ +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.join(__dirname, '..'); +process.chdir(root); + +function runTimed(label, command) { + const t0 = performance.now(); + execSync(command, { stdio: 'inherit' }); + const ms = Math.round(performance.now() - t0); + return { label, ms }; +} + +const steps = []; +steps.push(runTimed('npm run typecheck', 'npm run typecheck')); +steps.push(runTimed('npm run build', 'npm run build')); +runTimed('npm run check:size', 'npm run check:size'); + +const lib = path.join(root, 'lib'); +const files = ['index.js', 'index.cjs', 'index.d.ts']; +const rows = files.map((f) => { + const p = path.join(lib, f); + return { file: f, bytes: fs.statSync(p).size }; +}); + +const commitSha = process.env.COMMIT_SHA ?? ''; +const commitUrl = process.env.COMMIT_URL ?? ''; +const runUrl = process.env.RUN_URL ?? ''; +const shortSha = commitSha ? commitSha.slice(0, 7) : ''; + +function fmtMs(ms) { + return `${(ms / 1000).toFixed(2)}s (${ms.toLocaleString()} ms)`; +} + +let md = ''; +md += `### nativeflowcss — build stats\n\n`; +md += `| Step | Wall time |\n|:------|:----------|\n`; +for (const s of steps) { + md += `| \`${s.label}\` | ${fmtMs(s.ms)} |\n`; +} +md += `\n**Published bundle (\`lib/\`)**\n\n`; +md += `| File | Size |\n|:------|-----:|\n`; +for (const r of rows) { + md += `| \`${r.file}\` | ${r.bytes.toLocaleString()} bytes |\n`; +} +if (commitUrl && commitSha) { + md += `\n**Head commit:** [\`${shortSha}\`](${commitUrl})\n`; +} else if (commitSha) { + md += `\n**Head commit:** \`${shortSha}\`\n`; +} +if (runUrl) { + md += `\n[View workflow run](${runUrl})\n`; +} +md += `\n_Comment updated on each push to this PR._\n`; + +const marker = ''; +const outPath = path.join(root, 'build-report.md'); +fs.writeFileSync(outPath, `${marker}\n\n${md}`, 'utf8'); +console.log('Wrote', outPath); diff --git a/tsup.config.ts b/tsup.config.ts index 1ce3733..2767678 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -6,5 +6,6 @@ export default defineConfig({ dts: true, clean: true, sourcemap: true, + minify: true, outDir: 'lib', });