@open-press/core 1.0.0 → 1.1.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @open-press/core
2
2
 
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.
3
+ Package-owned runtime, render 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
 
@@ -8,7 +8,7 @@ Most users do **not** install this package directly. Instead, scaffold a workspa
8
8
  npx @open-press/cli init my-doc
9
9
  ```
10
10
 
11
- The scaffolded workspace contains a snapshot of this package. Starter files are supplied by skills, not by `@open-press/core`.
11
+ The scaffolded workspace depends on this package; it does not vendor a copy of the runtime. Starter files are supplied by skills or by project-specific `press/` source files.
12
12
 
13
13
  ## Direct use
14
14
 
@@ -36,7 +36,16 @@ import { Sections, Toc } from "@open-press/core/manuscript";
36
36
  For the maintenance contract around Press Tree, page geometry presets, and the
37
37
  allocation pipeline, see [`docs/press-tree.md`](https://github.com/quan0715/open-press/blob/main/docs/press-tree.md).
38
38
 
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).
39
+ The public CLI bin lives in `@open-press/cli` and delegates runtime commands to this package:
40
+
41
+ ```bash
42
+ npm install @open-press/core @open-press/cli
43
+ npx open-press dev .
44
+ npx open-press render .
45
+ npx open-press pdf .
46
+ ```
47
+
48
+ `@open-press/core` owns the internal browser shell (`index.html`), Vite config, React app runtime, render pipeline, and static server. A workspace owns `press/`, `package.json`, media, components, and theme files.
40
49
 
41
50
  ## License
42
51
 
package/engine/cli.mjs CHANGED
@@ -73,7 +73,7 @@ async function main(commandName, argv) {
73
73
  }
74
74
 
75
75
  async function printHelp() {
76
- console.log(`Usage: node engine/cli.mjs <command> [path] [options]
76
+ console.log(`Usage: open-press <command> [path] [options]
77
77
 
78
78
  Commands:
79
79
  validate
@@ -1,4 +1,5 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { createRequire } from "node:module";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
@@ -8,8 +9,14 @@ import { exportDocument } from "../document-export.mjs";
8
9
  import { optimizePdfMediaForStaticRoot } from "../output/pdf-media.mjs";
9
10
 
10
11
  export const ENGINE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
+ export const FRAMEWORK_ROOT = path.resolve(ENGINE_DIR, "..");
11
13
  export const CLI_ENTRY = path.join(ENGINE_DIR, "cli.mjs");
12
14
  export const STATIC_SERVER = path.join(ENGINE_DIR, "output", "static-server.mjs");
15
+ export const VITE_CONFIG = path.join(FRAMEWORK_ROOT, "vite.config.ts");
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const VITE_PACKAGE_JSON = require.resolve("vite/package.json");
19
+ export const VITE_BIN = path.join(path.dirname(VITE_PACKAGE_JSON), "bin", "vite.js");
13
20
 
14
21
  export function parseOptions(argv) {
15
22
  const options = {};
@@ -47,8 +54,12 @@ export function formatDisplayPath(absolutePath) {
47
54
  return relative;
48
55
  }
49
56
 
50
- export function runCommand(commandName, commandArgs, cwd) {
51
- const result = spawnSync(commandName, commandArgs, { cwd, stdio: "inherit" });
57
+ export function runCommand(commandName, commandArgs, cwd, opts = {}) {
58
+ const result = spawnSync(commandName, commandArgs, {
59
+ cwd,
60
+ env: { ...process.env, ...(opts.env ?? {}) },
61
+ stdio: "inherit",
62
+ });
52
63
  return result.status ?? 1;
53
64
  }
54
65
 
@@ -58,6 +69,24 @@ export function formatNodeScriptCommand(root, scriptPath) {
58
69
  return `node ${displayPath}`;
59
70
  }
60
71
 
72
+ export function formatOpenPressCommand(args = []) {
73
+ return `open-press ${args.join(" ")}`.trim();
74
+ }
75
+
76
+ export function workspaceRuntimeEnv(root) {
77
+ return { OPENPRESS_WORKSPACE_ROOT: path.resolve(root) };
78
+ }
79
+
80
+ export function viteCommandArgs(args = []) {
81
+ return [VITE_BIN, ...args];
82
+ }
83
+
84
+ export function formatViteCommand(root, args = []) {
85
+ const script = formatNodeScriptCommand(root, VITE_BIN);
86
+ const config = formatDisplayPath(VITE_CONFIG);
87
+ return `${script} ${args.join(" ")} --config ${config}`.replace(/\s+/g, " ").trim();
88
+ }
89
+
61
90
  export async function buildReactStatic({ root, noBuild = false, recurse, silent = false }) {
62
91
  if (noBuild) return 0;
63
92
  if (!silent) {
@@ -65,9 +94,10 @@ export async function buildReactStatic({ root, noBuild = false, recurse, silent
65
94
  }
66
95
 
67
96
  await exportDocument(root);
68
- const result = spawnSync("npx", ["vite", "build", "--config", "vite.config.ts"], {
97
+ const result = spawnSync("node", viteCommandArgs(["build", "--config", VITE_CONFIG]), {
69
98
  cwd: root,
70
99
  encoding: "utf8",
100
+ env: { ...process.env, ...workspaceRuntimeEnv(root) },
71
101
  });
72
102
  return result.status ?? 1;
73
103
  }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { deploySync } from "../output/deploy-sync.mjs";
3
- import { CLI_ENTRY, buildReactPdf, formatNodeScriptCommand, runCommand, writePdfStageDeployConfig } from "./_shared.mjs";
3
+ import { buildReactPdf, formatOpenPressCommand, runCommand, writePdfStageDeployConfig } from "./_shared.mjs";
4
4
 
5
5
  export async function run({ root, config, options, recurse }) {
6
6
  if (config.deploy.requiresConfirmation === true && !options.confirm) {
@@ -12,9 +12,9 @@ export async function run({ root, config, options, recurse }) {
12
12
  const commitDirty = config.deploy.commitDirty;
13
13
  if (options.dryRun) {
14
14
  console.log("OpenPress deploy dry run");
15
- console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} render . --renderer react`);
15
+ console.log(`Command: ${formatOpenPressCommand(["render", ".", "--renderer", "react"])}`);
16
16
  console.log(`Step: deploy-sync (copy ${config.outputDir} → ${source})`);
17
- console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} pdf . --output ${source}/${config.pdf.filename}`);
17
+ console.log(`Command: ${formatOpenPressCommand(["pdf", ".", "--output", `${source}/${config.pdf.filename}`])}`);
18
18
  console.log(`Step: write ${source}/openpress/deploy.json with deployment metadata`);
19
19
  console.log(`Command: npx wrangler pages deploy ${source}${projectName ? ` --project-name=${projectName}` : ""}${commitDirty ? " --commit-dirty=true" : ""}`);
20
20
  return 0;
@@ -1,6 +1,13 @@
1
1
  import { exportDocument } from "../document-export.mjs";
2
2
  import { diagnose } from "./doctor.mjs";
3
- import { CLI_ENTRY, formatNodeScriptCommand, runCommand } from "./_shared.mjs";
3
+ import {
4
+ VITE_CONFIG,
5
+ formatOpenPressCommand,
6
+ formatViteCommand,
7
+ runCommand,
8
+ viteCommandArgs,
9
+ workspaceRuntimeEnv,
10
+ } from "./_shared.mjs";
4
11
 
5
12
  export async function run({ root, options }) {
6
13
  const renderer = options.renderer ?? "react";
@@ -14,9 +21,9 @@ export async function run({ root, options }) {
14
21
  if (options.dryRun) {
15
22
  console.log(`OpenPress dev URL: ${url}`);
16
23
  if (!options.noBuild) {
17
- console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} export .`);
24
+ console.log(`Command: ${formatOpenPressCommand(["export", "."])}`);
18
25
  }
19
- console.log(`Command: npx vite --force --config vite.config.ts --host ${host} --port ${port}`);
26
+ console.log(`Command: ${formatViteCommand(root, ["--force", "--host", host, "--port", port])}`);
20
27
  return 0;
21
28
  }
22
29
  if (!options.noBuild) {
@@ -27,7 +34,9 @@ export async function run({ root, options }) {
27
34
  await printDoctorNoticeIfStale(root);
28
35
 
29
36
  console.log(`OpenPress dev: ${url}`);
30
- return runCommand("npx", ["vite", "--force", "--config", "vite.config.ts", "--host", host, "--port", port], root);
37
+ return runCommand("node", viteCommandArgs(["--force", "--config", VITE_CONFIG, "--host", host, "--port", port]), root, {
38
+ env: workspaceRuntimeEnv(root),
39
+ });
31
40
  }
32
41
 
33
42
  async function printDoctorNoticeIfStale(root) {
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { CLI_ENTRY, STATIC_SERVER, buildReactImages, formatNodeScriptCommand } from "./_shared.mjs";
2
+ import { STATIC_SERVER, buildReactImages, formatNodeScriptCommand, formatOpenPressCommand } from "./_shared.mjs";
3
3
 
4
4
  export async function run({ root, config, options, recurse }) {
5
5
  const outputDir = options.output ? path.resolve(root, options.output) : path.join(config.paths.outputDir, "images");
@@ -7,7 +7,7 @@ export async function run({ root, config, options, recurse }) {
7
7
  const port = options.port ?? "5186";
8
8
 
9
9
  if (options.dryRun) {
10
- console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} render . --renderer react`);
10
+ console.log(`Command: ${formatOpenPressCommand(["render", ".", "--renderer", "react"])}`);
11
11
  console.log(`Command: ${formatNodeScriptCommand(root, STATIC_SERVER)} ${config.outputDir} --host ${host} --port ${port} --workspace .`);
12
12
  console.log(`Chrome image export URL: http://${host}:${port}/?print=1`);
13
13
  console.log(`Output: ${path.relative(root, path.join(outputDir, "page-001.png"))}`);
@@ -1,5 +1,6 @@
1
1
  import { inspectWorkspace } from "../runtime/inspection.mjs";
2
2
  import { exitCodeForIssueReport } from "../runtime/issue-report.mjs";
3
+ import { STATIC_SERVER, formatNodeScriptCommand, formatOpenPressCommand } from "./_shared.mjs";
3
4
 
4
5
  export async function run({ root, config, options, recurse }) {
5
6
  const host = options.host ?? "127.0.0.1";
@@ -8,9 +9,9 @@ export async function run({ root, config, options, recurse }) {
8
9
 
9
10
  if (options.dryRun) {
10
11
  if (!options.noBuild) {
11
- console.log("Command: node engine/cli.mjs render . --renderer react");
12
+ console.log(`Command: ${formatOpenPressCommand(["render", ".", "--renderer", "react"])}`);
12
13
  }
13
- console.log(`Command: node engine/output/static-server.mjs ${config.outputDir} --host ${host} --port ${port} --workspace .`);
14
+ console.log(`Command: ${formatNodeScriptCommand(root, STATIC_SERVER)} ${config.outputDir} --host ${host} --port ${port} --workspace .`);
14
15
  console.log(`Chrome inspection URL: ${url}`);
15
16
  return 0;
16
17
  }
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { CLI_ENTRY, STATIC_SERVER, buildReactPdf, formatNodeScriptCommand } from "./_shared.mjs";
2
+ import { STATIC_SERVER, buildReactPdf, formatNodeScriptCommand, formatOpenPressCommand } from "./_shared.mjs";
3
3
 
4
4
  export async function run({ root, config, options, recurse }) {
5
5
  const outputPath = options.output ? path.resolve(root, options.output) : undefined;
@@ -7,7 +7,7 @@ export async function run({ root, config, options, recurse }) {
7
7
  const relOutput = path.relative(root, outputPath ?? config.paths.pdf);
8
8
  const host = options.host ?? "127.0.0.1";
9
9
  const port = options.port ?? "5185";
10
- console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} render . --renderer react`);
10
+ console.log(`Command: ${formatOpenPressCommand(["render", ".", "--renderer", "react"])}`);
11
11
  console.log(`Command: ${formatNodeScriptCommand(root, STATIC_SERVER)} ${config.outputDir} --host ${host} --port ${port} --workspace .`);
12
12
  console.log(`Command: Chrome --print-to-pdf=${relOutput} http://${host}:${port}/?print=1`);
13
13
  return 0;
@@ -1,4 +1,4 @@
1
- import { CLI_ENTRY, STATIC_SERVER, formatNodeScriptCommand, runCommand } from "./_shared.mjs";
1
+ import { STATIC_SERVER, formatNodeScriptCommand, formatOpenPressCommand, runCommand } from "./_shared.mjs";
2
2
 
3
3
  export async function run({ root, config, options, recurse }) {
4
4
  const renderer = options.renderer ?? "react";
@@ -12,7 +12,7 @@ export async function run({ root, config, options, recurse }) {
12
12
  if (options.dryRun) {
13
13
  console.log(`OpenPress preview URL: ${url}`);
14
14
  if (!options.noBuild) {
15
- console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} render . --renderer react`);
15
+ console.log(`Command: ${formatOpenPressCommand(["render", ".", "--renderer", "react"])}`);
16
16
  }
17
17
  console.log(`Command: ${formatNodeScriptCommand(root, STATIC_SERVER)} ${config.outputDir} --host ${host} --port ${port} --workspace .`);
18
18
  return 0;
@@ -1,5 +1,5 @@
1
1
  import { exportDocument } from "../document-export.mjs";
2
- import { runCommand } from "./_shared.mjs";
2
+ import { VITE_CONFIG, formatOpenPressCommand, formatViteCommand, runCommand, viteCommandArgs, workspaceRuntimeEnv } from "./_shared.mjs";
3
3
 
4
4
  export async function run({ root, options }) {
5
5
  const renderer = options.renderer ?? "react";
@@ -8,10 +8,12 @@ export async function run({ root, options }) {
8
8
  return 2;
9
9
  }
10
10
  if (options.dryRun) {
11
- console.log("Command: node engine/cli.mjs export .");
12
- console.log("Command: npx vite build --config vite.config.ts");
11
+ console.log(`Command: ${formatOpenPressCommand(["export", "."])}`);
12
+ console.log(`Command: ${formatViteCommand(root, ["build"])}`);
13
13
  return 0;
14
14
  }
15
15
  await exportDocument(root);
16
- return runCommand("npx", ["vite", "build", "--config", "vite.config.ts"], root);
16
+ return runCommand("node", viteCommandArgs(["build", "--config", VITE_CONFIG]), root, {
17
+ env: workspaceRuntimeEnv(root),
18
+ });
17
19
  }
@@ -3,7 +3,7 @@ import { replaceSourceText } from "../runtime/source-text-tools.mjs";
3
3
  export async function run({ config, options }) {
4
4
  const args = replaceArgsFromOptions(options);
5
5
  if (!args) {
6
- console.error("Usage: node engine/cli.mjs replace [path] <from> <to> [--json] [--apply] [--scope content|all] [--include-code] [--case-sensitive]");
6
+ console.error("Usage: open-press replace [path] <from> <to> [--json] [--apply] [--scope content|all] [--include-code] [--case-sensitive]");
7
7
  return 2;
8
8
  }
9
9
 
@@ -3,7 +3,7 @@ import { searchSourceText } from "../runtime/source-text-tools.mjs";
3
3
  export async function run({ config, options }) {
4
4
  const query = searchQueryFromOptions(options);
5
5
  if (!query) {
6
- console.error("Usage: node engine/cli.mjs search [path] <query> [--json] [--scope content|all] [--case-sensitive]");
6
+ console.error("Usage: open-press search [path] <query> [--json] [--scope content|all] [--case-sensitive]");
7
7
  return 2;
8
8
  }
9
9
 
@@ -1,7 +1,9 @@
1
1
  import fs from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { createRequire } from "node:module";
4
- import { runCommand } from "./_shared.mjs";
5
+ import { FRAMEWORK_ROOT, runCommand } from "./_shared.mjs";
6
+ import { loadConfig } from "../runtime/config.mjs";
5
7
 
6
8
  // Run typecheck via the locally installed typescript. The previous
7
9
  // implementation used `npx tsc`; npm 11 + Node 24 (our CI / release
@@ -17,14 +19,15 @@ import { runCommand } from "./_shared.mjs";
17
19
  // even when bare require.resolve doesn't, which is what CI hits.
18
20
  export async function run({ root }) {
19
21
  const absoluteRoot = path.resolve(root);
22
+ const projectPath = await resolveTypecheckProject(absoluteRoot);
20
23
 
21
24
  const tscBin = resolveTscBin(absoluteRoot);
22
25
  if (tscBin) {
23
- return runCommand("node", [tscBin, "--noEmit", "-p", "tsconfig.json"], absoluteRoot);
26
+ return runCommand("node", [tscBin, "--noEmit", "-p", projectPath], absoluteRoot);
24
27
  }
25
28
 
26
29
  if (hasCommand("pnpm")) {
27
- return runCommand("pnpm", ["exec", "tsc", "--noEmit", "-p", "tsconfig.json"], absoluteRoot);
30
+ return runCommand("pnpm", ["exec", "tsc", "--noEmit", "-p", projectPath], absoluteRoot);
28
31
  }
29
32
 
30
33
  console.error("[openpress] typescript is not installed in this workspace.");
@@ -32,6 +35,88 @@ export async function run({ root }) {
32
35
  return 1;
33
36
  }
34
37
 
38
+ async function resolveTypecheckProject(absoluteRoot) {
39
+ const workspaceProject = path.join(absoluteRoot, "tsconfig.json");
40
+ if (fs.existsSync(workspaceProject)) return workspaceProject;
41
+ return await writeGeneratedTypecheckProject(absoluteRoot);
42
+ }
43
+
44
+ async function writeGeneratedTypecheckProject(absoluteRoot) {
45
+ const config = await loadConfig(absoluteRoot);
46
+ const outDir = path.join(absoluteRoot, ".openpress");
47
+ await mkdir(outDir, { recursive: true });
48
+
49
+ const projectPath = path.join(outDir, "typecheck.tsconfig.json");
50
+ const fromProjectDir = (target) => normalizePath(path.relative(outDir, target)) || ".";
51
+ const fromWorkspace = (target) => normalizePath(path.relative(absoluteRoot, target)) || ".";
52
+ const includeRoot = (target) => {
53
+ const relative = fromProjectDir(target);
54
+ return relative === "." ? "." : relative;
55
+ };
56
+
57
+ const typeRoots = [
58
+ path.join(absoluteRoot, "node_modules", "@types"),
59
+ path.join(FRAMEWORK_ROOT, "node_modules", "@types"),
60
+ ]
61
+ .filter((dir) => fs.existsSync(dir))
62
+ .map((dir) => fromProjectDir(dir));
63
+
64
+ const typecheckConfig = {
65
+ compilerOptions: {
66
+ target: "ES2022",
67
+ useDefineForClassFields: true,
68
+ lib: ["DOM", "DOM.Iterable", "ES2022"],
69
+ allowJs: false,
70
+ skipLibCheck: true,
71
+ esModuleInterop: true,
72
+ allowSyntheticDefaultImports: true,
73
+ strict: true,
74
+ forceConsistentCasingInFileNames: true,
75
+ module: "ESNext",
76
+ moduleResolution: "Bundler",
77
+ types: ["node"],
78
+ ...(typeRoots.length > 0 ? { typeRoots } : {}),
79
+ resolveJsonModule: true,
80
+ isolatedModules: true,
81
+ noEmit: true,
82
+ jsx: "react-jsx",
83
+ ignoreDeprecations: "6.0",
84
+ baseUrl: "..",
85
+ paths: {
86
+ "@open-press/core": [fromWorkspace(path.join(FRAMEWORK_ROOT, "src", "openpress", "core", "index.tsx"))],
87
+ "@open-press/core/mdx": [fromWorkspace(path.join(FRAMEWORK_ROOT, "src", "openpress", "mdx", "index.ts"))],
88
+ "@open-press/core/manuscript": [fromWorkspace(path.join(FRAMEWORK_ROOT, "src", "openpress", "manuscript", "index.tsx"))],
89
+ "@open-press/core/numbering": [fromWorkspace(path.join(FRAMEWORK_ROOT, "src", "openpress", "numbering", "index.ts"))],
90
+ "@/components": [
91
+ `${fromWorkspace(config.paths.componentsDir)}/index.ts`,
92
+ `${fromWorkspace(config.paths.componentsDir)}/index.tsx`,
93
+ ],
94
+ "@/components/*": [`${fromWorkspace(config.paths.componentsDir)}/*`],
95
+ "@workspace/content": [fromWorkspace(config.paths.sourceDir)],
96
+ "@workspace/content/*": [`${fromWorkspace(config.paths.sourceDir)}/*`],
97
+ "@workspace/media": [fromWorkspace(config.paths.mediaDir)],
98
+ "@workspace/media/*": [`${fromWorkspace(config.paths.mediaDir)}/*`],
99
+ "@workspace/components": [fromWorkspace(config.paths.componentsDir)],
100
+ "@workspace/components/*": [`${fromWorkspace(config.paths.componentsDir)}/*`],
101
+ "@/*": [`${fromWorkspace(path.join(FRAMEWORK_ROOT, "src"))}/*`],
102
+ },
103
+ },
104
+ include: [
105
+ `${includeRoot(config.paths.documentRoot)}/**/*.ts`,
106
+ `${includeRoot(config.paths.documentRoot)}/**/*.tsx`,
107
+ "../tests/**/*.test.ts",
108
+ "../tests/**/*.test.tsx",
109
+ ],
110
+ };
111
+
112
+ await writeFile(projectPath, `${JSON.stringify(typecheckConfig, null, 2)}\n`, "utf8");
113
+ return projectPath;
114
+ }
115
+
116
+ function normalizePath(value) {
117
+ return value.replaceAll("\\", "/");
118
+ }
119
+
35
120
  function resolveTscBin(absoluteRoot) {
36
121
  try {
37
122
  const require = createRequire(path.join(absoluteRoot, "package.json"));
@@ -41,6 +126,14 @@ function resolveTscBin(absoluteRoot) {
41
126
  // fall through to .bin probe
42
127
  }
43
128
 
129
+ try {
130
+ const require = createRequire(import.meta.url);
131
+ const pkgPath = require.resolve("typescript/package.json");
132
+ return path.join(path.dirname(pkgPath), "bin", "tsc");
133
+ } catch {
134
+ // fall through to .bin probe
135
+ }
136
+
44
137
  let dir = absoluteRoot;
45
138
  while (true) {
46
139
  const candidate = path.join(dir, "node_modules", ".bin", "tsc");
@@ -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",
@@ -210,7 +214,7 @@ async function handleLocalPdfExportRequest(req, res) {
210
214
  ok: result.code === 0 && exists,
211
215
  code: result.code,
212
216
  pdf: `/__openpress/local-pdf-file?ts=${Date.now()}`,
213
- command: "node engine/cli.mjs pdf .",
217
+ command: "open-press pdf .",
214
218
  stdout: result.stdout,
215
219
  stderr: result.stderr,
216
220
  });
@@ -250,7 +254,7 @@ async function handleDeployRequest(req, res) {
250
254
  deploy_adapter: config.deploy.adapter,
251
255
  deploy_source: config.deploy.source,
252
256
  deploy_project_name: config.deploy.projectName,
253
- command: "node engine/cli.mjs deploy . --confirm",
257
+ command: "open-press deploy . --confirm",
254
258
  });
255
259
  return;
256
260
  }
@@ -269,7 +273,7 @@ async function handleDeployRequest(req, res) {
269
273
  pdf: deployedUrl ? `${deployedUrl}/${config.pdf.filename}` : deploymentInfo.pdf,
270
274
  public_url: publicUrl,
271
275
  dirty: false,
272
- command: "node engine/cli.mjs deploy . --confirm",
276
+ command: "open-press deploy . --confirm",
273
277
  stdout: result.stdout,
274
278
  stderr: result.stderr,
275
279
  });
@@ -354,7 +358,7 @@ async function handleMediaFileRequest(req, res, url) {
354
358
 
355
359
  function runLocalPdfExport() {
356
360
  return new Promise((resolve) => {
357
- const child = spawn("node", ["engine/cli.mjs", "pdf", "."], {
361
+ const child = spawn("node", [CLI_ENTRY, "pdf", "."], {
358
362
  cwd: workspace,
359
363
  shell: false,
360
364
  });
@@ -377,7 +381,7 @@ function runLocalPdfExport() {
377
381
 
378
382
  function runDeploy() {
379
383
  return new Promise((resolve) => {
380
- const child = spawn("node", ["engine/cli.mjs", "deploy", ".", "--confirm"], {
384
+ const child = spawn("node", [CLI_ENTRY, "deploy", ".", "--confirm"], {
381
385
  cwd: workspace,
382
386
  shell: false,
383
387
  });
@@ -471,10 +475,10 @@ function getDeploymentSourcePaths() {
471
475
  config.paths.themeDir,
472
476
  config.paths.designDoc,
473
477
  config.paths.componentsDir,
474
- path.join(workspace, "src"),
475
- path.join(workspace, "index.html"),
478
+ path.join(FRAMEWORK_ROOT, "src"),
479
+ path.join(FRAMEWORK_ROOT, "index.html"),
480
+ path.join(FRAMEWORK_ROOT, "vite.config.ts"),
476
481
  path.join(workspace, "package.json"),
477
- path.join(workspace, "vite.config.ts"),
478
482
  ];
479
483
  }
480
484
 
package/index.html CHANGED
@@ -4,6 +4,10 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
7
+ <link rel="stylesheet" href="/openpress/fonts.css" />
8
+ <link rel="stylesheet" href="/openpress/tokens.css" />
9
+ <link rel="stylesheet" href="/openpress/content.css" />
10
+ <link rel="stylesheet" href="/openpress/components.css" />
7
11
  <title>OpenPress Workspace</title>
8
12
  </head>
9
13
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-press/core",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "description": "open-press core — runtime primitives, CLI, and render pipeline for AI-first fixed-layout documents.",
6
6
  "license": "MIT",
@@ -31,13 +31,12 @@
31
31
  "./manuscript": "./src/openpress/manuscript/index.tsx",
32
32
  "./numbering": "./src/openpress/numbering/index.ts"
33
33
  },
34
- "bin": {
35
- "open-press": "engine/cli.mjs"
36
- },
37
34
  "files": [
38
35
  "engine",
36
+ "src/main.tsx",
39
37
  "src/openpress",
40
38
  "src/styles",
39
+ "src/vite-env.d.ts",
41
40
  "index.html",
42
41
  "vite.config.ts",
43
42
  "tsconfig.json"
@@ -52,13 +51,16 @@
52
51
  "js-yaml": "^4.1.1",
53
52
  "katex": "^0.16.47",
54
53
  "lucide-react": "^1.16.0",
54
+ "@vitejs/plugin-react": "^6.0.2",
55
55
  "playwright": "^1.60.0",
56
56
  "postcss": "^8.5.6",
57
57
  "react": "^19.2.6",
58
58
  "react-dom": "^19.2.6",
59
59
  "rehype-katex": "^7.0.1",
60
60
  "remark-gfm": "^4.0.1",
61
- "remark-math": "^6.0.0"
61
+ "remark-math": "^6.0.0",
62
+ "typescript": "^6.0.3",
63
+ "vite": "^8.0.13"
62
64
  },
63
65
  "devDependencies": {
64
66
  "@playwright/test": "^1.60.0",
@@ -66,10 +68,7 @@
66
68
  "@types/node": "^25.8.0",
67
69
  "@types/react": "^19.2.14",
68
70
  "@types/react-dom": "^19.2.3",
69
- "@vitejs/plugin-react": "^6.0.2",
70
71
  "jsdom": "^26.1.0",
71
- "typescript": "^6.0.3",
72
- "vite": "^8.0.13",
73
72
  "vitest": "^4.1.6"
74
73
  },
75
74
  "scripts": {
package/src/main.tsx ADDED
@@ -0,0 +1,16 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { OpenPressApp } from "./openpress/app";
4
+ import "./styles/openpress.css";
5
+
6
+ const rootElement = document.getElementById("root");
7
+
8
+ if (!rootElement) {
9
+ throw new Error("OpenPress renderer requires a #root element.");
10
+ }
11
+
12
+ createRoot(rootElement).render(
13
+ <StrictMode>
14
+ <OpenPressApp />
15
+ </StrictMode>,
16
+ );
@@ -1,5 +1,5 @@
1
- // Source directory paths come from vite.config.ts build-time defines, which
2
- // in turn read openpress.config.mjs. The React app does not hardcode `document/`.
1
+ // Source directory paths come from package-owned Vite build-time defines,
2
+ // which read the active workspace conventions and package.json config.
3
3
 
4
4
  export const PROJECT_SOURCES = {
5
5
  content: {
@@ -1,8 +1,3 @@
1
- @import url("/openpress/fonts.css");
2
- @import url("/openpress/tokens.css");
3
- @import url("/openpress/content.css");
4
- @import url("/openpress/components.css");
5
-
6
1
  @import "./openpress/app-shell.css";
7
2
  @import "./openpress/workspace-gallery.css";
8
3
  @import "./openpress/workbench.css";
@@ -0,0 +1,8 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // Workspace path constants injected by vite.config.ts at build time.
4
+ // These come from package-owned workspace conventions and package.json config.
5
+ declare const __OPENPRESS_CONTENT_PATH__: string;
6
+ declare const __OPENPRESS_MEDIA_PATH__: string;
7
+ declare const __OPENPRESS_COMPONENTS_PATH__: string;
8
+ declare const __OPENPRESS_PDF_HREF__: string;
package/vite.config.ts CHANGED
@@ -54,8 +54,10 @@ const workspaceDefines = {
54
54
  };
55
55
 
56
56
  export default defineConfig({
57
+ root: frameworkRoot,
57
58
  base: "./",
58
59
  cacheDir: path.join(workspaceRoot, ".openpress", "vite-client"),
60
+ publicDir: path.join(workspaceRoot, "public"),
59
61
  plugins: [openpressLocalDeployPlugin(), react()],
60
62
  define: workspaceDefines,
61
63
  resolve: {
@@ -409,7 +411,7 @@ function isLocalDeployConfigured() {
409
411
  function localDeploySetupMessage() {
410
412
  if (isLocalDeployConfigured()) return undefined;
411
413
  if (openpressConfig.deploy.adapter === "cloudflare-pages") {
412
- return "Cloudflare Pages deployment requires `deploy.projectName` in openpress.config.mjs.";
414
+ return "Cloudflare Pages deployment requires `openpress.deploy.projectName` in package.json.";
413
415
  }
414
416
  return `Deployment adapter \`${openpressConfig.deploy.adapter}\` is not configured.`;
415
417
  }
@@ -472,18 +474,16 @@ function getLocalDeploymentSourcePaths() {
472
474
  openpressConfig.paths.designDoc,
473
475
  openpressConfig.paths.componentsDir,
474
476
  path.join(frameworkRoot, "src"),
475
- path.join(workspaceRoot, "index.html"),
477
+ path.join(frameworkRoot, "index.html"),
478
+ path.join(frameworkRoot, "vite.config.ts"),
476
479
  path.join(workspaceRoot, "package.json"),
477
480
  path.join(workspaceRoot, "openpress.config.mjs"),
478
481
  openpressConfig.configPath,
479
- path.join(workspaceRoot, "vite.config.ts"),
480
482
  ];
481
483
  }
482
484
 
483
485
  function openpressCliCommand(args: string[]) {
484
- const relativeCliPath = path.relative(workspaceRoot, openpressCliPath).replaceAll("\\", "/");
485
- const displayCliPath = relativeCliPath && !relativeCliPath.startsWith("../") ? relativeCliPath : openpressCliPath;
486
- return `node ${displayCliPath} ${args.join(" ")}`;
486
+ return `open-press ${args.join(" ")}`;
487
487
  }
488
488
 
489
489
  async function findNewestLocalSourceMtime(paths: string[]) {