@open-press/core 1.2.1 → 1.3.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 +2 -2
- package/engine/commands/typecheck.mjs +1 -1
- package/engine/document-export.mjs +1 -1
- package/engine/output/page-block.mjs +11 -2
- package/engine/output/public-assets.mjs +41 -6
- package/engine/output/static-server.mjs +68 -15
- package/engine/react/caption-numbering.mjs +2 -2
- package/engine/react/comment-marker.mjs +1 -2
- package/engine/react/document-entry.mjs +64 -11
- package/engine/react/document-export.d.mts +6 -0
- package/engine/react/document-export.mjs +158 -28
- package/engine/react/mdx-compile.mjs +4 -4
- package/engine/react/measurement-css.mjs +3 -3
- package/engine/react/page-folio.mjs +37 -0
- package/engine/react/pagination/allocator.mjs +4 -4
- package/engine/react/pipeline/frame-measurement.mjs +34 -16
- package/engine/react/press-tree-inspection.mjs +43 -13
- package/engine/react/project-asset-endpoint.mjs +45 -11
- package/engine/react/sources/heading-numbering.mjs +2 -2
- package/engine/react/sources/mdx-resolver.mjs +3 -3
- package/engine/react/style-discovery.mjs +60 -11
- package/engine/react/text-source-transform.mjs +18 -4
- package/engine/runtime/config.mjs +22 -22
- package/engine/runtime/file-utils.mjs +57 -13
- package/engine/runtime/inspection.mjs +40 -15
- package/engine/runtime/page-geometry.mjs +6 -6
- package/engine/runtime/source-text-tools.mjs +28 -4
- package/engine/runtime/source-workspace.mjs +6 -9
- package/engine/runtime/validation.mjs +42 -24
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +6 -15
- package/src/openpress/app/OpenPressRuntime.tsx +3 -3
- package/src/openpress/app/WorkspaceGalleryPage.tsx +1 -1
- package/src/openpress/core/PageFolio.tsx +115 -0
- package/src/openpress/core/Press.tsx +5 -10
- package/src/openpress/core/Slide.tsx +11 -0
- package/src/openpress/core/index.tsx +4 -0
- package/src/openpress/core/types.ts +21 -13
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/document-model/workspaceManifestModel.ts +4 -9
- 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
|
|
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
|
|
209
|
-
|
|
210
|
-
|
|
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(
|
|
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.
|
|
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[] = [];
|