diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 70f3d88f3e..a9e5f185ec 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -21,6 +21,7 @@ import { GithubPreparer, Preparers, GithubPublisher, + CreateReactAppTemplater, Templaters, } from '@backstage/plugin-scaffolder-backend'; import { Octokit } from '@octokit/rest'; @@ -29,8 +30,10 @@ import Docker from 'dockerode'; export default async function createPlugin({ logger }: PluginEnvironment) { const cookiecutterTemplater = new CookieCutter(); + const craTemplater = new CreateReactAppTemplater(); const templaters = new Templaters(); templaters.register('cookiecutter', cookiecutterTemplater); + templaters.register('cra', craTemplater); const filePreparer = new FilePreparer(); const githubPreparer = new GithubPreparer(); diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index c416dd6828..5a4321e2e3 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -39,7 +39,8 @@ "morgan": "^1.10.0", "nodegit": "0.26.5", "uuid": "^8.2.0", - "winston": "^3.2.1" + "winston": "^3.2.1", + "yaml": "^1.10.0" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.14", diff --git a/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml b/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml new file mode 100644 index 0000000000..5569fcd714 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml @@ -0,0 +1,33 @@ +apiVersion: backstage.io/v1alpha1 +kind: Template +metadata: + name: create-react-app-template + title: Create React App Template + description: A template that scaffolds an applications using the Create React App (CRA) templater. + tags: + - Experimental + - React + - CRA +spec: + owner: web@example.com + templater: cra + type: website + path: '.' + schema: + required: + - component_id + - use_typescript + properties: + component_id: + title: Name + type: string + description: Unique name of the component + description: + title: Description + type: string + description: Description of the component + use_typescript: + title: Use Typescript + type: boolean + description: Include typescript + default: true diff --git a/plugins/scaffolder-backend/scripts/mock-data.sh b/plugins/scaffolder-backend/scripts/mock-data.sh index 2424d15094..86bf9d87f6 100755 --- a/plugins/scaffolder-backend/scripts/mock-data.sh +++ b/plugins/scaffolder-backend/scripts/mock-data.sh @@ -3,6 +3,7 @@ for URL in \ 'react-ssr-template' \ 'springboot-template' \ + 'create-react-app' \ ; do \ curl \ --location \ diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/logger.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/logger.ts index 2e88dd34fe..a35be88832 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/logger.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/logger.ts @@ -23,7 +23,10 @@ export const makeLogStream = (meta: Record) => { // Create an empty stream to collect all the log lines into // one variable for the API. const stream = new PassThrough(); - stream.on('data', chunk => log.push(chunk.toString())); + stream.on('data', chunk => { + const textValue = chunk.toString().trim(); + if (textValue?.length > 1) log.push(textValue); + }); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts new file mode 100644 index 0000000000..da33149c24 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts @@ -0,0 +1,80 @@ +/* + * 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 { runDockerContainer } from './helpers'; +import { TemplaterBase, TemplaterRunOptions } from '.'; +import path from 'path'; +import { TemplaterRunResult } from './types'; +import * as yaml from 'yaml'; + +export class CreateReactAppTemplater implements TemplaterBase { + public async run(options: TemplaterRunOptions): Promise { + const { + component_id: componentName, + use_typescript: withTypescript, + description, + owner, + } = options.values; + + const resultDir = await fs.promises.mkdtemp(`${options.directory}-result`); + + await runDockerContainer({ + imageName: 'node:lts-alpine', + args: [ + 'create-react-app', + componentName as string, + withTypescript ? ' --template typescript' : '', + ], + templateDir: options.directory, + resultDir, + logStream: options.logStream, + dockerClient: options.dockerClient, + createOptions: { + Entrypoint: ['npx'], + WorkingDir: '/result', + }, + }); + + // Need to also make a component-info.yaml to store the data about the service. + const componentInfo = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: componentName, + description, + }, + spec: { + type: 'website', + lifecycle: 'experimental', + owner, + }, + }; + + const finalDir = path.resolve( + resultDir, + options.values.component_id as string, + ); + + await fs.promises.writeFile( + `${finalDir}/component-info.yaml`, + yaml.stringify(componentInfo), + ); + + return { + resultDir: finalDir, + }; + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts index 0267d5eeab..b99b1510ff 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts @@ -26,6 +26,7 @@ export type RunDockerContainerOptions = { resultDir: string; templateDir: string; dockerClient: Docker; + createOptions?: Docker.ContainerCreateOptions; }; /** @@ -59,8 +60,18 @@ export const runDockerContainer = async ({ resultDir, templateDir, dockerClient, + createOptions = {}, }: RunDockerContainerOptions) => { - await dockerClient.pull(imageName, {}); + await new Promise((resolve, reject) => + dockerClient.pull(imageName, {}, (err, res) => { + console.warn(err, res); + if (err) { + reject(err); + } else { + resolve(res); + } + }), + ); const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run( imageName, args, @@ -75,6 +86,7 @@ export const runDockerContainer = async ({ `${await fs.promises.realpath(templateDir)}:/template`, ], }, + ...createOptions, }, ); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts index 56358f5c5b..45b787543a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/index.ts @@ -17,3 +17,4 @@ export * from './cookiecutter'; export * from './types'; export * from './helpers'; export * from './templaters'; +export * from './cra'; diff --git a/plugins/scaffolder/src/components/JobStage/JobStage.tsx b/plugins/scaffolder/src/components/JobStage/JobStage.tsx index 71b73b752c..1e9adafb18 100644 --- a/plugins/scaffolder/src/components/JobStage/JobStage.tsx +++ b/plugins/scaffolder/src/components/JobStage/JobStage.tsx @@ -126,7 +126,7 @@ export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => { ) : ( }>
- +
)}