@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/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 { frontmatterLine } from './compiler.ts'
5
+ import { build } from './build.ts'
6
+ import type { BuildReport } from './build.ts'
6
7
  import type { Compilation } from './compiler.ts'
7
- import { build } from './renderers.ts'
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.frontmatter,
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
- export async function pluginChecks(compilation: Compilation, options: { externalLinks?: boolean } = {}): Promise<Finding[]> {
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
- export function buildView(compilation: Compilation, options: { to?: string[], outDir?: string } = {}): Promise<BuildReport> {
55
- return build(compilation, { outDir: options.outDir ?? join(compilation.deckDir, 'dist'), to: options.to })
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
+ }