@pathmx/core 0.5.0
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.md +172 -0
- package/README.md +111 -0
- package/actions/actions.ts +307 -0
- package/actions/draft.ts +105 -0
- package/actions/transaction.ts +164 -0
- package/assets.ts +149 -0
- package/authority/access.ts +820 -0
- package/authority/actors.ts +271 -0
- package/authority/agents.ts +95 -0
- package/authority/credentials.ts +144 -0
- package/authority/model.ts +71 -0
- package/authority/sharing.ts +89 -0
- package/browser-bundle.ts +61 -0
- package/canonical.ts +41 -0
- package/cli/dev.ts +134 -0
- package/cli/export.ts +74 -0
- package/cli/index.ts +52 -0
- package/cli/lint.ts +164 -0
- package/cli/mv.ts +30 -0
- package/cli/perms.ts +182 -0
- package/cli/plugin-packages.ts +159 -0
- package/cli/plugins.ts +95 -0
- package/cli/query.ts +225 -0
- package/cli/readme.md +95 -0
- package/cli/release.ts +75 -0
- package/cli/rm.ts +22 -0
- package/cli/serve.ts +100 -0
- package/cli/tree.ts +131 -0
- package/cli/update.ts +78 -0
- package/cli/utils.ts +53 -0
- package/core.ts +96 -0
- package/database.ts +189 -0
- package/dependencies.ts +60 -0
- package/dist/pathmx.js +1224 -0
- package/document.ts +194 -0
- package/environment.ts +110 -0
- package/environments/bun-plugins.ts +68 -0
- package/environments/bun-publish.ts +194 -0
- package/environments/bun.ts +223 -0
- package/globals.d.ts +14 -0
- package/graph.ts +153 -0
- package/host/compiler.ts +314 -0
- package/host/create.ts +66 -0
- package/host/engine.ts +630 -0
- package/host/plugin-host.ts +487 -0
- package/host/render.ts +102 -0
- package/host/response.ts +20 -0
- package/host/shell.html +15 -0
- package/host/source-sync.ts +254 -0
- package/html-attributes.ts +15 -0
- package/html.ts +48 -0
- package/icons/LICENSE +43 -0
- package/icons/lucide.ts +54 -0
- package/index.ts +75 -0
- package/invalidation.ts +85 -0
- package/log.ts +84 -0
- package/ops/href.ts +87 -0
- package/ops/index.ts +9 -0
- package/ops/move-source.ts +114 -0
- package/ops/plan.ts +47 -0
- package/ops/remove-source.ts +70 -0
- package/package.json +46 -0
- package/path.ts +75 -0
- package/pattern.ts +314 -0
- package/perf.ts +88 -0
- package/plugins/actor-path.ts +43 -0
- package/plugins/actor-profile.ts +55 -0
- package/plugins/agent-browsing.ts +183 -0
- package/plugins/app-component.ts +23 -0
- package/plugins/block-layout/index.ts +78 -0
- package/plugins/block-layout/theme.css +90 -0
- package/plugins/callout/index.ts +236 -0
- package/plugins/callout/theme.css +138 -0
- package/plugins/canonical-links.ts +144 -0
- package/plugins/code-highlight/client.ts +89 -0
- package/plugins/code-highlight/index.ts +147 -0
- package/plugins/code-highlight/theme.css +89 -0
- package/plugins/context.ts +131 -0
- package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
- package/plugins/datatype/OFL.txt +96 -0
- package/plugins/datatype/index.ts +287 -0
- package/plugins/definition-list.ts +236 -0
- package/plugins/directive-definitions.ts +13 -0
- package/plugins/embedding/client.ts +88 -0
- package/plugins/embedding/index.ts +43 -0
- package/plugins/image/index.ts +68 -0
- package/plugins/image/theme.css +43 -0
- package/plugins/include.ts +119 -0
- package/plugins/index.ts +91 -0
- package/plugins/input/authoring.ts +236 -0
- package/plugins/input/config.ts +85 -0
- package/plugins/input/index.ts +359 -0
- package/plugins/input/model.ts +173 -0
- package/plugins/input/state.ts +272 -0
- package/plugins/layout.ts +313 -0
- package/plugins/literate/client.ts +208 -0
- package/plugins/literate/index.ts +742 -0
- package/plugins/math/index.ts +140 -0
- package/plugins/meta.ts +42 -0
- package/plugins/navigation/index.ts +158 -0
- package/plugins/navigation/model.ts +364 -0
- package/plugins/preset.ts +65 -0
- package/plugins/query/index.ts +322 -0
- package/plugins/rss/index.ts +46 -0
- package/plugins/runtime/actions.ts +62 -0
- package/plugins/runtime/client.ts +8 -0
- package/plugins/runtime/document.ts +506 -0
- package/plugins/runtime/error.ts +154 -0
- package/plugins/runtime/heading-links.ts +31 -0
- package/plugins/runtime/index.ts +104 -0
- package/plugins/runtime/live.ts +51 -0
- package/plugins/runtime/navigation.ts +113 -0
- package/plugins/runtime/pending.ts +53 -0
- package/plugins/runtime/publication.ts +19 -0
- package/plugins/runtime/start.ts +177 -0
- package/plugins/runtime/static-client.ts +6 -0
- package/plugins/runtime/static-live.ts +188 -0
- package/plugins/runtime/view-transition.ts +72 -0
- package/plugins/runtime/viewport.ts +110 -0
- package/plugins/schema.ts +24 -0
- package/plugins/scripts.ts +140 -0
- package/plugins/source-preview/client.ts +256 -0
- package/plugins/source-preview/index.ts +132 -0
- package/plugins/source-preview/model.ts +11 -0
- package/plugins/source-preview/preview.css +107 -0
- package/plugins/styles.ts +145 -0
- package/plugins/theme/components.ts +10 -0
- package/plugins/theme/foundation.css +358 -0
- package/plugins/theme/index.ts +333 -0
- package/plugins/theme/prose.css +284 -0
- package/plugins/theme/scheme.js +63 -0
- package/plugins/theme/toggle.css +44 -0
- package/plugins/theme/view-transition.css +68 -0
- package/plugins/toc/client.ts +146 -0
- package/plugins/toc/index.ts +232 -0
- package/plugins/toc/theme.css +172 -0
- package/plugins/types.ts +538 -0
- package/problem.ts +41 -0
- package/project.ts +6 -0
- package/publish/build.ts +402 -0
- package/publish/concurrent.ts +32 -0
- package/publish/identity.ts +63 -0
- package/publish/index.ts +11 -0
- package/publish/location.ts +167 -0
- package/publish/model.ts +58 -0
- package/publish/plan.ts +300 -0
- package/publish/validate.ts +150 -0
- package/query/candidate-index.ts +101 -0
- package/query/dependency-index.ts +99 -0
- package/query/fields.ts +216 -0
- package/query/invalidation.ts +48 -0
- package/query/kernel.ts +776 -0
- package/query/model.ts +79 -0
- package/repository.ts +374 -0
- package/runtime-profile.ts +34 -0
- package/schema/model.ts +92 -0
- package/schema/registry.ts +642 -0
- package/schema/standard.ts +226 -0
- package/server/context.ts +94 -0
- package/server/css-imports.ts +66 -0
- package/server/http.ts +333 -0
- package/server/page.ts +21 -0
- package/server/protocols.ts +12 -0
- package/server/representations.ts +66 -0
- package/server/router.ts +131 -0
- package/server/routes.ts +202 -0
- package/server/serve.ts +5 -0
- package/server/watch.ts +120 -0
- package/source/blocks.ts +71 -0
- package/source/format.ts +164 -0
- package/source/index.ts +67 -0
- package/source/interpolate.ts +63 -0
- package/source/markdown/blocks.ts +46 -0
- package/source/markdown/comments.ts +32 -0
- package/source/markdown/fences.ts +67 -0
- package/source/markdown/headings.ts +88 -0
- package/source/markdown/index.ts +34 -0
- package/source/markdown/inline-code.ts +6 -0
- package/source/markdown/links.ts +326 -0
- package/source/markdown/lists.ts +223 -0
- package/source/markdown/prose.ts +74 -0
- package/source/markdown/scan.ts +79 -0
- package/source/markdown/text.ts +82 -0
- package/source/meta.ts +87 -0
- package/source/source.ts +121 -0
- package/source/topmatter.ts +57 -0
- package/source/utils.ts +21 -0
- package/source-diagnostic.ts +63 -0
- package/text-file.ts +26 -0
- package/util.ts +35 -0
- package/view/components.ts +499 -0
- package/view/model.ts +91 -0
- package/view/projection.ts +162 -0
- package/view/session.ts +146 -0
- package/view/syntax.ts +372 -0
package/pattern.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type { PathMXEnvironment } from "./environment.ts"
|
|
2
|
+
import type { StandardSchemaV1 } from "./schema/standard.ts"
|
|
3
|
+
import { formatSchemaIssues } from "./schema/standard.ts"
|
|
4
|
+
import {
|
|
5
|
+
readTextFileState,
|
|
6
|
+
sameTextFileState,
|
|
7
|
+
type TextFileState,
|
|
8
|
+
} from "./text-file.ts"
|
|
9
|
+
|
|
10
|
+
type Awaitable<T> = T | Promise<T>
|
|
11
|
+
|
|
12
|
+
export type PatternRoot = "paths" | "project"
|
|
13
|
+
|
|
14
|
+
export type PatternFile = Readonly<{
|
|
15
|
+
root: PatternRoot
|
|
16
|
+
path: string
|
|
17
|
+
content: string
|
|
18
|
+
}>
|
|
19
|
+
|
|
20
|
+
export type Pattern<Input = undefined> = Readonly<{
|
|
21
|
+
id: string
|
|
22
|
+
description?: string
|
|
23
|
+
input?: StandardSchemaV1<unknown, Input>
|
|
24
|
+
generate(input: Input): Awaitable<readonly PatternFile[]>
|
|
25
|
+
}>
|
|
26
|
+
|
|
27
|
+
export type PatternConflictPolicy = "error" | "preserve" | "overwrite"
|
|
28
|
+
|
|
29
|
+
export type PatternInstallOptions = Readonly<{
|
|
30
|
+
conflicts?: PatternConflictPolicy
|
|
31
|
+
}>
|
|
32
|
+
|
|
33
|
+
export type PatternFileStatus = "create" | "unchanged" | "conflict" | "replace"
|
|
34
|
+
|
|
35
|
+
export type PatternPlanFile = PatternFile &
|
|
36
|
+
Readonly<{
|
|
37
|
+
destination: string
|
|
38
|
+
status: PatternFileStatus
|
|
39
|
+
before: TextFileState
|
|
40
|
+
}>
|
|
41
|
+
|
|
42
|
+
export type PatternPlan = Readonly<{
|
|
43
|
+
pattern: string
|
|
44
|
+
conflicts: PatternConflictPolicy
|
|
45
|
+
files: readonly PatternPlanFile[]
|
|
46
|
+
}>
|
|
47
|
+
|
|
48
|
+
export type PatternRoots = Readonly<{
|
|
49
|
+
paths: string
|
|
50
|
+
project: string
|
|
51
|
+
}>
|
|
52
|
+
|
|
53
|
+
export type PatternInstaller = Readonly<{
|
|
54
|
+
plan<Input>(
|
|
55
|
+
pattern: Pattern<Input>,
|
|
56
|
+
input?: unknown,
|
|
57
|
+
options?: PatternInstallOptions,
|
|
58
|
+
): Promise<PatternPlan>
|
|
59
|
+
apply(plan: PatternPlan): Promise<PatternPlan>
|
|
60
|
+
install<Input>(
|
|
61
|
+
pattern: Pattern<Input>,
|
|
62
|
+
input?: unknown,
|
|
63
|
+
options?: PatternInstallOptions,
|
|
64
|
+
): Promise<PatternPlan>
|
|
65
|
+
}>
|
|
66
|
+
|
|
67
|
+
export class PatternError extends Error {
|
|
68
|
+
constructor(message: string) {
|
|
69
|
+
super(message)
|
|
70
|
+
this.name = "PatternError"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const PATTERN_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*(?::[a-z0-9]+(?:-[a-z0-9]+)*)?$/
|
|
75
|
+
|
|
76
|
+
function canonicalPatternId(id: string) {
|
|
77
|
+
if (!PATTERN_ID.test(id)) throw new PatternError(`Invalid Pattern id: ${id}`)
|
|
78
|
+
return id
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Preserve input inference for install-time project Patterns. */
|
|
82
|
+
export function definePattern<Input = undefined>(pattern: Pattern<Input>) {
|
|
83
|
+
return pattern
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function validateInput<Input>(pattern: Pattern<Input>, input: unknown) {
|
|
87
|
+
if (!pattern.input) {
|
|
88
|
+
if (input !== undefined) {
|
|
89
|
+
throw new PatternError(`Pattern ${pattern.id} does not accept input.`)
|
|
90
|
+
}
|
|
91
|
+
return undefined as Input
|
|
92
|
+
}
|
|
93
|
+
const result = await pattern.input["~standard"].validate(input)
|
|
94
|
+
if (result.issues) {
|
|
95
|
+
throw new PatternError(
|
|
96
|
+
`Invalid input for Pattern ${pattern.id}: ${formatSchemaIssues(result.issues)}`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
return result.value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizedFile(
|
|
103
|
+
environment: Pick<PathMXEnvironment, "path">,
|
|
104
|
+
roots: PatternRoots,
|
|
105
|
+
file: PatternFile,
|
|
106
|
+
) {
|
|
107
|
+
if (file.root !== "paths" && file.root !== "project") {
|
|
108
|
+
throw new PatternError(`Invalid Pattern root: ${String(file.root)}`)
|
|
109
|
+
}
|
|
110
|
+
if (typeof file.content !== "string") {
|
|
111
|
+
throw new PatternError(`Pattern file content must be text: ${file.path}`)
|
|
112
|
+
}
|
|
113
|
+
const raw = file.path.replaceAll("\\", "/")
|
|
114
|
+
if (
|
|
115
|
+
!raw ||
|
|
116
|
+
raw.startsWith("/") ||
|
|
117
|
+
/^[a-zA-Z]:\//.test(raw) ||
|
|
118
|
+
raw.split("/").includes("..")
|
|
119
|
+
) {
|
|
120
|
+
throw new PatternError(
|
|
121
|
+
`Pattern path escapes its ${file.root} root: ${file.path}`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
const path = environment.path.normalize(raw)
|
|
125
|
+
if (!path || path === ".") {
|
|
126
|
+
throw new PatternError(`Invalid Pattern path: ${file.path}`)
|
|
127
|
+
}
|
|
128
|
+
const root = roots[file.root]
|
|
129
|
+
const destination = environment.path.join(root, path)
|
|
130
|
+
const relative = environment.path.relative(root, destination)
|
|
131
|
+
if (!relative || relative === ".." || relative.startsWith("../")) {
|
|
132
|
+
throw new PatternError(
|
|
133
|
+
`Pattern path escapes its ${file.root} root: ${file.path}`,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return Object.freeze({ ...file, path, destination })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function status(
|
|
140
|
+
before: TextFileState,
|
|
141
|
+
content: string,
|
|
142
|
+
conflicts: PatternConflictPolicy,
|
|
143
|
+
): PatternFileStatus {
|
|
144
|
+
if (before.type === "missing") return "create"
|
|
145
|
+
if (before.content === content) return "unchanged"
|
|
146
|
+
return conflicts === "overwrite" ? "replace" : "conflict"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function conflictPolicy(conflicts: PatternConflictPolicy = "error") {
|
|
150
|
+
if (
|
|
151
|
+
conflicts !== "error" &&
|
|
152
|
+
conflicts !== "preserve" &&
|
|
153
|
+
conflicts !== "overwrite"
|
|
154
|
+
) {
|
|
155
|
+
throw new PatternError(
|
|
156
|
+
`Invalid Pattern conflict policy: ${String(conflicts)}`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
return conflicts
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function rollback(
|
|
163
|
+
storage: PathMXEnvironment["storage"],
|
|
164
|
+
attempted: readonly PatternPlanFile[],
|
|
165
|
+
) {
|
|
166
|
+
const errors: unknown[] = []
|
|
167
|
+
for (const file of [...attempted].reverse()) {
|
|
168
|
+
try {
|
|
169
|
+
const current = await readTextFileState(storage, file.destination)
|
|
170
|
+
if (sameTextFileState(current, file.before)) continue
|
|
171
|
+
if (current.type !== "present" || current.content !== file.content) {
|
|
172
|
+
throw new PatternError(
|
|
173
|
+
`Pattern destination changed during rollback: ${file.destination}`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
if (file.before.type === "missing") {
|
|
177
|
+
await storage.delete(file.destination)
|
|
178
|
+
} else {
|
|
179
|
+
await storage.writeText(file.destination, file.before.content)
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
errors.push(error)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return errors
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createPatternInstaller(
|
|
189
|
+
environment: Pick<PathMXEnvironment, "path" | "storage">,
|
|
190
|
+
roots: PatternRoots,
|
|
191
|
+
): PatternInstaller {
|
|
192
|
+
const installRoots = Object.freeze({ ...roots })
|
|
193
|
+
const resolve = (file: PatternFile) =>
|
|
194
|
+
normalizedFile(environment, installRoots, file)
|
|
195
|
+
|
|
196
|
+
const plan: PatternInstaller["plan"] = async (pattern, input, options) => {
|
|
197
|
+
const id = canonicalPatternId(pattern.id)
|
|
198
|
+
const conflicts = conflictPolicy(options?.conflicts)
|
|
199
|
+
const generated = await pattern.generate(
|
|
200
|
+
await validateInput(pattern, input),
|
|
201
|
+
)
|
|
202
|
+
const resolved = generated.map(resolve)
|
|
203
|
+
const destinations = new Set<string>()
|
|
204
|
+
for (const file of resolved) {
|
|
205
|
+
if (destinations.has(file.destination)) {
|
|
206
|
+
throw new PatternError(
|
|
207
|
+
`Duplicate Pattern destination: ${file.destination}`,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
destinations.add(file.destination)
|
|
211
|
+
}
|
|
212
|
+
const files = await Promise.all(
|
|
213
|
+
resolved.map(async (file): Promise<PatternPlanFile> => {
|
|
214
|
+
const before = await readTextFileState(
|
|
215
|
+
environment.storage,
|
|
216
|
+
file.destination,
|
|
217
|
+
)
|
|
218
|
+
return Object.freeze({
|
|
219
|
+
...file,
|
|
220
|
+
before,
|
|
221
|
+
status: status(before, file.content, conflicts),
|
|
222
|
+
})
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
pattern: id,
|
|
227
|
+
conflicts,
|
|
228
|
+
files: Object.freeze(files),
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const apply: PatternInstaller["apply"] = async (patternPlan) => {
|
|
233
|
+
canonicalPatternId(patternPlan.pattern)
|
|
234
|
+
conflictPolicy(patternPlan.conflicts)
|
|
235
|
+
for (const file of patternPlan.files) {
|
|
236
|
+
const expected = status(file.before, file.content, patternPlan.conflicts)
|
|
237
|
+
if (file.status !== expected) {
|
|
238
|
+
throw new PatternError(`Invalid Pattern plan: ${file.destination}`)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const conflicts = patternPlan.files.filter(
|
|
242
|
+
(file) => file.status === "conflict",
|
|
243
|
+
)
|
|
244
|
+
if (patternPlan.conflicts === "error" && conflicts.length) {
|
|
245
|
+
throw new PatternError(
|
|
246
|
+
`Pattern ${patternPlan.pattern} conflicts with existing files: ${conflicts
|
|
247
|
+
.map((file) => file.destination)
|
|
248
|
+
.join(", ")}`,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
const writes = patternPlan.files.filter(
|
|
252
|
+
(file) => file.status === "create" || file.status === "replace",
|
|
253
|
+
)
|
|
254
|
+
const resolved = writes.map((file) => {
|
|
255
|
+
const current = resolve({
|
|
256
|
+
root: file.root,
|
|
257
|
+
path: file.path,
|
|
258
|
+
content: file.content,
|
|
259
|
+
})
|
|
260
|
+
if (current.destination !== file.destination) {
|
|
261
|
+
throw new PatternError(`Invalid Pattern plan: ${file.destination}`)
|
|
262
|
+
}
|
|
263
|
+
return { ...file, destination: current.destination }
|
|
264
|
+
})
|
|
265
|
+
await Promise.all(
|
|
266
|
+
resolved.map(async (file) => {
|
|
267
|
+
let current: TextFileState
|
|
268
|
+
try {
|
|
269
|
+
current = await readTextFileState(
|
|
270
|
+
environment.storage,
|
|
271
|
+
file.destination,
|
|
272
|
+
)
|
|
273
|
+
} catch {
|
|
274
|
+
throw new PatternError(
|
|
275
|
+
`Pattern destination changed before install: ${file.destination}`,
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
if (!sameTextFileState(current, file.before)) {
|
|
279
|
+
throw new PatternError(
|
|
280
|
+
`Pattern destination changed before install: ${file.destination}`,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
}),
|
|
284
|
+
)
|
|
285
|
+
const attempted: PatternPlanFile[] = []
|
|
286
|
+
try {
|
|
287
|
+
for (const file of resolved) {
|
|
288
|
+
attempted.push(file)
|
|
289
|
+
await environment.storage.writeText(file.destination, file.content)
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const rollbackErrors = await rollback(environment.storage, attempted)
|
|
293
|
+
if (rollbackErrors.length) {
|
|
294
|
+
throw new AggregateError(
|
|
295
|
+
[error, ...rollbackErrors],
|
|
296
|
+
`Pattern ${patternPlan.pattern} install and rollback failed.`,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
throw error
|
|
300
|
+
}
|
|
301
|
+
return patternPlan
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const install: PatternInstaller["install"] = async (
|
|
305
|
+
pattern,
|
|
306
|
+
input,
|
|
307
|
+
options,
|
|
308
|
+
) => {
|
|
309
|
+
const patternPlan = await plan(pattern, input, options)
|
|
310
|
+
return apply(patternPlan)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return Object.freeze({ plan, apply, install })
|
|
314
|
+
}
|
package/perf.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type PerfRecord = {
|
|
2
|
+
name: string
|
|
3
|
+
count: number
|
|
4
|
+
totalMs: number
|
|
5
|
+
maxMs: number
|
|
6
|
+
lastMs: number
|
|
7
|
+
maxLabel?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class Perf {
|
|
11
|
+
readonly records = new Map<string, PerfRecord>()
|
|
12
|
+
|
|
13
|
+
measure<T>(name: string, fn: () => T, label?: string): T {
|
|
14
|
+
const start = performance.now()
|
|
15
|
+
try {
|
|
16
|
+
return fn()
|
|
17
|
+
} finally {
|
|
18
|
+
this.add(name, performance.now() - start, label)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async measureAsync<T>(
|
|
23
|
+
name: string,
|
|
24
|
+
fn: () => Promise<T>,
|
|
25
|
+
label?: string,
|
|
26
|
+
): Promise<T> {
|
|
27
|
+
const start = performance.now()
|
|
28
|
+
try {
|
|
29
|
+
return await fn()
|
|
30
|
+
} finally {
|
|
31
|
+
this.add(name, performance.now() - start, label)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Record an externally measured duration under a span name. */
|
|
36
|
+
record(name: string, ms: number, label?: string) {
|
|
37
|
+
this.add(name, ms, label)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
snapshot(): PerfRecord[] {
|
|
41
|
+
return [...this.records.values()]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
format() {
|
|
45
|
+
const rows = this.snapshot()
|
|
46
|
+
if (!rows.length) return ""
|
|
47
|
+
const nameWidth = Math.max(...rows.map((row) => row.name.length))
|
|
48
|
+
const countWidth = Math.max(
|
|
49
|
+
...rows.map((row) => String(row.count).length),
|
|
50
|
+
)
|
|
51
|
+
return rows
|
|
52
|
+
.map((row) => {
|
|
53
|
+
const name = row.name.padEnd(nameWidth)
|
|
54
|
+
const count = `n=${row.count}`.padEnd(countWidth + 2)
|
|
55
|
+
const total = `${row.totalMs.toFixed(1)} ms`
|
|
56
|
+
const max = `max=${row.maxMs.toFixed(1)} ms`
|
|
57
|
+
const label = row.maxLabel ? ` ${row.maxLabel}` : ""
|
|
58
|
+
return `${name} ${count} ${total} ${max}${label}`
|
|
59
|
+
})
|
|
60
|
+
.join("\n")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
reset() {
|
|
64
|
+
this.records.clear()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private add(name: string, ms: number, label?: string) {
|
|
68
|
+
const current = this.records.get(name)
|
|
69
|
+
if (!current) {
|
|
70
|
+
this.records.set(name, {
|
|
71
|
+
name,
|
|
72
|
+
count: 1,
|
|
73
|
+
totalMs: ms,
|
|
74
|
+
maxMs: ms,
|
|
75
|
+
lastMs: ms,
|
|
76
|
+
maxLabel: label,
|
|
77
|
+
})
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
current.count += 1
|
|
81
|
+
current.totalMs += ms
|
|
82
|
+
current.lastMs = ms
|
|
83
|
+
if (ms >= current.maxMs) {
|
|
84
|
+
current.maxMs = ms
|
|
85
|
+
current.maxLabel = label
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Plugin } from "./types.ts"
|
|
2
|
+
import { escapeHtml } from "../util.ts"
|
|
3
|
+
|
|
4
|
+
export function ActorPathPlugin(): Plugin {
|
|
5
|
+
return {
|
|
6
|
+
id: "actor-path",
|
|
7
|
+
// Rewrite Home-relative paths only after a Principal has been admitted.
|
|
8
|
+
streamSelectors({ principal, source }) {
|
|
9
|
+
const actor = principal.type === "actor" ? principal.actor : undefined
|
|
10
|
+
return {
|
|
11
|
+
"[href^='~/']": {
|
|
12
|
+
element(el) {
|
|
13
|
+
const href = el.getAttribute("href")
|
|
14
|
+
if (!href?.startsWith("~/")) return
|
|
15
|
+
if (actor) el.setAttribute("href", `${actor.home}/${href.slice(2)}`)
|
|
16
|
+
else el.removeAttribute("href")
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
"form[action^='~/']": {
|
|
20
|
+
element(el) {
|
|
21
|
+
const action = el.getAttribute("action")
|
|
22
|
+
if (!action?.startsWith("~/")) return
|
|
23
|
+
if (actor)
|
|
24
|
+
el.setAttribute("action", `${actor.home}/${action.slice(2)}`)
|
|
25
|
+
else el.remove()
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
'form[action^="/actions/"]': {
|
|
29
|
+
element(el) {
|
|
30
|
+
if (!actor) {
|
|
31
|
+
el.remove()
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
el.prepend(
|
|
35
|
+
`<input type="hidden" name="_target" value="${escapeHtml(source.id)}" />`,
|
|
36
|
+
{ html: true },
|
|
37
|
+
)
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Plugin } from "./types.ts"
|
|
2
|
+
import { isActorSource } from "../authority/actors.ts"
|
|
3
|
+
|
|
4
|
+
export function actorProfileDependency(actorId: string) {
|
|
5
|
+
return {
|
|
6
|
+
type: "plugin" as const,
|
|
7
|
+
plugin: "actor-profile",
|
|
8
|
+
key: actorId,
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ActorProfilePlugin(): Plugin {
|
|
13
|
+
return {
|
|
14
|
+
id: "actor-profile",
|
|
15
|
+
invalidate({ before, after }) {
|
|
16
|
+
const actor = after ?? before
|
|
17
|
+
if (!actor || !isActorSource(actor)) return
|
|
18
|
+
return { dependencies: [actorProfileDependency(actor.id)] }
|
|
19
|
+
},
|
|
20
|
+
streamSelectors({ principal }, app) {
|
|
21
|
+
return {
|
|
22
|
+
"[data-pmx-profile-name]": {
|
|
23
|
+
element(element) {
|
|
24
|
+
const id = element.getAttribute("data-pmx-profile-name") ?? ""
|
|
25
|
+
const profile = app.actors.profile(id)
|
|
26
|
+
element.removeAttribute("data-pmx-profile-name")
|
|
27
|
+
if (profile) element.setInnerContent(profile.name)
|
|
28
|
+
else element.remove()
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
"[data-pmx-profile-avatar]": {
|
|
32
|
+
element(element) {
|
|
33
|
+
const id = element.getAttribute("data-pmx-profile-avatar") ?? ""
|
|
34
|
+
const profile = app.actors.profile(id)
|
|
35
|
+
element.removeAttribute("data-pmx-profile-avatar")
|
|
36
|
+
if (profile?.avatar) element.setAttribute("src", profile.avatar)
|
|
37
|
+
else element.remove()
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
"[data-pmx-owner]": {
|
|
41
|
+
element(element) {
|
|
42
|
+
const owner = element.getAttribute("data-pmx-owner")
|
|
43
|
+
const actor =
|
|
44
|
+
principal.type === "actor" ? principal.actor : undefined
|
|
45
|
+
const allowed =
|
|
46
|
+
actor?.id === owner ||
|
|
47
|
+
(actor?.type === "user" && actor.role === "staff")
|
|
48
|
+
element.removeAttribute("data-pmx-owner")
|
|
49
|
+
if (!allowed) element.remove()
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { deriveSourceMeta, type Source } from "../source/index.ts"
|
|
2
|
+
import { isPageSource, publicPath } from "../server/router.ts"
|
|
3
|
+
import { AGENT_BROWSING_PLUGIN_ID } from "../server/protocols.ts"
|
|
4
|
+
import { escapeHtml } from "../util.ts"
|
|
5
|
+
import {
|
|
6
|
+
route,
|
|
7
|
+
type Plugin,
|
|
8
|
+
type ServerContext,
|
|
9
|
+
type StreamHandler,
|
|
10
|
+
} from "./types.ts"
|
|
11
|
+
|
|
12
|
+
const PUBLIC_CACHE = "public, max-age=300"
|
|
13
|
+
const NO_STREAM_SELECTORS: Record<string, StreamHandler> = Object.freeze({})
|
|
14
|
+
|
|
15
|
+
function hasPublicBacklink(source: Source, ctx: ServerContext) {
|
|
16
|
+
return ctx.graph.linksTo(source.path).some((edge) => {
|
|
17
|
+
if (edge.from === source.path) return false
|
|
18
|
+
const origin = ctx.source(edge.from)
|
|
19
|
+
return origin != null && ctx.isPublic(origin)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function discoverablePages(ctx: ServerContext) {
|
|
24
|
+
return ctx
|
|
25
|
+
.sources()
|
|
26
|
+
.filter((source) => {
|
|
27
|
+
if (!isPageSource(source) || !ctx.isPublic(source)) return false
|
|
28
|
+
if (ctx.resolve(publicPath(source)) !== source) return false
|
|
29
|
+
return publicPath(source) === "/" || hasPublicBacklink(source, ctx)
|
|
30
|
+
})
|
|
31
|
+
.sort((left, right) => {
|
|
32
|
+
const leftPath = publicPath(left)
|
|
33
|
+
const rightPath = publicPath(right)
|
|
34
|
+
return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function publicationBasePath(ctx: ServerContext) {
|
|
39
|
+
const suffix = `/${ctx.path}`
|
|
40
|
+
if (!ctx.url.pathname.endsWith(suffix)) return "/"
|
|
41
|
+
return ctx.url.pathname.slice(0, -suffix.length) || "/"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function mountedPath(ctx: ServerContext, pathname: string) {
|
|
45
|
+
const basePath = publicationBasePath(ctx)
|
|
46
|
+
if (basePath === "/") return pathname
|
|
47
|
+
return pathname === "/" ? basePath : `${basePath}${pathname}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function absoluteUrl(ctx: ServerContext, pathname: string) {
|
|
51
|
+
return new URL(mountedPath(ctx, pathname), ctx.url.origin).href
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function markdownText(value: string) {
|
|
55
|
+
return value
|
|
56
|
+
.replaceAll("\\", "\\\\")
|
|
57
|
+
.replaceAll("[", "\\[")
|
|
58
|
+
.replaceAll("]", "\\]")
|
|
59
|
+
.replace(/\s+/g, " ")
|
|
60
|
+
.trim()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function llmsTxt(ctx: ServerContext) {
|
|
64
|
+
const pages = discoverablePages(ctx)
|
|
65
|
+
const home = pages.find((source) => publicPath(source) === "/") ?? pages[0]
|
|
66
|
+
const homeMeta = home ? deriveSourceMeta(home) : undefined
|
|
67
|
+
const title = markdownText(homeMeta?.title || "PathMX")
|
|
68
|
+
const lines = [
|
|
69
|
+
`# ${title}`,
|
|
70
|
+
"",
|
|
71
|
+
...(homeMeta?.description
|
|
72
|
+
? [`> ${markdownText(homeMeta.description)}`, ""]
|
|
73
|
+
: []),
|
|
74
|
+
"## When to use this site",
|
|
75
|
+
"",
|
|
76
|
+
"Use this PathMX site to read, navigate, or cite its public Sources. The listed links return the authored Markdown representation directly. Use the sitemap for the canonical HTML URL index.",
|
|
77
|
+
"",
|
|
78
|
+
"## Public pages",
|
|
79
|
+
"",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
if (!pages.length) lines.push("No public pages are currently available.")
|
|
83
|
+
for (const source of pages) {
|
|
84
|
+
const meta = deriveSourceMeta(source)
|
|
85
|
+
const description = meta.description
|
|
86
|
+
? ` — ${markdownText(meta.description)}`
|
|
87
|
+
: ""
|
|
88
|
+
lines.push(
|
|
89
|
+
`- [${markdownText(meta.title)}](${absoluteUrl(ctx, source.path)})${description}`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lines.push("", `- [XML sitemap](${absoluteUrl(ctx, "/sitemap.xml")})`, "")
|
|
94
|
+
return lines.join("\n")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function robotsTxt(ctx: ServerContext) {
|
|
98
|
+
const basePath = publicationBasePath(ctx)
|
|
99
|
+
const root = basePath === "/" ? "/" : `${basePath}/`
|
|
100
|
+
return [
|
|
101
|
+
"User-agent: *",
|
|
102
|
+
`Allow: ${root}`,
|
|
103
|
+
`Disallow: ${mountedPath(ctx, "/.pmx/")}`,
|
|
104
|
+
`Disallow: ${mountedPath(ctx, "/actions/")}`,
|
|
105
|
+
`Disallow: ${mountedPath(ctx, "/api/runtime/")}`,
|
|
106
|
+
"",
|
|
107
|
+
`Host: ${ctx.url.origin}`,
|
|
108
|
+
`Sitemap: ${absoluteUrl(ctx, "/sitemap.xml")}`,
|
|
109
|
+
"",
|
|
110
|
+
].join("\n")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function escapeXml(value: string) {
|
|
114
|
+
return value
|
|
115
|
+
.replaceAll("&", "&")
|
|
116
|
+
.replaceAll('"', """)
|
|
117
|
+
.replaceAll("'", "'")
|
|
118
|
+
.replaceAll("<", "<")
|
|
119
|
+
.replaceAll(">", ">")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sitemapXml(ctx: ServerContext) {
|
|
123
|
+
const urls = discoverablePages(ctx)
|
|
124
|
+
.map(
|
|
125
|
+
(source) =>
|
|
126
|
+
` <url><loc>${escapeXml(absoluteUrl(ctx, publicPath(source)))}</loc></url>`,
|
|
127
|
+
)
|
|
128
|
+
.join("\n")
|
|
129
|
+
return [
|
|
130
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
131
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
132
|
+
urls,
|
|
133
|
+
"</urlset>",
|
|
134
|
+
"",
|
|
135
|
+
].join("\n")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function publicText(body: string, contentType: string) {
|
|
139
|
+
return new Response(body, {
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": contentType,
|
|
142
|
+
"Cache-Control": PUBLIC_CACHE,
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Public discovery documents for LLMs, crawlers, and other browsing agents. */
|
|
148
|
+
export function AgentBrowsingPlugin(): Plugin {
|
|
149
|
+
return {
|
|
150
|
+
id: AGENT_BROWSING_PLUGIN_ID,
|
|
151
|
+
name: "AgentBrowsingPlugin",
|
|
152
|
+
routes: [
|
|
153
|
+
route.get("llms.txt", (ctx) =>
|
|
154
|
+
publicText(llmsTxt(ctx), "text/plain; charset=utf-8"),
|
|
155
|
+
),
|
|
156
|
+
route.get("robots.txt", (ctx) =>
|
|
157
|
+
publicText(robotsTxt(ctx), "text/plain; charset=utf-8"),
|
|
158
|
+
),
|
|
159
|
+
route.get("sitemap.xml", (ctx) =>
|
|
160
|
+
publicText(sitemapXml(ctx), "application/xml; charset=utf-8"),
|
|
161
|
+
),
|
|
162
|
+
],
|
|
163
|
+
publish(ctx) {
|
|
164
|
+
ctx.route("/llms.txt")
|
|
165
|
+
ctx.route("/robots.txt")
|
|
166
|
+
ctx.route("/sitemap.xml")
|
|
167
|
+
},
|
|
168
|
+
streamSelectors({ source }, app) {
|
|
169
|
+
if (!app.sources.isPage(source)) return NO_STREAM_SELECTORS
|
|
170
|
+
const href = escapeHtml(source.path)
|
|
171
|
+
return {
|
|
172
|
+
head: {
|
|
173
|
+
element(element) {
|
|
174
|
+
element.prepend(
|
|
175
|
+
`<link rel="alternate" type="text/markdown" href="${href}">`,
|
|
176
|
+
{ html: true },
|
|
177
|
+
)
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { stringifyHtml } from "../html.ts"
|
|
2
|
+
import { stampHtmlRoot } from "../view/syntax.ts"
|
|
3
|
+
import type { ViewComponent } from "./types.ts"
|
|
4
|
+
|
|
5
|
+
/** @internal Browser-owned roots are opaque to document morphing. */
|
|
6
|
+
export const APP_COMPONENT_ROOT_ATTRIBUTE = "data-pmx-app"
|
|
7
|
+
|
|
8
|
+
/** Define a browser-owned View Component whose client is bundled when authored. */
|
|
9
|
+
export function defineAppComponent<Compiled = undefined>(
|
|
10
|
+
definition: ViewComponent<Compiled> & Readonly<{ client: string | URL }>,
|
|
11
|
+
): ViewComponent<Compiled> {
|
|
12
|
+
const { render, ...component } = definition
|
|
13
|
+
return {
|
|
14
|
+
...component,
|
|
15
|
+
async render(use, compiled, ctx, app) {
|
|
16
|
+
return stampHtmlRoot(
|
|
17
|
+
stringifyHtml(await render(use, compiled, ctx, app)),
|
|
18
|
+
{ [APP_COMPONENT_ROOT_ATTRIBUTE]: "" },
|
|
19
|
+
app.html.rewrite,
|
|
20
|
+
)
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|