Skip to content

Unify LocatorJS configuration with four-layer resolver (Phase 1)#213

Open
infi-pc wants to merge 2 commits into
v2from
infi-pc/banjul
Open

Unify LocatorJS configuration with four-layer resolver (Phase 1)#213
infi-pc wants to merge 2 commits into
v2from
infi-pc/banjul

Conversation

@infi-pc

@infi-pc infi-pc commented May 2, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1 of v2 config unification. Collapses fragmented config surfaces (localStorage, chrome.storage, setup() args, data-* HTML attributes, two settings UIs) into a single canonical type with a strict per-key priority stack: default < team < user-extension < user-project.

UI rewrite is deferred to Phase 2 (separate PR). This PR keeps existing Hope UI popup and bespoke-CSS popover; visual polish is intentionally WIP since v2 is unreleased.

What changes

Data model (@locator/shared)

  • Canonical LocatorOptions with split target fields (targetId references targets map; targetTemplate is raw URL, wins over id).
  • Pure resolve() returns {effective, provenance}. replacePath merges atomically (later layer fully replaces).
  • resolveTarget() handles targetTemplate > targetId-in-map > first-target fallback with a kind discriminator for "unavailable on this site" UX.

Runtime (@locator/runtime)

  • optionsStore aggregates four layers via createMemo, exposes effective(), provenance(), uiState(), allTargets(), setUserProject(), setUiState().
  • Team layer is a live module-scoped signal so late setup() calls re-drive the resolver after extension auto-init.
  • Call sites simplified: linkTemplateUrl uses resolveTarget; buildLink reads effective.projectPath (setInternalProjectPath deleted); modifiers use effective.mouseModifiers (data-attribute fallback removed).
  • welcomeScreenDismissed moved to nested uiState in user-project blob.
  • popupBridge exposes window.__LOCATOR_RUNTIME__ + responds to snapshot and site-local-write postMessage protocol; uses onCleanup for prod hygiene.

Extension

  • Content script injects window.__LOCATOR_USER_EXTENSION_OPTIONS__ on page load and on every chrome.storage change, plus postMessage broadcast (no mount-race window).
  • snapshotBridge relays popup↔page requests with requestId + 1s timeout.
  • Popup rewired on existing Hope UI: unified syncedState over chrome.storage.local["userOptions"], polls active tab snapshot, ProvenanceBadge per row, SiteLocalToggle for "set for this site only".
  • Removed: tracking/social/clickCount UI + storage keys, requestEnable round-trip, per-attribute data-* bridge, SharePage.

Cleanup, not migration

  • Legacy LOCATOR_OPTIONS localStorage key + legacy chrome.storage.local keys (target, controls, allowTracking, sharedOnSocialMedia, clickCount, enableExperimentalFeatures) removed on first read. Users reset config on v2 upgrade; v2 is unreleased so no data-loss path.

Tests

  • 30 shared unit tests: 16 resolver permutations + split-field target resolution + atomic replacePath + cleanup idempotency.
  • 11 runtime integration tests (jsdom): late setup() live update, postMessage and storage-event layer updates, user-project override semantics, window.enableLocator, atomic replacePath, uiState isolation, team-targets override, popup bridge snapshot + site-local-write protocol.
  • vitest.config.ts in runtime resolves solid-js via browser conditions so reactivity works in jsdom.
  • Extension typecheck clean.

Total: 85 tests pass (30 shared + 55 runtime).

Not in this PR

  • Phase 2 — separate PR: Kobalte + solid-ui rewrite of popover and popup, Tailwind into Shadow DOM, packages/ui repurposed.
  • Playwright e2e for new flows: blocked by pre-existing webpack config on mastersolid-icons ships ESM+JSX in node_modules and the current babel-loader rule excludes node_modules. Confirmed broken on a clean master checkout. Out of Phase 1 scope.

Test plan

  • pnpm --filter '@locator/shared' test → 30 pass
  • pnpm --filter '@locator/runtime' test → 55 pass
  • pnpm --filter '@locator/runtime' exec tsc --noEmit clean
  • pnpm --filter locatorjs-extension exec tsc --noEmit clean
  • Manual: library-only popover edits persist across reload
  • Manual: extension popup edits persist across reload + next navigation
  • Manual: combined library + extension scenario, provenance badges show correct layer
  • Manual: late setup({projectPath:"/foo"}) after extension auto-boot updates link generation

🤖 Generated with Claude Code

Collapse fragmented config surfaces (localStorage, chrome.storage, setup() args,
data-* HTML attributes, two settings UIs) into a single canonical type with a
strict per-key priority stack: default < team < user-extension < user-project.

Data model
- Canonical LocatorOptions in @locator/shared with split target fields
  (targetId references targets map; targetTemplate is raw URL, wins over id).
- Pure resolve() returns {effective, provenance} and treats replacePath as an
  atomic value (later layer fully replaces).
- resolveTarget() handles targetTemplate > targetId-in-map > first-target
  fallback with kind discriminator for "unavailable on this site" UX.

Runtime
- optionsStore aggregates four layers via createMemo, exposes effective(),
  provenance(), uiState(), allTargets(), setUserProject(), setUiState().
- Team layer is a live module-scoped signal so late setup() calls re-drive the
  resolver after extension auto-init.
- Call sites simplified: linkTemplateUrl uses resolveTarget; buildLink reads
  effective.projectPath (setInternalProjectPath deleted); modifiers use
  effective.mouseModifiers (data-attribute fallback removed).
- welcomeScreenDismissed moved to nested uiState in user-project blob.
- popupBridge exposes window.__LOCATOR_RUNTIME__ + responds to snapshot and
  site-local-write postMessage protocol; uses onCleanup for prod hygiene.

Extension
- Content script injects window.__LOCATOR_USER_EXTENSION_OPTIONS__ on page load
  and on every chrome.storage change, plus postMessage broadcast (no mount race).
- snapshotBridge relays popup<->page requests with requestId + 1s timeout.
- Popup rewired on existing Hope UI (visual rewrite deferred to Phase 2):
  unified syncedState over chrome.storage.local["userOptions"], polls active tab
  snapshot, ProvenanceBadge per row, SiteLocalToggle for "set for this site only".
- Removed: tracking/social/clickCount UI + storage keys, requestEnable round-trip,
  per-attribute data-* bridge, SharePage.

Cleanup, not migration
- Legacy LOCATOR_OPTIONS localStorage key + legacy chrome.storage.local keys
  (target, controls, allowTracking, sharedOnSocialMedia, clickCount,
  enableExperimentalFeatures) removed on first read. Users reset config on v2;
  v2 is unreleased so no data-loss path.

Tests
- 30 shared tests: 16 resolver permutations + split-field target resolution +
  atomic replacePath + cleanup idempotency.
- 11 runtime integration tests: late setup() live update, postMessage and
  storage-event layer updates, user-project override semantics,
  window.enableLocator, atomic replacePath, uiState isolation, team-targets
  override, popup bridge snapshot + site-local-write protocol.
- vitest.config.ts in runtime resolves solid-js via browser conditions so
  reactivity works in jsdom. Extension typecheck clean.

Phase 2 (separate PR): Kobalte + solid-ui rewrite of popover and popup,
Tailwind into Shadow DOM, packages/ui repurposed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel

vercel Bot commented May 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
locatorjs Ready Ready Preview, Comment May 2, 2026 3:47pm

solid-icons 1.2.0 ships a packaging bug: sub-packages (e.g. solid-icons/bs)
do `import { IconTemplate } from '../lib/index.jsx'` via relative path, which
bypasses package.json `exports` and forces webpack to resolve the raw JSX file.
With babel-loader excluded from node_modules, the build fails to parse JSX.
Vite users avoid this because vite-plugin-solid runs babel across all of
node_modules; webpack/Rsbuild/parcel users hit it.

Switch the two usages to lucide-solid:
- `HiSolidCog` -> `Settings` from lucide-solid
- `BsGithub` -> inline SVG (lucide deliberately removed brand icons for
  trademark reasons), matching the existing inline-SVG pattern in Home.tsx

lucide-solid ships a self-contained ESM bundle with zero relative-jsx imports,
so it works on webpack without any loader hacks. This unblocks the extension
build and keeps Phase 2 (Kobalte) free of node_modules babel-loader workarounds.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant