Skip to content

Commit e1e8a44

Browse files
tkirda-bisonclaude
andcommitted
refactor!: rewrite source in typescript, ship esm + umd + types
Phase 2 of the modernization plan: src/jquery.autocomplete.js (one big ES5 UMD file) replaced by ~8 TypeScript modules under src/, compiled to a UMD bundle, an ESM bundle, a minified UMD bundle, and tsc-emitted .d.ts files in dist/. All 31 specs pass unmodified — that is the behavior contract. What changed - src/ now contains: index.ts (ESM entry), umd-body.ts (UMD entry), jquery-plugin.ts (installAutocomplete), Autocomplete.ts (the class), defaults.ts, format.ts, utils.ts, jquery-ref.ts (live $ binding), types.ts. Old src/jquery.autocomplete.js removed. - typings/jquery.autocomplete.d.ts (hand-maintained, drifted) removed; types are generated from source by tsc into dist/. - scripts/build.mjs rewritten: uses esbuild for both bundles, wraps the IIFE output in a hand-written UMD shim that matches the pre-2.0 detection format byte-for-byte. terser dropped (esbuild handles minify). - tsconfig.json added: target ES2020, strict, emit declarations only. - package.json: version 2.0.0, jquery peer >=3.0, exports map with types/import/require conditions, new typecheck script. - .github/workflows/ci.yml: typecheck step added between format:check and test. - vitest.config.js: pool switched to forks + isolate:false. The threads pool starved the worker handshake once src moved to TS (transform overhead pushed startup past the 60s timeout). isolate:false keeps the shared-module-state model the original Jasmine runner used. - test/setup.js: calls installAutocomplete(jQuery) directly instead of loading the dist UMD wrapper. Same end-state, simpler chain. - dist/license.txt removed; the canonical license.txt lives at repo root and the build no longer mirrors it into dist (avoids drift). BREAKING CHANGE: drops IE-era browsers (source targets ES2020) and drops jquery <3.0 support (peer bumped from >=1.9 to >=3.0). Public surface unchanged — every option, callback, method, CSS class, and suggestion shape behaves identically. The 31-spec test suite is the enforcement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4d5f1a9 commit e1e8a44

35 files changed

Lines changed: 3612 additions & 2749 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ jobs:
2020

2121
- run: npm run lint
2222
- run: npm run format:check
23+
- run: npm run typecheck
2324
- run: npm test
2425
- run: npm run build

CLAUDE.md

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,75 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project shape
66

7-
This is a single-file jQuery plugin (Ajax Autocomplete). The entire implementation lives in `src/jquery.autocomplete.js` (~1k lines). Everything else — `dist/`, `typings/`, `spec/`, `scripts/`, `index.htm` — is produced from, tests, types, or demos that one source file. Edits almost always belong in `src/jquery.autocomplete.js`; never edit `dist/*` directly (the build overwrites it).
7+
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.
8+
9+
`src/` layout:
10+
11+
| File | Role |
12+
|---|---|
13+
| `src/index.ts` | ESM entry point. Imports jquery, calls `installAutocomplete(jQuery)`, re-exports `Autocomplete` and types. |
14+
| `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. |
15+
| `src/jquery-plugin.ts` | `installAutocomplete($)` — registers `$.Autocomplete`, `$.fn.devbridgeAutocomplete`, and conditionally `$.fn.autocomplete` (jQuery UI guard). |
16+
| `src/Autocomplete.ts` | The plugin class. |
17+
| `src/defaults.ts` | The `Autocomplete.defaults` options object. |
18+
| `src/format.ts` | Default `formatResult`, `formatGroup`, `lookupFilter`, `transformResult`. |
19+
| `src/utils.ts` | `escapeRegExChars`, `createNode`, `keys` constants. |
20+
| `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. |
21+
| `src/types.ts` | Public types (`AutocompleteOptions`, `Suggestion`, callback signatures). |
822

923
## Commands
1024

1125
- `npm test` — Vitest run (headless, jsdom). Single-shot, exits nonzero on failure.
12-
- `npm run test:watch` — Vitest watch mode for local TDD.
13-
- `npm run lint` — ESLint (flat config in `eslint.config.mjs`) over `src/`, `test/`, and `scripts/build.mjs`.
14-
- `npm run format` — Prettier rewrite of `src/`, `test/`, and `scripts/build.mjs` (100-col, 4-space, ES5 trailing commas — config in `package.json`). Demo files under `scripts/` (`countries.js`, `demo.js`) are intentionally excluded.
15-
- `npm run format:check` — Prettier check-only, same scope. Used by CI; fails if anything would be rewritten.
16-
- `npm run build` — runs `scripts/build.mjs` (Node ESM, no bundler): copies `src/jquery.autocomplete.js` to `dist/jquery.autocomplete.js` while substituting the `%version%` placeholder with `package.json` `version`, minifies to `dist/jquery.autocomplete.min.js` via terser with a fresh banner, and syncs `devbridge-autocomplete.jquery.json` version. Run this before release/commit when source changes.
26+
- `npm run test:watch` — Vitest watch mode.
27+
- `npm run lint` — ESLint over `test/` and `scripts/build.mjs`. **TS source is not linted by ESLint**`tsc --noEmit` covers it via the `typecheck` script.
28+
- `npm run typecheck``tsc --noEmit`. Strict mode; runs on `src/`.
29+
- `npm run format` — Prettier rewrite of `src/`, `test/`, and `scripts/build.mjs` (100-col, 4-space, ES5 trailing commas). Demo files under `scripts/` (`countries.js`, `demo.js`) are intentionally excluded.
30+
- `npm run format:check` — Prettier check-only, same scope. CI gate.
31+
- `npm run build` — runs `scripts/build.mjs` (Node ESM): esbuild emits `dist/jquery.autocomplete.esm.js` (ESM) and `dist/jquery.autocomplete.js` / `.min.js` (UMD, hand-wrapped); `tsc --declaration` emits the `.d.ts` files; the version field in `devbridge-autocomplete.jquery.json` is synced from `package.json`.
1732

1833
## CI
1934

20-
`.github/workflows/ci.yml` runs on every push to `master` and every pull request: `npm ci`, then `lint`, `format:check`, `test`, `build` — in that order, all required. Node 20 LTS, Ubuntu, single job. The `engines.node` field in `package.json` mirrors the runner version.
35+
`.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.
2136

2237
## Tests
2338

24-
Vitest + jsdom, headless. Specs live in `test/autocomplete.test.js`. `test/setup.js` attaches a single jQuery instance to the jsdom `window` and `globalThis`, registers `jquery-mockjax` against it, silences mockjax's per-request console logging, then loads `src/jquery.autocomplete.js`. The plugin's UMD wrapper picks up `globalThis.jQuery` and registers `$.Autocomplete` plus `$.fn.autocomplete`/`$.fn.devbridgeAutocomplete` against that same instance.
39+
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.
2540

26-
The Vitest pool is pinned to `threads` in `vitest.config.js` — the default `forks` pool times out the worker handshake on Windows when the setup file does heavy synchronous work (jQuery + mockjax + UMD plugin load). Don't switch pools without re-verifying.
41+
`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.
2742

28-
To run a single test, use `npx vitest run -t "test name substring"` or temporarily change `describe`/`it` to `describe.only`/`it.only`.
43+
To run a single test: `npx vitest run -t "test name substring"` or temporarily `describe.only` / `it.only`.
2944

30-
The demo page `index.htm` is the manual test surface (Ajax lookup, local lookup with grouping, custom container, dynamic width). It loads jQuery + mockjax from CDN; just open in a browser.
45+
The demo page `index.htm` is the manual test surface (Ajax lookup, local lookup with grouping, custom container, dynamic width). It loads jQuery + mockjax from CDN; open in a browser.
46+
47+
## Build internals
48+
49+
`scripts/build.mjs` does three things in order:
50+
51+
1. **ESM bundle** (`dist/jquery.autocomplete.esm.js`) — esbuild bundles `src/index.ts` with `external: ['jquery']`. Consumers `import 'devbridge-autocomplete'` and the plugin self-registers.
52+
2. **UMD bundles** (`dist/jquery.autocomplete.js` and `.min.js`) — esbuild bundles `src/umd-body.ts` as IIFE (no `external`); 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.
53+
3. **Types** (`dist/*.d.ts`) — `tsc --declaration --emitDeclarationOnly`. One `.d.ts` per source file; `package.json` `types` points at `dist/index.d.ts`.
54+
55+
The minified UMD is ~13 KB; the unminified is ~26 KB.
3156

3257
## Release/version flow
3358

3459
1. Bump `version` in `package.json`.
35-
2. `npm run build`this propagates the new version into `dist/jquery.autocomplete.js` (via `%version%` placeholder) and `devbridge-autocomplete.jquery.json`. The placeholder only exists in `src/`; do not hand-edit version strings in `dist/`.
60+
2. `npm run build` — propagates the new version into the banner of each `dist/` JS file (via the build script) and syncs `devbridge-autocomplete.jquery.json`.
3661

37-
## Architecture notes that aren't obvious from a single file
62+
## Architecture notes that aren't obvious from a glance
3863

39-
- **UMD wrapper** at the top of `src/jquery.autocomplete.js` registers under AMD, CommonJS, or browser global — keep all three branches working when touching the wrapper.
40-
- **Dual plugin name**: registers `$.fn.devbridgeAutocomplete` unconditionally, and aliases `$.fn.autocomplete` only if it is not already taken (jQuery UI defines one). Tests and the README rely on this fallback behavior — don't remove the guard.
41-
- **Defaults live on `Autocomplete.defaults`** and are merged per-instance via `$.extend(true, {}, defaults, options)`. New options must be added to the defaults object so deep-merge picks them up, and mirrored in `typings/jquery.autocomplete.d.ts` and the option tables in `readme.md`.
42-
- **Response normalization**: server responses pass through `transformResult` (default JSON.parse for `dataType: 'text'`). Local `lookup` may be an array or a `function(query, done)` callback; both paths converge on the same `{ value, data }` suggestion shape used everywhere downstream (`formatResult`, `onSelect`, grouping).
43-
- **Caching + bad-query guard**: `cachedResponse` keys by query string, and `preventBadQueries` records prefixes that returned no results so future queries with the same prefix short-circuit. `clearCache` / `clear` reset these — when adding new request paths, decide whether they should populate or honor these caches.
64+
- **Dual plugin name**: `$.fn.devbridgeAutocomplete` is always registered. `$.fn.autocomplete` is 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.
65+
- **Live `$` binding**: `src/jquery-ref.ts` exports a `let $` that `installAutocomplete` mutates at install time via `setJQuery`. Every other module (Autocomplete, format, etc.) imports `{ $ }` and sees the live value. This avoids passing `$` through every constructor / function signature.
66+
- **Defaults**: `Autocomplete.defaults` (in `src/defaults.ts`) is merged per-instance via `$.extend(true, {}, defaults, options)`. New options must be added there AND in the `AutocompleteOptions` interface in `src/types.ts`; the option tables in `readme.md` also need updating.
67+
- **Defaults uses `() => {}` not `$.noop`** as the no-op callback. That avoids load-time `$` access (the file is imported before `installAutocomplete` runs). Specs don't assert on identity, only behavior.
68+
- **Response normalization**: server responses pass through `transformResult` (default JSON.parse for `dataType: 'text'`). Local `lookup` may be an array or a `function(query, done)` callback; both paths converge on the same `{ value, data }` suggestion shape used everywhere downstream.
69+
- **Caching + bad-query guard**: `cachedResponse` keys by query string; `preventBadQueries` records prefixes that returned no results so future queries with the same prefix short-circuit. `clearCache` / `clear` reset these — when adding new request paths, decide whether they should populate or honor these caches.
4470

4571
## Conventions
4672

47-
- Source style is the existing one: ES5 inside the IIFE (no `let`/`const`/arrow funcs in `src/jquery.autocomplete.js`), `"use strict"`, `var that = this` pattern. ESLint config targets `ecmaVersion: 2022` but the file is intentionally written to run in old browsers — match the existing style rather than modernizing.
48-
- Prettier owns formatting; run `npm run format` before committing source changes.
73+
- TypeScript strict mode is on. Don't introduce `any` in public types. Internal `as unknown as X` casts are OK at jQuery boundaries where typings are imprecise.
74+
- The IE-only `document.selection` branch in `isCursorAtEnd` is kept for behavioral parity with the pre-2.0 source. Modern browsers fall through it.
75+
- Prettier owns formatting. Run `npm run format` before committing source changes.
4976

5077
## Commit messages
5178

@@ -56,4 +83,4 @@ Use [Conventional Commits](https://www.conventionalcommits.org/): `<type>(<optio
5683
- Breaking changes: append `!` after the type/scope (`feat!: ...`) and/or add a `BREAKING CHANGE:` footer.
5784
- Scope, when useful, names the area touched: `build`, `test`, `deps`, `autocomplete`.
5885

59-
Examples: `test: port specs to vitest + jsdom`, `build: replace grunt with node script`, `chore(deps): bump prettier to 3.6.2`, `feat!: drop ie11 support`.
86+
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`.

devbridge-autocomplete.jquery.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"ajax",
77
"autocomplete"
88
],
9-
"version": "1.5.0",
9+
"version": "2.0.0",
1010
"author": {
1111
"name": "Tomas Kirda",
1212
"url": "https://github.com/tkirda"
@@ -25,4 +25,4 @@
2525
"dependencies": {
2626
"jquery": ">=1.7"
2727
}
28-
}
28+
}

dist/Autocomplete.d.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { AutocompleteOptions, AutocompleteResponse, LookupArray, Orientation, Suggestion } from "./types";
2+
interface Classes {
3+
selected: string;
4+
suggestion: string;
5+
}
6+
export declare class Autocomplete {
7+
static defaults: AutocompleteOptions;
8+
static utils: {
9+
escapeRegExChars(value: string): string;
10+
createNode(containerClass: string): HTMLDivElement;
11+
};
12+
element: HTMLInputElement;
13+
el: JQuery;
14+
suggestions: Suggestion[];
15+
badQueries: string[];
16+
selectedIndex: number;
17+
currentValue: string;
18+
timeoutId: number | null;
19+
cachedResponse: Record<string, AutocompleteResponse>;
20+
onChangeTimeout: ReturnType<typeof setTimeout> | null;
21+
onChange: (() => void) | null;
22+
isLocal: boolean;
23+
suggestionsContainer: HTMLDivElement;
24+
noSuggestionsContainer: HTMLElement;
25+
options: AutocompleteOptions;
26+
classes: Classes;
27+
hint: Suggestion | null;
28+
hintValue: string;
29+
selection: Suggestion | null;
30+
disabled?: boolean;
31+
visible?: boolean;
32+
ignoreValueChange?: boolean;
33+
blurTimeoutId?: ReturnType<typeof setTimeout>;
34+
fixPositionCapture?: () => void;
35+
currentRequest: JQuery.jqXHR | null;
36+
constructor(el: HTMLInputElement, options?: AutocompleteOptions);
37+
initialize(): void;
38+
onFocus(): void;
39+
onBlur(): void;
40+
abortAjax(): void;
41+
setOptions(suppliedOptions?: AutocompleteOptions): void;
42+
clearCache(): void;
43+
clear(): void;
44+
disable(): void;
45+
enable(): void;
46+
fixPosition(): void;
47+
isCursorAtEnd(): boolean;
48+
onKeyPress(e: JQuery.KeyDownEvent): void;
49+
onKeyUp(e: JQuery.TriggeredEvent): void;
50+
onValueChange(): void;
51+
isExactMatch(query: string): boolean;
52+
getQuery(value: string): string;
53+
getSuggestionsLocal(query: string): AutocompleteResponse;
54+
getSuggestions(q: string): void;
55+
isBadQuery(q: string): boolean;
56+
hide(): void;
57+
suggest(): void;
58+
noSuggestions(): void;
59+
adjustContainerWidth(): void;
60+
findBestHint(): void;
61+
onHint(suggestion: Suggestion | null): void;
62+
verifySuggestionsFormat(suggestions: LookupArray): Suggestion[];
63+
validateOrientation(orientation: Orientation | undefined, fallback: Orientation): Orientation;
64+
processResponse(result: AutocompleteResponse, originalQuery: string, cacheKey: string): void;
65+
activate(index: number): HTMLElement | null;
66+
selectHint(): void;
67+
select(i: number): void;
68+
moveUp(): void;
69+
moveDown(): void;
70+
adjustScroll(index: number): void;
71+
onSelect(index: number): void;
72+
getValue(value: string): string;
73+
dispose(): void;
74+
}
75+
export {};

dist/defaults.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import type { AutocompleteOptions } from "./types";
2+
export declare const defaults: AutocompleteOptions;

dist/format.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { AutocompleteResponse, Suggestion } from "./types";
2+
export declare function lookupFilter(suggestion: Suggestion, _originalQuery: string, queryLowerCase: string): boolean;
3+
export declare function transformResult(response: string | AutocompleteResponse): AutocompleteResponse;
4+
export declare function formatResult(suggestion: Suggestion, currentValue: string): string;
5+
export declare function formatGroup(_suggestion: Suggestion, category: string): string;

dist/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Autocomplete } from "./Autocomplete";
2+
export type * from "./types";

dist/jquery-plugin.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Autocomplete } from "./Autocomplete";
2+
import type { AutocompleteOptions } from "./types";
3+
declare global {
4+
interface JQueryStatic {
5+
Autocomplete: typeof Autocomplete;
6+
}
7+
interface JQuery {
8+
autocomplete(options?: AutocompleteOptions | string, args?: unknown): JQuery | Autocomplete;
9+
devbridgeAutocomplete(options?: AutocompleteOptions | string, args?: unknown): JQuery | Autocomplete;
10+
}
11+
}
12+
export declare function installAutocomplete($: JQueryStatic): void;

dist/jquery-ref.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export declare let $: JQueryStatic;
2+
export declare function setJQuery(jq: JQueryStatic): void;

0 commit comments

Comments
 (0)