@pathmx/core 0.5.0 → 0.5.2
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/README.md +9 -1
- package/assets.ts +116 -31
- package/cli/dev.ts +3 -2
- package/cli/lint.ts +50 -8
- package/cli/update.ts +47 -5
- package/dist/pathmx.js +2 -1223
- package/document.ts +15 -5
- package/environments/bun.ts +5 -3
- package/host/authored-assets.ts +98 -0
- package/host/engine.ts +45 -26
- package/host/shell.html +1 -0
- package/host/source-sync.ts +1 -1
- package/ops/href.ts +0 -11
- package/ops/move-source.ts +5 -3
- package/package.json +1 -1
- package/plugins/runtime/document.ts +23 -4
- package/plugins/runtime/runtime.css +6 -0
- package/plugins/runtime/start.ts +1 -0
- package/plugins/styles.ts +25 -24
- package/plugins/types.ts +1 -0
- package/publish/build.ts +15 -5
- package/server/http.ts +52 -8
- package/server/watch.ts +8 -1
- package/source/source.ts +0 -1
- package/storage-path.ts +8 -0
package/document.ts
CHANGED
|
@@ -19,9 +19,12 @@ export function documentAssetRank(asset: DocumentAsset) {
|
|
|
19
19
|
export function documentAssetKey(asset: DocumentAsset) {
|
|
20
20
|
if (asset.type === "style") return `style:${asset.text}`
|
|
21
21
|
if (asset.type === "script") {
|
|
22
|
-
|
|
23
|
-
if ("
|
|
24
|
-
|
|
22
|
+
const mode = asset.module ? "module" : "classic"
|
|
23
|
+
if ("name" in asset) return `script:name:${asset.name}:${mode}`
|
|
24
|
+
if ("source" in asset) return `script:source:${asset.source}:${mode}`
|
|
25
|
+
return "href" in asset
|
|
26
|
+
? `script:href:${asset.href}:${mode}`
|
|
27
|
+
: `script:inline:${asset.text}:${mode}`
|
|
25
28
|
}
|
|
26
29
|
if (asset.type === "title") return "title"
|
|
27
30
|
if (asset.type === "preload") {
|
|
@@ -31,8 +34,10 @@ export function documentAssetKey(asset: DocumentAsset) {
|
|
|
31
34
|
const { charset, name, property } = asset.attrs
|
|
32
35
|
return `meta:${charset != null ? "charset" : (name ?? property ?? JSON.stringify(asset.attrs))}:${asset.attrs.media ?? ""}`
|
|
33
36
|
}
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
const media = asset.media ?? "all"
|
|
38
|
+
if ("name" in asset) return `stylesheet:name:${asset.name}:${media}`
|
|
39
|
+
if ("source" in asset) return `stylesheet:source:${asset.source}:${media}`
|
|
40
|
+
return `stylesheet:href:${"href" in asset ? asset.href : ""}:${media}`
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
function pushUniqueAsset(assets: DocumentAsset[], asset: DocumentAsset) {
|
|
@@ -81,6 +86,11 @@ export function renderDocumentAssets(assets: readonly DocumentAsset[]) {
|
|
|
81
86
|
return `<link rel="preload" href="${escapeHtml(asset.href)}" as="${escapeHtml(asset.as)}"${contentType}${crossorigin}>`
|
|
82
87
|
}
|
|
83
88
|
if (asset.type === "stylesheet") {
|
|
89
|
+
if ("source" in asset) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Unresolved authored stylesheet asset: ${asset.source}`,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
84
94
|
const href = "href" in asset ? asset.href : ""
|
|
85
95
|
const media = asset.media ? ` media="${escapeHtml(asset.media)}"` : ""
|
|
86
96
|
return `<link rel="stylesheet" href="${escapeHtml(href)}"${media}>`
|
package/environments/bun.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
HtmlRewrite,
|
|
16
16
|
PathMXEnvironment,
|
|
17
17
|
} from "../environment.ts"
|
|
18
|
+
import { isAuthoredStoragePath } from "../storage-path.ts"
|
|
18
19
|
|
|
19
20
|
export { publishToDirectory, writePublication } from "./bun-publish.ts"
|
|
20
21
|
|
|
@@ -201,9 +202,10 @@ export const bunEnvironment: PathMXEnvironment = {
|
|
|
201
202
|
stringify: (value) => YAML.stringify(value, null, 2),
|
|
202
203
|
},
|
|
203
204
|
storage: {
|
|
204
|
-
list: (root) =>
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
list: (root) =>
|
|
206
|
+
[
|
|
207
|
+
...new Bun.Glob("**/*").scanSync({ cwd: root, onlyFiles: true }),
|
|
208
|
+
].filter(isAuthoredStoragePath),
|
|
207
209
|
readText: (path) => Bun.file(path).text(),
|
|
208
210
|
readBlob: (path) => Bun.file(path),
|
|
209
211
|
writeText: async (path, content) => {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { canonicalPath } from "../canonical.ts"
|
|
2
|
+
import type { PathMXEnvironment } from "../environment.ts"
|
|
3
|
+
import type { Repository } from "../repository.ts"
|
|
4
|
+
import { resolveRoute } from "../server/router.ts"
|
|
5
|
+
import type { AssetStore } from "../assets.ts"
|
|
6
|
+
|
|
7
|
+
type Resolution = Readonly<{
|
|
8
|
+
generation: number
|
|
9
|
+
href: Promise<string>
|
|
10
|
+
}>
|
|
11
|
+
|
|
12
|
+
export class AuthoredAssetError extends Error {
|
|
13
|
+
constructor(
|
|
14
|
+
readonly sourcePath: string,
|
|
15
|
+
message: string,
|
|
16
|
+
) {
|
|
17
|
+
super(`Authored asset ${sourcePath}: ${message}`)
|
|
18
|
+
this.name = "AuthoredAssetError"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Resolve repository assets once per source revision without pinning old Blobs. */
|
|
23
|
+
export class AuthoredAssets {
|
|
24
|
+
private generations = new Map<string, number>()
|
|
25
|
+
private resolutions = new Map<string, Map<string, Resolution>>()
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private repo: Repository,
|
|
29
|
+
private environment: PathMXEnvironment,
|
|
30
|
+
private store: AssetStore,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
async resolve(source: string, contentType: string) {
|
|
34
|
+
const sourcePath = canonicalPath(source)
|
|
35
|
+
|
|
36
|
+
while (true) {
|
|
37
|
+
const generation = this.generations.get(sourcePath) ?? 0
|
|
38
|
+
const byContentType = this.resolutions.get(sourcePath) ?? new Map()
|
|
39
|
+
let resolution = byContentType.get(contentType)
|
|
40
|
+
if (!resolution || resolution.generation !== generation) {
|
|
41
|
+
resolution = {
|
|
42
|
+
generation,
|
|
43
|
+
href: this.publish(sourcePath, contentType),
|
|
44
|
+
}
|
|
45
|
+
byContentType.set(contentType, resolution)
|
|
46
|
+
this.resolutions.set(sourcePath, byContentType)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let href: string
|
|
50
|
+
try {
|
|
51
|
+
href = await resolution.href
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (byContentType.get(contentType) === resolution) {
|
|
54
|
+
byContentType.delete(contentType)
|
|
55
|
+
if (!byContentType.size) this.resolutions.delete(sourcePath)
|
|
56
|
+
}
|
|
57
|
+
throw error
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if ((this.generations.get(sourcePath) ?? 0) !== generation) continue
|
|
61
|
+
if (this.store.get(href)) return href
|
|
62
|
+
|
|
63
|
+
if (byContentType.get(contentType) === resolution) {
|
|
64
|
+
byContentType.delete(contentType)
|
|
65
|
+
if (!byContentType.size) this.resolutions.delete(sourcePath)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
invalidate(paths: Iterable<string>) {
|
|
71
|
+
for (const path of paths) {
|
|
72
|
+
const sourcePath = canonicalPath(this.repo.graphPath(path))
|
|
73
|
+
this.generations.set(
|
|
74
|
+
sourcePath,
|
|
75
|
+
(this.generations.get(sourcePath) ?? 0) + 1,
|
|
76
|
+
)
|
|
77
|
+
this.resolutions.delete(sourcePath)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async publish(sourcePath: string, contentType: string) {
|
|
82
|
+
const storagePath = this.repo.storagePath(sourcePath)
|
|
83
|
+
if (!storagePath || !(await this.repo.isFile(sourcePath))) {
|
|
84
|
+
throw new AuthoredAssetError(sourcePath, "source file was not found.")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const blob = await this.environment.storage.readBlob(storagePath)
|
|
88
|
+
const href = await this.store.publishAuthored(sourcePath, blob, contentType)
|
|
89
|
+
if ((await this.repo.isFile(href)) || resolveRoute(this.repo, href)) {
|
|
90
|
+
this.store.delete(href)
|
|
91
|
+
throw new AuthoredAssetError(
|
|
92
|
+
sourcePath,
|
|
93
|
+
`delivery URL conflicts with an authored route: ${href}`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
return href
|
|
97
|
+
}
|
|
98
|
+
}
|
package/host/engine.ts
CHANGED
|
@@ -54,10 +54,11 @@ import {
|
|
|
54
54
|
documentAssetRank,
|
|
55
55
|
renderDocumentAssets,
|
|
56
56
|
} from "../document.ts"
|
|
57
|
-
import {
|
|
57
|
+
import { AssetStore } from "../assets.ts"
|
|
58
58
|
import { PluginHost } from "./plugin-host.ts"
|
|
59
59
|
import { PageCompiler } from "./compiler.ts"
|
|
60
60
|
import { SourceSynchronizer } from "./source-sync.ts"
|
|
61
|
+
import { AuthoredAssets } from "./authored-assets.ts"
|
|
61
62
|
import { PluginDatabases } from "../database.ts"
|
|
62
63
|
import { projectDirectory } from "../project.ts"
|
|
63
64
|
import { browserBundleAssets } from "../browser-bundle.ts"
|
|
@@ -88,8 +89,8 @@ export class Engine {
|
|
|
88
89
|
perf = new Perf()
|
|
89
90
|
log: Log
|
|
90
91
|
private bundles = new Map<string, Promise<BundledAsset[]>>()
|
|
91
|
-
private
|
|
92
|
-
private
|
|
92
|
+
private assetStore: AssetStore
|
|
93
|
+
private authoredAssets: AuthoredAssets
|
|
93
94
|
private actorIndex: KnownActorIndex
|
|
94
95
|
private accessPolicy: SourceAccessPolicy
|
|
95
96
|
private authorityErrors = new Map<string, string>()
|
|
@@ -113,10 +114,11 @@ export class Engine {
|
|
|
113
114
|
this.repo = repo
|
|
114
115
|
this.environment = environment
|
|
115
116
|
this.runtime = createRuntimeProfile(runtime)
|
|
116
|
-
this.
|
|
117
|
+
this.assetStore =
|
|
117
118
|
this.runtime.target === "static"
|
|
118
|
-
? new
|
|
119
|
-
: new
|
|
119
|
+
? new AssetStore(Number.POSITIVE_INFINITY)
|
|
120
|
+
: new AssetStore()
|
|
121
|
+
this.authoredAssets = new AuthoredAssets(repo, environment, this.assetStore)
|
|
120
122
|
this.graph = new Graph(repo)
|
|
121
123
|
this.actorIndex = actors
|
|
122
124
|
this.profiles = profiles
|
|
@@ -212,30 +214,33 @@ export class Engine {
|
|
|
212
214
|
const ctx = { page, addAsset, addBrowserBundle }
|
|
213
215
|
await this.pluginHost.documentAssets(ctx, this)
|
|
214
216
|
addAsset(...(page?.assets ?? []))
|
|
215
|
-
|
|
216
|
-
.map((asset) => this.resolveDocumentAsset(asset))
|
|
217
|
-
|
|
217
|
+
const resolved = await Promise.all(
|
|
218
|
+
assets.map((asset) => this.resolveDocumentAsset(asset)),
|
|
219
|
+
)
|
|
220
|
+
return resolved.sort((a, b) => documentAssetRank(a) - documentAssetRank(b))
|
|
218
221
|
}
|
|
219
222
|
|
|
220
223
|
servedAsset(pathname: string) {
|
|
221
|
-
return this.
|
|
224
|
+
return this.assetStore.get(pathname)
|
|
222
225
|
}
|
|
223
226
|
|
|
224
|
-
/** @internal Enumerate retained
|
|
225
|
-
|
|
226
|
-
return this.
|
|
227
|
+
/** @internal Enumerate retained artifacts during static publication. */
|
|
228
|
+
servedAssetEntries() {
|
|
229
|
+
return this.assetStore.entries()
|
|
227
230
|
}
|
|
228
231
|
|
|
229
232
|
/** Publish an immutable, content-hashed binary asset under `/.pmx/`. */
|
|
230
233
|
async publishAsset(name: string, blob: Blob, contentType = blob.type) {
|
|
231
|
-
return this.
|
|
234
|
+
return this.assetStore.publishBlob(name, blob, contentType)
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
onSync(listener: (event: RouteInvalidation) => void) {
|
|
235
238
|
return this.sourceSync.onSync(listener)
|
|
236
239
|
}
|
|
237
240
|
|
|
238
|
-
private resolveDocumentAsset(
|
|
241
|
+
private async resolveDocumentAsset(
|
|
242
|
+
asset: DocumentAsset,
|
|
243
|
+
): Promise<DocumentAsset> {
|
|
239
244
|
if (asset.type === "stylesheet" && "name" in asset) {
|
|
240
245
|
const href = this.storeNamed(
|
|
241
246
|
asset.name,
|
|
@@ -248,6 +253,17 @@ export class Engine {
|
|
|
248
253
|
...(asset.media ? { media: asset.media } : {}),
|
|
249
254
|
}
|
|
250
255
|
}
|
|
256
|
+
if (asset.type === "stylesheet" && "source" in asset) {
|
|
257
|
+
const href = await this.authoredAssets.resolve(
|
|
258
|
+
asset.source,
|
|
259
|
+
"text/css; charset=utf-8",
|
|
260
|
+
)
|
|
261
|
+
return {
|
|
262
|
+
type: "stylesheet",
|
|
263
|
+
href,
|
|
264
|
+
...(asset.media ? { media: asset.media } : {}),
|
|
265
|
+
}
|
|
266
|
+
}
|
|
251
267
|
if (asset.type === "script" && "name" in asset) {
|
|
252
268
|
const href = this.storeNamed(
|
|
253
269
|
asset.name,
|
|
@@ -257,25 +273,21 @@ export class Engine {
|
|
|
257
273
|
return { type: "script", href, module: asset.module }
|
|
258
274
|
}
|
|
259
275
|
if (asset.type === "script" && "source" in asset) {
|
|
260
|
-
const
|
|
261
|
-
|
|
276
|
+
const href = await this.authoredAssets.resolve(
|
|
277
|
+
asset.source,
|
|
278
|
+
"text/javascript; charset=utf-8",
|
|
279
|
+
)
|
|
262
280
|
return { type: "script", href, module: asset.module }
|
|
263
281
|
}
|
|
264
282
|
return asset
|
|
265
283
|
}
|
|
266
284
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const graphPath = this.repo.graphPath(path)
|
|
270
|
-
this.assetRevisions.set(
|
|
271
|
-
graphPath,
|
|
272
|
-
(this.assetRevisions.get(graphPath) ?? 0) + 1,
|
|
273
|
-
)
|
|
274
|
-
}
|
|
285
|
+
invalidateAuthoredAssets(paths: Iterable<string>) {
|
|
286
|
+
this.authoredAssets.invalidate(paths)
|
|
275
287
|
}
|
|
276
288
|
|
|
277
289
|
private storeNamed(name: string, text: string, contentType: string) {
|
|
278
|
-
return this.
|
|
290
|
+
return this.assetStore.publishText(name, text, contentType)
|
|
279
291
|
}
|
|
280
292
|
|
|
281
293
|
async documentHead(page?: CompiledPage) {
|
|
@@ -422,6 +434,13 @@ export class Engine {
|
|
|
422
434
|
"index.source",
|
|
423
435
|
() => {
|
|
424
436
|
this.forgetNode(source.path)
|
|
437
|
+
if (Object.hasOwn(source.data, "id")) {
|
|
438
|
+
this.log.warn("source.id.ignored", {
|
|
439
|
+
source: source.path,
|
|
440
|
+
message:
|
|
441
|
+
'Source frontmatter "id" is ignored; Source ids are derived from file paths.',
|
|
442
|
+
})
|
|
443
|
+
}
|
|
425
444
|
this.compiler.indexSource(source)
|
|
426
445
|
this.authorityErrors.delete(source.id)
|
|
427
446
|
const capture = (error: unknown) => {
|
package/host/shell.html
CHANGED
package/host/source-sync.ts
CHANGED
|
@@ -229,7 +229,7 @@ export class SourceSynchronizer {
|
|
|
229
229
|
|
|
230
230
|
invalidateAssets(paths: Iterable<string>) {
|
|
231
231
|
const changed = [...paths]
|
|
232
|
-
this.engine.
|
|
232
|
+
this.engine.invalidateAuthoredAssets(changed)
|
|
233
233
|
const invalidations = new RouteInvalidations()
|
|
234
234
|
for (const path of changed) {
|
|
235
235
|
const graphPath = this.engine.repo.graphPath(path)
|
package/ops/href.ts
CHANGED
|
@@ -23,16 +23,6 @@ export function isRelativeHref(pathname: string) {
|
|
|
23
23
|
)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function authoredId(source: Source) {
|
|
27
|
-
const id = source.data.id
|
|
28
|
-
return typeof id === "string" && id.trim() ? id.trim() : undefined
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function isAuthoredIdHref(pathname: string, source: Source) {
|
|
32
|
-
const id = authoredId(source)
|
|
33
|
-
return Boolean(id && (pathname === id || pathname === `/${id}`))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
26
|
export function relativeHref(
|
|
37
27
|
fromPath: string,
|
|
38
28
|
targetPath: string,
|
|
@@ -75,7 +65,6 @@ export function retargetHref(
|
|
|
75
65
|
): string | undefined {
|
|
76
66
|
const { pathname, query, fragment } = splitHref(href)
|
|
77
67
|
if (!pathname) return
|
|
78
|
-
if (isAuthoredIdHref(pathname, oldSource)) return
|
|
79
68
|
|
|
80
69
|
const explicitMd = pathname.endsWith(".md")
|
|
81
70
|
const target = explicitMd ? newSource.path : publicPath(newSource)
|
package/ops/move-source.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { posixPath } from "../path.ts"
|
|
|
3
3
|
import { isIndexSource, publicPath } from "../server/router.ts"
|
|
4
4
|
import { createSource, rewriteLinks, type Source } from "../source/index.ts"
|
|
5
5
|
import {
|
|
6
|
-
authoredId,
|
|
7
6
|
isRelativeHref,
|
|
8
7
|
rebaseRelativeHref,
|
|
9
8
|
retargetHref,
|
|
@@ -104,9 +103,12 @@ export async function moveSource(
|
|
|
104
103
|
`Moving index source ${source.path} changes the directory route ${publicPath(source)}`,
|
|
105
104
|
)
|
|
106
105
|
}
|
|
107
|
-
if (
|
|
106
|
+
if (
|
|
107
|
+
!isIndexSource(source) &&
|
|
108
|
+
publicPath(source) !== publicPath(preview)
|
|
109
|
+
) {
|
|
108
110
|
plan.warnings.push(
|
|
109
|
-
`Moving ${source.path} changes its public URL from ${source
|
|
111
|
+
`Moving ${source.path} changes its public URL from ${publicPath(source)} to ${publicPath(preview)}`,
|
|
110
112
|
)
|
|
111
113
|
}
|
|
112
114
|
|
package/package.json
CHANGED
|
@@ -42,6 +42,15 @@ export class PathMXReloadRequiredError extends PathMXDocumentError {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
const AUGMENTATIONS_SELECTOR = "body > [data-pmx-augmentations]"
|
|
46
|
+
|
|
47
|
+
function augmentationLayer(target: Document) {
|
|
48
|
+
const layers = target.querySelectorAll(AUGMENTATIONS_SELECTOR)
|
|
49
|
+
return layers.length === 1 && layers[0] instanceof HTMLElement
|
|
50
|
+
? layers[0]
|
|
51
|
+
: undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
function assetUrl(element: Element, attribute: "href" | "src") {
|
|
46
55
|
try {
|
|
47
56
|
return new URL(element.getAttribute(attribute) || "", location.href)
|
|
@@ -55,21 +64,23 @@ function stylesheetKey(link: Element) {
|
|
|
55
64
|
if (!href) return link.getAttribute("href") || ""
|
|
56
65
|
href.search = ""
|
|
57
66
|
if (isHashedAsset(href.pathname)) {
|
|
58
|
-
return href.pathname.replace(
|
|
67
|
+
return href.pathname.replace(HASHED_ASSET, "")
|
|
59
68
|
}
|
|
60
69
|
return href.pathname
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
function isHashedAsset(pathname: string) {
|
|
64
|
-
return
|
|
73
|
+
return HASHED_ASSET.test(pathname)
|
|
65
74
|
}
|
|
66
75
|
|
|
76
|
+
const HASHED_ASSET = /-[0-9a-f]{16}(?=\.[^/]+$)/
|
|
77
|
+
|
|
67
78
|
function scriptKey(script: Element) {
|
|
68
79
|
const src = assetUrl(script, "src")
|
|
69
80
|
if (!src) return script.getAttribute("src") || ""
|
|
70
81
|
src.search = ""
|
|
71
82
|
if (isHashedAsset(src.pathname)) {
|
|
72
|
-
return src.pathname.replace(
|
|
83
|
+
return src.pathname.replace(HASHED_ASSET, "")
|
|
73
84
|
}
|
|
74
85
|
return src.href
|
|
75
86
|
}
|
|
@@ -157,7 +168,7 @@ function reconcileStylesheets(
|
|
|
157
168
|
current.append(incomingLink.cloneNode(true))
|
|
158
169
|
continue
|
|
159
170
|
}
|
|
160
|
-
if (isHashedAsset(
|
|
171
|
+
if (isHashedAsset(assetUrl(incomingLink, "href")?.pathname ?? "")) {
|
|
161
172
|
if (currentLink.getAttribute("href") === nextHref) continue
|
|
162
173
|
const added = incomingLink.cloneNode(true) as HTMLLinkElement
|
|
163
174
|
added.addEventListener("load", () => currentLink.remove(), { once: true })
|
|
@@ -415,6 +426,11 @@ export async function readPathMXDocument(
|
|
|
415
426
|
if (views.length !== 1 || !(views[0] instanceof HTMLElement)) {
|
|
416
427
|
throw new PathMXDocumentError("The response has no canonical PathMX view.")
|
|
417
428
|
}
|
|
429
|
+
if (!augmentationLayer(incoming)) {
|
|
430
|
+
throw new PathMXDocumentError(
|
|
431
|
+
"The response has no canonical PathMX augmentation layer.",
|
|
432
|
+
)
|
|
433
|
+
}
|
|
418
434
|
return {
|
|
419
435
|
document: incoming,
|
|
420
436
|
requestedUrl,
|
|
@@ -446,6 +462,9 @@ export async function reconcilePathMXDocument(
|
|
|
446
462
|
viewTransitionTypes?: readonly string[]
|
|
447
463
|
},
|
|
448
464
|
) {
|
|
465
|
+
if (!augmentationLayer(document)) {
|
|
466
|
+
throw new PathMXReloadRequiredError("The PathMX document shell changed.")
|
|
467
|
+
}
|
|
449
468
|
const viewTransitionTypes = options.viewTransitionTypes ?? []
|
|
450
469
|
const apply = () => {
|
|
451
470
|
const canonicalView = window.__pmxCanonicalView ?? currentView()
|
package/plugins/runtime/start.ts
CHANGED
package/plugins/styles.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { cachedRouteTargets } from "../invalidation.ts"
|
|
2
2
|
import { parseLinks, type Source, type SourceLink } from "../source/index.ts"
|
|
3
|
-
import type {
|
|
4
|
-
DependencyContext,
|
|
5
|
-
DocumentAsset,
|
|
6
|
-
DocumentStylesheet,
|
|
7
|
-
Plugin,
|
|
8
|
-
} from "./types.ts"
|
|
3
|
+
import type { DependencyContext, Plugin } from "./types.ts"
|
|
9
4
|
import { directiveDefinition } from "./directive-definitions.ts"
|
|
10
5
|
|
|
11
6
|
const MEDIA = new Set(["print", "screen"])
|
|
12
7
|
const EXTERNAL = /^[a-z][a-z\d+.-]*:/i
|
|
8
|
+
type AuthoredStylesheet = Readonly<{
|
|
9
|
+
type: "stylesheet"
|
|
10
|
+
source: string
|
|
11
|
+
media?: string
|
|
12
|
+
}>
|
|
13
13
|
|
|
14
14
|
function resolveTarget(source: Source, target: string) {
|
|
15
15
|
return target.startsWith("/")
|
|
@@ -24,7 +24,7 @@ function styleLinks(source: Source, links: readonly SourceLink[]) {
|
|
|
24
24
|
function resolveStylesheet(
|
|
25
25
|
source: Source,
|
|
26
26
|
link: SourceLink,
|
|
27
|
-
):
|
|
27
|
+
): AuthoredStylesheet | undefined {
|
|
28
28
|
if (link.directive?.name !== "styles") return
|
|
29
29
|
const raw = link.href.split("#")[0]?.split("?")[0] ?? ""
|
|
30
30
|
if (
|
|
@@ -34,28 +34,31 @@ function resolveStylesheet(
|
|
|
34
34
|
raw.startsWith("~/")
|
|
35
35
|
)
|
|
36
36
|
return
|
|
37
|
-
const
|
|
38
|
-
if (!
|
|
37
|
+
const sourcePath = resolveTarget(source, raw)
|
|
38
|
+
if (!sourcePath.endsWith(".css")) return
|
|
39
39
|
const media = link.directive.qualifiers.find((item) => MEDIA.has(item))
|
|
40
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
type: "stylesheet",
|
|
42
|
+
source: sourcePath,
|
|
43
|
+
...(media ? { media } : {}),
|
|
44
|
+
}
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
function
|
|
44
|
-
return asset.
|
|
47
|
+
function stylesheetKey(asset: AuthoredStylesheet) {
|
|
48
|
+
return `${asset.source}\0${asset.media ?? ""}`
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
function pushUnique(assets:
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
51
|
+
function pushUnique(assets: AuthoredStylesheet[], asset: AuthoredStylesheet) {
|
|
52
|
+
const key = stylesheetKey(asset)
|
|
53
|
+
if (assets.some((item) => stylesheetKey(item) === key)) return
|
|
50
54
|
assets.push(asset)
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
function addStylesheetDependency(
|
|
54
58
|
dependencies: DependencyContext,
|
|
55
|
-
asset:
|
|
59
|
+
asset: AuthoredStylesheet,
|
|
56
60
|
) {
|
|
57
|
-
|
|
58
|
-
if (href) dependencies.depend({ type: "source", path: href })
|
|
61
|
+
dependencies.depend({ type: "source", path: asset.source })
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
function sourceStyleLinks(source: Source) {
|
|
@@ -65,7 +68,7 @@ function sourceStyleLinks(source: Source) {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
function globalStylesheets(source: Source, links: readonly SourceLink[]) {
|
|
68
|
-
const assets:
|
|
71
|
+
const assets: AuthoredStylesheet[] = []
|
|
69
72
|
for (const link of styleLinks(source, links)) {
|
|
70
73
|
if (!link.directive?.qualifiers.includes("global")) continue
|
|
71
74
|
const asset = resolveStylesheet(source, link)
|
|
@@ -74,10 +77,8 @@ function globalStylesheets(source: Source, links: readonly SourceLink[]) {
|
|
|
74
77
|
return assets
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
function stylesheetSignature(assets: readonly
|
|
78
|
-
return assets
|
|
79
|
-
.map((asset) => `${stylesheetHref(asset) ?? ""}\0${asset.media ?? ""}`)
|
|
80
|
-
.join("\n")
|
|
80
|
+
function stylesheetSignature(assets: readonly AuthoredStylesheet[]) {
|
|
81
|
+
return assets.map(stylesheetKey).join("\n")
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
function authoredGlobalSignature(source?: Source) {
|
|
@@ -87,7 +88,7 @@ function authoredGlobalSignature(source?: Source) {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
export function StylesPlugin(): Plugin {
|
|
90
|
-
const global = new Map<string,
|
|
91
|
+
const global = new Map<string, AuthoredStylesheet[]>()
|
|
91
92
|
|
|
92
93
|
return {
|
|
93
94
|
id: "styles",
|
package/plugins/types.ts
CHANGED
|
@@ -47,6 +47,7 @@ export type CompileContext = DependencyContext & {
|
|
|
47
47
|
|
|
48
48
|
export type DocumentStylesheet =
|
|
49
49
|
| Readonly<{ type: "stylesheet"; href: string; media?: string }>
|
|
50
|
+
| Readonly<{ type: "stylesheet"; source: string; media?: string }>
|
|
50
51
|
| Readonly<{
|
|
51
52
|
type: "stylesheet"
|
|
52
53
|
name: string
|
package/publish/build.ts
CHANGED
|
@@ -257,20 +257,30 @@ async function addAuthoredAssets(
|
|
|
257
257
|
plan.addRoute({
|
|
258
258
|
pathname: mounted,
|
|
259
259
|
type: "asset",
|
|
260
|
-
representations: [
|
|
260
|
+
representations: [
|
|
261
|
+
fileRepresentation(artifact, type, {
|
|
262
|
+
"cache-control": "public, max-age=0, must-revalidate",
|
|
263
|
+
}),
|
|
264
|
+
],
|
|
261
265
|
})
|
|
262
266
|
}
|
|
263
267
|
}
|
|
264
268
|
|
|
265
|
-
function
|
|
269
|
+
function addStoredAssets(
|
|
266
270
|
app: Engine,
|
|
267
271
|
plan: PublicationPlan,
|
|
268
272
|
location: PublicationLocation,
|
|
269
273
|
) {
|
|
270
|
-
for (const [pathname, asset] of app.
|
|
274
|
+
for (const [pathname, asset] of app.servedAssetEntries()) {
|
|
275
|
+
if (
|
|
276
|
+
asset.type === "authored" &&
|
|
277
|
+
app.readAccess(anonymousPrincipal, asset.sourcePath) !== "public"
|
|
278
|
+
) {
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
271
281
|
const mounted = location.pathname(pathname)
|
|
272
282
|
if (plan.hasRoute(mounted)) {
|
|
273
|
-
throw new PublicationError(`
|
|
283
|
+
throw new PublicationError(`Stored asset route collision: ${pathname}`)
|
|
274
284
|
}
|
|
275
285
|
const artifact = exactArtifact(mounted)
|
|
276
286
|
plan.addArtifact({
|
|
@@ -384,7 +394,7 @@ export async function createPublication(
|
|
|
384
394
|
await addPublishedRoutes(app, plan, location, published)
|
|
385
395
|
await addSources(app, plan, store, location)
|
|
386
396
|
await addAuthoredAssets(app, plan, location)
|
|
387
|
-
|
|
397
|
+
addStoredAssets(app, plan, location)
|
|
388
398
|
await plan.materialize(store)
|
|
389
399
|
const build = await plan.buildRoot(location.origin, location.basePath)
|
|
390
400
|
const pages = addPageMap(plan, build, location)
|