diff --git a/packages/cli/src/lib/new/factories/index.ts b/packages/cli/src/lib/new/factories/index.ts index 9fc69e0ca4..232e762b91 100644 --- a/packages/cli/src/lib/new/factories/index.ts +++ b/packages/cli/src/lib/new/factories/index.ts @@ -19,4 +19,5 @@ export { backendPlugin } from './backendPlugin'; export { webLibraryPackage } from './webLibraryPackage'; export { pluginCommon } from './pluginCommon'; export { pluginNode } from './pluginNode'; +export { pluginWeb } from './pluginWeb'; export { scaffolderModule } from './scaffolderModule'; diff --git a/packages/cli/src/lib/new/factories/pluginWeb.test.ts b/packages/cli/src/lib/new/factories/pluginWeb.test.ts new file mode 100644 index 0000000000..bcd62f3bcd --- /dev/null +++ b/packages/cli/src/lib/new/factories/pluginWeb.test.ts @@ -0,0 +1,114 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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'; +import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; +import { pluginWeb } from './pluginWeb'; + +describe('pluginWeb factory', () => { + beforeEach(() => { + mockPaths({ + targetRoot: '/root', + }); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a react plugin package', async () => { + mockFs({ + '/root': { + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(pluginWeb, { + id: 'test', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await pluginWeb.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + '', + 'Creating web plugin library backstage-plugin-test-react', + 'Checking Prerequisites:', + `availability plugins${sep}test-react`, + 'creating temp dir', + 'Executing Template:', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'templating index.ts.hbs', + 'copying setupTests.ts', + 'copying index.ts', + 'copying ExampleComponent.test.tsx', + 'copying ExampleComponent.tsx', + 'copying index.ts', + 'copying index.ts', + 'copying index.ts', + 'copying useExample.ts', + 'Installing:', + `moving plugins${sep}test-react`, + ]); + + await expect( + fs.readJson('/root/plugins/test-react/package.json'), + ).resolves.toEqual( + expect.objectContaining({ + name: 'backstage-plugin-test-react', + description: 'Web library for the test plugin', + private: true, + version: '1.0.0', + }), + ); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: resolvePath('/root/plugins/test-react'), + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: resolvePath('/root/plugins/test-react'), + optional: true, + }); + }); +}); diff --git a/packages/cli/src/lib/new/factories/pluginWeb.ts b/packages/cli/src/lib/new/factories/pluginWeb.ts new file mode 100644 index 0000000000..8189654eb2 --- /dev/null +++ b/packages/cli/src/lib/new/factories/pluginWeb.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 chalk from 'chalk'; +import { paths } from '../../paths'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; +import { createFactory, CreateContext } from '../types'; +import { Task } from '../../tasks'; +import { ownerPrompt, pluginIdPrompt } from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; + +type Options = { + id: string; + owner?: string; + codeOwnersPath?: string; +}; + +export const pluginWeb = createFactory({ + name: 'plugin-react', + description: 'A new web library plugin package', + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [pluginIdPrompt(), ownerPrompt()], + async create(options: Options, ctx: CreateContext) { + const { id } = options; + const suffix = `${id}-react`; + const name = ctx.scope + ? `@${ctx.scope}/plugin-${suffix}` + : `backstage-plugin-${suffix}`; + + Task.log(); + Task.log(`Creating web plugin library ${chalk.cyan(name)}`); + + const targetDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', suffix) + : paths.resolveTargetRoot(`backstage-plugin-${suffix}`); + + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'default-react-plugin-package', + values: { + id, + name, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + pluginVersion: ctx.defaultVersion, + }, + }); + + if (options.owner) { + await addCodeownersEntry(`/plugins/${suffix}`, options.owner); + } + + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: targetDir, + optional: true, + }); + }, +}); diff --git a/packages/cli/templates/default-react-plugin-package/.eslintrc.js b/packages/cli/templates/default-react-plugin-package/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli/templates/default-react-plugin-package/README.md.hbs b/packages/cli/templates/default-react-plugin-package/README.md.hbs new file mode 100644 index 0000000000..ab1c36da6b --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/README.md.hbs @@ -0,0 +1,5 @@ +# {{name}} + +Welcome to the web library package for the {{id}} plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/default-react-plugin-package/package.json.hbs b/packages/cli/templates/default-react-plugin-package/package.json.hbs new file mode 100644 index 0000000000..e0ee408eda --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/package.json.hbs @@ -0,0 +1,47 @@ +{ + "name": "{{name}}", + "description": "Web library for the {{id}} plugin", + "version": "{{pluginVersion}}", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", +{{#if privatePackage}} + "private": {{privatePackage}}, +{{/if}} + "publishConfig": { +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", +{{/if}} + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-plugin-api": "{{versionQuery '@backstage/core-plugin-api'}}", + "@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}" + }, + "peerDependencies": { + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0'}}" + }, + "devDependencies": { + "@backstage/cli": "{{versionQuery '@backstage/cli'}}", + "@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}", + "@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '5.10.1'}}", + "@testing-library/react": "{{versionQuery '@testing-library/react' '12.1.3'}}" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/ExampleComponent.test.tsx b/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 0000000000..c770cca8cb --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { ExampleComponent } from './ExampleComponent'; + +describe('ExampleComponent', () => { + it('should render', async () => { + await renderInTestApp() + + expect(screen.getByText('Hello World')).toBeInTheDocument(); + }); + + it('should display a custom message', async () => { + await renderInTestApp() + + expect(screen.getByText('Hello Example')).toBeInTheDocument(); + }) +}) diff --git a/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/ExampleComponent.tsx b/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 0000000000..a1ab0937db --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/ExampleComponent.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Typography } from '@material-ui/core'; + +/** + * Props for {@link ExampleComponent}. + * + * @public + */ +export interface ExampleComponentProps { + message?: string; +} + +/** + * Displays an example. + * + * @remarks + * + * Longer descriptions should be put after the `@remarks` tag. That way the initial summary + * will show up in the API docs overview section, while the longer description will only be + * displayed on the page for the specific API. + * + * @public + */ +export function ExampleComponent(props: ExampleComponentProps) { + // By destructuring props here rather than in the signature the API docs will look nicer + const { message = 'Hello World' } = props; + + return {message}; +} diff --git a/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/index.ts b/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/index.ts new file mode 100644 index 0000000000..9bb2f2c378 --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/src/components/ExampleComponent/index.ts @@ -0,0 +1,2 @@ +export { ExampleComponent } from './ExampleComponent'; +export type { ExampleComponentProps } from './ExampleComponent'; diff --git a/packages/cli/templates/default-react-plugin-package/src/components/index.ts b/packages/cli/templates/default-react-plugin-package/src/components/index.ts new file mode 100644 index 0000000000..0380b8ce5f --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/src/components/index.ts @@ -0,0 +1,5 @@ +/***/ +// The index file in ./components/ is typically responsible for selecting +// which components are public API and should be exported from the package. + +export * from './ExampleComponent'; diff --git a/packages/cli/templates/default-react-plugin-package/src/hooks/index.ts b/packages/cli/templates/default-react-plugin-package/src/hooks/index.ts new file mode 100644 index 0000000000..cec94f4b04 --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/src/hooks/index.ts @@ -0,0 +1,5 @@ +/***/ +// The index file in ./hooks/ is typically responsible for selecting +// which hooks are public API and should be exported from the package. + +export * from './useExample'; diff --git a/packages/cli/templates/default-react-plugin-package/src/hooks/useExample/index.ts b/packages/cli/templates/default-react-plugin-package/src/hooks/useExample/index.ts new file mode 100644 index 0000000000..f1fd276930 --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/src/hooks/useExample/index.ts @@ -0,0 +1 @@ +export { useExample } from './useExample'; diff --git a/packages/cli/templates/default-react-plugin-package/src/hooks/useExample/useExample.ts b/packages/cli/templates/default-react-plugin-package/src/hooks/useExample/useExample.ts new file mode 100644 index 0000000000..911d255238 --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/src/hooks/useExample/useExample.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; +import { useApi, alertApiRef } from '@backstage/core-plugin-api'; + +/** + * Shows an example alert. + * + * @public + */ +export function useExample() { + const alertApi = useApi(alertApiRef); + + useEffect(() => { + alertApi.post({ message: 'Hello World!' }); + }, [alertApi]) +} diff --git a/packages/cli/templates/default-react-plugin-package/src/index.ts.hbs b/packages/cli/templates/default-react-plugin-package/src/index.ts.hbs new file mode 100644 index 0000000000..a7ebb26f00 --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/src/index.ts.hbs @@ -0,0 +1,12 @@ +/***/ +/** + * Web library for the {{id}} plugin. + * + * @packageDocumentation + */ + +// In this package you might for example export components or hooks +// that are useful to other plugins or modules. + +export * from './components' +export * from './hooks' diff --git a/packages/cli/templates/default-react-plugin-package/src/setupTests.ts b/packages/cli/templates/default-react-plugin-package/src/setupTests.ts new file mode 100644 index 0000000000..7b0828bfa8 --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/packages/cli/templates/default-react-plugin-package/tsconfig.json b/packages/cli/templates/default-react-plugin-package/tsconfig.json new file mode 100644 index 0000000000..ce3409d31f --- /dev/null +++ b/packages/cli/templates/default-react-plugin-package/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": [ + "src", + ], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": "." + } +}