@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.
- package/LICENSE.md +172 -0
- package/README.md +111 -0
- package/actions/actions.ts +307 -0
- package/actions/draft.ts +105 -0
- package/actions/transaction.ts +164 -0
- package/assets.ts +149 -0
- package/authority/access.ts +820 -0
- package/authority/actors.ts +271 -0
- package/authority/agents.ts +95 -0
- package/authority/credentials.ts +144 -0
- package/authority/model.ts +71 -0
- package/authority/sharing.ts +89 -0
- package/browser-bundle.ts +61 -0
- package/canonical.ts +41 -0
- package/cli/dev.ts +134 -0
- package/cli/export.ts +74 -0
- package/cli/index.ts +52 -0
- package/cli/lint.ts +164 -0
- package/cli/mv.ts +30 -0
- package/cli/perms.ts +182 -0
- package/cli/plugin-packages.ts +159 -0
- package/cli/plugins.ts +95 -0
- package/cli/query.ts +225 -0
- package/cli/readme.md +95 -0
- package/cli/release.ts +75 -0
- package/cli/rm.ts +22 -0
- package/cli/serve.ts +100 -0
- package/cli/tree.ts +131 -0
- package/cli/update.ts +78 -0
- package/cli/utils.ts +53 -0
- package/core.ts +96 -0
- package/database.ts +189 -0
- package/dependencies.ts +60 -0
- package/dist/pathmx.js +1224 -0
- package/document.ts +194 -0
- package/environment.ts +110 -0
- package/environments/bun-plugins.ts +68 -0
- package/environments/bun-publish.ts +194 -0
- package/environments/bun.ts +223 -0
- package/globals.d.ts +14 -0
- package/graph.ts +153 -0
- package/host/compiler.ts +314 -0
- package/host/create.ts +66 -0
- package/host/engine.ts +630 -0
- package/host/plugin-host.ts +487 -0
- package/host/render.ts +102 -0
- package/host/response.ts +20 -0
- package/host/shell.html +15 -0
- package/host/source-sync.ts +254 -0
- package/html-attributes.ts +15 -0
- package/html.ts +48 -0
- package/icons/LICENSE +43 -0
- package/icons/lucide.ts +54 -0
- package/index.ts +75 -0
- package/invalidation.ts +85 -0
- package/log.ts +84 -0
- package/ops/href.ts +87 -0
- package/ops/index.ts +9 -0
- package/ops/move-source.ts +114 -0
- package/ops/plan.ts +47 -0
- package/ops/remove-source.ts +70 -0
- package/package.json +46 -0
- package/path.ts +75 -0
- package/pattern.ts +314 -0
- package/perf.ts +88 -0
- package/plugins/actor-path.ts +43 -0
- package/plugins/actor-profile.ts +55 -0
- package/plugins/agent-browsing.ts +183 -0
- package/plugins/app-component.ts +23 -0
- package/plugins/block-layout/index.ts +78 -0
- package/plugins/block-layout/theme.css +90 -0
- package/plugins/callout/index.ts +236 -0
- package/plugins/callout/theme.css +138 -0
- package/plugins/canonical-links.ts +144 -0
- package/plugins/code-highlight/client.ts +89 -0
- package/plugins/code-highlight/index.ts +147 -0
- package/plugins/code-highlight/theme.css +89 -0
- package/plugins/context.ts +131 -0
- package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
- package/plugins/datatype/OFL.txt +96 -0
- package/plugins/datatype/index.ts +287 -0
- package/plugins/definition-list.ts +236 -0
- package/plugins/directive-definitions.ts +13 -0
- package/plugins/embedding/client.ts +88 -0
- package/plugins/embedding/index.ts +43 -0
- package/plugins/image/index.ts +68 -0
- package/plugins/image/theme.css +43 -0
- package/plugins/include.ts +119 -0
- package/plugins/index.ts +91 -0
- package/plugins/input/authoring.ts +236 -0
- package/plugins/input/config.ts +85 -0
- package/plugins/input/index.ts +359 -0
- package/plugins/input/model.ts +173 -0
- package/plugins/input/state.ts +272 -0
- package/plugins/layout.ts +313 -0
- package/plugins/literate/client.ts +208 -0
- package/plugins/literate/index.ts +742 -0
- package/plugins/math/index.ts +140 -0
- package/plugins/meta.ts +42 -0
- package/plugins/navigation/index.ts +158 -0
- package/plugins/navigation/model.ts +364 -0
- package/plugins/preset.ts +65 -0
- package/plugins/query/index.ts +322 -0
- package/plugins/rss/index.ts +46 -0
- package/plugins/runtime/actions.ts +62 -0
- package/plugins/runtime/client.ts +8 -0
- package/plugins/runtime/document.ts +506 -0
- package/plugins/runtime/error.ts +154 -0
- package/plugins/runtime/heading-links.ts +31 -0
- package/plugins/runtime/index.ts +104 -0
- package/plugins/runtime/live.ts +51 -0
- package/plugins/runtime/navigation.ts +113 -0
- package/plugins/runtime/pending.ts +53 -0
- package/plugins/runtime/publication.ts +19 -0
- package/plugins/runtime/start.ts +177 -0
- package/plugins/runtime/static-client.ts +6 -0
- package/plugins/runtime/static-live.ts +188 -0
- package/plugins/runtime/view-transition.ts +72 -0
- package/plugins/runtime/viewport.ts +110 -0
- package/plugins/schema.ts +24 -0
- package/plugins/scripts.ts +140 -0
- package/plugins/source-preview/client.ts +256 -0
- package/plugins/source-preview/index.ts +132 -0
- package/plugins/source-preview/model.ts +11 -0
- package/plugins/source-preview/preview.css +107 -0
- package/plugins/styles.ts +145 -0
- package/plugins/theme/components.ts +10 -0
- package/plugins/theme/foundation.css +358 -0
- package/plugins/theme/index.ts +333 -0
- package/plugins/theme/prose.css +284 -0
- package/plugins/theme/scheme.js +63 -0
- package/plugins/theme/toggle.css +44 -0
- package/plugins/theme/view-transition.css +68 -0
- package/plugins/toc/client.ts +146 -0
- package/plugins/toc/index.ts +232 -0
- package/plugins/toc/theme.css +172 -0
- package/plugins/types.ts +538 -0
- package/problem.ts +41 -0
- package/project.ts +6 -0
- package/publish/build.ts +402 -0
- package/publish/concurrent.ts +32 -0
- package/publish/identity.ts +63 -0
- package/publish/index.ts +11 -0
- package/publish/location.ts +167 -0
- package/publish/model.ts +58 -0
- package/publish/plan.ts +300 -0
- package/publish/validate.ts +150 -0
- package/query/candidate-index.ts +101 -0
- package/query/dependency-index.ts +99 -0
- package/query/fields.ts +216 -0
- package/query/invalidation.ts +48 -0
- package/query/kernel.ts +776 -0
- package/query/model.ts +79 -0
- package/repository.ts +374 -0
- package/runtime-profile.ts +34 -0
- package/schema/model.ts +92 -0
- package/schema/registry.ts +642 -0
- package/schema/standard.ts +226 -0
- package/server/context.ts +94 -0
- package/server/css-imports.ts +66 -0
- package/server/http.ts +333 -0
- package/server/page.ts +21 -0
- package/server/protocols.ts +12 -0
- package/server/representations.ts +66 -0
- package/server/router.ts +131 -0
- package/server/routes.ts +202 -0
- package/server/serve.ts +5 -0
- package/server/watch.ts +120 -0
- package/source/blocks.ts +71 -0
- package/source/format.ts +164 -0
- package/source/index.ts +67 -0
- package/source/interpolate.ts +63 -0
- package/source/markdown/blocks.ts +46 -0
- package/source/markdown/comments.ts +32 -0
- package/source/markdown/fences.ts +67 -0
- package/source/markdown/headings.ts +88 -0
- package/source/markdown/index.ts +34 -0
- package/source/markdown/inline-code.ts +6 -0
- package/source/markdown/links.ts +326 -0
- package/source/markdown/lists.ts +223 -0
- package/source/markdown/prose.ts +74 -0
- package/source/markdown/scan.ts +79 -0
- package/source/markdown/text.ts +82 -0
- package/source/meta.ts +87 -0
- package/source/source.ts +121 -0
- package/source/topmatter.ts +57 -0
- package/source/utils.ts +21 -0
- package/source-diagnostic.ts +63 -0
- package/text-file.ts +26 -0
- package/util.ts +35 -0
- package/view/components.ts +499 -0
- package/view/model.ts +91 -0
- package/view/projection.ts +162 -0
- package/view/session.ts +146 -0
- package/view/syntax.ts +372 -0
package/query/model.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { SqlDatabase } from "../database.ts"
|
|
2
|
+
import type { DependencyContext } from "../dependencies.ts"
|
|
3
|
+
import type { PathMXDataSchema, StandardSchemaV1 } from "../schema/standard.ts"
|
|
4
|
+
import type { BlockSummary, SourceSummary, View } from "../view/model.ts"
|
|
5
|
+
|
|
6
|
+
export type QueryScalar = string | number | boolean | null
|
|
7
|
+
|
|
8
|
+
export type QuerySort = Readonly<{
|
|
9
|
+
by: string
|
|
10
|
+
direction?: "asc" | "desc"
|
|
11
|
+
}>
|
|
12
|
+
|
|
13
|
+
export type QueryRequest<Input = undefined> = Readonly<{
|
|
14
|
+
from?: string
|
|
15
|
+
input?: Input
|
|
16
|
+
where?: Readonly<Record<string, QueryScalar>>
|
|
17
|
+
sort?: readonly QuerySort[]
|
|
18
|
+
limit?: number
|
|
19
|
+
}>
|
|
20
|
+
|
|
21
|
+
export type SourceQueryRequest = QueryRequest &
|
|
22
|
+
Readonly<{
|
|
23
|
+
type?: string
|
|
24
|
+
has?: string | readonly string[]
|
|
25
|
+
recursive?: boolean
|
|
26
|
+
}>
|
|
27
|
+
|
|
28
|
+
export type BlockQueryRequest = SourceQueryRequest
|
|
29
|
+
|
|
30
|
+
type Awaitable<T> = T | Promise<T>
|
|
31
|
+
type DataSchema = PathMXDataSchema<unknown, unknown>
|
|
32
|
+
type OutputSchema = PathMXDataSchema<unknown, readonly unknown[]>
|
|
33
|
+
|
|
34
|
+
export type QueryDefinition<
|
|
35
|
+
InputSchema extends DataSchema | undefined = DataSchema | undefined,
|
|
36
|
+
ResultSchema extends OutputSchema = OutputSchema,
|
|
37
|
+
> = Readonly<{
|
|
38
|
+
input?: InputSchema
|
|
39
|
+
output: ResultSchema
|
|
40
|
+
resolve(
|
|
41
|
+
input: InputSchema extends DataSchema
|
|
42
|
+
? StandardSchemaV1.InferOutput<InputSchema>
|
|
43
|
+
: undefined,
|
|
44
|
+
context: QueryResolverContext,
|
|
45
|
+
): Awaitable<StandardSchemaV1.InferOutput<ResultSchema>>
|
|
46
|
+
}>
|
|
47
|
+
|
|
48
|
+
export type QueryContribution = QueryDefinition<any, any>
|
|
49
|
+
|
|
50
|
+
export type QueryDefinitionInput<Definition extends QueryDefinition> =
|
|
51
|
+
Definition["input"] extends DataSchema
|
|
52
|
+
? StandardSchemaV1.InferInput<Definition["input"]>
|
|
53
|
+
: undefined
|
|
54
|
+
|
|
55
|
+
export type QueryDefinitionOutput<Definition extends QueryDefinition> =
|
|
56
|
+
StandardSchemaV1.InferOutput<Definition["output"]>
|
|
57
|
+
|
|
58
|
+
export type QueryRunner = {
|
|
59
|
+
<Definition extends QueryDefinition>(
|
|
60
|
+
definition: Definition,
|
|
61
|
+
request?: QueryRequest<QueryDefinitionInput<Definition>>,
|
|
62
|
+
): Promise<QueryDefinitionOutput<Definition>>
|
|
63
|
+
(id: string, request?: QueryRequest<unknown>): Promise<readonly unknown[]>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type ViewQuery = Readonly<{
|
|
67
|
+
sources(request?: SourceQueryRequest): Promise<readonly SourceSummary[]>
|
|
68
|
+
blocks(request?: BlockQueryRequest): Promise<readonly BlockSummary[]>
|
|
69
|
+
run: QueryRunner
|
|
70
|
+
}>
|
|
71
|
+
|
|
72
|
+
export type QueryResolverContext = DependencyContext &
|
|
73
|
+
Readonly<{
|
|
74
|
+
view: View
|
|
75
|
+
from: string
|
|
76
|
+
query: ViewQuery
|
|
77
|
+
/** Database isolated to the query's owning Plugin. */
|
|
78
|
+
database?: SqlDatabase
|
|
79
|
+
}>
|
package/repository.ts
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import type { PathMXEnvironment } from "./environment.ts"
|
|
2
|
+
import { type Source, createSource, resolveSourceId } from "./source/index.ts"
|
|
3
|
+
import { canonicalSourceId } from "./canonical.ts"
|
|
4
|
+
import {
|
|
5
|
+
readTextFileState,
|
|
6
|
+
sameTextFileState,
|
|
7
|
+
type TextFileState,
|
|
8
|
+
} from "./text-file.ts"
|
|
9
|
+
|
|
10
|
+
const LOAD_CONCURRENCY = 16
|
|
11
|
+
|
|
12
|
+
export type SourceChange =
|
|
13
|
+
| { type: "create"; path: string; content: string }
|
|
14
|
+
| { type: "replace"; path: string; content: string }
|
|
15
|
+
| { type: "delete"; path: string }
|
|
16
|
+
|
|
17
|
+
export type SourceChangeSet = {
|
|
18
|
+
changes: SourceChange[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CommitResult = {
|
|
22
|
+
applied: SourceChange[]
|
|
23
|
+
error?: Error
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type UpsertResult =
|
|
27
|
+
| { type: "updated"; source: Source }
|
|
28
|
+
| { type: "deleted" }
|
|
29
|
+
| { type: "error"; error: SourceLoadError; source?: Source }
|
|
30
|
+
|
|
31
|
+
export class DuplicateSourceIdError extends Error {
|
|
32
|
+
readonly id: string
|
|
33
|
+
readonly existingPath: string
|
|
34
|
+
readonly incomingPath: string
|
|
35
|
+
|
|
36
|
+
constructor(id: string, existingPath: string, incomingPath: string) {
|
|
37
|
+
super(`Duplicate source id "${id}": ${existingPath} and ${incomingPath}`)
|
|
38
|
+
this.name = "DuplicateSourceIdError"
|
|
39
|
+
this.id = id
|
|
40
|
+
this.existingPath = existingPath
|
|
41
|
+
this.incomingPath = incomingPath
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class SourceLoadError extends Error {
|
|
46
|
+
readonly path: string
|
|
47
|
+
|
|
48
|
+
constructor(path: string, cause: unknown) {
|
|
49
|
+
const detail = cause instanceof Error ? cause.message : String(cause)
|
|
50
|
+
super(`Failed to load source ${path}: ${detail}`)
|
|
51
|
+
this.name = "SourceLoadError"
|
|
52
|
+
this.path = path
|
|
53
|
+
this.cause = cause
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class SourceCommitError extends Error {
|
|
58
|
+
constructor(message: string) {
|
|
59
|
+
super(message)
|
|
60
|
+
this.name = "SourceCommitError"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class SourceConflictError extends SourceCommitError {
|
|
65
|
+
readonly path: string
|
|
66
|
+
|
|
67
|
+
constructor(path: string) {
|
|
68
|
+
super(`Source changed before commit: ${path}`)
|
|
69
|
+
this.name = "SourceConflictError"
|
|
70
|
+
this.path = path
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isNotFound(error: unknown) {
|
|
75
|
+
return (
|
|
76
|
+
error instanceof Error &&
|
|
77
|
+
"code" in error &&
|
|
78
|
+
(error as { code?: string }).code === "ENOENT"
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class Repository {
|
|
83
|
+
rootPath: string
|
|
84
|
+
environment: PathMXEnvironment
|
|
85
|
+
private byId = new Map<string, Source>()
|
|
86
|
+
private byPath = new Map<string, Source>()
|
|
87
|
+
private revisionValue = 0
|
|
88
|
+
|
|
89
|
+
constructor(rootPath: string, environment: PathMXEnvironment) {
|
|
90
|
+
this.rootPath = rootPath
|
|
91
|
+
this.environment = environment
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
graphPath(relPath: string) {
|
|
95
|
+
return this.environment.path.join("/", relPath.replaceAll("\\", "/"))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
pathToId(path: string) {
|
|
99
|
+
return resolveSourceId(this.graphPath(path), {})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get(id: string) {
|
|
103
|
+
return this.byId.get(id)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
atPath(path: string) {
|
|
107
|
+
const routePath = this.graphPath(path).replace(/\/$/, "") || "/"
|
|
108
|
+
return this.byPath.get(routePath)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @deprecated Use atPath. */
|
|
112
|
+
sourceAtPath(path: string) {
|
|
113
|
+
return this.atPath(path)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
all() {
|
|
117
|
+
return [...this.byPath.values()]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Changes whenever repository membership or source identity changes. */
|
|
121
|
+
get revision() {
|
|
122
|
+
return this.revisionValue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async load() {
|
|
126
|
+
this.byId.clear()
|
|
127
|
+
this.byPath.clear()
|
|
128
|
+
this.revisionValue++
|
|
129
|
+
const files = [
|
|
130
|
+
...(await this.environment.storage.list(this.rootPath)),
|
|
131
|
+
].filter((file) => file.endsWith(".md"))
|
|
132
|
+
|
|
133
|
+
for (let start = 0; start < files.length; start += LOAD_CONCURRENCY) {
|
|
134
|
+
const loaded = await Promise.all(
|
|
135
|
+
files
|
|
136
|
+
.slice(start, start + LOAD_CONCURRENCY)
|
|
137
|
+
.map((file) => this.readListedSource(file)),
|
|
138
|
+
)
|
|
139
|
+
for (const item of loaded) {
|
|
140
|
+
if (!item) continue
|
|
141
|
+
if ("error" in item) throw item.error
|
|
142
|
+
try {
|
|
143
|
+
this.put(createSource(item.path, item.content, this.environment.yaml))
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error instanceof DuplicateSourceIdError) throw error
|
|
146
|
+
throw new SourceLoadError(item.path, error)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async readListedSource(relPath: string) {
|
|
153
|
+
const path = this.graphPath(relPath)
|
|
154
|
+
const storagePath = this.storagePath(relPath)
|
|
155
|
+
if (!storagePath) return
|
|
156
|
+
try {
|
|
157
|
+
return {
|
|
158
|
+
path,
|
|
159
|
+
content: await this.environment.storage.readText(storagePath),
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (isNotFound(error)) return
|
|
163
|
+
try {
|
|
164
|
+
if (!(await this.environment.storage.exists(storagePath))) return
|
|
165
|
+
} catch {
|
|
166
|
+
// Preserve the original read error when existence cannot be checked.
|
|
167
|
+
}
|
|
168
|
+
return { error: new SourceLoadError(path, error) }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Index a constructed Source after enforcing the one-to-one id/path invariant.
|
|
174
|
+
*/
|
|
175
|
+
put(source: Source) {
|
|
176
|
+
canonicalSourceId(source.id)
|
|
177
|
+
const existing = this.byId.get(source.id)
|
|
178
|
+
if (existing && existing.path !== source.path) {
|
|
179
|
+
throw new DuplicateSourceIdError(source.id, existing.path, source.path)
|
|
180
|
+
}
|
|
181
|
+
const previous = this.byPath.get(source.path)
|
|
182
|
+
if (previous && previous.id !== source.id) this.byId.delete(previous.id)
|
|
183
|
+
this.byId.set(source.id, source)
|
|
184
|
+
this.byPath.set(source.path, source)
|
|
185
|
+
this.revisionValue++
|
|
186
|
+
return source
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async upsert(
|
|
190
|
+
relPath: string,
|
|
191
|
+
options: { onError?: "throw" | "preserve" } = {},
|
|
192
|
+
): Promise<UpsertResult> {
|
|
193
|
+
const onError = options.onError ?? "preserve"
|
|
194
|
+
const path = this.graphPath(relPath)
|
|
195
|
+
const storagePath = this.storagePath(relPath)
|
|
196
|
+
const previous = this.atPath(path)
|
|
197
|
+
if (!storagePath) {
|
|
198
|
+
this.drop(previous)
|
|
199
|
+
return { type: "deleted" }
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
if (!(await this.environment.storage.exists(storagePath))) {
|
|
203
|
+
this.drop(previous)
|
|
204
|
+
return { type: "deleted" }
|
|
205
|
+
}
|
|
206
|
+
const content = await this.environment.storage.readText(storagePath)
|
|
207
|
+
// Same text: keep the indexed Source. createSource() starts with
|
|
208
|
+
// empty blocks; replacing here made the watch echo after commit
|
|
209
|
+
// drop them (syncSource returns early when content is unchanged).
|
|
210
|
+
if (previous && previous.content === content) {
|
|
211
|
+
return { type: "updated", source: previous }
|
|
212
|
+
}
|
|
213
|
+
const source = createSource(path, content, this.environment.yaml)
|
|
214
|
+
this.put(source)
|
|
215
|
+
return { type: "updated", source }
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (isNotFound(error)) {
|
|
218
|
+
this.drop(previous)
|
|
219
|
+
return { type: "deleted" }
|
|
220
|
+
}
|
|
221
|
+
if (error instanceof DuplicateSourceIdError) {
|
|
222
|
+
if (onError === "throw") throw error
|
|
223
|
+
return {
|
|
224
|
+
type: "error",
|
|
225
|
+
error: new SourceLoadError(path, error),
|
|
226
|
+
source: previous,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const loadError = new SourceLoadError(path, error)
|
|
230
|
+
if (onError === "throw") throw loadError
|
|
231
|
+
return { type: "error", error: loadError, source: previous }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
storagePath(targetPath: string) {
|
|
236
|
+
const rel = targetPath.replaceAll("\\", "/").replace(/^\/+/, "")
|
|
237
|
+
if (!rel || rel.split("/").includes("..")) return
|
|
238
|
+
const normalizedRoot = this.rootPath.replaceAll("\\", "/")
|
|
239
|
+
const root =
|
|
240
|
+
normalizedRoot === "/" ? "/" : normalizedRoot.replace(/\/+$/, "")
|
|
241
|
+
if (root === "/") return `/${rel}`
|
|
242
|
+
return root ? `${root}/${rel}` : rel
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async isFile(targetPath: string) {
|
|
246
|
+
const storagePath = this.storagePath(targetPath)
|
|
247
|
+
if (!storagePath) return false
|
|
248
|
+
try {
|
|
249
|
+
return await this.environment.storage.exists(storagePath)
|
|
250
|
+
} catch {
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
remove(relPath: string) {
|
|
256
|
+
this.drop(this.atPath(relPath))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async commit(changeSet: SourceChangeSet): Promise<CommitResult> {
|
|
260
|
+
const changes = changeSet.changes.map((change) => {
|
|
261
|
+
if (change.path.split(/[\\/]/).includes("..")) {
|
|
262
|
+
throw new SourceCommitError(`Path escapes repository: ${change.path}`)
|
|
263
|
+
}
|
|
264
|
+
return { ...change, path: this.graphPath(change.path) }
|
|
265
|
+
})
|
|
266
|
+
this.preflight(changes)
|
|
267
|
+
await this.verifyStorage(changes)
|
|
268
|
+
|
|
269
|
+
const applied: SourceChange[] = []
|
|
270
|
+
try {
|
|
271
|
+
for (const change of changes) {
|
|
272
|
+
const file = this.storagePath(change.path)
|
|
273
|
+
if (!file)
|
|
274
|
+
throw new SourceCommitError(`Path escapes repository: ${change.path}`)
|
|
275
|
+
if (change.type === "delete") {
|
|
276
|
+
if (await this.environment.storage.exists(file)) {
|
|
277
|
+
await this.environment.storage.delete(file)
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
await this.environment.storage.writeText(file, change.content)
|
|
281
|
+
}
|
|
282
|
+
applied.push(change)
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
await this.resync(applied)
|
|
286
|
+
return {
|
|
287
|
+
applied,
|
|
288
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await this.resync(applied)
|
|
293
|
+
return { applied }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private preflight(changes: SourceChange[]) {
|
|
297
|
+
const nextByPath = new Map(this.byPath)
|
|
298
|
+
const nextById = new Map(this.byId)
|
|
299
|
+
|
|
300
|
+
for (const change of changes) {
|
|
301
|
+
const file = this.storagePath(change.path)
|
|
302
|
+
if (!file) {
|
|
303
|
+
throw new SourceCommitError(`Path escapes repository: ${change.path}`)
|
|
304
|
+
}
|
|
305
|
+
const previous = nextByPath.get(change.path)
|
|
306
|
+
if (change.type === "delete") {
|
|
307
|
+
if (!previous) {
|
|
308
|
+
throw new SourceCommitError(`Path does not exist: ${change.path}`)
|
|
309
|
+
}
|
|
310
|
+
nextByPath.delete(change.path)
|
|
311
|
+
nextById.delete(previous.id)
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const source = createSource(
|
|
316
|
+
change.path,
|
|
317
|
+
change.content,
|
|
318
|
+
this.environment.yaml,
|
|
319
|
+
)
|
|
320
|
+
if (change.type === "create" && previous) {
|
|
321
|
+
throw new SourceCommitError(`Path already exists: ${change.path}`)
|
|
322
|
+
}
|
|
323
|
+
if (change.type === "replace" && !previous) {
|
|
324
|
+
throw new SourceCommitError(`Path does not exist: ${change.path}`)
|
|
325
|
+
}
|
|
326
|
+
if (previous && previous.id !== source.id) nextById.delete(previous.id)
|
|
327
|
+
const owner = nextById.get(source.id)
|
|
328
|
+
if (owner && owner.path !== change.path) {
|
|
329
|
+
throw new DuplicateSourceIdError(source.id, owner.path, change.path)
|
|
330
|
+
}
|
|
331
|
+
nextByPath.set(change.path, source)
|
|
332
|
+
nextById.set(source.id, source)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async verifyStorage(changes: SourceChange[]) {
|
|
337
|
+
for (const change of changes) {
|
|
338
|
+
const file = this.storagePath(change.path)
|
|
339
|
+
if (!file)
|
|
340
|
+
throw new SourceCommitError(`Path escapes repository: ${change.path}`)
|
|
341
|
+
const before = this.atPath(change.path)
|
|
342
|
+
if (change.type !== "create" && !before) {
|
|
343
|
+
throw new SourceConflictError(change.path)
|
|
344
|
+
}
|
|
345
|
+
const expected: TextFileState = before
|
|
346
|
+
? { type: "present", content: before.content }
|
|
347
|
+
: { type: "missing" }
|
|
348
|
+
let current: TextFileState
|
|
349
|
+
try {
|
|
350
|
+
current = await readTextFileState(this.environment.storage, file)
|
|
351
|
+
} catch {
|
|
352
|
+
throw new SourceConflictError(change.path)
|
|
353
|
+
}
|
|
354
|
+
if (!sameTextFileState(current, expected)) {
|
|
355
|
+
throw new SourceConflictError(change.path)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async resync(changes: SourceChange[]) {
|
|
361
|
+
for (const change of changes) {
|
|
362
|
+
const rel = change.path.replace(/^\/+/, "")
|
|
363
|
+
if (change.type === "delete") this.remove(change.path)
|
|
364
|
+
else await this.upsert(rel, { onError: "throw" })
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private drop(source?: Source) {
|
|
369
|
+
if (!source) return
|
|
370
|
+
this.byId.delete(source.id)
|
|
371
|
+
this.byPath.delete(source.path)
|
|
372
|
+
this.revisionValue++
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type RuntimeProfileOptions = Readonly<{
|
|
2
|
+
mode?: "development" | "production"
|
|
3
|
+
target?: "server" | "static"
|
|
4
|
+
capabilities?: readonly Readonly<{ plugin: string; key: string }>[]
|
|
5
|
+
}>
|
|
6
|
+
|
|
7
|
+
export type RuntimeProfile = Readonly<{
|
|
8
|
+
mode: NonNullable<RuntimeProfileOptions["mode"]>
|
|
9
|
+
target: NonNullable<RuntimeProfileOptions["target"]>
|
|
10
|
+
has(plugin: string, key: string): boolean
|
|
11
|
+
}>
|
|
12
|
+
|
|
13
|
+
/** Normalize portable startup configuration into an immutable runtime view. */
|
|
14
|
+
export function createRuntimeProfile(
|
|
15
|
+
options: RuntimeProfileOptions = {},
|
|
16
|
+
): RuntimeProfile {
|
|
17
|
+
const capabilities = new Map<string, Set<string>>()
|
|
18
|
+
for (const capability of options.capabilities ?? []) {
|
|
19
|
+
let keys = capabilities.get(capability.plugin)
|
|
20
|
+
if (!keys) {
|
|
21
|
+
keys = new Set()
|
|
22
|
+
capabilities.set(capability.plugin, keys)
|
|
23
|
+
}
|
|
24
|
+
keys.add(capability.key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
mode: options.mode ?? "production",
|
|
29
|
+
target: options.target ?? "server",
|
|
30
|
+
has(plugin: string, key: string) {
|
|
31
|
+
return capabilities.get(plugin)?.has(key) ?? false
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
}
|
package/schema/model.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ActionContribution } from "../actions/actions.ts"
|
|
2
|
+
import type { QueryContribution } from "../query/model.ts"
|
|
3
|
+
import type { PathMXDataSchema } from "./standard.ts"
|
|
4
|
+
import type { SourceNode } from "../source/index.ts"
|
|
5
|
+
|
|
6
|
+
export type NodeTypeDefinition = Readonly<{
|
|
7
|
+
/** Singular human label. Falls back to the title-cased node type. */
|
|
8
|
+
label?: string
|
|
9
|
+
description?: string
|
|
10
|
+
/** Renderer-owned icon name, such as a Lucide icon id. */
|
|
11
|
+
icon?: string
|
|
12
|
+
/** Renderer-owned color token or CSS color. */
|
|
13
|
+
color?: string
|
|
14
|
+
/** Validate the complete authored data map for this node type. */
|
|
15
|
+
schema?: PathMXDataSchema
|
|
16
|
+
}>
|
|
17
|
+
|
|
18
|
+
export type SourceTypeDefinition = NodeTypeDefinition
|
|
19
|
+
export type BlockTypeDefinition = NodeTypeDefinition
|
|
20
|
+
|
|
21
|
+
export type SchemaContribution = Readonly<{
|
|
22
|
+
/** Ordered Source-type contributions. Later Plugins may refine presentation. */
|
|
23
|
+
sources?: Readonly<Record<string, SourceTypeDefinition>>
|
|
24
|
+
/** Ordered Block-type contributions. Later Plugins may refine presentation. */
|
|
25
|
+
blocks?: Readonly<Record<string, BlockTypeDefinition>>
|
|
26
|
+
/** Exclusively owned Source/Block data props and their value schemas. */
|
|
27
|
+
props?: Readonly<Record<string, PathMXDataSchema>>
|
|
28
|
+
/** Plugin-local query names installed externally as `<plugin>:<name>`. */
|
|
29
|
+
query?: Readonly<Record<string, QueryContribution>>
|
|
30
|
+
/** Plugin-local Action names installed externally as `<plugin>:<name>`. */
|
|
31
|
+
actions?: Readonly<Record<string, ActionContribution>>
|
|
32
|
+
}>
|
|
33
|
+
|
|
34
|
+
/** Preserve the complete inferred shape of one Plugin-owned Schema fragment. */
|
|
35
|
+
export function defineSchema<const Definition extends SchemaContribution>(
|
|
36
|
+
definition: Definition,
|
|
37
|
+
) {
|
|
38
|
+
return definition
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Serializable node-type meaning safe to project into Views and UIs. */
|
|
42
|
+
export type NodeType = Readonly<{
|
|
43
|
+
type: string
|
|
44
|
+
label: string
|
|
45
|
+
description?: string
|
|
46
|
+
icon: string
|
|
47
|
+
color: string
|
|
48
|
+
origin: "defined" | "observed"
|
|
49
|
+
plugins: readonly string[]
|
|
50
|
+
}>
|
|
51
|
+
|
|
52
|
+
export type SourceType = NodeType
|
|
53
|
+
export type BlockType = NodeType
|
|
54
|
+
|
|
55
|
+
export type DataProp = Readonly<{
|
|
56
|
+
name: string
|
|
57
|
+
plugin: string
|
|
58
|
+
schema: PathMXDataSchema
|
|
59
|
+
inputJsonSchema: Readonly<Record<string, unknown>>
|
|
60
|
+
outputJsonSchema: Readonly<Record<string, unknown>>
|
|
61
|
+
}>
|
|
62
|
+
|
|
63
|
+
export type NodeSchema = Readonly<{
|
|
64
|
+
plugin: string
|
|
65
|
+
schema: PathMXDataSchema
|
|
66
|
+
inputJsonSchema: Readonly<Record<string, unknown>>
|
|
67
|
+
outputJsonSchema: Readonly<Record<string, unknown>>
|
|
68
|
+
}>
|
|
69
|
+
|
|
70
|
+
export type QuerySchema = Readonly<{
|
|
71
|
+
id: string
|
|
72
|
+
plugin: string
|
|
73
|
+
input?: PathMXDataSchema
|
|
74
|
+
output: PathMXDataSchema<unknown, readonly unknown[]>
|
|
75
|
+
inputJsonSchema?: Readonly<Record<string, unknown>>
|
|
76
|
+
outputJsonSchema: Readonly<Record<string, unknown>>
|
|
77
|
+
}>
|
|
78
|
+
|
|
79
|
+
export type ApplicationSchema = Readonly<{
|
|
80
|
+
sourceType(sourceOrType: SourceNode | string): SourceType
|
|
81
|
+
sourceTypes(): readonly SourceType[]
|
|
82
|
+
blockType(blockOrType: SourceNode | string): BlockType
|
|
83
|
+
blockTypes(): readonly BlockType[]
|
|
84
|
+
sourceSchemas(sourceOrType: SourceNode | string): readonly NodeSchema[]
|
|
85
|
+
blockSchemas(blockOrType: SourceNode | string): readonly NodeSchema[]
|
|
86
|
+
query(id: string): QuerySchema | undefined
|
|
87
|
+
queries(): readonly QuerySchema[]
|
|
88
|
+
prop(name: string): DataProp | undefined
|
|
89
|
+
props(node?: SourceNode): readonly DataProp[]
|
|
90
|
+
has(node: SourceNode, prop: string): boolean
|
|
91
|
+
value(node: SourceNode, prop: string): unknown
|
|
92
|
+
}>
|