Skip to content

Commit b60435c

Browse files
authored
Refactor: Modernize Utils Helpers (#208)
1 parent 361d492 commit b60435c

15 files changed

Lines changed: 342 additions & 56 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ xx.xx.xxxx
3838
* **Feature** - Added `includeMargin`, `includePadding`, and `includeBorder` options to `naturalWidth()` and `naturalHeight()` — allows measuring unconstrained intrinsic dimensions while preserving the element's box model.
3939

4040
### Utils
41+
* **BREAKING** - `noop` now returns `undefined` (truly no-op). Previously it returned its first argument (identity). Use the new `identity` export if you need the old behavior.
42+
* **Feature** - Added `identity` export — returns its first argument unchanged.
4143
* **Feature** - Added [`deepFreeze`](https://next.semantic-ui.com/docs/api/utils/cloning#deepfreeze) — recursively freezes a value in place and returns the same reference. Walks arrays and plain objects only, leaving `Date`, `Map`, `Set`, `RegExp`, DOM nodes, and custom class instances untouched so their internal slots keep working. Cycle-safe via an internal `WeakSet`; already-frozen inputs take a fast-path no-op.
4244
* **Feature** - Added [`createCache`](https://next.semantic-ui.com/docs/api/utils/cache) — a bounded, Map-like cache factory with pluggable eviction (`lru` default, `fifo`, `flush`) and an `onEvict` hook. Collapses ad-hoc `new Map()` + size-check patterns behind one named primitive.
4345
* **Feature** - Added `assignInPlace()` for syncing an object's contents to match a source without replacing the reference — supports `preserveExistingKeys` to skip deletion, `preserveGetters` to keep computed properties (own getter descriptors) intact across syncs, and `returnChanged` to detect modifications

ai/plans/ROADMAP.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,10 @@ Behavioral changes and API contracts that downstream agents and consumers will t
112112
|---|------|-------|------|-------|-------|
113113
| 2a | [Signal Performance](active/signal-performance.md) | 4-5h + audit | pair | scoped | `safety` preset system (`freeze` / `reference` / `none`) replacing `allowClone`. Audit of `.get()` call sites for get-mutate-set patterns gates the default flip. |
114114
| 2a.1 | [Fine-Grained Reactivity](active/fine-grained-reactivity.md) | 6-8h | pair | initial | `ReactiveDataContext` — per-key Signal bag — at `{#each}` items, subtemplate `reactiveData`, snippet args. Eliminates the N×M coarse invalidation pattern. Lands after 2a. |
115-
| 2a.2 | [FGR — As-Mode Per-Field Isolation](active/fgr-as-mode-per-field-isolation.md) | 3-4h | pair | initial | Closes the per-FIELD gap in `{#each todo in todos}` where one field's mutation wakes every binding reading the item. Two `it.fails` contracts in `subtree-spurious` come off. Fast-follow to 2a.1. |
116-
| 2a.3 | [Reactivity Hardening](reactivity-hardening.md) | 6-8h (up to 16h) | pair | scoped | Council-flagged Reaction/Scheduler/Dependency cleanup — `afterFlush` scheduling, terminal `stop`, throw-safety, set-swap, lazy refcounted computed (audit-cleared), plus benchmarks gating a conditional dep-tracking rewrite. Parallel to `2a`, minimal file overlap. |
117115
| 2b | [Value Schema](value-schema.md) | 16-24h (2-3d) | pair | initial | Contract for ~20-30 form components. `value` setting + schema + `change` event. Gates form/form-field and the wrapper architecture. |
118116
| 2c | [State from Settings](state-from-settings.md) | 8h | pair | scoped | `{ default: 'all', from: 'setting' }` in `defaultState`. Eliminates manual shadowing for components that accept initial values from attributes but own them as state. |
119117
| 2d | [Subtemplate Settings](subtemplate-settings.md) | 8-12h | pair | initial | Reactive `defaultSettings` on subtemplates with merged proxy over parent web component settings. Same upgrade path: add `tagName` and the subtemplate becomes a web component with no API change. |
120-
| 2e | [Template Match Blocks](template-match-blocks.md) | 8-16h (1-2d) | pair | scoped | `{#match}`/`{is}`/`{else}` — value-based branching. Replaces verbose `{#if is x 'a'}...{else if is x 'b'}` chains. **Sequence after `P16`** so `{#match}` inherits attribute-position support; otherwise it ships with the same hidden regression `{#if}` carries today. |
118+
| 2e | [Template Match Blocks](template-match-blocks.md) | 8-16h (1-2d) | pair | scoped | `{#match}`/`{is}`/`{else}` — value-based branching. Replaces verbose `{#if is x 'a'}...{else if is x 'b'}` chains. |
121119
| 2f | [Internationalization](i18n.md) | TBD | pair | initial | i18n as a built-in framework primitive — locale, formatters, RTL, language switching. Lands before Phase 4 to avoid retrofitting 60+ components. Pair session needed to scope. |
122120

123121
---
@@ -158,8 +156,8 @@ An agent needs eyes and tooling to work in isolation. Phase 4 has two gates befo
158156

159157
Wrappers are the adoption surface for the majority of consumers — React/Vue/Svelte developers using `@semantic-ui/react` etc. rather than registering web components manually. Architecture defines the generation pipeline; per-framework packages are the artifacts. Blocked on Value Schema (2b); iterates alongside Phase 4 primitive generation. Sequenced ahead of authored documentation because adoption needs the wrappers — docs alone don't convert most users.
160158

161-
| # | Plan | Hours | Mode | Scope | Notes |
162-
|---|------|-------|------|-------|-------|
159+
| # | What | Hours | Mode | Notes |
160+
|---|------|-------|------|-------|
163161
| 5a | [Wrapper Architecture](wrapper-architecture.md) | 40-56h (5-7d) | pair | initial | Generation pipeline for React/Vue/Svelte wrappers. Blocked on value schema. |
164162
| 5b | Wrapper Packages | 96-160h (12-20d) | pair | initial | `@semantic-ui/react`, `/vue`, `/svelte`. Blocked on wrapper architecture. |
165163

@@ -202,9 +200,6 @@ Slot in wherever there's a gap; not phase-gated.
202200
| P12 | [Template Spread Syntax](template-spread-syntax.md) | 4-8h | pair | scoped | `{>card ...friend}` — object spread in data passing. Ship when component templates demonstrate need. |
203201
| P13 | [Template Content Projection](template-wrapper-snippets.md) | 12-16h (1.5-2d) | pair | scoped | `{>content}` — content projection for snippets + subtemplates. Ship when component templates demonstrate need. |
204202
| P14 | [Template Let Bindings](template-let-bindings.md) | 10-14h (1-2d) | pair | scoped | `{#let}...{/let}` — snippet-for-vars. Ship when component templates demonstrate need. |
205-
| P15 | [Native Renderer — Perf Wins](native-renderer-perf-wins.md) | 4-6h | pair | scoped | Three small renderer cleanups: cache collapse (Option A — keep string cache for unsafeHTML), module-scoped TreeWalker, unsafeHTML dirty-check (~10000× savings ratio at unchanged values). Item 3 ships standalone for clean bench attribution. |
206-
| P16 | [Block Dispatch Unification](block-dispatch-unification.md) | 20-30h (2.5-4d) | pair | scoped | Restore block-position introspection (regression: `{#if}` in attribute values renders literal comment text) and fold expression handling into the block model. Every AST node dispatches via `getBlock(type)(bag)`; every block receives `entry.classification` and renders position-appropriately, matching Lit's `partInfo` contract. Step 1 ships independently as the regression bugfix. **Should land before `2e Template Match Blocks`** so match inherits attribute-position support. |
207-
| P17 | [defineBlock — Mount-Cost Reduction](defineblock-mount-cost.md) | 4-6h | pair | scoped | Eliminate per-dispatch closure construction in `defineBlock`. Krausest `create-1000`/`create-10000` wins. **Blocked on `2a.2` (FGR — As-Mode Per-Field Isolation) merge** to avoid `each.js` conflicts. |
208203

209204
---
210205

ai/plans/utils-modernization.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Utils Modernization
2+
3+
## Goal
4+
5+
`@semantic-ui/utils` has accumulated a mix of principled next-gen design and lodash-parity leftovers. The library's instinct is right — when two functions exist, they should capture meaningfully distinct behaviors, not arg-shape variants. A few leftovers violate that, and a few modern patterns are missing.
6+
7+
This plan captures the decisions needed to reconcile the library with that principle and stage the breaking changes behind a single release window.
8+
9+
**Naming principle**: two exports earn their place when they express **distinct intent at the call site**, even if the implementations overlap. Two names for the same intent (`any = some`) is a wart; two names for distinct intents that happen to share a body (`wrapFunction` as defensive normalization vs `constant` as explicit factory) is a feature — the name is for the reader.
10+
11+
## Design / Implementation
12+
13+
### Removals (breaking)
14+
15+
| Export | Issue | Proposed action |
16+
|--------|-------|-----------------|
17+
| `any` | Exact alias of `some` (`arrays.js:237`) — two names, one function | Delete. Document `some` as canonical (matches `Array.prototype.some`). |
18+
| `onlyKeys` | Same operation as `pick` but takes array instead of spread args | Pick one shape. Most callers pass an array today (from config/filters/`Object.keys`) — recommend standardizing on `pick(obj, keys)` taking an array, delete `onlyKeys`. |
19+
20+
### Signature changes (breaking)
21+
22+
| Export | Issue | Proposed action |
23+
|--------|-------|-----------------|
24+
| `first` / `last` | Return-type polymorphism: `first(arr)` returns element, `first(arr, 2)` returns array | Split: `first(arr)` always returns the element. Add `firstN(arr, n)` / `lastN(arr, n)` that always return arrays. |
25+
26+
### Additions
27+
28+
| Export | Rationale |
29+
|--------|-----------|
30+
| `returnsSelf`, `returnsNull`, `returnsTrue`, `returnsFalse`, `returnsUndefined` | Named, shared, reference-stable functions for the common constant returns. Ship the common cases as direct exports so every consumer dedupes to the same closure — enables reference-equality checks like `template.js`'s `this.onThemeChangedCallback !== noop` pattern to generalize across the codebase. |
31+
| `constant = wrapFunction` | Alias for the explicit-factory intent. Same body as `wrapFunction`, different semantic intent at the call site: `wrapFunction(x)` reads as defensive normalization ("value-or-function, normalize"); `constant(x)` reads as explicit factory ("function that returns this"). Covers the programmatic-value case (`constant(config.default)`) where a direct named export can't. |
32+
| `identity` → rename to `returnsSelf` | `identity` is brand-new in this release (from the noop/identity split). Rename to `returnsSelf` for consistency with the prefix family and to avoid FP jargon. Ship both renames together in this release window rather than churning twice. |
33+
| `pipe(...fns)(x)` | Functional composition is idiomatic in modern TS; lodash's `flow` is clunky. Small primitive, pairs well with existing polymorphic iterators. |
34+
| `attempt(fn, fallback)` or `safely(fn)` | Several sites in the codebase use `try { ... } catch { /* noop */ }` (see `browser.js:113,152,178,191`). A named helper makes intent greppable and documents the pattern. |
35+
| `tap(fn)` | Passes value through while running a side effect. Small, useful in pipes and reactive chains. |
36+
37+
### Decisions needed
38+
39+
- **Do `any` and `onlyKeys` get deprecation shims in this release, or hard-deleted?** Hard-delete is cleaner for a 1.0-era library; deprecation adds complexity for a short window. Recommend hard-delete + loud CHANGELOG entry + migration note.
40+
- **`first`/`last` strategy:** (a) always-scalar + new `firstN`/`lastN`, or (b) always-array (breaking for all current 1-arg callers)? Recommend (a) — smaller blast radius, clearer semantics at call site.
41+
- **`pick` arg shape final answer:** spread (`pick(obj, 'a', 'b')`) or array (`pick(obj, ['a', 'b'])`)? Recommend array — composes with `Object.keys`, filter results, config. Spread is 2014 ergonomics for hand-literal keys that rarely appear in modern code.
42+
- **`pipe` return style:** eager (`pipe(fns, x)`) or curried (`pipe(...fns)(x)`)? Curried composes better; eager is simpler.
43+
- **`attempt` vs `safely` naming:** `attempt(fn, fallback)` reads naturally ("attempt this, else fallback"). `safely(fn)` suggests "swallow errors" without the fallback value — less useful.
44+
- **Shape resolved.** Ship shared direct exports for the common constant returns (`returnsSelf`, `returnsNull`, `returnsTrue`, `returnsFalse`, `returnsUndefined`) — reference-stable across consumers, enables dedup and ref-equality checks. `constant` is an alias of `wrapFunction` covering the programmatic-value case. `identity` renames to `returnsSelf` in the same release window.
45+
- Rejected names along the way: `always` (asserts continued truth when actual use is ephemeral-default), bare `returns(value)` / `constant(value)` as the *only* surface (works, but loses the dedup win from sharing primitive closures across the codebase).
46+
47+
### Out of scope
48+
49+
- Changes to `extend` / `deepExtend` / `assignInPlace` — the three merge variants already capture distinct contracts (shallow / recursive / sync-with-deletion). Leave as-is.
50+
- Rewriting `proxyObject`, `weightedObjectSearch`, `hashCode` — these are niche, principled, and earn their export.
51+
52+
### Completed as one-line wins (not part of this plan)
53+
54+
- `proxyObject` default swapped from `noop` to `() => ({})`. *Revisit:* `() => ({})` allocates a fresh closure on each defaulted call — once `always` lands, change to `always({})` for a shared reference.
55+
- `hasProperty` reduced to thin re-export of `Object.hasOwn` (ES2022).
56+
- `utility-functions.md` skill updated for `noop` / `identity` split.
57+
58+
### Adoption once `always` lands
59+
60+
Audit the codebase for `() => <literal>` patterns that are candidates to migrate:
61+
- `NO_EQUALITY = () => false` in `reactivity/src/signal.js``always(false)`
62+
- `filter: () => true` defaults (e.g. `docs/src/examples/templates/expressions-fn/filter-list.js:7`)
63+
- `proxyObject` default (above)
64+
- Any `defaultSettings` entries that use bare `() => x` closures
65+
66+
## Open Questions
67+
68+
See "Decisions needed" above. Five design calls, all quick — should fit in one pair session.
69+
70+
Once decisions are made:
71+
- Codemod for `any``some`, `onlyKeys``pick` across `packages/`, `src/`, `docs/`, `tools/`.
72+
- Audit `first(arr, N)` / `last(arr, N)` call sites — all need rewrite to `firstN` / `lastN`.
73+
- CHANGELOG entries under **BREAKING** for each removal/signature change.
74+
- Type-file updates.
75+
- Skill doc (`ai/skills/authoring/utility-functions.md`) updates.
76+
77+
## Dependencies
78+
79+
None. Can land anytime — breaking changes batch naturally with other utils changes in a release window.
80+
81+
## Status
82+
83+
`initial` — design decisions pending. Created 2026-04-16 off the utils review following the signal-safety PR.

ai/skills/authoring/utility-functions.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -367,11 +367,17 @@ truncate('こんにちは世界です', 8, { locale: 'ja' }); // 'こ
367367

368368
### Core
369369
```javascript
370-
import { noop, wrapFunction } from '@semantic-ui/utils';
370+
import { noop, identity, wrapFunction } from '@semantic-ui/utils';
371371

372-
// Identity function — returns its argument
373-
noop(42); // 42
374-
noop('hello'); // 'hello'
372+
// noop — swallows arguments, returns undefined. Use as a reusable empty
373+
// callback to avoid allocating fresh () => {} closures.
374+
noop(); // undefined
375+
noop(42, 'ignored'); // undefined
376+
377+
// identity — returns its first argument unchanged. Use as a pass-through
378+
// default for transforms (e.g. `transform = mapFn ?? identity`).
379+
identity(42); // 42
380+
identity('hello'); // 'hello'
375381

376382
// Wraps non-functions into a function that returns the value
377383
const fn = wrapFunction('default'); // () => 'default'
@@ -827,7 +833,8 @@ const pattern = new RegExp(escapeRegExp('price ($5.00)'), 'i');
827833
### Functions (functions.js)
828834
| Function | Signature | Returns |
829835
|----------|-----------|---------|
830-
| `noop` | `(v)` | `v` (identity function) |
836+
| `noop` | `()` | `undefined` — swallows args, reusable empty callback |
837+
| `identity` | `(v)` | `v` — returns first arg unchanged |
831838
| `wrapFunction` | `(x)` | `x` if function, else `() => x` |
832839
| `memoize` | `(fn, hashFn?)` | Memoized function |
833840
| `wait` | `(ms, opts?)` | Promise |

0 commit comments

Comments
 (0)