@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.
Files changed (195) hide show
  1. package/LICENSE.md +172 -0
  2. package/README.md +111 -0
  3. package/actions/actions.ts +307 -0
  4. package/actions/draft.ts +105 -0
  5. package/actions/transaction.ts +164 -0
  6. package/assets.ts +149 -0
  7. package/authority/access.ts +820 -0
  8. package/authority/actors.ts +271 -0
  9. package/authority/agents.ts +95 -0
  10. package/authority/credentials.ts +144 -0
  11. package/authority/model.ts +71 -0
  12. package/authority/sharing.ts +89 -0
  13. package/browser-bundle.ts +61 -0
  14. package/canonical.ts +41 -0
  15. package/cli/dev.ts +134 -0
  16. package/cli/export.ts +74 -0
  17. package/cli/index.ts +52 -0
  18. package/cli/lint.ts +164 -0
  19. package/cli/mv.ts +30 -0
  20. package/cli/perms.ts +182 -0
  21. package/cli/plugin-packages.ts +159 -0
  22. package/cli/plugins.ts +95 -0
  23. package/cli/query.ts +225 -0
  24. package/cli/readme.md +95 -0
  25. package/cli/release.ts +75 -0
  26. package/cli/rm.ts +22 -0
  27. package/cli/serve.ts +100 -0
  28. package/cli/tree.ts +131 -0
  29. package/cli/update.ts +78 -0
  30. package/cli/utils.ts +53 -0
  31. package/core.ts +96 -0
  32. package/database.ts +189 -0
  33. package/dependencies.ts +60 -0
  34. package/dist/pathmx.js +1224 -0
  35. package/document.ts +194 -0
  36. package/environment.ts +110 -0
  37. package/environments/bun-plugins.ts +68 -0
  38. package/environments/bun-publish.ts +194 -0
  39. package/environments/bun.ts +223 -0
  40. package/globals.d.ts +14 -0
  41. package/graph.ts +153 -0
  42. package/host/compiler.ts +314 -0
  43. package/host/create.ts +66 -0
  44. package/host/engine.ts +630 -0
  45. package/host/plugin-host.ts +487 -0
  46. package/host/render.ts +102 -0
  47. package/host/response.ts +20 -0
  48. package/host/shell.html +15 -0
  49. package/host/source-sync.ts +254 -0
  50. package/html-attributes.ts +15 -0
  51. package/html.ts +48 -0
  52. package/icons/LICENSE +43 -0
  53. package/icons/lucide.ts +54 -0
  54. package/index.ts +75 -0
  55. package/invalidation.ts +85 -0
  56. package/log.ts +84 -0
  57. package/ops/href.ts +87 -0
  58. package/ops/index.ts +9 -0
  59. package/ops/move-source.ts +114 -0
  60. package/ops/plan.ts +47 -0
  61. package/ops/remove-source.ts +70 -0
  62. package/package.json +46 -0
  63. package/path.ts +75 -0
  64. package/pattern.ts +314 -0
  65. package/perf.ts +88 -0
  66. package/plugins/actor-path.ts +43 -0
  67. package/plugins/actor-profile.ts +55 -0
  68. package/plugins/agent-browsing.ts +183 -0
  69. package/plugins/app-component.ts +23 -0
  70. package/plugins/block-layout/index.ts +78 -0
  71. package/plugins/block-layout/theme.css +90 -0
  72. package/plugins/callout/index.ts +236 -0
  73. package/plugins/callout/theme.css +138 -0
  74. package/plugins/canonical-links.ts +144 -0
  75. package/plugins/code-highlight/client.ts +89 -0
  76. package/plugins/code-highlight/index.ts +147 -0
  77. package/plugins/code-highlight/theme.css +89 -0
  78. package/plugins/context.ts +131 -0
  79. package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
  80. package/plugins/datatype/OFL.txt +96 -0
  81. package/plugins/datatype/index.ts +287 -0
  82. package/plugins/definition-list.ts +236 -0
  83. package/plugins/directive-definitions.ts +13 -0
  84. package/plugins/embedding/client.ts +88 -0
  85. package/plugins/embedding/index.ts +43 -0
  86. package/plugins/image/index.ts +68 -0
  87. package/plugins/image/theme.css +43 -0
  88. package/plugins/include.ts +119 -0
  89. package/plugins/index.ts +91 -0
  90. package/plugins/input/authoring.ts +236 -0
  91. package/plugins/input/config.ts +85 -0
  92. package/plugins/input/index.ts +359 -0
  93. package/plugins/input/model.ts +173 -0
  94. package/plugins/input/state.ts +272 -0
  95. package/plugins/layout.ts +313 -0
  96. package/plugins/literate/client.ts +208 -0
  97. package/plugins/literate/index.ts +742 -0
  98. package/plugins/math/index.ts +140 -0
  99. package/plugins/meta.ts +42 -0
  100. package/plugins/navigation/index.ts +158 -0
  101. package/plugins/navigation/model.ts +364 -0
  102. package/plugins/preset.ts +65 -0
  103. package/plugins/query/index.ts +322 -0
  104. package/plugins/rss/index.ts +46 -0
  105. package/plugins/runtime/actions.ts +62 -0
  106. package/plugins/runtime/client.ts +8 -0
  107. package/plugins/runtime/document.ts +506 -0
  108. package/plugins/runtime/error.ts +154 -0
  109. package/plugins/runtime/heading-links.ts +31 -0
  110. package/plugins/runtime/index.ts +104 -0
  111. package/plugins/runtime/live.ts +51 -0
  112. package/plugins/runtime/navigation.ts +113 -0
  113. package/plugins/runtime/pending.ts +53 -0
  114. package/plugins/runtime/publication.ts +19 -0
  115. package/plugins/runtime/start.ts +177 -0
  116. package/plugins/runtime/static-client.ts +6 -0
  117. package/plugins/runtime/static-live.ts +188 -0
  118. package/plugins/runtime/view-transition.ts +72 -0
  119. package/plugins/runtime/viewport.ts +110 -0
  120. package/plugins/schema.ts +24 -0
  121. package/plugins/scripts.ts +140 -0
  122. package/plugins/source-preview/client.ts +256 -0
  123. package/plugins/source-preview/index.ts +132 -0
  124. package/plugins/source-preview/model.ts +11 -0
  125. package/plugins/source-preview/preview.css +107 -0
  126. package/plugins/styles.ts +145 -0
  127. package/plugins/theme/components.ts +10 -0
  128. package/plugins/theme/foundation.css +358 -0
  129. package/plugins/theme/index.ts +333 -0
  130. package/plugins/theme/prose.css +284 -0
  131. package/plugins/theme/scheme.js +63 -0
  132. package/plugins/theme/toggle.css +44 -0
  133. package/plugins/theme/view-transition.css +68 -0
  134. package/plugins/toc/client.ts +146 -0
  135. package/plugins/toc/index.ts +232 -0
  136. package/plugins/toc/theme.css +172 -0
  137. package/plugins/types.ts +538 -0
  138. package/problem.ts +41 -0
  139. package/project.ts +6 -0
  140. package/publish/build.ts +402 -0
  141. package/publish/concurrent.ts +32 -0
  142. package/publish/identity.ts +63 -0
  143. package/publish/index.ts +11 -0
  144. package/publish/location.ts +167 -0
  145. package/publish/model.ts +58 -0
  146. package/publish/plan.ts +300 -0
  147. package/publish/validate.ts +150 -0
  148. package/query/candidate-index.ts +101 -0
  149. package/query/dependency-index.ts +99 -0
  150. package/query/fields.ts +216 -0
  151. package/query/invalidation.ts +48 -0
  152. package/query/kernel.ts +776 -0
  153. package/query/model.ts +79 -0
  154. package/repository.ts +374 -0
  155. package/runtime-profile.ts +34 -0
  156. package/schema/model.ts +92 -0
  157. package/schema/registry.ts +642 -0
  158. package/schema/standard.ts +226 -0
  159. package/server/context.ts +94 -0
  160. package/server/css-imports.ts +66 -0
  161. package/server/http.ts +333 -0
  162. package/server/page.ts +21 -0
  163. package/server/protocols.ts +12 -0
  164. package/server/representations.ts +66 -0
  165. package/server/router.ts +131 -0
  166. package/server/routes.ts +202 -0
  167. package/server/serve.ts +5 -0
  168. package/server/watch.ts +120 -0
  169. package/source/blocks.ts +71 -0
  170. package/source/format.ts +164 -0
  171. package/source/index.ts +67 -0
  172. package/source/interpolate.ts +63 -0
  173. package/source/markdown/blocks.ts +46 -0
  174. package/source/markdown/comments.ts +32 -0
  175. package/source/markdown/fences.ts +67 -0
  176. package/source/markdown/headings.ts +88 -0
  177. package/source/markdown/index.ts +34 -0
  178. package/source/markdown/inline-code.ts +6 -0
  179. package/source/markdown/links.ts +326 -0
  180. package/source/markdown/lists.ts +223 -0
  181. package/source/markdown/prose.ts +74 -0
  182. package/source/markdown/scan.ts +79 -0
  183. package/source/markdown/text.ts +82 -0
  184. package/source/meta.ts +87 -0
  185. package/source/source.ts +121 -0
  186. package/source/topmatter.ts +57 -0
  187. package/source/utils.ts +21 -0
  188. package/source-diagnostic.ts +63 -0
  189. package/text-file.ts +26 -0
  190. package/util.ts +35 -0
  191. package/view/components.ts +499 -0
  192. package/view/model.ts +91 -0
  193. package/view/projection.ts +162 -0
  194. package/view/session.ts +146 -0
  195. package/view/syntax.ts +372 -0
@@ -0,0 +1,487 @@
1
+ import type { Engine } from "./engine.ts"
2
+ import {
3
+ anonymousCredentialHost,
4
+ type CredentialHost,
5
+ } from "../authority/credentials.ts"
6
+ import {
7
+ ViewComponentRegistry,
8
+ type CompiledViewPlan,
9
+ } from "../view/components.ts"
10
+ import {
11
+ canonicalPluginId,
12
+ type AppContext,
13
+ type CompileContext,
14
+ type DocumentAssetContext,
15
+ type PageContext,
16
+ type Plugin,
17
+ type PluginContext,
18
+ type PluginFactory,
19
+ type PluginInvalidation,
20
+ type SourceTransition,
21
+ type ViewContext,
22
+ } from "../plugins/types.ts"
23
+ import type { Source } from "../source/index.ts"
24
+ import type { KnownActorIndex, Principal } from "../authority/model.ts"
25
+ import { PluginDatabases } from "../database.ts"
26
+ import { createAppContext, createPluginContext } from "../plugins/context.ts"
27
+ import { ServerRouteRegistry } from "../server/routes.ts"
28
+ import {
29
+ describeAction,
30
+ type ActionAffordance,
31
+ type ActionContribution,
32
+ } from "../actions/actions.ts"
33
+ import { applicationSchemaView, SchemaRegistry } from "../schema/registry.ts"
34
+ import { instantiatePlugin } from "../plugins/preset.ts"
35
+ import type { HtmlRewriteRule } from "../environment.ts"
36
+ import type { QueryContribution } from "../query/model.ts"
37
+
38
+ export type { CompiledViewPlan }
39
+
40
+ export class PluginRegistrationError extends Error {
41
+ constructor(message: string) {
42
+ super(message)
43
+ this.name = "PluginRegistrationError"
44
+ }
45
+ }
46
+
47
+ type HookErrorHandler = (error: unknown) => boolean
48
+
49
+ function validatePluginRequirements(plugins: readonly Plugin[]) {
50
+ const positions = new Map(plugins.map((plugin, index) => [plugin.id, index]))
51
+ for (const plugin of plugins) {
52
+ if (plugin.requires === undefined) continue
53
+ if (!Array.isArray(plugin.requires)) {
54
+ throw new PluginRegistrationError(
55
+ `Plugin ${plugin.id} requires must be a list of plugin ids.`,
56
+ )
57
+ }
58
+ const seen = new Set<string>()
59
+ for (const dependency of plugin.requires) {
60
+ try {
61
+ canonicalPluginId(dependency)
62
+ } catch (error) {
63
+ throw new PluginRegistrationError(
64
+ error instanceof Error ? error.message : String(error),
65
+ )
66
+ }
67
+ if (dependency === plugin.id) {
68
+ throw new PluginRegistrationError(
69
+ `Plugin ${plugin.id} cannot require itself.`,
70
+ )
71
+ }
72
+ if (seen.has(dependency)) {
73
+ throw new PluginRegistrationError(
74
+ `Plugin ${plugin.id} requires ${dependency} more than once.`,
75
+ )
76
+ }
77
+ seen.add(dependency)
78
+ const dependencyPosition = positions.get(dependency)
79
+ if (dependencyPosition === undefined) {
80
+ throw new PluginRegistrationError(
81
+ `Plugin ${plugin.id} requires missing plugin ${dependency}.`,
82
+ )
83
+ }
84
+ if (dependencyPosition > positions.get(plugin.id)!) {
85
+ throw new PluginRegistrationError(
86
+ `Plugin ${plugin.id} requires plugin ${dependency} to be registered first.`,
87
+ )
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ /** Own plugin installation, View Components, routes, and ordered hook phases. */
94
+ export class PluginHost {
95
+ private installed: Plugin[] = []
96
+ private ids = new Set<string>()
97
+ private frozen = false
98
+ private credentialHost: CredentialHost = anonymousCredentialHost()
99
+ private credentialOwner?: string
100
+ private credentialClosing?: Promise<void>
101
+ private closing?: Promise<void>
102
+ private views = new ViewComponentRegistry()
103
+ private routes = new ServerRouteRegistry()
104
+ private actions = new Map<string, ActionContribution>()
105
+ private lifecycleContexts = new Map<string, PluginContext>()
106
+ private appContexts = new Map<string, AppContext>()
107
+ private schemaRegistry = new SchemaRegistry()
108
+ readonly schema = applicationSchemaView(this.schemaRegistry)
109
+
110
+ constructor(
111
+ private databases = new PluginDatabases(undefined, ".pathmx/state"),
112
+ ) {}
113
+
114
+ get plugins(): readonly Plugin[] {
115
+ return this.installed
116
+ }
117
+
118
+ has(id: string) {
119
+ return this.ids.has(id)
120
+ }
121
+
122
+ register(plugin: PluginFactory | Plugin) {
123
+ if (this.frozen) {
124
+ throw new PluginRegistrationError(
125
+ "Plugin registration is frozen after the engine is ready.",
126
+ )
127
+ }
128
+
129
+ const instance = instantiatePlugin(plugin)
130
+ canonicalPluginId(instance.id)
131
+ if (this.ids.has(instance.id)) {
132
+ throw new PluginRegistrationError(`Duplicate plugin id: ${instance.id}`)
133
+ }
134
+ if (instance.credentials && this.credentialOwner) {
135
+ throw new PluginRegistrationError(
136
+ `Credential provider already registered by plugin ${this.credentialOwner}.`,
137
+ )
138
+ }
139
+ let routes: ReturnType<ServerRouteRegistry["prepare"]>
140
+ let actions: readonly (readonly [string, ActionContribution])[]
141
+ let schema: ReturnType<SchemaRegistry["prepare"]>
142
+ let database
143
+ try {
144
+ database = instance.database
145
+ ? this.databases.prepare(instance.database)
146
+ : undefined
147
+ routes = this.routes.prepare(instance)
148
+ schema = this.schemaRegistry.prepare(instance.id, instance.schema)
149
+ actions = this.prepareActions(schema.actions)
150
+ this.views.register(instance)
151
+ } catch (error) {
152
+ throw new PluginRegistrationError(
153
+ error instanceof Error ? error.message : String(error),
154
+ )
155
+ }
156
+
157
+ if (database) this.databases.register(instance.id, database)
158
+ this.routes.register(routes)
159
+ for (const [name, action] of actions) this.actions.set(name, action)
160
+ this.schemaRegistry.register(schema)
161
+ if (instance.credentials) {
162
+ this.credentialHost = instance.credentials
163
+ this.credentialOwner = instance.id
164
+ }
165
+ this.ids.add(instance.id)
166
+ this.installed.push(instance)
167
+ }
168
+
169
+ private prepareActions(
170
+ actions: readonly (readonly [string, ActionContribution])[],
171
+ ) {
172
+ for (const [name, action] of actions) {
173
+ if (this.actions.has(name)) {
174
+ throw new Error(`Duplicate Plugin Action: ${name}`)
175
+ }
176
+ }
177
+ return actions
178
+ }
179
+
180
+ action(name: string) {
181
+ return this.actions.get(name)
182
+ }
183
+
184
+ async actionAffordances(
185
+ principal: Principal,
186
+ target: Source,
187
+ env: Engine,
188
+ ): Promise<readonly ActionAffordance[]> {
189
+ const described = await Promise.all(
190
+ [...this.actions].map(([name, action]) =>
191
+ describeAction(name, action, {
192
+ principal,
193
+ target,
194
+ yaml: env.environment.yaml,
195
+ can: (capability) =>
196
+ env.hasSourceCapability(principal, target, capability),
197
+ }),
198
+ ),
199
+ )
200
+ return Object.freeze(
201
+ described.filter((item): item is ActionAffordance => item !== undefined),
202
+ )
203
+ }
204
+
205
+ freeze() {
206
+ validatePluginRequirements(this.installed)
207
+ this.frozen = true
208
+ }
209
+
210
+ private lifecycleContext(plugin: Plugin | string, engine: Engine) {
211
+ const id = typeof plugin === "string" ? plugin : plugin.id
212
+ const existing = this.lifecycleContexts.get(id)
213
+ if (existing) return existing
214
+ const context = createPluginContext(id, engine, this.databases.get(id))
215
+ this.lifecycleContexts.set(id, context)
216
+ return context
217
+ }
218
+
219
+ appContext(plugin: Plugin | string, engine: Engine) {
220
+ const id = typeof plugin === "string" ? plugin : plugin.id
221
+ const existing = this.appContexts.get(id)
222
+ if (existing) return existing
223
+ const context = createAppContext(
224
+ this.lifecycleContext(id, engine),
225
+ engine,
226
+ this.componentContext(id, engine),
227
+ )
228
+ this.appContexts.set(id, context)
229
+ return context
230
+ }
231
+
232
+ private componentContext(
233
+ plugin: string,
234
+ engine: Engine,
235
+ ): AppContext["components"] {
236
+ return Object.freeze({
237
+ claim: (sourcePath, tags) =>
238
+ this.replaceAuthoredComponents(sourcePath, tags),
239
+ release: (sourcePath) => this.deleteAuthoredComponents(sourcePath),
240
+ has: (tag) => this.hasView(tag),
241
+ replaceData: (sourcePath, components) =>
242
+ this.replaceAuthoredDataComponents(plugin, sourcePath, components),
243
+ prepareData: (tag, props, origin, ctx) =>
244
+ this.prepareDataComponent(tag, props, origin, ctx, engine),
245
+ renderData: (prepared, data, ctx) =>
246
+ this.renderDataComponent(prepared, data, ctx, engine),
247
+ })
248
+ }
249
+
250
+ async setup(env: Engine) {
251
+ for (const plugin of this.installed) {
252
+ await plugin.setup?.(this.lifecycleContext(plugin, env))
253
+ }
254
+ }
255
+
256
+ private async shutdown(env: Engine) {
257
+ const errors: unknown[] = []
258
+ try {
259
+ await this.closeCredentials()
260
+ } catch (error) {
261
+ errors.push(error)
262
+ }
263
+ for (const plugin of [...this.installed].reverse()) {
264
+ try {
265
+ await plugin.close?.(this.lifecycleContext(plugin, env))
266
+ } catch (error) {
267
+ errors.push(error)
268
+ }
269
+ }
270
+ try {
271
+ await this.databases.close()
272
+ } catch (error) {
273
+ errors.push(error)
274
+ }
275
+ if (errors.length)
276
+ throw new AggregateError(errors, "Plugin shutdown failed.")
277
+ }
278
+
279
+ close(env: Engine) {
280
+ return (this.closing ??= this.shutdown(env))
281
+ }
282
+
283
+ private async measure<T>(
284
+ plugin: Plugin,
285
+ hook: string,
286
+ label: string | undefined,
287
+ env: Engine,
288
+ run: () => T | Promise<T>,
289
+ ) {
290
+ const start = performance.now()
291
+ try {
292
+ return await run()
293
+ } finally {
294
+ env.perf.record(
295
+ `${plugin.name ?? "plugin"}.${hook}`,
296
+ performance.now() - start,
297
+ label,
298
+ )
299
+ }
300
+ }
301
+
302
+ index(source: Source, env: Engine, onError?: HookErrorHandler) {
303
+ this.schemaRegistry.index(source, onError)
304
+ for (const plugin of this.installed) {
305
+ try {
306
+ plugin.index?.(source, this.appContext(plugin, env))
307
+ } catch (error) {
308
+ if (!onError?.(error)) throw error
309
+ }
310
+ }
311
+ }
312
+
313
+ unindex(sourcePath: string, env: Engine) {
314
+ for (const plugin of this.installed) {
315
+ plugin.unindex?.(sourcePath, this.appContext(plugin, env))
316
+ }
317
+ }
318
+
319
+ async precompile(markdown: string, ctx: CompileContext, env: Engine) {
320
+ for (const plugin of this.installed) {
321
+ if (!plugin.precompile) continue
322
+ markdown =
323
+ (await this.measure(plugin, "precompile", ctx.source.path, env, () =>
324
+ plugin.precompile!(markdown, ctx, this.appContext(plugin, env)),
325
+ )) ?? markdown
326
+ }
327
+ return this.views.compileSyntax(markdown, ctx)
328
+ }
329
+
330
+ compileVariables(markdown: string, ctx: CompileContext) {
331
+ return this.views.compileVariables(markdown, ctx)
332
+ }
333
+
334
+ async postcompile(html: string, ctx: CompileContext, env: Engine) {
335
+ html = this.views.restoreSyntax(html, ctx)
336
+ if (this.views.hasAny(ctx.elements)) {
337
+ html = this.views.stampOrigins(
338
+ html,
339
+ ctx.source.path,
340
+ env.environment.rewriteHtml,
341
+ )
342
+ }
343
+ for (const plugin of this.installed) {
344
+ if (!plugin.postcompile) continue
345
+ html =
346
+ (await this.measure(plugin, "postcompile", ctx.source.path, env, () =>
347
+ plugin.postcompile!(html, ctx, this.appContext(plugin, env)),
348
+ )) ?? html
349
+ }
350
+ return html
351
+ }
352
+
353
+ async page(
354
+ html: string,
355
+ ctx: PageContext,
356
+ env: Engine,
357
+ excludedPluginId?: string,
358
+ ) {
359
+ for (const plugin of this.installed) {
360
+ if (!plugin.page || plugin.id === excludedPluginId) continue
361
+ html =
362
+ (await this.measure(plugin, "page", ctx.source.path, env, () =>
363
+ plugin.page!(html, ctx, this.appContext(plugin, env)),
364
+ )) ?? html
365
+ }
366
+ return html
367
+ }
368
+
369
+ invalidations(transition: SourceTransition, env: Engine) {
370
+ const invalidations: PluginInvalidation[] = []
371
+ for (const plugin of this.installed) {
372
+ const invalidation = plugin.invalidate?.(
373
+ transition,
374
+ this.appContext(plugin, env),
375
+ )
376
+ if (invalidation) invalidations.push(invalidation)
377
+ }
378
+ return invalidations
379
+ }
380
+
381
+ async documentAssets(ctx: DocumentAssetContext, env: Engine) {
382
+ for (const plugin of this.installed) {
383
+ await plugin.documentAssets?.(ctx, this.appContext(plugin, env))
384
+ }
385
+ }
386
+
387
+ replaceAuthoredComponents(sourcePath: string, tags: Iterable<string>) {
388
+ this.views.replaceAuthored(sourcePath, tags)
389
+ }
390
+
391
+ replaceAuthoredDataComponents(
392
+ plugin: string,
393
+ sourcePath: string,
394
+ components: Parameters<ViewComponentRegistry["replaceData"]>[2],
395
+ ) {
396
+ this.views.replaceData(plugin, sourcePath, components)
397
+ }
398
+
399
+ deleteAuthoredComponents(sourcePath: string) {
400
+ this.views.deleteAuthored(sourcePath)
401
+ }
402
+
403
+ hasView(tag: string) {
404
+ return this.views.has(tag)
405
+ }
406
+
407
+ prepareDataComponent(
408
+ tag: string,
409
+ props: Readonly<Record<string, string>>,
410
+ origin: Source,
411
+ ctx: PageContext,
412
+ env: Engine,
413
+ ) {
414
+ return this.views.prepareData(tag, props, origin, ctx, env)
415
+ }
416
+
417
+ renderDataComponent(
418
+ prepared: unknown,
419
+ data: unknown,
420
+ ctx: ViewContext,
421
+ env: Engine,
422
+ ) {
423
+ return this.views.renderData(prepared, data, ctx, env)
424
+ }
425
+
426
+ query(reference: string | QueryContribution) {
427
+ return this.schemaRegistry.registeredQuery(reference)
428
+ }
429
+
430
+ queries() {
431
+ return this.schemaRegistry.queries()
432
+ }
433
+
434
+ compileViewComponents(
435
+ html: string,
436
+ source: Source,
437
+ ctx: PageContext,
438
+ env: Engine,
439
+ ) {
440
+ return this.views.compile(html, source, ctx, env)
441
+ }
442
+
443
+ async renderView(
444
+ html: string,
445
+ plan: CompiledViewPlan | undefined,
446
+ ctx: ViewContext,
447
+ env: Engine,
448
+ init?: ResponseInit,
449
+ ) {
450
+ const segments = await this.views.renderSegments(html, plan, ctx, env)
451
+ const rules: HtmlRewriteRule[] = []
452
+ for (const plugin of this.installed) {
453
+ if (!plugin.streamSelectors) continue
454
+ for (const [selector, handler] of Object.entries(
455
+ plugin.streamSelectors(ctx, this.appContext(plugin, env)),
456
+ )) {
457
+ rules.push([selector, handler])
458
+ }
459
+ }
460
+ const body = new Blob(segments, { type: "text/html; charset=utf-8" })
461
+ return env.environment.rewriteHtml(new Response(body, init), rules)
462
+ }
463
+
464
+ resolveCredentials(request: Request, actors: KnownActorIndex, env: Engine) {
465
+ return env.perf.measureAsync(
466
+ "credentials.resolve",
467
+ () => this.credentialHost.resolve(request, actors),
468
+ new URL(request.url).pathname,
469
+ )
470
+ }
471
+
472
+ credentialRequest(request: Request, actors: KnownActorIndex) {
473
+ return this.credentialHost.fetch(request, actors)
474
+ }
475
+
476
+ readyCredentials(actors: KnownActorIndex) {
477
+ return this.credentialHost.ready(actors)
478
+ }
479
+
480
+ closeCredentials() {
481
+ return (this.credentialClosing ??= this.credentialHost.close())
482
+ }
483
+
484
+ serverRoute(request: Request, url: URL, env: Engine, publicUrl = url) {
485
+ return this.routes.dispatch(request, url, env, publicUrl)
486
+ }
487
+ }
package/host/render.ts ADDED
@@ -0,0 +1,102 @@
1
+ import type { CredentialSession, Principal } from "../authority/model.ts"
2
+ import { interpolateText, type Source } from "../source/index.ts"
3
+ import { escapeHtml } from "../util.ts"
4
+ import type { CompiledPage } from "../plugins/types.ts"
5
+ import type { Engine } from "./engine.ts"
6
+ import { withReadCache } from "./response.ts"
7
+ import shellHtml from "./shell.html" with { type: "text" }
8
+
9
+ // Bun types `*.html` as HTMLBundle; `type: "text"` inlines the file as a string.
10
+ const shell = shellHtml as unknown as string
11
+
12
+ export type PageRenderOptions = Readonly<{
13
+ status?: number
14
+ headers?: HeadersInit
15
+ scheme?: "light" | "dark"
16
+ }>
17
+
18
+ export function pageNotFound() {
19
+ return new Response(
20
+ "# Not Found\n\nThe requested resource does not exist or is not available. Start at [the site root](/) or browse the public index in [/llms.txt](/llms.txt).\n",
21
+ {
22
+ status: 404,
23
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
24
+ },
25
+ )
26
+ }
27
+
28
+ async function renderCompiledPage(
29
+ app: Engine,
30
+ session: CredentialSession,
31
+ principal: Principal,
32
+ source: Source,
33
+ page: CompiledPage,
34
+ options: PageRenderOptions = {},
35
+ ) {
36
+ const assets = await app.documentHead(page)
37
+ const htmlAttrs = options.scheme
38
+ ? ` data-pmx-scheme="${options.scheme}" style="color-scheme: ${options.scheme}"`
39
+ : ""
40
+ const actorId = principal.type === "actor" ? principal.actor.id : ""
41
+ const html = interpolateText(shell, {
42
+ head: assets,
43
+ html_attrs: htmlAttrs,
44
+ route: escapeHtml(page.route),
45
+ actor: escapeHtml(actorId),
46
+ content: page.html,
47
+ })
48
+ const headers = new Headers(options.headers)
49
+ if (!headers.has("Content-Type")) {
50
+ headers.set("Content-Type", "text/html; charset=utf-8")
51
+ }
52
+ return app.rewrite(session, html, source, {
53
+ status: options.status,
54
+ headers,
55
+ })
56
+ }
57
+
58
+ /** Render one Source through the canonical cached server pipeline. */
59
+ export async function renderPage(
60
+ app: Engine,
61
+ session: CredentialSession,
62
+ principal: Principal,
63
+ source: Source,
64
+ options: PageRenderOptions = {},
65
+ ) {
66
+ const access = app.readAccess(principal, source)
67
+ if (access === "denied") return pageNotFound()
68
+ const response = await renderCompiledPage(
69
+ app,
70
+ session,
71
+ principal,
72
+ source,
73
+ await app.ensurePage(source),
74
+ options,
75
+ )
76
+ return withReadCache(response, access, session)
77
+ }
78
+
79
+ /** Render once without retaining live recompilation state. */
80
+ export async function renderUncachedPage(
81
+ app: Engine,
82
+ session: CredentialSession,
83
+ principal: Principal,
84
+ source: Source,
85
+ options: PageRenderOptions = {},
86
+ ) {
87
+ const access = app.readAccess(principal, source)
88
+ if (access === "denied") return pageNotFound()
89
+ try {
90
+ const response = await renderCompiledPage(
91
+ app,
92
+ session,
93
+ principal,
94
+ source,
95
+ await app.compilePage(source),
96
+ options,
97
+ )
98
+ return withReadCache(response, access, session)
99
+ } finally {
100
+ app.forgetCompiledPage(source.path)
101
+ }
102
+ }
@@ -0,0 +1,20 @@
1
+ import type { CredentialSession } from "../authority/model.ts"
2
+
3
+ export function privateResponse(response: Response) {
4
+ response.headers.set("Cache-Control", "private, no-store")
5
+ const vary = response.headers.get("Vary")
6
+ if (!vary?.split(",").some((value) => value.trim() === "Cookie")) {
7
+ response.headers.append("Vary", "Cookie")
8
+ }
9
+ return response
10
+ }
11
+
12
+ export function withReadCache(
13
+ response: Response,
14
+ access: "public" | "private",
15
+ session: CredentialSession,
16
+ ) {
17
+ return access === "private" || session.type === "authenticated"
18
+ ? privateResponse(response)
19
+ : response
20
+ }
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en" data-pmx-document="1" {{ html_attrs }}>
3
+ <head>
4
+ {{ head }}
5
+ </head>
6
+ <body>
7
+ <div
8
+ data-pmx-view
9
+ data-pmx-route="{{ route }}"
10
+ data-pmx-actor="{{ actor }}"
11
+ >
12
+ {{ content }}
13
+ </div>
14
+ </body>
15
+ </html>