@docusaurus/plugin-content-docs 2.0.0-beta.1ab8aa0af → 2.0.0-beta.2

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 (47) hide show
  1. package/lib/.tsbuildinfo +1 -4661
  2. package/lib/cli.js +15 -10
  3. package/lib/client/docsClientUtils.d.ts +1 -1
  4. package/lib/client/docsClientUtils.js +3 -10
  5. package/lib/docFrontMatter.d.ts +1 -14
  6. package/lib/docFrontMatter.js +3 -2
  7. package/lib/docs.d.ts +1 -1
  8. package/lib/docs.js +22 -12
  9. package/lib/index.js +38 -17
  10. package/lib/lastUpdate.js +6 -6
  11. package/lib/markdown/linkify.js +1 -1
  12. package/lib/options.js +1 -6
  13. package/lib/props.js +4 -3
  14. package/lib/sidebarItemsGenerator.js +3 -3
  15. package/lib/sidebars.d.ts +3 -2
  16. package/lib/sidebars.js +28 -17
  17. package/lib/slug.js +2 -2
  18. package/lib/types.d.ts +18 -5
  19. package/lib/versions.js +54 -22
  20. package/package.json +13 -12
  21. package/src/__tests__/__fixtures__/simple-site/docs/foo/baz.md +4 -1
  22. package/src/__tests__/__fixtures__/simple-site/docs/hello.md +1 -0
  23. package/src/__tests__/__snapshots__/index.test.ts.snap +22 -13
  24. package/src/__tests__/cli.test.ts +16 -16
  25. package/src/__tests__/docFrontMatter.test.ts +47 -7
  26. package/src/__tests__/docs.test.ts +8 -5
  27. package/src/__tests__/index.test.ts +52 -12
  28. package/src/__tests__/lastUpdate.test.ts +3 -2
  29. package/src/__tests__/options.test.ts +0 -1
  30. package/src/__tests__/sidebars.test.ts +9 -8
  31. package/src/__tests__/versions.test.ts +34 -11
  32. package/src/cli.ts +17 -11
  33. package/src/client/__tests__/docsClientUtils.test.ts +6 -7
  34. package/src/client/docsClientUtils.ts +6 -16
  35. package/src/docFrontMatter.ts +5 -17
  36. package/src/docs.ts +28 -10
  37. package/src/index.ts +58 -21
  38. package/src/lastUpdate.ts +10 -6
  39. package/src/markdown/linkify.ts +1 -1
  40. package/src/options.ts +1 -15
  41. package/src/plugin-content-docs.d.ts +21 -0
  42. package/src/props.ts +8 -3
  43. package/src/sidebarItemsGenerator.ts +3 -3
  44. package/src/sidebars.ts +52 -19
  45. package/src/slug.ts +2 -2
  46. package/src/types.ts +23 -7
  47. package/src/versions.ts +88 -27
@@ -74,7 +74,7 @@ async function readCategoryMetadatasFile(
74
74
  } catch (e) {
75
75
  console.error(
76
76
  chalk.red(
77
- `The docs sidebar category metadata file looks invalid!\nPath=${filePath}`,
77
+ `The docs sidebar category metadata file looks invalid!\nPath: ${filePath}`,
78
78
  ),
79
79
  );
80
80
  throw e;
@@ -139,7 +139,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async functio
139
139
  ): string {
140
140
  if (!isInAutogeneratedDir(doc)) {
141
141
  throw new Error(
142
- 'getDocDirRelativeToAutogenDir() can only be called for subdocs of the sidebar autogen dir',
142
+ 'getDocDirRelativeToAutogenDir() can only be called for subdocs of the sidebar autogen dir.',
143
143
  );
144
144
  }
145
145
  // Is there a node API to compare 2 relative paths more easily?
@@ -160,7 +160,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async functio
160
160
  if (docs.length === 0) {
161
161
  console.warn(
162
162
  chalk.yellow(
163
- `No docs found in dir ${item.dirName}: can't auto-generate a sidebar`,
163
+ `No docs found in dir ${item.dirName}: can't auto-generate a sidebar.`,
164
164
  ),
165
165
  );
166
166
  }
package/src/sidebars.ts CHANGED
@@ -25,11 +25,13 @@ import {
25
25
  SidebarItemsGeneratorVersion,
26
26
  NumberPrefixParser,
27
27
  SidebarItemsGeneratorOption,
28
+ PluginOptions,
28
29
  } from './types';
29
30
  import {mapValues, flatten, flatMap, difference, pick, memoize} from 'lodash';
30
- import {getElementsAround} from '@docusaurus/utils';
31
+ import {getElementsAround, toMessageRelativeFilePath} from '@docusaurus/utils';
31
32
  import combinePromises from 'combine-promises';
32
33
  import {DefaultSidebarItemsGenerator} from './sidebarItemsGenerator';
34
+ import path from 'path';
33
35
 
34
36
  type SidebarItemCategoryJSON = SidebarItemBase & {
35
37
  type: 'category';
@@ -116,18 +118,21 @@ function assertIsCategory(
116
118
  assertItem(item, ['items', 'label', 'collapsed', 'customProps']);
117
119
  if (typeof item.label !== 'string') {
118
120
  throw new Error(
119
- `Error loading ${JSON.stringify(item)}. "label" must be a string.`,
121
+ `Error loading ${JSON.stringify(item)}: "label" must be a string.`,
120
122
  );
121
123
  }
122
124
  if (!Array.isArray(item.items)) {
123
125
  throw new Error(
124
- `Error loading ${JSON.stringify(item)}. "items" must be an array.`,
126
+ `Error loading ${JSON.stringify(item)}: "items" must be an array.`,
125
127
  );
126
128
  }
127
129
  // "collapsed" is an optional property
128
- if (item.hasOwnProperty('collapsed') && typeof item.collapsed !== 'boolean') {
130
+ if (
131
+ typeof item.collapsed !== 'undefined' &&
132
+ typeof item.collapsed !== 'boolean'
133
+ ) {
129
134
  throw new Error(
130
- `Error loading ${JSON.stringify(item)}. "collapsed" must be a boolean.`,
135
+ `Error loading ${JSON.stringify(item)}: "collapsed" must be a boolean.`,
131
136
  );
132
137
  }
133
138
  }
@@ -138,14 +143,14 @@ function assertIsAutogenerated(
138
143
  assertItem(item, ['dirName', 'customProps']);
139
144
  if (typeof item.dirName !== 'string') {
140
145
  throw new Error(
141
- `Error loading ${JSON.stringify(item)}. "dirName" must be a string.`,
146
+ `Error loading ${JSON.stringify(item)}: "dirName" must be a string.`,
142
147
  );
143
148
  }
144
149
  if (item.dirName.startsWith('/') || item.dirName.endsWith('/')) {
145
150
  throw new Error(
146
151
  `Error loading ${JSON.stringify(
147
152
  item,
148
- )}. "dirName" must be a dir path relative to the docs folder root, and should not start or end with /`,
153
+ )}: "dirName" must be a dir path relative to the docs folder root, and should not start or end with slash`,
149
154
  );
150
155
  }
151
156
  }
@@ -156,13 +161,13 @@ function assertIsDoc(
156
161
  assertItem(item, ['id', 'label', 'customProps']);
157
162
  if (typeof item.id !== 'string') {
158
163
  throw new Error(
159
- `Error loading ${JSON.stringify(item)}. "id" must be a string.`,
164
+ `Error loading ${JSON.stringify(item)}: "id" must be a string.`,
160
165
  );
161
166
  }
162
167
 
163
168
  if (item.label && typeof item.label !== 'string') {
164
169
  throw new Error(
165
- `Error loading ${JSON.stringify(item)}. "label" must be a string.`,
170
+ `Error loading ${JSON.stringify(item)}: "label" must be a string.`,
166
171
  );
167
172
  }
168
173
  }
@@ -173,12 +178,12 @@ function assertIsLink(
173
178
  assertItem(item, ['href', 'label', 'customProps']);
174
179
  if (typeof item.href !== 'string') {
175
180
  throw new Error(
176
- `Error loading ${JSON.stringify(item)}. "href" must be a string.`,
181
+ `Error loading ${JSON.stringify(item)}: "href" must be a string.`,
177
182
  );
178
183
  }
179
184
  if (typeof item.label !== 'string') {
180
185
  throw new Error(
181
- `Error loading ${JSON.stringify(item)}. "label" must be a string.`,
186
+ `Error loading ${JSON.stringify(item)}: "label" must be a string.`,
182
187
  );
183
188
  }
184
189
  }
@@ -222,12 +227,12 @@ function normalizeItem(item: SidebarItemJSON): UnprocessedSidebarItem[] {
222
227
  default: {
223
228
  const extraMigrationError =
224
229
  item.type === 'subcategory'
225
- ? "Docusaurus v2: 'subcategory' has been renamed as 'category'"
230
+ ? 'Docusaurus v2: "subcategory" has been renamed as "category".'
226
231
  : '';
227
232
  throw new Error(
228
- `Unknown sidebar item type [${
233
+ `Unknown sidebar item type "${
229
234
  item.type
230
- }]. Sidebar item=${JSON.stringify(item)} ${extraMigrationError}`,
235
+ }". Sidebar item is ${JSON.stringify(item)}.\n${extraMigrationError}`,
231
236
  );
232
237
  }
233
238
  }
@@ -256,7 +261,19 @@ export const DefaultSidebars: UnprocessedSidebars = {
256
261
 
257
262
  export const DisabledSidebars: UnprocessedSidebars = {};
258
263
 
264
+ // If a path is provided, make it absolute
265
+ // use this before loadSidebars()
266
+ export function resolveSidebarPathOption(
267
+ siteDir: string,
268
+ sidebarPathOption: PluginOptions['sidebarPath'],
269
+ ): PluginOptions['sidebarPath'] {
270
+ return sidebarPathOption
271
+ ? path.resolve(siteDir, sidebarPathOption)
272
+ : sidebarPathOption;
273
+ }
274
+
259
275
  // TODO refactor: make async
276
+ // Note: sidebarFilePath must be absolute, use resolveSidebarPathOption
260
277
  export function loadSidebars(
261
278
  sidebarFilePath: string | false | undefined,
262
279
  ): UnprocessedSidebars {
@@ -279,6 +296,7 @@ export function loadSidebars(
279
296
 
280
297
  // We don't want sidebars to be cached because of hot reloading.
281
298
  const sidebarJson = importFresh(sidebarFilePath) as SidebarsJSON;
299
+
282
300
  return normalizeSidebars(sidebarJson);
283
301
  }
284
302
 
@@ -423,7 +441,20 @@ export function collectSidebarsDocIds(
423
441
  });
424
442
  }
425
443
 
426
- export function createSidebarsUtils(sidebars: Sidebars) {
444
+ export function createSidebarsUtils(
445
+ sidebars: Sidebars,
446
+ ): {
447
+ getFirstDocIdOfFirstSidebar: () => string | undefined;
448
+ getSidebarNameByDocId: (docId: string) => string | undefined;
449
+ getDocNavigation: (
450
+ docId: string,
451
+ ) => {
452
+ sidebarName: string | undefined;
453
+ previousId: string | undefined;
454
+ nextId: string | undefined;
455
+ };
456
+ checkSidebarsDocIds: (validDocIds: string[], sidebarFilePath: string) => void;
457
+ } {
427
458
  const sidebarNameToDocIds = collectSidebarsDocIds(sidebars);
428
459
 
429
460
  function getFirstDocIdOfFirstSidebar(): string | undefined {
@@ -465,16 +496,18 @@ export function createSidebarsUtils(sidebars: Sidebars) {
465
496
  }
466
497
  }
467
498
 
468
- function checkSidebarsDocIds(validDocIds: string[]) {
499
+ function checkSidebarsDocIds(validDocIds: string[], sidebarFilePath: string) {
469
500
  const allSidebarDocIds = flatten(Object.values(sidebarNameToDocIds));
470
501
  const invalidSidebarDocIds = difference(allSidebarDocIds, validDocIds);
471
502
  if (invalidSidebarDocIds.length > 0) {
472
503
  throw new Error(
473
- `Bad sidebars file.
504
+ `Invalid sidebar file at "${toMessageRelativeFilePath(
505
+ sidebarFilePath,
506
+ )}".
474
507
  These sidebar document ids do not exist:
475
- - ${invalidSidebarDocIds.sort().join('\n- ')},
508
+ - ${invalidSidebarDocIds.sort().join('\n- ')}
476
509
 
477
- Available document ids=
510
+ Available document ids are:
478
511
  - ${validDocIds.sort().join('\n- ')}`,
479
512
  );
480
513
  }
package/src/slug.ts CHANGED
@@ -47,8 +47,8 @@ export default function getSlug({
47
47
 
48
48
  if (!isValidPathname(slug)) {
49
49
  throw new Error(
50
- `We couldn't compute a valid slug for document with id=${baseID} in folder=${dirName}
51
- The slug we computed looks invalid: ${slug}
50
+ `We couldn't compute a valid slug for document with id "${baseID}" in "${dirName}" directory.
51
+ The slug we computed looks invalid: ${slug}.
52
52
  Maybe your slug frontmatter is incorrect or you use weird chars in the file path?
53
53
  By using the slug frontmatter, you should be able to fix this error, by using the slug of your choice:
54
54
 
package/src/types.ts CHANGED
@@ -30,6 +30,7 @@ export type VersionMetadata = ContentPaths & {
30
30
  versionPath: string; // /baseUrl/docs/1.0.0
31
31
  versionEditUrl?: string | undefined;
32
32
  versionEditUrlLocalized?: string | undefined;
33
+ versionBanner: VersionBanner;
33
34
  isLast: boolean;
34
35
  sidebarFilePath: string | false | undefined; // versioned_sidebars/1.0.0.json
35
36
  routePriority: number | undefined; // -1 for the latest docs
@@ -59,9 +60,13 @@ export type PathOptions = {
59
60
  sidebarPath?: string | false | undefined;
60
61
  };
61
62
 
63
+ // TODO support custom version banner? {type: "error", content: "html content"}
64
+ export type VersionBanner = 'none' | 'unreleased' | 'unmaintained';
65
+
62
66
  export type VersionOptions = {
63
67
  path?: string;
64
68
  label?: string;
69
+ banner?: VersionBanner;
65
70
  };
66
71
 
67
72
  export type VersionsOptions = {
@@ -80,7 +85,6 @@ export type PluginOptions = MetadataOptions &
80
85
  docItemComponent: string;
81
86
  admonitions: Record<string, unknown>;
82
87
  disableVersioning: boolean;
83
- excludeNextVersionDocs?: boolean;
84
88
  includeCurrentVersion: boolean;
85
89
  sidebarItemsGenerator: SidebarItemsGeneratorOption;
86
90
  };
@@ -181,9 +185,23 @@ export type LastUpdateData = {
181
185
  lastUpdatedBy?: string;
182
186
  };
183
187
 
184
- export type FrontMatter = {
185
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
- [key: string]: any;
188
+ export type DocFrontMatter = {
189
+ // Front matter uses snake case
190
+ /* eslint-disable camelcase */
191
+ id?: string;
192
+ title?: string;
193
+ hide_title?: boolean;
194
+ hide_table_of_contents?: boolean;
195
+ keywords?: string[];
196
+ image?: string;
197
+ description?: string;
198
+ slug?: string;
199
+ sidebar_label?: string;
200
+ sidebar_position?: number;
201
+ pagination_label?: string;
202
+ custom_edit_url?: string | null;
203
+ parse_number_prefixes?: boolean;
204
+ /* eslint-enable camelcase */
187
205
  };
188
206
 
189
207
  export type DocMetadataBase = LastUpdateData & {
@@ -197,11 +215,9 @@ export type DocMetadataBase = LastUpdateData & {
197
215
  sourceDirName: string; // relative to the docs folder (can be ".")
198
216
  slug: string;
199
217
  permalink: string;
200
- // eslint-disable-next-line camelcase
201
- sidebar_label?: string;
202
218
  sidebarPosition?: number;
203
219
  editUrl?: string | null;
204
- frontMatter: FrontMatter;
220
+ frontMatter: DocFrontMatter & Record<string, unknown>;
205
221
  };
206
222
 
207
223
  export type DocNavLink = {
package/src/versions.ts CHANGED
@@ -9,6 +9,7 @@ import path from 'path';
9
9
  import fs from 'fs-extra';
10
10
  import {
11
11
  PluginOptions,
12
+ VersionBanner,
12
13
  VersionMetadata,
13
14
  VersionOptions,
14
15
  VersionsOptions,
@@ -24,6 +25,7 @@ import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
24
25
  import {LoadContext} from '@docusaurus/types';
25
26
  import {getPluginI18nPath, normalizeUrl, posixPath} from '@docusaurus/utils';
26
27
  import {difference} from 'lodash';
28
+ import {resolveSidebarPathOption} from './sidebars';
27
29
 
28
30
  // retro-compatibility: no prefix for the default plugin id
29
31
  function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
@@ -58,12 +60,12 @@ export function getVersionsFilePath(siteDir: string, pluginId: string): string {
58
60
  function ensureValidVersionString(version: unknown): asserts version is string {
59
61
  if (typeof version !== 'string') {
60
62
  throw new Error(
61
- `versions should be strings. Found type=[${typeof version}] for version=[${version}]`,
63
+ `Versions should be strings. Found type "${typeof version}" for version "${version}".`,
62
64
  );
63
65
  }
64
66
  // Should we forbid versions with special chars like / ?
65
67
  if (version.trim().length === 0) {
66
- throw new Error(`Invalid version=[${version}]`);
68
+ throw new Error(`Invalid version "${version}".`);
67
69
  }
68
70
  }
69
71
 
@@ -72,7 +74,7 @@ function ensureValidVersionArray(
72
74
  ): asserts versionArray is string[] {
73
75
  if (!(versionArray instanceof Array)) {
74
76
  throw new Error(
75
- `The versions file should contain an array of versions! Found content=${JSON.stringify(
77
+ `The versions file should contain an array of versions! Found content: ${JSON.stringify(
76
78
  versionArray,
77
79
  )}`,
78
80
  );
@@ -105,7 +107,7 @@ function readVersionNames(
105
107
 
106
108
  if (!versionFileContent && options.disableVersioning) {
107
109
  throw new Error(
108
- `Docs: using disableVersioning=${options.disableVersioning} option on a non-versioned site does not make sense`,
110
+ `Docs: using "disableVersioning=${options.disableVersioning}" option on a non-versioned site does not make sense.`,
109
111
  );
110
112
  }
111
113
 
@@ -123,7 +125,7 @@ function readVersionNames(
123
125
 
124
126
  if (versions.length === 0) {
125
127
  throw new Error(
126
- `It is not possible to use docs without any version. Please check the configuration of these options: includeCurrentVersion=${options.includeCurrentVersion} disableVersioning=${options.disableVersioning}`,
128
+ `It is not possible to use docs without any version. Please check the configuration of these options: "includeCurrentVersion=${options.includeCurrentVersion}", "disableVersioning=${options.disableVersioning}".`,
127
129
  );
128
130
  }
129
131
 
@@ -184,9 +186,7 @@ function getVersionMetadataPaths({
184
186
 
185
187
  function getSidebarFilePath() {
186
188
  if (isCurrentVersion) {
187
- return options.sidebarPath
188
- ? path.resolve(context.siteDir, options.sidebarPath)
189
- : options.sidebarPath;
189
+ return resolveSidebarPathOption(context.siteDir, options.sidebarPath);
190
190
  } else {
191
191
  return path.join(
192
192
  getVersionedSidebarsDirPath(context.siteDir, options.id),
@@ -256,14 +256,64 @@ function getVersionEditUrls({
256
256
  };
257
257
  }
258
258
 
259
+ function getDefaultVersionBanner({
260
+ versionName,
261
+ versionNames,
262
+ lastVersionName,
263
+ }: {
264
+ versionName: string;
265
+ versionNames: string[];
266
+ lastVersionName: string;
267
+ }): VersionBanner {
268
+ // Current version: good, no banner
269
+ if (versionName === lastVersionName) {
270
+ return 'none';
271
+ }
272
+ // Upcoming versions: unreleased banner
273
+ else if (
274
+ versionNames.indexOf(versionName) < versionNames.indexOf(lastVersionName)
275
+ ) {
276
+ return 'unreleased';
277
+ }
278
+ // Older versions: display unmaintained banner
279
+ else {
280
+ return 'unmaintained';
281
+ }
282
+ }
283
+
284
+ function getVersionBanner({
285
+ versionName,
286
+ versionNames,
287
+ lastVersionName,
288
+ options,
289
+ }: {
290
+ versionName: string;
291
+ versionNames: string[];
292
+ lastVersionName: string;
293
+ options: Pick<PluginOptions, 'versions'>;
294
+ }): VersionBanner {
295
+ const versionOptionBanner = options.versions[versionName]?.banner;
296
+
297
+ return (
298
+ versionOptionBanner ??
299
+ getDefaultVersionBanner({
300
+ versionName,
301
+ versionNames,
302
+ lastVersionName,
303
+ })
304
+ );
305
+ }
306
+
259
307
  function createVersionMetadata({
260
308
  versionName,
261
- isLast,
309
+ versionNames,
310
+ lastVersionName,
262
311
  context,
263
312
  options,
264
313
  }: {
265
314
  versionName: string;
266
- isLast: boolean;
315
+ versionNames: string[];
316
+ lastVersionName: string;
267
317
  context: Pick<LoadContext, 'siteDir' | 'baseUrl' | 'i18n'>;
268
318
  options: Pick<
269
319
  PluginOptions,
@@ -286,14 +336,18 @@ function createVersionMetadata({
286
336
  options,
287
337
  });
288
338
 
339
+ const isLast = versionName === lastVersionName;
340
+
289
341
  // retro-compatible values
290
342
  const defaultVersionLabel =
291
343
  versionName === CURRENT_VERSION_NAME ? 'Next' : versionName;
292
- const defaultVersionPathPart = isLast
293
- ? ''
294
- : versionName === CURRENT_VERSION_NAME
295
- ? 'next'
296
- : versionName;
344
+ function getDefaultVersionPathPart() {
345
+ if (isLast) {
346
+ return '';
347
+ }
348
+ return versionName === CURRENT_VERSION_NAME ? 'next' : versionName;
349
+ }
350
+ const defaultVersionPathPart = getDefaultVersionPathPart();
297
351
 
298
352
  const versionOptions: VersionOptions = options.versions[versionName] ?? {};
299
353
 
@@ -322,6 +376,12 @@ function createVersionMetadata({
322
376
  versionPath,
323
377
  versionEditUrl: versionEditUrls?.versionEditUrl,
324
378
  versionEditUrlLocalized: versionEditUrls?.versionEditUrlLocalized,
379
+ versionBanner: getVersionBanner({
380
+ versionName,
381
+ versionNames,
382
+ lastVersionName,
383
+ options,
384
+ }),
325
385
  isLast,
326
386
  routePriority,
327
387
  sidebarFilePath,
@@ -343,10 +403,10 @@ function checkVersionMetadataPaths({
343
403
 
344
404
  if (!fs.existsSync(contentPath)) {
345
405
  throw new Error(
346
- `The docs folder does not exist for version [${versionName}]. A docs folder is expected to be found at ${path.relative(
406
+ `The docs folder does not exist for version "${versionName}". A docs folder is expected to be found at ${path.relative(
347
407
  siteDir,
348
408
  contentPath,
349
- )}`,
409
+ )}.`,
350
410
  );
351
411
  }
352
412
 
@@ -359,11 +419,11 @@ function checkVersionMetadataPaths({
359
419
  typeof sidebarFilePath === 'string' &&
360
420
  !fs.existsSync(sidebarFilePath)
361
421
  ) {
362
- throw new Error(`The path to the sidebar file does not exist at [${path.relative(
422
+ throw new Error(`The path to the sidebar file does not exist at "${path.relative(
363
423
  siteDir,
364
424
  sidebarFilePath,
365
- )}].
366
- Please set the docs [sidebarPath] field in your config file to:
425
+ )}".
426
+ Please set the docs "sidebarPath" field in your config file to:
367
427
  - a sidebars path that exists
368
428
  - false: to disable the sidebar
369
429
  - undefined: for Docusaurus generates it automatically`);
@@ -404,16 +464,16 @@ function checkVersionsOptions(
404
464
  );
405
465
  if (unknownVersionConfigNames.length > 0) {
406
466
  throw new Error(
407
- `Bad docs options.versions: unknown versions found: ${unknownVersionConfigNames.join(
467
+ `Invalid docs option "versions": unknown versions (${unknownVersionConfigNames.join(
408
468
  ',',
409
- )}. ${availableVersionNamesMsg}`,
469
+ )}) found. ${availableVersionNamesMsg}`,
410
470
  );
411
471
  }
412
472
 
413
473
  if (options.onlyIncludeVersions) {
414
474
  if (options.onlyIncludeVersions.length === 0) {
415
475
  throw new Error(
416
- `Bad docs options.onlyIncludeVersions: an empty array is not allowed, at least one version is needed`,
476
+ `Invalid docs option "onlyIncludeVersions": an empty array is not allowed, at least one version is needed.`,
417
477
  );
418
478
  }
419
479
  const unknownOnlyIncludeVersionNames = difference(
@@ -422,9 +482,9 @@ function checkVersionsOptions(
422
482
  );
423
483
  if (unknownOnlyIncludeVersionNames.length > 0) {
424
484
  throw new Error(
425
- `Bad docs options.onlyIncludeVersions: unknown versions found: ${unknownOnlyIncludeVersionNames.join(
485
+ `Invalid docs option "onlyIncludeVersions": unknown versions (${unknownOnlyIncludeVersionNames.join(
426
486
  ',',
427
- )}. ${availableVersionNamesMsg}`,
487
+ )}) found. ${availableVersionNamesMsg}`,
428
488
  );
429
489
  }
430
490
  if (
@@ -432,7 +492,7 @@ function checkVersionsOptions(
432
492
  !options.onlyIncludeVersions.includes(options.lastVersion)
433
493
  ) {
434
494
  throw new Error(
435
- `Bad docs options.lastVersion: if you use both the onlyIncludeVersions and lastVersion options, then lastVersion must be present in the provided onlyIncludeVersions array`,
495
+ `Invalid docs option "lastVersion": if you use both the "onlyIncludeVersions" and "lastVersion" options, then "lastVersion" must be present in the provided "onlyIncludeVersions" array.`,
436
496
  );
437
497
  }
438
498
  }
@@ -487,7 +547,8 @@ export function readVersionsMetadata({
487
547
  const versionsMetadata = versionNames.map((versionName) =>
488
548
  createVersionMetadata({
489
549
  versionName,
490
- isLast: versionName === lastVersionName,
550
+ versionNames,
551
+ lastVersionName,
491
552
  context,
492
553
  options,
493
554
  }),