diff --git a/src/cli/task-generate.ts b/src/cli/task-generate.ts index 66373795e89..0f0189a4d55 100644 --- a/src/cli/task-generate.ts +++ b/src/cli/task-generate.ts @@ -43,14 +43,20 @@ export const taskGenerate = async (config: ValidatedConfig): Promise => { config.logger.error(tagError); return config.sys.exit(1); } - const filesToGenerateExt = await chooseFilesToGenerate(); - if (undefined === filesToGenerateExt) { + + let cssExtension: GeneratableStylingExtension = 'css'; + if (!!config.plugins.find((plugin) => plugin.name === 'sass')) { + cssExtension = await chooseSassExtension(); + } else if (!!config.plugins.find((plugin) => plugin.name === 'less')) { + cssExtension = 'less'; + } + const filesToGenerateExt = await chooseFilesToGenerate(cssExtension); + if (!filesToGenerateExt) { // in some shells (e.g. Windows PowerShell), hitting Ctrl+C results in a TypeError printed to the console. // explicitly return here to avoid printing the error message. return; } - const extensionsToGenerate: GenerableExtension[] = ['tsx', ...filesToGenerateExt]; - + const extensionsToGenerate: GeneratableExtension[] = ['tsx', ...filesToGenerateExt]; const testFolder = extensionsToGenerate.some(isTest) ? 'test' : ''; const outDir = join(absoluteSrcDir, 'components', dir, componentName); @@ -64,7 +70,16 @@ export const taskGenerate = async (config: ValidatedConfig): Promise => { const writtenFiles = await Promise.all( filesToGenerate.map((file) => - getBoilerplateAndWriteFile(config, componentName, extensionsToGenerate.includes('css'), file), + getBoilerplateAndWriteFile( + config, + componentName, + extensionsToGenerate.includes('css') || + extensionsToGenerate.includes('sass') || + extensionsToGenerate.includes('scss') || + extensionsToGenerate.includes('less'), + file, + cssExtension, + ), ), ).catch((error) => config.logger.error(error)); @@ -88,10 +103,11 @@ export const taskGenerate = async (config: ValidatedConfig): Promise => { /** * Show a checkbox prompt to select the files to be generated. * - * @returns a read-only array of `GenerableExtension`, the extensions that the user has decided + * @param cssExtension the extension of the CSS file to be generated + * @returns a read-only array of `GeneratableExtension`, the extensions that the user has decided * to generate */ -const chooseFilesToGenerate = async (): Promise> => { +const chooseFilesToGenerate = async (cssExtension: string): Promise> => { const { prompt } = await import('prompts'); return ( await prompt({ @@ -99,7 +115,7 @@ const chooseFilesToGenerate = async (): Promise { + const { prompt } = await import('prompts'); + return ( + await prompt({ + name: 'sassFormat', + type: 'select', + message: + 'Which Sass format would you like to use? (More info: https://sass-lang.com/documentation/syntax/#the-indented-syntax)', + choices: [ + { value: 'sass', title: `*.sass Format`, selected: true }, + { value: 'scss', title: '*.scss Format' }, + ], + }) + ).sassFormat; +}; + /** * Get a filepath for a file we want to generate! * @@ -119,7 +151,7 @@ const chooseFilesToGenerate = async (): Promise +const getFilepathForFile = (filePath: string, componentName: string, extension: GeneratableExtension): string => isTest(extension) ? normalizePath(join(filePath, 'test', `${componentName}.${extension}`)) : normalizePath(join(filePath, `${componentName}.${extension}`)); @@ -131,6 +163,7 @@ const getFilepathForFile = (filePath: string, componentName: string, extension: * @param componentName the component name (user-supplied) * @param withCss are we generating CSS? * @param file the file we want to write + * @param styleExtension extension used for styles * @returns a `Promise` which holds the full filepath we've written to, * used to print out a little summary of our activity to the user. */ @@ -139,8 +172,9 @@ const getBoilerplateAndWriteFile = async ( componentName: string, withCss: boolean, file: BoilerplateFile, + styleExtension: GeneratableStylingExtension, ): Promise => { - const boilerplate = getBoilerplateByExtension(componentName, file.extension, withCss); + const boilerplate = getBoilerplateByExtension(componentName, file.extension, withCss, styleExtension); await config.sys.writeFile(normalizePath(file.path), boilerplate); return file.path; }; @@ -183,7 +217,7 @@ const checkForOverwrite = async (files: readonly BoilerplateFile[], config: Vali * @param extension the extension we want to check * @returns a boolean indicating whether or not its a test */ -const isTest = (extension: GenerableExtension): boolean => { +const isTest = (extension: GeneratableExtension): boolean => { return extension === 'e2e.ts' || extension === 'spec.tsx'; }; @@ -193,15 +227,24 @@ const isTest = (extension: GenerableExtension): boolean => { * @param tagName the name of the component we're generating * @param extension the file extension we want boilerplate for (.css, tsx, etc) * @param withCss a boolean indicating whether we're generating a CSS file + * @param styleExtension extension used for styles * @returns a string container the file boilerplate for the supplied extension */ -export const getBoilerplateByExtension = (tagName: string, extension: GenerableExtension, withCss: boolean): string => { +export const getBoilerplateByExtension = ( + tagName: string, + extension: GeneratableExtension, + withCss: boolean, + styleExtension: GeneratableStylingExtension, +): string => { switch (extension) { case 'tsx': - return getComponentBoilerplate(tagName, withCss); + return getComponentBoilerplate(tagName, withCss, styleExtension); case 'css': - return getStyleUrlBoilerplate(); + case 'less': + case 'sass': + case 'scss': + return getStyleUrlBoilerplate(styleExtension); case 'spec.tsx': return getSpecTestBoilerplate(tagName); @@ -218,13 +261,18 @@ export const getBoilerplateByExtension = (tagName: string, extension: GenerableE * Get the boilerplate for a file containing the definition of a component * @param tagName the name of the tag to give the component * @param hasStyle designates if the component has an external stylesheet or not + * @param styleExtension extension used for styles * @returns the contents of a file that defines a component */ -const getComponentBoilerplate = (tagName: string, hasStyle: boolean): string => { +const getComponentBoilerplate = ( + tagName: string, + hasStyle: boolean, + styleExtension: GeneratableStylingExtension, +): string => { const decorator = [`{`]; decorator.push(` tag: '${tagName}',`); if (hasStyle) { - decorator.push(` styleUrl: '${tagName}.css',`); + decorator.push(` styleUrl: '${tagName}.${styleExtension}',`); } decorator.push(` shadow: true,`); decorator.push(`}`); @@ -233,7 +281,6 @@ const getComponentBoilerplate = (tagName: string, hasStyle: boolean): string => @Component(${decorator.join('\n')}) export class ${toPascalCase(tagName)} { - render() { return ( @@ -241,17 +288,21 @@ export class ${toPascalCase(tagName)} { ); } - } `; }; /** * Get the boilerplate for style for a generated component + * @param ext extension used for styles * @returns a boilerplate CSS block */ -const getStyleUrlBoilerplate = (): string => - `:host { +const getStyleUrlBoilerplate = (ext: GeneratableExtension): string => + ext === 'sass' + ? `:host + display: block +` + : `:host { display: block; } `; @@ -312,14 +363,19 @@ const toPascalCase = (str: string): string => /** * Extensions available to generate. */ -export type GenerableExtension = 'tsx' | 'css' | 'spec.tsx' | 'e2e.ts'; +export type GeneratableExtension = 'tsx' | 'spec.tsx' | 'e2e.ts' | GeneratableStylingExtension; + +/** + * Extensions available to generate. + */ +export type GeneratableStylingExtension = 'css' | 'sass' | 'scss' | 'less'; /** * A little interface to wrap up the info we need to pass around for generating * and writing boilerplate. */ export interface BoilerplateFile { - extension: GenerableExtension; + extension: GeneratableExtension; /** * The full path to the file we want to generate. */ diff --git a/src/cli/test/task-generate.spec.ts b/src/cli/test/task-generate.spec.ts index 68817cd7b9e..e5ba522de2e 100644 --- a/src/cli/test/task-generate.spec.ts +++ b/src/cli/test/task-generate.spec.ts @@ -11,13 +11,16 @@ jest.mock('prompts', () => ({ prompt: promptMock, })); -const setup = async () => { +let formatToPick = 'css'; + +const setup = async (plugins: any[] = []) => { const sys = mockCompilerSystem(); const config: d.ValidatedConfig = mockValidatedConfig({ configPath: '/testing-path', flags: createConfigFlags({ task: 'generate' }), srcDir: '/src', sys, + plugins, }); // set up some mocks / spies @@ -28,9 +31,16 @@ const setup = async () => { // mock prompt usage: tagName and filesToGenerate are the keys used for // different calls, so we can cheat here and just do a single // mockResolvedValue - promptMock.mockResolvedValue({ - tagName: 'my-component', - filesToGenerate: ['css', 'spec.tsx', 'e2e.ts'], + let format = formatToPick; + promptMock.mockImplementation((params) => { + if (params.name === 'sassFormat') { + format = 'sass'; + return { sassFormat: 'sass' }; + } + return { + tagName: 'my-component', + filesToGenerate: [format, 'spec.tsx', 'e2e.ts'], + }; }); return { config, errorSpy, validateTagSpy }; @@ -53,6 +63,7 @@ describe('generate task', () => { jest.restoreAllMocks(); jest.clearAllMocks(); jest.resetModules(); + formatToPick = 'css'; }); afterAll(() => { @@ -117,7 +128,7 @@ describe('generate task', () => { userChoices.forEach((file) => { expect(writeFileSpy).toHaveBeenCalledWith( file.path, - getBoilerplateByExtension('my-component', file.extension, true), + getBoilerplateByExtension('my-component', file.extension, true, 'css'), ); }); }); @@ -135,4 +146,43 @@ describe('generate task', () => { ); expect(config.sys.exit).toHaveBeenCalledWith(1); }); + + it('should generate files for sass projects', async () => { + const { config } = await setup([{ name: 'sass' }]); + const writeFileSpy = jest.spyOn(config.sys, 'writeFile'); + await silentGenerate(config); + const userChoices: ReadonlyArray = [ + { extension: 'tsx', path: '/src/components/my-component/my-component.tsx' }, + { extension: 'sass', path: '/src/components/my-component/my-component.sass' }, + { extension: 'spec.tsx', path: '/src/components/my-component/test/my-component.spec.tsx' }, + { extension: 'e2e.ts', path: '/src/components/my-component/test/my-component.e2e.ts' }, + ]; + + userChoices.forEach((file) => { + expect(writeFileSpy).toHaveBeenCalledWith( + file.path, + getBoilerplateByExtension('my-component', file.extension, true, 'sass'), + ); + }); + }); + + it('should generate files for less projects', async () => { + formatToPick = 'less'; + const { config } = await setup([{ name: 'less' }]); + const writeFileSpy = jest.spyOn(config.sys, 'writeFile'); + await silentGenerate(config); + const userChoices: ReadonlyArray = [ + { extension: 'tsx', path: '/src/components/my-component/my-component.tsx' }, + { extension: 'less', path: '/src/components/my-component/my-component.less' }, + { extension: 'spec.tsx', path: '/src/components/my-component/test/my-component.spec.tsx' }, + { extension: 'e2e.ts', path: '/src/components/my-component/test/my-component.e2e.ts' }, + ]; + + userChoices.forEach((file) => { + expect(writeFileSpy).toHaveBeenCalledWith( + file.path, + getBoilerplateByExtension('my-component', file.extension, true, 'less'), + ); + }); + }); });