@lincyaw/workspace 1.0.0-rc.1 → 1.0.0-rc.10

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.
Files changed (2) hide show
  1. package/package.json +5 -5
  2. package/src/node/build.ts +67 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lincyaw/workspace",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.10",
4
4
  "description": "Presmium's workspace service (presmium serve): documents, their history, reviews and the workspace site",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -33,12 +33,12 @@
33
33
  "image-size": "2.0.4",
34
34
  "mime-types": "3.0.2",
35
35
  "yaml": "^2.9.1",
36
- "@lincyaw/server": "1.0.0-rc.1",
37
- "@lincyaw/kernel": "1.0.0-rc.1",
38
- "@lincyaw/compiler": "1.0.0-rc.1"
36
+ "@lincyaw/compiler": "1.0.0-rc.10",
37
+ "@lincyaw/kernel": "1.0.0-rc.10",
38
+ "@lincyaw/server": "1.0.0-rc.10"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/mime-types": "^3.0.1",
42
- "@lincyaw/mcp": "1.0.0-rc.1"
42
+ "@lincyaw/mcp": "1.0.0-rc.10"
43
43
  }
44
44
  }
package/src/node/build.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { join, resolve } from 'node:path'
2
- import { Compiler } from '@lincyaw/compiler'
3
- import type { RunStats } from '@lincyaw/compiler'
4
- import { errorMessage } from '@lincyaw/kernel'
1
+ import { cpSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { isAbsolute, join, resolve } from 'node:path'
3
+ import { build, Compiler } from '@lincyaw/compiler'
4
+ import type { Compilation, RunStats } from '@lincyaw/compiler'
5
+ import { errorMessage, isRecord } from '@lincyaw/kernel'
5
6
  import type { Finding } from '@lincyaw/kernel'
6
7
  import { listDocuments, readWorkspaceConfig } from './config.ts'
7
8
  import { siteContext, siteDocument, siteIR, siteSpec, siteTranslations, webRenderer } from './site.ts'
@@ -18,8 +19,12 @@ export async function buildWorkspace(root: string, options: { outDir?: string, b
18
19
  const findings: Finding[] = []
19
20
  const documents: WorkspaceBuild['documents'] = []
20
21
  const site = siteSpec(config, [])
22
+ const base = siteBase(config.site.base)
23
+ const forms: { dir: string, compilation: Compilation, to: string[] }[] = []
21
24
  for (const document of found) {
22
25
  const compilation = await new Compiler(join(config.root, document.entry), { bundled: options.bundled, settings: config.settings }).compile({ pruneCache: true })
26
+ const to = presentations(compilation, `${base}${urlPath(document.dir)}/`, siteLook(config.site, config.root))
27
+ if (to.length > 0) forms.push({ dir: document.dir, compilation, to })
23
28
  const translated = await siteTranslations(compilation)
24
29
  findings.push(...compilation.doc.diagnostics, ...translated.findings)
25
30
  documents.push({ dir: document.dir, title: document.title, runs: compilation.runStats })
@@ -27,15 +32,72 @@ export async function buildWorkspace(root: string, options: { outDir?: string, b
27
32
  }
28
33
  const outDir = join(resolve(options.outDir ?? join(config.root, 'dist')), 'web')
29
34
  const renderer = await webRenderer(config.root, options.bundled)
30
- const ctx = siteContext({ root: config.root, outDir, workDir: join(config.root, '.presmium', 'site'), base: siteBase(config.site.base), findings })
35
+ const ctx = siteContext({ root: config.root, outDir, workDir: join(config.root, '.presmium', 'site'), base, findings })
31
36
  try {
32
37
  await renderer.buildSite!(site, ctx)
33
38
  } catch (error) {
34
39
  findings.push({ checkId: 'presmium.render', severity: 'error', message: `the workspace site failed: ${errorMessage(error)}` })
40
+ return { outDir, documents, findings }
41
+ }
42
+ // A document's presentation and its narrated lecture are built beside its pages: `<dir>/slides/` and `<dir>/lecture/`.
43
+ for (const { dir, compilation, to } of forms) {
44
+ const work = join(config.root, '.presmium', 'forms', dir)
45
+ rmSync(work, { recursive: true, force: true })
46
+ try {
47
+ const report = await build(compilation, { outDir: work, to })
48
+ findings.push(...report.findings)
49
+ for (const form of to) if (existsSync(join(work, form))) cpSync(join(work, form), join(outDir, dir, form), { recursive: true })
50
+ if (to.includes('slides')) shareSlidesApp(join(outDir, dir, 'slides'), `${base}${urlPath(dir)}/slides/`, outDir, base)
51
+ } catch (error) {
52
+ findings.push({ checkId: 'presmium.render', severity: 'error', message: `document ${dir}: its ${to.join(' and ')} failed: ${errorMessage(error)}` })
53
+ }
35
54
  }
36
55
  return { outDir, documents, findings }
37
56
  }
38
57
 
58
+ const PRESENTATIONS = ['slides', 'lecture']
59
+
60
+ /** The site's theme and its parameters, for its presentations to look like its pages; a theme directory made absolute. */
61
+ function siteLook(site: Record<string, unknown>, root: string): Record<string, unknown> {
62
+ const theme = typeof site.theme === 'string' && (site.theme.startsWith('.') || isAbsolute(site.theme)) ? resolve(root, site.theme) : site.theme
63
+ return { ...theme !== undefined ? { theme } : {}, ...isRecord(site.params) ? { params: site.params } : {} }
64
+ }
65
+
66
+ const urlPath = (dir: string) => dir.split('/').map(encodeURIComponent).join('/')
67
+
68
+ /**
69
+ * Every document's slides run the same viewer app: it moves to the site's `_app/<key>/`, beside the web pages' own,
70
+ * so a reader downloads it once for all the presentations of the site, and the slides' page points there.
71
+ */
72
+ function shareSlidesApp(slides: string, slidesBase: string, outDir: string, base: string): void {
73
+ const app = join(slides, '_app')
74
+ if (!existsSync(app)) return
75
+ for (const key of readdirSync(app)) if (!existsSync(join(outDir, '_app', key))) cpSync(join(app, key), join(outDir, '_app', key), { recursive: true })
76
+ rmSync(app, { recursive: true, force: true })
77
+ const page = join(slides, 'index.html')
78
+ writeFileSync(page, readFileSync(page, 'utf8').replaceAll(`${slidesBase}_app/`, `${base}_app/`))
79
+ }
80
+
81
+ /**
82
+ * The presentation forms a document asks for (`metadata.renderers.slides` or `lecture`), set to be served below its
83
+ * pages at `prefix`: the slides at `slides/`, and the web pages and the slides linked to each other.
84
+ */
85
+ function presentations(compilation: Compilation, prefix: string, look: Record<string, unknown>): string[] {
86
+ const { renderers } = compilation.doc.config
87
+ const to = PRESENTATIONS.filter(form => renderers[form] !== undefined)
88
+ if (to.length === 0) return to
89
+ if (to.includes('slides')) renderers.slides = { ...look, ...isRecord(renderers.slides) ? renderers.slides : {}, base: `${prefix}slides/` }
90
+ const settings = compilation.doc.config.presmium ??= {}
91
+ const web = isRecord(settings['presmium.web']) ? settings['presmium.web'] : {}
92
+ settings['presmium.web'] = { ...web, webUrl: prefix, ...to.includes('slides') ? { slidesUrl: `${prefix}slides/` } : {} }
93
+ // The slides find the lecture where it is built, without probing `slides/lecture/` first.
94
+ if (to.includes('lecture')) {
95
+ const lecture = isRecord(settings['presmium.lecture']) ? settings['presmium.lecture'] : {}
96
+ settings['presmium.lecture'] = { url: `${prefix}lecture/`, ...lecture }
97
+ }
98
+ return to
99
+ }
100
+
39
101
  function siteBase(value: unknown): string {
40
102
  const path = typeof value === 'string' ? value.trim().replace(/^\/+|\/+$/g, '') : ''
41
103
  return path ? `/${path}/` : '/'