@lincyaw/compiler 1.0.0-rc.0 → 1.0.0-rc.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 +8 -4
- package/package.json +12 -13
- package/src/assets.ts +163 -0
- package/src/audience.ts +21 -216
- package/src/build.ts +101 -0
- package/src/{host.ts → compilation-actions.ts} +14 -7
- package/src/compile-queue.ts +28 -0
- package/src/compiler.ts +269 -195
- package/src/dev.ts +222 -0
- package/src/directives.ts +1 -0
- package/src/document-lowering.ts +176 -0
- package/src/environments.ts +6 -4
- package/src/execute.ts +50 -36
- package/src/export.ts +49 -0
- package/src/forbid.ts +6 -12
- package/src/frontmatter.ts +16 -0
- package/src/index.ts +21 -11
- package/src/{dependencies.ts → inputs.ts} +22 -13
- package/src/links.ts +21 -45
- package/src/logging.ts +16 -0
- package/src/node-services.ts +33 -0
- package/src/paths.ts +12 -26
- package/src/plugins.ts +53 -10
- package/src/references.ts +31 -20
- package/src/renderers.ts +61 -401
- package/src/sections.ts +6 -80
- package/src/serve.ts +58 -56
- package/src/source-lines.ts +47 -0
- package/src/translations.ts +51 -57
- package/src/validate.ts +3 -1
- package/src/views.ts +2 -0
- package/src/workspace-settings.ts +59 -0
- package/src/workspace.ts +17 -7
- package/src/blocks.ts +0 -557
- package/src/published.ts +0 -105
- package/src/settings.ts +0 -105
- package/src/source.ts +0 -300
- package/src/steps.ts +0 -66
package/src/compiler.ts
CHANGED
|
@@ -1,50 +1,49 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
2
|
import { existsSync, statSync } from 'node:fs'
|
|
3
3
|
import { readFile } from 'node:fs/promises'
|
|
4
4
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path'
|
|
5
|
-
import { ANCHOR_LOCK_PATH, AnchorLock, DEFAULT_LANG, IR_VERSION, PROCESS_SANDBOX, createAnchorResolverRegistry, docBlocks, environmentExecutor, errorMessage, failedRun, isRecord } from '@lincyaw/kernel'
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import { audienceDocument,
|
|
9
|
-
import type { Withheld } from './audience.ts'
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import type { CommittedFiles, Revision } from './dependencies.ts'
|
|
14
|
-
import { checkLanguages, checkRuns, checkSecrets, environmentExecutors, environmentProviders, readExecute } from './environments.ts'
|
|
5
|
+
import { ANCHOR_LOCK_PATH, AnchorLock, DEFAULT_LANG, IR_VERSION, PROCESS_SANDBOX, createAnchorResolverRegistry, docBlocks, environmentExecutor, errorMessage, failedRun, fnv1a64, isRecord, productionAssetIds, validateDocument } from '@lincyaw/kernel'
|
|
6
|
+
import { operatorSpeechSecrets, productionAssetFile } from '@lincyaw/kernel/node'
|
|
7
|
+
import type { AnchorResolverRegistry, CanonicalSource, CodeNode, EnvironmentContext, EnvironmentProvider, EnvironmentSpec, ExecuteConfig, Executor, Finding, IRDocument, IRPass, IRSection, NodeServiceContext, PassContext, RunContext, RunRequest, RunResult, ServiceToken, SourceCapabilities, SourceReader, TeachingDocument } from '@lincyaw/kernel'
|
|
8
|
+
import { audienceDocument, emptyReferences, filterForAudience, scanOutputs, withheld } from './audience.ts'
|
|
9
|
+
import type { AudienceMark, Withheld } from './audience.ts'
|
|
10
|
+
import { runAllChecks } from './compilation-actions.ts'
|
|
11
|
+
import { enqueueCompile } from './compile-queue.ts'
|
|
12
|
+
import { checkLanguages, checkRuns, checkSecrets, environmentExecutors, environmentProviders, toExecuteConfig } from './environments.ts'
|
|
15
13
|
import { executePass, FREEZE_DIR, FreezeCache, runEnvironment } from './execute.ts'
|
|
16
|
-
import type {
|
|
14
|
+
import type { RunEnvironmentOptions, RunStats } from './execute.ts'
|
|
17
15
|
import { numberFootnotes } from './footnotes.ts'
|
|
18
16
|
import { checkForbidden } from './forbid.ts'
|
|
19
|
-
import {
|
|
17
|
+
import { CompileInputs } from './inputs.ts'
|
|
18
|
+
import type { CommittedFiles, GitRead } from './inputs.ts'
|
|
20
19
|
import { checkLinks } from './links.ts'
|
|
21
|
-
import {
|
|
20
|
+
import { pluginLogger } from './logging.ts'
|
|
21
|
+
import { NodeServices } from './node-services.ts'
|
|
22
|
+
import { lowerDocument } from './document-lowering.ts'
|
|
23
|
+
import { DECK_ENTRY } from './paths.ts'
|
|
24
|
+
import { currentPluginName, DECK_PLUGIN, directoryComponents, isRetiredPlugin, listedPlugins, loadOperatorPlugins, loadPlugins, pluginId } from './plugins.ts'
|
|
22
25
|
import type { LoadedPlugin, PluginProblem, PluginSources } from './plugins.ts'
|
|
23
26
|
import { resolveReferences } from './references.ts'
|
|
24
|
-
import { splitSections } from './sections.ts'
|
|
25
|
-
import { NodeServices, operatorDir, operatorPlugins, pluginLogger, workspaceSettings } from './settings.ts'
|
|
26
|
-
import type { OperatorSettings } from './settings.ts'
|
|
27
|
-
import { readFrontmatter, sourceOf, withoutBom } from './source.ts'
|
|
28
|
-
import type { Parser } from './source.ts'
|
|
29
|
-
import { scheduleSteps } from './steps.ts'
|
|
30
27
|
import { applyTranslations, translationFields } from './translations.ts'
|
|
31
28
|
import { validateIR } from './validate.ts'
|
|
32
29
|
import { createSourceReader, findWorkspaceRoot } from './workspace.ts'
|
|
30
|
+
import { operatorDir, operatorPlugins, workspaceSettings } from './workspace-settings.ts'
|
|
31
|
+
import type { OperatorSettings } from './workspace-settings.ts'
|
|
33
32
|
|
|
34
|
-
export
|
|
33
|
+
export { DECK_ENTRY }
|
|
35
34
|
|
|
36
35
|
const BUILTIN_RENDERERS = ['web', 'slides', 'notebook']
|
|
37
36
|
|
|
38
|
-
const MYST_ROLES = new Set((defaultOptions.roles as unknown as { name: string, alias?: string[] }[]).flatMap(role => [role.name, ...role.alias ?? []]))
|
|
39
37
|
|
|
40
38
|
export interface CompilerOptions {
|
|
39
|
+
priority?: 'interactive' | 'background'
|
|
41
40
|
settings?: OperatorSettings
|
|
42
41
|
services?: [ServiceToken<unknown>, unknown][]
|
|
43
42
|
workspaceRoot?: string
|
|
44
43
|
bundled?: string
|
|
45
44
|
operatorDir?: string
|
|
46
45
|
secret?: (name: string) => string | undefined
|
|
47
|
-
execution?:
|
|
46
|
+
execution?: RunEnvironmentOptions
|
|
48
47
|
audience?: string
|
|
49
48
|
headmatter?: Record<string, unknown>
|
|
50
49
|
deckDir?: string
|
|
@@ -65,13 +64,19 @@ export interface DeclaredEnvironment {
|
|
|
65
64
|
|
|
66
65
|
export interface Compilation {
|
|
67
66
|
doc: IRDocument
|
|
67
|
+
sourceCapabilities?: SourceCapabilities
|
|
68
68
|
options: CompilerOptions
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
/** The canonical JSON authoring entry read by this compilation. */
|
|
70
|
+
sourceFiles: string[]
|
|
71
|
+
/** Fingerprints of exact authoring text, including BOM. Keys are absolute source paths. */
|
|
72
|
+
sourceFingerprints?: Record<string, string>
|
|
73
|
+
/** The other files the compilation read, and the files it read at a git ref. */
|
|
74
|
+
inputs: { files: string[], gitReads: GitRead[] }
|
|
75
|
+
runStats: RunStats
|
|
72
76
|
deckDir: string
|
|
73
77
|
entry: string
|
|
74
|
-
|
|
78
|
+
/** The entry's headmatter, merged with `CompilerOptions.headmatter`. */
|
|
79
|
+
headmatter: Record<string, unknown>
|
|
75
80
|
workspaceRoot: string
|
|
76
81
|
src: SourceReader
|
|
77
82
|
resolvers: AnchorResolverRegistry
|
|
@@ -88,19 +93,30 @@ export interface Compilation {
|
|
|
88
93
|
runContext: RunContext
|
|
89
94
|
run(node: CodeNode, signal?: AbortSignal): Promise<RunResult>
|
|
90
95
|
audience?: AudienceCompilation
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
/** The document for one audience, with the blocks that leak withheld content removed. */
|
|
97
|
+
audienceDocument(name: string): IRDocument
|
|
98
|
+
audienceCompilation(name: string): AudienceCompilation
|
|
93
99
|
publishChecks(options?: { externalLinks?: boolean }): Promise<Finding[]>
|
|
94
100
|
check(options?: { externalLinks?: boolean }): Promise<Finding[]>
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
export interface CompileOptions {
|
|
98
104
|
pruneCache?: boolean
|
|
105
|
+
signal?: AbortSignal
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Plugin services assembled from the exact authoring source, without running compilation passes. */
|
|
109
|
+
export interface DocumentNodeServices {
|
|
110
|
+
source: CanonicalSource
|
|
111
|
+
findings: Finding[]
|
|
112
|
+
service<T>(token: ServiceToken<T>): T | undefined
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
export function resolveEntry(deck: string): string {
|
|
102
116
|
const path = resolve(deck)
|
|
103
|
-
|
|
117
|
+
const entry = existsSync(path) && statSync(path).isDirectory() ? join(path, DECK_ENTRY) : path
|
|
118
|
+
if (basename(entry) !== DECK_ENTRY) throw new Error(`The document entry is ${DECK_ENTRY}; migrate the old source before compiling.`)
|
|
119
|
+
return entry
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
interface Workspace {
|
|
@@ -117,7 +133,46 @@ interface Plugins {
|
|
|
117
133
|
key: string
|
|
118
134
|
plugins: LoadedPlugin[]
|
|
119
135
|
problems: PluginProblem[]
|
|
120
|
-
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type Report = (finding: Finding) => void
|
|
139
|
+
|
|
140
|
+
type HeadmatterLine = (needle: string) => { file: string, line: number }
|
|
141
|
+
|
|
142
|
+
/** What the plugins of one compilation contribute to it. */
|
|
143
|
+
interface Contributions {
|
|
144
|
+
resolvers: AnchorResolverRegistry
|
|
145
|
+
executors: Executor[]
|
|
146
|
+
components: Map<string, string>
|
|
147
|
+
environments: Map<string, EnvironmentProvider>
|
|
148
|
+
environmentContext: EnvironmentContext
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The state of one compilation that its steps share. */
|
|
152
|
+
interface CompileRun {
|
|
153
|
+
workspace: Workspace
|
|
154
|
+
headmatter: Record<string, unknown>
|
|
155
|
+
lang: string
|
|
156
|
+
plugins: LoadedPlugin[]
|
|
157
|
+
configOf(plugin: LoadedPlugin): Record<string, unknown>
|
|
158
|
+
service<T>(token: ServiceToken<T>): T | undefined
|
|
159
|
+
execute: ExecuteConfig
|
|
160
|
+
inputs: CompileInputs
|
|
161
|
+
findings: Finding[]
|
|
162
|
+
report: Report
|
|
163
|
+
at: HeadmatterLine
|
|
164
|
+
signal?: AbortSignal
|
|
165
|
+
document: TeachingDocument
|
|
166
|
+
rawSource: string
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface PassResults {
|
|
170
|
+
lock: AnchorLock
|
|
171
|
+
runStats: RunStats
|
|
172
|
+
runContext: RunContext
|
|
173
|
+
cache: FreezeCache
|
|
174
|
+
filled: WeakSet<object>
|
|
175
|
+
invalid: Finding[]
|
|
121
176
|
}
|
|
122
177
|
|
|
123
178
|
export class Compiler {
|
|
@@ -127,8 +182,7 @@ export class Compiler {
|
|
|
127
182
|
#workspace?: Workspace
|
|
128
183
|
readonly #committed: CommittedFiles = new Map()
|
|
129
184
|
#plugins?: Plugins
|
|
130
|
-
#
|
|
131
|
-
readonly #environments = new Map<string, Executor>()
|
|
185
|
+
readonly #environments = new WeakMap<EnvironmentProvider, Map<string, Executor>>()
|
|
132
186
|
|
|
133
187
|
constructor(deck: string, options: CompilerOptions = {}) {
|
|
134
188
|
this.entry = resolveEntry(deck)
|
|
@@ -136,67 +190,148 @@ export class Compiler {
|
|
|
136
190
|
this.options = options
|
|
137
191
|
}
|
|
138
192
|
|
|
139
|
-
|
|
193
|
+
compile(options: CompileOptions = {}): Promise<Compilation> {
|
|
194
|
+
return enqueueCompile(() => this.#compile(options), this.options.priority === 'interactive')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Authoring services must also work when a scene or a compilation pass cannot be built. */
|
|
198
|
+
async nodeServices(options: Pick<CompileOptions, 'signal'> = {}): Promise<DocumentNodeServices> {
|
|
199
|
+
const run = await this.#start(options)
|
|
200
|
+
options.signal?.throwIfAborted()
|
|
201
|
+
return {
|
|
202
|
+
source: { documentId: run.document.id, version: createHash('sha256').update(run.rawSource).digest('hex').slice(0, 20), fingerprint: fnv1a64(run.rawSource) },
|
|
203
|
+
findings: [...run.findings],
|
|
204
|
+
service: run.service,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async #compile(options: CompileOptions): Promise<Compilation> {
|
|
209
|
+
const run = await this.#start(options)
|
|
210
|
+
const { headmatter, execute, findings, report } = run
|
|
211
|
+
const contributions = await this.#collectContributions(run)
|
|
212
|
+
const { sections, marks, sourceFiles, sourceFingerprints } = await this.#compileSections(run, contributions)
|
|
213
|
+
|
|
214
|
+
const { title: _title, lang: _lang, plugins: _plugins, presmium, renderers, sectionDepth: _sectionDepth, execute: _execute, translations: _translations, translationOf: _translationOf, ...rest } = headmatter
|
|
215
|
+
const doc: IRDocument = {
|
|
216
|
+
ir: IR_VERSION,
|
|
217
|
+
source: { documentId: run.document.id, version: createHash('sha256').update(run.rawSource).digest('hex').slice(0, 20), fingerprint: fnv1a64(run.rawSource) },
|
|
218
|
+
meta: { ...rest, title: titleOf(headmatter, this.entry), lang: run.lang, entry: this.entry, documentId: run.document.id },
|
|
219
|
+
config: {
|
|
220
|
+
plugins: listedPlugins(stringList(headmatter.plugins)),
|
|
221
|
+
presmium: isRecord(presmium) ? presmium : {},
|
|
222
|
+
renderers: isRecord(renderers) ? recordOfRecords(renderers) : {},
|
|
223
|
+
...Object.keys(execute).length > 0 ? { execute } : {},
|
|
224
|
+
},
|
|
225
|
+
sections,
|
|
226
|
+
diagnostics: [],
|
|
227
|
+
production: structuredClone(run.document.production),
|
|
228
|
+
productionAssets: Object.fromEntries(productionAssetIds(run.document.production, run.document.sectionOrder.map(id => ({ id, stepIds: run.document.sections[id]!.stepOrder }))).map(id => [id, structuredClone(run.document.assets[id]!)])),
|
|
229
|
+
}
|
|
230
|
+
applyTranslations(doc, translationFields(headmatter), report)
|
|
231
|
+
let audience: AudienceCompilation | undefined
|
|
232
|
+
if (this.options.audience) {
|
|
233
|
+
const name = this.options.audience
|
|
234
|
+
const filtered = filterForAudience(doc, name, marks)
|
|
235
|
+
doc.sections = filtered.doc.sections
|
|
236
|
+
doc.meta.audience = name
|
|
237
|
+
doc.production = filtered.doc.production
|
|
238
|
+
doc.productionAssets = filtered.doc.productionAssets
|
|
239
|
+
audience = audienceCompilation(name, withheld(filtered.removed, doc))
|
|
240
|
+
const left = findings.filter(finding => !filtered.keeps(finding))
|
|
241
|
+
findings.splice(0, findings.length, ...findings.filter(filtered.keeps))
|
|
242
|
+
if (left.length > 0) {
|
|
243
|
+
const severity = left.some(f => f.severity === 'error') ? 'error' : left.some(f => f.severity === 'warning') ? 'warning' : 'info'
|
|
244
|
+
report({ checkId: 'presmium.audience', severity, message: `${left.length} finding${left.length === 1 ? ' is' : 's are'} in the content left out for ${name}, not shown here: presmium check without --audience shows ${left.length === 1 ? 'it' : 'them'}`, source: { file: this.entry, line: 1 } })
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
checkRuns(doc, execute, report)
|
|
248
|
+
const unsupported = checkLanguages(doc, execute, contributions.executors, report)
|
|
249
|
+
|
|
250
|
+
numberFootnotes(doc, sourceFiles)
|
|
251
|
+
|
|
252
|
+
const passes = await this.#runPasses(doc, run, contributions, unsupported)
|
|
253
|
+
findings.push(...passes.invalid)
|
|
254
|
+
options.signal?.throwIfAborted()
|
|
255
|
+
doc.diagnostics = sortFindings(findings)
|
|
256
|
+
// Reassert provenance after extension passes; authored metadata cannot replace the source identity.
|
|
257
|
+
doc.source = { documentId: run.document.id, version: createHash('sha256').update(run.rawSource).digest('hex').slice(0, 20), fingerprint: fnv1a64(run.rawSource) }
|
|
258
|
+
if (options.pruneCache && !audience && !findings.some(f => f.severity === 'error')) passes.cache.prune()
|
|
259
|
+
return this.#assemble(doc, run, contributions, { sourceFiles, sourceFingerprints, marks, audience, passes })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Reads the entry's headmatter, loads the plugins with their Node services and reads the execution settings. */
|
|
263
|
+
async #start(options: CompileOptions): Promise<CompileRun> {
|
|
264
|
+
options.signal?.throwIfAborted()
|
|
140
265
|
const workspace = this.#workspace ??= await this.#openWorkspace()
|
|
141
|
-
const { root: workspaceRoot,
|
|
266
|
+
const { root: workspaceRoot, settings } = workspace
|
|
142
267
|
const findings: Finding[] = []
|
|
143
268
|
const report = (finding: Finding) => findings.push(finding)
|
|
144
269
|
const read = await readFile(this.entry, 'utf8').catch(() => undefined)
|
|
145
270
|
if (read === undefined) throw new Error(`${this.entry} not found`)
|
|
146
|
-
const text =
|
|
271
|
+
const text = read.replace(/^\uFEFF/, '')
|
|
147
272
|
if (workspace.problem) report({ checkId: 'presmium.workspace', severity: 'error', message: workspace.problem, source: { file: this.entry, line: 1 } })
|
|
148
273
|
if (workspace.operatorProblem) report({ checkId: 'presmium.workspace', severity: 'error', message: workspace.operatorProblem, source: { file: this.entry, line: 1 } })
|
|
149
|
-
|
|
274
|
+
let parsed: unknown
|
|
275
|
+
try { parsed = JSON.parse(text) } catch (error) { throw new Error(`Invalid ${DECK_ENTRY}: ${errorMessage(error)}`) }
|
|
276
|
+
const validated = validateDocument(parsed)
|
|
277
|
+
if (!validated.document) throw new Error(validated.issues.map(issue => `${issue.path || '/'}: ${issue.message}`).join('\n'))
|
|
278
|
+
const document = validated.document
|
|
279
|
+
const headmatter = withHeadmatter({ ...document.metadata }, this.options.headmatter)
|
|
280
|
+
const defaultVoice = document.production.defaults.voiceSource
|
|
281
|
+
const lecture = isRecord((headmatter.renderers as Record<string, unknown> | undefined)?.lecture) ? (headmatter.renderers as Record<string, Record<string, unknown>>).lecture : {}
|
|
282
|
+
if (lecture && Object.hasOwn(lecture, 'script')) throw new Error('/metadata/renderers/lecture/script: External lecture scripts are not supported. Author narration in lecture.json narrations and bind it through each step narrationIds.')
|
|
283
|
+
if (defaultVoice?.type === 'tts' || document.production.defaults.trailingPauseMs !== undefined || Object.keys(lecture ?? {}).length) headmatter.renderers = { ...(isRecord(headmatter.renderers) ? headmatter.renderers : {}), lecture: { ...lecture, ...defaultVoice?.type === 'tts' ? { voice: defaultVoice.voice } : {}, ...document.production.defaults.trailingPauseMs !== undefined ? { gap: document.production.defaults.trailingPauseMs } : {} } }
|
|
150
284
|
const lang = typeof headmatter.lang === 'string' && headmatter.lang.trim() ? headmatter.lang.trim() : DEFAULT_LANG
|
|
151
|
-
const
|
|
152
|
-
const watch = (...files: string[]) => dependencies.watch(...files)
|
|
285
|
+
const inputs = new CompileInputs(workspaceRoot, this.#committed)
|
|
153
286
|
const presmium = isRecord(headmatter.presmium) ? headmatter.presmium : {}
|
|
154
287
|
const configOf = (plugin: LoadedPlugin) => {
|
|
155
288
|
const config = presmium[pluginId(plugin)]
|
|
156
289
|
return isRecord(config) ? config : {}
|
|
157
290
|
}
|
|
158
291
|
|
|
159
|
-
const
|
|
160
|
-
const
|
|
292
|
+
const listed = stringList(headmatter.plugins)
|
|
293
|
+
const loaded = await this.#loadPlugins(listedPlugins(listed), workspace)
|
|
294
|
+
const { plugins } = loaded
|
|
295
|
+
const problems = [...loaded.problems]
|
|
296
|
+
const services = new NodeServices(this.options.services)
|
|
297
|
+
for (const plugin of plugins) addServices(services, plugin, {
|
|
161
298
|
deckDir: this.deckDir,
|
|
162
299
|
workspaceRoot,
|
|
163
|
-
config: configOf(
|
|
164
|
-
settings: settings[pluginId(
|
|
300
|
+
config: configOf(plugin),
|
|
301
|
+
settings: settings[pluginId(plugin)] ?? {},
|
|
165
302
|
lang,
|
|
166
|
-
secret: this.options.secret ?? (name => process.env[name]),
|
|
303
|
+
secret: this.options.secret ?? operatorSpeechSecrets(workspace.operatorDir, name => process.env[name]),
|
|
167
304
|
...workspace.operatorDir ? { operatorDir: workspace.operatorDir } : {},
|
|
168
|
-
log: pluginLogger(pluginId(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
305
|
+
log: pluginLogger(pluginId(plugin)),
|
|
306
|
+
service: token => services.tryGet(token),
|
|
307
|
+
}, problems)
|
|
308
|
+
const service = <T>(token: ServiceToken<T>) => services.tryGet(token)
|
|
309
|
+
const at = (_name: string) => ({ file: this.entry, line: 1, format: 'json' as const, pointer: '/metadata' })
|
|
172
310
|
for (const p of problems) report({ checkId: 'presmium.plugins', severity: p.severity, message: p.message, source: at(p.name) })
|
|
173
|
-
const
|
|
311
|
+
for (const name of listed) {
|
|
312
|
+
if (isRetiredPlugin(name)) report({ checkId: 'presmium.plugins', severity: 'info', message: `plugin "${name}" is no longer needed: the viewer shows the slides itself; remove it from plugins`, source: at(name) })
|
|
313
|
+
else if (currentPluginName(name) !== name) report({ checkId: 'presmium.plugins', severity: 'info', message: `plugin "${name}" is now named "${currentPluginName(name)}"; the old name still works`, source: at(name) })
|
|
314
|
+
}
|
|
315
|
+
const execute = toExecuteConfig(headmatter.execute, report, at)
|
|
174
316
|
checkSecrets(headmatter, report, at)
|
|
317
|
+
return { workspace, headmatter, lang, plugins, configOf, service, execute, inputs, findings, report, at, signal: options.signal, document, rawSource: read }
|
|
318
|
+
}
|
|
175
319
|
|
|
176
|
-
|
|
177
|
-
|
|
320
|
+
/** Registers each plugin's directives, roles, anchor resolvers, executors, environment providers and components. */
|
|
321
|
+
async #collectContributions(run: CompileRun): Promise<Contributions> {
|
|
322
|
+
const { plugins, configOf, execute, report, at } = run
|
|
178
323
|
const resolvers = createAnchorResolverRegistry()
|
|
179
324
|
const executors: Executor[] = []
|
|
180
325
|
const providers = new Map<string, EnvironmentProvider>()
|
|
181
326
|
const components = new Map<string, string>()
|
|
182
327
|
for (const loaded of plugins) {
|
|
183
|
-
for (const name of await directoryComponents(loaded.dir))
|
|
328
|
+
for (const name of await directoryComponents(loaded.dir)) {
|
|
329
|
+
const owner = components.get(name)
|
|
330
|
+
if (owner === undefined) components.set(name, pluginId(loaded))
|
|
331
|
+
else report({ checkId: 'presmium.plugins', severity: 'warning', message: `plugin ${pluginId(loaded)}: component ${name} is already registered by ${owner}; its component is ignored`, source: at(loaded.name) })
|
|
332
|
+
}
|
|
184
333
|
const { plugin } = loaded
|
|
185
334
|
if (!plugin) continue
|
|
186
|
-
for (const spec of plugin.directives ?? []) {
|
|
187
|
-
if (BUILTIN_DIRECTIVES.has(spec.name) || directives.has(spec.name)) {
|
|
188
|
-
report({ checkId: 'presmium.plugins', severity: 'error', message: `plugin ${plugin.id}: directive {${spec.name}} is already defined`, source: at(loaded.name) })
|
|
189
|
-
} else {
|
|
190
|
-
directives.set(spec.name, { plugin: plugin.id, config: configOf(loaded), spec })
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
for (const spec of plugin.roles ?? []) {
|
|
194
|
-
if (MYST_ROLES.has(spec.name) || roles.has(spec.name)) {
|
|
195
|
-
report({ checkId: 'presmium.plugins', severity: 'error', message: `plugin ${plugin.id}: role {${spec.name}} is already defined`, source: at(loaded.name) })
|
|
196
|
-
} else {
|
|
197
|
-
roles.set(spec.name, { plugin: plugin.id, config: configOf(loaded), spec })
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
335
|
try {
|
|
201
336
|
for (const resolver of plugin.resolvers ?? []) resolvers.register(resolver)
|
|
202
337
|
} catch (error) {
|
|
@@ -207,84 +342,27 @@ export class Compiler {
|
|
|
207
342
|
}
|
|
208
343
|
for (const name of await directoryComponents(this.deckDir)) components.set(name, DECK_PLUGIN)
|
|
209
344
|
const secret = this.options.secret ?? (name => process.env[name])
|
|
210
|
-
const environmentContext: EnvironmentContext = { deckDir: this.deckDir, workspaceRoot, secret, log: pluginLogger('presmium.execute') }
|
|
211
|
-
const
|
|
212
|
-
executors.push(...await environmentExecutors(execute,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const source = await parser.expand(this.entry)
|
|
216
|
-
findings.push(...source.findings)
|
|
217
|
-
dependencies.watch(...source.missing)
|
|
218
|
-
const { title: _title, lang: _lang, plugins: _plugins, presmium: _presmium, renderers, sectionDepth: depth, execute: _execute, translations: _translations, translationOf: _translationOf, ...rest } = headmatter
|
|
219
|
-
const title = typeof headmatter.title === 'string' ? headmatter.title : basename(this.entry, extname(this.entry))
|
|
220
|
-
const sectionDepth = Number.isInteger(depth) && (depth as number) >= 1 ? depth as number : 2
|
|
221
|
-
|
|
222
|
-
const lifted = liftScopes(source.children, sectionDepth, (node, audience) => report({
|
|
223
|
-
checkId: 'presmium.audience',
|
|
224
|
-
severity: 'warning',
|
|
225
|
-
message: `this content follows an {include} with :audience: whose file opens a section, so it is part of that section, which is only for ${audience.join(', ')}`,
|
|
226
|
-
hint: 'start a section of its own with a heading after the {include}, or move it before the {include}',
|
|
227
|
-
source: sourceOf(node, this.entry),
|
|
228
|
-
}))
|
|
229
|
-
const marks = audienceMarks(lifted, this.entry)
|
|
230
|
-
const raw = splitSections(lifted, { entry: this.entry, title, sectionDepth }, report)
|
|
231
|
-
const rendererIds = new Set([...BUILTIN_RENDERERS, ...isRecord(renderers) ? Object.keys(renderers) : []])
|
|
232
|
-
const ctx = { entry: this.entry, deckDir: this.deckDir, workspaceRoot, lang, renderers: rendererIds, parser, directives, roles, components, execute, report, watch }
|
|
233
|
-
const sections: IRSection[] = []
|
|
234
|
-
for (const [i, section] of raw.entries()) {
|
|
235
|
-
const { blocks: drafts, hints, config, audience } = await sectionBlocks(section.nodes, ctx)
|
|
236
|
-
const { blocks, steps } = scheduleSteps(drafts, report)
|
|
237
|
-
drafts.forEach((draft, j) => {
|
|
238
|
-
if (draft.audience) blocks[j]!.audience = draft.audience
|
|
239
|
-
})
|
|
240
|
-
sections.push({
|
|
241
|
-
id: section.id,
|
|
242
|
-
index: i + 1,
|
|
243
|
-
title: section.title,
|
|
244
|
-
depth: section.depth,
|
|
245
|
-
source: section.source,
|
|
246
|
-
steps,
|
|
247
|
-
hints,
|
|
248
|
-
...Object.keys(config).length > 0 ? { config } : {},
|
|
249
|
-
blocks,
|
|
250
|
-
...audience ? { audience } : {},
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
markBlocks(sections, marks)
|
|
345
|
+
const environmentContext: EnvironmentContext = { deckDir: this.deckDir, workspaceRoot: run.workspace.root, secret, log: pluginLogger('presmium.execute') }
|
|
346
|
+
const environments = environmentProviders(execute, providers, report, at)
|
|
347
|
+
executors.push(...await environmentExecutors(execute, environments, environmentContext, this.#environments, report, at))
|
|
348
|
+
return { resolvers, executors, components, environments, environmentContext }
|
|
349
|
+
}
|
|
255
350
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
renderers: isRecord(renderers) ? recordOfRecords(renderers) : {},
|
|
263
|
-
...Object.keys(execute).length > 0 ? { execute } : {},
|
|
264
|
-
},
|
|
265
|
-
sections,
|
|
266
|
-
diagnostics: [],
|
|
267
|
-
}
|
|
268
|
-
applyTranslations(doc, translationFields(headmatter), report)
|
|
269
|
-
let audience: AudienceCompilation | undefined
|
|
270
|
-
if (this.options.audience) {
|
|
271
|
-
const name = this.options.audience
|
|
272
|
-
const view = audienceView(doc, name, marks)
|
|
273
|
-
doc.sections = view.doc.sections
|
|
274
|
-
doc.meta.audience = name
|
|
275
|
-
audience = audienceCompilation(name, withheld(view.removed, doc))
|
|
276
|
-
const left = findings.filter(finding => !view.keeps(finding))
|
|
277
|
-
findings.splice(0, findings.length, ...findings.filter(view.keeps))
|
|
278
|
-
if (left.length > 0) {
|
|
279
|
-
const severity = left.some(f => f.severity === 'error') ? 'error' : left.some(f => f.severity === 'warning') ? 'warning' : 'info'
|
|
280
|
-
report({ checkId: 'presmium.audience', severity, message: `${left.length} finding${left.length === 1 ? ' is' : 's are'} in the content left out for ${name}, not shown here: presmium check without --audience shows ${left.length === 1 ? 'it' : 'them'}`, source: { file: this.entry, line: 1 } })
|
|
281
|
-
}
|
|
351
|
+
/** Validated JSON is the sole runtime source; compiler output remains a derived snapshot. */
|
|
352
|
+
async #compileSections(run: CompileRun, _contributions: Contributions): Promise<{ sections: IRSection[], marks: AudienceMark[], sourceFiles: string[], sourceFingerprints: Record<string, string> }> {
|
|
353
|
+
const sections = lowerDocument(run.document, this.entry, this.deckDir, run.report, (...files) => run.inputs.watch(...files))
|
|
354
|
+
for (const id of productionAssetIds(run.document.production, run.document.sectionOrder.map(id => ({ id, stepIds: run.document.sections[id]!.stepOrder })))) {
|
|
355
|
+
const asset = run.document.assets[id], file = asset && productionAssetFile(this.deckDir, asset.location)
|
|
356
|
+
if (file) run.inputs.watch(file)
|
|
282
357
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
numberFootnotes(doc, source.files)
|
|
358
|
+
run.inputs.watch(this.entry)
|
|
359
|
+
return { sections, marks: [], sourceFiles: [this.entry], sourceFingerprints: { [this.entry]: fnv1a64(run.rawSource) } }
|
|
360
|
+
}
|
|
287
361
|
|
|
362
|
+
/** Runs the plugins' passes, resolves references and runs the code at compile time, validating the IR in between. */
|
|
363
|
+
async #runPasses(doc: IRDocument, run: CompileRun, contributions: Contributions, unsupported: ReadonlySet<CodeNode>): Promise<PassResults> {
|
|
364
|
+
const { plugins, configOf, workspace, inputs, report, at, signal } = run
|
|
365
|
+
const { executors, resolvers } = contributions
|
|
288
366
|
const lockFile = join(this.deckDir, ANCHOR_LOCK_PATH)
|
|
289
367
|
let lock = AnchorLock.empty()
|
|
290
368
|
try {
|
|
@@ -293,31 +371,33 @@ export class Compiler {
|
|
|
293
371
|
report({ checkId: 'presmium.lock', severity: 'error', message: errorMessage(error), source: { file: lockFile, line: 1 } })
|
|
294
372
|
}
|
|
295
373
|
const cache = new FreezeCache(join(this.deckDir, FREEZE_DIR), relative(this.deckDir, this.entry))
|
|
296
|
-
const runContext = this.#runContext(
|
|
374
|
+
const runContext = this.#runContext(workspace.root, workspace.settings, run.service, run.lang)
|
|
297
375
|
const passes = plugins.flatMap(loaded => (loaded.plugin?.passes ?? []).map(pass => ({ pass, loaded })))
|
|
298
376
|
.sort((a, b) => (a.pass.order ?? 0) - (b.pass.order ?? 0))
|
|
299
377
|
const runPass = async ({ pass, loaded }: { pass: IRPass, loaded: LoadedPlugin }) => {
|
|
378
|
+
signal?.throwIfAborted()
|
|
300
379
|
const config = configOf(loaded)
|
|
301
|
-
const
|
|
380
|
+
const passContext: PassContext = {
|
|
302
381
|
deckDir: this.deckDir,
|
|
303
|
-
workspaceRoot,
|
|
304
|
-
lang,
|
|
305
|
-
src:
|
|
306
|
-
resolvers:
|
|
382
|
+
workspaceRoot: workspace.root,
|
|
383
|
+
lang: run.lang,
|
|
384
|
+
src: inputs.reader(workspace.src),
|
|
385
|
+
resolvers: inputs.resolvers(resolvers),
|
|
307
386
|
lock: lock.toJSON(),
|
|
308
387
|
executors,
|
|
309
388
|
cache,
|
|
310
389
|
config,
|
|
311
390
|
configFor: section => ({ ...config, ...section.config?.[pluginId(loaded)] }),
|
|
312
391
|
report: finding => report({ ...finding, source: finding.source ?? at(loaded.name) }),
|
|
313
|
-
watch,
|
|
314
|
-
service,
|
|
392
|
+
watch: (...files) => inputs.watch(...files),
|
|
393
|
+
service: run.service,
|
|
315
394
|
}
|
|
316
395
|
try {
|
|
317
|
-
await pass.run(doc,
|
|
396
|
+
await pass.run(doc, passContext)
|
|
318
397
|
} catch (error) {
|
|
319
398
|
report({ checkId: 'presmium.plugins', severity: 'error', message: `pass ${pass.name} of plugin ${pluginId(loaded)} failed: ${errorMessage(error)}`, source: at(loaded.name) })
|
|
320
399
|
}
|
|
400
|
+
signal?.throwIfAborted()
|
|
321
401
|
}
|
|
322
402
|
const blame = (findings: Finding[], culprit: string): Finding[] => findings.map(finding => ({ ...finding, hint: culprit }))
|
|
323
403
|
let invalid = blame(validateIR(doc, this.entry), 'the IR is invalid before the plugin passes: a directive wrote it')
|
|
@@ -330,52 +410,58 @@ export class Compiler {
|
|
|
330
410
|
}
|
|
331
411
|
}
|
|
332
412
|
|
|
333
|
-
let
|
|
413
|
+
let runStats: RunStats = { executed: 0, cached: 0 }
|
|
334
414
|
let filled = new WeakSet<object>()
|
|
335
415
|
if (invalid.length === 0) {
|
|
336
416
|
filled = emptyReferences(doc)
|
|
337
417
|
resolveReferences(doc, report)
|
|
338
|
-
|
|
418
|
+
runStats = await executePass(doc, executors, cache, report, { ...this.options.execution, signal, rerun: run.execute.rerun === 'environment', context: runContext, unsupported })
|
|
339
419
|
for (const step of passes.filter(({ pass }) => pass.after === 'execute')) await runPass(step)
|
|
340
420
|
invalid = validateIR(doc, this.entry)
|
|
341
421
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (options.pruneCache && !audience && !findings.some(f => f.severity === 'error')) cache.prune()
|
|
345
|
-
const publish = { doc, entry: this.entry, deckDir: this.deckDir, workspaceRoot, renderers: publishedRenderers(doc, plugins) }
|
|
422
|
+
return { lock, runStats, runContext, cache, filled, invalid }
|
|
423
|
+
}
|
|
346
424
|
|
|
425
|
+
#assemble(doc: IRDocument, run: CompileRun, contributions: Contributions, results: { sourceFiles: string[], sourceFingerprints: Record<string, string>, marks: AudienceMark[], audience?: AudienceCompilation, passes: PassResults }): Compilation {
|
|
426
|
+
const { workspace, headmatter, plugins, execute, inputs } = run
|
|
427
|
+
const { executors } = contributions
|
|
428
|
+
const { marks, audience, passes } = results
|
|
429
|
+
const { runContext } = passes
|
|
430
|
+
const publish = { doc, entry: this.entry, deckDir: this.deckDir, workspaceRoot: workspace.root, renderers: Object.keys(run.document.metadata.renderers ?? {}).length ? Object.keys(run.document.metadata.renderers!) : [...new Set([...BUILTIN_RENDERERS, ...plugins.flatMap(loaded => loaded.plugin?.renderer ? [loaded.plugin.renderer.id] : [])])] }
|
|
347
431
|
const compilation: Compilation = {
|
|
348
432
|
doc,
|
|
433
|
+
sourceCapabilities: {},
|
|
349
434
|
options: this.options,
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
435
|
+
sourceFiles: results.sourceFiles,
|
|
436
|
+
sourceFingerprints: results.sourceFingerprints,
|
|
437
|
+
inputs: { files: inputs.files, gitReads: inputs.gitReads },
|
|
438
|
+
runStats: passes.runStats,
|
|
353
439
|
deckDir: this.deckDir,
|
|
354
440
|
entry: this.entry,
|
|
355
|
-
|
|
356
|
-
workspaceRoot,
|
|
357
|
-
src,
|
|
358
|
-
resolvers,
|
|
359
|
-
lock,
|
|
441
|
+
headmatter,
|
|
442
|
+
workspaceRoot: workspace.root,
|
|
443
|
+
src: workspace.src,
|
|
444
|
+
resolvers: contributions.resolvers,
|
|
445
|
+
lock: passes.lock,
|
|
360
446
|
plugins,
|
|
361
|
-
components,
|
|
362
|
-
service,
|
|
363
|
-
settings,
|
|
447
|
+
components: contributions.components,
|
|
448
|
+
service: run.service,
|
|
449
|
+
settings: workspace.settings,
|
|
364
450
|
operatorDir: workspace.operatorDir,
|
|
365
451
|
pluginSources: workspace.sources,
|
|
366
452
|
execute,
|
|
367
|
-
environments: [...
|
|
453
|
+
environments: [...contributions.environments].map(([name, provider]) => ({ name, spec: execute.environments![name]!, provider, context: contributions.environmentContext })),
|
|
368
454
|
executors,
|
|
369
455
|
runContext,
|
|
370
456
|
run: (node, signal) => this.#run(doc, node, executors, runContext, signal),
|
|
371
457
|
...audience ? { audience } : {},
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
return audienceCompilation(name, withheld(
|
|
458
|
+
audienceDocument: name => audienceDocument(doc, name, passes.filled, marks),
|
|
459
|
+
audienceCompilation: (name) => {
|
|
460
|
+
const filtered = filterForAudience(doc, name, marks)
|
|
461
|
+
return audienceCompilation(name, withheld(filtered.removed, filtered.doc))
|
|
376
462
|
},
|
|
377
463
|
publishChecks: async (checks = {}) => [...await checkLinks(publish, checks), ...checkForbidden(publish, headmatter)],
|
|
378
|
-
check: checks =>
|
|
464
|
+
check: checks => runAllChecks(compilation, checks),
|
|
379
465
|
}
|
|
380
466
|
return compilation
|
|
381
467
|
}
|
|
@@ -409,10 +495,10 @@ export class Compiler {
|
|
|
409
495
|
}
|
|
410
496
|
nodes = [...before, node]
|
|
411
497
|
}
|
|
412
|
-
const
|
|
413
|
-
const requests = nodes.map(code => ({ ...request(code), ...
|
|
498
|
+
const runSession = run.session ? `run:${randomUUID()}` : undefined
|
|
499
|
+
const requests = nodes.map(code => ({ ...request(code), ...runSession ? { session: runSession } : {} }))
|
|
414
500
|
const results = await Promise.all(requests.map(r => executor.run(r, signal, context)))
|
|
415
|
-
if (
|
|
501
|
+
if (runSession) await executor.endSession?.(runSession)
|
|
416
502
|
return results.at(-1)!
|
|
417
503
|
}
|
|
418
504
|
|
|
@@ -429,7 +515,7 @@ export class Compiler {
|
|
|
429
515
|
return workspace
|
|
430
516
|
}
|
|
431
517
|
|
|
432
|
-
async #loadPlugins(names: string[], workspace: Workspace
|
|
518
|
+
async #loadPlugins(names: string[], workspace: Workspace): Promise<Plugins> {
|
|
433
519
|
const key = JSON.stringify(names)
|
|
434
520
|
if (this.#plugins?.key === key) return this.#plugins
|
|
435
521
|
const operators = await loadOperatorPlugins(workspace.sources)
|
|
@@ -437,19 +523,10 @@ export class Compiler {
|
|
|
437
523
|
const dirs = new Set(operators.plugins.map(loaded => loaded.dir))
|
|
438
524
|
const plugins = [...operators.plugins, ...deck.plugins.filter(loaded => !dirs.has(loaded.dir))]
|
|
439
525
|
const problems = [...operators.problems, ...deck.problems]
|
|
440
|
-
|
|
441
|
-
for (const loaded of plugins) addServices(services, loaded, { ...context(loaded), service: token => services.get(token) }, problems)
|
|
442
|
-
this.#plugins = { key, plugins, problems, services }
|
|
526
|
+
this.#plugins = { key, plugins, problems }
|
|
443
527
|
return this.#plugins
|
|
444
528
|
}
|
|
445
529
|
|
|
446
|
-
#parserFor(directives: Map<string, PluginDirective>, roles: Map<string, PluginRole>): Parser {
|
|
447
|
-
const syntax = new Map([...BUILTIN_DIRECTIVES, ...[...directives.keys()].map(name => [name, false] as const)])
|
|
448
|
-
const options = new Map<string, Record<string, DirectiveOptionType> | undefined>([...roles].map(([name, { spec }]) => [name, spec.options]))
|
|
449
|
-
const key = JSON.stringify([[...syntax].sort(), [...options].sort()])
|
|
450
|
-
if (this.#parser?.key !== key) this.#parser = { key, parser: new AudienceParser(syntax, options) }
|
|
451
|
-
return this.#parser.parser
|
|
452
|
-
}
|
|
453
530
|
}
|
|
454
531
|
|
|
455
532
|
function addServices(services: NodeServices, loaded: LoadedPlugin, context: NodeServiceContext, problems: PluginProblem[]): void {
|
|
@@ -478,13 +555,13 @@ export async function workspacePlugins(root: string, options: CompilerOptions =
|
|
|
478
555
|
config: {},
|
|
479
556
|
settings: settings[pluginId(loaded)] ?? {},
|
|
480
557
|
lang: DEFAULT_LANG,
|
|
481
|
-
secret: options.secret ?? (name => process.env[name]),
|
|
558
|
+
secret: options.secret ?? operatorSpeechSecrets(operator, name => process.env[name]),
|
|
482
559
|
...operator ? { operatorDir: operator } : {},
|
|
483
|
-
service: token => services.
|
|
560
|
+
service: token => services.tryGet(token),
|
|
484
561
|
log: pluginLogger(pluginId(loaded)),
|
|
485
562
|
}, problems)
|
|
486
563
|
}
|
|
487
|
-
return { plugins, problems, service: token => services.
|
|
564
|
+
return { plugins, problems, service: token => services.tryGet(token) }
|
|
488
565
|
}
|
|
489
566
|
|
|
490
567
|
export function audienceCompilation(name: string, left: Withheld[]): AudienceCompilation {
|
|
@@ -498,11 +575,8 @@ function withHeadmatter(own: Record<string, unknown>, replaced?: Record<string,
|
|
|
498
575
|
return merged
|
|
499
576
|
}
|
|
500
577
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const end = lines.indexOf('---', 1)
|
|
504
|
-
const index = lines.slice(0, end < 0 ? undefined : end).findIndex(line => line.includes(needle))
|
|
505
|
-
return index < 0 ? 1 : index + 1
|
|
578
|
+
function titleOf(headmatter: Record<string, unknown>, entry: string): string {
|
|
579
|
+
return typeof headmatter.title === 'string' ? headmatter.title : basename(entry, extname(entry))
|
|
506
580
|
}
|
|
507
581
|
|
|
508
582
|
export function sortFindings(findings: Finding[]): Finding[] {
|