diff --git a/.changeset/orama-db-deterministic.md b/.changeset/orama-db-deterministic.md new file mode 100644 index 00000000..9d197b90 --- /dev/null +++ b/.changeset/orama-db-deterministic.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/generator-react': patch +--- + +`orama-db.json` is now identical between builds of the same docs: documents get a stable `href` id instead of a time-based one, and are sorted by `href`. diff --git a/packages/react/src/orama-db/__tests__/generate.test.mjs b/packages/react/src/orama-db/__tests__/generate.test.mjs new file mode 100644 index 00000000..dec6abf8 --- /dev/null +++ b/packages/react/src/orama-db/__tests__/generate.test.mjs @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { setConfig } from '@doc-kit/core/utils/configuration/index.mjs'; +import { create, load, search } from '@orama/orama'; +import { u } from 'unist-builder'; + +import { generate } from '../generate.mjs'; + +const config = await setConfig({ target: ['orama-db'] }); + +// Return the saved index instead of writing it to disk +config['orama-db'].output = undefined; + +const entry = (api, path, depth, name, slug, text) => ({ + api, + path, + heading: { depth, data: { name, slug } }, + content: u('root', [u('paragraph', [u('text', text)])]), +}); + +const fs = [ + entry('fs', '/fs', 1, 'File system', 'file-system', 'Files.'), + entry('fs', '/fs', 2, 'Class: FileHandle', 'class-filehandle', 'A handle.'), + entry('fs', '/fs', 3, 'readFile', 'readfile', 'Reads a file.'), +]; + +const http = [ + entry('http', '/http', 1, 'HTTP', 'http', 'Requests.'), + entry('http', '/http', 2, 'request', 'request', 'Sends a request.'), +]; + +describe('orama-db', () => { + it('produces the same output on every run', async () => { + const first = await generate([...fs, ...http]); + const second = await generate([...fs, ...http]); + + assert.equal(JSON.stringify(first), JSON.stringify(second)); + }); + + it('produces the same output whatever order the modules arrive in', async () => { + const forward = await generate([...fs, ...http]); + const reversed = await generate([...http, ...fs]); + + assert.equal(JSON.stringify(forward), JSON.stringify(reversed)); + }); + + it('keeps each module hierarchical title', async () => { + const db = create({ schema: {} }); + load(db, await generate([...http, ...fs])); + + const { hits } = await search(db, { term: 'readFile' }); + + assert.equal(hits[0].document.title, 'Class: FileHandle > readFile'); + }); + + it('can still be searched', async () => { + const db = create({ schema: {} }); + load(db, await generate([...fs, ...http])); + + const { hits } = await search(db, { term: 'request' }); + + assert.equal(hits[0].document.href, '/http.html#request'); + }); +}); diff --git a/packages/react/src/orama-db/generate.mjs b/packages/react/src/orama-db/generate.mjs index a67dfa07..9432907b 100644 --- a/packages/react/src/orama-db/generate.mjs +++ b/packages/react/src/orama-db/generate.mjs @@ -32,17 +32,27 @@ export async function generate(input) { child => child.type === 'paragraph' ); + const href = `${entry.path}.html#${entry.heading.data.slug}`; + return { + // Orama makes up a time-based id when none is given, which would + // change the saved index on every build + id: href, title: hierarchicalTitle, description: paragraph ? transformNodeToString(paragraph, true) : undefined, - href: `${entry.path}.html#${entry.heading.data.slug}`, + href, siteSection: headings[0].heading.data.name, }; }) ); + // The saved index follows insertion order, which depends on the order the + // generator's input arrives in. Sort without a locale so it is stable on + // every machine. + documents.sort((a, b) => (a.href < b.href ? -1 : a.href > b.href ? 1 : 0)); + // Insert all documents await insertMultiple(db, documents);