@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/references.ts
CHANGED
|
@@ -1,42 +1,53 @@
|
|
|
1
1
|
import { irBlocks, nodeMdast } from '@lincyaw/kernel'
|
|
2
|
-
import type { Finding, IRDocument, MdastNode } from '@lincyaw/kernel'
|
|
2
|
+
import type { Finding, IRDocument, MdastNode, SourcePos } from '@lincyaw/kernel'
|
|
3
3
|
import type { GenericParent } from 'myst-common'
|
|
4
4
|
import { normalizeLabel } from 'myst-common'
|
|
5
5
|
import { enumerateTargetsTransform, ReferenceState } from 'myst-transforms'
|
|
6
6
|
|
|
7
7
|
type Reference = Parameters<ReferenceState['resolveReferenceContent']>[0]
|
|
8
|
+
const IMPLICIT_TARGETS = new Set(['container', 'mathGroup', 'math', 'heading', 'proof'])
|
|
8
9
|
|
|
9
10
|
export function resolveReferences(doc: IRDocument, report: (finding: Finding) => void): void {
|
|
11
|
+
const references: { node: MdastNode, source: SourcePos }[] = []
|
|
12
|
+
for (const section of doc.sections) {
|
|
13
|
+
for (const block of irBlocks(section.blocks)) {
|
|
14
|
+
for (const tree of nodeMdast(block.node)) collectReferences(tree, block.source, references)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
10
17
|
const state = new ReferenceState(doc.meta.entry)
|
|
11
18
|
const targets: MdastNode[] = []
|
|
12
19
|
for (const section of doc.sections) {
|
|
13
20
|
targets.push({ type: 'heading', depth: Math.max(section.depth, 1), ...normalizeLabel(section.id), children: [{ type: 'text', value: section.title }] })
|
|
14
21
|
for (const block of irBlocks(section.blocks)) {
|
|
15
|
-
if (block.node.type === 'markdown')
|
|
22
|
+
if (block.node.type === 'markdown') for (const child of block.node.mdast.children ?? []) collectTargets(child, targets)
|
|
16
23
|
}
|
|
17
24
|
}
|
|
18
25
|
enumerateTargetsTransform({ type: 'root', children: targets } as GenericParent, { state })
|
|
19
26
|
|
|
20
|
-
for (const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} else if (!reference.children?.length) {
|
|
33
|
-
reference.children = resolved.children as MdastNode[]
|
|
34
|
-
}
|
|
35
|
-
}
|
|
27
|
+
for (const { node: reference, source } of references) {
|
|
28
|
+
const resolved = { ...reference } as Reference
|
|
29
|
+
state.resolveReferenceContent(resolved)
|
|
30
|
+
if (!resolved.resolved) {
|
|
31
|
+
report({
|
|
32
|
+
checkId: 'presmium.references',
|
|
33
|
+
severity: 'warning',
|
|
34
|
+
message: `{${String(reference.kind ?? 'ref')}} target "${String(reference.label ?? reference.identifier)}" not found`,
|
|
35
|
+
source: { file: source.file, line: source.line },
|
|
36
|
+
})
|
|
37
|
+
} else if (!reference.children?.length) {
|
|
38
|
+
reference.children = resolved.children as MdastNode[]
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
function
|
|
41
|
-
|
|
43
|
+
function collectReferences(node: MdastNode, source: SourcePos, into: { node: MdastNode, source: SourcePos }[]): void {
|
|
44
|
+
if (node.type === 'crossReference') into.push({ node, source })
|
|
45
|
+
else for (const child of node.children ?? []) collectReferences(child, source, into)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collectTargets(node: MdastNode, into: MdastNode[]): void {
|
|
49
|
+
// Keep target subtrees intact, including containers' subfigures and math groups.
|
|
50
|
+
// Unlabelled prose cannot change the enumeration or reference state.
|
|
51
|
+
if (node.identifier || node.enumerated || IMPLICIT_TARGETS.has(node.type)) into.push(node)
|
|
52
|
+
else for (const child of node.children ?? []) collectTargets(child, into)
|
|
42
53
|
}
|
package/src/renderers.ts
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import { existsSync, realpathSync, statSync, watch } from 'node:fs'
|
|
2
|
-
import type { FSWatcher } from 'node:fs'
|
|
3
1
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
4
|
-
import {
|
|
5
|
-
import { errorMessage,
|
|
6
|
-
import type { ComponentNode, ComponentRenderer,
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import { revisionFiles } from './dependencies.ts'
|
|
11
|
-
import { localAsset, portable, relativeIR } from './paths.ts'
|
|
12
|
-
import { directoryAsset, inside, lineOf, publishesDirectory } from './published.ts'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { errorMessage, findComponentRenderer, isRecord } from '@lincyaw/kernel'
|
|
4
|
+
import type { BoundRendererExtension, ComponentNode, ComponentRenderer, Exporter, Finding, IRBlock, IRDocument, NodeVitePlugin, RenderContext, RenderOutput, Renderer } from '@lincyaw/kernel'
|
|
5
|
+
import { documentLink, publishedAsset } from './assets.ts'
|
|
6
|
+
import type { Compilation, Compiler } from './compiler.ts'
|
|
7
|
+
import { portable } from './paths.ts'
|
|
13
8
|
import { loadPlugin, pluginId } from './plugins.ts'
|
|
9
|
+
import type { LoadedPlugin } from './plugins.ts'
|
|
14
10
|
import { compileTranslations } from './translations.ts'
|
|
15
11
|
import type { CompiledTranslation } from './translations.ts'
|
|
16
|
-
|
|
17
|
-
export const WORK_DIR = '.presmium'
|
|
12
|
+
import { viewFor } from './views.ts'
|
|
18
13
|
|
|
19
14
|
export const DEFAULT_RENDERER_PACKAGES: Record<string, string> = {
|
|
20
15
|
web: '@lincyaw/web',
|
|
@@ -22,30 +17,38 @@ export const DEFAULT_RENDERER_PACKAGES: Record<string, string> = {
|
|
|
22
17
|
notebook: '@lincyaw/render-notebook',
|
|
23
18
|
}
|
|
24
19
|
|
|
20
|
+
export const WEB = 'web'
|
|
21
|
+
|
|
25
22
|
export interface InstalledRenderer {
|
|
26
23
|
renderer: Renderer
|
|
27
24
|
package: string
|
|
25
|
+
/** The exporters of the renderer's package, with its configuration in the document. */
|
|
26
|
+
exporters: DocumentExporter[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** An exporter a document can use, with its plugin's configuration in the document. */
|
|
30
|
+
export interface DocumentExporter {
|
|
31
|
+
exporter: Exporter
|
|
32
|
+
config: Record<string, unknown>
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
workDir: string
|
|
34
|
-
base: string
|
|
35
|
-
skipped: RenderOutput['skipped']
|
|
35
|
+
function exportersOf(compilation: Compilation, loaded: LoadedPlugin): DocumentExporter[] {
|
|
36
|
+
const config = compilation.doc.config.presmium[pluginId(loaded)]
|
|
37
|
+
return (loaded.plugin?.exporters ?? []).map(exporter => ({ exporter, config: isRecord(config) ? config : {} }))
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
/** The renderers of one build or dev, and the base path of each. */
|
|
41
|
+
export interface RenderRun {
|
|
42
|
+
ids: string[]
|
|
43
|
+
bases: Map<string, string>
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
async function installedRenderers(compilation: Compilation): Promise<{ renderers: Map<string, InstalledRenderer>, missing: Map<string, string> }> {
|
|
45
47
|
const renderers = new Map<string, InstalledRenderer>()
|
|
46
48
|
const missing = new Map<string, string>()
|
|
47
|
-
for (const
|
|
48
|
-
|
|
49
|
+
for (const loaded of compilation.plugins) {
|
|
50
|
+
const { name, plugin } = loaded
|
|
51
|
+
if (plugin?.renderer && !renderers.has(plugin.renderer.id)) renderers.set(plugin.renderer.id, { renderer: plugin.renderer, package: name, exporters: exportersOf(compilation, loaded) })
|
|
49
52
|
}
|
|
50
53
|
const config = compilation.doc.config.renderers
|
|
51
54
|
for (const id of new Set([...Object.keys(DEFAULT_RENDERER_PACKAGES), ...Object.keys(config)])) {
|
|
@@ -57,13 +60,13 @@ async function installedRenderers(compilation: Compilation): Promise<{ renderers
|
|
|
57
60
|
}
|
|
58
61
|
const { loaded, problem } = await loadPlugin(pkg, compilation.deckDir, compilation.pluginSources)
|
|
59
62
|
const renderer = loaded?.plugin?.renderer
|
|
60
|
-
if (renderer?.id === id) renderers.set(id, { renderer, package: pkg })
|
|
63
|
+
if (loaded && renderer?.id === id) renderers.set(id, { renderer, package: pkg, exporters: exportersOf(compilation, loaded) })
|
|
61
64
|
else missing.set(id, problem?.message ?? `package ${pkg} provides no renderer "${id}"`)
|
|
62
65
|
}
|
|
63
66
|
return { renderers, missing }
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
async function selectRenderers(compilation: Compilation, to?: string[]): Promise<{ selected: InstalledRenderer[], findings: Finding[] }> {
|
|
69
|
+
export async function selectRenderers(compilation: Compilation, to?: string[]): Promise<{ selected: InstalledRenderer[], findings: Finding[] }> {
|
|
67
70
|
const { renderers, missing } = await installedRenderers(compilation)
|
|
68
71
|
const configured = Object.keys(compilation.doc.config.renderers)
|
|
69
72
|
const ids = to ?? (configured.length > 0 ? configured : [...renderers.keys()])
|
|
@@ -77,353 +80,57 @@ async function selectRenderers(compilation: Compilation, to?: string[]): Promise
|
|
|
77
80
|
return { selected, findings }
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
/** The exporters of the document's plugins and of its renderers' packages; one id once, the plugins' first. */
|
|
84
|
+
export async function documentExporters(compilation: Compilation): Promise<DocumentExporter[]> {
|
|
85
|
+
const { selected } = await selectRenderers(compilation)
|
|
86
|
+
const found = new Map<string, DocumentExporter>()
|
|
87
|
+
for (const own of [...compilation.plugins.flatMap(loaded => exportersOf(compilation, loaded)), ...selected.flatMap(installed => installed.exporters)]) {
|
|
88
|
+
if (!found.has(own.exporter.id)) found.set(own.exporter.id, own)
|
|
89
|
+
}
|
|
90
|
+
return [...found.values()]
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
function configuredBase(value: unknown): string | undefined {
|
|
93
|
+
export function configuredBase(value: unknown): string | undefined {
|
|
86
94
|
if (typeof value !== 'string' || !value.trim()) return undefined
|
|
87
95
|
const path = value.trim().replace(/^\/+|\/+$/g, '')
|
|
88
96
|
return path ? `/${path}/` : '/'
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
|
|
99
|
+
/** With `shared`, site renderers without a configured base share the output under `/<id>/`. */
|
|
100
|
+
export function planRenderRun(compilation: Compilation, selected: InstalledRenderer[], shared: boolean, base?: string): RenderRun {
|
|
92
101
|
const renderers = selected.map(({ renderer }) => renderer)
|
|
93
102
|
const configured = new Map(renderers.map(({ id }) => [id, configuredBase(compilation.doc.config.renderers[id]?.base)]))
|
|
94
103
|
const sites = renderers.filter(renderer => renderer.site)
|
|
95
104
|
const together = shared && sites.length > 1 && sites.every(({ id }) => configured.get(id) === undefined)
|
|
96
|
-
|
|
105
|
+
const run: RenderRun = {
|
|
97
106
|
ids: renderers.map(({ id }) => id),
|
|
98
107
|
bases: new Map(renderers.map(({ id, site }) => [id, configured.get(id) ?? (together && site ? `/${id}/` : '/')])),
|
|
99
108
|
}
|
|
109
|
+
const given = configuredBase(base)
|
|
110
|
+
if (given) for (const id of run.ids) run.bases.set(id, given)
|
|
111
|
+
return run
|
|
100
112
|
}
|
|
101
113
|
|
|
102
|
-
export
|
|
103
|
-
outDir: string
|
|
104
|
-
to?: string[]
|
|
105
|
-
audience?: string
|
|
106
|
-
quote?: boolean
|
|
107
|
-
scan?: 'outDir' | 'written'
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function build(whole: Compilation, options: BuildOptions): Promise<BuildReport> {
|
|
111
|
-
const compilation = options.audience && options.audience !== whole.audience?.name
|
|
112
|
-
? { ...whole, doc: whole.forAudience(options.audience), audience: whole.audienceOf(options.audience) }
|
|
113
|
-
: whole
|
|
114
|
-
const { doc } = compilation
|
|
115
|
-
const ir = await writeIR(relativeIR(doc, compilation.deckDir, compilation.workspaceRoot), options.outDir)
|
|
116
|
-
const { selected, findings } = await selectRenderers(compilation, options.to)
|
|
117
|
-
if (selected.length === 0) findings.push(renderFinding(compilation, 'warning', `no renderer available; only the IR was written to ${ir}`))
|
|
118
|
-
const run = runOf(compilation, selected, true)
|
|
119
|
-
const translated = await translationsFor(compilation, run)
|
|
120
|
-
findings.push(...translated.findings)
|
|
121
|
-
const results = await Promise.all(selected.map(async ({ renderer }) => {
|
|
122
|
-
const reported: Finding[] = []
|
|
123
|
-
const ctx = renderContext(compilation, renderer.id, join(options.outDir, renderer.id), join(compilation.deckDir, WORK_DIR, renderer.id), run, reported)
|
|
124
|
-
if (renderer.id === WEB) ctx.translations = translated.views
|
|
125
|
-
const view = viewFor(doc, renderer.id)
|
|
126
|
-
const unsupported = unsupportedBlocks(view, renderer, ctx.extensions)
|
|
127
|
-
try {
|
|
128
|
-
await mkdir(ctx.outDir, { recursive: true })
|
|
129
|
-
const output = await renderer.build(view, ctx)
|
|
130
|
-
const listed = new Set(output.skipped.map(s => `${s.section}#${s.block}`))
|
|
131
|
-
const result: RendererOutput = { renderer: renderer.id, outDir: output.outDir, workDir: ctx.workDir, base: ctx.base, skipped: [...output.skipped, ...unsupported.filter(s => !listed.has(`${s.section}#${s.block}`))] }
|
|
132
|
-
return { result, reported }
|
|
133
|
-
} catch (error) {
|
|
134
|
-
reported.push(renderFinding(compilation, 'error', `renderer ${renderer.id} failed: ${errorMessage(error)}`))
|
|
135
|
-
return { reported }
|
|
136
|
-
}
|
|
137
|
-
}))
|
|
138
|
-
const outputs = results.flatMap(({ result }) => result ? [result] : [])
|
|
139
|
-
if (compilation.audience) {
|
|
140
|
-
const scan = audienceScan(compilation.audience, translated.translations, doc)
|
|
141
|
-
const paths = options.scan === 'written' ? [ir, ...outputs.map(output => output.outDir)] : undefined
|
|
142
|
-
findings.push(...await scan.scan(options.outDir, paths, { quote: options.quote ?? true }))
|
|
143
|
-
}
|
|
144
|
-
return { ir, outputs, findings: unique([...doc.diagnostics, ...findings, ...results.flatMap(({ reported }) => reported)]) }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function audienceScan(audience: AudienceCompilation, translations: CompiledTranslation[], doc: IRDocument): AudienceCompilation {
|
|
148
|
-
const kept = [doc, ...translations.map(translation => translation.doc)]
|
|
149
|
-
const left = [audience, ...translations.flatMap(translation => (translation.audience ? [translation.audience] : []))].flatMap(own => own.withheld)
|
|
150
|
-
return audienceCompilation(audience.name, unshown(left, kept))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const WEB = 'web'
|
|
154
|
-
|
|
155
|
-
async function translationsFor(compilation: Compilation, run: Run, compilers?: Map<string, Compiler>): Promise<{ translations: CompiledTranslation[], views: NonNullable<RenderContext['translations']>, findings: Finding[] }> {
|
|
114
|
+
export async function translationsFor(compilation: Compilation, run: RenderRun, compilers?: Map<string, Compiler>): Promise<{ translations: CompiledTranslation[], views: NonNullable<RenderContext['translations']>, findings: Finding[] }> {
|
|
156
115
|
if (!run.ids.includes(WEB) || !compilation.doc.config.translations) return { translations: [], views: [], findings: [] }
|
|
157
116
|
const { translations, findings } = await compileTranslations(compilation, compilers)
|
|
158
117
|
return { translations, views: translations.map(({ lang, doc }) => ({ lang, doc: viewFor(doc, WEB) })), findings }
|
|
159
118
|
}
|
|
160
119
|
|
|
161
|
-
function
|
|
162
|
-
const seen = new Set<string>()
|
|
163
|
-
return findings.filter((finding) => {
|
|
164
|
-
const key = JSON.stringify([finding.checkId, finding.severity, finding.message, finding.source?.file, finding.source?.line])
|
|
165
|
-
if (seen.has(key)) return false
|
|
166
|
-
seen.add(key)
|
|
167
|
-
return true
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export async function prepare(compilation: Compilation, options: { outDir: string, to?: string[], base?: string }): Promise<{ outputs: Omit<RendererOutput, 'skipped'>[], findings: Finding[] }> {
|
|
172
|
-
const { selected, findings } = await selectRenderers(compilation, options.to)
|
|
173
|
-
const run = runOf(compilation, selected, true)
|
|
174
|
-
const base = configuredBase(options.base)
|
|
175
|
-
if (base) for (const id of run.ids) run.bases.set(id, base)
|
|
176
|
-
const outputs: Omit<RendererOutput, 'skipped'>[] = []
|
|
177
|
-
for (const { renderer } of selected) {
|
|
178
|
-
const ctx = renderContext(compilation, renderer.id, join(options.outDir, renderer.id), join(compilation.deckDir, WORK_DIR, renderer.id), run, findings)
|
|
179
|
-
if (!renderer.prepare) {
|
|
180
|
-
findings.push(renderFinding(compilation, 'warning', `renderer ${renderer.id} prepares nothing`))
|
|
181
|
-
continue
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
await renderer.prepare(viewFor(compilation.doc, renderer.id), ctx)
|
|
185
|
-
outputs.push({ renderer: renderer.id, outDir: ctx.outDir, workDir: ctx.workDir, base: ctx.base })
|
|
186
|
-
} catch (error) {
|
|
187
|
-
findings.push(renderFinding(compilation, 'error', `renderer ${renderer.id} failed: ${errorMessage(error)}`))
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return { outputs, findings: [...compilation.doc.diagnostics, ...findings] }
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export interface DevSession {
|
|
194
|
-
servers: { renderer: string, url: string }[]
|
|
195
|
-
ir: string
|
|
196
|
-
update(compilation: Compilation): Promise<void>
|
|
197
|
-
close(): Promise<void>
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export interface DevOptions {
|
|
201
|
-
outDir: string
|
|
202
|
-
to?: string[]
|
|
203
|
-
server?: { url: string }
|
|
204
|
-
onCompile?(compilation: Compilation, findings: Finding[]): void
|
|
205
|
-
compilation?: Compilation
|
|
206
|
-
base?: string
|
|
207
|
-
port?: number
|
|
208
|
-
preview?: { origins: string[] }
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export async function dev(compiler: Compiler, options: DevOptions): Promise<DevSession> {
|
|
212
|
-
let started = Date.now()
|
|
213
|
-
let compilation = options.compilation ?? await compiler.compile()
|
|
214
|
-
const ir = await writeIR(compilation.doc, options.outDir)
|
|
215
|
-
const { selected, findings } = await selectRenderers(compilation, options.to)
|
|
216
|
-
const run = runOf(compilation, selected, false)
|
|
217
|
-
const base = configuredBase(options.base)
|
|
218
|
-
if (base) for (const id of run.ids) run.bases.set(id, base)
|
|
219
|
-
const translators = new Map<string, Compiler>()
|
|
220
|
-
let translated = await translationsFor(compilation, run, translators)
|
|
221
|
-
findings.push(...translated.findings)
|
|
222
|
-
const translations = translated.views
|
|
223
|
-
const servers: { renderer: string, url: string, server: RenderDevServer }[] = []
|
|
224
|
-
for (const { renderer } of selected) {
|
|
225
|
-
if (!renderer.dev) {
|
|
226
|
-
findings.push(renderFinding(compilation, 'warning', `renderer ${renderer.id} has no dev server`))
|
|
227
|
-
continue
|
|
228
|
-
}
|
|
229
|
-
const outDir = join(options.outDir, renderer.id)
|
|
230
|
-
const ctx = renderContext(compilation, renderer.id, outDir, outDir, run, findings, options.server)
|
|
231
|
-
if (options.port !== undefined) ctx.config = { ...ctx.config, port: options.port }
|
|
232
|
-
if (options.preview) ctx.preview = { origins: [...options.preview.origins] }
|
|
233
|
-
if (renderer.id === WEB) {
|
|
234
|
-
ctx.translations = translations
|
|
235
|
-
}
|
|
236
|
-
try {
|
|
237
|
-
await mkdir(ctx.outDir, { recursive: true })
|
|
238
|
-
const server = await renderer.dev(viewFor(compilation.doc, renderer.id), ctx)
|
|
239
|
-
servers.push({ renderer: renderer.id, url: server.url, server })
|
|
240
|
-
} catch (error) {
|
|
241
|
-
findings.push(renderFinding(compilation, 'error', `renderer ${renderer.id} failed to start: ${errorMessage(error)}`))
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
if (servers.length === 0) findings.push(renderFinding(compilation, 'warning', `no renderer dev server; watching the source and writing the IR to ${ir}`))
|
|
245
|
-
const setup = (c: Compilation) => JSON.stringify([c.doc.config.plugins, c.doc.config.renderers])
|
|
246
|
-
const initial = setup(compilation)
|
|
247
|
-
|
|
248
|
-
const watchers = new Map<string, FSWatcher>()
|
|
249
|
-
let relevant = new Map<string, Set<string>>()
|
|
250
|
-
let watched = new Set<string>()
|
|
251
|
-
let timer: NodeJS.Timeout | undefined
|
|
252
|
-
let running = Promise.resolve()
|
|
253
|
-
let queued = false
|
|
254
|
-
let closed = false
|
|
255
|
-
const schedule = () => {
|
|
256
|
-
if (closed) return
|
|
257
|
-
clearTimeout(timer)
|
|
258
|
-
timer = setTimeout(() => {
|
|
259
|
-
if (queued) return
|
|
260
|
-
queued = true
|
|
261
|
-
running = running.then(() => {
|
|
262
|
-
queued = false
|
|
263
|
-
clearTimeout(timer)
|
|
264
|
-
return recompile()
|
|
265
|
-
})
|
|
266
|
-
}, 30)
|
|
267
|
-
}
|
|
268
|
-
const watchFiles = async () => {
|
|
269
|
-
const { files, revisions } = compilation.dependencies
|
|
270
|
-
const revs = new Map<string, Set<string>>()
|
|
271
|
-
for (const { file, rev } of revisions) revs.set(dirname(file), (revs.get(dirname(file)) ?? new Set<string>()).add(rev))
|
|
272
|
-
const git = await Promise.all([...revs].map(([dir, names]) => revisionFiles(dir, names)))
|
|
273
|
-
if (closed) return
|
|
274
|
-
watched = new Set([...compilation.files, ...files, ...git.flat(), ...translated.translations.flatMap(t => t.files)])
|
|
275
|
-
const next = new Map<string, Set<string>>()
|
|
276
|
-
for (const file of watched) {
|
|
277
|
-
let dir = dirname(file)
|
|
278
|
-
let name = basename(file)
|
|
279
|
-
while (!existsSync(dir) && dirname(dir) !== dir) {
|
|
280
|
-
name = basename(dir)
|
|
281
|
-
dir = dirname(dir)
|
|
282
|
-
}
|
|
283
|
-
next.set(dir, (next.get(dir) ?? new Set<string>()).add(name))
|
|
284
|
-
}
|
|
285
|
-
relevant = next
|
|
286
|
-
for (const [dir, watcher] of watchers) {
|
|
287
|
-
if (relevant.has(dir)) continue
|
|
288
|
-
watcher.close()
|
|
289
|
-
watchers.delete(dir)
|
|
290
|
-
}
|
|
291
|
-
for (const dir of relevant.keys()) {
|
|
292
|
-
if (watchers.has(dir)) continue
|
|
293
|
-
try {
|
|
294
|
-
watchers.set(dir, watch(dir, (_event, name) => {
|
|
295
|
-
if (name && relevant.get(dir)?.has(basename(name))) schedule()
|
|
296
|
-
}))
|
|
297
|
-
} catch {
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
if (!queued && [...watched].some(file => modifiedSince(file, started))) schedule()
|
|
301
|
-
}
|
|
302
|
-
const push = async (next: Compilation, reported: Finding[]) => {
|
|
303
|
-
compilation = next
|
|
304
|
-
await writeIR(compilation.doc, options.outDir)
|
|
305
|
-
reported.push(...compilation.doc.diagnostics)
|
|
306
|
-
translated = await translationsFor(compilation, run, translators)
|
|
307
|
-
reported.push(...translated.findings)
|
|
308
|
-
translations.splice(0, translations.length, ...translated.views)
|
|
309
|
-
if (servers.length > 0 && setup(compilation) !== initial) {
|
|
310
|
-
reported.push(renderFinding(compilation, 'warning', 'the plugins or renderers in the headmatter changed; restart presmium dev to load them in the dev servers'))
|
|
311
|
-
}
|
|
312
|
-
for (const { renderer, server } of servers) {
|
|
313
|
-
await server.update(viewFor(compilation.doc, renderer)).catch((error: unknown) => {
|
|
314
|
-
reported.push(renderFinding(compilation, 'error', `renderer ${renderer} failed to update: ${errorMessage(error)}`))
|
|
315
|
-
})
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
const recompile = async () => {
|
|
319
|
-
started = Date.now()
|
|
320
|
-
const reported: Finding[] = []
|
|
321
|
-
try {
|
|
322
|
-
await push(await compiler.compile(), reported)
|
|
323
|
-
} catch (error) {
|
|
324
|
-
reported.push(renderFinding(compilation, 'error', errorMessage(error)))
|
|
325
|
-
}
|
|
326
|
-
await watchFiles()
|
|
327
|
-
options.onCompile?.(compilation, reported)
|
|
328
|
-
}
|
|
329
|
-
if (!options.compilation) await watchFiles()
|
|
330
|
-
options.onCompile?.(compilation, [...compilation.doc.diagnostics, ...findings])
|
|
331
|
-
|
|
332
|
-
return {
|
|
333
|
-
servers: servers.map(({ renderer, url }) => ({ renderer, url })),
|
|
334
|
-
ir,
|
|
335
|
-
async update(next) {
|
|
336
|
-
if (closed) return
|
|
337
|
-
const reported: Finding[] = []
|
|
338
|
-
running = running.then(() => push(next, reported)).catch((error: unknown) => {
|
|
339
|
-
reported.push(renderFinding(next, 'error', errorMessage(error)))
|
|
340
|
-
})
|
|
341
|
-
await running
|
|
342
|
-
options.onCompile?.(compilation, reported)
|
|
343
|
-
},
|
|
344
|
-
async close() {
|
|
345
|
-
closed = true
|
|
346
|
-
clearTimeout(timer)
|
|
347
|
-
for (const watcher of watchers.values()) watcher.close()
|
|
348
|
-
await running
|
|
349
|
-
await Promise.all(servers.map(s => s.server.close()))
|
|
350
|
-
},
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function modifiedSince(file: string, time: number): boolean {
|
|
355
|
-
const modified = statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? 0
|
|
356
|
-
return modified >= time - 20 && modified <= Date.now()
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
export function viewFor(doc: IRDocument, renderer: string): IRDocument {
|
|
360
|
-
let changed = false
|
|
361
|
-
const keep = (blocks: IRBlock[]): IRBlock[] => blocks.flatMap((block) => {
|
|
362
|
-
if (block.only && !block.only.includes(renderer)) {
|
|
363
|
-
changed = true
|
|
364
|
-
return []
|
|
365
|
-
}
|
|
366
|
-
return block.node.type === 'component' && block.node.children ? [{ ...block, node: { ...block.node, children: keep(block.node.children) } }] : [block]
|
|
367
|
-
})
|
|
368
|
-
const sections = doc.sections.map((section) => {
|
|
369
|
-
changed = false
|
|
370
|
-
const blocks = keep(section.blocks)
|
|
371
|
-
return changed ? compactSteps({ ...section, blocks }) : section
|
|
372
|
-
})
|
|
373
|
-
return { ...doc, sections }
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function compactSteps(section: IRSection): IRSection {
|
|
377
|
-
const used = new Set([0])
|
|
378
|
-
for (const block of irBlocks(section.blocks)) {
|
|
379
|
-
if (block.role !== 'visual') continue
|
|
380
|
-
for (let step = block.at; step <= block.at + (block.span ?? 0); step++) used.add(step)
|
|
381
|
-
if (block.until !== undefined) used.add(block.until)
|
|
382
|
-
}
|
|
383
|
-
if (used.size > section.steps) return section
|
|
384
|
-
const stepMap: number[] = []
|
|
385
|
-
for (let step = 0, n = -1; step <= section.steps; step++) stepMap.push(used.has(step) ? ++n : n)
|
|
386
|
-
const map = (step: number) => stepMap[Math.min(step, section.steps)]!
|
|
387
|
-
const renumber = (blocks: IRBlock[]): IRBlock[] => blocks.map(block => ({
|
|
388
|
-
...block,
|
|
389
|
-
at: map(block.at),
|
|
390
|
-
...block.until === undefined ? {} : { until: map(block.until) },
|
|
391
|
-
...block.node.type === 'component' && block.node.children ? { node: { ...block.node, children: renumber(block.node.children) } } : {},
|
|
392
|
-
}))
|
|
393
|
-
return { ...section, steps: map(section.steps), stepMap, blocks: renumber(section.blocks) }
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
async function writeIR(doc: IRDocument, outDir: string): Promise<string> {
|
|
120
|
+
export async function writeIR(doc: IRDocument, outDir: string): Promise<string> {
|
|
397
121
|
const file = join(outDir, 'ir.json')
|
|
398
122
|
await mkdir(outDir, { recursive: true })
|
|
399
123
|
await writeFile(file, `${JSON.stringify(doc, null, 2)}\n`)
|
|
400
124
|
return file
|
|
401
125
|
}
|
|
402
126
|
|
|
403
|
-
function
|
|
404
|
-
const owner = extensions.find(e => e.plugin === node.plugin && e.components?.[node.name])
|
|
405
|
-
?? (node.plugin ? undefined : extensions.find(e => e.components?.[node.name]))
|
|
406
|
-
return owner?.components?.[node.name]
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function renderContext(compilation: Compilation, id: string, outDir: string, workDir: string, run: Run, findings: Finding[], server?: { url: string }): RenderContext {
|
|
127
|
+
export function renderContext(compilation: Compilation, id: string, outDir: string, workDir: string, run: RenderRun, findings: Finding[], server?: { url: string }): RenderContext {
|
|
410
128
|
const { deckDir, workspaceRoot, doc } = compilation
|
|
411
129
|
const config = doc.config.renderers[id]
|
|
412
130
|
const report = (finding: Finding) => {
|
|
413
131
|
const located = { ...finding, source: finding.source ?? { file: compilation.entry, line: 1 } }
|
|
414
132
|
if (!findings.some(f => JSON.stringify(f) === JSON.stringify(located))) findings.push(located)
|
|
415
133
|
}
|
|
416
|
-
const vitePlugins = compilation.plugins.flatMap((loaded) => {
|
|
417
|
-
const contribute = loaded.plugin?.vitePlugins
|
|
418
|
-
if (!contribute) return []
|
|
419
|
-
const options = doc.config.presmium[pluginId(loaded)]
|
|
420
|
-
try {
|
|
421
|
-
return contribute.call(loaded.plugin, { deckDir, workspaceRoot, config: isRecord(options) ? options : {} })
|
|
422
|
-
} catch (error) {
|
|
423
|
-
report({ checkId: 'presmium.render', severity: 'error', message: `plugin ${pluginId(loaded)} failed to give its Vite plugins: ${errorMessage(error)}` })
|
|
424
|
-
return []
|
|
425
|
-
}
|
|
426
|
-
})
|
|
427
134
|
const ctx: RenderContext = {
|
|
428
135
|
deckDir,
|
|
429
136
|
workspaceRoot,
|
|
@@ -436,8 +143,8 @@ function renderContext(compilation: Compilation, id: string, outDir: string, wor
|
|
|
436
143
|
lang: doc.meta.lang,
|
|
437
144
|
renderers: run.ids,
|
|
438
145
|
base: run.bases.get(id) ?? '/',
|
|
439
|
-
|
|
440
|
-
extensionFor: node =>
|
|
146
|
+
nodeVitePlugins: nodeVitePlugins(compilation),
|
|
147
|
+
extensionFor: node => findComponentRenderer(node, ctx.extensions),
|
|
441
148
|
portable: (value, from) => portable(value, from, workspaceRoot),
|
|
442
149
|
localAsset: (source, url) => publishedAsset(compilation, source, url, report),
|
|
443
150
|
documentLink: (source, url) => documentLink(compilation, source, url),
|
|
@@ -446,7 +153,7 @@ function renderContext(compilation: Compilation, id: string, outDir: string, wor
|
|
|
446
153
|
...server ? { server } : {},
|
|
447
154
|
}
|
|
448
155
|
const bind = (render: ComponentRenderer) => (node: ComponentNode, block: IRBlock) => render(node, block, ctx)
|
|
449
|
-
ctx.extensions = compilation.plugins.flatMap((loaded):
|
|
156
|
+
ctx.extensions = compilation.plugins.flatMap((loaded): BoundRendererExtension[] => {
|
|
450
157
|
const contribution = loaded.plugin?.rendererExtensions?.[id]
|
|
451
158
|
if (!contribution) return []
|
|
452
159
|
const components = contribution.components && Object.fromEntries(Object.entries(contribution.components).map(([name, render]) => [name, bind(render)]))
|
|
@@ -455,64 +162,17 @@ function renderContext(compilation: Compilation, id: string, outDir: string, wor
|
|
|
455
162
|
return ctx
|
|
456
163
|
}
|
|
457
164
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
let name = path
|
|
467
|
-
try {
|
|
468
|
-
name = decodeURIComponent(path)
|
|
469
|
-
} catch {
|
|
470
|
-
}
|
|
471
|
-
return resolve(dirname(source.file), name)
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function documentLink(compilation: Compilation, source: SourcePos, url: string): DocumentLink | undefined {
|
|
475
|
-
const target = localTarget(source, url)
|
|
476
|
-
if (!target) return undefined
|
|
477
|
-
const dir = basename(target) === DECK_ENTRY && statSync(target, { throwIfNoEntry: false })?.isFile() ? dirname(target)
|
|
478
|
-
: statSync(target, { throwIfNoEntry: false })?.isDirectory() && existsSync(join(target, DECK_ENTRY)) ? target
|
|
479
|
-
: undefined
|
|
480
|
-
if (dir === undefined || !inside(dir, compilation.workspaceRoot)) return undefined
|
|
481
|
-
const hash = url.indexOf('#')
|
|
482
|
-
let fragment = hash < 0 ? '' : url.slice(hash + 1)
|
|
483
|
-
try {
|
|
484
|
-
fragment = decodeURIComponent(fragment)
|
|
485
|
-
} catch {
|
|
486
|
-
}
|
|
487
|
-
return { dir: relative(compilation.workspaceRoot, dir).split(sep).join('/'), ...fragment ? { fragment } : {} }
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function publishedAsset(compilation: Compilation, source: SourcePos, url: string, report: (finding: Finding) => void): LocalAsset | undefined {
|
|
491
|
-
const { deckDir, workspaceRoot } = compilation
|
|
492
|
-
const warn = (message: string) => report({ checkId: 'presmium.links', severity: 'warning', message, source: { file: source.file, line: lineOf(source, url) } })
|
|
493
|
-
const target = localTarget(source, url)
|
|
494
|
-
const found = localAsset(deckDir, source, url)
|
|
495
|
-
if (found && target && basename(realpathSync(found.file)) === DECK_ENTRY) {
|
|
496
|
-
const linked = documentLink(compilation, source, url)
|
|
497
|
-
warn(`${url} is not published: it is the source of ${linked?.dir ? `the document ${linked.dir}` : 'a document'}, which a workspace site links as its page; this build keeps the link as written`)
|
|
498
|
-
return undefined
|
|
499
|
-
}
|
|
500
|
-
if (found && !inside(found.file, workspaceRoot) && existsSync(join(workspaceRoot, '.git'))) {
|
|
501
|
-
warn(`${url} is not published: the file lies outside the deck's repository`)
|
|
502
|
-
return undefined
|
|
503
|
-
}
|
|
504
|
-
if (found) return { ...found, directory: false }
|
|
505
|
-
if (!target) return undefined
|
|
506
|
-
if (!existsSync(target)) {
|
|
507
|
-
warn(`${url} is not published: there is no such file`)
|
|
508
|
-
return undefined
|
|
509
|
-
}
|
|
510
|
-
if (!publishesDirectory(target, deckDir)) return undefined
|
|
511
|
-
const directory = directoryAsset(target, deckDir, join(deckDir, WORK_DIR, FILES_DIR), compilation.doc.meta.lang, message => warn(`${url} is not published: ${message}`))
|
|
512
|
-
return directory && { ...directory, directory: true }
|
|
165
|
+
/** The Node entries of the plugins whose browser parts need them (run-time files, document modules), with their configuration. */
|
|
166
|
+
export function nodeVitePlugins(compilation: Pick<Compilation, 'plugins' | 'doc'>): NodeVitePlugin[] {
|
|
167
|
+
return compilation.plugins.flatMap((loaded) => {
|
|
168
|
+
const plugin = loaded.plugin
|
|
169
|
+
if (!loaded.entry || !(plugin?.assets || plugin?.documentModules)) return []
|
|
170
|
+
const config = compilation.doc.config.presmium[pluginId(loaded)]
|
|
171
|
+
return [{ id: pluginId(loaded), entry: loaded.entry, config: isRecord(config) ? config : {} }]
|
|
172
|
+
})
|
|
513
173
|
}
|
|
514
174
|
|
|
515
|
-
function unsupportedBlocks(doc: IRDocument, renderer: Renderer, extensions:
|
|
175
|
+
export function unsupportedBlocks(doc: IRDocument, renderer: Renderer, extensions: BoundRendererExtension[]): RenderOutput['skipped'] {
|
|
516
176
|
const skipped: RenderOutput['skipped'] = []
|
|
517
177
|
const visit = (section: string, blocks: IRBlock[]) => {
|
|
518
178
|
for (const block of blocks) {
|
|
@@ -530,6 +190,6 @@ function unsupportedBlocks(doc: IRDocument, renderer: Renderer, extensions: Rend
|
|
|
530
190
|
return skipped
|
|
531
191
|
}
|
|
532
192
|
|
|
533
|
-
function renderFinding(compilation: Compilation, severity: Finding['severity'], message: string): Finding {
|
|
193
|
+
export function renderFinding(compilation: Compilation, severity: Finding['severity'], message: string): Finding {
|
|
534
194
|
return { checkId: 'presmium.render', severity, message, source: { file: compilation.entry, line: 1 } }
|
|
535
195
|
}
|