@farm.js/search 0.1.0-beta.1 → 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.d.ts +2 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +75 -18
- package/dist/client-runtime.d.ts.map +1 -1
- package/dist/client-runtime.js +89 -32
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/package.json +2 -2
package/dist/build.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface SearchBuildInput {
|
|
|
4
4
|
publicDir?: string;
|
|
5
5
|
preset: string;
|
|
6
6
|
basePath: string;
|
|
7
|
+
/** Prefix already present in emitted HTML paths (localized Farm SSG output). */
|
|
8
|
+
htmlBasePath?: string;
|
|
7
9
|
options: ResolvedSearchOptions;
|
|
8
10
|
}
|
|
9
11
|
export interface SearchBuildResult {
|
package/dist/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoF1F;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKhF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGnE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKpE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKtD;AAYD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAwB3E"}
|
package/dist/build.js
CHANGED
|
@@ -11,13 +11,13 @@ export async function writeSearchIndex(input) {
|
|
|
11
11
|
const routes = htmlFiles
|
|
12
12
|
.map((file) => ({
|
|
13
13
|
file,
|
|
14
|
-
logicalRoute: stripBasePath(routeFromHtmlFile(path.relative(publicDir, file)),
|
|
14
|
+
logicalRoute: stripBasePath(routeFromHtmlFile(path.relative(publicDir, file)), input.htmlBasePath ?? "/"),
|
|
15
15
|
}))
|
|
16
16
|
.sort((left, right) => left.logicalRoute.localeCompare(right.logicalRoute));
|
|
17
17
|
const selected = routes.filter(({ logicalRoute }) => shouldIndexRoute(logicalRoute, input.options));
|
|
18
18
|
const skippedRoutes = routes
|
|
19
19
|
.filter(({ logicalRoute }) => !shouldIndexRoute(logicalRoute, input.options))
|
|
20
|
-
.map(({ logicalRoute }) =>
|
|
20
|
+
.map(({ logicalRoute }) => toResultUrl(logicalRoute, basePath));
|
|
21
21
|
if (selected.length === 0) {
|
|
22
22
|
throw new Error("[farm:search] No static HTML pages matched the configured routes. " +
|
|
23
23
|
"Mark searchable pages as static or update search include/exclude patterns.");
|
|
@@ -43,7 +43,7 @@ export async function writeSearchIndex(input) {
|
|
|
43
43
|
const indexedRoutes = [];
|
|
44
44
|
try {
|
|
45
45
|
for (const { file, logicalRoute } of selected) {
|
|
46
|
-
const route =
|
|
46
|
+
const route = toResultUrl(logicalRoute, basePath);
|
|
47
47
|
const result = await index.addHTMLFile({
|
|
48
48
|
url: route,
|
|
49
49
|
content: await readFile(file, "utf8"),
|
|
@@ -53,6 +53,7 @@ export async function writeSearchIndex(input) {
|
|
|
53
53
|
}
|
|
54
54
|
indexedRoutes.push(route);
|
|
55
55
|
}
|
|
56
|
+
await assertOutputPathIsOwned(outputPath);
|
|
56
57
|
await rm(outputPath, { recursive: true, force: true });
|
|
57
58
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
58
59
|
const written = await index.writeFiles({ outputPath });
|
|
@@ -87,9 +88,6 @@ export function withBasePath(route, basePath) {
|
|
|
87
88
|
const normalizedRoute = route === "/" ? "/" : `/${route.replace(/^\/+|\/+$/g, "")}`;
|
|
88
89
|
if (normalizedBase === "/")
|
|
89
90
|
return normalizedRoute;
|
|
90
|
-
if (normalizedRoute === normalizedBase || normalizedRoute.startsWith(`${normalizedBase}/`)) {
|
|
91
|
-
return normalizedRoute;
|
|
92
|
-
}
|
|
93
91
|
return normalizedRoute === "/" ? normalizedBase : `${normalizedBase}${normalizedRoute}`;
|
|
94
92
|
}
|
|
95
93
|
export function stripBasePath(route, basePath) {
|
|
@@ -106,20 +104,31 @@ export function routeFromHtmlFile(file) {
|
|
|
106
104
|
const route = withoutIndex ? withoutIndex.replace(/\.html$/i, "").replace(/^\/+|\/+$/g, "") : "";
|
|
107
105
|
return route ? `/${route}` : "/";
|
|
108
106
|
}
|
|
107
|
+
function toResultUrl(route, basePath) {
|
|
108
|
+
// Routes came from decoded filesystem names. Encode each segment once, after
|
|
109
|
+
// filtering and removing any explicit HTML prefix; basePath is already a URL path.
|
|
110
|
+
const pathname = route
|
|
111
|
+
.split("/")
|
|
112
|
+
.map((segment) => encodeURIComponent(segment))
|
|
113
|
+
.join("/");
|
|
114
|
+
return withBasePath(pathname, basePath);
|
|
115
|
+
}
|
|
109
116
|
export function matchesRoutePattern(route, pattern) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return route === parent || route.startsWith(parent === "/" ? "/" : `${parent}/`);
|
|
113
|
-
}
|
|
117
|
+
const descendants = pattern.endsWith("/**");
|
|
118
|
+
const parentPattern = descendants ? pattern.slice(0, -3) : pattern;
|
|
114
119
|
let source = "";
|
|
115
|
-
for (let index = 0; index <
|
|
116
|
-
const character =
|
|
117
|
-
if (character === "*"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
for (let index = 0; index < parentPattern.length; index++) {
|
|
121
|
+
const character = parentPattern[index];
|
|
122
|
+
if (character === "*") {
|
|
123
|
+
// Collapse the whole run. Emitting one `.*` per adjacent pair turned
|
|
124
|
+
// `****` into `.*.*`, and each extra pair multiplied the ways a failing
|
|
125
|
+
// route could be split between them, so a pattern with a handful of
|
|
126
|
+
// consecutive stars hung the build instead of matching.
|
|
127
|
+
let run = 1;
|
|
128
|
+
while (parentPattern[index + run] === "*")
|
|
129
|
+
run++;
|
|
130
|
+
source += run > 1 ? ".*" : "[^/]*";
|
|
131
|
+
index += run - 1;
|
|
123
132
|
}
|
|
124
133
|
else if (character === "?") {
|
|
125
134
|
source += "[^/]";
|
|
@@ -128,6 +137,9 @@ export function matchesRoutePattern(route, pattern) {
|
|
|
128
137
|
source += character.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
129
138
|
}
|
|
130
139
|
}
|
|
140
|
+
// Compile the parent too: a terminal /** must not turn earlier wildcards into literals.
|
|
141
|
+
if (descendants)
|
|
142
|
+
source += parentPattern ? "(?:/.*)?" : "/.*";
|
|
131
143
|
return new RegExp(`^${source}/?$`).test(route);
|
|
132
144
|
}
|
|
133
145
|
function shouldIndexRoute(route, options) {
|
|
@@ -172,6 +184,51 @@ function resolveOutputPath(publicDir, bundlePath) {
|
|
|
172
184
|
}
|
|
173
185
|
return outputPath;
|
|
174
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Refuse to delete prerendered pages that happen to live where the index goes.
|
|
189
|
+
*
|
|
190
|
+
* The index directory is wiped before it is rewritten, and it comes from the
|
|
191
|
+
* `output` option, so `output: "search"` on a site that also prerenders a
|
|
192
|
+
* `/search` page pointed the wipe at the app's own pages. `collectHtmlFiles`
|
|
193
|
+
* skips this directory, so those pages were left out of the index and then
|
|
194
|
+
* deleted from the deploy output, with a success log and a zero exit.
|
|
195
|
+
*
|
|
196
|
+
* Only HTML is treated as the app's: a previous bundle leaves non-HTML files
|
|
197
|
+
* behind, and clearing those is this function's normal job.
|
|
198
|
+
*/
|
|
199
|
+
async function assertOutputPathIsOwned(outputPath) {
|
|
200
|
+
const pages = await findHtmlFile(outputPath);
|
|
201
|
+
if (!pages)
|
|
202
|
+
return;
|
|
203
|
+
throw new Error(`The search index directory ${outputPath} contains a prerendered page (${path.relative(outputPath, pages)}), ` +
|
|
204
|
+
`so it will not be replaced. Give \`search({ output })\` a directory of its own, ` +
|
|
205
|
+
`or move the conflicting route, then build again.`);
|
|
206
|
+
}
|
|
207
|
+
/** The first HTML file at any depth, or undefined when there is none. */
|
|
208
|
+
async function findHtmlFile(directory) {
|
|
209
|
+
let entries;
|
|
210
|
+
try {
|
|
211
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
// Nothing there yet is the ordinary first build.
|
|
215
|
+
if (error.code === "ENOENT")
|
|
216
|
+
return undefined;
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
const entryPath = path.join(directory, entry.name);
|
|
221
|
+
if (entry.isDirectory()) {
|
|
222
|
+
const nested = await findHtmlFile(entryPath);
|
|
223
|
+
if (nested)
|
|
224
|
+
return nested;
|
|
225
|
+
}
|
|
226
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
|
|
227
|
+
return entryPath;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
175
232
|
async function assertOutputPathInside(publicDir, outputPath) {
|
|
176
233
|
const realPublicDir = await realpath(publicDir);
|
|
177
234
|
let existingAncestor = outputPath;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-runtime.d.ts","sourceRoot":"","sources":["../src/client-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,EAInB,MAAM,YAAY,CAAC;AAEpB,UAAU,sBAAsB;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,WAAW,EAAE,KAAK,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;CACJ;AAED,UAAU,oBAAoB;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,IAAI,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACzC;AAED,UAAU,sBAAsB;IAC9B,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,YAAY,EAAE,kBAAkB,CAAC;IACjC,OAAO,EACH;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAClD,KAAK,CAAC;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;CACR;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAChG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,OAAO,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D,cAAc,CAAC,CACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACrD;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAEnF,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,sBAAsB,EAC3C,OAAO,EAAE,mBAAmB,EAC5B,UAAU,EAAE,oBAAoB,GAC/B,gBAAgB,
|
|
1
|
+
{"version":3,"file":"client-runtime.d.ts","sourceRoot":"","sources":["../src/client-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,EAInB,MAAM,YAAY,CAAC;AAEpB,UAAU,sBAAsB;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,WAAW,EAAE,KAAK,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;CACJ;AAED,UAAU,oBAAoB;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,IAAI,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACzC;AAED,UAAU,sBAAsB;IAC9B,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,YAAY,EAAE,kBAAkB,CAAC;IACjC,OAAO,EACH;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAClD,KAAK,CAAC;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;CACR;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAChG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,OAAO,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D,cAAc,CAAC,CACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACrD;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAEnF,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,sBAAsB,EAC3C,OAAO,EAAE,mBAAmB,EAC5B,UAAU,EAAE,oBAAoB,GAC/B,gBAAgB,CA2JlB"}
|
package/dist/client-runtime.js
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
export function createSearchClient(resolveConfig, options, loadModule) {
|
|
2
|
-
let
|
|
3
|
-
|
|
2
|
+
let loading;
|
|
3
|
+
/**
|
|
4
|
+
* Stop handing this instance out and destroy it once every in-flight call
|
|
5
|
+
* has released it. Destroying eagerly is the bug this replaces: a
|
|
6
|
+
* bundle-path change or destroy() could tear the engine down under a
|
|
7
|
+
* search(), preload(), or filters() call that had already resolved it.
|
|
8
|
+
*/
|
|
9
|
+
const retire = (instance) => {
|
|
10
|
+
if (!instance.retired) {
|
|
11
|
+
instance.retired = true;
|
|
12
|
+
instance.drained = (async () => {
|
|
13
|
+
if (instance.uses > 0) {
|
|
14
|
+
await new Promise((resolve) => {
|
|
15
|
+
instance.resolveDrained = resolve;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
await instance.api.destroy();
|
|
19
|
+
})();
|
|
20
|
+
}
|
|
21
|
+
return instance.drained;
|
|
22
|
+
};
|
|
23
|
+
const release = (instance) => {
|
|
24
|
+
instance.uses -= 1;
|
|
25
|
+
if (instance.retired && instance.uses === 0)
|
|
26
|
+
instance.resolveDrained?.();
|
|
27
|
+
};
|
|
28
|
+
const getInstance = async () => {
|
|
4
29
|
const config = resolveConfig();
|
|
5
30
|
if (!config.available && !options.bundlePath) {
|
|
6
31
|
throw new Error("[farm:search] The search index is generated by production builds. " +
|
|
7
32
|
"Run farm build and serve the production output before searching.");
|
|
8
33
|
}
|
|
9
34
|
const bundlePath = normalizeBundlePath(options.bundlePath ?? config.bundlePath);
|
|
10
|
-
if (!
|
|
11
|
-
const
|
|
35
|
+
if (!loading) {
|
|
36
|
+
const attempt = loadModule(`${bundlePath}pagefind.js`)
|
|
12
37
|
.then(async (module) => {
|
|
13
38
|
const highlightParam = options.highlightParam === false
|
|
14
39
|
? undefined
|
|
@@ -22,21 +47,49 @@ export function createSearchClient(resolveConfig, options, loadModule) {
|
|
|
22
47
|
if (!module.createInstance)
|
|
23
48
|
await api.options(pagefindOptions);
|
|
24
49
|
await api.init();
|
|
25
|
-
return { api, bundlePath };
|
|
50
|
+
return { api, bundlePath, uses: 0, retired: false };
|
|
26
51
|
})
|
|
27
52
|
.catch((error) => {
|
|
28
|
-
|
|
53
|
+
if (loading === attempt)
|
|
54
|
+
loading = undefined;
|
|
29
55
|
throw error;
|
|
30
56
|
});
|
|
31
|
-
|
|
57
|
+
loading = attempt;
|
|
32
58
|
}
|
|
33
|
-
const
|
|
59
|
+
const attempt = loading;
|
|
60
|
+
const current = await attempt;
|
|
34
61
|
if (current.bundlePath !== bundlePath) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
// Only clear our own load: a concurrent caller that noticed the same
|
|
63
|
+
// stale bundle may already have started the replacement.
|
|
64
|
+
if (loading === attempt)
|
|
65
|
+
loading = undefined;
|
|
66
|
+
// Drain the old engine in the background; the swap must not wait for
|
|
67
|
+
// calls that are still finishing against it. Its destroy() failure has
|
|
68
|
+
// no caller to surface to here — an explicit destroy() still reports it.
|
|
69
|
+
void retire(current).catch(() => { });
|
|
70
|
+
return getInstance();
|
|
71
|
+
}
|
|
72
|
+
return current;
|
|
73
|
+
};
|
|
74
|
+
/** Resolve an instance and hold it for the duration of one call. */
|
|
75
|
+
const withApi = async (run) => {
|
|
76
|
+
for (;;) {
|
|
77
|
+
const instance = await getInstance();
|
|
78
|
+
instance.uses += 1;
|
|
79
|
+
if (instance.retired) {
|
|
80
|
+
// Retired between resolving and acquiring; hand it back and take the
|
|
81
|
+
// replacement instead of running against an engine that may already
|
|
82
|
+
// be destroyed.
|
|
83
|
+
release(instance);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return await run(instance.api);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
release(instance);
|
|
91
|
+
}
|
|
38
92
|
}
|
|
39
|
-
return current.api;
|
|
40
93
|
};
|
|
41
94
|
return {
|
|
42
95
|
async search(term, queryOptions = {}) {
|
|
@@ -46,36 +99,40 @@ export function createSearchClient(resolveConfig, options, loadModule) {
|
|
|
46
99
|
const limit = normalizePositiveInteger(queryOptions.limit ?? 20, "limit");
|
|
47
100
|
const offset = normalizeOffset(queryOptions.offset ?? 0);
|
|
48
101
|
const pagefindOptions = engineOptions(queryOptions);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
results
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
102
|
+
// Result fragments load lazily, so the instance stays held until they
|
|
103
|
+
// have loaded too, not just through the initial query.
|
|
104
|
+
return withApi(async (api) => {
|
|
105
|
+
const response = await api.search(term, pagefindOptions);
|
|
106
|
+
const handles = response.results.slice(offset, offset + limit);
|
|
107
|
+
const results = await Promise.all(handles.map(loadResult));
|
|
108
|
+
return {
|
|
109
|
+
results,
|
|
110
|
+
total: response.results.length,
|
|
111
|
+
unfilteredTotal: response.unfilteredResultCount,
|
|
112
|
+
filters: response.filters,
|
|
113
|
+
totalFilters: response.totalFilters,
|
|
114
|
+
timings: combineTimings(response.timings),
|
|
115
|
+
};
|
|
116
|
+
});
|
|
61
117
|
},
|
|
62
118
|
async preload(term, queryOptions = {}) {
|
|
63
119
|
if (typeof term !== "string")
|
|
64
120
|
throw new TypeError("Search preload term must be a string");
|
|
65
121
|
const pagefindOptions = engineOptions(queryOptions);
|
|
66
|
-
|
|
67
|
-
await api.preload(term, pagefindOptions);
|
|
122
|
+
await withApi((api) => api.preload(term, pagefindOptions));
|
|
68
123
|
},
|
|
69
124
|
async filters() {
|
|
70
|
-
|
|
71
|
-
return api.filters();
|
|
125
|
+
return withApi((api) => api.filters());
|
|
72
126
|
},
|
|
73
127
|
async destroy() {
|
|
74
|
-
if (!
|
|
128
|
+
if (!loading)
|
|
75
129
|
return;
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
130
|
+
const attempt = loading;
|
|
131
|
+
const current = await attempt.catch(() => undefined);
|
|
132
|
+
if (loading === attempt)
|
|
133
|
+
loading = undefined;
|
|
134
|
+
if (current)
|
|
135
|
+
await retire(current);
|
|
79
136
|
},
|
|
80
137
|
};
|
|
81
138
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type SearchOptions } from "./config.js";
|
|
2
|
+
import type { FarmSearchPublicConfig } from "./types.js";
|
|
2
3
|
export type { ResolvedSearchOptions, SearchOptions } from "./config.js";
|
|
3
4
|
export type { FarmSearchClient, SearchClientOptions, SearchFilterCounts, SearchFilterExpression, SearchFilterPrimitive, SearchFilters, SearchQueryOptions, SearchResponse, SearchResult, SearchSort, SearchSubResult, SearchTimings, } from "./types.js";
|
|
4
5
|
/** Build a static Pagefind index from Farm's emitted HTML pages. */
|
|
5
|
-
export declare function search(options?: SearchOptions): import("@farm.js/core/plugin").FarmPlugin
|
|
6
|
+
export declare function search(options?: SearchOptions): import("@farm.js/core/plugin").FarmPlugin<undefined, Record<string, never>, void, FarmSearchPublicConfig, unknown, false, import("@farm.js/core/plugin").PluginRoutes>;
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,YAAY,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,YAAY,EACV,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,oEAAoE;AACpE,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,0KAkGjD"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolveSearchOptions } from "./config.js";
|
|
|
5
5
|
export function search(options = {}) {
|
|
6
6
|
const resolved = resolveSearchOptions(options);
|
|
7
7
|
let configuredBasePath = "/";
|
|
8
|
+
let htmlBasePath = "/";
|
|
8
9
|
let generated = false;
|
|
9
10
|
const publicConfig = {
|
|
10
11
|
available: false,
|
|
@@ -20,6 +21,14 @@ export function search(options = {}) {
|
|
|
20
21
|
throw new Error("[farm:search] Configure search in one search() plugin instance");
|
|
21
22
|
}
|
|
22
23
|
configuredBasePath = normalizeBasePath(config.basePath);
|
|
24
|
+
// RouteManager emits ordinary SSG paths relative to the app, while its
|
|
25
|
+
// localized SSG paths already include basePath. Do not infer this from
|
|
26
|
+
// folder names: an application route can have the same name as basePath.
|
|
27
|
+
const i18n = config.i18n;
|
|
28
|
+
htmlBasePath =
|
|
29
|
+
i18n && typeof i18n === "object" && "enabled" in i18n && i18n.enabled
|
|
30
|
+
? configuredBasePath
|
|
31
|
+
: "/";
|
|
23
32
|
publicConfig.bundlePath = `${withBasePath(`/${resolved.output}`, configuredBasePath)}/`;
|
|
24
33
|
},
|
|
25
34
|
build: {
|
|
@@ -37,6 +46,7 @@ export function search(options = {}) {
|
|
|
37
46
|
publicDir: nitroConfig.output?.publicDir,
|
|
38
47
|
preset: nitroConfig.preset ?? "node-server",
|
|
39
48
|
basePath: configuredBasePath,
|
|
49
|
+
htmlBasePath,
|
|
40
50
|
options: resolved,
|
|
41
51
|
});
|
|
42
52
|
generated = true;
|
|
@@ -60,6 +70,7 @@ export function search(options = {}) {
|
|
|
60
70
|
outputDir: result.outputDir ?? `${result.root}/.farm/.output`,
|
|
61
71
|
preset: result.preset,
|
|
62
72
|
basePath: configuredBasePath,
|
|
73
|
+
htmlBasePath,
|
|
63
74
|
options: resolved,
|
|
64
75
|
});
|
|
65
76
|
generated = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farm.js/search",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"description": "Build-time static page search for Farm.js applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"farm.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"pagefind": "^1.5.2",
|
|
40
|
-
"@farm.js/core": "0.1.0-beta.
|
|
40
|
+
"@farm.js/core": "0.1.0-beta.108"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^20.10.5",
|