Skip to content

Commit 4c62c93

Browse files
committed
Allow submodule version aliases for import paths
Closes #195 Add a `remark` plugin that lets a docs author include an `import` statement that points to a directory in the current version submodule of the docs site. This makes it possible to import the path of a static asset or, with the `!!raw-loader!` directive, the full content, using an `import` statement, without having to know which versioned submodule an asset is in at a given time. To use this, add the string `@version` to the start of an import path, e.g.,: ```jsx import PNGPath from '@version/docs/img/myimg.png' ``` In a `gravitational/teleport` clone at the current default version of the docs site, the new plugin would add the following: ```jsx import PNGPath from '@site/content/17.x/docs/img/myimg.png' ``` Note that we currently use a workaround in which we place some static assets in the `/static` directory, which exists outside the submodule directory tree. This is approach is cumbersome since it requires a change to `docs-website` for every asset we want to include using an `import` statement. The `@site` alias is a Docusaurus feature that the docs engine replaces with the root path of the Docusaurus project. The plugin fills in the path of the current `gravitational/teleport` submodule. To import the content of a text asset, rather than the file path, you would use the `!!raw-loader` syntax as you would for any Docusaurus asset: ```jsx import PNGPath from '!!raw-loader!@version/docs/img/myimg.png' ```
1 parent f72e440 commit 4c62c93

File tree

5 files changed

+1341
-1189
lines changed

5 files changed

+1341
-1189
lines changed

docusaurus.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import remarkUpdateAssetPaths from "./server/remark-update-asset-paths";
1313
import remarkIncludes from "./server/remark-includes";
1414
import remarkVariables from "./server/remark-variables";
15+
import remarkVersionAlias from "./server/remark-version-alias";
1516
import remarkCodeSnippet from "./server/remark-code-snippet";
1617
import { fetchVideoMeta } from "./server/youtube-meta";
1718
import { getRedirects } from "./server/redirects";
@@ -214,6 +215,7 @@ const config: Config = {
214215
versions: getDocusaurusConfigVersionOptions(),
215216
// Our custom plugins need to be before default plugins
216217
beforeDefaultRemarkPlugins: [
218+
[remarkVersionAlias, latestVersion],
217219
[
218220
remarkIncludes,
219221
{

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,19 @@
5656
"@docusaurus/tsconfig": "^3.7.0",
5757
"@inkeep/cxkit-react": "^0.5.71",
5858
"@mdx-js/react": "^3.0.0",
59+
"@types/estree": "^1.0.7",
5960
"classnames": "^2.3.1",
6061
"clsx": "^2.1.1",
6162
"date-fns": "^4.1.0",
6263
"dotenv": "^16.5.0",
6364
"highlightjs-terraform": "https://github.com/highlightjs/highlightjs-terraform#eb1b9661e143a43dff6b58b391128ce5cdad31d4",
6465
"lowlight": "^3.1.0",
6566
"mdast-util-from-markdown": "^2.0.1",
67+
"mdast-util-mdxjs-esm": "^2.0.1",
6668
"nanoid": "^5.1.0",
6769
"postcss-preset-env": "^10.1.6",
6870
"prism-react-renderer": "^2.3.0",
71+
"raw-loader": "^4.0.2",
6972
"react": "^18.3.1",
7073
"react-dom": "^18.3.1",
7174
"react-loadable": "^5.5.0",
@@ -139,4 +142,4 @@
139142
"engines": {
140143
"node": ">=18.0"
141144
}
142-
}
145+
}

server/remark-version-alias.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { describe, expect, test } from "@jest/globals";
2+
import { VFile, VFileOptions } from "vfile";
3+
import { remark } from "remark";
4+
import mdx from "remark-mdx";
5+
import remarkVersionAlias from "./remark-version-alias";
6+
import remarkFrontmatter from "remark-frontmatter";
7+
8+
const transformer = (vfileOptions: VFileOptions) => {
9+
const file: VFile = new VFile(vfileOptions);
10+
11+
return remark()
12+
.use(mdx as any)
13+
.use(remarkFrontmatter) // Test cases use frontmatter
14+
.use(remarkVersionAlias as any, "15.x")
15+
.processSync(file as any);
16+
};
17+
18+
describe("server/remark-version-alias", () => {
19+
interface testCase {
20+
description: string;
21+
input: string;
22+
expected: string;
23+
path: string;
24+
}
25+
26+
const testCases: Array<testCase> = [
27+
{
28+
description: "import statement in latest-version docs path",
29+
input: `---
30+
title: My page
31+
description: My page
32+
---
33+
34+
import CodeExample from "@version/examples/access-plugin-minimal/config.go"
35+
36+
This is a paragraph.`,
37+
expected: `---
38+
title: My page
39+
description: My page
40+
---
41+
42+
import CodeExample from '@site/content/15.x/examples/access-plugin-minimal/config.go'
43+
44+
This is a paragraph.
45+
`,
46+
path: "docs/mypage.mdx",
47+
},
48+
{
49+
description: "import statement in non-latest docs path",
50+
input: `---
51+
title: My page
52+
description: My page
53+
---
54+
55+
import CodeExample from "@version/examples/access-plugin-minimal/config.go"
56+
57+
This is a paragraph.`,
58+
expected: `---
59+
title: My page
60+
description: My page
61+
---
62+
63+
import CodeExample from '@site/content/16.x/examples/access-plugin-minimal/config.go'
64+
65+
This is a paragraph.
66+
`,
67+
path: "versioned_docs/version-16.x/mypage.mdx",
68+
},
69+
{
70+
description: "raw loader",
71+
input: `---
72+
title: My page
73+
description: My page
74+
---
75+
76+
import CodeExample from "!!raw-loader!@version/examples/access-plugin-minimal/config.go"
77+
78+
This is a paragraph.`,
79+
expected: `---
80+
title: My page
81+
description: My page
82+
---
83+
84+
import CodeExample from '!!raw-loader!@site/content/16.x/examples/access-plugin-minimal/config.go'
85+
86+
This is a paragraph.
87+
`,
88+
path: "versioned_docs/version-16.x/mypage.mdx",
89+
},
90+
];
91+
92+
test.each(testCases)("$description", (tc) => {
93+
const result = transformer({
94+
value: tc.input,
95+
path: tc.path,
96+
}).toString();
97+
98+
expect(result).toEqual(tc.expected);
99+
});
100+
});

server/remark-version-alias.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { ImportDeclaration } from "estree";
2+
import type { MdxjsEsm } from "mdast-util-mdxjs-esm";
3+
import type { Root, Paragraph, Literal } from "mdast";
4+
import type { VFile } from "vfile";
5+
import type { Transformer } from "unified";
6+
import type { Node } from "unist";
7+
import { visit, CONTINUE, SKIP } from "unist-util-visit";
8+
9+
const versionedDocsPattern = `versioned_docs/version-([0-9]+\\.x)/`;
10+
11+
export default function remarkVersionAlias(latestVersion: string): Transformer {
12+
return (root: Root, vfile: VFile) => {
13+
visit(root, (node: Node) => {
14+
if (node.type != "mdxjsEsm") {
15+
return CONTINUE;
16+
}
17+
18+
// Only process import statements that import an identifier from a default
19+
// export.
20+
const esm = node as unknown as MdxjsEsm;
21+
if (
22+
!esm.data ||
23+
!esm.data.estree ||
24+
esm.data.estree.body.length !== 1 ||
25+
esm.data.estree.body[0]["type"] != "ImportDeclaration" ||
26+
esm.data.estree.body[0].specifiers.length !== 1 ||
27+
esm.data.estree.body[0].specifiers[0].type != "ImportDefaultSpecifier"
28+
) {
29+
return CONTINUE;
30+
}
31+
32+
let version: string = latestVersion;
33+
const versionedPathParts = vfile.path.match(versionedDocsPattern);
34+
if (versionedPathParts) {
35+
version = versionedPathParts[1];
36+
}
37+
38+
const decl = esm.data.estree.body[0] as ImportDeclaration;
39+
40+
const newPath = (decl.source.value as string).replace(
41+
"@version",
42+
`@site/content/${version}`,
43+
);
44+
45+
esm.value = `import ${esm.data.estree.body[0].specifiers[0].local.name} from '${newPath}'`;
46+
decl.source = {
47+
type: "Literal",
48+
value: newPath,
49+
raw: `"${newPath}"`,
50+
};
51+
52+
return SKIP;
53+
});
54+
};
55+
}

0 commit comments

Comments
 (0)