Merge pull request #651 from spotify/blam/scaffolder
[Plugin] Start of the Scaffolder Plugin
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"@backstage/plugin-home-page": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-inventory": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-lighthouse": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-scaffolder": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-tech-radar": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-welcome": "^0.1.1-alpha.4",
|
||||
"@backstage/theme": "^0.1.1-alpha.4",
|
||||
|
||||
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
||||
import { Link, makeStyles, Typography } from '@material-ui/core';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import CreateComponentIcon from '@material-ui/icons/Create';
|
||||
import AccountTreeIcon from '@material-ui/icons/AccountTree';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -80,6 +81,7 @@ const Root: FC<{}> = ({ children }) => (
|
||||
<SidebarSpacer />
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={HomeIcon} to="/" text="Home" />
|
||||
<SidebarItem icon={CreateComponentIcon} to="/scaffolder" text="Create" />
|
||||
<SidebarItem icon={AccountTreeIcon} to="/inventory" text="Inventory" />
|
||||
<SidebarItem icon={AccountCircle} to="/login" text="Login" />
|
||||
<SidebarDivider />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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()'
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
# Title
|
||||
Welcome to the scaffolder plugin!
|
||||
|
||||
## Sub-section 1
|
||||
|
||||
## Sub-section 2
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+12
@@ -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 ||:
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# no way to ignore files in cookiecutter, so move node_modules out while building
|
||||
# to avoid cookiecutter from copying all of them
|
||||
mv ../../\{\{cookiecutter.component_id\}\}/node_modules ../../node_modules.tmp 2>/dev/null ||:
|
||||
|
||||
# cookicutter really doesn't like the next.js build directory, so if the app has
|
||||
# been built from inside the template folder, that folders needs to be moved out as well
|
||||
mv ../../\{\{cookiecutter.component_id\}\}/build ../../build.tmp 2>/dev/null ||:
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "react-ssr-template",
|
||||
"name": "SSR React Website",
|
||||
"description": "Next.js application skeleton for creating isomorphic web applications.",
|
||||
"ownerId": "something"
|
||||
}
|
||||
+13
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
module.exports = require('@spotify/web-scripts/config/eslintrc.js');
|
||||
+39
@@ -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
|
||||
+18
@@ -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*
|
||||
+1
@@ -0,0 +1 @@
|
||||
lts/*
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# {{ cookiecutter.component_id }}
|
||||
|
||||
## Description
|
||||
|
||||
{{ cookiecutter.description }}
|
||||
|
||||
## Develop
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
$ yarn
|
||||
# start development server
|
||||
$ yarn dev
|
||||
```
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
plugins: ['babel-plugin-styled-components'],
|
||||
presets: ['next/babel', '@zeit/next-typescript/babel'],
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
...require('@spotify/web-scripts/config/jest.config.js'),
|
||||
testEnvironment: 'jsdom',
|
||||
testPathIgnorePatterns: ['/node_modules/', '/build/'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'babel-jest',
|
||||
},
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// read more about this file here ---> https://github.com/zeit/next.js/blob/canary/docs/basic-features/typescript.md
|
||||
/* eslint spaced-comment: ["error", "always", { "markers": ["/"] }] */
|
||||
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
distDir: 'build',
|
||||
};
|
||||
+52
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
module.exports = require('@spotify/web-scripts/config/prettier.config.js');
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { cleanup, render } from '@testing-library/react';
|
||||
|
||||
import Index from '../pages/index';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('Index', () => {
|
||||
it('Says hello', () => {
|
||||
const { queryByText } = render(<Index />);
|
||||
expect(queryByText('Hello!')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Header = () => <h1>Header</h1>;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import App from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import styled from 'styled-components';
|
||||
import { Header } from '../components/Header';
|
||||
|
||||
const StyledApp = styled.div`
|
||||
> * {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Main = styled.div`
|
||||
margin: 2em auto;
|
||||
height: 85vh;
|
||||
`;
|
||||
|
||||
class CustomApp extends App {
|
||||
render() {
|
||||
const { Component, pageProps } = this.props;
|
||||
|
||||
return (
|
||||
<StyledApp>
|
||||
<Head>
|
||||
<link rel="stylesheet" href="/static/fonts.css" />
|
||||
</Head>
|
||||
<Header />
|
||||
<Main>
|
||||
<Component {...pageProps} />
|
||||
</Main>
|
||||
</StyledApp>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomApp;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* This file extends the <Document /> and injects the server side rendered styles into the <head>
|
||||
* By server-side rendering CSS we avoid visual changes in the layout while loading the JS.
|
||||
*
|
||||
* Taken from this example:
|
||||
* https://github.com/zeit/next.js/tree/master/examples/with-styled-components
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Document, { DocumentContext, DocumentInitialProps } from 'next/document';
|
||||
import { ServerStyleSheet } from 'styled-components';
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
static async getInitialProps(
|
||||
ctx: DocumentContext,
|
||||
): Promise<DocumentInitialProps> {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
|
||||
try {
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: (App: any) => props =>
|
||||
sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return {
|
||||
...initialProps,
|
||||
styles: (
|
||||
<>
|
||||
{initialProps.styles}
|
||||
{sheet.getStyleElement()}
|
||||
</>
|
||||
),
|
||||
};
|
||||
} finally {
|
||||
sheet.seal();
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default function handle(_: NextApiRequest, res: NextApiResponse) {
|
||||
res.status(200).send('ok');
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const Index = () => <h1>Hello!</h1>;
|
||||
|
||||
export default Index;
|
||||
+20
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TemplaterConfig, createTemplater } from './lib/templater';
|
||||
import { createStorage, StorageConfig } from './lib/storage';
|
||||
import express from 'express';
|
||||
|
||||
export const createScaffolder = (config?: TemplaterConfig & StorageConfig) => {
|
||||
const store = createStorage(config);
|
||||
|
||||
const templater = createTemplater(config);
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/v1/templates', async (_, res) => {
|
||||
const templates = await store.list();
|
||||
res.status(200).json(templates);
|
||||
});
|
||||
|
||||
router.post('/v1/job/create', async (_, res) => {
|
||||
// TODO(blam): Actually make this function work'
|
||||
|
||||
const mock = 'templateid';
|
||||
res.status(201).json({ accepted: true });
|
||||
|
||||
const path = await store.prepare(mock);
|
||||
await templater.run({ directory: path, values: { componentId: 'test' } });
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as winston from 'winston';
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
//
|
||||
// - Write all logs with level `error` and below to `error.log`
|
||||
// - Write all logs with level `info` and below to `combined.log`
|
||||
//
|
||||
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'combined.log' }),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// If we're not in production then log to the `console` with the format:
|
||||
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
|
||||
//
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { DiskStorage } from './disk';
|
||||
import * as path from 'path';
|
||||
describe('Disk Storage', () => {
|
||||
it('should load a simple template from a simple directory', async () => {
|
||||
const testTemplateDir = path.resolve(
|
||||
__dirname,
|
||||
'../../../test/mock-simple-template-dir',
|
||||
);
|
||||
const templateInfo = require(`${testTemplateDir}/mock-template/template-info.json`);
|
||||
|
||||
const repository = new DiskStorage(testTemplateDir);
|
||||
|
||||
await repository.reindex();
|
||||
|
||||
const templates = await repository.list();
|
||||
|
||||
expect(templates).toHaveLength(1);
|
||||
expect(templates[0].id).toBe(templateInfo.id);
|
||||
expect(templates[0].name).toBe(templateInfo.name);
|
||||
expect(templates[0].description).toBe(templateInfo.description);
|
||||
expect(templates[0].ownerId).toBe(templateInfo.ownerId);
|
||||
});
|
||||
|
||||
it('should successfully load multiple templates from the same folder', async () => {
|
||||
const testTemplateDir = path.resolve(
|
||||
__dirname,
|
||||
'../../../test/mock-multiple-templates-dir',
|
||||
);
|
||||
|
||||
const repository = new DiskStorage(testTemplateDir);
|
||||
|
||||
await repository.reindex();
|
||||
|
||||
const templates = await repository.list();
|
||||
|
||||
expect(templates).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should return empty array when there are no templates', async () => {
|
||||
const testTemplateDir = path.resolve(
|
||||
__dirname,
|
||||
'/some-folder-that-deffo-does-not-exist',
|
||||
);
|
||||
|
||||
const repository = new DiskStorage(testTemplateDir);
|
||||
|
||||
await repository.reindex();
|
||||
|
||||
const templates = await repository.list();
|
||||
|
||||
expect(templates).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should be able to handle templates with invalid json and ignore them from the returned array', async () => {
|
||||
const testTemplateDir = path.resolve(
|
||||
__dirname,
|
||||
'../../../test/mock-failing-template-dir',
|
||||
);
|
||||
|
||||
const repository = new DiskStorage(testTemplateDir);
|
||||
|
||||
await repository.reindex();
|
||||
|
||||
const templates = await repository.list();
|
||||
|
||||
expect(templates).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import glob from 'glob';
|
||||
import fs from 'fs-extra';
|
||||
import { logger } from '../logger';
|
||||
import { Template, StorageBase as Base } from '.';
|
||||
|
||||
interface DiskIndexEntry {
|
||||
contents: Template;
|
||||
location: string;
|
||||
}
|
||||
|
||||
export class DiskStorage implements Base {
|
||||
private repository: Template[] = [];
|
||||
private localIndex: DiskIndexEntry[] = [];
|
||||
|
||||
constructor(private repoDir = `${__dirname}/../../../sample-templates`) {
|
||||
console.warn(require('path').resolve(repoDir));
|
||||
}
|
||||
public async list(): Promise<Template[]> {
|
||||
if (this.repository.length === 0) {
|
||||
await this.reindex();
|
||||
}
|
||||
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
public async reindex(): Promise<void> {
|
||||
this.localIndex = await this.index();
|
||||
this.repository = this.localIndex.map(({ contents }) => contents);
|
||||
}
|
||||
|
||||
public async prepare(templateId: string): Promise<string> {
|
||||
const template = this.localIndex.find(
|
||||
({ contents }) => contents.id === templateId,
|
||||
);
|
||||
|
||||
if (!template) {
|
||||
throw new Error('Template no found');
|
||||
}
|
||||
|
||||
const tempDir = await fs.promises.mkdtemp(templateId);
|
||||
await fs.copy(template.location, tempDir);
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
private async index(): Promise<DiskIndexEntry[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
glob(`${this.repoDir}/**/template-info.json`, async (err, matches) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
const fileContents: Array<{
|
||||
location: string;
|
||||
contents: string;
|
||||
}> = await Promise.all(
|
||||
matches.map(async (location: string) => ({
|
||||
location,
|
||||
contents: await fs.readFile(location, 'utf-8'),
|
||||
})),
|
||||
);
|
||||
|
||||
const validFiles = fileContents.reduce(
|
||||
(diskIndexEntries: DiskIndexEntry[], currentFile) => {
|
||||
try {
|
||||
const parsed: Template = JSON.parse(currentFile.contents);
|
||||
return [
|
||||
...diskIndexEntries,
|
||||
{ location: currentFile.location, contents: parsed },
|
||||
];
|
||||
} catch (ex) {
|
||||
logger.error('Failure parsing JSON for template', {
|
||||
path: currentFile.location,
|
||||
});
|
||||
}
|
||||
|
||||
return diskIndexEntries;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
resolve(validFiles);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { StorageBase, createStorage } from '.';
|
||||
|
||||
describe('Storage Interface Test', () => {
|
||||
const mockStore = new (class MockStorage implements StorageBase {
|
||||
list = jest.fn();
|
||||
prepare = jest.fn();
|
||||
reindex = jest.fn();
|
||||
|
||||
public reset = () => {
|
||||
this.list.mockReset();
|
||||
this.prepare.mockReset();
|
||||
this.reindex.mockReset();
|
||||
};
|
||||
})();
|
||||
|
||||
afterEach(() => mockStore.reset());
|
||||
|
||||
it('should call list of the set repo when calling list', async () => {
|
||||
const store = createStorage({ store: mockStore });
|
||||
await store.list();
|
||||
|
||||
expect(mockStore.list).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reindex on the repo when calling reindex', async () => {
|
||||
const store = createStorage({ store: mockStore });
|
||||
|
||||
await store.reindex();
|
||||
|
||||
expect(mockStore.reindex).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call prepare with the correct id when calling prepare', async () => {
|
||||
const store = createStorage({ store: mockStore });
|
||||
|
||||
await store.prepare('testid');
|
||||
|
||||
expect(mockStore.prepare).toHaveBeenCalledWith('testid');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { DiskStorage } from './disk';
|
||||
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
}
|
||||
|
||||
export abstract class StorageBase {
|
||||
// lists all templates available
|
||||
abstract async list(): Promise<Template[]>;
|
||||
// can be used to build an index of the available templates;
|
||||
abstract async reindex(): Promise<void>;
|
||||
// returns a directory to run the templaterin
|
||||
abstract async prepare(id: string): Promise<string>;
|
||||
}
|
||||
|
||||
export interface StorageConfig {
|
||||
store?: StorageBase;
|
||||
}
|
||||
|
||||
class Storage implements StorageBase {
|
||||
store?: StorageBase;
|
||||
|
||||
constructor({ store }: StorageConfig) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
list = () => this.store!.list();
|
||||
prepare = (id: string) => this.store!.prepare(id);
|
||||
reindex = () => this.store!.reindex();
|
||||
}
|
||||
|
||||
export const createStorage = (
|
||||
config: StorageConfig = { store: new DiskStorage() },
|
||||
): StorageBase => {
|
||||
return new Storage(config);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { TemplaterBase, TemplaterRunOptions } from '.';
|
||||
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export class CookieCutter implements TemplaterBase {
|
||||
public async run(options: TemplaterRunOptions): Promise<string> {
|
||||
// first we need to make cookiecutter.json in the directory provided with the input values.
|
||||
const cookieInfo = {
|
||||
_copy_without_render: ['.github/workflows/*'],
|
||||
...options.values,
|
||||
};
|
||||
|
||||
await fs.writeJSON(options.directory, cookieInfo);
|
||||
return '';
|
||||
// run cookie cutter with new json
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CookieCutter } from './cookiecutter';
|
||||
|
||||
export interface RequiredTemplateValues {
|
||||
componentId: string;
|
||||
}
|
||||
export interface TemplaterRunOptions {
|
||||
directory: string;
|
||||
values: RequiredTemplateValues & object;
|
||||
}
|
||||
|
||||
export abstract class TemplaterBase {
|
||||
// runs the templating with the values and returns the directory to push the VCS
|
||||
abstract async run(opts: TemplaterRunOptions): Promise<string>;
|
||||
}
|
||||
|
||||
export interface TemplaterConfig {
|
||||
templater?: TemplaterBase;
|
||||
}
|
||||
|
||||
class Templater implements TemplaterBase {
|
||||
templater?: TemplaterBase;
|
||||
|
||||
constructor({ templater }: TemplaterConfig) {
|
||||
this.templater = templater;
|
||||
}
|
||||
|
||||
public async run(opts: TemplaterRunOptions) {
|
||||
return this.templater!.run(opts);
|
||||
}
|
||||
}
|
||||
|
||||
export const createTemplater = (
|
||||
config: TemplaterConfig = { templater: new CookieCutter() },
|
||||
): TemplaterBase => {
|
||||
return new Templater(config);
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "mock-template-2",
|
||||
"name": "mockmannen",
|
||||
"description": "mock template for building stuff in backstage",
|
||||
"ownerId": "blam"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "mock-template",
|
||||
"name": "mockmannen",
|
||||
"description": "mock template for building stuff in backstage",
|
||||
"ownerId": "blam",
|
||||
<heres some invalid hson>
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "mock-template-2",
|
||||
"name": "mockmannen",
|
||||
"description": "mock template for building stuff in backstage",
|
||||
"ownerId": "blam"
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "mock-template",
|
||||
"name": "mockmannen",
|
||||
"description": "mock template for building stuff in backstage",
|
||||
"ownerId": "blam"
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "mock-template",
|
||||
"name": "mockmannen",
|
||||
"description": "mock template for building stuff in backstage",
|
||||
"ownerId": "blam"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../packages/backend/tsconfig.json",
|
||||
"include": [
|
||||
"./src"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src",
|
||||
"outDir": "./dist",
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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]
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"start": "backstage-cli plugin:serve",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "5.0.2",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.4",
|
||||
"@backstage/theme": "^0.1.1-alpha.4",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-use": "^13.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Content, InfoCard, Header, Page, pageTheme } from '@backstage/core';
|
||||
|
||||
// TODO(blam): Connect to backend
|
||||
const STATIC_DATA = [
|
||||
{
|
||||
id: 'react-ssr-template',
|
||||
name: 'SSR React Website',
|
||||
description:
|
||||
'Next.js application skeleton for creating isomorphic web applications.',
|
||||
ownerId: 'something',
|
||||
},
|
||||
];
|
||||
const ScaffolderPage: React.FC<{}> = () => {
|
||||
return (
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header title="Create a new component" subtitle="All your stuff" />
|
||||
<Content>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{STATIC_DATA.map(item => {
|
||||
return (
|
||||
<InfoCard
|
||||
title={item.name}
|
||||
deepLink={{ title: 'Create', link: '#' }}
|
||||
>
|
||||
<p>{item.description}</p>
|
||||
</InfoCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScaffolderPage;
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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();
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user