Merge branch 'master' of github.com:spotify/backstage into mob/cookiecutter-templater

* 'master' of github.com:spotify/backstage: (43 commits)
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  ...
This commit is contained in:
blam
2020-06-24 17:39:30 +02:00
34 changed files with 366 additions and 137 deletions
+35
View File
@@ -0,0 +1,35 @@
name: TechDocs
on:
pull_request:
paths:
- '.github/workflows/techdocs.yml'
- 'packages/techdocs-cli/**'
- 'plugins/techdocs/**'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
python-version: [3.7]
env:
TECHDOCS_CORE_PATH: ./plugins/techdocs/mkdocs/container/techdocs-core
name: Python ${{ matrix.node-version }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
# Lint Python code for techdocs-core package
- name: prepare python environment
run: |
python3 -m pip install --index-url https://pypi.org/simple/ setuptools
python3 -m pip install --upgrade pip
python3 -m pip install --index-url https://pypi.org/simple/ -r $TECHDOCS_CORE_PATH/requirements.txt
- name: lint techdocs-core package
run: |
python3 -m black --check $TECHDOCS_CORE_PATH/src
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: experimental
owner: tools@example.com
owner: artists@example.com
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: production
owner: tools@example.com
owner: guest
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: experimental
owner: tools@example.com
owner: players@example.com
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: production
owner: tools@example.com
owner: guest
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: production
owner: tools@example.com
owner: guest
@@ -173,7 +173,7 @@ export type ProfileInfo = {
/**
* Email ID.
*/
email: string;
email?: string;
/**
* Display name that can be presented to the user.
@@ -29,7 +29,6 @@ import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
import { SessionStateTracker } from '../../../../lib/AuthSessionManager/SessionStateTracker';
type CreateOptions = {
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth
@@ -95,44 +94,34 @@ class GithubAuth implements OAuthApi, SessionStateApi {
return new GithubAuth(sessionManager);
}
private readonly sessionStateTracker = new SessionStateTracker();
sessionState$(): Observable<SessionState> {
return this.sessionStateTracker.observable;
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<GithubSession>) {}
async getAccessToken(scope?: string, options?: AuthRequestOptions) {
const normalizedScopes = GithubAuth.normalizeScope(scope);
const session = await this.sessionManager.getSession({
...options,
scopes: normalizedScopes,
scopes: GithubAuth.normalizeScope(scope),
});
this.sessionStateTracker.setIsSignedId(!!session);
if (session) {
return session.providerInfo.accessToken;
}
return '';
return session?.providerInfo.accessToken ?? '';
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
this.sessionStateTracker.setIsSignedId(!!session);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
this.sessionStateTracker.setIsSignedId(!!session);
return session?.profile;
}
async logout() {
await this.sessionManager.removeSession();
this.sessionStateTracker.setIsSignedId(false);
}
static normalizeScope(scope?: string): Set<string> {
@@ -32,7 +32,6 @@ import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
import { SessionStateTracker } from '../../../../lib/AuthSessionManager/SessionStateTracker';
type CreateOptions = {
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth
@@ -117,10 +116,8 @@ class GoogleAuth
return new GoogleAuth(sessionManager);
}
private readonly sessionStateTracker = new SessionStateTracker();
sessionState$(): Observable<SessionState> {
return this.sessionStateTracker.observable;
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<GoogleSession>) {}
@@ -129,43 +126,31 @@ class GoogleAuth
scope?: string | string[],
options?: AuthRequestOptions,
) {
const normalizedScopes = GoogleAuth.normalizeScopes(scope);
const session = await this.sessionManager.getSession({
...options,
scopes: normalizedScopes,
scopes: GoogleAuth.normalizeScopes(scope),
});
this.sessionStateTracker.setIsSignedId(!!session);
if (session) {
return session.providerInfo.accessToken;
}
return '';
return session?.providerInfo.accessToken ?? '';
}
async getIdToken(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
this.sessionStateTracker.setIsSignedId(!!session);
if (session) {
return session.providerInfo.idToken;
}
return '';
return session?.providerInfo.idToken ?? '';
}
async logout() {
await this.sessionManager.removeSession();
this.sessionStateTracker.setIsSignedId(false);
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
this.sessionStateTracker.setIsSignedId(!!session);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
this.sessionStateTracker.setIsSignedId(!!session);
return session?.profile;
}
@@ -38,6 +38,7 @@ class LocalStorage {
class MockManager implements SessionManager<string> {
getSession = jest.fn();
removeSession = jest.fn();
sessionState$ = jest.fn();
}
describe('GheAuth AuthSessionStore', () => {
@@ -119,4 +120,11 @@ describe('GheAuth AuthSessionStore', () => {
expect(localStorage.getItem('my-key')).toBe(null);
});
it('should forward sessionState calls', () => {
const manager = new MockManager();
const store = new AuthSessionStore({ manager, ...defaultOptions });
store.sessionState$();
expect(manager.sessionState$).toHaveBeenCalled();
});
});
@@ -82,6 +82,10 @@ export class AuthSessionStore<T> implements SessionManager<T> {
await this.manager.removeSession();
}
sessionState$() {
return this.manager.sessionState$();
}
private loadSession(): T | undefined {
try {
const sessionJson = localStorage.getItem(this.storageKey);
@@ -15,6 +15,7 @@
*/
import { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager';
import { SessionState } from '../../apis';
const defaultOptions = {
sessionScopes: (session: { scopes: Set<string> }) => session.scopes,
@@ -22,21 +23,44 @@ const defaultOptions = {
};
describe('RefreshingAuthSessionManager', () => {
it('should save result form createSession', async () => {
it('should save result from createSession', async () => {
const createSession = jest.fn().mockResolvedValue({ expired: false });
const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE'));
const removeSession = jest.fn();
const manager = new RefreshingAuthSessionManager({
connector: { createSession, refreshSession },
connector: { createSession, refreshSession, removeSession },
...defaultOptions,
} as any);
const stateSubscriber = jest.fn();
manager.sessionState$().subscribe(stateSubscriber);
await Promise.resolve(); // Wait a tick for observer to post a value
expect(stateSubscriber.mock.calls).toEqual([[SessionState.SignedOut]]);
await manager.getSession({});
expect(createSession).toBeCalledTimes(1);
expect(stateSubscriber.mock.calls).toEqual([
[SessionState.SignedOut],
[SessionState.SignedIn],
]);
await manager.getSession({});
expect(createSession).toBeCalledTimes(1);
expect(refreshSession).toBeCalledTimes(1);
expect(stateSubscriber.mock.calls).toEqual([
[SessionState.SignedOut],
[SessionState.SignedIn],
]);
expect(removeSession).toHaveBeenCalledTimes(0);
await manager.removeSession();
expect(removeSession).toHaveBeenCalledTimes(1);
expect(stateSubscriber.mock.calls).toEqual([
[SessionState.SignedOut],
[SessionState.SignedIn],
[SessionState.SignedOut],
]);
});
it('should ask consent only if scopes have changed', async () => {
@@ -130,7 +154,7 @@ describe('RefreshingAuthSessionManager', () => {
expect(refreshSession).toBeCalledTimes(1);
});
it('should remove session and reload', async () => {
it('should remove session straight away', async () => {
const removeSession = jest.fn();
const manager = new RefreshingAuthSessionManager({
connector: { removeSession },
@@ -22,6 +22,7 @@ import {
} from './types';
import { AuthConnector } from '../AuthConnector';
import { SessionScopeHelper, hasScopes } from './common';
import { SessionStateTracker } from './SessionStateTracker';
type Options<T> = {
/** The connector used for acting on the auth session */
@@ -43,6 +44,7 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
private readonly helper: SessionScopeHelper<T>;
private readonly sessionScopesFunc: SessionScopesFunc<T>;
private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc<T>;
private readonly stateTracker = new SessionStateTracker();
private refreshPromise?: Promise<T>;
private currentSession: T | undefined;
@@ -109,16 +111,18 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
...options,
scopes: this.helper.getExtendedScope(this.currentSession, options.scopes),
});
this.stateTracker.setIsSignedIn(true);
return this.currentSession;
}
async removeSession() {
this.currentSession = undefined;
await this.connector.removeSession();
this.stateTracker.setIsSignedIn(false);
}
async getCurrentSession() {
return this.currentSession;
sessionState$() {
return this.stateTracker.sessionState$();
}
private async collapsedSessionRefresh(): Promise<T> {
@@ -129,7 +133,9 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
this.refreshPromise = this.connector.refreshSession();
try {
return await this.refreshPromise;
const session = await this.refreshPromise;
this.stateTracker.setIsSignedIn(true);
return session;
} finally {
delete this.refreshPromise;
}
@@ -16,17 +16,25 @@
import { BehaviorSubject } from '..';
import { SessionState } from '../../apis';
import { Observable } from '../../types';
export class SessionStateTracker {
private signedIn: boolean = false;
observable = new BehaviorSubject<SessionState>(SessionState.SignedOut);
private readonly subject = new BehaviorSubject<SessionState>(
SessionState.SignedOut,
);
setIsSignedId(isSignedIn: boolean) {
private signedIn: boolean = false;
setIsSignedIn(isSignedIn: boolean) {
if (this.signedIn !== isSignedIn) {
this.signedIn = isSignedIn;
this.observable.next(
this.subject.next(
this.signedIn ? SessionState.SignedIn : SessionState.SignedOut,
);
}
}
sessionState$(): Observable<SessionState> {
return this.subject;
}
}
@@ -17,6 +17,7 @@
import { SessionManager, GetSessionOptions } from './types';
import { AuthConnector } from '../AuthConnector';
import { SessionScopeHelper } from './common';
import { SessionStateTracker } from './SessionStateTracker';
type Options<T> = {
/** The connector used for acting on the auth session */
@@ -33,6 +34,7 @@ type Options<T> = {
export class StaticAuthSessionManager<T> implements SessionManager<T> {
private readonly connector: AuthConnector<T>;
private readonly helper: SessionScopeHelper<T>;
private readonly stateTracker = new SessionStateTracker();
private currentSession: T | undefined;
@@ -60,11 +62,17 @@ export class StaticAuthSessionManager<T> implements SessionManager<T> {
...options,
scopes: this.helper.getExtendedScope(this.currentSession, options.scopes),
});
this.stateTracker.setIsSignedIn(true);
return this.currentSession;
}
async removeSession() {
this.currentSession = undefined;
await this.connector.removeSession();
this.stateTracker.setIsSignedIn(false);
}
sessionState$() {
return this.stateTracker.sessionState$();
}
}
@@ -14,6 +14,9 @@
* limitations under the License.
*/
import { Observable } from '../../types';
import { SessionState } from '../../apis';
export type GetSessionOptions = {
optional?: boolean;
instantPopup?: boolean;
@@ -29,6 +32,8 @@ export type SessionManager<T> = {
getSession(options: GetSessionOptions): Promise<T | undefined>;
removeSession(): Promise<void>;
sessionState$(): Observable<SessionState>;
};
/**
@@ -34,14 +34,16 @@ export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({
}) => {
const ref = useRef<Element>(); // for scrolling down when collapse item opens
const classes = useStyles();
const profile = useApi(identityApiRef).getProfile();
const identityApi = useApi(identityApiRef);
const handleClick = () => {
setOpen(!open);
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300);
};
const displayName = profile.displayName ?? profile.email;
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
const displayName = profile.displayName ?? userId;
const SignInAvatar = () => (
<Avatar src={profile.picture} className={classes.avatar}>
{displayName[0]}
@@ -23,13 +23,9 @@ import {
verifyNonce,
OAuthProvider,
} from './OAuthProvider';
import {
WebMessageResponse,
OAuthProviderHandlers,
OAuthResponse,
} from '../providers/types';
import { WebMessageResponse, OAuthProviderHandlers } from '../providers/types';
const mockResponseData: OAuthResponse = {
const mockResponseData = {
providerInfo: {
accessToken: 'ACCESS_TOKEN',
idToken: 'ID_TOKEN',
@@ -39,6 +35,9 @@ const mockResponseData: OAuthResponse = {
profile: {
email: 'foo@bar.com',
},
backstageIdentity: {
id: 'foo',
},
};
describe('OAuthProvider Utils', () => {
@@ -350,7 +349,7 @@ describe('OAuthProvider', () => {
expect(mockResponse.send).toHaveBeenCalledWith({
...mockResponseData,
backstageIdentity: {
id: mockResponseData.profile.email,
id: mockResponseData.backstageIdentity.id,
idToken: 'my-id-token',
},
});
+21 -19
View File
@@ -18,10 +18,10 @@ import express from 'express';
import crypto from 'crypto';
import { URL } from 'url';
import {
AuthResponse,
AuthProviderRouteHandlers,
OAuthProviderHandlers,
WebMessageResponse,
BackstageIdentity,
} from '../providers/types';
import { InputError } from '@backstage/backend-common';
import { TokenIssuer } from '../identity';
@@ -147,19 +147,12 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
this.setRefreshTokenCookie(res, refreshToken);
}
const id = response.profile.email;
const idToken = await this.options.tokenIssuer.issueToken({
claims: { sub: id },
});
const fullResponse: AuthResponse<unknown> = {
...response,
backstageIdentity: { id, idToken },
};
await this.populateIdentity(response.backstageIdentity);
// post message back to popup if successful
return postMessageResponse(res, this.options.appOrigin, {
type: 'authorization_response',
response: fullResponse,
response,
});
} catch (error) {
// post error message back to popup if failure
@@ -213,21 +206,30 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
// get new access_token
const response = await this.providerHandlers.refresh(refreshToken, scope);
const id = response.profile.email;
const idToken = await this.options.tokenIssuer.issueToken({
claims: { sub: id },
});
const fullResponse: AuthResponse<unknown> = {
...response,
backstageIdentity: { id, idToken },
};
await this.populateIdentity(response.backstageIdentity);
res.send(fullResponse);
res.send(response);
} catch (error) {
res.status(401).send(`${error.message}`);
}
}
/**
* If the response from the OAuth provider includes a Backstage identity, we
* make sure it's populated with all the information we can derive from the user ID.
*/
private async populateIdentity(identity?: BackstageIdentity) {
if (!identity) {
return;
}
if (!identity.idToken) {
identity.idToken = await this.options.tokenIssuer.issueToken({
claims: { sub: identity.id },
});
}
}
private setNonceCookie = (res: express.Response, nonce: string) => {
res.cookie(`${this.options.providerId}-nonce`, nonce, {
maxAge: TEN_MINUTES_MS,
@@ -57,10 +57,6 @@ export const makeProfileInfo = (
}
}
if (!email) {
throw new Error('No email received in profile info');
}
return {
email,
picture,
@@ -72,15 +72,13 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
return await executeRedirectStrategy(req, this._strategy, options);
}
async handler(req: express.Request): Promise<{ response: OAuthResponse }> {
const result = await executeFrameHandlerStrategy<OAuthResponse>(
async handler(req: express.Request) {
const { response } = await executeFrameHandlerStrategy<OAuthResponse>(
req,
this._strategy,
);
return {
response: result.response,
};
return { response };
}
}
@@ -97,14 +97,14 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
const result = await executeFrameHandlerStrategy<
const { response, privateInfo } = await executeFrameHandlerStrategy<
OAuthResponse,
PrivateInfo
>(req, this._strategy);
return {
response: result.response,
refreshToken: result.privateInfo.refreshToken,
response: await this.populateIdentity(response),
refreshToken: privateInfo.refreshToken,
};
}
@@ -121,7 +121,7 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
params.id_token,
);
return {
return this.populateIdentity({
providerInfo: {
accessToken,
idToken: params.id_token,
@@ -129,7 +129,22 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
scope: params.scope,
},
profile,
};
});
}
private async populateIdentity(
response: OAuthResponse,
): Promise<OAuthResponse> {
const { profile } = response;
if (!profile.email) {
throw new Error('Google profile contained no email');
}
// TODO(Rugvip): Hardcoded to the local part of the email for now
const id = profile.email.split('@')[0];
return { ...response, backstageIdentity: { id } };
}
}
+12 -9
View File
@@ -100,14 +100,20 @@ export interface OAuthProviderHandlers {
*/
handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken?: string }>;
): Promise<{
response: AuthResponse<OAuthProviderInfo>;
refreshToken?: string;
}>;
/**
* (Optional) Given a refresh token and scope fetches a new access token from the auth provider.
* @param {string} refreshToken
* @param {string} scope
*/
refresh?(refreshToken: string, scope: string): Promise<OAuthResponse>;
refresh?(
refreshToken: string,
scope: string,
): Promise<AuthResponse<OAuthProviderInfo>>;
/**
* (Optional) Sign out of the auth provider.
@@ -192,13 +198,10 @@ export type AuthProviderFactory = (
export type AuthResponse<ProviderInfo> = {
providerInfo: ProviderInfo;
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity?: BackstageIdentity;
};
export type OAuthResponse = Omit<
AuthResponse<OAuthProviderInfo>,
'backstageIdentity'
>;
export type OAuthResponse = AuthResponse<OAuthProviderInfo>;
export type BackstageIdentity = {
/**
@@ -209,7 +212,7 @@ export type BackstageIdentity = {
/**
* An ID token that can be used to authenticate the user within Backstage.
*/
idToken: string;
idToken?: string;
};
export type OAuthProviderInfo = {
@@ -279,7 +282,7 @@ export type ProfileInfo = {
/**
* Email ID of the signed in user.
*/
email: string;
email?: string;
/**
* Display name that can be presented to the signed in user.
*/
+5 -3
View File
@@ -6,8 +6,10 @@ Welcome to MkDocs. This is the TechDocs implementation of MkDocs.
## Getting started
```
docker build ./container -t mkdocs-container
```bash
docker build ./container -t mkdocs-container
docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it mkdocs-container serve -a 0.0.0.0:8000
docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it mkdocs-container serve -a 0.0.0.0:8000
```
Then open up `http://localhost:8000` on your local machine.
+11 -12
View File
@@ -1,22 +1,21 @@
# Copyright 2020 Spotify AB
# Copyright 2020 Spotify AB
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM python:3.7.7-alpine3.12
RUN apk update && apk --no-cache add gcc musl-dev
RUN pip install mkdocs==1.1.2 mkdocs-material==5.3.2
RUN pip install --upgrade pip && pip install mkdocs==1.1.2 mkdocs-material==5.3.2 mkdocs-monorepo-plugin==0.4.5 pymdown-extensions==7.1
ADD ./techdocs-core /techdocs-core
RUN pip install --no-index /techdocs-core
@@ -0,0 +1,2 @@
.tox
*.egg-info
@@ -0,0 +1,45 @@
# techdocs-core
This is the base [Mkdocs](https://mkdocs.org) plugin used when using Mkdocs with Spotify's TechDocs. It is written in Python and packages all of our Mkdocs defaults, such as theming, plugins, etc in a single plugin.
## Usage
**Installation instructions TBD.** We haven't published it to a Python registry yet.
Once you have installed the `mkdocs-techdocs-core` plugin, you'll need to add it to your `mkdocs.yml`.
```yaml
site_name: Backstage Docs
nav:
- Home: index.md
- Developing a Plugin: developing-a-plugin.md
plugins:
- techdocs-core
```
## Running Locally
You can install this package locally using `pip` and the `--editable` flag used for making developing Python packages.
```bash
pip install --editable .
```
You'll then have the `techdocs-core` package available to use in Mkdocs and `pip` will point the dependency to this folder.
## Running with Docker
In the parent `Dockerfile` we add this folder to the build and install the package locally in the container. In the future, we'll probably move away from this approach and have it download directly from a Python registry (and this folder will publish to one).
See the `README.md` located in the `mkdocs/` folder for more details on how to build and run the Docker container.
## Linting
```bash
pip install -r requirements.txt
python -m black src/
```
**Note:** This will write to all Python files in `src/` with the formatted code. If you would like to only check to see if it passes, simply append the `--check` flag.
@@ -1 +1,9 @@
# The "base" version of the Mkdocs project.
# Note: if you update this, also update `install_requires` in setup.py
# https://github.com/mkdocs/mkdocs
mkdocs==1.1.2
# The linter using for Python
# Note: This requires Python 3.6+ to run, but can format Python 2 code too.
# https://github.com/psf/black
black==19.10b0
@@ -1,17 +1,17 @@
"""
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
Copyright 2020 Spotify AB
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from setuptools import setup, find_packages
@@ -19,17 +19,67 @@ from mkdocs.theme import Theme
from mkdocs.contrib.search import SearchPlugin
from mkdocs_monorepo_plugin.plugin import MonorepoPlugin
class TechDocsCore(BasePlugin):
def on_config(self, config):
# Theme
config['theme'] = Theme(name="material")
# Theme
config["theme"] = Theme(name="material")
# Plugins
del config['plugins']['techdocs-core']
# Plugins
del config["plugins"]["techdocs-core"]
search_plugin = SearchPlugin()
search_plugin.load_config({})
config['plugins']['search'] = search_plugin
search_plugin = SearchPlugin()
search_plugin.load_config({})
return config
monorepo_plugin = MonorepoPlugin()
monorepo_plugin.load_config({})
config["plugins"]["search"] = search_plugin
config["plugins"]["monorepo"] = monorepo_plugin
search_plugin = SearchPlugin()
search_plugin.load_config({})
config["plugins"]["search"] = search_plugin
# Markdown Extensions
config['markdown_extensions'].append('admonition')
config['markdown_extensions'].append('abbr')
config['markdown_extensions'].append('attr_list')
config['markdown_extensions'].append('def_list')
config['markdown_extensions'].append('codehilite')
config['mdx_configs']['codehilite'] = {
'linenums': True,
'guess_lang': False,
'pygments_style': 'friendly',
}
config['markdown_extensions'].append('toc')
config['mdx_configs']['toc'] = {
'permalink': True,
}
config['markdown_extensions'].append('footnotes')
config['markdown_extensions'].append('markdown.extensions.tables')
config['markdown_extensions'].append('pymdownx.betterem')
config['mdx_configs']['pymdownx.betterem'] = {
'smart_enable': 'all',
}
config['markdown_extensions'].append('pymdownx.caret')
config['markdown_extensions'].append('pymdownx.critic')
config['markdown_extensions'].append('pymdownx.details')
config['markdown_extensions'].append('pymdownx.emoji')
config['mdx_configs']['pymdownx.emoji'] = {
'emoji_generator': '!!python/name:pymdownx.emoji.to_svg',
}
config['markdown_extensions'].append('pymdownx.inlinehilite')
config['markdown_extensions'].append('pymdownx.magiclink')
config['markdown_extensions'].append('pymdownx.mark')
config['markdown_extensions'].append('pymdownx.smartsymbols')
config['markdown_extensions'].append('pymdownx.superfences')
config['markdown_extensions'].append('pymdownx.tasklist')
config['mdx_configs']['pymdownx.tasklist'] = {
'custom_checkbox': True,
}
config['markdown_extensions'].append('pymdownx.tilde')
return config
@@ -1 +1,32 @@
## hello mock docs
!!! test
Testing somethin
Some text about MOCDOC
\*[MOCDOC]: Mock Documentation
This is a paragraph.
{: #test_id .test_class }
Apple
: Pomaceous fruit of plants of the genus Malus in
the family Rosaceae.
```javascript
import { test } from 'something';
const addThingToThing = (a, b) a + b;
```
- [abc](#abc)
- [xyz](#xyz)
## abc
This is a b c.
## xyz
This is x y z.
+1 -1
View File
@@ -2,7 +2,7 @@ site_name: 'mock-docs'
nav:
- Home: index.md
- SubDocs: '!include ./sub-docs/mkdocs.yml'
plugins:
- techdocs-core
@@ -0,0 +1 @@
### This is an md file in another docs folder using the [MkDocs Monorepo Plugin](https://github.com/spotify/mkdocs-monorepo-plugin)
@@ -0,0 +1,4 @@
site_name: subdocs
nav:
- Home 2: "index.md"