feat(js): Split JS bundles for search only (#2920)
This commit is contained in:
parent
629fce7902
commit
e0d21a6d4f
11 changed files with 237 additions and 133 deletions
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 don’t 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 don’t 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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
122
packages/docsearch-js/src/createDocSearch.tsx
Normal file
122
packages/docsearch-js/src/createDocSearch.tsx
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
2
packages/docsearch-js/src/docsearch.ts
Normal file
2
packages/docsearch-js/src/docsearch.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { docsearch as default } from './docsearchComponent';
|
||||
export type { DocSearchCallbacks, DocSearchInstance, DocSearchProps, TemplateHelpers } from './docsearchComponent';
|
||||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
17
packages/docsearch-js/src/docsearchAi.tsx
Normal file
17
packages/docsearch-js/src/docsearchAi.tsx
Normal 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,
|
||||
);
|
||||
16
packages/docsearch-js/src/docsearchComponent.tsx
Normal file
16
packages/docsearch-js/src/docsearchComponent.tsx
Normal 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,
|
||||
);
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue