@uniweb/unipress 0.4.18 → 0.4.19

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 CHANGED
@@ -90,7 +90,7 @@ The first PDF run downloads Typst 0.14.2 to `~/Library/Caches/unipress/typst/0.1
90
90
  Any foundation that declares an `outputs: { … }` map on its default export can drive unipress. Point `document.yml`'s `foundation:` at:
91
91
 
92
92
  - a registry ref: `@<namespace>/<name>@<version>` — fetched from the Uniweb registry, cached locally,
93
- - a URL: `https://…/foundation.js`,
93
+ - a URL: `https://…/entry.js`,
94
94
  - a local filesystem path: `./foundation`, `/abs/path`, etc.
95
95
 
96
96
  The local-path form is the everyday dev loop — point unipress at a foundation directory you're iterating on, no publish step needed:
@@ -113,7 +113,7 @@ unipress compile <dir> [options]
113
113
  Overrides the format: field in document.yml.
114
114
  --foundation <ref> Override document.yml's foundation. Accepts:
115
115
  - registry ref: @<namespace>/<name>@<version>
116
- - URL: https://…/foundation.js
116
+ - URL: https://…/entry.js
117
117
  - path: ./foundation, /abs/path, …
118
118
  --out <path> Output file (default: ./<dir-basename>.<ext>).
119
119
  --config <path> Explicit config file (default: <dir>/unipress.config.js).
@@ -54,7 +54,7 @@ Set `foundation:` in `document.yml`, or pass `--foundation <ref>`. Four ref form
54
54
  foundation: "@uniweb/book@0.1.0"
55
55
 
56
56
  # Or a full URL.
57
- foundation: "https://example.com/foundations/my-foundation/foundation.js"
57
+ foundation: "https://example.com/foundations/my-foundation/entry.js"
58
58
 
59
59
  # Or a local filesystem path (relative to document.yml).
60
60
  foundation: "./path/to/foundation"
@@ -72,7 +72,7 @@ The package isn't installed anywhere `Node` can see from the content directory.
72
72
 
73
73
  Inside a pnpm workspace, foundations linked via `pnpm-workspace.yaml` globs resolve automatically.
74
74
 
75
- ## `FoundationResolutionError: expected built foundation at <path>/dist/foundation.js`
75
+ ## `FoundationResolutionError: expected built foundation at <path>/dist/entry.js`
76
76
 
77
77
  The foundation package isn't built. From the workspace root:
78
78
 
@@ -80,7 +80,7 @@ The foundation package isn't built. From the workspace root:
80
80
  pnpm --filter <foundation-name> build
81
81
  ```
82
82
 
83
- unipress consumes the built artifact (`dist/foundation.js`), not the source.
83
+ unipress consumes the built artifact (`dist/entry.js`), not the source.
84
84
 
85
85
  ## `CompileError: foundation does not expose compileDocument`
86
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/unipress",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
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,10 +49,10 @@
49
49
  "prompts": "^2.4.2",
50
50
  "react": "^19.0.0",
51
51
  "react-dom": "^19.0.0",
52
- "@uniweb/runtime": "0.8.18",
53
52
  "@uniweb/build": "0.14.13",
54
- "@uniweb/core": "0.7.13",
53
+ "@uniweb/runtime": "0.8.18",
55
54
  "@uniweb/content-reader": "1.1.12",
55
+ "@uniweb/core": "0.7.13",
56
56
  "@uniweb/semantic-parser": "1.1.17"
57
57
  },
58
58
  "scripts": {
@@ -2,23 +2,29 @@
2
2
  // full dist tree so Node can import it.
3
3
  //
4
4
  // Cache layout:
5
- // <unipress cache>/foundations/<host>/<pathish>/foundation.js
5
+ // <unipress cache>/foundations/<host>/<pathish>/entry.js
6
6
  // <unipress cache>/foundations/<host>/<pathish>/<chunk>.js
7
7
  // ... etc
8
8
  //
9
9
  // where <pathish> is the URL's path with leading slashes stripped and
10
- // the trailing /foundation.js stripped — the same URL always maps to
10
+ // the trailing entry filename stripped — the same URL always maps to
11
11
  // the same cache path.
12
12
  //
13
- // A built foundation is a small entry (`foundation.js`) plus a set of
14
- // sibling chunks referenced by relative imports (and further chunks
15
- // dynamically imported from those). `fetchFoundationToCache` walks the
16
- // import graph: it downloads foundation.js, scans for `./*.js` imports
17
- // (static and dynamic), fetches each, and recurses. That's enough to
18
- // cover @uniweb/build's output — all sibling chunks live next to
19
- // foundation.js. Imports NOT starting with './' are bare specifiers
20
- // (external like 'react') or absolute URLs and are not part of the
21
- // fetch graph.
13
+ // A built foundation is a small entry (`entry.js`, the @uniweb/build
14
+ // standard output name) plus a set of sibling chunks referenced by
15
+ // relative imports (and further chunks dynamically imported from those).
16
+ // `fetchFoundationToCache` walks the import graph: it downloads the entry,
17
+ // scans for `./*.js` imports (static and dynamic), fetches each, and
18
+ // recurses. That's enough to cover @uniweb/build's output — all sibling
19
+ // chunks live next to the entry. Imports NOT starting with './' are bare
20
+ // specifiers (external like 'react') or absolute URLs and are not part of
21
+ // the fetch graph.
22
+ //
23
+ // Entry name: the framework standard is `entry.js`. Foundations published
24
+ // before @uniweb/build's entry rename serve `foundation.js`, so the fetch
25
+ // probes `entry.js` first and falls back to `foundation.js` (see
26
+ // ENTRY_CANDIDATES) — both when probing the registry and when reusing a
27
+ // cached download.
22
28
  //
23
29
  // Integrity: the fetch is unverified today. Once the Uniweb registry
24
30
  // publishes integrity data, the catalog entry can carry an `integrity:`
@@ -52,6 +58,13 @@ const EXTERNAL_PACKAGES = [
52
58
  // picks up Node-core strings embedded in bundled commonjs shims.
53
59
  const RELATIVE_JS_IMPORT = /(?:from\s*|\bimport\s*\(?\s*)['"](\.\/[^'"]+\.(?:js|mjs|cjs))['"]/g
54
60
 
61
+ // Foundation entry filename. The framework standard is `entry.js`;
62
+ // `foundation.js` is the legacy name some already-published foundations
63
+ // still serve. Prefer the standard, fall back to the legacy name — both
64
+ // when probing the registry and when reusing a cached download.
65
+ const ENTRY_CANDIDATES = ['entry.js', 'foundation.js']
66
+ const ENTRY_FILENAME_RE = /\/(?:entry|foundation)\.js$/i
67
+
55
68
  export function getFoundationCacheDir(url) {
56
69
  let parsed
57
70
  try {
@@ -63,33 +76,49 @@ export function getFoundationCacheDir(url) {
63
76
  )
64
77
  }
65
78
  const host = parsed.host.replace(/:/g, '_')
66
- let p = parsed.pathname.replace(/^\/+/, '').replace(/\/foundation\.js$/i, '')
79
+ // Strip a leading slash and any trailing entry filename or slash so the
80
+ // same foundation maps to one cache dir whether the URL carries entry.js,
81
+ // foundation.js, or just the containing directory.
82
+ let p = parsed.pathname.replace(/^\/+/, '').replace(ENTRY_FILENAME_RE, '').replace(/\/$/, '')
67
83
  return join(getCacheDir(), 'foundations', host, p)
68
84
  }
69
85
 
70
86
  export function getFoundationCachePath(url) {
71
- return join(getFoundationCacheDir(url), 'foundation.js')
87
+ const dir = getFoundationCacheDir(url)
88
+ for (const name of ENTRY_CANDIDATES) {
89
+ const candidate = join(dir, name)
90
+ if (existsSync(candidate)) return candidate
91
+ }
92
+ return join(dir, ENTRY_CANDIDATES[0])
72
93
  }
73
94
 
74
95
  export async function fetchFoundationToCache(url, { onProgress = () => {} } = {}) {
75
96
  const cacheDir = getFoundationCacheDir(url)
76
- const entryPath = join(cacheDir, 'foundation.js')
77
- if (existsSync(entryPath)) {
78
- onProgress(`using cached foundation: ${entryPath}`)
79
- return entryPath
97
+
98
+ // Reuse a prior download under either the standard or legacy entry name.
99
+ for (const name of ENTRY_CANDIDATES) {
100
+ const cached = join(cacheDir, name)
101
+ if (existsSync(cached)) {
102
+ onProgress(`using cached foundation: ${cached}`)
103
+ return cached
104
+ }
80
105
  }
81
106
  onProgress(`downloading foundation graph from ${url}`)
82
107
  await mkdir(cacheDir, { recursive: true })
83
108
 
84
109
  // Directory URL the individual chunk files live under. Works whether
85
- // the input URL ends in /foundation.js or at the containing directory.
110
+ // the input URL ends in an entry filename or at the containing directory.
86
111
  const urlObj = new URL(url)
87
- const pathname = urlObj.pathname.replace(/\/foundation\.js$/i, '/')
112
+ const pathname = urlObj.pathname.replace(ENTRY_FILENAME_RE, '/')
88
113
  urlObj.pathname = pathname.endsWith('/') ? pathname : pathname + '/'
89
114
  const baseUrl = urlObj.toString()
90
115
 
116
+ // Prefer entry.js (framework standard); fall back to foundation.js
117
+ // (foundations published before the entry rename).
118
+ const entryName = await pickRemoteEntry(baseUrl, onProgress)
119
+
91
120
  const fetched = new Set()
92
- const queue = ['foundation.js']
121
+ const queue = [entryName]
93
122
  while (queue.length > 0) {
94
123
  const rel = queue.shift()
95
124
  if (fetched.has(rel)) continue
@@ -110,14 +139,42 @@ export async function fetchFoundationToCache(url, { onProgress = () => {} } = {}
110
139
  }
111
140
  }
112
141
  await linkExternals(cacheDir, onProgress)
142
+ const entryPath = join(cacheDir, entryName)
113
143
  onProgress(`foundation cached at ${cacheDir} (${fetched.size} file(s))`)
114
144
  return entryPath
115
145
  }
116
146
 
147
+ // Probe the registry for the foundation's entry file. Returns the first
148
+ // of ENTRY_CANDIDATES the host serves (prefer `entry.js`, tolerate the
149
+ // legacy `foundation.js`). A plain GET is used rather than HEAD so the
150
+ // check works against any static host; only the status is needed, so the
151
+ // body is cancelled — the graph walk re-fetches the entry.
152
+ async function pickRemoteEntry(baseUrl, onProgress) {
153
+ for (const name of ENTRY_CANDIDATES) {
154
+ const probeUrl = new URL(name, baseUrl).toString()
155
+ let res
156
+ try {
157
+ res = await fetch(probeUrl)
158
+ } catch {
159
+ continue
160
+ }
161
+ res.body?.cancel?.()
162
+ if (res.ok) return name
163
+ if (name !== ENTRY_CANDIDATES[ENTRY_CANDIDATES.length - 1]) {
164
+ onProgress(` ${name} not found, trying legacy entry name…`)
165
+ }
166
+ }
167
+ throw new FoundationFetchError(
168
+ `foundation entry not found under ${baseUrl}\n` +
169
+ `tried: ${ENTRY_CANDIDATES.join(', ')}\n` +
170
+ `hint: check the foundation is published at that version`
171
+ )
172
+ }
173
+
117
174
  // Make unipress's own copies of the externalized packages reachable
118
175
  // from the cache dir. Node's ESM loader walks up from the importing
119
176
  // file looking for `node_modules/<name>` — by placing a node_modules
120
- // directory next to the cached foundation.js with symlinks to each
177
+ // directory next to the cached entry with symlinks to each
121
178
  // external's package directory, bare imports inside the foundation
122
179
  // resolve to unipress's already-installed copies. This keeps a single
123
180
  // React instance (unipress's) across host and foundation.
@@ -7,8 +7,8 @@
7
7
  //
8
8
  // Five accepted ref forms:
9
9
  //
10
- // 1. URL ............ https://.../foundation.js — downloaded + cached
11
- // 2. Local path ..... ./foo, ../foo, /abs/foo, ./foo/dist/foundation.js
10
+ // 1. URL ............ https://.../entry.js — downloaded + cached
11
+ // 2. Local path ..... ./foo, ../foo, /abs/foo, ./foo/dist/entry.js
12
12
  // 3. Registry ref ... @<namespace>/<name>@<version> — constructed into a
13
13
  // URL via the registry base (UNIWEB_REGISTRY_URL or
14
14
  // the production default), then resolved as URL
@@ -24,7 +24,7 @@
24
24
  //
25
25
  // The chosen entry for a package is its `exports['./dist']` map, NOT the
26
26
  // default `.` entry — the default points at source (_entry.generated.js)
27
- // and is only valid inside a Vite build. `dist/foundation.js` is the
27
+ // and is only valid inside a Vite build. `dist/entry.js` is the
28
28
  // built artifact (the federated module) that Node can import.
29
29
 
30
30
  import { existsSync, statSync, readFileSync } from 'node:fs'
@@ -45,14 +45,14 @@ const REGISTRY_REF_PATTERN = /^@([a-z0-9][a-z0-9._-]*)\/([a-z0-9][a-z0-9._-]*)@(
45
45
  // unipress's bundled foundations are distributed as static artifacts on
46
46
  // the unipress repo's GitHub Pages site. URL pattern:
47
47
  //
48
- // https://uniweb.github.io/unipress/foundations/<name>/<version>/foundation.js
48
+ // https://uniweb.github.io/unipress/foundations/<name>/<version>/entry.js
49
49
  //
50
50
  // The namespace portion of a registry ref is implicit — every foundation
51
51
  // served from this base is under @uniweb/. Set UNIWEB_REGISTRY_URL to
52
52
  // override (e.g., for testing against a local http.server during foundation
53
53
  // development, or for users with a private alternative).
54
54
  const DEFAULT_REGISTRY_BASE = 'https://uniweb.github.io/unipress'
55
- const DEFAULT_BUILT_ENTRY = 'dist/foundation.js'
55
+ const DEFAULT_BUILT_ENTRY = 'dist/entry.js'
56
56
 
57
57
  function getRegistryBase() {
58
58
  const raw = process.env.UNIWEB_REGISTRY_URL || DEFAULT_REGISTRY_BASE
@@ -67,7 +67,7 @@ function getRegistryBase() {
67
67
  // `UNIWEB_REGISTRY_URL=https://my.host/by-ns/uniweb` and live without the
68
68
  // per-namespace split, or maintain a redirect rule on that host).
69
69
  function buildRegistryUrl(namespace, name, version) {
70
- return `${getRegistryBase()}/foundations/${name}/${version}/foundation.js`
70
+ return `${getRegistryBase()}/foundations/${name}/${version}/entry.js`
71
71
  }
72
72
 
73
73
  export async function resolveFoundationRef(ref, { anchorDir, onProgress = () => {} } = {}) {
@@ -188,7 +188,7 @@ async function resolvePackageRef(name, anchorDir) {
188
188
  : null
189
189
 
190
190
  // Prefer the explicit `./dist` exports subpath if declared; otherwise
191
- // fall back to the convention `dist/foundation.js`.
191
+ // fall back to the convention `dist/entry.js`.
192
192
  const distExport = pickBuiltEntry(pkg?.exports)
193
193
  const file = distExport
194
194
  ? resolve(pkgRoot, distExport)
@@ -218,7 +218,7 @@ function findPackageDir(name, fromDir) {
218
218
  }
219
219
  }
220
220
 
221
- // A foundation's `exports['./dist']` entry points at the built `foundation.js`.
221
+ // A foundation's `exports['./dist']` entry points at the built `entry.js`.
222
222
  // The value can be a string or a conditional-exports object; we accept both.
223
223
  function pickBuiltEntry(exports) {
224
224
  if (!exports || typeof exports !== 'object') return null
@@ -16,7 +16,7 @@
16
16
  * Foundations are deployed as static artifacts to the unipress repo's
17
17
  * GitHub Pages site. URL pattern:
18
18
  *
19
- * https://uniweb.github.io/unipress/foundations/<name>/<version>/foundation.js
19
+ * https://uniweb.github.io/unipress/foundations/<name>/<version>/entry.js
20
20
  *
21
21
  * On every push to main, `.github/workflows/deploy-foundations.yml`
22
22
  * builds each `foundations/<name>/` and layers the resulting `dist/` into
@@ -68,7 +68,7 @@
68
68
  const PUBLIC_FOUNDATIONS_BASE = 'https://uniweb.github.io/unipress/foundations'
69
69
 
70
70
  function publicUrl(name, version) {
71
- return `${PUBLIC_FOUNDATIONS_BASE}/${name}/${version}/foundation.js`
71
+ return `${PUBLIC_FOUNDATIONS_BASE}/${name}/${version}/entry.js`
72
72
  }
73
73
 
74
74
  const BOOK_FOUNDATION = {
@@ -37,7 +37,7 @@ export async function importFoundation(resolvedPath) {
37
37
  throw new FoundationResolutionError(
38
38
  `failed to import foundation at ${resolvedPath}\n` +
39
39
  `cause: ${err.message}\n` +
40
- `hint: confirm the foundation was built with @uniweb/build (dist/foundation.js)`
40
+ `hint: confirm the foundation was built with @uniweb/build (dist/entry.js)`
41
41
  )
42
42
  }
43
43
  }