@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
package/ops/href.ts ADDED
@@ -0,0 +1,87 @@
1
+ import { posixPath } from "../path.ts"
2
+ import { publicPath } from "../server/router.ts"
3
+ import type { Source } from "../source/index.ts"
4
+
5
+ export function splitHref(href: string) {
6
+ const hash = href.indexOf("#")
7
+ const withoutHash = hash === -1 ? href : href.slice(0, hash)
8
+ const query = withoutHash.indexOf("?")
9
+ return {
10
+ pathname: query === -1 ? withoutHash : withoutHash.slice(0, query),
11
+ query: query === -1 ? "" : withoutHash.slice(query),
12
+ fragment: hash === -1 ? "" : href.slice(hash),
13
+ }
14
+ }
15
+
16
+ export function joinHref(pathname: string, query: string, fragment: string) {
17
+ return `${pathname}${query}${fragment}`
18
+ }
19
+
20
+ export function isRelativeHref(pathname: string) {
21
+ return (
22
+ Boolean(pathname) && !pathname.startsWith("/") && !pathname.startsWith("~")
23
+ )
24
+ }
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
+ export function relativeHref(
37
+ fromPath: string,
38
+ targetPath: string,
39
+ originalHref: string,
40
+ ) {
41
+ const fromDir = posixPath.dirname(fromPath)
42
+ let rel = posixPath.relative(fromDir, targetPath)
43
+ if (!rel) rel = posixPath.basename(targetPath)
44
+ if (
45
+ (originalHref.startsWith("./") &&
46
+ !rel.startsWith("../") &&
47
+ !rel.startsWith("/")) ||
48
+ (!rel.startsWith(".") && !rel.startsWith("/"))
49
+ ) {
50
+ rel = `./${rel}`
51
+ }
52
+ return rel
53
+ }
54
+
55
+ export function rebaseRelativeHref(
56
+ href: string,
57
+ oldDir: string,
58
+ newDir: string,
59
+ ) {
60
+ const { pathname, query, fragment } = splitHref(href)
61
+ if (!isRelativeHref(pathname)) return href
62
+ const oldTarget = posixPath.normalize(posixPath.join(oldDir, pathname))
63
+ return joinHref(
64
+ relativeHref(posixPath.join(newDir, "_"), oldTarget, href),
65
+ query,
66
+ fragment,
67
+ )
68
+ }
69
+
70
+ export function retargetHref(
71
+ href: string,
72
+ fromPath: string,
73
+ oldSource: Source,
74
+ newSource: Source,
75
+ ): string | undefined {
76
+ const { pathname, query, fragment } = splitHref(href)
77
+ if (!pathname) return
78
+ if (isAuthoredIdHref(pathname, oldSource)) return
79
+
80
+ const explicitMd = pathname.endsWith(".md")
81
+ const target = explicitMd ? newSource.path : publicPath(newSource)
82
+ const absTarget = target.startsWith("/") ? target : `/${target}`
83
+ const next = isRelativeHref(pathname)
84
+ ? relativeHref(fromPath, absTarget, href)
85
+ : target
86
+ return joinHref(next, query, fragment)
87
+ }
package/ops/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export {
2
+ applySourceOpPlan,
3
+ emptyPlan,
4
+ SourceOpError,
5
+ type SourceOpPlan,
6
+ type SourceOpWrite,
7
+ } from "./plan.ts"
8
+ export { moveSource } from "./move-source.ts"
9
+ export { removeSource } from "./remove-source.ts"
@@ -0,0 +1,114 @@
1
+ import type { Engine } from "../host/engine.ts"
2
+ import { posixPath } from "../path.ts"
3
+ import { isIndexSource, publicPath } from "../server/router.ts"
4
+ import { createSource, rewriteLinks, type Source } from "../source/index.ts"
5
+ import {
6
+ authoredId,
7
+ isRelativeHref,
8
+ rebaseRelativeHref,
9
+ retargetHref,
10
+ splitHref,
11
+ } from "./href.ts"
12
+ import { emptyPlan, SourceOpError, type SourceOpPlan } from "./plan.ts"
13
+
14
+ function resolveExisting(app: Engine, input: string): Source {
15
+ const graphPath = app.repo.graphPath(input)
16
+ const source =
17
+ app.repo.atPath(graphPath) ??
18
+ app.repo.get(input) ??
19
+ app.repo.get(app.repo.pathToId(graphPath))
20
+ if (!source) throw new SourceOpError(`Source not found: ${input}`)
21
+ if (!source.path.endsWith(".md")) {
22
+ throw new SourceOpError(`Only markdown sources can be moved: ${input}`)
23
+ }
24
+ return source
25
+ }
26
+
27
+ async function resolveDest(app: Engine, input: string) {
28
+ const path = app.repo.graphPath(input)
29
+ if (!path.endsWith(".md")) {
30
+ throw new SourceOpError(`Destination must be a markdown path: ${input}`)
31
+ }
32
+ const storagePath = app.repo.storagePath(path)
33
+ if (!storagePath)
34
+ throw new SourceOpError(`Destination escapes the source root: ${input}`)
35
+ if (
36
+ app.repo.atPath(path) ||
37
+ (await app.environment.storage.exists(storagePath))
38
+ ) {
39
+ throw new SourceOpError(`Destination already exists: ${path}`)
40
+ }
41
+ return path
42
+ }
43
+
44
+ function referrerPath(from: string) {
45
+ const hash = from.indexOf("#")
46
+ return hash === -1 ? from : from.slice(0, hash)
47
+ }
48
+
49
+ export async function moveSource(
50
+ app: Engine,
51
+ from: string,
52
+ to: string,
53
+ ): Promise<SourceOpPlan> {
54
+ const source = resolveExisting(app, from)
55
+ const dest = await resolveDest(app, to)
56
+ if (posixPath.normalize(source.path) === posixPath.normalize(dest)) {
57
+ throw new SourceOpError(`Source and destination are the same path: ${dest}`)
58
+ }
59
+
60
+ const preview = createSource(dest, source.content, app.environment.yaml)
61
+ const oldDir = posixPath.dirname(source.path)
62
+ const newDir = posixPath.dirname(dest)
63
+ const selfHrefs = new Set(
64
+ app.graph
65
+ .linksFrom(source.path)
66
+ .filter((edge) => edge.to === source.path)
67
+ .map((edge) => edge.href),
68
+ )
69
+
70
+ const movedContent = rewriteLinks(source.content, (href) => {
71
+ if (selfHrefs.has(href)) return retargetHref(href, dest, source, preview)
72
+ const { pathname } = splitHref(href)
73
+ if (isRelativeHref(pathname))
74
+ return rebaseRelativeHref(href, oldDir, newDir)
75
+ })
76
+
77
+ const plan = emptyPlan()
78
+ plan.writes.push({ path: dest, content: movedContent })
79
+ plan.deletes.push(source.path)
80
+
81
+ const inbound = app.graph.linksTo(source.path)
82
+ const byFile = new Map<string, Set<string>>()
83
+ for (const edge of inbound) {
84
+ const file = referrerPath(edge.from)
85
+ if (file === source.path) continue
86
+ const hrefs = byFile.get(file) ?? new Set()
87
+ hrefs.add(edge.href)
88
+ byFile.set(file, hrefs)
89
+ }
90
+
91
+ for (const [file, hrefs] of byFile) {
92
+ const referrer = app.repo.atPath(file)
93
+ if (!referrer) continue
94
+ const next = rewriteLinks(referrer.content, (href) => {
95
+ if (!hrefs.has(href)) return
96
+ return retargetHref(href, file, source, preview)
97
+ })
98
+ if (next !== referrer.content)
99
+ plan.writes.push({ path: file, content: next })
100
+ }
101
+
102
+ if (isIndexSource(source)) {
103
+ plan.warnings.push(
104
+ `Moving index source ${source.path} changes the directory route ${publicPath(source)}`,
105
+ )
106
+ }
107
+ if (!authoredId(source) && source.id !== preview.id) {
108
+ plan.warnings.push(
109
+ `Moving ${source.path} changes its public URL from ${source.id} to ${preview.id}`,
110
+ )
111
+ }
112
+
113
+ return plan
114
+ }
package/ops/plan.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { Engine } from "../host/engine.ts"
2
+ import type { SourceChange } from "../repository.ts"
3
+
4
+ export type SourceOpWrite = {
5
+ path: string
6
+ content: string
7
+ }
8
+
9
+ export type SourceOpPlan = {
10
+ writes: SourceOpWrite[]
11
+ deletes: string[]
12
+ warnings: string[]
13
+ }
14
+
15
+ export class SourceOpError extends Error {
16
+ constructor(message: string) {
17
+ super(message)
18
+ this.name = "SourceOpError"
19
+ }
20
+ }
21
+
22
+ export function emptyPlan(): SourceOpPlan {
23
+ return { writes: [], deletes: [], warnings: [] }
24
+ }
25
+
26
+ export async function applySourceOpPlan(app: Engine, plan: SourceOpPlan) {
27
+ const changes: SourceChange[] = []
28
+ for (const path of plan.deletes) {
29
+ const graphPath = app.repo.graphPath(path)
30
+ if (!app.repo.storagePath(graphPath)) {
31
+ throw new SourceOpError(`Path escapes repository: ${path}`)
32
+ }
33
+ changes.push({ type: "delete", path: graphPath })
34
+ }
35
+ for (const write of plan.writes) {
36
+ const path = app.repo.graphPath(write.path)
37
+ if (!app.repo.storagePath(path)) {
38
+ throw new SourceOpError(`Path escapes repository: ${write.path}`)
39
+ }
40
+ changes.push({
41
+ type: app.repo.atPath(path) ? "replace" : "create",
42
+ path,
43
+ content: write.content,
44
+ })
45
+ }
46
+ if (changes.length) await app.commit({ changes })
47
+ }
@@ -0,0 +1,70 @@
1
+ import type { Engine } from "../host/engine.ts"
2
+ import { isIndexSource, publicPath } from "../server/router.ts"
3
+ import { unlinkLinks, type Source } from "../source/index.ts"
4
+ import { emptyPlan, SourceOpError, type SourceOpPlan } from "./plan.ts"
5
+
6
+ function resolveExisting(app: Engine, input: string): Source {
7
+ const graphPath = app.repo.graphPath(input)
8
+ const source =
9
+ app.repo.atPath(graphPath) ??
10
+ app.repo.get(input) ??
11
+ app.repo.get(app.repo.pathToId(graphPath))
12
+ if (!source) throw new SourceOpError(`Source not found: ${input}`)
13
+ if (!source.path.endsWith(".md")) {
14
+ throw new SourceOpError(`Only markdown sources can be removed: ${input}`)
15
+ }
16
+ return source
17
+ }
18
+
19
+ function referrerPath(from: string) {
20
+ const hash = from.indexOf("#")
21
+ return hash === -1 ? from : from.slice(0, hash)
22
+ }
23
+
24
+ export async function removeSource(
25
+ app: Engine,
26
+ path: string,
27
+ ): Promise<SourceOpPlan> {
28
+ const source = resolveExisting(app, path)
29
+ const inbound = app.graph.linksTo(source.path)
30
+ const byFile = new Map<string, Set<string>>()
31
+ let blockTargets = 0
32
+
33
+ for (const edge of inbound) {
34
+ const file = referrerPath(edge.from)
35
+ if (file === source.path) continue
36
+ const hrefs = byFile.get(file) ?? new Set()
37
+ hrefs.add(edge.href)
38
+ byFile.set(file, hrefs)
39
+ if (
40
+ edge.fragment &&
41
+ source.blocks.some((block) => block.id === edge.fragment)
42
+ ) {
43
+ blockTargets++
44
+ }
45
+ }
46
+
47
+ const plan = emptyPlan()
48
+ plan.deletes.push(source.path)
49
+
50
+ for (const [file, hrefs] of byFile) {
51
+ const referrer = app.repo.atPath(file)
52
+ if (!referrer) continue
53
+ const next = unlinkLinks(referrer.content, (href) => hrefs.has(href))
54
+ if (next !== referrer.content)
55
+ plan.writes.push({ path: file, content: next })
56
+ }
57
+
58
+ if (isIndexSource(source)) {
59
+ plan.warnings.push(
60
+ `Removing index source ${source.path} will 404 the directory route ${publicPath(source)}`,
61
+ )
62
+ }
63
+ if (blockTargets) {
64
+ plan.warnings.push(
65
+ `Removing ${source.path} unlinks ${blockTargets} block target${blockTargets === 1 ? "" : "s"}`,
66
+ )
67
+ }
68
+
69
+ return plan
70
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@pathmx/core",
3
+ "description": "A minimal, plugin-first framework for Paths and learning applications.",
4
+ "version": "0.5.0",
5
+ "license": "SEE LICENSE IN LICENSE.md",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/pathmx/pathmx-beta.git",
9
+ "directory": "pathmx/core"
10
+ },
11
+ "homepage": "https://github.com/pathmx/pathmx-beta#readme",
12
+ "bugs": "https://github.com/pathmx/pathmx-beta/issues",
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "registry": "https://registry.npmjs.org/",
16
+ "tag": "beta"
17
+ },
18
+ "type": "module",
19
+ "types": "./core.ts",
20
+ "module": "./core.ts",
21
+ "bin": {
22
+ "pathmx": "dist/pathmx.js",
23
+ "pmx": "dist/pathmx.js"
24
+ },
25
+ "exports": {
26
+ ".": "./core.ts",
27
+ "./bun": "./index.ts",
28
+ "./publish": "./publish/index.ts",
29
+ "./plugins": "./plugins/index.ts"
30
+ },
31
+ "engines": {
32
+ "bun": ">=1.4.0"
33
+ },
34
+ "pathmxRelease": {
35
+ "channel": "beta",
36
+ "testedBun": "1.4.0"
37
+ },
38
+ "dependencies": {
39
+ "@parcel/watcher": "^2.6.0",
40
+ "commander": "^15.0.0",
41
+ "highlight.js": "^11.12.0",
42
+ "morphdom": "^2.7.8",
43
+ "picocolors": "^1.1.1",
44
+ "temml": "^0.13.4"
45
+ }
46
+ }
package/path.ts ADDED
@@ -0,0 +1,75 @@
1
+ function parts(value: string) {
2
+ return value.split("/").filter(Boolean)
3
+ }
4
+
5
+ function normalize(value: string) {
6
+ if (!value) return "."
7
+ const absolute = value.startsWith("/")
8
+ const stack: string[] = []
9
+ for (const part of value.split("/")) {
10
+ if (!part || part === ".") continue
11
+ if (part === "..") {
12
+ if (stack.length && stack.at(-1) !== "..") stack.pop()
13
+ else if (!absolute) stack.push(part)
14
+ } else {
15
+ stack.push(part)
16
+ }
17
+ }
18
+ const normalized = `${absolute ? "/" : ""}${stack.join("/")}`
19
+ return normalized || (absolute ? "/" : ".")
20
+ }
21
+
22
+ function join(...values: string[]) {
23
+ return normalize(values.filter(Boolean).join("/"))
24
+ }
25
+
26
+ function dirname(value: string) {
27
+ const normalized = normalize(value)
28
+ if (normalized === "/" || normalized === ".") return normalized
29
+ const index = normalized.lastIndexOf("/")
30
+ if (index === -1) return "."
31
+ return index === 0 ? "/" : normalized.slice(0, index)
32
+ }
33
+
34
+ function basename(value: string) {
35
+ const normalized = normalize(value)
36
+ if (normalized === "/" || normalized === ".") return normalized
37
+ return normalized.slice(normalized.lastIndexOf("/") + 1)
38
+ }
39
+
40
+ function relative(from: string, to: string) {
41
+ const source = parts(normalize(from))
42
+ const target = parts(normalize(to))
43
+ let shared = 0
44
+ while (
45
+ shared < source.length &&
46
+ shared < target.length &&
47
+ source[shared] === target[shared]
48
+ ) {
49
+ shared++
50
+ }
51
+ return [
52
+ ...Array.from({ length: source.length - shared }, () => ".."),
53
+ ...target.slice(shared),
54
+ ].join("/")
55
+ }
56
+
57
+ function extname(value: string) {
58
+ const name = basename(value)
59
+ const index = name.lastIndexOf(".")
60
+ return index > 0 ? name.slice(index) : ""
61
+ }
62
+
63
+ function resolve(base: string, target: string) {
64
+ return normalize(target.startsWith("/") ? target : join(base, target))
65
+ }
66
+
67
+ export const posixPath = {
68
+ basename,
69
+ dirname,
70
+ extname,
71
+ join,
72
+ normalize,
73
+ relative,
74
+ resolve,
75
+ }