@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,61 @@
1
+ import type { BundledAsset, PathMXEnvironment } from "./environment.ts"
2
+ import { documentAssetRank } from "./document.ts"
3
+
4
+ type BrowserBundleHost = Pick<PathMXEnvironment, "bundle">
5
+
6
+ type BrowserDocumentAsset =
7
+ | Readonly<{
8
+ type: "stylesheet"
9
+ name: string
10
+ text: string
11
+ }>
12
+ | Readonly<{
13
+ type: "script"
14
+ name: string
15
+ text: string
16
+ module: true
17
+ }>
18
+
19
+ function documentAsset(
20
+ stem: string,
21
+ output: BundledAsset,
22
+ ): BrowserDocumentAsset {
23
+ if (output.type === "file") {
24
+ throw new Error(
25
+ `Browser bundle ${stem} emitted unsupported file asset ${output.path}.`,
26
+ )
27
+ }
28
+ return output.type === "style"
29
+ ? {
30
+ type: "stylesheet",
31
+ name: `${stem}.css`,
32
+ text: output.text,
33
+ }
34
+ : {
35
+ type: "script",
36
+ name: `${stem}.js`,
37
+ text: output.text,
38
+ module: true,
39
+ }
40
+ }
41
+
42
+ /** @internal Bundle one browser entrypoint into ordered document assets. */
43
+ export async function browserBundleAssets(
44
+ host: BrowserBundleHost,
45
+ entrypoint: string | URL,
46
+ name: string,
47
+ ) {
48
+ const stem = name.replaceAll(":", "-")
49
+ const names = new Set<string>()
50
+ const assets = (await host.bundle(entrypoint)).map((output) => {
51
+ const asset = documentAsset(stem, output)
52
+ if (names.has(asset.name)) {
53
+ throw new Error(`Browser bundle ${stem} emitted duplicate ${asset.name}.`)
54
+ }
55
+ names.add(asset.name)
56
+ return asset
57
+ })
58
+ return assets.sort(
59
+ (left, right) => documentAssetRank(left) - documentAssetRank(right),
60
+ )
61
+ }
package/canonical.ts ADDED
@@ -0,0 +1,41 @@
1
+ export class CanonicalPathError extends Error {
2
+ constructor(value: string) {
3
+ super(`Invalid canonical path: ${value}`)
4
+ this.name = "CanonicalPathError"
5
+ }
6
+ }
7
+
8
+ export function canonicalPath(value: string) {
9
+ if (
10
+ !value.startsWith("/") ||
11
+ value.includes("\\") ||
12
+ value.includes("\0") ||
13
+ value.includes("?") ||
14
+ value.includes("#") ||
15
+ (value.length > 1 && value.endsWith("/"))
16
+ ) {
17
+ throw new CanonicalPathError(value)
18
+ }
19
+ if (value === "/") return value
20
+ const segments = value.slice(1).split("/")
21
+ if (
22
+ segments.some((segment) => !segment || segment === "." || segment === "..")
23
+ ) {
24
+ throw new CanonicalPathError(value)
25
+ }
26
+ return value
27
+ }
28
+
29
+ export function canonicalSourceId(value: string) {
30
+ const path = canonicalPath(value)
31
+ if (path === "/" || path.endsWith(".md")) throw new CanonicalPathError(value)
32
+ return path
33
+ }
34
+
35
+ export function tryCanonicalPath(value: string) {
36
+ try {
37
+ return canonicalPath(value)
38
+ } catch {
39
+ return undefined
40
+ }
41
+ }
package/cli/dev.ts ADDED
@@ -0,0 +1,134 @@
1
+ import watcher from "@parcel/watcher"
2
+ import { fileURLToPath } from "node:url"
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
4
+ import type { Command } from "commander"
5
+ import { defaultPluginsRoot } from "../environments/bun-plugins.ts"
6
+ import { fail, rootFromCommand } from "./utils.ts"
7
+ import { listenPort, startServer } from "./serve.ts"
8
+
9
+ // `pmx dev` runs in two layers. Bun restarts imported server modules; the outer
10
+ // watcher covers newly discovered and browser-only plugin files. Source edits
11
+ // stay on the in-process sync path shared by `serve` and `dev`.
12
+ const DEV_CHILD_ENV = "PMX_DEV_CHILD"
13
+
14
+ export function registerDev(program: Command) {
15
+ program
16
+ .command("dev")
17
+ .description("Start the development server (hot-restarts on code changes)")
18
+ .argument("[root]", "source root (defaults to ./paths)")
19
+ .option("-p, --port <port>", "port to listen on", listenPort)
20
+ .action(
21
+ async (
22
+ root: string | undefined,
23
+ opts: { port?: number },
24
+ cmd: Command,
25
+ ) => {
26
+ const resolved = rootFromCommand(cmd, root)
27
+ if (process.env[DEV_CHILD_ENV]) {
28
+ try {
29
+ await startServer({
30
+ root: resolved.root,
31
+ actionsRoot: resolved.actionsRoot,
32
+ port: opts.port,
33
+ verbose: true,
34
+ mode: "dev",
35
+ })
36
+ } catch (error) {
37
+ // Don't exit: `bun --watch` keeps the process alive so the server
38
+ // can come back on the next code change instead of dying for good.
39
+ console.error(error)
40
+ }
41
+ return
42
+ }
43
+ try {
44
+ await runWatched(resolved, opts.port)
45
+ } catch (error) {
46
+ fail(error)
47
+ }
48
+ },
49
+ )
50
+ }
51
+
52
+ async function runWatched(
53
+ resolved: { root: string; actionsRoot?: string },
54
+ port?: number,
55
+ ) {
56
+ const entry = fileURLToPath(new URL("./index.ts", import.meta.url))
57
+ const args = [process.execPath, "--watch", entry, "dev", resolved.root]
58
+ if (resolved.actionsRoot) args.push("--actions", resolved.actionsRoot)
59
+ if (port !== undefined) args.push("--port", String(port))
60
+
61
+ const pluginsRoot = resolve(defaultPluginsRoot(resolved.root))
62
+ const projectRoot = dirname(pluginsRoot)
63
+ let child: ReturnType<typeof Bun.spawn> | undefined
64
+ let stopped = false
65
+ let requestedGeneration = 0
66
+ let restartTimer: ReturnType<typeof setTimeout> | undefined
67
+
68
+ const spawn = () =>
69
+ Bun.spawn(args, {
70
+ env: { ...process.env, [DEV_CHILD_ENV]: "1" },
71
+ stdin: "inherit",
72
+ stdout: "inherit",
73
+ stderr: "inherit",
74
+ })
75
+
76
+ const restart = () => {
77
+ requestedGeneration++
78
+ if (restartTimer) clearTimeout(restartTimer)
79
+ restartTimer = setTimeout(() => {
80
+ restartTimer = undefined
81
+ child?.kill()
82
+ }, 75)
83
+ }
84
+
85
+ const subscription = await watcher.subscribe(
86
+ projectRoot,
87
+ (error, events) => {
88
+ if (error) {
89
+ console.error(error)
90
+ return
91
+ }
92
+ if (events.some((event) => isPluginEvent(pluginsRoot, event.path))) {
93
+ restart()
94
+ }
95
+ },
96
+ {
97
+ ignore: [
98
+ resolve(resolved.root),
99
+ join(projectRoot, ".git"),
100
+ join(projectRoot, "node_modules"),
101
+ ],
102
+ },
103
+ )
104
+
105
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
106
+ process.on(signal, () => {
107
+ stopped = true
108
+ if (restartTimer) clearTimeout(restartTimer)
109
+ child?.kill(signal)
110
+ })
111
+ }
112
+
113
+ let exitCode = 0
114
+ while (!stopped) {
115
+ const generation = requestedGeneration
116
+ child = spawn()
117
+ exitCode = await child.exited
118
+ child = undefined
119
+ if (requestedGeneration === generation) break
120
+ }
121
+ await subscription.unsubscribe()
122
+ process.exit(exitCode)
123
+ }
124
+
125
+ function isPluginEvent(root: string, path: string) {
126
+ const file = relative(root, path)
127
+ return (
128
+ !!file &&
129
+ !isAbsolute(file) &&
130
+ file !== ".." &&
131
+ !file.startsWith(`..${sep}`) &&
132
+ !file.split(/[\\/]/).includes("node_modules")
133
+ )
134
+ }
package/cli/export.ts ADDED
@@ -0,0 +1,74 @@
1
+ import type { Command } from "commander"
2
+ import { isAbsolute, relative, resolve } from "node:path"
3
+ import { publishToDirectory } from "../environments/bun-publish.ts"
4
+ import { PathMX } from "../index.ts"
5
+ import { projectDirectory } from "../project.ts"
6
+ import { createPublication } from "../publish/build.ts"
7
+ import { fail, rootFromCommand } from "./utils.ts"
8
+
9
+ function contains(parent: string, child: string) {
10
+ const path = relative(parent, child)
11
+ return path === "" || (!path.startsWith("..") && !isAbsolute(path))
12
+ }
13
+
14
+ function overlaps(left: string, right: string) {
15
+ return contains(left, right) || contains(right, left)
16
+ }
17
+
18
+ function outputPath(root: string, explicit?: string) {
19
+ const output = resolve(explicit ?? projectDirectory(root, "dist"))
20
+ if (overlaps(output, resolve(root))) {
21
+ throw new Error(
22
+ "Export output must not contain or be inside the paths root.",
23
+ )
24
+ }
25
+ return output
26
+ }
27
+
28
+ export function registerExport(program: Command) {
29
+ program
30
+ .command("export")
31
+ .description("Export the public application as a static publication")
32
+ .argument("[root]", "source root (defaults to ./paths)")
33
+ .requiredOption("--origin <url>", "canonical public HTTP(S) origin")
34
+ .option("--base-path <path>", "public mount path (defaults to /)")
35
+ .option("-o, --out <path>", "output directory (defaults beside paths)")
36
+ .action(
37
+ async (
38
+ root: string | undefined,
39
+ options: { origin: string; basePath?: string; out?: string },
40
+ command: Command,
41
+ ) => {
42
+ let app
43
+ let failed = false
44
+ let failure: unknown
45
+ try {
46
+ const resolved = rootFromCommand(command, root)
47
+ const output = outputPath(resolved.root, options.out)
48
+ const exportingApp = await PathMX(resolved.root, {
49
+ actionsRoot: resolved.actionsRoot,
50
+ diagnostics: "throw",
51
+ runtime: { mode: "production", target: "static" },
52
+ })
53
+ app = exportingApp
54
+ const manifest = await publishToDirectory(output, async (store) => {
55
+ const publication = await createPublication(exportingApp, {
56
+ origin: options.origin,
57
+ basePath: options.basePath,
58
+ store,
59
+ })
60
+ await exportingApp.close()
61
+ app = undefined
62
+ return publication
63
+ })
64
+ console.log(`Exported ${manifest.routes.length} routes to ${output}`)
65
+ } catch (error) {
66
+ failed = true
67
+ failure = error
68
+ } finally {
69
+ await app?.close().catch(() => undefined)
70
+ }
71
+ if (failed) fail(failure)
72
+ },
73
+ )
74
+ }
package/cli/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander"
3
+ import { registerDev } from "./dev.ts"
4
+ import { registerExport } from "./export.ts"
5
+ import { registerLint } from "./lint.ts"
6
+ import { registerMv } from "./mv.ts"
7
+ import { registerPerms } from "./perms.ts"
8
+ import { registerRm } from "./rm.ts"
9
+ import { registerServe } from "./serve.ts"
10
+ import { registerTree } from "./tree.ts"
11
+ import { registerPluginCommands } from "./plugins.ts"
12
+ import { registerPluginPackages } from "./plugin-packages.ts"
13
+ import { registerQuery } from "./query.ts"
14
+ import { checkBunVersion, release } from "./release.ts"
15
+ import { registerUpdate } from "./update.ts"
16
+
17
+ export function createProgram() {
18
+ const program = new Command()
19
+ .name("pathmx")
20
+ .description("Manage and serve PathMX projects")
21
+ .version(release.version)
22
+ .option("-r, --root <path>", "source root (defaults to ./paths)")
23
+ .option("-a, --actions <path>", "actions root")
24
+
25
+ registerServe(program)
26
+ registerDev(program)
27
+ registerExport(program)
28
+ registerMv(program)
29
+ registerRm(program)
30
+ registerTree(program)
31
+ registerPerms(program)
32
+ registerLint(program)
33
+ registerQuery(program)
34
+ registerPluginPackages(program)
35
+ registerUpdate(program)
36
+ return program
37
+ }
38
+
39
+ export async function runCli(argv = process.argv) {
40
+ checkBunVersion()
41
+ const program = createProgram()
42
+ const close = await registerPluginCommands(program, argv)
43
+ try {
44
+ await program.parseAsync(argv)
45
+ } finally {
46
+ await close()
47
+ }
48
+ }
49
+
50
+ if (import.meta.main) {
51
+ await runCli()
52
+ }
package/cli/lint.ts ADDED
@@ -0,0 +1,164 @@
1
+ import type { Command } from "commander"
2
+ import pc from "picocolors"
3
+ import type { Engine } from "../host/engine.ts"
4
+ import { Log } from "../log.ts"
5
+ import { SourceDiagnosticError } from "../source-diagnostic.ts"
6
+ import { fail, loadApp, rootFromCommand } from "./utils.ts"
7
+
8
+ export type UnresolvedLinkIssue = Readonly<{
9
+ type: "link.unresolved"
10
+ source: string
11
+ href: string
12
+ label?: string
13
+ }>
14
+
15
+ export type InvalidSourceIssue = Readonly<{
16
+ type: "source.invalid"
17
+ source: string
18
+ plugin: string
19
+ message: string
20
+ }>
21
+
22
+ export type LintIssue = UnresolvedLinkIssue | InvalidSourceIssue
23
+
24
+ export function collectUnresolved(app: Engine) {
25
+ const issues: UnresolvedLinkIssue[] = []
26
+ for (const edge of app.graph.unresolvedLinks()) {
27
+ const source = app.repo.atPath(edge.from)
28
+ const link = source?.links.find((item) => item.href === edge.href)
29
+ issues.push({
30
+ type: "link.unresolved",
31
+ source: edge.from,
32
+ href: edge.href,
33
+ ...(link?.label ? { label: link.label } : {}),
34
+ })
35
+ }
36
+ return issues
37
+ }
38
+
39
+ function issueKey(issue: LintIssue) {
40
+ return issue.type === "link.unresolved"
41
+ ? `${issue.type}\0${issue.source}\0${issue.href}\0${issue.label ?? ""}`
42
+ : `${issue.type}\0${issue.source}\0${issue.plugin}\0${issue.message}`
43
+ }
44
+
45
+ function compareIssues(a: LintIssue, b: LintIssue) {
46
+ return (
47
+ a.type.localeCompare(b.type) ||
48
+ a.source.localeCompare(b.source) ||
49
+ issueKey(a).localeCompare(issueKey(b))
50
+ )
51
+ }
52
+
53
+ function linkText(issue: UnresolvedLinkIssue) {
54
+ const link = issue.label ? `[${issue.label}](${issue.href})` : issue.href
55
+ return `${issue.source}: ${link}`
56
+ }
57
+
58
+ export function formatLintIssues(issues: readonly LintIssue[]) {
59
+ if (!issues.length) return pc.green("No issues found")
60
+
61
+ const links: UnresolvedLinkIssue[] = []
62
+ const authoring: InvalidSourceIssue[] = []
63
+ for (const issue of issues) {
64
+ switch (issue.type) {
65
+ case "link.unresolved":
66
+ links.push(issue)
67
+ break
68
+ case "source.invalid":
69
+ authoring.push(issue)
70
+ break
71
+ default:
72
+ issue satisfies never
73
+ }
74
+ }
75
+ const sections: string[] = []
76
+ if (links.length) {
77
+ sections.push(
78
+ [
79
+ pc.bold(`Unresolved links (${links.length})`),
80
+ ...links.map(linkText),
81
+ ].join("\n"),
82
+ )
83
+ }
84
+ if (authoring.length) {
85
+ sections.push(
86
+ [
87
+ pc.bold(`Authoring issues (${authoring.length})`),
88
+ ...authoring.map((issue) => issue.message),
89
+ ].join("\n"),
90
+ )
91
+ }
92
+
93
+ const noun = issues.length === 1 ? "issue" : "issues"
94
+ sections.push(pc.red(`${issues.length} ${noun} found`))
95
+ return sections.join("\n\n")
96
+ }
97
+
98
+ export async function collectLintIssues(app: Engine) {
99
+ const issues = new Map<string, LintIssue>(
100
+ collectUnresolved(app).map((issue) => [issueKey(issue), issue]),
101
+ )
102
+ const addDiagnostic = (diagnostic: {
103
+ source: string
104
+ plugin: string
105
+ message: string
106
+ }) => {
107
+ const issue: InvalidSourceIssue = {
108
+ type: "source.invalid",
109
+ ...diagnostic,
110
+ }
111
+ issues.set(issueKey(issue), issue)
112
+ }
113
+ for (const diagnostic of app.sourceDiagnostics()) {
114
+ addDiagnostic(diagnostic)
115
+ }
116
+ for (const source of app.repo.all()) {
117
+ if (!app.isPage(source)) continue
118
+ try {
119
+ await app.compilePage(source)
120
+ } catch (error) {
121
+ if (!(error instanceof SourceDiagnosticError)) throw error
122
+ addDiagnostic(error.diagnostic)
123
+ }
124
+ }
125
+ for (const plugin of app.plugins) {
126
+ if (!plugin.lint) continue
127
+ await plugin.lint((source, message) => {
128
+ const normalized = message.trim()
129
+ if (!normalized) {
130
+ throw new Error(`Plugin ${plugin.id} reported an empty lint message.`)
131
+ }
132
+ addDiagnostic({
133
+ source: source.path,
134
+ plugin: plugin.id,
135
+ message: normalized,
136
+ })
137
+ }, app.appContext(plugin.id))
138
+ }
139
+ return [...issues.values()].sort(compareIssues)
140
+ }
141
+
142
+ export function registerLint(program: Command) {
143
+ program
144
+ .command("lint")
145
+ .description("Validate authored Sources without starting a server")
146
+ .argument("[root]", "source root (defaults to ./paths)")
147
+ .action(async (root: string | undefined, _opts: unknown, cmd: Command) => {
148
+ try {
149
+ const resolved = rootFromCommand(cmd, root)
150
+ const app = await loadApp(resolved.root, {
151
+ actionsRoot: resolved.actionsRoot,
152
+ buildCache: false,
153
+ diagnostics: "collect",
154
+ log: new Log({ sink() {} }),
155
+ })
156
+ const issues = await collectLintIssues(app)
157
+ console.log(formatLintIssues(issues))
158
+ if (!issues.length) return
159
+ process.exitCode = 1
160
+ } catch (error) {
161
+ fail(error)
162
+ }
163
+ })
164
+ }
package/cli/mv.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { Command } from "commander"
2
+ import { applySourceOpPlan, moveSource } from "../ops/index.ts"
3
+ import { fail, loadApp, printPlan, rootFromCommand } from "./utils.ts"
4
+
5
+ export function registerMv(program: Command) {
6
+ program
7
+ .command("mv")
8
+ .description("Move a source and rewrite linked references")
9
+ .argument("<old-path>", "existing source path")
10
+ .argument("<new-path>", "destination markdown path")
11
+ .option("--dry-run", "print the plan without writing")
12
+ .action(
13
+ async (
14
+ oldPath: string,
15
+ newPath: string,
16
+ opts: { dryRun?: boolean },
17
+ cmd: Command,
18
+ ) => {
19
+ try {
20
+ const { root, actionsRoot } = rootFromCommand(cmd)
21
+ const app = await loadApp(root, { actionsRoot, buildCache: false })
22
+ const plan = await moveSource(app, oldPath, newPath)
23
+ printPlan(plan, Boolean(opts.dryRun))
24
+ if (!opts.dryRun) await applySourceOpPlan(app, plan)
25
+ } catch (error) {
26
+ fail(error)
27
+ }
28
+ },
29
+ )
30
+ }