@anydocs/core 1.0.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 (124) hide show
  1. package/dist/config/index.d.ts +2 -0
  2. package/dist/config/index.d.ts.map +1 -0
  3. package/dist/config/index.js +1 -0
  4. package/dist/config/project-config.d.ts +10 -0
  5. package/dist/config/project-config.d.ts.map +1 -0
  6. package/dist/config/project-config.js +52 -0
  7. package/dist/errors/domain-error.d.ts +12 -0
  8. package/dist/errors/domain-error.d.ts.map +1 -0
  9. package/dist/errors/domain-error.js +8 -0
  10. package/dist/errors/index.d.ts +3 -0
  11. package/dist/errors/index.d.ts.map +1 -0
  12. package/dist/errors/index.js +2 -0
  13. package/dist/errors/validation-error.d.ts +5 -0
  14. package/dist/errors/validation-error.d.ts.map +1 -0
  15. package/dist/errors/validation-error.js +7 -0
  16. package/dist/fs/api-source-repository.d.ts +16 -0
  17. package/dist/fs/api-source-repository.d.ts.map +1 -0
  18. package/dist/fs/api-source-repository.js +89 -0
  19. package/dist/fs/content-repository.d.ts +13 -0
  20. package/dist/fs/content-repository.d.ts.map +1 -0
  21. package/dist/fs/content-repository.js +171 -0
  22. package/dist/fs/docs-repository.d.ts +26 -0
  23. package/dist/fs/docs-repository.d.ts.map +1 -0
  24. package/dist/fs/docs-repository.js +270 -0
  25. package/dist/fs/index.d.ts +5 -0
  26. package/dist/fs/index.d.ts.map +1 -0
  27. package/dist/fs/index.js +4 -0
  28. package/dist/fs/project-paths.d.ts +4 -0
  29. package/dist/fs/project-paths.d.ts.map +1 -0
  30. package/dist/fs/project-paths.js +55 -0
  31. package/dist/index.d.ts +9 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +8 -0
  34. package/dist/publishing/build-artifacts.d.ts +4 -0
  35. package/dist/publishing/build-artifacts.d.ts.map +1 -0
  36. package/dist/publishing/build-artifacts.js +453 -0
  37. package/dist/publishing/build-openapi-artifacts.d.ts +3 -0
  38. package/dist/publishing/build-openapi-artifacts.d.ts.map +1 -0
  39. package/dist/publishing/build-openapi-artifacts.js +253 -0
  40. package/dist/publishing/index.d.ts +4 -0
  41. package/dist/publishing/index.d.ts.map +1 -0
  42. package/dist/publishing/index.js +3 -0
  43. package/dist/publishing/publication-filter.d.ts +22 -0
  44. package/dist/publishing/publication-filter.d.ts.map +1 -0
  45. package/dist/publishing/publication-filter.js +98 -0
  46. package/dist/schemas/api-source-schema.d.ts +3 -0
  47. package/dist/schemas/api-source-schema.d.ts.map +1 -0
  48. package/dist/schemas/api-source-schema.js +110 -0
  49. package/dist/schemas/docs-schema.d.ts +7 -0
  50. package/dist/schemas/docs-schema.d.ts.map +1 -0
  51. package/dist/schemas/docs-schema.js +212 -0
  52. package/dist/schemas/index.d.ts +4 -0
  53. package/dist/schemas/index.d.ts.map +1 -0
  54. package/dist/schemas/index.js +3 -0
  55. package/dist/schemas/project-schema.d.ts +3 -0
  56. package/dist/schemas/project-schema.d.ts.map +1 -0
  57. package/dist/schemas/project-schema.js +268 -0
  58. package/dist/services/authoring-service.d.ts +137 -0
  59. package/dist/services/authoring-service.d.ts.map +1 -0
  60. package/dist/services/authoring-service.js +583 -0
  61. package/dist/services/build-service.d.ts +35 -0
  62. package/dist/services/build-service.d.ts.map +1 -0
  63. package/dist/services/build-service.js +84 -0
  64. package/dist/services/index.d.ts +11 -0
  65. package/dist/services/index.d.ts.map +1 -0
  66. package/dist/services/index.js +10 -0
  67. package/dist/services/init-service.d.ts +15 -0
  68. package/dist/services/init-service.d.ts.map +1 -0
  69. package/dist/services/init-service.js +127 -0
  70. package/dist/services/legacy-conversion-service.d.ts +8 -0
  71. package/dist/services/legacy-conversion-service.d.ts.map +1 -0
  72. package/dist/services/legacy-conversion-service.js +601 -0
  73. package/dist/services/legacy-import-service.d.ts +10 -0
  74. package/dist/services/legacy-import-service.d.ts.map +1 -0
  75. package/dist/services/legacy-import-service.js +239 -0
  76. package/dist/services/page-template-service.d.ts +81 -0
  77. package/dist/services/page-template-service.d.ts.map +1 -0
  78. package/dist/services/page-template-service.js +342 -0
  79. package/dist/services/preview-service.d.ts +29 -0
  80. package/dist/services/preview-service.d.ts.map +1 -0
  81. package/dist/services/preview-service.js +45 -0
  82. package/dist/services/watch-service.d.ts +24 -0
  83. package/dist/services/watch-service.d.ts.map +1 -0
  84. package/dist/services/watch-service.js +216 -0
  85. package/dist/services/web-runtime-bridge.d.ts +33 -0
  86. package/dist/services/web-runtime-bridge.d.ts.map +1 -0
  87. package/dist/services/web-runtime-bridge.js +330 -0
  88. package/dist/services/workflow-compatibility-service.d.ts +3 -0
  89. package/dist/services/workflow-compatibility-service.d.ts.map +1 -0
  90. package/dist/services/workflow-compatibility-service.js +53 -0
  91. package/dist/services/workflow-standard-service.d.ts +9 -0
  92. package/dist/services/workflow-standard-service.d.ts.map +1 -0
  93. package/dist/services/workflow-standard-service.js +372 -0
  94. package/dist/types/api-source.d.ts +34 -0
  95. package/dist/types/api-source.d.ts.map +1 -0
  96. package/dist/types/api-source.js +8 -0
  97. package/dist/types/docs.d.ts +65 -0
  98. package/dist/types/docs.d.ts.map +1 -0
  99. package/dist/types/docs.js +8 -0
  100. package/dist/types/index.d.ts +6 -0
  101. package/dist/types/index.d.ts.map +1 -0
  102. package/dist/types/index.js +5 -0
  103. package/dist/types/legacy-import.d.ts +72 -0
  104. package/dist/types/legacy-import.d.ts.map +1 -0
  105. package/dist/types/legacy-import.js +1 -0
  106. package/dist/types/project.d.ts +85 -0
  107. package/dist/types/project.d.ts.map +1 -0
  108. package/dist/types/project.js +5 -0
  109. package/dist/types/workflow-standard.d.ts +51 -0
  110. package/dist/types/workflow-standard.d.ts.map +1 -0
  111. package/dist/types/workflow-standard.js +1 -0
  112. package/dist/utils/index.d.ts +4 -0
  113. package/dist/utils/index.d.ts.map +1 -0
  114. package/dist/utils/index.js +3 -0
  115. package/dist/utils/slug.d.ts +3 -0
  116. package/dist/utils/slug.d.ts.map +1 -0
  117. package/dist/utils/slug.js +21 -0
  118. package/dist/utils/yoopta-content.d.ts +13 -0
  119. package/dist/utils/yoopta-content.d.ts.map +1 -0
  120. package/dist/utils/yoopta-content.js +73 -0
  121. package/dist/utils/yoopta-render.d.ts +7 -0
  122. package/dist/utils/yoopta-render.d.ts.map +1 -0
  123. package/dist/utils/yoopta-render.js +155 -0
  124. package/package.json +30 -0
@@ -0,0 +1,583 @@
1
+ import path from 'node:path';
2
+ import { ValidationError } from "../errors/validation-error.js";
3
+ import { isPageApprovedForPublication } from "../publishing/publication-filter.js";
4
+ import { validatePageDoc } from "../schemas/docs-schema.js";
5
+ import { createDocsRepository, deletePage as deletePageFromRepository, findPageBySlug, listPages, loadPage, loadProjectContract, loadNavigation, updateProjectConfig, savePage, saveNavigation, } from "../fs/index.js";
6
+ import { DOCS_YOOPTA_ALLOWED_MARKS, DOCS_YOOPTA_ALLOWED_TYPES, assertValidYooptaContentValue, normalizeSlug, renderYooptaContent, } from "../utils/index.js";
7
+ function pageFilePath(projectRoot, lang, pageId) {
8
+ return path.join(projectRoot, 'pages', lang, `${pageId}.json`);
9
+ }
10
+ function currentTimestamp() {
11
+ return new Date().toISOString();
12
+ }
13
+ function assertValidAuthoringContent(content, pageId) {
14
+ try {
15
+ assertValidYooptaContentValue(content);
16
+ }
17
+ catch (error) {
18
+ throw new ValidationError('Page content must use the supported Yoopta block structure.', {
19
+ entity: 'page-doc',
20
+ rule: 'page-content-must-be-valid-yoopta',
21
+ remediation: 'Provide content as a Yoopta block map using supported block types, block ids, block value arrays, and numeric meta.order/meta.depth fields.',
22
+ metadata: {
23
+ pageId: pageId ?? null,
24
+ cause: error instanceof Error ? error.message : String(error),
25
+ allowedBlockTypes: [...DOCS_YOOPTA_ALLOWED_TYPES],
26
+ allowedMarks: [...DOCS_YOOPTA_ALLOWED_MARKS],
27
+ },
28
+ });
29
+ }
30
+ }
31
+ function ensureUniqueBatchPageIds(pageIds, rule) {
32
+ const seen = new Set();
33
+ for (const pageId of pageIds) {
34
+ if (!seen.has(pageId)) {
35
+ seen.add(pageId);
36
+ continue;
37
+ }
38
+ throw new ValidationError(`Duplicate page id "${pageId}" in batch operation.`, {
39
+ entity: 'page-doc',
40
+ rule,
41
+ remediation: 'Provide each page id at most once in a single batch request.',
42
+ metadata: { pageId },
43
+ });
44
+ }
45
+ }
46
+ function collectRequiredTopLevelGroupIds(contract) {
47
+ return (contract.config.site.navigation?.topNav ?? [])
48
+ .filter((item) => item.type === 'nav-group')
49
+ .map((item) => item.groupId);
50
+ }
51
+ function navigationFilePath(projectRoot, lang) {
52
+ return path.join(projectRoot, 'navigation', `${lang}.json`);
53
+ }
54
+ function createNavigationPathError(rule, remediation, metadata) {
55
+ return new ValidationError(`Navigation path validation failed for rule "${rule}".`, {
56
+ entity: 'navigation-doc',
57
+ rule,
58
+ remediation,
59
+ metadata,
60
+ });
61
+ }
62
+ function parseNavigationItemPath(pathValue, key) {
63
+ if (pathValue == null || pathValue.trim().length === 0) {
64
+ return [];
65
+ }
66
+ const segments = pathValue.split('/');
67
+ if (segments.some((segment) => !/^\d+$/.test(segment))) {
68
+ throw createNavigationPathError('navigation-item-path-format', `Provide "${key}" as slash-separated zero-based indexes, for example "0/1/2".`, { key, path: pathValue });
69
+ }
70
+ return segments.map((segment) => Number.parseInt(segment, 10));
71
+ }
72
+ function assertValidInsertIndex(index, length, key) {
73
+ if (!Number.isInteger(index) || index < 0 || index > length) {
74
+ throw createNavigationPathError('navigation-insert-index-range', `Provide "${key}" as an integer between 0 and ${length}.`, { key, index, maxInclusive: length });
75
+ }
76
+ }
77
+ function cloneNavItems(items) {
78
+ return items.map((item) => {
79
+ if (item.type === 'section' || item.type === 'folder') {
80
+ return {
81
+ ...item,
82
+ children: cloneNavItems(item.children),
83
+ };
84
+ }
85
+ return { ...item };
86
+ });
87
+ }
88
+ function updateNavItemsAtParent(items, parentPath, update, originalPath = '') {
89
+ if (parentPath.length === 0) {
90
+ return update(items);
91
+ }
92
+ const [index, ...rest] = parentPath;
93
+ const target = items[index];
94
+ const pathLabel = originalPath ? `${originalPath}/${index}` : String(index);
95
+ if (!target) {
96
+ throw createNavigationPathError('navigation-item-path-must-exist', 'Use an existing navigation item path.', { path: pathLabel });
97
+ }
98
+ if (target.type !== 'section' && target.type !== 'folder') {
99
+ throw createNavigationPathError('navigation-parent-path-must-target-group', 'Use a section or folder path as the navigation parent target.', { path: pathLabel, itemType: target.type });
100
+ }
101
+ const nextItems = [...items];
102
+ nextItems[index] = {
103
+ ...target,
104
+ children: updateNavItemsAtParent(target.children, rest, update, pathLabel),
105
+ };
106
+ return nextItems;
107
+ }
108
+ function removeNavItemAtPath(items, itemPath, originalPath = '') {
109
+ if (itemPath.length === 0) {
110
+ throw createNavigationPathError('navigation-item-path-required', 'Provide a non-empty navigation item path for delete or move operations.', { path: originalPath });
111
+ }
112
+ const [index, ...rest] = itemPath;
113
+ const pathLabel = originalPath ? `${originalPath}/${index}` : String(index);
114
+ const target = items[index];
115
+ if (!target) {
116
+ throw createNavigationPathError('navigation-item-path-must-exist', 'Use an existing navigation item path.', { path: pathLabel });
117
+ }
118
+ if (rest.length === 0) {
119
+ return {
120
+ items: items.filter((_, itemIndex) => itemIndex !== index),
121
+ removedItem: target,
122
+ parentPath: itemPath.slice(0, -1),
123
+ removedIndex: index,
124
+ };
125
+ }
126
+ if (target.type !== 'section' && target.type !== 'folder') {
127
+ throw createNavigationPathError('navigation-item-path-must-target-group-for-descendants', 'Only section and folder items can contain nested navigation children.', { path: pathLabel, itemType: target.type });
128
+ }
129
+ const nested = removeNavItemAtPath(target.children, rest, pathLabel);
130
+ const nextItems = [...items];
131
+ nextItems[index] = {
132
+ ...target,
133
+ children: nested.items,
134
+ };
135
+ return {
136
+ items: nextItems,
137
+ removedItem: nested.removedItem,
138
+ parentPath: [index, ...nested.parentPath],
139
+ removedIndex: nested.removedIndex,
140
+ };
141
+ }
142
+ function insertNavItemAtParent(items, parentPath, item, index) {
143
+ return updateNavItemsAtParent(items, parentPath, (children) => {
144
+ const nextChildren = [...children];
145
+ const insertIndex = index ?? nextChildren.length;
146
+ assertValidInsertIndex(insertIndex, nextChildren.length, 'index');
147
+ nextChildren.splice(insertIndex, 0, item);
148
+ return nextChildren;
149
+ });
150
+ }
151
+ function isSamePath(left, right) {
152
+ return left.length === right.length && left.every((segment, index) => segment === right[index]);
153
+ }
154
+ function isDescendantPath(pathValue, ancestor) {
155
+ return ancestor.length < pathValue.length && ancestor.every((segment, index) => segment === pathValue[index]);
156
+ }
157
+ async function loadAuthoringContext(projectRoot, lang) {
158
+ const contractResult = await loadProjectContract(projectRoot);
159
+ if (!contractResult.ok) {
160
+ throw contractResult.error;
161
+ }
162
+ const contract = contractResult.value;
163
+ if (!contract.config.languages.includes(lang)) {
164
+ throw new ValidationError(`Language "${lang}" is not enabled for the project.`, {
165
+ entity: 'page-doc',
166
+ rule: 'page-language-must-be-enabled-for-project',
167
+ remediation: 'Use one of the enabled languages from anydocs.config.json before mutating pages.',
168
+ metadata: {
169
+ lang,
170
+ enabledLanguages: contract.config.languages,
171
+ projectRoot: contract.paths.projectRoot,
172
+ },
173
+ });
174
+ }
175
+ return {
176
+ contract,
177
+ repository: createDocsRepository(contract.paths.projectRoot),
178
+ };
179
+ }
180
+ async function assertSlugAvailable(projectRoot, lang, pageId, slug, findExisting) {
181
+ const existing = await findExisting();
182
+ if (!existing || existing.id === pageId) {
183
+ return;
184
+ }
185
+ throw new ValidationError(`Duplicate slug "${slug}" detected.`, {
186
+ entity: 'page-doc',
187
+ rule: 'page-slug-unique-per-language',
188
+ remediation: 'Choose a unique slug for the page within the same language.',
189
+ metadata: {
190
+ pageId,
191
+ duplicatePageId: existing.id,
192
+ slug,
193
+ lang,
194
+ projectRoot,
195
+ },
196
+ });
197
+ }
198
+ function validateAndNormalizePageCandidate(page, lang) {
199
+ const validatedPage = validatePageDoc(page);
200
+ if (validatedPage.lang !== lang) {
201
+ throw new ValidationError(`Page language "${validatedPage.lang}" does not match requested language "${lang}".`, {
202
+ entity: 'page-doc',
203
+ rule: 'page-language-matches-target-language',
204
+ remediation: 'Save the page under the same language as its page.lang field.',
205
+ metadata: { pageId: validatedPage.id, pageLang: validatedPage.lang, targetLang: lang },
206
+ });
207
+ }
208
+ const slug = normalizeSlug(validatedPage.slug);
209
+ if (!slug) {
210
+ throw new ValidationError('Page slug is required.', {
211
+ entity: 'page-doc',
212
+ rule: 'page-slug-required',
213
+ remediation: 'Provide a non-empty slug before saving the page.',
214
+ metadata: { pageId: validatedPage.id },
215
+ });
216
+ }
217
+ const normalizedPage = {
218
+ ...validatedPage,
219
+ slug,
220
+ };
221
+ if (normalizedPage.status === 'published' && !isPageApprovedForPublication(normalizedPage)) {
222
+ throw new ValidationError('Page review must be explicitly approved before publication.', {
223
+ entity: 'page-doc',
224
+ rule: 'page-review-must-be-approved-before-publication',
225
+ remediation: 'Use the explicit approve-for-publication action on reviewed imported or AI-generated content before setting status to published.',
226
+ metadata: {
227
+ pageId: normalizedPage.id,
228
+ lang,
229
+ reviewRequired: normalizedPage.review?.required ?? false,
230
+ approvedAt: normalizedPage.review?.approvedAt ?? null,
231
+ },
232
+ });
233
+ }
234
+ return normalizedPage;
235
+ }
236
+ function resolveNextRender(existingPage, patch, regenerateRender) {
237
+ if (patch.render) {
238
+ return patch.render;
239
+ }
240
+ if (!regenerateRender) {
241
+ return existingPage.render;
242
+ }
243
+ const nextContent = 'content' in patch ? patch.content : existingPage.content;
244
+ return renderYooptaContent(nextContent);
245
+ }
246
+ function assertSlugAvailableInPageMap(pagesById, candidate, projectRoot, lang) {
247
+ const duplicate = [...pagesById.values()].find((page) => page.id !== candidate.id && page.slug === candidate.slug);
248
+ if (!duplicate) {
249
+ return;
250
+ }
251
+ throw new ValidationError(`Duplicate slug "${candidate.slug}" detected.`, {
252
+ entity: 'page-doc',
253
+ rule: 'page-slug-unique-per-language',
254
+ remediation: 'Choose a unique slug for the page within the same language.',
255
+ metadata: {
256
+ pageId: candidate.id,
257
+ duplicatePageId: duplicate.id,
258
+ slug: candidate.slug,
259
+ lang,
260
+ projectRoot,
261
+ },
262
+ });
263
+ }
264
+ async function persistBatchPages(repository, projectRoot, lang, pages) {
265
+ const persistedPages = [];
266
+ const files = [];
267
+ for (const page of pages) {
268
+ const persisted = await savePage(repository, lang, page);
269
+ persistedPages.push(persisted);
270
+ files.push(pageFilePath(projectRoot, lang, persisted.id));
271
+ }
272
+ return {
273
+ count: persistedPages.length,
274
+ files,
275
+ pages: persistedPages,
276
+ };
277
+ }
278
+ export async function createPage(input) {
279
+ assertValidAuthoringContent(input.page.content ?? {}, input.page.id);
280
+ const { contract, repository } = await loadAuthoringContext(input.projectRoot, input.lang);
281
+ const existingPage = await loadPage(repository, input.lang, input.page.id);
282
+ if (existingPage) {
283
+ throw new ValidationError(`Page "${input.page.id}" already exists.`, {
284
+ entity: 'page-doc',
285
+ rule: 'page-create-target-must-not-exist',
286
+ remediation: 'Use a new page id for page_create, or use page_update to modify an existing page.',
287
+ metadata: {
288
+ lang: input.lang,
289
+ pageId: input.page.id,
290
+ projectRoot: contract.paths.projectRoot,
291
+ },
292
+ });
293
+ }
294
+ await assertSlugAvailable(contract.paths.projectRoot, input.lang, input.page.id, input.page.slug, () => findPageBySlug(repository, input.lang, input.page.slug));
295
+ const page = await savePage(repository, input.lang, {
296
+ id: input.page.id,
297
+ lang: input.lang,
298
+ slug: input.page.slug,
299
+ title: input.page.title,
300
+ description: input.page.description,
301
+ tags: input.page.tags,
302
+ status: input.page.status ?? 'draft',
303
+ content: (input.page.content ?? {}),
304
+ render: input.page.render,
305
+ review: input.page.review,
306
+ updatedAt: currentTimestamp(),
307
+ });
308
+ return {
309
+ filePath: pageFilePath(contract.paths.projectRoot, input.lang, page.id),
310
+ page,
311
+ };
312
+ }
313
+ export async function createPagesBatch(input) {
314
+ const { contract, repository } = await loadAuthoringContext(input.projectRoot, input.lang);
315
+ ensureUniqueBatchPageIds(input.pages.map((page) => page.id), 'page-batch-create-page-ids-must-be-unique');
316
+ const existingPages = await listPages(repository, input.lang);
317
+ const pagesById = new Map(existingPages.map((page) => [page.id, page]));
318
+ const timestamp = currentTimestamp();
319
+ const plannedPages = [];
320
+ for (const pageInput of input.pages) {
321
+ assertValidAuthoringContent(pageInput.content ?? {}, pageInput.id);
322
+ if (pagesById.has(pageInput.id)) {
323
+ throw new ValidationError(`Page "${pageInput.id}" already exists.`, {
324
+ entity: 'page-doc',
325
+ rule: 'page-create-target-must-not-exist',
326
+ remediation: 'Use a new page id for page_batch_create, or use page_batch_update to modify existing pages.',
327
+ metadata: {
328
+ lang: input.lang,
329
+ pageId: pageInput.id,
330
+ projectRoot: contract.paths.projectRoot,
331
+ },
332
+ });
333
+ }
334
+ const candidate = validateAndNormalizePageCandidate({
335
+ id: pageInput.id,
336
+ lang: input.lang,
337
+ slug: pageInput.slug,
338
+ title: pageInput.title,
339
+ ...(pageInput.description ? { description: pageInput.description } : {}),
340
+ ...(pageInput.tags ? { tags: pageInput.tags } : {}),
341
+ status: pageInput.status ?? 'draft',
342
+ content: (pageInput.content ?? {}),
343
+ ...(pageInput.render ? { render: pageInput.render } : {}),
344
+ ...(pageInput.review ? { review: pageInput.review } : {}),
345
+ updatedAt: timestamp,
346
+ }, input.lang);
347
+ assertSlugAvailableInPageMap(pagesById, candidate, contract.paths.projectRoot, input.lang);
348
+ pagesById.set(candidate.id, candidate);
349
+ plannedPages.push(candidate);
350
+ }
351
+ return persistBatchPages(repository, contract.paths.projectRoot, input.lang, plannedPages);
352
+ }
353
+ export async function updatePage(input) {
354
+ if ('content' in input.patch) {
355
+ assertValidAuthoringContent(input.patch.content, input.pageId);
356
+ }
357
+ const { contract, repository } = await loadAuthoringContext(input.projectRoot, input.lang);
358
+ const existingPage = await loadPage(repository, input.lang, input.pageId);
359
+ if (!existingPage) {
360
+ throw new ValidationError(`Page "${input.pageId}" not found.`, {
361
+ entity: 'page-doc',
362
+ rule: 'page-must-exist',
363
+ remediation: 'Use page_list or page_find to inspect available pages before retrying.',
364
+ metadata: {
365
+ lang: input.lang,
366
+ pageId: input.pageId,
367
+ projectRoot: contract.paths.projectRoot,
368
+ },
369
+ });
370
+ }
371
+ const nextSlug = input.patch.slug ?? existingPage.slug;
372
+ await assertSlugAvailable(contract.paths.projectRoot, input.lang, existingPage.id, nextSlug, () => findPageBySlug(repository, input.lang, nextSlug));
373
+ const page = await savePage(repository, input.lang, {
374
+ ...existingPage,
375
+ ...input.patch,
376
+ lang: input.lang,
377
+ id: existingPage.id,
378
+ status: existingPage.status,
379
+ render: resolveNextRender(existingPage, input.patch, input.regenerateRender),
380
+ updatedAt: currentTimestamp(),
381
+ });
382
+ return {
383
+ filePath: pageFilePath(contract.paths.projectRoot, input.lang, page.id),
384
+ page,
385
+ };
386
+ }
387
+ export async function updatePagesBatch(input) {
388
+ const { contract, repository } = await loadAuthoringContext(input.projectRoot, input.lang);
389
+ ensureUniqueBatchPageIds(input.updates.map((entry) => entry.pageId), 'page-batch-update-page-ids-must-be-unique');
390
+ const existingPages = await listPages(repository, input.lang);
391
+ const pagesById = new Map(existingPages.map((page) => [page.id, page]));
392
+ const timestamp = currentTimestamp();
393
+ const plannedPages = [];
394
+ for (const entry of input.updates) {
395
+ if ('content' in entry.patch) {
396
+ assertValidAuthoringContent(entry.patch.content, entry.pageId);
397
+ }
398
+ const existingPage = pagesById.get(entry.pageId);
399
+ if (!existingPage) {
400
+ throw new ValidationError(`Page "${entry.pageId}" not found.`, {
401
+ entity: 'page-doc',
402
+ rule: 'page-must-exist',
403
+ remediation: 'Use page_list or page_find to inspect available pages before retrying.',
404
+ metadata: {
405
+ lang: input.lang,
406
+ pageId: entry.pageId,
407
+ projectRoot: contract.paths.projectRoot,
408
+ },
409
+ });
410
+ }
411
+ const candidate = validateAndNormalizePageCandidate({
412
+ ...existingPage,
413
+ ...entry.patch,
414
+ lang: input.lang,
415
+ id: existingPage.id,
416
+ status: existingPage.status,
417
+ render: resolveNextRender(existingPage, entry.patch, entry.regenerateRender),
418
+ updatedAt: timestamp,
419
+ }, input.lang);
420
+ assertSlugAvailableInPageMap(pagesById, candidate, contract.paths.projectRoot, input.lang);
421
+ pagesById.set(candidate.id, candidate);
422
+ plannedPages.push(candidate);
423
+ }
424
+ return persistBatchPages(repository, contract.paths.projectRoot, input.lang, plannedPages);
425
+ }
426
+ export async function setPageStatus(input) {
427
+ const { contract, repository } = await loadAuthoringContext(input.projectRoot, input.lang);
428
+ const existingPage = await loadPage(repository, input.lang, input.pageId);
429
+ if (!existingPage) {
430
+ throw new ValidationError(`Page "${input.pageId}" not found.`, {
431
+ entity: 'page-doc',
432
+ rule: 'page-must-exist',
433
+ remediation: 'Use page_list or page_find to inspect available pages before retrying.',
434
+ metadata: {
435
+ lang: input.lang,
436
+ pageId: input.pageId,
437
+ projectRoot: contract.paths.projectRoot,
438
+ },
439
+ });
440
+ }
441
+ const page = await savePage(repository, input.lang, {
442
+ ...existingPage,
443
+ status: input.status,
444
+ updatedAt: currentTimestamp(),
445
+ });
446
+ return {
447
+ filePath: pageFilePath(contract.paths.projectRoot, input.lang, page.id),
448
+ page,
449
+ };
450
+ }
451
+ export async function setPageStatusesBatch(input) {
452
+ const { contract, repository } = await loadAuthoringContext(input.projectRoot, input.lang);
453
+ ensureUniqueBatchPageIds(input.updates.map((entry) => entry.pageId), 'page-batch-status-page-ids-must-be-unique');
454
+ const existingPages = await listPages(repository, input.lang);
455
+ const pagesById = new Map(existingPages.map((page) => [page.id, page]));
456
+ const timestamp = currentTimestamp();
457
+ const plannedPages = [];
458
+ for (const entry of input.updates) {
459
+ const existingPage = pagesById.get(entry.pageId);
460
+ if (!existingPage) {
461
+ throw new ValidationError(`Page "${entry.pageId}" not found.`, {
462
+ entity: 'page-doc',
463
+ rule: 'page-must-exist',
464
+ remediation: 'Use page_list or page_find to inspect available pages before retrying.',
465
+ metadata: {
466
+ lang: input.lang,
467
+ pageId: entry.pageId,
468
+ projectRoot: contract.paths.projectRoot,
469
+ },
470
+ });
471
+ }
472
+ const candidate = validateAndNormalizePageCandidate({
473
+ ...existingPage,
474
+ status: entry.status,
475
+ updatedAt: timestamp,
476
+ }, input.lang);
477
+ pagesById.set(candidate.id, candidate);
478
+ plannedPages.push(candidate);
479
+ }
480
+ return persistBatchPages(repository, contract.paths.projectRoot, input.lang, plannedPages);
481
+ }
482
+ export async function deleteAuthoredPage(input) {
483
+ const { contract, repository } = await loadAuthoringContext(input.projectRoot, input.lang);
484
+ const result = await deletePageFromRepository(repository, input.lang, input.pageId);
485
+ return {
486
+ filePath: pageFilePath(contract.paths.projectRoot, input.lang, input.pageId),
487
+ ...result,
488
+ };
489
+ }
490
+ export async function setNavigation(input) {
491
+ const { contract, repository } = await loadAuthoringContext(input.projectRoot, input.lang);
492
+ const pages = await listPages(repository, input.lang);
493
+ const navigation = await saveNavigation(repository, input.lang, input.navigation, {
494
+ existingPageIds: pages.map((page) => page.id),
495
+ requiredTopLevelGroupIds: collectRequiredTopLevelGroupIds(contract),
496
+ });
497
+ return {
498
+ filePath: navigationFilePath(contract.paths.projectRoot, input.lang),
499
+ navigation,
500
+ };
501
+ }
502
+ export async function replaceNavigationItems(input) {
503
+ const { repository } = await loadAuthoringContext(input.projectRoot, input.lang);
504
+ const existingNavigation = await loadNavigation(repository, input.lang);
505
+ return setNavigation({
506
+ projectRoot: input.projectRoot,
507
+ lang: input.lang,
508
+ navigation: {
509
+ ...existingNavigation,
510
+ items: input.items,
511
+ },
512
+ });
513
+ }
514
+ export async function insertNavigationItem(input) {
515
+ const parentPath = parseNavigationItemPath(input.parentPath, 'parentPath');
516
+ const { repository } = await loadAuthoringContext(input.projectRoot, input.lang);
517
+ const existingNavigation = await loadNavigation(repository, input.lang);
518
+ const nextItems = insertNavItemAtParent(cloneNavItems(existingNavigation.items), parentPath, input.item, input.index);
519
+ return setNavigation({
520
+ projectRoot: input.projectRoot,
521
+ lang: input.lang,
522
+ navigation: {
523
+ ...existingNavigation,
524
+ items: nextItems,
525
+ },
526
+ });
527
+ }
528
+ export async function deleteNavigationItem(input) {
529
+ const itemPath = parseNavigationItemPath(input.itemPath, 'itemPath');
530
+ const { repository } = await loadAuthoringContext(input.projectRoot, input.lang);
531
+ const existingNavigation = await loadNavigation(repository, input.lang);
532
+ const nextItems = removeNavItemAtPath(cloneNavItems(existingNavigation.items), itemPath).items;
533
+ return setNavigation({
534
+ projectRoot: input.projectRoot,
535
+ lang: input.lang,
536
+ navigation: {
537
+ ...existingNavigation,
538
+ items: nextItems,
539
+ },
540
+ });
541
+ }
542
+ export async function moveNavigationItem(input) {
543
+ const itemPath = parseNavigationItemPath(input.itemPath, 'itemPath');
544
+ const parentPath = parseNavigationItemPath(input.parentPath, 'parentPath');
545
+ if (isDescendantPath(parentPath, itemPath) || isSamePath(parentPath, itemPath)) {
546
+ throw createNavigationPathError('navigation-move-target-must-not-be-source-descendant', 'Move a navigation item to the root or to a different section/folder outside the moved subtree.', { itemPath: input.itemPath, parentPath: input.parentPath ?? '' });
547
+ }
548
+ const { repository } = await loadAuthoringContext(input.projectRoot, input.lang);
549
+ const existingNavigation = await loadNavigation(repository, input.lang);
550
+ const removed = removeNavItemAtPath(cloneNavItems(existingNavigation.items), itemPath);
551
+ let nextIndex = input.index;
552
+ if (nextIndex != null &&
553
+ isSamePath(removed.parentPath, parentPath) &&
554
+ removed.removedIndex < nextIndex) {
555
+ nextIndex -= 1;
556
+ }
557
+ const nextItems = insertNavItemAtParent(removed.items, parentPath, removed.removedItem, nextIndex);
558
+ return setNavigation({
559
+ projectRoot: input.projectRoot,
560
+ lang: input.lang,
561
+ navigation: {
562
+ ...existingNavigation,
563
+ items: nextItems,
564
+ },
565
+ });
566
+ }
567
+ export async function setProjectLanguages(input) {
568
+ const result = await updateProjectConfig(input.projectRoot, {
569
+ languages: input.languages,
570
+ ...(input.defaultLanguage ? { defaultLanguage: input.defaultLanguage } : {}),
571
+ });
572
+ if (!result.ok) {
573
+ throw result.error;
574
+ }
575
+ const contractResult = await loadProjectContract(input.projectRoot);
576
+ if (!contractResult.ok) {
577
+ throw contractResult.error;
578
+ }
579
+ return {
580
+ filePath: contractResult.value.paths.configFile,
581
+ config: result.value,
582
+ };
583
+ }
@@ -0,0 +1,35 @@
1
+ import { type PublishedLanguageContent, type PublishedSiteLanguageContent } from '../publishing/publication-filter.ts';
2
+ import type { DocsLanguage } from '../types/project.ts';
3
+ export type BuildWorkflowOptions = {
4
+ repoRoot: string;
5
+ projectId?: string;
6
+ outputDir?: string;
7
+ };
8
+ export type BuildWorkflowLanguageSummary = {
9
+ lang: DocsLanguage;
10
+ totalPages: number;
11
+ publishedPages: number;
12
+ navigationItems: number;
13
+ };
14
+ export type BuildWorkflowLanguageResult<TContent = unknown> = {
15
+ lang: DocsLanguage;
16
+ content: PublishedLanguageContent<TContent>;
17
+ summary: BuildWorkflowLanguageSummary;
18
+ };
19
+ export type BuildWorkflowPublishedSiteResult<TContent = unknown> = {
20
+ lang: DocsLanguage;
21
+ content: PublishedSiteLanguageContent<TContent>;
22
+ summary: BuildWorkflowLanguageSummary;
23
+ };
24
+ export type BuildWorkflowResult = {
25
+ projectId: string;
26
+ artifactRoot: string;
27
+ machineReadableRoot: string;
28
+ entryHtmlFile: string;
29
+ defaultDocsPath: string;
30
+ languages: BuildWorkflowLanguageSummary[];
31
+ };
32
+ export declare function runBuildWorkflow(options: BuildWorkflowOptions): Promise<BuildWorkflowResult>;
33
+ export declare function loadPublishedBuildArtifacts<TContent = unknown>(options: BuildWorkflowOptions): Promise<BuildWorkflowLanguageResult<TContent>[]>;
34
+ export declare function loadPublishedSiteBuildArtifacts<TContent = unknown>(options: BuildWorkflowOptions): Promise<BuildWorkflowPublishedSiteResult<TContent>[]>;
35
+ //# sourceMappingURL=build-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-service.d.ts","sourceRoot":"","sources":["../../src/services/build-service.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,4BAA4B,EAClC,MAAM,qCAAqC,CAAC;AAG7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,IAAI,EAAE,YAAY,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,2BAA2B,CAAC,QAAQ,GAAG,OAAO,IAAI;IAC5D,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IAC5C,OAAO,EAAE,4BAA4B,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,gCAAgC,CAAC,QAAQ,GAAG,OAAO,IAAI;IACjE,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,4BAA4B,CAAC,QAAQ,CAAC,CAAC;IAChD,OAAO,EAAE,4BAA4B,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,4BAA4B,EAAE,CAAC;CAC3C,CAAC;AAaF,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA+BlG;AAED,wBAAsB,2BAA2B,CAAC,QAAQ,GAAG,OAAO,EAClE,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,2BAA2B,CAAC,QAAQ,CAAC,EAAE,CAAC,CAUlD;AAED,wBAAsB,+BAA+B,CAAC,QAAQ,GAAG,OAAO,EACtE,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,gCAAgC,CAAC,QAAQ,CAAC,EAAE,CAAC,CA6BvD"}