@pathmx/core 0.5.0 → 0.5.1
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 +5 -1
- package/assets.ts +116 -31
- package/cli/dev.ts +3 -2
- package/dist/pathmx.js +2 -1223
- package/document.ts +15 -5
- package/host/authored-assets.ts +98 -0
- package/host/engine.ts +38 -26
- package/host/shell.html +1 -0
- package/host/source-sync.ts +1 -1
- 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/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}>`
|
|
@@ -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) {
|
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/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)
|
package/server/http.ts
CHANGED
|
@@ -216,7 +216,7 @@ async function dispatchRequest(
|
|
|
216
216
|
function serveGenerated(app: Engine, pathname: string, head: boolean) {
|
|
217
217
|
if (!pathname.startsWith("/.pmx/")) return
|
|
218
218
|
const asset = app.servedAsset(pathname)
|
|
219
|
-
if (
|
|
219
|
+
if (asset?.type !== "generated") return
|
|
220
220
|
return new Response(head ? null : asset.body, {
|
|
221
221
|
headers: {
|
|
222
222
|
"Content-Type": asset.contentType,
|
|
@@ -248,6 +248,15 @@ async function dispatch(
|
|
|
248
248
|
})
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
const authored = serveAuthored(
|
|
252
|
+
app,
|
|
253
|
+
url.pathname,
|
|
254
|
+
req.method === "HEAD",
|
|
255
|
+
session,
|
|
256
|
+
principal,
|
|
257
|
+
)
|
|
258
|
+
if (authored) return authored
|
|
259
|
+
|
|
251
260
|
const route = resolveRoute(app.repo, url.pathname)
|
|
252
261
|
if (route?.type === "source") {
|
|
253
262
|
return serveSource(
|
|
@@ -292,6 +301,39 @@ async function dispatch(
|
|
|
292
301
|
)
|
|
293
302
|
}
|
|
294
303
|
|
|
304
|
+
function decodePathname(pathname: string) {
|
|
305
|
+
try {
|
|
306
|
+
return decodeURIComponent(pathname)
|
|
307
|
+
} catch {
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function serveAuthored(
|
|
313
|
+
app: Engine,
|
|
314
|
+
pathname: string,
|
|
315
|
+
head: boolean,
|
|
316
|
+
session: CredentialSession,
|
|
317
|
+
principal: Principal,
|
|
318
|
+
) {
|
|
319
|
+
const path = decodePathname(pathname)
|
|
320
|
+
if (!path) return
|
|
321
|
+
const asset = app.servedAsset(path)
|
|
322
|
+
if (asset?.type !== "authored") return
|
|
323
|
+
const access = app.readAccess(principal, asset.sourcePath)
|
|
324
|
+
if (access === "denied") return pageNotFound()
|
|
325
|
+
return withReadCache(
|
|
326
|
+
new Response(head ? null : asset.body, {
|
|
327
|
+
headers: {
|
|
328
|
+
"Content-Type": asset.contentType,
|
|
329
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
330
|
+
},
|
|
331
|
+
}),
|
|
332
|
+
access,
|
|
333
|
+
session,
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
295
337
|
function serveSource(
|
|
296
338
|
app: Engine,
|
|
297
339
|
head: boolean,
|
|
@@ -317,17 +359,19 @@ async function serveAsset(
|
|
|
317
359
|
session: CredentialSession,
|
|
318
360
|
principal: Principal,
|
|
319
361
|
) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
path = decodeURIComponent(pathname)
|
|
323
|
-
} catch {
|
|
324
|
-
return
|
|
325
|
-
}
|
|
362
|
+
const path = decodePathname(pathname)
|
|
363
|
+
if (!path) return
|
|
326
364
|
if (path.includes("..")) return
|
|
327
365
|
const storagePath = app.repo.storagePath(path)
|
|
328
366
|
if (!storagePath || !(await app.repo.isFile(path))) return
|
|
329
367
|
const access = app.readAccess(principal, path)
|
|
330
368
|
if (access === "denied") return pageNotFound()
|
|
331
369
|
const body = head ? null : await app.environment.storage.readBlob(storagePath)
|
|
332
|
-
return withReadCache(
|
|
370
|
+
return withReadCache(
|
|
371
|
+
new Response(body, {
|
|
372
|
+
headers: { "Cache-Control": "public, max-age=0, must-revalidate" },
|
|
373
|
+
}),
|
|
374
|
+
access,
|
|
375
|
+
session,
|
|
376
|
+
)
|
|
333
377
|
}
|