Merge pull request #651 from spotify/blam/scaffolder

[Plugin] Start of the Scaffolder Plugin
This commit is contained in:
Ben Lambert
2020-05-01 17:52:58 +02:00
committed by GitHub
55 changed files with 1059 additions and 0 deletions
+1
View File
@@ -23,6 +23,7 @@
"@backstage/plugin-home-page": "^0.1.1-alpha.4",
"@backstage/plugin-inventory": "^0.1.1-alpha.4",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.4",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.4",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.4",
"@backstage/plugin-welcome": "^0.1.1-alpha.4",
"@backstage/theme": "^0.1.1-alpha.4",
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import { Link, makeStyles, Typography } from '@material-ui/core';
import HomeIcon from '@material-ui/icons/Home';
import AccountCircle from '@material-ui/icons/AccountCircle';
import CreateComponentIcon from '@material-ui/icons/Create';
import AccountTreeIcon from '@material-ui/icons/AccountTree';
import {
Sidebar,
@@ -80,6 +81,7 @@ const Root: FC<{}> = ({ children }) => (
<SidebarSpacer />
<SidebarDivider />
<SidebarItem icon={HomeIcon} to="/" text="Home" />
<SidebarItem icon={CreateComponentIcon} to="/scaffolder" text="Create" />
<SidebarItem icon={AccountTreeIcon} to="/inventory" text="Inventory" />
<SidebarItem icon={AccountCircle} to="/login" text="Login" />
<SidebarDivider />
+1
View File
@@ -18,4 +18,5 @@ export { plugin as HomePagePlugin } from '@backstage/plugin-home-page';
export { plugin as WelcomePlugin } from '@backstage/plugin-welcome';
export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
export { plugin as InventoryPlugin } from '@backstage/plugin-inventory';
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
export { plugin as TechRadar } from '@backstage/plugin-tech-radar';
+1
View File
@@ -17,6 +17,7 @@
"dependencies": {
"@backstage/backend-common": "0.1.1-alpha.4",
"@backstage/plugin-inventory-backend": "0.1.1-alpha.4",
"@backstage/plugin-scaffolder-backend": "0.1.1-alpha.4",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
+4
View File
@@ -34,12 +34,15 @@ import cors from 'cors';
import express from 'express';
import helmet from 'helmet';
import { testRouter } from './test';
import { createScaffolder } from '@backstage/plugin-scaffolder-backend';
const DEFAULT_PORT = 7000;
const PORT = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT;
const app = express();
const scaffolder = createScaffolder();
app.use(helmet());
app.use(cors());
app.use(compression());
@@ -47,6 +50,7 @@ app.use(express.json());
app.use(requestLoggingHandler());
app.use('/test', testRouter);
app.use('/inventory', inventoryRouter);
app.use('/scaffolder', scaffolder);
app.use(errorHandler());
app.use(notFoundHandler());
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
ignorePatterns: ['sample-templates/'],
rules: {
'no-console': 0, // Permitted in console programs
'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()'
},
};
+6
View File
@@ -0,0 +1,6 @@
# Title
Welcome to the scaffolder plugin!
## Sub-section 1
## Sub-section 2
+25
View File
@@ -0,0 +1,25 @@
{
"name": "@backstage/plugin-scaffolder-backend",
"version": "0.1.1-alpha.4",
"main": "dist",
"license": "Apache-2.0",
"private": true,
"scripts": {
"build": "tsc",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"clean": "backstage-cli clean"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.4",
"@types/fs-extra": "^8.1.0",
"@types/supertest": "^2.0.8",
"supertest": "^4.0.2"
},
"dependencies": {
"express": "^4.17.1",
"fs-extra": "^9.0.0",
"glob": "^7.1.6",
"winston": "^3.2.1"
}
}
@@ -0,0 +1,12 @@
#!/bin/bash
# package name is "__component_id__" so that yarn doesn't throw an error
# about invalid characters when running yarn commands. here we replace it with the actual name
sed -i -e "s/__component_id__/{{ cookiecutter.component_id }}/g" package.json
# node_modules was moved out of the template folder, during the pre_gen hook,
# to avoid cookie_cutter from copying all of them. time to move it back
mv ../../node_modules.tmp ../../\{\{cookiecutter.component_id\}\}/node_modules 2>/dev/null ||:
# move back the build directory that was moved out in the pre_gen hook (if it exists)
mv ../../build.tmp ../../\{\{cookiecutter.component_id\}\}/build 2>/dev/null ||:
@@ -0,0 +1,9 @@
#!/bin/bash
# no way to ignore files in cookiecutter, so move node_modules out while building
# to avoid cookiecutter from copying all of them
mv ../../\{\{cookiecutter.component_id\}\}/node_modules ../../node_modules.tmp 2>/dev/null ||:
# cookicutter really doesn't like the next.js build directory, so if the app has
# been built from inside the template folder, that folders needs to be moved out as well
mv ../../\{\{cookiecutter.component_id\}\}/build ../../build.tmp 2>/dev/null ||:
@@ -0,0 +1,6 @@
{
"id": "react-ssr-template",
"name": "SSR React Website",
"description": "Next.js application skeleton for creating isomorphic web applications.",
"ownerId": "something"
}
@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
insert_final_newline = false
@@ -0,0 +1 @@
module.exports = require('@spotify/web-scripts/config/eslintrc.js');
@@ -0,0 +1,39 @@
name: Frontend CI
on:
push:
paths:
- '.'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- name: checkout code
uses: actions/checkout@v1
- name: get yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: yarn install, build, and test
working-directory: .
run: |
yarn install
yarn build --if-present
yarn test
env:
CI: true
@@ -0,0 +1,18 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
junit.xml
# build
/build
# misc
.DS_Store
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,14 @@
# {{ cookiecutter.component_id }}
## Description
{{ cookiecutter.description }}
## Develop
```bash
# install dependencies
$ yarn
# start development server
$ yarn dev
```
@@ -0,0 +1,4 @@
module.exports = {
plugins: ['babel-plugin-styled-components'],
presets: ['next/babel', '@zeit/next-typescript/babel'],
};
@@ -0,0 +1,8 @@
module.exports = {
...require('@spotify/web-scripts/config/jest.config.js'),
testEnvironment: 'jsdom',
testPathIgnorePatterns: ['/node_modules/', '/build/'],
transform: {
'^.+\\.tsx?$': 'babel-jest',
},
};
@@ -0,0 +1,5 @@
// read more about this file here ---> https://github.com/zeit/next.js/blob/canary/docs/basic-features/typescript.md
/* eslint spaced-comment: ["error", "always", { "markers": ["/"] }] */
/// <reference types="next" />
/// <reference types="next/types/global" />
@@ -0,0 +1,3 @@
module.exports = {
distDir: 'build',
};
@@ -0,0 +1,52 @@
{
"name": "__component_id__",
"version": "0.0.0",
"description": "{{ cookiecutter.description }}",
"license": "UNLICENSED",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"lint": "web-scripts lint --ignore-path=.gitignore",
"test": "web-scripts test --config jest.config.js",
"pretest:ci": "yarn lint",
"test:ci": "yarn test --ci --coverage --reporters=default --reporters=jest-junit"
},
"dependencies": {
"@zeit/next-typescript": "^1.1.1",
"babel-plugin-styled-components": "^1.10.6",
"next": "^9.1.1",
"react": "^16.8.5",
"react-dom": "^16.8.5",
"styled-components": "^4.3.2"
},
"devDependencies": {
"@spotify/tsconfig": "^5.0.0",
"@spotify/web-scripts": "^5.0.0",
"@testing-library/react": "^8.0.1",
"@types/node": "^13.1.4",
"@types/react": "^16.8.7",
"@types/react-dom": "^16.8.2",
"@types/styled-components": "^4.1.18",
"husky": "^2.7.0",
"jest-junit": "^8.0.0",
"typescript": "^3.4.5"
},
"husky": {
"hooks": {
"pre-commit": "web-scripts precommit"
}
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
@@ -0,0 +1 @@
module.exports = require('@spotify/web-scripts/config/prettier.config.js');
@@ -0,0 +1,13 @@
import React from 'react';
import { cleanup, render } from '@testing-library/react';
import Index from '../pages/index';
afterEach(cleanup);
describe('Index', () => {
it('Says hello', () => {
const { queryByText } = render(<Index />);
expect(queryByText('Hello!')).toBeTruthy();
});
});
@@ -0,0 +1,3 @@
import React from 'react';
export const Header = () => <h1>Header</h1>;
@@ -0,0 +1,37 @@
import React from 'react';
import App from 'next/app';
import Head from 'next/head';
import styled from 'styled-components';
import { Header } from '../components/Header';
const StyledApp = styled.div`
> * {
padding-left: 16px;
padding-right: 16px;
}
`;
const Main = styled.div`
margin: 2em auto;
height: 85vh;
`;
class CustomApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<StyledApp>
<Head>
<link rel="stylesheet" href="/static/fonts.css" />
</Head>
<Header />
<Main>
<Component {...pageProps} />
</Main>
</StyledApp>
);
}
}
export default CustomApp;
@@ -0,0 +1,41 @@
/**
* This file extends the <Document /> and injects the server side rendered styles into the <head>
* By server-side rendering CSS we avoid visual changes in the layout while loading the JS.
*
* Taken from this example:
* https://github.com/zeit/next.js/tree/master/examples/with-styled-components
*/
import React from 'react';
import Document, { DocumentContext, DocumentInitialProps } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
static async getInitialProps(
ctx: DocumentContext,
): Promise<DocumentInitialProps> {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App: any) => props =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
}
@@ -0,0 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next';
export default function handle(_: NextApiRequest, res: NextApiResponse) {
res.status(200).send('ok');
}
@@ -0,0 +1,5 @@
import React from 'react';
const Index = () => <h1>Hello!</h1>;
export default Index;
@@ -0,0 +1,20 @@
{
"extends": "@spotify/tsconfig",
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": ["node_modules", "output/node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
+44
View File
@@ -0,0 +1,44 @@
/*
* 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.
*/
import { TemplaterConfig, createTemplater } from './lib/templater';
import { createStorage, StorageConfig } from './lib/storage';
import express from 'express';
export const createScaffolder = (config?: TemplaterConfig & StorageConfig) => {
const store = createStorage(config);
const templater = createTemplater(config);
const router = express.Router();
router.get('/v1/templates', async (_, res) => {
const templates = await store.list();
res.status(200).json(templates);
});
router.post('/v1/job/create', async (_, res) => {
// TODO(blam): Actually make this function work'
const mock = 'templateid';
res.status(201).json({ accepted: true });
const path = await store.prepare(mock);
await templater.run({ directory: path, values: { componentId: 'test' } });
});
return router;
};
@@ -0,0 +1,41 @@
/*
* 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.
*/
import * as winston from 'winston';
export const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
//
// - Write all logs with level `error` and below to `error.log`
// - Write all logs with level `info` and below to `combined.log`
//
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
//
if (process.env.NODE_ENV !== 'production') {
logger.add(
new winston.transports.Console({
format: winston.format.simple(),
}),
);
}
@@ -0,0 +1,83 @@
/*
* 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.
*/
import { DiskStorage } from './disk';
import * as path from 'path';
describe('Disk Storage', () => {
it('should load a simple template from a simple directory', async () => {
const testTemplateDir = path.resolve(
__dirname,
'../../../test/mock-simple-template-dir',
);
const templateInfo = require(`${testTemplateDir}/mock-template/template-info.json`);
const repository = new DiskStorage(testTemplateDir);
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(1);
expect(templates[0].id).toBe(templateInfo.id);
expect(templates[0].name).toBe(templateInfo.name);
expect(templates[0].description).toBe(templateInfo.description);
expect(templates[0].ownerId).toBe(templateInfo.ownerId);
});
it('should successfully load multiple templates from the same folder', async () => {
const testTemplateDir = path.resolve(
__dirname,
'../../../test/mock-multiple-templates-dir',
);
const repository = new DiskStorage(testTemplateDir);
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(2);
});
it('should return empty array when there are no templates', async () => {
const testTemplateDir = path.resolve(
__dirname,
'/some-folder-that-deffo-does-not-exist',
);
const repository = new DiskStorage(testTemplateDir);
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(0);
});
it('should be able to handle templates with invalid json and ignore them from the returned array', async () => {
const testTemplateDir = path.resolve(
__dirname,
'../../../test/mock-failing-template-dir',
);
const repository = new DiskStorage(testTemplateDir);
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(1);
});
});
@@ -0,0 +1,101 @@
/*
* 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.
*/
import glob from 'glob';
import fs from 'fs-extra';
import { logger } from '../logger';
import { Template, StorageBase as Base } from '.';
interface DiskIndexEntry {
contents: Template;
location: string;
}
export class DiskStorage implements Base {
private repository: Template[] = [];
private localIndex: DiskIndexEntry[] = [];
constructor(private repoDir = `${__dirname}/../../../sample-templates`) {
console.warn(require('path').resolve(repoDir));
}
public async list(): Promise<Template[]> {
if (this.repository.length === 0) {
await this.reindex();
}
return this.repository;
}
public async reindex(): Promise<void> {
this.localIndex = await this.index();
this.repository = this.localIndex.map(({ contents }) => contents);
}
public async prepare(templateId: string): Promise<string> {
const template = this.localIndex.find(
({ contents }) => contents.id === templateId,
);
if (!template) {
throw new Error('Template no found');
}
const tempDir = await fs.promises.mkdtemp(templateId);
await fs.copy(template.location, tempDir);
return tempDir;
}
private async index(): Promise<DiskIndexEntry[]> {
return new Promise((resolve, reject) => {
glob(`${this.repoDir}/**/template-info.json`, async (err, matches) => {
if (err) {
reject(err);
}
const fileContents: Array<{
location: string;
contents: string;
}> = await Promise.all(
matches.map(async (location: string) => ({
location,
contents: await fs.readFile(location, 'utf-8'),
})),
);
const validFiles = fileContents.reduce(
(diskIndexEntries: DiskIndexEntry[], currentFile) => {
try {
const parsed: Template = JSON.parse(currentFile.contents);
return [
...diskIndexEntries,
{ location: currentFile.location, contents: parsed },
];
} catch (ex) {
logger.error('Failure parsing JSON for template', {
path: currentFile.location,
});
}
return diskIndexEntries;
},
[],
);
resolve(validFiles);
});
});
}
}
@@ -0,0 +1,55 @@
/*
* 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.
*/
import { StorageBase, createStorage } from '.';
describe('Storage Interface Test', () => {
const mockStore = new (class MockStorage implements StorageBase {
list = jest.fn();
prepare = jest.fn();
reindex = jest.fn();
public reset = () => {
this.list.mockReset();
this.prepare.mockReset();
this.reindex.mockReset();
};
})();
afterEach(() => mockStore.reset());
it('should call list of the set repo when calling list', async () => {
const store = createStorage({ store: mockStore });
await store.list();
expect(mockStore.list).toHaveBeenCalled();
});
it('should reindex on the repo when calling reindex', async () => {
const store = createStorage({ store: mockStore });
await store.reindex();
expect(mockStore.reindex).toHaveBeenCalled();
});
it('should call prepare with the correct id when calling prepare', async () => {
const store = createStorage({ store: mockStore });
await store.prepare('testid');
expect(mockStore.prepare).toHaveBeenCalledWith('testid');
});
});
@@ -0,0 +1,54 @@
import { DiskStorage } from './disk';
/*
* 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.
*/
export interface Template {
id: string;
name: string;
description: string;
ownerId: string;
}
export abstract class StorageBase {
// lists all templates available
abstract async list(): Promise<Template[]>;
// can be used to build an index of the available templates;
abstract async reindex(): Promise<void>;
// returns a directory to run the templaterin
abstract async prepare(id: string): Promise<string>;
}
export interface StorageConfig {
store?: StorageBase;
}
class Storage implements StorageBase {
store?: StorageBase;
constructor({ store }: StorageConfig) {
this.store = store;
}
list = () => this.store!.list();
prepare = (id: string) => this.store!.prepare(id);
reindex = () => this.store!.reindex();
}
export const createStorage = (
config: StorageConfig = { store: new DiskStorage() },
): StorageBase => {
return new Storage(config);
};
@@ -0,0 +1,32 @@
import { TemplaterBase, TemplaterRunOptions } from '.';
/*
* 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.
*/
import fs from 'fs-extra';
export class CookieCutter implements TemplaterBase {
public async run(options: TemplaterRunOptions): Promise<string> {
// first we need to make cookiecutter.json in the directory provided with the input values.
const cookieInfo = {
_copy_without_render: ['.github/workflows/*'],
...options.values,
};
await fs.writeJSON(options.directory, cookieInfo);
return '';
// run cookie cutter with new json
}
}
@@ -0,0 +1,52 @@
/*
* 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.
*/
import { CookieCutter } from './cookiecutter';
export interface RequiredTemplateValues {
componentId: string;
}
export interface TemplaterRunOptions {
directory: string;
values: RequiredTemplateValues & object;
}
export abstract class TemplaterBase {
// runs the templating with the values and returns the directory to push the VCS
abstract async run(opts: TemplaterRunOptions): Promise<string>;
}
export interface TemplaterConfig {
templater?: TemplaterBase;
}
class Templater implements TemplaterBase {
templater?: TemplaterBase;
constructor({ templater }: TemplaterConfig) {
this.templater = templater;
}
public async run(opts: TemplaterRunOptions) {
return this.templater!.run(opts);
}
}
export const createTemplater = (
config: TemplaterConfig = { templater: new CookieCutter() },
): TemplaterBase => {
return new Templater(config);
};
@@ -0,0 +1,6 @@
{
"id": "mock-template-2",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
@@ -0,0 +1,7 @@
{
"id": "mock-template",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam",
<heres some invalid hson>
}
@@ -0,0 +1,6 @@
{
"id": "mock-template-2",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
@@ -0,0 +1,6 @@
{
"id": "mock-template",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
@@ -0,0 +1,6 @@
{
"id": "mock-template",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../packages/backend/tsconfig.json",
"include": [
"./src"
],
"compilerOptions": {
"baseUrl": "./src",
"outDir": "./dist",
"skipLibCheck": true
}
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+13
View File
@@ -0,0 +1,13 @@
# Inventory Frontend
WORK IN PROGRESS
This is the frontend part of the default inventory plugin.
It will implement the core API for handling your inventory of software, and
supply the base views to show and manage them.
## Links
- (Backend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/inventory-backend]
- (The Backstage homepage)[https://backstage.io]
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@backstage/plugin-scaffolder",
"version": "0.1.1-alpha.4",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts",
"license": "Apache-2.0",
"private": true,
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"clean": "backstage-cli clean"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.4",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/testing-library__jest-dom": "5.0.2",
"jest-fetch-mock": "^3.0.3"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.4",
"@backstage/theme": "^0.1.1-alpha.4",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-use": "^13.0.0"
},
"files": [
"dist"
]
}
@@ -0,0 +1,52 @@
/*
* 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.
*/
import React from 'react';
import { Content, InfoCard, Header, Page, pageTheme } from '@backstage/core';
// TODO(blam): Connect to backend
const STATIC_DATA = [
{
id: 'react-ssr-template',
name: 'SSR React Website',
description:
'Next.js application skeleton for creating isomorphic web applications.',
ownerId: 'something',
},
];
const ScaffolderPage: React.FC<{}> = () => {
return (
<Page theme={pageTheme.home}>
<Header title="Create a new component" subtitle="All your stuff" />
<Content>
<div style={{ display: 'flex' }}>
{STATIC_DATA.map(item => {
return (
<InfoCard
title={item.name}
deepLink={{ title: 'Create', link: '#' }}
>
<p>{item.description}</p>
</InfoCard>
);
})}
</div>
</Content>
</Page>
);
};
export default ScaffolderPage;
+17
View File
@@ -0,0 +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.
*/
export { plugin } from './plugin';
+23
View File
@@ -0,0 +1,23 @@
/*
* 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.
*/
import { plugin } from './plugin';
describe('inventory', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
});
+25
View File
@@ -0,0 +1,25 @@
/*
* 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.
*/
import { createPlugin } from '@backstage/core';
import ScaffolderPage from './components/ScaffolderPage';
export const plugin = createPlugin({
id: 'scaffolder',
register({ router }) {
router.registerRoute('/scaffolder', ScaffolderPage);
},
});
+18
View File
@@ -0,0 +1,18 @@
/*
* 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.
*/
import '@testing-library/jest-dom/extend-expect';
require('jest-fetch-mock').enableMocks();
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {
"baseUrl": "src"
}
}