Skip to content

Commit 650a7ed

Browse files
authored
Allow submodule version aliases for import paths (#202)
* 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' ``` * Fix dependency configurations Revert unnecessary dependency changes. Remove @types/estree. Also remove an extraneous `mdast-util-mdxjs-esm` mention in `package.json`. Add swc/core to the list of dependencies with allowed licenses for the dependency review workflow. While the package uses the Apache 2.0 license, which is already configured as allowed in the workflow, the workflow also detects an unknown license that does not appear to be present in the package's code repository.
1 parent d97a052 commit 650a7ed

File tree

6 files changed

+1486
-1332
lines changed

6 files changed

+1486
-1332
lines changed

.github/workflows/dependency-review.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ jobs:
1212
with:
1313
allow-additional-licenses: >
1414
LicenseRef-scancode-public-domain AND Unlicense
15+
# @swc/core@1.11.24 uses Apache-2.0, but
16+
# LicenseRef-scancode-unknown-license-reference is also detected.
17+
# https://www.npmjs.com/package/@swc/core/v/1.11.24?activeTab=code
18+
# https://scancode-licensedb.aboutcode.org/unknown-license-reference.html
1519
allow-dependencies-licenses: >
20+
pkg:npm/swc/core,
1621
pkg:npm/%40inkeep/cxkit-color-mode,
1722
pkg:npm/%40inkeep/cxkit-primitives,
1823
pkg:npm/%40inkeep/cxkit-react,

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"nanoid": "^5.1.0",
6767
"postcss-preset-env": "^10.1.6",
6868
"prism-react-renderer": "^2.3.0",
69+
"raw-loader": "^4.0.2",
6970
"react": "^18.3.1",
7071
"react-dom": "^18.3.1",
7172
"react-loadable": "^5.5.0",
@@ -139,4 +140,4 @@
139140
"engines": {
140141
"node": ">=18.0"
141142
}
142-
}
143+
}

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

0 commit comments

Comments
 (0)