@uniweb/unipress 0.4.26 → 0.4.28
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/package.json +5 -5
- package/src/foundation-fetch.js +32 -75
- package/src/orchestrator.js +12 -3
- package/src/runtime-externals.js +138 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/unipress",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.28",
|
|
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": "^19.0.0",
|
|
51
51
|
"react-dom": "^19.0.0",
|
|
52
|
-
"@uniweb/core": "0.7.14",
|
|
53
|
-
"@uniweb/build": "0.14.19",
|
|
54
52
|
"@uniweb/content-reader": "1.1.12",
|
|
55
|
-
"@uniweb/
|
|
56
|
-
"@uniweb/
|
|
53
|
+
"@uniweb/build": "0.14.20",
|
|
54
|
+
"@uniweb/core": "0.7.14",
|
|
55
|
+
"@uniweb/runtime": "0.8.20",
|
|
56
|
+
"@uniweb/semantic-parser": "1.1.17"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"vitest": "^4.1.7"
|
package/src/foundation-fetch.js
CHANGED
|
@@ -31,27 +31,11 @@
|
|
|
31
31
|
// field and this module will verify bytes before caching.
|
|
32
32
|
|
|
33
33
|
import { existsSync } from 'node:fs'
|
|
34
|
-
import { mkdir, writeFile
|
|
35
|
-
import { dirname, join, posix
|
|
36
|
-
import { createRequire } from 'node:module'
|
|
37
|
-
import { fileURLToPath } from 'node:url'
|
|
34
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
35
|
+
import { dirname, join, posix } from 'node:path'
|
|
38
36
|
import { FoundationFetchError } from './errors.js'
|
|
39
37
|
import { getCacheDir } from './typst/binary-manager.js'
|
|
40
|
-
|
|
41
|
-
const require = createRequire(import.meta.url)
|
|
42
|
-
|
|
43
|
-
// Bare specifiers that the foundation expects to resolve externally
|
|
44
|
-
// (matches DEFAULT_EXTERNALS in @uniweb/build). At import time, Node
|
|
45
|
-
// walks up from the cache dir looking for a node_modules/<name>. The
|
|
46
|
-
// cache dir isn't inside any package tree, so we link unipress's own
|
|
47
|
-
// installations into a co-located node_modules.
|
|
48
|
-
const EXTERNAL_PACKAGES = [
|
|
49
|
-
'react',
|
|
50
|
-
'react-dom',
|
|
51
|
-
'react/jsx-runtime',
|
|
52
|
-
'react/jsx-dev-runtime',
|
|
53
|
-
'@uniweb/core',
|
|
54
|
-
]
|
|
38
|
+
import { externalShimPackages } from './runtime-externals.js'
|
|
55
39
|
|
|
56
40
|
// Match `./chunk.js` only in actual ESM import positions — `from './x'`,
|
|
57
41
|
// `import './x'`, or `import('./x')`. A broad "match any quoted ./x.js"
|
|
@@ -100,6 +84,10 @@ export async function fetchFoundationToCache(url, { onProgress = () => {} } = {}
|
|
|
100
84
|
const cached = join(cacheDir, name)
|
|
101
85
|
if (existsSync(cached)) {
|
|
102
86
|
onProgress(`using cached foundation: ${cached}`)
|
|
87
|
+
// Refresh the external shims even on a cache hit: it's a handful of tiny
|
|
88
|
+
// files, and it self-heals a cache populated by an older unipress (which
|
|
89
|
+
// left an empty node_modules or dead symlinks that no longer resolve).
|
|
90
|
+
await writeExternalShims(cacheDir, onProgress)
|
|
103
91
|
return cached
|
|
104
92
|
}
|
|
105
93
|
}
|
|
@@ -138,7 +126,7 @@ export async function fetchFoundationToCache(url, { onProgress = () => {} } = {}
|
|
|
138
126
|
if (!fetched.has(discovered)) queue.push(discovered)
|
|
139
127
|
}
|
|
140
128
|
}
|
|
141
|
-
await
|
|
129
|
+
await writeExternalShims(cacheDir, onProgress)
|
|
142
130
|
const entryPath = join(cacheDir, entryName)
|
|
143
131
|
onProgress(`foundation cached at ${cacheDir} (${fetched.size} file(s))`)
|
|
144
132
|
return entryPath
|
|
@@ -171,63 +159,32 @@ async function pickRemoteEntry(baseUrl, onProgress) {
|
|
|
171
159
|
)
|
|
172
160
|
}
|
|
173
161
|
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
162
|
+
// Lay shim modules for the foundation's externalized peer deps into a
|
|
163
|
+
// node_modules beside the cached entry, so the dynamically imported foundation
|
|
164
|
+
// resolves react / react-dom / @uniweb/core to unipress's own bundled instances
|
|
165
|
+
// (via the globalThis bridge in runtime-externals.js) rather than hunting for a
|
|
166
|
+
// node_modules that isn't there.
|
|
167
|
+
//
|
|
168
|
+
// Replaces the old symlink-to-local-copies approach, which couldn't locate
|
|
169
|
+
// on-disk copies inside the `bun --compile` binary: the deps are bundled into
|
|
170
|
+
// the binary, and require.resolve resolves relative to the CWD, so it found
|
|
171
|
+
// nothing when unipress ran from a project with no react up its tree. The shims
|
|
172
|
+
// read the bridge instead of resolving a real package, so they work from any
|
|
173
|
+
// directory and in any distribution. See runtime-externals.js for the full
|
|
174
|
+
// rationale and the single-instance guarantee.
|
|
175
|
+
//
|
|
176
|
+
// Idempotent and overwriting: cheap (a handful of tiny files), and self-heals a
|
|
177
|
+
// cache left in a bad state by an older unipress.
|
|
178
|
+
async function writeExternalShims(cacheDir, onProgress) {
|
|
182
179
|
const nmDir = join(cacheDir, 'node_modules')
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
? spec.split('/').slice(0, 2).join('/') // '@scope/name'
|
|
190
|
-
: spec.split('/')[0]) // 'name'
|
|
191
|
-
}
|
|
192
|
-
for (const name of rootSpecs) {
|
|
193
|
-
const dest = name.startsWith('@')
|
|
194
|
-
? join(nmDir, ...name.split('/')) // node_modules/@scope/name
|
|
195
|
-
: join(nmDir, name) // node_modules/name
|
|
196
|
-
if (existsSync(dest)) continue
|
|
197
|
-
try {
|
|
198
|
-
const pkgDir = findPackageRoot(name)
|
|
199
|
-
if (!pkgDir) {
|
|
200
|
-
onProgress(` warn: cannot find '${name}' in unipress deps — foundation may fail to import`)
|
|
201
|
-
continue
|
|
202
|
-
}
|
|
203
|
-
await mkdir(dirname(dest), { recursive: true })
|
|
204
|
-
await symlink(pkgDir, dest, 'dir')
|
|
205
|
-
onProgress(` linked ${name} → ${pkgDir}`)
|
|
206
|
-
} catch (err) {
|
|
207
|
-
onProgress(` warn: linking ${name} failed: ${err.message}`)
|
|
180
|
+
for (const pkg of externalShimPackages()) {
|
|
181
|
+
const pkgDir = join(nmDir, ...pkg.dir.split('/'))
|
|
182
|
+
await mkdir(pkgDir, { recursive: true })
|
|
183
|
+
await writeFile(join(pkgDir, 'package.json'), pkg.packageJson)
|
|
184
|
+
for (const [rel, source] of Object.entries(pkg.files)) {
|
|
185
|
+
await writeFile(join(pkgDir, rel), source)
|
|
208
186
|
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function findPackageRoot(name) {
|
|
213
|
-
try {
|
|
214
|
-
// Resolve the package's `package.json` to find its root.
|
|
215
|
-
const pkgJson = require.resolve(`${name}/package.json`)
|
|
216
|
-
return dirname(pkgJson)
|
|
217
|
-
} catch {
|
|
218
|
-
// Fallback: resolve a default export and strip back.
|
|
219
|
-
try {
|
|
220
|
-
const entry = require.resolve(name)
|
|
221
|
-
let dir = dirname(entry)
|
|
222
|
-
while (dir !== dirname(dir)) {
|
|
223
|
-
if (existsSync(join(dir, 'package.json'))) {
|
|
224
|
-
const pkg = JSON.parse(require('fs').readFileSync(join(dir, 'package.json'), 'utf8'))
|
|
225
|
-
if (pkg.name === name || pkg.name === name.split('/')[0]) return dir
|
|
226
|
-
}
|
|
227
|
-
dir = dirname(dir)
|
|
228
|
-
}
|
|
229
|
-
} catch {}
|
|
230
|
-
return null
|
|
187
|
+
onProgress(` shimmed ${pkg.dir} → unipress's bundled copy`)
|
|
231
188
|
}
|
|
232
189
|
}
|
|
233
190
|
|
package/src/orchestrator.js
CHANGED
|
@@ -21,14 +21,23 @@
|
|
|
21
21
|
//
|
|
22
22
|
// React-instance note (gotcha #2): @uniweb/runtime/ssr (built bundle) imports
|
|
23
23
|
// React as an external; the foundation does the same. Both must resolve to the
|
|
24
|
-
//
|
|
25
|
-
// hook call"
|
|
26
|
-
//
|
|
24
|
+
// SAME React instance, otherwise hooks in foundation components throw "Invalid
|
|
25
|
+
// hook call" — and the foundation must see the SAME @uniweb/core unipress built
|
|
26
|
+
// the Website graph with. Importing runtime-externals.js (below) publishes
|
|
27
|
+
// unipress's own bundled react / react-dom / @uniweb/core on a globalThis
|
|
28
|
+
// bridge and drives the cache-side shim modules that the fetched foundation
|
|
29
|
+
// resolves against, guaranteeing one shared instance regardless of how unipress
|
|
30
|
+
// is run (source, npm install, or the bun --compile binary). See
|
|
31
|
+
// runtime-externals.js for the full rationale.
|
|
27
32
|
|
|
28
33
|
import { pathToFileURL } from 'node:url'
|
|
29
34
|
import { readFile } from 'node:fs/promises'
|
|
30
35
|
import { initPrerender } from '@uniweb/runtime/ssr'
|
|
31
36
|
import { FoundationResolutionError, CompileError } from './errors.js'
|
|
37
|
+
// Side-effect import: sets the globalThis bridges before any foundation is
|
|
38
|
+
// dynamically imported. ESM evaluates it once (foundation-fetch.js imports it
|
|
39
|
+
// too, to generate the matching shims).
|
|
40
|
+
import './runtime-externals.js'
|
|
32
41
|
|
|
33
42
|
export async function importFoundation(resolvedPath) {
|
|
34
43
|
try {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Single-instance linkage for dynamically imported foundations.
|
|
2
|
+
//
|
|
3
|
+
// A built foundation externalizes its peer deps — react, react-dom,
|
|
4
|
+
// react-dom/server, react/jsx-runtime, react/jsx-dev-runtime, @uniweb/core
|
|
5
|
+
// (the set in @uniweb/build's foundation/config.js DEFAULT_EXTERNALS). When
|
|
6
|
+
// unipress fetches a foundation to its cache and dynamically imports the built
|
|
7
|
+
// entry.js, those bare imports must resolve to unipress's OWN copies so the
|
|
8
|
+
// foundation shares one React instance with @uniweb/runtime/ssr and one
|
|
9
|
+
// @uniweb/core with the Website graph the orchestrator builds.
|
|
10
|
+
//
|
|
11
|
+
// The old approach symlinked unipress's node_modules copies beside the cached
|
|
12
|
+
// entry. That relies on those copies existing on disk and being locatable via
|
|
13
|
+
// require.resolve — which fails in the `bun --compile` standalone binary: the
|
|
14
|
+
// deps are bundled INTO the binary, and require.resolve does CWD-relative
|
|
15
|
+
// resolution, so it finds nothing when unipress runs from a project (like a
|
|
16
|
+
// loose book folder) that has no react up its directory tree. The symlink step
|
|
17
|
+
// then produced an empty node_modules and the foundation import failed with
|
|
18
|
+
// "Cannot find module 'react/jsx-runtime'".
|
|
19
|
+
//
|
|
20
|
+
// Instead we mirror @uniweb/runtime's worker-isolate shims (its
|
|
21
|
+
// build-worker.js): publish each package on a globalThis bridge here, and write
|
|
22
|
+
// tiny shim modules into the cache's co-located node_modules (see
|
|
23
|
+
// writeExternalShims in foundation-fetch.js) that re-export from the bridge.
|
|
24
|
+
// Bun/Node resolve the foundation's bare imports to the shims (nearest
|
|
25
|
+
// node_modules wins); each shim reads the bridge and yields unipress's exact
|
|
26
|
+
// instance — no on-disk package needed, works identically in the compiled
|
|
27
|
+
// binary and in source/npm mode.
|
|
28
|
+
//
|
|
29
|
+
// Why globalThis rather than a direct import inside the shim: the shim lives on
|
|
30
|
+
// disk beside the foundation; an `import 'react'` from it would re-enter
|
|
31
|
+
// resolution (and in the binary resolve to nothing). Reading a global sidesteps
|
|
32
|
+
// module resolution entirely — the same reason the isolate uses it.
|
|
33
|
+
//
|
|
34
|
+
// Importing this module has the SIDE EFFECT of setting the globalThis bridges,
|
|
35
|
+
// so it must be imported before any foundation is dynamically imported. Both
|
|
36
|
+
// foundation-fetch.js (which writes the shims) and orchestrator.js (which does
|
|
37
|
+
// the import) import it; ESM evaluates it once.
|
|
38
|
+
|
|
39
|
+
import * as React from 'react'
|
|
40
|
+
import * as ReactJsxRuntime from 'react/jsx-runtime'
|
|
41
|
+
import * as ReactJsxDevRuntime from 'react/jsx-dev-runtime'
|
|
42
|
+
import * as ReactDom from 'react-dom'
|
|
43
|
+
import * as ReactDomServer from 'react-dom/server'
|
|
44
|
+
import * as UniwebCore from '@uniweb/core'
|
|
45
|
+
|
|
46
|
+
// globalThis bridge keys. Namespaced to unipress so they never collide with
|
|
47
|
+
// @uniweb/runtime's worker isolate (__PLATFORM_*), which a foundation could
|
|
48
|
+
// conceivably bundle into the same realm.
|
|
49
|
+
const REACT_KEY = '__UNIPRESS_REACT'
|
|
50
|
+
const JSX_RUNTIME_KEY = '__UNIPRESS_JSX_RUNTIME'
|
|
51
|
+
const JSX_DEV_RUNTIME_KEY = '__UNIPRESS_JSX_DEV_RUNTIME'
|
|
52
|
+
const REACT_DOM_KEY = '__UNIPRESS_REACT_DOM'
|
|
53
|
+
const REACT_DOM_SERVER_KEY = '__UNIPRESS_REACT_DOM_SERVER'
|
|
54
|
+
const UNIWEB_CORE_KEY = '__UNIPRESS_UNIWEB_CORE'
|
|
55
|
+
|
|
56
|
+
globalThis[REACT_KEY] = React
|
|
57
|
+
globalThis[JSX_RUNTIME_KEY] = ReactJsxRuntime
|
|
58
|
+
globalThis[JSX_DEV_RUNTIME_KEY] = ReactJsxDevRuntime
|
|
59
|
+
globalThis[REACT_DOM_KEY] = ReactDom
|
|
60
|
+
globalThis[REACT_DOM_SERVER_KEY] = ReactDomServer
|
|
61
|
+
globalThis[UNIWEB_CORE_KEY] = UniwebCore
|
|
62
|
+
|
|
63
|
+
const VALID_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
64
|
+
|
|
65
|
+
// Generate an ESM shim module that re-exports every named export of `ns` from
|
|
66
|
+
// the globalThis bridge. Names are enumerated from the live namespace (as
|
|
67
|
+
// build-worker.js does) so the shim tracks the actual React / core surface
|
|
68
|
+
// without a hardcoded list. Non-identifier keys (e.g. "module.exports" from CJS
|
|
69
|
+
// interop) are skipped; `default` is re-exported explicitly.
|
|
70
|
+
function shimSource(globalKey, ns) {
|
|
71
|
+
const names = Object.keys(ns).filter(
|
|
72
|
+
(name) => name !== 'default' && VALID_IDENTIFIER.test(name)
|
|
73
|
+
)
|
|
74
|
+
return [
|
|
75
|
+
`// AUTO-GENERATED by unipress — re-exports from globalThis.${globalKey}.`,
|
|
76
|
+
`// unipress publishes its own bundled instance there (see runtime-externals.js),`,
|
|
77
|
+
`// giving this dynamically-imported foundation the SAME instance unipress uses.`,
|
|
78
|
+
`const __ns = globalThis.${globalKey}`,
|
|
79
|
+
`export default __ns?.default ?? __ns`,
|
|
80
|
+
...names.map((name) => `export const ${name} = __ns.${name}`),
|
|
81
|
+
''
|
|
82
|
+
].join('\n')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function modulePackageJson(name, exportsMap) {
|
|
86
|
+
return JSON.stringify(
|
|
87
|
+
{
|
|
88
|
+
name,
|
|
89
|
+
type: 'module',
|
|
90
|
+
exports: { ...exportsMap, './package.json': './package.json' }
|
|
91
|
+
},
|
|
92
|
+
null,
|
|
93
|
+
2
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The shim packages to lay into a fetched foundation's co-located node_modules.
|
|
98
|
+
// Each entry: the package directory (relative to node_modules, POSIX-style),
|
|
99
|
+
// its package.json text, and the shim files keyed by relative path. Subpath
|
|
100
|
+
// specifiers (react/jsx-runtime, react-dom/server) are served through the
|
|
101
|
+
// package's `exports` map so Node/Bun resolve them to the right shim file.
|
|
102
|
+
export function externalShimPackages() {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
dir: 'react',
|
|
106
|
+
packageJson: modulePackageJson('react', {
|
|
107
|
+
'.': './index.js',
|
|
108
|
+
'./jsx-runtime': './jsx-runtime.js',
|
|
109
|
+
'./jsx-dev-runtime': './jsx-dev-runtime.js'
|
|
110
|
+
}),
|
|
111
|
+
files: {
|
|
112
|
+
'index.js': shimSource(REACT_KEY, React),
|
|
113
|
+
'jsx-runtime.js': shimSource(JSX_RUNTIME_KEY, ReactJsxRuntime),
|
|
114
|
+
'jsx-dev-runtime.js': shimSource(JSX_DEV_RUNTIME_KEY, ReactJsxDevRuntime)
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
dir: 'react-dom',
|
|
119
|
+
packageJson: modulePackageJson('react-dom', {
|
|
120
|
+
'.': './index.js',
|
|
121
|
+
'./server': './server.js'
|
|
122
|
+
}),
|
|
123
|
+
files: {
|
|
124
|
+
'index.js': shimSource(REACT_DOM_KEY, ReactDom),
|
|
125
|
+
'server.js': shimSource(REACT_DOM_SERVER_KEY, ReactDomServer)
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
dir: '@uniweb/core',
|
|
130
|
+
packageJson: modulePackageJson('@uniweb/core', {
|
|
131
|
+
'.': './index.js'
|
|
132
|
+
}),
|
|
133
|
+
files: {
|
|
134
|
+
'index.js': shimSource(UNIWEB_CORE_KEY, UniwebCore)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|