@gusnips/vite 0.8.43 → 0.8.45
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/README.md +185 -1
- package/dist/contrast.d.ts +60 -1
- package/dist/contrast.d.ts.map +1 -1
- package/dist/contrast.js +277 -9
- package/dist/contrast.js.map +1 -1
- package/dist/discovery.d.ts +76 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +37 -0
- package/dist/discovery.js.map +1 -0
- package/dist/head.d.ts +6 -6
- package/dist/head.d.ts.map +1 -1
- package/dist/head.js +11 -10
- package/dist/head.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/contrast-tokens.test.ts +256 -0
- package/src/contrast.ts +375 -8
- package/src/discovery.test.ts +120 -0
- package/src/discovery.ts +103 -0
- package/src/fixtures/contrast/tokens/ok-media.css +32 -0
- package/src/fixtures/contrast/tokens/ok.css +30 -0
- package/src/head.test.ts +41 -11
- package/src/head.ts +12 -10
- package/src/index.ts +13 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { llmsTxt, mcpDiscoveryJson, type McpDiscoveryDocument } from "./discovery.ts";
|
|
3
|
+
|
|
4
|
+
describe("llmsTxt", () => {
|
|
5
|
+
it("writes the title, summary and links in the supplied order", () => {
|
|
6
|
+
expect(
|
|
7
|
+
llmsTxt({
|
|
8
|
+
title: "Example",
|
|
9
|
+
summary: "Read and update tasks.\nSign in before making changes.",
|
|
10
|
+
sections: [
|
|
11
|
+
{
|
|
12
|
+
title: "Reference",
|
|
13
|
+
links: [
|
|
14
|
+
{
|
|
15
|
+
title: "Connect",
|
|
16
|
+
url: "https://example.com/docs/mcp",
|
|
17
|
+
description: "Set up access.",
|
|
18
|
+
},
|
|
19
|
+
{ title: "Full reference", url: "https://example.com/llms-full.txt" },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
}),
|
|
24
|
+
).toBe(
|
|
25
|
+
"# Example\n\n" +
|
|
26
|
+
"> Read and update tasks.\n> Sign in before making changes.\n\n" +
|
|
27
|
+
"## Reference\n\n" +
|
|
28
|
+
"- [Connect](https://example.com/docs/mcp): Set up access.\n" +
|
|
29
|
+
"- [Full reference](https://example.com/llms-full.txt)\n",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("keeps app-owned Markdown and writes exactly one final newline", () => {
|
|
34
|
+
const body = '## Tools\n\n```json\n{"name":"list_tasks"}\n```\n\n';
|
|
35
|
+
expect(llmsTxt({ title: "Example", body })).toBe(`# Example\n\n${body.trimEnd()}\n`);
|
|
36
|
+
expect(llmsTxt({ title: "Example" })).toBe("# Example\n");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("refuses an empty title", () => {
|
|
40
|
+
expect(() => llmsTxt({ title: " " })).toThrow(/title.*empty.*Name/);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const DOCUMENT: McpDiscoveryDocument = {
|
|
45
|
+
name: "Example",
|
|
46
|
+
homepage: "https://example.com",
|
|
47
|
+
documentation: "https://example.com/docs/mcp",
|
|
48
|
+
llmsTxt: "https://example.com/llms.txt",
|
|
49
|
+
llmsFullTxt: "https://example.com/llms-full.txt",
|
|
50
|
+
openApi: "https://api.example.com/openapi.json",
|
|
51
|
+
servers: [
|
|
52
|
+
{
|
|
53
|
+
name: "full",
|
|
54
|
+
transport: "streamable-http",
|
|
55
|
+
url: "https://api.example.com/mcp",
|
|
56
|
+
authentication: [
|
|
57
|
+
{
|
|
58
|
+
type: "oauth2",
|
|
59
|
+
protectedResourceMetadata:
|
|
60
|
+
"https://api.example.com/.well-known/oauth-protected-resource/mcp",
|
|
61
|
+
},
|
|
62
|
+
{ type: "api-key", in: "header", name: "Authorization", prefix: "Bearer " },
|
|
63
|
+
],
|
|
64
|
+
tools: [
|
|
65
|
+
{
|
|
66
|
+
name: "update_task",
|
|
67
|
+
title: "Update a task",
|
|
68
|
+
description: "Change a task's title.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: { id: { type: "string" }, title: { type: "string", minLength: 1 } },
|
|
72
|
+
required: ["id", "title"],
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "read-only",
|
|
81
|
+
transport: "streamable-http",
|
|
82
|
+
url: "https://api.example.com/mcp/read",
|
|
83
|
+
authentication: [{ type: "none" }],
|
|
84
|
+
tools: ["list_tasks"],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "local",
|
|
88
|
+
transport: "stdio",
|
|
89
|
+
command: "bunx",
|
|
90
|
+
args: ["example-mcp"],
|
|
91
|
+
authentication: [
|
|
92
|
+
{ type: "session", description: "Connect the local app before you run a tool." },
|
|
93
|
+
{ type: "api-key", in: "environment", name: "EXAMPLE_API_KEY" },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
describe("mcpDiscoveryJson", () => {
|
|
100
|
+
it("keeps per-server transports, access methods and full tool cards unchanged", () => {
|
|
101
|
+
const output = mcpDiscoveryJson(DOCUMENT);
|
|
102
|
+
expect(JSON.parse(output)).toEqual(DOCUMENT);
|
|
103
|
+
expect(output.startsWith('{\n "name": "Example",')).toBe(true);
|
|
104
|
+
expect(output.endsWith("\n")).toBe(true);
|
|
105
|
+
expect(output.endsWith("\n\n")).toBe(false);
|
|
106
|
+
expect(output).not.toContain('"url": "stdio');
|
|
107
|
+
expect(output).not.toContain('"tools": []');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("refuses empty names, server lists or access declarations", () => {
|
|
111
|
+
expect(() => mcpDiscoveryJson({ ...DOCUMENT, name: " " })).toThrow(/name.*empty/);
|
|
112
|
+
expect(() => mcpDiscoveryJson({ ...DOCUMENT, servers: [] })).toThrow(/servers.*empty/);
|
|
113
|
+
expect(() =>
|
|
114
|
+
mcpDiscoveryJson({
|
|
115
|
+
...DOCUMENT,
|
|
116
|
+
servers: [{ name: "local", transport: "stdio", command: "bunx", authentication: [] }],
|
|
117
|
+
}),
|
|
118
|
+
).toThrow(/local.*no authentication.*Declare/);
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Static discovery files. Content, addresses and access rules stay in the app. */
|
|
2
|
+
|
|
3
|
+
export interface LlmsLink {
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LlmsSection {
|
|
10
|
+
title: string;
|
|
11
|
+
links: readonly LlmsLink[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LlmsTxtOptions {
|
|
15
|
+
title: string;
|
|
16
|
+
/** A short description, written as a blockquote. */
|
|
17
|
+
summary?: string;
|
|
18
|
+
/** Markdown kept as supplied. Use this for the text in llms-full.txt. */
|
|
19
|
+
body?: string;
|
|
20
|
+
sections?: readonly LlmsSection[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** llms.txt or llms-full.txt, as Markdown with a trailing newline. */
|
|
24
|
+
export function llmsTxt({ title, summary, body, sections = [] }: LlmsTxtOptions): string {
|
|
25
|
+
if (title.trim() === "") throw new Error("llms: `title` is empty. Name the site or package.");
|
|
26
|
+
const parts = [`# ${title}`];
|
|
27
|
+
if (summary)
|
|
28
|
+
parts.push(
|
|
29
|
+
summary
|
|
30
|
+
.split(/\r?\n/)
|
|
31
|
+
.map((line) => `> ${line}`)
|
|
32
|
+
.join("\n"),
|
|
33
|
+
);
|
|
34
|
+
if (body) parts.push(body);
|
|
35
|
+
for (const section of sections) {
|
|
36
|
+
parts.push(`## ${section.title}`);
|
|
37
|
+
parts.push(
|
|
38
|
+
section.links
|
|
39
|
+
.map(
|
|
40
|
+
(link) =>
|
|
41
|
+
`- [${link.title}](${link.url})${link.description ? `: ${link.description}` : ""}`,
|
|
42
|
+
)
|
|
43
|
+
.join("\n"),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return `${parts.join("\n\n").trimEnd()}\n`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Declare each accepted method. This is documentation, not an access check. */
|
|
50
|
+
export type McpDiscoveryAuthentication = (
|
|
51
|
+
| { type: "oauth2"; protectedResourceMetadata: string }
|
|
52
|
+
| { type: "api-key"; in: "header" | "environment"; name: string; prefix?: string }
|
|
53
|
+
| { type: "none" }
|
|
54
|
+
| { type: "session"; description: string }
|
|
55
|
+
) & { description?: string; documentation?: string };
|
|
56
|
+
|
|
57
|
+
/** A tool as this server exposes it, not the combined catalog of every server. */
|
|
58
|
+
export interface McpDiscoveryTool {
|
|
59
|
+
name: string;
|
|
60
|
+
title?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
inputSchema?: object;
|
|
63
|
+
annotations?: object;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type McpDiscoveryServer = {
|
|
67
|
+
name: string;
|
|
68
|
+
/** Several entries mean the server accepts any one of these methods. */
|
|
69
|
+
authentication: readonly McpDiscoveryAuthentication[];
|
|
70
|
+
tools?: readonly (string | McpDiscoveryTool)[];
|
|
71
|
+
} & (
|
|
72
|
+
| { transport: "streamable-http"; url: string }
|
|
73
|
+
| { transport: "stdio"; command: string; args?: readonly string[] }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
export interface McpDiscoveryDocument {
|
|
77
|
+
name: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
homepage: string;
|
|
80
|
+
documentation: string;
|
|
81
|
+
llmsTxt?: string;
|
|
82
|
+
llmsFullTxt?: string;
|
|
83
|
+
openApi?: string;
|
|
84
|
+
servers: readonly McpDiscoveryServer[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* /.well-known/mcp.json, as JSON with a trailing newline. This is a discovery convention,
|
|
89
|
+
* not a standard client configuration or a replacement for MCP tools/list.
|
|
90
|
+
* No transport, access rule or tool is inferred, and supplied schemas and annotations are kept.
|
|
91
|
+
*/
|
|
92
|
+
export function mcpDiscoveryJson(document: McpDiscoveryDocument): string {
|
|
93
|
+
if (document.name.trim() === "")
|
|
94
|
+
throw new Error("mcp discovery: `name` is empty. Name the site or package.");
|
|
95
|
+
if (document.servers.length === 0)
|
|
96
|
+
throw new Error("mcp discovery: `servers` is empty. List the servers this file describes.");
|
|
97
|
+
for (const server of document.servers)
|
|
98
|
+
if (server.authentication.length === 0)
|
|
99
|
+
throw new Error(
|
|
100
|
+
`mcp discovery: "${server.name}" has no authentication entry. Declare how access works, including type "none" when it is public.`,
|
|
101
|
+
);
|
|
102
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* The same palette with both dark blocks wrapped in @media. */
|
|
2
|
+
:root {
|
|
3
|
+
--ground: #ffffff;
|
|
4
|
+
--ink: #1a1a1a;
|
|
5
|
+
--muted: #555555;
|
|
6
|
+
--line: #767676;
|
|
7
|
+
--accent: #0b5cad;
|
|
8
|
+
--accent-ink: #fff;
|
|
9
|
+
--radius: 8px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@media screen {
|
|
13
|
+
:root[data-theme="dark"] {
|
|
14
|
+
--ground: #121212;
|
|
15
|
+
--ink: #f2f2f2;
|
|
16
|
+
--muted: #b3b3b3;
|
|
17
|
+
--line: #8a8a8a;
|
|
18
|
+
--accent: #7ab7ff;
|
|
19
|
+
--accent-ink: #0a1a2b;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@media (prefers-color-scheme: dark) {
|
|
23
|
+
:root:not([data-theme]) {
|
|
24
|
+
--ground: #121212;
|
|
25
|
+
--ink: #f2f2f2;
|
|
26
|
+
--muted: #b3b3b3;
|
|
27
|
+
--line: #8a8a8a;
|
|
28
|
+
--accent: #7ab7ff;
|
|
29
|
+
--accent-ink: #0a1a2b;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* Flat colour blocks, written the way a plain-CSS app writes them. A {brace} in a comment is no block. */
|
|
2
|
+
:root {
|
|
3
|
+
--ground: #ffffff;
|
|
4
|
+
--ink: #1a1a1a;
|
|
5
|
+
--muted: #555555;
|
|
6
|
+
--line: #767676;
|
|
7
|
+
--accent: #0b5cad;
|
|
8
|
+
--accent-ink: #fff;
|
|
9
|
+
--radius: 8px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
:root[data-theme="dark"] {
|
|
13
|
+
--ground: #121212;
|
|
14
|
+
--ink: #f2f2f2;
|
|
15
|
+
--muted: #b3b3b3;
|
|
16
|
+
--line: #8a8a8a;
|
|
17
|
+
--accent: #7ab7ff;
|
|
18
|
+
--accent-ink: #0a1a2b;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@media (prefers-color-scheme: dark) {
|
|
22
|
+
:root:not([data-theme]) {
|
|
23
|
+
--ground: #121212;
|
|
24
|
+
--ink: #f2f2f2;
|
|
25
|
+
--muted: #b3b3b3;
|
|
26
|
+
--line: #8a8a8a;
|
|
27
|
+
--accent: #7ab7ff;
|
|
28
|
+
--accent-ink: #0a1a2b;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/head.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { PRERENDERED_ROUTE_ATTR } from "@gusnips/react";
|
|
3
|
-
import { assertRendered, bakeHead, faqJsonLd, ogLocale } from "./head.ts";
|
|
3
|
+
import { assertRendered, bakeHead, faqJsonLd, jsonLd, ogLocale } from "./head.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Every failure `bakeHead` has is invisible until somebody reads a search result months later
|
|
@@ -433,6 +433,37 @@ describe("assertRendered", () => {
|
|
|
433
433
|
});
|
|
434
434
|
});
|
|
435
435
|
|
|
436
|
+
describe("jsonLd", () => {
|
|
437
|
+
it("keeps the caller's schema and fields", () => {
|
|
438
|
+
const data = {
|
|
439
|
+
"@context": "https://schema.org",
|
|
440
|
+
"@graph": [
|
|
441
|
+
{
|
|
442
|
+
"@type": "SoftwareApplication",
|
|
443
|
+
name: "Example",
|
|
444
|
+
applicationCategory: "DeveloperApplication",
|
|
445
|
+
},
|
|
446
|
+
{ "@type": "WebAPI", name: "Example API", documentation: "https://example.com/docs" },
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
const script = jsonLd(data);
|
|
450
|
+
expect(script.startsWith('<script type="application/ld+json">')).toBe(true);
|
|
451
|
+
expect(JSON.parse(script.replace(/^<script[^>]*>/, "").replace(/<\/script>$/, ""))).toEqual(
|
|
452
|
+
data,
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("round-trips HTML parser control text without exposing markup", () => {
|
|
457
|
+
const data = { name: '</ScRiPt><img src=x onerror="x"> <!-- <script> $$ \\u003c' };
|
|
458
|
+
const script = jsonLd(data);
|
|
459
|
+
const inner = script.replace(/^<script[^>]*>/, "").replace(/<\/script>$/, "");
|
|
460
|
+
expect(inner).not.toContain("<");
|
|
461
|
+
expect(JSON.parse(inner)).toEqual(data);
|
|
462
|
+
expect(script.match(/<script/g)).toHaveLength(1);
|
|
463
|
+
expect(script.match(/<\/script>/g)).toHaveLength(1);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
436
467
|
describe("faqJsonLd", () => {
|
|
437
468
|
// One donor's template carried four questions where the page renders seven: the fix is to
|
|
438
469
|
// generate the block from the same strings, not to keep a second copy beside them.
|
|
@@ -448,15 +479,12 @@ describe("faqJsonLd", () => {
|
|
|
448
479
|
expect(script.match(/"@type":"Question"/g)).toHaveLength(2);
|
|
449
480
|
});
|
|
450
481
|
|
|
451
|
-
// A `</script>` inside an answer ends the element early and ships the rest of the JSON as
|
|
452
|
-
// markup. `<\/` is the same string to a JSON parser and inert to an HTML one.
|
|
453
482
|
it("escapes a script-closing sequence inside an answer", () => {
|
|
454
483
|
const script = faqJsonLd([
|
|
455
484
|
{ question: "What if I write code?", answer: "Wrap it: </script> ends the block." },
|
|
456
485
|
]);
|
|
457
486
|
expect(script).not.toContain("</script> ends");
|
|
458
|
-
expect(script).toContain("
|
|
459
|
-
// …and what it emits still parses, back to the original copy.
|
|
487
|
+
expect(script).toContain("\\u003c/script> ends");
|
|
460
488
|
const json = script.replace(/^<script[^>]*>/, "").replace(/<\/script>$/, "");
|
|
461
489
|
const parsed = JSON.parse(json) as {
|
|
462
490
|
mainEntity: Array<{ acceptedAnswer: { text: string } }>;
|
|
@@ -464,11 +492,13 @@ describe("faqJsonLd", () => {
|
|
|
464
492
|
expect(parsed.mainEntity[0]?.acceptedAnswer.text).toBe("Wrap it: </script> ends the block.");
|
|
465
493
|
});
|
|
466
494
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
expect(inner).
|
|
495
|
+
it("keeps comments valid JSON and inert to the HTML parser", () => {
|
|
496
|
+
const answer = "a </b> b <!-- <script> c";
|
|
497
|
+
const script = faqJsonLd([{ question: "q", answer }]);
|
|
498
|
+
const inner = script.replace(/^<script[^>]*>/, "").replace(/<\/script>$/, "");
|
|
499
|
+
expect(inner).not.toContain("<");
|
|
500
|
+
expect(JSON.parse(inner)).toMatchObject({
|
|
501
|
+
mainEntity: [{ acceptedAnswer: { text: answer } }],
|
|
502
|
+
});
|
|
473
503
|
});
|
|
474
504
|
});
|
package/src/head.ts
CHANGED
|
@@ -193,16 +193,20 @@ export interface FaqEntry {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
*
|
|
196
|
+
* Structured data for `headExtra`. The caller owns the schema and its claims.
|
|
197
197
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* structural characters are all brackets, quotes and digits. The replacement only ever touches
|
|
202
|
-
* copy.
|
|
198
|
+
* Escape every less-than character as JSON, not HTML: `</script>` can close the element, and
|
|
199
|
+
* `<!--<script>` can make the HTML parser swallow the real closing tag. Escaping only a slash
|
|
200
|
+
* misses that second case; adding a backslash before `!` makes the JSON invalid.
|
|
203
201
|
*/
|
|
202
|
+
export function jsonLd(data: object): string {
|
|
203
|
+
const json = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
204
|
+
return `<script type="application/ld+json">${json}</script>`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** FAQ structured data from the same strings the page renders. */
|
|
204
208
|
export function faqJsonLd(faqs: readonly FaqEntry[]): string {
|
|
205
|
-
|
|
209
|
+
return jsonLd({
|
|
206
210
|
"@context": "https://schema.org",
|
|
207
211
|
"@type": "FAQPage",
|
|
208
212
|
mainEntity: faqs.map((faq) => ({
|
|
@@ -210,9 +214,7 @@ export function faqJsonLd(faqs: readonly FaqEntry[]): string {
|
|
|
210
214
|
name: faq.question,
|
|
211
215
|
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
|
212
216
|
})),
|
|
213
|
-
};
|
|
214
|
-
const json = JSON.stringify(data).replace(/<\//g, "<\\/").replace(/<!--/g, "<\\!--");
|
|
215
|
-
return `<script type="application/ld+json">${json}</script>`;
|
|
217
|
+
});
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
/**
|
package/src/index.ts
CHANGED
|
@@ -13,11 +13,22 @@
|
|
|
13
13
|
* otherwise force on everyone: the vite config preset at `@gusnips/vite/preset` (the React and
|
|
14
14
|
* Tailwind plugins), `renderTree` at `@gusnips/vite/render`, the theme pre-paint plugin at
|
|
15
15
|
* `@gusnips/vite/theme` (both React itself), the catalog check at `@gusnips/vite/i18n`, and the
|
|
16
|
-
* contrast gate at `@gusnips/vite/contrast` (tailwindcss). The last two run in CI and never in a
|
|
16
|
+
* contrast gate at `@gusnips/vite/contrast` (tailwindcss, for the class reading only). The last two run in CI and never in a
|
|
17
17
|
* build. Nothing here imports React, vite or tailwindcss, so a prerender script, an OG generator
|
|
18
18
|
* and a repo that only wants a sitemap all install exactly what they use.
|
|
19
19
|
*/
|
|
20
20
|
export { resolveKey } from "./catalog.ts";
|
|
21
|
+
export {
|
|
22
|
+
llmsTxt,
|
|
23
|
+
mcpDiscoveryJson,
|
|
24
|
+
type LlmsLink,
|
|
25
|
+
type LlmsSection,
|
|
26
|
+
type LlmsTxtOptions,
|
|
27
|
+
type McpDiscoveryAuthentication,
|
|
28
|
+
type McpDiscoveryDocument,
|
|
29
|
+
type McpDiscoveryServer,
|
|
30
|
+
type McpDiscoveryTool,
|
|
31
|
+
} from "./discovery.ts";
|
|
21
32
|
// `headExtra` takes raw markup, so the moment an adopter builds a tag out of its own copy it
|
|
22
33
|
// needs the same escaping every tag this package writes already gets. Exported rather than
|
|
23
34
|
// re-invented per app: an adopter's hand-rolled copy is one that can drift from ours, and the
|
|
@@ -28,6 +39,7 @@ export {
|
|
|
28
39
|
bakeHead,
|
|
29
40
|
EMPTY_ROOT,
|
|
30
41
|
faqJsonLd,
|
|
42
|
+
jsonLd,
|
|
31
43
|
ogLocale,
|
|
32
44
|
type Alternate,
|
|
33
45
|
type FaqEntry,
|