diff --git a/README.md b/README.md index bd4af51e..7ba3473a 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,11 @@ tcm -p 'src/**/*.icss' . With `-w` or `--watch`, this CLI watches files in the input directory. +#### validating type files + +With `-l` or `--listDifferent`, list any files that are different than those that would be generated. +If any are different, exit with a status code 1. + #### camelize CSS token With `-c` or `--camelCase`, kebab-cased CSS classes(such as `.my-class {...}`) are exported as camelized TypeScript varibale name(`export const myClass: string`). diff --git a/src/cli.ts b/src/cli.ts index 31896b99..5e4ffa72 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,6 +16,12 @@ const yarg = yargs .alias('o', 'outDir') .describe('o', 'Output directory') .string('o') + .alias('l', 'listDifferent') + .describe( + 'l', + 'List any files that are different than those that would be generated. If any are different, exit with a status code 1.', + ) + .boolean('l') .alias('w', 'watch') .describe('w', "Watch input directory's css files or pattern") .boolean('w') @@ -63,5 +69,6 @@ async function main(): Promise { namedExports: argv.e, dropExtension: argv.d, silent: argv.s, + listDifferent: argv.l, }); } diff --git a/src/dts-content.ts b/src/dts-content.ts index 81e02416..98fd0cd4 100644 --- a/src/dts-content.ts +++ b/src/dts-content.ts @@ -5,6 +5,7 @@ import isThere from 'is-there'; import * as mkdirp from 'mkdirp'; import * as util from 'util'; import camelcase from 'camelcase'; +import chalk from 'chalk'; const writeFile = util.promisify(fs.writeFile); const readFile = util.promisify(fs.readFile); @@ -90,6 +91,26 @@ export class DtsContent { return path.join(this.rootDir, this.searchDir, this.rInputPath); } + public get relativeInputFilePath(): string { + return path.join(this.searchDir, this.rInputPath); + } + + public async checkFile(postprocessor = (formatted: string) => formatted): Promise { + if (!isThere(this.outputFilePath)) { + console.error(chalk.red(`[ERROR] Type file needs to be generated for '${this.relativeInputFilePath}'`)); + return false; + } + + const finalOutput = postprocessor(this.formatted); + const fileContent = (await readFile(this.outputFilePath)).toString(); + + if (fileContent !== finalOutput) { + console.error(chalk.red(`[ERROR] Check type definitions for '${this.outputFilePath}'`)); + return false; + } + return true; + } + public async writeFile(postprocessor = (formatted: string) => formatted): Promise { const finalOutput = postprocessor(this.formatted); diff --git a/src/run.ts b/src/run.ts index 8ac660fd..8c17d8f0 100644 --- a/src/run.ts +++ b/src/run.ts @@ -16,6 +16,7 @@ interface RunOptions { namedExports?: boolean; dropExtension?: boolean; silent?: boolean; + listDifferent?: boolean; } export async function run(searchDir: string, options: RunOptions = {}): Promise { @@ -30,6 +31,16 @@ export async function run(searchDir: string, options: RunOptions = {}): Promise< dropExtension: options.dropExtension, }); + const checkFile = async (f: string): Promise => { + try { + const content: DtsContent = await creator.create(f, undefined, false); + return await content.checkFile(); + } catch (error) { + console.error(chalk.red(`[ERROR] An error occurred checking '${f}':\n${error}`)); + return false; + } + }; + const writeFile = async (f: string): Promise => { try { const content: DtsContent = await creator.create(f, undefined, !!options.watch); @@ -43,6 +54,15 @@ export async function run(searchDir: string, options: RunOptions = {}): Promise< } }; + if (options.listDifferent) { + const files = await glob(filesPattern); + const hasErrors = (await Promise.all(files.map(checkFile))).includes(false); + if (hasErrors) { + process.exit(1); + } + return; + } + if (!options.watch) { const files = await glob(filesPattern); await Promise.all(files.map(writeFile)); diff --git a/test/different.css b/test/different.css new file mode 100644 index 00000000..9a768bac --- /dev/null +++ b/test/different.css @@ -0,0 +1 @@ +.myClass {color: red;} diff --git a/test/different.css.d.ts b/test/different.css.d.ts new file mode 100644 index 00000000..e31e5041 --- /dev/null +++ b/test/different.css.d.ts @@ -0,0 +1,4 @@ +declare const styles: { + readonly "differentClass": string; +}; +export = styles; diff --git a/test/dts-creator.spec.ts b/test/dts-creator.spec.ts index 6cf0358d..fc896900 100644 --- a/test/dts-creator.spec.ts +++ b/test/dts-creator.spec.ts @@ -3,8 +3,8 @@ import * as path from 'path'; import * as assert from 'assert'; -import * as os from 'os'; import { DtsCreator } from '../src/dts-creator'; +import SpyInstance = jest.SpyInstance; describe('DtsCreator', () => { var creator = new DtsCreator(); @@ -91,6 +91,15 @@ describe('DtsContent', () => { }); }); + describe('#relativeInputFilePath', () => { + it('returns relative original CSS file name', done => { + new DtsCreator().create(path.normalize('test/testStyle.css')).then(content => { + assert.equal(content.relativeInputFilePath, 'test/testStyle.css'); + done(); + }); + }); + }); + describe('#outputFilePath', () => { it('adds d.ts to the original filename', done => { new DtsCreator().create(path.normalize('test/testStyle.css')).then(content => { @@ -210,6 +219,66 @@ export = styles; }); }); + describe('#checkFile', () => { + let mockExit: SpyInstance; + let mockConsoleLog: SpyInstance; + let mockConsoleError: SpyInstance; + + beforeAll(() => { + mockExit = jest.spyOn(process, 'exit').mockImplementation(exitCode => { + throw new Error(`process.exit: ${exitCode}`); + }); + mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); + mockConsoleError = jest.spyOn(console, 'error').mockImplementation(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + mockExit.mockRestore(); + mockConsoleLog.mockRestore(); + mockConsoleError.mockRestore(); + }); + + it('return false if type file is missing', done => { + new DtsCreator() + .create('test/empty.css') + .then(content => { + return content.checkFile(); + }) + .then(result => { + assert.equal(result, false); + done(); + }); + }); + + it('returns false if type file content is different', done => { + new DtsCreator() + .create('test/different.css') + .then(content => { + return content.checkFile(); + }) + .then(result => { + assert.equal(result, false); + done(); + }); + }); + + it('returns true if type files match', done => { + new DtsCreator() + .create('test/testStyle.css') + .then(content => { + return content.checkFile(); + }) + .then(result => { + assert.equal(result, true); + done(); + }); + }); + }); + describe('#writeFile', () => { it('accepts a postprocessor function', done => { new DtsCreator()