@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.
- package/README.md +6 -3
- package/engine/cli.mjs +8 -8
- package/engine/commands/_shared.mjs +37 -15
- package/engine/commands/dev.mjs +2 -2
- package/engine/commands/image.mjs +29 -0
- package/engine/commands/skills-sync.mjs +71 -0
- package/engine/commands/typecheck.mjs +63 -1
- package/engine/commands/upgrade.mjs +3 -3
- package/engine/document-export.mjs +1 -1
- package/engine/output/chrome-pdf.mjs +110 -3
- package/engine/output/static-server.mjs +87 -9
- package/engine/react/comment-endpoint.mjs +13 -39
- package/engine/react/comment-marker.mjs +43 -19
- package/engine/react/document-entry.mjs +46 -28
- package/engine/react/document-export.mjs +328 -164
- package/engine/react/http-json.mjs +24 -0
- package/engine/react/mdx-compile.mjs +126 -3
- package/engine/react/measurement-css.mjs +114 -1
- package/engine/react/object-entities.mjs +204 -0
- package/engine/react/pagination/allocator.mjs +48 -3
- package/engine/react/pagination.mjs +1 -1
- package/engine/react/pipeline/allocate.mjs +41 -72
- package/engine/react/pipeline/frame-measurement.mjs +6 -0
- package/engine/react/press-tree-inspection.mjs +172 -0
- package/engine/react/project-asset-endpoint.mjs +6 -24
- package/engine/react/source-edit-endpoint.d.mts +10 -0
- package/engine/react/source-edit-endpoint.mjs +75 -0
- package/engine/react/sources/mdx-resolver.mjs +13 -15
- package/engine/react/style-discovery.mjs +23 -8
- package/engine/runtime/config.d.mts +8 -0
- package/engine/runtime/config.mjs +57 -60
- package/engine/runtime/file-utils.mjs +9 -1
- package/engine/runtime/file-walk.mjs +22 -0
- package/engine/runtime/inspection.mjs +1 -20
- package/engine/runtime/page-geometry.mjs +131 -0
- package/engine/runtime/path-utils.mjs +20 -0
- package/engine/runtime/source-text-tools.d.mts +102 -0
- package/engine/runtime/source-text-tools.mjs +551 -16
- package/engine/runtime/source-workspace.mjs +16 -34
- package/engine/runtime/validation.mjs +19 -10
- package/package.json +3 -5
- package/src/openpress/app/OpenPressApp.tsx +296 -0
- package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
- package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
- package/src/openpress/app/index.ts +2 -0
- package/src/openpress/core/Frame.tsx +26 -15
- package/src/openpress/core/FrameContext.tsx +10 -3
- package/src/openpress/core/MdxArea.tsx +11 -12
- package/src/openpress/core/Press.tsx +25 -4
- package/src/openpress/core/Workspace.tsx +36 -0
- package/src/openpress/core/cn.ts +4 -0
- package/src/openpress/core/index.tsx +11 -3
- package/src/openpress/core/primitives.tsx +74 -6
- package/src/openpress/core/types.ts +94 -41
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
- package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
- package/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
- package/src/openpress/document-model/index.ts +7 -0
- package/src/openpress/document-model/objectEntityModel.ts +55 -0
- package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
- package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
- package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
- package/src/openpress/manuscript/index.tsx +49 -7
- package/src/openpress/mdx/index.ts +15 -7
- package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
- package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
- package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
- package/src/openpress/reader/index.ts +11 -0
- package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
- package/src/openpress/reader/readerTypes.ts +4 -0
- package/src/openpress/reader/usePageViewportScale.ts +119 -0
- package/src/openpress/reader/usePanelState.ts +56 -0
- package/src/openpress/reader/useReaderHashSync.ts +61 -0
- package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
- package/src/openpress/reader/useReaderRuntime.ts +146 -0
- package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
- package/src/openpress/shared/Panel.tsx +77 -0
- package/src/openpress/shared/index.ts +4 -0
- package/src/openpress/shared/numberUtils.ts +3 -0
- package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
- package/src/openpress/workbench/Workbench.tsx +506 -0
- package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
- package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
- package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
- package/src/openpress/workbench/actions/index.ts +6 -0
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
- package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
- package/src/openpress/workbench/dialog/index.ts +1 -0
- package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
- package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
- package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
- package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
- package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
- package/src/openpress/workbench/document/index.ts +10 -0
- package/src/openpress/workbench/index.ts +2 -0
- package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
- package/src/openpress/workbench/inspector/index.ts +5 -0
- package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
- package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
- package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
- package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
- package/src/openpress/workbench/mentions/index.ts +2 -0
- package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
- package/src/openpress/workbench/panels/Panel.tsx +1 -0
- package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
- package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
- package/src/openpress/workbench/panels/index.ts +3 -0
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
- package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
- package/src/openpress/workbench/project/index.ts +2 -0
- package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
- package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
- package/src/openpress/workbench/shell/index.ts +1 -0
- package/src/openpress/workbench/workbenchFormatters.ts +120 -0
- package/src/openpress/workbench/workbenchTypes.ts +35 -0
- package/src/styles/openpress/print-route.css +0 -2
- package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
- package/src/styles/openpress/public-viewer.css +25 -320
- package/src/styles/openpress/reader-runtime.css +252 -55
- package/src/styles/openpress/responsive.css +145 -270
- package/src/styles/openpress/workbench-panels.css +327 -178
- package/src/styles/openpress/workbench.css +986 -451
- package/src/styles/openpress/workspace-gallery.css +300 -0
- package/src/styles/openpress.css +2 -1
- package/tsconfig.json +1 -1
- package/vite.config.ts +50 -0
- package/engine/commands/init.mjs +0 -24
- package/engine/init.mjs +0 -90
- package/src/openpress/App.tsx +0 -127
- package/src/openpress/inspector.ts +0 -282
- package/src/openpress/projectWorkspace.tsx +0 -919
- package/src/openpress/readerRuntime.ts +0 -230
- package/src/openpress/workbench.tsx +0 -1265
- package/src/openpress/workbenchTypes.ts +0 -4
- /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
- /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
- /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
- /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
- /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
- /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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
126
|
-
res
|
|
127
|
-
|
|
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 `
|
|
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 `
|
|
11
|
-
// is "inside the workspace's authored `
|
|
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
|
-
/^
|
|
15
|
-
/^
|
|
14
|
+
/^press\/.+\.mdx$/,
|
|
15
|
+
/^press\/.+\.tsx$/,
|
|
16
16
|
];
|
|
17
|
-
const COMMENT_MARKER_RE =
|
|
18
|
-
const COMMENT_LINE_RE = /^\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
|
-
|
|
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 `
|
|
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 `
|
|
166
|
-
// the incoming path is documentRoot-relative, prepend `
|
|
167
|
-
if (!posix.startsWith("
|
|
168
|
-
return `
|
|
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 `
|
|
175
|
-
// `
|
|
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
|
-
"
|
|
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 `
|
|
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 {
|
|
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 =
|
|
30
|
-
if (!
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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, "
|
|
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);
|