@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,65 @@
1
+ import type { Plugin, PluginFactory } from "./types.ts"
2
+
3
+ /** An ordered, nestable composition recipe with no runtime identity of its own. */
4
+ export type PluginPreset = readonly PluginContribution[]
5
+
6
+ export type PluginContribution = Plugin | PluginFactory | PluginPreset
7
+
8
+ type PluginLeaf = Plugin | PluginFactory
9
+
10
+ /** Define an immutable Plugin composition recipe. */
11
+ export function definePreset(
12
+ contributions: readonly PluginContribution[],
13
+ ): PluginPreset {
14
+ return Object.freeze([...contributions])
15
+ }
16
+
17
+ function isPlugin(value: unknown): value is Plugin {
18
+ return (
19
+ typeof value === "object" &&
20
+ value !== null &&
21
+ "id" in value &&
22
+ typeof value.id === "string"
23
+ )
24
+ }
25
+
26
+ /** Flatten nested presets in declaration order before Plugin registration. */
27
+ export function expandPluginContributions(
28
+ contributions: readonly unknown[],
29
+ ): readonly PluginLeaf[] {
30
+ const leaves: PluginLeaf[] = []
31
+ const active = new Set<readonly unknown[]>()
32
+
33
+ function visit(contribution: unknown, location: string) {
34
+ if (Array.isArray(contribution)) {
35
+ if (active.has(contribution)) {
36
+ throw new Error(`Cyclic PluginPreset at ${location}.`)
37
+ }
38
+ active.add(contribution)
39
+ contribution.forEach((nested, index) =>
40
+ visit(nested, `${location}[${index}]`),
41
+ )
42
+ active.delete(contribution)
43
+ return
44
+ }
45
+ if (typeof contribution === "function" || isPlugin(contribution)) {
46
+ leaves.push(contribution as PluginLeaf)
47
+ return
48
+ }
49
+ throw new Error(
50
+ `Invalid Plugin contribution at ${location}: expected a Plugin, PluginFactory, or PluginPreset.`,
51
+ )
52
+ }
53
+
54
+ contributions.forEach((contribution, index) =>
55
+ visit(contribution, `plugins[${index}]`),
56
+ )
57
+ return Object.freeze(leaves)
58
+ }
59
+
60
+ export function instantiatePlugin(plugin: PluginLeaf): Plugin {
61
+ if (typeof plugin !== "function") return plugin
62
+ const instance = plugin()
63
+ if (!instance.name && plugin.name) instance.name = plugin.name
64
+ return instance
65
+ }
@@ -0,0 +1,322 @@
1
+ import { posixPath } from "../../path.ts"
2
+ import { queryRecordValue, type QueryField } from "../../query/fields.ts"
3
+ import { queryDependency, queryPlanFields } from "../../query/kernel.ts"
4
+ import type { QueryRequest, SourceQueryRequest } from "../../query/model.ts"
5
+ import { isFenceClose, matchFenceOpen } from "../../source/index.ts"
6
+ import { escapeHtml } from "../../util.ts"
7
+ import { authoredViewTag } from "../../view/syntax.ts"
8
+ import {
9
+ defineViewComponent,
10
+ type AppContext,
11
+ type CompileContext,
12
+ type PageContext,
13
+ type Plugin,
14
+ type ViewContext,
15
+ } from "../types.ts"
16
+
17
+ const QUERY_COMPONENT = "query:results"
18
+ const QUERY_TAG = authoredViewTag(QUERY_COMPONENT)
19
+
20
+ type StockView =
21
+ | Readonly<{ type: "list" }>
22
+ | Readonly<{ type: "table"; columns: readonly string[] }>
23
+ | Readonly<{
24
+ type: "components"
25
+ component: string
26
+ props: Readonly<Record<string, string>>
27
+ }>
28
+
29
+ type CompiledStockView =
30
+ | Readonly<{ type: "list" }>
31
+ | Readonly<{ type: "table"; columns: readonly QueryField[] }>
32
+ | (Extract<StockView, { type: "components" }> &
33
+ Readonly<{ prepared: unknown }>)
34
+
35
+ type CompiledQuery = Readonly<{
36
+ query: string
37
+ request: QueryRequest<unknown>
38
+ view: CompiledStockView
39
+ }>
40
+
41
+ function record(value: unknown, subject: string) {
42
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
43
+ throw new Error(`${subject} must be an object.`)
44
+ }
45
+ return value as Record<string, unknown>
46
+ }
47
+
48
+ function componentProps(value: unknown) {
49
+ if (value === undefined) return Object.freeze({})
50
+ const props = record(value, "Query component props")
51
+ return Object.freeze(
52
+ Object.fromEntries(
53
+ Object.entries(props).map(([key, item]) => {
54
+ if (
55
+ item == null ||
56
+ (typeof item !== "string" &&
57
+ typeof item !== "number" &&
58
+ typeof item !== "boolean")
59
+ ) {
60
+ throw new Error(`Query component prop ${key} must be a scalar.`)
61
+ }
62
+ return [key, String(item)]
63
+ }),
64
+ ),
65
+ )
66
+ }
67
+
68
+ function requireKeys(
69
+ value: Readonly<Record<string, unknown>>,
70
+ allowed: readonly string[],
71
+ subject: string,
72
+ ) {
73
+ for (const key of Object.keys(value)) {
74
+ if (!allowed.includes(key))
75
+ throw new Error(`Unknown ${subject} field: ${key}`)
76
+ }
77
+ }
78
+
79
+ function viewConfig(value: unknown): StockView {
80
+ if (value === undefined) return Object.freeze({ type: "list" })
81
+ if (typeof value === "string") {
82
+ if (value === "list") return Object.freeze({ type: "list" })
83
+ throw new Error(`Unsupported query view: ${value}`)
84
+ }
85
+ const config = record(value, "Query view")
86
+ const type = config.type ?? "list"
87
+ if (type === "list") {
88
+ requireKeys(config, ["type"], "query list view")
89
+ return Object.freeze({ type })
90
+ }
91
+ if (type === "table") {
92
+ requireKeys(config, ["type", "columns"], "query table view")
93
+ const columns = config.columns ?? []
94
+ if (
95
+ !Array.isArray(columns) ||
96
+ columns.some((column) => typeof column !== "string" || !column.trim())
97
+ ) {
98
+ throw new Error("Query table columns must be a list of field names.")
99
+ }
100
+ return Object.freeze({
101
+ type,
102
+ columns: Object.freeze(columns.map((column) => column.trim())),
103
+ })
104
+ }
105
+ if (type === "components") {
106
+ requireKeys(config, ["type", "component", "props"], "query components view")
107
+ if (typeof config.component !== "string" || !config.component.trim()) {
108
+ throw new Error("Query components view requires component.")
109
+ }
110
+ return Object.freeze({
111
+ type,
112
+ component: config.component.trim(),
113
+ props: componentProps(config.props),
114
+ })
115
+ }
116
+ throw new Error(`Unsupported query view: ${String(type)}`)
117
+ }
118
+
119
+ function authoredScope(value: unknown, ctx: CompileContext) {
120
+ const base = posixPath.dirname(ctx.source.path)
121
+ if (value === undefined || value === ".") return base
122
+ if (typeof value !== "string") throw new Error("Query from must be a string.")
123
+ if (value.startsWith("~/") || value.startsWith("/")) return value
124
+ return posixPath.resolve(base, value)
125
+ }
126
+
127
+ function parseConfig(body: string, ctx: CompileContext, app: AppContext) {
128
+ const parsed = record(app.yaml.parse(body) ?? {}, "Query fence")
129
+ const query = parsed.query ?? "sources"
130
+ if (typeof query !== "string" || !query.trim()) {
131
+ throw new Error("Query fence query must be a string.")
132
+ }
133
+ const view = viewConfig(parsed.view)
134
+ const request: Record<string, unknown> = {
135
+ ...parsed,
136
+ from: authoredScope(parsed.from, ctx),
137
+ }
138
+ delete request.query
139
+ delete request.view
140
+ return { query: query.trim(), request, view }
141
+ }
142
+
143
+ async function compileQueryFence(
144
+ config: ReturnType<typeof parseConfig>,
145
+ ctx: PageContext,
146
+ app: AppContext,
147
+ ) {
148
+ const plan = await app.query.prepare(config.query, config.request)
149
+ ctx.depend(queryDependency(plan))
150
+ const base = {
151
+ query: config.query,
152
+ request: Object.freeze({ ...config.request }),
153
+ }
154
+ if (config.view.type === "table") {
155
+ const available = queryPlanFields(app.schema, plan)
156
+ const byPath = new Map(available.map((field) => [field.path, field]))
157
+ const paths = config.view.columns.length
158
+ ? config.view.columns
159
+ : available.slice(0, 8).map(({ path }) => path)
160
+ const columns = paths.map((path) => {
161
+ const field = byPath.get(path)
162
+ if (!field) throw new Error(`Unknown query table column: ${path}`)
163
+ return field
164
+ })
165
+ return Object.freeze({
166
+ ...base,
167
+ view: Object.freeze({ type: "table", columns: Object.freeze(columns) }),
168
+ }) satisfies CompiledQuery
169
+ }
170
+ if (config.view.type === "components") {
171
+ const prepared = await app.components.prepareData(
172
+ config.view.component,
173
+ config.view.props,
174
+ ctx.source,
175
+ ctx,
176
+ )
177
+ return Object.freeze({
178
+ ...base,
179
+ view: Object.freeze({ ...config.view, prepared }),
180
+ }) satisfies CompiledQuery
181
+ }
182
+ return Object.freeze({
183
+ ...base,
184
+ view: config.view,
185
+ }) satisfies CompiledQuery
186
+ }
187
+
188
+ async function transformQueryFences(
189
+ markdown: string,
190
+ ctx: CompileContext,
191
+ app: AppContext,
192
+ ) {
193
+ if (!/query/i.test(markdown)) return markdown
194
+ const lines = markdown.split(/\r?\n/)
195
+ const output: string[] = []
196
+ for (let index = 0; index < lines.length; index++) {
197
+ const line = lines[index] ?? ""
198
+ const fence = matchFenceOpen(line)
199
+ if (!fence) {
200
+ output.push(line)
201
+ continue
202
+ }
203
+ let close = index + 1
204
+ while (close < lines.length && !isFenceClose(lines[close]!, fence.marker)) {
205
+ close++
206
+ }
207
+ if (close === lines.length) {
208
+ output.push(...lines.slice(index))
209
+ break
210
+ }
211
+ const language = fence.info.split(/\s+/, 1)[0]?.toLowerCase()
212
+ if (language !== "query") {
213
+ output.push(...lines.slice(index, close + 1))
214
+ index = close
215
+ continue
216
+ }
217
+ const config = parseConfig(
218
+ lines.slice(index + 1, close).join("\n"),
219
+ ctx,
220
+ app,
221
+ )
222
+ const encoded = encodeURIComponent(JSON.stringify(config))
223
+ output.push(
224
+ `${fence.indent}<${QUERY_TAG} config="${escapeHtml(encoded)}" />`,
225
+ )
226
+ index = close
227
+ }
228
+ return output.join("\n")
229
+ }
230
+
231
+ function resultLabel(result: unknown) {
232
+ for (const field of ["meta.title", "title", "label", "name", "id"]) {
233
+ const value = queryRecordValue(result, field)
234
+ if (typeof value === "string" || typeof value === "number") {
235
+ return String(value)
236
+ }
237
+ }
238
+ return JSON.stringify(result)
239
+ }
240
+
241
+ function renderList(results: readonly unknown[]) {
242
+ return `<ul class="pmx-query-list">${results
243
+ .map((result) => {
244
+ const label = escapeHtml(resultLabel(result))
245
+ const href = queryRecordValue(result, "href")
246
+ return `<li>${typeof href === "string" ? `<a href="${escapeHtml(href)}">${label}</a>` : label}</li>`
247
+ })
248
+ .join("")}</ul>`
249
+ }
250
+
251
+ function renderTable(
252
+ results: readonly unknown[],
253
+ columns: readonly QueryField[],
254
+ ) {
255
+ const headings = columns
256
+ .map((column) => `<th>${escapeHtml(column.label ?? column.path)}</th>`)
257
+ .join("")
258
+ const rows = results
259
+ .map(
260
+ (result) =>
261
+ `<tr>${columns
262
+ .map(({ path }) => {
263
+ const value = queryRecordValue(result, path)
264
+ const text =
265
+ value == null
266
+ ? ""
267
+ : typeof value === "object"
268
+ ? JSON.stringify(value)
269
+ : String(value)
270
+ return `<td>${escapeHtml(text)}</td>`
271
+ })
272
+ .join("")}</tr>`,
273
+ )
274
+ .join("")
275
+ return `<table class="pmx-query-table"><thead><tr>${headings}</tr></thead><tbody>${rows}</tbody></table>`
276
+ }
277
+
278
+ async function executeQuery(compiled: CompiledQuery, ctx: ViewContext) {
279
+ if (compiled.query === "sources") {
280
+ return ctx.view.query.sources(compiled.request as SourceQueryRequest)
281
+ }
282
+ if (compiled.query === "blocks") {
283
+ return ctx.view.query.blocks(compiled.request as SourceQueryRequest)
284
+ }
285
+ return ctx.view.query.run(compiled.query, compiled.request)
286
+ }
287
+
288
+ export function QueryPlugin(): Plugin {
289
+ const Results = defineViewComponent({
290
+ tag: QUERY_COMPONENT,
291
+ async compile(use, ctx, app) {
292
+ const raw = use.props.config
293
+ if (!raw) throw new Error(`Missing query config in ${use.origin.path}.`)
294
+ const config = JSON.parse(decodeURIComponent(raw)) as ReturnType<
295
+ typeof parseConfig
296
+ >
297
+ return compileQueryFence(config, ctx, app)
298
+ },
299
+ async render(_use, compiled, ctx, app) {
300
+ const results = await executeQuery(compiled, ctx)
301
+ const view = compiled.view
302
+ if (view.type === "list") return renderList(results)
303
+ if (view.type === "table") {
304
+ return renderTable(results, view.columns)
305
+ }
306
+ const rendered = await Promise.all(
307
+ results.map((result) =>
308
+ app.components.renderData(view.prepared, result, ctx),
309
+ ),
310
+ )
311
+ return `<div class="pmx-query-components">${rendered.join("")}</div>`
312
+ },
313
+ })
314
+
315
+ return {
316
+ id: "query",
317
+ components: [Results],
318
+ precompile(markdown, ctx, app) {
319
+ return transformQueryFences(markdown, ctx, app)
320
+ },
321
+ }
322
+ }
@@ -0,0 +1,46 @@
1
+ // @ts-nocheck just prototyping here... don't take any of this too literally...
2
+
3
+ const rssPlugin = definePlugin({
4
+ id: "rss",
5
+ name: "RSS",
6
+ // these routes attach to the server runtime at /api/rss/<route>
7
+ // I'm not sure if routes should have default scoping/perms or not?
8
+ routes: {
9
+ // default is GET
10
+ "/feed/:blogId": (req, context) => {
11
+ // not real code, just a shape/idea
12
+ const blogId = req.params.blogId
13
+ const graph = context.graph
14
+ const posts = graph.query({ blog: blogId, type: "post" })
15
+ const rss = posts.map((post) => ({
16
+ title: post.title,
17
+ description: post.description,
18
+ url: post.url,
19
+ date: post.date,
20
+ }))
21
+ return new Response(JSON.stringify(rss), {
22
+ headers: {
23
+ "Content-Type": "application/rss+xml",
24
+ },
25
+ })
26
+ },
27
+ "POST /subscribe": (req, context) => {
28
+ const { email } = await req.json()
29
+ // DB our source here?
30
+ context.db.insert("subscriptions").values({ email }).execute()
31
+ return new Response(JSON.stringify({ message: "Subscribed!" }), {
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ },
35
+ })
36
+ },
37
+ },
38
+ actions: {
39
+ post: ({ title, body, blog }, context: ActionContext) => {
40
+ // add a .post source
41
+ },
42
+ createBlog: ({ title }: { title: string }, context: ActionContext) => {
43
+ // create a new blog source
44
+ },
45
+ },
46
+ })
@@ -0,0 +1,62 @@
1
+ import {
2
+ clearActionError,
3
+ describeActionError,
4
+ readActionRejection,
5
+ showActionError,
6
+ } from "./error.ts"
7
+ import type { RuntimeActionInstaller } from "./start.ts"
8
+
9
+ function isActionForm(form: HTMLFormElement) {
10
+ try {
11
+ return new URL(form.action, location.href).pathname.startsWith("/actions/")
12
+ } catch {
13
+ return false
14
+ }
15
+ }
16
+
17
+ export const installActions: RuntimeActionInstaller = (runtime) => {
18
+ document.addEventListener("submit", async (event) => {
19
+ const form = event.target
20
+ if (
21
+ !(form instanceof HTMLFormElement) ||
22
+ !isActionForm(form) ||
23
+ runtime.isNavigating()
24
+ ) {
25
+ return
26
+ }
27
+ event.preventDefault()
28
+ if (form.getAttribute("data-pmx-failed") === "uncertain") return
29
+ const from = new URL(location.href)
30
+ const submitter = event instanceof SubmitEvent ? event.submitter : null
31
+ const body = new URLSearchParams(
32
+ [...new FormData(form, submitter)].filter(
33
+ (entry): entry is [string, string] => typeof entry[1] === "string",
34
+ ),
35
+ )
36
+ const load = runtime.begin({ form, submitter })
37
+ if (!load) return
38
+ clearActionError(form)
39
+ try {
40
+ const response = await fetch(form.action, {
41
+ method: "POST",
42
+ headers: {
43
+ Accept: "application/problem+json, text/html",
44
+ "Content-Type": "application/x-www-form-urlencoded",
45
+ Referer: from.href,
46
+ },
47
+ body,
48
+ redirect: "follow",
49
+ signal: load.signal,
50
+ })
51
+ if (!response.ok) throw await readActionRejection(response)
52
+ await load.apply(response, from)
53
+ } catch (error) {
54
+ if (load.signal.aborted) return
55
+ load.finish()
56
+ showActionError(form, submitter, describeActionError(error))
57
+ console.error("PathMX action refresh failed.", error)
58
+ } finally {
59
+ load.finish()
60
+ }
61
+ })
62
+ }
@@ -0,0 +1,8 @@
1
+ import { installActions } from "./actions.ts"
2
+ import { createLiveSubscription } from "./live.ts"
3
+ import { startRuntime } from "./start.ts"
4
+
5
+ startRuntime({
6
+ actions: installActions,
7
+ live: createLiveSubscription,
8
+ })