@lincyaw/compiler 1.0.0-rc.0 → 1.0.0-rc.1
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/build.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { errorMessage } from '@lincyaw/kernel'
|
|
4
|
+
import type { Finding, IRDocument, RenderOutput } from '@lincyaw/kernel'
|
|
5
|
+
import { unshown } from './audience.ts'
|
|
6
|
+
import { audienceCompilation } from './compiler.ts'
|
|
7
|
+
import type { AudienceCompilation, Compilation } from './compiler.ts'
|
|
8
|
+
import { relativeIR, WORK_DIR } from './paths.ts'
|
|
9
|
+
import { planRenderRun, renderContext, renderFinding, selectRenderers, translationsFor, unsupportedBlocks, WEB, writeIR } from './renderers.ts'
|
|
10
|
+
import type { InstalledRenderer } from './renderers.ts'
|
|
11
|
+
import type { CompiledTranslation } from './translations.ts'
|
|
12
|
+
import { viewFor } from './views.ts'
|
|
13
|
+
|
|
14
|
+
export interface RendererOutput {
|
|
15
|
+
renderer: string
|
|
16
|
+
outDir: string
|
|
17
|
+
workDir: string
|
|
18
|
+
base: string
|
|
19
|
+
skipped: RenderOutput['skipped']
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BuildReport {
|
|
23
|
+
ir: string
|
|
24
|
+
outputs: RendererOutput[]
|
|
25
|
+
findings: Finding[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BuildOptions {
|
|
29
|
+
outDir: string
|
|
30
|
+
to?: string[]
|
|
31
|
+
audience?: string
|
|
32
|
+
quote?: boolean
|
|
33
|
+
scan?: 'outDir' | 'written'
|
|
34
|
+
/** The path every renderer's output is served at, in place of each renderer's own. */
|
|
35
|
+
base?: string
|
|
36
|
+
/** The renderers' tools' output, a line at a time (`RenderContext.log`). */
|
|
37
|
+
log?(line: string): void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function build(whole: Compilation, options: BuildOptions): Promise<BuildReport> {
|
|
41
|
+
const compilation = options.audience && options.audience !== whole.audience?.name
|
|
42
|
+
? { ...whole, doc: whole.audienceDocument(options.audience), audience: whole.audienceCompilation(options.audience) }
|
|
43
|
+
: whole
|
|
44
|
+
const { doc } = compilation
|
|
45
|
+
const ir = await writeIR(relativeIR(doc, compilation.deckDir, compilation.workspaceRoot), options.outDir)
|
|
46
|
+
const { selected, findings } = await selectRenderers(compilation, options.to)
|
|
47
|
+
if (selected.length === 0) findings.push(renderFinding(compilation, 'warning', `no renderer available; only the IR was written to ${ir}`))
|
|
48
|
+
const run = planRenderRun(compilation, selected, true, options.base)
|
|
49
|
+
const translated = await translationsFor(compilation, run)
|
|
50
|
+
findings.push(...translated.findings)
|
|
51
|
+
const render = async ({ renderer }: InstalledRenderer) => {
|
|
52
|
+
const reported: Finding[] = []
|
|
53
|
+
const ctx = renderContext(compilation, renderer.id, join(options.outDir, renderer.id), join(compilation.deckDir, WORK_DIR, renderer.id), run, reported)
|
|
54
|
+
if (renderer.id === WEB) ctx.translations = translated.views
|
|
55
|
+
if (options.log) ctx.log = options.log
|
|
56
|
+
const view = viewFor(doc, renderer.id)
|
|
57
|
+
const unsupported = unsupportedBlocks(view, renderer, ctx.extensions)
|
|
58
|
+
try {
|
|
59
|
+
await mkdir(ctx.outDir, { recursive: true })
|
|
60
|
+
const output = await renderer.build(view, ctx)
|
|
61
|
+
const listed = new Set(output.skipped.map(s => `${s.section}#${s.block}`))
|
|
62
|
+
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}`))] }
|
|
63
|
+
return { result, reported }
|
|
64
|
+
} catch (error) {
|
|
65
|
+
reported.push(renderFinding(compilation, 'error', `renderer ${renderer.id} failed: ${errorMessage(error)}`))
|
|
66
|
+
return { reported }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Each renderer can start its own toolchain. Do not multiply their native pools.
|
|
70
|
+
const results: Awaited<ReturnType<typeof render>>[] = []
|
|
71
|
+
for (const renderer of selected) results.push(await render(renderer))
|
|
72
|
+
const outputs = results.flatMap(({ result }) => result ? [result] : [])
|
|
73
|
+
if (compilation.audience) {
|
|
74
|
+
const scan = audienceScan(compilation.audience, translated.translations, doc)
|
|
75
|
+
const paths = options.scan === 'written' ? [ir, ...outputs.map(output => output.outDir)] : undefined
|
|
76
|
+
findings.push(...await scan.scan(options.outDir, paths, { quote: options.quote ?? true }))
|
|
77
|
+
}
|
|
78
|
+
return { ir, outputs, findings: unique([...doc.diagnostics, ...findings, ...results.flatMap(({ reported }) => reported)]) }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function audienceScan(audience: AudienceCompilation, translations: CompiledTranslation[], doc: IRDocument): AudienceCompilation {
|
|
82
|
+
const kept = [doc, ...translations.map(translation => translation.doc)]
|
|
83
|
+
const left = [audience, ...translations.flatMap(translation => (translation.audience ? [translation.audience] : []))].flatMap(own => own.withheld)
|
|
84
|
+
return audienceCompilation(audience.name, unshown(left, kept))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function unique(findings: Finding[]): Finding[] {
|
|
88
|
+
const seen = new Set<string>()
|
|
89
|
+
return findings.filter((finding) => {
|
|
90
|
+
const key = JSON.stringify([finding.checkId, finding.severity, finding.message, finding.source?.file, finding.source?.line])
|
|
91
|
+
if (seen.has(key)) return false
|
|
92
|
+
seen.add(key)
|
|
93
|
+
return true
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The renderers the document is made with: those its configuration names, else every installed one. */
|
|
98
|
+
export async function availableRenderers(compilation: Compilation): Promise<string[]> {
|
|
99
|
+
return (await selectRenderers(compilation)).selected.map(({ renderer }) => renderer.id)
|
|
100
|
+
}
|
|
101
|
+
|
|
@@ -2,16 +2,16 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { CheckRegistry, errorMessage, irBlocks } from '@lincyaw/kernel'
|
|
4
4
|
import type { CheckContext, Finding, RunResult } from '@lincyaw/kernel'
|
|
5
|
-
import {
|
|
5
|
+
import { build } from './build.ts'
|
|
6
|
+
import type { BuildReport } from './build.ts'
|
|
6
7
|
import type { Compilation } from './compiler.ts'
|
|
7
|
-
import {
|
|
8
|
-
import type { BuildReport } from './renderers.ts'
|
|
8
|
+
import { frontmatterLine } from './source-lines.ts'
|
|
9
9
|
|
|
10
10
|
export function checkContext(compilation: Compilation): CheckContext {
|
|
11
11
|
return {
|
|
12
12
|
deckDir: compilation.deckDir,
|
|
13
13
|
workspaceRoot: compilation.workspaceRoot,
|
|
14
|
-
headmatter: compilation.
|
|
14
|
+
headmatter: compilation.headmatter,
|
|
15
15
|
src: compilation.src,
|
|
16
16
|
resolvers: compilation.resolvers,
|
|
17
17
|
lock: compilation.lock.toJSON(),
|
|
@@ -20,7 +20,8 @@ export function checkContext(compilation: Compilation): CheckContext {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/** The plugins' checks and the publication checks; a crashed check or a duplicate check ID becomes a finding. */
|
|
24
|
+
export async function runAllChecks(compilation: Compilation, options: { externalLinks?: boolean } = {}): Promise<Finding[]> {
|
|
24
25
|
const { entry } = compilation
|
|
25
26
|
const text = await readFile(entry, 'utf8').catch(() => '')
|
|
26
27
|
const at = (name: string) => ({ file: entry, line: frontmatterLine(text, name) })
|
|
@@ -51,6 +52,12 @@ export async function runBlock(compilation: Compilation, target: { section: stri
|
|
|
51
52
|
return compilation.run(block.node, signal)
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
/** Builds the compiled document into the deck's `dist/`. */
|
|
56
|
+
export function buildToDist(compilation: Compilation, options: { to?: string[] } = {}): Promise<BuildReport> {
|
|
57
|
+
return build(compilation, { outDir: join(compilation.deckDir, 'dist'), to: options.to })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** What a host answers for a build: each renderer's output directory, and the findings. */
|
|
61
|
+
export function buildSummary(report: BuildReport): { outputs: { renderer: string, outDir: string }[], findings: Finding[] } {
|
|
62
|
+
return { outputs: report.outputs.map(({ renderer, outDir }) => ({ renderer, outDir })), findings: report.findings }
|
|
56
63
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { setImmediate } from 'node:timers/promises'
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
3
|
+
|
|
4
|
+
type Job = { run(): Promise<void>, interactive: boolean }
|
|
5
|
+
const waiting: Job[] = []
|
|
6
|
+
let running = false
|
|
7
|
+
const nesting = new AsyncLocalStorage<boolean>()
|
|
8
|
+
|
|
9
|
+
/** One compilation per host process. Interactive work precedes queued batch builds. */
|
|
10
|
+
export function enqueueCompile<T>(run: () => Promise<T>, interactive: boolean): Promise<T> {
|
|
11
|
+
if (nesting.getStore()) return run()
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
waiting.push({ interactive, run: async () => { try { resolve(await nesting.run(true, run)) } catch (error) { reject(error) } } })
|
|
14
|
+
void next()
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function next(): Promise<void> {
|
|
19
|
+
if (running || waiting.length === 0) return
|
|
20
|
+
running = true
|
|
21
|
+
const firstInteractive = waiting.findIndex(job => job.interactive)
|
|
22
|
+
const job = waiting.splice(firstInteractive < 0 ? 0 : firstInteractive, 1)[0]!
|
|
23
|
+
try { await job.run() } finally {
|
|
24
|
+
await setImmediate()
|
|
25
|
+
running = false
|
|
26
|
+
void next()
|
|
27
|
+
}
|
|
28
|
+
}
|