@open-press/core 1.2.1 → 1.3.2

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 (45) hide show
  1. package/README.md +2 -2
  2. package/engine/commands/typecheck.mjs +1 -1
  3. package/engine/document-export.mjs +1 -1
  4. package/engine/output/page-block.mjs +11 -2
  5. package/engine/output/public-assets.mjs +41 -6
  6. package/engine/output/static-server.mjs +68 -15
  7. package/engine/react/caption-numbering.mjs +2 -2
  8. package/engine/react/comment-marker.mjs +1 -2
  9. package/engine/react/document-entry.mjs +64 -11
  10. package/engine/react/document-export.d.mts +6 -0
  11. package/engine/react/document-export.mjs +158 -28
  12. package/engine/react/mdx-compile.mjs +4 -4
  13. package/engine/react/measurement-css.mjs +3 -3
  14. package/engine/react/page-folio.mjs +37 -0
  15. package/engine/react/pagination/allocator.mjs +4 -4
  16. package/engine/react/pipeline/frame-measurement.mjs +34 -16
  17. package/engine/react/press-tree-inspection.mjs +43 -13
  18. package/engine/react/project-asset-endpoint.mjs +45 -11
  19. package/engine/react/sources/heading-numbering.mjs +2 -2
  20. package/engine/react/sources/mdx-resolver.mjs +3 -3
  21. package/engine/react/style-discovery.mjs +60 -11
  22. package/engine/react/text-source-transform.mjs +18 -4
  23. package/engine/runtime/config.mjs +22 -22
  24. package/engine/runtime/file-utils.mjs +57 -13
  25. package/engine/runtime/inspection.mjs +40 -15
  26. package/engine/runtime/page-geometry.mjs +6 -6
  27. package/engine/runtime/source-text-tools.mjs +28 -4
  28. package/engine/runtime/source-workspace.mjs +6 -9
  29. package/engine/runtime/validation.mjs +42 -24
  30. package/package.json +1 -1
  31. package/src/openpress/app/OpenPressApp.tsx +20 -18
  32. package/src/openpress/app/OpenPressRuntime.tsx +3 -3
  33. package/src/openpress/app/WorkspaceGalleryPage.tsx +65 -39
  34. package/src/openpress/core/PageFolio.tsx +115 -0
  35. package/src/openpress/core/Press.tsx +5 -10
  36. package/src/openpress/core/Slide.tsx +11 -0
  37. package/src/openpress/core/index.tsx +4 -0
  38. package/src/openpress/core/types.ts +21 -13
  39. package/src/openpress/core/useSource.ts +1 -1
  40. package/src/openpress/document-model/workspaceManifestModel.ts +4 -9
  41. package/src/openpress/reader/SlidePresentationPage.tsx +7 -3
  42. package/src/openpress/workbench/shell/WorkbenchToolbarActions.tsx +46 -43
  43. package/src/styles/openpress/workbench-toolbar.css +33 -0
  44. package/src/styles/openpress/workspace-gallery.css +130 -47
  45. package/vite.config.ts +82 -16
package/vite.config.ts CHANGED
@@ -10,6 +10,7 @@ import { searchSourceText } from "./engine/runtime/source-text-tools.mjs";
10
10
  import { handleCommentRequest } from "./engine/react/comment-endpoint.mjs";
11
11
  import { handleProjectAssetRequest } from "./engine/react/project-asset-endpoint.mjs";
12
12
  import { handleSourceEditRequest } from "./engine/react/source-edit-endpoint.mjs";
13
+ import { exportReactDocument } from "./engine/react/document-export.mjs";
13
14
 
14
15
  const frameworkRoot = fileURLToPath(new URL("./", import.meta.url));
15
16
  const workspaceRoot = process.env.OPENPRESS_WORKSPACE_ROOT
@@ -26,10 +27,7 @@ const openpressConfig = await loadConfig(workspaceRoot);
26
27
  const outputDir = openpressConfig.paths.outputDir;
27
28
  const reactDocumentRoot = openpressConfig.paths.documentRoot;
28
29
  const reactDocumentComponentsRoot = openpressConfig.paths.componentsDir;
29
- const reactDocumentEntry = path.join(reactDocumentRoot, "index.tsx");
30
- const activeContentDir = await fileExists(reactDocumentEntry)
31
- ? path.join(reactDocumentRoot, "chapters")
32
- : openpressConfig.paths.sourceDir;
30
+ const activeContentDir = reactDocumentRoot;
33
31
 
34
32
  // Workspace directories — Vite resolves these at build time so that
35
33
  // `import.meta.glob("@workspace/content/**")` and friends follow the active
@@ -112,6 +110,11 @@ export default defineConfig({
112
110
  });
113
111
 
114
112
  function openpressLocalDeployPlugin() {
113
+ // Suppress auto-reload when source-edit endpoint triggers an export (avoids double reload).
114
+ let watcherSuppressedUntil = 0;
115
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
116
+ let exporting = false;
117
+
115
118
  return {
116
119
  name: "openpress-local-deploy-endpoint",
117
120
  configureServer(server: { middlewares: { use: (path: string, handler: (req: IncomingMessage, res: ServerResponse) => void) => void } }) {
@@ -128,6 +131,7 @@ function openpressLocalDeployPlugin() {
128
131
  void handleLocalSearchRequest(req, res);
129
132
  });
130
133
  server.middlewares.use("/__openpress/source-edit", (req, res) => {
134
+ if (req.method === "POST") watcherSuppressedUntil = Date.now() + 5000;
131
135
  void handleSourceEditRequest(req, res, { root: workspaceRoot });
132
136
  });
133
137
  server.middlewares.use("/__openpress/deploy", (req, res) => {
@@ -146,6 +150,31 @@ function openpressLocalDeployPlugin() {
146
150
  void handleLocalMediaFileRequest(req, res);
147
151
  });
148
152
  },
153
+ async handleHotUpdate({ file, server }: { file: string; server: { ws: { send: (payload: unknown) => void } } }) {
154
+ // Only react to changes inside the press/document directory.
155
+ const inDocumentRoot = file.startsWith(reactDocumentRoot + path.sep) || file === reactDocumentRoot;
156
+ const inContentDir = file.startsWith(activeContentDir + path.sep) || file === activeContentDir;
157
+ if (!inDocumentRoot && !inContentDir) return;
158
+
159
+ // Skip when source-edit already handled the export to avoid a double reload.
160
+ if (Date.now() < watcherSuppressedUntil) return [];
161
+
162
+ if (debounceTimer) clearTimeout(debounceTimer);
163
+ debounceTimer = setTimeout(async () => {
164
+ if (exporting) return;
165
+ exporting = true;
166
+ try {
167
+ await exportReactDocument(workspaceRoot, { syncAssets: false });
168
+ } catch {
169
+ // Export failure must not crash the dev server.
170
+ } finally {
171
+ exporting = false;
172
+ }
173
+ server.ws.send({ type: "full-reload" });
174
+ }, 300);
175
+
176
+ return []; // Suppress Vite's premature HMR until our export finishes.
177
+ },
149
178
  };
150
179
  }
151
180
 
@@ -205,14 +234,12 @@ async function handleLocalMediaFileRequest(req: IncomingMessage, res: ServerResp
205
234
  writeJson(res, 404, { ok: false, message: "Media file not found." });
206
235
  return;
207
236
  }
208
- const targetPath = path.join(openpressConfig.paths.mediaDir, fileName);
209
- const resolvedTarget = path.resolve(targetPath);
210
- const mediaRoot = path.resolve(openpressConfig.paths.mediaDir);
211
- if (!resolvedTarget.startsWith(`${mediaRoot}${path.sep}`) && resolvedTarget !== mediaRoot) {
212
- writeJson(res, 403, { ok: false, message: "Forbidden." });
237
+ const mediaPath = await findLocalMediaFile(fileName);
238
+ if (!mediaPath) {
239
+ writeJson(res, 404, { ok: false, message: "Media file not found." });
213
240
  return;
214
241
  }
215
- const body = await fs.readFile(resolvedTarget);
242
+ const body = await fs.readFile(mediaPath);
216
243
  res.writeHead(200, {
217
244
  "Content-Type": mediaMimeType(fileName),
218
245
  "Cache-Control": "no-store",
@@ -517,16 +544,11 @@ async function isLocalDeploymentDirty(deployedAt: string | undefined) {
517
544
 
518
545
  function getLocalDeploymentSourcePaths() {
519
546
  return [
520
- openpressConfig.paths.sourceDir,
521
- openpressConfig.paths.mediaDir,
522
- openpressConfig.paths.themeDir,
523
- openpressConfig.paths.designDoc,
524
- openpressConfig.paths.componentsDir,
547
+ openpressConfig.paths.documentRoot,
525
548
  path.join(frameworkRoot, "src"),
526
549
  path.join(frameworkRoot, "index.html"),
527
550
  path.join(frameworkRoot, "vite.config.ts"),
528
551
  path.join(workspaceRoot, "package.json"),
529
- path.join(workspaceRoot, "openpress.config.mjs"),
530
552
  openpressConfig.configPath,
531
553
  ];
532
554
  }
@@ -603,6 +625,50 @@ async function uniqueMediaFileName(mediaDir: string, fileName: string) {
603
625
  return candidate;
604
626
  }
605
627
 
628
+ async function findLocalMediaFile(fileName: string): Promise<string | null> {
629
+ for (const mediaRoot of await collectLocalMediaRoots()) {
630
+ const resolvedRoot = path.resolve(mediaRoot);
631
+ const candidate = path.resolve(mediaRoot, fileName);
632
+ if (!isInsideRoot(candidate, resolvedRoot)) continue;
633
+ if (await fileExists(candidate)) return candidate;
634
+ }
635
+ return null;
636
+ }
637
+
638
+ async function collectLocalMediaRoots(): Promise<string[]> {
639
+ const roots = [
640
+ openpressConfig.paths.mediaDir,
641
+ path.join(openpressConfig.paths.publicDir, "media"),
642
+ ];
643
+ try {
644
+ const entries = await fs.readdir(openpressConfig.paths.documentRoot, { withFileTypes: true });
645
+ for (const entry of entries) {
646
+ if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "shared") continue;
647
+ roots.push(path.join(openpressConfig.paths.documentRoot, entry.name, "media"));
648
+ }
649
+ } catch {
650
+ // Missing press/ is handled by the render/validate commands.
651
+ }
652
+ return uniquePaths(roots);
653
+ }
654
+
655
+ function uniquePaths(paths: string[]): string[] {
656
+ const out: string[] = [];
657
+ const seen = new Set<string>();
658
+ for (const candidate of paths) {
659
+ const normalized = path.resolve(candidate);
660
+ if (seen.has(normalized)) continue;
661
+ seen.add(normalized);
662
+ out.push(normalized);
663
+ }
664
+ return out;
665
+ }
666
+
667
+ function isInsideRoot(candidate: string, rootDir: string): boolean {
668
+ const relative = path.relative(rootDir, candidate);
669
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
670
+ }
671
+
606
672
  function readRequestBuffer(req: IncomingMessage, maxBytes: number) {
607
673
  return new Promise<Buffer>((resolve, reject) => {
608
674
  const chunks: Buffer[] = [];