@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,402 @@
1
+ import {
2
+ anonymousPrincipal,
3
+ type CredentialSession,
4
+ } from "../authority/model.ts"
5
+ import type { Engine } from "../host/engine.ts"
6
+ import { renderUncachedPage } from "../host/render.ts"
7
+ import { AGENT_BROWSING_PLUGIN_ID } from "../server/protocols.ts"
8
+ import { handleRequestAtPath } from "../server/http.ts"
9
+ import { publicPath } from "../server/router.ts"
10
+ import type { PublishContext } from "../plugins/types.ts"
11
+ import type { Source } from "../source/index.ts"
12
+ import type {
13
+ PublicationBundle,
14
+ PublicationRepresentation,
15
+ PublicationStore,
16
+ } from "./model.ts"
17
+ import { mapConcurrent } from "./concurrent.ts"
18
+ import {
19
+ canonicalPublishedPath,
20
+ comparePaths,
21
+ exactArtifact,
22
+ fileRepresentation,
23
+ pageArtifact,
24
+ PublicationError,
25
+ PublicationPlan,
26
+ responseHeaders,
27
+ } from "./plan.ts"
28
+ import { validatePublication } from "./validate.ts"
29
+ import {
30
+ createPublicationLocation,
31
+ type PublicationLocation,
32
+ } from "./location.ts"
33
+
34
+ const anonymousSession = Object.freeze({
35
+ type: "anonymous",
36
+ signIn: null,
37
+ }) satisfies CredentialSession
38
+
39
+ const RESERVED_ROUTES = ["/actions", "/api"] as const
40
+
41
+ const memoryStore: PublicationStore = Object.freeze({
42
+ async put(_path: string, body: Blob) {
43
+ return Object.freeze({
44
+ size: body.size,
45
+ read: async () => body,
46
+ })
47
+ },
48
+ })
49
+
50
+ function isReserved(pathname: string) {
51
+ return RESERVED_ROUTES.some(
52
+ (root) => pathname === root || pathname.startsWith(`${root}/`),
53
+ )
54
+ }
55
+
56
+ function publicSource(app: Engine, source: Source) {
57
+ return app.readAccess(anonymousPrincipal, source) === "public"
58
+ }
59
+
60
+ function contentType(response: Response, fallback: string) {
61
+ return response.headers.get("content-type") || fallback
62
+ }
63
+
64
+ async function responseBlob(response: Response, pathname: string) {
65
+ if (!response.ok) {
66
+ throw new PublicationError(
67
+ `Publication route ${pathname} returned ${response.status}.`,
68
+ )
69
+ }
70
+ return response.blob()
71
+ }
72
+
73
+ async function collectPluginRoutes(app: Engine) {
74
+ const routes = new Set<string>()
75
+ const issues: string[] = []
76
+ for (const plugin of app.plugins) {
77
+ if (!plugin.publish) continue
78
+ const ctx: PublishContext = Object.freeze({
79
+ route(pathname) {
80
+ routes.add(canonicalPublishedPath(pathname))
81
+ },
82
+ unsupported(source, message) {
83
+ issues.push(`${plugin.id}: ${source.path}: ${message}`)
84
+ },
85
+ })
86
+ await plugin.publish(ctx, app.appContext(plugin.id))
87
+ }
88
+ if (issues.length) {
89
+ throw new PublicationError(
90
+ `Static publication is unsupported:\n${issues.map((issue) => `- ${issue}`).join("\n")}`,
91
+ )
92
+ }
93
+ return [...routes].sort(comparePaths)
94
+ }
95
+
96
+ async function addPublishedRoutes(
97
+ app: Engine,
98
+ plan: PublicationPlan,
99
+ location: PublicationLocation,
100
+ pathnames: readonly string[],
101
+ ) {
102
+ for (const pathname of pathnames) {
103
+ const mounted = location.pathname(pathname)
104
+ const response = await handleRequestAtPath(
105
+ app,
106
+ new Request(location.url(pathname), {
107
+ headers: { Accept: "*/*" },
108
+ }),
109
+ pathname,
110
+ )
111
+ const type = contentType(response, "application/octet-stream")
112
+ const artifact = exactArtifact(mounted)
113
+ const body = await location.relocate(
114
+ await responseBlob(response, pathname),
115
+ pathname,
116
+ type,
117
+ )
118
+ plan.addArtifact({
119
+ path: artifact,
120
+ contentType: type,
121
+ read: async () => body,
122
+ })
123
+ plan.addRoute({
124
+ pathname: mounted,
125
+ type: "artifact",
126
+ representations: [
127
+ fileRepresentation(artifact, type, responseHeaders(response)),
128
+ ],
129
+ })
130
+ }
131
+ }
132
+
133
+ async function addSources(
134
+ app: Engine,
135
+ plan: PublicationPlan,
136
+ store: PublicationStore,
137
+ location: PublicationLocation,
138
+ ) {
139
+ const sources = app.repo
140
+ .all()
141
+ .filter((source) => publicSource(app, source))
142
+ .sort((left, right) => comparePaths(left.path, right.path))
143
+ const markdownArtifacts = new Map<string, string>()
144
+
145
+ for (const source of sources) {
146
+ const pathname = location.pathname(source.path)
147
+ if (plan.hasRoute(pathname) || isReserved(source.path)) continue
148
+ const artifact = exactArtifact(pathname)
149
+ const type = "text/markdown; charset=utf-8"
150
+ const content = source.content
151
+ plan.addArtifact({
152
+ path: artifact,
153
+ contentType: type,
154
+ read: async () => new Blob([content], { type }),
155
+ })
156
+ plan.addRoute({
157
+ pathname,
158
+ type: "source",
159
+ sourcePath: source.path,
160
+ representations: [
161
+ Object.freeze({
162
+ type: "markdown",
163
+ default: true,
164
+ artifact,
165
+ contentType: type,
166
+ }),
167
+ ],
168
+ })
169
+ markdownArtifacts.set(source.path, artifact)
170
+ }
171
+
172
+ const pages = sources.filter((source) => {
173
+ if (!app.isPage(source)) return false
174
+ const pathname = publicPath(source)
175
+ const mounted = location.pathname(pathname)
176
+ return (
177
+ !plan.hasRoute(mounted) &&
178
+ !isReserved(pathname) &&
179
+ app.resolve(pathname) === source
180
+ )
181
+ })
182
+ await mapConcurrent(pages, async (source) => {
183
+ const pathname = publicPath(source)
184
+ const mounted = location.pathname(pathname)
185
+ const response = await renderUncachedPage(
186
+ app,
187
+ anonymousSession,
188
+ anonymousPrincipal,
189
+ source,
190
+ )
191
+ const type = contentType(response, "text/html; charset=utf-8")
192
+ const body = await location.relocate(
193
+ await responseBlob(response, pathname),
194
+ pathname,
195
+ type,
196
+ )
197
+ const artifact = pageArtifact(mounted)
198
+ plan.addArtifact({
199
+ path: artifact,
200
+ contentType: type,
201
+ read: async () => body,
202
+ })
203
+ const representations: PublicationRepresentation[] = [
204
+ Object.freeze({
205
+ type: "html",
206
+ default: true,
207
+ artifact,
208
+ contentType: type,
209
+ }),
210
+ ]
211
+ const markdown = markdownArtifacts.get(source.path)
212
+ if (markdown && app.hasPlugin(AGENT_BROWSING_PLUGIN_ID)) {
213
+ representations.push(
214
+ Object.freeze({
215
+ type: "markdown",
216
+ accept: "text/markdown",
217
+ artifact: markdown,
218
+ contentType: "text/markdown; charset=utf-8",
219
+ alternatePath: location.pathname(source.path),
220
+ }),
221
+ )
222
+ }
223
+ plan.addRoute({
224
+ pathname: mounted,
225
+ type: "page",
226
+ sourcePath: source.path,
227
+ representations,
228
+ })
229
+ await plan.stagePage(artifact, store)
230
+ })
231
+ }
232
+
233
+ async function addAuthoredAssets(
234
+ app: Engine,
235
+ plan: PublicationPlan,
236
+ location: PublicationLocation,
237
+ ) {
238
+ const storage = app.environment.storage
239
+ const files = [...(await storage.list(app.repo.rootPath))].sort(comparePaths)
240
+ for (const relative of files) {
241
+ if (relative.endsWith(".md")) continue
242
+ const pathname = app.repo.graphPath(relative)
243
+ const mounted = location.pathname(pathname)
244
+ if (plan.hasRoute(mounted) || isReserved(pathname)) continue
245
+ if (app.readAccess(anonymousPrincipal, pathname) !== "public") continue
246
+ const storagePath = app.repo.storagePath(pathname)
247
+ if (!storagePath) continue
248
+ const body = await storage.readBlob(storagePath)
249
+ const type = body.type || "application/octet-stream"
250
+ const artifact = exactArtifact(mounted)
251
+ plan.addArtifact({
252
+ path: artifact,
253
+ contentType: type,
254
+ read: async () =>
255
+ location.relocate(await storage.readBlob(storagePath), pathname, type),
256
+ })
257
+ plan.addRoute({
258
+ pathname: mounted,
259
+ type: "asset",
260
+ representations: [fileRepresentation(artifact, type)],
261
+ })
262
+ }
263
+ }
264
+
265
+ function addGeneratedAssets(
266
+ app: Engine,
267
+ plan: PublicationPlan,
268
+ location: PublicationLocation,
269
+ ) {
270
+ for (const [pathname, asset] of app.generatedAssetEntries()) {
271
+ const mounted = location.pathname(pathname)
272
+ if (plan.hasRoute(mounted)) {
273
+ throw new PublicationError(`Generated asset route collision: ${pathname}`)
274
+ }
275
+ const artifact = exactArtifact(mounted)
276
+ plan.addArtifact({
277
+ path: artifact,
278
+ contentType: asset.contentType,
279
+ read: async () => {
280
+ const body =
281
+ typeof asset.body === "string"
282
+ ? new Blob([asset.body], { type: asset.contentType })
283
+ : asset.body
284
+ return location.relocate(body, pathname, asset.contentType)
285
+ },
286
+ })
287
+ plan.addRoute({
288
+ pathname: mounted,
289
+ type: "asset",
290
+ representations: [
291
+ fileRepresentation(artifact, asset.contentType, {
292
+ "cache-control": "public, max-age=31536000, immutable",
293
+ }),
294
+ ],
295
+ })
296
+ }
297
+ }
298
+
299
+ function addPageMap(
300
+ plan: PublicationPlan,
301
+ build: string,
302
+ location: PublicationLocation,
303
+ ) {
304
+ const pathname = location.pathname(`/.pmx/pages-${build}.json`)
305
+ const artifact = exactArtifact(pathname)
306
+ const type = "application/json; charset=utf-8"
307
+ const body = new Blob(
308
+ [
309
+ JSON.stringify({
310
+ version: 1,
311
+ build,
312
+ pages: plan.pageLeaves(),
313
+ }) + "\n",
314
+ ],
315
+ { type },
316
+ )
317
+ plan.addArtifact({
318
+ path: artifact,
319
+ contentType: type,
320
+ size: body.size,
321
+ read: async () => body,
322
+ })
323
+ plan.addRoute({
324
+ pathname,
325
+ type: "artifact",
326
+ representations: [
327
+ fileRepresentation(artifact, type, {
328
+ "cache-control": "public, max-age=31536000, immutable",
329
+ }),
330
+ ],
331
+ })
332
+ return pathname
333
+ }
334
+
335
+ function addBuildRecord(
336
+ plan: PublicationPlan,
337
+ build: string,
338
+ pages: string,
339
+ location: PublicationLocation,
340
+ ) {
341
+ const pathname = location.pathname("/.pmx/build.json")
342
+ const artifact = exactArtifact(pathname)
343
+ const type = "application/json; charset=utf-8"
344
+ const body = new Blob([JSON.stringify({ version: build, pages }) + "\n"], {
345
+ type,
346
+ })
347
+ plan.addArtifact({
348
+ path: artifact,
349
+ contentType: type,
350
+ size: body.size,
351
+ read: async () => body,
352
+ })
353
+ plan.addRoute({
354
+ pathname,
355
+ type: "artifact",
356
+ representations: [
357
+ fileRepresentation(artifact, type, { "cache-control": "no-store" }),
358
+ ],
359
+ })
360
+ }
361
+
362
+ /** Build a canonical public route bundle without selecting a hosting platform. */
363
+ export async function createPublication(
364
+ app: Engine,
365
+ options: Readonly<{
366
+ origin: string | URL
367
+ basePath?: string
368
+ store?: PublicationStore
369
+ }>,
370
+ ): Promise<PublicationBundle> {
371
+ if (app.runtime.target !== "static") {
372
+ throw new PublicationError(
373
+ "Static publication requires runtime.target to be static.",
374
+ )
375
+ }
376
+ const location = createPublicationLocation(
377
+ app,
378
+ options.origin,
379
+ options.basePath,
380
+ )
381
+ const store = options.store ?? memoryStore
382
+ const plan = new PublicationPlan()
383
+ const published = await collectPluginRoutes(app)
384
+ await addPublishedRoutes(app, plan, location, published)
385
+ await addSources(app, plan, store, location)
386
+ await addAuthoredAssets(app, plan, location)
387
+ addGeneratedAssets(app, plan, location)
388
+ await plan.materialize(store)
389
+ const build = await plan.buildRoot(location.origin, location.basePath)
390
+ const pages = addPageMap(plan, build, location)
391
+ await plan.materialize(store)
392
+ addBuildRecord(plan, build, pages, location)
393
+ await plan.materialize(store)
394
+ const publication = Object.freeze({
395
+ manifest: plan.manifest(build, location.origin, location.basePath),
396
+ artifacts: Object.freeze(plan.artifactList()),
397
+ })
398
+ await validatePublication(app, publication)
399
+ return publication
400
+ }
401
+
402
+ export { PublicationError } from "./plan.ts"
@@ -0,0 +1,32 @@
1
+ export const PUBLICATION_CONCURRENCY = 16
2
+
3
+ /** Map publication work concurrently while preserving input order. */
4
+ export async function mapConcurrent<T, Result>(
5
+ items: readonly T[],
6
+ map: (item: T) => Promise<Result>,
7
+ ) {
8
+ const results = new Array<Result>(items.length)
9
+ let cursor = 0
10
+ let failed = false
11
+ let failure: unknown
12
+ const worker = async () => {
13
+ while (!failed) {
14
+ const index = cursor++
15
+ if (index >= items.length) return
16
+ try {
17
+ results[index] = await map(items[index]!)
18
+ } catch (error) {
19
+ failed = true
20
+ failure = error
21
+ }
22
+ }
23
+ }
24
+ await Promise.all(
25
+ Array.from(
26
+ { length: Math.min(PUBLICATION_CONCURRENCY, items.length) },
27
+ worker,
28
+ ),
29
+ )
30
+ if (failed) throw failure
31
+ return results
32
+ }
@@ -0,0 +1,63 @@
1
+ import { blobFingerprint } from "../assets.ts"
2
+ import {
3
+ PUBLICATION_VERSION,
4
+ type PublicationArtifactRecord,
5
+ type PublicationRoute,
6
+ } from "./model.ts"
7
+ import { mapConcurrent } from "./concurrent.ts"
8
+
9
+ const ARTIFACT_DOMAIN = "application/vnd.pathmx.artifact-leaf"
10
+ const CONTRACT_DOMAIN = "application/vnd.pathmx.publication-contract"
11
+ const NODE_DOMAIN = "application/vnd.pathmx.merkle-node"
12
+
13
+ export function artifactLeaf(path: string, contentType: string, body: Blob) {
14
+ return blobFingerprint(body, `${ARTIFACT_DOMAIN}\0${path}\0${contentType}`)
15
+ }
16
+
17
+ function contractLeaf(
18
+ origin: URL,
19
+ basePath: string,
20
+ routes: readonly PublicationRoute[],
21
+ ) {
22
+ return blobFingerprint(
23
+ new Blob([
24
+ JSON.stringify({
25
+ version: PUBLICATION_VERSION,
26
+ origin: origin.origin,
27
+ basePath,
28
+ routes,
29
+ }),
30
+ ]),
31
+ CONTRACT_DOMAIN,
32
+ )
33
+ }
34
+
35
+ function parentLeaf(left: string, right: string) {
36
+ return blobFingerprint(new Blob([left, "\0", right]), NODE_DOMAIN)
37
+ }
38
+
39
+ async function merkleRoot(leaves: readonly string[]) {
40
+ let level = [...leaves]
41
+ while (level.length > 1) {
42
+ const previous = level
43
+ level = await mapConcurrent(
44
+ Array.from(
45
+ { length: Math.ceil(previous.length / 2) },
46
+ (_, index) => index * 2,
47
+ ),
48
+ (index) =>
49
+ parentLeaf(previous[index]!, previous[index + 1] ?? previous[index]!),
50
+ )
51
+ }
52
+ return level[0]!
53
+ }
54
+
55
+ export async function publicationRoot(
56
+ origin: URL,
57
+ basePath: string,
58
+ routes: readonly PublicationRoute[],
59
+ artifacts: readonly PublicationArtifactRecord[],
60
+ ) {
61
+ const contract = await contractLeaf(origin, basePath, routes)
62
+ return merkleRoot([contract, ...artifacts.map((artifact) => artifact.leaf)])
63
+ }
@@ -0,0 +1,11 @@
1
+ export { createPublication, PublicationError } from "./build.ts"
2
+ export type {
3
+ PublicationArtifact,
4
+ PublicationArtifactRecord,
5
+ PublicationBundle,
6
+ PublicationManifest,
7
+ PublicationRepresentation,
8
+ PublicationRoute,
9
+ PublicationStore,
10
+ PublicationStoredBody,
11
+ } from "./model.ts"
@@ -0,0 +1,167 @@
1
+ import type { Engine } from "../host/engine.ts"
2
+ import {
3
+ canonicalBasePath,
4
+ mountedPublishedPath,
5
+ PublicationError,
6
+ } from "./plan.ts"
7
+
8
+ const NETWORK_URL = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i
9
+ const CSS_URL = /url\(\s*(?:(["'])(.*?)\1|([^)'"\s][^)]*?))\s*\)/gi
10
+ const CSS_IMPORT = /(@import\s+)(["'])(.*?)\2/gi
11
+
12
+ export type PublicationLocation = Readonly<{
13
+ origin: URL
14
+ basePath: string
15
+ pathname(pathname: string): string
16
+ url(pathname: string): URL
17
+ relocate(body: Blob, route: string, contentType: string): Promise<Blob>
18
+ }>
19
+
20
+ function canonicalOrigin(value: string | URL) {
21
+ let origin: URL
22
+ try {
23
+ origin = new URL(value)
24
+ } catch {
25
+ throw new PublicationError(`Invalid publication origin: ${value}`)
26
+ }
27
+ if (
28
+ (origin.protocol !== "http:" && origin.protocol !== "https:") ||
29
+ origin.username ||
30
+ origin.password ||
31
+ origin.pathname !== "/" ||
32
+ origin.search ||
33
+ origin.hash
34
+ ) {
35
+ throw new PublicationError(
36
+ `Publication origin must be an HTTP(S) origin without a path: ${origin}`,
37
+ )
38
+ }
39
+ return origin
40
+ }
41
+
42
+ function relocatedUrl(
43
+ value: string,
44
+ origin: URL,
45
+ basePath: string,
46
+ route: string,
47
+ ) {
48
+ const input = value.trim()
49
+ if (!input || input.startsWith("#") || NETWORK_URL.test(input)) return
50
+ try {
51
+ const resolved = new URL(input, new URL(route, origin))
52
+ if (resolved.origin !== origin.origin) return
53
+ return `${mountedPublishedPath(basePath, resolved.pathname)}${resolved.search}${resolved.hash}`
54
+ } catch {
55
+ return
56
+ }
57
+ }
58
+
59
+ function relocateCss(
60
+ css: string,
61
+ origin: URL,
62
+ basePath: string,
63
+ route: string,
64
+ ) {
65
+ const relocate = (value: string) =>
66
+ relocatedUrl(value, origin, basePath, route)
67
+ return css
68
+ .replace(CSS_URL, (match, quote, quoted, bare) => {
69
+ const value = quoted ?? bare?.trim()
70
+ const relocated = value ? relocate(value) : undefined
71
+ return relocated ? `url(${quote ?? ""}${relocated}${quote ?? ""})` : match
72
+ })
73
+ .replace(CSS_IMPORT, (match, prefix, quote, value) => {
74
+ const relocated = relocate(value)
75
+ return relocated ? `${prefix}${quote}${relocated}${quote}` : match
76
+ })
77
+ }
78
+
79
+ function relocateHtml(
80
+ app: Engine,
81
+ html: string,
82
+ origin: URL,
83
+ basePath: string,
84
+ route: string,
85
+ ) {
86
+ const mountedRoute = mountedPublishedPath(basePath, route)
87
+ const relocated = app.environment.rewriteHtml(html, [
88
+ [
89
+ "html",
90
+ {
91
+ element(element) {
92
+ element.setAttribute("data-pmx-base", basePath)
93
+ },
94
+ },
95
+ ],
96
+ [
97
+ "[data-pmx-route]",
98
+ {
99
+ element(element) {
100
+ element.setAttribute("data-pmx-route", mountedRoute)
101
+ },
102
+ },
103
+ ],
104
+ [
105
+ "[href], [src], [action], [poster], object[data]",
106
+ {
107
+ element(element) {
108
+ for (const attribute of ["href", "src", "action", "poster", "data"]) {
109
+ const value = element.getAttribute(attribute)
110
+ const relocated = value
111
+ ? relocatedUrl(value, origin, basePath, route)
112
+ : undefined
113
+ if (relocated) element.setAttribute(attribute, relocated)
114
+ }
115
+ },
116
+ },
117
+ ],
118
+ [
119
+ "[style]",
120
+ {
121
+ element(element) {
122
+ const value = element.getAttribute("style")
123
+ if (value) {
124
+ element.setAttribute(
125
+ "style",
126
+ relocateCss(value, origin, basePath, route),
127
+ )
128
+ }
129
+ },
130
+ },
131
+ ],
132
+ ])
133
+ return relocated.replace(
134
+ /(<style\b[^>]*>)([\s\S]*?)(<\/style\s*>)/gi,
135
+ (_match, open, css, close) =>
136
+ `${open}${relocateCss(css, origin, basePath, route)}${close}`,
137
+ )
138
+ }
139
+
140
+ export function createPublicationLocation(
141
+ app: Engine,
142
+ originValue: string | URL,
143
+ basePathValue?: string,
144
+ ): PublicationLocation {
145
+ const origin = canonicalOrigin(originValue)
146
+ const basePath = canonicalBasePath(basePathValue)
147
+ const pathname = (path: string) => mountedPublishedPath(basePath, path)
148
+ return Object.freeze({
149
+ origin,
150
+ basePath,
151
+ pathname,
152
+ url: (path: string) => new URL(pathname(path), origin),
153
+ async relocate(body: Blob, route: string, contentType: string) {
154
+ if (
155
+ !contentType.includes("text/html") &&
156
+ !contentType.includes("text/css")
157
+ ) {
158
+ return body
159
+ }
160
+ const text = await body.text()
161
+ const relocated = contentType.includes("text/html")
162
+ ? relocateHtml(app, text, origin, basePath, route)
163
+ : relocateCss(text, origin, basePath, route)
164
+ return new Blob([relocated], { type: contentType })
165
+ },
166
+ })
167
+ }