Skip to content

feat: memoize the boundGetAsset function#2

Open
sempostma wants to merge 156 commits into
mainfrom
v4.beta
Open

feat: memoize the boundGetAsset function#2
sempostma wants to merge 156 commits into
mainfrom
v4.beta

Conversation

@sempostma
Copy link
Copy Markdown
Member

No description provided.

sempostma and others added 30 commits February 8, 2026 19:14
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
claude and others added 30 commits May 3, 2026 22:19
This eliminates the last connect() usage in decap-cms-core; only the
typed-hook re-exports in hooks/useRedux.tsx still import from react-redux.
The github/gitlab/gitea/bitbucket/git-gateway/azure/aws-cognito backend
registrations were commented out, which made the corresponding imports
dead code and caused 'Backend not found' at runtime for any config that
referenced them. The TestBackend and ProxyBackend registrations were
already active.
Migrate to TypeScript and modernize build tooling
The slate-based `decap-cms-widget-markdown` and Plate-based
`decap-cms-widget-richtext` had been commented out of `extensions.ts`
since the v4.beta migration began — neither the imports nor the
registrations were active, but the packages still shipped, were lint
targets, and dragged in 30+ orphaned dependencies (slate*, plate*,
remark-slate*, rehype-*, dompurify, is-hotkey, …). The
`decap-cms-widget-lexicaleditor` directory was a git submodule pointer
to a commit that doesn't exist in this repo, with no `.gitmodules`
backing it.

Removed:
- `packages/decap-cms-widget-markdown/`
- `packages/decap-cms-widget-richtext/`
- `packages/decap-cms-widget-lexicaleditor` (submodule placeholder)
- 14 `cypress/e2e/{markdown,richtext}_widget_*_spec.ts` files
- The commented-out import + registration in `decap-cms-app/extensions.ts`
- Source aliases for the three widget paths in `vite.config.demo.ts`
- The widget entry in `scripts/pack-and-install.sh`
- Entire `slate:` and `plate:` catalogs in `pnpm-workspace.yaml`
- Unused entries from the `markdown:` catalog (kept only what
  `decap-cms-core/src/formats/frontmatter.tsx`,
  `lib/registry.tsx`, and `EditorControl.tsx` actually import)
- `widget: 'markdown'` and `widget: 'richtext'` field types in
  `dev-test/config.yml` were swapped for `widget: 'text'` so the demo
  doesn't fault on missing widgets

Re-applied the same narrow `eslint-disable-next-line
react-hooks/exhaustive-deps` directives with reasons to the five backend
OAuth `AuthenticationPage` mount-only completion effects, the
`MediaLibrary` prev-refs effect, the `MediaLibraryCard` mount-only
displayURL load, the `Notifications` in-place idMap mutation effect, the
`useNavigationBlocker` setup effect, and routed `getValueType` /
`validateSize` through `latestRef` in `ListControl`'s
`useImperativeHandle` to silence the deps complaint properly.

Verification:
- `pnpm type-check` clean
- `npx vitest run` 931/931 pass
- `pnpm lint:js` 0 errors, 3 warnings (all
  `cypress/unsafe-to-chain-command` false positives on `prevSubject:
  true` custom commands)

Net -21,559 lines.
Remove markdown and richtext widget packages
`@iarna/toml` (which uses unsafe eval) and `@types/iarna__toml` are no
longer referenced by any package — frontmatter parsing migrated to
`smol-toml`. The whole `formats:` catalog (`@iarna/toml`, `tomlify-j0.4`,
`micromark-extension-frontmatter`, `micromark`) had no consumers either.

- Remove the dead workspace catalog entries.
- Remove the orphaned `tomlify-j0.4.d.ts` module declaration in
  decap-cms-core (the package itself was already gone).
- Drop the stale "tomlify" comment in formats/toml.ts.

Verified: monorepo type-check clean, 931 tests pass.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
Reduces from six double casts to one. The remaining cast on line 1390
is genuinely required and documented — \`getBackend()\` returns a
\`{ init }\` factory whose \`init()\` call eventually produces the full
\`Implementation\` after \`authenticate()\`.

Changes:
- Remove the cast on \`entry.mediaFiles\` — \`CmsMediaFileMap[]\` is
  already structurally compatible with the local \`MediaFile\`.
- Remove the no-op \`entry as unknown as CmsEntry\` in \`entryToRaw\`
  (the variable was already \`CmsEntry\`).
- Replace \`as unknown as CmsEntry\` and \`as unknown as Record<...>\`
  with single-cast type assertions.
- Make \`newRecord\`, \`status\` optional on \`CmsEntry\`, and widen
  \`isModification\` to \`boolean | null | undefined\`. These align
  \`CmsEntry\` with the actual runtime values produced by the reducers
  and let \`EntryValue\` flow into helpers like \`selectMediaFolders\`
  without casting.

Verified: monorepo type-check clean, 931 tests pass, lint clean.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
…-casts

Remove unnecessary `as unknown as` double casts in backend
chore: drop dead @iarna/toml and unused formats catalog entries
…policy

Immutable.js is already absent from the codebase (closing the earlier
"Remove Immutable.js" debt item). Lock that decision in via a lint rule
so the package can't sneak back in: any \`from 'immutable'\` or
\`from 'immutable/*'\` import now fails lint with a message pointing
contributors at ES6 spread / optional chaining instead.

Verified: lint clean.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
The demo bundle (\`pnpm run build:demo\`) was failing because the
\`codemirror\` workspace catalog entry pinned the package at v6, but
\`react-codemirror2\` and the language loaders in
\`decap-cms-widget-code\` use the CodeMirror 5 API (\`codemirror/mode/*\`
imports and the \`react-codemirror2\` keymap-by-string convention).
CM6 doesn't expose \`mode/*\` in its \`exports\` field, so rolldown
errored on first such import.

- Pin \`codemirror\` to \`^5.65.21\` in pnpm-workspace.yaml.
- Drop unused CM6 packages (\`@replit/codemirror-emacs\`,
  \`@replit/codemirror-vscode-keymap\`, \`@replit/codemirror-vim\`,
  \`@uiw/codemirror-theme-material\`) — none of them are imported.
- Drop the matching \`peerDependencies\` entries in
  \`decap-cms-widget-code\`.

While in there, prune now-dead cypress infrastructure that targeted
the removed Slate-based markdown widget:

- Drop \`getMarkdownEditor\`, \`confirmMarkdownEditorContent\`,
  \`clearMarkdownEditorContent\`, \`confirmRawEditorContent\`,
  \`clickToolbarButton\`, \`insertEditorComponent\`, \`clickModeToggle\`,
  \`insertCodeBlock\`, and the nine toolbar \`click*Button\` commands
  from \`cypress/support/commands.ts\` and \`index.d.ts\`. None are
  referenced by any remaining spec.
- Drop the \`rehype\` / \`unist-util-visit\` / \`common-tags\` imports and
  the \`toPlainTree\` / \`removeSlateArtifacts\` helpers used only by
  those commands.
- In \`cypress/utils/steps.ts\`:
  - Replace \`cy.getMarkdownEditor()\` calls with \`cy.get('textarea')\`
    in \`populateEntry\` (body field) and \`validateListFields\`
    (description field). The dev-test config already replaced
    \`widget: 'markdown'\` with \`widget: 'text'\`.
  - Drop the \`isMarkdown\` branch in \`assertColorOn\`; both branches
    now collapse to the standard parents().next() DOM walk.

Verified: \`pnpm run build:demo\` succeeds, \`pnpm lint:js\` clean,
\`tsc -p cypress/tsconfig.json --noEmit\` clean, 931 unit tests pass.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
Three runtime issues kept the dev-test demo unable to load entries:

1. \`isSingleCollection\` in EntryListing required \`'fields' in collections\`,
   which is true for folder collections but not for file collections (which
   carry \`files\` instead). The wrong branch was taken so file-based
   collections silently rendered as the multi-collection variant against
   a single-collection shape, producing an empty <ul>.

2. \`dev-test/backends/*/index.html\` shadowed the bundle's \`window.CMS\`
   global with \`var CMS = window.DecapCms.default || window.DecapCms\`.
   The IIFE bundle has no \`default\` export and only exposes
   \`{ DecapCmsApp, h }\` as named exports, so \`CMS.registerPreviewTemplate\`
   threw on every preview registration. Switched to \`window.CMS\` /
   \`window.h\` which the bundle assigns as side effects.

3. \`dev-test/backends/*/config.yml\` still referenced the removed
   \`widget: 'markdown'\`, which fails config validation now that the
   markdown widget package is gone. Replaced all occurrences with
   \`widget: 'text'\` so the config loads.

Backend index.html / config.yml are the source of truth — \`copyBackendFiles\`
in \`cypress/utils/config.ts\` copies them over \`dev-test/index.html\` and
\`dev-test/config.yml\` at the start of each spec, so editing those direct
output files did nothing.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
…elds

Function-component widgets like StringControl and TextControl don't use
\`React.forwardRef\`, so their \`ref\` callback never fires. The Widget
wrapper used \`processInnerControlRef\` (the React ref callback) as the
*only* trigger to call the parent EditorControlPane's \`controlRef\`,
so non-forwardRef widgets never registered themselves and their
validation was silently skipped.

- Move \`controlRef\` registration into \`componentDidMount\` so it runs
  for every Widget regardless of whether the inner control accepts a
  ref. Add a \`componentWillUnmount\` to release it.
- Default \`Widget.wrappedControlValid\` to \`truthy\` (assume valid)
  when no inner ref is set, instead of throwing an opaque "are you
  sure widget is registered" error from \`validateWrappedControl\`.
- Catch the \`Promise.reject()\` that \`persistEntry\` throws on
  validation failure in \`useEditor.handlePersistEntry\` and
  \`useEntry.persist\`, so the unhandled-rejection doesn't surface as
  an "uncaught exception" inside Cypress.
- Tag \`ControlErrorsList\` with a stable \`className\` so the existing
  \`ul[class*=ControlErrorsList]\` cypress selectors match (emotion's
  production classnames are hashed).

With these together the "can validate object fields" e2e test passes;
the nested-object case is still flaky on the assertion side but the
underlying validation now runs.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
… selectors

The "validate fields nested in an object field" cypress test was failing
because Widget's shouldComponentUpdate only re-rendered on \`value\`,
\`classNameWrapper\`, or \`hasActiveStyle\` changes. When validation
populated \`fieldsErrors\` for a descendant, the container Widget (e.g.
the object widget for \`posts\`) skipped re-rendering, so its inner
ObjectControl never re-rendered, so the leaf EditorControl never saw
the new errors and the inline error UI never appeared.

The "list fields" test then failed because:
- \`ObjectControl.processControlRef\` was calling
  \`props.controlRef?.(childRef)\` to propagate sub-field widget refs up.
  Since \`controlRef\` is bound to the parent slot in EditorControlPane,
  this overwrote \`childRefs[<list-field-name>]\` with whichever
  sub-field widget mounted last, so the parent EditorControlPane
  validated a sibling-leaf widget instead of the list itself.
- ListControl's old \`processControlRef\` read \`childRef.props.validationKey\`
  off what's actually a forwardRef imperative handle (no \`.props\`),
  throwing a TypeError on mount of an authors entry.
- Several styled components emit hashed \`css-XXX\` classnames in the
  production build the cypress tests run against, but the cypress
  selectors look for \`class*=ControlErrorsList\`,
  \`class*=SortableListItem\`, \`class*=TopBarButton-button\`, and
  \`class*=NestedObjectLabel\`. Tagged each with a stable className.

Changes:
- \`Widget.shouldComponentUpdate\`: also return true when
  \`fieldsErrors\` reference changes, so container widgets propagate
  validation state down to leaf controls.
- \`ObjectControl.processControlRef\`: drop the upward propagation of
  sub-field refs.
- \`ListControl.processControlRef\`: take the item key as an argument
  (returned closure) instead of reading \`childRef.props.validationKey\`.
- \`ObjectControl\`: \`validationKey\` is no longer required.
- Stable classNames on \`ControlErrorsList\` (already done),
  \`SortableListItem\`, \`TopBarButton\`, \`NestedObjectLabel\`.
- Updated \`validateListFields\` step to scope the typed-in input/textarea
  to the new list item by visibility.

\`field_validations_spec.ts\` now passes 2/4 tests (was 0/4); the
remaining 2 are about typing into a freshly-added list item's
sub-fields — still being investigated.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
…data

ListControl's handleChangeFor mutated values that ultimately come from
the immer-frozen redux state:

- \`value[index] = newObjectValue\` mutated the captured \`inputValue\`
  array; with frozen state this either threw or, if shallow-copied
  later, propagated stale data so the controlled input never updated
  (each typed character resets the value).
- \`ov[name] = newValue\` mutated the per-item object value taken
  straight out of state — same problem.
- \`Object.assign(metadata ?? {}, newMetadata)\` mutated the metadata
  object from state, which threw "Cannot add property cities, object
  is not extensible" once the user typed into a freshly-added nested
  list item.

Replace each mutation with a shallow clone (\`[...inputValue]\`,
\`{ ...getObjectValue(index) }\`, \`{ ...metadata, ...newMetadata }\`).

This makes the last two field_validations e2e tests pass, including
the deeply-nested one that types into a 4-level list-of-list-of-objects.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
\`PostPreview\` and \`GeneralPreview\` (inline preview templates in
\`dev-test/backends/*/index.html\`) used \`entry.getIn(['data', 'title'])\`
— Immutable.js's nested getter. With Immutable.js gone, \`entry\` is a
plain JS object so \`getIn\` is undefined and the preview crashes,
breaking every e2e test that has the preview pane visible.

Replaced \`.getIn(['a','b'])\` with \`?.a?.b\` across all eight backend
index.html copies and the root dev-test/index.html. The remaining
\`.get('thumb')\` and similar Immutable APIs are not exercised by the
test_backend specs.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
…e classNames

Search-suggestion, view-filters, and view-groups e2e specs all hit the
same emotion-classname-vs-cypress-selector pattern: the production
build emits hashed \`css-XXX\` classnames, but the cypress selectors
look for the original styled-component name via \`class*=…\`.

Tagged the remaining widgets the existing specs reach for:
- \`SearchInput\` and \`SuggestionsContainer\` in CollectionSearch.
- \`SearchResultHeading\` in Collection.
- \`ListCardLink\` in EntryCard.
- \`GroupContainer\` in EntriesCollection.

This makes search_suggestion_spec, view_filters_spec, view_groups_spec
all pass, and the remaining field_validations_spec is unaffected.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
…classNames

Three issues fixed in one commit:

1. \`simple_workflow_spec_test_backend.ts\` and
   \`editorial_workflow_spec_test_backend.ts\` did not call
   \`cy.clock(0, ['Date'])\` despite asserting against
   \`/1970-01-01-…\` permalinks. Added \`beforeEach\` clock freeze.
   This unblocks all of simple_workflow (5 → 9/9) and most of
   editorial_workflow (1 → 13/18).

2. \`publishWorkflowEntry\` passed \`{ timeout }\` to \`cy.contains\`
   even when the caller didn't specify a timeout. Cypress 15 treats
   \`{ timeout: undefined }\` as if no default applied, so the
   assertion failed immediately. Pass the options object only when a
   timeout was actually given.

3. Tagged two more styled components the cypress selectors look for
   via \`class*=…\`: \`SidebarNavList\` (collection sidebar) and
   \`CardsGrid\` (entry listing). Without these, the production-build
   emotion classnames (\`css-XXX\`) don't match.

The remaining 4 editorial_workflow failures are all in the
"nested-collection items" block — they require pre-populated
\`_pages\` repoFiles in the test backend that aren't there yet.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
- entryDraft: stop bailing out of DRAFT_CHANGE_FIELD before computing
  hasChanged / fieldsMetaData on i18n changes (Save button now enables
  after editing translated fields)
- EditorInterface: wrap each SplitPane child in <Pane> with stable Pane1
  / Pane2 classNames so cypress can find them
- App: collections/:name/filter/* route — react-router v6 splat syntax
  (the old :filterTerm* form silently dropped path segments)
- EditorControl: return the meta-field validator's result so Widget's
  validation loop doesn't read .error off undefined
- dev-test/backends/test config: add subfolders: false on the pages
  nested collection so leaf entries show up in the sidebar tree

i18n_editorial_workflow now 6/6 green; editorial_workflow goes from
13/18 to 15/18.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
- StringControl: stop forcing the caret to position 0 on every render.
  We were eagerly restoring a 'last seen' selection that was 0 on mount,
  which made cypress' focus-then-type land at the start of the existing
  value (so '/new-path' got prepended to 'directory/sub-directory'
  instead of appended). Only restore selection after a user-initiated
  change.
- NestedCollection: expand the tree to filterTerm on initial mount, and
  re-apply filter-based expansion when the URL filter changes even if
  the user previously collapsed something. Without this, navigating
  back from the editor to a /filter/<path> URL left the sidebar
  collapsed and the just-saved entry invisible.

Brings editorial_workflow_spec_test_backend from 15/18 to 17/18.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
- MediaLibraryCardGrid: displayURLs is a plain object now, not an
  Immutable Map; switch the lookup from .get(id) to bracket access.
- MediaLibrary{Card,Header}, ViewStyleControl, EntryCard: tag the
  styled.* nodes the cypress media spec selects on (CardImage,
  CloseButton, ViewControls, grid CardImage) with stable classNames so
  the [class*="..."] selectors keep working under emotion's hashed
  classes.
- selectMediaFiles: only fall back to the entry draft's mediaFiles
  when the library was opened for a specific field. The global Media
  tab now always reflects the persisted store, so a leftover
  entryDraft from a still-mounted Editor doesn't leak draft uploads
  into it. Fixes "should not show draft entry image in global media
  library".

media_library_spec_test_backend now 8/8 green, with no regression
in editorial_workflow (still 17/18; the remaining failure pre-dates
this branch).

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
The legacy class-component behavior was: on every prop update with
useFilter still on, walk the tree with \`path = '/' + filterTerm\` and
expand any node whose path is a prefix. With filterTerm empty that
becomes '/' which only matches the root, so the root would auto-expand
once entries loaded.

The hooks rewrite gated this expansion on \`filterTerm\` being truthy,
so navigating back to /collections/<name> with no filter (e.g. after
saving a moved entry) left the whole tree collapsed and the just-saved
entry invisible.

Two changes:
- Initial useState now applies the same prefix-expansion when entries
  are already in the store on first render (typical after returning
  from the editor).
- The componentDidUpdate-equivalent branch drops the truthy-filterTerm
  guard so '/' still expands the root when entries arrive.

Brings editorial_workflow_spec_test_backend to 18/18.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
- Convert \`import { ... } from 'lodash/fp'\` to per-function default
  imports from 'lodash/fp/<name>.js'. Under tsx's ESM resolver,
  lodash/fp resolves as a CJS module that doesn't expose named ESM
  bindings — the proxy server crashed at startup with either
  ERR_MODULE_NOT_FOUND or "does not provide an export named X".
- populateEntry: stop using \`cy.get('textarea').first()\` for the body
  field. The proxy backend's posts collection has Description (text
  widget = textarea) listed before Body, so the .first() match was
  populating the wrong field and Body's required-validation kept
  killing the save. Use the same \`[id^="\${key}-field"]\` selector as
  every other field.
- MediaLibraryModal: align displayURLs prop type with the now-Record
  shape so the build passes.

Brings i18n_simple_workflow_spec_proxy_fs_backend (3/3),
simple_workflow_spec_proxy_fs_backend (2/2),
simple_workflow_spec_proxy_git_backend (1/2 + 1 intentional pending),
and media_library_spec_proxy_git_backend (8/8) to green.

https://claude.ai/code/session_01XY3VYqreamkxmH38z52AWg
Remove markdown editor commands and fix widget validation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.