@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/publish/build.ts
ADDED
|
@@ -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
|
+
}
|
package/publish/index.ts
ADDED
|
@@ -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
|
+
}
|