Merge pull request #109 from spotify/mob/more-templates

[cli]: Better handling of files in the template directory
This commit is contained in:
Patrik Oldsberg
2020-02-24 11:09:44 +01:00
committed by GitHub
21 changed files with 173 additions and 102 deletions
+8 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@spotify-backstage/cli",
"version": "1.1.0",
"version": "1.2.0",
"main": "src/index.ts",
"main:src": "src/index.ts",
"license": "Apache-2.0",
@@ -13,8 +13,10 @@
},
"devDependencies": {
"@spotify/web-scripts": "^6.0.0",
"@types/node": "^13.7.2",
"@types/fs-extra": "^8.1.0",
"@types/inquirer": "^6.5.0",
"@types/node": "^13.7.2",
"@types/recursive-readdir": "^2.2.0",
"nodemon": "^2.0.2",
"ts-node": "^8.6.2"
},
@@ -24,9 +26,11 @@
"dependencies": {
"commander": "^4.1.1",
"dashify": "^2.0.0",
"fs-extra": "^8.1.0",
"handlebars": "^4.7.3",
"replace-in-file": "^5.0.2",
"inquirer": "^7.0.4"
"inquirer": "^7.0.4",
"recursive-readdir": "^2.2.2",
"replace-in-file": "^5.0.2"
},
"files": [
"templates",
@@ -1,7 +1,11 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
import { createPluginFolder, createFileFromTemplate } from './createPlugin';
import {
createFileFromTemplate,
createFromTemplateDir,
createPluginFolder,
} from './createPlugin';
describe('createPlugin', () => {
describe('createPluginFolder', () => {
@@ -49,4 +53,34 @@ describe('createPlugin', () => {
}
});
});
describe('createFromTemplateDir', () => {
it('should create sub-directories and files', async () => {
const templateRootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
const templateSubDir = fs.mkdtempSync(path.join(templateRootDir, 'sub-'));
fs.writeFileSync(path.join(templateSubDir, 'test.txt'), 'testing');
const destinationRootDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'test-'),
);
const subDir = path.join(
destinationRootDir,
path.basename(templateSubDir),
);
const testFile = path.join(
destinationRootDir,
path.basename(templateSubDir),
'test.txt',
);
try {
await createFromTemplateDir(templateRootDir, destinationRootDir, {});
expect(fs.existsSync(subDir)).toBe(true);
expect(fs.existsSync(testFile)).toBe(true);
} finally {
fs.rmdirSync(templateRootDir, { recursive: true });
fs.rmdirSync(destinationRootDir, { recursive: true });
}
});
xit('should handle errors on reading template directory', async () => {});
});
});
@@ -1,7 +1,8 @@
import fs from 'fs';
import fs from 'fs-extra';
import path from 'path';
import handlebars from 'handlebars';
import inquirer from 'inquirer';
import inquirer, { Answers, Question } from 'inquirer';
import recursive from 'recursive-readdir';
export const createPluginFolder = (rootDir: string, id: string): string => {
const destination = path.join(rootDir, 'packages', 'plugins', id);
@@ -23,26 +24,56 @@ export const createPluginFolder = (rootDir: string, id: string): string => {
};
export const createFileFromTemplate = (
sourcePath: string,
destinationPath: string,
answers: { [key: string]: string },
source: string,
destination: string,
answers: Answers,
) => {
const template = fs.readFileSync(sourcePath);
const template = fs.readFileSync(source);
const compiled = handlebars.compile(template.toString());
const contents = compiled({
name: path.basename(destinationPath),
name: path.basename(destination),
...answers,
});
try {
fs.writeFileSync(destinationPath, contents);
fs.writeFileSync(destination, contents);
} catch (e) {
throw new Error(`Failed to create file: ${destinationPath}: ${e.message}`);
throw new Error(`Failed to create file: ${destination}: ${e.message}`);
}
};
export const createFromTemplateDir = async (
templateFolder: string,
destinationFolder: string,
answers: Answers,
) => {
let files = [];
try {
files = await recursive(templateFolder);
} catch (e) {
throw new Error(`Failed to read files in template directory: ${e.message}`);
}
files.forEach(file => {
fs.ensureDirSync(
file
.replace(templateFolder, destinationFolder)
.replace(path.basename(file), ''),
);
if (file.endsWith('hbs')) {
createFileFromTemplate(
file,
file.replace(templateFolder, destinationFolder).replace(/\.hbs$/, ''),
answers,
);
} else {
fs.copyFileSync(file, file.replace(templateFolder, destinationFolder));
}
});
};
const createPlugin = async (): Promise<any> => {
const currentDir = process.argv[1];
const questions = [
const questions: Question[] = [
{
type: 'input',
name: 'id',
@@ -51,13 +82,11 @@ const createPlugin = async (): Promise<any> => {
value ? true : 'Please enter an ID for the plugin',
},
];
const answers: { [key: string]: string } = await inquirer.prompt(questions);
const answers: Answers = await inquirer.prompt(questions);
const destinationFolder = createPluginFolder(
path.join(currentDir, '..', '..', '..'),
answers.id,
);
const templateFolder = path.join(
currentDir,
'..',
@@ -67,15 +96,14 @@ const createPlugin = async (): Promise<any> => {
'templates',
'default-plugin',
);
const files = [{ input: 'package.json.hbs', output: 'package.json' }];
files.forEach(file => {
createFileFromTemplate(
path.join(templateFolder, file.input),
path.join(destinationFolder, file.output),
answers,
);
});
await createFromTemplateDir(templateFolder, destinationFolder, answers);
console.log(
`✨ You have created a Backstage Plugin packages/plugins/${answers.id}`,
);
console.log('');
console.log('Run yarn start in the plugin directory to start it');
return destinationFolder;
};
+2 -2
View File
@@ -18,5 +18,5 @@ const main = (argv: string[]) => {
program.parse(argv);
};
// main(process.argv);
main([process.argv[0], process.argv[1], 'create-plugin']);
main(process.argv);
// main([process.argv[0], process.argv[1], 'create-plugin']);
@@ -0,0 +1,6 @@
# Title
Welcome to your {{id}} plugin!
## Sub-section 1
## Sub-section 2
@@ -1,32 +0,0 @@
import React{{#if ts}}, { FC }{{/if}} from 'react';
import { Typography } from '@material-ui/core';
import { theme } from 'core/app/PageThemeProvider';
import InfoCard from 'shared/components/InfoCard';
import { Content, ContentHeader, Header, HeaderLabel, Navigation, NavItem, Page } from 'shared/components/layout';
/**
* This component demonstrates how to render a page with its own navigation.
* You typically create layouts that can be shared between multiple pages.
* Layouts can be found in {@link src/shared/components/layout/}
*/
const {{id}}Page{{#if ts}}: FC<>{{/if}} = () => {
return (
<Page theme={theme.tool}>
<Header type="Tool" title="{{id}}">
<HeaderLabel label="Owner" value="{{owner}}" url="/org/{{owner}}" />
<HeaderLabel label="Lifecycle" value="Experimental" />
</Header>
<Navigation>
<NavItem title="Overview" href="{{route}}" />
</Navigation>
<Content>
<ContentHeader title="{{id}} Title" />
<InfoCard>
<Typography>Hello world, this is your new plugin!</Typography>
</InfoCard>
</Content>
</Page>
);
};
export default {{id}}Page;
@@ -1,14 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInThemedTestApp } from 'testUtils';
import {{id}}Page from './{{id}}Page';
const minProps = {};
describe('<{{id}}Page />', () => {
it('renders without exploding', () => {
const { getByText } = render(wrapInThemedTestApp(<{{id}}Page {...minProps} />));
expect(getByText(/Hello world.*/)).toBeInTheDocument();
});
});
@@ -1,14 +0,0 @@
import React from 'react';
import { createPlugin } from 'shared/pluginApi';
const {{id}}Page = React.lazy(
/* istanbul ignore next */ () => import(/* webpackChunkName: "{{folder}}" */ 'plugins/{{folder}}/{{id}}Page'),
);
export default createPlugin({
manifest: require('./plugin-info.yaml'),
register({ router }) {
router.registerRoute('{{route}}', {{id}}Page);
}
});
@@ -1 +0,0 @@
export { default as {{id}} } from 'plugins/{{folder}}/{{id}}Plugin';
@@ -1 +0,0 @@
export { default as ScaffoldPlugin } from 'plugins/scaffold/ScaffoldPlugin';
@@ -0,0 +1,4 @@
module.exports = {
...require('@spotify/web-scripts/config/jest.config.js'),
setupFilesAfterEnv: ['../jest.setup.ts'],
};
@@ -0,0 +1 @@
import '@testing-library/jest-dom/extend-expect';
@@ -1,6 +1,6 @@
{
"name": "@spotify-backstage/{{id}}",
"version": "{{version}}",
"version": "0.0.1",
"main": "src/index.ts",
"main:src": "src/index.ts",
"license": "Apache-2.0",
@@ -1,10 +0,0 @@
---
id: {{id}}
title: {{title}}
description: {{desc}}
owner: {{owner}}
visibility: public
facts:
support_channel: {{support_channel}}
authors:
- slack: {{author}}
@@ -0,0 +1,10 @@
import React from 'react';
import { render } from '@testing-library/react';
import ExampleComponent from './ExampleComponent';
describe('ExampleComponent', () => {
it('should render', () => {
const rendered = render(<ExampleComponent />);
expect(rendered.getByText('Hello!')).toBeInTheDocument();
});
});
@@ -0,0 +1,16 @@
import React, { FC } from 'react';
import Button from '@material-ui/core/Button';
const ExampleComponent: FC<{}> = () => {
return (
<Button
variant="contained"
color="primary"
onClick={() => window.location.reload()}
>
Hello!
</Button>
);
};
export default ExampleComponent;
@@ -0,0 +1 @@
export { default } from './ExampleComponent';
@@ -0,0 +1,2 @@
export { default } from './plugin';
export { default as ExampleComponent } from './components/ExampleComponent';
@@ -0,0 +1,7 @@
import plugin from './plugin';
describe('{{ id }}', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
});
@@ -0,0 +1,5 @@
import { createPlugin } from '@backstage/core';
export default createPlugin({
id: '{{ id }}',
});
+26 -1
View File
@@ -2558,6 +2558,17 @@
dependencies:
type-detect "4.0.8"
"@spotify-backstage/cli@file:packages/cli":
version "1.1.0"
dependencies:
commander "^4.1.1"
dashify "^2.0.0"
fs-extra "^8.1.0"
handlebars "^4.7.3"
inquirer "^7.0.4"
recursive-readdir "^2.2.2"
replace-in-file "^5.0.2"
"@spotify/eslint-config-base@^6.0.0":
version "6.0.0"
resolved "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-6.0.0.tgz#487da7dbb89380b4eb778f4b902b8bba9b1f389f"
@@ -2846,6 +2857,13 @@
resolved "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/fs-extra@^8.1.0":
version "8.1.0"
resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.0.tgz#1114834b53c3914806cd03b3304b37b3bd221a4d"
integrity sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg==
dependencies:
"@types/node" "*"
"@types/glob@^7.1.1":
version "7.1.1"
resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
@@ -2992,6 +3010,13 @@
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/recursive-readdir@^2.2.0":
version "2.2.0"
resolved "https://registry.npmjs.org/@types/recursive-readdir/-/recursive-readdir-2.2.0.tgz#b39cd5474fd58ea727fe434d5c68b7a20ba9121c"
integrity sha512-HGk753KRu2N4mWduovY4BLjYq4jTOL29gV2OfGdGxHcPSWGFkC5RRIdk+VTs5XmYd7MVAD+JwKrcb5+5Y7FOCg==
dependencies:
"@types/node" "*"
"@types/retry@^0.12.0":
version "0.12.0"
resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
@@ -13887,7 +13912,7 @@ recompose@0.30.0:
react-lifecycles-compat "^3.0.2"
symbol-observable "^1.0.4"
recursive-readdir@2.2.2:
recursive-readdir@2.2.2, recursive-readdir@^2.2.2:
version "2.2.2"
resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==