diff --git a/packages/app/package.json b/packages/app/package.json
index 7ccb6c9a91..a7a370b108 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -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",
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 61adbe9ddc..2831ff0a72 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -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 }) => (
+
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index a7e425b869..06c5084bd0 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -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';
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 3ac52f6c4e..32be276ef2 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -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",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index e0fd44430d..1a2c7b70e6 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -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());
diff --git a/plugins/scaffolder-backend/.eslintrc.js b/plugins/scaffolder-backend/.eslintrc.js
new file mode 100644
index 0000000000..56b9c0bb2e
--- /dev/null
+++ b/plugins/scaffolder-backend/.eslintrc.js
@@ -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()'
+ },
+};
diff --git a/plugins/scaffolder-backend/README.md b/plugins/scaffolder-backend/README.md
new file mode 100644
index 0000000000..7ffbeae1d1
--- /dev/null
+++ b/plugins/scaffolder-backend/README.md
@@ -0,0 +1,6 @@
+# Title
+Welcome to the scaffolder plugin!
+
+## Sub-section 1
+
+## Sub-section 2
diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json
new file mode 100644
index 0000000000..5bfde524c7
--- /dev/null
+++ b/plugins/scaffolder-backend/package.json
@@ -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"
+ }
+}
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/post_gen_project.sh b/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/post_gen_project.sh
new file mode 100644
index 0000000000..c6d477d91a
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/post_gen_project.sh
@@ -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 ||:
\ No newline at end of file
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/pre_gen_project.sh b/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/pre_gen_project.sh
new file mode 100644
index 0000000000..142fda9ad9
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/pre_gen_project.sh
@@ -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 ||:
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json
new file mode 100644
index 0000000000..0a03fd95d6
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json
@@ -0,0 +1,6 @@
+{
+ "id": "react-ssr-template",
+ "name": "SSR React Website",
+ "description": "Next.js application skeleton for creating isomorphic web applications.",
+ "ownerId": "something"
+}
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.editorconfig b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.editorconfig
new file mode 100644
index 0000000000..23c3268a0f
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.editorconfig
@@ -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
\ No newline at end of file
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.eslintignore b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.eslintignore
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.eslintignore
@@ -0,0 +1 @@
+
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.eslintrc.js b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.eslintrc.js
new file mode 100644
index 0000000000..8623614899
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@spotify/web-scripts/config/eslintrc.js');
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml
new file mode 100644
index 0000000000..9087876ce2
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml
@@ -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
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.gitignore b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.gitignore
new file mode 100644
index 0000000000..4d1e0d9635
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.gitignore
@@ -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*
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.nvmrc b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.nvmrc
new file mode 100644
index 0000000000..b009dfb9d9
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.nvmrc
@@ -0,0 +1 @@
+lts/*
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/README.md b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/README.md
new file mode 100644
index 0000000000..415065179b
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/README.md
@@ -0,0 +1,14 @@
+# {{ cookiecutter.component_id }}
+
+## Description
+
+{{ cookiecutter.description }}
+
+## Develop
+
+```bash
+# install dependencies
+$ yarn
+# start development server
+$ yarn dev
+```
\ No newline at end of file
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/babel.config.js b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/babel.config.js
new file mode 100644
index 0000000000..db2d280677
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/babel.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ plugins: ['babel-plugin-styled-components'],
+ presets: ['next/babel', '@zeit/next-typescript/babel'],
+};
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/jest.config.js b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/jest.config.js
new file mode 100644
index 0000000000..0dc2a9c0ca
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/jest.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ ...require('@spotify/web-scripts/config/jest.config.js'),
+ testEnvironment: 'jsdom',
+ testPathIgnorePatterns: ['/node_modules/', '/build/'],
+ transform: {
+ '^.+\\.tsx?$': 'babel-jest',
+ },
+};
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/next-env.d.ts b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/next-env.d.ts
new file mode 100644
index 0000000000..fd10310f68
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/next-env.d.ts
@@ -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": ["/"] }] */
+
+///
+///
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/next.config.js b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/next.config.js
new file mode 100644
index 0000000000..be8b159307
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/next.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ distDir: 'build',
+};
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/package.json b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/package.json
new file mode 100644
index 0000000000..cf9d469e39
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/package.json
@@ -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"
+ ]
+ }
+}
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/prettier.config.js b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/prettier.config.js
new file mode 100644
index 0000000000..93df970dd6
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/prettier.config.js
@@ -0,0 +1 @@
+module.exports = require('@spotify/web-scripts/config/prettier.config.js');
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/public/static/fonts.css b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/public/static/fonts.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/__tests__/index.test.tsx b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/__tests__/index.test.tsx
new file mode 100644
index 0000000000..8612ead08f
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/__tests__/index.test.tsx
@@ -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();
+ expect(queryByText('Hello!')).toBeTruthy();
+ });
+});
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/components/Header.tsx b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/components/Header.tsx
new file mode 100644
index 0000000000..f8ba94310a
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/components/Header.tsx
@@ -0,0 +1,3 @@
+import React from 'react';
+
+export const Header = () =>
Header
;
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/_app.tsx b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/_app.tsx
new file mode 100644
index 0000000000..a3853e47c9
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/_app.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default CustomApp;
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/_document.tsx b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/_document.tsx
new file mode 100644
index 0000000000..210de6412c
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/_document.tsx
@@ -0,0 +1,41 @@
+/**
+ * This file extends the and injects the server side rendered styles into the
+ * 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 {
+ const sheet = new ServerStyleSheet();
+ const originalRenderPage = ctx.renderPage;
+
+ try {
+ ctx.renderPage = () =>
+ originalRenderPage({
+ enhanceApp: (App: any) => props =>
+ sheet.collectStyles(),
+ });
+
+ const initialProps = await Document.getInitialProps(ctx);
+ return {
+ ...initialProps,
+ styles: (
+ <>
+ {initialProps.styles}
+ {sheet.getStyleElement()}
+ >
+ ),
+ };
+ } finally {
+ sheet.seal();
+ }
+ }
+}
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/api/ping.ts b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/api/ping.ts
new file mode 100644
index 0000000000..8bc772c182
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/api/ping.ts
@@ -0,0 +1,5 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+
+export default function handle(_: NextApiRequest, res: NextApiResponse) {
+ res.status(200).send('ok');
+}
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/index.tsx b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/index.tsx
new file mode 100644
index 0000000000..c01d844f9f
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/index.tsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+const Index = () =>
Hello!
;
+
+export default Index;
diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/tsconfig.json b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/tsconfig.json
new file mode 100644
index 0000000000..03508ff043
--- /dev/null
+++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/tsconfig.json
@@ -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"]
+}
diff --git a/plugins/scaffolder-backend/src/index.ts b/plugins/scaffolder-backend/src/index.ts
new file mode 100644
index 0000000000..8c790c51f9
--- /dev/null
+++ b/plugins/scaffolder-backend/src/index.ts
@@ -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;
+};
diff --git a/plugins/scaffolder-backend/src/lib/logger.ts b/plugins/scaffolder-backend/src/lib/logger.ts
new file mode 100644
index 0000000000..c3a4a4e26c
--- /dev/null
+++ b/plugins/scaffolder-backend/src/lib/logger.ts
@@ -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(),
+ }),
+ );
+}
diff --git a/plugins/scaffolder-backend/src/lib/storage/disk.test.ts b/plugins/scaffolder-backend/src/lib/storage/disk.test.ts
new file mode 100644
index 0000000000..d06d56a519
--- /dev/null
+++ b/plugins/scaffolder-backend/src/lib/storage/disk.test.ts
@@ -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);
+ });
+});
diff --git a/plugins/scaffolder-backend/src/lib/storage/disk.ts b/plugins/scaffolder-backend/src/lib/storage/disk.ts
new file mode 100644
index 0000000000..6f3c5f14dc
--- /dev/null
+++ b/plugins/scaffolder-backend/src/lib/storage/disk.ts
@@ -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 {
+ if (this.repository.length === 0) {
+ await this.reindex();
+ }
+
+ return this.repository;
+ }
+
+ public async reindex(): Promise {
+ this.localIndex = await this.index();
+ this.repository = this.localIndex.map(({ contents }) => contents);
+ }
+
+ public async prepare(templateId: string): Promise {
+ 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 {
+ 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);
+ });
+ });
+ }
+}
diff --git a/plugins/scaffolder-backend/src/lib/storage/index.test.ts b/plugins/scaffolder-backend/src/lib/storage/index.test.ts
new file mode 100644
index 0000000000..2f44936023
--- /dev/null
+++ b/plugins/scaffolder-backend/src/lib/storage/index.test.ts
@@ -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');
+ });
+});
diff --git a/plugins/scaffolder-backend/src/lib/storage/index.ts b/plugins/scaffolder-backend/src/lib/storage/index.ts
new file mode 100644
index 0000000000..20677614d6
--- /dev/null
+++ b/plugins/scaffolder-backend/src/lib/storage/index.ts
@@ -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;
+ // can be used to build an index of the available templates;
+ abstract async reindex(): Promise;
+ // returns a directory to run the templaterin
+ abstract async prepare(id: string): Promise;
+}
+
+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);
+};
diff --git a/plugins/scaffolder-backend/src/lib/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/lib/templater/cookiecutter.ts
new file mode 100644
index 0000000000..c2bf472165
--- /dev/null
+++ b/plugins/scaffolder-backend/src/lib/templater/cookiecutter.ts
@@ -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 {
+ // 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
+ }
+}
diff --git a/plugins/scaffolder-backend/src/lib/templater/index.ts b/plugins/scaffolder-backend/src/lib/templater/index.ts
new file mode 100644
index 0000000000..b7d7edf243
--- /dev/null
+++ b/plugins/scaffolder-backend/src/lib/templater/index.ts
@@ -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;
+}
+
+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);
+};
diff --git a/plugins/scaffolder-backend/test/mock-failing-template-dir/mock-template-2/template-info.json b/plugins/scaffolder-backend/test/mock-failing-template-dir/mock-template-2/template-info.json
new file mode 100644
index 0000000000..47f83f9f5c
--- /dev/null
+++ b/plugins/scaffolder-backend/test/mock-failing-template-dir/mock-template-2/template-info.json
@@ -0,0 +1,6 @@
+{
+ "id": "mock-template-2",
+ "name": "mockmannen",
+ "description": "mock template for building stuff in backstage",
+ "ownerId": "blam"
+}
diff --git a/plugins/scaffolder-backend/test/mock-failing-template-dir/mock-template/template-info.json b/plugins/scaffolder-backend/test/mock-failing-template-dir/mock-template/template-info.json
new file mode 100644
index 0000000000..9a3458fba1
--- /dev/null
+++ b/plugins/scaffolder-backend/test/mock-failing-template-dir/mock-template/template-info.json
@@ -0,0 +1,7 @@
+{
+ "id": "mock-template",
+ "name": "mockmannen",
+ "description": "mock template for building stuff in backstage",
+ "ownerId": "blam",
+
+}
diff --git a/plugins/scaffolder-backend/test/mock-multiple-templates-dir/mock-template-2/template-info.json b/plugins/scaffolder-backend/test/mock-multiple-templates-dir/mock-template-2/template-info.json
new file mode 100644
index 0000000000..47f83f9f5c
--- /dev/null
+++ b/plugins/scaffolder-backend/test/mock-multiple-templates-dir/mock-template-2/template-info.json
@@ -0,0 +1,6 @@
+{
+ "id": "mock-template-2",
+ "name": "mockmannen",
+ "description": "mock template for building stuff in backstage",
+ "ownerId": "blam"
+}
diff --git a/plugins/scaffolder-backend/test/mock-multiple-templates-dir/mock-template/template-info.json b/plugins/scaffolder-backend/test/mock-multiple-templates-dir/mock-template/template-info.json
new file mode 100644
index 0000000000..e0d323c987
--- /dev/null
+++ b/plugins/scaffolder-backend/test/mock-multiple-templates-dir/mock-template/template-info.json
@@ -0,0 +1,6 @@
+{
+ "id": "mock-template",
+ "name": "mockmannen",
+ "description": "mock template for building stuff in backstage",
+ "ownerId": "blam"
+}
diff --git a/plugins/scaffolder-backend/test/mock-simple-template-dir/mock-template/template-info.json b/plugins/scaffolder-backend/test/mock-simple-template-dir/mock-template/template-info.json
new file mode 100644
index 0000000000..e0d323c987
--- /dev/null
+++ b/plugins/scaffolder-backend/test/mock-simple-template-dir/mock-template/template-info.json
@@ -0,0 +1,6 @@
+{
+ "id": "mock-template",
+ "name": "mockmannen",
+ "description": "mock template for building stuff in backstage",
+ "ownerId": "blam"
+}
diff --git a/plugins/scaffolder-backend/tsconfig.json b/plugins/scaffolder-backend/tsconfig.json
new file mode 100644
index 0000000000..7d4ea182e2
--- /dev/null
+++ b/plugins/scaffolder-backend/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../packages/backend/tsconfig.json",
+ "include": [
+ "./src"
+ ],
+ "compilerOptions": {
+ "baseUrl": "./src",
+ "outDir": "./dist",
+ "skipLibCheck": true
+ }
+}
diff --git a/plugins/scaffolder/.eslintrc.js b/plugins/scaffolder/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/plugins/scaffolder/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/plugins/scaffolder/README.md b/plugins/scaffolder/README.md
new file mode 100644
index 0000000000..e77a77148f
--- /dev/null
+++ b/plugins/scaffolder/README.md
@@ -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]
diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json
new file mode 100644
index 0000000000..878b4ddbf2
--- /dev/null
+++ b/plugins/scaffolder/package.json
@@ -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"
+ ]
+}
diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx
new file mode 100644
index 0000000000..e6adaca099
--- /dev/null
+++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx
@@ -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 (
+
+
+
+
+ {STATIC_DATA.map(item => {
+ return (
+
+
{item.description}
+
+ );
+ })}
+
+
+
+ );
+};
+
+export default ScaffolderPage;
diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts
new file mode 100644
index 0000000000..3a0a0fe2d3
--- /dev/null
+++ b/plugins/scaffolder/src/index.ts
@@ -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';
diff --git a/plugins/scaffolder/src/plugin.test.ts b/plugins/scaffolder/src/plugin.test.ts
new file mode 100644
index 0000000000..4fac72593f
--- /dev/null
+++ b/plugins/scaffolder/src/plugin.test.ts
@@ -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();
+ });
+});
diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts
new file mode 100644
index 0000000000..3dfa5d0b89
--- /dev/null
+++ b/plugins/scaffolder/src/plugin.ts
@@ -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);
+ },
+});
diff --git a/plugins/scaffolder/src/setupTests.ts b/plugins/scaffolder/src/setupTests.ts
new file mode 100644
index 0000000000..1a907ab8e6
--- /dev/null
+++ b/plugins/scaffolder/src/setupTests.ts
@@ -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();
diff --git a/plugins/scaffolder/tsconfig.json b/plugins/scaffolder/tsconfig.json
new file mode 100644
index 0000000000..7b73db2f0f
--- /dev/null
+++ b/plugins/scaffolder/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "baseUrl": "src"
+ }
+}