@open-press/core 0.5.0 → 0.7.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 +9 -5
- package/engine/cli.mjs +2 -5
- package/engine/commands/_shared.mjs +4 -4
- package/engine/commands/deploy.mjs +1 -1
- package/engine/commands/inspect.mjs +3 -3
- package/engine/commands/replace.mjs +1 -1
- package/engine/commands/search.mjs +1 -1
- package/engine/commands/validate.mjs +2 -2
- package/engine/document-export.mjs +1 -1
- package/engine/{chrome-pdf.mjs → output/chrome-pdf.mjs} +1 -2
- package/engine/{deploy-sync.mjs → output/deploy-sync.mjs} +2 -2
- package/engine/{fonts.mjs → output/fonts.mjs} +1 -1
- package/engine/{public-assets.mjs → output/public-assets.mjs} +2 -2
- package/engine/{static-server.mjs → output/static-server.mjs} +2 -2
- package/engine/react/caption-numbering.mjs +73 -0
- package/engine/react/comment-marker.mjs +54 -10
- package/engine/react/document-entry.mjs +124 -64
- package/engine/react/document-export.mjs +252 -311
- package/engine/react/mdx-compile.mjs +123 -3
- package/engine/react/measurement-css.mjs +3 -3
- package/engine/react/pagination/allocator.mjs +122 -0
- package/engine/react/pagination/regions.mjs +81 -0
- package/engine/react/pagination.mjs +9 -121
- package/engine/react/pipeline/allocate.mjs +248 -0
- package/engine/react/pipeline/final-render.mjs +94 -0
- package/engine/react/pipeline/frame-measurement.mjs +271 -0
- package/engine/react/pipeline/press-tree.mjs +135 -0
- package/engine/react/project-asset-endpoint.mjs +2 -2
- package/engine/react/{chapter-css.mjs → section-css.mjs} +12 -9
- package/engine/react/sources/heading-numbering.mjs +132 -0
- package/engine/react/sources/mdx-resolver.mjs +441 -0
- package/engine/react/{workspace-discovery.mjs → style-discovery.mjs} +29 -40
- package/engine/{config.mjs → runtime/config.mjs} +15 -0
- package/engine/{file-utils.mjs → runtime/file-utils.mjs} +1 -1
- package/engine/{inspection.mjs → runtime/inspection.mjs} +3 -4
- package/engine/{source-text-tools.mjs → runtime/source-text-tools.mjs} +24 -7
- package/engine/runtime/source-workspace.mjs +186 -0
- package/engine/{validation.mjs → runtime/validation.mjs} +19 -17
- package/package.json +5 -2
- package/src/openpress/anchorMap.ts +27 -0
- package/src/openpress/core/Frame.tsx +80 -0
- package/src/openpress/core/FrameContext.tsx +19 -0
- package/src/openpress/core/MdxArea.tsx +35 -0
- package/src/openpress/core/Press.tsx +34 -0
- package/src/openpress/core/index.tsx +34 -15
- package/src/openpress/core/primitives.tsx +23 -0
- package/src/openpress/core/types.ts +131 -19
- package/src/openpress/core/useSource.ts +28 -0
- package/src/openpress/manuscript/index.tsx +196 -0
- package/src/openpress/mdx/index.ts +88 -0
- package/src/openpress/numbering/index.ts +294 -0
- package/src/openpress/publicPage.tsx +4 -186
- package/src/openpress/reactDocumentMetadata.ts +2 -16
- package/src/openpress/types.ts +0 -16
- package/src/openpress/workbench.tsx +2 -36
- package/src/styles/openpress/responsive.css +0 -14
- package/tsconfig.json +4 -1
- package/vite.config.ts +10 -3
- package/engine/commands/migrate-to-react.mjs +0 -27
- package/engine/page-renderer.mjs +0 -217
- package/engine/react/migrate-to-react.mjs +0 -355
- package/engine/source-workspace.mjs +0 -76
- package/src/openpress/core/basePages.tsx +0 -87
- package/src/openpress/pagination.ts +0 -845
- /package/engine/{chrome-pdf.d.mts → output/chrome-pdf.d.mts} +0 -0
- /package/engine/{katex-assets.mjs → output/katex-assets.mjs} +0 -0
- /package/engine/{page-block.mjs → output/page-block.mjs} +0 -0
- /package/engine/{pdf-media.mjs → output/pdf-media.mjs} +0 -0
- /package/engine/{config.d.mts → runtime/config.d.mts} +0 -0
- /package/engine/{issue-report.mjs → runtime/issue-report.mjs} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @open-press/core
|
|
2
2
|
|
|
3
|
-
Framework runtime, CLI engine, and
|
|
3
|
+
Framework runtime, CLI engine, and Press Tree primitives for [open-press](https://github.com/quan0715/open-press) — an AI-first fixed-layout document workspace.
|
|
4
4
|
|
|
5
5
|
Most users do **not** install this package directly. Instead, scaffold a workspace with the CLI:
|
|
6
6
|
|
|
@@ -20,15 +20,19 @@ npm install @open-press/core
|
|
|
20
20
|
|
|
21
21
|
```tsx
|
|
22
22
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
BaseBackCoverPage,
|
|
23
|
+
Press,
|
|
24
|
+
Frame,
|
|
25
|
+
MdxArea,
|
|
27
26
|
BaseFigure,
|
|
28
27
|
BaseCallout,
|
|
29
28
|
} from "@open-press/core";
|
|
29
|
+
|
|
30
|
+
import { mdxSource } from "@open-press/core/mdx";
|
|
31
|
+
import { Sections, Toc } from "@open-press/core/manuscript";
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
`document/index.tsx` default-exports a `<Press>` tree. `Frame` marks fixed-layout pages, `MdxArea` receives measured MDX blocks, and `mdxSource()` declares which MDX files participate in the render pipeline.
|
|
35
|
+
|
|
32
36
|
The CLI bin (`open-press`) supports dev / build / preview / validate / pdf / deploy / export commands. It requires a workspace with `openpress.config.mjs` and the surrounding framework files (which the scaffolder installs).
|
|
33
37
|
|
|
34
38
|
## License
|
package/engine/cli.mjs
CHANGED
|
@@ -6,7 +6,6 @@ import * as doctorCmd from "./commands/doctor.mjs";
|
|
|
6
6
|
import * as exportCmd from "./commands/export.mjs";
|
|
7
7
|
import * as initCmd from "./commands/init.mjs";
|
|
8
8
|
import * as inspectCmd from "./commands/inspect.mjs";
|
|
9
|
-
import * as migrateToReactCmd from "./commands/migrate-to-react.mjs";
|
|
10
9
|
import * as pdfCmd from "./commands/pdf.mjs";
|
|
11
10
|
import * as previewCmd from "./commands/preview.mjs";
|
|
12
11
|
import * as replaceCmd from "./commands/replace.mjs";
|
|
@@ -16,13 +15,12 @@ import * as typecheckCmd from "./commands/typecheck.mjs";
|
|
|
16
15
|
import * as upgradeCmd from "./commands/upgrade.mjs";
|
|
17
16
|
import * as validateCmd from "./commands/validate.mjs";
|
|
18
17
|
import { parseOptions } from "./commands/_shared.mjs";
|
|
19
|
-
import { loadConfig } from "./config.mjs";
|
|
18
|
+
import { loadConfig } from "./runtime/config.mjs";
|
|
20
19
|
import { listStylePackSkills } from "./init.mjs";
|
|
21
|
-
import { discoverWorkspace } from "./validation.mjs";
|
|
20
|
+
import { discoverWorkspace } from "./runtime/validation.mjs";
|
|
22
21
|
|
|
23
22
|
const COMMANDS = {
|
|
24
23
|
init: initCmd,
|
|
25
|
-
"migrate-to-react": migrateToReactCmd,
|
|
26
24
|
validate: validateCmd,
|
|
27
25
|
inspect: inspectCmd,
|
|
28
26
|
search: searchCmd,
|
|
@@ -79,7 +77,6 @@ async function printHelp() {
|
|
|
79
77
|
|
|
80
78
|
Commands:
|
|
81
79
|
init <target> [--skill <name>] [--force]
|
|
82
|
-
migrate-to-react [path] [--dry-run] [--force] [--json]
|
|
83
80
|
validate
|
|
84
81
|
inspect [--json] [--no-build] [--dry-run]
|
|
85
82
|
search [path] <query> [--json] [--scope content|all]
|
|
@@ -2,14 +2,14 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { printUrlToPdf, stopChildProcess, waitForPrintReady } from "../chrome-pdf.mjs";
|
|
6
|
-
import { loadConfig, publicPdfHref } from "../config.mjs";
|
|
5
|
+
import { printUrlToPdf, stopChildProcess, waitForPrintReady } from "../output/chrome-pdf.mjs";
|
|
6
|
+
import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
|
|
7
7
|
import { exportDocument } from "../document-export.mjs";
|
|
8
|
-
import { optimizePdfMediaForStaticRoot } from "../pdf-media.mjs";
|
|
8
|
+
import { optimizePdfMediaForStaticRoot } from "../output/pdf-media.mjs";
|
|
9
9
|
|
|
10
10
|
export const ENGINE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
11
|
export const CLI_ENTRY = path.join(ENGINE_DIR, "cli.mjs");
|
|
12
|
-
export const STATIC_SERVER = path.join(ENGINE_DIR, "static-server.mjs");
|
|
12
|
+
export const STATIC_SERVER = path.join(ENGINE_DIR, "output", "static-server.mjs");
|
|
13
13
|
|
|
14
14
|
export function parseOptions(argv) {
|
|
15
15
|
const options = {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { deploySync } from "../deploy-sync.mjs";
|
|
2
|
+
import { deploySync } from "../output/deploy-sync.mjs";
|
|
3
3
|
import { CLI_ENTRY, buildReactPdf, formatNodeScriptCommand, runCommand, writePdfStageDeployConfig } from "./_shared.mjs";
|
|
4
4
|
|
|
5
5
|
export async function run({ root, config, options, recurse }) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { inspectWorkspace } from "../inspection.mjs";
|
|
2
|
-
import { exitCodeForIssueReport } from "../issue-report.mjs";
|
|
1
|
+
import { inspectWorkspace } from "../runtime/inspection.mjs";
|
|
2
|
+
import { exitCodeForIssueReport } from "../runtime/issue-report.mjs";
|
|
3
3
|
|
|
4
4
|
export async function run({ root, config, options, recurse }) {
|
|
5
5
|
const host = options.host ?? "127.0.0.1";
|
|
@@ -10,7 +10,7 @@ export async function run({ root, config, options, recurse }) {
|
|
|
10
10
|
if (!options.noBuild) {
|
|
11
11
|
console.log("Command: node engine/cli.mjs render . --renderer react");
|
|
12
12
|
}
|
|
13
|
-
console.log(`Command: node engine/static-server.mjs ${config.outputDir} --host ${host} --port ${port} --workspace .`);
|
|
13
|
+
console.log(`Command: node engine/output/static-server.mjs ${config.outputDir} --host ${host} --port ${port} --workspace .`);
|
|
14
14
|
console.log(`Chrome inspection URL: ${url}`);
|
|
15
15
|
return 0;
|
|
16
16
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { validateWorkspace } from "../validation.mjs";
|
|
2
|
-
import { exitCodeForIssueReport } from "../issue-report.mjs";
|
|
1
|
+
import { validateWorkspace } from "../runtime/validation.mjs";
|
|
2
|
+
import { exitCodeForIssueReport } from "../runtime/issue-report.mjs";
|
|
3
3
|
|
|
4
4
|
export async function run({ root, options }) {
|
|
5
5
|
const report = await validateWorkspace(root);
|
|
@@ -10,6 +10,6 @@ export async function exportDocument(root = ROOT) {
|
|
|
10
10
|
if (reactResult) return reactResult;
|
|
11
11
|
|
|
12
12
|
throw new Error(
|
|
13
|
-
"React/MDX document entry not found. Expected document/index.tsx
|
|
13
|
+
"React/MDX document entry not found. Expected document/index.tsx with a Press default export before exporting.",
|
|
14
14
|
);
|
|
15
15
|
}
|
|
@@ -203,8 +203,7 @@ export async function waitForPrintReady(client) {
|
|
|
203
203
|
awaitPromise: true,
|
|
204
204
|
expression: `Promise.resolve().then(async () => {
|
|
205
205
|
const root = document.querySelector('[data-openpress-print-document="true"]');
|
|
206
|
-
|
|
207
|
-
if (!ready) return 0;
|
|
206
|
+
if (!root || root.querySelectorAll('.openpress-html-page').length === 0) return 0;
|
|
208
207
|
|
|
209
208
|
await document.fonts?.ready;
|
|
210
209
|
await Promise.all(Array.from(document.images).map(async (img) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { loadConfig } from "
|
|
4
|
-
import { copyDirectory } from "
|
|
3
|
+
import { loadConfig } from "../runtime/config.mjs";
|
|
4
|
+
import { copyDirectory } from "../runtime/file-utils.mjs";
|
|
5
5
|
|
|
6
6
|
export async function deploySync(root, sourceDir, deployDir) {
|
|
7
7
|
const config = await loadConfig(root);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { copyDirectory } from "
|
|
3
|
+
import { copyDirectory } from "../runtime/file-utils.mjs";
|
|
4
4
|
|
|
5
5
|
export async function copyThemeFonts(root, publicOutputDir, config) {
|
|
6
6
|
const themeDir = config?.paths?.themeDir ?? path.join(path.resolve(root), "theme");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { loadConfig } from "
|
|
4
|
-
import { copyDirectory, writeComponentsCss, writeContentCss } from "
|
|
3
|
+
import { loadConfig } from "../runtime/config.mjs";
|
|
4
|
+
import { copyDirectory, writeComponentsCss, writeContentCss } from "../runtime/file-utils.mjs";
|
|
5
5
|
import { copyThemeFonts } from "./fonts.mjs";
|
|
6
6
|
import { copyKatexFonts } from "./katex-assets.mjs";
|
|
7
7
|
|
|
@@ -2,8 +2,8 @@ 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 { loadConfig, publicPdfHref } from "
|
|
6
|
-
import { handleProjectAssetRequest } from "
|
|
5
|
+
import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
|
|
6
|
+
import { handleProjectAssetRequest } from "../react/project-asset-endpoint.mjs";
|
|
7
7
|
|
|
8
8
|
const [rootArg = "dist", ...rest] = process.argv.slice(2);
|
|
9
9
|
const host = valueAfter(rest, "--host") ?? "127.0.0.1";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const DEFAULT_CAPTION_NUMBERING = {
|
|
2
|
+
figure: "Figure",
|
|
3
|
+
table: "Table",
|
|
4
|
+
separator: " ",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function normalizeCaptionNumbering(value = {}) {
|
|
8
|
+
const input = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
9
|
+
return {
|
|
10
|
+
figure: stringOption(input.figure, DEFAULT_CAPTION_NUMBERING.figure),
|
|
11
|
+
table: stringOption(input.table, DEFAULT_CAPTION_NUMBERING.table),
|
|
12
|
+
separator: typeof input.separator === "string" ? input.separator : DEFAULT_CAPTION_NUMBERING.separator,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createCaptionNumberingState() {
|
|
17
|
+
return {
|
|
18
|
+
figure: 0,
|
|
19
|
+
table: 0,
|
|
20
|
+
seenTables: new Set(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function numberCaptionsInHtml(html, numbering, state = createCaptionNumberingState()) {
|
|
25
|
+
if (!html) return html;
|
|
26
|
+
const options = normalizeCaptionNumbering(numbering);
|
|
27
|
+
let out = String(html);
|
|
28
|
+
out = numberTableCaptions(out, options, state);
|
|
29
|
+
out = numberFigureCaptions(out, options, state);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function numberTableCaptions(html, options, state) {
|
|
34
|
+
return html.replace(/<table\b([^>]*)>([\s\S]*?<caption\b([^>]*)>)([\s\S]*?)(<\/caption>[\s\S]*?<\/table>)/g, (match, tableAttrs, beforeCaptionText, captionAttrs, captionText, afterCaptionText) => {
|
|
35
|
+
if (captionText.includes("data-openpress-caption-label=")) return match;
|
|
36
|
+
const tableId = attrValue(tableAttrs, "data-openpress-table-id");
|
|
37
|
+
if (tableId && state.seenTables.has(tableId)) return match;
|
|
38
|
+
if (tableId) state.seenTables.add(tableId);
|
|
39
|
+
state.table += 1;
|
|
40
|
+
const label = captionLabel(options.table, state.table, options.separator);
|
|
41
|
+
return `<table${tableAttrs}>${beforeCaptionText}${captionLabelSpan("table", state.table, label)} ${captionText}${afterCaptionText}`;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function numberFigureCaptions(html, options, state) {
|
|
46
|
+
return html.replace(/<figure\b([^>]*)>([\s\S]*?<figcaption\b([^>]*)>)([\s\S]*?)(<\/figcaption>[\s\S]*?<\/figure>)/g, (match, figureAttrs, beforeCaptionText, captionAttrs, captionText, afterCaptionText) => {
|
|
47
|
+
if (captionText.includes("data-openpress-caption-label=")) return match;
|
|
48
|
+
state.figure += 1;
|
|
49
|
+
const label = captionLabel(options.figure, state.figure, options.separator);
|
|
50
|
+
return `<figure${figureAttrs}>${beforeCaptionText}${captionLabelSpan("figure", state.figure, label)} ${captionText}${afterCaptionText}`;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function captionLabel(noun, number, separator) {
|
|
55
|
+
return `${noun}${separator}${number}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function captionLabelSpan(kind, number, label) {
|
|
59
|
+
return `<span class="openpress-caption-label" data-openpress-caption-label="${kind}" data-openpress-caption-number="${number}">${escapeHtml(label)}</span>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function attrValue(attrs, name) {
|
|
63
|
+
const pattern = new RegExp(`${name}=(["'])(.*?)\\1`);
|
|
64
|
+
return attrs.match(pattern)?.[2] ?? "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function stringOption(value, fallback) {
|
|
68
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function escapeHtml(value) {
|
|
72
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
73
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { loadConfig } from "../config.mjs";
|
|
5
|
-
import { collectSourceTextFiles } from "../source-text-tools.mjs";
|
|
6
|
-
|
|
4
|
+
import { loadConfig } from "../runtime/config.mjs";
|
|
5
|
+
import { collectSourceTextFiles } from "../runtime/source-text-tools.mjs";
|
|
6
|
+
|
|
7
|
+
// Any `.mdx` or `.tsx` file under `document/` is a legal comment target.
|
|
8
|
+
// The Press Tree allows arbitrary source layouts — `section-folders`,
|
|
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
|
|
12
|
+
// like an editable React/MDX source" by extension.
|
|
7
13
|
const EDITABLE_COMMENT_SOURCE_PATTERNS = [
|
|
8
|
-
/^document
|
|
9
|
-
/^document
|
|
10
|
-
/^document\/chapters\/[^/]+\/chapter\.tsx$/,
|
|
11
|
-
/^document\/chapters\/[^/]+\/components\/.+\.tsx$/,
|
|
12
|
-
/^document\/components\/.+\.tsx$/,
|
|
14
|
+
/^document\/.+\.mdx$/,
|
|
15
|
+
/^document\/.+\.tsx$/,
|
|
13
16
|
];
|
|
14
17
|
const COMMENT_MARKER_RE = /\{\/\*\s*@openpress-comment\b(?<attrs>[^*]*)\*\/\}/g;
|
|
15
18
|
const COMMENT_LINE_RE = /^\s*\{\/\*\s*@openpress-comment\b[^*]*\*\/\}\s*$/;
|
|
@@ -138,8 +141,14 @@ export function assertEditableCommentPath(relativePath) {
|
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
// Strict check against workspace-relative paths. Callers walking the
|
|
145
|
+
// workspace (`applyCommentMarker` via `collectSourceTextFiles`) already
|
|
146
|
+
// receive paths with the `document/` prefix and must not have system
|
|
147
|
+
// paths silently mapped into the editable set.
|
|
141
148
|
export function isEditableCommentPath(relativePath) {
|
|
142
|
-
|
|
149
|
+
if (typeof relativePath !== "string" || !relativePath) return false;
|
|
150
|
+
const trimmed = relativePath.trim().replaceAll("\\", "/").replace(/^\.\//, "");
|
|
151
|
+
return EDITABLE_COMMENT_SOURCE_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
function normalizeEditableSourcePath(value) {
|
|
@@ -150,7 +159,42 @@ function normalizeEditableSourcePath(value) {
|
|
|
150
159
|
if (path.posix.isAbsolute(normalized) || normalized.includes("\0") || normalized === "." || normalized.startsWith("../")) {
|
|
151
160
|
throw new Error(`OpenPress comment target path is invalid: ${value}`);
|
|
152
161
|
}
|
|
153
|
-
|
|
162
|
+
const posix = path.posix.normalize(normalized);
|
|
163
|
+
// The Press Tree source resolver emits paths relative to `document/`
|
|
164
|
+
// (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}`;
|
|
169
|
+
}
|
|
170
|
+
return posix;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 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
|
|
176
|
+
// is intentionally tight so we never silently rewrite engine internals
|
|
177
|
+
// (e.g. `src/openpress/...`) into "editable" workspace paths.
|
|
178
|
+
const SYSTEM_PATH_PREFIXES = [
|
|
179
|
+
"document/",
|
|
180
|
+
"src/",
|
|
181
|
+
"engine/",
|
|
182
|
+
"dist/",
|
|
183
|
+
"dist-react/",
|
|
184
|
+
"node_modules/",
|
|
185
|
+
"tests/",
|
|
186
|
+
"public/",
|
|
187
|
+
"packages/",
|
|
188
|
+
".openpress/",
|
|
189
|
+
".deploy/",
|
|
190
|
+
".changeset/",
|
|
191
|
+
".github/",
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
function looksDocumentRelative(posixPath) {
|
|
195
|
+
if (!/\.(mdx|tsx)$/.test(posixPath)) return false;
|
|
196
|
+
if (SYSTEM_PATH_PREFIXES.some((prefix) => posixPath.startsWith(prefix))) return false;
|
|
197
|
+
return true;
|
|
154
198
|
}
|
|
155
199
|
|
|
156
200
|
function normalizeLineNumber(value) {
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Layer 1 — Document entry loader.
|
|
2
|
+
//
|
|
3
|
+
// Loads `document/index.tsx`, validates it exports a Press component as
|
|
4
|
+
// default, reads optional `config` and `sources` named exports, and sets
|
|
5
|
+
// up the vite SSR server with `@open-press/core` aliases (including the
|
|
6
|
+
// subpaths `/mdx` and `/manuscript`).
|
|
7
|
+
|
|
1
8
|
import fs from "node:fs/promises";
|
|
2
9
|
import { createRequire } from "node:module";
|
|
3
10
|
import path from "node:path";
|
|
@@ -5,16 +12,19 @@ import { fileURLToPath } from "node:url";
|
|
|
5
12
|
import react from "@vitejs/plugin-react";
|
|
6
13
|
import ts from "typescript";
|
|
7
14
|
import { createServer as createViteServer } from "vite";
|
|
8
|
-
import { normalizeConfig } from "../config.mjs";
|
|
15
|
+
import { normalizeConfig } from "../runtime/config.mjs";
|
|
9
16
|
|
|
10
17
|
const ENGINE_REACT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
11
18
|
const FRAMEWORK_ROOT = path.resolve(ENGINE_REACT_DIR, "..", "..");
|
|
12
|
-
const CORE_ENTRY = path.join(FRAMEWORK_ROOT, "src", "openpress", "core", "index.tsx");
|
|
19
|
+
export const CORE_ENTRY = path.join(FRAMEWORK_ROOT, "src", "openpress", "core", "index.tsx");
|
|
20
|
+
export const MDX_ENTRY = path.join(FRAMEWORK_ROOT, "src", "openpress", "mdx", "index.ts");
|
|
21
|
+
export const MANUSCRIPT_ENTRY = path.join(FRAMEWORK_ROOT, "src", "openpress", "manuscript", "index.tsx");
|
|
22
|
+
export const NUMBERING_ENTRY = path.join(FRAMEWORK_ROOT, "src", "openpress", "numbering", "index.ts");
|
|
13
23
|
const REACT_PACKAGE_ROOT = path.join(FRAMEWORK_ROOT, "node_modules", "react");
|
|
14
24
|
const require = createRequire(import.meta.url);
|
|
15
25
|
const REACT_EXPORT_NAMES = Object.keys(require("react")).filter((name) => /^[A-Za-z_$][\w$]*$/.test(name));
|
|
16
26
|
|
|
17
|
-
export async function loadReactDocumentEntry(root = ".") {
|
|
27
|
+
export async function loadReactDocumentEntry(root = ".", { server: externalServer } = {}) {
|
|
18
28
|
const workspaceRoot = path.resolve(root);
|
|
19
29
|
const entryPath = path.join(workspaceRoot, "document", "index.tsx");
|
|
20
30
|
if (!(await fileExists(entryPath))) return null;
|
|
@@ -22,22 +32,34 @@ export async function loadReactDocumentEntry(root = ".") {
|
|
|
22
32
|
const source = await fs.readFile(entryPath, "utf8");
|
|
23
33
|
assertNoObviousTopLevelSideEffects(source, entryPath);
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
// If caller provides a server, reuse it so module identity is shared
|
|
36
|
+
// across the pipeline (PressContext, React, etc.). Otherwise open a
|
|
37
|
+
// temporary server for one-shot config reads.
|
|
38
|
+
const ownServer = externalServer ?? (await createReactSsrServer(workspaceRoot));
|
|
27
39
|
try {
|
|
28
|
-
const mod = await
|
|
40
|
+
const mod = await ownServer.ssrLoadModule(entryPath);
|
|
41
|
+
|
|
42
|
+
// Press default export is required for export/render but not for
|
|
43
|
+
// config-only commands (search, replace, validate). Validate if present;
|
|
44
|
+
// export pipeline throws separately if it's missing when actually needed.
|
|
45
|
+
const Press = typeof mod.default === "function" ? mod.default : null;
|
|
46
|
+
|
|
29
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).`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
return {
|
|
31
56
|
entryPath,
|
|
32
57
|
config,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
toc: mod.toc ?? null,
|
|
36
|
-
backCover: mod.backCover ?? null,
|
|
37
|
-
},
|
|
58
|
+
Press,
|
|
59
|
+
sources,
|
|
38
60
|
};
|
|
39
61
|
} finally {
|
|
40
|
-
await
|
|
62
|
+
if (!externalServer) await ownServer.close();
|
|
41
63
|
}
|
|
42
64
|
}
|
|
43
65
|
|
|
@@ -51,7 +73,12 @@ export async function createReactSsrServer(workspaceRoot = ".") {
|
|
|
51
73
|
plugins: [reactRuntimePlugin(), react()],
|
|
52
74
|
resolve: {
|
|
53
75
|
alias: [
|
|
54
|
-
|
|
76
|
+
// ORDER MATTERS: subpath aliases must precede the base alias so that
|
|
77
|
+
// `@open-press/core/mdx` doesn't resolve to `@open-press/core` + `/mdx`.
|
|
78
|
+
{ find: "@open-press/core/mdx", replacement: MDX_ENTRY },
|
|
79
|
+
{ find: "@open-press/core/manuscript", replacement: MANUSCRIPT_ENTRY },
|
|
80
|
+
{ find: "@open-press/core/numbering", replacement: NUMBERING_ENTRY },
|
|
81
|
+
{ find: "@open-press/core", replacement: CORE_ENTRY },
|
|
55
82
|
{ find: "@/components", replacement: path.join(resolvedWorkspaceRoot, "document", "components") },
|
|
56
83
|
],
|
|
57
84
|
},
|
|
@@ -64,23 +91,6 @@ export async function createReactSsrServer(workspaceRoot = ".") {
|
|
|
64
91
|
});
|
|
65
92
|
}
|
|
66
93
|
|
|
67
|
-
function normalizeReactDocumentConfig(workspaceRoot, entryPath, config) {
|
|
68
|
-
if (config != null && (typeof config !== "object" || Array.isArray(config))) {
|
|
69
|
-
throw new Error("OpenPress React document entry `config` export must be an object when provided.");
|
|
70
|
-
}
|
|
71
|
-
const rawConfig = config ?? {};
|
|
72
|
-
const paths = rawConfig.paths ?? {};
|
|
73
|
-
return normalizeConfig(workspaceRoot, {
|
|
74
|
-
...rawConfig,
|
|
75
|
-
documentDir: rawConfig.documentDir ?? paths.documentDir ?? "document",
|
|
76
|
-
sourceDir: rawConfig.sourceDir ?? paths.chaptersDir ?? paths.sourceDir ?? "chapters",
|
|
77
|
-
componentsDir: rawConfig.componentsDir ?? paths.componentsDir ?? "components",
|
|
78
|
-
mediaDir: rawConfig.mediaDir ?? paths.mediaDir ?? "media",
|
|
79
|
-
themeDir: rawConfig.themeDir ?? paths.themeDir ?? "theme",
|
|
80
|
-
designDoc: rawConfig.designDoc ?? paths.designDoc ?? "design.md",
|
|
81
|
-
}, entryPath);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
94
|
function assertNoObviousTopLevelSideEffects(source, entryPath) {
|
|
85
95
|
const sourceFile = ts.createSourceFile(entryPath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
86
96
|
for (const statement of sourceFile.statements) {
|
|
@@ -89,84 +99,117 @@ function assertNoObviousTopLevelSideEffects(source, entryPath) {
|
|
|
89
99
|
continue;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement))
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (ts.isExportDeclaration(statement) && statement.isTypeOnly) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
102
|
+
if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) continue;
|
|
103
|
+
if (ts.isExportDeclaration(statement) && statement.isTypeOnly) continue;
|
|
104
|
+
if (ts.isFunctionDeclaration(statement)) continue;
|
|
100
105
|
if (ts.isVariableStatement(statement)) {
|
|
101
|
-
|
|
106
|
+
assertTopLevelVariableStatement(statement, entryPath);
|
|
102
107
|
continue;
|
|
103
108
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
if (ts.isExportAssignment(statement)) {
|
|
110
|
+
assertPureDefaultExport(statement.expression, entryPath);
|
|
111
|
+
continue;
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
throw new Error(`OpenPress
|
|
114
|
+
throw new Error(`OpenPress document entry has unsupported top-level code in ${entryPath}: ${statementKindName(statement)}`);
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
function assertPureImport(statement, entryPath) {
|
|
114
119
|
if (!statement.importClause) {
|
|
115
|
-
throw new Error(`OpenPress
|
|
120
|
+
throw new Error(`OpenPress document entry has an unsupported side-effect import in ${entryPath}`);
|
|
116
121
|
}
|
|
117
122
|
const moduleName = stringLiteralText(statement.moduleSpecifier);
|
|
118
123
|
if (!statement.importClause.isTypeOnly && isFileSystemModule(moduleName)) {
|
|
119
|
-
throw new Error(`OpenPress
|
|
124
|
+
throw new Error(`OpenPress document entry imports filesystem APIs at top level in ${entryPath}`);
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
|
|
123
|
-
function
|
|
124
|
-
if (!hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
125
|
-
throw new Error(`OpenPress React document entry only allows exported const declarations at top level in ${entryPath}`);
|
|
126
|
-
}
|
|
128
|
+
function assertTopLevelVariableStatement(statement, entryPath) {
|
|
127
129
|
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
|
|
128
|
-
throw new Error(`OpenPress
|
|
130
|
+
throw new Error(`OpenPress document entry only allows top-level const declarations in ${entryPath}`);
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
const exported = hasModifier(statement, ts.SyntaxKind.ExportKeyword);
|
|
131
134
|
for (const declaration of statement.declarationList.declarations) {
|
|
132
|
-
if (
|
|
135
|
+
if (!ts.isIdentifier(declaration.name)) {
|
|
136
|
+
throw new Error(`OpenPress document entry only allows identifier const declarations at top level in ${entryPath}`);
|
|
137
|
+
}
|
|
138
|
+
const name = declaration.name.text;
|
|
139
|
+
if (exported && name !== "config" && name !== "sources") {
|
|
140
|
+
throw new Error(`OpenPress document entry only allows exported const config and sources in ${entryPath}`);
|
|
141
|
+
}
|
|
142
|
+
if (!declaration.initializer) {
|
|
143
|
+
throw new Error(`OpenPress document entry const "${name}" must have an initializer in ${entryPath}`);
|
|
144
|
+
}
|
|
145
|
+
if (name === "config") {
|
|
146
|
+
assertPureExpression(declaration.initializer, entryPath, { allowMdxSourceCall: false });
|
|
147
|
+
} else if (name === "sources") {
|
|
148
|
+
assertPureSourcesInitializer(declaration.initializer, entryPath);
|
|
149
|
+
} else {
|
|
150
|
+
assertPureStaticInitializer(declaration.initializer, entryPath);
|
|
151
|
+
}
|
|
133
152
|
}
|
|
134
153
|
}
|
|
135
154
|
|
|
136
|
-
function
|
|
137
|
-
|
|
155
|
+
function assertPureSourcesInitializer(node, entryPath) {
|
|
156
|
+
const expression = skipExpressionWrappers(node);
|
|
157
|
+
if (!ts.isObjectLiteralExpression(expression)) {
|
|
158
|
+
throw new Error(`OpenPress document entry exported sources must be an object literal in ${entryPath}`);
|
|
159
|
+
}
|
|
160
|
+
assertPureExpression(expression, entryPath, { allowMdxSourceCall: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function assertPureStaticInitializer(node, entryPath) {
|
|
164
|
+
const expression = skipExpressionWrappers(node);
|
|
165
|
+
if (ts.isFunctionExpression(expression) || ts.isArrowFunction(expression) || ts.isClassExpression(expression)) return;
|
|
166
|
+
assertPureExpression(expression, entryPath, { allowMdxSourceCall: false });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function assertPureDefaultExport(node, entryPath) {
|
|
170
|
+
const expression = skipExpressionWrappers(node);
|
|
171
|
+
if (ts.isFunctionExpression(expression) || ts.isArrowFunction(expression) || ts.isIdentifier(expression)) return;
|
|
172
|
+
throw new Error(`OpenPress document entry default export must be a function component in ${entryPath}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function assertPureExpression(node, entryPath, { allowMdxSourceCall }) {
|
|
176
|
+
visitExpression(node, (child) => {
|
|
138
177
|
if (ts.isAwaitExpression(child)) {
|
|
139
|
-
throw new Error(`OpenPress
|
|
178
|
+
throw new Error(`OpenPress document entry has an unsupported top-level side effect: await in ${entryPath}`);
|
|
140
179
|
}
|
|
141
|
-
|
|
142
180
|
if (ts.isCallExpression(child)) {
|
|
143
181
|
const callee = skipExpressionWrappers(child.expression);
|
|
182
|
+
if (allowMdxSourceCall && ts.isIdentifier(callee) && callee.text === "mdxSource") return;
|
|
144
183
|
if (ts.isIdentifier(callee) && callee.text === "fetch") {
|
|
145
|
-
throw new Error(`OpenPress
|
|
184
|
+
throw new Error(`OpenPress document entry has an unsupported top-level side effect: fetch(...) in ${entryPath}`);
|
|
146
185
|
}
|
|
147
186
|
if (ts.isPropertyAccessExpression(callee) && isIdentifierText(callee.expression, "console")) {
|
|
148
|
-
throw new Error(`OpenPress
|
|
187
|
+
throw new Error(`OpenPress document entry has an unsupported top-level side effect: console.${callee.name.text}(...) in ${entryPath}`);
|
|
149
188
|
}
|
|
150
189
|
if (ts.isPropertyAccessExpression(callee) && isIdentifierText(callee.expression, "fs")) {
|
|
151
|
-
throw new Error(`OpenPress
|
|
190
|
+
throw new Error(`OpenPress document entry has an unsupported top-level side effect: fs.${callee.name.text}(...) in ${entryPath}`);
|
|
152
191
|
}
|
|
153
|
-
throw new Error(`OpenPress
|
|
192
|
+
throw new Error(`OpenPress document entry cannot execute top-level function calls outside sources.mdxSource(...) in ${entryPath}`);
|
|
154
193
|
}
|
|
155
|
-
|
|
156
194
|
if (ts.isPropertyAccessExpression(child) && isProcessEnvAccess(child)) {
|
|
157
|
-
throw new Error(`OpenPress
|
|
195
|
+
throw new Error(`OpenPress document entry cannot read process.env at top level in ${entryPath}`);
|
|
158
196
|
}
|
|
159
197
|
});
|
|
160
198
|
}
|
|
161
199
|
|
|
162
|
-
function
|
|
200
|
+
function visitExpression(node, visitor) {
|
|
163
201
|
visitor(node);
|
|
164
|
-
|
|
202
|
+
if (node !== undefined && node !== null && isFunctionLikeExpression(node)) return;
|
|
203
|
+
ts.forEachChild(node, (child) => visitExpression(child, visitor));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isFunctionLikeExpression(node) {
|
|
207
|
+
return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isClassExpression(node);
|
|
165
208
|
}
|
|
166
209
|
|
|
167
210
|
function skipExpressionWrappers(node) {
|
|
168
211
|
let current = node;
|
|
169
|
-
while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
|
|
212
|
+
while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isTypeAssertionExpression(current) || ts.isNonNullExpression(current)) {
|
|
170
213
|
current = current.expression;
|
|
171
214
|
}
|
|
172
215
|
return current;
|
|
@@ -196,6 +239,23 @@ function isFileSystemModule(moduleName) {
|
|
|
196
239
|
return moduleName === "fs" || moduleName === "node:fs" || moduleName === "fs/promises" || moduleName === "node:fs/promises";
|
|
197
240
|
}
|
|
198
241
|
|
|
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
|
+
|
|
199
259
|
async function fileExists(filePath) {
|
|
200
260
|
try {
|
|
201
261
|
const stat = await fs.stat(filePath);
|