Better handling of files in the template directory, and tests.
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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}}
|
||||
+10
@@ -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();
|
||||
});
|
||||
});
|
||||
+16
@@ -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;
|
||||
+1
@@ -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
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user