Remove old factory cod

Signed-off-by: Min Kim <minkimcello@gmail.com>
This commit is contained in:
Min Kim
2024-12-19 16:17:19 -05:00
parent fbd18c60a5
commit 1ef678f015
20 changed files with 0 additions and 2264 deletions
-126
View File
@@ -1,126 +0,0 @@
/*
* 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 inquirer, { Answers } from 'inquirer';
import { AnyFactory, Prompt } from './types';
import * as factories from './factories';
import partition from 'lodash/partition';
function applyPromptMessageTransforms<T extends Answers>(
prompt: Prompt<T>,
transforms: {
message: (msg: string) => string;
error: (msg: string) => string;
},
): Prompt<T> {
return {
...prompt,
message:
prompt.message &&
(async answers => {
if (typeof prompt.message === 'function') {
return transforms.message(await prompt.message(answers));
}
return transforms.message(await prompt.message!);
}),
validate:
prompt.validate &&
(async (...args) => {
const result = await prompt.validate!(...args);
if (typeof result === 'string') {
return transforms.error(result);
}
return result;
}),
};
}
export class FactoryRegistry {
private static factoryMap = new Map<string, AnyFactory>(
Object.values(factories).map(factory => [factory.name, factory]),
);
static async interactiveSelect(preselected?: string): Promise<AnyFactory> {
let selected = preselected;
if (!selected) {
const answers = await inquirer.prompt<{ name: string }>([
{
type: 'list',
name: 'name',
message: 'What do you want to create?',
choices: Array.from(this.factoryMap.values()).map(factory => ({
name: `${factory.name} - ${factory.description}`,
value: factory.name,
})),
},
]);
selected = answers.name;
}
const factory = this.factoryMap.get(selected);
if (!factory) {
throw new Error(`Unknown selection '${selected}'`);
}
return factory;
}
static async populateOptions(
factory: AnyFactory,
provided: Record<string, string>,
): Promise<Record<string, string>> {
let currentOptions = provided;
if (factory.optionsDiscovery) {
const discoveredOptions = await factory.optionsDiscovery();
currentOptions = {
...currentOptions,
...(discoveredOptions as Record<string, string>),
};
}
if (factory.optionsPrompts) {
const [hasAnswers, needsAnswers] = partition(
factory.optionsPrompts,
option => option.name in currentOptions,
);
for (const option of hasAnswers) {
const value = provided[option.name];
if (option.validate) {
const result = option.validate(value);
if (result !== true) {
throw new Error(`Invalid option '${option.name}'. ${result}`);
}
}
}
currentOptions = await inquirer.prompt(
needsAnswers.map(option =>
applyPromptMessageTransforms(option, {
message: chalk.blue,
error: chalk.red,
}),
),
currentOptions,
);
}
return currentOptions;
}
}
@@ -1,143 +0,0 @@
/*
* 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 { sep } from 'path';
import { Task } from '../../tasks';
import { FactoryRegistry } from '../FactoryRegistry';
import {
createMockOutputStream,
expectLogsToMatch,
mockPaths,
} from './common/testUtils';
import { backendModule } from './backendModule';
import { createMockDirectory } from '@backstage/backend-test-utils';
const backendIndexTsContent = `
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.start();
`;
describe('backendModule factory', () => {
const mockDir = createMockDirectory();
beforeEach(() => {
mockPaths({
targetRoot: mockDir.path,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should create a backend plugin', async () => {
mockDir.setContent({
packages: {
backend: {
'package.json': JSON.stringify({}),
src: {
'index.ts': backendIndexTsContent,
},
},
},
plugins: {},
});
const options = await FactoryRegistry.populateOptions(backendModule, {
id: 'test',
moduleId: 'tester-two',
});
let modified = false;
const [output, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await backendModule.create(options, {
private: true,
isMonoRepo: true,
defaultVersion: '1.0.0',
markAsModified: () => {
modified = true;
},
createTemporaryDirectory: () => fs.mkdtemp('test'),
license: 'Apache-2.0',
});
expect(modified).toBe(true);
expectLogsToMatch(output, [
'Creating backend module backstage-plugin-test-backend-module-tester-two',
'Checking Prerequisites:',
`availability plugins${sep}test-backend-module-tester-two`,
'creating temp dir',
'Executing Template:',
'templating .eslintrc.js.hbs',
'templating README.md.hbs',
'templating package.json.hbs',
'templating index.ts.hbs',
'templating module.ts.hbs',
'Installing:',
`moving plugins${sep}test-backend-module-tester-two`,
'backend adding dependency',
'backend adding module',
]);
await expect(
fs.readFile(mockDir.resolve('packages/backend/src/index.ts'), 'utf8'),
).resolves.toBe(`
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.add(import('backstage-plugin-test-backend-module-tester-two'));
backend.start();
`);
await expect(
fs.readJson(mockDir.resolve('packages/backend/package.json')),
).resolves.toEqual({
dependencies: {
'backstage-plugin-test-backend-module-tester-two': '^1.0.0',
},
});
const moduleFile = await fs.readFile(
mockDir.resolve('plugins/test-backend-module-tester-two/src/module.ts'),
'utf-8',
);
expect(moduleFile).toContain(
`const testModuleTesterTwo = createBackendModule({`,
);
expect(moduleFile).toContain(`pluginId: 'test',`);
expect(moduleFile).toContain(`moduleId: 'tester-two',`);
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve('plugins/test-backend-module-tester-two'),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve('plugins/test-backend-module-tester-two'),
optional: true,
});
});
});
@@ -1,109 +0,0 @@
/*
* 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 chalk from 'chalk';
import camelCase from 'lodash/camelCase';
import { paths } from '../../paths';
import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners';
import { CreateContext, createFactory } from '../types';
import { addPackageDependency, addToBackend, Task } from '../../tasks';
import {
moduleIdIdPrompt,
ownerPrompt,
pluginIdPrompt,
} from './common/prompts';
import { executePluginPackageTemplate } from './common/tasks';
import { resolvePackageName } from './common/util';
type Options = {
id: string;
moduleId: string;
owner?: string;
codeOwnersPath?: string;
};
export const backendModule = createFactory<Options>({
name: 'backend-module',
description:
'A new backend module that extends an existing backend plugin with additional features',
optionsDiscovery: async () => ({
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
}),
optionsPrompts: [pluginIdPrompt(), moduleIdIdPrompt(), ownerPrompt()],
async create(options: Options, ctx: CreateContext) {
const { id: pluginId, moduleId } = options;
const dirName = `${pluginId}-backend-module-${moduleId}`;
const name = resolvePackageName({
baseName: dirName,
scope: ctx.scope,
plugin: true,
});
Task.log();
Task.log(`Creating backend module ${chalk.cyan(name)}`);
const targetDir = ctx.isMonoRepo
? paths.resolveTargetRoot('plugins', dirName)
: paths.resolveTargetRoot(`backstage-plugin-${dirName}`);
const moduleCamelCase = camelCase(moduleId);
const modulePascalCase =
moduleCamelCase[0].toUpperCase() + moduleCamelCase.slice(1);
const moduleVar = `${camelCase(pluginId)}Module${modulePascalCase}`;
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: 'default-backend-module',
values: {
pluginId,
moduleId,
name,
moduleVar,
packageVersion: ctx.defaultVersion,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
license: ctx.license,
},
});
if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) {
await Task.forItem('backend', 'adding dependency', async () => {
await addPackageDependency(
paths.resolveTargetRoot('packages/backend/package.json'),
{
dependencies: {
[name]: `^${ctx.defaultVersion}`,
},
},
);
});
}
await addToBackend(name, {
type: 'module',
});
if (options.owner) {
await addCodeownersEntry(`/plugins/${dirName}`, options.owner);
}
await Task.forCommand('yarn install', { cwd: targetDir, optional: true });
await Task.forCommand('yarn lint --fix', {
cwd: targetDir,
optional: true,
});
},
});
@@ -1,140 +0,0 @@
/*
* 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 { sep } from 'path';
import { Task } from '../../tasks';
import { FactoryRegistry } from '../FactoryRegistry';
import {
createMockOutputStream,
expectLogsToMatch,
mockPaths,
} from './common/testUtils';
import { backendPlugin } from './backendPlugin';
import { createMockDirectory } from '@backstage/backend-test-utils';
const backendIndexTsContent = `
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.start();
`;
describe('backendPlugin factory', () => {
const mockDir = createMockDirectory();
beforeEach(() => {
mockPaths({
targetRoot: mockDir.path,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should create a backend plugin', async () => {
mockDir.setContent({
packages: {
backend: {
'package.json': JSON.stringify({}),
src: {
'index.ts': backendIndexTsContent,
},
},
},
plugins: {},
});
const options = await FactoryRegistry.populateOptions(backendPlugin, {
id: 'test',
});
let modified = false;
const [output, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await backendPlugin.create(options, {
private: true,
isMonoRepo: true,
defaultVersion: '1.0.0',
markAsModified: () => {
modified = true;
},
createTemporaryDirectory: () => fs.mkdtemp('test'),
license: 'Apache-2.0',
});
expect(modified).toBe(true);
expectLogsToMatch(output, [
'Creating backend plugin backstage-plugin-test-backend',
'Checking Prerequisites:',
`availability plugins${sep}test-backend`,
'creating temp dir',
'Executing Template:',
'templating .eslintrc.js.hbs',
'templating README.md.hbs',
'templating index.ts.hbs',
'templating index.ts.hbs',
'templating package.json.hbs',
'templating plugin.ts.hbs',
'templating plugin.test.ts.hbs',
'copying index.ts',
'copying setupTests.ts',
'copying router.ts',
'copying router.test.ts',
'copying createTodoListService.ts',
'copying types.ts',
'Installing:',
`moving plugins${sep}test-backend`,
'backend adding dependency',
'backend adding plugin',
]);
await expect(
fs.readJson(mockDir.resolve('packages/backend/package.json')),
).resolves.toEqual({
dependencies: {
'backstage-plugin-test-backend': '^1.0.0',
},
});
await expect(
fs.readFile(mockDir.resolve('packages/backend/src/index.ts'), 'utf8'),
).resolves.toBe(`
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.add(import('backstage-plugin-test-backend'));
backend.start();
`);
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve('plugins/test-backend'),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve('plugins/test-backend'),
optional: true,
});
});
});
@@ -1,98 +0,0 @@
/*
* 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 chalk from 'chalk';
import camelCase from 'lodash/camelCase';
import { paths } from '../../paths';
import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners';
import { CreateContext, createFactory } from '../types';
import { addPackageDependency, addToBackend, Task } from '../../tasks';
import { ownerPrompt, pluginIdPrompt } from './common/prompts';
import { executePluginPackageTemplate } from './common/tasks';
import { resolvePackageName } from './common/util';
type Options = {
id: string;
owner?: string;
codeOwnersPath?: string;
};
export const backendPlugin = createFactory<Options>({
name: 'backend-plugin',
description: 'A new backend plugin',
optionsDiscovery: async () => ({
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options: Options, ctx: CreateContext) {
const { id } = options;
const pluginId = `${id}-backend`;
const name = resolvePackageName({
baseName: pluginId,
scope: ctx.scope,
plugin: true,
});
Task.log();
Task.log(`Creating backend plugin ${chalk.cyan(name)}`);
const targetDir = ctx.isMonoRepo
? paths.resolveTargetRoot('plugins', pluginId)
: paths.resolveTargetRoot(`backstage-plugin-${pluginId}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: 'default-backend-plugin',
values: {
id,
name,
pluginVar: `${camelCase(id)}Plugin`,
pluginVersion: ctx.defaultVersion,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
license: ctx.license,
},
});
if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) {
await Task.forItem('backend', 'adding dependency', async () => {
await addPackageDependency(
paths.resolveTargetRoot('packages/backend/package.json'),
{
dependencies: {
[name]: `^${ctx.defaultVersion}`,
},
},
);
});
}
await addToBackend(name, {
type: 'plugin',
});
if (options.owner) {
await addCodeownersEntry(`/plugins/${id}`, options.owner);
}
await Task.forCommand('yarn install', { cwd: targetDir, optional: true });
await Task.forCommand('yarn lint --fix', {
cwd: targetDir,
optional: true,
});
},
});
@@ -1,213 +0,0 @@
/*
* 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 { sep } from 'path';
import { Task } from '../../tasks';
import { FactoryRegistry } from '../FactoryRegistry';
import {
createMockOutputStream,
expectLogsToMatch,
mockPaths,
} from './common/testUtils';
import { frontendPlugin } from './frontendPlugin';
import { createMockDirectory } from '@backstage/backend-test-utils';
const appTsxContent = `
import { createApp } from '@backstage/app-defaults';
const router = (
<FlatRoutes>
<Route path="/" element={<Home />} />
</FlatRoutes>
)
`;
describe('frontendPlugin factory', () => {
const mockDir = createMockDirectory();
beforeEach(() => {
mockPaths({
targetRoot: mockDir.path,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should create a frontend plugin', async () => {
mockDir.setContent({
packages: {
app: {
'package.json': JSON.stringify({}),
src: {
'App.tsx': appTsxContent,
},
},
},
plugins: {},
});
const options = await FactoryRegistry.populateOptions(frontendPlugin, {
id: 'test',
});
let modified = false;
const [output, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await frontendPlugin.create(options, {
private: true,
isMonoRepo: true,
defaultVersion: '1.0.0',
markAsModified: () => {
modified = true;
},
createTemporaryDirectory: () => fs.mkdtemp('test'),
license: 'Apache-2.0',
});
expect(modified).toBe(true);
expectLogsToMatch(output, [
'Creating frontend plugin backstage-plugin-test',
'Checking Prerequisites:',
`availability plugins${sep}test`,
'creating temp dir',
'Executing Template:',
'templating .eslintrc.js.hbs',
'templating README.md.hbs',
'templating package.json.hbs',
'templating index.tsx.hbs',
'templating index.ts.hbs',
'templating plugin.test.ts.hbs',
'templating plugin.ts.hbs',
'templating routes.ts.hbs',
'copying setupTests.ts',
'templating ExampleComponent.test.tsx.hbs',
'templating ExampleComponent.tsx.hbs',
'copying index.ts',
'templating ExampleFetchComponent.test.tsx.hbs',
'templating ExampleFetchComponent.tsx.hbs',
'copying index.ts',
'Installing:',
`moving plugins${sep}test`,
'app adding dependency',
'app adding import',
]);
await expect(
fs.readJson(mockDir.resolve('packages/app/package.json')),
).resolves.toEqual({
dependencies: {
'backstage-plugin-test': '^1.0.0',
},
});
await expect(
fs.readFile(mockDir.resolve('packages/app/src/App.tsx'), 'utf8'),
).resolves.toBe(`
import { createApp } from '@backstage/app-defaults';
import { TestPage } from 'backstage-plugin-test';
const router = (
<FlatRoutes>
<Route path="/" element={<Home />} />
<Route path="/test" element={<TestPage />} />
</FlatRoutes>
)
`);
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve('plugins/test'),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve('plugins/test'),
optional: true,
});
});
it('should create a frontend plugin with more options and codeowners', async () => {
mockDir.setContent({
CODEOWNERS: '',
packages: {
app: {
'package.json': JSON.stringify({}),
src: {
'App.tsx': appTsxContent,
},
},
},
plugins: {},
});
const options = await FactoryRegistry.populateOptions(frontendPlugin, {
id: 'test',
owner: '@test-user',
});
const [, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await frontendPlugin.create(options, {
scope: 'internal',
private: true,
isMonoRepo: true,
defaultVersion: '1.0.0',
markAsModified: () => {},
createTemporaryDirectory: () => fs.mkdtemp('test'),
license: 'Apache-2.0',
});
await expect(
fs.readJson(mockDir.resolve('packages/app/package.json')),
).resolves.toEqual({
dependencies: {
'@internal/backstage-plugin-test': '^1.0.0',
},
});
await expect(
fs.readFile(mockDir.resolve('packages/app/src/App.tsx'), 'utf8'),
).resolves.toBe(`
import { createApp } from '@backstage/app-defaults';
import { TestPage } from '@internal/backstage-plugin-test';
const router = (
<FlatRoutes>
<Route path="/" element={<Home />} />
<Route path="/test" element={<TestPage />} />
</FlatRoutes>
)
`);
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve('plugins/test'),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve('plugins/test'),
optional: true,
});
});
});
@@ -1,133 +0,0 @@
/*
* 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 chalk from 'chalk';
import camelCase from 'lodash/camelCase';
import upperFirst from 'lodash/upperFirst';
import { paths } from '../../paths';
import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners';
import { CreateContext, createFactory } from '../types';
import { addPackageDependency, Task } from '../../tasks';
import { ownerPrompt, pluginIdPrompt } from './common/prompts';
import { executePluginPackageTemplate } from './common/tasks';
import { resolvePackageName } from './common/util';
type Options = {
id: string;
owner?: string;
codeOwnersPath?: string;
};
export const frontendPlugin = createFactory<Options>({
name: 'plugin',
description: 'A new frontend plugin',
optionsDiscovery: async () => ({
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options: Options, ctx: CreateContext) {
const { id } = options;
const name = resolvePackageName({
baseName: id,
scope: ctx.scope,
plugin: true,
});
const extensionName = `${upperFirst(camelCase(id))}Page`;
Task.log();
Task.log(`Creating frontend plugin ${chalk.cyan(name)}`);
const targetDir = ctx.isMonoRepo
? paths.resolveTargetRoot('plugins', id)
: paths.resolveTargetRoot(`backstage-plugin-${id}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: 'default-plugin',
values: {
id,
name,
extensionName,
pluginVar: `${camelCase(id)}Plugin`,
pluginVersion: ctx.defaultVersion,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
license: ctx.license,
},
});
if (await fs.pathExists(paths.resolveTargetRoot('packages/app'))) {
await Task.forItem('app', 'adding dependency', async () => {
await addPackageDependency(
paths.resolveTargetRoot('packages/app/package.json'),
{
dependencies: {
[name]: `^${ctx.defaultVersion}`,
},
},
);
});
await Task.forItem('app', 'adding import', async () => {
const pluginsFilePath = paths.resolveTargetRoot(
'packages/app/src/App.tsx',
);
if (!(await fs.pathExists(pluginsFilePath))) {
return;
}
const content = await fs.readFile(pluginsFilePath, 'utf8');
const revLines = content.split('\n').reverse();
const lastImportIndex = revLines.findIndex(line =>
line.match(/ from ("|').*("|')/),
);
const lastRouteIndex = revLines.findIndex(line =>
line.match(/<\/FlatRoutes/),
);
if (lastImportIndex !== -1 && lastRouteIndex !== -1) {
const importLine = `import { ${extensionName} } from '${name}';`;
if (!content.includes(importLine)) {
revLines.splice(lastImportIndex, 0, importLine);
}
const componentLine = `<Route path="/${id}" element={<${extensionName} />} />`;
if (!content.includes(componentLine)) {
const [indentation] =
revLines[lastRouteIndex + 1].match(/^\s*/) ?? [];
revLines.splice(lastRouteIndex + 1, 0, indentation + componentLine);
}
const newContent = revLines.reverse().join('\n');
await fs.writeFile(pluginsFilePath, newContent, 'utf8');
}
});
}
if (options.owner) {
await addCodeownersEntry(`/plugins/${id}`, options.owner);
}
await Task.forCommand('yarn install', { cwd: targetDir, optional: true });
await Task.forCommand('yarn lint --fix', {
cwd: targetDir,
optional: true,
});
},
});
@@ -1,25 +0,0 @@
/*
* 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.
*/
export { frontendPlugin } from './frontendPlugin';
export { backendPlugin } from './backendPlugin';
export { backendModule } from './backendModule';
export { nodeLibraryPackage } from './nodeLibraryPackage';
export { webLibraryPackage } from './webLibraryPackage';
export { pluginCommon } from './pluginCommon';
export { pluginNode } from './pluginNode';
export { pluginWeb } from './pluginWeb';
export { scaffolderModule } from './scaffolderModule';
@@ -1,151 +0,0 @@
/*
* Copyright 2022 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 { join as joinPath } from 'path';
import { Task } from '../../tasks';
import { FactoryRegistry } from '../FactoryRegistry';
import {
createMockOutputStream,
expectLogsToMatch,
mockPaths,
} from './common/testUtils';
import { nodeLibraryPackage } from './nodeLibraryPackage';
import { createMockDirectory } from '@backstage/backend-test-utils';
describe('nodeLibraryPackage factory', () => {
const mockDir = createMockDirectory();
beforeEach(() => {
mockPaths({
targetRoot: mockDir.path,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should create a node library package', async () => {
const expectedNodeLibraryPackageName = 'test';
mockDir.setContent({
packages: {},
});
const options = await FactoryRegistry.populateOptions(nodeLibraryPackage, {
id: 'test', // name of node library package
});
let modified = false;
const [output, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await nodeLibraryPackage.create(options, {
private: true,
isMonoRepo: true,
defaultVersion: '1.0.0',
markAsModified: () => {
modified = true;
},
createTemporaryDirectory: () => fs.mkdtemp('test'),
license: 'Apache-2.0',
});
expect(modified).toBe(true);
expectLogsToMatch(output, [
`Creating node-library package ${expectedNodeLibraryPackageName}`,
'Checking Prerequisites:',
`availability ${joinPath('packages', expectedNodeLibraryPackageName)}`,
'creating temp dir',
'Executing Template:',
'templating .eslintrc.js.hbs',
'templating README.md.hbs',
'templating package.json.hbs',
'templating index.ts.hbs',
'copying setupTests.ts',
'Installing:',
`moving ${joinPath('packages', expectedNodeLibraryPackageName)}`,
]);
await expect(
fs.readJson(
mockDir.resolve(
'packages',
expectedNodeLibraryPackageName,
'package.json',
),
),
).resolves.toEqual(
expect.objectContaining({
name: expectedNodeLibraryPackageName,
private: true,
version: '1.0.0',
}),
);
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve('packages', expectedNodeLibraryPackageName),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve('packages', expectedNodeLibraryPackageName),
optional: true,
});
});
it('should create a node library plugin with options and codeowners', async () => {
const expectedNodeLibraryPackageName = 'test';
mockDir.setContent({
CODEOWNERS: '',
packages: {},
});
const options = await FactoryRegistry.populateOptions(nodeLibraryPackage, {
id: 'test',
owner: '@backstage/test-owners',
});
const [, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await nodeLibraryPackage.create(options, {
scope: 'internal',
private: true,
isMonoRepo: false,
defaultVersion: '1.0.0',
markAsModified: () => {},
createTemporaryDirectory: () => fs.mkdtemp('test'),
license: 'Apache-2.0',
});
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve(expectedNodeLibraryPackageName),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve(expectedNodeLibraryPackageName),
optional: true,
});
});
});
@@ -1,78 +0,0 @@
/*
* Copyright 2022 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 { CreateContext, createFactory } from '../types';
import { Task } from '../../tasks';
import { ownerPrompt, pluginIdPrompt } from './common/prompts';
import { executePluginPackageTemplate } from './common/tasks';
import { resolvePackageName } from './common/util';
type Options = {
id: string;
owner?: string;
codeOwnersPath?: string;
};
export const nodeLibraryPackage = createFactory<Options>({
name: 'node-library',
description:
'A new node-library package, exporting shared functionality for backend plugins and modules',
optionsDiscovery: async () => ({
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options: Options, ctx: CreateContext) {
const { id } = options;
const name = resolvePackageName({
baseName: id,
scope: ctx.scope,
plugin: false,
});
Task.log();
Task.log(`Creating node-library package ${chalk.cyan(name)}`);
const targetDir = ctx.isMonoRepo
? paths.resolveTargetRoot('packages', id)
: paths.resolveTargetRoot(`${id}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: 'node-library-package',
values: {
id,
name,
pluginVersion: ctx.defaultVersion,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
license: ctx.license,
},
});
if (options.owner) {
await addCodeownersEntry(`/packages/${id}`, options.owner);
}
await Task.forCommand('yarn install', { cwd: targetDir, optional: true });
await Task.forCommand('yarn lint --fix', {
cwd: targetDir,
optional: true,
});
},
});
@@ -1,106 +0,0 @@
/*
* 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 { sep } from 'path';
import { Task } from '../../tasks';
import { FactoryRegistry } from '../FactoryRegistry';
import {
createMockOutputStream,
expectLogsToMatch,
mockPaths,
} from './common/testUtils';
import { pluginCommon } from './pluginCommon';
import { createMockDirectory } from '@backstage/backend-test-utils';
describe('pluginCommon factory', () => {
const mockDir = createMockDirectory();
beforeEach(() => {
mockPaths({
targetRoot: mockDir.path,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should create a common plugin package', async () => {
mockDir.setContent({
plugins: {},
});
const options = await FactoryRegistry.populateOptions(pluginCommon, {
id: 'test',
});
let modified = false;
const [output, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await pluginCommon.create(options, {
private: true,
isMonoRepo: true,
defaultVersion: '1.0.0',
markAsModified: () => {
modified = true;
},
createTemporaryDirectory: () => fs.mkdtemp('test'),
license: 'Apache-2.0',
});
expect(modified).toBe(true);
expectLogsToMatch(output, [
'Creating common plugin package backstage-plugin-test-common',
'Checking Prerequisites:',
`availability plugins${sep}test-common`,
'creating temp dir',
'Executing Template:',
'templating .eslintrc.js.hbs',
'templating README.md.hbs',
'templating package.json.hbs',
'templating index.ts.hbs',
'copying setupTests.ts',
'Installing:',
`moving plugins${sep}test-common`,
]);
await expect(
fs.readJson(mockDir.resolve('plugins/test-common/package.json')),
).resolves.toEqual(
expect.objectContaining({
name: 'backstage-plugin-test-common',
description: 'Common functionalities for the test plugin',
private: true,
version: '1.0.0',
}),
);
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve('plugins/test-common'),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve('plugins/test-common'),
optional: true,
});
});
});
@@ -1,78 +0,0 @@
/*
* 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 { CreateContext, createFactory } from '../types';
import { Task } from '../../tasks';
import { ownerPrompt, pluginIdPrompt } from './common/prompts';
import { executePluginPackageTemplate } from './common/tasks';
import { resolvePackageName } from './common/util';
type Options = {
id: string;
owner?: string;
codeOwnersPath?: string;
};
export const pluginCommon = createFactory<Options>({
name: 'plugin-common',
description: 'A new isomorphic common plugin package',
optionsDiscovery: async () => ({
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options: Options, ctx: CreateContext) {
const { id } = options;
const suffix = `${id}-common`;
const name = resolvePackageName({
baseName: suffix,
scope: ctx.scope,
plugin: true,
});
Task.log();
Task.log(`Creating common plugin package ${chalk.cyan(name)}`);
const targetDir = ctx.isMonoRepo
? paths.resolveTargetRoot('plugins', suffix)
: paths.resolveTargetRoot(`backstage-plugin-${suffix}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: 'default-common-plugin-package',
values: {
id,
name,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
pluginVersion: ctx.defaultVersion,
license: ctx.license,
},
});
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,
});
},
});
@@ -1,106 +0,0 @@
/*
* 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 { sep } from 'path';
import { Task } from '../../tasks';
import { FactoryRegistry } from '../FactoryRegistry';
import {
createMockOutputStream,
expectLogsToMatch,
mockPaths,
} from './common/testUtils';
import { pluginNode } from './pluginNode';
import { createMockDirectory } from '@backstage/backend-test-utils';
describe('pluginNode factory', () => {
const mockDir = createMockDirectory();
beforeEach(() => {
mockPaths({
targetRoot: mockDir.path,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should create a node plugin package', async () => {
mockDir.setContent({
plugins: {},
});
const options = await FactoryRegistry.populateOptions(pluginNode, {
id: 'test',
});
let modified = false;
const [output, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await pluginNode.create(options, {
private: true,
isMonoRepo: true,
defaultVersion: '1.0.0',
markAsModified: () => {
modified = true;
},
createTemporaryDirectory: () => fs.mkdtemp('test'),
license: 'Apache-2.0',
});
expect(modified).toBe(true);
expectLogsToMatch(output, [
'Creating Node.js plugin library backstage-plugin-test-node',
'Checking Prerequisites:',
`availability plugins${sep}test-node`,
'creating temp dir',
'Executing Template:',
'templating .eslintrc.js.hbs',
'templating README.md.hbs',
'templating package.json.hbs',
'templating index.ts.hbs',
'copying setupTests.ts',
'Installing:',
`moving plugins${sep}test-node`,
]);
await expect(
fs.readJson(mockDir.resolve('plugins/test-node/package.json')),
).resolves.toEqual(
expect.objectContaining({
name: 'backstage-plugin-test-node',
description: 'Node.js library for the test plugin',
private: true,
version: '1.0.0',
}),
);
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve('plugins/test-node'),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve('plugins/test-node'),
optional: true,
});
});
});
@@ -1,78 +0,0 @@
/*
* 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 { CreateContext, createFactory } from '../types';
import { Task } from '../../tasks';
import { ownerPrompt, pluginIdPrompt } from './common/prompts';
import { executePluginPackageTemplate } from './common/tasks';
import { resolvePackageName } from './common/util';
type Options = {
id: string;
owner?: string;
codeOwnersPath?: string;
};
export const pluginNode = createFactory<Options>({
name: 'plugin-node',
description: 'A new Node.js 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}-node`;
const name = resolvePackageName({
baseName: suffix,
scope: ctx.scope,
plugin: true,
});
Task.log();
Task.log(`Creating Node.js plugin library ${chalk.cyan(name)}`);
const targetDir = ctx.isMonoRepo
? paths.resolveTargetRoot('plugins', suffix)
: paths.resolveTargetRoot(`backstage-plugin-${suffix}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: 'default-node-plugin-package',
values: {
id,
name,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
pluginVersion: ctx.defaultVersion,
license: ctx.license,
},
});
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,
});
},
});
@@ -1,113 +0,0 @@
/*
* 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 { sep } from 'path';
import { Task } from '../../tasks';
import { FactoryRegistry } from '../FactoryRegistry';
import {
createMockOutputStream,
expectLogsToMatch,
mockPaths,
} from './common/testUtils';
import { pluginWeb } from './pluginWeb';
import { createMockDirectory } from '@backstage/backend-test-utils';
describe('pluginWeb factory', () => {
const mockDir = createMockDirectory();
beforeEach(() => {
mockPaths({
targetRoot: mockDir.path,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should create a react plugin package', async () => {
mockDir.setContent({
plugins: {},
});
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'),
license: 'Apache-2.0',
});
expect(modified).toBe(true);
expectLogsToMatch(output, [
'Creating web plugin library backstage-plugin-test-react',
'Checking Prerequisites:',
`availability plugins${sep}test-react`,
'creating temp dir',
'Executing Template:',
'templating .eslintrc.js.hbs',
'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(mockDir.resolve('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: mockDir.resolve('plugins/test-react'),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve('plugins/test-react'),
optional: true,
});
});
});
@@ -1,78 +0,0 @@
/*
* 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 { CreateContext, createFactory } from '../types';
import { Task } from '../../tasks';
import { ownerPrompt, pluginIdPrompt } from './common/prompts';
import { executePluginPackageTemplate } from './common/tasks';
import { resolvePackageName } from './common/util';
type Options = {
id: string;
owner?: string;
codeOwnersPath?: string;
};
export const pluginWeb = createFactory<Options>({
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 = resolvePackageName({
baseName: suffix,
scope: ctx.scope,
plugin: true,
});
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,
license: ctx.license,
},
});
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,
});
},
});
@@ -1,147 +0,0 @@
/*
* 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 { sep } from 'path';
import { Task } from '../../tasks';
import { FactoryRegistry } from '../FactoryRegistry';
import {
createMockOutputStream,
expectLogsToMatch,
mockPaths,
} from './common/testUtils';
import { scaffolderModule } from './scaffolderModule';
import { createMockDirectory } from '@backstage/backend-test-utils';
const backendIndexTsContent = `
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.start();
`;
describe('scaffolderModule factory', () => {
const mockDir = createMockDirectory();
beforeEach(() => {
mockPaths({
targetRoot: mockDir.path,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should create a scaffolder backend module package', async () => {
mockDir.setContent({
packages: {
backend: {
'package.json': JSON.stringify({}),
src: {
'index.ts': backendIndexTsContent,
},
},
},
plugins: {},
});
const options = await FactoryRegistry.populateOptions(scaffolderModule, {
id: 'test',
});
let modified = false;
const [output, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await scaffolderModule.create(options, {
private: true,
isMonoRepo: true,
defaultVersion: '1.0.0',
markAsModified: () => {
modified = true;
},
createTemporaryDirectory: (name: string) => fs.mkdtemp(name),
license: 'Apache-2.0',
});
expect(modified).toBe(true);
expectLogsToMatch(output, [
'Creating module backstage-plugin-scaffolder-backend-module-test',
'Checking Prerequisites:',
`availability plugins${sep}scaffolder-backend-module-test`,
'creating temp dir',
'Executing Template:',
'templating .eslintrc.js.hbs',
'templating README.md.hbs',
'templating package.json.hbs',
'templating index.ts.hbs',
'copying example.test.ts',
'copying example.ts',
'copying module.ts',
'Installing:',
`moving plugins${sep}scaffolder-backend-module-test`,
'backend adding dependency',
'backend adding module',
]);
await expect(
fs.readFile(mockDir.resolve('packages/backend/src/index.ts'), 'utf8'),
).resolves.toBe(`
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.add(import('backstage-plugin-scaffolder-backend-module-test'));
backend.start();
`);
await expect(
fs.readJson(mockDir.resolve('packages/backend/package.json')),
).resolves.toEqual({
dependencies: {
'backstage-plugin-scaffolder-backend-module-test': '^1.0.0',
},
});
await expect(
fs.readJson(
mockDir.resolve('plugins/scaffolder-backend-module-test/package.json'),
),
).resolves.toEqual(
expect.objectContaining({
name: 'backstage-plugin-scaffolder-backend-module-test',
description: 'The test module for @backstage/plugin-scaffolder-backend',
private: true,
version: '1.0.0',
}),
);
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve('plugins/scaffolder-backend-module-test'),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve('plugins/scaffolder-backend-module-test'),
optional: true,
});
});
});
@@ -1,113 +0,0 @@
/*
* 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 chalk from 'chalk';
import { paths } from '../../paths';
import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners';
import { CreateContext, createFactory } from '../types';
import { addPackageDependency, addToBackend, Task } from '../../tasks';
import { ownerPrompt } from './common/prompts';
import { executePluginPackageTemplate } from './common/tasks';
import { resolvePackageName } from './common/util';
type Options = {
id: string;
owner?: string;
codeOwnersPath?: string;
};
export const scaffolderModule = createFactory<Options>({
name: 'scaffolder-module',
description:
'An module exporting custom actions for @backstage/plugin-scaffolder-backend',
optionsDiscovery: async () => ({
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
}),
optionsPrompts: [
{
type: 'input',
name: 'id',
message: 'Enter the name of the module [required]',
validate: (value: string) => {
if (!value) {
return 'Please enter the name of the module';
} else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
return 'Module names must be lowercase and contain only letters, digits, and dashes.';
}
return true;
},
},
ownerPrompt(),
],
async create(options: Options, ctx: CreateContext) {
const { id } = options;
const slug = `scaffolder-backend-module-${id}`;
const name = resolvePackageName({
baseName: slug,
scope: ctx.scope,
plugin: true,
});
Task.log();
Task.log(`Creating module ${chalk.cyan(name)}`);
const targetDir = ctx.isMonoRepo
? paths.resolveTargetRoot('plugins', slug)
: paths.resolveTargetRoot(`backstage-plugin-${slug}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: 'scaffolder-module',
values: {
id,
name,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
pluginVersion: ctx.defaultVersion,
license: ctx.license,
},
});
if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) {
await Task.forItem('backend', 'adding dependency', async () => {
await addPackageDependency(
paths.resolveTargetRoot('packages/backend/package.json'),
{
dependencies: {
[name]: `^${ctx.defaultVersion}`,
},
},
);
});
}
await addToBackend(name, {
type: 'module',
});
if (options.owner) {
await addCodeownersEntry(`/plugins/${slug}`, options.owner);
}
await Task.forCommand('yarn install', { cwd: targetDir, optional: true });
await Task.forCommand('yarn lint --fix', {
cwd: targetDir,
optional: true,
});
},
});
@@ -1,151 +0,0 @@
/*
* Copyright 2022 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 { join as joinPath } from 'path';
import { Task } from '../../tasks';
import { FactoryRegistry } from '../FactoryRegistry';
import {
createMockOutputStream,
expectLogsToMatch,
mockPaths,
} from './common/testUtils';
import { webLibraryPackage } from './webLibraryPackage';
import { createMockDirectory } from '@backstage/backend-test-utils';
describe('webLibraryPackage factory', () => {
const mockDir = createMockDirectory();
beforeEach(() => {
mockPaths({
targetRoot: mockDir.path,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should create a web library package', async () => {
const expectedwebLibraryPackageName = 'test';
mockDir.setContent({
packages: {},
});
const options = await FactoryRegistry.populateOptions(webLibraryPackage, {
id: 'test', // name of web library package
});
let modified = false;
const [output, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await webLibraryPackage.create(options, {
private: true,
isMonoRepo: true,
defaultVersion: '1.0.0',
markAsModified: () => {
modified = true;
},
createTemporaryDirectory: () => fs.mkdtemp('test'),
license: 'Apache-2.0',
});
expect(modified).toBe(true);
expectLogsToMatch(output, [
`Creating web-library package ${expectedwebLibraryPackageName}`,
'Checking Prerequisites:',
`availability ${joinPath('packages', expectedwebLibraryPackageName)}`,
'creating temp dir',
'Executing Template:',
'templating .eslintrc.js.hbs',
'templating README.md.hbs',
'templating package.json.hbs',
'templating index.ts.hbs',
'copying setupTests.ts',
'Installing:',
`moving ${joinPath('packages', expectedwebLibraryPackageName)}`,
]);
await expect(
fs.readJson(
mockDir.resolve(
'packages',
expectedwebLibraryPackageName,
'package.json',
),
),
).resolves.toEqual(
expect.objectContaining({
name: expectedwebLibraryPackageName,
private: true,
version: '1.0.0',
}),
);
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve('packages', expectedwebLibraryPackageName),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve('packages', expectedwebLibraryPackageName),
optional: true,
});
});
it('should create a web library plugin with options and codeowners', async () => {
const expectedwebLibraryPackageName = 'test';
mockDir.setContent({
CODEOWNERS: '',
packages: {},
});
const options = await FactoryRegistry.populateOptions(webLibraryPackage, {
id: 'test',
owner: '@backstage/test-owners',
});
const [, mockStream] = createMockOutputStream();
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
jest.spyOn(Task, 'forCommand').mockResolvedValue();
await webLibraryPackage.create(options, {
scope: 'internal',
private: true,
isMonoRepo: false,
defaultVersion: '1.0.0',
markAsModified: () => {},
createTemporaryDirectory: () => fs.mkdtemp('test'),
license: 'Apache-2.0',
});
expect(Task.forCommand).toHaveBeenCalledTimes(2);
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
cwd: mockDir.resolve(expectedwebLibraryPackageName),
optional: true,
});
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
cwd: mockDir.resolve(expectedwebLibraryPackageName),
optional: true,
});
});
});
@@ -1,78 +0,0 @@
/*
* Copyright 2022 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 { CreateContext, createFactory } from '../types';
import { Task } from '../../tasks';
import { ownerPrompt, pluginIdPrompt } from './common/prompts';
import { executePluginPackageTemplate } from './common/tasks';
import { resolvePackageName } from './common/util';
type Options = {
id: string;
owner?: string;
codeOwnersPath?: string;
};
export const webLibraryPackage = createFactory<Options>({
name: 'web-library',
description:
'A new web-library package, exporting shared functionality for frontend plugins',
optionsDiscovery: async () => ({
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options: Options, ctx: CreateContext) {
const { id } = options;
const name = resolvePackageName({
baseName: id,
scope: ctx.scope,
plugin: false,
});
Task.log();
Task.log(`Creating web-library package ${chalk.cyan(name)}`);
const targetDir = ctx.isMonoRepo
? paths.resolveTargetRoot('packages', id)
: paths.resolveTargetRoot(`${id}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: 'web-library-package',
values: {
id,
name,
pluginVersion: ctx.defaultVersion,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
license: ctx.license,
},
});
if (options.owner) {
await addCodeownersEntry(`/packages/${id}`, options.owner);
}
await Task.forCommand('yarn install', { cwd: targetDir, optional: true });
await Task.forCommand('yarn lint --fix', {
cwd: targetDir,
optional: true,
});
},
});