cli/new: merge + refactor preparation step
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -15,9 +15,10 @@
|
||||
*/
|
||||
|
||||
import { collectTemplateParams } from './collection/collectTemplateParams';
|
||||
import { loadNewConfig } from './config/loadNewConfig';
|
||||
import { loadConfig } from './preparation/loadConfig';
|
||||
import { executeNewTemplate } from './execution/executeTemplate';
|
||||
import { NewTemplateLoader } from './loader/NewTemplateLoader';
|
||||
import { selectTemplateInteractively } from './preparation/selectTemplateInteractively';
|
||||
import { loadTemplate } from './preparation/loadTemplate';
|
||||
|
||||
export type CreateNewPackageOptions = {
|
||||
preselectedTemplateId?: string;
|
||||
@@ -32,13 +33,13 @@ export type CreateNewPackageOptions = {
|
||||
};
|
||||
|
||||
export async function createNewPackage(options: CreateNewPackageOptions) {
|
||||
const config = await loadNewConfig();
|
||||
const config = await loadConfig();
|
||||
|
||||
const selectedTemplate = await NewTemplateLoader.selectTemplateInteractively(
|
||||
const selectedTemplate = await selectTemplateInteractively(
|
||||
config,
|
||||
options.preselectedTemplateId,
|
||||
);
|
||||
const template = await NewTemplateLoader.loadTemplate(selectedTemplate);
|
||||
const template = await loadTemplate(selectedTemplate);
|
||||
|
||||
const params = await collectTemplateParams({
|
||||
config,
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 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 { NewTemplateLoader } from './NewTemplateLoader';
|
||||
import { NewConfig } from '../config/types';
|
||||
import inquirer from 'inquirer';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('NewTemplateLoader.selectTemplateInteractively', () => {
|
||||
const mockConfig: NewConfig = {
|
||||
isUsingDefaultTemplates: false,
|
||||
templatePointers: [
|
||||
{ id: 'template1', target: 'path/to/template1' },
|
||||
{ id: 'template2', target: 'path/to/template2' },
|
||||
],
|
||||
globals: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should select a template interactively', async () => {
|
||||
jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ id: 'template1' });
|
||||
|
||||
const result = await NewTemplateLoader.selectTemplateInteractively(
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ id: 'template1', target: 'path/to/template1' });
|
||||
});
|
||||
|
||||
it('should error if interactive selections is not found', async () => {
|
||||
jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ id: 'nonexistent' });
|
||||
|
||||
await expect(
|
||||
NewTemplateLoader.selectTemplateInteractively(mockConfig),
|
||||
).rejects.toThrow("Template 'nonexistent' not found");
|
||||
});
|
||||
|
||||
it('should use preselected template id', async () => {
|
||||
const result = await NewTemplateLoader.selectTemplateInteractively(
|
||||
mockConfig,
|
||||
'template2',
|
||||
);
|
||||
|
||||
expect(result).toEqual({ id: 'template2', target: 'path/to/template2' });
|
||||
});
|
||||
|
||||
it('should throw an error if template is not found', async () => {
|
||||
await expect(
|
||||
NewTemplateLoader.selectTemplateInteractively(mockConfig, 'nonexistent'),
|
||||
).rejects.toThrow("Template 'nonexistent' not found");
|
||||
});
|
||||
|
||||
it('should rewrite plugin to frontend-plugin if default templates are used', async () => {
|
||||
await expect(
|
||||
NewTemplateLoader.selectTemplateInteractively(mockConfig, 'plugin'),
|
||||
).rejects.toThrow("Template 'plugin' not found");
|
||||
|
||||
const logs = await withLogCollector(async () => {
|
||||
await expect(
|
||||
NewTemplateLoader.selectTemplateInteractively(
|
||||
{ ...mockConfig, isUsingDefaultTemplates: true },
|
||||
'plugin',
|
||||
),
|
||||
).rejects.toThrow("Template 'frontend-plugin' not found");
|
||||
});
|
||||
expect(logs).toEqual({
|
||||
log: [],
|
||||
warn: [
|
||||
"DEPRECATION WARNING: The 'plugin' template is deprecated, use 'frontend-plugin' instead",
|
||||
],
|
||||
error: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('NewTemplateLoader.loadTemplate', () => {
|
||||
describe('NewTemplateLoader.loadTemplate', () => {
|
||||
it('should load a valid template', async () => {
|
||||
const mockDir = createMockDirectory({
|
||||
content: {
|
||||
'path/to/template1.yaml': `
|
||||
template: template1
|
||||
targetPath: plugins
|
||||
`,
|
||||
'path/to/template1/hello.txt': 'hello world',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await NewTemplateLoader.loadTemplate({
|
||||
id: 'template1',
|
||||
target: mockDir.resolve('path/to/template1.yaml'),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'template1',
|
||||
templatePath: mockDir.resolve('path/to/template1'),
|
||||
targetPath: 'plugins',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if template file does not exist', async () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
await expect(
|
||||
NewTemplateLoader.loadTemplate({
|
||||
id: 'template1',
|
||||
target: mockDir.resolve('path/to/template1.yaml'),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/^Failed to load template definition from '.*'; caused by Error: ENOENT/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if template definition is invalid', async () => {
|
||||
const mockDir = createMockDirectory({
|
||||
content: {
|
||||
'path/to/template1.yaml': `invalid: definition`,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
NewTemplateLoader.loadTemplate({
|
||||
id: 'template1',
|
||||
target: mockDir.resolve('path/to/template1.yaml'),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/Invalid template definition at '.*'; caused by Validation error/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if target is a remote URL', async () => {
|
||||
await expect(
|
||||
NewTemplateLoader.loadTemplate({
|
||||
id: 'template1',
|
||||
target: 'http://example.com',
|
||||
}),
|
||||
).rejects.toThrow('Remote templates are not supported yet');
|
||||
});
|
||||
|
||||
it('should throw an error if target directory does not exist', async () => {
|
||||
await expect(
|
||||
NewTemplateLoader.loadTemplate({
|
||||
id: 'template1',
|
||||
target: 'http://example.com',
|
||||
}),
|
||||
).rejects.toThrow('Remote templates are not supported yet');
|
||||
});
|
||||
|
||||
it('should throw an error if template directory does not exist', async () => {
|
||||
const mockDir = createMockDirectory({
|
||||
content: {
|
||||
'path/to/template1.yaml': `
|
||||
template: template1
|
||||
targetPath: plugins
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
NewTemplateLoader.loadTemplate({
|
||||
id: 'template1',
|
||||
target: mockDir.resolve('path/to/template1.yaml'),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`Failed to load template contents from '${mockDir.resolve(
|
||||
'path/to/template1',
|
||||
)}'`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 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 { z } from 'zod';
|
||||
import fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { dirname } from 'node:path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { paths } from '../../paths';
|
||||
import { NewConfig, NewTemplatePointer } from '../types';
|
||||
import { NewTemplate } from '../types';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { fromZodError } from 'zod-validation-error';
|
||||
|
||||
const templateDefinitionSchema = z
|
||||
.object({
|
||||
description: z.string().optional(),
|
||||
template: z.string(),
|
||||
targetPath: z.string(),
|
||||
plugin: z.boolean().optional(),
|
||||
backendModulePrefix: z.boolean().optional(),
|
||||
suffix: z.string().optional(),
|
||||
prompts: z
|
||||
.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
prompt: z.string(),
|
||||
validate: z.string().optional(),
|
||||
default: z.union([z.string(), z.boolean(), z.number()]).optional(),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.optional(),
|
||||
additionalActions: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export class NewTemplateLoader {
|
||||
static async selectTemplateInteractively(
|
||||
config: NewConfig,
|
||||
preselectedTemplateId?: string,
|
||||
): Promise<NewTemplatePointer> {
|
||||
let selectedId = preselectedTemplateId;
|
||||
|
||||
if (config.isUsingDefaultTemplates && selectedId === 'plugin') {
|
||||
console.warn(
|
||||
`DEPRECATION WARNING: The 'plugin' template is deprecated, use 'frontend-plugin' instead`,
|
||||
);
|
||||
selectedId = 'frontend-plugin';
|
||||
}
|
||||
|
||||
if (!selectedId) {
|
||||
const answers = await inquirer.prompt<{ id: string }>([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'id',
|
||||
message: 'What do you want to create?',
|
||||
choices: config.templatePointers.map(t => t.id),
|
||||
},
|
||||
]);
|
||||
selectedId = answers.id;
|
||||
}
|
||||
|
||||
const template = config.templatePointers.find(t => t.id === selectedId);
|
||||
if (!template) {
|
||||
throw new Error(`Template '${selectedId}' not found`);
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
static async loadTemplate({
|
||||
id,
|
||||
target,
|
||||
}: NewTemplatePointer): Promise<NewTemplate> {
|
||||
if (target.match(/https?:\/\//)) {
|
||||
throw new Error('Remote templates are not supported yet');
|
||||
}
|
||||
const templateContent = await fs
|
||||
.readFile(paths.resolveTargetRoot(target), 'utf-8')
|
||||
.catch(error => {
|
||||
throw new ForwardedError(
|
||||
`Failed to load template definition from '${target}'`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
const rawTemplate = parseYaml(templateContent);
|
||||
|
||||
const parsed = templateDefinitionSchema.safeParse(rawTemplate);
|
||||
if (!parsed.success) {
|
||||
throw new ForwardedError(
|
||||
`Invalid template definition at '${target}'`,
|
||||
fromZodError(parsed.error),
|
||||
);
|
||||
}
|
||||
|
||||
const { template, ...templateData } = parsed.data;
|
||||
|
||||
const templatePath = resolvePath(dirname(target), template);
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
throw new Error(
|
||||
`Failed to load template contents from '${templatePath}'`,
|
||||
);
|
||||
}
|
||||
return { id, templatePath, ...templateData };
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -47,7 +47,7 @@ const pkgJsonWithNewConfigSchema = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export async function loadNewConfig(): Promise<NewConfig> {
|
||||
export async function loadConfig(): Promise<NewConfig> {
|
||||
const pkgPath = paths.resolveTargetRoot('package.json');
|
||||
const pkgJson = await fs.readJson(pkgPath);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2025 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 { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { loadTemplate } from './loadTemplate';
|
||||
|
||||
describe('loadTemplate', () => {
|
||||
it('should load a valid template', async () => {
|
||||
const mockDir = createMockDirectory({
|
||||
content: {
|
||||
'path/to/template1.yaml': `
|
||||
template: template1
|
||||
targetPath: plugins
|
||||
`,
|
||||
'path/to/template1/hello.txt': 'hello world',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await loadTemplate({
|
||||
id: 'template1',
|
||||
target: mockDir.resolve('path/to/template1.yaml'),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'template1',
|
||||
templatePath: mockDir.resolve('path/to/template1'),
|
||||
targetPath: 'plugins',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if template file does not exist', async () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
await expect(
|
||||
loadTemplate({
|
||||
id: 'template1',
|
||||
target: mockDir.resolve('path/to/template1.yaml'),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/^Failed to load template definition from '.*'; caused by Error: ENOENT/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if template definition is invalid', async () => {
|
||||
const mockDir = createMockDirectory({
|
||||
content: {
|
||||
'path/to/template1.yaml': `invalid: definition`,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
loadTemplate({
|
||||
id: 'template1',
|
||||
target: mockDir.resolve('path/to/template1.yaml'),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/Invalid template definition at '.*'; caused by Validation error/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if target is a remote URL', async () => {
|
||||
await expect(
|
||||
loadTemplate({
|
||||
id: 'template1',
|
||||
target: 'http://example.com',
|
||||
}),
|
||||
).rejects.toThrow('Remote templates are not supported yet');
|
||||
});
|
||||
|
||||
it('should throw an error if target directory does not exist', async () => {
|
||||
await expect(
|
||||
loadTemplate({
|
||||
id: 'template1',
|
||||
target: 'http://example.com',
|
||||
}),
|
||||
).rejects.toThrow('Remote templates are not supported yet');
|
||||
});
|
||||
|
||||
it('should throw an error if template directory does not exist', async () => {
|
||||
const mockDir = createMockDirectory({
|
||||
content: {
|
||||
'path/to/template1.yaml': `
|
||||
template: template1
|
||||
targetPath: plugins
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
loadTemplate({
|
||||
id: 'template1',
|
||||
target: mockDir.resolve('path/to/template1.yaml'),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`Failed to load template contents from '${mockDir.resolve(
|
||||
'path/to/template1',
|
||||
)}'`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2025 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 { z } from 'zod';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { dirname } from 'node:path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { paths } from '../../paths';
|
||||
import { NewTemplatePointer } from '../types';
|
||||
import { NewTemplate } from '../types';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { fromZodError } from 'zod-validation-error';
|
||||
|
||||
const templateDefinitionSchema = z
|
||||
.object({
|
||||
description: z.string().optional(),
|
||||
template: z.string(),
|
||||
targetPath: z.string(),
|
||||
plugin: z.boolean().optional(),
|
||||
backendModulePrefix: z.boolean().optional(),
|
||||
suffix: z.string().optional(),
|
||||
prompts: z
|
||||
.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
prompt: z.string(),
|
||||
validate: z.string().optional(),
|
||||
default: z.union([z.string(), z.boolean(), z.number()]).optional(),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.optional(),
|
||||
additionalActions: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function loadTemplate({
|
||||
id,
|
||||
target,
|
||||
}: NewTemplatePointer): Promise<NewTemplate> {
|
||||
if (target.match(/https?:\/\//)) {
|
||||
throw new Error('Remote templates are not supported yet');
|
||||
}
|
||||
const templateContent = await fs
|
||||
.readFile(paths.resolveTargetRoot(target), 'utf-8')
|
||||
.catch(error => {
|
||||
throw new ForwardedError(
|
||||
`Failed to load template definition from '${target}'`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
const rawTemplate = parseYaml(templateContent);
|
||||
|
||||
const parsed = templateDefinitionSchema.safeParse(rawTemplate);
|
||||
if (!parsed.success) {
|
||||
throw new ForwardedError(
|
||||
`Invalid template definition at '${target}'`,
|
||||
fromZodError(parsed.error),
|
||||
);
|
||||
}
|
||||
|
||||
const { template, ...templateData } = parsed.data;
|
||||
|
||||
const templatePath = resolvePath(dirname(target), template);
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
throw new Error(`Failed to load template contents from '${templatePath}'`);
|
||||
}
|
||||
return { id, templatePath, ...templateData };
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2025 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 { NewConfig } from '../types';
|
||||
import inquirer from 'inquirer';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
import { selectTemplateInteractively } from './selectTemplateInteractively';
|
||||
|
||||
describe('selectTemplateInteractively', () => {
|
||||
const mockConfig: NewConfig = {
|
||||
isUsingDefaultTemplates: false,
|
||||
templatePointers: [
|
||||
{ id: 'template1', target: 'path/to/template1' },
|
||||
{ id: 'template2', target: 'path/to/template2' },
|
||||
],
|
||||
globals: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should select a template interactively', async () => {
|
||||
jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ id: 'template1' });
|
||||
|
||||
const result = await selectTemplateInteractively(mockConfig);
|
||||
|
||||
expect(result).toEqual({ id: 'template1', target: 'path/to/template1' });
|
||||
});
|
||||
|
||||
it('should error if interactive selections is not found', async () => {
|
||||
jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ id: 'nonexistent' });
|
||||
|
||||
await expect(selectTemplateInteractively(mockConfig)).rejects.toThrow(
|
||||
"Template 'nonexistent' not found",
|
||||
);
|
||||
});
|
||||
|
||||
it('should use preselected template id', async () => {
|
||||
const result = await selectTemplateInteractively(mockConfig, 'template2');
|
||||
|
||||
expect(result).toEqual({ id: 'template2', target: 'path/to/template2' });
|
||||
});
|
||||
|
||||
it('should throw an error if template is not found', async () => {
|
||||
await expect(
|
||||
selectTemplateInteractively(mockConfig, 'nonexistent'),
|
||||
).rejects.toThrow("Template 'nonexistent' not found");
|
||||
});
|
||||
|
||||
it('should rewrite plugin to frontend-plugin if default templates are used', async () => {
|
||||
await expect(
|
||||
selectTemplateInteractively(mockConfig, 'plugin'),
|
||||
).rejects.toThrow("Template 'plugin' not found");
|
||||
|
||||
const logs = await withLogCollector(async () => {
|
||||
await expect(
|
||||
selectTemplateInteractively(
|
||||
{ ...mockConfig, isUsingDefaultTemplates: true },
|
||||
'plugin',
|
||||
),
|
||||
).rejects.toThrow("Template 'frontend-plugin' not found");
|
||||
});
|
||||
expect(logs).toEqual({
|
||||
log: [],
|
||||
warn: [
|
||||
"DEPRECATION WARNING: The 'plugin' template is deprecated, use 'frontend-plugin' instead",
|
||||
],
|
||||
error: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 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 inquirer from 'inquirer';
|
||||
import { NewConfig, NewTemplatePointer } from '../types';
|
||||
|
||||
export async function selectTemplateInteractively(
|
||||
config: NewConfig,
|
||||
preselectedTemplateId?: string,
|
||||
): Promise<NewTemplatePointer> {
|
||||
let selectedId = preselectedTemplateId;
|
||||
|
||||
if (config.isUsingDefaultTemplates && selectedId === 'plugin') {
|
||||
console.warn(
|
||||
`DEPRECATION WARNING: The 'plugin' template is deprecated, use 'frontend-plugin' instead`,
|
||||
);
|
||||
selectedId = 'frontend-plugin';
|
||||
}
|
||||
|
||||
if (!selectedId) {
|
||||
const answers = await inquirer.prompt<{ id: string }>([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'id',
|
||||
message: 'What do you want to create?',
|
||||
choices: config.templatePointers.map(t => t.id),
|
||||
},
|
||||
]);
|
||||
selectedId = answers.id;
|
||||
}
|
||||
|
||||
const template = config.templatePointers.find(t => t.id === selectedId);
|
||||
if (!template) {
|
||||
throw new Error(`Template '${selectedId}' not found`);
|
||||
}
|
||||
return template;
|
||||
}
|
||||
Reference in New Issue
Block a user