Skip to content

feat(cli): support generation of sass and less files #5857

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'),
);
});
});
});