wiki-formant 0.51.0 → 0.52.1
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 +45 -17
- package/bin/_cli.mjs +50 -0
- package/bin/analytics-digest.mjs +4 -23
- package/bin/check-classes.mjs +23 -15
- package/bin/check-pages.mjs +6 -20
- package/bin/fts-ddl.mjs +4 -19
- package/bin/indexnow.mjs +4 -6
- package/bin/maintenance-log.mjs +6 -31
- package/bin/mark-verified.mjs +5 -30
- package/bin/passkey-invite.mjs +9 -19
- package/bin/prisma-models.mjs +40 -5
- package/dist/base.css +25 -0
- package/dist/block-views.js +2 -3
- package/dist/crawlers.d.ts +2 -2
- package/dist/db.d.ts +25 -0
- package/dist/db.js +32 -1
- package/dist/editor.d.ts +3 -0
- package/dist/editor.js +2 -0
- package/dist/headers.d.ts +28 -0
- package/dist/headers.js +33 -1
- package/dist/headings.d.ts +10 -0
- package/dist/headings.js +12 -0
- package/dist/history.js +1 -1
- package/dist/http.d.ts +7 -2
- package/dist/http.js +7 -2
- package/dist/indexnow.d.ts +7 -0
- package/dist/indexnow.js +22 -3
- package/dist/maintenance.js +2 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +3 -4
- package/dist/sanitize.js +1 -1
- package/dist/stats.d.ts +28 -0
- package/dist/stats.js +18 -1
- package/dist/tiptap.js +2 -4
- package/package.json +13 -83
package/bin/prisma-models.mjs
CHANGED
|
@@ -16,21 +16,56 @@
|
|
|
16
16
|
// npx prisma-models --check # exit 1 if either is missing or differs
|
|
17
17
|
// npx prisma-models analytics # only the ones named
|
|
18
18
|
// npx prisma-models --dir db/schema # another folder
|
|
19
|
+
//
|
|
20
|
+
// An app that keeps one schema file and hand-writes the models into it, with a
|
|
21
|
+
// relation the shipped ones cannot carry, checks them in place instead.
|
|
22
|
+
// `--tenant` lets each model's `siteId` drop its default and gain a relation:
|
|
23
|
+
//
|
|
24
|
+
// npx prisma-models analytics --check --in prisma/schema.prisma --tenant
|
|
19
25
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
20
26
|
import { join } from 'node:path';
|
|
27
|
+
import { arg } from './_cli.mjs';
|
|
21
28
|
|
|
22
29
|
const MODELS = ['analytics', 'passkey'];
|
|
23
30
|
const args = process.argv.slice(2);
|
|
24
31
|
const check = args.includes('--check');
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
32
|
+
const dir = arg('--dir') ?? 'prisma/schema';
|
|
33
|
+
const into = arg('--in');
|
|
34
|
+
const tenant = args.includes('--tenant');
|
|
35
|
+
const named = args.filter((a, i) => !a.startsWith('--') && !['--dir', '--in'].includes(args[i - 1]));
|
|
28
36
|
const unknown = named.filter(n => !MODELS.includes(n));
|
|
29
37
|
|
|
30
|
-
if (unknown.length || !dir) {
|
|
31
|
-
console.error(`usage: prisma-models [${MODELS.join('|')}…] [--dir <folder>] [--check]`);
|
|
38
|
+
if (unknown.length || !dir || (into !== undefined && !check)) {
|
|
39
|
+
console.error(`usage: prisma-models [${MODELS.join('|')}…] [--dir <folder>] [--check [--in <schema file> [--tenant]]]`);
|
|
32
40
|
process.exit(2);
|
|
33
41
|
}
|
|
42
|
+
|
|
43
|
+
// A model as `name → its lines`, comments, blank lines and spacing dropped.
|
|
44
|
+
const models = text =>
|
|
45
|
+
new Map(
|
|
46
|
+
[...text.matchAll(/^model\s+(\w+)\s*\{[ \t]*$([\s\S]*?)^\}/gm)].map(([, name, body]) => [
|
|
47
|
+
name,
|
|
48
|
+
body
|
|
49
|
+
.split('\n')
|
|
50
|
+
.map(l => l.replace(/\/\/.*/, '').trim().replace(/\s+/g, ' '))
|
|
51
|
+
.filter(l => l && !(tenant && /^\w+ \w+ @relation\(fields: \[siteId\]/.test(l)))
|
|
52
|
+
.map(l => (tenant ? l.replace(/^siteId String @default\(""\)$/, 'siteId String') : l))
|
|
53
|
+
.join('\n'),
|
|
54
|
+
]),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (into !== undefined) {
|
|
58
|
+
const local = models(readFileSync(into, 'utf8'));
|
|
59
|
+
let drift = 0;
|
|
60
|
+
for (const name of named.length ? named : MODELS) {
|
|
61
|
+
for (const [model, body] of models(readFileSync(new URL(`../prisma/${name}.prisma`, import.meta.url), 'utf8'))) {
|
|
62
|
+
if (local.get(model) === body) continue;
|
|
63
|
+
drift++;
|
|
64
|
+
console.error(`${into}: model ${model} ${local.has(model) ? 'differs from' : 'is missing from'} wiki-formant's prisma/${name}.prisma`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
process.exit(drift ? 1 : 0);
|
|
68
|
+
}
|
|
34
69
|
if (!existsSync(dir)) {
|
|
35
70
|
console.error(`prisma-models: no ${dir}/ here. Move prisma/schema.prisma into it and point prisma.config.ts's schema at the folder.`);
|
|
36
71
|
process.exit(2);
|
package/dist/base.css
CHANGED
|
@@ -40,6 +40,31 @@
|
|
|
40
40
|
.columns-layout > .column-view { align-self: stretch; }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/* ---- layout primitives ------------------------------------------------------
|
|
44
|
+
A column, a row and a row with its ends pushed apart. radix-wiki and caper
|
|
45
|
+
wrote these with the same gaps, and acuiq2 the column; a site that meant
|
|
46
|
+
something else by a name renamed its own. The gaps are the ones all of them
|
|
47
|
+
used, so they stay here rather than going back to every stylesheet. */
|
|
48
|
+
.stack, .stack-xs, .stack-sm, .stack-lg { display: flex; flex-direction: column; gap: 1rem; }
|
|
49
|
+
.stack-xs { gap: 0.25rem; }
|
|
50
|
+
.stack-sm { gap: 0.5rem; }
|
|
51
|
+
.stack-lg { gap: 1.5rem; }
|
|
52
|
+
.row, .row-xs, .row-md, .row-lg { display: flex; align-items: center; gap: 0.5rem; }
|
|
53
|
+
.row-xs { gap: 0.25rem; }
|
|
54
|
+
.row-md { gap: 0.75rem; }
|
|
55
|
+
.row-lg { gap: 1rem; }
|
|
56
|
+
.spread { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
57
|
+
.badge { display: inline-flex; align-items: center; border-radius: 9999px; }
|
|
58
|
+
|
|
59
|
+
/* A page whose first block is an infobox, one column with the infobox on top.
|
|
60
|
+
Where it splits into prose and a rail is the site's: a viewport width on one,
|
|
61
|
+
its column's width on another. Both items need `min-width: 0`, or the one
|
|
62
|
+
shared track sizes to the widest item's min-content and the other inherits
|
|
63
|
+
the overflow. */
|
|
64
|
+
.page-with-infobox { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-areas: "infobox" "content"; }
|
|
65
|
+
.page-with-infobox > .page-main-content { grid-area: content; min-width: 0; }
|
|
66
|
+
.page-with-infobox > .infobox { grid-area: infobox; min-width: 0; }
|
|
67
|
+
|
|
43
68
|
/* ---- the block views and the infobox ---------------------------------------
|
|
44
69
|
The structure three or four sites had each restated around this markup;
|
|
45
70
|
padding, gaps, radii and colour stay theirs. A stats block is two columns,
|
package/dist/block-views.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
// block-views.tsx — the block leaves the wikis share.
|
|
4
4
|
//
|
|
5
|
-
// Behind its own subpath for the reason `react.tsx` is: React is an
|
|
6
|
-
// peer, so a consumer that only wants the taxonomy or the MCP transport
|
|
7
|
-
// installs a package with no runtime dependencies.
|
|
5
|
+
// Behind its own subpath for the reason `react.tsx` is: React is an optional
|
|
6
|
+
// peer, so a consumer that only wants the taxonomy or the MCP transport needs no React.
|
|
8
7
|
//
|
|
9
8
|
// WHY THESE FIVE AND NOT THE SWITCH. The block union is per-repo and staying
|
|
10
9
|
// that way (see the Block Model note in the workspace CLAUDE.md) — a closed
|
package/dist/crawlers.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export interface AiCrawler {
|
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Structurally what Next's `MetadataRoute.Robots['rules']` wants, without
|
|
17
|
-
* importing Next: this package
|
|
18
|
-
*
|
|
17
|
+
* importing Next: this package does not depend on Next and is not going to for
|
|
18
|
+
* a shape that is three optional string fields.
|
|
19
19
|
*/
|
|
20
20
|
export interface RobotsGroup {
|
|
21
21
|
userAgent: string;
|
package/dist/db.d.ts
CHANGED
|
@@ -19,3 +19,28 @@ export interface PgPoolConfig {
|
|
|
19
19
|
* a page that fans out more than three queries in thirty seconds.
|
|
20
20
|
*/
|
|
21
21
|
export declare function pgPoolConfig(url: string | undefined): PgPoolConfig;
|
|
22
|
+
/**
|
|
23
|
+
* What every app's prisma.config.ts hands `defineConfig`, from the folder the
|
|
24
|
+
* file sits in: `defineConfig(prismaConfig(import.meta.dirname))`. Four apps
|
|
25
|
+
* carried the file, three of them byte for byte.
|
|
26
|
+
*
|
|
27
|
+
* Read only by the Prisma CLI, never by the app, so the URL prefers DIRECT_URL:
|
|
28
|
+
* every CLI operation that connects (db push, migrate diff, studio) needs a
|
|
29
|
+
* session connection, and Supabase's transaction pooler on 6543, which
|
|
30
|
+
* DATABASE_URL names, hangs on them rather than failing. A placeholder stands
|
|
31
|
+
* in where neither is set, as on a Vercel build: `prisma generate` reads the
|
|
32
|
+
* schema and never connects, and anything that does connect fails loudly.
|
|
33
|
+
*
|
|
34
|
+
* `schema` is a folder of .prisma files (`prisma-models` writes beside the
|
|
35
|
+
* app's own) or a single file. Paths resolve against `root`, so the CLI works
|
|
36
|
+
* from a subdirectory.
|
|
37
|
+
*/
|
|
38
|
+
export declare function prismaConfig(root: string, schema?: string): {
|
|
39
|
+
schema: string;
|
|
40
|
+
migrations: {
|
|
41
|
+
path: string;
|
|
42
|
+
};
|
|
43
|
+
datasource: {
|
|
44
|
+
url: string;
|
|
45
|
+
};
|
|
46
|
+
};
|
package/dist/db.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// db.ts — the pool settings every app here hands to `pg
|
|
1
|
+
// db.ts — the pool settings every app here hands to `pg`, and the config its prisma.config.ts hands the Prisma CLI.
|
|
2
2
|
//
|
|
3
3
|
// Four Prisma client modules carried the same block, comment and all. It is a
|
|
4
4
|
// value, not a client: the app still owns `new pg.Pool(...)`, the adapter and
|
|
@@ -25,3 +25,34 @@ export function pgPoolConfig(url) {
|
|
|
25
25
|
connectionTimeoutMillis: 30_000,
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* What every app's prisma.config.ts hands `defineConfig`, from the folder the
|
|
30
|
+
* file sits in: `defineConfig(prismaConfig(import.meta.dirname))`. Four apps
|
|
31
|
+
* carried the file, three of them byte for byte.
|
|
32
|
+
*
|
|
33
|
+
* Read only by the Prisma CLI, never by the app, so the URL prefers DIRECT_URL:
|
|
34
|
+
* every CLI operation that connects (db push, migrate diff, studio) needs a
|
|
35
|
+
* session connection, and Supabase's transaction pooler on 6543, which
|
|
36
|
+
* DATABASE_URL names, hangs on them rather than failing. A placeholder stands
|
|
37
|
+
* in where neither is set, as on a Vercel build: `prisma generate` reads the
|
|
38
|
+
* schema and never connects, and anything that does connect fails loudly.
|
|
39
|
+
*
|
|
40
|
+
* `schema` is a folder of .prisma files (`prisma-models` writes beside the
|
|
41
|
+
* app's own) or a single file. Paths resolve against `root`, so the CLI works
|
|
42
|
+
* from a subdirectory.
|
|
43
|
+
*/
|
|
44
|
+
export function prismaConfig(root, schema = 'prisma/schema') {
|
|
45
|
+
try {
|
|
46
|
+
process.loadEnvFile(`${root}/.env`);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// A Vercel build has no .env and needs none.
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
schema: `${root}/${schema}`,
|
|
53
|
+
migrations: { path: `${root}/prisma/migrations` },
|
|
54
|
+
datasource: {
|
|
55
|
+
url: process.env.DIRECT_URL ?? process.env.DATABASE_URL ?? 'postgresql://placeholder:placeholder@localhost:5432/placeholder',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/editor.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ export type { StarterKitOptions } from '@tiptap/starter-kit';
|
|
|
15
15
|
export type { LinkOptions } from '@tiptap/extension-link';
|
|
16
16
|
export type { ImageOptions } from '@tiptap/extension-image';
|
|
17
17
|
export type { TableOptions } from '@tiptap/extension-table';
|
|
18
|
+
/** The view and the editor's type, so a consumer imports nothing from `@tiptap/*` itself. */
|
|
19
|
+
export { EditorContent } from '@tiptap/react';
|
|
20
|
+
export type { Editor } from '@tiptap/core';
|
|
18
21
|
export interface WikiEditorExtensionOptions {
|
|
19
22
|
/** Empty-document prompt. */
|
|
20
23
|
placeholder?: string;
|
package/dist/editor.js
CHANGED
|
@@ -33,6 +33,8 @@ import { useEditor } from '@tiptap/react';
|
|
|
33
33
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
34
34
|
import { toMapEmbedUrl } from './maps.js';
|
|
35
35
|
import { cx } from './html.js';
|
|
36
|
+
/** The view and the editor's type, so a consumer imports nothing from `@tiptap/*` itself. */
|
|
37
|
+
export { EditorContent } from '@tiptap/react';
|
|
36
38
|
// ---- paste scrubbing --------------------------------------------------------
|
|
37
39
|
/**
|
|
38
40
|
* Reduce pasted HTML to structure plus the three attributes that carry meaning.
|
package/dist/headers.d.ts
CHANGED
|
@@ -25,3 +25,31 @@ export declare function contentSecurityPolicy(directives?: Partial<Record<CspDir
|
|
|
25
25
|
* sets its policy per request (a nonce) passes none.
|
|
26
26
|
*/
|
|
27
27
|
export declare function securityHeaders(csp?: string | null): HeaderEntry[];
|
|
28
|
+
/** The parts of a Next config `withWikiDefaults` fills in. */
|
|
29
|
+
interface WikiNextConfig {
|
|
30
|
+
distDir?: string;
|
|
31
|
+
experimental?: object;
|
|
32
|
+
images?: object;
|
|
33
|
+
headers?: () => Promise<unknown[]> | unknown[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An app's Next config with the settings every app here wrote out the same way:
|
|
37
|
+
*
|
|
38
|
+
* - `distDir` from `STUDIO_DIST_DIR`, set only by radix-studio's renders, which
|
|
39
|
+
* boot a second dev server beside the running one, and Next allows one per
|
|
40
|
+
* distDir.
|
|
41
|
+
* - `turbopackFileSystemCacheForBuild: false`. Next 16.3 turns Turbopack's
|
|
42
|
+
* build cache on and Vercel restores it from the last deployment; caper
|
|
43
|
+
* shipped a stylesheet two commits stale that way in October 2026.
|
|
44
|
+
* - AVIF before WebP from the image optimiser.
|
|
45
|
+
* - `securityHeaders` on every route, with `csp` enforced in production only:
|
|
46
|
+
* Next's dev runtime is inline and `eval`. An origin that sets its policy
|
|
47
|
+
* per request (a nonce) passes none.
|
|
48
|
+
*
|
|
49
|
+
* Anything the app sets wins, and its own `headers()` follow these. Pass
|
|
50
|
+
* `NextConfig` as `T` so the object is typed as Next's own config.
|
|
51
|
+
*/
|
|
52
|
+
export declare function withWikiDefaults<T extends WikiNextConfig>(config: T, { csp }?: {
|
|
53
|
+
csp?: string;
|
|
54
|
+
}): T;
|
|
55
|
+
export {};
|
package/dist/headers.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// `youtube-nocookie.com` its sanitiser kept, so those embeds survived cleaning
|
|
12
12
|
// and then rendered blank.
|
|
13
13
|
//
|
|
14
|
-
// Zero imports, so a config pays for nothing it does not use.
|
|
14
|
+
// Zero imports, so a config pays for nothing it does not use. `withWikiDefaults`
|
|
15
|
+
// is here for the same reason: next.config.ts is its only caller.
|
|
15
16
|
/**
|
|
16
17
|
* The embed hosts the editor's iframe, YouTube, tweet and map nodes produce:
|
|
17
18
|
* the sanitiser's default `iframeHosts` and the default `frame-src`, so an
|
|
@@ -78,3 +79,34 @@ export function securityHeaders(csp) {
|
|
|
78
79
|
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
|
|
79
80
|
];
|
|
80
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* An app's Next config with the settings every app here wrote out the same way:
|
|
84
|
+
*
|
|
85
|
+
* - `distDir` from `STUDIO_DIST_DIR`, set only by radix-studio's renders, which
|
|
86
|
+
* boot a second dev server beside the running one, and Next allows one per
|
|
87
|
+
* distDir.
|
|
88
|
+
* - `turbopackFileSystemCacheForBuild: false`. Next 16.3 turns Turbopack's
|
|
89
|
+
* build cache on and Vercel restores it from the last deployment; caper
|
|
90
|
+
* shipped a stylesheet two commits stale that way in October 2026.
|
|
91
|
+
* - AVIF before WebP from the image optimiser.
|
|
92
|
+
* - `securityHeaders` on every route, with `csp` enforced in production only:
|
|
93
|
+
* Next's dev runtime is inline and `eval`. An origin that sets its policy
|
|
94
|
+
* per request (a nonce) passes none.
|
|
95
|
+
*
|
|
96
|
+
* Anything the app sets wins, and its own `headers()` follow these. Pass
|
|
97
|
+
* `NextConfig` as `T` so the object is typed as Next's own config.
|
|
98
|
+
*/
|
|
99
|
+
export function withWikiDefaults(config, { csp } = {}) {
|
|
100
|
+
return {
|
|
101
|
+
...(process.env.STUDIO_DIST_DIR ? { distDir: process.env.STUDIO_DIST_DIR } : {}),
|
|
102
|
+
...config,
|
|
103
|
+
experimental: { turbopackFileSystemCacheForBuild: false, ...config.experimental },
|
|
104
|
+
images: { formats: ['image/avif', 'image/webp'], ...config.images },
|
|
105
|
+
async headers() {
|
|
106
|
+
return [
|
|
107
|
+
{ source: '/:path*', headers: securityHeaders(process.env.NODE_ENV === 'production' ? csp : null) },
|
|
108
|
+
...((await config.headers?.()) ?? []),
|
|
109
|
+
];
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
package/dist/headings.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NormaliseLinkOptions } from './links.js';
|
|
1
2
|
/** A heading found in a page's HTML, in document order. */
|
|
2
3
|
export interface Heading {
|
|
3
4
|
id: string;
|
|
@@ -84,3 +85,12 @@ export interface OutlineIssue {
|
|
|
84
85
|
* single `noH1` and nothing else, since every later rule would restate it.
|
|
85
86
|
*/
|
|
86
87
|
export declare function outlineIssues(html: string): OutlineIssue[];
|
|
88
|
+
/**
|
|
89
|
+
* The pass every app runs over a content block's stored HTML before it renders:
|
|
90
|
+
* heading ids, a scroll box round each table, and, given `links`, every anchor
|
|
91
|
+
* tagged internal or external. Four apps wrote the same chain in three orders.
|
|
92
|
+
* Pass one `used` set (and one `links.citedRefs`) across a page's blocks.
|
|
93
|
+
*/
|
|
94
|
+
export declare function articleHtml(html: string, { links, ...headings }?: HeadingIdOptions & {
|
|
95
|
+
links?: NormaliseLinkOptions;
|
|
96
|
+
}): string;
|
package/dist/headings.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// that with a fresh set each time, so a "Notes" in two blocks shipped two
|
|
15
15
|
// `id="notes"`. One `used` set per page is what makes the dedupe hold.
|
|
16
16
|
import { getAttr, stripTags } from './html.js';
|
|
17
|
+
import { scrollTables } from './dom.js';
|
|
18
|
+
import { normaliseLinks } from './links.js';
|
|
17
19
|
/** The default slug rule: lowercase words joined by hyphens. */
|
|
18
20
|
export function slugifyHeading(text) {
|
|
19
21
|
return text
|
|
@@ -131,3 +133,13 @@ export function outlineIssues(html) {
|
|
|
131
133
|
issues.push({ fault: 'noSubheading', level: 1 });
|
|
132
134
|
return issues;
|
|
133
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* The pass every app runs over a content block's stored HTML before it renders:
|
|
138
|
+
* heading ids, a scroll box round each table, and, given `links`, every anchor
|
|
139
|
+
* tagged internal or external. Four apps wrote the same chain in three orders.
|
|
140
|
+
* Pass one `used` set (and one `links.citedRefs`) across a page's blocks.
|
|
141
|
+
*/
|
|
142
|
+
export function articleHtml(html, { links, ...headings } = {}) {
|
|
143
|
+
const out = scrollTables(injectHeadingIds(html, headings));
|
|
144
|
+
return links ? normaliseLinks(out, links) : out;
|
|
145
|
+
}
|
package/dist/history.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `revisions.ts` decides which bump a save asks for and stays free of
|
|
4
4
|
// dependencies, because every write path imports it. This is the reading half:
|
|
5
5
|
// the same walk, turned into what a reviewer is shown. It needs a text diff, so
|
|
6
|
-
// it needs `fast-diff
|
|
6
|
+
// it needs `fast-diff`; hence its own subpath.
|
|
7
7
|
//
|
|
8
8
|
// radix-wiki had it first and shipped both HTML bodies of every modified block
|
|
9
9
|
// to the browser to diff there: 887 KB of payload for a 13-revision page whose
|
package/dist/http.d.ts
CHANGED
|
@@ -42,8 +42,12 @@ export declare function textHeaders(etag: string, lastModified: string, maxAge?:
|
|
|
42
42
|
*
|
|
43
43
|
* `lastModified` is optional because not every corpus has a row timestamp to
|
|
44
44
|
* offer, and a twin that stamps the epoch is worse than one that stamps
|
|
45
|
-
* nothing. `extra` carries whatever the mount needs on top
|
|
46
|
-
*
|
|
45
|
+
* nothing. `extra` carries whatever the mount needs on top.
|
|
46
|
+
*
|
|
47
|
+
* `noindex` is for a twin of an HTML page: a search engine that reads the twin
|
|
48
|
+
* has the page already, and Google indexed `.md` beside the HTML until four
|
|
49
|
+
* routes each set `X-Robots-Tag` by hand. A document with no HTML page, such
|
|
50
|
+
* as AGENTS.md, leaves it off.
|
|
47
51
|
*
|
|
48
52
|
* Pass `etag`. The twin is the single most recrawled URL a page has, and a
|
|
49
53
|
* response with no validator is a full render on every pass forever — the same
|
|
@@ -55,6 +59,7 @@ export declare function textHeaders(etag: string, lastModified: string, maxAge?:
|
|
|
55
59
|
export declare function markdownHeaders(lastModified?: string | null, opts?: {
|
|
56
60
|
maxAge?: number;
|
|
57
61
|
etag?: string;
|
|
62
|
+
noindex?: boolean;
|
|
58
63
|
extra?: Record<string, string>;
|
|
59
64
|
}): Record<string, string>;
|
|
60
65
|
/**
|
package/dist/http.js
CHANGED
|
@@ -100,8 +100,12 @@ export function textHeaders(etag, lastModified, maxAge = 3600) {
|
|
|
100
100
|
*
|
|
101
101
|
* `lastModified` is optional because not every corpus has a row timestamp to
|
|
102
102
|
* offer, and a twin that stamps the epoch is worse than one that stamps
|
|
103
|
-
* nothing. `extra` carries whatever the mount needs on top
|
|
104
|
-
*
|
|
103
|
+
* nothing. `extra` carries whatever the mount needs on top.
|
|
104
|
+
*
|
|
105
|
+
* `noindex` is for a twin of an HTML page: a search engine that reads the twin
|
|
106
|
+
* has the page already, and Google indexed `.md` beside the HTML until four
|
|
107
|
+
* routes each set `X-Robots-Tag` by hand. A document with no HTML page, such
|
|
108
|
+
* as AGENTS.md, leaves it off.
|
|
105
109
|
*
|
|
106
110
|
* Pass `etag`. The twin is the single most recrawled URL a page has, and a
|
|
107
111
|
* response with no validator is a full render on every pass forever — the same
|
|
@@ -117,6 +121,7 @@ export function markdownHeaders(lastModified, opts = {}) {
|
|
|
117
121
|
'Cache-Control': `public, s-maxage=${maxAge}, stale-while-revalidate=${maxAge * 24}`,
|
|
118
122
|
...(opts.etag ? { ETag: opts.etag } : {}),
|
|
119
123
|
...(lastModified ? { 'Last-Modified': lastModified } : {}),
|
|
124
|
+
...(opts.noindex ? { 'X-Robots-Tag': 'noindex' } : {}),
|
|
120
125
|
...opts.extra,
|
|
121
126
|
};
|
|
122
127
|
}
|
package/dist/indexnow.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ export interface IndexNowEntry {
|
|
|
7
7
|
}
|
|
8
8
|
/** The `<loc>` and `<lastmod>` of every `<url>` in a sitemap. */
|
|
9
9
|
export declare function sitemapEntries(xml: string): IndexNowEntry[];
|
|
10
|
+
/**
|
|
11
|
+
* Every entry of the sitemap at `url`, following a sitemap index one level down
|
|
12
|
+
* to at most 20 children. The `indexnow` and `check-pages` bins each parsed
|
|
13
|
+
* sitemaps, and only one of them read an index. Throws when `url` itself does
|
|
14
|
+
* not answer 200; a child that fails is skipped.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readSitemap(url: string, fetchImpl?: typeof fetch): Promise<IndexNowEntry[]>;
|
|
10
17
|
/**
|
|
11
18
|
* The URLs modified at or after `since`. An entry with no `lastModified` is
|
|
12
19
|
* left out: nothing says it changed, and the protocol asks for changed pages
|
package/dist/indexnow.js
CHANGED
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
// The key is not a secret. An engine verifies a submission by fetching
|
|
13
13
|
// `/<key>.txt` from the origin, so the key is public by design, and the file in
|
|
14
14
|
// each app's `public/` is the only copy.
|
|
15
|
+
import { decodeEntities } from './markdown.js';
|
|
15
16
|
export const INDEXNOW_ENDPOINT = 'https://api.indexnow.org/indexnow';
|
|
16
17
|
/** The protocol's cap on the URLs in one POST. */
|
|
17
18
|
export const INDEXNOW_BATCH = 10_000;
|
|
18
|
-
const XML_ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
|
|
19
|
-
const unescapeXml = (s) => s.trim().replace(/&(amp|lt|gt|quot|apos);/g, (_, e) => XML_ENTITIES[e]);
|
|
20
19
|
/** The `<loc>` and `<lastmod>` of every `<url>` in a sitemap. */
|
|
21
20
|
export function sitemapEntries(xml) {
|
|
22
21
|
const entries = [];
|
|
@@ -25,10 +24,30 @@ export function sitemapEntries(xml) {
|
|
|
25
24
|
if (!loc)
|
|
26
25
|
continue;
|
|
27
26
|
const lastmod = /<lastmod>([\s\S]*?)<\/lastmod>/.exec(body)?.[1];
|
|
28
|
-
|
|
27
|
+
const url = decodeEntities(loc.trim());
|
|
28
|
+
entries.push(lastmod ? { url, lastModified: lastmod.trim() } : { url });
|
|
29
29
|
}
|
|
30
30
|
return entries;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Every entry of the sitemap at `url`, following a sitemap index one level down
|
|
34
|
+
* to at most 20 children. The `indexnow` and `check-pages` bins each parsed
|
|
35
|
+
* sitemaps, and only one of them read an index. Throws when `url` itself does
|
|
36
|
+
* not answer 200; a child that fails is skipped.
|
|
37
|
+
*/
|
|
38
|
+
export async function readSitemap(url, fetchImpl = fetch) {
|
|
39
|
+
const res = await fetchImpl(url);
|
|
40
|
+
if (!res.ok)
|
|
41
|
+
throw new Error(`${url} answered ${res.status}`);
|
|
42
|
+
const xml = await res.text();
|
|
43
|
+
if (!/<sitemapindex/i.test(xml))
|
|
44
|
+
return sitemapEntries(xml);
|
|
45
|
+
const children = [...xml.matchAll(/<loc>([\s\S]*?)<\/loc>/g)].slice(0, 20).map(m => decodeEntities(m[1].trim()));
|
|
46
|
+
const entries = [];
|
|
47
|
+
for (const child of children)
|
|
48
|
+
entries.push(...(await readSitemap(child, fetchImpl).catch(() => [])));
|
|
49
|
+
return entries;
|
|
50
|
+
}
|
|
32
51
|
/**
|
|
33
52
|
* The URLs modified at or after `since`. An entry with no `lastModified` is
|
|
34
53
|
* left out: nothing says it changed, and the protocol asks for changed pages
|
package/dist/maintenance.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// which is what lets an audit spend no probes on its flag list.
|
|
18
18
|
//
|
|
19
19
|
// No subpath exports this: the bins and the tests import it from dist.
|
|
20
|
+
import { escXml } from './feed.js';
|
|
20
21
|
/** Held in state. */
|
|
21
22
|
const MAX = { backlog: 60, runs: 30, feedback: 40, sources: 150 };
|
|
22
23
|
/** Shown on the page. What a reader pays is words, and the entries run long. */
|
|
@@ -63,7 +64,7 @@ export function addBacklog(state, items, today) {
|
|
|
63
64
|
}
|
|
64
65
|
return added;
|
|
65
66
|
}
|
|
66
|
-
const esc = (s) => String(s ?? '')
|
|
67
|
+
const esc = (s) => escXml(String(s ?? ''));
|
|
67
68
|
const more = (shown, total, what) => total > shown ? `<p><em>${total - shown} older ${what} held in state.</em></p>` : '';
|
|
68
69
|
const table = (head, rows) => `<table><tbody><tr>${head.map(h => `<td><strong>${h}</strong></td>`).join('')}</tr>` +
|
|
69
70
|
(rows.join('') || `<tr>${head.map(() => '<td>–</td>').join('')}</tr>`) +
|
package/dist/react.d.ts
CHANGED
|
@@ -171,7 +171,7 @@ export interface TypeaheadState<T> {
|
|
|
171
171
|
* per mount, not module-level, so a stale result cannot outlive the page.
|
|
172
172
|
*
|
|
173
173
|
* The debounce is inlined rather than pulled from a package: this one is nine
|
|
174
|
-
* lines, and
|
|
174
|
+
* lines, and a dependency for nine lines is not worth one.
|
|
175
175
|
*/
|
|
176
176
|
export declare function useTypeahead<T>({ fetch, onPick, onEscape, delay, minLength, cache, }: TypeaheadOptions<T>): TypeaheadState<T>;
|
|
177
177
|
export interface LinkPreviewOptions<T> {
|
package/dist/react.js
CHANGED
|
@@ -3,9 +3,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
// react.tsx — the collapsible wiki sidebar, and the table of contents inside it.
|
|
4
4
|
//
|
|
5
5
|
// This is the one part of the package that needs React, so it is the one part
|
|
6
|
-
// behind its own subpath: `wiki-formant/react`. React is an
|
|
7
|
-
//
|
|
8
|
-
// still installs a package with no runtime dependencies at all.
|
|
6
|
+
// behind its own subpath: `wiki-formant/react`. React is an optional peer, so a
|
|
7
|
+
// consumer that only wants the taxonomy or the MCP transport needs no React.
|
|
9
8
|
//
|
|
10
9
|
// What is shared here is behaviour, not appearance. Every class name is passed
|
|
11
10
|
// in, because the three wikis style their rails differently and always will;
|
|
@@ -292,7 +291,7 @@ export function useClickOutside(onClose) {
|
|
|
292
291
|
* per mount, not module-level, so a stale result cannot outlive the page.
|
|
293
292
|
*
|
|
294
293
|
* The debounce is inlined rather than pulled from a package: this one is nine
|
|
295
|
-
* lines, and
|
|
294
|
+
* lines, and a dependency for nine lines is not worth one.
|
|
296
295
|
*/
|
|
297
296
|
export function useTypeahead({ fetch, onPick, onEscape, delay = 200, minLength = 1, cache = true, }) {
|
|
298
297
|
const [query, setQuery] = useState('');
|
package/dist/sanitize.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Everything is an allowlist: unknown tags, unknown attributes (every on*
|
|
12
12
|
// handler among them) and non-http(s) URLs are dropped, not escaped. Run it
|
|
13
13
|
// server-side, on the render path, so it covers rows written before it existed
|
|
14
|
-
// and no sanitiser ships to the client.
|
|
14
|
+
// and no sanitiser ships to the client.
|
|
15
15
|
import sanitizeHtml from 'sanitize-html';
|
|
16
16
|
import { DEFAULT_IFRAME_HOSTS } from './headers.js';
|
|
17
17
|
// Inline SVG, presentational elements only. The infographics pipeline embeds
|
package/dist/stats.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import { type Digest, type StatsFilter } from './analytics.js';
|
|
3
|
+
import type { PasskeyGate } from './passkey.js';
|
|
2
4
|
export interface StatsProps {
|
|
3
5
|
/** One window's figures, from `digest` in `wiki-formant/analytics`. */
|
|
4
6
|
digest: Digest;
|
|
@@ -26,3 +28,29 @@ export declare function statsQuery(params: Record<string, string | string[] | un
|
|
|
26
28
|
* until the importing stylesheet gives them a colour.
|
|
27
29
|
*/
|
|
28
30
|
export declare function Stats({ digest: d, days, filter, ranges }: StatsProps): import("react").JSX.Element;
|
|
31
|
+
export interface StatsGateProps {
|
|
32
|
+
gate: PasskeyGate;
|
|
33
|
+
/** The gate's cookie from the request: `(await cookies()).get(gate.cookie)?.value`. */
|
|
34
|
+
session: string | undefined;
|
|
35
|
+
/** `?invite=` from the link, when there is one. */
|
|
36
|
+
invite?: string | undefined;
|
|
37
|
+
/** The site's host, named in the prompt. */
|
|
38
|
+
host: string;
|
|
39
|
+
/** The route that serves `gate.route`. */
|
|
40
|
+
endpoint?: string;
|
|
41
|
+
/** What a signed-in reader sees, rendered only for one. */
|
|
42
|
+
children: () => ReactNode | Promise<ReactNode>;
|
|
43
|
+
classNames?: {
|
|
44
|
+
text?: string;
|
|
45
|
+
actions?: string;
|
|
46
|
+
button?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A stats page's door: `children` for a signed-in reader, else a line saying
|
|
52
|
+
* what the passkey prompt will do and the button that does it. With an invite
|
|
53
|
+
* it saves a passkey, as it does while the site has none; otherwise it signs
|
|
54
|
+
* in. Three apps wrote this branch and its three sentences.
|
|
55
|
+
*/
|
|
56
|
+
export declare function StatsGate({ gate, session, invite, host, endpoint, children, classNames: c }: StatsGateProps): Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").JSX.Element | null | undefined>;
|
package/dist/stats.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { STATS_FILTERS } from './analytics.js';
|
|
3
|
+
import { PasskeyButton } from './passkey-button.js';
|
|
3
4
|
import { StatsFigures } from './stats-figures.js';
|
|
4
5
|
/** The windows the stats page offers, in days; `Infinity` is all time. */
|
|
5
6
|
const STATS_RANGES = [1, 7, 30, 90, 365, Infinity];
|
|
@@ -74,3 +75,19 @@ export function Stats({ digest: d, days, filter = {}, ranges = STATS_RANGES }) {
|
|
|
74
75
|
const windows = (_jsx("div", { role: "group", "aria-label": "Window", children: ranges.map(n => (_jsx("a", { href: href(n, filter), "aria-current": n === days ? 'page' : undefined, children: windowLabel(n) }, n))) }));
|
|
75
76
|
return (_jsxs("div", { className: "stats", children: [narrowed.length > 0 && (_jsxs("div", { className: "stats-filters", children: [_jsx("ul", { "aria-label": "Narrowed to", children: narrowed.map(k => (_jsx("li", { children: _jsxs("a", { href: href(days, { ...filter, [k]: undefined }), title: "Remove", children: [NAMES[k], ": ", k === 'country' ? country(filter[k]) : filter[k], " \u00D7"] }) }, k))) }), start < counted && d.counted_from && (_jsxs("p", { children: ["Narrowed figures start on ", d.counted_from, ", the first day this site counted its own visits."] }))] })), _jsx(StatsFigures, { digest: d, first: first, windows: windows }), _jsxs("div", { className: "stats-lists", children: [_jsx(StatsList, { title: "Pages", rows: rows(d.top_pages, 'page', 'visitors'), to: to('page') }), _jsx(StatsList, { title: "Entry pages", rows: rows(d.entry_pages, 'page', 'visitors'), to: to('entry') }), _jsx(StatsList, { title: "Sources", rows: rows(d.top_sources, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "From X posts", rows: rows(d.from_posts, 'page', 'visitors'), to: v => href(days, { ...filter, source: 'x', entry: v }) }), _jsx(StatsList, { title: "Sibling sites", rows: rows(d.sibling_referrals, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "Countries", rows: rows(d.countries, 'country', 'visitors'), label: country, to: to('country') }), _jsx(StatsList, { title: "Devices", rows: rows(d.devices, 'device', 'visitors'), to: to('device') }), _jsx(StatsList, { title: "Agent tool calls", rows: rows(d.agent_tool_calls, 'tool', 'events') }), _jsx(StatsList, { title: "Searches that found nothing", rows: rows(d.gaps, 'query', 'events') })] })] }));
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* A stats page's door: `children` for a signed-in reader, else a line saying
|
|
80
|
+
* what the passkey prompt will do and the button that does it. With an invite
|
|
81
|
+
* it saves a passkey, as it does while the site has none; otherwise it signs
|
|
82
|
+
* in. Three apps wrote this branch and its three sentences.
|
|
83
|
+
*/
|
|
84
|
+
export async function StatsGate({ gate, session, invite, host, endpoint = '/api/passkey', children, classNames: c = {} }) {
|
|
85
|
+
if (!invite && (await gate.signedIn(session)))
|
|
86
|
+
return children();
|
|
87
|
+
const open = !invite && (await gate.open());
|
|
88
|
+
return (_jsxs(_Fragment, { children: [_jsx("p", { className: c.text, children: invite
|
|
89
|
+
? `Save a passkey on this device to open ${host}'s stats.`
|
|
90
|
+
: open
|
|
91
|
+
? `No passkey is saved for ${host} yet. Save the first one here; any after that needs an invite.`
|
|
92
|
+
: 'Sign in with a passkey saved for this site.' }), _jsx("div", { className: c.actions, children: _jsx(PasskeyButton, { endpoint: endpoint, invite: invite, create: open, className: c.button, errorClassName: c.error }) })] }));
|
|
93
|
+
}
|
package/dist/tiptap.js
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// tiptap.tsx — the custom editor nodes the wikis share.
|
|
4
4
|
//
|
|
5
|
-
// Behind its own subpath
|
|
6
|
-
// `@tiptap/*` packages
|
|
7
|
-
// wants the taxonomy or the MCP transport still installs a package with no
|
|
8
|
-
// runtime dependencies at all.
|
|
5
|
+
// Behind its own subpath so a module that only wants the taxonomy or the MCP
|
|
6
|
+
// transport never loads the `@tiptap/*` packages.
|
|
9
7
|
//
|
|
10
8
|
// What is shared is the node schema and its behaviour, never appearance. Class
|
|
11
9
|
// names and icons are injected, which is what lets one wiki keep `text-jupiter`
|