1
0
Fork 0

feat(js): Split JS bundles for search only (#2920)

This commit is contained in:
Paul Jankowski 2026-07-16 11:04:20 -04:00 committed by GitHub
parent 629fce7902
commit e0d21a6d4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 237 additions and 133 deletions

View file

@ -10,6 +10,10 @@
},
{
"path": "packages/docsearch-js/dist/umd/index.js",
"maxSize": "180 kB"
},
{
"path": "packages/docsearch-js/dist/umd/docsearch.js",
"maxSize": "100 kB"
},
{

View file

@ -1,5 +1,3 @@
import type { AutocompleteState } from '@algolia/autocomplete-core';
import type { InitialAskAiMessage } from '@docsearch/core';
import docsearch, { type DocSearchInstance, type TemplateHelpers } from '@docsearch/js';
import sidepanel, { type SidepanelInstance } from '@docsearch/sidepanel-js';
@ -72,7 +70,10 @@ docsearchInstance = docsearch({
indexName: 'docsearch',
appId: 'PMZUYBQDAK',
apiKey: '24b09689d5b4223813d9b8e48563c8f6',
interceptAskAiEvent: (initialMessage: InitialAskAiMessage) => {
askAi: {
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
},
interceptAskAiEvent: (initialMessage) => {
docsearchInstance?.close();
sidepanelInstance.open(initialMessage);
return true;
@ -90,7 +91,7 @@ docsearchInstance = docsearch({
// eslint-disable-next-line no-console
console.log('[demo-js] docsearch onClose()');
},
resultsFooterComponent: ({ state }: { state: AutocompleteState<any> }, helpers?: TemplateHelpers) => {
resultsFooterComponent: ({ state }, helpers?: TemplateHelpers) => {
const { html } = helpers || {};
if (!html) return null;

View file

@ -5,7 +5,7 @@ JavaScript package for [DocSearch](http://docsearch.algolia.com/), the best sear
## Installation
```sh
npm install @docsearch/js@4
npm install @docsearch/js@5
```
## Get started
@ -13,7 +13,7 @@ npm install @docsearch/js@4
If you dont want to use a package manager, you can use a standalone endpoint:
```html
<script src="https://cdn.jsdelivr.net/npm/@docsearch/js@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@docsearch/js@5"></script>
```
To get started, you need a [`container`](https://docsearch.algolia.com/docs/api#container) for your DocSearch component to go in. If you dont have one already, you can insert one into your markup:
@ -39,6 +39,41 @@ docsearch({
});
```
The default entry includes keyword search and Ask AI. Configure `askAi` or call `openAskAi()` on the returned instance when using Ask AI.
## Keyword-only entry
Use the `docsearch` subpath when your integration only needs keyword search. This entry excludes Ask AI code.
```js app.js
import docsearch from '@docsearch/js/docsearch';
import '@docsearch/css';
docsearch({
container: '#docsearch',
appId: 'YOUR_APP_ID',
indexName: 'YOUR_INDEX_NAME',
apiKey: 'YOUR_SEARCH_API_KEY',
});
```
For a standalone keyword-only script, load the explicit UMD file:
```html
<script src="https://cdn.jsdelivr.net/npm/@docsearch/js@5/dist/umd/docsearch.js"></script>
<script>
window.docsearch({
container: '#docsearch',
appId: 'YOUR_APP_ID',
indexName: 'YOUR_INDEX_NAME',
apiKey: 'YOUR_SEARCH_API_KEY',
});
</script>
```
Both UMD entries expose callable `window.docsearch`. Load only one of them: the package default loads the AI-capable `dist/umd/index.js`, while `dist/umd/docsearch.js` is keyword-only.
## Documentation
[Read documentation →](https://docsearch.algolia.com/docs/docsearch-v3)

View file

@ -17,6 +17,10 @@
"files": [
"dist/"
],
"exports": {
".": "./dist/esm/index.js",
"./docsearch": "./dist/esm/docsearch.js"
},
"source": "src/index.ts",
"types": "dist/esm/index.d.ts",
"module": "dist/esm/index.js",

View file

@ -0,0 +1,122 @@
import type { DocSearchRef, InitialAskAiMessage } from '@docsearch/core';
import htm from 'htm';
import type { ComponentType, JSX, Attributes } from 'preact';
import { createElement, createRef, isValidElement, render, unmountComponentAtNode } from 'preact/compat';
export interface DocSearchInstance {
readonly isReady: boolean;
readonly isOpen: boolean;
open(): void;
close(): void;
openAskAi(initialMessage?: InitialAskAiMessage): void;
destroy(): void;
}
export interface DocSearchCallbacks {
onReady?: () => void;
onOpen?: () => void;
onClose?: () => void;
interceptAskAiEvent?: (initialMessage: InitialAskAiMessage) => boolean | void;
}
export type DocSearchProps<TProps> = DocSearchCallbacks &
Omit<TProps, 'onSidepanelClose' | 'onSidepanelOpen'> & {
container: HTMLElement | string;
environment?: typeof window;
};
function getHTMLElement(value: HTMLElement | string, env: typeof window | undefined): HTMLElement {
if (typeof value !== 'string') return value;
if (!env) throw new Error('Cannot resolve a selector without a browser environment.');
const element = env.document.querySelector<HTMLElement>(value);
if (!element) throw new Error(`Container selector did not match any element: "${value}"`);
return element;
}
const html = htm.bind(createElement) as unknown as (strings: TemplateStringsArray, ...values: unknown[]) => JSX.Element;
export type TemplateHelpers = Record<string, unknown> & { html: typeof html };
function createTemplateFunction<P extends Record<string, unknown>, R = JSX.Element | string | (() => JSX.Element)>(
original: ((props: P, helpers?: TemplateHelpers) => R) | undefined,
): ((props: P) => JSX.Element) | undefined {
if (!original) return undefined;
return (props: P) => {
const output = original(props, { html });
if (isValidElement(output)) return output;
if (typeof output === 'function') return output(props);
if (typeof output === 'string') return createElement('span', null, output);
return output as JSX.Element;
};
}
interface ComponentProps {
hitComponent?: (props: Record<string, unknown>, helpers?: TemplateHelpers) => JSX.Element;
resultsFooterComponent?: (props: Record<string, unknown>, helpers?: TemplateHelpers) => JSX.Element | null;
transformSearchClient?: (searchClient: unknown) => unknown;
}
export function createDocSearch<TComponentProps, TInputProps = TComponentProps>(
Component: ComponentType<TComponentProps>,
version: string,
): (allProps: DocSearchProps<TInputProps>) => DocSearchInstance {
return (allProps) => {
const input = allProps as unknown as DocSearchProps<ComponentProps>;
const { container, environment, transformSearchClient, hitComponent, resultsFooterComponent, ...rest } = input;
const containerElement = getHTMLElement(
container,
environment || (typeof window !== 'undefined' ? window : undefined),
);
const ref = createRef<DocSearchRef>();
let isReady = false;
const props: TComponentProps = {
...rest,
ref,
hitComponent: createTemplateFunction(hitComponent),
resultsFooterComponent: createTemplateFunction(resultsFooterComponent),
transformSearchClient: (searchClient: unknown): unknown => {
if (
typeof searchClient === 'object' &&
searchClient !== null &&
'addAlgoliaAgent' in searchClient &&
typeof searchClient.addAlgoliaAgent === 'function'
) {
searchClient.addAlgoliaAgent('docsearch.js', version);
}
return typeof transformSearchClient === 'function' ? transformSearchClient(searchClient) : searchClient;
},
} as unknown as TComponentProps;
render(createElement(Component, props as Attributes & TComponentProps), containerElement);
isReady = true;
return {
open(): void {
ref.current?.open();
},
close(): void {
ref.current?.close();
},
openAskAi(initialMessage?: InitialAskAiMessage): void {
ref.current?.openAskAi(initialMessage);
},
get isReady(): boolean {
return isReady;
},
get isOpen(): boolean {
return ref.current?.isOpen ?? false;
},
destroy(): void {
unmountComponentAtNode(containerElement);
isReady = false;
},
};
};
}

View file

@ -0,0 +1,2 @@
export { docsearch as default } from './docsearchComponent';
export type { DocSearchCallbacks, DocSearchInstance, DocSearchProps, TemplateHelpers } from './docsearchComponent';

View file

@ -1,124 +0,0 @@
import type { DocSearchRef, InitialAskAiMessage } from '@docsearch/core';
import type { DocSearchProps as DocSearchComponentProps } from '@docsearch/react';
import { DocSearch, version as docSearchVersion } from '@docsearch/react';
import htm from 'htm';
import type { JSX } from 'preact';
import { createElement, render, isValidElement, unmountComponentAtNode, createRef } from 'preact/compat';
/**
* Instance returned by docsearch() for programmatic control.
*/
export interface DocSearchInstance {
/** Returns true once the component is mounted and ready. */
readonly isReady: boolean;
/** Returns true if the modal is currently open. */
readonly isOpen: boolean;
/** Opens the search modal. */
open(): void;
/** Closes the search modal. */
close(): void;
/** Opens Ask AI mode (modal). */
openAskAi(initialMessage?: InitialAskAiMessage): void;
/** Unmounts the DocSearch component and cleans up. */
destroy(): void;
}
/**
* Lifecycle callbacks for the DocSearch instance.
*/
export interface DocSearchCallbacks {
/** Called once DocSearch is mounted and ready for interaction. */
onReady?: () => void;
/** Called when the modal opens. */
onOpen?: () => void;
/** Called when the modal closes. */
onClose?: () => void;
interceptAskAiEvent?: (initialMessage: InitialAskAiMessage) => boolean | void;
}
export type DocSearchProps = DocSearchCallbacks &
Omit<DocSearchComponentProps, 'onSidepanelClose' | 'onSidepanelOpen'> & {
container: HTMLElement | string;
environment?: typeof window;
};
function getHTMLElement(value: HTMLElement | string, env: typeof window | undefined): HTMLElement {
if (typeof value !== 'string') return value;
if (!env) throw new Error('Cannot resolve a selector without a browser environment.');
const el = env.document.querySelector<HTMLElement>(value);
if (!el) throw new Error(`Container selector did not match any element: "${value}"`);
return el;
}
// Tiny `html` helper bound to Preact createElement
const html = htm.bind(createElement) as unknown as (strings: TemplateStringsArray, ...values: unknown[]) => JSX.Element;
export type TemplateHelpers = Record<string, unknown> & { html: typeof html };
function createTemplateFunction<P extends Record<string, unknown>, R = JSX.Element | string | (() => JSX.Element)>(
original: ((props: P, helpers?: TemplateHelpers) => R) | undefined,
): ((props: P) => JSX.Element) | undefined {
if (!original) return undefined;
return (props: P) => {
const out = original(props, { html });
// Element, return as is
if (isValidElement(out)) return out;
// Component function, call with same props
if (typeof out === 'function') return out(props);
// String, render as plain text to avoid XSS
if (typeof out === 'string') return createElement('span', null, out);
// Fallback
return out as JSX.Element;
};
}
export function docsearch(allProps: DocSearchProps): DocSearchInstance {
const { container, environment, transformSearchClient, hitComponent, resultsFooterComponent, ...rest } = allProps;
const containerEl = getHTMLElement(container, environment || (typeof window !== 'undefined' ? window : undefined));
const ref = createRef<DocSearchRef>();
let isReady = false;
const props = {
...rest,
ref,
hitComponent: createTemplateFunction(hitComponent),
resultsFooterComponent: createTemplateFunction(resultsFooterComponent),
transformSearchClient: (searchClient: any): any => {
if (searchClient?.addAlgoliaAgent) {
searchClient.addAlgoliaAgent('docsearch.js', docSearchVersion);
}
return typeof transformSearchClient === 'function' ? transformSearchClient(searchClient) : searchClient;
},
} satisfies DocSearchComponentProps & { ref: typeof ref };
render(createElement(DocSearch, props), containerEl);
// Mark as ready after render completes
isReady = true;
return {
open(): void {
ref.current?.open();
},
close(): void {
ref.current?.close();
},
openAskAi(initialMessage?: InitialAskAiMessage): void {
ref.current?.openAskAi(initialMessage);
},
get isReady(): boolean {
return isReady;
},
get isOpen(): boolean {
return ref.current?.isOpen ?? false;
},
destroy(): void {
unmountComponentAtNode(containerEl);
isReady = false;
},
};
}

View file

@ -0,0 +1,17 @@
import type { DocSearchAIProps as DocSearchComponentProps } from '@docsearch/react/docsearchAi';
import { DocSearchAI } from '@docsearch/react/docsearchAi';
import { version } from '@docsearch/react/version';
import {
createDocSearch,
type DocSearchInstance,
type DocSearchProps as CreateDocSearchProps,
} from './createDocSearch';
export type { DocSearchCallbacks, DocSearchInstance, TemplateHelpers } from './createDocSearch';
export type DocSearchAIProps = CreateDocSearchProps<DocSearchComponentProps>;
export const docsearchAi: (allProps: DocSearchAIProps) => DocSearchInstance = createDocSearch<DocSearchComponentProps>(
DocSearchAI,
version,
);

View file

@ -0,0 +1,16 @@
import type { DocSearchProps as DocSearchComponentProps } from '@docsearch/react';
import { DocSearch, version } from '@docsearch/react';
import {
createDocSearch,
type DocSearchInstance,
type DocSearchProps as CreateDocSearchProps,
} from './createDocSearch';
export type { DocSearchCallbacks, DocSearchInstance, TemplateHelpers } from './createDocSearch';
export type DocSearchProps = CreateDocSearchProps<DocSearchComponentProps>;
export const docsearch: (allProps: DocSearchProps) => DocSearchInstance = createDocSearch<DocSearchComponentProps>(
DocSearch,
version,
);

View file

@ -1,2 +1,7 @@
export { docsearch as default } from './docsearch';
export type { DocSearchProps, DocSearchInstance, DocSearchCallbacks, TemplateHelpers } from './docsearch';
export { docsearchAi as default } from './docsearchAi';
export type {
DocSearchAIProps as DocSearchProps,
DocSearchInstance,
DocSearchCallbacks,
TemplateHelpers,
} from './docsearchAi';

View file

@ -7,7 +7,6 @@ import { defines } from '../../tsdown.base.ts';
import pkg from './package.json' with { type: 'json' };
const sharedConfig: UserConfig = {
entry: 'src/index.ts',
platform: 'browser',
target: 'es2017',
define: defines,
@ -32,6 +31,7 @@ const sharedConfig: UserConfig = {
export default defineConfig([
{
...sharedConfig,
entry: 'src/index.ts',
dts: true,
format: 'esm',
outDir: 'dist/esm',
@ -39,6 +39,28 @@ export default defineConfig([
},
{
...sharedConfig,
clean: false,
entry: 'src/docsearch.ts',
dts: true,
format: 'esm',
outDir: 'dist/esm',
minify: false,
},
{
...sharedConfig,
entry: 'src/index.ts',
dts: false,
globalName: 'docsearch',
outDir: 'dist/umd',
outputOptions: {
entryFileNames: '[name].js',
},
format: 'umd',
minify: true,
},
{
...sharedConfig,
entry: 'src/docsearch.ts',
dts: false,
globalName: 'docsearch',
outDir: 'dist/umd',