@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,254 @@
1
+ import { dependencyKey } from "../dependencies.ts"
2
+ import { RouteInvalidations, type RouteInvalidation } from "../invalidation.ts"
3
+ import * as queryInvalidation from "../query/invalidation.ts"
4
+ import type { CommitResult, SourceChangeSet } from "../repository.ts"
5
+ import type { Source } from "../source/index.ts"
6
+ import type { SourceTransition } from "../plugins/types.ts"
7
+ import type { PageCompiler } from "./compiler.ts"
8
+ import type { Engine } from "./engine.ts"
9
+ import type { PluginHost } from "./plugin-host.ts"
10
+
11
+ type RefreshTransition = SourceTransition & {
12
+ path: string
13
+ }
14
+
15
+ /** Owns serialized source changes, recompilation, and route invalidation. */
16
+ export class SourceSynchronizer {
17
+ private listeners = new Set<(event: RouteInvalidation) => void>()
18
+ private commitQueue: Promise<void> = Promise.resolve()
19
+
20
+ constructor(
21
+ private engine: Engine,
22
+ private plugins: PluginHost,
23
+ private compiler: PageCompiler,
24
+ ) {}
25
+
26
+ onSync(listener: (event: RouteInvalidation) => void) {
27
+ this.listeners.add(listener)
28
+ return () => {
29
+ this.listeners.delete(listener)
30
+ }
31
+ }
32
+
33
+ private emit(event: RouteInvalidation) {
34
+ if (!event.targets.length) return
35
+ for (const listener of this.listeners) listener(event)
36
+ }
37
+
38
+ private referrerPaths(path: string) {
39
+ return this.engine.graph.linksTo(path).map((edge) => edge.from)
40
+ }
41
+
42
+ private addPageInvalidations(
43
+ invalidations: RouteInvalidations,
44
+ pagePaths: Iterable<string>,
45
+ ) {
46
+ for (const pagePath of pagePaths) {
47
+ const page = this.compiler.pageCache.get(pagePath)
48
+ if (page) invalidations.add(page.route)
49
+ }
50
+ }
51
+
52
+ private applyPluginInvalidations(
53
+ dirty: Set<string>,
54
+ invalidations: RouteInvalidations,
55
+ transition: SourceTransition,
56
+ ) {
57
+ for (const invalidation of this.plugins.invalidations(
58
+ transition,
59
+ this.engine,
60
+ )) {
61
+ const dependencies = [...(invalidation.dependencies ?? [])]
62
+ queryInvalidation.dependencies(this.engine, invalidations, dependencies)
63
+ for (const dependency of dependencies) {
64
+ for (const pagePath of this.compiler.dependentsOf(
65
+ dependencyKey(dependency),
66
+ )) {
67
+ dirty.add(pagePath)
68
+ }
69
+ }
70
+ for (const sourceId of invalidation.sourceIds ?? []) {
71
+ const source = this.engine.repo.get(sourceId)
72
+ if (source && this.compiler.isPage(source)) dirty.add(source.path)
73
+ }
74
+ for (const target of invalidation.targets ?? []) {
75
+ if (target.type === "actor") {
76
+ invalidations.addActorTargets(target.actors)
77
+ } else if (target.actors == null) invalidations.add(target.route)
78
+ else invalidations.addActors(target.route, target.actors)
79
+ }
80
+ }
81
+ }
82
+
83
+ private async rebuildGraph() {
84
+ await this.engine.perf.measureAsync("graph.rebuild", () =>
85
+ this.engine.graph.rebuild(),
86
+ )
87
+ }
88
+
89
+ private async refreshTransitions(transitions: RefreshTransition[]) {
90
+ return this.engine.perf.measureAsync(
91
+ "notify",
92
+ async () => {
93
+ const invalidations = new RouteInvalidations()
94
+ const dirty = new Set<string>()
95
+
96
+ for (const transition of transitions) {
97
+ this.applyPluginInvalidations(dirty, invalidations, transition)
98
+ const graphPath = this.engine.repo.graphPath(transition.path)
99
+ this.engine.log.debug("rebuild", { path: graphPath })
100
+ const cached = this.compiler.pageCache.get(graphPath)
101
+ if (cached) invalidations.add(cached.route)
102
+ for (const path of this.referrerPaths(graphPath)) dirty.add(path)
103
+ for (const path of this.compiler.dependentsOf(graphPath))
104
+ dirty.add(path)
105
+
106
+ if (!transition.after) {
107
+ queryInvalidation.source(
108
+ this.engine,
109
+ invalidations,
110
+ transition.before,
111
+ transition.after,
112
+ graphPath,
113
+ )
114
+ if (transition.before) {
115
+ this.engine.clearAuthorityError(transition.before.id)
116
+ }
117
+ this.engine.forgetNode(graphPath)
118
+ this.compiler.forgetCompiled(graphPath)
119
+ continue
120
+ }
121
+
122
+ this.engine.indexSource(transition.after, true)
123
+ queryInvalidation.source(
124
+ this.engine,
125
+ invalidations,
126
+ transition.before,
127
+ transition.after,
128
+ graphPath,
129
+ )
130
+ if (this.compiler.isPage(transition.after)) {
131
+ dirty.add(transition.after.path)
132
+ } else {
133
+ this.compiler.pageCache.delete(transition.after.path)
134
+ }
135
+ }
136
+
137
+ await this.rebuildGraph()
138
+ for (const transition of transitions) {
139
+ for (const path of this.referrerPaths(transition.path))
140
+ dirty.add(path)
141
+ }
142
+ await this.compiler.recompilePages(dirty)
143
+ this.addPageInvalidations(invalidations, dirty)
144
+ return invalidations.event()
145
+ },
146
+ transitions.map((transition) => transition.path).join(","),
147
+ )
148
+ }
149
+
150
+ dropNode(nodePath: string) {
151
+ const source =
152
+ this.engine.repo.atPath(nodePath) ?? this.engine.repo.get(nodePath)
153
+ const path = source?.path ?? this.engine.repo.graphPath(nodePath)
154
+ this.engine.repo.remove(path)
155
+ this.engine.forgetNode(path)
156
+ this.compiler.forgetCompiled(path)
157
+ }
158
+
159
+ commit(changeSet: SourceChangeSet) {
160
+ return this.commitPrepared(async () => changeSet)
161
+ }
162
+
163
+ /** Serialize intent preparation, durable commit, refresh, and publication. */
164
+ commitPrepared(prepare: () => Promise<SourceChangeSet>) {
165
+ const pending = this.commitQueue.then(async () => {
166
+ const changeSet = await prepare()
167
+ if (!changeSet.changes.length) return { applied: [] }
168
+ return this.commitNow(changeSet)
169
+ })
170
+ this.commitQueue = pending.then(
171
+ () => undefined,
172
+ () => undefined,
173
+ )
174
+ return pending
175
+ }
176
+
177
+ private async commitNow(changeSet: SourceChangeSet) {
178
+ const before = new Map(
179
+ changeSet.changes.map((change) => {
180
+ const path = this.engine.repo.graphPath(change.path)
181
+ return [path, this.engine.repo.atPath(path)] as const
182
+ }),
183
+ )
184
+ const result = await this.engine.repo.commit(changeSet)
185
+ await this.refresh(result, before)
186
+ if (result.error) throw result.error
187
+ return result
188
+ }
189
+
190
+ async refresh(
191
+ result: CommitResult,
192
+ before = new Map<string, Source | undefined>(),
193
+ ) {
194
+ const event = await this.refreshTransitions(
195
+ result.applied.map((change) => {
196
+ const path = this.engine.repo.graphPath(change.path)
197
+ return {
198
+ path,
199
+ before: before.get(path),
200
+ after: this.engine.repo.atPath(path),
201
+ }
202
+ }),
203
+ )
204
+ this.emit(event)
205
+ }
206
+
207
+ async syncSource(path: string) {
208
+ const graphPath = this.engine.repo.graphPath(path)
209
+ const previous = this.engine.repo.atPath(graphPath)
210
+ const result = await this.engine.repo.upsert(path, { onError: "preserve" })
211
+ if (result.type === "error") {
212
+ this.engine.log.error("source.parse", {
213
+ path: graphPath,
214
+ message: result.error.message,
215
+ })
216
+ return
217
+ }
218
+ const source = result.type === "updated" ? result.source : undefined
219
+ if (source && source.content === previous?.content) return
220
+ const event = await this.refreshTransitions([
221
+ { path: graphPath, before: previous, after: source },
222
+ ])
223
+ this.emit(event)
224
+ }
225
+
226
+ invalidateAsset(path: string) {
227
+ this.invalidateAssets([path])
228
+ }
229
+
230
+ invalidateAssets(paths: Iterable<string>) {
231
+ const changed = [...paths]
232
+ this.engine.bumpAssetRevisions(changed)
233
+ const invalidations = new RouteInvalidations()
234
+ for (const path of changed) {
235
+ const graphPath = this.engine.repo.graphPath(path)
236
+ this.addPageInvalidations(
237
+ invalidations,
238
+ this.compiler.dependentsOf(graphPath),
239
+ )
240
+ }
241
+ this.emit(invalidations.event())
242
+ }
243
+
244
+ async syncFromStorage(relPath: string) {
245
+ return this.engine.perf.measureAsync(
246
+ "sync-from-storage",
247
+ async () => {
248
+ if (!this.engine.repo.storagePath(relPath)) return
249
+ await this.syncSource(this.engine.repo.graphPath(relPath).slice(1))
250
+ },
251
+ relPath,
252
+ )
253
+ }
254
+ }
@@ -0,0 +1,15 @@
1
+ import { decodeHtmlEntities } from "./util.ts"
2
+
3
+ const ATTRIBUTE =
4
+ /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g
5
+
6
+ /** @internal Parse the constrained attributes accepted by authored empty elements. */
7
+ export function parseHtmlAttributes(raw: string | undefined) {
8
+ const attrs: Record<string, string> = {}
9
+ for (const match of raw?.matchAll(ATTRIBUTE) ?? []) {
10
+ const name = match[1]!.toLowerCase()
11
+ if (Object.hasOwn(attrs, name)) continue
12
+ attrs[name] = decodeHtmlEntities(match[2] ?? match[3] ?? match[4] ?? "")
13
+ }
14
+ return attrs
15
+ }
package/html.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { escapeHtml } from "./util.ts"
2
+
3
+ const HTML_FRAGMENT = Symbol("PathMX.Html")
4
+
5
+ type HtmlFragment = Readonly<{
6
+ [HTML_FRAGMENT]: string
7
+ toString(): string
8
+ }>
9
+
10
+ /** HTML returned by plugins: an existing string or a safely composed fragment. */
11
+ export type Html = string | HtmlFragment
12
+
13
+ type HtmlValue = Html | number | boolean | null | undefined | readonly HtmlValue[]
14
+
15
+ function fragment(value: string): HtmlFragment {
16
+ return Object.freeze({
17
+ [HTML_FRAGMENT]: value,
18
+ toString: () => value,
19
+ })
20
+ }
21
+
22
+ function isFragment(value: HtmlValue): value is HtmlFragment {
23
+ return typeof value === "object" && value !== null && HTML_FRAGMENT in value
24
+ }
25
+
26
+ function interpolate(value: HtmlValue): string {
27
+ if (value == null || typeof value === "boolean") return ""
28
+ if (Array.isArray(value)) return value.map(interpolate).join("")
29
+ if (isFragment(value)) return value[HTML_FRAGMENT]
30
+ return escapeHtml(String(value)).replace(/'/g, "&#39;")
31
+ }
32
+
33
+ /** Compose trusted markup while escaping every ordinary interpolated value. */
34
+ export function html(
35
+ strings: TemplateStringsArray,
36
+ ...values: readonly HtmlValue[]
37
+ ): HtmlFragment {
38
+ let output = strings[0] ?? ""
39
+ for (let index = 0; index < values.length; index++) {
40
+ output += interpolate(values[index]) + (strings[index + 1] ?? "")
41
+ }
42
+ return fragment(output)
43
+ }
44
+
45
+ /** Normalize plugin HTML at the private rendering boundary. */
46
+ export function stringifyHtml(value: Html): string {
47
+ return typeof value === "string" ? value : value[HTML_FRAGMENT]
48
+ }
package/icons/LICENSE ADDED
@@ -0,0 +1,43 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Lucide Icons and Contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ ---
18
+
19
+ The following Lucide icons are derived from the Feather project:
20
+
21
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
22
+
23
+ The MIT License (MIT) (for the icons listed above)
24
+
25
+ Copyright (c) 2013-present Cole Bemis
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to deal
29
+ in the Software without restriction, including without limitation the rights
30
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all
35
+ copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
+ SOFTWARE.
@@ -0,0 +1,54 @@
1
+ /** Generated by scripts/generate-lucide-icons.ts from @lucide/icons v1.34.0. */
2
+ export const bugIconSvg =
3
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug" aria-hidden="true" focusable="false"><path d="M12 20v-9"></path><path d="M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z"></path><path d="M14.12 3.88 16 2"></path><path d="M21 21a4 4 0 0 0-3.81-4"></path><path d="M21 5a4 4 0 0 1-3.55 3.97"></path><path d="M22 13h-4"></path><path d="M3 21a4 4 0 0 1 3.81-4"></path><path d="M3 5a4 4 0 0 0 3.55 3.97"></path><path d="M6 13H2"></path><path d="m8 2 1.88 1.88"></path><path d="M9 7.13V6a3 3 0 1 1 6 0v1.13"></path></svg>'
4
+
5
+ export const checkIconSvg =
6
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check" aria-hidden="true" focusable="false"><path d="M20 6 9 17l-5-5"></path></svg>'
7
+
8
+ export const chevronRightIconSvg =
9
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right" aria-hidden="true" focusable="false"><path d="m9 18 6-6-6-6"></path></svg>'
10
+
11
+ export const circleCheckBigIconSvg =
12
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-big lucide-check-circle" aria-hidden="true" focusable="false"><path d="M21.801 10A10 10 0 1 1 17 3.335"></path><path d="m9 11 3 3L22 4"></path></svg>'
13
+
14
+ export const circleQuestionIconSvg =
15
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-question-mark lucide-help-circle lucide-circle-help" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><path d="M12 17h.01"></path></svg>'
16
+
17
+ export const clipboardListIconSvg =
18
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard-list" aria-hidden="true" focusable="false"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>'
19
+
20
+ export const copyIconSvg =
21
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true" focusable="false"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>'
22
+
23
+ export const flameIconSvg =
24
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame" aria-hidden="true" focusable="false"><path d="M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4"></path></svg>'
25
+
26
+ export const infoIconSvg =
27
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>'
28
+
29
+ export const listIconSvg =
30
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list" aria-hidden="true" focusable="false"><path d="M3 5h.01"></path><path d="M3 12h.01"></path><path d="M3 19h.01"></path><path d="M8 5h13"></path><path d="M8 12h13"></path><path d="M8 19h13"></path></svg>'
31
+
32
+ export const listTodoIconSvg =
33
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-todo" aria-hidden="true" focusable="false"><path d="M13 5h8"></path><path d="M13 12h8"></path><path d="M13 19h8"></path><path d="m3 17 2 2 4-4"></path><rect x="3" y="4" width="6" height="6" rx="1"></rect></svg>'
34
+
35
+ export const moonIconSvg =
36
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-moon" aria-hidden="true" focusable="false"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"></path></svg>'
37
+
38
+ export const pencilIconSvg =
39
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pencil" aria-hidden="true" focusable="false"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"></path><path d="m15 5 4 4"></path></svg>'
40
+
41
+ export const quoteIconSvg =
42
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-quote" aria-hidden="true" focusable="false"><path d="M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"></path><path d="M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"></path></svg>'
43
+
44
+ export const sunIconSvg =
45
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sun" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2"></path><path d="M12 20v2"></path><path d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path></svg>'
46
+
47
+ export const triangleAlertIconSvg =
48
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert lucide-alert-triangle" aria-hidden="true" focusable="false"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>'
49
+
50
+ export const xIconSvg =
51
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x" aria-hidden="true" focusable="false"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>'
52
+
53
+ export const zapIconSvg =
54
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap" aria-hidden="true" focusable="false"><path d="M15.914 4a1.5 1.5 0 00-2.474-1.561l-9 9A1.5 1.5 0 005.5 14h4.002a.5.5 0 01.471.666L8.086 20a1.5 1.5 0 002.475 1.56l9-9A1.5 1.5 0 0018.5 10h-3.997a.5.5 0 01-.472-.667z"></path></svg>'
package/index.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { join } from "node:path"
2
+ import { createApp, type CreateAppOptions } from "./host/create.ts"
3
+ import { bunEnvironment } from "./environments/bun.ts"
4
+ import { discoverRepositoryPlugins } from "./environments/bun-plugins.ts"
5
+ import { Log, parseLogLevel } from "./log.ts"
6
+ import { defaultPlugins } from "./plugins/index.ts"
7
+ import {
8
+ expandPluginContributions,
9
+ instantiatePlugin,
10
+ type PluginContribution,
11
+ } from "./plugins/preset.ts"
12
+
13
+ export * from "./core.ts"
14
+ export * from "./environments/bun.ts"
15
+
16
+ type PathMXOptions = Omit<CreateAppOptions, "root" | "environment"> & {
17
+ root?: string
18
+ environment?: CreateAppOptions["environment"]
19
+ }
20
+
21
+ function composeRepositoryPlugins(
22
+ configured: readonly PluginContribution[],
23
+ discovered: readonly PluginContribution[],
24
+ allowDefaultReplacement: boolean,
25
+ ) {
26
+ if (!allowDefaultReplacement) return [...configured, ...discovered]
27
+
28
+ const plugins = expandPluginContributions(configured).map(instantiatePlugin)
29
+ const replacements = new Map(
30
+ plugins.map((plugin, index) => [plugin.id, index]),
31
+ )
32
+ const repositoryIds = new Set<string>()
33
+ for (const plugin of expandPluginContributions(discovered).map(
34
+ instantiatePlugin,
35
+ )) {
36
+ if (repositoryIds.has(plugin.id)) {
37
+ throw new Error(`Duplicate repository plugin id: ${plugin.id}`)
38
+ }
39
+ repositoryIds.add(plugin.id)
40
+ const index = replacements.get(plugin.id)
41
+ if (index === undefined) plugins.push(plugin)
42
+ else plugins[index] = plugin
43
+ }
44
+ return plugins
45
+ }
46
+
47
+ export async function PathMX(
48
+ rootOrOptions: string | PathMXOptions = {},
49
+ options: PathMXOptions = {},
50
+ ) {
51
+ const merged =
52
+ typeof rootOrOptions === "string"
53
+ ? { ...options, root: rootOrOptions }
54
+ : { ...rootOrOptions, ...options }
55
+ const root = merged.root ?? join(process.cwd(), "paths")
56
+ const environment = merged.environment ?? bunEnvironment
57
+ const log =
58
+ merged.log ?? new Log({ level: parseLogLevel(process.env.PATHMX_LOG) })
59
+ const discoveredPlugins =
60
+ environment === bunEnvironment
61
+ ? await discoverRepositoryPlugins(root, log)
62
+ : []
63
+ const configuredPlugins = merged.plugins ?? defaultPlugins
64
+ return createApp({
65
+ ...merged,
66
+ root,
67
+ environment,
68
+ plugins: composeRepositoryPlugins(
69
+ configuredPlugins,
70
+ discoveredPlugins,
71
+ merged.plugins === undefined,
72
+ ),
73
+ log,
74
+ })
75
+ }
@@ -0,0 +1,85 @@
1
+ export type RouteInvalidationTarget =
2
+ | { type: "route"; route: string; actors?: string[] }
3
+ | { type: "actor"; actors: string[] }
4
+
5
+ export type RouteInvalidation = {
6
+ targets: RouteInvalidationTarget[]
7
+ }
8
+
9
+ function canonicalRoute(route: string) {
10
+ if (!route) return "/"
11
+ const rooted = route.startsWith("/") ? route : "/" + route
12
+ return rooted.length > 1 ? rooted.replace(/\/+$/, "") : rooted
13
+ }
14
+
15
+ /** Collect route invalidations, collapsing Actor-specific targets into all-Actor targets. */
16
+ export class RouteInvalidations {
17
+ private routes = new Map<string, Set<string> | undefined>()
18
+ private actors = new Set<string>()
19
+
20
+ add(route: string, actor?: string) {
21
+ const key = canonicalRoute(route)
22
+ if (this.routes.has(key) && this.routes.get(key) === undefined) return
23
+ if (!actor) {
24
+ this.routes.set(key, undefined)
25
+ return
26
+ }
27
+ const actors = this.routes.get(key) ?? new Set<string>()
28
+ actors.add(actor)
29
+ this.routes.set(key, actors)
30
+ }
31
+
32
+ addActors(route: string, actors: Iterable<string>) {
33
+ for (const actor of actors) this.add(route, actor)
34
+ }
35
+
36
+ addActorTargets(actors: Iterable<string>) {
37
+ for (const actor of actors) this.actors.add(actor)
38
+ }
39
+
40
+ event(): RouteInvalidation {
41
+ return {
42
+ targets: [
43
+ ...[...this.routes]
44
+ .sort(([a], [b]) => a.localeCompare(b))
45
+ .map(([route, actors]) => ({
46
+ type: "route" as const,
47
+ route,
48
+ ...(actors ? { actors: [...actors].sort() } : {}),
49
+ })),
50
+ ...(this.actors.size
51
+ ? [
52
+ {
53
+ type: "actor" as const,
54
+ actors: [...this.actors].sort(),
55
+ },
56
+ ]
57
+ : []),
58
+ ],
59
+ }
60
+ }
61
+ }
62
+
63
+ export function cachedRouteTargets(pages: Iterable<{ route: string }>) {
64
+ return [...pages].map((page) => ({
65
+ type: "route" as const,
66
+ route: page.route,
67
+ }))
68
+ }
69
+
70
+ export function matchesInvalidation(
71
+ event: RouteInvalidation,
72
+ route: string,
73
+ actor?: string,
74
+ ) {
75
+ const key = canonicalRoute(route)
76
+ return event.targets.some((target) => {
77
+ if (target.type === "actor") {
78
+ return actor != null && target.actors.includes(actor)
79
+ }
80
+ return (
81
+ target.route === key &&
82
+ (!target.actors || (actor != null && target.actors.includes(actor)))
83
+ )
84
+ })
85
+ }
package/log.ts ADDED
@@ -0,0 +1,84 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error"
2
+
3
+ export type LogRecord = {
4
+ time: number
5
+ level: LogLevel
6
+ message: string
7
+ context?: Record<string, unknown>
8
+ }
9
+
10
+ export type LogSink = (record: LogRecord) => void
11
+
12
+ const RANK: Record<LogLevel, number> = {
13
+ debug: 10,
14
+ info: 20,
15
+ warn: 30,
16
+ error: 40,
17
+ }
18
+
19
+ export function parseLogLevel(value: string | undefined): LogLevel {
20
+ if (
21
+ value === "debug" ||
22
+ value === "info" ||
23
+ value === "warn" ||
24
+ value === "error"
25
+ )
26
+ return value
27
+ return "info"
28
+ }
29
+
30
+ export function formatLog(record: LogRecord) {
31
+ const time = new Date(record.time).toISOString()
32
+ const extras = record.context
33
+ ? Object.entries(record.context)
34
+ .filter(([, value]) => value !== undefined)
35
+ .map(([key, value]) => `${key}=${String(value)}`)
36
+ .join(" ")
37
+ : ""
38
+ return extras
39
+ ? `${time} ${record.level} ${record.message} ${extras}`
40
+ : `${time} ${record.level} ${record.message}`
41
+ }
42
+
43
+ export function consoleSink(record: LogRecord) {
44
+ const line = formatLog(record)
45
+ if (record.level === "error") console.error(line)
46
+ else if (record.level === "warn") console.warn(line)
47
+ else console.log(line)
48
+ }
49
+
50
+ export class Log {
51
+ level: LogLevel
52
+ sink: LogSink
53
+ records: LogRecord[] = []
54
+ limit: number
55
+
56
+ constructor(options?: { level?: LogLevel; sink?: LogSink; limit?: number }) {
57
+ this.level = options?.level ?? "info"
58
+ this.sink = options?.sink ?? consoleSink
59
+ this.limit = options?.limit ?? 1000
60
+ }
61
+
62
+ write(level: LogLevel, message: string, context?: Record<string, unknown>) {
63
+ const record: LogRecord = { time: Date.now(), level, message, context }
64
+ this.records.push(record)
65
+ if (this.records.length > this.limit) this.records.shift()
66
+ if (RANK[level] >= RANK[this.level]) this.sink(record)
67
+ }
68
+
69
+ debug(message: string, context?: Record<string, unknown>) {
70
+ this.write("debug", message, context)
71
+ }
72
+
73
+ info(message: string, context?: Record<string, unknown>) {
74
+ this.write("info", message, context)
75
+ }
76
+
77
+ warn(message: string, context?: Record<string, unknown>) {
78
+ this.write("warn", message, context)
79
+ }
80
+
81
+ error(message: string, context?: Record<string, unknown>) {
82
+ this.write("error", message, context)
83
+ }
84
+ }