From 59764c3a7f2995b956409baebf861aaa8b570fb3 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 21 Feb 2020 14:37:31 +0100 Subject: [PATCH] Better handling of files in the template directory, and tests. --- frontend/packages/cli/package.json | 12 ++-- .../cli/src/commands/createPlugin.test.ts | 36 +++++++++- .../packages/cli/src/commands/createPlugin.ts | 70 +++++++++++++------ frontend/packages/cli/src/index.ts | 4 +- .../templates/default-plugin/README.md.hbs | 6 ++ .../templates/default-plugin/ScaffoldPage.hbs | 32 --------- .../default-plugin/ScaffoldPage.test.hbs | 14 ---- .../default-plugin/ScaffoldPlugin.hbs | 14 ---- .../cli/templates/default-plugin/index.hbs | 1 - .../cli/templates/default-plugin/index.js | 1 - .../templates/default-plugin/jest.config.js | 4 ++ .../templates/default-plugin/jest.setup.ts | 1 + .../templates/default-plugin/package.json.hbs | 2 +- .../templates/default-plugin/plugin-info.hbs | 10 --- .../ExampleComponent.test.tsx | 10 +++ .../ExampleComponent/ExampleComponent.tsx | 16 +++++ .../src/components/ExampleComponent/index.ts | 1 + .../cli/templates/default-plugin/src/index.ts | 2 + .../default-plugin/src/plugin.test.ts.hbs | 7 ++ .../default-plugin/src/plugin.ts.hbs | 5 ++ frontend/yarn.lock | 27 ++++++- 21 files changed, 173 insertions(+), 102 deletions(-) create mode 100644 frontend/packages/cli/templates/default-plugin/README.md.hbs delete mode 100644 frontend/packages/cli/templates/default-plugin/ScaffoldPage.hbs delete mode 100644 frontend/packages/cli/templates/default-plugin/ScaffoldPage.test.hbs delete mode 100644 frontend/packages/cli/templates/default-plugin/ScaffoldPlugin.hbs delete mode 100644 frontend/packages/cli/templates/default-plugin/index.hbs delete mode 100644 frontend/packages/cli/templates/default-plugin/index.js create mode 100644 frontend/packages/cli/templates/default-plugin/jest.config.js create mode 100644 frontend/packages/cli/templates/default-plugin/jest.setup.ts delete mode 100644 frontend/packages/cli/templates/default-plugin/plugin-info.hbs create mode 100644 frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx create mode 100644 frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx create mode 100644 frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts create mode 100644 frontend/packages/cli/templates/default-plugin/src/index.ts create mode 100644 frontend/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs create mode 100644 frontend/packages/cli/templates/default-plugin/src/plugin.ts.hbs diff --git a/frontend/packages/cli/package.json b/frontend/packages/cli/package.json index 2318e5ffc3..2d9dad085e 100644 --- a/frontend/packages/cli/package.json +++ b/frontend/packages/cli/package.json @@ -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", diff --git a/frontend/packages/cli/src/commands/createPlugin.test.ts b/frontend/packages/cli/src/commands/createPlugin.test.ts index 46d7bc982f..b5c0958e9c 100644 --- a/frontend/packages/cli/src/commands/createPlugin.test.ts +++ b/frontend/packages/cli/src/commands/createPlugin.test.ts @@ -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 () => {}); + }); }); diff --git a/frontend/packages/cli/src/commands/createPlugin.ts b/frontend/packages/cli/src/commands/createPlugin.ts index 03e4366b0b..fc044dfc9a 100644 --- a/frontend/packages/cli/src/commands/createPlugin.ts +++ b/frontend/packages/cli/src/commands/createPlugin.ts @@ -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 => { const currentDir = process.argv[1]; - const questions = [ + const questions: Question[] = [ { type: 'input', name: 'id', @@ -51,13 +82,11 @@ const createPlugin = async (): Promise => { 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 => { '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; }; diff --git a/frontend/packages/cli/src/index.ts b/frontend/packages/cli/src/index.ts index ba9462dc3d..4186de36c5 100644 --- a/frontend/packages/cli/src/index.ts +++ b/frontend/packages/cli/src/index.ts @@ -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']); diff --git a/frontend/packages/cli/templates/default-plugin/README.md.hbs b/frontend/packages/cli/templates/default-plugin/README.md.hbs new file mode 100644 index 0000000000..d027d97e0e --- /dev/null +++ b/frontend/packages/cli/templates/default-plugin/README.md.hbs @@ -0,0 +1,6 @@ +# Title +Welcome to your {{id}} plugin! + +## Sub-section 1 + +## Sub-section 2 diff --git a/frontend/packages/cli/templates/default-plugin/ScaffoldPage.hbs b/frontend/packages/cli/templates/default-plugin/ScaffoldPage.hbs deleted file mode 100644 index 3a02ea4403..0000000000 --- a/frontend/packages/cli/templates/default-plugin/ScaffoldPage.hbs +++ /dev/null @@ -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 ( - -
- - -
- - - - - - - Hello world, this is your new plugin! - - -
- ); -}; - -export default {{id}}Page; diff --git a/frontend/packages/cli/templates/default-plugin/ScaffoldPage.test.hbs b/frontend/packages/cli/templates/default-plugin/ScaffoldPage.test.hbs deleted file mode 100644 index b1f12525c9..0000000000 --- a/frontend/packages/cli/templates/default-plugin/ScaffoldPage.test.hbs +++ /dev/null @@ -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(); - }); -}); diff --git a/frontend/packages/cli/templates/default-plugin/ScaffoldPlugin.hbs b/frontend/packages/cli/templates/default-plugin/ScaffoldPlugin.hbs deleted file mode 100644 index 770e379565..0000000000 --- a/frontend/packages/cli/templates/default-plugin/ScaffoldPlugin.hbs +++ /dev/null @@ -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); - } -}); diff --git a/frontend/packages/cli/templates/default-plugin/index.hbs b/frontend/packages/cli/templates/default-plugin/index.hbs deleted file mode 100644 index 07b190dcb7..0000000000 --- a/frontend/packages/cli/templates/default-plugin/index.hbs +++ /dev/null @@ -1 +0,0 @@ -export { default as {{id}} } from 'plugins/{{folder}}/{{id}}Plugin'; diff --git a/frontend/packages/cli/templates/default-plugin/index.js b/frontend/packages/cli/templates/default-plugin/index.js deleted file mode 100644 index 3243cf662b..0000000000 --- a/frontend/packages/cli/templates/default-plugin/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as ScaffoldPlugin } from 'plugins/scaffold/ScaffoldPlugin'; diff --git a/frontend/packages/cli/templates/default-plugin/jest.config.js b/frontend/packages/cli/templates/default-plugin/jest.config.js new file mode 100644 index 0000000000..6b28dacb3d --- /dev/null +++ b/frontend/packages/cli/templates/default-plugin/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + ...require('@spotify/web-scripts/config/jest.config.js'), + setupFilesAfterEnv: ['../jest.setup.ts'], +}; diff --git a/frontend/packages/cli/templates/default-plugin/jest.setup.ts b/frontend/packages/cli/templates/default-plugin/jest.setup.ts new file mode 100644 index 0000000000..666127af39 --- /dev/null +++ b/frontend/packages/cli/templates/default-plugin/jest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/extend-expect'; diff --git a/frontend/packages/cli/templates/default-plugin/package.json.hbs b/frontend/packages/cli/templates/default-plugin/package.json.hbs index d091fbc1ec..aad674775d 100644 --- a/frontend/packages/cli/templates/default-plugin/package.json.hbs +++ b/frontend/packages/cli/templates/default-plugin/package.json.hbs @@ -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", diff --git a/frontend/packages/cli/templates/default-plugin/plugin-info.hbs b/frontend/packages/cli/templates/default-plugin/plugin-info.hbs deleted file mode 100644 index 125d7a66f3..0000000000 --- a/frontend/packages/cli/templates/default-plugin/plugin-info.hbs +++ /dev/null @@ -1,10 +0,0 @@ ---- -id: {{id}} -title: {{title}} -description: {{desc}} -owner: {{owner}} -visibility: public -facts: - support_channel: {{support_channel}} - authors: - - slack: {{author}} diff --git a/frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx b/frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 0000000000..c231eea700 --- /dev/null +++ b/frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -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(); + expect(rendered.getByText('Hello!')).toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx b/frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 0000000000..bd7c9235ae --- /dev/null +++ b/frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; +import Button from '@material-ui/core/Button'; + +const ExampleComponent: FC<{}> = () => { + return ( + + ); +}; + +export default ExampleComponent; diff --git a/frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts b/frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts new file mode 100644 index 0000000000..520a3bf553 --- /dev/null +++ b/frontend/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts @@ -0,0 +1 @@ +export { default } from './ExampleComponent'; diff --git a/frontend/packages/cli/templates/default-plugin/src/index.ts b/frontend/packages/cli/templates/default-plugin/src/index.ts new file mode 100644 index 0000000000..a2e45f3a79 --- /dev/null +++ b/frontend/packages/cli/templates/default-plugin/src/index.ts @@ -0,0 +1,2 @@ +export { default } from './plugin'; +export { default as ExampleComponent } from './components/ExampleComponent'; diff --git a/frontend/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs b/frontend/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs new file mode 100644 index 0000000000..0139d97727 --- /dev/null +++ b/frontend/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs @@ -0,0 +1,7 @@ +import plugin from './plugin'; + +describe('{{ id }}', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/frontend/packages/cli/templates/default-plugin/src/plugin.ts.hbs b/frontend/packages/cli/templates/default-plugin/src/plugin.ts.hbs new file mode 100644 index 0000000000..32a80e1b5f --- /dev/null +++ b/frontend/packages/cli/templates/default-plugin/src/plugin.ts.hbs @@ -0,0 +1,5 @@ +import { createPlugin } from '@backstage/core'; + +export default createPlugin({ + id: '{{ id }}', +}); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 9d0342195a..c763a05dd5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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==