@docusaurus/plugin-content-docs 3.7.0 → 3.8.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.
package/lib/cli.js CHANGED
@@ -76,6 +76,23 @@ async function cliDocsVersionCommand(version, { id: pluginId, path: docsPath, si
76
76
  versionName: version,
77
77
  });
78
78
  await fs_extra_1.default.copy(docsDir, newVersionDir);
79
+ // Copy version JSON translation file for this locale
80
+ // i18n/<l>/docusaurus-plugin-content-docs/current.json => version-v1.json
81
+ // See https://docusaurus.io/docs/next/api/plugins/@docusaurus/plugin-content-docs#translation-files-location
82
+ if (locale !== i18n.defaultLocale) {
83
+ const dir = (0, files_1.getPluginDirPathLocalized)({
84
+ localizationDir,
85
+ pluginId,
86
+ });
87
+ const sourceFile = path_1.default.join(dir, 'current.json');
88
+ const dest = path_1.default.join(dir, `version-${version}.json`);
89
+ if (await fs_extra_1.default.pathExists(sourceFile)) {
90
+ await fs_extra_1.default.copy(sourceFile, dest);
91
+ }
92
+ else {
93
+ logger_1.default.warn `${pluginIdLogPrefix}: i18n translation file does not exist in path=${sourceFile}. Skipping.`;
94
+ }
95
+ }
79
96
  }));
80
97
  await createVersionedSidebarFile({
81
98
  siteDir,
@@ -34,6 +34,11 @@ export declare function findFirstSidebarItemLink(item: PropSidebarItem): string
34
34
  * on category index pages.
35
35
  */
36
36
  export declare function useCurrentSidebarCategory(): PropSidebarItemCategory;
37
+ /**
38
+ * Gets the category associated with the current location. Should only be used
39
+ * on category index pages.
40
+ */
41
+ export declare function useCurrentSidebarSiblings(): PropSidebarItem[];
37
42
  /**
38
43
  * Checks if a sidebar item should be active, based on the active path.
39
44
  */
@@ -91,6 +91,24 @@ export function useCurrentSidebarCategory() {
91
91
  }
92
92
  return deepestCategory;
93
93
  }
94
+ /**
95
+ * Gets the category associated with the current location. Should only be used
96
+ * on category index pages.
97
+ */
98
+ export function useCurrentSidebarSiblings() {
99
+ const { pathname } = useLocation();
100
+ const sidebar = useDocsSidebar();
101
+ if (!sidebar) {
102
+ throw new Error('Unexpected: cant find current sidebar in context');
103
+ }
104
+ const categoryBreadcrumbs = getSidebarBreadcrumbs({
105
+ sidebarItems: sidebar.items,
106
+ pathname,
107
+ onlyCategories: true,
108
+ });
109
+ const deepestCategory = categoryBreadcrumbs.slice(-1)[0];
110
+ return deepestCategory?.items ?? sidebar.items;
111
+ }
94
112
  const isActive = (testedPath, activePath) => typeof testedPath !== 'undefined' && isSamePath(testedPath, activePath);
95
113
  const containsActiveSidebarItem = (items, activePath) => items.some((subItem) => isActiveSidebarItem(subItem, activePath));
96
114
  /**
@@ -5,7 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import type { UseDataOptions } from '@docusaurus/types';
8
- export { useDocById, findSidebarCategory, findFirstSidebarItemLink, isActiveSidebarItem, isVisibleSidebarItem, useVisibleSidebarItems, useSidebarBreadcrumbs, useDocsVersionCandidates, useLayoutDoc, useLayoutDocsSidebar, useDocRootMetadata, useCurrentSidebarCategory, filterDocCardListItems, } from './docsUtils';
8
+ export { useDocById, findSidebarCategory, findFirstSidebarItemLink, isActiveSidebarItem, isVisibleSidebarItem, useVisibleSidebarItems, useSidebarBreadcrumbs, useDocsVersionCandidates, useLayoutDoc, useLayoutDocsSidebar, useDocRootMetadata, useCurrentSidebarCategory, useCurrentSidebarSiblings, filterDocCardListItems, } from './docsUtils';
9
9
  export { useDocsPreferredVersion } from './docsPreferredVersion';
10
10
  export { DocSidebarItemsExpandedStateProvider, useDocSidebarItemsExpandedState, } from './docSidebarItemsExpandedState';
11
11
  export { DocsVersionProvider, useDocsVersion } from './docsVersion';
@@ -13,6 +13,7 @@ export { DocsSidebarProvider, useDocsSidebar } from './docsSidebar';
13
13
  export { DocProvider, useDoc, type DocContextValue } from './doc';
14
14
  export { useDocsPreferredVersionByPluginId, DocsPreferredVersionContextProvider, } from './docsPreferredVersion';
15
15
  export { useDocsContextualSearchTags, getDocsVersionSearchTag, } from './docsSearch';
16
+ export { useBreadcrumbsStructuredData } from './structuredDataUtils';
16
17
  export type ActivePlugin = {
17
18
  pluginId: string;
18
19
  pluginData: GlobalPluginData;
@@ -7,7 +7,7 @@
7
7
  import { useLocation } from '@docusaurus/router';
8
8
  import { useAllPluginInstancesData, usePluginData, } from '@docusaurus/useGlobalData';
9
9
  import { getActivePlugin, getLatestVersion, getActiveVersion, getActiveDocContext, getDocVersionSuggestions, } from './docsClientUtils';
10
- export { useDocById, findSidebarCategory, findFirstSidebarItemLink, isActiveSidebarItem, isVisibleSidebarItem, useVisibleSidebarItems, useSidebarBreadcrumbs, useDocsVersionCandidates, useLayoutDoc, useLayoutDocsSidebar, useDocRootMetadata, useCurrentSidebarCategory, filterDocCardListItems, } from './docsUtils';
10
+ export { useDocById, findSidebarCategory, findFirstSidebarItemLink, isActiveSidebarItem, isVisibleSidebarItem, useVisibleSidebarItems, useSidebarBreadcrumbs, useDocsVersionCandidates, useLayoutDoc, useLayoutDocsSidebar, useDocRootMetadata, useCurrentSidebarCategory, useCurrentSidebarSiblings, filterDocCardListItems, } from './docsUtils';
11
11
  export { useDocsPreferredVersion } from './docsPreferredVersion';
12
12
  export { DocSidebarItemsExpandedStateProvider, useDocSidebarItemsExpandedState, } from './docSidebarItemsExpandedState';
13
13
  export { DocsVersionProvider, useDocsVersion } from './docsVersion';
@@ -15,6 +15,7 @@ export { DocsSidebarProvider, useDocsSidebar } from './docsSidebar';
15
15
  export { DocProvider, useDoc } from './doc';
16
16
  export { useDocsPreferredVersionByPluginId, DocsPreferredVersionContextProvider, } from './docsPreferredVersion';
17
17
  export { useDocsContextualSearchTags, getDocsVersionSearchTag, } from './docsSearch';
18
+ export { useBreadcrumbsStructuredData } from './structuredDataUtils';
18
19
  // Important to use a constant object to avoid React useEffect executions etc.
19
20
  // see https://github.com/facebook/docusaurus/issues/5089
20
21
  const StableEmptyObject = {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import type { PropSidebarBreadcrumbsItem } from '@docusaurus/plugin-content-docs';
8
+ import type { WithContext, BreadcrumbList } from 'schema-dts';
9
+ export declare function useBreadcrumbsStructuredData({ breadcrumbs, }: {
10
+ breadcrumbs: PropSidebarBreadcrumbsItem[];
11
+ }): WithContext<BreadcrumbList>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
8
+ export function useBreadcrumbsStructuredData({ breadcrumbs, }) {
9
+ const { siteConfig } = useDocusaurusContext();
10
+ return {
11
+ '@context': 'https://schema.org',
12
+ '@type': 'BreadcrumbList',
13
+ itemListElement: breadcrumbs
14
+ // We filter breadcrumb items without links, they are not allowed
15
+ // See also https://github.com/facebook/docusaurus/issues/9319#issuecomment-2643560845
16
+ .filter((breadcrumb) => breadcrumb.href)
17
+ .map((breadcrumb, index) => ({
18
+ '@type': 'ListItem',
19
+ position: index + 1,
20
+ name: breadcrumb.label,
21
+ item: `${siteConfig.url}${breadcrumb.href}`,
22
+ })),
23
+ };
24
+ }
package/lib/index.js CHANGED
@@ -10,6 +10,7 @@ exports.validateOptions = void 0;
10
10
  exports.default = pluginContentDocs;
11
11
  const tslib_1 = require("tslib");
12
12
  const path_1 = tslib_1.__importDefault(require("path"));
13
+ const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
13
14
  const lodash_1 = tslib_1.__importDefault(require("lodash"));
14
15
  const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
15
16
  const utils_1 = require("@docusaurus/utils");
@@ -26,6 +27,21 @@ const translations_1 = require("./translations");
26
27
  const routes_1 = require("./routes");
27
28
  const utils_2 = require("./sidebars/utils");
28
29
  const contentHelpers_1 = require("./contentHelpers");
30
+ // MDX loader is not 100% deterministic, leading to cache invalidation issue
31
+ // This permits to invalidate the MDX loader cache entries when content changes
32
+ // Problem documented here: https://github.com/facebook/docusaurus/pull/10934
33
+ // TODO this is not a perfect solution, find better?
34
+ async function createMdxLoaderDependencyFile({ dataDir, options, versionsMetadata, }) {
35
+ const filePath = path_1.default.join(dataDir, '__mdx-loader-dependency.json');
36
+ // the cache is invalidated whenever this file content changes
37
+ const fileContent = {
38
+ options,
39
+ versionsMetadata,
40
+ };
41
+ await fs_extra_1.default.ensureDir(dataDir);
42
+ await fs_extra_1.default.writeFile(filePath, JSON.stringify(fileContent));
43
+ return filePath;
44
+ }
29
45
  async function pluginContentDocs(context, options) {
30
46
  const { siteDir, generatedFilesDir, baseUrl, siteConfig } = context;
31
47
  // Mutate options to resolve sidebar path according to siteDir
@@ -50,6 +66,13 @@ async function pluginContentDocs(context, options) {
50
66
  return (0, mdx_loader_1.createMDXLoaderRule)({
51
67
  include: contentDirs,
52
68
  options: {
69
+ dependencies: [
70
+ await createMdxLoaderDependencyFile({
71
+ dataDir,
72
+ options,
73
+ versionsMetadata,
74
+ }),
75
+ ].filter((d) => typeof d === 'string'),
53
76
  useCrossCompilerCache: siteConfig.future.experimental_faster.mdxCrossCompilerCache,
54
77
  admonitions: options.admonitions,
55
78
  remarkPlugins,
@@ -15,6 +15,10 @@ export declare function getDocsDirPathLocalized({ localizationDir, pluginId, ver
15
15
  pluginId: string;
16
16
  versionName: string;
17
17
  }): string;
18
+ export declare function getPluginDirPathLocalized({ localizationDir, pluginId, }: {
19
+ localizationDir: string;
20
+ pluginId: string;
21
+ }): string;
18
22
  /** `community` => `[siteDir]/community_versions.json` */
19
23
  export declare function getVersionsFilePath(siteDir: string, pluginId: string): string;
20
24
  /**
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.getVersionDocsDirPath = getVersionDocsDirPath;
10
10
  exports.getVersionSidebarsPath = getVersionSidebarsPath;
11
11
  exports.getDocsDirPathLocalized = getDocsDirPathLocalized;
12
+ exports.getPluginDirPathLocalized = getPluginDirPathLocalized;
12
13
  exports.getVersionsFilePath = getVersionsFilePath;
13
14
  exports.readVersionsFile = readVersionsFile;
14
15
  exports.readVersionNames = readVersionNames;
@@ -45,6 +46,14 @@ function getDocsDirPathLocalized({ localizationDir, pluginId, versionName, }) {
45
46
  ],
46
47
  });
47
48
  }
49
+ function getPluginDirPathLocalized({ localizationDir, pluginId, }) {
50
+ return (0, utils_1.getPluginI18nPath)({
51
+ localizationDir,
52
+ pluginName: 'docusaurus-plugin-content-docs',
53
+ pluginId,
54
+ subPaths: [],
55
+ });
56
+ }
48
57
  /** `community` => `[siteDir]/community_versions.json` */
49
58
  function getVersionsFilePath(siteDir, pluginId) {
50
59
  return path_1.default.join(siteDir, addPluginIdPrefix(constants_1.VERSIONS_JSON_FILE, pluginId));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docusaurus/plugin-content-docs",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "Docs plugin for Docusaurus.",
5
5
  "main": "lib/index.js",
6
6
  "sideEffects": false,
@@ -35,20 +35,21 @@
35
35
  },
36
36
  "license": "MIT",
37
37
  "dependencies": {
38
- "@docusaurus/core": "3.7.0",
39
- "@docusaurus/logger": "3.7.0",
40
- "@docusaurus/mdx-loader": "3.7.0",
41
- "@docusaurus/module-type-aliases": "3.7.0",
42
- "@docusaurus/theme-common": "3.7.0",
43
- "@docusaurus/types": "3.7.0",
44
- "@docusaurus/utils": "3.7.0",
45
- "@docusaurus/utils-common": "3.7.0",
46
- "@docusaurus/utils-validation": "3.7.0",
38
+ "@docusaurus/core": "3.8.0",
39
+ "@docusaurus/logger": "3.8.0",
40
+ "@docusaurus/mdx-loader": "3.8.0",
41
+ "@docusaurus/module-type-aliases": "3.8.0",
42
+ "@docusaurus/theme-common": "3.8.0",
43
+ "@docusaurus/types": "3.8.0",
44
+ "@docusaurus/utils": "3.8.0",
45
+ "@docusaurus/utils-common": "3.8.0",
46
+ "@docusaurus/utils-validation": "3.8.0",
47
47
  "@types/react-router-config": "^5.0.7",
48
48
  "combine-promises": "^1.1.0",
49
49
  "fs-extra": "^11.1.1",
50
50
  "js-yaml": "^4.1.0",
51
51
  "lodash": "^4.17.21",
52
+ "schema-dts": "^1.1.2",
52
53
  "tslib": "^2.6.0",
53
54
  "utility-types": "^3.10.0",
54
55
  "webpack": "^5.88.1"
@@ -57,8 +58,7 @@
57
58
  "@types/js-yaml": "^4.0.5",
58
59
  "@types/picomatch": "^2.3.0",
59
60
  "commander": "^5.1.0",
60
- "picomatch": "^2.3.1",
61
- "shelljs": "^0.8.5"
61
+ "picomatch": "^2.3.1"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "react": "^18.0.0 || ^19.0.0",
@@ -67,5 +67,5 @@
67
67
  "engines": {
68
68
  "node": ">=18.0"
69
69
  },
70
- "gitHead": "dd59750c16fe6908a26f18806a54d4c3dbe6db43"
70
+ "gitHead": "948d63c42fad0ba24b7b531a9deb6167e64dc845"
71
71
  }
package/src/cli.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  getVersionDocsDirPath,
15
15
  getVersionSidebarsPath,
16
16
  getDocsDirPathLocalized,
17
+ getPluginDirPathLocalized,
17
18
  readVersionsFile,
18
19
  } from './versions/files';
19
20
  import {validateVersionName} from './versions/validation';
@@ -123,6 +124,23 @@ async function cliDocsVersionCommand(
123
124
  versionName: version,
124
125
  });
125
126
  await fs.copy(docsDir, newVersionDir);
127
+
128
+ // Copy version JSON translation file for this locale
129
+ // i18n/<l>/docusaurus-plugin-content-docs/current.json => version-v1.json
130
+ // See https://docusaurus.io/docs/next/api/plugins/@docusaurus/plugin-content-docs#translation-files-location
131
+ if (locale !== i18n.defaultLocale) {
132
+ const dir = getPluginDirPathLocalized({
133
+ localizationDir,
134
+ pluginId,
135
+ });
136
+ const sourceFile = path.join(dir, 'current.json');
137
+ const dest = path.join(dir, `version-${version}.json`);
138
+ if (await fs.pathExists(sourceFile)) {
139
+ await fs.copy(sourceFile, dest);
140
+ } else {
141
+ logger.warn`${pluginIdLogPrefix}: i18n translation file does not exist in path=${sourceFile}. Skipping.`;
142
+ }
143
+ }
126
144
  }),
127
145
  );
128
146
 
@@ -132,6 +132,25 @@ export function useCurrentSidebarCategory(): PropSidebarItemCategory {
132
132
  return deepestCategory;
133
133
  }
134
134
 
135
+ /**
136
+ * Gets the category associated with the current location. Should only be used
137
+ * on category index pages.
138
+ */
139
+ export function useCurrentSidebarSiblings(): PropSidebarItem[] {
140
+ const {pathname} = useLocation();
141
+ const sidebar = useDocsSidebar();
142
+ if (!sidebar) {
143
+ throw new Error('Unexpected: cant find current sidebar in context');
144
+ }
145
+ const categoryBreadcrumbs = getSidebarBreadcrumbs({
146
+ sidebarItems: sidebar.items,
147
+ pathname,
148
+ onlyCategories: true,
149
+ });
150
+ const deepestCategory = categoryBreadcrumbs.slice(-1)[0];
151
+ return deepestCategory?.items ?? sidebar.items;
152
+ }
153
+
135
154
  const isActive = (testedPath: string | undefined, activePath: string) =>
136
155
  typeof testedPath !== 'undefined' && isSamePath(testedPath, activePath);
137
156
  const containsActiveSidebarItem = (
@@ -33,6 +33,7 @@ export {
33
33
  useLayoutDocsSidebar,
34
34
  useDocRootMetadata,
35
35
  useCurrentSidebarCategory,
36
+ useCurrentSidebarSiblings,
36
37
  filterDocCardListItems,
37
38
  } from './docsUtils';
38
39
 
@@ -59,6 +60,8 @@ export {
59
60
  getDocsVersionSearchTag,
60
61
  } from './docsSearch';
61
62
 
63
+ export {useBreadcrumbsStructuredData} from './structuredDataUtils';
64
+
62
65
  export type ActivePlugin = {
63
66
  pluginId: string;
64
67
  pluginData: GlobalPluginData;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
9
+ import type {PropSidebarBreadcrumbsItem} from '@docusaurus/plugin-content-docs';
10
+ import type {WithContext, BreadcrumbList} from 'schema-dts';
11
+
12
+ export function useBreadcrumbsStructuredData({
13
+ breadcrumbs,
14
+ }: {
15
+ breadcrumbs: PropSidebarBreadcrumbsItem[];
16
+ }): WithContext<BreadcrumbList> {
17
+ const {siteConfig} = useDocusaurusContext();
18
+ return {
19
+ '@context': 'https://schema.org',
20
+ '@type': 'BreadcrumbList',
21
+ itemListElement: breadcrumbs
22
+ // We filter breadcrumb items without links, they are not allowed
23
+ // See also https://github.com/facebook/docusaurus/issues/9319#issuecomment-2643560845
24
+ .filter((breadcrumb) => breadcrumb.href)
25
+ .map((breadcrumb, index) => ({
26
+ '@type': 'ListItem',
27
+ position: index + 1,
28
+ name: breadcrumb.label,
29
+ item: `${siteConfig.url}${breadcrumb.href}`,
30
+ })),
31
+ };
32
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import path from 'path';
9
+ import fs from 'fs-extra';
9
10
  import _ from 'lodash';
10
11
  import logger from '@docusaurus/logger';
11
12
  import {
@@ -63,6 +64,30 @@ import type {LoadContext, Plugin} from '@docusaurus/types';
63
64
  import type {DocFile, FullVersion} from './types';
64
65
  import type {RuleSetRule} from 'webpack';
65
66
 
67
+ // MDX loader is not 100% deterministic, leading to cache invalidation issue
68
+ // This permits to invalidate the MDX loader cache entries when content changes
69
+ // Problem documented here: https://github.com/facebook/docusaurus/pull/10934
70
+ // TODO this is not a perfect solution, find better?
71
+ async function createMdxLoaderDependencyFile({
72
+ dataDir,
73
+ options,
74
+ versionsMetadata,
75
+ }: {
76
+ dataDir: string;
77
+ options: PluginOptions;
78
+ versionsMetadata: VersionMetadata[];
79
+ }): Promise<string | undefined> {
80
+ const filePath = path.join(dataDir, '__mdx-loader-dependency.json');
81
+ // the cache is invalidated whenever this file content changes
82
+ const fileContent = {
83
+ options,
84
+ versionsMetadata,
85
+ };
86
+ await fs.ensureDir(dataDir);
87
+ await fs.writeFile(filePath, JSON.stringify(fileContent));
88
+ return filePath;
89
+ }
90
+
66
91
  export default async function pluginContentDocs(
67
92
  context: LoadContext,
68
93
  options: PluginOptions,
@@ -107,6 +132,14 @@ export default async function pluginContentDocs(
107
132
  return createMDXLoaderRule({
108
133
  include: contentDirs,
109
134
  options: {
135
+ dependencies: [
136
+ await createMdxLoaderDependencyFile({
137
+ dataDir,
138
+ options,
139
+ versionsMetadata,
140
+ }),
141
+ ].filter((d): d is string => typeof d === 'string'),
142
+
110
143
  useCrossCompilerCache:
111
144
  siteConfig.future.experimental_faster.mdxCrossCompilerCache,
112
145
  admonitions: options.admonitions,
@@ -20,7 +20,11 @@ declare module '@docusaurus/plugin-content-docs' {
20
20
  TagMetadata,
21
21
  TagsPluginOptions,
22
22
  } from '@docusaurus/utils';
23
- import type {Plugin, LoadContext} from '@docusaurus/types';
23
+ import type {
24
+ Plugin,
25
+ LoadContext,
26
+ OptionValidationContext,
27
+ } from '@docusaurus/types';
24
28
  import type {Overwrite, Required} from 'utility-types';
25
29
 
26
30
  export type Assets = {
@@ -559,6 +563,10 @@ declare module '@docusaurus/plugin-content-docs' {
559
563
  context: LoadContext,
560
564
  options: PluginOptions,
561
565
  ): Promise<Plugin<LoadedContent>>;
566
+
567
+ export function validateOptions(
568
+ args: OptionValidationContext<Options | undefined, PluginOptions>,
569
+ ): PluginOptions;
562
570
  }
563
571
 
564
572
  declare module '@theme/DocItem' {
@@ -75,6 +75,21 @@ export function getDocsDirPathLocalized({
75
75
  });
76
76
  }
77
77
 
78
+ export function getPluginDirPathLocalized({
79
+ localizationDir,
80
+ pluginId,
81
+ }: {
82
+ localizationDir: string;
83
+ pluginId: string;
84
+ }): string {
85
+ return getPluginI18nPath({
86
+ localizationDir,
87
+ pluginName: 'docusaurus-plugin-content-docs',
88
+ pluginId,
89
+ subPaths: [],
90
+ });
91
+ }
92
+
78
93
  /** `community` => `[siteDir]/community_versions.json` */
79
94
  export function getVersionsFilePath(siteDir: string, pluginId: string): string {
80
95
  return path.join(siteDir, addPluginIdPrefix(VERSIONS_JSON_FILE, pluginId));