diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts
new file mode 100644
index 0000000000..71e8ec50e9
--- /dev/null
+++ b/packages/cli/src/commands/create-app/createApp.ts
@@ -0,0 +1,150 @@
+/*
+ * 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 fs from 'fs-extra';
+import path from 'path';
+import { promisify } from 'util';
+import chalk from 'chalk';
+import inquirer, { Answers, Question } from 'inquirer';
+import { exec as execCb } from 'child_process';
+import { resolve as resolvePath } from 'path';
+import { realpathSync } from 'fs';
+import os from 'os';
+import { Task, templatingTask } from '../../helpers/tasks';
+const exec = promisify(execCb);
+
+async function checkExists(rootDir: string, name: string) {
+ await Task.forItem('checking', name, async () => {
+ const destination = path.join(rootDir, 'plugins', name);
+
+ if (await fs.pathExists(destination)) {
+ const existing = chalk.cyan(destination.replace(`${rootDir}/`, ''));
+ throw new Error(
+ `A directory with the same name already exists: ${existing}\nPlease try again with a different app name`,
+ );
+ }
+ });
+}
+
+export async function createTemporaryAppFolder(tempDir: string) {
+ await Task.forItem('creating', 'temporary directory', async () => {
+ try {
+ await fs.mkdir(tempDir);
+ } catch (error) {
+ throw new Error(
+ `Failed to create temporary app directory: ${error.message}`,
+ );
+ }
+ });
+}
+
+async function cleanUp(tempDir: string) {
+ await Task.forItem('remove', 'temporary directory', async () => {
+ await fs.remove(tempDir);
+ });
+}
+
+async function buildApp(pluginFolder: string) {
+ const commands = ['yarn install', 'yarn build'];
+ for (const command of commands) {
+ await Task.forItem('executing', command, async () => {
+ process.chdir(pluginFolder);
+
+ await exec(command).catch(error => {
+ process.stdout.write(error.stderr);
+ process.stdout.write(error.stdout);
+ throw new Error(`Could not execute command ${chalk.cyan(command)}`);
+ });
+ });
+ }
+}
+
+export async function moveApp(
+ tempDir: string,
+ destination: string,
+ id: string,
+) {
+ await Task.forItem('moving', id, async () => {
+ await fs.move(tempDir, destination).catch(error => {
+ throw new Error(
+ `Failed to move plugin from ${tempDir} to ${destination}: ${error.message}`,
+ );
+ });
+ });
+}
+
+export default async () => {
+ const questions: Question[] = [
+ {
+ type: 'input',
+ name: 'name',
+ message: chalk.blue('Enter a name for the app [required]'),
+ validate: (value: any) => {
+ if (!value) {
+ return chalk.red('Please enter a name for the app');
+ } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
+ return chalk.red(
+ 'Plugin name must be kebab-cased and contain only letters, digits, and dashes.',
+ );
+ }
+ return true;
+ },
+ },
+ ];
+ const answers: Answers = await inquirer.prompt(questions);
+
+ const rootDir = realpathSync(process.cwd());
+ const cliPackage = resolvePath(__dirname, '../../..');
+ const templateDir = resolvePath(cliPackage, 'templates', 'default-app');
+ const tempDir = path.join(os.tmpdir(), answers.name);
+ const appDir = path.join(rootDir, answers.name);
+ const version = require(resolvePath(cliPackage, 'package.json')).version;
+
+ Task.log();
+ Task.log('Creating the app...');
+
+ try {
+ Task.section('Checking if the directory is available');
+ await checkExists(rootDir, answers.name);
+
+ Task.section('Creating a temporary app directory');
+ await createTemporaryAppFolder(tempDir);
+
+ Task.section('Preparing files');
+ await templatingTask(templateDir, tempDir, { ...answers, version });
+
+ Task.section('Moving to final location');
+ await moveApp(tempDir, appDir, answers.name);
+
+ Task.section('Building the app');
+ await buildApp(appDir);
+
+ Task.log();
+ Task.log(
+ chalk.green(`🥇 Successfully created ${chalk.cyan(answers.name)}`),
+ );
+ Task.log();
+ } catch (error) {
+ Task.error(error.message);
+
+ Task.log('It seems that something went wrong when creating the app 🤔');
+ Task.log('We are going to clean up, and then you can try again.');
+
+ Task.section('Cleanup');
+ await cleanUp(tempDir);
+ Task.error('🔥 Failed to create app!');
+ }
+};
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index 5f24190237..3eafc6dc6b 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -17,6 +17,7 @@
import program from 'commander';
import chalk from 'chalk';
import fs from 'fs';
+import createAppCommand from './commands/create-app/createApp';
import createPluginCommand from './commands/create-plugin/createPlugin';
import watch from './commands/watch-deps';
import lintCommand from './commands/lint';
@@ -32,6 +33,11 @@ const main = (argv: string[]) => {
program.name('backstage-cli').version(packageJson.version ?? '0.0.0');
+ program
+ .command('create-app')
+ .description('Creates a new app in a new directory')
+ .action(actionHandler(createAppCommand));
+
program
.command('app:build')
.description('Build an app for a production release')
diff --git a/packages/cli/templates/default-app/README.md b/packages/cli/templates/default-app/README.md
new file mode 100644
index 0000000000..b2594d1e53
--- /dev/null
+++ b/packages/cli/templates/default-app/README.md
@@ -0,0 +1,3 @@
+# [Backstage](https://backstage.io)
+
+This is your newly scaffolded Backstage App, Good Luck!
diff --git a/packages/cli/templates/default-app/lerna.json b/packages/cli/templates/default-app/lerna.json
new file mode 100644
index 0000000000..322929db1d
--- /dev/null
+++ b/packages/cli/templates/default-app/lerna.json
@@ -0,0 +1,6 @@
+{
+ "packages": ["packages/*", "plugins/*"],
+ "npmClient": "yarn",
+ "useWorkspaces": true,
+ "version": "0.1.0"
+}
diff --git a/packages/cli/templates/default-app/package.json.hbs b/packages/cli/templates/default-app/package.json.hbs
new file mode 100644
index 0000000000..def7b3178b
--- /dev/null
+++ b/packages/cli/templates/default-app/package.json.hbs
@@ -0,0 +1,24 @@
+{
+ "name": "root",
+ "private": true,
+ "scripts": {
+ "start": "yarn build && yarn workspace app start",
+ "build": "lerna run build",
+ "test": "cross-env CI=true lerna run test -- --coverage",
+ "create-plugin": "backstage-cli create-plugin",
+ "lint": "lerna run lint"
+ },
+ "workspaces": {
+ "packages": [
+ "packages/*",
+ "plugins/*"
+ ]
+ },
+ "version": "1.0.0",
+ "devDependencies": {
+ "@backstage/cli": "^{{version}}",
+ "cross-env": "^7.0.0",
+ "lerna": "^3.20.2",
+ "prettier": "^1.19.1"
+ }
+}
diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs
new file mode 100644
index 0000000000..fd0a19fed0
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/package.json.hbs
@@ -0,0 +1,39 @@
+{
+ "name": "app",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@material-ui/core": "^4.9.1",
+ "@material-ui/icons": "^4.9.1",
+ "@backstage/cli": "^{{version}}",
+ "@backstage/core": "^{{version}}",
+ "@testing-library/jest-dom": "^4.2.4",
+ "@testing-library/react": "^9.3.2",
+ "@testing-library/user-event": "^7.1.2",
+ "@types/jest": "^24.0.0",
+ "@types/node": "^12.0.0",
+ "@types/react-router-dom": "^5.1.3",
+ "plugin-welcome": "0.0.0",
+ "react": "^16.12.0",
+ "react-dom": "^16.12.0",
+ "react-router-dom": "^5.1.2"
+ },
+ "scripts": {
+ "start": "backstage-cli app:serve",
+ "build": "backstage-cli app:build",
+ "test": "backstage-cli test",
+ "lint": "backstage-cli lint"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/packages/cli/templates/default-app/packages/app/public/android-chrome-192x192.png b/packages/cli/templates/default-app/packages/app/public/android-chrome-192x192.png
new file mode 100644
index 0000000000..3d56edbb0d
Binary files /dev/null and b/packages/cli/templates/default-app/packages/app/public/android-chrome-192x192.png differ
diff --git a/packages/cli/templates/default-app/packages/app/public/apple-touch-icon.png b/packages/cli/templates/default-app/packages/app/public/apple-touch-icon.png
new file mode 100644
index 0000000000..0977175f6f
Binary files /dev/null and b/packages/cli/templates/default-app/packages/app/public/apple-touch-icon.png differ
diff --git a/packages/cli/templates/default-app/packages/app/public/favicon-16x16.png b/packages/cli/templates/default-app/packages/app/public/favicon-16x16.png
new file mode 100644
index 0000000000..a455ffac7b
Binary files /dev/null and b/packages/cli/templates/default-app/packages/app/public/favicon-16x16.png differ
diff --git a/packages/cli/templates/default-app/packages/app/public/favicon-32x32.png b/packages/cli/templates/default-app/packages/app/public/favicon-32x32.png
new file mode 100644
index 0000000000..e2707f2d1e
Binary files /dev/null and b/packages/cli/templates/default-app/packages/app/public/favicon-32x32.png differ
diff --git a/packages/cli/templates/default-app/packages/app/public/favicon.ico b/packages/cli/templates/default-app/packages/app/public/favicon.ico
new file mode 100644
index 0000000000..5b582704a1
Binary files /dev/null and b/packages/cli/templates/default-app/packages/app/public/favicon.ico differ
diff --git a/packages/cli/templates/default-app/packages/app/public/index.html b/packages/cli/templates/default-app/packages/app/public/index.html
new file mode 100644
index 0000000000..ac2e9a6a73
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/public/index.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Backstage
+
+
+
+
+
+
+
diff --git a/packages/cli/templates/default-app/packages/app/public/manifest.json b/packages/cli/templates/default-app/packages/app/public/manifest.json
new file mode 100644
index 0000000000..4a7c1b4ec4
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "Backstage",
+ "name": "Backstage",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "48x48",
+ "type": "image/png"
+ }
+ ],
+ "start_url": "./index.html",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/packages/cli/templates/default-app/packages/app/public/robots.txt b/packages/cli/templates/default-app/packages/app/public/robots.txt
new file mode 100644
index 0000000000..01b0f9a107
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/public/robots.txt
@@ -0,0 +1,2 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
diff --git a/packages/cli/templates/default-app/packages/app/public/safari-pinned-tab.svg b/packages/cli/templates/default-app/packages/app/public/safari-pinned-tab.svg
new file mode 100644
index 0000000000..3b0f666390
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/public/safari-pinned-tab.svg
@@ -0,0 +1,28 @@
+
+
+
diff --git a/packages/cli/templates/default-app/packages/app/src/App.test.tsx b/packages/cli/templates/default-app/packages/app/src/App.test.tsx
new file mode 100644
index 0000000000..0074416375
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/src/App.test.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import App from './App';
+
+describe('App', () => {
+ it('should render', () => {
+ const rendered = render();
+ expect(rendered.baseElement).toBeInTheDocument();
+ });
+});
diff --git a/packages/cli/templates/default-app/packages/app/src/App.tsx b/packages/cli/templates/default-app/packages/app/src/App.tsx
new file mode 100644
index 0000000000..14a07870b2
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/src/App.tsx
@@ -0,0 +1,42 @@
+import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core';
+import { BackstageTheme, createApp } from '@backstage/core';
+import React, { FC } from 'react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import * as plugins from './plugins';
+
+const useStyles = makeStyles(theme => ({
+ '@global': {
+ html: {
+ height: '100%',
+ fontFamily: theme.typography.fontFamily,
+ },
+ body: {
+ height: '100%',
+ fontFamily: theme.typography.fontFamily,
+ 'overscroll-behavior-y': 'none',
+ },
+ a: {
+ color: 'inherit',
+ textDecoration: 'none',
+ },
+ },
+}));
+
+const app = createApp();
+app.registerPlugin(...Object.values(plugins));
+const AppComponent = app.build();
+
+const App: FC<{}> = () => {
+ useStyles();
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/packages/cli/templates/default-app/packages/app/src/index.tsx b/packages/cli/templates/default-app/packages/app/src/index.tsx
new file mode 100644
index 0000000000..b597a44232
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/src/index.tsx
@@ -0,0 +1,5 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+
+ReactDOM.render(, document.getElementById('root'));
diff --git a/packages/cli/templates/default-app/packages/app/src/plugins.ts b/packages/cli/templates/default-app/packages/app/src/plugins.ts
new file mode 100644
index 0000000000..6639319bbd
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/src/plugins.ts
@@ -0,0 +1 @@
+export { default as WelcomePlugin } from 'plugin-welcome';
diff --git a/packages/cli/templates/default-app/packages/app/src/setupTests.ts b/packages/cli/templates/default-app/packages/app/src/setupTests.ts
new file mode 100644
index 0000000000..666127af39
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/src/setupTests.ts
@@ -0,0 +1 @@
+import '@testing-library/jest-dom/extend-expect';
diff --git a/packages/cli/templates/default-app/packages/app/tsconfig.json b/packages/cli/templates/default-app/packages/app/tsconfig.json
new file mode 100644
index 0000000000..0457810645
--- /dev/null
+++ b/packages/cli/templates/default-app/packages/app/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "@spotify/web-scripts/config/tsconfig.json",
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react",
+ "incremental": false
+ },
+ "include": ["src"]
+}
diff --git a/packages/cli/templates/default-app/plugins/welcome/README.md b/packages/cli/templates/default-app/plugins/welcome/README.md
new file mode 100644
index 0000000000..e9b9dd0edf
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/README.md
@@ -0,0 +1,3 @@
+# Title
+
+Welcome to the welcome plugin!
diff --git a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs
new file mode 100644
index 0000000000..1aa7f2c5c7
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs
@@ -0,0 +1,20 @@
+{
+ "name": "plugin-welcome",
+ "version": "0.0.0",
+ "main": "dist/cjs/index.js",
+ "types": "dist/cjs/index.d.ts",
+ "private": true,
+ "scripts": {
+ "build": "backstage-cli plugin:build",
+ "lint": "backstage-cli lint",
+ "test": "backstage-cli test"
+ },
+ "devDependencies": {
+ "@backstage/core": "^{{version}}",
+ "@backstage/cli": "^{{version}}",
+ "@types/testing-library__jest-dom": "5.0.2"
+ },
+ "dependencies": {
+ "@material-ui/lab": "4.0.0-alpha.45"
+ }
+}
diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/Timer.tsx b/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/Timer.tsx
new file mode 100644
index 0000000000..24c79f91ee
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/Timer.tsx
@@ -0,0 +1,58 @@
+import React, { FC } from 'react';
+import { HeaderLabel } from '@backstage/core';
+
+const timeFormat = { hour: '2-digit', minute: '2-digit' };
+const utcOptions = { timeZone: 'UTC', ...timeFormat };
+const nycOptions = { timeZone: 'America/New_York', ...timeFormat };
+const tyoOptions = { timeZone: 'Asia/Tokyo', ...timeFormat };
+const stoOptions = { timeZone: 'Europe/Stockholm', ...timeFormat };
+
+const defaultTimes = {
+ timeNY: '',
+ timeUTC: '',
+ timeTYO: '',
+ timeSTO: '',
+};
+
+function getTimes() {
+ const d = new Date();
+ const lang = window.navigator.language;
+
+ // Using the browser native toLocaleTimeString instead of huge moment-tz
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString
+ const timeNY = d.toLocaleTimeString(lang, nycOptions);
+ const timeUTC = d.toLocaleTimeString(lang, utcOptions);
+ const timeTYO = d.toLocaleTimeString(lang, tyoOptions);
+ const timeSTO = d.toLocaleTimeString(lang, stoOptions);
+
+ return { timeNY, timeUTC, timeTYO, timeSTO };
+}
+
+const HomePageTimer: FC<{}> = () => {
+ const [{ timeNY, timeUTC, timeTYO, timeSTO }, setTimes] = React.useState(
+ defaultTimes,
+ );
+
+ React.useEffect(() => {
+ setTimes(getTimes());
+
+ const intervalId = setInterval(() => {
+ setTimes(getTimes());
+ }, 1000);
+
+ return () => {
+ clearInterval(intervalId);
+ };
+ }, []);
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default HomePageTimer;
diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/index.ts b/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/index.ts
new file mode 100644
index 0000000000..f1fc55bfe9
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/index.ts
@@ -0,0 +1 @@
+export { default } from './Timer';
diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx
new file mode 100644
index 0000000000..4b9b12913d
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import WelcomePage from './WelcomePage';
+import { ThemeProvider } from '@material-ui/core';
+import { BackstageTheme } from '@backstage/core';
+
+describe('WelcomePage', () => {
+ it('should render', () => {
+ const rendered = render(
+
+
+ ,
+ );
+ expect(rendered.baseElement).toBeInTheDocument();
+ });
+});
diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
new file mode 100644
index 0000000000..19f9ee55e2
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
@@ -0,0 +1,106 @@
+import React, { FC } from 'react';
+import { Link as RouterLink } from 'react-router-dom';
+import {
+ Typography,
+ Grid,
+ List,
+ ListItem,
+ ListItemText,
+ Link,
+} from '@material-ui/core';
+import Timer from '../Timer';
+import {
+ Content,
+ InfoCard,
+ Header,
+ Page,
+ pageTheme,
+ ContentHeader,
+ SupportButton,
+} from '@backstage/core';
+
+const WelcomePage: FC<{}> = () => {
+ const profile = { givenName: '' };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ You now have a running instance of Backstage!
+
+ 🎉
+
+ Let's make sure you get the most out of this platform by walking
+ you through the basics.
+
+
+ The Setup
+
+
+ Backstage is put together from three base concepts: the core,
+ the app and the plugins.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Try It Out
+
+
+ We suggest you either check out the documentation for{' '}
+
+ creating a plugin
+ {' '}
+ or have a look in the code for the{' '}
+
+ Home Page
+ {' '}
+ in the directory "plugins/home-page/src".
+
+
+
+
+
+ Quick Links
+
+
+ backstage.io
+
+
+
+ Create a plugin
+
+
+
+
+
+
+
+
+ );
+};
+
+export default WelcomePage;
diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/index.ts b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/index.ts
new file mode 100644
index 0000000000..b031301e7e
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/index.ts
@@ -0,0 +1 @@
+export { default } from './WelcomePage';
diff --git a/packages/cli/templates/default-app/plugins/welcome/src/index.ts b/packages/cli/templates/default-app/plugins/welcome/src/index.ts
new file mode 100644
index 0000000000..b68aea57f9
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/src/index.ts
@@ -0,0 +1 @@
+export { default } from './plugin';
diff --git a/packages/cli/templates/default-app/plugins/welcome/src/plugin.test.ts b/packages/cli/templates/default-app/plugins/welcome/src/plugin.test.ts
new file mode 100644
index 0000000000..f61dee5690
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/src/plugin.test.ts
@@ -0,0 +1,7 @@
+import plugin from './plugin';
+
+describe('welcome', () => {
+ it('should export plugin', () => {
+ expect(plugin).toBeDefined();
+ });
+});
diff --git a/packages/cli/templates/default-app/plugins/welcome/src/plugin.ts b/packages/cli/templates/default-app/plugins/welcome/src/plugin.ts
new file mode 100644
index 0000000000..35ceddd65f
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/src/plugin.ts
@@ -0,0 +1,9 @@
+import { createPlugin } from '@backstage/core';
+import WelcomePage from './components/WelcomePage';
+
+export default createPlugin({
+ id: 'welcome',
+ register({ router }) {
+ router.registerRoute('/', WelcomePage);
+ },
+});
diff --git a/packages/cli/templates/default-app/plugins/welcome/src/setupTests.ts b/packages/cli/templates/default-app/plugins/welcome/src/setupTests.ts
new file mode 100644
index 0000000000..666127af39
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/src/setupTests.ts
@@ -0,0 +1 @@
+import '@testing-library/jest-dom/extend-expect';
diff --git a/packages/cli/templates/default-app/plugins/welcome/tsconfig.json b/packages/cli/templates/default-app/plugins/welcome/tsconfig.json
new file mode 100644
index 0000000000..596e2cf729
--- /dev/null
+++ b/packages/cli/templates/default-app/plugins/welcome/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.json",
+ "include": ["src"]
+}
diff --git a/packages/cli/templates/default-app/prettier.config.js b/packages/cli/templates/default-app/prettier.config.js
new file mode 100644
index 0000000000..93df970dd6
--- /dev/null
+++ b/packages/cli/templates/default-app/prettier.config.js
@@ -0,0 +1 @@
+module.exports = require('@spotify/web-scripts/config/prettier.config.js');
diff --git a/packages/cli/templates/default-app/tsconfig.json b/packages/cli/templates/default-app/tsconfig.json
new file mode 100644
index 0000000000..25f61cc933
--- /dev/null
+++ b/packages/cli/templates/default-app/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "@spotify/web-scripts/config/tsconfig.json",
+ "exclude": ["**/*.test.*"],
+ "compilerOptions": {
+ "allowJs": true,
+ "noEmit": false,
+ "declarationMap": true,
+ "incremental": true
+ }
+}