@open-press/core 0.8.0 → 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/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 +92 -0
- package/engine/output/static-server.mjs +48 -9
- 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/package.json +3 -5
- 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/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 -0
- package/tsconfig.json +1 -1
- package/engine/commands/init.mjs +0 -24
- package/engine/init.mjs +0 -90
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@ Framework runtime, CLI engine, and Press Tree primitives for [open-press](https:
|
|
|
5
5
|
Most users do **not** install this package directly. Instead, scaffold a workspace with the CLI:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @open-press/cli init my-doc
|
|
8
|
+
npx @open-press/cli init my-doc
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The scaffolded workspace contains a snapshot of this package.
|
|
11
|
+
The scaffolded workspace contains a snapshot of this package. Starter files are supplied by skills, not by `@open-press/core`.
|
|
12
12
|
|
|
13
13
|
## Direct use
|
|
14
14
|
|
|
@@ -31,7 +31,10 @@ import { mdxSource } from "@open-press/core/mdx";
|
|
|
31
31
|
import { Sections, Toc } from "@open-press/core/manuscript";
|
|
32
32
|
```
|
|
33
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.
|
|
34
|
+
`press/index.tsx` or transitional `document/index.tsx` default-exports a `<Workspace>/<Press>` tree. `Frame` marks fixed-layout pages, `MdxArea` receives measured MDX blocks, and `mdxSource()` declares which MDX files participate in the render pipeline.
|
|
35
|
+
|
|
36
|
+
For the maintenance contract around Press Tree, page geometry presets, and the
|
|
37
|
+
allocation pipeline, see [`docs/press-tree.md`](https://github.com/quan0715/open-press/blob/main/docs/press-tree.md).
|
|
35
38
|
|
|
36
39
|
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).
|
|
37
40
|
|
package/engine/cli.mjs
CHANGED
|
@@ -4,25 +4,25 @@ import * as deployCmd from "./commands/deploy.mjs";
|
|
|
4
4
|
import * as devCmd from "./commands/dev.mjs";
|
|
5
5
|
import * as doctorCmd from "./commands/doctor.mjs";
|
|
6
6
|
import * as exportCmd from "./commands/export.mjs";
|
|
7
|
-
import * as initCmd from "./commands/init.mjs";
|
|
8
7
|
import * as inspectCmd from "./commands/inspect.mjs";
|
|
8
|
+
import * as imageCmd from "./commands/image.mjs";
|
|
9
9
|
import * as pdfCmd from "./commands/pdf.mjs";
|
|
10
10
|
import * as previewCmd from "./commands/preview.mjs";
|
|
11
11
|
import * as replaceCmd from "./commands/replace.mjs";
|
|
12
12
|
import * as renderCmd from "./commands/render.mjs";
|
|
13
13
|
import * as searchCmd from "./commands/search.mjs";
|
|
14
|
+
import * as skillsSyncCmd from "./commands/skills-sync.mjs";
|
|
14
15
|
import * as typecheckCmd from "./commands/typecheck.mjs";
|
|
15
16
|
import * as upgradeCmd from "./commands/upgrade.mjs";
|
|
16
17
|
import * as validateCmd from "./commands/validate.mjs";
|
|
17
18
|
import { parseOptions } from "./commands/_shared.mjs";
|
|
18
19
|
import { loadConfig } from "./runtime/config.mjs";
|
|
19
|
-
import { listStylePackSkills } from "./init.mjs";
|
|
20
20
|
import { discoverWorkspace } from "./runtime/validation.mjs";
|
|
21
21
|
|
|
22
22
|
const COMMANDS = {
|
|
23
|
-
init: initCmd,
|
|
24
23
|
validate: validateCmd,
|
|
25
24
|
inspect: inspectCmd,
|
|
25
|
+
image: imageCmd,
|
|
26
26
|
search: searchCmd,
|
|
27
27
|
replace: replaceCmd,
|
|
28
28
|
export: exportCmd,
|
|
@@ -34,6 +34,8 @@ const COMMANDS = {
|
|
|
34
34
|
deploy: deployCmd,
|
|
35
35
|
doctor: doctorCmd,
|
|
36
36
|
upgrade: upgradeCmd,
|
|
37
|
+
migrate: upgradeCmd,
|
|
38
|
+
"skills:sync": skillsSyncCmd,
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
const args = process.argv.slice(2);
|
|
@@ -71,12 +73,9 @@ async function main(commandName, argv) {
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
async function printHelp() {
|
|
74
|
-
const packs = await listStylePackSkills();
|
|
75
|
-
const skillList = packs.length ? packs.join(" | ") : "(none installed)";
|
|
76
76
|
console.log(`Usage: node engine/cli.mjs <command> [path] [options]
|
|
77
77
|
|
|
78
78
|
Commands:
|
|
79
|
-
init <target> [--skill <name>] [--force]
|
|
80
79
|
validate
|
|
81
80
|
inspect [--json] [--no-build] [--dry-run]
|
|
82
81
|
search [path] <query> [--json] [--scope content|all]
|
|
@@ -86,11 +85,12 @@ Commands:
|
|
|
86
85
|
preview --renderer react [--host 127.0.0.1] [--port 5173] [--no-build] [--dry-run]
|
|
87
86
|
dev --renderer react [--host 127.0.0.1] [--port 5173] [--no-build] [--dry-run]
|
|
88
87
|
typecheck
|
|
88
|
+
image [--output <outputDir>] [--no-build] [--dry-run]
|
|
89
89
|
pdf [--output <outputDir>/<pdf.filename>] [--no-build] [--dry-run]
|
|
90
90
|
deploy --confirm [--dry-run]
|
|
91
91
|
doctor [--json] [--no-cache] # version + skill staleness check
|
|
92
92
|
upgrade [--dry-run] [--no-deps] [--no-skills] [--json] # apply updates; agent-driven
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
migrate [--dry-run] [--no-deps] [--no-skills] [--json] # alias for upgrade; reads migration notes
|
|
94
|
+
skills:sync [--source <owner/repo>] [--dry-run] # refresh installed agent skills
|
|
95
95
|
`);
|
|
96
96
|
}
|
|
@@ -2,7 +2,7 @@ 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 "../output/chrome-pdf.mjs";
|
|
5
|
+
import { captureUrlPagesToPng, printUrlToPdf, stopChildProcess, waitForPrintReady } from "../output/chrome-pdf.mjs";
|
|
6
6
|
import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
|
|
7
7
|
import { exportDocument } from "../document-export.mjs";
|
|
8
8
|
import { optimizePdfMediaForStaticRoot } from "../output/pdf-media.mjs";
|
|
@@ -23,6 +23,9 @@ export function parseOptions(argv) {
|
|
|
23
23
|
else if (value === "--force") options.force = true;
|
|
24
24
|
else if (value === "--confirm") options.confirm = true;
|
|
25
25
|
else if (value === "--json") options.json = true;
|
|
26
|
+
else if (value === "--no-cache") options.noCache = true;
|
|
27
|
+
else if (value === "--no-deps") options.noDeps = true;
|
|
28
|
+
else if (value === "--no-skills") options.noSkills = true;
|
|
26
29
|
else if (value === "--no-build") options.noBuild = true;
|
|
27
30
|
else if (value === "--apply") options.apply = true;
|
|
28
31
|
else if (value === "--include-code") options.includeCode = true;
|
|
@@ -38,20 +41,6 @@ export function parseOptions(argv) {
|
|
|
38
41
|
return options;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
export function parseInitOptions(argv) {
|
|
42
|
-
const options = { force: false };
|
|
43
|
-
const positional = [];
|
|
44
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
45
|
-
const value = argv[i];
|
|
46
|
-
if (value === "--skill") options.skill = argv[++i];
|
|
47
|
-
else if (value === "--force") options.force = true;
|
|
48
|
-
else if (value.startsWith("--")) throw new Error(`Unknown option: ${value}`);
|
|
49
|
-
else positional.push(value);
|
|
50
|
-
}
|
|
51
|
-
options.target = positional[0];
|
|
52
|
-
return options;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
44
|
export function formatDisplayPath(absolutePath) {
|
|
56
45
|
const relative = path.relative(process.cwd(), absolutePath);
|
|
57
46
|
if (!relative || relative.startsWith("..")) return absolutePath;
|
|
@@ -118,6 +107,39 @@ export async function buildReactPdf({
|
|
|
118
107
|
return { pdfPath: outPath };
|
|
119
108
|
}
|
|
120
109
|
|
|
110
|
+
export async function buildReactImages({
|
|
111
|
+
root,
|
|
112
|
+
config,
|
|
113
|
+
outDir,
|
|
114
|
+
host = "127.0.0.1",
|
|
115
|
+
port = "5186",
|
|
116
|
+
noBuild = false,
|
|
117
|
+
recurse,
|
|
118
|
+
}) {
|
|
119
|
+
config ??= await loadConfig(root);
|
|
120
|
+
outDir ??= path.join(config.paths.outputDir, "images");
|
|
121
|
+
const renderCode = await buildReactStatic({ root, noBuild, recurse });
|
|
122
|
+
if (renderCode !== 0) throw new Error(`React render failed with exit code ${renderCode}`);
|
|
123
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
124
|
+
|
|
125
|
+
const server = await startStaticServer(root, config, host, port);
|
|
126
|
+
try {
|
|
127
|
+
const result = await captureUrlPagesToPng({
|
|
128
|
+
root,
|
|
129
|
+
url: `http://${host}:${port}/?print=1`,
|
|
130
|
+
outDir,
|
|
131
|
+
waitForReady: waitForPrintReady,
|
|
132
|
+
debuggingPortBase: 9700,
|
|
133
|
+
debuggingPortRange: 600,
|
|
134
|
+
profilePrefix: "chrome-image",
|
|
135
|
+
});
|
|
136
|
+
console.log(`${result.files.length} OpenPress pages exported to PNG`);
|
|
137
|
+
return { outDir, files: result.files, pageCount: result.pageCount };
|
|
138
|
+
} finally {
|
|
139
|
+
await stopChildProcess(server);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
121
143
|
export function startStaticServer(root, config, host, port) {
|
|
122
144
|
return new Promise((resolve, reject) => {
|
|
123
145
|
const child = spawn("node", [STATIC_SERVER, config.outputDir, "--host", host, "--port", port, "--workspace", "."], {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { CLI_ENTRY, STATIC_SERVER, buildReactImages, formatNodeScriptCommand } from "./_shared.mjs";
|
|
3
|
+
|
|
4
|
+
export async function run({ root, config, options, recurse }) {
|
|
5
|
+
const outputDir = options.output ? path.resolve(root, options.output) : path.join(config.paths.outputDir, "images");
|
|
6
|
+
const host = options.host ?? "127.0.0.1";
|
|
7
|
+
const port = options.port ?? "5186";
|
|
8
|
+
|
|
9
|
+
if (options.dryRun) {
|
|
10
|
+
console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} render . --renderer react`);
|
|
11
|
+
console.log(`Command: ${formatNodeScriptCommand(root, STATIC_SERVER)} ${config.outputDir} --host ${host} --port ${port} --workspace .`);
|
|
12
|
+
console.log(`Chrome image export URL: http://${host}:${port}/?print=1`);
|
|
13
|
+
console.log(`Output: ${path.relative(root, path.join(outputDir, "page-001.png"))}`);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result = await buildReactImages({
|
|
18
|
+
root,
|
|
19
|
+
config,
|
|
20
|
+
outDir: outputDir,
|
|
21
|
+
host,
|
|
22
|
+
port,
|
|
23
|
+
noBuild: options.noBuild,
|
|
24
|
+
recurse,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
console.log(`OpenPress images: ${path.relative(root, result.outDir)} (${result.files.length} pages)`);
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { runCommand } from "./_shared.mjs";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SOURCE = "quan0715/open-press";
|
|
7
|
+
|
|
8
|
+
// Refresh installed agent skills against the workspace's lock file.
|
|
9
|
+
// Behavior:
|
|
10
|
+
// - If skills-lock.json exists, run `npx skills upgrade` (refreshes all
|
|
11
|
+
// currently-installed sources to their latest published versions).
|
|
12
|
+
// - If skills-lock.json is missing, install the OpenPress framework
|
|
13
|
+
// skill bundle (and any user-supplied --source) as a first-time setup.
|
|
14
|
+
// - If a --source flag is passed, also add that source on top of any
|
|
15
|
+
// existing installations.
|
|
16
|
+
//
|
|
17
|
+
// Always exits 0 unless the underlying `skills` tool fails.
|
|
18
|
+
export async function run({ root, options }) {
|
|
19
|
+
const lockPath = path.join(root, "skills-lock.json");
|
|
20
|
+
const lockExists = existsSync(lockPath);
|
|
21
|
+
const extraSource = options?.source;
|
|
22
|
+
|
|
23
|
+
if (options?.dryRun) {
|
|
24
|
+
if (lockExists) {
|
|
25
|
+
console.log("Command: npx -y skills@latest upgrade");
|
|
26
|
+
} else {
|
|
27
|
+
console.log(`Command: npx -y skills@latest add ${DEFAULT_SOURCE}`);
|
|
28
|
+
}
|
|
29
|
+
if (extraSource) {
|
|
30
|
+
console.log(`Command: npx -y skills@latest add ${extraSource}`);
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (lockExists) {
|
|
36
|
+
const sources = await readLockSources(lockPath);
|
|
37
|
+
if (sources.length === 0) {
|
|
38
|
+
console.log("skills-lock.json has no sources; installing framework default…");
|
|
39
|
+
const code = await runCommand("npx", ["-y", "skills@latest", "add", DEFAULT_SOURCE], root);
|
|
40
|
+
if (code !== 0) return code;
|
|
41
|
+
} else {
|
|
42
|
+
console.log(`Refreshing ${sources.length} installed source(s)…`);
|
|
43
|
+
for (const src of sources) console.log(` ${src}`);
|
|
44
|
+
const code = await runCommand("npx", ["-y", "skills@latest", "upgrade"], root);
|
|
45
|
+
if (code !== 0) return code;
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
console.log(`No skills-lock.json; installing framework default: ${DEFAULT_SOURCE}`);
|
|
49
|
+
const code = await runCommand("npx", ["-y", "skills@latest", "add", DEFAULT_SOURCE], root);
|
|
50
|
+
if (code !== 0) return code;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (extraSource) {
|
|
54
|
+
console.log(`Adding extra source: ${extraSource}`);
|
|
55
|
+
const code = await runCommand("npx", ["-y", "skills@latest", "add", extraSource], root);
|
|
56
|
+
if (code !== 0) return code;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log("✓ Skills synced");
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readLockSources(lockPath) {
|
|
64
|
+
try {
|
|
65
|
+
const lock = JSON.parse(await readFile(lockPath, "utf8"));
|
|
66
|
+
const sources = Array.isArray(lock?.sources) ? lock.sources : [];
|
|
67
|
+
return sources.map((s) => s?.source).filter((s) => typeof s === "string");
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,5 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
1
4
|
import { runCommand } from "./_shared.mjs";
|
|
2
5
|
|
|
6
|
+
// Run typecheck via the locally installed typescript. The previous
|
|
7
|
+
// implementation used `npx tsc`; npm 11 + Node 24 (our CI / release
|
|
8
|
+
// pin) changed npx's bin lookup so it no longer walks pnpm's nested
|
|
9
|
+
// `.bin/` symlink farm and falls back to fetching the legacy
|
|
10
|
+
// `tsc@2.0.4` shim, which crashes.
|
|
11
|
+
//
|
|
12
|
+
// Resolution order:
|
|
13
|
+
// 1. `node <resolved tsc>` via require.resolve(typescript/package.json)
|
|
14
|
+
// — works with npm-hoisted layouts and most pnpm installs.
|
|
15
|
+
// 2. Walk up node_modules/.bin/tsc — covers downstream npm/yarn.
|
|
16
|
+
// 3. Fall back to `pnpm exec tsc` — pnpm knows its own symlink farm
|
|
17
|
+
// even when bare require.resolve doesn't, which is what CI hits.
|
|
3
18
|
export async function run({ root }) {
|
|
4
|
-
|
|
19
|
+
const absoluteRoot = path.resolve(root);
|
|
20
|
+
|
|
21
|
+
const tscBin = resolveTscBin(absoluteRoot);
|
|
22
|
+
if (tscBin) {
|
|
23
|
+
return runCommand("node", [tscBin, "--noEmit", "-p", "tsconfig.json"], absoluteRoot);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (hasCommand("pnpm")) {
|
|
27
|
+
return runCommand("pnpm", ["exec", "tsc", "--noEmit", "-p", "tsconfig.json"], absoluteRoot);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.error("[openpress] typescript is not installed in this workspace.");
|
|
31
|
+
console.error("Add it with: npm install --save-dev typescript");
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveTscBin(absoluteRoot) {
|
|
36
|
+
try {
|
|
37
|
+
const require = createRequire(path.join(absoluteRoot, "package.json"));
|
|
38
|
+
const pkgPath = require.resolve("typescript/package.json");
|
|
39
|
+
return path.join(path.dirname(pkgPath), "bin", "tsc");
|
|
40
|
+
} catch {
|
|
41
|
+
// fall through to .bin probe
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let dir = absoluteRoot;
|
|
45
|
+
while (true) {
|
|
46
|
+
const candidate = path.join(dir, "node_modules", ".bin", "tsc");
|
|
47
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
48
|
+
const parent = path.dirname(dir);
|
|
49
|
+
if (parent === dir) return null;
|
|
50
|
+
dir = parent;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasCommand(name) {
|
|
55
|
+
const PATH = process.env.PATH ?? "";
|
|
56
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
57
|
+
const exts = process.platform === "win32"
|
|
58
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")
|
|
59
|
+
: [""];
|
|
60
|
+
for (const dir of PATH.split(sep)) {
|
|
61
|
+
if (!dir) continue;
|
|
62
|
+
for (const ext of exts) {
|
|
63
|
+
if (fs.existsSync(path.join(dir, name + ext))) return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
5
67
|
}
|
|
@@ -46,7 +46,7 @@ export async function run({ root, options }) {
|
|
|
46
46
|
if (!json) {
|
|
47
47
|
process.stdout.write("dry run — nothing changed. The agent should:\n");
|
|
48
48
|
process.stdout.write(" 1. read each docs/migrations/<version>.md for document-level changes\n");
|
|
49
|
-
process.stdout.write(" 2. apply edits to
|
|
49
|
+
process.stdout.write(" 2. apply edits to press/ where needed\n");
|
|
50
50
|
process.stdout.write(" 3. re-run: npx open-press upgrade (without --dry-run)\n");
|
|
51
51
|
} else {
|
|
52
52
|
process.stdout.write(JSON.stringify({ status: "dry-run", before }, null, 2) + "\n");
|
|
@@ -98,11 +98,11 @@ export async function run({ root, options }) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
process.stdout.write(
|
|
101
|
-
"\nAgent: open each file, identify document-level changes, grep
|
|
101
|
+
"\nAgent: open each file, identify document-level changes, grep press/ for affected patterns, propose edits before applying.\n",
|
|
102
102
|
);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
process.stdout.write("\nVerify with:\n npm run
|
|
105
|
+
process.stdout.write("\nVerify with:\n npm run build\n\n");
|
|
106
106
|
return 0;
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -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
|
|
13
|
+
"React/MDX document entry not found. Expected press/index.tsx with a Press default export before exporting.",
|
|
14
14
|
);
|
|
15
15
|
}
|
|
@@ -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();
|
|
@@ -73,17 +73,45 @@ const server = http.createServer(async (req, res) => {
|
|
|
73
73
|
res.end("Forbidden");
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
}
|
|
81
96
|
} catch {
|
|
82
97
|
res.writeHead(404);
|
|
83
98
|
res.end("Not found");
|
|
84
99
|
}
|
|
85
100
|
});
|
|
86
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
|
+
|
|
87
115
|
server.listen(port, host, () => {
|
|
88
116
|
console.log(`OpenPress static preview: http://${host}:${port}/`);
|
|
89
117
|
});
|
|
@@ -149,7 +177,10 @@ function valueAfter(args, flag) {
|
|
|
149
177
|
|
|
150
178
|
async function inferWorkspaceRoot(staticRoot) {
|
|
151
179
|
for (const candidate of [staticRoot, path.dirname(staticRoot), path.dirname(path.dirname(staticRoot))]) {
|
|
152
|
-
|
|
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;
|
|
153
184
|
}
|
|
154
185
|
if (path.basename(path.dirname(staticRoot)) === ".deploy") {
|
|
155
186
|
return path.dirname(path.dirname(staticRoot));
|
|
@@ -157,6 +188,16 @@ async function inferWorkspaceRoot(staticRoot) {
|
|
|
157
188
|
return process.cwd();
|
|
158
189
|
}
|
|
159
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
|
+
|
|
160
201
|
async function handleLocalPdfExportRequest(req, res) {
|
|
161
202
|
if (req.method !== "POST") {
|
|
162
203
|
writeJson(res, 405, { ok: false, message: "Local PDF export endpoint requires POST." });
|
|
@@ -367,7 +408,7 @@ function isDeployConfigured() {
|
|
|
367
408
|
function deploySetupMessage() {
|
|
368
409
|
if (isDeployConfigured()) return undefined;
|
|
369
410
|
if (config.deploy.adapter === "cloudflare-pages") {
|
|
370
|
-
return
|
|
411
|
+
return 'Cloudflare Pages deployment requires `openpress.deploy.projectName` in package.json.';
|
|
371
412
|
}
|
|
372
413
|
return `Deployment adapter \`${config.deploy.adapter}\` is not configured.`;
|
|
373
414
|
}
|
|
@@ -433,8 +474,6 @@ function getDeploymentSourcePaths() {
|
|
|
433
474
|
path.join(workspace, "src"),
|
|
434
475
|
path.join(workspace, "index.html"),
|
|
435
476
|
path.join(workspace, "package.json"),
|
|
436
|
-
path.join(workspace, "openpress.config.mjs"),
|
|
437
|
-
config.configPath,
|
|
438
477
|
path.join(workspace, "vite.config.ts"),
|
|
439
478
|
];
|
|
440
479
|
}
|
|
@@ -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/",
|