diff --git a/packages/cli/src/lib/new/loader/NewTemplateLoader.test.ts b/packages/cli/src/lib/new/loader/NewTemplateLoader.test.ts index e63de999a6..dad06a3ccd 100644 --- a/packages/cli/src/lib/new/loader/NewTemplateLoader.test.ts +++ b/packages/cli/src/lib/new/loader/NewTemplateLoader.test.ts @@ -18,6 +18,7 @@ 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 = { @@ -29,6 +30,10 @@ describe('NewTemplateLoader.selectTemplateInteractively', () => { globals: {}, }; + beforeEach(() => { + jest.resetAllMocks(); + }); + it('should select a template interactively', async () => { jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ id: 'template1' }); @@ -84,3 +89,100 @@ describe('NewTemplateLoader.selectTemplateInteractively', () => { }); }); }); + +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', + )}'`, + ); + }); + }); +}); diff --git a/packages/cli/src/lib/new/loader/NewTemplateLoader.ts b/packages/cli/src/lib/new/loader/NewTemplateLoader.ts index c4bb873971..35adfff2fd 100644 --- a/packages/cli/src/lib/new/loader/NewTemplateLoader.ts +++ b/packages/cli/src/lib/new/loader/NewTemplateLoader.ts @@ -17,6 +17,7 @@ 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'; @@ -90,12 +91,15 @@ export class NewTemplateLoader { if (target.match(/https?:\/\//)) { throw new Error('Remote templates are not supported yet'); } - if (!fs.existsSync(paths.resolveTargetRoot(target))) { - throw new Error(`Your CLI template does not exist: ${target}`); - } - const rawTemplate = parseYaml( - fs.readFileSync(paths.resolveTargetRoot(target), 'utf-8'), - ); + 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) { @@ -105,17 +109,14 @@ export class NewTemplateLoader { ); } - const template = parsed.data; + const { template, ...templateData } = parsed.data; - const templatePath = paths.resolveTargetRoot( - dirname(target), - template.template, - ); + const templatePath = resolvePath(dirname(target), template); if (!fs.existsSync(templatePath)) { throw new Error( - `Your CLI template skeleton does not exist: ${templatePath}`, + `Failed to load template contents from '${templatePath}'`, ); } - return { id, templatePath, ...template }; + return { id, templatePath, ...templateData }; } } diff --git a/packages/cli/src/lib/new/templateSelector.test.ts b/packages/cli/src/lib/new/templateSelector.test.ts deleted file mode 100644 index d8adc074ec..0000000000 --- a/packages/cli/src/lib/new/templateSelector.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2024 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 { readCliConfig, verifyTemplate } from './templateSelector'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -describe('verifyTemplate', () => { - it('throws an error if template target is a remote URL', () => { - expect(() => verifyTemplate({ id: '', target: 'http' })).toThrow( - 'Remote templates are not supported yet', - ); - }); - - it('throws an error if template yaml file does not exist', () => { - expect(() => verifyTemplate({ id: '', target: '/foo' })).toThrow( - 'Your CLI template does not exist: /foo', - ); - }); - - it('throws an error if the skeleton of the template does not exist', () => { - const mockDir = createMockDirectory({ - content: { 'template.yaml': 'template: "foo"' }, - }); - expect(() => - verifyTemplate({ id: '', target: mockDir.resolve('template.yaml') }), - ).toThrow(); - }); - - it('throws an error if template is missing a targetPath', () => { - const mockDir = createMockDirectory({ - content: { 'template.yaml': 'template: "foo"', foo: 'bar' }, - }); - expect(() => - verifyTemplate({ id: '', target: mockDir.resolve('template.yaml') }), - ).toThrow(); - }); -});