@open-press/cli 1.0.0 → 1.1.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.
Files changed (175) hide show
  1. package/README.md +11 -12
  2. package/dist/cli.js +298 -79
  3. package/package.json +9 -7
  4. package/template/core/AGENTS.md +0 -130
  5. package/template/core/CHANGELOG.md +0 -218
  6. package/template/core/README.md +0 -43
  7. package/template/core/engine/cli.mjs +0 -96
  8. package/template/core/engine/commands/_shared.mjs +0 -199
  9. package/template/core/engine/commands/deploy.mjs +0 -31
  10. package/template/core/engine/commands/dev.mjs +0 -49
  11. package/template/core/engine/commands/doctor.mjs +0 -229
  12. package/template/core/engine/commands/export.mjs +0 -8
  13. package/template/core/engine/commands/image.mjs +0 -29
  14. package/template/core/engine/commands/inspect.mjs +0 -35
  15. package/template/core/engine/commands/pdf.mjs +0 -26
  16. package/template/core/engine/commands/preview.mjs +0 -26
  17. package/template/core/engine/commands/render.mjs +0 -17
  18. package/template/core/engine/commands/replace.mjs +0 -41
  19. package/template/core/engine/commands/search.mjs +0 -33
  20. package/template/core/engine/commands/skills-sync.mjs +0 -71
  21. package/template/core/engine/commands/typecheck.mjs +0 -67
  22. package/template/core/engine/commands/upgrade.mjs +0 -159
  23. package/template/core/engine/commands/validate.mjs +0 -17
  24. package/template/core/engine/document-export.mjs +0 -15
  25. package/template/core/engine/output/chrome-pdf.d.mts +0 -34
  26. package/template/core/engine/output/chrome-pdf.mjs +0 -450
  27. package/template/core/engine/output/deploy-sync.mjs +0 -15
  28. package/template/core/engine/output/fonts.mjs +0 -62
  29. package/template/core/engine/output/katex-assets.mjs +0 -45
  30. package/template/core/engine/output/page-block.mjs +0 -30
  31. package/template/core/engine/output/pdf-media.mjs +0 -45
  32. package/template/core/engine/output/public-assets.mjs +0 -19
  33. package/template/core/engine/output/static-server.mjs +0 -571
  34. package/template/core/engine/react/caption-numbering.mjs +0 -73
  35. package/template/core/engine/react/comment-endpoint.d.mts +0 -11
  36. package/template/core/engine/react/comment-endpoint.mjs +0 -102
  37. package/template/core/engine/react/comment-marker.mjs +0 -374
  38. package/template/core/engine/react/document-entry.mjs +0 -331
  39. package/template/core/engine/react/document-export.mjs +0 -512
  40. package/template/core/engine/react/http-json.mjs +0 -24
  41. package/template/core/engine/react/mdx-compile.mjs +0 -629
  42. package/template/core/engine/react/measurement-css.mjs +0 -157
  43. package/template/core/engine/react/object-entities.mjs +0 -204
  44. package/template/core/engine/react/pagination/allocator.mjs +0 -167
  45. package/template/core/engine/react/pagination/regions.mjs +0 -81
  46. package/template/core/engine/react/pagination-constants.mjs +0 -3
  47. package/template/core/engine/react/pagination.mjs +0 -9
  48. package/template/core/engine/react/pipeline/allocate.mjs +0 -217
  49. package/template/core/engine/react/pipeline/final-render.mjs +0 -94
  50. package/template/core/engine/react/pipeline/frame-measurement.mjs +0 -306
  51. package/template/core/engine/react/pipeline/press-tree.mjs +0 -135
  52. package/template/core/engine/react/press-tree-inspection.mjs +0 -172
  53. package/template/core/engine/react/project-asset-endpoint.d.mts +0 -10
  54. package/template/core/engine/react/project-asset-endpoint.mjs +0 -361
  55. package/template/core/engine/react/section-css.mjs +0 -56
  56. package/template/core/engine/react/source-edit-endpoint.d.mts +0 -10
  57. package/template/core/engine/react/source-edit-endpoint.mjs +0 -75
  58. package/template/core/engine/react/sources/heading-numbering.mjs +0 -132
  59. package/template/core/engine/react/sources/mdx-resolver.mjs +0 -439
  60. package/template/core/engine/react/style-discovery.mjs +0 -160
  61. package/template/core/engine/runtime/config.d.mts +0 -48
  62. package/template/core/engine/runtime/config.mjs +0 -172
  63. package/template/core/engine/runtime/file-utils.mjs +0 -114
  64. package/template/core/engine/runtime/file-walk.mjs +0 -22
  65. package/template/core/engine/runtime/inspection.mjs +0 -328
  66. package/template/core/engine/runtime/issue-report.mjs +0 -44
  67. package/template/core/engine/runtime/page-geometry.mjs +0 -131
  68. package/template/core/engine/runtime/path-utils.mjs +0 -20
  69. package/template/core/engine/runtime/source-text-tools.d.mts +0 -102
  70. package/template/core/engine/runtime/source-text-tools.mjs +0 -832
  71. package/template/core/engine/runtime/source-workspace.mjs +0 -168
  72. package/template/core/engine/runtime/validation.mjs +0 -183
  73. package/template/core/index.html +0 -13
  74. package/template/core/openpress.config.mjs +0 -8
  75. package/template/core/package.json +0 -89
  76. package/template/core/src/main.tsx +0 -16
  77. package/template/core/src/openpress/app/OpenPressApp.tsx +0 -296
  78. package/template/core/src/openpress/app/OpenPressRuntime.tsx +0 -102
  79. package/template/core/src/openpress/app/WorkspaceGalleryPage.tsx +0 -219
  80. package/template/core/src/openpress/app/index.ts +0 -2
  81. package/template/core/src/openpress/core/Frame.tsx +0 -91
  82. package/template/core/src/openpress/core/FrameContext.tsx +0 -26
  83. package/template/core/src/openpress/core/MdxArea.tsx +0 -34
  84. package/template/core/src/openpress/core/Press.tsx +0 -55
  85. package/template/core/src/openpress/core/Workspace.tsx +0 -36
  86. package/template/core/src/openpress/core/cn.ts +0 -4
  87. package/template/core/src/openpress/core/index.tsx +0 -47
  88. package/template/core/src/openpress/core/primitives.tsx +0 -91
  89. package/template/core/src/openpress/core/types.ts +0 -236
  90. package/template/core/src/openpress/core/useSource.ts +0 -28
  91. package/template/core/src/openpress/document-model/anchorMapModel.ts +0 -27
  92. package/template/core/src/openpress/document-model/documentIndexes.ts +0 -329
  93. package/template/core/src/openpress/document-model/documentTypes.ts +0 -147
  94. package/template/core/src/openpress/document-model/index.ts +0 -7
  95. package/template/core/src/openpress/document-model/objectEntityModel.ts +0 -55
  96. package/template/core/src/openpress/document-model/projectIdentityModel.ts +0 -15
  97. package/template/core/src/openpress/document-model/reactDocumentMetadataModel.ts +0 -27
  98. package/template/core/src/openpress/document-model/workspaceManifestModel.ts +0 -57
  99. package/template/core/src/openpress/manuscript/index.tsx +0 -238
  100. package/template/core/src/openpress/mdx/index.ts +0 -96
  101. package/template/core/src/openpress/numbering/index.ts +0 -294
  102. package/template/core/src/openpress/reader/PageThumbnailsPanel.tsx +0 -168
  103. package/template/core/src/openpress/reader/PublicReaderPage.tsx +0 -267
  104. package/template/core/src/openpress/reader/ReaderNavigationPanel.tsx +0 -123
  105. package/template/core/src/openpress/reader/index.ts +0 -11
  106. package/template/core/src/openpress/reader/pageViewportScaleModel.ts +0 -73
  107. package/template/core/src/openpress/reader/readerPageRegistry.ts +0 -41
  108. package/template/core/src/openpress/reader/readerPageRoute.ts +0 -21
  109. package/template/core/src/openpress/reader/readerScroll.ts +0 -92
  110. package/template/core/src/openpress/reader/readerStateModel.ts +0 -15
  111. package/template/core/src/openpress/reader/readerTypes.ts +0 -4
  112. package/template/core/src/openpress/reader/usePageViewportScale.ts +0 -119
  113. package/template/core/src/openpress/reader/usePanelState.ts +0 -56
  114. package/template/core/src/openpress/reader/useReaderHashSync.ts +0 -61
  115. package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +0 -48
  116. package/template/core/src/openpress/reader/useReaderRuntime.ts +0 -146
  117. package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +0 -64
  118. package/template/core/src/openpress/shared/Panel.tsx +0 -77
  119. package/template/core/src/openpress/shared/frameScheduler.ts +0 -32
  120. package/template/core/src/openpress/shared/index.ts +0 -4
  121. package/template/core/src/openpress/shared/numberUtils.ts +0 -3
  122. package/template/core/src/openpress/shared/runtimeMode.ts +0 -11
  123. package/template/core/src/openpress/workbench/Workbench.tsx +0 -506
  124. package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +0 -157
  125. package/template/core/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
  126. package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +0 -182
  127. package/template/core/src/openpress/workbench/actions/SearchControl.tsx +0 -345
  128. package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +0 -112
  129. package/template/core/src/openpress/workbench/actions/index.ts +0 -6
  130. package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +0 -136
  131. package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +0 -72
  132. package/template/core/src/openpress/workbench/dialog/index.ts +0 -1
  133. package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +0 -127
  134. package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +0 -207
  135. package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +0 -9
  136. package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +0 -34
  137. package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +0 -525
  138. package/template/core/src/openpress/workbench/document/index.ts +0 -10
  139. package/template/core/src/openpress/workbench/index.ts +0 -2
  140. package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +0 -459
  141. package/template/core/src/openpress/workbench/inspector/index.ts +0 -5
  142. package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +0 -125
  143. package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +0 -160
  144. package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +0 -408
  145. package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +0 -254
  146. package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +0 -41
  147. package/template/core/src/openpress/workbench/mentions/index.ts +0 -2
  148. package/template/core/src/openpress/workbench/mentions/useComposerMentions.ts +0 -185
  149. package/template/core/src/openpress/workbench/panels/Panel.tsx +0 -1
  150. package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +0 -80
  151. package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +0 -29
  152. package/template/core/src/openpress/workbench/panels/index.ts +0 -3
  153. package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +0 -525
  154. package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +0 -35
  155. package/template/core/src/openpress/workbench/project/index.ts +0 -2
  156. package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +0 -11
  157. package/template/core/src/openpress/workbench/project/projectSourceModel.ts +0 -24
  158. package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +0 -167
  159. package/template/core/src/openpress/workbench/shell/index.ts +0 -1
  160. package/template/core/src/openpress/workbench/workbenchFormatters.ts +0 -120
  161. package/template/core/src/openpress/workbench/workbenchTypes.ts +0 -35
  162. package/template/core/src/styles/openpress/app-shell.css +0 -251
  163. package/template/core/src/styles/openpress/media-workspace.css +0 -230
  164. package/template/core/src/styles/openpress/print-route.css +0 -184
  165. package/template/core/src/styles/openpress/project-preview-panel.css +0 -924
  166. package/template/core/src/styles/openpress/public-viewer.css +0 -688
  167. package/template/core/src/styles/openpress/reader-runtime.css +0 -989
  168. package/template/core/src/styles/openpress/responsive.css +0 -245
  169. package/template/core/src/styles/openpress/workbench-panels.css +0 -707
  170. package/template/core/src/styles/openpress/workbench.css +0 -1255
  171. package/template/core/src/styles/openpress/workspace-gallery.css +0 -300
  172. package/template/core/src/styles/openpress.css +0 -15
  173. package/template/core/src/vite-env.d.ts +0 -9
  174. package/template/core/tsconfig.json +0 -40
  175. package/template/core/vite.config.ts +0 -584
@@ -1,135 +0,0 @@
1
- // Layer 2 — Press Tree Expansion.
2
- //
3
- // SSR-renders the user's Press tree with a PressContext provider that
4
- // supplies resolved sources and (optionally) allocation hints. Output:
5
- // - rendered HTML (used by Layer 3 for measurement and Layer 5 for final)
6
- // - extracted frame metadata (frameKey, role, chrome, sequence position)
7
- // - per-frame MdxArea slots (chainId, sequence index within frame)
8
- //
9
- // Frames are discovered by parsing the rendered HTML for elements with the
10
- // `data-openpress-frame-key` attribute. This works because <Frame> renders
11
- // to a deterministic `<section>` with that attribute set.
12
-
13
- import React from "react";
14
- import { renderToStaticMarkup } from "react-dom/server";
15
-
16
- const FRAME_OPEN_RE = /<section\b([^>]*)\bdata-openpress-frame-key="([^"]+)"([^>]*)>/g;
17
- const ATTR_RE = (name) => new RegExp(`\\b${name}="([^"]*)"`);
18
-
19
- /**
20
- * Render the Press tree and extract frame structure.
21
- *
22
- * @param {object} opts
23
- * @param {React.ComponentType} opts.Press The user's default-exported Press component.
24
- * @param {object} opts.PressContext The PressContext from @open-press/core.
25
- * @param {Record<string, object>} opts.sources Resolved sources keyed by sourceId.
26
- * @param {object|null} opts.hints Allocation hints (or null on first pass).
27
- * @param {object|null} opts.allocation FrameAllocation map (or null for measurement).
28
- * @returns {{ html: string, frames: Array<FrameInstance> }}
29
- */
30
- export function expandPressTree({ Press: UserPress, PressContext, sources, hints = null, allocation = null, toc = null }) {
31
- const html = renderToStaticMarkup(
32
- React.createElement(
33
- PressContext.Provider,
34
- { value: { sources, allocation, hints, toc } },
35
- React.createElement(UserPress),
36
- ),
37
- );
38
-
39
- const frames = extractFrames(html);
40
- enforceUniqueFrameKeys(frames);
41
- return { html, frames };
42
- }
43
-
44
- function extractFrames(html) {
45
- const frames = [];
46
- let match;
47
- FRAME_OPEN_RE.lastIndex = 0;
48
- while ((match = FRAME_OPEN_RE.exec(html)) !== null) {
49
- const attrsBefore = match[1] ?? "";
50
- const frameKey = match[2];
51
- const attrsAfter = match[3] ?? "";
52
- const allAttrs = `${attrsBefore} ${attrsAfter}`;
53
- const role = pickAttr(allAttrs, "data-frame-role") || undefined;
54
- const chromeRaw = pickAttr(allAttrs, "data-frame-chrome");
55
- const chrome = chromeRaw === "false" ? false : true;
56
- const openIndex = match.index;
57
- const sectionHtml = sliceSection(html, openIndex);
58
- const mdxAreas = extractMdxAreas(sectionHtml);
59
- frames.push({
60
- frameKey,
61
- role,
62
- chrome,
63
- mdxAreas,
64
- htmlStart: openIndex,
65
- htmlEnd: openIndex + sectionHtml.length,
66
- html: sectionHtml,
67
- });
68
- }
69
- return frames;
70
- }
71
-
72
- const MDX_AREA_RE = /<([a-z][a-z0-9-]*)\b([^>]*)\bdata-openpress-mdx-area="true"([^>]*)>/gi;
73
-
74
- function extractMdxAreas(sectionHtml) {
75
- const areas = [];
76
- let match;
77
- MDX_AREA_RE.lastIndex = 0;
78
- while ((match = MDX_AREA_RE.exec(sectionHtml)) !== null) {
79
- const attrs = `${match[2] ?? ""} ${match[3] ?? ""}`;
80
- const chainId = pickAttr(attrs, "data-openpress-mdx-area-chain");
81
- const overflow = pickAttr(attrs, "data-openpress-mdx-area-overflow") || "extend";
82
- if (!chainId) continue;
83
- const indexInFrame = areas.filter((a) => a.chainId === chainId).length;
84
- areas.push({ chainId, overflow, indexInFrame, indexAcrossFrame: areas.length });
85
- }
86
- return areas;
87
- }
88
-
89
- function pickAttr(attrs, name) {
90
- const match = ATTR_RE(name).exec(attrs);
91
- return match ? match[1] : "";
92
- }
93
-
94
- // Find the end of a <section> opening at `start`, returning the full
95
- // `<section ...>...</section>` substring. Handles nested <section> elements
96
- // by depth-counting.
97
- function sliceSection(html, start) {
98
- const sectionOpen = /<section\b[^>]*>/g;
99
- const sectionClose = /<\/section\s*>/g;
100
- sectionOpen.lastIndex = start + 1;
101
- sectionClose.lastIndex = start + 1;
102
- let depth = 1;
103
- while (depth > 0) {
104
- const nextOpen = sectionOpen.exec(html);
105
- const nextClose = sectionClose.exec(html);
106
- if (!nextClose) {
107
- throw new Error(`Unterminated <section> in Press tree HTML near offset ${start}`);
108
- }
109
- if (nextOpen && nextOpen.index < nextClose.index) {
110
- depth += 1;
111
- sectionClose.lastIndex = nextOpen.index + 1;
112
- continue;
113
- }
114
- depth -= 1;
115
- if (depth === 0) {
116
- return html.slice(start, nextClose.index + nextClose[0].length);
117
- }
118
- sectionOpen.lastIndex = nextClose.index + 1;
119
- }
120
- throw new Error(`Section depth balance bug at offset ${start}`);
121
- }
122
-
123
- function enforceUniqueFrameKeys(frames) {
124
- const seen = new Map();
125
- for (const frame of frames) {
126
- if (seen.has(frame.frameKey)) {
127
- const prior = seen.get(frame.frameKey);
128
- throw new Error(
129
- `Duplicate frameKey "${frame.frameKey}" found in Press tree. ` +
130
- `First seen with role "${prior.role ?? "?"}", second with role "${frame.role ?? "?"}".`,
131
- );
132
- }
133
- seen.set(frame.frameKey, frame);
134
- }
135
- }
@@ -1,172 +0,0 @@
1
- // Walks the user's default-exported component tree to extract
2
- // <Workspace> and <Press> metadata declared as JSX props.
3
- //
4
- // The 1.0 contract says <Press> carries every per-document setting on
5
- // its props (title, page, sources, slug, theme, componentsDir) and is
6
- // always nested inside <Workspace>. This helper invokes the user's
7
- // component once at load time to inspect those props before the engine
8
- // runs its render pipeline.
9
- //
10
- // Safe to call because Workspace, Press, and (typically) the user's
11
- // default export are inert function components that just return JSX —
12
- // they don't use hooks at the entry boundary.
13
-
14
- import React from "react";
15
-
16
- /**
17
- * Inspect the user's default export and extract every <Press> child
18
- * of the (optional) <Workspace> wrapper. The export pipeline iterates
19
- * the returned `presses` array — single-Press workspaces simply have
20
- * length 1, multi-Press have length N. There is no separate code path
21
- * for the single-Press case.
22
- *
23
- * @param {object} opts
24
- * @param {Function} opts.UserComponent The default export of press/index.tsx.
25
- * @param {symbol} opts.PRESS_MARKER Marker identifying Press components.
26
- * @param {symbol} opts.WORKSPACE_MARKER Marker identifying Workspace components.
27
- * @returns {{
28
- * workspaceProps: Record<string, unknown>,
29
- * presses: Array<{
30
- * element: object, // ReactElement
31
- * props: Record<string, unknown>, // Press JSX props (no children)
32
- * metadata: {
33
- * title?: string,
34
- * page?: unknown,
35
- * slug?: string,
36
- * theme?: string,
37
- * componentsDir?: string,
38
- * captionNumbering?: unknown,
39
- * },
40
- * sources: Record<string, unknown> | null, // mdxSource() descriptors keyed by id
41
- * children: unknown, // raw children for re-rendering
42
- * index: number, // position in the Workspace
43
- * }>,
44
- * wrappedInWorkspace: boolean,
45
- * }}
46
- */
47
- export function inspectPressTree({ UserComponent, PRESS_MARKER, WORKSPACE_MARKER }) {
48
- if (typeof UserComponent !== "function") {
49
- return emptyResult();
50
- }
51
-
52
- let root;
53
- try {
54
- root = UserComponent({});
55
- } catch (err) {
56
- // The user's default export threw before returning JSX. This is rare
57
- // (function components at the entry boundary don't normally use hooks
58
- // that could fail), but we treat it as "no Press metadata declared"
59
- // and let the render pipeline surface the real error later with
60
- // full React error context.
61
- return emptyResult();
62
- }
63
-
64
- if (!isReactElement(root)) return emptyResult();
65
-
66
- const workspaceProps = isMarked(root, WORKSPACE_MARKER) ? extractProps(root) : {};
67
- const wrappedInWorkspace = isMarked(root, WORKSPACE_MARKER);
68
-
69
- // Find every <Press> element in the tree (Workspace child, or root itself).
70
- const pressElements = collectPressElements(root, PRESS_MARKER);
71
-
72
- const presses = pressElements.map((element, index) => {
73
- const props = extractProps(element);
74
- return {
75
- element,
76
- props,
77
- metadata: pickPressMetadata(props),
78
- sources: extractSources(props),
79
- children: element.props?.children ?? null,
80
- index,
81
- };
82
- });
83
-
84
- return {
85
- workspaceProps,
86
- presses,
87
- wrappedInWorkspace,
88
- };
89
- }
90
-
91
- function emptyResult() {
92
- return {
93
- workspaceProps: {},
94
- presses: [],
95
- wrappedInWorkspace: false,
96
- };
97
- }
98
-
99
- function isReactElement(value) {
100
- return value && typeof value === "object" && "type" in value && "props" in value;
101
- }
102
-
103
- function isMarked(element, marker) {
104
- if (!isReactElement(element)) return false;
105
- const type = element.type;
106
- if (!type) return false;
107
- // Components are tagged via `Component.openpressMarker = MARKER`.
108
- return type.openpressMarker === marker;
109
- }
110
-
111
- function extractProps(element) {
112
- if (!isReactElement(element) || !element.props) return {};
113
- // Drop children — props are non-tree metadata only.
114
- const { children, ...rest } = element.props;
115
- return rest;
116
- }
117
-
118
- function collectPressElements(root, PRESS_MARKER) {
119
- const found = [];
120
- walk(root);
121
- return found;
122
-
123
- function walk(node) {
124
- if (!isReactElement(node)) {
125
- // Could be array / fragment / string / number — flatten and recurse.
126
- if (Array.isArray(node)) {
127
- for (const child of node) walk(child);
128
- }
129
- return;
130
- }
131
- if (isMarked(node, PRESS_MARKER)) {
132
- found.push(node);
133
- // Don't descend into Press — its children are the document tree,
134
- // not more workspace structure.
135
- return;
136
- }
137
- // Recurse into children + Fragment-like wrappers.
138
- const children = node.props?.children;
139
- if (children == null) return;
140
- React.Children.forEach(children, walk);
141
- }
142
- }
143
-
144
- function pickPressMetadata(pressProps) {
145
- const out = {};
146
- if (typeof pressProps.title === "string") out.title = pressProps.title;
147
- if (pressProps.page !== undefined) out.page = pressProps.page;
148
- if (typeof pressProps.slug === "string") out.slug = pressProps.slug;
149
- if (typeof pressProps.theme === "string") out.theme = pressProps.theme;
150
- if (typeof pressProps.componentsDir === "string") out.componentsDir = pressProps.componentsDir;
151
- if (pressProps.captionNumbering !== undefined) out.captionNumbering = pressProps.captionNumbering;
152
- return out;
153
- }
154
-
155
- // Convert the v1.0 <Press sources={[ mdxSource({ id, ... }), ... ]}> array
156
- // into the engine's expected sources record { [id]: descriptor }. Returns
157
- // null if no sources prop was declared (engine falls back to the named
158
- // `export const sources` from the entry module — the v0.x shape).
159
- function extractSources(pressProps) {
160
- if (!Array.isArray(pressProps.sources)) return null;
161
- const out = {};
162
- for (const entry of pressProps.sources) {
163
- if (!entry || typeof entry !== "object") continue;
164
- const id = typeof entry.id === "string" ? entry.id : null;
165
- if (!id) continue;
166
- // Strip the id field — the engine's descriptor shape doesn't carry it
167
- // (id was the record key in v0.x).
168
- const { id: _omit, ...descriptor } = entry;
169
- out[id] = descriptor;
170
- }
171
- return out;
172
- }
@@ -1,10 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
-
3
- export function handleProjectAssetRequest(
4
- req: IncomingMessage,
5
- res: ServerResponse,
6
- options?: {
7
- root?: string;
8
- timestamp?: string;
9
- },
10
- ): Promise<void>;
@@ -1,361 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { loadConfig } from "../runtime/config.mjs";
4
- import { collectSourceTextFiles } from "../runtime/source-text-tools.mjs";
5
- import { insertCommentMarker } from "./comment-marker.mjs";
6
- import { readJsonBody, writeJson } from "./http-json.mjs";
7
-
8
- export async function handleProjectAssetRequest(req, res, {
9
- root = ".",
10
- timestamp = undefined,
11
- } = {}) {
12
- if (req.method !== "POST") {
13
- writeJson(res, 405, { ok: false, message: "OpenPress project asset endpoint requires POST." });
14
- return;
15
- }
16
-
17
- try {
18
- const body = await readJsonBody(req, { bodyLabel: "Project asset request" });
19
- const config = await loadConfig(root);
20
- const action = stringValue(body?.action);
21
- const kind = stringValue(body?.kind);
22
- const name = stringValue(body?.name);
23
-
24
- if (kind !== "media" && kind !== "component") {
25
- throw new Error("Project asset kind must be `media` or `component`.");
26
- }
27
- if (!name) throw new Error("Project asset action requires a name.");
28
-
29
- if (action === "rename") {
30
- const result = await renameProjectAsset({
31
- config,
32
- kind,
33
- name,
34
- nextName: body?.nextName,
35
- });
36
- writeJson(res, 200, { ok: true, ...result });
37
- return;
38
- }
39
-
40
- if (action === "delete") {
41
- const result = await deleteProjectAsset({ config, kind, name });
42
- const status = result.needsReferenceCleanup ? 409 : 200;
43
- writeJson(res, status, { ok: !result.needsReferenceCleanup, ...result });
44
- return;
45
- }
46
-
47
- if (action === "comment") {
48
- const result = await createProjectAssetComment({
49
- config,
50
- kind,
51
- name,
52
- note: body?.note,
53
- commentTarget: body?.commentTarget,
54
- currentSource: body?.currentSource,
55
- objectEntity: body?.objectEntity,
56
- timestamp,
57
- });
58
- writeJson(res, 200, { ok: true, ...result });
59
- return;
60
- }
61
-
62
- throw new Error("Project asset action must be `rename`, `delete`, or `comment`.");
63
- } catch (error) {
64
- writeJson(res, 400, {
65
- ok: false,
66
- message: error instanceof Error ? error.message : String(error),
67
- });
68
- }
69
- }
70
-
71
- async function renameProjectAsset({ config, kind, name, nextName }) {
72
- const normalizedCurrentName = normalizeAssetName(kind, name);
73
- const normalizedNextName = normalizeAssetName(kind, stringValue(nextName), normalizedCurrentName);
74
- if (!normalizedNextName || normalizedNextName === normalizedCurrentName) {
75
- throw new Error("Rename requires a different valid name.");
76
- }
77
-
78
- const currentPath = resolveAssetPath(config, kind, normalizedCurrentName);
79
- const nextPath = resolveAssetPath(config, kind, normalizedNextName);
80
- await assertPathExists(currentPath, `${kind} asset not found: ${normalizedCurrentName}`);
81
- if (await fileExists(nextPath)) throw new Error(`${kind} asset already exists: ${normalizedNextName}`);
82
-
83
- await fs.rename(currentPath, nextPath);
84
- const referenceResult = await replaceProjectAssetReferences({
85
- config,
86
- kind,
87
- from: normalizedCurrentName,
88
- to: normalizedNextName,
89
- });
90
-
91
- return {
92
- action: "rename",
93
- kind,
94
- name: normalizedCurrentName,
95
- nextName: normalizedNextName,
96
- referenceCount: referenceResult.referenceCount,
97
- fileCount: referenceResult.fileCount,
98
- };
99
- }
100
-
101
- async function deleteProjectAsset({ config, kind, name }) {
102
- const normalizedName = normalizeAssetName(kind, name);
103
- const references = await findProjectAssetReferences({ config, kind, name: normalizedName });
104
- if (references.length > 0) {
105
- return {
106
- action: "delete",
107
- kind,
108
- name: normalizedName,
109
- needsReferenceCleanup: true,
110
- referenceCount: references.length,
111
- references: references.slice(0, 12),
112
- message: `Cannot delete ${kind} asset while ${references.length} reference(s) still exist.`,
113
- };
114
- }
115
-
116
- const targetPath = resolveAssetPath(config, kind, normalizedName);
117
- await assertPathExists(targetPath, `${kind} asset not found: ${normalizedName}`);
118
- await fs.rm(targetPath, { recursive: true, force: true });
119
-
120
- return {
121
- action: "delete",
122
- kind,
123
- name: normalizedName,
124
- needsReferenceCleanup: false,
125
- referenceCount: 0,
126
- };
127
- }
128
-
129
- async function createProjectAssetComment({
130
- config,
131
- kind,
132
- name,
133
- note,
134
- commentTarget,
135
- currentSource,
136
- objectEntity,
137
- timestamp,
138
- }) {
139
- const normalizedName = normalizeAssetName(kind, name);
140
- const noteText = stringValue(note);
141
- if (!noteText) throw new Error("Project asset comment requires a note.");
142
-
143
- const target = await resolveCommentTarget({
144
- config,
145
- kind,
146
- name: normalizedName,
147
- commentTarget: stringValue(commentTarget),
148
- currentSource,
149
- });
150
- const objectHint = stringValue(objectEntity?.id) ? ` object=${stringValue(objectEntity.id)}` : "";
151
-
152
- const result = await insertCommentMarker({
153
- root: config.root,
154
- path: target.path,
155
- source: { line: target.line, column: 1 },
156
- note: `${assetLabel(kind, normalizedName)}:${noteText}`,
157
- hint: `openpress-project-asset kind=${kind} action=comment target=${target.reason} asset=${normalizedName}${objectHint}`,
158
- timestamp,
159
- });
160
-
161
- return {
162
- action: "comment",
163
- kind,
164
- name: normalizedName,
165
- comment: {
166
- id: result.id,
167
- timestamp: result.timestamp,
168
- path: result.path,
169
- line: result.line,
170
- },
171
- };
172
- }
173
-
174
- async function resolveCommentTarget({ config, kind, name, commentTarget, currentSource }) {
175
- if (commentTarget === "current-page") {
176
- const currentPath = stringValue(currentSource?.path);
177
- if (currentPath) {
178
- return {
179
- path: currentPath,
180
- line: normalizePositiveInteger(currentSource?.line) ?? 1,
181
- reason: "current-page",
182
- };
183
- }
184
- }
185
-
186
- const references = await findProjectAssetReferences({ config, kind, name });
187
- const preferred = references.find((reference) => {
188
- if (kind === "component") return reference.preview.includes("data-openpress-component");
189
- return reference.path.includes("/content/") || reference.path.endsWith(".mdx");
190
- }) ?? references[0];
191
- if (!preferred) {
192
- throw new Error(`No editable reference found for ${kind} asset: ${name}`);
193
- }
194
- return {
195
- path: preferred.path,
196
- line: preferred.line,
197
- reason: "asset-reference",
198
- };
199
- }
200
-
201
- async function replaceProjectAssetReferences({ config, kind, from, to }) {
202
- const replacements = replacementPairs(kind, from, to);
203
- const files = await collectSourceTextFiles(config, { scope: "all" });
204
- let referenceCount = 0;
205
- let fileCount = 0;
206
-
207
- for (const file of files) {
208
- let text = file.text;
209
- let changed = false;
210
- for (const [fromText, toText] of replacements) {
211
- if (!fromText || fromText === toText || !text.includes(fromText)) continue;
212
- const count = text.split(fromText).length - 1;
213
- text = text.split(fromText).join(toText);
214
- referenceCount += count;
215
- changed = true;
216
- }
217
- if (!changed) continue;
218
- fileCount += 1;
219
- await fs.writeFile(file.absolutePath, text, "utf8");
220
- }
221
-
222
- return { referenceCount, fileCount };
223
- }
224
-
225
- async function findProjectAssetReferences({ config, kind, name }) {
226
- const tokens = referenceTokens(kind, name);
227
- const files = await collectSourceTextFiles(config, { scope: "all" });
228
- const references = [];
229
-
230
- for (const file of files) {
231
- const lines = file.text.split(/\r?\n/);
232
- lines.forEach((line, index) => {
233
- if (!tokens.some((token) => token && line.includes(token))) return;
234
- references.push({
235
- path: file.relativePath,
236
- line: index + 1,
237
- preview: line.trim().slice(0, 180),
238
- });
239
- });
240
- }
241
-
242
- return references;
243
- }
244
-
245
- function resolveAssetPath(config, kind, name) {
246
- const root = kind === "media" ? config.paths.mediaDir : config.paths.componentsDir;
247
- const target = path.resolve(root, name);
248
- const resolvedRoot = path.resolve(root);
249
- if (!target.startsWith(`${resolvedRoot}${path.sep}`) && target !== resolvedRoot) {
250
- throw new Error(`Project asset path escapes ${kind} directory: ${name}`);
251
- }
252
- return target;
253
- }
254
-
255
- function normalizeAssetName(kind, value, currentName = "") {
256
- if (kind === "media") return sanitizeMediaFileName(value, currentName);
257
- return sanitizeComponentName(value);
258
- }
259
-
260
- function sanitizeMediaFileName(value, currentName = "") {
261
- const rawName = stringValue(value);
262
- if (!rawName) return "";
263
- const currentExt = path.extname(currentName);
264
- const suppliedExt = path.extname(rawName);
265
- const baseName = path.basename(suppliedExt ? rawName : `${rawName}${currentExt}`).trim();
266
- if (!baseName) return "";
267
- const ext = path.extname(baseName);
268
- const stem = path.basename(baseName, ext)
269
- .replace(/[\\/:*?"<>|#%{}^~[\]`]/g, "-")
270
- .replace(/\s+/g, "-")
271
- .replace(/-+/g, "-")
272
- .replace(/^-|-$/g, "");
273
- if (!stem || !ext || !isAllowedMediaFile(`${stem}${ext}`)) return "";
274
- return `${stem}${ext.toLowerCase()}`;
275
- }
276
-
277
- function sanitizeComponentName(value) {
278
- const raw = stringValue(value).replaceAll("\\", "/").split("/").pop() ?? "";
279
- const normalized = raw
280
- .trim()
281
- .replace(/\s+/g, "-")
282
- .replace(/[^a-zA-Z0-9_-]/g, "-")
283
- .replace(/-+/g, "-")
284
- .replace(/^-|-$/g, "");
285
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(normalized)) return "";
286
- return normalized;
287
- }
288
-
289
- function isAllowedMediaFile(fileName) {
290
- return /\.(png|jpe?g|gif|svg|webp)$/i.test(fileName);
291
- }
292
-
293
- function replacementPairs(kind, from, to) {
294
- if (kind === "media") {
295
- return uniquePairs([
296
- [from, to],
297
- [encodeURIComponent(from), encodeURIComponent(to)],
298
- [`@media/${from}`, `@media/${to}`],
299
- ]);
300
- }
301
- return uniquePairs([
302
- [from, to],
303
- [`@component/${from}`, `@component/${to}`],
304
- ]);
305
- }
306
-
307
- function referenceTokens(kind, name) {
308
- if (kind === "media") {
309
- return uniqueValues([
310
- name,
311
- encodeURIComponent(name),
312
- `@media/${name}`,
313
- ]);
314
- }
315
- return uniqueValues([
316
- name,
317
- `@component/${name}`,
318
- `data-openpress-component="${name}"`,
319
- `data-openpress-component='${name}'`,
320
- ]);
321
- }
322
-
323
- function uniquePairs(pairs) {
324
- const seen = new Set();
325
- return pairs.filter(([from, to]) => {
326
- const key = `${from}\0${to}`;
327
- if (seen.has(key)) return false;
328
- seen.add(key);
329
- return true;
330
- });
331
- }
332
-
333
- function uniqueValues(values) {
334
- return Array.from(new Set(values.filter(Boolean)));
335
- }
336
-
337
- function assetLabel(kind, name) {
338
- return kind === "media" ? `Media ${name}` : `Component ${name}`;
339
- }
340
-
341
- function stringValue(value) {
342
- return typeof value === "string" ? value.trim() : "";
343
- }
344
-
345
- function normalizePositiveInteger(value) {
346
- const number = Number(value);
347
- return Number.isInteger(number) && number > 0 ? number : null;
348
- }
349
-
350
- async function assertPathExists(filePath, message) {
351
- if (!(await fileExists(filePath))) throw new Error(message);
352
- }
353
-
354
- async function fileExists(filePath) {
355
- try {
356
- await fs.access(filePath);
357
- return true;
358
- } catch {
359
- return false;
360
- }
361
- }