Skip to content

Commit

Permalink
docs: Switch to mkdocs, mkdocs-material (#1232)
Browse files Browse the repository at this point in the history
# Background

When we originally set up the documentation website,
there were a couple core requirements:

- Inter-linking of documentation pages should be "checked".
  That is, instead of writing "../foo.html"
  and hoping that that generated page exists,
  we wanted to write "../foo.md", have the system check that it exists,
  and turn it into "foo.html" or equivalent as needed.

- It must be possible to include code snippets in documentation
  from sections of real code.

We didn't find anything at the time that met the latter requirement
well enough, so we resorted to a custom solution:
we used `mdox` and a custom shell script to extract code snippets
from source and include them in the documentation.
This has worked well okay, but it is a bit annoying to have around.

At the time we evaluated options, mkdocs almost won,
except that its snippets plugin only supported line numbers,
not named regions of code the way we wanted.

The mkdocs snippets plugin has since added support
for including named regions of code:

https://facelessuser.github.io/pymdown-extensions/extensions/snippets/#snippet-sections
This unblocks us from using mkdocs for our documentation.

# Description

This PR deletes all custom setup, mdox integration, etc.
in favor of using mkdocs to generate documentation for Fx.
It migrates all documentation as-is over from VuePress to mkdocs.

High-level notes:

- Navigation tree which was previously specified in
  docs/.vuepress/config.js is now specified in docs/mkdocs.yml.
  (It was always weird to have it in a hidden directory anyway.)
- mkdocs is configured to verify validity of all interlinking,
  including links to headers within a page.
  The build will fail if any links are invalid.
- We're using [uv](https://docs.astral.sh/uv/)
  to manage our Python dependencies.
  This will also manage the Python version as needed.
- There is no longer need for a custom page redirect component.
  We can use the mkdocs-redirects plugin for this purpose.
- The existing Google Analytics tag has been carried over.
- The mkdocs-material theme supports a fair bit of customization,
  including a dark/light mode toggle. This resolves #1071.

## Syntax changes

- Admonitions were previously specified as:

    ```
    ::: tip

    This is a tip.

    :::
    ```

    They are now specified as:

    ```
    !!! tip

        This is a tip.
    ```

- Tabbed blocks were previously specified as:

    ```
    <code-group>
    <code-block title="Go">
    ...
    </code-block>
    <code-block title="Python">
    ...
    </code-block>
    </code-group>
    ```

    They are now specified as:

    ```
    === "Go"

        ...

    === "Python"

        ...
    ```

- Code snippets were previously included as:

    ```
    go mdox-exec='region ex/path/to/file.go snippet-name'
    ```

    They are now included as:

    ```
    --8<-- "path/to/file.go:snippet-name"
    ```

- Code regions were previously marked in code as:

    ```
    // region snippet-name
    ...
    // endregion snippet-name
    ```

    They are now marked as:

    ```
    // --8<-- [start:snippet-name]
    ...
    // --8<-- [end:snippet-name]
    ```

    The snippets are processed at Markdown file-read time,
    so they don't result in code duplicated into the Markdown file.

- Multi-paragraph list items must be indented 4 spaces
  instead of just aligning to the list item text
  (which was 2 spaces for `-` and 3 spaces for `1. `).

    So, before:

    ```
    - First paragraph

      Second paragraph

    - Next item
    ```

    Becomes:

    ```
    - First paragraph

        Second paragraph

    - Next item
    ```

    The following is also valid:

    ```
    -   First paragraph

        Second paragraph

    -   Next item
    ```

# Demo

A demo site is available at https://abhinav.github.io/fx/.
It will be deleted if/when this PR is merged.

# Backwards compatibility

mkdocs has been configured to generate file URLs instead of directory
URLs.

    foo.md       -> foo.html
    foo/index.md -> foo/index.html

This matches the configuration we had for VuePress.

## Testing

To test this, I hacked together a quick script to crawl the old website,
and verify that all links still work if the host is changed to the demo
site.

<details>
  <sumamry>Script to check URLs</summary>

```go
package main

import (
	"cmp"
	"container/list"
	"fmt"
	"iter"
	"log"
	"net/http"
	"net/url"
	"path"

	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
)

const (
	_oldHost = "uber-go.github.io"
	_newHost = "abhinav.github.io"
)

func main() {
	log.SetFlags(0)

	for link, err := range oldLinks() {
		if err != nil {
			panic(err)
		}

		if err := checkNewLink(link); err != nil {
			log.Printf("  not migrated (%v): %s", link, err)
		}
	}
}

func checkNewLink(oldLink string) error {
	u, err := url.Parse(oldLink)
	if err != nil {
		return err
	}
	u.Host = _newHost

	log.Println("Checking:", u.String())
	res, err := http.Get(u.String())
	if err != nil {
		return err
	}

	if res.StatusCode != http.StatusOK {
		return fmt.Errorf("status: %s", res.Status)
	}

	return nil
}

func oldLinks() iter.Seq2[string, error] {
	seen := make(map[string]struct{})
	pending := list.New() // list[string]
	pending.PushBack("https://" + _oldHost + "/fx")
	return func(yield func(string, error) bool) {
		for pending.Len() > 0 {
			u := pending.Remove(pending.Front()).(string)
			if _, ok := seen[u]; ok {
				continue
			}
			seen[u] = struct{}{}

			if !yield(u, nil) {
				return
			}

			log.Println("Fetching:", u)
			res, err := http.Get(u)
			if err != nil {
				yield("", err)
				return
			}
			if res.StatusCode != http.StatusOK {
				log.Println("  skipping:", res.Status)
				continue
			}

			url, err := url.Parse(u)
			if err != nil {
				yield("", err)
				return
			}

			links, err := extractLocalLinks(url.Path, res)
			_ = res.Body.Close()
			if err != nil {
				yield("", err)
				return
			}

			for _, link := range links {
				pending.PushBack(link)
			}
		}
	}
}

func extractLocalLinks(fromPath string, res *http.Response) ([]string, error) {
	doc, err := html.Parse(res.Body)
	if err != nil {
		return nil, err
	}

	var links []string
	var visit func(*html.Node)
	visit = func(n *html.Node) {
		if n.Type == html.ElementNode && n.DataAtom == atom.A {
			for _, a := range n.Attr {
				if a.Key != "href" {
					continue
				}

				if a.Val == "" {
					continue
				}

				u, err := url.Parse(a.Val)
				if err != nil {
					continue
				}
				u.Host = cmp.Or(u.Host, _oldHost)
				u.Scheme = cmp.Or(u.Scheme, "https")
				u.Fragment = ""

				if u.Host != _oldHost {
					continue // external link
				}

				if !path.IsAbs(u.Path) {
					u.Path = path.Join(path.Dir(fromPath), u.Path)
				}

				links = append(links, u.String())
				break
			}
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			visit(c)
		}
	}
	visit(doc)

	return links, nil
}
```

</details>
  • Loading branch information
abhinav committed Aug 27, 2024
1 parent f195420 commit 67ed361
Show file tree
Hide file tree
Showing 79 changed files with 1,995 additions and 12,742 deletions.
21 changes: 9 additions & 12 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ concurrency:
group: "pages"
cancel-in-progress: true


env:
UV_VERSION: 0.3.3

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -41,24 +45,17 @@ jobs:
with:
ref: ${{ inputs.head }}

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '16'
cache: 'yarn'
cache-dependency-path: docs/yarn.lock

- name: Install dependencies
run: yarn install
working-directory: docs
- name: Install uv
run: |
curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh" | sh
- name: Build
run: make docs

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/dist/
path: docs/_site

deploy:
needs: build # run only after a successful build
Expand All @@ -69,7 +66,7 @@ jobs:

environment:
name: github-pages
url: ${{ steps.deployment.output.pages_url }}
url: ${{ steps.deployment.outputs.page_url }}

runs-on: ubuntu-latest
steps:
Expand Down
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
sidebarDepth: 0
search: false
search:
exclude: true
---

# Changelog
Expand Down
237 changes: 100 additions & 137 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
sidebarDepth: 2
search: false
search:
exclude: true
---

# Contributing
Expand All @@ -12,89 +12,86 @@ please [open an issue](https://github.com/uber-go/fx/issues/new)
describing your proposal.
Discussing API changes ahead of time makes pull request review much smoother.

::: tip
You'll need to sign [Uber's CLA](https://cla-assistant.io/uber-go/fx)
before we can accept any of your contributions.
If necessary, a bot will remind
you to accept the CLA when you open your pull request.
:::
!!! tip

You'll need to sign [Uber's CLA](https://cla-assistant.io/uber-go/fx)
before we can accept any of your contributions.
If necessary, a bot will remind
you to accept the CLA when you open your pull request.


## Contribute code

Set up your local development environment to contribute to Fx.

1. [Fork](https://github.com/uber-go/fx/fork), then clone the repository.

<code-group>
<code-block title="Git">
```bash
git clone https://github.com/your_github_username/fx.git
cd fx
git remote add upstream https://github.com/uber-go/fx.git
git fetch upstream
```
</code-block>

<code-block title="GitHub CLI">
```bash
gh repo fork --clone uber-go/fx
```
</code-block>
</code-group>
=== "Git"

```bash
git clone https://github.com/your_github_username/fx.git
cd fx
git remote add upstream https://github.com/uber-go/fx.git
git fetch upstream
```

=== "GitHub CLI"

```bash
gh repo fork --clone uber-go/fx
```

2. Install Fx's dependencies:

```bash
go mod download
```
```bash
go mod download
```

3. Verify that tests and other checks pass locally.

```bash
make lint
make test
```
```bash
make lint
make test
```

Note that for `make lint` to work,
you must be using the latest stable version of Go.
If you're on an older version, you can still contribute your change,
but we may discover style violations when you open the pull request.
Note that for `make lint` to work,
you must be using the latest stable version of Go.
If you're on an older version, you can still contribute your change,
but we may discover style violations when you open the pull request.
Next, make your changes.
1. Create a new feature branch.
```bash
git checkout master
git pull
git checkout -b cool_new_feature
```
```bash
git checkout master
git pull
git checkout -b cool_new_feature
```
2. Make your changes, and verify that all tests and lints still pass.
```bash
$EDITOR app.go
make lint
make test
```
```bash
$EDITOR app.go
make lint
make test
```
3. When you're satisfied with the change,
push it to your fork and make a pull request.

<code-group>
<code-block title="Git">
```bash
git push origin cool_new_feature
# Open a PR at https://github.com/uber-go/fx/compare
```
</code-block>
<code-block title="GitHub CLI">
```bash
gh pr create
```
</code-block>
</code-group>
=== "Git"

```bash
git push origin cool_new_feature
# Open a PR at https://github.com/uber-go/fx/compare
```

=== "GitHub CLI"

```bash
gh pr create
```

At this point, you're waiting on us to review your changes.
We *try* to respond to issues and pull requests within a few business days,
Expand All @@ -115,18 +112,14 @@ To contribute documentation to Fx,
1. Set up your local development environment
as you would to [contribute code](#contribute-code).
2. Install the documentation website dependencies.

```bash
cd docs
yarn install
```
2. [Install uv](https://docs.astral.sh/uv/getting-started/installation/).
We use this to manage our Python dependencies.
3. Run the development server.
```bash
yarn dev
```
```bash
make serve
```
4. Make your changes.
Expand Down Expand Up @@ -208,84 +201,54 @@ Markdown will reflow this into a "normal" pargraph when rendering.
All code samples in documentation must be buildable and testable.
To aid in this, we have two tools:
To make this possible, we put code samples in the "ex/" directory,
and use the [PyMdown Snippets extension](https://facelessuser.github.io/pymdown-extensions/extensions/snippets/)
to include them in the documentation.
- [mdox](https://github.com/bwplotka/mdox/)
- the `region` shell script
To include code snippets in your documentation,
take the following steps:
#### mdox
1. Add source code under the `ex/` directory.
Usually, the file will be placed in a directory
with a name matching the documentation file
that will include the snippet.
mdox is a Markdown file formatter that includes support for
running a command and using its output as part of a code block.
To use this, declare a regular code block and tag it with `mdoc-exec`.
For example,
for src/annotation.md, examples will reside in ex/annotation/.
```markdown
```go mdox-exec='cat foo.go'
// ...
```
2. Inside the source file, name regions of code with comments in the forms:
The contents of the code block will be replaced
with the output of the command when you run `make fmt`
in the docs directory.
`make check` will ensure that the contents are up-to-date.
```
// --8<-- [start:name]
...
// --8<-- [end:name]
```
The command runs with the working directory set to docs/.
Store code samples in ex/ and reference them directly.
Where `name` is the name of the snippet.
For example:
#### region
```go
// --8<-- [start:New]
func New() *http.Server {
// ...
}
// --8<-- [end:New]
```
The `region` shell script is a command intended to be used with `mdox-exec`.
3. Include the snippet in a code block with the following syntax:
```plain mdox-exec='region' mdox-expect-exit-code='1'
USAGE: region FILE REGION1 REGION2 ...
```markdown
```go
;--8<-- "path/to/file.go:name"
```
Extracts text from FILE marked by "// region" blocks.
```
Where `path/to/file.go` is the path to the file containing the snippet
relative to the `ex/` directory,
and `name` is the name of the snippet.
For example, given the file:
You can include multiple snippets from the same file like so:
```
foo
// region myregion
bar
// endregion myregion
baz
```
Running `region $FILE myregion` will print:
```
bar
```
The same region name may be used multiple times
to pull different snippets from the same file.
For example, given the file:
```go
// region provide-foo
func main() {
fx.New(
fx.Provide(
NewFoo,
// endregion provide-foo
NewBar,
// region provide-foo
),
).Run()
}

// endregion provide-foo
```
`region $FILE provide-foo` will print,
```go
func main() {
fx.New(
fx.Provide(
NewFoo,
),
).Run()
}
```
```
;--8<-- "path/to/file.go:snippet1"
;--8<-- "path/to/file.go:snippet2"
```
13 changes: 2 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export GOBIN ?= $(PROJECT_ROOT)/bin
export PATH := $(GOBIN):$(PATH)

FXLINT = $(GOBIN)/fxlint
MDOX = $(GOBIN)/mdox

MODULES = . ./tools ./docs ./internal/e2e

Expand All @@ -21,7 +20,7 @@ build:
go build ./...

.PHONY: lint
lint: golangci-lint tidy-lint fx-lint docs-lint
lint: golangci-lint tidy-lint fx-lint

.PHONY: test
test:
Expand All @@ -41,7 +40,7 @@ tidy:

.PHONY: docs
docs:
cd docs && yarn build
cd docs && make build

.PHONY: golangci-lint
golangci-lint:
Expand All @@ -62,13 +61,5 @@ tidy-lint:
fx-lint: $(FXLINT)
@$(FXLINT) ./...

.PHONY: docs-lint
docs-lint: $(MDOX)
@echo "Checking documentation"
@make -C docs check

$(MDOX): tools/go.mod
cd tools && go install github.com/bwplotka/mdox

$(FXLINT): tools/cmd/fxlint/main.go
cd tools && go install go.uber.org/fx/tools/cmd/fxlint
Loading

0 comments on commit 67ed361

Please sign in to comment.