@farm.js/search 0.1.0-beta.0 → 0.1.0-beta.2
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 +65 -20
- package/dist/client-runtime.d.ts.map +1 -1
- package/dist/client-runtime.js +89 -32
- package/dist/config.js +20 -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,CAmF1F;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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, rm } from "node:fs/promises";
|
|
1
|
+
import { lstat, mkdir, readFile, readdir, realpath, rm } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import * as pagefind from "pagefind";
|
|
4
4
|
export async function writeSearchIndex(input) {
|
|
@@ -6,17 +6,18 @@ export async function writeSearchIndex(input) {
|
|
|
6
6
|
const basePath = normalizeBasePath(input.basePath);
|
|
7
7
|
const bundlePath = withBasePath(`/${input.options.output}`, basePath);
|
|
8
8
|
const outputPath = resolveOutputPath(publicDir, bundlePath);
|
|
9
|
+
await assertOutputPathInside(publicDir, outputPath);
|
|
9
10
|
const htmlFiles = await collectHtmlFiles(publicDir, outputPath);
|
|
10
11
|
const routes = htmlFiles
|
|
11
12
|
.map((file) => ({
|
|
12
13
|
file,
|
|
13
|
-
logicalRoute: stripBasePath(routeFromHtmlFile(path.relative(publicDir, file)),
|
|
14
|
+
logicalRoute: stripBasePath(routeFromHtmlFile(path.relative(publicDir, file)), input.htmlBasePath ?? "/"),
|
|
14
15
|
}))
|
|
15
16
|
.sort((left, right) => left.logicalRoute.localeCompare(right.logicalRoute));
|
|
16
17
|
const selected = routes.filter(({ logicalRoute }) => shouldIndexRoute(logicalRoute, input.options));
|
|
17
18
|
const skippedRoutes = routes
|
|
18
19
|
.filter(({ logicalRoute }) => !shouldIndexRoute(logicalRoute, input.options))
|
|
19
|
-
.map(({ logicalRoute }) =>
|
|
20
|
+
.map(({ logicalRoute }) => toResultUrl(logicalRoute, basePath));
|
|
20
21
|
if (selected.length === 0) {
|
|
21
22
|
throw new Error("[farm:search] No static HTML pages matched the configured routes. " +
|
|
22
23
|
"Mark searchable pages as static or update search include/exclude patterns.");
|
|
@@ -42,7 +43,7 @@ export async function writeSearchIndex(input) {
|
|
|
42
43
|
const indexedRoutes = [];
|
|
43
44
|
try {
|
|
44
45
|
for (const { file, logicalRoute } of selected) {
|
|
45
|
-
const route =
|
|
46
|
+
const route = toResultUrl(logicalRoute, basePath);
|
|
46
47
|
const result = await index.addHTMLFile({
|
|
47
48
|
url: route,
|
|
48
49
|
content: await readFile(file, "utf8"),
|
|
@@ -86,9 +87,6 @@ export function withBasePath(route, basePath) {
|
|
|
86
87
|
const normalizedRoute = route === "/" ? "/" : `/${route.replace(/^\/+|\/+$/g, "")}`;
|
|
87
88
|
if (normalizedBase === "/")
|
|
88
89
|
return normalizedRoute;
|
|
89
|
-
if (normalizedRoute === normalizedBase || normalizedRoute.startsWith(`${normalizedBase}/`)) {
|
|
90
|
-
return normalizedRoute;
|
|
91
|
-
}
|
|
92
90
|
return normalizedRoute === "/" ? normalizedBase : `${normalizedBase}${normalizedRoute}`;
|
|
93
91
|
}
|
|
94
92
|
export function stripBasePath(route, basePath) {
|
|
@@ -105,20 +103,31 @@ export function routeFromHtmlFile(file) {
|
|
|
105
103
|
const route = withoutIndex ? withoutIndex.replace(/\.html$/i, "").replace(/^\/+|\/+$/g, "") : "";
|
|
106
104
|
return route ? `/${route}` : "/";
|
|
107
105
|
}
|
|
106
|
+
function toResultUrl(route, basePath) {
|
|
107
|
+
// Routes came from decoded filesystem names. Encode each segment once, after
|
|
108
|
+
// filtering and removing any explicit HTML prefix; basePath is already a URL path.
|
|
109
|
+
const pathname = route
|
|
110
|
+
.split("/")
|
|
111
|
+
.map((segment) => encodeURIComponent(segment))
|
|
112
|
+
.join("/");
|
|
113
|
+
return withBasePath(pathname, basePath);
|
|
114
|
+
}
|
|
108
115
|
export function matchesRoutePattern(route, pattern) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return route === parent || route.startsWith(parent === "/" ? "/" : `${parent}/`);
|
|
112
|
-
}
|
|
116
|
+
const descendants = pattern.endsWith("/**");
|
|
117
|
+
const parentPattern = descendants ? pattern.slice(0, -3) : pattern;
|
|
113
118
|
let source = "";
|
|
114
|
-
for (let index = 0; index <
|
|
115
|
-
const character =
|
|
116
|
-
if (character === "*"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
for (let index = 0; index < parentPattern.length; index++) {
|
|
120
|
+
const character = parentPattern[index];
|
|
121
|
+
if (character === "*") {
|
|
122
|
+
// Collapse the whole run. Emitting one `.*` per adjacent pair turned
|
|
123
|
+
// `****` into `.*.*`, and each extra pair multiplied the ways a failing
|
|
124
|
+
// route could be split between them, so a pattern with a handful of
|
|
125
|
+
// consecutive stars hung the build instead of matching.
|
|
126
|
+
let run = 1;
|
|
127
|
+
while (parentPattern[index + run] === "*")
|
|
128
|
+
run++;
|
|
129
|
+
source += run > 1 ? ".*" : "[^/]*";
|
|
130
|
+
index += run - 1;
|
|
122
131
|
}
|
|
123
132
|
else if (character === "?") {
|
|
124
133
|
source += "[^/]";
|
|
@@ -127,6 +136,9 @@ export function matchesRoutePattern(route, pattern) {
|
|
|
127
136
|
source += character.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
128
137
|
}
|
|
129
138
|
}
|
|
139
|
+
// Compile the parent too: a terminal /** must not turn earlier wildcards into literals.
|
|
140
|
+
if (descendants)
|
|
141
|
+
source += parentPattern ? "(?:/.*)?" : "/.*";
|
|
130
142
|
return new RegExp(`^${source}/?$`).test(route);
|
|
131
143
|
}
|
|
132
144
|
function shouldIndexRoute(route, options) {
|
|
@@ -163,11 +175,44 @@ async function collectHtmlFiles(root, ignoredDirectory) {
|
|
|
163
175
|
function resolveOutputPath(publicDir, bundlePath) {
|
|
164
176
|
const outputPath = path.resolve(publicDir, `.${bundlePath}`);
|
|
165
177
|
const relative = path.relative(publicDir, outputPath);
|
|
166
|
-
if (!relative ||
|
|
178
|
+
if (!relative ||
|
|
179
|
+
relative === ".." ||
|
|
180
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
181
|
+
path.isAbsolute(relative)) {
|
|
167
182
|
throw new Error("[farm:search] Search output must stay inside the public output directory");
|
|
168
183
|
}
|
|
169
184
|
return outputPath;
|
|
170
185
|
}
|
|
186
|
+
async function assertOutputPathInside(publicDir, outputPath) {
|
|
187
|
+
const realPublicDir = await realpath(publicDir);
|
|
188
|
+
let existingAncestor = outputPath;
|
|
189
|
+
while (true) {
|
|
190
|
+
try {
|
|
191
|
+
existingAncestor = await realpath(existingAncestor);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error.code !== "ENOENT")
|
|
196
|
+
throw error;
|
|
197
|
+
try {
|
|
198
|
+
await lstat(existingAncestor);
|
|
199
|
+
throw new Error("[farm:search] Search output must stay inside the public output directory, including through symlinks");
|
|
200
|
+
}
|
|
201
|
+
catch (statError) {
|
|
202
|
+
if (statError.code !== "ENOENT")
|
|
203
|
+
throw statError;
|
|
204
|
+
}
|
|
205
|
+
const parent = path.dirname(existingAncestor);
|
|
206
|
+
if (parent === existingAncestor)
|
|
207
|
+
throw error;
|
|
208
|
+
existingAncestor = parent;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const relative = path.relative(realPublicDir, existingAncestor);
|
|
212
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
213
|
+
throw new Error("[farm:search] Search output must stay inside the public output directory, including through symlinks");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
171
216
|
function pagefindError(action, errors) {
|
|
172
217
|
const details = errors.length ? `:\n${errors.map((error) => ` - ${error}`).join("\n")}` : "";
|
|
173
218
|
return new Error(`[farm:search] Pagefind ${action}${details}`);
|
|
@@ -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/config.js
CHANGED
|
@@ -57,11 +57,30 @@ function normalizeOutput(value) {
|
|
|
57
57
|
value.includes("\\") ||
|
|
58
58
|
value.includes("?") ||
|
|
59
59
|
value.includes("#") ||
|
|
60
|
-
segments.some((segment) =>
|
|
60
|
+
segments.some((segment) => !isStableOutputSegment(segment))) {
|
|
61
61
|
throw new TypeError("Search output must stay inside the public output directory");
|
|
62
62
|
}
|
|
63
63
|
return normalized;
|
|
64
64
|
}
|
|
65
|
+
function isStableOutputSegment(segment) {
|
|
66
|
+
if (!segment)
|
|
67
|
+
return false;
|
|
68
|
+
let decoded = segment;
|
|
69
|
+
try {
|
|
70
|
+
decoded = decodeURIComponent(segment);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Malformed escapes remain literal in browser pathnames.
|
|
74
|
+
}
|
|
75
|
+
return (decoded !== "." &&
|
|
76
|
+
decoded !== ".." &&
|
|
77
|
+
!decoded.includes("/") &&
|
|
78
|
+
!decoded.includes("\\") &&
|
|
79
|
+
!Array.from(decoded).some((character) => {
|
|
80
|
+
const code = character.charCodeAt(0);
|
|
81
|
+
return code <= 31 || (code >= 127 && code <= 159);
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
65
84
|
function normalizeLanguage(value) {
|
|
66
85
|
const normalized = normalizeOptionalString(value, "language")?.toLowerCase();
|
|
67
86
|
if (normalized && !/^[a-z]{2}$/.test(normalized)) {
|
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;AAGvE,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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAGvE,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,6CAkGjD"}
|
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.2",
|
|
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.105"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^20.10.5",
|