feat(scaffolder): adding some more stuff and some initial sample template

This commit is contained in:
blam
2020-04-28 22:38:34 +02:00
parent e104fb007c
commit 9635983e5a
53 changed files with 2679 additions and 1600 deletions
+1
View File
@@ -1,4 +1,5 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
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,12 @@
#!/bin/bash
# package name is "__component_id__" so that yarn doesn't throw an error
# about invalid characters when running yarn commands. here we replace it with the actual name
sed -i -e "s/__component_id__/{{ cookiecutter.component_id }}/g" package.json
# node_modules was moved out of the template folder, during the pre_gen hook,
# to avoid cookie_cutter from copying all of them. time to move it back
mv ../../node_modules.tmp ../../\{\{cookiecutter.component_id\}\}/node_modules 2>/dev/null ||:
# move back the build directory that was moved out in the pre_gen hook (if it exists)
mv ../../build.tmp ../../\{\{cookiecutter.component_id\}\}/build 2>/dev/null ||:
@@ -0,0 +1,9 @@
#!/bin/bash
# no way to ignore files in cookiecutter, so move node_modules out while building
# to avoid cookiecutter from copying all of them
mv ../../\{\{cookiecutter.component_id\}\}/node_modules ../../node_modules.tmp 2>/dev/null ||:
# cookicutter really doesn't like the next.js build directory, so if the app has
# been built from inside the template folder, that folders needs to be moved out as well
mv ../../\{\{cookiecutter.component_id\}\}/build ../../build.tmp 2>/dev/null ||:
@@ -0,0 +1,6 @@
{
"id": "react-ssr-template",
"name": "SSR React Website",
"description": "Next.js application skeleton for creating isomorphic web applications.",
"ownerId": "something"
}
@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
insert_final_newline = false
@@ -0,0 +1 @@
module.exports = require('@spotify/web-scripts/config/eslintrc.js');
@@ -0,0 +1,39 @@
name: Frontend CI
on:
push:
paths:
- '.'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- name: checkout code
uses: actions/checkout@v1
- name: get yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: yarn install, build, and test
working-directory: .
run: |
yarn install
yarn build --if-present
yarn test
env:
CI: true
@@ -0,0 +1,18 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
junit.xml
# build
/build
# misc
.DS_Store
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,14 @@
# {{ cookiecutter.component_id }}
## Description
{{ cookiecutter.description }}
## Develop
```bash
# install dependencies
$ yarn
# start development server
$ yarn dev
```
@@ -0,0 +1,4 @@
module.exports = {
plugins: ['babel-plugin-styled-components'],
presets: ['next/babel', '@zeit/next-typescript/babel'],
};
@@ -0,0 +1,8 @@
module.exports = {
...require('@spotify/web-scripts/config/jest.config.js'),
testEnvironment: 'jsdom',
testPathIgnorePatterns: ['/node_modules/', '/build/'],
transform: {
'^.+\\.tsx?$': 'babel-jest',
},
};
@@ -0,0 +1,5 @@
// read more about this file here ---> https://github.com/zeit/next.js/blob/canary/docs/basic-features/typescript.md
/* eslint spaced-comment: ["error", "always", { "markers": ["/"] }] */
/// <reference types="next" />
/// <reference types="next/types/global" />
@@ -0,0 +1,3 @@
module.exports = {
distDir: 'build',
};
@@ -0,0 +1,52 @@
{
"name": "__component_id__",
"version": "0.0.0",
"description": "{{ cookiecutter.description }}",
"license": "UNLICENSED",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"lint": "web-scripts lint --ignore-path=.gitignore",
"test": "web-scripts test --config jest.config.js",
"pretest:ci": "yarn lint",
"test:ci": "yarn test --ci --coverage --reporters=default --reporters=jest-junit"
},
"dependencies": {
"@zeit/next-typescript": "^1.1.1",
"babel-plugin-styled-components": "^1.10.6",
"next": "^9.1.1",
"react": "^16.8.5",
"react-dom": "^16.8.5",
"styled-components": "^4.3.2"
},
"devDependencies": {
"@spotify/tsconfig": "^5.0.0",
"@spotify/web-scripts": "^5.0.0",
"@testing-library/react": "^8.0.1",
"@types/node": "^13.1.4",
"@types/react": "^16.8.7",
"@types/react-dom": "^16.8.2",
"@types/styled-components": "^4.1.18",
"husky": "^2.7.0",
"jest-junit": "^8.0.0",
"typescript": "^3.4.5"
},
"husky": {
"hooks": {
"pre-commit": "web-scripts precommit"
}
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
@@ -0,0 +1 @@
module.exports = require('@spotify/web-scripts/config/prettier.config.js');
@@ -0,0 +1,13 @@
import React from 'react';
import { cleanup, render } from '@testing-library/react';
import Index from '../pages/index';
afterEach(cleanup);
describe('Index', () => {
it('Says hello', () => {
const { queryByText } = render(<Index />);
expect(queryByText('Hello!')).toBeTruthy();
});
});
@@ -0,0 +1,3 @@
import React from 'react';
export const Header = () => <h1>Header</h1>;
@@ -0,0 +1,37 @@
import React from 'react';
import App from 'next/app';
import Head from 'next/head';
import styled from 'styled-components';
import { Header } from '../components/Header';
const StyledApp = styled.div`
> * {
padding-left: 16px;
padding-right: 16px;
}
`;
const Main = styled.div`
margin: 2em auto;
height: 85vh;
`;
class CustomApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<StyledApp>
<Head>
<link rel="stylesheet" href="/static/fonts.css" />
</Head>
<Header />
<Main>
<Component {...pageProps} />
</Main>
</StyledApp>
);
}
}
export default CustomApp;
@@ -0,0 +1,41 @@
/**
* This file extends the <Document /> and injects the server side rendered styles into the <head>
* By server-side rendering CSS we avoid visual changes in the layout while loading the JS.
*
* Taken from this example:
* https://github.com/zeit/next.js/tree/master/examples/with-styled-components
*/
import React from 'react';
import Document, { DocumentContext, DocumentInitialProps } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
static async getInitialProps(
ctx: DocumentContext,
): Promise<DocumentInitialProps> {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App: any) => props =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
}
@@ -0,0 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next';
export default function handle(_: NextApiRequest, res: NextApiResponse) {
res.status(200).send('ok');
}
@@ -0,0 +1,5 @@
import React from 'react';
const Index = () => <h1>Hello!</h1>;
export default Index;
@@ -0,0 +1,20 @@
{
"extends": "@spotify/tsconfig",
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": ["node_modules", "output/node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
+18 -2
View File
@@ -14,5 +14,21 @@
* limitations under the License.
*/
export { router } from './plugin';
export const createScaffolder = () => {};
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);
});
return router;
};
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DiskRepository } from './disk';
import { DiskStorage } from './disk';
import * as path from 'path';
describe('Template Repository', () => {
describe('Disk Storage', () => {
it('should load a simple template from a simple directory', async () => {
const testTemplateDir = path.resolve(
__dirname,
@@ -23,7 +23,7 @@ describe('Template Repository', () => {
);
const templateInfo = require(`${testTemplateDir}/mock-template/template-info.json`);
const repository = new DiskRepository(testTemplateDir);
const repository = new DiskStorage(testTemplateDir);
await repository.reindex();
@@ -42,7 +42,7 @@ describe('Template Repository', () => {
'../../../test/mock-multiple-templates-dir',
);
const repository = new DiskRepository(testTemplateDir);
const repository = new DiskStorage(testTemplateDir);
await repository.reindex();
@@ -57,7 +57,7 @@ describe('Template Repository', () => {
'/some-folder-that-deffo-does-not-exist',
);
const repository = new DiskRepository(testTemplateDir);
const repository = new DiskStorage(testTemplateDir);
await repository.reindex();
@@ -72,7 +72,7 @@ describe('Template Repository', () => {
'../../../test/mock-failing-template-dir',
);
const repository = new DiskRepository(testTemplateDir);
const repository = new DiskStorage(testTemplateDir);
await repository.reindex();
@@ -16,19 +16,21 @@
import glob from 'glob';
import fs from 'fs-extra';
import { logger } from 'lib/logger';
import { Template, RepositoryBase as Base } from '.';
import { logger } from '../logger';
import { Template, StorageBase as Base } from '.';
interface DiskIndexEntry {
contents: Template;
location: string;
}
export class DiskRepository implements Base {
export class DiskStorage implements Base {
private repository: Template[] = [];
private localIndex: DiskIndexEntry[] = [];
constructor(private repoDir = `${__dirname}/templates`) {}
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();
@@ -97,5 +99,3 @@ export class DiskRepository implements Base {
});
}
}
export const Repository = new DiskRepository();
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RepositoryBase, Repository } from '.';
import { StorageBase, createStorage } from '.';
describe('Repository Interface Test', () => {
const mockRepo = new (class MockRepository implements RepositoryBase {
describe('Storage Interface Test', () => {
const mockStore = new (class MockStorage implements StorageBase {
list = jest.fn();
prepare = jest.fn();
reindex = jest.fn();
@@ -28,29 +28,28 @@ describe('Repository Interface Test', () => {
};
})();
afterEach(() => mockRepo.reset());
afterEach(() => mockStore.reset());
it('should call list of the set repo when calling list', async () => {
Repository.setRepository(mockRepo);
const store = createStorage({ store: mockStore });
await store.list();
await Repository.list();
expect(mockRepo.list).toHaveBeenCalled();
expect(mockStore.list).toHaveBeenCalled();
});
it('should reindex on the repo when calling reindex', async () => {
Repository.setRepository(mockRepo);
const store = createStorage({ store: mockStore });
await Repository.reindex();
await store.reindex();
expect(mockRepo.reindex).toHaveBeenCalled();
expect(mockStore.reindex).toHaveBeenCalled();
});
it('should call prepare with the correct id when calling prepare', async () => {
Repository.setRepository(mockRepo);
const store = createStorage({ store: mockStore });
await Repository.prepare('testid');
await store.prepare('testid');
expect(mockRepo.prepare).toHaveBeenCalledWith('testid');
expect(mockStore.prepare).toHaveBeenCalledWith('testid');
});
});
@@ -1,4 +1,4 @@
import { Repository as DiskRepository } from './disk';
import { DiskStorage } from './disk';
/*
* Copyright 2020 Spotify AB
@@ -22,7 +22,7 @@ export interface Template {
ownerId: string;
}
export abstract class RepositoryBase {
export abstract class StorageBase {
// lists all templates available
abstract async list(): Promise<Template[]>;
// can be used to build an index of the available templates;
@@ -31,20 +31,24 @@ export abstract class RepositoryBase {
abstract async prepare(id: string): Promise<string>;
}
class RepositoryImplementation implements RepositoryBase {
repo?: RepositoryBase;
constructor() {
this.repo = DiskRepository;
}
public setRepository(repo: RepositoryBase) {
this.repo = repo;
}
list = () => this.repo!.list();
prepare = (id: string) => this.repo!.prepare(id);
reindex = () => this.repo!.reindex();
export interface StorageConfig {
store?: StorageBase;
}
export const Repository = new RepositoryImplementation();
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);
};
@@ -17,7 +17,7 @@ import { TemplaterBase, TemplaterRunOptions } from '.';
*/
import fs from 'fs-extra';
class CookieCutter implements TemplaterBase {
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 = {
@@ -30,5 +30,3 @@ class CookieCutter implements TemplaterBase {
// run cookie cutter with new json
}
}
export const CookieCutterTemplater = new CookieCutter();
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { CookieCutterTemplater } from './cookiecutter';
import { CookieCutter } from './cookiecutter';
export interface RequiredTemplateValues {
componentId: string;
@@ -29,14 +29,14 @@ export abstract class TemplaterBase {
abstract async run(opts: TemplaterRunOptions): Promise<string>;
}
class TemplaterImplementation implements TemplaterBase {
export interface TemplaterConfig {
templater?: TemplaterBase;
}
class Templater implements TemplaterBase {
templater?: TemplaterBase;
constructor() {
this.templater = CookieCutterTemplater;
}
public setTemplater(templater: TemplaterBase) {
constructor({ templater }: TemplaterConfig) {
this.templater = templater;
}
@@ -45,4 +45,8 @@ class TemplaterImplementation implements TemplaterBase {
}
}
export const Templater = new TemplaterImplementation();
export const createTemplater = (
config: TemplaterConfig = { templater: new CookieCutter() },
): TemplaterBase => {
return new Templater(config);
};
@@ -1,43 +0,0 @@
/*
* 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 { router } from './plugin';
import { Repository, Template } from 'lib/repo';
import supertest from 'supertest';
import express from 'express';
describe('scaffolder backend', () => {
const app = express().use(router);
const mockTemplate: Template = {
id: 'test-mock-template',
name: 'mock',
description: 'test for tests',
ownerId: 'lol',
};
it('should return a list of templates that are in the directory which the plugin is initialised in', async () => {
jest.spyOn(Repository, 'list').mockResolvedValue([mockTemplate]);
const { body } = await supertest(app)
.get('/v1/templates')
.send();
expect(body.length).toBe(1);
expect(body[0]).toStrictEqual(mockTemplate);
});
});
-82
View File
@@ -1,82 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
export const router = express.Router();
router.get('/v1/templates', async (_, res) => {
const templates = await Repository.list();
res.status(200).json(templates);
});
router.get('/v1/template/:templateId', async (_, res) => {
res
.status(200)
.send([
{ id: 'component1' },
{ id: 'component2' },
{ id: 'component3' },
{ id: 'component4' },
]);
});
router.post('/v1/jobs', async (_, res) => {
res
.status(200)
.send([
{ id: 'component1' },
{ id: 'component2' },
{ id: 'component3' },
{ id: 'component4' },
]);
});
router.get('/v1/jobs', async (_, res) => {
res
.status(200)
.send([
{ id: 'component1' },
{ id: 'component2' },
{ id: 'component3' },
{ id: 'component4' },
]);
});
router.get('/v1/job/:jobId', async (_, res) => {
res
.status(200)
.send([
{ id: 'component1' },
{ id: 'component2' },
{ id: 'component3' },
{ id: 'component4' },
]);
});
@@ -0,0 +1,6 @@
{
"id": "mock-template-2",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
@@ -0,0 +1,7 @@
{
"id": "mock-template",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam",
<heres some invalid hson>
}
@@ -0,0 +1,6 @@
{
"id": "mock-template-2",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
@@ -0,0 +1,6 @@
{
"id": "mock-template",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
@@ -0,0 +1,6 @@
{
"id": "mock-template",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
+7 -10
View File
@@ -1,14 +1,11 @@
{
"include": ["src"],
"extends": "../../packages/backend/tsconfig.json",
"include": [
"./src"
],
"compilerOptions": {
"baseUrl": "src",
"outDir": "dist",
"incremental": true,
"sourceMap": true,
"declaration": true,
"strict": true,
"target": "es5",
"module": "commonjs",
"esModuleInterop": true
"baseUrl": "./src",
"outDir": "./dist",
"skipLibCheck": true
}
}