@pterodoc/core 0.2.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 (130) hide show
  1. package/LICENCE.md +10 -0
  2. package/README.md +12 -0
  3. package/lib/chunks/capture-BhdvHwrN.js +428 -0
  4. package/lib/chunks/capture-BhdvHwrN.js.map +1 -0
  5. package/lib/chunks/issues-DBsPh6ek.js +55 -0
  6. package/lib/chunks/issues-DBsPh6ek.js.map +1 -0
  7. package/lib/chunks/mime-2SUyOolJ.js +62 -0
  8. package/lib/chunks/mime-2SUyOolJ.js.map +1 -0
  9. package/lib/chunks/paths-Bh6UsRI9.js +177 -0
  10. package/lib/chunks/paths-Bh6UsRI9.js.map +1 -0
  11. package/lib/config/load.d.ts +123 -0
  12. package/lib/config/load.d.ts.map +1 -0
  13. package/lib/config/types.d.ts +136 -0
  14. package/lib/config/types.d.ts.map +1 -0
  15. package/lib/errors.d.ts +53 -0
  16. package/lib/errors.d.ts.map +1 -0
  17. package/lib/index.d.ts +23 -0
  18. package/lib/index.d.ts.map +1 -0
  19. package/lib/index.js +807 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/model/capture.d.ts +37 -0
  22. package/lib/model/capture.d.ts.map +1 -0
  23. package/lib/model/index.d.ts +13 -0
  24. package/lib/model/index.d.ts.map +1 -0
  25. package/lib/model/index.js +7 -0
  26. package/lib/model/index.js.map +1 -0
  27. package/lib/model/reader.d.ts +25 -0
  28. package/lib/model/reader.d.ts.map +1 -0
  29. package/lib/model/tree.d.ts +81 -0
  30. package/lib/model/tree.d.ts.map +1 -0
  31. package/lib/model/types.d.ts +166 -0
  32. package/lib/model/types.d.ts.map +1 -0
  33. package/lib/render/admonitions.d.ts +26 -0
  34. package/lib/render/admonitions.d.ts.map +1 -0
  35. package/lib/render/blocks.d.ts +39 -0
  36. package/lib/render/blocks.d.ts.map +1 -0
  37. package/lib/render/code.d.ts +37 -0
  38. package/lib/render/code.d.ts.map +1 -0
  39. package/lib/render/components.d.ts +49 -0
  40. package/lib/render/components.d.ts.map +1 -0
  41. package/lib/render/excerpt.d.ts +9 -0
  42. package/lib/render/excerpt.d.ts.map +1 -0
  43. package/lib/render/images.d.ts +58 -0
  44. package/lib/render/images.d.ts.map +1 -0
  45. package/lib/render/index.d.ts +80 -0
  46. package/lib/render/index.d.ts.map +1 -0
  47. package/lib/render/index.js +1336 -0
  48. package/lib/render/index.js.map +1 -0
  49. package/lib/render/inline.d.ts +25 -0
  50. package/lib/render/inline.d.ts.map +1 -0
  51. package/lib/render/links.d.ts +45 -0
  52. package/lib/render/links.d.ts.map +1 -0
  53. package/lib/render/mdx.d.ts +37 -0
  54. package/lib/render/mdx.d.ts.map +1 -0
  55. package/lib/render/page.d.ts +85 -0
  56. package/lib/render/page.d.ts.map +1 -0
  57. package/lib/render/parse.d.ts +27 -0
  58. package/lib/render/parse.d.ts.map +1 -0
  59. package/lib/render/references.d.ts +20 -0
  60. package/lib/render/references.d.ts.map +1 -0
  61. package/lib/render/renderers.d.ts +57 -0
  62. package/lib/render/renderers.d.ts.map +1 -0
  63. package/lib/render/slug.d.ts +34 -0
  64. package/lib/render/slug.d.ts.map +1 -0
  65. package/lib/render/theme.d.ts +62 -0
  66. package/lib/render/theme.d.ts.map +1 -0
  67. package/lib/sync/artifacts.d.ts +46 -0
  68. package/lib/sync/artifacts.d.ts.map +1 -0
  69. package/lib/sync/plan.d.ts +59 -0
  70. package/lib/sync/plan.d.ts.map +1 -0
  71. package/lib/sync/run.d.ts +39 -0
  72. package/lib/sync/run.d.ts.map +1 -0
  73. package/lib/target/index.d.ts +8 -0
  74. package/lib/target/index.d.ts.map +1 -0
  75. package/lib/target/index.js +2 -0
  76. package/lib/target/index.js.map +1 -0
  77. package/lib/target/target.d.ts +152 -0
  78. package/lib/target/target.d.ts.map +1 -0
  79. package/lib/util/hash.d.ts +10 -0
  80. package/lib/util/hash.d.ts.map +1 -0
  81. package/lib/util/index.d.ts +14 -0
  82. package/lib/util/index.d.ts.map +1 -0
  83. package/lib/util/index.js +7 -0
  84. package/lib/util/index.js.map +1 -0
  85. package/lib/util/issues.d.ts +52 -0
  86. package/lib/util/issues.d.ts.map +1 -0
  87. package/lib/util/mime.d.ts +16 -0
  88. package/lib/util/mime.d.ts.map +1 -0
  89. package/lib/util/paths.d.ts +42 -0
  90. package/lib/util/paths.d.ts.map +1 -0
  91. package/lib/version.d.ts +6 -0
  92. package/lib/version.d.ts.map +1 -0
  93. package/package.json +43 -0
  94. package/src/config/load.ts +387 -0
  95. package/src/config/types.ts +137 -0
  96. package/src/errors.ts +77 -0
  97. package/src/globals.d.ts +10 -0
  98. package/src/index.ts +31 -0
  99. package/src/model/capture.ts +92 -0
  100. package/src/model/index.ts +25 -0
  101. package/src/model/reader.ts +37 -0
  102. package/src/model/tree.ts +416 -0
  103. package/src/model/types.ts +167 -0
  104. package/src/render/admonitions.ts +74 -0
  105. package/src/render/blocks.ts +79 -0
  106. package/src/render/code.ts +112 -0
  107. package/src/render/components.ts +230 -0
  108. package/src/render/excerpt.ts +16 -0
  109. package/src/render/images.ts +113 -0
  110. package/src/render/index.ts +129 -0
  111. package/src/render/inline.ts +62 -0
  112. package/src/render/links.ts +94 -0
  113. package/src/render/mdx.ts +178 -0
  114. package/src/render/page.ts +289 -0
  115. package/src/render/parse.ts +65 -0
  116. package/src/render/references.ts +69 -0
  117. package/src/render/renderers.ts +346 -0
  118. package/src/render/slug.ts +63 -0
  119. package/src/render/theme.ts +92 -0
  120. package/src/sync/artifacts.ts +70 -0
  121. package/src/sync/plan.ts +69 -0
  122. package/src/sync/run.ts +641 -0
  123. package/src/target/index.ts +18 -0
  124. package/src/target/target.ts +146 -0
  125. package/src/util/hash.ts +14 -0
  126. package/src/util/index.ts +23 -0
  127. package/src/util/issues.ts +86 -0
  128. package/src/util/mime.ts +47 -0
  129. package/src/util/paths.ts +101 -0
  130. package/src/version.ts +31 -0
package/lib/index.js ADDED
@@ -0,0 +1,807 @@
1
+ import { C as ConfigError, e as toSlugSegments, V as VERSION, t as titleCase } from './chunks/paths-Bh6UsRI9.js';
2
+ export { E as EXIT, P as PterodocError, T as TargetError, U as USER_AGENT, a as UnsupportedContentError } from './chunks/paths-Bh6UsRI9.js';
3
+ import fs, { readFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import dotenv from 'dotenv';
7
+ import { DEFAULT_LAYOUT, DEFAULT_STRINGS, createTheme, renderVersionBanner, collectImages, resolveImage, renderDoc, composePage, excerptFor } from './render/index.js';
8
+ import fs$1 from 'node:fs/promises';
9
+ import { I as IssueCollector } from './chunks/issues-DBsPh6ek.js';
10
+ export { c as compareSeverity, f as formatIssue } from './chunks/issues-DBsPh6ek.js';
11
+ import { c as contentHash, m as mimeTypeFor } from './chunks/mime-2SUyOolJ.js';
12
+ import { b as buildPageTree } from './chunks/capture-BhdvHwrN.js';
13
+ export { c as createCaptureReader, a as createMemoryReader, r as readCapture, s as serializeModel, w as writeCapture } from './chunks/capture-BhdvHwrN.js';
14
+ import 'gray-matter';
15
+ import 'unified';
16
+ import 'remark-parse';
17
+ import 'remark-gfm';
18
+ import 'remark-directive';
19
+ import 'remark-frontmatter';
20
+ import 'remark-mdx';
21
+ import 'unist-util-visit';
22
+ import 'mdast-util-to-hast';
23
+ import 'hast-util-to-html';
24
+ import 'github-slugger';
25
+ import 'mdast-util-to-string';
26
+ import 'node:crypto';
27
+
28
+ /**
29
+ * The configuration a site writes, and the resolved shape the tool reads.
30
+ *
31
+ * Credentials never appear here: the config names the environment variables
32
+ * that hold them, so a config file is safe to commit.
33
+ */
34
+ /**
35
+ * Identity function that types a configuration file.
36
+ *
37
+ * @param config The configuration.
38
+ */
39
+ function defineConfig(config) {
40
+ return config;
41
+ }
42
+
43
+ /**
44
+ * Discovering, merging and validating configuration.
45
+ *
46
+ * Precedence is flags, then the environment, then the config file, then the
47
+ * defaults. Nothing here touches `process.env` or the disk except through the
48
+ * dependencies it is given, so it is testable without either.
49
+ */
50
+ /** File names tried, in order, when no config is named. */
51
+ const CONFIG_NAMES = [
52
+ 'pterodoc.config.mjs',
53
+ 'pterodoc.config.js',
54
+ 'pterodoc.config.cjs',
55
+ 'pterodoc.config.ts',
56
+ 'pterodoc.config.json',
57
+ ];
58
+ /** Find a configuration file next to the site. */
59
+ function discoverConfigFile(siteDir, deps) {
60
+ for (const name of CONFIG_NAMES) {
61
+ const candidate = path.join(siteDir, name);
62
+ if (deps.existsSync(candidate))
63
+ return candidate;
64
+ }
65
+ return undefined;
66
+ }
67
+ /** Load a configuration file. */
68
+ async function readConfigFile(file) {
69
+ if (file.endsWith('.json')) {
70
+ try {
71
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
72
+ }
73
+ catch (error) {
74
+ throw new ConfigError(`${file} is not valid JSON: ${error.message}`);
75
+ }
76
+ }
77
+ try {
78
+ const module = (await import(pathToFileURL(file).href));
79
+ const config = module.default ?? module;
80
+ if (!config || typeof config !== 'object') {
81
+ throw new Error('the file exports no configuration object');
82
+ }
83
+ return config;
84
+ }
85
+ catch (error) {
86
+ const message = error.message;
87
+ if (file.endsWith('.ts')) {
88
+ throw new ConfigError(`Could not load ${file}: ${message}\nA TypeScript config needs Node 22 or newer, which reads it directly. On an older Node, use pterodoc.config.mjs.`);
89
+ }
90
+ throw new ConfigError(`Could not load ${file}: ${message}`);
91
+ }
92
+ }
93
+ /** One value, from the flags, the environment, the file, or the default. */
94
+ function pick(...candidates) {
95
+ for (const candidate of candidates) {
96
+ if (candidate !== undefined && candidate !== '')
97
+ return candidate;
98
+ }
99
+ return undefined;
100
+ }
101
+ /**
102
+ * Names that changed when this tool was extracted.
103
+ *
104
+ * The credential and URL variables kept their names, because they are what
105
+ * every existing setup already sets; only these two were renamed, and using
106
+ * one still works but says so.
107
+ */
108
+ const RENAMED = {
109
+ WP_ROOT_PATH: 'PTERODOC_WP_ROOT',
110
+ WP_DOCS_BASE: 'PTERODOC_WP_BASE',
111
+ };
112
+ /**
113
+ * Read an environment value.
114
+ *
115
+ * `PTERODOC_`-prefixed names win; the plain names are equally supported except
116
+ * where one was renamed, which is reported.
117
+ */
118
+ function fromEnv(env, name, aliases, notices) {
119
+ const prefixed = env[`PTERODOC_${name}`];
120
+ if (prefixed)
121
+ return prefixed;
122
+ for (const alias of aliases) {
123
+ const value = env[alias];
124
+ if (!value)
125
+ continue;
126
+ const current = RENAMED[alias];
127
+ if (current)
128
+ notices.push(`Using ${alias}; it is now called ${current}.`);
129
+ return value;
130
+ }
131
+ return undefined;
132
+ }
133
+ /**
134
+ * Resolve everything into the shape the run reads.
135
+ *
136
+ * @param input Flags, the file's contents, and the environment.
137
+ */
138
+ function resolveConfig(input) {
139
+ const flags = input.flags ?? {};
140
+ const file = input.file ?? {};
141
+ const env = input.env ?? {};
142
+ const notices = [];
143
+ const site = file.site ?? {};
144
+ const target = file.target ?? {};
145
+ const render = file.render ?? {};
146
+ const media = file.media ?? {};
147
+ const output = file.output ?? {};
148
+ const baseDir = input.fileDir ?? process.cwd();
149
+ const siteDir = path.resolve(baseDir, pick(flags.siteDir, site.dir) ?? '.');
150
+ const userEnvName = target.auth?.userEnv ?? 'WP_USER';
151
+ const passwordEnvName = target.auth?.passwordEnv ?? 'WP_APP_PASSWORD';
152
+ const targetUrl = (pick(fromEnv(env, 'WP_URL', ['WP_URL'], notices), target.url) ?? '').replace(/\/+$/, '');
153
+ const user = (env[userEnvName] ?? '').trim();
154
+ const appPassword = (env[passwordEnvName] ?? '').replace(/\s+/g, '');
155
+ if (targetUrl && !/^https?:\/\//.test(targetUrl)) {
156
+ throw new ConfigError(`The target URL must start with http:// or https:// (got "${targetUrl}").`);
157
+ }
158
+ const missingCredentials = !targetUrl || !user || !appPassword;
159
+ const offline = flags.offline === true || missingCredentials;
160
+ if (missingCredentials && flags.offline !== true) {
161
+ notices.push(`No target URL or credentials (${userEnvName}, ${passwordEnvName}): running offline. Pages are rendered and nothing is sent.`);
162
+ }
163
+ const status = (pick(flags.status, fromEnv(env, 'WP_STATUS', ['WP_STATUS'], notices), target.status) ??
164
+ 'publish');
165
+ if (status !== 'publish' && status !== 'draft' && status !== 'private') {
166
+ throw new ConfigError(`Status must be publish, draft or private (got "${status}").`);
167
+ }
168
+ const rootPath = pick(flags.root, fromEnv(env, 'WP_ROOT', ['WP_ROOT_PATH'], notices), target.root) ?? '/docs';
169
+ const basePath = flags.base !== undefined
170
+ ? flags.base
171
+ : (pick(fromEnv(env, 'WP_BASE', ['WP_DOCS_BASE'], notices), target.base) ?? '');
172
+ const layout = { ...DEFAULT_LAYOUT, ...file.layout };
173
+ if (!['', 'wide', 'full'].includes(layout.align)) {
174
+ throw new ConfigError(`Layout alignment must be "", "wide" or "full" (got "${layout.align}").`);
175
+ }
176
+ const locales = flags.allLocales
177
+ ? 'all'
178
+ : flags.locale && flags.locale.length > 0
179
+ ? flags.locale
180
+ : (site.locales ?? 'default');
181
+ const versions = flags.allVersions
182
+ ? 'all'
183
+ : flags.docsVersion && flags.docsVersion.length > 0
184
+ ? flags.docsVersion
185
+ : (site.versions ?? 'last');
186
+ const outDir = path.resolve(baseDir, pick(flags.out, fromEnv(env, 'OUT', [], notices), output.dir) ?? '.pterodoc');
187
+ return {
188
+ siteDir,
189
+ configFile: input.configFile,
190
+ docusaurusConfig: pick(flags.docusaurusConfig, site.config),
191
+ modelFile: flags.model ? path.resolve(baseDir, flags.model) : undefined,
192
+ instances: flags.instance && flags.instance.length > 0 ? flags.instance : (site.instances ?? 'all'),
193
+ sidebars: site.sidebars ?? 'all',
194
+ versions,
195
+ locales,
196
+ includeDrafts: site.includeDrafts === true,
197
+ includeUnlisted: site.includeUnlisted === true,
198
+ includeOrphans: site.includeOrphans === true,
199
+ targetType: target.type ?? 'wordpress',
200
+ targetUrl,
201
+ user,
202
+ appPassword,
203
+ rootSegments: toSlugSegments(rootPath, 'the target root path'),
204
+ baseSegments: toSlugSegments(basePath, 'the target base'),
205
+ docsTitle: target.title ?? '',
206
+ status,
207
+ template: target.template ?? '',
208
+ lang: pick(fromEnv(env, 'WP_LANG', ['WP_LANG'], notices), target.lang) ?? '',
209
+ metaDescriptionKey: target.meta?.description ?? '',
210
+ methodOverride: fromEnv(env, 'METHOD_OVERRIDE', ['WP_METHOD_OVERRIDE'], notices) === '1' ||
211
+ target.methodOverride === true,
212
+ retry: {
213
+ attempts: target.retry?.attempts ?? 4,
214
+ baseDelayMs: target.retry?.baseDelayMs ?? 1000,
215
+ maxDelayMs: target.retry?.maxDelayMs ?? 30_000,
216
+ },
217
+ layout,
218
+ classPrefix: render.classPrefix ?? 'pterodoc',
219
+ blocks: render.blocks === 'plugin' ? 'plugin' : 'core',
220
+ dedupeTitle: render.dedupeTitle !== false,
221
+ unpublishedLinks: render.unpublishedLinks ?? 'site',
222
+ siteUrl: render.siteUrl ?? '',
223
+ excerptLength: render.excerptLength ?? 160,
224
+ strings: { ...DEFAULT_STRINGS, ...render.strings },
225
+ localeStrings: render.localeStrings ?? {},
226
+ mdxOnUnknown: file.mdx?.onUnknown ?? 'report',
227
+ uploadMedia: flags.noMedia === true ? false : media.upload !== false,
228
+ uploadRemoteMedia: media.uploadRemote === true,
229
+ mediaOnMissing: media.onMissing ?? 'warning',
230
+ mediaSlugPrefix: media.slugPrefix ?? 'pterodoc',
231
+ outDir,
232
+ writePages: output.pages !== false,
233
+ only: (flags.only ?? '').replace(/^\/+|\/+$/g, ''),
234
+ dryRun: flags.dryRun === true || offline,
235
+ prune: flags.prune === true,
236
+ offline,
237
+ strict: flags.strict === true,
238
+ strictAt: file.strict ?? 'error',
239
+ notices,
240
+ };
241
+ }
242
+ /**
243
+ * Discover, read and resolve the configuration.
244
+ *
245
+ * @param flags Parsed command line flags.
246
+ * @param env The environment to read.
247
+ * @param deps Injected filesystem access.
248
+ */
249
+ async function loadConfig(flags = {}, env = process.env, deps = {}) {
250
+ const existsSync = deps.existsSync ?? ((file) => fs.existsSync(file));
251
+ const cwd = deps.cwd ?? (() => process.cwd());
252
+ const startDir = path.resolve(cwd(), flags.siteDir ?? '.');
253
+ const configFile = flags.config
254
+ ? path.resolve(cwd(), flags.config)
255
+ : discoverConfigFile(startDir, { existsSync });
256
+ if (flags.config && !existsSync(configFile)) {
257
+ throw new ConfigError(`No configuration file at ${configFile}.`);
258
+ }
259
+ const file = configFile ? await readConfigFile(configFile) : {};
260
+ const fileDir = configFile ? path.dirname(configFile) : startDir;
261
+ // A named env file is read only when asked for, so a developer's own .env
262
+ // can never leak into a test or a scripted run.
263
+ const envFile = flags.envFile ?? env['PTERODOC_ENV_FILE'];
264
+ const merged = envFile
265
+ ? { ...dotenv.parse(fs.readFileSync(path.resolve(cwd(), envFile))), ...env }
266
+ : env;
267
+ return resolveConfig({ flags, file, fileDir, env: merged, configFile });
268
+ }
269
+
270
+ /** What a run intends to do, and what it did. */
271
+ /** Count the actions by operation. */
272
+ function summarise(actions) {
273
+ const summary = {};
274
+ for (const action of actions)
275
+ summary[action.op] = (summary[action.op] ?? 0) + 1;
276
+ return summary;
277
+ }
278
+
279
+ /**
280
+ * What a run leaves behind.
281
+ *
282
+ * The rendered pages and the plan are how a change is reviewed before it is
283
+ * published, and how a run that failed part way through is diagnosed.
284
+ */
285
+ /** Where one page's rendered body is written. */
286
+ function pageFile(outDir, locale, versionName, treePath) {
287
+ const parts = [outDir, 'pages', locale, versionName];
288
+ const name = treePath === '' ? 'index.html' : `${treePath}.html`;
289
+ return path.join(...parts, name);
290
+ }
291
+ /**
292
+ * Write the rendered pages, the manifest, the media map and the plan.
293
+ *
294
+ * Never called from a `finally`: a failure to write the artefacts must not
295
+ * replace the error that actually stopped the run.
296
+ */
297
+ async function writeArtifacts(outDir, artifacts, options) {
298
+ await fs$1.mkdir(outDir, { recursive: true });
299
+ if (options.writePages) {
300
+ await fs$1.rm(path.join(outDir, 'pages'), { recursive: true, force: true });
301
+ for (const { page, locale, versionName } of artifacts.pages) {
302
+ const file = pageFile(outDir, locale, versionName, page.path);
303
+ await fs$1.mkdir(path.dirname(file), { recursive: true });
304
+ await fs$1.writeFile(file, page.content, 'utf8');
305
+ }
306
+ }
307
+ const write = async (name, value) => {
308
+ await fs$1.writeFile(path.join(outDir, name), `${JSON.stringify(value, null, 2)}\n`, 'utf8');
309
+ };
310
+ await write('manifest.json', artifacts.manifest);
311
+ await write('media.json', artifacts.media);
312
+ await write('plan.json', artifacts.plan);
313
+ }
314
+
315
+ /**
316
+ * The reconciler.
317
+ *
318
+ * Two phases, because WordPress's navigation block needs a page id before any
319
+ * body can reference it: first make sure every page exists, then render every
320
+ * body against the real ids and write only what differs.
321
+ */
322
+ /** Run the sync. */
323
+ async function runSync(config, deps) {
324
+ const log = deps.log ?? (() => { });
325
+ const issues = new IssueCollector();
326
+ const actions = [];
327
+ const prepared = [];
328
+ const mediaRecords = [];
329
+ let requests = 0;
330
+ let mediaPending = 0;
331
+ let docusaurusVersion = null;
332
+ const locales = await selectLocales(config, deps.reader);
333
+ for (const locale of locales) {
334
+ const model = await deps.reader.read(locale);
335
+ docusaurusVersion ??= model.docusaurusVersion;
336
+ const theme = createTheme({
337
+ classPrefix: config.classPrefix,
338
+ blocks: config.blocks,
339
+ strings: { ...config.strings, ...config.localeStrings[model.locale] },
340
+ });
341
+ const session = deps.target && !deps.renderOnly && !config.offline
342
+ ? await deps.target.open({ locale: model.locale, dryRun: config.dryRun })
343
+ : undefined;
344
+ for (const instance of model.instances) {
345
+ for (const version of instance.versions) {
346
+ const result = await syncVersion({
347
+ config,
348
+ model,
349
+ instance: { admonitionKeywords: instance.admonitionKeywords },
350
+ version,
351
+ theme,
352
+ target: deps.target,
353
+ session,
354
+ issues,
355
+ log,
356
+ });
357
+ actions.push(...result.actions);
358
+ prepared.push(...result.prepared);
359
+ mediaRecords.push(...result.media);
360
+ mediaPending += result.mediaPending;
361
+ }
362
+ }
363
+ if (session)
364
+ requests += session.requestCount();
365
+ }
366
+ const rootPath = deps.target?.rootPath ?? '/';
367
+ const plan = {
368
+ generatedAt: new Date().toISOString(),
369
+ versions: { pterodoc: VERSION, docusaurus: docusaurusVersion, node: process.version },
370
+ dryRun: config.dryRun,
371
+ offline: config.offline,
372
+ site: config.targetUrl || null,
373
+ rootPath,
374
+ locales,
375
+ versionNames: [...new Set(prepared.map((entry) => entry.versionName))],
376
+ actions,
377
+ issues: issues.issues,
378
+ summary: summarise(actions),
379
+ requests,
380
+ mediaPending,
381
+ artifactError: null,
382
+ };
383
+ const artifacts = {
384
+ pages: prepared.map(({ page, locale, versionName }) => ({ page, locale, versionName })),
385
+ manifest: prepared.map(({ node, page, locale, versionName }) => ({
386
+ path: page.path,
387
+ title: page.title,
388
+ slug: page.slug,
389
+ parent: node.parent ? node.parent.path : null,
390
+ menuOrder: page.menuOrder,
391
+ locale,
392
+ versionName,
393
+ file: page.file ?? null,
394
+ href: deps.target ? deps.target.hrefFor(page.path, { versionName, locale }) : page.path,
395
+ })),
396
+ media: mediaRecords,
397
+ plan,
398
+ };
399
+ try {
400
+ await writeArtifacts(config.outDir, artifacts, { writePages: config.writePages });
401
+ }
402
+ catch (error) {
403
+ plan.artifactError = error instanceof Error ? error.message : String(error);
404
+ }
405
+ return { plan, artifacts };
406
+ }
407
+ /** Which locales to publish. */
408
+ async function selectLocales(config, reader) {
409
+ if (Array.isArray(config.locales))
410
+ return config.locales;
411
+ if (config.locales === 'all')
412
+ return reader.locales();
413
+ const model = await reader.read();
414
+ return [model.defaultLocale ?? model.locale];
415
+ }
416
+ /** Publish one version of one docs instance. */
417
+ async function syncVersion(input) {
418
+ const { config, model, version, theme, target, session, issues, log } = input;
419
+ const actions = [];
420
+ const locale = model.locale;
421
+ const tree = buildPageTree({
422
+ version,
423
+ sidebars: config.sidebars,
424
+ rootTitle: config.docsTitle,
425
+ includeUnlisted: config.includeUnlisted,
426
+ includeOrphans: config.includeOrphans,
427
+ issues,
428
+ });
429
+ const context = { versionName: version.name, locale };
430
+ const href = (treePath) => target ? target.hrefFor(treePath, context) : `/${treePath}`;
431
+ // Phase 0 and 1: make sure the pages exist, so their ids are known.
432
+ const ids = new Map();
433
+ const created = new Set();
434
+ let rootParentId = null;
435
+ if (session) {
436
+ await session.loadIndex();
437
+ const root = await session.ensureRootParent();
438
+ rootParentId = root.id;
439
+ for (const entry of root.created) {
440
+ actions.push({ op: 'create-root', path: entry.path, id: entry.id, locale });
441
+ }
442
+ }
443
+ const inScope = (treePath) => !config.only || treePath === config.only || treePath.startsWith(`${config.only}/`);
444
+ const isNeeded = (treePath) => inScope(treePath) || treePath === '' || config.only.startsWith(`${treePath}/`);
445
+ const nodes = [tree.root, ...tree.chain];
446
+ for (const node of nodes) {
447
+ if (!isNeeded(node.path))
448
+ continue;
449
+ const parentId = node.parent ? (ids.get(node.parent.path) ?? null) : rootParentId;
450
+ if (!session) {
451
+ ids.set(node.path, null);
452
+ continue;
453
+ }
454
+ const result = await session.ensurePage({
455
+ path: node.path,
456
+ slug: node.slug,
457
+ parentId,
458
+ title: node.title,
459
+ menuOrder: node.menuOrder,
460
+ isRoot: node.path === '',
461
+ });
462
+ for (const warning of result.warnings) {
463
+ issues.add({ code: 'target-warning', severity: 'warning', message: warning, path: node.path });
464
+ }
465
+ ids.set(node.path, result.id);
466
+ if (result.created) {
467
+ created.add(node.path);
468
+ actions.push({
469
+ op: 'create',
470
+ path: node.path,
471
+ id: result.id,
472
+ locale,
473
+ versionName: version.name,
474
+ file: node.doc?.sourceRelativePath ?? null,
475
+ });
476
+ if (result.id !== null)
477
+ log(`created ${node.path || '(root)'} (id ${result.id})`);
478
+ }
479
+ }
480
+ // Media: resolve every referenced file, upload what is new, and remember
481
+ // where each one ended up so the renderer can point at it.
482
+ const media = new Map();
483
+ const mediaRecords = [];
484
+ let mediaPending = 0;
485
+ if (config.uploadMedia) {
486
+ const result = await syncMedia({ config, model, version, tree, session, issues });
487
+ for (const [url, ref] of result.byUrl)
488
+ media.set(url, ref);
489
+ mediaRecords.push(...result.records);
490
+ mediaPending = result.pending;
491
+ actions.push(...result.actions);
492
+ }
493
+ // Phase 2: render against the real ids, and write what differs.
494
+ const navRootId = ids.get('') ?? null;
495
+ const prepared = [];
496
+ const banner = version.banner && !version.isLast
497
+ ? renderVersionBanner(theme, version.label, version.banner)
498
+ : undefined;
499
+ for (const node of nodes) {
500
+ const page = renderPageFor({
501
+ node,
502
+ tree,
503
+ config,
504
+ model,
505
+ theme,
506
+ href,
507
+ navRootId,
508
+ admonitionKeywords: input.instance.admonitionKeywords,
509
+ media,
510
+ issues,
511
+ ...(banner ? { banner } : {}),
512
+ });
513
+ prepared.push({ node, page, locale, versionName: version.name });
514
+ const id = ids.get(node.path);
515
+ if (!session || id === null || id === undefined)
516
+ continue;
517
+ if (!inScope(node.path) && !created.has(node.path))
518
+ continue;
519
+ const parentId = node.parent ? (ids.get(node.parent.path) ?? 0) : (rootParentId ?? 0);
520
+ const remote = await session.fetchPage(id);
521
+ const changed = session.diffPage(remote, page, parentId ?? 0);
522
+ if (changed.length === 0) {
523
+ actions.push({
524
+ op: 'unchanged',
525
+ path: node.path,
526
+ id,
527
+ locale,
528
+ versionName: version.name,
529
+ file: page.file ?? null,
530
+ });
531
+ continue;
532
+ }
533
+ actions.push({
534
+ op: 'update',
535
+ path: node.path,
536
+ id,
537
+ changed,
538
+ locale,
539
+ versionName: version.name,
540
+ file: page.file ?? null,
541
+ });
542
+ if (config.dryRun)
543
+ continue;
544
+ const { warnings } = await session.writePage(id, page, parentId ?? 0);
545
+ for (const warning of warnings) {
546
+ issues.add({ code: 'target-warning', severity: 'warning', message: warning, path: node.path });
547
+ }
548
+ log(`updated ${node.path || '(root)'} (${changed.join(', ')})`);
549
+ }
550
+ // Anything under this version's root that no document accounts for.
551
+ if (session && navRootId !== null) {
552
+ if (config.only) {
553
+ issues.add({
554
+ code: 'prune-skipped',
555
+ severity: 'info',
556
+ message: 'Pruning was skipped because --only limited the run.',
557
+ });
558
+ }
559
+ else {
560
+ const index = await session.loadIndex();
561
+ const keep = new Set();
562
+ for (const id of ids.values())
563
+ if (typeof id === 'number')
564
+ keep.add(id);
565
+ for (const page of session.computePrune(index, navRootId, keep)) {
566
+ actions.push({
567
+ op: 'prune',
568
+ path: page.slug,
569
+ id: page.id,
570
+ locale,
571
+ versionName: version.name,
572
+ applied: config.prune && !config.dryRun,
573
+ });
574
+ if (config.prune && !config.dryRun) {
575
+ await session.removePage(page);
576
+ log(`trashed ${page.link}`);
577
+ }
578
+ }
579
+ }
580
+ }
581
+ return { actions, prepared, media: mediaRecords, mediaPending };
582
+ }
583
+ /** Render one page, body and all. */
584
+ function renderPageFor(input) {
585
+ const { node, tree, config, model, theme, issues } = input;
586
+ const doc = node.doc;
587
+ let body = '';
588
+ let links = new Set();
589
+ let firstParagraph = '';
590
+ if (doc) {
591
+ const markdown = readDocument(doc, issues);
592
+ if (markdown !== undefined) {
593
+ const rendered = renderDoc({
594
+ markdown,
595
+ file: doc.sourceRelativePath,
596
+ permalink: doc.permalink,
597
+ format: doc.format,
598
+ theme,
599
+ admonitionKeywords: input.admonitionKeywords,
600
+ maintainCase: model.maintainCase,
601
+ dedupeTitle: config.dedupeTitle,
602
+ onUnknownJsx: config.mdxOnUnknown,
603
+ media: input.media,
604
+ issues,
605
+ resolveLink: makeLinkResolver(doc, tree, config, model, input.href),
606
+ });
607
+ body = rendered.body;
608
+ links = rendered.links;
609
+ firstParagraph = rendered.firstParagraph;
610
+ }
611
+ }
612
+ const content = composePage({
613
+ node,
614
+ body,
615
+ links,
616
+ href: input.href,
617
+ lookup: (treePath) => tree.byPath.get(treePath),
618
+ theme,
619
+ layout: config.layout,
620
+ navRootId: input.navRootId,
621
+ ...(input.banner ? { banner: input.banner } : {}),
622
+ });
623
+ const excerpt = excerptFor(doc?.description ?? '', firstParagraph, config.excerptLength);
624
+ const meta = {};
625
+ if (config.metaDescriptionKey && excerpt)
626
+ meta[config.metaDescriptionKey] = excerpt;
627
+ return {
628
+ path: node.path,
629
+ slug: node.slug,
630
+ title: node.title,
631
+ content,
632
+ excerpt,
633
+ menuOrder: node.menuOrder,
634
+ meta,
635
+ file: doc?.sourceRelativePath,
636
+ versionName: node.versionName,
637
+ };
638
+ }
639
+ /** Read a document's source, reporting rather than throwing when it is gone. */
640
+ function readDocument(doc, issues) {
641
+ try {
642
+ return readFileSync(doc.sourceAbsolutePath, 'utf8');
643
+ }
644
+ catch (error) {
645
+ issues.addFor(doc, {
646
+ code: 'source-unreadable',
647
+ severity: 'error',
648
+ message: `Could not read ${doc.sourceRelativePath}: ${error.message}`,
649
+ });
650
+ return undefined;
651
+ }
652
+ }
653
+ /** Decide where each link in a document should point. */
654
+ function makeLinkResolver(doc, tree, config, model, href) {
655
+ const siteUrl = (config.siteUrl || model.url).replace(/\/+$/, '');
656
+ const baseUrl = model.baseUrl.replace(/\/+$/, '');
657
+ /**
658
+ * A root-relative markdown link is relative to the site, not to the server.
659
+ * Docusaurus's router prepends the base URL, so a link written as
660
+ * `/docs/api/` on a site served from `/project/` really means
661
+ * `/project/docs/api/`.
662
+ */
663
+ const withBaseUrl = (urlPath) => !baseUrl || urlPath === baseUrl || urlPath.startsWith(`${baseUrl}/`)
664
+ ? urlPath
665
+ : `${baseUrl}${urlPath}`;
666
+ return (rawHref) => {
667
+ if (!rawHref || rawHref.startsWith('#'))
668
+ return { href: rawHref };
669
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(rawHref) || rawHref.startsWith('//')) {
670
+ return { href: rawHref };
671
+ }
672
+ const hashIndex = rawHref.indexOf('#');
673
+ const hash = hashIndex === -1 ? '' : rawHref.slice(hashIndex);
674
+ const target = hashIndex === -1 ? rawHref : rawHref.slice(0, hashIndex);
675
+ const node = target.startsWith('/')
676
+ ? findByUrl(tree, withBaseUrl(target))
677
+ : findBySourcePath(tree, doc, target);
678
+ if (node)
679
+ return { href: `${href(node.path)}${hash}`, path: node.path };
680
+ if (config.unpublishedLinks === 'drop')
681
+ return { href: null };
682
+ // Nothing here publishes it, so point at the documentation site, using the
683
+ // URL Docusaurus itself would serve.
684
+ const urlPath = target.startsWith('/')
685
+ ? withBaseUrl(path.posix.normalize(target))
686
+ : path.posix.normalize(path.posix.join(doc.permalink, '..', target.replace(/\.mdx?$/, '')));
687
+ return { href: `${siteUrl}${urlPath}${hash}` };
688
+ };
689
+ }
690
+ /** A page whose document is served at this URL. */
691
+ function findByUrl(tree, target) {
692
+ const urlPath = path.posix.normalize(target).replace(/\.mdx?$/, '');
693
+ return tree.byPermalink.get(urlPath) ?? tree.byPermalink.get(`${urlPath}/`);
694
+ }
695
+ /**
696
+ * A page whose source file a relative link points at.
697
+ *
698
+ * Relative links are resolved against the file, which is how Docusaurus
699
+ * resolves them. Resolving against the URL instead would break every document
700
+ * whose front matter gives it a slug of its own.
701
+ */
702
+ function findBySourcePath(tree, from, target) {
703
+ const base = path.posix.dirname(from.sourceRelativePath);
704
+ const resolved = path.posix.normalize(path.posix.join(base, target));
705
+ const withoutExtension = resolved.replace(/\.mdx?$/, '');
706
+ const candidates = [
707
+ resolved,
708
+ `${withoutExtension}.md`,
709
+ `${withoutExtension}.mdx`,
710
+ `${withoutExtension}/index.md`,
711
+ `${withoutExtension}/index.mdx`,
712
+ `${withoutExtension}/README.md`,
713
+ ];
714
+ for (const candidate of candidates) {
715
+ const node = tree.bySourcePath.get(candidate);
716
+ if (node)
717
+ return node;
718
+ }
719
+ return undefined;
720
+ }
721
+ /** Resolve, upload and record every file the documents reference. */
722
+ async function syncMedia(input) {
723
+ const { config, model, version, tree, session, issues } = input;
724
+ const byUrl = new Map();
725
+ const records = [];
726
+ const actions = [];
727
+ let pending = 0;
728
+ const remote = session ? await session.loadMediaIndex() : new Map();
729
+ const uploadedThisRun = new Map();
730
+ for (const node of [tree.root, ...tree.chain]) {
731
+ const doc = node.doc;
732
+ if (!doc)
733
+ continue;
734
+ let markdown;
735
+ try {
736
+ markdown = await fs$1.readFile(doc.sourceAbsolutePath, 'utf8');
737
+ }
738
+ catch {
739
+ continue;
740
+ }
741
+ for (const image of collectImages(markdown, doc.format)) {
742
+ if (byUrl.has(image.url))
743
+ continue;
744
+ const resolved = resolveImage(image.url, {
745
+ sourceAbsolutePath: doc.sourceAbsolutePath,
746
+ contentPath: version.contentPath,
747
+ contentPathLocalized: version.contentPathLocalized,
748
+ staticDirs: model.staticDirs,
749
+ baseUrl: model.baseUrl,
750
+ siteDir: model.siteDir,
751
+ });
752
+ if (resolved.kind === 'external')
753
+ continue;
754
+ if (resolved.kind === 'missing') {
755
+ if (config.mediaOnMissing !== 'ignore') {
756
+ issues.addFor(doc, {
757
+ code: 'asset-missing',
758
+ severity: config.mediaOnMissing,
759
+ message: `The file "${image.url}" was not found, so the reference was left as written.`,
760
+ });
761
+ }
762
+ continue;
763
+ }
764
+ const bytes = await fs$1.readFile(resolved.file);
765
+ const hash = contentHash(bytes);
766
+ const known = uploadedThisRun.get(hash) ?? remote.get(hash);
767
+ if (known) {
768
+ byUrl.set(image.url, { id: known.id, url: known.url });
769
+ records.push({ hash, file: resolved.file, url: known.url, uploaded: false });
770
+ actions.push({ op: 'reuse-media', path: node.path, id: known.id, file: resolved.file });
771
+ continue;
772
+ }
773
+ if (!session || config.dryRun) {
774
+ pending += 1;
775
+ records.push({ hash, file: resolved.file, url: null, uploaded: false });
776
+ actions.push({ op: 'upload-media', path: node.path, id: null, file: resolved.file });
777
+ continue;
778
+ }
779
+ const filename = path.basename(resolved.file);
780
+ const mime = mimeTypeFor(filename);
781
+ if (!mime) {
782
+ issues.addFor(doc, {
783
+ code: 'asset-unknown-type',
784
+ severity: 'warning',
785
+ message: `Not uploading "${image.url}": its file type is not one WordPress accepts.`,
786
+ });
787
+ continue;
788
+ }
789
+ const uploaded = await session.uploadMedia({
790
+ bytes,
791
+ filename,
792
+ hash,
793
+ mime,
794
+ alt: image.alt,
795
+ title: image.title ?? titleCase(path.parse(filename).name),
796
+ });
797
+ uploadedThisRun.set(hash, uploaded);
798
+ byUrl.set(image.url, { id: uploaded.id, url: uploaded.url });
799
+ records.push({ hash, file: resolved.file, url: uploaded.url, uploaded: true });
800
+ actions.push({ op: 'upload-media', path: node.path, id: uploaded.id, file: resolved.file });
801
+ }
802
+ }
803
+ return { byUrl, records, actions, pending };
804
+ }
805
+
806
+ export { CONFIG_NAMES, ConfigError, DEFAULT_LAYOUT, IssueCollector, VERSION, buildPageTree, composePage, createTheme, defineConfig, discoverConfigFile, loadConfig, renderDoc, resolveConfig, runSync };
807
+ //# sourceMappingURL=index.js.map