@open-press/core 0.8.0 → 1.1.0

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 (77) hide show
  1. package/README.md +17 -5
  2. package/engine/cli.mjs +9 -9
  3. package/engine/commands/_shared.mjs +70 -18
  4. package/engine/commands/deploy.mjs +3 -3
  5. package/engine/commands/dev.mjs +13 -4
  6. package/engine/commands/image.mjs +29 -0
  7. package/engine/commands/inspect.mjs +3 -2
  8. package/engine/commands/pdf.mjs +2 -2
  9. package/engine/commands/preview.mjs +2 -2
  10. package/engine/commands/render.mjs +6 -4
  11. package/engine/commands/replace.mjs +1 -1
  12. package/engine/commands/search.mjs +1 -1
  13. package/engine/commands/skills-sync.mjs +71 -0
  14. package/engine/commands/typecheck.mjs +71 -1
  15. package/engine/commands/upgrade.mjs +3 -3
  16. package/engine/document-export.mjs +1 -1
  17. package/engine/output/chrome-pdf.mjs +92 -0
  18. package/engine/output/static-server.mjs +60 -17
  19. package/engine/react/comment-marker.mjs +13 -13
  20. package/engine/react/document-entry.mjs +35 -28
  21. package/engine/react/document-export.mjs +309 -170
  22. package/engine/react/mdx-compile.mjs +30 -0
  23. package/engine/react/measurement-css.mjs +21 -0
  24. package/engine/react/object-entities.mjs +85 -0
  25. package/engine/react/pagination/allocator.mjs +48 -3
  26. package/engine/react/pagination.mjs +1 -1
  27. package/engine/react/pipeline/allocate.mjs +31 -65
  28. package/engine/react/pipeline/frame-measurement.mjs +4 -0
  29. package/engine/react/press-tree-inspection.mjs +172 -0
  30. package/engine/react/sources/mdx-resolver.mjs +1 -1
  31. package/engine/react/style-discovery.mjs +22 -4
  32. package/engine/runtime/config.d.mts +8 -0
  33. package/engine/runtime/config.mjs +57 -60
  34. package/engine/runtime/file-utils.mjs +9 -1
  35. package/engine/runtime/page-geometry.mjs +131 -0
  36. package/engine/runtime/source-text-tools.mjs +1 -1
  37. package/engine/runtime/source-workspace.mjs +12 -3
  38. package/engine/runtime/validation.mjs +19 -10
  39. package/index.html +4 -0
  40. package/package.json +9 -12
  41. package/src/main.tsx +16 -0
  42. package/src/openpress/app/OpenPressApp.tsx +173 -17
  43. package/src/openpress/app/OpenPressRuntime.tsx +10 -2
  44. package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
  45. package/src/openpress/core/Frame.tsx +20 -7
  46. package/src/openpress/core/FrameContext.tsx +2 -0
  47. package/src/openpress/core/Press.tsx +25 -4
  48. package/src/openpress/core/Workspace.tsx +36 -0
  49. package/src/openpress/core/index.tsx +10 -3
  50. package/src/openpress/core/primitives.tsx +48 -1
  51. package/src/openpress/core/types.ts +86 -41
  52. package/src/openpress/core/useSource.ts +1 -1
  53. package/src/openpress/document-model/documentTypes.ts +9 -0
  54. package/src/openpress/document-model/index.ts +1 -0
  55. package/src/openpress/document-model/objectEntityModel.ts +4 -0
  56. package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
  57. package/src/openpress/mdx/index.ts +15 -7
  58. package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
  59. package/src/openpress/reader/index.ts +1 -0
  60. package/src/openpress/workbench/Workbench.tsx +120 -21
  61. package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
  62. package/src/openpress/workbench/actions/SearchControl.tsx +3 -3
  63. package/src/openpress/workbench/actions/index.ts +1 -0
  64. package/src/openpress/workbench/inspector/useInspectorComments.ts +7 -1
  65. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +5 -1
  66. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +4 -2
  67. package/src/openpress/workbench/project/projectSourceModel.ts +2 -2
  68. package/src/openpress/workbench/workbenchFormatters.ts +2 -2
  69. package/src/styles/openpress/reader-runtime.css +9 -0
  70. package/src/styles/openpress/workbench-panels.css +113 -0
  71. package/src/styles/openpress/workspace-gallery.css +300 -0
  72. package/src/styles/openpress.css +1 -5
  73. package/src/vite-env.d.ts +8 -0
  74. package/tsconfig.json +1 -1
  75. package/vite.config.ts +6 -6
  76. package/engine/commands/init.mjs +0 -24
  77. package/engine/init.mjs +0 -90
@@ -152,6 +152,78 @@ export async function printUrlToPdf({
152
152
  }
153
153
  }
154
154
 
155
+ export async function captureUrlPagesToPng({
156
+ root,
157
+ url,
158
+ outDir,
159
+ chrome,
160
+ waitForReady = waitForPrintReady,
161
+ viewport = DEFAULT_PRINT_VIEWPORT,
162
+ debuggingPortBase = 9700,
163
+ debuggingPortRange = 300,
164
+ profilePrefix = "chrome-image",
165
+ }) {
166
+ chrome ??= resolveChromePath();
167
+ await fs.mkdir(outDir, { recursive: true });
168
+
169
+ const debuggingPort = String(debuggingPortBase + Math.floor(Math.random() * debuggingPortRange));
170
+ const profileDir = path.join(root, ".openpress", "tmp", `${profilePrefix}-${process.pid}-${Date.now()}`);
171
+ await fs.mkdir(profileDir, { recursive: true });
172
+
173
+ const child = spawn(
174
+ chrome,
175
+ [
176
+ "--headless=new",
177
+ "--disable-gpu",
178
+ "--no-sandbox",
179
+ `--remote-debugging-port=${debuggingPort}`,
180
+ `--user-data-dir=${profileDir}`,
181
+ "about:blank",
182
+ ],
183
+ { cwd: root, stdio: ["ignore", "pipe", "pipe"] },
184
+ );
185
+
186
+ try {
187
+ const tab = await waitForChromeTab(debuggingPort);
188
+ const client = await connectChromeDevTools(tab.webSocketDebuggerUrl);
189
+ try {
190
+ await preparePdfPage(client, { viewport });
191
+ await client.send("Page.navigate", { url });
192
+ const pageCount = await waitForReady(client);
193
+ const rects = await getPrintPageRects(client);
194
+ if (rects.length === 0) throw new Error("No OpenPress pages found for image export.");
195
+
196
+ const padWidth = Math.max(3, String(rects.length).length);
197
+ const files = [];
198
+ for (const [index, rect] of rects.entries()) {
199
+ const filename = `page-${String(index + 1).padStart(padWidth, "0")}.png`;
200
+ const filePath = path.join(outDir, filename);
201
+ const result = await client.send("Page.captureScreenshot", {
202
+ format: "png",
203
+ fromSurface: true,
204
+ captureBeyondViewport: true,
205
+ clip: {
206
+ x: Math.max(0, rect.x),
207
+ y: Math.max(0, rect.y),
208
+ width: rect.width,
209
+ height: rect.height,
210
+ scale: 1,
211
+ },
212
+ });
213
+ await fs.writeFile(filePath, Buffer.from(String(result.data ?? ""), "base64"));
214
+ files.push(filePath);
215
+ }
216
+
217
+ return { pageCount, files };
218
+ } finally {
219
+ client.close();
220
+ }
221
+ } finally {
222
+ await stopChildProcess(child);
223
+ await cleanupChromeProfile(profileDir);
224
+ }
225
+ }
226
+
155
227
  export async function preparePdfPage(client, { viewport = DEFAULT_PRINT_VIEWPORT } = {}) {
156
228
  await client.send("Page.enable");
157
229
  await client.send("Runtime.enable");
@@ -265,6 +337,26 @@ export async function waitForPrintReady(client) {
265
337
  throw new Error("Timed out waiting for OpenPress pagination before PDF export.");
266
338
  }
267
339
 
340
+ async function getPrintPageRects(client) {
341
+ const result = await client.send("Runtime.evaluate", {
342
+ returnByValue: true,
343
+ expression: `(() => {
344
+ return Array.from(document.querySelectorAll('.openpress-public-page > .openpress-html-page')).map((page, index) => {
345
+ const target = page.querySelector('.openpress-html-page__html') || page;
346
+ const rect = target.getBoundingClientRect();
347
+ return {
348
+ index,
349
+ x: rect.left + window.scrollX,
350
+ y: rect.top + window.scrollY,
351
+ width: rect.width,
352
+ height: rect.height,
353
+ };
354
+ }).filter((rect) => rect.width > 0 && rect.height > 0);
355
+ })()`,
356
+ });
357
+ return Array.isArray(result.result?.value) ? result.result.value : [];
358
+ }
359
+
268
360
  export async function stopChildProcess(child) {
269
361
  if (child.exitCode !== null || child.signalCode !== null) return;
270
362
  child.kill();
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import http from "node:http";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
6
7
  import { searchSourceText } from "../runtime/source-text-tools.mjs";
7
8
  import { handleProjectAssetRequest } from "../react/project-asset-endpoint.mjs";
@@ -13,6 +14,9 @@ const port = Number(valueAfter(rest, "--port") ?? "8765");
13
14
  const root = path.resolve(rootArg);
14
15
  const workspace = path.resolve(valueAfter(rest, "--workspace") ?? await inferWorkspaceRoot(root));
15
16
  const config = await loadConfig(workspace);
17
+ const ENGINE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
18
+ const FRAMEWORK_ROOT = path.resolve(ENGINE_DIR, "..");
19
+ const CLI_ENTRY = path.join(ENGINE_DIR, "cli.mjs");
16
20
 
17
21
  const mimeTypes = {
18
22
  ".html": "text/html; charset=utf-8",
@@ -73,17 +77,45 @@ const server = http.createServer(async (req, res) => {
73
77
  res.end("Forbidden");
74
78
  return;
75
79
  }
76
- const stat = await fs.stat(target);
77
- const filePath = stat.isDirectory() ? path.join(target, "index.html") : target;
78
- const body = await fs.readFile(filePath);
79
- res.writeHead(200, { "Content-Type": mimeTypes[path.extname(filePath)] ?? "application/octet-stream" });
80
- res.end(body);
80
+ try {
81
+ const stat = await fs.stat(target);
82
+ const filePath = stat.isDirectory() ? path.join(target, "index.html") : target;
83
+ const body = await fs.readFile(filePath);
84
+ res.writeHead(200, { "Content-Type": mimeTypes[path.extname(filePath)] ?? "application/octet-stream" });
85
+ res.end(body);
86
+ } catch (err) {
87
+ // SPA fallback: when a path doesn't map to a real file AND it
88
+ // looks like a client-side route (no extension, not under a
89
+ // reserved namespace), serve index.html so the reader's URL-based
90
+ // routing can take over. This lets /cheatsheet / /proposal etc.
91
+ // reload correctly without needing host-level rewrite rules.
92
+ if (err?.code === "ENOENT" && shouldFallbackToIndex(url.pathname)) {
93
+ const indexBody = await fs.readFile(path.join(root, "index.html"));
94
+ res.writeHead(200, { "Content-Type": "text/html" });
95
+ res.end(indexBody);
96
+ return;
97
+ }
98
+ throw err;
99
+ }
81
100
  } catch {
82
101
  res.writeHead(404);
83
102
  res.end("Not found");
84
103
  }
85
104
  });
86
105
 
106
+ function shouldFallbackToIndex(pathname) {
107
+ // Reserved namespaces — real resources whose 404s should stay 404.
108
+ if (pathname.startsWith("/openpress/")) return false;
109
+ if (pathname.startsWith("/__openpress/")) return false;
110
+ if (pathname.startsWith("/assets/")) return false;
111
+ // Anything with a file extension is an asset miss; fall through.
112
+ const lastSlash = pathname.lastIndexOf("/");
113
+ const tail = pathname.slice(lastSlash + 1);
114
+ if (tail.includes(".")) return false;
115
+ // Otherwise: looks like a client-side route — serve the SPA shell.
116
+ return true;
117
+ }
118
+
87
119
  server.listen(port, host, () => {
88
120
  console.log(`OpenPress static preview: http://${host}:${port}/`);
89
121
  });
@@ -149,7 +181,10 @@ function valueAfter(args, flag) {
149
181
 
150
182
  async function inferWorkspaceRoot(staticRoot) {
151
183
  for (const candidate of [staticRoot, path.dirname(staticRoot), path.dirname(path.dirname(staticRoot))]) {
152
- if (await fileExists(path.join(candidate, "openpress.config.mjs"))) return candidate;
184
+ // 1.0 workspace markers: press/index.tsx (the document entry) or
185
+ // package.json with an "openpress" field. Either is sufficient.
186
+ if (await fileExists(path.join(candidate, "press", "index.tsx"))) return candidate;
187
+ if (await hasOpenpressPackageField(candidate)) return candidate;
153
188
  }
154
189
  if (path.basename(path.dirname(staticRoot)) === ".deploy") {
155
190
  return path.dirname(path.dirname(staticRoot));
@@ -157,6 +192,16 @@ async function inferWorkspaceRoot(staticRoot) {
157
192
  return process.cwd();
158
193
  }
159
194
 
195
+ async function hasOpenpressPackageField(dir) {
196
+ try {
197
+ const text = await fs.readFile(path.join(dir, "package.json"), "utf8");
198
+ const parsed = JSON.parse(text);
199
+ return parsed?.openpress && typeof parsed.openpress === "object";
200
+ } catch {
201
+ return false;
202
+ }
203
+ }
204
+
160
205
  async function handleLocalPdfExportRequest(req, res) {
161
206
  if (req.method !== "POST") {
162
207
  writeJson(res, 405, { ok: false, message: "Local PDF export endpoint requires POST." });
@@ -169,7 +214,7 @@ async function handleLocalPdfExportRequest(req, res) {
169
214
  ok: result.code === 0 && exists,
170
215
  code: result.code,
171
216
  pdf: `/__openpress/local-pdf-file?ts=${Date.now()}`,
172
- command: "node engine/cli.mjs pdf .",
217
+ command: "open-press pdf .",
173
218
  stdout: result.stdout,
174
219
  stderr: result.stderr,
175
220
  });
@@ -209,7 +254,7 @@ async function handleDeployRequest(req, res) {
209
254
  deploy_adapter: config.deploy.adapter,
210
255
  deploy_source: config.deploy.source,
211
256
  deploy_project_name: config.deploy.projectName,
212
- command: "node engine/cli.mjs deploy . --confirm",
257
+ command: "open-press deploy . --confirm",
213
258
  });
214
259
  return;
215
260
  }
@@ -228,7 +273,7 @@ async function handleDeployRequest(req, res) {
228
273
  pdf: deployedUrl ? `${deployedUrl}/${config.pdf.filename}` : deploymentInfo.pdf,
229
274
  public_url: publicUrl,
230
275
  dirty: false,
231
- command: "node engine/cli.mjs deploy . --confirm",
276
+ command: "open-press deploy . --confirm",
232
277
  stdout: result.stdout,
233
278
  stderr: result.stderr,
234
279
  });
@@ -313,7 +358,7 @@ async function handleMediaFileRequest(req, res, url) {
313
358
 
314
359
  function runLocalPdfExport() {
315
360
  return new Promise((resolve) => {
316
- const child = spawn("node", ["engine/cli.mjs", "pdf", "."], {
361
+ const child = spawn("node", [CLI_ENTRY, "pdf", "."], {
317
362
  cwd: workspace,
318
363
  shell: false,
319
364
  });
@@ -336,7 +381,7 @@ function runLocalPdfExport() {
336
381
 
337
382
  function runDeploy() {
338
383
  return new Promise((resolve) => {
339
- const child = spawn("node", ["engine/cli.mjs", "deploy", ".", "--confirm"], {
384
+ const child = spawn("node", [CLI_ENTRY, "deploy", ".", "--confirm"], {
340
385
  cwd: workspace,
341
386
  shell: false,
342
387
  });
@@ -367,7 +412,7 @@ function isDeployConfigured() {
367
412
  function deploySetupMessage() {
368
413
  if (isDeployConfigured()) return undefined;
369
414
  if (config.deploy.adapter === "cloudflare-pages") {
370
- return "Cloudflare Pages deployment requires `deploy.projectName` in openpress.config.mjs.";
415
+ return 'Cloudflare Pages deployment requires `openpress.deploy.projectName` in package.json.';
371
416
  }
372
417
  return `Deployment adapter \`${config.deploy.adapter}\` is not configured.`;
373
418
  }
@@ -430,12 +475,10 @@ function getDeploymentSourcePaths() {
430
475
  config.paths.themeDir,
431
476
  config.paths.designDoc,
432
477
  config.paths.componentsDir,
433
- path.join(workspace, "src"),
434
- path.join(workspace, "index.html"),
478
+ path.join(FRAMEWORK_ROOT, "src"),
479
+ path.join(FRAMEWORK_ROOT, "index.html"),
480
+ path.join(FRAMEWORK_ROOT, "vite.config.ts"),
435
481
  path.join(workspace, "package.json"),
436
- path.join(workspace, "openpress.config.mjs"),
437
- config.configPath,
438
- path.join(workspace, "vite.config.ts"),
439
482
  ];
440
483
  }
441
484
 
@@ -4,15 +4,15 @@ import path from "node:path";
4
4
  import { loadConfig } from "../runtime/config.mjs";
5
5
  import { collectSourceTextFiles } from "../runtime/source-text-tools.mjs";
6
6
 
7
- // Any `.mdx` or `.tsx` file under `document/` is a legal comment target.
7
+ // Any `.mdx` or `.tsx` file under `press/` is a legal comment target.
8
8
  // The Press Tree allows arbitrary source layouts — `section-folders`,
9
9
  // `section-files`, `file-list`, custom `root` paths, etc. — so we no
10
- // longer hardcode `document/chapters/<slug>/content/*.mdx`. The boundary
11
- // is "inside the workspace's authored `document/` directory" and "looks
10
+ // longer hardcode `press/chapters/<slug>/content/*.mdx`. The boundary
11
+ // is "inside the workspace's authored `press/` directory" and "looks
12
12
  // like an editable React/MDX source" by extension.
13
13
  const EDITABLE_COMMENT_SOURCE_PATTERNS = [
14
- /^document\/.+\.mdx$/,
15
- /^document\/.+\.tsx$/,
14
+ /^press\/.+\.mdx$/,
15
+ /^press\/.+\.tsx$/,
16
16
  ];
17
17
  const COMMENT_MARKER_RE = /(?:\{\/\*|\/\*)\s*@openpress-comment\b(?<attrs>[^*]*)\*\/\}?/g;
18
18
  const COMMENT_LINE_RE = /^\s*(?:\{\/\*|\/\*)\s*@openpress-comment\b[^*]*\*\/\}?\s*$/;
@@ -167,23 +167,23 @@ function normalizeEditableSourcePath(value) {
167
167
  throw new Error(`OpenPress comment target path is invalid: ${value}`);
168
168
  }
169
169
  const posix = path.posix.normalize(normalized);
170
- // The Press Tree source resolver emits paths relative to `document/`
170
+ // The Press Tree source resolver emits paths relative to `press/`
171
171
  // (e.g. "chapters/01-start/content/01-start.mdx"). The comment marker
172
- // works in workspace-relative paths (with the `document/` prefix). If
173
- // the incoming path is documentRoot-relative, prepend `document/`.
174
- if (!posix.startsWith("document/") && looksDocumentRelative(posix)) {
175
- return `document/${posix}`;
172
+ // works in workspace-relative paths (with the `press/` prefix). If
173
+ // the incoming path is documentRoot-relative, prepend `press/`.
174
+ if (!posix.startsWith("press/") && looksDocumentRelative(posix)) {
175
+ return `press/${posix}`;
176
176
  }
177
177
  return posix;
178
178
  }
179
179
 
180
180
  // Identify paths the Press Tree source resolver emits — those are relative
181
- // to `document/`. Match `.mdx` / `.tsx` files that don't already have the
182
- // `document/` prefix and don't look like system / engine paths. The check
181
+ // to `press/`. Match `.mdx` / `.tsx` files that don't already have the
182
+ // `press/` prefix and don't look like system / engine paths. The check
183
183
  // is intentionally tight so we never silently rewrite engine internals
184
184
  // (e.g. `src/openpress/...`) into "editable" workspace paths.
185
185
  const SYSTEM_PATH_PREFIXES = [
186
- "document/",
186
+ "press/",
187
187
  "src/",
188
188
  "engine/",
189
189
  "dist/",
@@ -1,6 +1,6 @@
1
1
  // Layer 1 — Document entry loader.
2
2
  //
3
- // Loads `document/index.tsx`, validates it exports a Press component as
3
+ // Loads `press/index.tsx`, validates it exports a Press component as
4
4
  // default, reads optional `config` and `sources` named exports, and sets
5
5
  // up the vite SSR server with `@open-press/core` aliases (including the
6
6
  // subpaths `/mdx` and `/manuscript`).
@@ -12,7 +12,8 @@ import { fileURLToPath } from "node:url";
12
12
  import react from "@vitejs/plugin-react";
13
13
  import ts from "typescript";
14
14
  import { createServer as createViteServer } from "vite";
15
- import { normalizeConfig } from "../runtime/config.mjs";
15
+ import { loadConfig } from "../runtime/config.mjs";
16
+ import { inspectPressTree } from "./press-tree-inspection.mjs";
16
17
 
17
18
  const ENGINE_REACT_DIR = path.dirname(fileURLToPath(import.meta.url));
18
19
  const FRAMEWORK_ROOT = path.resolve(ENGINE_REACT_DIR, "..", "..");
@@ -24,10 +25,17 @@ const REACT_PACKAGE_ROOT = path.join(FRAMEWORK_ROOT, "node_modules", "react");
24
25
  const require = createRequire(import.meta.url);
25
26
  const REACT_EXPORT_NAMES = Object.keys(require("react")).filter((name) => /^[A-Za-z_$][\w$]*$/.test(name));
26
27
 
28
+ // 1.0 contract: the document entry lives at press/index.tsx.
29
+ async function resolveEntryPath(workspaceRoot) {
30
+ const candidate = path.join(workspaceRoot, "press", "index.tsx");
31
+ if (await fileExists(candidate)) return candidate;
32
+ return null;
33
+ }
34
+
27
35
  export async function loadReactDocumentEntry(root = ".", { server: externalServer } = {}) {
28
36
  const workspaceRoot = path.resolve(root);
29
- const entryPath = path.join(workspaceRoot, "document", "index.tsx");
30
- if (!(await fileExists(entryPath))) return null;
37
+ const entryPath = await resolveEntryPath(workspaceRoot);
38
+ if (!entryPath) return null;
31
39
 
32
40
  const source = await fs.readFile(entryPath, "utf8");
33
41
  assertNoObviousTopLevelSideEffects(source, entryPath);
@@ -44,19 +52,35 @@ export async function loadReactDocumentEntry(root = ".", { server: externalServe
44
52
  // export pipeline throws separately if it's missing when actually needed.
45
53
  const Press = typeof mod.default === "function" ? mod.default : null;
46
54
 
47
- const config = normalizeReactDocumentConfig(workspaceRoot, entryPath, mod.config);
48
- const sources = mod.sources ?? {};
49
- if (sources && (typeof sources !== "object" || Array.isArray(sources))) {
50
- throw new Error(
51
- `OpenPress document entry ${entryPath} \`sources\` export must be an object literal (or omitted).`,
55
+ // Inspect the JSX tree returned by the user's default export to
56
+ // pull <Workspace> / <Press> props declared inline. The 1.0 contract
57
+ // treats workspaces uniformly as "array of Press children" the
58
+ // single-doc case is just length 1.
59
+ let inspection = { workspaceProps: {}, presses: [], wrappedInWorkspace: false };
60
+ if (Press) {
61
+ const coreModule = await ownServer.ssrLoadModule(
62
+ path.join(FRAMEWORK_ROOT, "src", "openpress", "core", "index.tsx"),
52
63
  );
64
+ inspection = inspectPressTree({
65
+ UserComponent: Press,
66
+ PRESS_MARKER: coreModule.PRESS_MARKER,
67
+ WORKSPACE_MARKER: coreModule.WORKSPACE_MARKER,
68
+ });
53
69
  }
54
70
 
71
+ // Workspace-level config (deploy, pdf, captionNumbering defaults)
72
+ // comes from package.json "openpress" via loadConfig. Each Press
73
+ // overlays its own metadata via JSX props at export time.
74
+ const config = await loadConfig(workspaceRoot);
75
+
55
76
  return {
56
77
  entryPath,
57
78
  config,
58
79
  Press,
59
- sources,
80
+ presses: inspection.presses,
81
+ workspaceProps: inspection.workspaceProps,
82
+ pressCount: inspection.presses.length,
83
+ wrappedInWorkspace: inspection.wrappedInWorkspace,
60
84
  };
61
85
  } finally {
62
86
  if (!externalServer) await ownServer.close();
@@ -80,7 +104,7 @@ export async function createReactSsrServer(workspaceRoot = ".") {
80
104
  { find: "@open-press/core/manuscript", replacement: MANUSCRIPT_ENTRY },
81
105
  { find: "@open-press/core/numbering", replacement: NUMBERING_ENTRY },
82
106
  { find: "@open-press/core", replacement: CORE_ENTRY },
83
- { find: "@/components", replacement: path.join(resolvedWorkspaceRoot, "document", "components") },
107
+ { find: "@/components", replacement: path.join(resolvedWorkspaceRoot, "press", "components") },
84
108
  ],
85
109
  },
86
110
  optimizeDeps: {
@@ -250,23 +274,6 @@ function isFileSystemModule(moduleName) {
250
274
  return moduleName === "fs" || moduleName === "node:fs" || moduleName === "fs/promises" || moduleName === "node:fs/promises";
251
275
  }
252
276
 
253
- function normalizeReactDocumentConfig(workspaceRoot, entryPath, config) {
254
- if (config != null && (typeof config !== "object" || Array.isArray(config))) {
255
- throw new Error("OpenPress React document entry `config` export must be an object when provided.");
256
- }
257
- const rawConfig = config ?? {};
258
- const paths = rawConfig.paths ?? {};
259
- return normalizeConfig(workspaceRoot, {
260
- ...rawConfig,
261
- documentDir: rawConfig.documentDir ?? paths.documentDir ?? "document",
262
- sourceDir: rawConfig.sourceDir ?? paths.chaptersDir ?? paths.sourceDir ?? "chapters",
263
- componentsDir: rawConfig.componentsDir ?? paths.componentsDir ?? "components",
264
- mediaDir: rawConfig.mediaDir ?? paths.mediaDir ?? "media",
265
- themeDir: rawConfig.themeDir ?? paths.themeDir ?? "theme",
266
- designDoc: rawConfig.designDoc ?? paths.designDoc ?? "design.md",
267
- }, entryPath);
268
- }
269
-
270
277
  async function fileExists(filePath) {
271
278
  try {
272
279
  const stat = await fs.stat(filePath);