@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.
- package/README.md +17 -5
- package/engine/cli.mjs +9 -9
- package/engine/commands/_shared.mjs +70 -18
- package/engine/commands/deploy.mjs +3 -3
- package/engine/commands/dev.mjs +13 -4
- package/engine/commands/image.mjs +29 -0
- package/engine/commands/inspect.mjs +3 -2
- package/engine/commands/pdf.mjs +2 -2
- package/engine/commands/preview.mjs +2 -2
- package/engine/commands/render.mjs +6 -4
- package/engine/commands/replace.mjs +1 -1
- package/engine/commands/search.mjs +1 -1
- package/engine/commands/skills-sync.mjs +71 -0
- package/engine/commands/typecheck.mjs +71 -1
- package/engine/commands/upgrade.mjs +3 -3
- package/engine/document-export.mjs +1 -1
- package/engine/output/chrome-pdf.mjs +92 -0
- package/engine/output/static-server.mjs +60 -17
- package/engine/react/comment-marker.mjs +13 -13
- package/engine/react/document-entry.mjs +35 -28
- package/engine/react/document-export.mjs +309 -170
- package/engine/react/mdx-compile.mjs +30 -0
- package/engine/react/measurement-css.mjs +21 -0
- package/engine/react/object-entities.mjs +85 -0
- package/engine/react/pagination/allocator.mjs +48 -3
- package/engine/react/pagination.mjs +1 -1
- package/engine/react/pipeline/allocate.mjs +31 -65
- package/engine/react/pipeline/frame-measurement.mjs +4 -0
- package/engine/react/press-tree-inspection.mjs +172 -0
- package/engine/react/sources/mdx-resolver.mjs +1 -1
- package/engine/react/style-discovery.mjs +22 -4
- 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/page-geometry.mjs +131 -0
- package/engine/runtime/source-text-tools.mjs +1 -1
- package/engine/runtime/source-workspace.mjs +12 -3
- package/engine/runtime/validation.mjs +19 -10
- package/index.html +4 -0
- package/package.json +9 -12
- package/src/main.tsx +16 -0
- package/src/openpress/app/OpenPressApp.tsx +173 -17
- package/src/openpress/app/OpenPressRuntime.tsx +10 -2
- package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
- package/src/openpress/core/Frame.tsx +20 -7
- package/src/openpress/core/FrameContext.tsx +2 -0
- package/src/openpress/core/Press.tsx +25 -4
- package/src/openpress/core/Workspace.tsx +36 -0
- package/src/openpress/core/index.tsx +10 -3
- package/src/openpress/core/primitives.tsx +48 -1
- package/src/openpress/core/types.ts +86 -41
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/document-model/documentTypes.ts +9 -0
- package/src/openpress/document-model/index.ts +1 -0
- package/src/openpress/document-model/objectEntityModel.ts +4 -0
- package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
- package/src/openpress/mdx/index.ts +15 -7
- package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
- package/src/openpress/reader/index.ts +1 -0
- package/src/openpress/workbench/Workbench.tsx +120 -21
- package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +3 -3
- package/src/openpress/workbench/actions/index.ts +1 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +7 -1
- package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +5 -1
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +4 -2
- package/src/openpress/workbench/project/projectSourceModel.ts +2 -2
- package/src/openpress/workbench/workbenchFormatters.ts +2 -2
- package/src/styles/openpress/reader-runtime.css +9 -0
- package/src/styles/openpress/workbench-panels.css +113 -0
- package/src/styles/openpress/workspace-gallery.css +300 -0
- package/src/styles/openpress.css +1 -5
- package/src/vite-env.d.ts +8 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +6 -6
- package/engine/commands/init.mjs +0 -24
- 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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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", [
|
|
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", [
|
|
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
|
|
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(
|
|
434
|
-
path.join(
|
|
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 `
|
|
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
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 `
|
|
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 `
|
|
173
|
-
// the incoming path is documentRoot-relative, prepend `
|
|
174
|
-
if (!posix.startsWith("
|
|
175
|
-
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}`;
|
|
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 `
|
|
182
|
-
// `
|
|
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
|
-
"
|
|
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 `
|
|
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();
|
|
@@ -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, "
|
|
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);
|