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" + } +}