@open-press/core 0.6.0 → 0.7.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.
Files changed (71) hide show
  1. package/README.md +9 -5
  2. package/engine/cli.mjs +2 -5
  3. package/engine/commands/_shared.mjs +4 -4
  4. package/engine/commands/deploy.mjs +1 -1
  5. package/engine/commands/inspect.mjs +3 -3
  6. package/engine/commands/replace.mjs +1 -1
  7. package/engine/commands/search.mjs +1 -1
  8. package/engine/commands/upgrade.mjs +47 -5
  9. package/engine/commands/validate.mjs +2 -2
  10. package/engine/document-export.mjs +1 -1
  11. package/engine/{chrome-pdf.mjs → output/chrome-pdf.mjs} +1 -2
  12. package/engine/{deploy-sync.mjs → output/deploy-sync.mjs} +2 -2
  13. package/engine/{fonts.mjs → output/fonts.mjs} +1 -1
  14. package/engine/{public-assets.mjs → output/public-assets.mjs} +2 -2
  15. package/engine/{static-server.mjs → output/static-server.mjs} +2 -2
  16. package/engine/react/caption-numbering.mjs +73 -0
  17. package/engine/react/comment-marker.mjs +54 -10
  18. package/engine/react/document-entry.mjs +124 -64
  19. package/engine/react/document-export.mjs +266 -310
  20. package/engine/react/mdx-compile.mjs +214 -3
  21. package/engine/react/measurement-css.mjs +3 -3
  22. package/engine/react/pagination/allocator.mjs +122 -0
  23. package/engine/react/pagination/regions.mjs +81 -0
  24. package/engine/react/pagination.mjs +9 -121
  25. package/engine/react/pipeline/allocate.mjs +248 -0
  26. package/engine/react/pipeline/final-render.mjs +94 -0
  27. package/engine/react/pipeline/frame-measurement.mjs +300 -0
  28. package/engine/react/pipeline/press-tree.mjs +135 -0
  29. package/engine/react/project-asset-endpoint.mjs +2 -2
  30. package/engine/react/{chapter-css.mjs → section-css.mjs} +12 -9
  31. package/engine/react/sources/heading-numbering.mjs +132 -0
  32. package/engine/react/sources/mdx-resolver.mjs +441 -0
  33. package/engine/react/{workspace-discovery.mjs → style-discovery.mjs} +29 -40
  34. package/engine/{config.mjs → runtime/config.mjs} +15 -0
  35. package/engine/{file-utils.mjs → runtime/file-utils.mjs} +1 -1
  36. package/engine/{inspection.mjs → runtime/inspection.mjs} +3 -4
  37. package/engine/{source-text-tools.mjs → runtime/source-text-tools.mjs} +24 -7
  38. package/engine/runtime/source-workspace.mjs +186 -0
  39. package/engine/{validation.mjs → runtime/validation.mjs} +19 -17
  40. package/package.json +5 -2
  41. package/src/openpress/anchorMap.ts +27 -0
  42. package/src/openpress/core/Frame.tsx +80 -0
  43. package/src/openpress/core/FrameContext.tsx +19 -0
  44. package/src/openpress/core/MdxArea.tsx +35 -0
  45. package/src/openpress/core/Press.tsx +34 -0
  46. package/src/openpress/core/index.tsx +34 -15
  47. package/src/openpress/core/primitives.tsx +23 -0
  48. package/src/openpress/core/types.ts +131 -19
  49. package/src/openpress/core/useSource.ts +28 -0
  50. package/src/openpress/manuscript/index.tsx +196 -0
  51. package/src/openpress/mdx/index.ts +88 -0
  52. package/src/openpress/numbering/index.ts +294 -0
  53. package/src/openpress/publicPage.tsx +4 -186
  54. package/src/openpress/reactDocumentMetadata.ts +2 -16
  55. package/src/openpress/types.ts +0 -16
  56. package/src/openpress/workbench.tsx +2 -36
  57. package/src/styles/openpress/responsive.css +0 -14
  58. package/tsconfig.json +4 -1
  59. package/vite.config.ts +10 -3
  60. package/engine/commands/migrate-to-react.mjs +0 -27
  61. package/engine/page-renderer.mjs +0 -217
  62. package/engine/react/migrate-to-react.mjs +0 -355
  63. package/engine/source-workspace.mjs +0 -76
  64. package/src/openpress/core/basePages.tsx +0 -87
  65. package/src/openpress/pagination.ts +0 -845
  66. /package/engine/{chrome-pdf.d.mts → output/chrome-pdf.d.mts} +0 -0
  67. /package/engine/{katex-assets.mjs → output/katex-assets.mjs} +0 -0
  68. /package/engine/{page-block.mjs → output/page-block.mjs} +0 -0
  69. /package/engine/{pdf-media.mjs → output/pdf-media.mjs} +0 -0
  70. /package/engine/{config.d.mts → runtime/config.d.mts} +0 -0
  71. /package/engine/{issue-report.mjs → runtime/issue-report.mjs} +0 -0
@@ -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
- const server = await createReactSsrServer(workspaceRoot);
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 server.ssrLoadModule(entryPath);
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
- shell: {
34
- cover: mod.cover ?? null,
35
- toc: mod.toc ?? null,
36
- backCover: mod.backCover ?? null,
37
- },
58
+ Press,
59
+ sources,
38
60
  };
39
61
  } finally {
40
- await server.close();
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
- { find: "@openpress/core", replacement: CORE_ENTRY },
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
- continue;
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
- assertExportedConstStatement(statement, entryPath);
106
+ assertTopLevelVariableStatement(statement, entryPath);
102
107
  continue;
103
108
  }
104
-
105
- if (ts.isExpressionStatement(statement)) {
106
- assertPureTopLevelInitializer(statement.expression, entryPath);
109
+ if (ts.isExportAssignment(statement)) {
110
+ assertPureDefaultExport(statement.expression, entryPath);
111
+ continue;
107
112
  }
108
113
 
109
- throw new Error(`OpenPress React document entry has unsupported top-level code in ${entryPath}: ${statementKindName(statement)}`);
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 React document entry has an unsupported side-effect import in ${entryPath}`);
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 React document entry imports filesystem APIs at top level in ${entryPath}`);
124
+ throw new Error(`OpenPress document entry imports filesystem APIs at top level in ${entryPath}`);
120
125
  }
121
126
  }
122
127
 
123
- function assertExportedConstStatement(statement, entryPath) {
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 React document entry only allows exported const declarations at top level in ${entryPath}`);
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 (declaration.initializer) assertPureTopLevelInitializer(declaration.initializer, entryPath);
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 assertPureTopLevelInitializer(node, entryPath) {
137
- visitNode(node, (child) => {
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 React document entry has an unsupported top-level side effect: await in ${entryPath}`);
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 React document entry has an unsupported top-level side effect: fetch(...) in ${entryPath}`);
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 React document entry has an unsupported top-level side effect: console.${callee.name.text}(...) in ${entryPath}`);
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 React document entry has an unsupported top-level side effect: fs.${callee.name.text}(...) in ${entryPath}`);
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 React document entry cannot execute top-level function calls in exported config or shell JSX in ${entryPath}`);
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 React document entry cannot read process.env in exported config or shell JSX in ${entryPath}`);
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 visitNode(node, visitor) {
200
+ function visitExpression(node, visitor) {
163
201
  visitor(node);
164
- ts.forEachChild(node, (child) => visitNode(child, visitor));
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);