This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
TypeScript source under src/ (~700 lines split into ~8 modules) compiles to a UMD bundle + an ESM bundle + a minified UMD + bundled .d.ts types in dist/. Edits belong in src/*.ts; never touch dist/* (the build overwrites it). The plugin is jQuery-only — jQuery >=3.0 is a peer dependency.
src/ layout:
| File | Role |
|---|---|
src/index.ts |
ESM entry point. Imports jquery, calls installAutocomplete(jQuery), re-exports Autocomplete and types. |
src/umd-body.ts |
UMD entry point. References $ as a free variable that the hand-written UMD wrapper (in scripts/build.mjs) provides as a factory parameter. |
src/jquery-plugin.ts |
installAutocomplete($) — registers $.Autocomplete, $.fn.devbridgeAutocomplete, and conditionally $.fn.autocomplete (jQuery UI guard). |
src/Autocomplete.ts |
The plugin class. |
src/defaults.ts |
The Autocomplete.defaults options object. |
src/format.ts |
Default formatResult, formatGroup, lookupFilter, transformResult. |
src/utils.ts |
escapeRegExChars, createNode, keys constants. |
src/jquery-ref.ts |
export let $: JQueryStatic set at install time via setJQuery. Live ES-module binding — every importer sees the value once installAutocomplete has run. |
src/types.ts |
Public types (AutocompleteOptions, ResolvedOptions, Suggestion, callback signatures). |
npm test— Vitest run (headless, jsdom). Single-shot, exits nonzero on failure.npm run test:watch— Vitest watch mode.npm run lint— ESLint overtest/andscripts/build.mjs. TS source is not linted by ESLint —tsc --noEmitcovers it via thetypecheckscript.npm run typecheck—tsc --noEmit. Strict mode; runs onsrc/.npm run format— Prettier rewrite ofsrc/,test/, andscripts/build.mjs(100-col, 4-space, ES5 trailing commas). Demo files underdocs/are intentionally excluded.npm run format:check— Prettier check-only, same scope. CI gate.npm run build— runsscripts/build.mjs(Node ESM): esbuild emitsdist/jquery.autocomplete.esm.js(ESM) anddist/jquery.autocomplete.js/.min.js(UMD, hand-wrapped);tsc --declarationemits the.d.tsfiles; the version field indevbridge-autocomplete.jquery.jsonis synced frompackage.json.
.github/workflows/ci.yml runs on every push to master and every pull request: npm ci, then lint, format:check, typecheck, test, build — in that order, all required. Node 20 LTS, Ubuntu, single job. The engines.node field in package.json mirrors the runner version.
Vitest + jsdom, headless. Specs live in test/autocomplete.test.js. test/setup.js attaches a single jQuery instance to globalThis / the jsdom window, registers jquery-mockjax, silences mockjax's per-request console logging, then calls installAutocomplete(jQuery) directly (bypassing the UMD wrapper). All test code shares one jQuery instance, one DOM, one set of plugin registrations.
vitest.config.js pins pool: "forks" with isolate: false. Don't change either. threads pool starved the worker handshake once we moved to TS source (esbuild transform overhead pushed startup past the 60s timeout). isolate: false keeps every spec in one process — same shared-module-state model the original Jasmine runner used, so describe blocks that mutate global jQuery state stay consistent.
To run a single test: npx vitest run -t "test name substring" or temporarily describe.only / it.only.
The demo page docs/index.htm is the manual test surface (Ajax lookup, local lookup with grouping, custom container, dynamic width) and the live demo published at https://devbridge.github.io/jQuery-Autocomplete/ via GitHub Pages (configured to serve from master/docs). It loads jQuery, mockjax, and the plugin itself from CDN (cdn.jsdelivr.net/npm/devbridge-autocomplete@2/...); open in a browser.
scripts/build.mjs does three things in order:
- ESM bundle (
dist/jquery.autocomplete.esm.js) — esbuild bundlessrc/index.tswithexternal: ['jquery']. Consumersimport 'devbridge-autocomplete'and the plugin self-registers. - UMD bundles (
dist/jquery.autocomplete.jsand.min.js) — esbuild bundlessrc/umd-body.tsas IIFE (noexternal); the result is wrapped by a hand-written UMD detection shim (AMD / CommonJS / browser-global), with$flowing in as the factory parameter. The shim format intentionally matches the JS source that shipped before 2.0.0 so consumers don't see a contract change. - Types (
dist/*.d.ts) —tsc --declaration --emitDeclarationOnly. One.d.tsper source file;package.jsontypespoints atdist/index.d.ts.
The minified UMD is ~13 KB; the unminified is ~26 KB.
- Bump
versioninpackage.json. npm run build— propagates the new version into the banner of eachdist/JS file (via the build script) and syncsdevbridge-autocomplete.jquery.json.
- Dual plugin name:
$.fn.devbridgeAutocompleteis always registered.$.fn.autocompleteis only aliased to it if not already taken (jQuery UI defines its own). Tests and the README rely on this fallback — don't remove the guard. - Live
$binding:src/jquery-ref.tsexports alet $thatinstallAutocompletemutates at install time viasetJQuery. Every other module (Autocomplete, format, etc.) imports{ $ }and sees the live value. This avoids passing$through every constructor / function signature. - Two options types — pick the right one.
AutocompleteOptionsis what consumers pass (everything optional).ResolvedOptionsis what the constructor produces after deep-merging withAutocomplete.defaults— the ~29 defaulted fields are required, the ~12 truly optional ones stay optional.this.optionsand the staticdefaultsare typedResolvedOptions; method bodies read them directly withoutas number/as stringcasts. When adding a new option: put it inDefaultedOptions(andsrc/defaults.ts) if it has a default, otherwise inOptionalOptionsMixin. Also add it to the option tables inreadme.md. - Defaults uses
() => {}not$.noopas the no-op callback. That avoids load-time$access (the file is imported beforeinstallAutocompleteruns). Specs don't assert on identity, only behavior. - Cached jQuery wrappers:
this.$containerandthis.$noSuggestionsContainerare set once ininitialize()and used throughout. Don't re-wrap the underlying DOM nodes with$(this.suggestionsContainer)— the hot paths (suggest,fixPosition,adjustScroll,hide,activate) were deliberately refactored away from that. - Response normalization: server responses pass through
transformResult(default JSON.parse fordataType: 'text'). Locallookupmay be an array or afunction(query, done)callback; both paths converge on the same{ value, data }suggestion shape used everywhere downstream. - Caching + bad-query guard:
cachedResponsekeys by query string;preventBadQueriesrecords prefixes that returned no results so future queries with the same prefix short-circuit.clearCache/clearreset these — when adding new request paths, decide whether they should populate or honor these caches. findBestHintis first-match-wins. It usesArray.prototype.findbecause the original$.eachcallback stopped at the first prefix match by returningfalse. Don't "improve" it to aforloop that keeps scanning — that's a different algorithm (last-match wins) and breaks the hint behavior on adjacent matches.
- TypeScript strict mode is on. Don't introduce
anyin public types. Internalas unknown as Xcasts are OK at jQuery boundaries where typings are imprecise. - The
Autocompleteclass explicitly marks every memberprivateunless it's part of the documented public API inreadme.md(setOptions,clear,clearCache,disable,enable,hide,select,dispose, plus theoptionsfield and the staticdefaults/utils). When adding a new member, default toprivate; only drop the keyword when you also add a row to the README's instance-method table. JS callers can still reach private members at runtime — they're a TypeScript-surface contract, not a runtime fence — but the contract is what consumers will rely on. - Methods use
thisdirectly. The one exception isinitialize(), which aliasesconst self = thisfor the jQuery delegation handlers (function (this: HTMLElement)) that need both$(this)for the matched element AND access to the Autocomplete instance. Don't add newthat = thisaliases elsewhere — use arrow functions for callbacks instead. - Prefer native array methods (
.map,.filter,.find,.some,.indexOf) over jQuery's$.each/$.grep/$.map/$.inArrayin new code; the JS-source equivalents have already been swapped where semantics match. - Prettier owns formatting. Run
npm run formatbefore committing source changes.
Use Conventional Commits: <type>(<optional scope>): <description>.
- Types:
feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert. - Description is imperative, lowercase, no trailing period (e.g.
fix: handle null suggestion data). - Breaking changes: append
!after the type/scope (feat!: ...) and/or add aBREAKING CHANGE:footer. - Scope, when useful, names the area touched:
build,test,deps,autocomplete.
Examples: test: port specs to vitest + jsdom, build: replace grunt with node script, chore(deps): bump prettier to 3.6.2, refactor!: rewrite source in typescript.
Never push without explicit confirmation. Make the commit, show the diff if non-trivial, and wait for the user to say "push" (or equivalent) before git push. This applies to every branch including master, every push including tag pushes that fire the release workflow, and every working tree including this one and any auxiliary clones (e.g. GHSA private forks). Bundling commit + push in one shell invocation counts as pushing without confirmation — split them.