Skip to content

Commit

Permalink
feat(cli): support generation of sass and less files (#5857)
Browse files Browse the repository at this point in the history
* feat(cli): support generation of sass and less files

* prettier

* add unit test

* add unit tests

* prettier
  • Loading branch information
christian-bromann authored Jun 26, 2024
1 parent 61bb5e3 commit 1883812
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 27 deletions.
100 changes: 78 additions & 22 deletions src/cli/task-generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,20 @@ export const taskGenerate = async (config: ValidatedConfig): Promise<void> => {
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);
Expand All @@ -64,7 +70,16 @@ export const taskGenerate = async (config: ValidatedConfig): Promise<void> => {

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));

Expand All @@ -88,25 +103,42 @@ export const taskGenerate = async (config: ValidatedConfig): Promise<void> => {
/**
* 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<ReadonlyArray<GenerableExtension>> => {
const chooseFilesToGenerate = async (cssExtension: string): Promise<ReadonlyArray<GeneratableExtension>> => {
const { prompt } = await import('prompts');
return (
await prompt({
name: 'filesToGenerate',
type: 'multiselect',
message: 'Which additional files do you want to generate?',
choices: [
{ value: 'css', title: 'Stylesheet (.css)', selected: true },
{ value: cssExtension, title: `Stylesheet (.${cssExtension})`, selected: true },
{ value: 'spec.tsx', title: 'Spec Test (.spec.tsx)', selected: true },
{ value: 'e2e.ts', title: 'E2E Test (.e2e.ts)', selected: true },
],
})
).filesToGenerate;
};

const chooseSassExtension = async () => {
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!
*
Expand All @@ -119,7 +151,7 @@ const chooseFilesToGenerate = async (): Promise<ReadonlyArray<GenerableExtension
* @returns the full filepath to the component (with a possible `test` directory
* added)
*/
const getFilepathForFile = (filePath: string, componentName: string, extension: GenerableExtension): string =>
const getFilepathForFile = (filePath: string, componentName: string, extension: GeneratableExtension): string =>
isTest(extension)
? normalizePath(join(filePath, 'test', `${componentName}.${extension}`))
: normalizePath(join(filePath, `${componentName}.${extension}`));
Expand All @@ -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<string>` which holds the full filepath we've written to,
* used to print out a little summary of our activity to the user.
*/
Expand All @@ -139,8 +172,9 @@ const getBoilerplateAndWriteFile = async (
componentName: string,
withCss: boolean,
file: BoilerplateFile,
styleExtension: GeneratableStylingExtension,
): Promise<string> => {
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;
};
Expand Down Expand Up @@ -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';
};

Expand All @@ -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);
Expand All @@ -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(`}`);
Expand All @@ -233,25 +281,28 @@ const getComponentBoilerplate = (tagName: string, hasStyle: boolean): string =>
@Component(${decorator.join('\n')})
export class ${toPascalCase(tagName)} {
render() {
return (
<Host>
<slot></slot>
</Host>
);
}
}
`;
};

/**
* 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;
}
`;
Expand Down Expand Up @@ -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.
*/
Expand Down
60 changes: 55 additions & 5 deletions src/cli/test/task-generate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 };
Expand All @@ -53,6 +63,7 @@ describe('generate task', () => {
jest.restoreAllMocks();
jest.clearAllMocks();
jest.resetModules();
formatToPick = 'css';
});

afterAll(() => {
Expand Down Expand Up @@ -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'),
);
});
});
Expand All @@ -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<BoilerplateFile> = [
{ 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<BoilerplateFile> = [
{ 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'),
);
});
});
});

0 comments on commit 1883812

Please sign in to comment.