@open-press/core 0.7.1 → 1.0.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 (144) hide show
  1. package/README.md +6 -3
  2. package/engine/cli.mjs +8 -8
  3. package/engine/commands/_shared.mjs +37 -15
  4. package/engine/commands/dev.mjs +2 -2
  5. package/engine/commands/image.mjs +29 -0
  6. package/engine/commands/skills-sync.mjs +71 -0
  7. package/engine/commands/typecheck.mjs +63 -1
  8. package/engine/commands/upgrade.mjs +3 -3
  9. package/engine/document-export.mjs +1 -1
  10. package/engine/output/chrome-pdf.mjs +110 -3
  11. package/engine/output/static-server.mjs +87 -9
  12. package/engine/react/comment-endpoint.mjs +13 -39
  13. package/engine/react/comment-marker.mjs +43 -19
  14. package/engine/react/document-entry.mjs +46 -28
  15. package/engine/react/document-export.mjs +328 -164
  16. package/engine/react/http-json.mjs +24 -0
  17. package/engine/react/mdx-compile.mjs +126 -3
  18. package/engine/react/measurement-css.mjs +114 -1
  19. package/engine/react/object-entities.mjs +204 -0
  20. package/engine/react/pagination/allocator.mjs +48 -3
  21. package/engine/react/pagination.mjs +1 -1
  22. package/engine/react/pipeline/allocate.mjs +41 -72
  23. package/engine/react/pipeline/frame-measurement.mjs +6 -0
  24. package/engine/react/press-tree-inspection.mjs +172 -0
  25. package/engine/react/project-asset-endpoint.mjs +6 -24
  26. package/engine/react/source-edit-endpoint.d.mts +10 -0
  27. package/engine/react/source-edit-endpoint.mjs +75 -0
  28. package/engine/react/sources/mdx-resolver.mjs +13 -15
  29. package/engine/react/style-discovery.mjs +23 -8
  30. package/engine/runtime/config.d.mts +8 -0
  31. package/engine/runtime/config.mjs +57 -60
  32. package/engine/runtime/file-utils.mjs +9 -1
  33. package/engine/runtime/file-walk.mjs +22 -0
  34. package/engine/runtime/inspection.mjs +1 -20
  35. package/engine/runtime/page-geometry.mjs +131 -0
  36. package/engine/runtime/path-utils.mjs +20 -0
  37. package/engine/runtime/source-text-tools.d.mts +102 -0
  38. package/engine/runtime/source-text-tools.mjs +551 -16
  39. package/engine/runtime/source-workspace.mjs +16 -34
  40. package/engine/runtime/validation.mjs +19 -10
  41. package/package.json +3 -5
  42. package/src/openpress/app/OpenPressApp.tsx +296 -0
  43. package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
  44. package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
  45. package/src/openpress/app/index.ts +2 -0
  46. package/src/openpress/core/Frame.tsx +26 -15
  47. package/src/openpress/core/FrameContext.tsx +10 -3
  48. package/src/openpress/core/MdxArea.tsx +11 -12
  49. package/src/openpress/core/Press.tsx +25 -4
  50. package/src/openpress/core/Workspace.tsx +36 -0
  51. package/src/openpress/core/cn.ts +4 -0
  52. package/src/openpress/core/index.tsx +11 -3
  53. package/src/openpress/core/primitives.tsx +74 -6
  54. package/src/openpress/core/types.ts +94 -41
  55. package/src/openpress/core/useSource.ts +1 -1
  56. package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  57. package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  58. package/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
  59. package/src/openpress/document-model/index.ts +7 -0
  60. package/src/openpress/document-model/objectEntityModel.ts +55 -0
  61. package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  62. package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  63. package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
  64. package/src/openpress/manuscript/index.tsx +49 -7
  65. package/src/openpress/mdx/index.ts +15 -7
  66. package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
  67. package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  68. package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  69. package/src/openpress/reader/index.ts +11 -0
  70. package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  71. package/src/openpress/reader/readerTypes.ts +4 -0
  72. package/src/openpress/reader/usePageViewportScale.ts +119 -0
  73. package/src/openpress/reader/usePanelState.ts +56 -0
  74. package/src/openpress/reader/useReaderHashSync.ts +61 -0
  75. package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  76. package/src/openpress/reader/useReaderRuntime.ts +146 -0
  77. package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  78. package/src/openpress/shared/Panel.tsx +77 -0
  79. package/src/openpress/shared/index.ts +4 -0
  80. package/src/openpress/shared/numberUtils.ts +3 -0
  81. package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  82. package/src/openpress/workbench/Workbench.tsx +506 -0
  83. package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  84. package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
  85. package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  86. package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  87. package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  88. package/src/openpress/workbench/actions/index.ts +6 -0
  89. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  90. package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  91. package/src/openpress/workbench/dialog/index.ts +1 -0
  92. package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  93. package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  94. package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  95. package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  96. package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  97. package/src/openpress/workbench/document/index.ts +10 -0
  98. package/src/openpress/workbench/index.ts +2 -0
  99. package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  100. package/src/openpress/workbench/inspector/index.ts +5 -0
  101. package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  102. package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  103. package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  104. package/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
  105. package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  106. package/src/openpress/workbench/mentions/index.ts +2 -0
  107. package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  108. package/src/openpress/workbench/panels/Panel.tsx +1 -0
  109. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
  110. package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  111. package/src/openpress/workbench/panels/index.ts +3 -0
  112. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
  113. package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  114. package/src/openpress/workbench/project/index.ts +2 -0
  115. package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  116. package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  117. package/src/openpress/workbench/shell/index.ts +1 -0
  118. package/src/openpress/workbench/workbenchFormatters.ts +120 -0
  119. package/src/openpress/workbench/workbenchTypes.ts +35 -0
  120. package/src/styles/openpress/print-route.css +0 -2
  121. package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  122. package/src/styles/openpress/public-viewer.css +25 -320
  123. package/src/styles/openpress/reader-runtime.css +252 -55
  124. package/src/styles/openpress/responsive.css +145 -270
  125. package/src/styles/openpress/workbench-panels.css +327 -178
  126. package/src/styles/openpress/workbench.css +986 -451
  127. package/src/styles/openpress/workspace-gallery.css +300 -0
  128. package/src/styles/openpress.css +2 -1
  129. package/tsconfig.json +1 -1
  130. package/vite.config.ts +50 -0
  131. package/engine/commands/init.mjs +0 -24
  132. package/engine/init.mjs +0 -90
  133. package/src/openpress/App.tsx +0 -127
  134. package/src/openpress/inspector.ts +0 -282
  135. package/src/openpress/projectWorkspace.tsx +0 -919
  136. package/src/openpress/readerRuntime.ts +0 -230
  137. package/src/openpress/workbench.tsx +0 -1265
  138. package/src/openpress/workbenchTypes.ts +0 -4
  139. /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  140. /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  141. /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  142. /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  143. /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  144. /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
@@ -3,7 +3,9 @@ import http from "node:http";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
6
+ import { searchSourceText } from "../runtime/source-text-tools.mjs";
6
7
  import { handleProjectAssetRequest } from "../react/project-asset-endpoint.mjs";
8
+ import { handleSourceEditRequest } from "../react/source-edit-endpoint.mjs";
7
9
 
8
10
  const [rootArg = "dist", ...rest] = process.argv.slice(2);
9
11
  const host = valueAfter(rest, "--host") ?? "127.0.0.1";
@@ -32,6 +34,14 @@ const server = http.createServer(async (req, res) => {
32
34
  await handleStatusRequest(req, res);
33
35
  return;
34
36
  }
37
+ if (url.pathname === "/__openpress/search") {
38
+ await handleSearchRequest(req, res, url);
39
+ return;
40
+ }
41
+ if (url.pathname === "/__openpress/source-edit") {
42
+ await handleSourceEditRequest(req, res, { root: workspace });
43
+ return;
44
+ }
35
45
  if (url.pathname === "/__openpress/local-pdf-export") {
36
46
  await handleLocalPdfExportRequest(req, res);
37
47
  return;
@@ -63,17 +73,45 @@ const server = http.createServer(async (req, res) => {
63
73
  res.end("Forbidden");
64
74
  return;
65
75
  }
66
- const stat = await fs.stat(target);
67
- const filePath = stat.isDirectory() ? path.join(target, "index.html") : target;
68
- const body = await fs.readFile(filePath);
69
- res.writeHead(200, { "Content-Type": mimeTypes[path.extname(filePath)] ?? "application/octet-stream" });
70
- res.end(body);
76
+ try {
77
+ const stat = await fs.stat(target);
78
+ const filePath = stat.isDirectory() ? path.join(target, "index.html") : target;
79
+ const body = await fs.readFile(filePath);
80
+ res.writeHead(200, { "Content-Type": mimeTypes[path.extname(filePath)] ?? "application/octet-stream" });
81
+ res.end(body);
82
+ } catch (err) {
83
+ // SPA fallback: when a path doesn't map to a real file AND it
84
+ // looks like a client-side route (no extension, not under a
85
+ // reserved namespace), serve index.html so the reader's URL-based
86
+ // routing can take over. This lets /cheatsheet / /proposal etc.
87
+ // reload correctly without needing host-level rewrite rules.
88
+ if (err?.code === "ENOENT" && shouldFallbackToIndex(url.pathname)) {
89
+ const indexBody = await fs.readFile(path.join(root, "index.html"));
90
+ res.writeHead(200, { "Content-Type": "text/html" });
91
+ res.end(indexBody);
92
+ return;
93
+ }
94
+ throw err;
95
+ }
71
96
  } catch {
72
97
  res.writeHead(404);
73
98
  res.end("Not found");
74
99
  }
75
100
  });
76
101
 
102
+ function shouldFallbackToIndex(pathname) {
103
+ // Reserved namespaces — real resources whose 404s should stay 404.
104
+ if (pathname.startsWith("/openpress/")) return false;
105
+ if (pathname.startsWith("/__openpress/")) return false;
106
+ if (pathname.startsWith("/assets/")) return false;
107
+ // Anything with a file extension is an asset miss; fall through.
108
+ const lastSlash = pathname.lastIndexOf("/");
109
+ const tail = pathname.slice(lastSlash + 1);
110
+ if (tail.includes(".")) return false;
111
+ // Otherwise: looks like a client-side route — serve the SPA shell.
112
+ return true;
113
+ }
114
+
77
115
  server.listen(port, host, () => {
78
116
  console.log(`OpenPress static preview: http://${host}:${port}/`);
79
117
  });
@@ -103,6 +141,35 @@ async function handleStatusRequest(req, res) {
103
141
  });
104
142
  }
105
143
 
144
+ async function handleSearchRequest(req, res, url) {
145
+ if (req.method !== "GET") {
146
+ writeJson(res, 405, { ok: false, message: "Search endpoint requires GET." });
147
+ return;
148
+ }
149
+
150
+ const query = (url.searchParams.get("q") ?? "").trim();
151
+ if (!query) {
152
+ writeJson(res, 400, { ok: false, message: "Search query is required." });
153
+ return;
154
+ }
155
+
156
+ try {
157
+ const report = await searchSourceText({
158
+ config,
159
+ query,
160
+ scope: searchScopeFrom(url),
161
+ caseSensitive: url.searchParams.get("caseSensitive") === "true",
162
+ });
163
+ writeJson(res, 200, { ok: true, ...report });
164
+ } catch (error) {
165
+ writeJson(res, 500, { ok: false, message: error instanceof Error ? error.message : String(error) });
166
+ }
167
+ }
168
+
169
+ function searchScopeFrom(url) {
170
+ return url.searchParams.get("scope") === "all" ? "all" : "content";
171
+ }
172
+
106
173
  function valueAfter(args, flag) {
107
174
  const index = args.indexOf(flag);
108
175
  return index >= 0 ? args[index + 1] : undefined;
@@ -110,7 +177,10 @@ function valueAfter(args, flag) {
110
177
 
111
178
  async function inferWorkspaceRoot(staticRoot) {
112
179
  for (const candidate of [staticRoot, path.dirname(staticRoot), path.dirname(path.dirname(staticRoot))]) {
113
- if (await fileExists(path.join(candidate, "openpress.config.mjs"))) return candidate;
180
+ // 1.0 workspace markers: press/index.tsx (the document entry) or
181
+ // package.json with an "openpress" field. Either is sufficient.
182
+ if (await fileExists(path.join(candidate, "press", "index.tsx"))) return candidate;
183
+ if (await hasOpenpressPackageField(candidate)) return candidate;
114
184
  }
115
185
  if (path.basename(path.dirname(staticRoot)) === ".deploy") {
116
186
  return path.dirname(path.dirname(staticRoot));
@@ -118,6 +188,16 @@ async function inferWorkspaceRoot(staticRoot) {
118
188
  return process.cwd();
119
189
  }
120
190
 
191
+ async function hasOpenpressPackageField(dir) {
192
+ try {
193
+ const text = await fs.readFile(path.join(dir, "package.json"), "utf8");
194
+ const parsed = JSON.parse(text);
195
+ return parsed?.openpress && typeof parsed.openpress === "object";
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+
121
201
  async function handleLocalPdfExportRequest(req, res) {
122
202
  if (req.method !== "POST") {
123
203
  writeJson(res, 405, { ok: false, message: "Local PDF export endpoint requires POST." });
@@ -328,7 +408,7 @@ function isDeployConfigured() {
328
408
  function deploySetupMessage() {
329
409
  if (isDeployConfigured()) return undefined;
330
410
  if (config.deploy.adapter === "cloudflare-pages") {
331
- return "Cloudflare Pages deployment requires `deploy.projectName` in openpress.config.mjs.";
411
+ return 'Cloudflare Pages deployment requires `openpress.deploy.projectName` in package.json.';
332
412
  }
333
413
  return `Deployment adapter \`${config.deploy.adapter}\` is not configured.`;
334
414
  }
@@ -394,8 +474,6 @@ function getDeploymentSourcePaths() {
394
474
  path.join(workspace, "src"),
395
475
  path.join(workspace, "index.html"),
396
476
  path.join(workspace, "package.json"),
397
- path.join(workspace, "openpress.config.mjs"),
398
- config.configPath,
399
477
  path.join(workspace, "vite.config.ts"),
400
478
  ];
401
479
  }
@@ -4,8 +4,7 @@ import {
4
4
  listCommentMarkers,
5
5
  updateCommentMarker,
6
6
  } from "./comment-marker.mjs";
7
-
8
- const MAX_COMMENT_BODY_BYTES = 64 * 1024;
7
+ import { readJsonBody, writeJson } from "./http-json.mjs";
9
8
 
10
9
  export async function handleCommentRequest(req, res, {
11
10
  root = ".",
@@ -16,17 +15,14 @@ export async function handleCommentRequest(req, res, {
16
15
  try {
17
16
  writeJson(res, 200, { ok: true, comments: await listCommentMarkers({ root }) });
18
17
  } catch (error) {
19
- writeJson(res, 400, {
20
- ok: false,
21
- message: error instanceof Error ? error.message : String(error),
22
- });
18
+ writeErrorJson(res, error);
23
19
  }
24
20
  return;
25
21
  }
26
22
 
27
23
  if (req.method === "DELETE") {
28
24
  try {
29
- const body = await readJsonBody(req);
25
+ const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
30
26
  const result = await clearCommentMarkers({
31
27
  root,
32
28
  id: body?.id,
@@ -34,17 +30,14 @@ export async function handleCommentRequest(req, res, {
34
30
  });
35
31
  writeJson(res, 200, { ok: true, ...result });
36
32
  } catch (error) {
37
- writeJson(res, 400, {
38
- ok: false,
39
- message: error instanceof Error ? error.message : String(error),
40
- });
33
+ writeErrorJson(res, error);
41
34
  }
42
35
  return;
43
36
  }
44
37
 
45
38
  if (req.method === "PATCH") {
46
39
  try {
47
- const body = await readJsonBody(req);
40
+ const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
48
41
  const result = await updateCommentMarker({
49
42
  root,
50
43
  id: body?.id,
@@ -64,10 +57,7 @@ export async function handleCommentRequest(req, res, {
64
57
  },
65
58
  });
66
59
  } catch (error) {
67
- writeJson(res, 400, {
68
- ok: false,
69
- message: error instanceof Error ? error.message : String(error),
70
- });
60
+ writeErrorJson(res, error);
71
61
  }
72
62
  return;
73
63
  }
@@ -78,7 +68,7 @@ export async function handleCommentRequest(req, res, {
78
68
  }
79
69
 
80
70
  try {
81
- const body = await readJsonBody(req);
71
+ const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
82
72
  const target = body?.target ?? {};
83
73
  const result = await insertCommentMarker({
84
74
  root,
@@ -100,29 +90,13 @@ export async function handleCommentRequest(req, res, {
100
90
  },
101
91
  });
102
92
  } catch (error) {
103
- writeJson(res, 400, {
104
- ok: false,
105
- message: error instanceof Error ? error.message : String(error),
106
- });
107
- }
108
- }
109
-
110
- async function readJsonBody(req) {
111
- let body = "";
112
- for await (const chunk of req) {
113
- body += String(chunk);
114
- if (Buffer.byteLength(body, "utf8") > MAX_COMMENT_BODY_BYTES) {
115
- throw new Error("OpenPress comment request body is too large.");
116
- }
117
- }
118
- try {
119
- return JSON.parse(body || "{}");
120
- } catch {
121
- throw new Error("OpenPress comment request body must be valid JSON.");
93
+ writeErrorJson(res, error);
122
94
  }
123
95
  }
124
96
 
125
- function writeJson(res, status, body) {
126
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
127
- res.end(`${JSON.stringify(body, null, 2)}\n`);
97
+ function writeErrorJson(res, error) {
98
+ writeJson(res, 400, {
99
+ ok: false,
100
+ message: error instanceof Error ? error.message : String(error),
101
+ });
128
102
  }
@@ -4,18 +4,18 @@ 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
- const COMMENT_MARKER_RE = /\{\/\*\s*@openpress-comment\b(?<attrs>[^*]*)\*\/\}/g;
18
- const COMMENT_LINE_RE = /^\s*\{\/\*\s*@openpress-comment\b[^*]*\*\/\}\s*$/;
17
+ const COMMENT_MARKER_RE = /(?:\{\/\*|\/\*)\s*@openpress-comment\b(?<attrs>[^*]*)\*\/\}?/g;
18
+ const COMMENT_LINE_RE = /^\s*(?:\{\/\*|\/\*)\s*@openpress-comment\b[^*]*\*\/\}?\s*$/;
19
19
 
20
20
  export async function insertCommentMarker({
21
21
  root = ".",
@@ -35,9 +35,15 @@ export async function insertCommentMarker({
35
35
  }
36
36
 
37
37
  const noteText = normalizedNote(note);
38
- const marker = createCommentMarker({ id, timestamp, note: noteText, hint });
39
38
  const line = normalizeLineNumber(source?.line);
40
39
  const text = await fs.readFile(absolutePath, "utf8");
40
+ const marker = createCommentMarker({
41
+ id,
42
+ timestamp,
43
+ note: noteText,
44
+ hint,
45
+ syntax: commentMarkerSyntaxForInsert(text, line, relativePath),
46
+ });
41
47
  const nextText = insertLineBefore(text, line, marker);
42
48
  await fs.writeFile(absolutePath, nextText, "utf8");
43
49
 
@@ -51,9 +57,10 @@ export async function insertCommentMarker({
51
57
  };
52
58
  }
53
59
 
54
- export function createCommentMarker({ id = createCommentId(), timestamp = new Date().toISOString(), note, hint } = {}) {
60
+ export function createCommentMarker({ id = createCommentId(), timestamp = new Date().toISOString(), note, hint, syntax = "jsx" } = {}) {
55
61
  const payload = { note: normalizedNote(note), ...(typeof hint === "string" && hint.trim() ? { hint: hint.trim() } : {}) };
56
- return `{/* @openpress-comment id="${escapeMarkerAttribute(id)}" ts="${escapeMarkerAttribute(timestamp)}" text="${encodeCommentPayload(payload)}" */}`;
62
+ const body = `@openpress-comment id="${escapeMarkerAttribute(id)}" ts="${escapeMarkerAttribute(timestamp)}" text="${encodeCommentPayload(payload)}"`;
63
+ return syntax === "block" ? `/* ${body} */` : `{/* ${body} */}`;
57
64
  }
58
65
 
59
66
  export function decodeCommentMarkerText(marker) {
@@ -160,23 +167,23 @@ function normalizeEditableSourcePath(value) {
160
167
  throw new Error(`OpenPress comment target path is invalid: ${value}`);
161
168
  }
162
169
  const posix = path.posix.normalize(normalized);
163
- // The Press Tree source resolver emits paths relative to `document/`
170
+ // The Press Tree source resolver emits paths relative to `press/`
164
171
  // (e.g. "chapters/01-start/content/01-start.mdx"). The comment marker
165
- // works in workspace-relative paths (with the `document/` prefix). If
166
- // the incoming path is documentRoot-relative, prepend `document/`.
167
- if (!posix.startsWith("document/") && looksDocumentRelative(posix)) {
168
- 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}`;
169
176
  }
170
177
  return posix;
171
178
  }
172
179
 
173
180
  // Identify paths the Press Tree source resolver emits — those are relative
174
- // to `document/`. Match `.mdx` / `.tsx` files that don't already have the
175
- // `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
176
183
  // is intentionally tight so we never silently rewrite engine internals
177
184
  // (e.g. `src/openpress/...`) into "editable" workspace paths.
178
185
  const SYSTEM_PATH_PREFIXES = [
179
- "document/",
186
+ "press/",
180
187
  "src/",
181
188
  "engine/",
182
189
  "dist/",
@@ -285,7 +292,7 @@ function replaceCommentMarkerLine(text, { id, note, hint, timestamp }) {
285
292
  if (!COMMENT_LINE_RE.test(line)) continue;
286
293
  const attrs = parseMarkerAttributes(line);
287
294
  if (attrs.id !== id) continue;
288
- const marker = createCommentMarker({ id, timestamp, note, hint });
295
+ const marker = createCommentMarker({ id, timestamp, note, hint, syntax: commentMarkerSyntaxForLine(line) });
289
296
  lines[index] = `${line.match(/^\s*/)?.[0] ?? ""}${marker}`;
290
297
  return {
291
298
  text: `${lines.join(newline)}${hasTrailingNewline ? newline : ""}`,
@@ -311,6 +318,23 @@ function parseMarkerAttributes(value) {
311
318
  return attrs;
312
319
  }
313
320
 
321
+ function commentMarkerSyntaxForInsert(text, line, relativePath) {
322
+ if (!String(relativePath).endsWith(".tsx")) return "jsx";
323
+ const lines = String(text ?? "").split(/\r?\n/);
324
+ const index = Math.min(Math.max(line - 1, 0), lines.length);
325
+ const priorContent = lines.slice(0, index).some((entry) => entry.trim().length > 0);
326
+ if (!priorContent) return "block";
327
+ const targetLine = lines[index] ?? "";
328
+ if (/^\s*(import\b|export\b|function\b|const\b|let\b|var\b|type\b|interface\b|return\b)/.test(targetLine)) {
329
+ return "block";
330
+ }
331
+ return "jsx";
332
+ }
333
+
334
+ function commentMarkerSyntaxForLine(line) {
335
+ return /^\s*\/\*/.test(line) ? "block" : "jsx";
336
+ }
337
+
314
338
  function lineStartOffsets(text) {
315
339
  const starts = [0];
316
340
  for (let index = 0; index < text.length; index += 1) {
@@ -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();
@@ -68,6 +92,7 @@ export async function createReactSsrServer(workspaceRoot = ".") {
68
92
  return createViteServer({
69
93
  configFile: false,
70
94
  root: FRAMEWORK_ROOT,
95
+ cacheDir: path.join(resolvedWorkspaceRoot, ".openpress", "vite-ssr"),
71
96
  appType: "custom",
72
97
  logLevel: "silent",
73
98
  plugins: [reactRuntimePlugin(), react()],
@@ -79,7 +104,17 @@ export async function createReactSsrServer(workspaceRoot = ".") {
79
104
  { find: "@open-press/core/manuscript", replacement: MANUSCRIPT_ENTRY },
80
105
  { find: "@open-press/core/numbering", replacement: NUMBERING_ENTRY },
81
106
  { find: "@open-press/core", replacement: CORE_ENTRY },
82
- { find: "@/components", replacement: path.join(resolvedWorkspaceRoot, "document", "components") },
107
+ { find: "@/components", replacement: path.join(resolvedWorkspaceRoot, "press", "components") },
108
+ ],
109
+ },
110
+ optimizeDeps: {
111
+ include: [
112
+ "@mdx-js/react",
113
+ "react",
114
+ "react-dom",
115
+ "react-dom/server",
116
+ "react/jsx-dev-runtime",
117
+ "react/jsx-runtime",
83
118
  ],
84
119
  },
85
120
  server: {
@@ -239,23 +274,6 @@ function isFileSystemModule(moduleName) {
239
274
  return moduleName === "fs" || moduleName === "node:fs" || moduleName === "fs/promises" || moduleName === "node:fs/promises";
240
275
  }
241
276
 
242
- function normalizeReactDocumentConfig(workspaceRoot, entryPath, config) {
243
- if (config != null && (typeof config !== "object" || Array.isArray(config))) {
244
- throw new Error("OpenPress React document entry `config` export must be an object when provided.");
245
- }
246
- const rawConfig = config ?? {};
247
- const paths = rawConfig.paths ?? {};
248
- return normalizeConfig(workspaceRoot, {
249
- ...rawConfig,
250
- documentDir: rawConfig.documentDir ?? paths.documentDir ?? "document",
251
- sourceDir: rawConfig.sourceDir ?? paths.chaptersDir ?? paths.sourceDir ?? "chapters",
252
- componentsDir: rawConfig.componentsDir ?? paths.componentsDir ?? "components",
253
- mediaDir: rawConfig.mediaDir ?? paths.mediaDir ?? "media",
254
- themeDir: rawConfig.themeDir ?? paths.themeDir ?? "theme",
255
- designDoc: rawConfig.designDoc ?? paths.designDoc ?? "design.md",
256
- }, entryPath);
257
- }
258
-
259
277
  async function fileExists(filePath) {
260
278
  try {
261
279
  const stat = await fs.stat(filePath);