Skip to content

Commit

Permalink
Get yarn markdown-lint to work (#21)
Browse files Browse the repository at this point in the history
* Get `yarn markdown-lint` to work

On `gravitational/docs`, the `yarn markdown-lint` script runs the docs
site's `remark`-based linters on the Teleport documentation content.
This change ensures that the script works as expected on Docusaurus
content.

- Add dependencies for `yarn markdown-lint`.
- Add a `build-remark` script for building remark plugins in order to
  run the linter.
- Copy `.remarkrc.mjs` from `gravitational/docs`, but add an
  `updatePaths` function to remark-includes to work with the plugin's
  new interface.
- Edit `getVersionFromFile` to accommodate linting. Get a version if the
  file is currently in the `content` directory, which we need to do in
  order to get `remarkIncludes` to work when linting files in `content`.
  Also add branching in `getCurrentDir` to work with the structure of
  the `content` directory.
- Update package.json to use the versions of `remark` packages found in
  gravitational/docs. This minimizes the content changes we'll need to
  make to accommodate the linting configuration.

* Respond to taraspos feedback

- Revert some dependency downgrades
- Remove highlightjs-terraform

* Remove the external link check

It is not enabled in CI, and has led to issues with rate limiting from
third-party sites in the past.
  • Loading branch information
ptgott authored Nov 5, 2024
1 parent 307ba21 commit 70ddb42
Show file tree
Hide file tree
Showing 16 changed files with 1,942 additions and 368 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ data/*

# Migration node dependencies
.build

# remark node dependencies
.remark-build
81 changes: 81 additions & 0 deletions .remarkrc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { resolve } from "path";
import remarkVariables from "./.remark-build/server/remark-variables.mjs";
import remarkIncludes from "./.remark-build/server/remark-includes.mjs";
import remarkTOC from "./.remark-build/server/remark-toc.mjs";
import { remarkLintTeleportDocsLinks } from "./.remark-build/server/lint-teleport-docs-links.mjs";
import {
getVersion,
getVersionRootPath,
} from "./.remark-build/server/docs-helpers.mjs";
import { loadConfig } from "./.remark-build/server/config-docs.mjs";
import {
updatePathsInIncludes,
} from "./.remark-build//server/asset-path-helpers.mjs";


const configFix = {
settings: {
bullet: "-",
ruleRepetition: 3,
fences: true,
incrementListMarker: true,
checkBlanks: true,
resourceLink: true,
emphasis: "*",
tablePipeAlign: false,
tableCellPadding: true,
listItemIndent: 1,
},
plugins: ["frontmatter", "mdx"],
};

const configLint = {
plugins: [
"frontmatter",
"mdx",
"preset-lint-markdown-style-guide",
["lint-table-pipe-alignment", false],
["lint-table-cell-padding", false],
["lint-maximum-line-length", false],
["lint-no-consecutive-blank-lines", false],
["lint-no-emphasis-as-heading", false],
["lint-fenced-code-flag", { allowEmpty: true }],
["lint-file-extension", false],
["lint-no-duplicate-headings", false],
["lint-list-item-spacing", { checkBlanks: true }],
["lint-no-shell-dollars", false],
["lint-list-item-indent", "space"],
["lint-ordered-list-marker-value", "single"],
["lint-maximum-heading-length", false],
["lint-no-shortcut-reference-link", false],
["lint-no-file-name-irregular-characters", false],
[remarkTOC],
[
remarkIncludes, // Lints (!include.ext!) syntax
{
lint: true,
rootDir: (vfile) => getVersionRootPath(vfile.path),
updatePaths: updatePathsInIncludes,
},
],
[
remarkVariables, // Lints (=variable=) syntax
{
lint: true,
variables: (vfile) => {
return loadConfig(getVersion(vfile.path)).variables || {};
},
},
],
// validate-links must be run after remarkVariables since some links
// include variables in their references, e.g.,
// [CM-08 Information System Component Inventory]((=fedramp.control_url=)CM-8)
["validate-links", { repository: false }],
[remarkLintTeleportDocsLinks],
// Disabling the remarkLintFrontmatter check until we fix
// gravitational/docs#80
// [remarkLintFrontmatter, ["error"]],
],
};

export default process.env.FIX ? configFix : configLint;
18 changes: 13 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"scripts": {
"spellcheck": "bash scripts/check-spelling.sh",
"build-remark": "rm -rf .remark-build && tsc --build tsconfig.node.json && tsc-esm-fix --target='.remark-build' --ext='.mjs'",
"git-update": "git submodule update --init --remote --progress --depth 1 --single-branch",
"prepare-files": "npx vite-node ./scripts/prepare-files.mts",
"prepare-sanity-data": "npx vite-node ./scripts/prepare-sanity-data.mts",
Expand All @@ -22,8 +23,7 @@
"base:prettier": "prettier '**/*.{js,jsx,ts,tsx,json}'",
"lint": "yarn base:eslint --fix && yarn base:prettier --write -l",
"lint-check": "yarn base:eslint && yarn base:prettier --check",
"markdown-lint": "remark --rc-path .remarkrc.mjs 'content/**/docs/pages/**/*.mdx' --quiet --frail --ignore-pattern '**/includes/**' --silently-ignore",
"markdown-lint-external-links": "WITH_EXTERNAL_LINKS=true yarn markdown-lint"
"markdown-lint": "yarn build-remark && remark --rc-path .remarkrc.mjs 'content/**/docs/pages/**/*.mdx' --quiet --frail --ignore-pattern '**/includes/**' --silently-ignore"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
Expand All @@ -49,8 +49,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-loadable": "^5.5.0",
"react-use": "^17.5.0",
"highlightjs-terraform": "https://github.com/highlightjs/highlightjs-terraform#eb1b9661e143a43dff6b58b391128ce5cdad31d4"
"react-use": "^17.5.0"
},
"devDependencies": {
"@csstools/postcss-global-data": "^2.1.1",
Expand All @@ -77,8 +76,17 @@
"postcss-preset-env": "^9.5.14",
"rehype-highlight": "^7.0.0",
"rehype-stringify": "^10.0.1",
"remark": "^15.0.1",
"remark-cli": "10.0.1",
"remark-copy-linked-files": "^1.5.0",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
"remark-mdx": "^2.1.1",
"remark-parse": "^10.0.1",
"remark-preset-lint-markdown-style-guide": "^5.1.2",
"remark-rehype": "^10.1.0",
"remark-validate-links": "^11.0.2",
"to-vfile": "^8.0.0",
"tsc-esm-fix": "^3.1.0",
"tsm": "^2.3.0",
"typescript": "~5.5.2",
"unified": "^11.0.5",
Expand Down
2 changes: 1 addition & 1 deletion scripts/prepare-files.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { copyFileSync, rmSync, existsSync, mkdirSync } from "fs";
import { resolve, dirname } from "path";
import glob from "glob";
import { glob } from "glob";
import { docusaurusifyNavigation } from "../server/config-docs";
import {
getCurrentVersion,
Expand Down
58 changes: 46 additions & 12 deletions server/asset-path-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Root, RootContent } from "mdast";
import type { Parent, Image, Link, Definition } from "mdast";
import type { Node } from "unist";
import type { VFile } from "vfile";
import { resolve, relative, dirname } from "path";
import { getCurrentVersion, getLatestVersion } from "./config-site";
Expand All @@ -7,7 +8,12 @@ import { isLocalAssetFile } from "../src/utils/url";
const current = getCurrentVersion();
const latest = getLatestVersion();

const REGEXP_VERSION = /^\/versioned_docs\/version-([^\/]+)\//;
// The directory path pattern for versioned content transformed by the migration
// script
const REGEXP_POST_PREPARE_VERSION = /^\/versioned_docs\/version-([^\/]+)\//;
// The directory path pattern for versioned content not yet transformed by the
// migration script
const REGEXP_PRE_PREPARE_VERSION = /^\/?content\/([^\/]+)\//;
const REGEXP_EXTENSION = /(\/index)?\.mdx$/;

export type DocsMeta = {
Expand All @@ -27,20 +33,42 @@ const getProjectPath = (vfile: VFile) => vfile.path.replace(process.cwd(), "");

const isCurrent = (vfile: VFile) => getProjectPath(vfile).startsWith("/docs/");

// getVersionFromVFile extracts the docs version of a post-migration docs page
// so we can find the appropriate pre-migration version. If the docs page is
// already in the pre-migration directory, return the version number of that
// directory.
export const getVersionFromVFile = (vfile: VFile): string => {
return isCurrent(vfile)
? current
: REGEXP_VERSION.exec(getProjectPath(vfile))[1];
if (isCurrent(vfile)) {
return current;
}
const projectPath = getProjectPath(vfile);

const postPrepVersion = REGEXP_POST_PREPARE_VERSION.exec(projectPath);
if (!!postPrepVersion) {
return postPrepVersion[1];
}

const prePrepVersion = REGEXP_PRE_PREPARE_VERSION.exec(projectPath);
if (!!prePrepVersion) {
return prePrepVersion[1];
}

throw new Error(`unable to extract a version from filepath ${projectPath}`);
};

export const getRootDir = (vfile: VFile): string => {
return resolve("content", getVersionFromVFile(vfile));
};

const getCurrentDir = (vfile: VFile) =>
isCurrent(vfile)
const getCurrentDir = (vfile: VFile) => {
// The page is in the pre-migration directory, i.e., we're linting it
if (vfile.path.startsWith("content")) {
return resolve(`content/${getVersionFromVFile(vfile)}/docs/pages`);
}
return isCurrent(vfile)
? resolve("docs")
: resolve(`versioned_docs/version-${getVersionFromVFile(vfile)}`);
};

const getPagesDir = (vfile: VFile): string =>
resolve(getRootDir(vfile), "docs/pages");
Expand Down Expand Up @@ -85,7 +113,7 @@ export const updatePathsInIncludes = ({
includePath,
vfile,
}: {
node: Root | RootContent;
node: Node;
versionRootDir: string;
includePath: string;
vfile: VFile;
Expand All @@ -95,7 +123,7 @@ export const updatePathsInIncludes = ({
node.type === "link" ||
node.type === "definition"
) {
const href = node.url;
const href = (node as Link | Image | Definition).url;

// Ignore non-strings, absolute paths, web URLs, and links consisting only
// of anchors (these will end up pointing to the containing page).
Expand All @@ -117,18 +145,24 @@ export const updatePathsInIncludes = ({
href
).replace(getPagesDir(vfile), getCurrentDir(vfile));

node.url = relative(absMdxPath, absTargetPath);
(node as Link | Image | Definition).url = relative(
absMdxPath,
absTargetPath
);
} else {
const absMdxPath = resolve(getOriginalPath(vfile));

const absTargetPath = resolve(versionRootDir, dirname(includePath), href);

node.url = relative(dirname(absMdxPath), absTargetPath);
(node as Link | Image | Definition).url = relative(
dirname(absMdxPath),
absTargetPath
);
}
}

if ("children" in node) {
node.children?.forEach?.((child) =>
(node as Parent).children?.forEach?.((child) =>
updatePathsInIncludes({ node: child, versionRootDir, includePath, vfile })
);
}
Expand Down
21 changes: 21 additions & 0 deletions server/docs-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { resolve } from "path";

export const getVersion = (filepath: string) => {
const result = /content\/([^/]+)\/docs\//.exec(filepath);
return result ? result[1] : "";
};

/**
* Used by some remark plugins to resolve paths to assets based on the
* current docs folders. E. g. remark-includes.
*/
export const getVersionRootPath = (filepath: string) => {
const version = getVersion(filepath);

if (version) {
return resolve(`content/${version}`);
} else {
// CI task for linting stored files in the root of the content folder
return resolve("content");
}
};
62 changes: 62 additions & 0 deletions server/lint-teleport-docs-links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Node } from "unist";
import type { Transformer } from "unified";
import type { Root, Link as MdastLink } from "mdast";
import type { EsmNode, MdxAnyElement } from "./types-unist";

import { visit } from "unist-util-visit";
import { isExternalLink, isHash, isPage } from "../utils/url";

interface ObjectHref {
src: string;
}

type Href = string | ObjectHref;

const mdxNodeTypes = new Set(["mdxJsxFlowElement", "mdxJsxTextElement"]);

const isMdxComponentWithHref = (node: Node): node is MdxAnyElement => {
return (
mdxNodeTypes.has(node.type) &&
(node as MdxAnyElement).attributes.some(
({ name, value }) => name === "href"
)
);
};

const isAnAbsoluteDocsLink = (href: string): boolean => {
return (
href.startsWith("/docs") || href.startsWith("https://goteleport.com/docs")
);
};

export function remarkLintTeleportDocsLinks(): Transformer {
return (root: Root, vfile) => {
visit(root, (node: Node) => {
if (
node.type == "link" &&
isAnAbsoluteDocsLink((node as MdastLink).url)
) {
vfile.message(
`Link reference ${
(node as MdastLink).url
} must be a relative link to an *.mdx page`,
node.position
);
return;
}

if (isMdxComponentWithHref(node)) {
const hrefAttribute = node.attributes.find(
({ name }) => name === "href"
);

if (isAnAbsoluteDocsLink(hrefAttribute.value as string)) {
vfile.message(
`Component href ${hrefAttribute.value} must be a relative link to an *.mdx page`,
node.position
);
}
}
});
};
}
Loading

0 comments on commit 70ddb42

Please sign in to comment.