@pas7/llm-seo 0.1.6
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/LICENSE +201 -0
- package/README.md +164 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +673 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/next/index.d.ts +292 -0
- package/dist/adapters/next/index.js +673 -0
- package/dist/adapters/next/index.js.map +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +2232 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/config.schema-DCnBx3Gm.d.ts +824 -0
- package/dist/core/index.d.ts +752 -0
- package/dist/core/index.js +1330 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1909 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.schema-B_z3rxRV.d.ts +384 -0
- package/dist/schema/index.d.ts +72 -0
- package/dist/schema/index.js +422 -0
- package/dist/schema/index.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1330 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
// src/core/normalize/url.ts
|
|
4
|
+
function normalizePath(path, preserveTrailingSlash = false) {
|
|
5
|
+
if (!path || path === "/") {
|
|
6
|
+
return "/";
|
|
7
|
+
}
|
|
8
|
+
const hadTrailingSlash = path.endsWith("/") && path !== "/";
|
|
9
|
+
let normalized = path.startsWith("/") ? path : `/${path}`;
|
|
10
|
+
normalized = normalized.replace(/\/{2,}/g, "/");
|
|
11
|
+
const segments = [];
|
|
12
|
+
const parts = normalized.split("/");
|
|
13
|
+
for (const part of parts) {
|
|
14
|
+
if (part === "." || part === "") {
|
|
15
|
+
continue;
|
|
16
|
+
} else if (part === "..") {
|
|
17
|
+
if (segments.length > 0) {
|
|
18
|
+
segments.pop();
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
segments.push(part);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let result = "/" + segments.join("/");
|
|
25
|
+
if (preserveTrailingSlash && hadTrailingSlash && result !== "/") {
|
|
26
|
+
result += "/";
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
function joinUrlParts(...parts) {
|
|
31
|
+
if (parts.length === 0) {
|
|
32
|
+
return "/";
|
|
33
|
+
}
|
|
34
|
+
const filteredParts = parts.filter((part) => part.length > 0);
|
|
35
|
+
if (filteredParts.length === 0) {
|
|
36
|
+
return "/";
|
|
37
|
+
}
|
|
38
|
+
const joined = filteredParts.map((part) => {
|
|
39
|
+
let p = part.replace(/^\/+/, "");
|
|
40
|
+
p = p.replace(/\/+$/, "");
|
|
41
|
+
return p;
|
|
42
|
+
}).filter((p) => p.length > 0).join("/");
|
|
43
|
+
return joined.length > 0 ? `/${joined}` : "/";
|
|
44
|
+
}
|
|
45
|
+
function normalizeUrl(options) {
|
|
46
|
+
const { baseUrl, path, trailingSlash, stripQuery = true, stripHash = true } = options;
|
|
47
|
+
let parsedBase;
|
|
48
|
+
try {
|
|
49
|
+
parsedBase = new URL(baseUrl);
|
|
50
|
+
} catch {
|
|
51
|
+
throw new TypeError(`Invalid baseUrl: ${baseUrl}`);
|
|
52
|
+
}
|
|
53
|
+
const shouldPreserveTrailingSlash = trailingSlash === "preserve";
|
|
54
|
+
const normalizedPath = normalizePath(path, shouldPreserveTrailingSlash);
|
|
55
|
+
let finalPath = normalizedPath;
|
|
56
|
+
if (trailingSlash === "always") {
|
|
57
|
+
if (!finalPath.endsWith("/")) {
|
|
58
|
+
finalPath = `${finalPath}/`;
|
|
59
|
+
}
|
|
60
|
+
} else if (trailingSlash === "never") {
|
|
61
|
+
if (finalPath !== "/" && finalPath.endsWith("/")) {
|
|
62
|
+
finalPath = finalPath.slice(0, -1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const protocol = parsedBase.protocol.toLowerCase();
|
|
66
|
+
let hostname = parsedBase.hostname.toLowerCase();
|
|
67
|
+
let port = parsedBase.port;
|
|
68
|
+
if (port) {
|
|
69
|
+
const isDefaultPort = protocol === "http:" && port === "80" || protocol === "https:" && port === "443";
|
|
70
|
+
if (!isDefaultPort) {
|
|
71
|
+
hostname = `${hostname}:${port}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
let fullUrl = `${protocol}//${hostname}${finalPath}`;
|
|
75
|
+
if (!stripQuery && parsedBase.search) {
|
|
76
|
+
fullUrl += parsedBase.search;
|
|
77
|
+
}
|
|
78
|
+
if (!stripHash && parsedBase.hash) {
|
|
79
|
+
fullUrl += parsedBase.hash;
|
|
80
|
+
}
|
|
81
|
+
return fullUrl;
|
|
82
|
+
}
|
|
83
|
+
function sortUrls(urls) {
|
|
84
|
+
return [...urls].sort((a, b) => a.localeCompare(b));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/core/normalize/sort.ts
|
|
88
|
+
function compareStrings(a, b) {
|
|
89
|
+
return a.localeCompare(b, "en", { sensitivity: "case", numeric: true });
|
|
90
|
+
}
|
|
91
|
+
function sortStrings(items) {
|
|
92
|
+
return [...items].sort(compareStrings);
|
|
93
|
+
}
|
|
94
|
+
function sortBy(items, keyFn) {
|
|
95
|
+
return [...items].sort((a, b) => compareStrings(keyFn(a), keyFn(b)));
|
|
96
|
+
}
|
|
97
|
+
function stableSortStrings(items) {
|
|
98
|
+
return [...items].sort((a, b) => a.localeCompare(b, "en", { sensitivity: "case", numeric: true }));
|
|
99
|
+
}
|
|
100
|
+
function countPathSegments(url) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = new URL(url);
|
|
103
|
+
const path = parsed.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
104
|
+
if (!path) return 0;
|
|
105
|
+
return path.split("/").length;
|
|
106
|
+
} catch {
|
|
107
|
+
const cleaned = url.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
108
|
+
if (!cleaned) return 0;
|
|
109
|
+
return cleaned.split("/").length;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function sortUrlsByPath(urls) {
|
|
113
|
+
return [...urls].sort((a, b) => {
|
|
114
|
+
const segmentsA = countPathSegments(a);
|
|
115
|
+
const segmentsB = countPathSegments(b);
|
|
116
|
+
if (segmentsA !== segmentsB) {
|
|
117
|
+
return segmentsA - segmentsB;
|
|
118
|
+
}
|
|
119
|
+
return a.localeCompare(b, "en", { sensitivity: "case", numeric: true });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/core/normalize/text.ts
|
|
124
|
+
function normalizeWhitespace(text) {
|
|
125
|
+
return text.replace(/\s+/g, " ").trim();
|
|
126
|
+
}
|
|
127
|
+
function normalizeLineEndings(text, lineEndings) {
|
|
128
|
+
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
129
|
+
return lineEndings === "crlf" ? normalized.replace(/\n/g, "\r\n") : normalized;
|
|
130
|
+
}
|
|
131
|
+
function normalizeLineWhitespace(text) {
|
|
132
|
+
const lines = text.split(/\r?\n/);
|
|
133
|
+
return lines.map((line) => line.trimEnd().replace(/[ \t]+/g, (match) => " ".repeat(match.length === 0 ? 0 : match.length))).map((line) => line.replace(/ +/g, " ")).join("\n");
|
|
134
|
+
}
|
|
135
|
+
function normalizeSeoText(text, maxLength) {
|
|
136
|
+
const normalized = normalizeWhitespace(text);
|
|
137
|
+
if (normalized.length <= maxLength) {
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
const truncated = normalized.slice(0, maxLength);
|
|
141
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
142
|
+
return lastSpace > 0 ? `${truncated.slice(0, lastSpace)}\u2026` : `${truncated.slice(0, -1)}\u2026`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/core/canonical/locale.ts
|
|
146
|
+
function selectCanonicalLocale(options) {
|
|
147
|
+
const { defaultLocale, availableLocales } = options;
|
|
148
|
+
if (!availableLocales || availableLocales.length === 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const validLocales = availableLocales.filter(
|
|
152
|
+
(locale) => typeof locale === "string" && locale.length > 0
|
|
153
|
+
);
|
|
154
|
+
if (validLocales.length === 0) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (defaultLocale && validLocales.includes(defaultLocale)) {
|
|
158
|
+
return defaultLocale;
|
|
159
|
+
}
|
|
160
|
+
const sorted = stableSortStrings(validLocales);
|
|
161
|
+
return sorted[0] ?? null;
|
|
162
|
+
}
|
|
163
|
+
function localizePath(path, locale, config) {
|
|
164
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
165
|
+
if (locale === config.default && config.strategy === "subdirectory") {
|
|
166
|
+
return normalizedPath;
|
|
167
|
+
}
|
|
168
|
+
if (config.strategy === "subdirectory") {
|
|
169
|
+
return `/${locale}${normalizedPath === "/" ? "" : normalizedPath}`;
|
|
170
|
+
}
|
|
171
|
+
return normalizedPath;
|
|
172
|
+
}
|
|
173
|
+
function extractLocaleFromPath(path, config) {
|
|
174
|
+
if (config.strategy !== "subdirectory") {
|
|
175
|
+
return [config.default, path];
|
|
176
|
+
}
|
|
177
|
+
const match = path.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)(\/|$)/);
|
|
178
|
+
if (match?.[1] && config.supported.includes(match[1])) {
|
|
179
|
+
const locale = match[1];
|
|
180
|
+
const remainingPath = path.slice(locale.length + 1) || "/";
|
|
181
|
+
return [locale, remainingPath];
|
|
182
|
+
}
|
|
183
|
+
return [config.default, path];
|
|
184
|
+
}
|
|
185
|
+
function generateAlternateUrls(baseUrl, path, config) {
|
|
186
|
+
const urlMap = /* @__PURE__ */ new Map();
|
|
187
|
+
for (const locale of config.supported) {
|
|
188
|
+
const localePath = localizePath(path, locale, config);
|
|
189
|
+
const fullUrl = `${baseUrl}${localePath}`;
|
|
190
|
+
urlMap.set(locale, fullUrl);
|
|
191
|
+
}
|
|
192
|
+
return urlMap;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/core/canonical/canonical-from-manifest.ts
|
|
196
|
+
function generateCanonicalUrl(manifest, path, options = {}) {
|
|
197
|
+
const { trailingSlash = false, lowercase = true } = options;
|
|
198
|
+
const basePath = path.startsWith("/") ? path : `/${path}`;
|
|
199
|
+
const normalizedPath = trailingSlash ? `${basePath}${basePath.endsWith("/") ? "" : "/"}` : basePath.replace(/\/+$/, "") || "/";
|
|
200
|
+
const fullUrl = `${manifest.baseUrl}${normalizedPath}`;
|
|
201
|
+
return lowercase ? fullUrl.toLowerCase() : fullUrl;
|
|
202
|
+
}
|
|
203
|
+
function extractCanonicalUrls(manifest, options = {}) {
|
|
204
|
+
return manifest.pages.map((page) => generateCanonicalUrl(manifest, page.path, options));
|
|
205
|
+
}
|
|
206
|
+
function dedupeUrls(urls) {
|
|
207
|
+
return [...new Set(urls)];
|
|
208
|
+
}
|
|
209
|
+
function buildLocalePrefix(locale, strategy, defaultLocale) {
|
|
210
|
+
if (strategy === "none") {
|
|
211
|
+
return "";
|
|
212
|
+
}
|
|
213
|
+
if (strategy === "subdomain") {
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
if (strategy === "prefix" && locale === defaultLocale) {
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
return `/${locale}`;
|
|
220
|
+
}
|
|
221
|
+
function buildBaseUrlWithSubdomain(baseUrl, locale, strategy, defaultLocale) {
|
|
222
|
+
if (strategy !== "subdomain" || locale === defaultLocale) {
|
|
223
|
+
return baseUrl;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const parsed = new URL(baseUrl);
|
|
227
|
+
return `${parsed.protocol}//${locale}.${parsed.host}`;
|
|
228
|
+
} catch {
|
|
229
|
+
return baseUrl;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function createCanonicalUrlForItem(item, options) {
|
|
233
|
+
const { baseUrl, routePrefix, defaultLocale, trailingSlash, localeStrategy } = options;
|
|
234
|
+
if (item.canonicalOverride && typeof item.canonicalOverride === "string") {
|
|
235
|
+
return item.canonicalOverride;
|
|
236
|
+
}
|
|
237
|
+
const availableLocales = item.locales ?? [defaultLocale];
|
|
238
|
+
const canonicalLocale = selectCanonicalLocale({
|
|
239
|
+
defaultLocale,
|
|
240
|
+
availableLocales
|
|
241
|
+
});
|
|
242
|
+
const locale = canonicalLocale ?? defaultLocale;
|
|
243
|
+
const localePrefix = buildLocalePrefix(locale, localeStrategy, defaultLocale);
|
|
244
|
+
const effectiveBaseUrl = buildBaseUrlWithSubdomain(baseUrl, locale, localeStrategy, defaultLocale);
|
|
245
|
+
const parts = [];
|
|
246
|
+
if (localePrefix) {
|
|
247
|
+
parts.push(localePrefix);
|
|
248
|
+
}
|
|
249
|
+
if (routePrefix) {
|
|
250
|
+
parts.push(routePrefix);
|
|
251
|
+
}
|
|
252
|
+
parts.push(item.slug);
|
|
253
|
+
const fullPath = joinUrlParts(...parts);
|
|
254
|
+
return normalizeUrl({
|
|
255
|
+
baseUrl: effectiveBaseUrl,
|
|
256
|
+
path: fullPath,
|
|
257
|
+
trailingSlash,
|
|
258
|
+
stripQuery: true,
|
|
259
|
+
stripHash: true
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function createCanonicalUrlsFromManifest(options) {
|
|
263
|
+
const { items } = options;
|
|
264
|
+
if (!items || items.length === 0) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
const urls = items.map((item) => createCanonicalUrlForItem(item, options));
|
|
268
|
+
const deduped = dedupeUrls(urls);
|
|
269
|
+
return sortUrlsByPath(deduped);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/core/generate/llms-txt.ts
|
|
273
|
+
function createLlmsTxt(options) {
|
|
274
|
+
const { config, canonicalUrls } = options;
|
|
275
|
+
const lineEndings = config.format?.lineEndings ?? "lf";
|
|
276
|
+
const lines = [];
|
|
277
|
+
lines.push(`# ${config.brand.name}`);
|
|
278
|
+
lines.push("");
|
|
279
|
+
if (config.brand.tagline) {
|
|
280
|
+
lines.push(`> ${config.brand.tagline}`);
|
|
281
|
+
lines.push("");
|
|
282
|
+
}
|
|
283
|
+
if (config.brand.description) {
|
|
284
|
+
lines.push(config.brand.description);
|
|
285
|
+
lines.push("");
|
|
286
|
+
}
|
|
287
|
+
const hubs = config.sections?.hubs ?? [];
|
|
288
|
+
if (hubs.length > 0) {
|
|
289
|
+
lines.push("## Sections");
|
|
290
|
+
lines.push("");
|
|
291
|
+
const sortedHubs = sortStrings(hubs);
|
|
292
|
+
for (const hub of sortedHubs) {
|
|
293
|
+
const hubLabel = getHubLabel(hub);
|
|
294
|
+
lines.push(`- [${hub}](${hub}) - ${hubLabel}`);
|
|
295
|
+
}
|
|
296
|
+
lines.push("");
|
|
297
|
+
}
|
|
298
|
+
if (canonicalUrls.length > 0) {
|
|
299
|
+
lines.push("## URLs");
|
|
300
|
+
lines.push("");
|
|
301
|
+
const sortedUrls = sortStrings(canonicalUrls);
|
|
302
|
+
for (const url of sortedUrls) {
|
|
303
|
+
lines.push(`- ${url}`);
|
|
304
|
+
}
|
|
305
|
+
lines.push("");
|
|
306
|
+
}
|
|
307
|
+
const hasPolicies = config.policy?.geoPolicy || config.policy?.citationRules || config.policy?.restrictedClaims;
|
|
308
|
+
if (hasPolicies) {
|
|
309
|
+
lines.push("## Policies");
|
|
310
|
+
lines.push("");
|
|
311
|
+
if (config.policy?.geoPolicy) {
|
|
312
|
+
lines.push(`- GEO: ${config.policy.geoPolicy}`);
|
|
313
|
+
}
|
|
314
|
+
if (config.policy?.citationRules) {
|
|
315
|
+
lines.push(`- Citations: ${config.policy.citationRules}`);
|
|
316
|
+
}
|
|
317
|
+
if (config.policy?.restrictedClaims) {
|
|
318
|
+
const status = config.policy.restrictedClaims.enable ? "Enabled" : "Disabled";
|
|
319
|
+
lines.push(`- Restricted Claims: ${status}`);
|
|
320
|
+
}
|
|
321
|
+
lines.push("");
|
|
322
|
+
}
|
|
323
|
+
const hasContact = config.contact?.email || config.contact?.social || config.contact?.phone;
|
|
324
|
+
const hasBooking = config.booking?.url;
|
|
325
|
+
if (hasContact || hasBooking) {
|
|
326
|
+
lines.push("## Contact");
|
|
327
|
+
lines.push("");
|
|
328
|
+
if (config.contact?.email) {
|
|
329
|
+
lines.push(`- Email: ${config.contact.email}`);
|
|
330
|
+
}
|
|
331
|
+
if (config.contact?.phone) {
|
|
332
|
+
lines.push(`- Phone: ${config.contact.phone}`);
|
|
333
|
+
}
|
|
334
|
+
if (config.contact?.social) {
|
|
335
|
+
if (config.contact.social.twitter) {
|
|
336
|
+
lines.push(`- Twitter: ${config.contact.social.twitter}`);
|
|
337
|
+
}
|
|
338
|
+
if (config.contact.social.linkedin) {
|
|
339
|
+
lines.push(`- LinkedIn: ${config.contact.social.linkedin}`);
|
|
340
|
+
}
|
|
341
|
+
if (config.contact.social.github) {
|
|
342
|
+
lines.push(`- GitHub: ${config.contact.social.github}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (config.booking?.url) {
|
|
346
|
+
const label = config.booking.label ?? "Book consultation";
|
|
347
|
+
lines.push(`- Booking: ${config.booking.url} (${label})`);
|
|
348
|
+
}
|
|
349
|
+
lines.push("");
|
|
350
|
+
}
|
|
351
|
+
const hasMachineHints = config.machineHints?.robots || config.machineHints?.sitemap || config.machineHints?.llmsTxt || config.machineHints?.llmsFullTxt;
|
|
352
|
+
if (hasMachineHints) {
|
|
353
|
+
lines.push("## Machine Hints");
|
|
354
|
+
lines.push("");
|
|
355
|
+
if (config.machineHints?.robots) {
|
|
356
|
+
lines.push(`- robots.txt: ${config.machineHints.robots}`);
|
|
357
|
+
}
|
|
358
|
+
if (config.machineHints?.sitemap) {
|
|
359
|
+
lines.push(`- sitemap.xml: ${config.machineHints.sitemap}`);
|
|
360
|
+
}
|
|
361
|
+
if (config.machineHints?.llmsTxt) {
|
|
362
|
+
lines.push(`- llms.txt: ${config.machineHints.llmsTxt}`);
|
|
363
|
+
}
|
|
364
|
+
if (config.machineHints?.llmsFullTxt) {
|
|
365
|
+
lines.push(`- llms-full.txt: ${config.machineHints.llmsFullTxt}`);
|
|
366
|
+
}
|
|
367
|
+
lines.push("");
|
|
368
|
+
}
|
|
369
|
+
let content = lines.join("\n");
|
|
370
|
+
content = normalizeLineWhitespace(content);
|
|
371
|
+
content = normalizeLineEndings(content, lineEndings);
|
|
372
|
+
const finalLines = content.split(lineEndings === "crlf" ? "\r\n" : "\n");
|
|
373
|
+
return {
|
|
374
|
+
content,
|
|
375
|
+
byteSize: Buffer.byteLength(content, "utf-8"),
|
|
376
|
+
lineCount: finalLines.length
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function getHubLabel(hub) {
|
|
380
|
+
const labels = {
|
|
381
|
+
"/services": "Services overview",
|
|
382
|
+
"/blog": "Blog posts",
|
|
383
|
+
"/projects": "Our projects",
|
|
384
|
+
"/cases": "Case studies",
|
|
385
|
+
"/contact": "Contact us",
|
|
386
|
+
"/about": "About us",
|
|
387
|
+
"/products": "Products",
|
|
388
|
+
"/docs": "Documentation",
|
|
389
|
+
"/faq": "Frequently asked questions",
|
|
390
|
+
"/pricing": "Pricing information",
|
|
391
|
+
"/team": "Our team",
|
|
392
|
+
"/careers": "Career opportunities",
|
|
393
|
+
"/news": "News and updates",
|
|
394
|
+
"/resources": "Resources",
|
|
395
|
+
"/support": "Support center"
|
|
396
|
+
};
|
|
397
|
+
return labels[hub] ?? formatHubLabel(hub);
|
|
398
|
+
}
|
|
399
|
+
function formatHubLabel(hub) {
|
|
400
|
+
const clean = hub.replace(/^\//, "");
|
|
401
|
+
return clean.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
402
|
+
}
|
|
403
|
+
function generateLlmsTxt(manifest, _options) {
|
|
404
|
+
const canonicalUrls = manifest.pages.map((page) => `${manifest.baseUrl}${page.path}`);
|
|
405
|
+
const config = {
|
|
406
|
+
site: { baseUrl: manifest.baseUrl },
|
|
407
|
+
brand: {
|
|
408
|
+
name: manifest.title,
|
|
409
|
+
locales: ["en"],
|
|
410
|
+
...manifest.description && { description: manifest.description }
|
|
411
|
+
},
|
|
412
|
+
manifests: {},
|
|
413
|
+
output: {
|
|
414
|
+
paths: {
|
|
415
|
+
llmsTxt: "public/llms.txt",
|
|
416
|
+
llmsFullTxt: "public/llms-full.txt"
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
return createLlmsTxt({ config, canonicalUrls }).content;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/core/generate/llms-full-txt.ts
|
|
424
|
+
function createLlmsFullTxt(options) {
|
|
425
|
+
const { config, canonicalUrls, manifestItems } = options;
|
|
426
|
+
const lineEndings = config.format?.lineEndings ?? "lf";
|
|
427
|
+
const lines = [];
|
|
428
|
+
lines.push(`# ${config.brand.name} - Full LLM Context`);
|
|
429
|
+
lines.push("");
|
|
430
|
+
if (config.brand.tagline) {
|
|
431
|
+
lines.push(`> ${config.brand.tagline}`);
|
|
432
|
+
lines.push("");
|
|
433
|
+
}
|
|
434
|
+
if (config.brand.description) {
|
|
435
|
+
lines.push(config.brand.description);
|
|
436
|
+
lines.push("");
|
|
437
|
+
}
|
|
438
|
+
if (config.brand.org) {
|
|
439
|
+
lines.push(`Organization: ${config.brand.org}`);
|
|
440
|
+
}
|
|
441
|
+
lines.push(`Locales: ${config.brand.locales.join(", ")}`);
|
|
442
|
+
lines.push("");
|
|
443
|
+
if (canonicalUrls.length > 0) {
|
|
444
|
+
lines.push("## All Canonical URLs");
|
|
445
|
+
lines.push("");
|
|
446
|
+
const sortedUrls = sortStrings(canonicalUrls);
|
|
447
|
+
for (const url of sortedUrls) {
|
|
448
|
+
lines.push(`- ${url}`);
|
|
449
|
+
}
|
|
450
|
+
lines.push("");
|
|
451
|
+
}
|
|
452
|
+
const hasPolicies = config.policy?.geoPolicy || config.policy?.citationRules || config.policy?.restrictedClaims;
|
|
453
|
+
if (hasPolicies) {
|
|
454
|
+
lines.push("## Policies");
|
|
455
|
+
lines.push("");
|
|
456
|
+
if (config.policy?.geoPolicy) {
|
|
457
|
+
lines.push("### GEO Policy");
|
|
458
|
+
lines.push(config.policy.geoPolicy);
|
|
459
|
+
lines.push("");
|
|
460
|
+
}
|
|
461
|
+
if (config.policy?.citationRules) {
|
|
462
|
+
lines.push("### Citation Rules");
|
|
463
|
+
lines.push(config.policy.citationRules);
|
|
464
|
+
lines.push("");
|
|
465
|
+
}
|
|
466
|
+
if (config.policy?.restrictedClaims) {
|
|
467
|
+
lines.push("### Restricted Claims");
|
|
468
|
+
const status = config.policy.restrictedClaims.enable ? "Enabled" : "Disabled";
|
|
469
|
+
lines.push(`Status: ${status}`);
|
|
470
|
+
if (config.policy.restrictedClaims.forbidden && config.policy.restrictedClaims.forbidden.length > 0) {
|
|
471
|
+
lines.push(`Forbidden terms: ${config.policy.restrictedClaims.forbidden.join(", ")}`);
|
|
472
|
+
}
|
|
473
|
+
if (config.policy.restrictedClaims.whitelist && config.policy.restrictedClaims.whitelist.length > 0) {
|
|
474
|
+
lines.push(`Exceptions: ${config.policy.restrictedClaims.whitelist.join(", ")}`);
|
|
475
|
+
}
|
|
476
|
+
lines.push("");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const hasSocial = config.contact?.social?.twitter || config.contact?.social?.linkedin || config.contact?.social?.github;
|
|
480
|
+
const hasBooking = config.booking?.url;
|
|
481
|
+
if (hasSocial || hasBooking) {
|
|
482
|
+
lines.push("## Social & Booking");
|
|
483
|
+
lines.push("");
|
|
484
|
+
if (config.contact?.social?.twitter) {
|
|
485
|
+
lines.push(`- Twitter: ${config.contact.social.twitter}`);
|
|
486
|
+
}
|
|
487
|
+
if (config.contact?.social?.linkedin) {
|
|
488
|
+
lines.push(`- LinkedIn: ${config.contact.social.linkedin}`);
|
|
489
|
+
}
|
|
490
|
+
if (config.contact?.social?.github) {
|
|
491
|
+
lines.push(`- GitHub: ${config.contact.social.github}`);
|
|
492
|
+
}
|
|
493
|
+
if (config.booking?.url) {
|
|
494
|
+
const label = config.booking.label ?? "Book consultation";
|
|
495
|
+
lines.push(`- Booking: ${config.booking.url} (${label})`);
|
|
496
|
+
}
|
|
497
|
+
lines.push("");
|
|
498
|
+
}
|
|
499
|
+
const hasMachineHints = config.machineHints?.robots || config.machineHints?.sitemap || config.machineHints?.llmsTxt || config.machineHints?.llmsFullTxt;
|
|
500
|
+
if (hasMachineHints) {
|
|
501
|
+
lines.push("## Machine Hints");
|
|
502
|
+
lines.push("");
|
|
503
|
+
if (config.machineHints?.robots) {
|
|
504
|
+
lines.push(`- robots.txt: ${config.machineHints.robots}`);
|
|
505
|
+
}
|
|
506
|
+
if (config.machineHints?.sitemap) {
|
|
507
|
+
lines.push(`- sitemap.xml: ${config.machineHints.sitemap}`);
|
|
508
|
+
}
|
|
509
|
+
if (config.machineHints?.llmsTxt) {
|
|
510
|
+
lines.push(`- llms.txt: ${config.machineHints.llmsTxt}`);
|
|
511
|
+
}
|
|
512
|
+
if (config.machineHints?.llmsFullTxt) {
|
|
513
|
+
lines.push(`- llms-full.txt: ${config.machineHints.llmsFullTxt}`);
|
|
514
|
+
}
|
|
515
|
+
lines.push("");
|
|
516
|
+
}
|
|
517
|
+
const hubs = config.sections?.hubs ?? [];
|
|
518
|
+
if (hubs.length > 0 || manifestItems.length > 0) {
|
|
519
|
+
lines.push("## Sitemap");
|
|
520
|
+
lines.push("");
|
|
521
|
+
if (hubs.length > 0) {
|
|
522
|
+
const sortedHubs = sortStrings(hubs);
|
|
523
|
+
for (const hub of sortedHubs) {
|
|
524
|
+
lines.push(`- [${hub}](${hub}) - ${getHubLabel2(hub)}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (manifestItems.length > 0) {
|
|
528
|
+
const sortedItems = sortBy(manifestItems, (item) => item.slug);
|
|
529
|
+
for (const item of sortedItems) {
|
|
530
|
+
const url = item.canonicalOverride ?? `${config.site.baseUrl}${item.slug}`;
|
|
531
|
+
const title = item.title ?? item.slug;
|
|
532
|
+
const locales = item.locales?.join(", ") ?? config.brand.locales[0] ?? "en";
|
|
533
|
+
lines.push(`- [${title}](${url}) (${locales})`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
lines.push("");
|
|
537
|
+
}
|
|
538
|
+
let content = lines.join("\n");
|
|
539
|
+
content = normalizeLineWhitespace(content);
|
|
540
|
+
content = normalizeLineEndings(content, lineEndings);
|
|
541
|
+
const finalLines = content.split(lineEndings === "crlf" ? "\r\n" : "\n");
|
|
542
|
+
return {
|
|
543
|
+
content,
|
|
544
|
+
byteSize: Buffer.byteLength(content, "utf-8"),
|
|
545
|
+
lineCount: finalLines.length
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function getHubLabel2(hub) {
|
|
549
|
+
const labels = {
|
|
550
|
+
"/services": "Services overview",
|
|
551
|
+
"/blog": "Blog posts",
|
|
552
|
+
"/projects": "Our projects",
|
|
553
|
+
"/cases": "Case studies",
|
|
554
|
+
"/contact": "Contact us",
|
|
555
|
+
"/about": "About us",
|
|
556
|
+
"/products": "Products",
|
|
557
|
+
"/docs": "Documentation",
|
|
558
|
+
"/faq": "Frequently asked questions",
|
|
559
|
+
"/pricing": "Pricing information",
|
|
560
|
+
"/team": "Our team",
|
|
561
|
+
"/careers": "Career opportunities",
|
|
562
|
+
"/news": "News and updates",
|
|
563
|
+
"/resources": "Resources",
|
|
564
|
+
"/support": "Support center"
|
|
565
|
+
};
|
|
566
|
+
return labels[hub] ?? formatHubLabel2(hub);
|
|
567
|
+
}
|
|
568
|
+
function formatHubLabel2(hub) {
|
|
569
|
+
const clean = hub.replace(/^\//, "");
|
|
570
|
+
return clean.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
571
|
+
}
|
|
572
|
+
function generatePageContent(page, manifest, _options) {
|
|
573
|
+
const maxContentLength = _options?.maxContentLength ?? 0;
|
|
574
|
+
const lines = [];
|
|
575
|
+
const url = `${manifest.baseUrl}${page.path}`;
|
|
576
|
+
lines.push(`## ${page.title ?? page.path}`);
|
|
577
|
+
lines.push(`URL: ${url}`);
|
|
578
|
+
lines.push("");
|
|
579
|
+
if (page.description) {
|
|
580
|
+
lines.push(page.description);
|
|
581
|
+
lines.push("");
|
|
582
|
+
}
|
|
583
|
+
if (page.content) {
|
|
584
|
+
let content = page.content;
|
|
585
|
+
if (maxContentLength > 0 && content.length > maxContentLength) {
|
|
586
|
+
content = `${content.slice(0, maxContentLength)}...`;
|
|
587
|
+
}
|
|
588
|
+
lines.push(content);
|
|
589
|
+
lines.push("");
|
|
590
|
+
}
|
|
591
|
+
return lines.join("\n");
|
|
592
|
+
}
|
|
593
|
+
function generateLlmsFullTxt(manifest, _options) {
|
|
594
|
+
const canonicalUrls = manifest.pages.map((page) => `${manifest.baseUrl}${page.path}`);
|
|
595
|
+
const manifestItems = manifest.pages.map((page) => ({
|
|
596
|
+
slug: page.path,
|
|
597
|
+
title: page.title,
|
|
598
|
+
description: page.description
|
|
599
|
+
}));
|
|
600
|
+
const config = {
|
|
601
|
+
site: { baseUrl: manifest.baseUrl },
|
|
602
|
+
brand: {
|
|
603
|
+
name: manifest.title,
|
|
604
|
+
locales: ["en"],
|
|
605
|
+
...manifest.description && { description: manifest.description }
|
|
606
|
+
},
|
|
607
|
+
manifests: {},
|
|
608
|
+
output: {
|
|
609
|
+
paths: {
|
|
610
|
+
llmsTxt: "public/llms.txt",
|
|
611
|
+
llmsFullTxt: "public/llms-full.txt"
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
return createLlmsFullTxt({ config, canonicalUrls, manifestItems }).content;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/core/generate/citations.ts
|
|
619
|
+
function createCitationsJson(options) {
|
|
620
|
+
const { config, manifestItems, sectionName, fixedTimestamp } = options;
|
|
621
|
+
const sources = manifestItems.map((item) => {
|
|
622
|
+
const url = item.canonicalOverride ?? `${config.site.baseUrl}${item.slug}`;
|
|
623
|
+
const defaultLocale = config.site.defaultLocale ?? config.brand.locales[0] ?? "en";
|
|
624
|
+
return {
|
|
625
|
+
url,
|
|
626
|
+
priority: item.priority ?? 50,
|
|
627
|
+
section: sectionName,
|
|
628
|
+
locale: item.locales?.[0] ?? defaultLocale,
|
|
629
|
+
...item.publishedAt && { publishedAt: item.publishedAt },
|
|
630
|
+
...item.updatedAt && { updatedAt: item.updatedAt },
|
|
631
|
+
...item.title && { title: item.title }
|
|
632
|
+
};
|
|
633
|
+
});
|
|
634
|
+
const sortedSources = sources.sort((a, b) => {
|
|
635
|
+
if (a.priority !== b.priority) {
|
|
636
|
+
return b.priority - a.priority;
|
|
637
|
+
}
|
|
638
|
+
return a.url.localeCompare(b.url, "en", { sensitivity: "case", numeric: true });
|
|
639
|
+
});
|
|
640
|
+
const policy = {
|
|
641
|
+
restrictedClaimsEnabled: config.policy?.restrictedClaims?.enable ?? false,
|
|
642
|
+
...config.policy?.geoPolicy && { geoPolicy: config.policy.geoPolicy },
|
|
643
|
+
...config.policy?.citationRules && { citationRules: config.policy.citationRules }
|
|
644
|
+
};
|
|
645
|
+
return {
|
|
646
|
+
version: "1.0",
|
|
647
|
+
generated: fixedTimestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
648
|
+
site: {
|
|
649
|
+
baseUrl: config.site.baseUrl,
|
|
650
|
+
name: config.brand.name
|
|
651
|
+
},
|
|
652
|
+
sources: sortedSources,
|
|
653
|
+
policy
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function createCitationsJsonString(options) {
|
|
657
|
+
const citations = createCitationsJson(options);
|
|
658
|
+
return JSON.stringify(citations, null, 2);
|
|
659
|
+
}
|
|
660
|
+
function createCitation(page, manifest) {
|
|
661
|
+
const citation = {
|
|
662
|
+
url: `${manifest.baseUrl}${page.path}`,
|
|
663
|
+
title: page.title ?? page.path
|
|
664
|
+
};
|
|
665
|
+
if (page.description) {
|
|
666
|
+
citation.description = page.description;
|
|
667
|
+
}
|
|
668
|
+
return citation;
|
|
669
|
+
}
|
|
670
|
+
function citationToMarkdown(citation) {
|
|
671
|
+
if (citation.description) {
|
|
672
|
+
return `[${citation.title}](${citation.url}) - ${citation.description}`;
|
|
673
|
+
}
|
|
674
|
+
return `[${citation.title}](${citation.url})`;
|
|
675
|
+
}
|
|
676
|
+
function citationToJsonLd(citation) {
|
|
677
|
+
const jsonLd = {
|
|
678
|
+
"@type": "WebPage",
|
|
679
|
+
name: citation.title,
|
|
680
|
+
url: citation.url
|
|
681
|
+
};
|
|
682
|
+
if (citation.description) {
|
|
683
|
+
jsonLd.description = citation.description;
|
|
684
|
+
}
|
|
685
|
+
return jsonLd;
|
|
686
|
+
}
|
|
687
|
+
function generateReferenceList(citations) {
|
|
688
|
+
const lines = ["## References", ""];
|
|
689
|
+
for (let i = 0; i < citations.length; i++) {
|
|
690
|
+
const citation = citations[i];
|
|
691
|
+
if (citation) {
|
|
692
|
+
const num = i + 1;
|
|
693
|
+
lines.push(`${num}. ${citationToMarkdown(citation)}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
lines.push("");
|
|
697
|
+
return lines.join("\n");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/core/check/issues.ts
|
|
701
|
+
function createCheckIssue(severity, code, message, path = "", context) {
|
|
702
|
+
const issue = {
|
|
703
|
+
path,
|
|
704
|
+
code,
|
|
705
|
+
message,
|
|
706
|
+
severity
|
|
707
|
+
};
|
|
708
|
+
if (context !== void 0) {
|
|
709
|
+
issue.context = context;
|
|
710
|
+
}
|
|
711
|
+
return issue;
|
|
712
|
+
}
|
|
713
|
+
function createIssue(overrides) {
|
|
714
|
+
return {
|
|
715
|
+
category: "content",
|
|
716
|
+
...overrides
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function groupBySeverity(issues) {
|
|
720
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
721
|
+
for (const issue of issues) {
|
|
722
|
+
const existing = grouped.get(issue.severity) ?? [];
|
|
723
|
+
existing.push(issue);
|
|
724
|
+
grouped.set(issue.severity, existing);
|
|
725
|
+
}
|
|
726
|
+
return grouped;
|
|
727
|
+
}
|
|
728
|
+
function groupByPage(issues) {
|
|
729
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
730
|
+
for (const issue of issues) {
|
|
731
|
+
const existing = grouped.get(issue.pageId) ?? [];
|
|
732
|
+
existing.push(issue);
|
|
733
|
+
grouped.set(issue.pageId, existing);
|
|
734
|
+
}
|
|
735
|
+
return grouped;
|
|
736
|
+
}
|
|
737
|
+
function filterBySeverity(issues, minSeverity) {
|
|
738
|
+
const severityOrder = ["error", "warning", "info"];
|
|
739
|
+
const minIndex = severityOrder.indexOf(minSeverity);
|
|
740
|
+
return issues.filter((issue) => {
|
|
741
|
+
const issueIndex = severityOrder.indexOf(issue.severity);
|
|
742
|
+
return issueIndex <= minIndex;
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function countSeverities(issues) {
|
|
746
|
+
const counts = {
|
|
747
|
+
error: 0,
|
|
748
|
+
warning: 0,
|
|
749
|
+
info: 0
|
|
750
|
+
};
|
|
751
|
+
for (const issue of issues) {
|
|
752
|
+
counts[issue.severity]++;
|
|
753
|
+
}
|
|
754
|
+
return counts;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/core/check/rules-linter.ts
|
|
758
|
+
var LINT_RULES = [
|
|
759
|
+
{
|
|
760
|
+
id: "heading-structure",
|
|
761
|
+
description: "Ensures proper heading structure (h1 -> h2 -> h3)",
|
|
762
|
+
enabled: true,
|
|
763
|
+
lint: lintHeadingStructure
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
id: "url-format",
|
|
767
|
+
description: "Validates URL format in links",
|
|
768
|
+
enabled: true,
|
|
769
|
+
lint: lintUrlFormat
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
id: "trailing-whitespace",
|
|
773
|
+
description: "Checks for trailing whitespace on lines",
|
|
774
|
+
enabled: true,
|
|
775
|
+
lint: lintTrailingWhitespace
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
id: "consistent-list-markers",
|
|
779
|
+
description: "Ensures consistent list marker usage",
|
|
780
|
+
enabled: true,
|
|
781
|
+
lint: lintListMarkers
|
|
782
|
+
}
|
|
783
|
+
];
|
|
784
|
+
function lintContent(content, filePath, rules = LINT_RULES.filter((r) => r.enabled)) {
|
|
785
|
+
const issues = [];
|
|
786
|
+
for (const rule of rules) {
|
|
787
|
+
const ruleIssues = rule.lint(content, filePath);
|
|
788
|
+
issues.push(...ruleIssues);
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
filePath,
|
|
792
|
+
issues,
|
|
793
|
+
passed: issues.filter((i) => i.severity === "error").length === 0
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function checkForbiddenTerms(content, forbidden, whitelist = []) {
|
|
797
|
+
const issues = [];
|
|
798
|
+
const lines = content.split("\n");
|
|
799
|
+
const whitelistLower = whitelist.map((w) => w.toLowerCase());
|
|
800
|
+
for (let i = 0; i < lines.length; i++) {
|
|
801
|
+
const line = lines[i];
|
|
802
|
+
if (line === void 0) continue;
|
|
803
|
+
const trimmed = line.trim();
|
|
804
|
+
const loweredTrimmed = trimmed.toLowerCase();
|
|
805
|
+
if (loweredTrimmed.startsWith("forbidden terms:") || loweredTrimmed.startsWith("exceptions:")) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
for (const term of forbidden) {
|
|
809
|
+
const termLower = term.toLowerCase();
|
|
810
|
+
const lineLower = line.toLowerCase();
|
|
811
|
+
if (lineLower.includes(termLower)) {
|
|
812
|
+
const isWhitelisted = whitelistLower.some(
|
|
813
|
+
(w) => lineLower.includes(w) && w.includes(termLower)
|
|
814
|
+
);
|
|
815
|
+
if (!isWhitelisted) {
|
|
816
|
+
issues.push({
|
|
817
|
+
path: "",
|
|
818
|
+
code: "forbidden_term",
|
|
819
|
+
message: `Term "${term}" is forbidden by policy`,
|
|
820
|
+
severity: "warning",
|
|
821
|
+
line: i + 1,
|
|
822
|
+
context: trimmed.substring(0, 100)
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return issues;
|
|
829
|
+
}
|
|
830
|
+
function checkEmptySections(content) {
|
|
831
|
+
const issues = [];
|
|
832
|
+
const lines = content.split("\n");
|
|
833
|
+
let currentSection = null;
|
|
834
|
+
let sectionStartLine = 0;
|
|
835
|
+
let sectionHeadingLevel = 0;
|
|
836
|
+
let sectionHasContent = false;
|
|
837
|
+
let sectionWasFirst = false;
|
|
838
|
+
let isFirstHeading = true;
|
|
839
|
+
for (let i = 0; i < lines.length; i++) {
|
|
840
|
+
const line = lines[i];
|
|
841
|
+
if (line === void 0) continue;
|
|
842
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
843
|
+
if (headingMatch?.[2]) {
|
|
844
|
+
const nextHeadingLevel = headingMatch[1]?.length ?? 0;
|
|
845
|
+
if (currentSection !== null && nextHeadingLevel > sectionHeadingLevel) {
|
|
846
|
+
sectionHasContent = true;
|
|
847
|
+
}
|
|
848
|
+
if (currentSection !== null && !sectionHasContent && !sectionWasFirst) {
|
|
849
|
+
issues.push({
|
|
850
|
+
path: "",
|
|
851
|
+
code: "empty_section",
|
|
852
|
+
message: `Section "${currentSection}" has no content`,
|
|
853
|
+
severity: "info",
|
|
854
|
+
line: sectionStartLine
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
currentSection = headingMatch[2].trim();
|
|
858
|
+
sectionStartLine = i + 1;
|
|
859
|
+
sectionHeadingLevel = nextHeadingLevel;
|
|
860
|
+
sectionHasContent = false;
|
|
861
|
+
sectionWasFirst = isFirstHeading;
|
|
862
|
+
isFirstHeading = false;
|
|
863
|
+
} else if (currentSection !== null) {
|
|
864
|
+
if (line.trim().length > 0) {
|
|
865
|
+
sectionHasContent = true;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (currentSection !== null && !sectionHasContent && !sectionWasFirst) {
|
|
870
|
+
issues.push({
|
|
871
|
+
path: "",
|
|
872
|
+
code: "empty_section",
|
|
873
|
+
message: `Section "${currentSection}" has no content`,
|
|
874
|
+
severity: "info",
|
|
875
|
+
line: sectionStartLine
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
return issues;
|
|
879
|
+
}
|
|
880
|
+
function checkDuplicateUrls(content) {
|
|
881
|
+
const issues = [];
|
|
882
|
+
const lines = content.split("\n");
|
|
883
|
+
const seenUrls = /* @__PURE__ */ new Map();
|
|
884
|
+
const urlPattern = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
885
|
+
for (let i = 0; i < lines.length; i++) {
|
|
886
|
+
const line = lines[i];
|
|
887
|
+
if (line === void 0) continue;
|
|
888
|
+
let match = urlPattern.exec(line);
|
|
889
|
+
while (match !== null) {
|
|
890
|
+
const url = match[2];
|
|
891
|
+
if (url !== void 0) {
|
|
892
|
+
const firstOccurrence = seenUrls.get(url);
|
|
893
|
+
if (firstOccurrence !== void 0) {
|
|
894
|
+
issues.push({
|
|
895
|
+
path: "",
|
|
896
|
+
code: "duplicate_url",
|
|
897
|
+
message: `URL "${url}" appears multiple times (first at line ${firstOccurrence})`,
|
|
898
|
+
severity: "warning",
|
|
899
|
+
line: i + 1,
|
|
900
|
+
context: url
|
|
901
|
+
});
|
|
902
|
+
} else {
|
|
903
|
+
seenUrls.set(url, i + 1);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
match = urlPattern.exec(line);
|
|
907
|
+
}
|
|
908
|
+
urlPattern.lastIndex = 0;
|
|
909
|
+
}
|
|
910
|
+
return issues;
|
|
911
|
+
}
|
|
912
|
+
function lintHeadingStructure(content, filePath) {
|
|
913
|
+
const issues = [];
|
|
914
|
+
const lines = content.split("\n");
|
|
915
|
+
let prevLevel = 0;
|
|
916
|
+
for (let i = 0; i < lines.length; i++) {
|
|
917
|
+
const line = lines[i];
|
|
918
|
+
if (line === void 0) continue;
|
|
919
|
+
const match = line.match(/^(#{1,6})\s/);
|
|
920
|
+
if (match?.[1]) {
|
|
921
|
+
const level = match[1].length;
|
|
922
|
+
if (level > prevLevel + 1 && prevLevel > 0) {
|
|
923
|
+
issues.push(createIssue({
|
|
924
|
+
id: "heading-skip",
|
|
925
|
+
pageId: filePath,
|
|
926
|
+
severity: "warning",
|
|
927
|
+
message: `Heading level skipped: h${prevLevel} to h${level}`,
|
|
928
|
+
line: i + 1
|
|
929
|
+
}));
|
|
930
|
+
}
|
|
931
|
+
prevLevel = level;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return issues;
|
|
935
|
+
}
|
|
936
|
+
function lintUrlFormat(content, filePath) {
|
|
937
|
+
const issues = [];
|
|
938
|
+
const lines = content.split("\n");
|
|
939
|
+
const urlPattern = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
940
|
+
for (let i = 0; i < lines.length; i++) {
|
|
941
|
+
const line = lines[i];
|
|
942
|
+
if (line === void 0) continue;
|
|
943
|
+
let match = urlPattern.exec(line);
|
|
944
|
+
while (match !== null) {
|
|
945
|
+
const url = match[2];
|
|
946
|
+
if (url && !url.startsWith("/") && !url.startsWith("http") && !url.startsWith("#")) {
|
|
947
|
+
issues.push(createIssue({
|
|
948
|
+
id: "invalid-url",
|
|
949
|
+
pageId: filePath,
|
|
950
|
+
severity: "warning",
|
|
951
|
+
message: `Invalid URL format: ${url}`,
|
|
952
|
+
line: i + 1,
|
|
953
|
+
column: match.index + 1
|
|
954
|
+
}));
|
|
955
|
+
}
|
|
956
|
+
match = urlPattern.exec(line);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return issues;
|
|
960
|
+
}
|
|
961
|
+
function lintTrailingWhitespace(content, filePath) {
|
|
962
|
+
const issues = [];
|
|
963
|
+
const lines = content.split("\n");
|
|
964
|
+
for (let i = 0; i < lines.length; i++) {
|
|
965
|
+
const line = lines[i];
|
|
966
|
+
if (line !== void 0 && line.endsWith(" ")) {
|
|
967
|
+
issues.push(createIssue({
|
|
968
|
+
id: "trailing-whitespace",
|
|
969
|
+
pageId: filePath,
|
|
970
|
+
severity: "info",
|
|
971
|
+
message: "Line has trailing whitespace",
|
|
972
|
+
line: i + 1
|
|
973
|
+
}));
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return issues;
|
|
977
|
+
}
|
|
978
|
+
function lintListMarkers(content, filePath) {
|
|
979
|
+
const issues = [];
|
|
980
|
+
const lines = content.split("\n");
|
|
981
|
+
const dashCount = lines.filter((l) => /^\s*-\s/.test(l)).length;
|
|
982
|
+
const asteriskCount = lines.filter((l) => /^\s*\*\s/.test(l)).length;
|
|
983
|
+
if (dashCount > 0 && asteriskCount > 0) {
|
|
984
|
+
issues.push(createIssue({
|
|
985
|
+
id: "inconsistent-list-markers",
|
|
986
|
+
pageId: filePath,
|
|
987
|
+
severity: "info",
|
|
988
|
+
message: "Mix of - and * list markers detected"
|
|
989
|
+
}));
|
|
990
|
+
}
|
|
991
|
+
return issues;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/core/check/checker.ts
|
|
995
|
+
var DEFAULT_CHECKER_CONFIG = {
|
|
996
|
+
strict: false,
|
|
997
|
+
maxTitleLength: 60,
|
|
998
|
+
maxDescriptionLength: 160
|
|
999
|
+
};
|
|
1000
|
+
function checkManifest(manifest, config = {}) {
|
|
1001
|
+
const fullConfig = { ...DEFAULT_CHECKER_CONFIG, ...config };
|
|
1002
|
+
const issues = [];
|
|
1003
|
+
for (const page of manifest.pages) {
|
|
1004
|
+
const pageIssues = checkPage(page, fullConfig);
|
|
1005
|
+
issues.push(...pageIssues);
|
|
1006
|
+
}
|
|
1007
|
+
const summary = {
|
|
1008
|
+
error: 0,
|
|
1009
|
+
warning: 0,
|
|
1010
|
+
info: 0
|
|
1011
|
+
};
|
|
1012
|
+
for (const issue of issues) {
|
|
1013
|
+
summary[issue.severity]++;
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
passed: summary.error === 0,
|
|
1017
|
+
issues,
|
|
1018
|
+
pagesChecked: manifest.pages.length,
|
|
1019
|
+
summary
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function checkPage(page, config) {
|
|
1023
|
+
const issues = [];
|
|
1024
|
+
const pageId = page.path;
|
|
1025
|
+
if (!page.title) {
|
|
1026
|
+
issues.push({
|
|
1027
|
+
id: "missing-title",
|
|
1028
|
+
pageId,
|
|
1029
|
+
severity: "warning",
|
|
1030
|
+
message: "Page is missing a title"
|
|
1031
|
+
});
|
|
1032
|
+
} else if (page.title.length > config.maxTitleLength) {
|
|
1033
|
+
issues.push({
|
|
1034
|
+
id: "title-too-long",
|
|
1035
|
+
pageId,
|
|
1036
|
+
severity: "warning",
|
|
1037
|
+
message: `Title exceeds ${config.maxTitleLength} characters (${page.title.length})`
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
if (!page.description) {
|
|
1041
|
+
issues.push({
|
|
1042
|
+
id: "missing-description",
|
|
1043
|
+
pageId,
|
|
1044
|
+
severity: "warning",
|
|
1045
|
+
message: "Page is missing a description"
|
|
1046
|
+
});
|
|
1047
|
+
} else if (page.description.length > config.maxDescriptionLength) {
|
|
1048
|
+
issues.push({
|
|
1049
|
+
id: "description-too-long",
|
|
1050
|
+
pageId,
|
|
1051
|
+
severity: "info",
|
|
1052
|
+
message: `Description exceeds ${config.maxDescriptionLength} characters (${page.description.length})`
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
return issues;
|
|
1056
|
+
}
|
|
1057
|
+
async function checkGeneratedFiles(options) {
|
|
1058
|
+
const issues = [];
|
|
1059
|
+
let filesChecked = 0;
|
|
1060
|
+
let filesMissing = 0;
|
|
1061
|
+
let filesMismatch = 0;
|
|
1062
|
+
const { config, failOn } = options;
|
|
1063
|
+
const llmsTxtPath = options.llmsTxtPath ?? config.output.paths.llmsTxt;
|
|
1064
|
+
const llmsFullTxtPath = options.llmsFullTxtPath ?? config.output.paths.llmsFullTxt;
|
|
1065
|
+
const citationsPath = options.citationsPath ?? config.output.paths.citations;
|
|
1066
|
+
const llmsTxtResult = await checkFile(llmsTxtPath);
|
|
1067
|
+
if (!llmsTxtResult.exists) {
|
|
1068
|
+
issues.push(createCheckIssue(
|
|
1069
|
+
"error",
|
|
1070
|
+
"file_missing",
|
|
1071
|
+
`Required file does not exist: ${llmsTxtPath}`,
|
|
1072
|
+
llmsTxtPath
|
|
1073
|
+
));
|
|
1074
|
+
filesMissing++;
|
|
1075
|
+
} else if (llmsTxtResult.content === "") {
|
|
1076
|
+
issues.push(createCheckIssue(
|
|
1077
|
+
"warning",
|
|
1078
|
+
"file_empty",
|
|
1079
|
+
`File is empty: ${llmsTxtPath}`,
|
|
1080
|
+
llmsTxtPath
|
|
1081
|
+
));
|
|
1082
|
+
filesChecked++;
|
|
1083
|
+
} else {
|
|
1084
|
+
filesChecked++;
|
|
1085
|
+
const lintIssues = await lintFile(llmsTxtPath, llmsTxtResult.content, config);
|
|
1086
|
+
issues.push(...lintIssues);
|
|
1087
|
+
}
|
|
1088
|
+
const llmsFullTxtResult = await checkFile(llmsFullTxtPath);
|
|
1089
|
+
if (!llmsFullTxtResult.exists) {
|
|
1090
|
+
issues.push(createCheckIssue(
|
|
1091
|
+
"error",
|
|
1092
|
+
"file_missing",
|
|
1093
|
+
`Required file does not exist: ${llmsFullTxtPath}`,
|
|
1094
|
+
llmsFullTxtPath
|
|
1095
|
+
));
|
|
1096
|
+
filesMissing++;
|
|
1097
|
+
} else if (llmsFullTxtResult.content === "") {
|
|
1098
|
+
issues.push(createCheckIssue(
|
|
1099
|
+
"warning",
|
|
1100
|
+
"file_empty",
|
|
1101
|
+
`File is empty: ${llmsFullTxtPath}`,
|
|
1102
|
+
llmsFullTxtPath
|
|
1103
|
+
));
|
|
1104
|
+
filesChecked++;
|
|
1105
|
+
} else {
|
|
1106
|
+
filesChecked++;
|
|
1107
|
+
const lintIssues = await lintFile(llmsFullTxtPath, llmsFullTxtResult.content, config);
|
|
1108
|
+
issues.push(...lintIssues);
|
|
1109
|
+
}
|
|
1110
|
+
if (citationsPath) {
|
|
1111
|
+
const citationsResult = await checkFile(citationsPath);
|
|
1112
|
+
if (!citationsResult.exists) {
|
|
1113
|
+
issues.push(createCheckIssue(
|
|
1114
|
+
"warning",
|
|
1115
|
+
"file_missing",
|
|
1116
|
+
`Optional citations file does not exist: ${citationsPath}`,
|
|
1117
|
+
citationsPath
|
|
1118
|
+
));
|
|
1119
|
+
filesMissing++;
|
|
1120
|
+
} else {
|
|
1121
|
+
filesChecked++;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const severityCounts = countSeverities(issues);
|
|
1125
|
+
let exitCode;
|
|
1126
|
+
if (severityCounts.error > 0) {
|
|
1127
|
+
exitCode = 2;
|
|
1128
|
+
} else if (failOn === "warn" && severityCounts.warning > 0) {
|
|
1129
|
+
exitCode = 1;
|
|
1130
|
+
} else {
|
|
1131
|
+
exitCode = 0;
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
issues,
|
|
1135
|
+
summary: {
|
|
1136
|
+
errors: severityCounts.error,
|
|
1137
|
+
warnings: severityCounts.warning,
|
|
1138
|
+
info: severityCounts.info,
|
|
1139
|
+
filesChecked,
|
|
1140
|
+
filesMissing,
|
|
1141
|
+
filesMismatch
|
|
1142
|
+
},
|
|
1143
|
+
exitCode
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
async function checkFileExists(filePath) {
|
|
1147
|
+
try {
|
|
1148
|
+
await fs.access(filePath);
|
|
1149
|
+
return true;
|
|
1150
|
+
} catch {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
async function checkFile(filePath) {
|
|
1155
|
+
try {
|
|
1156
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1157
|
+
return { exists: true, content };
|
|
1158
|
+
} catch {
|
|
1159
|
+
return { exists: false, content: "" };
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
async function readFileContent(filePath) {
|
|
1163
|
+
try {
|
|
1164
|
+
return await fs.readFile(filePath, "utf-8");
|
|
1165
|
+
} catch {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function compareContent(expected, actual, maxContextLines = 5) {
|
|
1170
|
+
if (expected === actual) {
|
|
1171
|
+
return { match: true, context: "" };
|
|
1172
|
+
}
|
|
1173
|
+
const expectedLines = expected.split("\n");
|
|
1174
|
+
const actualLines = actual.split("\n");
|
|
1175
|
+
const contextLines = [];
|
|
1176
|
+
let diffCount = 0;
|
|
1177
|
+
const maxLines = Math.max(expectedLines.length, actualLines.length);
|
|
1178
|
+
for (let i = 0; i < maxLines && diffCount < maxContextLines; i++) {
|
|
1179
|
+
const expectedLine = expectedLines[i];
|
|
1180
|
+
const actualLine = actualLines[i];
|
|
1181
|
+
if (expectedLine !== actualLine) {
|
|
1182
|
+
diffCount++;
|
|
1183
|
+
const lineNum = i + 1;
|
|
1184
|
+
if (expectedLine !== void 0) {
|
|
1185
|
+
contextLines.push(`Expected line ${lineNum}: "${expectedLine}"`);
|
|
1186
|
+
}
|
|
1187
|
+
if (actualLine !== void 0) {
|
|
1188
|
+
contextLines.push(`Actual line ${lineNum}: "${actualLine}"`);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return {
|
|
1193
|
+
match: false,
|
|
1194
|
+
context: contextLines.join("\n")
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
async function lintFile(filePath, content, config) {
|
|
1198
|
+
const issues = [];
|
|
1199
|
+
const lintResult = lintContent(content, filePath);
|
|
1200
|
+
for (const issue of lintResult.issues) {
|
|
1201
|
+
const checkIssue = {
|
|
1202
|
+
path: filePath,
|
|
1203
|
+
code: issue.id,
|
|
1204
|
+
message: issue.message,
|
|
1205
|
+
severity: issue.severity
|
|
1206
|
+
};
|
|
1207
|
+
if (issue.line !== void 0) {
|
|
1208
|
+
checkIssue.line = issue.line;
|
|
1209
|
+
}
|
|
1210
|
+
if (issue.suggestion !== void 0) {
|
|
1211
|
+
checkIssue.context = issue.suggestion;
|
|
1212
|
+
}
|
|
1213
|
+
issues.push(checkIssue);
|
|
1214
|
+
}
|
|
1215
|
+
if (config.policy?.restrictedClaims?.enable) {
|
|
1216
|
+
const forbidden = config.policy.restrictedClaims.forbidden ?? [];
|
|
1217
|
+
const whitelist = config.policy.restrictedClaims.whitelist ?? [];
|
|
1218
|
+
const forbiddenIssues = checkForbiddenTerms(content, forbidden, whitelist);
|
|
1219
|
+
for (const issue of forbiddenIssues) {
|
|
1220
|
+
const checkIssue = {
|
|
1221
|
+
path: filePath,
|
|
1222
|
+
code: "forbidden_term",
|
|
1223
|
+
message: issue.message,
|
|
1224
|
+
severity: issue.severity
|
|
1225
|
+
};
|
|
1226
|
+
if (issue.line !== void 0) {
|
|
1227
|
+
checkIssue.line = issue.line;
|
|
1228
|
+
}
|
|
1229
|
+
if (issue.context !== void 0) {
|
|
1230
|
+
checkIssue.context = issue.context;
|
|
1231
|
+
}
|
|
1232
|
+
issues.push(checkIssue);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const emptySectionIssues = checkEmptySections(content);
|
|
1236
|
+
for (const issue of emptySectionIssues) {
|
|
1237
|
+
const checkIssue = {
|
|
1238
|
+
path: filePath,
|
|
1239
|
+
code: "empty_section",
|
|
1240
|
+
message: issue.message,
|
|
1241
|
+
severity: issue.severity
|
|
1242
|
+
};
|
|
1243
|
+
if (issue.line !== void 0) {
|
|
1244
|
+
checkIssue.line = issue.line;
|
|
1245
|
+
}
|
|
1246
|
+
if (issue.context !== void 0) {
|
|
1247
|
+
checkIssue.context = issue.context;
|
|
1248
|
+
}
|
|
1249
|
+
issues.push(checkIssue);
|
|
1250
|
+
}
|
|
1251
|
+
const duplicateUrlIssues = checkDuplicateUrls(content);
|
|
1252
|
+
for (const issue of duplicateUrlIssues) {
|
|
1253
|
+
const checkIssue = {
|
|
1254
|
+
path: filePath,
|
|
1255
|
+
code: "duplicate_url",
|
|
1256
|
+
message: issue.message,
|
|
1257
|
+
severity: issue.severity
|
|
1258
|
+
};
|
|
1259
|
+
if (issue.line !== void 0) {
|
|
1260
|
+
checkIssue.line = issue.line;
|
|
1261
|
+
}
|
|
1262
|
+
if (issue.context !== void 0) {
|
|
1263
|
+
checkIssue.context = issue.context;
|
|
1264
|
+
}
|
|
1265
|
+
issues.push(checkIssue);
|
|
1266
|
+
}
|
|
1267
|
+
return issues;
|
|
1268
|
+
}
|
|
1269
|
+
async function checkFilesAgainstExpected(llmsTxtPath, expectedLlmsTxt, llmsFullTxtPath, expectedLlmsFullTxt, maxContextLines = 5) {
|
|
1270
|
+
const issues = [];
|
|
1271
|
+
const llmsTxtContent = await readFileContent(llmsTxtPath);
|
|
1272
|
+
if (llmsTxtContent === null) {
|
|
1273
|
+
issues.push(createCheckIssue(
|
|
1274
|
+
"error",
|
|
1275
|
+
"file_missing",
|
|
1276
|
+
`Required file does not exist: ${llmsTxtPath}`,
|
|
1277
|
+
llmsTxtPath
|
|
1278
|
+
));
|
|
1279
|
+
} else if (llmsTxtContent === "") {
|
|
1280
|
+
issues.push(createCheckIssue(
|
|
1281
|
+
"warning",
|
|
1282
|
+
"file_empty",
|
|
1283
|
+
`File is empty: ${llmsTxtPath}`,
|
|
1284
|
+
llmsTxtPath
|
|
1285
|
+
));
|
|
1286
|
+
} else {
|
|
1287
|
+
const compareResult = compareContent(expectedLlmsTxt, llmsTxtContent, maxContextLines);
|
|
1288
|
+
if (!compareResult.match) {
|
|
1289
|
+
issues.push(createCheckIssue(
|
|
1290
|
+
"error",
|
|
1291
|
+
"file_mismatch",
|
|
1292
|
+
`Content differs from expected output`,
|
|
1293
|
+
llmsTxtPath,
|
|
1294
|
+
compareResult.context
|
|
1295
|
+
));
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const llmsFullTxtContent = await readFileContent(llmsFullTxtPath);
|
|
1299
|
+
if (llmsFullTxtContent === null) {
|
|
1300
|
+
issues.push(createCheckIssue(
|
|
1301
|
+
"error",
|
|
1302
|
+
"file_missing",
|
|
1303
|
+
`Required file does not exist: ${llmsFullTxtPath}`,
|
|
1304
|
+
llmsFullTxtPath
|
|
1305
|
+
));
|
|
1306
|
+
} else if (llmsFullTxtContent === "") {
|
|
1307
|
+
issues.push(createCheckIssue(
|
|
1308
|
+
"warning",
|
|
1309
|
+
"file_empty",
|
|
1310
|
+
`File is empty: ${llmsFullTxtPath}`,
|
|
1311
|
+
llmsFullTxtPath
|
|
1312
|
+
));
|
|
1313
|
+
} else {
|
|
1314
|
+
const compareResult = compareContent(expectedLlmsFullTxt, llmsFullTxtContent, maxContextLines);
|
|
1315
|
+
if (!compareResult.match) {
|
|
1316
|
+
issues.push(createCheckIssue(
|
|
1317
|
+
"error",
|
|
1318
|
+
"file_mismatch",
|
|
1319
|
+
`Content differs from expected output`,
|
|
1320
|
+
llmsFullTxtPath,
|
|
1321
|
+
compareResult.context
|
|
1322
|
+
));
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return issues;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
export { DEFAULT_CHECKER_CONFIG, LINT_RULES, checkFileExists, checkFilesAgainstExpected, checkGeneratedFiles, checkManifest, citationToJsonLd, citationToMarkdown, compareContent, compareStrings, createCanonicalUrlForItem, createCanonicalUrlsFromManifest, createCitation, createCitationsJson, createCitationsJsonString, createIssue, createLlmsFullTxt, createLlmsTxt, dedupeUrls, extractCanonicalUrls, extractLocaleFromPath, filterBySeverity, generateAlternateUrls, generateCanonicalUrl, generateLlmsFullTxt, generateLlmsTxt, generatePageContent, generateReferenceList, groupByPage, groupBySeverity, lintContent, localizePath, normalizeLineEndings, normalizeLineWhitespace, normalizeSeoText, normalizeUrl, normalizeWhitespace, readFileContent, sortBy, sortStrings, sortUrls };
|
|
1329
|
+
//# sourceMappingURL=index.js.map
|
|
1330
|
+
//# sourceMappingURL=index.js.map
|