Skip to content

Commit 1883812

Browse files
feat(cli): support generation of sass and less files (#5857)
* feat(cli): support generation of sass and less files * prettier * add unit test * add unit tests * prettier
1 parent 61bb5e3 commit 1883812

File tree

2 files changed

+133
-27
lines changed

2 files changed

+133
-27
lines changed

src/cli/task-generate.ts

+78-22
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,20 @@ export const taskGenerate = async (config: ValidatedConfig): Promise<void> => {
4343
config.logger.error(tagError);
4444
return config.sys.exit(1);
4545
}
46-
const filesToGenerateExt = await chooseFilesToGenerate();
47-
if (undefined === filesToGenerateExt) {
46+
47+
let cssExtension: GeneratableStylingExtension = 'css';
48+
if (!!config.plugins.find((plugin) => plugin.name === 'sass')) {
49+
cssExtension = await chooseSassExtension();
50+
} else if (!!config.plugins.find((plugin) => plugin.name === 'less')) {
51+
cssExtension = 'less';
52+
}
53+
const filesToGenerateExt = await chooseFilesToGenerate(cssExtension);
54+
if (!filesToGenerateExt) {
4855
// in some shells (e.g. Windows PowerShell), hitting Ctrl+C results in a TypeError printed to the console.
4956
// explicitly return here to avoid printing the error message.
5057
return;
5158
}
52-
const extensionsToGenerate: GenerableExtension[] = ['tsx', ...filesToGenerateExt];
53-
59+
const extensionsToGenerate: GeneratableExtension[] = ['tsx', ...filesToGenerateExt];
5460
const testFolder = extensionsToGenerate.some(isTest) ? 'test' : '';
5561

5662
const outDir = join(absoluteSrcDir, 'components', dir, componentName);
@@ -64,7 +70,16 @@ export const taskGenerate = async (config: ValidatedConfig): Promise<void> => {
6470

6571
const writtenFiles = await Promise.all(
6672
filesToGenerate.map((file) =>
67-
getBoilerplateAndWriteFile(config, componentName, extensionsToGenerate.includes('css'), file),
73+
getBoilerplateAndWriteFile(
74+
config,
75+
componentName,
76+
extensionsToGenerate.includes('css') ||
77+
extensionsToGenerate.includes('sass') ||
78+
extensionsToGenerate.includes('scss') ||
79+
extensionsToGenerate.includes('less'),
80+
file,
81+
cssExtension,
82+
),
6883
),
6984
).catch((error) => config.logger.error(error));
7085

@@ -88,25 +103,42 @@ export const taskGenerate = async (config: ValidatedConfig): Promise<void> => {
88103
/**
89104
* Show a checkbox prompt to select the files to be generated.
90105
*
91-
* @returns a read-only array of `GenerableExtension`, the extensions that the user has decided
106+
* @param cssExtension the extension of the CSS file to be generated
107+
* @returns a read-only array of `GeneratableExtension`, the extensions that the user has decided
92108
* to generate
93109
*/
94-
const chooseFilesToGenerate = async (): Promise<ReadonlyArray<GenerableExtension>> => {
110+
const chooseFilesToGenerate = async (cssExtension: string): Promise<ReadonlyArray<GeneratableExtension>> => {
95111
const { prompt } = await import('prompts');
96112
return (
97113
await prompt({
98114
name: 'filesToGenerate',
99115
type: 'multiselect',
100116
message: 'Which additional files do you want to generate?',
101117
choices: [
102-
{ value: 'css', title: 'Stylesheet (.css)', selected: true },
118+
{ value: cssExtension, title: `Stylesheet (.${cssExtension})`, selected: true },
103119
{ value: 'spec.tsx', title: 'Spec Test (.spec.tsx)', selected: true },
104120
{ value: 'e2e.ts', title: 'E2E Test (.e2e.ts)', selected: true },
105121
],
106122
})
107123
).filesToGenerate;
108124
};
109125

126+
const chooseSassExtension = async () => {
127+
const { prompt } = await import('prompts');
128+
return (
129+
await prompt({
130+
name: 'sassFormat',
131+
type: 'select',
132+
message:
133+
'Which Sass format would you like to use? (More info: https://sass-lang.com/documentation/syntax/#the-indented-syntax)',
134+
choices: [
135+
{ value: 'sass', title: `*.sass Format`, selected: true },
136+
{ value: 'scss', title: '*.scss Format' },
137+
],
138+
})
139+
).sassFormat;
140+
};
141+
110142
/**
111143
* Get a filepath for a file we want to generate!
112144
*
@@ -119,7 +151,7 @@ const chooseFilesToGenerate = async (): Promise<ReadonlyArray<GenerableExtension
119151
* @returns the full filepath to the component (with a possible `test` directory
120152
* added)
121153
*/
122-
const getFilepathForFile = (filePath: string, componentName: string, extension: GenerableExtension): string =>
154+
const getFilepathForFile = (filePath: string, componentName: string, extension: GeneratableExtension): string =>
123155
isTest(extension)
124156
? normalizePath(join(filePath, 'test', `${componentName}.${extension}`))
125157
: normalizePath(join(filePath, `${componentName}.${extension}`));
@@ -131,6 +163,7 @@ const getFilepathForFile = (filePath: string, componentName: string, extension:
131163
* @param componentName the component name (user-supplied)
132164
* @param withCss are we generating CSS?
133165
* @param file the file we want to write
166+
* @param styleExtension extension used for styles
134167
* @returns a `Promise<string>` which holds the full filepath we've written to,
135168
* used to print out a little summary of our activity to the user.
136169
*/
@@ -139,8 +172,9 @@ const getBoilerplateAndWriteFile = async (
139172
componentName: string,
140173
withCss: boolean,
141174
file: BoilerplateFile,
175+
styleExtension: GeneratableStylingExtension,
142176
): Promise<string> => {
143-
const boilerplate = getBoilerplateByExtension(componentName, file.extension, withCss);
177+
const boilerplate = getBoilerplateByExtension(componentName, file.extension, withCss, styleExtension);
144178
await config.sys.writeFile(normalizePath(file.path), boilerplate);
145179
return file.path;
146180
};
@@ -183,7 +217,7 @@ const checkForOverwrite = async (files: readonly BoilerplateFile[], config: Vali
183217
* @param extension the extension we want to check
184218
* @returns a boolean indicating whether or not its a test
185219
*/
186-
const isTest = (extension: GenerableExtension): boolean => {
220+
const isTest = (extension: GeneratableExtension): boolean => {
187221
return extension === 'e2e.ts' || extension === 'spec.tsx';
188222
};
189223

@@ -193,15 +227,24 @@ const isTest = (extension: GenerableExtension): boolean => {
193227
* @param tagName the name of the component we're generating
194228
* @param extension the file extension we want boilerplate for (.css, tsx, etc)
195229
* @param withCss a boolean indicating whether we're generating a CSS file
230+
* @param styleExtension extension used for styles
196231
* @returns a string container the file boilerplate for the supplied extension
197232
*/
198-
export const getBoilerplateByExtension = (tagName: string, extension: GenerableExtension, withCss: boolean): string => {
233+
export const getBoilerplateByExtension = (
234+
tagName: string,
235+
extension: GeneratableExtension,
236+
withCss: boolean,
237+
styleExtension: GeneratableStylingExtension,
238+
): string => {
199239
switch (extension) {
200240
case 'tsx':
201-
return getComponentBoilerplate(tagName, withCss);
241+
return getComponentBoilerplate(tagName, withCss, styleExtension);
202242

203243
case 'css':
204-
return getStyleUrlBoilerplate();
244+
case 'less':
245+
case 'sass':
246+
case 'scss':
247+
return getStyleUrlBoilerplate(styleExtension);
205248

206249
case 'spec.tsx':
207250
return getSpecTestBoilerplate(tagName);
@@ -218,13 +261,18 @@ export const getBoilerplateByExtension = (tagName: string, extension: GenerableE
218261
* Get the boilerplate for a file containing the definition of a component
219262
* @param tagName the name of the tag to give the component
220263
* @param hasStyle designates if the component has an external stylesheet or not
264+
* @param styleExtension extension used for styles
221265
* @returns the contents of a file that defines a component
222266
*/
223-
const getComponentBoilerplate = (tagName: string, hasStyle: boolean): string => {
267+
const getComponentBoilerplate = (
268+
tagName: string,
269+
hasStyle: boolean,
270+
styleExtension: GeneratableStylingExtension,
271+
): string => {
224272
const decorator = [`{`];
225273
decorator.push(` tag: '${tagName}',`);
226274
if (hasStyle) {
227-
decorator.push(` styleUrl: '${tagName}.css',`);
275+
decorator.push(` styleUrl: '${tagName}.${styleExtension}',`);
228276
}
229277
decorator.push(` shadow: true,`);
230278
decorator.push(`}`);
@@ -233,25 +281,28 @@ const getComponentBoilerplate = (tagName: string, hasStyle: boolean): string =>
233281
234282
@Component(${decorator.join('\n')})
235283
export class ${toPascalCase(tagName)} {
236-
237284
render() {
238285
return (
239286
<Host>
240287
<slot></slot>
241288
</Host>
242289
);
243290
}
244-
245291
}
246292
`;
247293
};
248294

249295
/**
250296
* Get the boilerplate for style for a generated component
297+
* @param ext extension used for styles
251298
* @returns a boilerplate CSS block
252299
*/
253-
const getStyleUrlBoilerplate = (): string =>
254-
`:host {
300+
const getStyleUrlBoilerplate = (ext: GeneratableExtension): string =>
301+
ext === 'sass'
302+
? `:host
303+
display: block
304+
`
305+
: `:host {
255306
display: block;
256307
}
257308
`;
@@ -312,14 +363,19 @@ const toPascalCase = (str: string): string =>
312363
/**
313364
* Extensions available to generate.
314365
*/
315-
export type GenerableExtension = 'tsx' | 'css' | 'spec.tsx' | 'e2e.ts';
366+
export type GeneratableExtension = 'tsx' | 'spec.tsx' | 'e2e.ts' | GeneratableStylingExtension;
367+
368+
/**
369+
* Extensions available to generate.
370+
*/
371+
export type GeneratableStylingExtension = 'css' | 'sass' | 'scss' | 'less';
316372

317373
/**
318374
* A little interface to wrap up the info we need to pass around for generating
319375
* and writing boilerplate.
320376
*/
321377
export interface BoilerplateFile {
322-
extension: GenerableExtension;
378+
extension: GeneratableExtension;
323379
/**
324380
* The full path to the file we want to generate.
325381
*/

src/cli/test/task-generate.spec.ts

+55-5
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ jest.mock('prompts', () => ({
1111
prompt: promptMock,
1212
}));
1313

14-
const setup = async () => {
14+
let formatToPick = 'css';
15+
16+
const setup = async (plugins: any[] = []) => {
1517
const sys = mockCompilerSystem();
1618
const config: d.ValidatedConfig = mockValidatedConfig({
1719
configPath: '/testing-path',
1820
flags: createConfigFlags({ task: 'generate' }),
1921
srcDir: '/src',
2022
sys,
23+
plugins,
2124
});
2225

2326
// set up some mocks / spies
@@ -28,9 +31,16 @@ const setup = async () => {
2831
// mock prompt usage: tagName and filesToGenerate are the keys used for
2932
// different calls, so we can cheat here and just do a single
3033
// mockResolvedValue
31-
promptMock.mockResolvedValue({
32-
tagName: 'my-component',
33-
filesToGenerate: ['css', 'spec.tsx', 'e2e.ts'],
34+
let format = formatToPick;
35+
promptMock.mockImplementation((params) => {
36+
if (params.name === 'sassFormat') {
37+
format = 'sass';
38+
return { sassFormat: 'sass' };
39+
}
40+
return {
41+
tagName: 'my-component',
42+
filesToGenerate: [format, 'spec.tsx', 'e2e.ts'],
43+
};
3444
});
3545

3646
return { config, errorSpy, validateTagSpy };
@@ -53,6 +63,7 @@ describe('generate task', () => {
5363
jest.restoreAllMocks();
5464
jest.clearAllMocks();
5565
jest.resetModules();
66+
formatToPick = 'css';
5667
});
5768

5869
afterAll(() => {
@@ -117,7 +128,7 @@ describe('generate task', () => {
117128
userChoices.forEach((file) => {
118129
expect(writeFileSpy).toHaveBeenCalledWith(
119130
file.path,
120-
getBoilerplateByExtension('my-component', file.extension, true),
131+
getBoilerplateByExtension('my-component', file.extension, true, 'css'),
121132
);
122133
});
123134
});
@@ -135,4 +146,43 @@ describe('generate task', () => {
135146
);
136147
expect(config.sys.exit).toHaveBeenCalledWith(1);
137148
});
149+
150+
it('should generate files for sass projects', async () => {
151+
const { config } = await setup([{ name: 'sass' }]);
152+
const writeFileSpy = jest.spyOn(config.sys, 'writeFile');
153+
await silentGenerate(config);
154+
const userChoices: ReadonlyArray<BoilerplateFile> = [
155+
{ extension: 'tsx', path: '/src/components/my-component/my-component.tsx' },
156+
{ extension: 'sass', path: '/src/components/my-component/my-component.sass' },
157+
{ extension: 'spec.tsx', path: '/src/components/my-component/test/my-component.spec.tsx' },
158+
{ extension: 'e2e.ts', path: '/src/components/my-component/test/my-component.e2e.ts' },
159+
];
160+
161+
userChoices.forEach((file) => {
162+
expect(writeFileSpy).toHaveBeenCalledWith(
163+
file.path,
164+
getBoilerplateByExtension('my-component', file.extension, true, 'sass'),
165+
);
166+
});
167+
});
168+
169+
it('should generate files for less projects', async () => {
170+
formatToPick = 'less';
171+
const { config } = await setup([{ name: 'less' }]);
172+
const writeFileSpy = jest.spyOn(config.sys, 'writeFile');
173+
await silentGenerate(config);
174+
const userChoices: ReadonlyArray<BoilerplateFile> = [
175+
{ extension: 'tsx', path: '/src/components/my-component/my-component.tsx' },
176+
{ extension: 'less', path: '/src/components/my-component/my-component.less' },
177+
{ extension: 'spec.tsx', path: '/src/components/my-component/test/my-component.spec.tsx' },
178+
{ extension: 'e2e.ts', path: '/src/components/my-component/test/my-component.e2e.ts' },
179+
];
180+
181+
userChoices.forEach((file) => {
182+
expect(writeFileSpy).toHaveBeenCalledWith(
183+
file.path,
184+
getBoilerplateByExtension('my-component', file.extension, true, 'less'),
185+
);
186+
});
187+
});
138188
});

0 commit comments

Comments
 (0)