@pathmx/core 0.5.1 → 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 +4 -0
- package/cli/lint.ts +50 -8
- package/cli/update.ts +47 -5
- package/environments/bun.ts +5 -3
- package/host/engine.ts +7 -0
- package/ops/href.ts +0 -11
- package/ops/move-source.ts +5 -3
- package/package.json +1 -1
- package/server/watch.ts +8 -1
- package/source/source.ts +0 -1
- package/storage-path.ts +8 -0
package/README.md
CHANGED
|
@@ -55,6 +55,10 @@ const app = await createApp({
|
|
|
55
55
|
})
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
Routes and Source identities are file-based. A Source-level frontmatter `id`
|
|
59
|
+
is ignored and reported as a non-failing `pmx lint` warning. Block-level ids
|
|
60
|
+
remain authorable for stable local targets.
|
|
61
|
+
|
|
58
62
|
Official packages follow the CLI's release channel:
|
|
59
63
|
|
|
60
64
|
```sh
|
package/cli/lint.ts
CHANGED
|
@@ -19,7 +19,13 @@ export type InvalidSourceIssue = Readonly<{
|
|
|
19
19
|
message: string
|
|
20
20
|
}>
|
|
21
21
|
|
|
22
|
-
export type
|
|
22
|
+
export type SourceWarning = Readonly<{
|
|
23
|
+
type: "source.warning"
|
|
24
|
+
source: string
|
|
25
|
+
message: string
|
|
26
|
+
}>
|
|
27
|
+
|
|
28
|
+
export type LintIssue = UnresolvedLinkIssue | InvalidSourceIssue | SourceWarning
|
|
23
29
|
|
|
24
30
|
export function collectUnresolved(app: Engine) {
|
|
25
31
|
const issues: UnresolvedLinkIssue[] = []
|
|
@@ -37,9 +43,16 @@ export function collectUnresolved(app: Engine) {
|
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
function issueKey(issue: LintIssue) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
switch (issue.type) {
|
|
47
|
+
case "link.unresolved":
|
|
48
|
+
return `${issue.type}\0${issue.source}\0${issue.href}\0${issue.label ?? ""}`
|
|
49
|
+
case "source.invalid":
|
|
50
|
+
return `${issue.type}\0${issue.source}\0${issue.plugin}\0${issue.message}`
|
|
51
|
+
case "source.warning":
|
|
52
|
+
return `${issue.type}\0${issue.source}\0${issue.message}`
|
|
53
|
+
default:
|
|
54
|
+
return issue satisfies never
|
|
55
|
+
}
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
function compareIssues(a: LintIssue, b: LintIssue) {
|
|
@@ -60,6 +73,7 @@ export function formatLintIssues(issues: readonly LintIssue[]) {
|
|
|
60
73
|
|
|
61
74
|
const links: UnresolvedLinkIssue[] = []
|
|
62
75
|
const authoring: InvalidSourceIssue[] = []
|
|
76
|
+
const warnings: SourceWarning[] = []
|
|
63
77
|
for (const issue of issues) {
|
|
64
78
|
switch (issue.type) {
|
|
65
79
|
case "link.unresolved":
|
|
@@ -68,6 +82,9 @@ export function formatLintIssues(issues: readonly LintIssue[]) {
|
|
|
68
82
|
case "source.invalid":
|
|
69
83
|
authoring.push(issue)
|
|
70
84
|
break
|
|
85
|
+
case "source.warning":
|
|
86
|
+
warnings.push(issue)
|
|
87
|
+
break
|
|
71
88
|
default:
|
|
72
89
|
issue satisfies never
|
|
73
90
|
}
|
|
@@ -89,9 +106,24 @@ export function formatLintIssues(issues: readonly LintIssue[]) {
|
|
|
89
106
|
].join("\n"),
|
|
90
107
|
)
|
|
91
108
|
}
|
|
109
|
+
if (warnings.length) {
|
|
110
|
+
sections.push(
|
|
111
|
+
[
|
|
112
|
+
pc.bold(`Warnings (${warnings.length})`),
|
|
113
|
+
...warnings.map((issue) => `${issue.source}: ${issue.message}`),
|
|
114
|
+
].join("\n"),
|
|
115
|
+
)
|
|
116
|
+
}
|
|
92
117
|
|
|
93
|
-
const
|
|
94
|
-
|
|
118
|
+
const failureCount = links.length + authoring.length
|
|
119
|
+
if (failureCount) {
|
|
120
|
+
const noun = failureCount === 1 ? "issue" : "issues"
|
|
121
|
+
sections.push(pc.red(`${failureCount} ${noun} found`))
|
|
122
|
+
}
|
|
123
|
+
if (warnings.length) {
|
|
124
|
+
const noun = warnings.length === 1 ? "warning" : "warnings"
|
|
125
|
+
sections.push(pc.yellow(`${warnings.length} ${noun} found`))
|
|
126
|
+
}
|
|
95
127
|
return sections.join("\n\n")
|
|
96
128
|
}
|
|
97
129
|
|
|
@@ -114,6 +146,15 @@ export async function collectLintIssues(app: Engine) {
|
|
|
114
146
|
addDiagnostic(diagnostic)
|
|
115
147
|
}
|
|
116
148
|
for (const source of app.repo.all()) {
|
|
149
|
+
if (Object.hasOwn(source.data, "id")) {
|
|
150
|
+
const warning: SourceWarning = {
|
|
151
|
+
type: "source.warning",
|
|
152
|
+
source: source.path,
|
|
153
|
+
message:
|
|
154
|
+
'Source frontmatter "id" is ignored; Source ids are derived from file paths. Remove this field.',
|
|
155
|
+
}
|
|
156
|
+
issues.set(issueKey(warning), warning)
|
|
157
|
+
}
|
|
117
158
|
if (!app.isPage(source)) continue
|
|
118
159
|
try {
|
|
119
160
|
await app.compilePage(source)
|
|
@@ -155,8 +196,9 @@ export function registerLint(program: Command) {
|
|
|
155
196
|
})
|
|
156
197
|
const issues = await collectLintIssues(app)
|
|
157
198
|
console.log(formatLintIssues(issues))
|
|
158
|
-
if (
|
|
159
|
-
|
|
199
|
+
if (issues.some((issue) => issue.type !== "source.warning")) {
|
|
200
|
+
process.exitCode = 1
|
|
201
|
+
}
|
|
160
202
|
} catch (error) {
|
|
161
203
|
fail(error)
|
|
162
204
|
}
|
package/cli/update.ts
CHANGED
|
@@ -35,15 +35,52 @@ export async function availableRelease(
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async function installRelease(spec: string) {
|
|
38
|
-
const child = Bun.spawn(
|
|
38
|
+
const child = Bun.spawn(
|
|
39
|
+
[process.execPath, "add", "--global", "--exact", spec],
|
|
40
|
+
{
|
|
41
|
+
stdin: "inherit",
|
|
42
|
+
stdout: "inherit",
|
|
43
|
+
stderr: "inherit",
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
return child.exited
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function globalPackageVersion(output: string, packageName: string) {
|
|
50
|
+
const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
51
|
+
return output.match(new RegExp(`${escaped}@([^\\s]+)`))?.[1]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function installedGlobalVersion() {
|
|
55
|
+
const child = Bun.spawn([process.execPath, "pm", "ls", "--global"], {
|
|
39
56
|
stdin: "inherit",
|
|
40
|
-
stdout: "
|
|
41
|
-
stderr: "
|
|
57
|
+
stdout: "pipe",
|
|
58
|
+
stderr: "pipe",
|
|
42
59
|
})
|
|
43
|
-
const exitCode = await
|
|
60
|
+
const [stdout, exitCode] = await Promise.all([
|
|
61
|
+
new Response(child.stdout).text(),
|
|
62
|
+
child.exited,
|
|
63
|
+
])
|
|
64
|
+
if (exitCode !== 0) return
|
|
65
|
+
return globalPackageVersion(stdout, release.package)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type InstallRelease = (spec: string) => Promise<number>
|
|
69
|
+
type InstalledVersion = () => Promise<string | undefined>
|
|
70
|
+
|
|
71
|
+
export async function installCheckedRelease(
|
|
72
|
+
spec: string,
|
|
73
|
+
expected: string,
|
|
74
|
+
install: InstallRelease = installRelease,
|
|
75
|
+
installed: InstalledVersion = installedGlobalVersion,
|
|
76
|
+
) {
|
|
77
|
+
const exitCode = await install(spec)
|
|
44
78
|
if (exitCode !== 0) {
|
|
79
|
+
if ((await installed()) === expected)
|
|
80
|
+
return { type: "installed-with-warning" as const, exitCode }
|
|
45
81
|
throw new Error(`Bun could not install ${spec} (exit ${exitCode}).`)
|
|
46
82
|
}
|
|
83
|
+
return { type: "installed" as const }
|
|
47
84
|
}
|
|
48
85
|
|
|
49
86
|
export function registerUpdate(program: Command) {
|
|
@@ -70,7 +107,12 @@ export function registerUpdate(program: Command) {
|
|
|
70
107
|
console.log(
|
|
71
108
|
`Updating ${release.package} ${release.version} → ${available}`,
|
|
72
109
|
)
|
|
73
|
-
await
|
|
110
|
+
const result = await installCheckedRelease(releaseSpec(), available)
|
|
111
|
+
if (result.type === "installed-with-warning") {
|
|
112
|
+
console.warn(
|
|
113
|
+
`warning: Bun exited with ${result.exitCode}, but ${release.package}@${available} was installed. Check your Bun global package manifest for an unrelated invalid dependency.`,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
74
116
|
console.log(
|
|
75
117
|
`PathMX ${available} installed. Run \`pathmx --version\` to verify.`,
|
|
76
118
|
)
|
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) => {
|
package/host/engine.ts
CHANGED
|
@@ -434,6 +434,13 @@ export class Engine {
|
|
|
434
434
|
"index.source",
|
|
435
435
|
() => {
|
|
436
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
|
+
}
|
|
437
444
|
this.compiler.indexSource(source)
|
|
438
445
|
this.authorityErrors.delete(source.id)
|
|
439
446
|
const capture = (error: unknown) => {
|
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
package/server/watch.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { realpathSync } from "node:fs"
|
|
|
3
3
|
import { relative } from "node:path"
|
|
4
4
|
import type { Engine } from "../host/engine.ts"
|
|
5
5
|
import { SourceDiagnosticError } from "../source-diagnostic.ts"
|
|
6
|
+
import { isAuthoredStoragePath } from "../storage-path.ts"
|
|
6
7
|
import { CssImportIndex } from "./css-imports.ts"
|
|
7
8
|
|
|
8
9
|
type UnresolvedLink = {
|
|
@@ -91,7 +92,13 @@ export function watchPaths(app: Engine, root: string) {
|
|
|
91
92
|
const changedStyles = new Set<string>()
|
|
92
93
|
for (const event of events) {
|
|
93
94
|
const rel = relative(base, event.path)
|
|
94
|
-
if (
|
|
95
|
+
if (
|
|
96
|
+
!rel ||
|
|
97
|
+
rel.startsWith("..") ||
|
|
98
|
+
!isAuthoredStoragePath(rel)
|
|
99
|
+
) {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
95
102
|
app.log.debug("watch", { file: rel, type: event.type })
|
|
96
103
|
if (rel.endsWith(".md")) await app.syncFromStorage(rel)
|
|
97
104
|
else if (rel.endsWith(".css")) {
|
package/source/source.ts
CHANGED
|
@@ -49,7 +49,6 @@ export function sourceIdFromPath(path: string, type = DEFAULT_SOURCE_TYPE) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function resolveSourceId(path: string, data: Topmatter) {
|
|
52
|
-
if (typeof data.id === "string" && data.id.trim()) return data.id.trim()
|
|
53
52
|
const type =
|
|
54
53
|
typeof data.type === "string" && data.type.trim()
|
|
55
54
|
? data.type.trim()
|
package/storage-path.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const CONTROL_DIRECTORIES = new Set([".git", ".pathmx", "node_modules"])
|
|
2
|
+
|
|
3
|
+
/** True when a storage-relative path belongs to authored project content. */
|
|
4
|
+
export function isAuthoredStoragePath(path: string) {
|
|
5
|
+
return path
|
|
6
|
+
.split(/[\\/]/)
|
|
7
|
+
.every((segment) => !CONTROL_DIRECTORIES.has(segment))
|
|
8
|
+
}
|