Upstream Pagefind/pagefind state of the touched files before f1eedafd11

This commit is contained in:
upstream committed 2026-10-03 01:43:17 +02:00
1 parent b572112ae7
commit 1308f3c1bb
10 files changed
+1706 -79

No files matched your search

+303
View File
@@ -0,0 +1,303 @@
import { PagefindService } from "./service.js";
import { decode } from "./encoding.js";
/**
* @typedef {import('pagefindInternal').InternalResponseCallback} InternalResponseCallback
* @typedef {import('pagefindInternal').InternalResponsePayload} InternalResponsePayload
*/
/**
* @type {PagefindService?}
*/
let persistentService;
const launch = () => {
if (!persistentService) {
persistentService = new PagefindService();
}
return persistentService;
}
/**
* @template T
* @param {function(any): void} resolve
* @param {function(any): void} reject
* @param {InternalResponseCallback} response_callback
* @param {function(InternalResponsePayload): T} resultFn
*/
const handleApiResponse = (resolve, reject, { exception, err, result }, resultFn) => {
if (exception) {
reject(exception);
} else {
resolve({
errors: err ? [err.message] : [],
...(result ? resultFn(result) : {})
});
}
}
/**
* @typedef {import('pagefindService').NewIndexResponse} NewIndexResponse
*
* @param {import('pagefindService').PagefindServiceConfig=} config
* @type {import('pagefindService').createIndex}
* */
export const createIndex = (config) => new Promise((resolve, reject) => {
// TODO: Validate `config` here, instead of waiting for the backend to throw an error.
// Ideally we create a global Pagefind config JSON schema that (a subset of) can be used here.
const action = 'NewIndex';
launch().sendMessage(
{
type: action,
config: {
root_selector: config?.rootSelector,
exclude_selectors: config?.excludeSelectors,
force_language: config?.forceLanguage,
verbose: config?.verbose,
logfile: config?.logfile,
keep_index_url: config?.keepIndexUrl,
write_playground: config?.writePlayground,
include_characters: config?.includeCharacters,
}
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<NewIndexResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== action) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
index: indexFns(success.index_id),
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @type {import('pagefindService').close}
*/
export const close = () => new Promise((resolve, reject) => {
persistentService?.close(null);
persistentService = null;
resolve(null);
});
/**
* @param {number} indexId
* @returns {import ('pagefindService').PagefindIndex}
*/
const indexFns = (indexId) => {
return {
addHTMLFile: (file) => addHTMLFile(indexId, file),
addCustomRecord: (record) => addCustomRecord(indexId, record),
addDirectory: (dir) => addDirectory(indexId, dir),
writeFiles: (options) => writeFiles(indexId, options),
getFiles: () => getFiles(indexId),
deleteIndex: () => deleteIndex(indexId)
}
}
/**
* @typedef {import('pagefindService').NewFileResponse} NewFileResponse
*
* @param {number} indexId
* @param {import('pagefindService').HTMLFile} file
* @returns {Promise<NewFileResponse>}
*/
const addHTMLFile = (indexId, file) => new Promise((resolve, reject) => {
const action = 'AddFile';
const responseAction = 'IndexedFile';
launch().sendMessage(
{
type: action,
index_id: indexId,
file_path: file.sourcePath,
url: file.url,
file_contents: file.content
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<NewFileResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== responseAction) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
file: {
uniqueWords: success.page_word_count,
url: success.page_url,
meta: success.page_meta,
}
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @param {number} indexId
* @param {import('pagefindService').CustomRecord} record
* @returns {Promise<NewFileResponse>}
*/
const addCustomRecord = (indexId, record) => new Promise((resolve, reject) => {
const action = 'AddRecord';
const responseAction = 'IndexedFile';
launch().sendMessage(
{
type: action,
index_id: indexId,
url: record.url,
content: record.content,
language: record.language,
meta: record.meta,
filters: record.filters,
sort: record.sort,
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<NewFileResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== responseAction) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
file: {
uniqueWords: success.page_word_count,
url: success.page_url,
meta: success.page_meta,
}
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @typedef {import('pagefindService').IndexingResponse} IndexingResponse
*
* @param {number} indexId
* @param {import('pagefindService').SiteDirectory} dir
* @returns {Promise<IndexingResponse>}
*/
const addDirectory = (indexId, dir) => new Promise((resolve, reject) => {
const action = 'AddDir';
const responseAction = 'IndexedDir';
launch().sendMessage(
{
type: action,
index_id: indexId,
path: dir.path,
glob: dir.glob
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<IndexingResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== responseAction) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
page_count: success.page_count
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @typedef {import ('pagefindService').WriteFilesResponse} WriteFilesResponse
*
* @param {number} indexId
* @param {import('pagefindService').WriteOptions=} options
* @returns {Promise<WriteFilesResponse>}
*/
const writeFiles = (indexId, options) => new Promise((resolve, reject) => {
const action = 'WriteFiles';
launch().sendMessage(
{
type: action,
index_id: indexId,
output_path: options?.outputPath
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<WriteFilesResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== action) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
outputPath: success.output_path
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @typedef {import ('pagefindService').GetFilesResponse} GetFilesResponse
*
* @param {number} indexId
* @returns {Promise<GetFilesResponse>}
*/
const getFiles = (indexId) => new Promise((resolve, reject) => {
const action = 'GetFiles';
launch().sendMessage(
{
type: action,
index_id: indexId,
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<GetFilesResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== action) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
files: success.files.map(file => {
return {
path: file.path,
content: decode(file.content)
}
})
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @param {number} indexId
* @returns {Promise<null>}
*/
const deleteIndex = (indexId) => new Promise((resolve, reject) => {
const action = 'DeleteIndex';
launch().sendMessage(
{
type: action,
index_id: indexId,
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<GetFilesResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== action) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return null;
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
+227
View File
@@ -0,0 +1,227 @@
/**
* Create a new Pagefind index that files can be added to
*/
export function createIndex(config?: PagefindServiceConfig): Promise<NewIndexResponse>;
/**
* Close the Pagefind service and clean up, stopping the binary altogether.
*
* Service _will_ be restarted if any new calls are made to any other function.
*
* Calling functions on an existing index after calling close() is undefined behavior,
* as that index may map to a new index after the service has restarted.
*/
export function close(): Promise<null>;
export interface PagefindServiceConfig {
/**
* The element Pagefind should treat as the root of the document, defaults to `html`.
* Usually you will want to use the data-pagefind-body attribute instead.
* @example ".my-html-outer"
*/
rootSelector?: string,
/**
* Custom selectors that Pagefind should ignore when indexing.
* Usually you will want to use the data-pagefind-ignore attribute instead.
* @example ["svg", ".my-code-blocks"]
*/
excludeSelectors?: string[],
/**
* Ignore any detected languages and index the whole site as a single language.
* Expects an ISO 639-1 code.
*/
forceLanguage?: string,
/**
* Print verbose logging while indexing the site. Does not impact the web-facing search.
* When running as a service, only impacts the logfile (if present).
*/
verbose?: boolean,
/**
* Path to a logfile to write to. Will replace the file on each run.
*/
logfile?: string,
/**
* Keep `index.html` at the end of search result paths.
* Defaults to false, stripping `index.html`.
*/
keepIndexUrl?: boolean,
/**
* When writing or outputting files, also write the Pagefind playground to /pagefind/playground/.
* Defaults to false, ensuring the playground isn't available on a live site.
*/
writePlayground?: boolean,
/**
* Include these characters when indexing and searching words.
* Useful for sites documenting technical topics such as programming languages.
* @example "<>$"
*/
includeCharacters?: string,
}
export interface NewIndexResponse {
errors: string[],
index?: PagefindIndex
}
/**
* A Pagefind index that exists in the backend service before being built
*/
export interface PagefindIndex {
addHTMLFile: typeof addHTMLFile,
addCustomRecord: typeof addCustomRecord,
addDirectory: typeof addDirectory,
writeFiles: typeof writeFiles,
getFiles: typeof getFiles,
deleteIndex: typeof deleteIndex,
}
/**
* Index an HTML file that isn't on disk
*/
declare function addHTMLFile(file: HTMLFile): Promise<NewFileResponse>;
/**
* Index a custom record that isn't backed by an HTML file
*/
declare function addCustomRecord(record: CustomRecord): Promise<NewFileResponse>;
/**
* Index a directory of HTML files from disk
*/
declare function addDirectory(path: SiteDirectory): Promise<IndexingResponse>;
/**
* The data required for Pagefind to index an HTML file that isn't on disk
* @example
* {
* sourcePath: "about/index.html",
* content: "<html lang='en'><body><h1>Meet the team</h1></body></html>"
* }
*/
export interface HTMLFile {
/**
* The source path of the HTML file if it were to exist on disk.
* Must be a relative path, or an absolute path within the current working directory.
* Pagefind will compute the result URL from this path.
*
* If not supplied, url must be supplied.
*
* @example "about/index.html"
* @example "/Users/user/Documents/site/about/index.html"
*/
sourcePath?: string,
/**
* An explicit URL to use, instead of having Pagefind
* compute the URL based on the sourcePath.
*
* If not supplied, sourcePath must be supplied.
*
* @example "/about/"
*/
url?: string,
/** The source HTML content of the file to be parsed */
content: string
}
/**
* The data required for Pagefind to index a custom record that isn't backed by an HTML file
* @example
* {
* url: "/about/",
* content: "Meet the team",
* language: "en"
* }
*/
export interface CustomRecord {
/** The output URL of this record. Pagefind will not alter this */
url: string,
/** The raw content of this record */
content: string,
/** What language is this record written in. Multiple languages will be split into separate indexes. Expects an ISO 639-1 code. */
language: string,
/** The metadata to attach to this record. Supplying a `title` is highly recommended */
meta?: Record<string, string>,
/** The filters to attach to this record */
filters?: Record<string, string[]>,
/** The sort keys to attach to this record */
sort?: Record<string, string>
}
/**
* The data required for Pagefind to index the files in a directory
* @example
* {
* path: "public",
* glob: "**\/*.{html}"
* }
*/
export interface SiteDirectory {
/**
* The path to the directory to index.
* If relative, is relative to the cwd.
*/
path: string,
/** Optionally, a custom glob to evaluate for finding files. Default to all HTML files. */
glob?: string
}
export interface IndexingResponse {
errors: string[],
page_count: number
}
export interface NewFileResponse {
errors: string[],
file: NewFile
}
/**
* Details about a new file that has been successfully added to the Pagefind index
*/
export interface NewFile {
uniqueWords: number,
url: string,
meta: Record<string, string>
}
/**
* Write the index files to disk
*/
declare function writeFiles(options?: WriteOptions): Promise<WriteFilesResponse>;
/**
* Options for writing a Pagefind index to disk
*/
export interface WriteOptions {
/**
* The path of the pagefind bundle directory to write to disk.
* If relative, is relative to the cwd.
* @example "./public/pagefind"
*/
outputPath: string
}
export interface WriteFilesResponse {
errors: string[],
outputPath: string
}
/**
* Get an in-memory copy of the built index files
*/
declare function getFiles(): Promise<GetFilesResponse>;
export interface GetFilesResponse {
errors: string[],
files: IndexFile[]
}
export interface IndexFile {
path: string,
content: Uint8Array
}
/**
* Delete this index and clear it from memory
*/
declare function deleteIndex(): Promise<null>;
+154
View File
@@ -0,0 +1,154 @@
// Requests to the backend.
/**
* The raw object passed to the backend binary
*/
export interface InternalServiceRequest {
message_id?: number,
payload: InternalRequestPayload
}
/**
* The payload describing an action to the backend binary
*/
export type InternalRequestPayload =
| InternalNewIndexRequest
| InternalAddFileRequest
| InternalAddRecordRequest
| InternalAddDirRequest
| InternalWriteFilesRequest
| InternalGetFilesRequest
| InternalDeleteIndexRequest;
export interface InternalNewIndexRequest {
type: 'NewIndex',
config?: InternalPagefindServiceConfig
}
export interface InternalPagefindServiceConfig {
root_selector?: string,
exclude_selectors?: string[],
force_language?: string,
verbose?: boolean,
logfile?: string,
keep_index_url?: boolean,
write_playground?: boolean,
include_characters?: string,
}
export interface InternalAddFileRequest {
type: 'AddFile',
index_id: number,
file_path?: string,
url?: string,
file_contents: string
}
export interface InternalAddRecordRequest {
type: 'AddRecord',
index_id: number,
url: string,
content: string,
language: string,
meta?: Record<string, string>,
filters?: Record<string, string[]>,
sort?: Record<string, string>
}
export interface InternalAddDirRequest {
type: 'AddDir',
index_id: number,
path: string,
glob?: string
}
export interface InternalWriteFilesRequest {
type: 'WriteFiles',
index_id: number,
output_path?: string
}
export interface InternalGetFilesRequest {
type: 'GetFiles',
index_id: number
}
export interface InternalDeleteIndexRequest {
type: 'DeleteIndex',
index_id: number
}
// Responses from the backend.
/**
* The raw object returned from the backend binary
*/
export interface InternalServiceResponse {
message_id: number,
payload: InternalResponsePayload | InternalResponseError
}
/**
* The response payload in the case of an error
*/
export interface InternalResponseError {
type: 'Error',
original_message?: string,
message: string
}
/**
* The response payload in the case of a success
*/
export type InternalResponsePayload =
| InternalNewIndexResponse
| InternalIndexedFileResponse
| InternalIndexedDirResponse
| InternalWriteFilesResponse
| InternalGetFilesResponse
| InternalDeleteIndexResponse;
export interface InternalNewIndexResponse {
type: 'NewIndex',
index_id: number
}
export interface InternalIndexedFileResponse {
type: 'IndexedFile',
page_word_count: number,
page_url: string,
page_meta: Record<string, string>
}
export interface InternalIndexedDirResponse {
type: 'IndexedDir',
page_count: number
}
export interface InternalWriteFilesResponse {
type: 'WriteFiles',
output_path: string,
}
export interface InternalGetFilesResponse {
type: 'GetFiles',
files: InternalSyntheticFile[],
}
export interface InternalSyntheticFile {
path: string,
content: string
}
export interface InternalDeleteIndexResponse {
type: 'DeleteIndex'
}
/**
* What the service returns to the wrapping javascript detailing a response
*/
export interface InternalResponseCallback {
exception: Error | null,
err: InternalResponseError | null,
result: InternalResponsePayload | null
}