@lincyaw/kernel 0.0.0-stage → 1.0.0-rc.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 +202 -0
- package/README.md +32 -2
- package/package.json +37 -3
- package/src/anchors.ts +54 -0
- package/src/blocks.ts +27 -0
- package/src/checks.ts +35 -0
- package/src/contracts/agent.ts +153 -0
- package/src/contracts/execution.ts +119 -0
- package/src/contracts/operations.ts +8 -0
- package/src/contracts/preview.ts +391 -0
- package/src/contracts/server.ts +158 -0
- package/src/contracts/service.ts +8 -0
- package/src/contracts/speech.ts +137 -0
- package/src/contracts.ts +285 -0
- package/src/errors.ts +7 -0
- package/src/events.ts +32 -0
- package/src/execution.ts +21 -0
- package/src/fingerprint.ts +31 -0
- package/src/follow.ts +37 -0
- package/src/git.ts +12 -0
- package/src/html.ts +41 -0
- package/src/index.ts +29 -0
- package/src/ir.ts +318 -0
- package/src/kernel.ts +177 -0
- package/src/lock.ts +85 -0
- package/src/logging.ts +11 -0
- package/src/markdown.ts +311 -0
- package/src/node/index.ts +133 -0
- package/src/node/strip-types.mjs +15 -0
- package/src/published.ts +11 -0
- package/src/reading.ts +91 -0
- package/src/requests.ts +28 -0
- package/src/runner.py +454 -0
- package/src/services.ts +146 -0
package/src/ir.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnchorLockFile,
|
|
3
|
+
AnchorResolverRegistry,
|
|
4
|
+
AnchorString,
|
|
5
|
+
ExecuteConfig,
|
|
6
|
+
Executor,
|
|
7
|
+
Finding,
|
|
8
|
+
ResolutionStatus,
|
|
9
|
+
RunResult,
|
|
10
|
+
ServiceToken,
|
|
11
|
+
SourceReader,
|
|
12
|
+
} from './contracts.ts'
|
|
13
|
+
|
|
14
|
+
export const IR_VERSION = 1
|
|
15
|
+
|
|
16
|
+
export type RendererId = string
|
|
17
|
+
|
|
18
|
+
export interface MdastNode {
|
|
19
|
+
type: string
|
|
20
|
+
children?: MdastNode[]
|
|
21
|
+
value?: string
|
|
22
|
+
[key: string]: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SourcePos {
|
|
26
|
+
file: string
|
|
27
|
+
line: number
|
|
28
|
+
endLine?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IRDocument {
|
|
32
|
+
ir: typeof IR_VERSION
|
|
33
|
+
meta: {
|
|
34
|
+
title: string
|
|
35
|
+
entry: string
|
|
36
|
+
lang?: string
|
|
37
|
+
translationOf?: { entry: string, digest: string }
|
|
38
|
+
[key: string]: unknown
|
|
39
|
+
}
|
|
40
|
+
config: {
|
|
41
|
+
plugins: string[]
|
|
42
|
+
presmium: Record<string, unknown>
|
|
43
|
+
renderers: Record<RendererId, Record<string, unknown>>
|
|
44
|
+
execute?: ExecuteConfig
|
|
45
|
+
translations?: Record<string, string>
|
|
46
|
+
}
|
|
47
|
+
sections: IRSection[]
|
|
48
|
+
diagnostics: Finding[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface IRSection {
|
|
52
|
+
id: string
|
|
53
|
+
index: number
|
|
54
|
+
title: string
|
|
55
|
+
depth: number
|
|
56
|
+
source: SourcePos
|
|
57
|
+
steps: number
|
|
58
|
+
stepMap?: number[]
|
|
59
|
+
hints: Record<RendererId, Record<string, unknown>>
|
|
60
|
+
config?: Record<string, Record<string, unknown>>
|
|
61
|
+
blocks: IRBlock[]
|
|
62
|
+
audience?: string[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type BlockRole = 'visual' | 'narration'
|
|
66
|
+
|
|
67
|
+
export interface IRBlock {
|
|
68
|
+
id: string
|
|
69
|
+
role: BlockRole
|
|
70
|
+
at: number
|
|
71
|
+
until?: number
|
|
72
|
+
span?: number
|
|
73
|
+
only?: RendererId[]
|
|
74
|
+
audience?: string[]
|
|
75
|
+
source: SourcePos
|
|
76
|
+
node: IRNode
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type IRNode = MarkdownNode | CodeNode | ComponentNode
|
|
80
|
+
|
|
81
|
+
export interface MarkdownNode {
|
|
82
|
+
type: 'markdown'
|
|
83
|
+
mdast: MdastNode
|
|
84
|
+
reveal?: 'items'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CodeOrigin {
|
|
88
|
+
anchor: AnchorString
|
|
89
|
+
file: string
|
|
90
|
+
startLine: number
|
|
91
|
+
endLine: number
|
|
92
|
+
pin?: string
|
|
93
|
+
permalink?: string
|
|
94
|
+
status: ResolutionStatus
|
|
95
|
+
units?: Record<string, { startLine: number; endLine: number }>
|
|
96
|
+
drift?: 'synced' | 'drifted'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface RunSpec {
|
|
100
|
+
lang: string
|
|
101
|
+
args?: Record<string, string>
|
|
102
|
+
stdin?: string
|
|
103
|
+
timeoutMs?: number
|
|
104
|
+
executor?: string
|
|
105
|
+
when: 'compile' | 'live' | 'both'
|
|
106
|
+
session?: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface RecordedRun extends RunResult {
|
|
110
|
+
cacheKey: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CodeVariant {
|
|
114
|
+
label: string
|
|
115
|
+
value: string
|
|
116
|
+
permalink?: string
|
|
117
|
+
startLine?: number
|
|
118
|
+
endLine?: number
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface CodeNode {
|
|
122
|
+
type: 'code'
|
|
123
|
+
lang: string
|
|
124
|
+
value: string
|
|
125
|
+
title?: string
|
|
126
|
+
origin?: CodeOrigin
|
|
127
|
+
highlight?: number[][]
|
|
128
|
+
run?: RunSpec
|
|
129
|
+
outputs?: RecordedRun[]
|
|
130
|
+
variants?: CodeVariant[]
|
|
131
|
+
echo?: false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface ComponentNode {
|
|
135
|
+
type: 'component'
|
|
136
|
+
plugin: string
|
|
137
|
+
name: string
|
|
138
|
+
props: Record<string, unknown>
|
|
139
|
+
children?: IRBlock[]
|
|
140
|
+
fallback?: MdastNode
|
|
141
|
+
assets?: Record<string, string>
|
|
142
|
+
layer?: 'overlay'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type DirectiveOptionType = 'string' | 'number' | 'boolean' | 'list'
|
|
146
|
+
|
|
147
|
+
export interface DirectiveInput {
|
|
148
|
+
name: string
|
|
149
|
+
arg?: string
|
|
150
|
+
options: Record<string, unknown>
|
|
151
|
+
body: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface DirectiveContext {
|
|
155
|
+
file: string
|
|
156
|
+
deckDir: string
|
|
157
|
+
workspaceRoot: string
|
|
158
|
+
lang?: string
|
|
159
|
+
config: Record<string, unknown>
|
|
160
|
+
source: SourcePos
|
|
161
|
+
parse(markdown: string): MdastNode
|
|
162
|
+
report(finding: Finding): void
|
|
163
|
+
watch(...files: string[]): void
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface DirectiveResult {
|
|
167
|
+
node: IRNode
|
|
168
|
+
role?: BlockRole
|
|
169
|
+
span?: number
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface RoleInput {
|
|
173
|
+
name: string
|
|
174
|
+
value: string
|
|
175
|
+
options: Record<string, unknown>
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface RoleSpec {
|
|
179
|
+
name: string
|
|
180
|
+
options?: Record<string, DirectiveOptionType>
|
|
181
|
+
children?(input: RoleInput, ctx: DirectiveContext): MdastNode[]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface PresmiumRoleNode extends MdastNode {
|
|
185
|
+
type: 'presmiumRole'
|
|
186
|
+
plugin: string
|
|
187
|
+
name: string
|
|
188
|
+
options: Record<string, unknown>
|
|
189
|
+
children: MdastNode[]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface DirectiveSpec {
|
|
193
|
+
name: string
|
|
194
|
+
arg?: boolean | 'anchor'
|
|
195
|
+
body?: boolean
|
|
196
|
+
options?: Record<string, DirectiveOptionType>
|
|
197
|
+
toIR(input: DirectiveInput, ctx: DirectiveContext): DirectiveResult | Promise<DirectiveResult>
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface RunCache {
|
|
201
|
+
get(key: string): RecordedRun | undefined
|
|
202
|
+
set(key: string, run: RecordedRun): void
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface PassContext {
|
|
206
|
+
deckDir: string
|
|
207
|
+
workspaceRoot: string
|
|
208
|
+
lang?: string
|
|
209
|
+
src: SourceReader
|
|
210
|
+
resolvers: AnchorResolverRegistry
|
|
211
|
+
lock: AnchorLockFile
|
|
212
|
+
executors: Executor[]
|
|
213
|
+
cache: RunCache
|
|
214
|
+
config: Record<string, unknown>
|
|
215
|
+
configFor(section: IRSection): Record<string, unknown>
|
|
216
|
+
report(finding: Finding): void
|
|
217
|
+
watch(...files: string[]): void
|
|
218
|
+
service?<T>(token: ServiceToken<T>): T | undefined
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface IRPass {
|
|
222
|
+
name: string
|
|
223
|
+
order?: number
|
|
224
|
+
after?: 'execute'
|
|
225
|
+
run(doc: IRDocument, ctx: PassContext): void | Promise<void>
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export type Support = 'full' | 'degraded' | 'none'
|
|
229
|
+
|
|
230
|
+
export interface RenderContext {
|
|
231
|
+
deckDir: string
|
|
232
|
+
workspaceRoot: string
|
|
233
|
+
outDir: string
|
|
234
|
+
workDir: string
|
|
235
|
+
config: Record<string, unknown>
|
|
236
|
+
extensions: RendererExtension[]
|
|
237
|
+
plugins: string[]
|
|
238
|
+
pluginDirs: Record<string, string>
|
|
239
|
+
lang?: string
|
|
240
|
+
renderers: RendererId[]
|
|
241
|
+
base: string
|
|
242
|
+
vitePlugins: unknown[]
|
|
243
|
+
extensionFor(node: ComponentNode): ((node: ComponentNode, block: IRBlock) => unknown) | undefined
|
|
244
|
+
portable(value: unknown, from: string): unknown
|
|
245
|
+
localAsset(source: SourcePos, url: string): LocalAsset | undefined
|
|
246
|
+
documentLink?(source: SourcePos, url: string): DocumentLink | undefined
|
|
247
|
+
report(finding: Finding): void
|
|
248
|
+
service?<T>(token: ServiceToken<T>): T | undefined
|
|
249
|
+
translations?: { lang: string, doc: IRDocument }[]
|
|
250
|
+
server?: { url: string }
|
|
251
|
+
preview?: { origins: string[] }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface LocalAsset {
|
|
255
|
+
file: string
|
|
256
|
+
name: string
|
|
257
|
+
directory?: boolean
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface DocumentLink {
|
|
261
|
+
dir: string
|
|
262
|
+
fragment?: string
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface RenderOutput {
|
|
266
|
+
outDir: string
|
|
267
|
+
skipped: { section: string; block: string; reason: string }[]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface RenderDevServer {
|
|
271
|
+
url: string
|
|
272
|
+
update(doc: IRDocument): Promise<void>
|
|
273
|
+
close(): Promise<void>
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface Renderer {
|
|
277
|
+
id: RendererId
|
|
278
|
+
site?: boolean
|
|
279
|
+
support(node: IRNode, extensions: RendererExtension[]): Support
|
|
280
|
+
prepare?(doc: IRDocument, ctx: RenderContext): Promise<void>
|
|
281
|
+
build(doc: IRDocument, ctx: RenderContext): Promise<RenderOutput>
|
|
282
|
+
dev?(doc: IRDocument, ctx: RenderContext): Promise<RenderDevServer>
|
|
283
|
+
buildSite?(site: SiteSpec, ctx: RenderContext): Promise<RenderOutput>
|
|
284
|
+
devSite?(site: SiteSpec, ctx: RenderContext, compile: (dir: string) => Promise<IRDocument>): Promise<SiteDevServer>
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface SiteDocument {
|
|
288
|
+
dir: string
|
|
289
|
+
title: string
|
|
290
|
+
lang?: string
|
|
291
|
+
tags?: string[]
|
|
292
|
+
doc?: IRDocument
|
|
293
|
+
translations?: { lang: string, doc: IRDocument }[]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface SiteSpec {
|
|
297
|
+
title: string
|
|
298
|
+
documents: SiteDocument[]
|
|
299
|
+
home?: string
|
|
300
|
+
config: Record<string, unknown>
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface SiteDevServer {
|
|
304
|
+
url: string
|
|
305
|
+
update(dir: string, doc: IRDocument): Promise<void>
|
|
306
|
+
close(): Promise<void>
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export type ComponentRenderer = (node: ComponentNode, block: IRBlock, ctx: RenderContext) => unknown
|
|
310
|
+
|
|
311
|
+
export interface RendererContribution {
|
|
312
|
+
components?: Record<string, ComponentRenderer>
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface RendererExtension {
|
|
316
|
+
plugin: string
|
|
317
|
+
components?: Record<string, (node: ComponentNode, block: IRBlock) => unknown>
|
|
318
|
+
}
|
package/src/kernel.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { KERNEL_VERSION } from './contracts.ts'
|
|
2
|
+
import type {
|
|
3
|
+
EnvironmentSpec,
|
|
4
|
+
EventBus,
|
|
5
|
+
KernelEvent,
|
|
6
|
+
Logger,
|
|
7
|
+
PluginContext,
|
|
8
|
+
PluginDefinition,
|
|
9
|
+
PluginReport,
|
|
10
|
+
PresentationMode,
|
|
11
|
+
ServiceToken,
|
|
12
|
+
StartReport,
|
|
13
|
+
} from './contracts.ts'
|
|
14
|
+
import { errorMessage } from './errors.ts'
|
|
15
|
+
import { createEventBus } from './events.ts'
|
|
16
|
+
import { prefixed } from './logging.ts'
|
|
17
|
+
import { ServiceRegistry } from './services.ts'
|
|
18
|
+
|
|
19
|
+
export interface KernelOptions {
|
|
20
|
+
mode: PresentationMode
|
|
21
|
+
config?: Record<string, unknown>
|
|
22
|
+
log?: Logger
|
|
23
|
+
events?: EventBus
|
|
24
|
+
environments?: Record<string, EnvironmentSpec>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class Kernel {
|
|
28
|
+
readonly version = KERNEL_VERSION
|
|
29
|
+
readonly mode: PresentationMode
|
|
30
|
+
readonly services = new ServiceRegistry()
|
|
31
|
+
readonly events: EventBus
|
|
32
|
+
#log: Logger
|
|
33
|
+
#config: Record<string, unknown>
|
|
34
|
+
#environments?: Readonly<Record<string, EnvironmentSpec>>
|
|
35
|
+
#plugins: PluginDefinition[] = []
|
|
36
|
+
#state: 'idle' | 'starting' | 'started' | 'disposed' = 'idle'
|
|
37
|
+
#report?: StartReport
|
|
38
|
+
|
|
39
|
+
constructor(options: KernelOptions) {
|
|
40
|
+
this.mode = options.mode
|
|
41
|
+
this.#config = options.config ?? {}
|
|
42
|
+
this.#environments = options.environments && Object.freeze({ ...options.environments })
|
|
43
|
+
this.#log = options.log ?? console
|
|
44
|
+
this.events = options.events ?? createEventBus((error, event) => {
|
|
45
|
+
this.#log.error(`[presmium] handler for "${event.type}" threw`, error)
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
register(plugin: PluginDefinition): void {
|
|
50
|
+
if (this.#state !== 'idle') throw new Error(`cannot register ${plugin.manifest.id}: kernel already started`)
|
|
51
|
+
if (this.#plugins.some(p => p.manifest.id === plugin.manifest.id)) {
|
|
52
|
+
throw new Error(`plugin ${plugin.manifest.id} is already registered`)
|
|
53
|
+
}
|
|
54
|
+
this.#plugins.push(plugin)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
provide<T>(token: ServiceToken<T>, impl: T): void {
|
|
58
|
+
this.services.provide(token, impl)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getService<T>(token: ServiceToken<T>): T | undefined {
|
|
62
|
+
return this.services.tryGet(token)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
report(): StartReport | undefined {
|
|
66
|
+
return this.#report
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async start(): Promise<StartReport> {
|
|
70
|
+
if (this.#state !== 'idle') throw new Error('kernel already started')
|
|
71
|
+
this.#state = 'starting'
|
|
72
|
+
const plan = this.services.plan(this.#plugins.map(p => p.manifest), this.version)
|
|
73
|
+
const reports = new Map<string, PluginReport>()
|
|
74
|
+
for (const [id, reason] of plan.excluded) reports.set(id, this.#disable(id, reason))
|
|
75
|
+
|
|
76
|
+
const order: string[] = []
|
|
77
|
+
for (const id of plan.order) {
|
|
78
|
+
const plugin = this.#plugins.find(p => p.manifest.id === id)!
|
|
79
|
+
const unavailable = (plugin.manifest.requires ?? []).filter(r => !this.services.has(r.token))
|
|
80
|
+
const missing = unavailable.map(r => r.token.id)
|
|
81
|
+
if (unavailable.some(r => !r.optional)) {
|
|
82
|
+
const required = unavailable.filter(r => !r.optional).map(r => r.token.id)
|
|
83
|
+
reports.set(id, this.#disable(id, `missing required services: ${required.join(', ')}`, missing))
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
order.push(id)
|
|
87
|
+
reports.set(id, await this.#setup(plugin, missing))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.#report = { order, plugins: this.#plugins.map(p => reports.get(p.manifest.id)!), cycles: plan.cycles }
|
|
91
|
+
this.#state = 'started'
|
|
92
|
+
return this.#report
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async dispose(): Promise<void> {
|
|
96
|
+
if (this.#state === 'disposed') return
|
|
97
|
+
this.#state = 'disposed'
|
|
98
|
+
const running = new Set(
|
|
99
|
+
this.#report?.plugins.filter(r => r.status === 'started' || r.status === 'degraded').map(r => r.id),
|
|
100
|
+
)
|
|
101
|
+
for (const id of [...(this.#report?.order ?? [])].reverse()) {
|
|
102
|
+
if (!running.has(id)) continue
|
|
103
|
+
const plugin = this.#plugins.find(p => p.manifest.id === id)!
|
|
104
|
+
try {
|
|
105
|
+
await plugin.dispose?.()
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.#log.error(`[presmium] ${id} dispose failed`, error)
|
|
108
|
+
}
|
|
109
|
+
this.services.revoke(id)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async #setup(plugin: PluginDefinition, missing: string[]): Promise<PluginReport> {
|
|
114
|
+
const { id, provides = [] } = plugin.manifest
|
|
115
|
+
try {
|
|
116
|
+
await plugin.setup(this.#context(plugin))
|
|
117
|
+
const unprovided = provides.filter(t => this.services.owner(t) !== id).map(t => t.id)
|
|
118
|
+
if (unprovided.length > 0) throw new Error(`declared but did not provide: ${unprovided.join(', ')}`)
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.services.revoke(id)
|
|
121
|
+
const reason = errorMessage(error)
|
|
122
|
+
this.#log.error(`[presmium] ${id} failed to start`, error)
|
|
123
|
+
this.#emit({ type: 'kernel.plugin.failed', payload: { id, error: reason } })
|
|
124
|
+
return { id, status: 'failed', reason }
|
|
125
|
+
}
|
|
126
|
+
if (missing.length > 0) {
|
|
127
|
+
this.#log.warn(`[presmium] ${id} started without optional services: ${missing.join(', ')}`)
|
|
128
|
+
this.#emit({ type: 'kernel.plugin.degraded', payload: { id, missing } })
|
|
129
|
+
return { id, status: 'degraded', reason: `optional services unavailable: ${missing.join(', ')}`, missing }
|
|
130
|
+
}
|
|
131
|
+
this.#emit({ type: 'kernel.plugin.started', payload: { id } })
|
|
132
|
+
return { id, status: 'started' }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#disable(id: string, reason: string, missing?: string[]): PluginReport {
|
|
136
|
+
this.#log.warn(`[presmium] ${id} disabled: ${reason}`)
|
|
137
|
+
this.#emit({ type: 'kernel.plugin.disabled', payload: { id, reason } })
|
|
138
|
+
return missing ? { id, status: 'disabled', reason, missing } : { id, status: 'disabled', reason }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#emit(event: KernelEvent): void {
|
|
142
|
+
this.events.emit(event)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#context(plugin: PluginDefinition): PluginContext {
|
|
146
|
+
const { id, provides = [], requires = [] } = plugin.manifest
|
|
147
|
+
const declared = new Set([...requires.map(r => r.token.id), ...provides.map(t => t.id)])
|
|
148
|
+
const assertDeclared = (token: ServiceToken<unknown>) => {
|
|
149
|
+
if (!declared.has(token.id)) throw new Error(`${id} did not declare service ${token.id} in its manifest`)
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
config: pluginConfig(this.#config[id]),
|
|
153
|
+
mode: this.mode,
|
|
154
|
+
provide: (token, impl) => {
|
|
155
|
+
if (!provides.some(t => t.id === token.id)) {
|
|
156
|
+
throw new Error(`${id} provides ${token.id} without declaring it in manifest.provides`)
|
|
157
|
+
}
|
|
158
|
+
this.services.provide(token, impl, id)
|
|
159
|
+
},
|
|
160
|
+
get: token => {
|
|
161
|
+
assertDeclared(token)
|
|
162
|
+
return this.services.get(token)
|
|
163
|
+
},
|
|
164
|
+
tryGet: token => {
|
|
165
|
+
assertDeclared(token)
|
|
166
|
+
return this.services.tryGet(token)
|
|
167
|
+
},
|
|
168
|
+
events: this.events,
|
|
169
|
+
log: prefixed(this.#log, id),
|
|
170
|
+
...this.#environments ? { environments: this.#environments } : {},
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function pluginConfig(value: unknown): Record<string, unknown> {
|
|
176
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {}
|
|
177
|
+
}
|
package/src/lock.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { isRenderable } from './contracts.ts'
|
|
2
|
+
import type { AnchorLockFile, RecordedAnchor, Resolution } from './contracts.ts'
|
|
3
|
+
import { errorMessage } from './errors.ts'
|
|
4
|
+
|
|
5
|
+
export const ANCHOR_LOCK_PATH = '.presmium/anchors.lock.json'
|
|
6
|
+
|
|
7
|
+
export class AnchorLock {
|
|
8
|
+
#anchors = new Map<string, RecordedAnchor>()
|
|
9
|
+
|
|
10
|
+
private constructor(anchors: Iterable<[string, RecordedAnchor]>) {
|
|
11
|
+
for (const [anchor, recorded] of anchors) this.set(anchor, recorded)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static empty(): AnchorLock {
|
|
15
|
+
return new AnchorLock([])
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static from(file: AnchorLockFile): AnchorLock {
|
|
19
|
+
return new AnchorLock(Object.entries(file.anchors))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static parse(text: string | undefined): AnchorLock {
|
|
23
|
+
if (text === undefined || text.trim() === '') return AnchorLock.empty()
|
|
24
|
+
let data: unknown
|
|
25
|
+
try {
|
|
26
|
+
data = JSON.parse(text)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(`invalid anchor lock: ${errorMessage(error)}`)
|
|
29
|
+
}
|
|
30
|
+
return AnchorLock.from(validate(data))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get size(): number {
|
|
34
|
+
return this.#anchors.size
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get(anchor: string): RecordedAnchor | undefined {
|
|
38
|
+
return this.#anchors.get(anchor)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set(anchor: string, recorded: RecordedAnchor): void {
|
|
42
|
+
this.#anchors.set(anchor, { fingerprint: recorded.fingerprint, file: recorded.file, startLine: recorded.startLine })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
record(anchor: string, resolution: Resolution): boolean {
|
|
46
|
+
const { location, fingerprint } = resolution
|
|
47
|
+
if (!isRenderable(resolution) || !location || !fingerprint) return false
|
|
48
|
+
this.set(anchor, { fingerprint, file: location.file, startLine: location.startLine })
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
retain(anchors: Iterable<string>): void {
|
|
53
|
+
const keep = new Set(anchors)
|
|
54
|
+
for (const anchor of [...this.#anchors.keys()]) {
|
|
55
|
+
if (!keep.has(anchor)) this.#anchors.delete(anchor)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
toJSON(): AnchorLockFile {
|
|
60
|
+
const keys = [...this.#anchors.keys()].sort()
|
|
61
|
+
return { version: 1, anchors: Object.fromEntries(keys.map(k => [k, this.#anchors.get(k)!])) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
serialize(): string {
|
|
65
|
+
return `${JSON.stringify(this.toJSON(), null, 2)}\n`
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validate(data: unknown): AnchorLockFile {
|
|
70
|
+
if (!isObject(data) || data.version !== 1 || !isObject(data.anchors)) {
|
|
71
|
+
throw new Error('invalid anchor lock: expected { "version": 1, "anchors": { ... } }')
|
|
72
|
+
}
|
|
73
|
+
for (const [anchor, record] of Object.entries(data.anchors)) {
|
|
74
|
+
const valid = isObject(record)
|
|
75
|
+
&& typeof record.fingerprint === 'string' && record.fingerprint !== ''
|
|
76
|
+
&& typeof record.file === 'string'
|
|
77
|
+
&& Number.isInteger(record.startLine) && (record.startLine as number) >= 1
|
|
78
|
+
if (!valid) throw new Error(`invalid anchor lock: bad record for "${anchor}"`)
|
|
79
|
+
}
|
|
80
|
+
return data as unknown as AnchorLockFile
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
84
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
85
|
+
}
|
package/src/logging.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Logger } from './contracts.ts'
|
|
2
|
+
|
|
3
|
+
export function prefixed(log: Logger, id: string): Logger {
|
|
4
|
+
const tag = `[${id}]`
|
|
5
|
+
return {
|
|
6
|
+
debug: (...args) => log.debug(tag, ...args),
|
|
7
|
+
info: (...args) => log.info(tag, ...args),
|
|
8
|
+
warn: (...args) => log.warn(tag, ...args),
|
|
9
|
+
error: (...args) => log.error(tag, ...args),
|
|
10
|
+
}
|
|
11
|
+
}
|