@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,601 @@
1
+ import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { ValidationError } from "../errors/validation-error.js";
4
+ import { createDocsRepository, listPages, loadNavigation, saveNavigation, savePage, } from "../fs/docs-repository.js";
5
+ import { loadProjectContract } from "../fs/content-repository.js";
6
+ import { isDocsLang } from "../types/docs.js";
7
+ import { assertValidPageId, normalizeSlug } from "../utils/slug.js";
8
+ const SUPPORTED_FRONTMATTER_KEYS = new Set(['title', 'description', 'tags']);
9
+ const MARKDOWN_REVIEW_PATTERNS = [
10
+ { code: 'code-fence', label: 'code fences', pattern: /^```/m },
11
+ { code: 'blockquote', label: 'blockquotes', pattern: /^>\s+/m },
12
+ { code: 'bulleted-list', label: 'bulleted lists', pattern: /^\s*[-*+]\s+/m },
13
+ { code: 'numbered-list', label: 'numbered lists', pattern: /^\s*\d+\.\s+/m },
14
+ { code: 'table', label: 'tables', pattern: /^\|.+\|$/m },
15
+ { code: 'link', label: 'markdown links', pattern: /\[[^\]]+\]\([^)]+\)/m },
16
+ { code: 'image', label: 'images', pattern: /!\[[^\]]*\]\([^)]+\)/m },
17
+ ];
18
+ function createConversionValidationError(message, rule, remediation, metadata) {
19
+ return new ValidationError(message, {
20
+ entity: 'legacy-conversion',
21
+ rule,
22
+ remediation,
23
+ metadata,
24
+ });
25
+ }
26
+ function isRecord(value) {
27
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
28
+ }
29
+ function assertSafeImportId(importId) {
30
+ if (typeof importId !== 'string' ||
31
+ importId.trim().length === 0 ||
32
+ importId.includes('/') ||
33
+ importId.includes('\\') ||
34
+ importId.includes('..')) {
35
+ throw createConversionValidationError(`Invalid import id "${importId}".`, 'legacy-import-id-safe-path', 'Use a generated importId without path separators or parent-directory traversal.', { importId });
36
+ }
37
+ }
38
+ async function readJsonWithValidation(filePath, entity, rule) {
39
+ const raw = await readFile(filePath, 'utf8').catch(() => {
40
+ throw createConversionValidationError(`Missing required ${entity} at "${filePath}".`, 'legacy-import-required-file-exists', `Restore the ${entity} or rerun the import workflow before conversion.`, { filePath, entity });
41
+ });
42
+ try {
43
+ return JSON.parse(raw);
44
+ }
45
+ catch (error) {
46
+ throw createConversionValidationError(`${entity} at "${filePath}" is not valid JSON.`, rule, `Fix the malformed ${entity} JSON or rerun the import workflow to regenerate it.`, {
47
+ filePath,
48
+ cause: error instanceof Error ? error.message : String(error),
49
+ });
50
+ }
51
+ }
52
+ function validateManifest(input, importId) {
53
+ if (!isRecord(input)) {
54
+ throw createConversionValidationError('Legacy import manifest must be a JSON object.', 'legacy-import-manifest-object', 'Restore the manifest.json file generated by the import workflow.', { received: input });
55
+ }
56
+ if (input.version !== 1) {
57
+ throw createConversionValidationError('Legacy import manifest version is unsupported.', 'legacy-import-manifest-version', 'Regenerate the staged import with the current anydocs import workflow.', { received: input.version });
58
+ }
59
+ if (input.importId !== importId) {
60
+ throw createConversionValidationError(`Legacy import manifest does not match import "${importId}".`, 'legacy-import-manifest-id-match', 'Use the manifest belonging to the requested importId.', { expected: importId, received: input.importId });
61
+ }
62
+ if (typeof input.projectId !== 'string' || input.projectId.trim().length === 0) {
63
+ throw createConversionValidationError('Legacy import manifest is missing a valid projectId.', 'legacy-import-manifest-project-id', 'Regenerate the staged import so it records the target project id.', { received: input.projectId });
64
+ }
65
+ if (typeof input.sourceRoot !== 'string' || input.sourceRoot.trim().length === 0) {
66
+ throw createConversionValidationError('Legacy import manifest is missing a valid sourceRoot.', 'legacy-import-manifest-source-root', 'Regenerate the staged import so it records the legacy source root.', { received: input.sourceRoot });
67
+ }
68
+ if (typeof input.importedAt !== 'string' || input.importedAt.trim().length === 0) {
69
+ throw createConversionValidationError('Legacy import manifest is missing a valid importedAt timestamp.', 'legacy-import-manifest-imported-at', 'Regenerate the staged import so it records when the import occurred.', { received: input.importedAt });
70
+ }
71
+ if (input.status !== 'staged' && input.status !== 'converted') {
72
+ throw createConversionValidationError('Legacy import manifest has an unsupported status.', 'legacy-import-manifest-status', 'Use a manifest generated by the current import workflow.', { received: input.status });
73
+ }
74
+ if (!Array.isArray(input.items)) {
75
+ throw createConversionValidationError('Legacy import manifest must include an items array.', 'legacy-import-manifest-items', 'Regenerate the staged import so the manifest lists imported items.', { received: input.items });
76
+ }
77
+ return {
78
+ version: 1,
79
+ importId,
80
+ projectId: input.projectId.trim(),
81
+ sourceRoot: input.sourceRoot.trim(),
82
+ importedAt: input.importedAt.trim(),
83
+ itemCount: typeof input.itemCount === 'number' ? input.itemCount : input.items.length,
84
+ status: input.status,
85
+ items: input.items.map((item) => {
86
+ if (!isRecord(item)) {
87
+ throw createConversionValidationError('Legacy import manifest item must be an object.', 'legacy-import-manifest-item-object', 'Regenerate the staged import so manifest items are valid JSON objects.', { received: item });
88
+ }
89
+ if (typeof item.id !== 'string' || item.id.trim().length === 0) {
90
+ throw createConversionValidationError('Legacy import manifest item is missing an id.', 'legacy-import-manifest-item-id', 'Regenerate the staged import so each item has a stable id.', { received: item.id });
91
+ }
92
+ if (!isDocsLang(item.lang)) {
93
+ throw createConversionValidationError('Legacy import manifest item language is invalid.', 'legacy-import-manifest-item-lang', 'Regenerate the staged import using a supported docs language.', { received: item.lang, itemId: item.id });
94
+ }
95
+ if (item.status !== 'staged' && item.status !== 'converted') {
96
+ throw createConversionValidationError('Legacy import manifest item status is unsupported.', 'legacy-import-manifest-item-status', 'Regenerate the staged import using the current workflow.', { received: item.status, itemId: item.id });
97
+ }
98
+ return {
99
+ id: item.id.trim(),
100
+ sourcePath: typeof item.sourcePath === 'string' ? item.sourcePath : '',
101
+ lang: item.lang,
102
+ slug: typeof item.slug === 'string' ? item.slug : '',
103
+ title: typeof item.title === 'string' ? item.title : '',
104
+ format: item.format === 'mdx' ? 'mdx' : 'markdown',
105
+ status: item.status,
106
+ };
107
+ }),
108
+ };
109
+ }
110
+ function validateImportItem(input, expectedItemId) {
111
+ if (!isRecord(input)) {
112
+ throw createConversionValidationError('Legacy import item must be a JSON object.', 'legacy-import-item-object', 'Restore the staged import item JSON or rerun the import workflow.', { received: input, expectedItemId });
113
+ }
114
+ if (typeof input.id !== 'string' || input.id.trim().length === 0 || input.id !== expectedItemId) {
115
+ throw createConversionValidationError(`Legacy import item does not match "${expectedItemId}".`, 'legacy-import-item-id-match', 'Use the staged import item file that belongs to the requested import manifest.', { expectedItemId, received: input.id });
116
+ }
117
+ if (!isDocsLang(input.lang)) {
118
+ throw createConversionValidationError('Legacy import item language is invalid.', 'legacy-import-item-lang', 'Regenerate the staged import using a supported docs language.', { received: input.lang, itemId: expectedItemId });
119
+ }
120
+ if (typeof input.slug !== 'string' || normalizeSlug(input.slug).length === 0) {
121
+ throw createConversionValidationError('Legacy import item slug is invalid.', 'legacy-import-item-slug', 'Fix the staged import item slug or rerun the import workflow.', { received: input.slug, itemId: expectedItemId });
122
+ }
123
+ if (typeof input.title !== 'string' || input.title.trim().length === 0) {
124
+ throw createConversionValidationError('Legacy import item title is invalid.', 'legacy-import-item-title', 'Provide a non-empty title in the staged import item before conversion.', { received: input.title, itemId: expectedItemId });
125
+ }
126
+ if (input.format !== 'markdown' && input.format !== 'mdx') {
127
+ throw createConversionValidationError('Legacy import item format is unsupported.', 'legacy-import-item-format', 'Use staged items generated from .md or .mdx sources only.', { received: input.format, itemId: expectedItemId });
128
+ }
129
+ if (typeof input.importedAt !== 'string' || input.importedAt.trim().length === 0) {
130
+ throw createConversionValidationError('Legacy import item importedAt is invalid.', 'legacy-import-item-imported-at', 'Regenerate the staged import so items record their import timestamp.', { received: input.importedAt, itemId: expectedItemId });
131
+ }
132
+ if (typeof input.rawContent !== 'string' || typeof input.body !== 'string') {
133
+ throw createConversionValidationError('Legacy import item content payload is invalid.', 'legacy-import-item-content', 'Regenerate the staged import so each item includes rawContent and body.', { itemId: expectedItemId });
134
+ }
135
+ if (!isRecord(input.frontmatter)) {
136
+ throw createConversionValidationError('Legacy import item frontmatter must be an object.', 'legacy-import-item-frontmatter', 'Regenerate the staged import so parsed frontmatter is stored as a JSON object.', { received: input.frontmatter, itemId: expectedItemId });
137
+ }
138
+ if (input.status !== 'staged' && input.status !== 'converted') {
139
+ throw createConversionValidationError('Legacy import item status is unsupported.', 'legacy-import-item-status', 'Use a staged import item generated by the current import workflow.', { received: input.status, itemId: expectedItemId });
140
+ }
141
+ const frontmatter = {};
142
+ for (const [key, value] of Object.entries(input.frontmatter)) {
143
+ if (typeof value === 'string') {
144
+ frontmatter[key] = value;
145
+ continue;
146
+ }
147
+ if (Array.isArray(value) && value.every((entry) => typeof entry === 'string')) {
148
+ frontmatter[key] = value;
149
+ }
150
+ }
151
+ return {
152
+ id: input.id.trim(),
153
+ sourcePath: typeof input.sourcePath === 'string' ? input.sourcePath : expectedItemId,
154
+ lang: input.lang,
155
+ slug: normalizeSlug(input.slug),
156
+ title: input.title.trim(),
157
+ description: typeof input.description === 'string' && input.description.trim() ? input.description.trim() : undefined,
158
+ tags: Array.isArray(input.tags) ? input.tags.filter((tag) => typeof tag === 'string' && tag.trim().length > 0) : undefined,
159
+ format: input.format,
160
+ importedAt: input.importedAt.trim(),
161
+ rawContent: input.rawContent,
162
+ body: input.body,
163
+ frontmatter,
164
+ status: input.status,
165
+ convertedAt: typeof input.convertedAt === 'string' ? input.convertedAt : undefined,
166
+ };
167
+ }
168
+ function stripMarkdown(markdown) {
169
+ return markdown
170
+ .replace(/```[\s\S]*?```/g, ' ')
171
+ .replace(/`[^`]*`/g, ' ')
172
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
173
+ .replace(/\[[^\]]*\]\([^)]*\)/g, ' ')
174
+ .replace(/[#>*_~]/g, ' ')
175
+ .replace(/\s+/g, ' ')
176
+ .trim();
177
+ }
178
+ function toTitleCase(segment) {
179
+ return segment
180
+ .split('-')
181
+ .filter(Boolean)
182
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
183
+ .join(' ');
184
+ }
185
+ function toYooptaParagraphBlock(blockId, elementId, text, order) {
186
+ return {
187
+ [blockId]: {
188
+ id: blockId,
189
+ type: 'Paragraph',
190
+ value: [
191
+ {
192
+ id: elementId,
193
+ type: 'paragraph',
194
+ children: [{ text }],
195
+ props: { nodeType: 'block' },
196
+ },
197
+ ],
198
+ meta: { order, depth: 0 },
199
+ },
200
+ };
201
+ }
202
+ function toYooptaHeadingBlock(blockId, elementId, text, headingType, elementType, order) {
203
+ return {
204
+ [blockId]: {
205
+ id: blockId,
206
+ type: headingType,
207
+ value: [
208
+ {
209
+ id: elementId,
210
+ type: elementType,
211
+ children: [{ text }],
212
+ props: { nodeType: 'block' },
213
+ },
214
+ ],
215
+ meta: { order, depth: 0 },
216
+ },
217
+ };
218
+ }
219
+ function createEditableLegacyContent(markdown) {
220
+ const trimmed = markdown.trim();
221
+ if (!trimmed) {
222
+ return toYooptaParagraphBlock('block-1', 'element-1', '', 0);
223
+ }
224
+ const chunks = trimmed
225
+ .split(/\n\s*\n/)
226
+ .map((chunk) => chunk.trim())
227
+ .filter(Boolean);
228
+ return chunks.reduce((content, chunk, index) => {
229
+ const blockId = `block-${index + 1}`;
230
+ const elementId = `element-${index + 1}`;
231
+ if (chunk.startsWith('# ')) {
232
+ return {
233
+ ...content,
234
+ ...toYooptaHeadingBlock(blockId, elementId, chunk.slice(2).trim(), 'HeadingOne', 'h1', index),
235
+ };
236
+ }
237
+ if (chunk.startsWith('## ')) {
238
+ return {
239
+ ...content,
240
+ ...toYooptaHeadingBlock(blockId, elementId, chunk.slice(3).trim(), 'HeadingTwo', 'h2', index),
241
+ };
242
+ }
243
+ if (chunk.startsWith('### ')) {
244
+ return {
245
+ ...content,
246
+ ...toYooptaHeadingBlock(blockId, elementId, chunk.slice(4).trim(), 'HeadingThree', 'h3', index),
247
+ };
248
+ }
249
+ const paragraphText = chunk.replace(/\n+/g, ' ').trim();
250
+ return {
251
+ ...content,
252
+ ...toYooptaParagraphBlock(blockId, elementId, paragraphText, index),
253
+ };
254
+ }, {});
255
+ }
256
+ function createUniquePageId(baseId, existingPageIds) {
257
+ assertValidPageId(baseId);
258
+ if (!existingPageIds.has(baseId)) {
259
+ existingPageIds.add(baseId);
260
+ return { value: baseId };
261
+ }
262
+ let suffix = 2;
263
+ let nextId = `${baseId}-${suffix}`;
264
+ while (existingPageIds.has(nextId)) {
265
+ suffix += 1;
266
+ nextId = `${baseId}-${suffix}`;
267
+ }
268
+ existingPageIds.add(nextId);
269
+ return {
270
+ value: nextId,
271
+ warning: {
272
+ code: 'legacy-import-page-id-conflict',
273
+ message: `Page id "${baseId}" already exists and was remapped to "${nextId}".`,
274
+ remediation: 'Review the converted page id and rename it if a more meaningful identifier is needed.',
275
+ metadata: { requestedPageId: baseId, convertedPageId: nextId },
276
+ },
277
+ };
278
+ }
279
+ function createUniqueSlug(baseSlug, existingSlugs) {
280
+ const normalized = normalizeSlug(baseSlug);
281
+ if (!normalized) {
282
+ throw createConversionValidationError('Legacy import item slug could not be normalized.', 'legacy-import-item-slug-normalized', 'Fix the staged import item slug before conversion.', { received: baseSlug });
283
+ }
284
+ if (!existingSlugs.has(normalized)) {
285
+ existingSlugs.add(normalized);
286
+ return { value: normalized };
287
+ }
288
+ const segments = normalized.split('/');
289
+ const leaf = segments.pop() ?? normalized;
290
+ let suffix = 2;
291
+ let candidate = [...segments, `${leaf}-${suffix}`].join('/');
292
+ while (existingSlugs.has(candidate)) {
293
+ suffix += 1;
294
+ candidate = [...segments, `${leaf}-${suffix}`].join('/');
295
+ }
296
+ existingSlugs.add(candidate);
297
+ return {
298
+ value: candidate,
299
+ warning: {
300
+ code: 'legacy-import-slug-conflict',
301
+ message: `Slug "${normalized}" already exists and was remapped to "${candidate}".`,
302
+ remediation: 'Review the converted slug and rename it if a cleaner route is desired.',
303
+ metadata: { requestedSlug: normalized, convertedSlug: candidate },
304
+ },
305
+ };
306
+ }
307
+ function collectItemWarnings(item) {
308
+ const warnings = [];
309
+ if (item.format === 'mdx') {
310
+ warnings.push({
311
+ itemId: item.id,
312
+ code: 'legacy-import-mdx-review-required',
313
+ message: `Imported "${item.sourcePath}" originated from MDX and should be reviewed for unsupported JSX or component usage.`,
314
+ remediation: 'Open the converted draft page and confirm the markdown/render output matches the intended MDX content.',
315
+ metadata: { sourcePath: item.sourcePath },
316
+ });
317
+ }
318
+ const unmappedFrontmatterKeys = Object.keys(item.frontmatter).filter((key) => !SUPPORTED_FRONTMATTER_KEYS.has(key));
319
+ if (unmappedFrontmatterKeys.length > 0) {
320
+ warnings.push({
321
+ itemId: item.id,
322
+ code: 'legacy-import-frontmatter-unmapped',
323
+ message: `Imported "${item.sourcePath}" includes frontmatter keys that were preserved in content metadata but not mapped to first-class page fields.`,
324
+ remediation: 'Review the converted page content metadata and manually map any important legacy frontmatter fields.',
325
+ metadata: { sourcePath: item.sourcePath, keys: unmappedFrontmatterKeys },
326
+ });
327
+ }
328
+ const detectedConstructs = MARKDOWN_REVIEW_PATTERNS.filter((entry) => entry.pattern.test(item.body));
329
+ if (detectedConstructs.length > 0) {
330
+ warnings.push({
331
+ itemId: item.id,
332
+ code: 'legacy-import-markdown-construct-review-required',
333
+ message: `Imported "${item.sourcePath}" contains markdown constructs that are preserved as markdown/render output but simplified in the editable Studio content.`,
334
+ remediation: 'Review the converted draft page and restore any lists, code blocks, tables, links, or quotes that should remain structured after import.',
335
+ metadata: {
336
+ sourcePath: item.sourcePath,
337
+ constructs: detectedConstructs.map((entry) => ({
338
+ code: entry.code,
339
+ label: entry.label,
340
+ })),
341
+ },
342
+ });
343
+ }
344
+ return warnings;
345
+ }
346
+ function createConvertedPage(item, importId, convertedAt, pageId, slug, warnings) {
347
+ return {
348
+ id: pageId,
349
+ lang: item.lang,
350
+ slug,
351
+ title: item.title,
352
+ ...(item.description ? { description: item.description } : {}),
353
+ ...(item.tags ? { tags: item.tags } : {}),
354
+ status: 'draft',
355
+ updatedAt: convertedAt,
356
+ content: createEditableLegacyContent(item.body),
357
+ render: {
358
+ markdown: item.body,
359
+ plainText: stripMarkdown(item.body),
360
+ },
361
+ review: {
362
+ required: true,
363
+ sourceType: 'legacy-import',
364
+ sourceId: importId,
365
+ itemId: item.id,
366
+ sourcePath: item.sourcePath,
367
+ metadata: {
368
+ format: item.format,
369
+ importedAt: item.importedAt,
370
+ convertedAt,
371
+ frontmatter: item.frontmatter,
372
+ rawContent: item.rawContent,
373
+ },
374
+ warnings: warnings.map((warning) => ({
375
+ code: warning.code,
376
+ message: warning.message,
377
+ ...(warning.remediation ? { remediation: warning.remediation } : {}),
378
+ ...(warning.metadata ? { metadata: warning.metadata } : {}),
379
+ })),
380
+ },
381
+ };
382
+ }
383
+ function createUpdatedItem(item, convertedAt) {
384
+ return {
385
+ ...item,
386
+ status: 'converted',
387
+ convertedAt,
388
+ };
389
+ }
390
+ function buildImportedNavigationSection(importId, items) {
391
+ const root = {
392
+ type: 'section',
393
+ title: `Imported: ${importId}`,
394
+ children: [],
395
+ };
396
+ const folderIndex = new Map();
397
+ for (const item of items) {
398
+ const segments = item.slug.split('/').filter(Boolean);
399
+ const folderSegments = segments.slice(0, -1);
400
+ let currentChildren = root.children;
401
+ let currentPath = '';
402
+ for (const folderSegment of folderSegments) {
403
+ currentPath = currentPath ? `${currentPath}/${folderSegment}` : folderSegment;
404
+ let folder = folderIndex.get(currentPath);
405
+ if (!folder) {
406
+ folder = {
407
+ type: 'folder',
408
+ title: toTitleCase(folderSegment),
409
+ children: [],
410
+ };
411
+ folderIndex.set(currentPath, folder);
412
+ currentChildren.push(folder);
413
+ }
414
+ currentChildren = folder.children;
415
+ }
416
+ currentChildren.push({
417
+ type: 'page',
418
+ pageId: item.pageId,
419
+ });
420
+ }
421
+ return root;
422
+ }
423
+ async function writeJson(filePath, value) {
424
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
425
+ }
426
+ async function snapshotPath(filePath) {
427
+ try {
428
+ const current = await stat(filePath);
429
+ if (current.isDirectory()) {
430
+ return { filePath, kind: 'directory' };
431
+ }
432
+ return {
433
+ filePath,
434
+ kind: 'file',
435
+ contents: await readFile(filePath, 'utf8'),
436
+ };
437
+ }
438
+ catch (error) {
439
+ if (error?.code === 'ENOENT') {
440
+ return { filePath, kind: 'missing' };
441
+ }
442
+ throw error;
443
+ }
444
+ }
445
+ async function restoreSnapshot(snapshot) {
446
+ if (snapshot.kind === 'missing') {
447
+ await rm(snapshot.filePath, { recursive: true, force: true });
448
+ return;
449
+ }
450
+ if (snapshot.kind === 'directory') {
451
+ await rm(snapshot.filePath, { recursive: true, force: true });
452
+ await mkdir(snapshot.filePath, { recursive: true });
453
+ return;
454
+ }
455
+ await rm(snapshot.filePath, { recursive: true, force: true });
456
+ await writeFile(snapshot.filePath, snapshot.contents, 'utf8');
457
+ }
458
+ function navigationFilePath(projectRoot, lang) {
459
+ return path.join(projectRoot, 'navigation', `${lang}.json`);
460
+ }
461
+ function pageFilePath(projectRoot, lang, pageId) {
462
+ return path.join(projectRoot, 'pages', lang, `${pageId}.json`);
463
+ }
464
+ export async function convertImportedLegacyContent(options) {
465
+ const contractResult = await loadProjectContract(options.repoRoot, options.projectId);
466
+ if (!contractResult.ok) {
467
+ throw contractResult.error;
468
+ }
469
+ assertSafeImportId(options.importId);
470
+ const contract = contractResult.value;
471
+ const importRoot = path.join(contract.paths.importsRoot, options.importId);
472
+ const manifestFile = path.join(importRoot, 'manifest.json');
473
+ const reportFile = path.join(importRoot, 'conversion-report.json');
474
+ const rawManifest = await readJsonWithValidation(manifestFile, 'legacy import manifest', 'legacy-import-manifest-json-valid');
475
+ const manifest = validateManifest(rawManifest, options.importId);
476
+ if (manifest.status !== 'staged') {
477
+ throw createConversionValidationError(`Legacy import "${options.importId}" has already been converted.`, 'legacy-import-already-converted', 'Review the existing converted pages or stage a fresh import before converting again.', { importId: options.importId, status: manifest.status });
478
+ }
479
+ const repository = createDocsRepository(contract.paths.projectRoot);
480
+ const convertedAt = new Date().toISOString();
481
+ const plansByLang = new Map();
482
+ const globalWarnings = [];
483
+ const enabledLanguages = new Set(contract.config.languages);
484
+ const unsupportedLanguageItem = manifest.items.find((item) => !enabledLanguages.has(item.lang));
485
+ if (unsupportedLanguageItem) {
486
+ throw createConversionValidationError(`Legacy import item "${unsupportedLanguageItem.id}" targets language "${unsupportedLanguageItem.lang}" which is not enabled for the project.`, 'legacy-import-item-language-enabled', 'Enable the language in anydocs.config.json or restage the import using one of the configured project languages.', {
487
+ itemId: unsupportedLanguageItem.id,
488
+ itemLanguage: unsupportedLanguageItem.lang,
489
+ projectLanguages: contract.config.languages,
490
+ });
491
+ }
492
+ for (const lang of contract.config.languages) {
493
+ plansByLang.set(lang, []);
494
+ }
495
+ for (const lang of contract.config.languages) {
496
+ const plans = plansByLang.get(lang);
497
+ if (!plans) {
498
+ continue;
499
+ }
500
+ const existingPages = await listPages(repository, lang);
501
+ const existingPageIds = new Set(existingPages.map((page) => page.id));
502
+ const existingSlugs = new Set(existingPages.map((page) => page.slug));
503
+ const languageManifestItems = manifest.items.filter((item) => item.lang === lang);
504
+ for (const manifestItem of languageManifestItems) {
505
+ const itemFile = path.join(importRoot, 'items', `${manifestItem.id}.json`);
506
+ const rawItem = await readJsonWithValidation(itemFile, 'legacy import item', 'legacy-import-item-json-valid');
507
+ const item = validateImportItem(rawItem, manifestItem.id);
508
+ const warnings = collectItemWarnings(item);
509
+ const pageIdResolution = createUniquePageId(item.id, existingPageIds);
510
+ if (pageIdResolution.warning) {
511
+ warnings.push({ ...pageIdResolution.warning, itemId: item.id });
512
+ }
513
+ const slugResolution = createUniqueSlug(item.slug, existingSlugs);
514
+ if (slugResolution.warning) {
515
+ warnings.push({ ...slugResolution.warning, itemId: item.id });
516
+ }
517
+ const entry = {
518
+ itemId: item.id,
519
+ pageId: pageIdResolution.value,
520
+ lang: item.lang,
521
+ slug: slugResolution.value,
522
+ status: 'draft',
523
+ warnings,
524
+ };
525
+ plans.push({
526
+ entry,
527
+ page: createConvertedPage(item, options.importId, convertedAt, pageIdResolution.value, slugResolution.value, warnings),
528
+ updatedItem: createUpdatedItem(item, convertedAt),
529
+ });
530
+ globalWarnings.push(...warnings);
531
+ }
532
+ }
533
+ const convertedItems = Array.from(plansByLang.values()).flatMap((plans) => plans.map((plan) => plan.entry));
534
+ const report = {
535
+ version: 1,
536
+ importId: options.importId,
537
+ projectId: contract.config.projectId,
538
+ convertedAt,
539
+ status: 'converted',
540
+ convertedCount: convertedItems.length,
541
+ items: convertedItems,
542
+ warnings: globalWarnings,
543
+ };
544
+ const updatedManifest = {
545
+ ...manifest,
546
+ status: 'converted',
547
+ items: manifest.items.map((item) => ({
548
+ ...item,
549
+ status: 'converted',
550
+ })),
551
+ };
552
+ const snapshotTargets = new Set([manifestFile, reportFile]);
553
+ for (const [lang, plans] of plansByLang) {
554
+ if (plans.length === 0) {
555
+ continue;
556
+ }
557
+ snapshotTargets.add(navigationFilePath(contract.paths.projectRoot, lang));
558
+ for (const plan of plans) {
559
+ snapshotTargets.add(pageFilePath(contract.paths.projectRoot, lang, plan.page.id));
560
+ snapshotTargets.add(path.join(importRoot, 'items', `${plan.updatedItem.id}.json`));
561
+ }
562
+ }
563
+ const snapshots = await Promise.all(Array.from(snapshotTargets, (filePath) => snapshotPath(filePath)));
564
+ try {
565
+ for (const [lang, plans] of plansByLang) {
566
+ if (plans.length === 0) {
567
+ continue;
568
+ }
569
+ for (const plan of plans) {
570
+ await savePage(repository, lang, plan.page);
571
+ }
572
+ const navigation = await loadNavigation(repository, lang);
573
+ const importedSection = buildImportedNavigationSection(options.importId, plans.map((plan) => plan.entry));
574
+ const updatedNavigation = {
575
+ version: navigation.version,
576
+ items: [...navigation.items, importedSection],
577
+ };
578
+ const existingPageIds = (await listPages(repository, lang)).map((page) => page.id);
579
+ await saveNavigation(repository, lang, updatedNavigation, { existingPageIds });
580
+ for (const plan of plans) {
581
+ await writeJson(path.join(importRoot, 'items', `${plan.updatedItem.id}.json`), plan.updatedItem);
582
+ }
583
+ }
584
+ await writeJson(reportFile, report);
585
+ await writeJson(manifestFile, updatedManifest);
586
+ }
587
+ catch (error) {
588
+ for (const snapshot of [...snapshots].reverse()) {
589
+ await restoreSnapshot(snapshot);
590
+ }
591
+ throw error;
592
+ }
593
+ return {
594
+ importId: options.importId,
595
+ importRoot,
596
+ reportFile,
597
+ convertedCount: convertedItems.length,
598
+ items: convertedItems,
599
+ warnings: globalWarnings,
600
+ };
601
+ }
@@ -0,0 +1,10 @@
1
+ import type { LegacyImportResult } from '../types/legacy-import.ts';
2
+ import type { DocsLanguage } from '../types/project.ts';
3
+ export type ImportLegacyDocumentationOptions = {
4
+ repoRoot: string;
5
+ sourceRoot: string;
6
+ projectId?: string;
7
+ lang?: DocsLanguage;
8
+ };
9
+ export declare function importLegacyDocumentation(options: ImportLegacyDocumentationOptions): Promise<LegacyImportResult>;
10
+ //# sourceMappingURL=legacy-import-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legacy-import-service.d.ts","sourceRoot":"","sources":["../../src/services/legacy-import-service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAMV,kBAAkB,EACnB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,MAAM,gCAAgC,GAAG;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB,CAAC;AA8PF,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,kBAAkB,CAAC,CA8E7B"}