diff --git a/.changeset/bright-elephants-sparkle.md b/.changeset/bright-elephants-sparkle.md new file mode 100644 index 0000000000..05dc6150c6 --- /dev/null +++ b/.changeset/bright-elephants-sparkle.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Created a flag for scaffolding apps using the new frontend system. diff --git a/packages/create-app/README.md b/packages/create-app/README.md index d5b5039d4e..22e35fdc00 100644 --- a/packages/create-app/README.md +++ b/packages/create-app/README.md @@ -19,6 +19,20 @@ yarn install yarn backstage-create-app ``` +## Testing Changes Locally + +If you want to be able to test changes to `create-app` locally you can run the following command: + +```sh +./path/to/your/backstage/fork/backstage/packages/create-app/bin/backstage-create-app +``` + +If your fork lives in `~/repos/forks` then it would be: + +```sh +~/repos/forks/backstage/packages/create-app/bin/backstage-create-app +``` + ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) diff --git a/packages/create-app/cli-report.md b/packages/create-app/cli-report.md index 982661fbf5..081371b0c0 100644 --- a/packages/create-app/cli-report.md +++ b/packages/create-app/cli-report.md @@ -8,6 +8,7 @@ Usage: backstage-create-app [options] Options: + --experimental-frontend --path [directory] --skip-install --template-path [directory] diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 844d2aee1b..02fc068f95 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -57,7 +57,7 @@ describe('command entrypoint', () => { jest.resetAllMocks(); }); - test('should call expected tasks with no path option', async () => { + test('should call expected tasks with no `--path` option', async () => { const cmd = {} as unknown as Command; await createApp(cmd); expect(checkAppExistsMock).toHaveBeenCalled(); @@ -74,11 +74,12 @@ describe('command entrypoint', () => { expect(templatingMock.mock.lastCall?.[1]).toContain( path.join(tmpdir(), 'MyApp'), ); + expect(templatingMock.mock.lastCall?.[3]).toEqual(['packages/app-next/']); expect(moveAppMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); }); - it('should call expected tasks with path option', async () => { + it('should call expected tasks with `--path` option', async () => { const cmd = { path: 'myDirectory' } as unknown as Command; await createApp(cmd); expect(checkPathExistsMock).toHaveBeenCalled(); @@ -93,10 +94,33 @@ describe('command entrypoint', () => { ), ); expect(templatingMock.mock.lastCall?.[1]).toEqual('myDirectory'); + expect(templatingMock.mock.lastCall?.[3]).toEqual(['packages/app-next/']); expect(buildAppMock).toHaveBeenCalled(); }); - it('should call expected tasks with relative --template-path option', async () => { + it('should call expected tasks when `--experimental-frontend` is supplied', async () => { + const cmd = { experimentalFrontend: true } as unknown as Command; + await createApp(cmd); + expect(checkAppExistsMock).toHaveBeenCalled(); + expect(tryInitGitRepositoryMock).toHaveBeenCalled(); + expect(templatingMock).toHaveBeenCalled(); + expect(templatingMock.mock.lastCall?.[0]).toEqual( + findPaths(__dirname).resolveTarget( + 'packages', + 'create-app', + 'templates', + 'default-app', + ), + ); + expect(templatingMock.mock.lastCall?.[1]).toContain( + path.join(tmpdir(), 'MyApp'), + ); + expect(templatingMock.mock.lastCall?.[3]).toEqual(['packages/app/']); + expect(moveAppMock).toHaveBeenCalled(); + expect(buildAppMock).toHaveBeenCalled(); + }); + + it('should call expected tasks with relative `--template-path` option', async () => { const cmd = { path: 'myDirectory', templatePath: 'templateDirectory', @@ -109,10 +133,11 @@ describe('command entrypoint', () => { findPaths(__dirname).resolveTarget('templateDirectory'), ); expect(templatingMock.mock.lastCall?.[1]).toEqual('myDirectory'); + expect(templatingMock.mock.lastCall?.[3]).toEqual([]); expect(buildAppMock).toHaveBeenCalled(); }); - it('should call expected tasks with absolute --template-path option', async () => { + it('should call expected tasks with absolute `--template-path` option', async () => { const cmd = { path: 'myDirectory', templatePath: path.resolve('somewhere', 'templateDirectory'), @@ -125,16 +150,17 @@ describe('command entrypoint', () => { path.resolve('somewhere', 'templateDirectory'), ); expect(templatingMock.mock.lastCall?.[1]).toEqual('myDirectory'); + expect(templatingMock.mock.lastCall?.[3]).toEqual([]); expect(buildAppMock).toHaveBeenCalled(); }); - it('should not call `buildAppTask` when `skipInstall` is supplied', async () => { + it('should not call `buildAppTask()` when `--skip-install` is supplied', async () => { const cmd = { skipInstall: true } as unknown as Command; await createApp(cmd); expect(buildAppMock).not.toHaveBeenCalled(); }); - it('should not call `initGitRepository` when `gitConfig` is undefined', async () => { + it('should not call `initGitRepository()` when `gitConfig` is undefined', async () => { const cmd = {} as unknown as Command; readGitConfig.mockResolvedValue(undefined); await createApp(cmd); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index d4d9dda18a..a2080bd45e 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -65,10 +65,17 @@ export default async (opts: OptionValues): Promise => { }, ]); + // Use `--template-path` argument as template when specified. Otherwise, use the default template. const templateDir = opts.templatePath ? paths.resolveTarget(opts.templatePath) : paths.resolveOwn('templates/default-app'); + // If using the default template, filter out `packages/app` when the `--experimental-frontend` flag + // is used or `packages/app-next` when its not used. + const excludedDirs = !opts.templatePath + ? [opts.experimentalFrontend ? 'packages/app/' : 'packages/app-next/'] + : []; + // Use `--path` argument as application directory when specified, otherwise // create a directory using `answers.name` const appDir = opts.path @@ -88,10 +95,15 @@ export default async (opts: OptionValues): Promise => { await checkPathExistsTask(appDir); Task.section('Preparing files'); - await templatingTask(templateDir, opts.path, { - ...answers, - defaultBranch: gitConfig?.defaultBranch ?? DEFAULT_BRANCH, - }); + await templatingTask( + templateDir, + opts.path, + { + ...answers, + defaultBranch: gitConfig?.defaultBranch ?? DEFAULT_BRANCH, + }, + excludedDirs, + ); } else { // Template to temporary location, and then move files @@ -102,10 +114,15 @@ export default async (opts: OptionValues): Promise => { const tempDir = await fs.mkdtemp(resolvePath(os.tmpdir(), answers.name)); Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, { - ...answers, - defaultBranch: gitConfig?.defaultBranch ?? DEFAULT_BRANCH, - }); + await templatingTask( + templateDir, + tempDir, + { + ...answers, + defaultBranch: gitConfig?.defaultBranch ?? DEFAULT_BRANCH, + }, + excludedDirs, + ); Task.section('Moving to final location'); await moveAppTask(tempDir, appDir, answers.name); diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index d9b1226848..46e9f188e4 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -31,6 +31,10 @@ const main = (argv: string[]) => { .name('backstage-create-app') .version(version) .description('Creates a new app in a new directory or specified path') + .option( + '--experimental-frontend', + 'Use the default template with the experimental frontend', + ) .option( '--path [directory]', 'Location to store the app defaulting to a new folder with the app name', diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index c5d5092410..47735900ec 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -49,59 +49,65 @@ jest.mock('child_process'); jest.mock('./versions', () => ({ packageVersions: { root: '1.2.3', - '@backstage/cli': '1.0.0', + '@backstage/app-defaults': '1.0.0', '@backstage/backend-defaults': '1.0.0', '@backstage/backend-tasks': '1.0.0', + '@backstage/canon': '1.0.0', '@backstage/catalog-model': '1.0.0', '@backstage/catalog-client': '1.0.0', + '@backstage/catalog-model': '1.0.0', + '@backstage/cli': '1.0.0', '@backstage/config': '1.0.0', + '@backstage/core-app-api': '1.0.0', + '@backstage/core-components': '1.0.0', + '@backstage/core-plugin-api': '1.0.0', + '@backstage/e2e-test-utils': '1.0.0', + '@backstage/frontend-defaults': '1.0.0', + '@backstage/frontend-plugin-api': '1.0.0', + '@backstage/integration-react': '1.0.0', + '@backstage/plugin-api-docs': '1.0.0', '@backstage/plugin-app-backend': '1.0.0', + '@backstage/plugin-app-visualizer': '1.0.0', '@backstage/plugin-auth-backend': '1.0.0', - '@backstage/plugin-auth-node': '1.0.0', '@backstage/plugin-auth-backend-module-github-provider': '1.0.0', '@backstage/plugin-auth-backend-module-guest-provider': '1.0.0', + '@backstage/plugin-auth-node': '1.0.0', + '@backstage/plugin-catalog': '1.0.0', '@backstage/plugin-catalog-backend': '1.0.0', '@backstage/plugin-catalog-backend-module-logs': '1.0.0', '@backstage/plugin-catalog-backend-module-scaffolder-entity-model': '1.0.0', - '@backstage/plugin-permission-common': '1.0.0', - '@backstage/plugin-permission-node': '1.0.0', + '@backstage/plugin-catalog-common': '1.0.0', + '@backstage/plugin-catalog-graph': '1.0.0', + '@backstage/plugin-catalog-import': '1.0.0', + '@backstage/plugin-catalog-react': '1.0.0', + '@backstage/plugin-home': '1.0.0', + '@backstage/plugin-kubernetes': '1.0.0', + '@backstage/plugin-kubernetes-backend': '1.0.0', + '@backstage/plugin-org': '1.0.0', '@backstage/plugin-permission-backend': '1.0.0', '@backstage/plugin-permission-backend-module-allow-all-policy': '1.0.0', + '@backstage/plugin-permission-common': '1.0.0', + '@backstage/plugin-permission-node': '1.0.0', + '@backstage/plugin-permission-react': '1.0.0', '@backstage/plugin-proxy-backend': '1.0.0', + '@backstage/plugin-scaffolder': '1.0.0', '@backstage/plugin-scaffolder-backend': '1.0.0', + '@backstage/plugin-scaffolder-backend-module-github': '1.0.0', + '@backstage/plugin-search': '1.0.0', '@backstage/plugin-search-backend': '1.0.0', '@backstage/plugin-search-backend-module-catalog': '1.0.0', '@backstage/plugin-search-backend-module-pg': '1.0.0', '@backstage/plugin-search-backend-module-techdocs': '1.0.0', '@backstage/plugin-search-backend-node': '1.0.0', - '@backstage/plugin-techdocs-backend': '1.0.0', - '@backstage/app-defaults': '1.0.0', - '@backstage/core-app-api': '1.0.0', - '@backstage/core-components': '1.0.0', - '@backstage/core-plugin-api': '1.0.0', - '@backstage/e2e-test-utils': '1.0.0', - '@backstage/integration-react': '1.0.0', - '@backstage/plugin-api-docs': '1.0.0', - '@backstage/plugin-catalog': '1.0.0', - '@backstage/plugin-catalog-common': '1.0.0', - '@backstage/plugin-catalog-graph': '1.0.0', - '@backstage/plugin-catalog-import': '1.0.0', - '@backstage/plugin-catalog-react': '1.0.0', - '@backstage/plugin-kubernetes': '1.0.0', - '@backstage/plugin-kubernetes-backend': '1.0.0', - '@backstage/plugin-org': '1.0.0', - '@backstage/plugin-scaffolder': '1.0.0', - '@backstage/plugin-scaffolder-backend-module-github': '1.0.0', - '@backstage/plugin-permission-react': '1.0.0', - '@backstage/plugin-search': '1.0.0', '@backstage/plugin-search-react': '1.0.0', '@backstage/plugin-techdocs': '1.0.0', - '@backstage/plugin-techdocs-react': '1.0.0', + '@backstage/plugin-techdocs-backend': '1.0.0', '@backstage/plugin-techdocs-module-addons-contrib': '1.0.0', + '@backstage/plugin-techdocs-react': '1.0.0', '@backstage/plugin-user-settings': '1.0.0', - '@backstage/theme': '1.0.0', '@backstage/test-utils': '1.0.0', '@backstage/ui': '1.0.0', + '@backstage/theme': '1.0.0', }, })); @@ -267,7 +273,7 @@ describe('tasks', () => { }); describe('templatingTask', () => { - it('should generate a project populating context parameters', async () => { + it('should generate a project and populate context parameters', async () => { const templateDir = resolvePath(__dirname, '../../templates/default-app'); const destinationDir = 'templatedApp'; const context = { @@ -291,6 +297,20 @@ describe('tasks', () => { fs.readFile('templatedApp/packages/backend/package.json', 'utf-8'), ).resolves.toContain('sqlite3"'); }); + + it('should generate a project and skip excluded directories', async () => { + const templateDir = resolvePath(__dirname, '../../templates/default-app'); + const destinationDir = 'templatedApp'; + const context = { name: 'Backstage', dbTypeSqlite: true }; + const excludedDirs = ['packages/app-next']; + await templatingTask(templateDir, destinationDir, context, excludedDirs); + expect(fs.existsSync('templatedApp/packages/app/package.json')).toBe( + true, + ); + expect(fs.existsSync('templatedApp/packages/app-next/package.json')).toBe( + false, + ); + }); }); describe('readGitConfig', () => { diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 51308275a6..197652e4c9 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -85,21 +85,32 @@ export class Task { * @param templateDir - location containing template files * @param destinationDir - location to save templated project * @param context - template parameters + * @param excludedDirs - template files to exclude */ export async function templatingTask( templateDir: string, destinationDir: string, context: any, + excludedDirs?: string[], ) { const files = await recursive(templateDir).catch(error => { throw new Error(`Failed to read template directory: ${error.message}`); }); for (const file of files) { - const destinationFile = resolvePath( - destinationDir, - relativePath(templateDir, file), - ); + const filePath = relativePath(templateDir, file); + + if ( + excludedDirs?.some(excludedDir => { + const normalizedFilePath = filePath.replace(/\\/g, '/'); + const normalizedExcludedDir = excludedDir.replace(/\\/g, '/'); + return normalizedFilePath.startsWith(normalizedExcludedDir); + }) + ) { + continue; + } + + const destinationFile = resolvePath(destinationDir, filePath); await fs.ensureDir(dirname(destinationFile)); if (file.endsWith('.hbs')) { diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 179506063b..7211f992c6 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -33,6 +33,7 @@ import { version as root } from '../../../../package.json'; import { version as appDefaults } from '../../../app-defaults/package.json'; import { version as backendDefaults } from '../../../backend-defaults/package.json'; +import { version as canon } from '../../../canon/package.json'; import { version as catalogClient } from '../../../catalog-client/package.json'; import { version as catalogModel } from '../../../catalog-model/package.json'; import { version as cli } from '../../../cli/package.json'; @@ -41,56 +42,58 @@ import { version as coreAppApi } from '../../../core-app-api/package.json'; import { version as coreComponents } from '../../../core-components/package.json'; import { version as corePluginApi } from '../../../core-plugin-api/package.json'; import { version as e2eTestUtils } from '../../../e2e-test-utils/package.json'; -import { version as errors } from '../../../errors/package.json'; +import { version as frontendDefaults } from '../../../frontend-defaults/package.json'; +import { version as frontendPluginApi } from '../../../frontend-plugin-api/package.json'; import { version as integrationReact } from '../../../integration-react/package.json'; import { version as testUtils } from '../../../test-utils/package.json'; import { version as theme } from '../../../theme/package.json'; -import { version as repoTools } from '../../../repo-tools/package.json'; import { version as ui } from '../../../ui/package.json'; - import { version as pluginApiDocs } from '../../../../plugins/api-docs/package.json'; import { version as pluginAppBackend } from '../../../../plugins/app-backend/package.json'; +import { version as pluginAppVisualizer } from '../../../../plugins/app-visualizer/package.json'; import { version as pluginAuthBackend } from '../../../../plugins/auth-backend/package.json'; import { version as pluginAuthBackendModuleGithubProvider } from '../../../../plugins/auth-backend-module-github-provider/package.json'; import { version as pluginAuthBackendModuleGuestProvider } from '../../../../plugins/auth-backend-module-guest-provider/package.json'; import { version as pluginAuthNode } from '../../../../plugins/auth-node/package.json'; import { version as pluginCatalog } from '../../../../plugins/catalog/package.json'; -import { version as pluginCatalogCommon } from '../../../../plugins/catalog-common/package.json'; -import { version as pluginCatalogReact } from '../../../../plugins/catalog-react/package.json'; import { version as pluginCatalogBackend } from '../../../../plugins/catalog-backend/package.json'; import { version as pluginCatalogBackendModuleLogs } from '../../../../plugins/catalog-backend-module-logs/package.json'; import { version as pluginCatalogBackendModuleScaffolderEntityModel } from '../../../../plugins/catalog-backend-module-scaffolder-entity-model/package.json'; +import { version as pluginCatalogCommon } from '../../../../plugins/catalog-common/package.json'; import { version as pluginCatalogGraph } from '../../../../plugins/catalog-graph/package.json'; import { version as pluginCatalogImport } from '../../../../plugins/catalog-import/package.json'; +import { version as pluginCatalogReact } from '../../../../plugins/catalog-react/package.json'; +import { version as pluginHome } from '../../../../plugins/home/package.json'; import { version as pluginKubernetes } from '../../../../plugins/kubernetes/package.json'; import { version as pluginKubernetesBackend } from '../../../../plugins/kubernetes-backend/package.json'; import { version as pluginOrg } from '../../../../plugins/org/package.json'; import { version as pluginPermissionBackend } from '../../../../plugins/permission-backend/package.json'; import { version as pluginPermissionBackendModulePolicyAllowAll } from '../../../../plugins/permission-backend-module-policy-allow-all/package.json'; import { version as pluginPermissionCommon } from '../../../../plugins/permission-common/package.json'; -import { version as pluginPermissionReact } from '../../../../plugins/permission-react/package.json'; import { version as pluginPermissionNode } from '../../../../plugins/permission-node/package.json'; +import { version as pluginPermissionReact } from '../../../../plugins/permission-react/package.json'; import { version as pluginProxyBackend } from '../../../../plugins/proxy-backend/package.json'; import { version as pluginScaffolder } from '../../../../plugins/scaffolder/package.json'; import { version as pluginScaffolderBackend } from '../../../../plugins/scaffolder-backend/package.json'; import { version as pluginScaffolderBackendModuleGithub } from '../../../../plugins/scaffolder-backend-module-github/package.json'; import { version as pluginSearch } from '../../../../plugins/search/package.json'; -import { version as pluginSearchReact } from '../../../../plugins/search-react/package.json'; import { version as pluginSearchBackend } from '../../../../plugins/search-backend/package.json'; import { version as pluginSearchBackendModuleCatalog } from '../../../../plugins/search-backend-module-catalog/package.json'; import { version as pluginSearchBackendModulePg } from '../../../../plugins/search-backend-module-pg/package.json'; import { version as pluginSearchBackendModuleTechdocs } from '../../../../plugins/search-backend-module-techdocs/package.json'; import { version as pluginSearchBackendNode } from '../../../../plugins/search-backend-node/package.json'; +import { version as pluginSearchReact } from '../../../../plugins/search-react/package.json'; import { version as pluginTechdocs } from '../../../../plugins/techdocs/package.json'; -import { version as pluginTechdocsReact } from '../../../../plugins/techdocs-react/package.json'; -import { version as pluginTechdocsModuleAddonsContrib } from '../../../../plugins/techdocs-module-addons-contrib/package.json'; import { version as pluginTechdocsBackend } from '../../../../plugins/techdocs-backend/package.json'; +import { version as pluginTechdocsModuleAddonsContrib } from '../../../../plugins/techdocs-module-addons-contrib/package.json'; +import { version as pluginTechdocsReact } from '../../../../plugins/techdocs-react/package.json'; import { version as pluginUserSettings } from '../../../../plugins/user-settings/package.json'; export const packageVersions = { root, '@backstage/app-defaults': appDefaults, '@backstage/backend-defaults': backendDefaults, + '@backstage/canon': canon, '@backstage/catalog-client': catalogClient, '@backstage/catalog-model': catalogModel, '@backstage/cli': cli, @@ -99,11 +102,12 @@ export const packageVersions = { '@backstage/core-components': coreComponents, '@backstage/core-plugin-api': corePluginApi, '@backstage/e2e-test-utils': e2eTestUtils, - '@backstage/errors': errors, + '@backstage/frontend-defaults': frontendDefaults, + '@backstage/frontend-plugin-api': frontendPluginApi, '@backstage/integration-react': integrationReact, - '@backstage/repo-tools': repoTools, '@backstage/plugin-api-docs': pluginApiDocs, '@backstage/plugin-app-backend': pluginAppBackend, + '@backstage/plugin-app-visualizer': pluginAppVisualizer, '@backstage/plugin-auth-backend': pluginAuthBackend, '@backstage/plugin-auth-backend-module-github-provider': pluginAuthBackendModuleGithubProvider, @@ -111,15 +115,16 @@ export const packageVersions = { pluginAuthBackendModuleGuestProvider, '@backstage/plugin-auth-node': pluginAuthNode, '@backstage/plugin-catalog': pluginCatalog, - '@backstage/plugin-catalog-common': pluginCatalogCommon, - '@backstage/plugin-catalog-react': pluginCatalogReact, '@backstage/plugin-catalog-backend': pluginCatalogBackend, '@backstage/plugin-catalog-backend-module-logs': pluginCatalogBackendModuleLogs, '@backstage/plugin-catalog-backend-module-scaffolder-entity-model': pluginCatalogBackendModuleScaffolderEntityModel, + '@backstage/plugin-catalog-common': pluginCatalogCommon, '@backstage/plugin-catalog-graph': pluginCatalogGraph, '@backstage/plugin-catalog-import': pluginCatalogImport, + '@backstage/plugin-catalog-react': pluginCatalogReact, + '@backstage/plugin-home': pluginHome, '@backstage/plugin-kubernetes': pluginKubernetes, '@backstage/plugin-kubernetes-backend': pluginKubernetesBackend, '@backstage/plugin-org': pluginOrg, @@ -135,7 +140,6 @@ export const packageVersions = { '@backstage/plugin-scaffolder-backend-module-github': pluginScaffolderBackendModuleGithub, '@backstage/plugin-search': pluginSearch, - '@backstage/plugin-search-react': pluginSearchReact, '@backstage/plugin-search-backend': pluginSearchBackend, '@backstage/plugin-search-backend-module-catalog': pluginSearchBackendModuleCatalog, @@ -143,11 +147,12 @@ export const packageVersions = { '@backstage/plugin-search-backend-module-techdocs': pluginSearchBackendModuleTechdocs, '@backstage/plugin-search-backend-node': pluginSearchBackendNode, + '@backstage/plugin-search-react': pluginSearchReact, '@backstage/plugin-techdocs': pluginTechdocs, - '@backstage/plugin-techdocs-react': pluginTechdocsReact, + '@backstage/plugin-techdocs-backend': pluginTechdocsBackend, '@backstage/plugin-techdocs-module-addons-contrib': pluginTechdocsModuleAddonsContrib, - '@backstage/plugin-techdocs-backend': pluginTechdocsBackend, + '@backstage/plugin-techdocs-react': pluginTechdocsReact, '@backstage/plugin-user-settings': pluginUserSettings, '@backstage/test-utils': testUtils, '@backstage/theme': theme, diff --git a/packages/create-app/templates/default-app/packages/app-next/.eslintignore b/packages/create-app/templates/default-app/packages/app-next/.eslintignore new file mode 100644 index 0000000000..a48cf0de7a --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/.eslintignore @@ -0,0 +1 @@ +public diff --git a/packages/create-app/templates/default-app/packages/app-next/.eslintrc.js.hbs b/packages/create-app/templates/default-app/packages/app-next/.eslintrc.js.hbs new file mode 100644 index 0000000000..e487f765b2 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/.eslintrc.js.hbs @@ -0,0 +1,5 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + '@backstage/no-top-level-material-ui-4-imports': 'error', + }, +}); diff --git a/packages/create-app/templates/default-app/packages/app-next/README.md b/packages/create-app/templates/default-app/packages/app-next/README.md new file mode 100644 index 0000000000..5bd1f2cb92 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/README.md @@ -0,0 +1,9 @@ +# example-app + +This package is an example of a Backstage application using +the [New Frontend System](https://backstage.io/docs/frontend-system/). + +To play with it, open a terminal and run the command: `yarn start` + +**NOTE:** Don't forget to open a second terminal and to launch the backend there, using `yarn start`! The frontend +requires a backend to connect to. diff --git a/packages/create-app/templates/default-app/packages/app-next/e2e-tests/app.test.ts b/packages/create-app/templates/default-app/packages/app-next/e2e-tests/app.test.ts new file mode 100644 index 0000000000..839ff883de --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/e2e-tests/app.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2020 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 { test, expect } from '@playwright/test'; + +test('App should render the welcome page', async ({ page }) => { + await page.goto('/'); + + const enterButton = page.getByRole('button', { name: 'Enter' }); + await expect(enterButton).toBeVisible(); + await enterButton.click(); + + await expect(page.getByText('My Company Catalog')).toBeVisible(); +}); diff --git a/packages/create-app/templates/default-app/packages/app-next/package.json.hbs b/packages/create-app/templates/default-app/packages/app-next/package.json.hbs new file mode 100644 index 0000000000..d94961ff1b --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/package.json.hbs @@ -0,0 +1,64 @@ +{ + "name": "app", + "version": "0.0.0", + "private": true, + "bundled": true, + "backstage": { + "role": "frontend" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "test": "backstage-cli package test", + "lint": "backstage-cli package lint" + }, + "dependencies": { + "@backstage/canon": "^{{version '@backstage/canon'}}", + "@backstage/cli": "^{{version '@backstage/cli'}}", + "@backstage/frontend-defaults": "^{{version '@backstage/frontend-defaults'}}", + "@backstage/frontend-plugin-api": "^{{version '@backstage/frontend-plugin-api'}}", + "@backstage/plugin-api-docs": "^{{version '@backstage/plugin-api-docs'}}", + "@backstage/plugin-app-visualizer": "^{{version '@backstage/plugin-app-visualizer'}}", + "@backstage/plugin-catalog": "^{{version '@backstage/plugin-catalog'}}", + "@backstage/plugin-catalog-graph": "^{{version '@backstage/plugin-catalog-graph'}}", + "@backstage/plugin-catalog-import": "^{{version '@backstage/plugin-catalog-import'}}", + "@backstage/plugin-home": "^{{version '@backstage/plugin-home'}}", + "@backstage/plugin-kubernetes": "^{{version '@backstage/plugin-kubernetes'}}", + "@backstage/plugin-org": "^{{version '@backstage/plugin-org'}}", + "@backstage/plugin-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}", + "@backstage/plugin-search": "^{{version '@backstage/plugin-search'}}", + "@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}", + "@backstage/plugin-techdocs-module-addons-contrib": "^{{version '@backstage/plugin-techdocs-module-addons-contrib'}}", + "@backstage/plugin-user-settings": "^{{version '@backstage/plugin-user-settings'}}", + "@backstage/theme": "^{{version '@backstage/theme'}}", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "react": "^18.0.2", + "react-dom": "^18.0.2", + "react-router": "^6.3.0", + "react-router-dom": "^6.3.0" + }, + "devDependencies": { + "@backstage/test-utils": "^{{version '@backstage/test-utils'}}", + "@playwright/test": "^1.32.3", + "@testing-library/jest-dom": "^6.0.0", + "@types/react": "*", + "@types/react-dom": "*" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "files": [ + "dist" + ] +} diff --git a/packages/create-app/templates/default-app/packages/app-next/public/android-chrome-192x192.png b/packages/create-app/templates/default-app/packages/app-next/public/android-chrome-192x192.png new file mode 100644 index 0000000000..eec0ae25b9 Binary files /dev/null and b/packages/create-app/templates/default-app/packages/app-next/public/android-chrome-192x192.png differ diff --git a/packages/create-app/templates/default-app/packages/app-next/public/android-chrome-512x512.png b/packages/create-app/templates/default-app/packages/app-next/public/android-chrome-512x512.png new file mode 100644 index 0000000000..ac0578133b Binary files /dev/null and b/packages/create-app/templates/default-app/packages/app-next/public/android-chrome-512x512.png differ diff --git a/packages/create-app/templates/default-app/packages/app-next/public/apple-touch-icon.png b/packages/create-app/templates/default-app/packages/app-next/public/apple-touch-icon.png new file mode 100644 index 0000000000..6a45afb38f Binary files /dev/null and b/packages/create-app/templates/default-app/packages/app-next/public/apple-touch-icon.png differ diff --git a/packages/create-app/templates/default-app/packages/app-next/public/favicon-16x16.png b/packages/create-app/templates/default-app/packages/app-next/public/favicon-16x16.png new file mode 100644 index 0000000000..58cf61a35e Binary files /dev/null and b/packages/create-app/templates/default-app/packages/app-next/public/favicon-16x16.png differ diff --git a/packages/create-app/templates/default-app/packages/app-next/public/favicon-32x32.png b/packages/create-app/templates/default-app/packages/app-next/public/favicon-32x32.png new file mode 100644 index 0000000000..c0915ece75 Binary files /dev/null and b/packages/create-app/templates/default-app/packages/app-next/public/favicon-32x32.png differ diff --git a/packages/create-app/templates/default-app/packages/app-next/public/favicon.ico b/packages/create-app/templates/default-app/packages/app-next/public/favicon.ico new file mode 100644 index 0000000000..5e45e5dfbd Binary files /dev/null and b/packages/create-app/templates/default-app/packages/app-next/public/favicon.ico differ diff --git a/packages/create-app/templates/default-app/packages/app-next/public/favicon.svg b/packages/create-app/templates/default-app/packages/app-next/public/favicon.svg new file mode 100644 index 0000000000..7f2d375cd8 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/public/favicon.svg @@ -0,0 +1 @@ +Backstage favicon \ No newline at end of file diff --git a/packages/create-app/templates/default-app/packages/app-next/public/index.html b/packages/create-app/templates/default-app/packages/app-next/public/index.html new file mode 100644 index 0000000000..63ab0bec0c --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/public/index.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + <%= config.getString('app.title') %> + + <% if (config.has('app.datadogRum')) { %> + + <% } %> + + + + +
+ + + diff --git a/packages/create-app/templates/default-app/packages/app-next/public/manifest.json b/packages/create-app/templates/default-app/packages/app-next/public/manifest.json new file mode 100644 index 0000000000..4a7c1b4ec4 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/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/create-app/templates/default-app/packages/app-next/public/robots.txt b/packages/create-app/templates/default-app/packages/app-next/public/robots.txt new file mode 100644 index 0000000000..01b0f9a107 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/public/robots.txt @@ -0,0 +1,2 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * diff --git a/packages/create-app/templates/default-app/packages/app-next/public/safari-pinned-tab.svg b/packages/create-app/templates/default-app/packages/app-next/public/safari-pinned-tab.svg new file mode 100644 index 0000000000..0f500b3002 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/public/safari-pinned-tab.svg @@ -0,0 +1 @@ +Created by potrace 1.11, written by Peter Selinger 2001-2013 \ No newline at end of file diff --git a/packages/create-app/templates/default-app/packages/app-next/src/App.test.tsx b/packages/create-app/templates/default-app/packages/app-next/src/App.test.tsx new file mode 100644 index 0000000000..ee456e1d72 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/src/App.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2020 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 { renderWithEffects } from '@backstage/test-utils'; + +// Rarely, and only in windows CI, do these tests take slightly more than the +// default five seconds +jest.setTimeout(15_000); + +describe('App', () => { + it('should render', async () => { + process.env = { + NODE_ENV: 'test', + APP_CONFIG: [ + { + data: { + app: { + title: 'Test', + support: { url: 'http://localhost:7007/support' }, + }, + backend: { baseUrl: 'http://localhost:7007' }, + lighthouse: { + baseUrl: 'http://localhost:3003', + }, + techdocs: { + storageUrl: 'http://localhost:7007/api/techdocs/static/docs', + }, + }, + context: 'test', + }, + ] as any, + }; + + const { default: app } = await import('./App'); + const rendered = await renderWithEffects(app); + expect(rendered.baseElement).toBeInTheDocument(); + }); +}); diff --git a/packages/create-app/templates/default-app/packages/app-next/src/App.tsx b/packages/create-app/templates/default-app/packages/app-next/src/App.tsx new file mode 100644 index 0000000000..5a3611ba65 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/src/App.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2023 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 { createApp } from '@backstage/frontend-defaults'; +import apiDocsPlugin from '@backstage/plugin-api-docs/alpha'; +import appVisualizerPlugin from '@backstage/plugin-app-visualizer'; +import catalogPlugin from '@backstage/plugin-catalog/alpha'; +import catalogGraphPlugin from '@backstage/plugin-catalog-graph/alpha'; +import catalogImportPlugin from '@backstage/plugin-catalog-import/alpha'; +import homePlugin from '@backstage/plugin-home/alpha'; +import kubernetesPlugin from '@backstage/plugin-kubernetes/alpha'; +import orgPlugin from '@backstage/plugin-org/alpha'; +import scaffolderPlugin from '@backstage/plugin-scaffolder/alpha'; +import searchPlugin from '@backstage/plugin-search/alpha'; +import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; +import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; + +import { createFrontendModule, PageBlueprint } from '@backstage/frontend-plugin-api'; +import { Navigate } from 'react-router'; + + +// This just defaults to `/catalog` instead of a blank page +const defaultPageExtension = PageBlueprint.make({ + name: 'default', + params: { + defaultPath: '/', + loader: () => Promise.resolve(), + }, +}); + +const app = createApp({ + features: [ + searchPlugin, + orgPlugin, + catalogPlugin, + apiDocsPlugin, + techdocsPlugin, + scaffolderPlugin, + homePlugin, + kubernetesPlugin, + catalogGraphPlugin, + catalogImportPlugin, + appVisualizerPlugin, + userSettingsPlugin, + createFrontendModule({ + pluginId: 'app', + extensions: [ + defaultPageExtension + ], + }), + ], +}); + +export default app.createRoot(); diff --git a/packages/create-app/templates/default-app/packages/app-next/src/index.tsx b/packages/create-app/templates/default-app/packages/app-next/src/index.tsx new file mode 100644 index 0000000000..82b9595c84 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/src/index.tsx @@ -0,0 +1,22 @@ +/* + * Copyright 2020 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 '@backstage/cli/asset-types'; +import ReactDOM from 'react-dom/client'; +import app from './App'; +import '@backstage/canon/css/styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render(app); diff --git a/packages/create-app/templates/default-app/packages/app-next/src/setupTests.ts b/packages/create-app/templates/default-app/packages/app-next/src/setupTests.ts new file mode 100644 index 0000000000..23fcbe9676 --- /dev/null +++ b/packages/create-app/templates/default-app/packages/app-next/src/setupTests.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2020 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. + */ + +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index c95b8294c7..f580281558 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -43,6 +43,7 @@ const templatePackagePaths = [ 'packages/cli/templates/frontend-plugin/package.json.hbs', 'packages/create-app/templates/default-app/package.json.hbs', 'packages/create-app/templates/default-app/packages/app/package.json.hbs', + 'packages/create-app/templates/default-app/packages/app-next/package.json.hbs', 'packages/create-app/templates/default-app/packages/backend/package.json.hbs', ];