@uniweb/unipress 0.2.3 → 0.2.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,93 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Changed
6
+
7
+ - **Release binaries are now signed.** macOS binaries are signed with
8
+ Apple Developer ID Application + Apple-notarized; Windows binaries
9
+ are signed via Azure Trusted Signing. macOS users no longer see a
10
+ Gatekeeper warning on first run. The 0.2.3 release assets were
11
+ re-signed in place — re-download from the release page if you
12
+ grabbed an unsigned copy before this change landed.
13
+
14
+ ### Removed
15
+
16
+ - `.github/workflows/release.yml`. Binaries are no longer built in
17
+ this public repo — they're built and signed in a private Proximify
18
+ pipeline. Pushing a `unipress@<version>` tag no longer triggers a
19
+ binary build; the maintainer dispatches the signed pipeline manually
20
+ after each tag, and the resulting binaries are uploaded to the same
21
+ GitHub Release on this repo. Source code, docs, npm publishing, and
22
+ the foundation-deploy workflow are unchanged.
23
+
24
+ ## 0.2.3
25
+
26
+ The first complete-out-of-the-box release. `npm i -g @uniweb/unipress`
27
+ + `unipress create my-book --template book` + `unipress compile my-book`
28
+ produces a real PDF on the first try, no setup, no auth, no local
29
+ servers, no `--foundation` overrides.
30
+
31
+ ### Added
32
+
33
+ - **GitHub Pages distribution for bundled foundations.**
34
+ `https://uniweb.github.io/unipress/foundations/<name>/<version>/foundation.js`
35
+ serves every published foundation. A new workflow
36
+ (`.github/workflows/deploy-foundations.yml`) builds each
37
+ `foundations/<name>/` on every push to main and accumulates versions
38
+ on the `gh-pages` branch — older versions stay reachable indefinitely.
39
+ No registry namespace claim, no Cloudflare worker, no platform
40
+ authorization needed for the open-source foundations.
41
+ - **`scripts/ci-flip-workspace-deps.js`** — pre-install rewrite that
42
+ translates `workspace:*` deps to npm version ranges in CI. Both
43
+ workflows (foundation deploy, binary release) call it as the first
44
+ step. Closes the standalone-install gap that the workspace:*
45
+ resolution introduced.
46
+ - **`prepublishOnly` regenerates `templates-data.js`.** `pnpm publish`
47
+ now refreshes the bundled template data from `documents/<name>/`
48
+ before packing, so a stale generated file can't ship even if a
49
+ developer forgot to regenerate before commit.
50
+
51
+ ### Fixed
52
+
53
+ - **EPUB cover image no longer skipped.** The foundation's
54
+ `buildEpubOptions` was using a `fetch`-based URL resolver that
55
+ silently 404'd in unipress's Node compile context. Replaced with
56
+ the same `loadAsset` abstraction the typst path uses. Cover image
57
+ now embeds at `OEBPS/images/<hash>.jpg` alongside a synthesized
58
+ `cover.xhtml` page.
59
+ - **DOCX inline-image path threaded through `loadAsset`.**
60
+ `framework/press`'s docx adapter walker now plumbs the host-supplied
61
+ byte loader from `buildDocument` → `convertChildren` →
62
+ `irToImageParagraph` → `fetchImageData` → `fetchAsset`. Future
63
+ docx-emitting templates with inline images compile cleanly in
64
+ unipress.
65
+ - **Cover round-trip in scaffolded projects** — front + back JPGs
66
+ ship via the binary-asset support added to the template generator.
67
+ EPUB readers + most PDF viewers now show the book's thumbnail
68
+ without the user adding any artwork.
69
+
70
+ ### Changed
71
+
72
+ - Catalog URLs in `src/foundations-data.js` flipped from local-registry
73
+ (`http://localhost:4001/...`) to GH Pages
74
+ (`https://uniweb.github.io/unipress/foundations/...`). The 0.2.0
75
+ caveat in the changelog is resolved.
76
+ - `foundation-loader.js`'s `DEFAULT_REGISTRY_BASE` flipped to
77
+ `https://uniweb.github.io/unipress`. `UNIWEB_REGISTRY_URL` env var
78
+ remains the override for local dev / private alternatives.
79
+ - `@uniweb/*` deps in `package.json` are `workspace:*` again (dev
80
+ resolves local siblings); `pnpm publish` translates them to real
81
+ version specs in the published tarball.
82
+
83
+ ### Notes
84
+
85
+ - Binary downloads (darwin-arm64, linux-x64, windows-x64) ship via
86
+ the existing `unipress@<version>` tag-driven release workflow. The
87
+ npm path is now equivalent for users with Node 20+.
88
+ - See `RELEASING.md` for the framework-publish + workspace:* flip-back
89
+ dance the next maintainer release follows.
90
+
3
91
  ## 0.2.0
4
92
 
5
93
  The credibility-shifting release. Two production-shaped foundations
package/README.md CHANGED
@@ -34,9 +34,17 @@ Foundations consume [@uniweb/press](https://github.com/uniweb/press) for the byt
34
34
 
35
35
  ## Install
36
36
 
37
- **Standalone binary (recommended for most users).** No Node, no npm. Download the asset for your platform from [the releases page](https://github.com/uniweb/unipress/releases), make it executable, and put it somewhere on your `PATH` — `/usr/local/bin/unipress` is a common choice on macOS and Linux. Then you can run `unipress` from any directory.
37
+ **Homebrew (macOS and Linux — recommended).** No Node, no manual download:
38
38
 
39
- **npm.** If you already have Node:
39
+ ```bash
40
+ brew install uniweb/unipress/unipress
41
+ ```
42
+
43
+ Updates land via `brew upgrade unipress`. The macOS binary is signed with the Proximify Inc. Developer ID and Apple-notarized — no Gatekeeper warnings.
44
+
45
+ **Manual download (Windows, or any platform without Homebrew).** Grab the asset for your platform from [the releases page](https://github.com/uniweb/unipress/releases), make it executable (Unix), and put it somewhere on your `PATH`. On macOS and Linux, `/usr/local/bin/unipress` is a common location.
46
+
47
+ **npm.** If you already have Node ≥ 20.19:
40
48
 
41
49
  ```bash
42
50
  npm i -g @uniweb/unipress
@@ -15,8 +15,7 @@ unipress compile . --format pdf --out my-book.pdf
15
15
  ```
16
16
  my-book/
17
17
  ├── document.yml pinned to @uniweb/book@<version>
18
- ├── pages/
19
- │ ├── folder.yml activates folder mode
18
+ ├── content/
20
19
  │ ├── 01-preface.md type: BackMatter — unnumbered front matter
21
20
  │ ├── 02-chapter-one.md
22
21
  │ └── 03-chapter-two.md
@@ -42,7 +41,7 @@ my-book/
42
41
 
43
42
  ## Add chapters
44
43
 
45
- Drop a markdown file under `pages/` and add its base name (without `.md`) to `document.yml`'s `pages:` list in the order you want it to appear. Chapters need no frontmatter — the foundation's `Chapter` section handles them by default. For non-chapter pages (acknowledgments, colophon, appendices), use `type: BackMatter` in frontmatter to skip chapter numbering.
44
+ Drop a markdown file under `content/` and add its base name (without `.md`) to `document.yml`'s `content:` list in the order you want it to appear. Chapters need no frontmatter — the foundation's `Chapter` section handles them by default. For non-chapter pages (acknowledgments, colophon, appendices), use `type: BackMatter` in frontmatter to skip chapter numbering.
46
45
 
47
46
  ## Common customizations
48
47
 
@@ -20,7 +20,7 @@ q3-metrics/
20
20
  ├── collections/
21
21
  │ ├── members/ three sample records (19th-century naturalists)
22
22
  │ └── queries/ saved query examples for the Population dropdown
23
- └── pages/
23
+ └── content/
24
24
  └── report/ cover, members, publications-by-{type,journal,year},
25
25
  publications-list, funding, supervisions
26
26
  ```
@@ -33,7 +33,7 @@ The starter ships three sample members so the first compile produces a non-empty
33
33
  |----------------------------|--------------------------------------------------------|
34
34
  | `name`, `author`, `year` | Workbook metadata (shown in the file's properties). |
35
35
  | `format` | `xlsx` (default) or `docx`. Override on the CLI. |
36
- | `index` | Routes `/` → `pages/<index>` (here: `report`). |
36
+ | `index` | Routes `/` → `content/<index>` (here: `report`). |
37
37
  | `collections.members.path` | Where the records live (default `collections/members`).|
38
38
  | `collections.members.queryable` | Filterable surface — drives the FilterPanel UI. |
39
39
  | `collections.queries.path` | Saved-view dropdown (Population selector). |
@@ -17,7 +17,7 @@ faculty/
17
17
  ├── document.yml pinned to @uniweb/data@<version>; one queryable collection
18
18
  ├── collections/
19
19
  │ └── members/ three sample records
20
- └── pages/
20
+ └── content/
21
21
  └── directory/ cover (Loom-rendered count) + listing
22
22
  ```
23
23
 
@@ -27,7 +27,7 @@ faculty/
27
27
  |-----------------------------|--------------------------------------------------------|
28
28
  | `name` | Workbook title. |
29
29
  | `format` | `xlsx` (default) or `docx`. |
30
- | `index` | Routes `/` → `pages/directory`. |
30
+ | `index` | Routes `/` → `content/directory`. |
31
31
  | `collections.members.path` | Where the records live (`collections/members`). |
32
32
  | `collections.members.queryable` | Filterable fields — drives the FilterPanel UI. |
33
33
 
@@ -58,7 +58,7 @@ Add a `joined_year:` range field, for example, if filtering by start year matter
58
58
 
59
59
  ## Hand-pick the displayed columns
60
60
 
61
- The Members section reads every key from each record by default. To restrict the columns shown in the web preview and the xlsx export, set `columns:` on the section frontmatter in `pages/directory/members.md`.
61
+ The Members section reads every key from each record by default. To restrict the columns shown in the web preview and the xlsx export, set `columns:` on the section frontmatter in `content/directory/members.md`.
62
62
 
63
63
  ## When to pick `directory` over `data-report`
64
64
 
@@ -15,8 +15,7 @@ unipress compile . --format pdf --out my-monograph.pdf
15
15
  ```
16
16
  my-monograph/
17
17
  ├── document.yml pinned to @uniweb/book@<version>; royal-octavo, EB Garamond, tocDepth: 3
18
- ├── pages/
19
- │ ├── folder.yml
18
+ ├── content/
20
19
  │ ├── 01-preface.md type: BackMatter
21
20
  │ ├── 02-introduction.md
22
21
  │ └── 03-chapter-one.md
@@ -15,8 +15,7 @@ unipress compile . --format pdf --out q3-report.pdf
15
15
  ```
16
16
  q3-report/
17
17
  ├── document.yml pinned to @uniweb/book@<version>; trade-7x10, block paragraphs
18
- ├── pages/
19
- │ ├── folder.yml
18
+ ├── content/
20
19
  │ ├── 01-summary.md executive summary + recommendations table
21
20
  │ ├── 02-findings.md body, with code listing and pull-quote
22
21
  │ └── 03-methodology.md
@@ -46,7 +45,7 @@ Same shape as `book` (see [book.md](./book.md)). The differences are defaults:
46
45
 
47
46
  ## Common customizations
48
47
 
49
- - **Add an appendix**: a new markdown file with `type: BackMatter` in frontmatter, listed in `pages:`.
48
+ - **Add an appendix**: a new markdown file with `type: BackMatter` in frontmatter, listed in `content:`.
50
49
  - **Wider code blocks**: bump `book.typography.codeMarginRelief` to `0.5in` (code extends 0.5in past each side of the body column).
51
50
  - **No TOC** for short reports: set `book.structure.toc: false`.
52
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/unipress",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Compile a content directory into a document (PDF, EPUB, Paged.js HTML, Typst source bundle, DOCX, XLSX) using a Uniweb foundation. Five built-in templates: book, monograph, report, data-report, directory.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -49,11 +49,11 @@
49
49
  "prompts": "^2.4.2",
50
50
  "react": "^18.0.0 || ^19.0.0",
51
51
  "react-dom": "^18.0.0 || ^19.0.0",
52
- "@uniweb/build": "0.11.2",
53
- "@uniweb/runtime": "0.8.4",
54
- "@uniweb/core": "0.7.3",
55
- "@uniweb/content-reader": "1.1.5",
56
- "@uniweb/semantic-parser": "1.1.11"
52
+ "@uniweb/build": "0.11.3",
53
+ "@uniweb/runtime": "0.8.5",
54
+ "@uniweb/core": "0.7.4",
55
+ "@uniweb/semantic-parser": "1.1.12",
56
+ "@uniweb/content-reader": "1.1.6"
57
57
  },
58
58
  "scripts": {
59
59
  "test": "echo \"no tests yet\" && exit 0",
@@ -10,10 +10,108 @@
10
10
 
11
11
  import { existsSync } from 'node:fs'
12
12
  import { resolve, join } from 'node:path'
13
- import { collectSiteContent } from '@uniweb/build/content'
13
+ import { collectSiteContent, processCollections } from '@uniweb/build/content'
14
14
  import { detectConfigFile, CONFIG_FILE_NAMES } from './document-yml.js'
15
15
  import { ContentDirectoryError, DocumentYmlError } from './errors.js'
16
16
 
17
+ // Match a parsed collection-backed fetch path. parseFetchConfig (in
18
+ // @uniweb/build) normalises `{ collection: <name> }` into
19
+ // `{ path: '/data/<name>.json', schema: <name>, ... }` — so this regex is
20
+ // the inverse: pull the collection name back out of the resolved path.
21
+ const COLLECTION_PATH_RE = /^\/data\/(.+)\.json$/
22
+
23
+ function attachData(section, schema, data) {
24
+ if (!section || !schema) return
25
+ if (!section.parsedContent) section.parsedContent = {}
26
+ if (!section.parsedContent.data) section.parsedContent.data = {}
27
+ // Don't clobber a section-level value with a cascaded page-level one.
28
+ // The Block constructor will spread parsedContent.data through, so the
29
+ // first writer wins for a given schema.
30
+ if (section.parsedContent.data[schema] === undefined) {
31
+ section.parsedContent.data[schema] = data
32
+ }
33
+ }
34
+
35
+ function findCollectionRecords(fetchConfig, resolved) {
36
+ if (!fetchConfig?.path || !fetchConfig?.schema) return null
37
+ const m = COLLECTION_PATH_RE.exec(fetchConfig.path)
38
+ if (!m) return null
39
+ const records = resolved[m[1]]
40
+ if (!Array.isArray(records)) return null
41
+ return records
42
+ }
43
+
44
+ function attachSectionFetches(sections, resolved) {
45
+ if (!Array.isArray(sections)) return
46
+ for (const section of sections) {
47
+ const records = findCollectionRecords(section.fetch, resolved)
48
+ if (records) attachData(section, section.fetch.schema, records)
49
+ if (Array.isArray(section.subsections) && section.subsections.length) {
50
+ attachSectionFetches(section.subsections, resolved)
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Materialize file-based collections into each section's
57
+ * `parsedContent.data` so the SSR render pipeline reads populated data
58
+ * synchronously (no `useFetched` round-trip; no `public/` directory).
59
+ *
60
+ * In a regular Uniweb site build, the Vite plugin runs `processCollections`
61
+ * + `writeCollectionFiles` and the runtime resolves `fetch:` declarations
62
+ * over HTTP at render time. Under `unipress compile` neither of those
63
+ * happens — there's no public dir, and SSR skips effects. We close the
64
+ * gap by resolving collections in-memory and attaching the records
65
+ * directly to each block's `parsedContent.data.<schema>`. The Block
66
+ * constructor (framework/core/src/block.js) preserves that field, and
67
+ * `prepareProps` then surfaces it as `content.data.<schema>` to the
68
+ * component — same shape the runtime would produce.
69
+ *
70
+ * Page-level fetch cascades to every section on the page; section-level
71
+ * fetch overrides on a per-section basis. Only collection-backed fetches
72
+ * (parsed `path: '/data/<name>.json'`) are resolved here — remote URL
73
+ * fetches, refine configs, and array-form `fetch: [...]` declarations
74
+ * are left untouched (those have their own gaps; out of scope here).
75
+ */
76
+ async function resolveLocalCollections(siteContent, sitePath) {
77
+ const collectionsConfig = siteContent?.config?.collections
78
+ if (!collectionsConfig || typeof collectionsConfig !== 'object') return
79
+ if (Object.keys(collectionsConfig).length === 0) return
80
+
81
+ const resolved = await processCollections(
82
+ sitePath,
83
+ collectionsConfig,
84
+ sitePath,
85
+ '/',
86
+ )
87
+
88
+ for (const page of siteContent.pages || []) {
89
+ const pageRecords = findCollectionRecords(page.fetch, resolved)
90
+ if (pageRecords) {
91
+ for (const section of page.sections || []) {
92
+ attachData(section, page.fetch.schema, pageRecords)
93
+ }
94
+ }
95
+ attachSectionFetches(page.sections, resolved)
96
+ }
97
+
98
+ // Stash the resolved arrays on the website config too, so any section
99
+ // (regardless of its own page's fetch declaration) can self-bootstrap
100
+ // — e.g., a Cite inset rendering inside a Chapter on page A needs the
101
+ // bibliography records that the Bibliography section declared on page
102
+ // B. Foundations read this via `block.website.config.collections.<name>.records`
103
+ // as a synchronous fallback.
104
+ if (!siteContent.config) siteContent.config = {}
105
+ if (!siteContent.config.collections) siteContent.config.collections = {}
106
+ for (const name of Object.keys(resolved)) {
107
+ const existing = siteContent.config.collections[name]
108
+ siteContent.config.collections[name] = {
109
+ ...(existing && typeof existing === 'object' ? existing : {}),
110
+ records: resolved[name],
111
+ }
112
+ }
113
+ }
114
+
17
115
  export async function loadContent(dir, options = {}) {
18
116
  const sitePath = resolve(dir)
19
117
 
@@ -52,5 +150,7 @@ export async function loadContent(dir, options = {}) {
52
150
  throw err
53
151
  }
54
152
 
153
+ await resolveLocalCollections(content, sitePath)
154
+
55
155
  return { content, configFile, sitePath }
56
156
  }
@@ -72,8 +72,8 @@ function publicUrl(name, version) {
72
72
  }
73
73
 
74
74
  const BOOK_FOUNDATION = {
75
- ref: '@uniweb/book@0.1.1',
76
- source: { url: publicUrl('book', '0.1.1') },
75
+ ref: '@uniweb/book@0.2.0',
76
+ source: { url: publicUrl('book', '0.2.0') },
77
77
  }
78
78
 
79
79
  const DATA_FOUNDATION = {