From 143b7ea211a56a7d68a356a998ede33ed57baf18 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 26 Aug 2019 10:17:12 +0100 Subject: [PATCH] Add TypeScript server logs for exceptions (#38) --- src/helpers/DtsSnapshotCreator.ts | 109 ++++++++++++++++++ src/helpers/Logger.ts | 19 +++ ...ots.test.ts => DtsSnapshotCreator.test.ts} | 11 +- ...s.snap => DtsSnapshotCreator.test.ts.snap} | 0 src/helpers/__tests__/createMatchers.test.ts | 6 +- src/helpers/config.ts | 1 + src/helpers/createMatchers.ts | 4 +- src/helpers/cssSnapshots.ts | 103 ----------------- src/index.ts | 20 +++- 9 files changed, 160 insertions(+), 113 deletions(-) create mode 100644 src/helpers/DtsSnapshotCreator.ts create mode 100644 src/helpers/Logger.ts rename src/helpers/__tests__/{cssSnapshots.test.ts => DtsSnapshotCreator.test.ts} (71%) rename src/helpers/__tests__/__snapshots__/{cssSnapshots.test.ts.snap => DtsSnapshotCreator.test.ts.snap} (100%) create mode 100644 src/helpers/config.ts delete mode 100644 src/helpers/cssSnapshots.ts diff --git a/src/helpers/DtsSnapshotCreator.ts b/src/helpers/DtsSnapshotCreator.ts new file mode 100644 index 0000000..32bfe69 --- /dev/null +++ b/src/helpers/DtsSnapshotCreator.ts @@ -0,0 +1,109 @@ +import { extractICSS, IICSSExports } from 'icss-utils'; +import * as postcss from 'postcss'; +import * as postcssIcssSelectors from 'postcss-icss-selectors'; +import * as ts_module from 'typescript/lib/tsserverlibrary'; +import * as less from 'less'; +import * as sass from 'sass'; +import * as reserved from 'reserved-words'; +import { transformClasses } from './classTransforms'; +import { Options } from '../options'; +import { Logger } from './Logger'; + +const NOT_CAMELCASE_REGEXP = /[\-_]/; +const processor = postcss(postcssIcssSelectors({ mode: 'local' })); + +const classNameToProperty = (className: string) => `'${className}': string;`; +const classNameToNamedExport = (className: string) => + `export const ${className}: string;`; + +const flattenClassNames = ( + previousValue: string[] = [], + currentValue: string[], +) => previousValue.concat(currentValue); + +export const enum FileTypes { + css = 'css', + less = 'less', + scss = 'scss', +} + +export const getFileType = (fileName: string) => { + if (fileName.endsWith('.css')) return FileTypes.css; + if (fileName.endsWith('.less')) return FileTypes.less; + return FileTypes.scss; +}; + +const getFilePath = (fileName: string) => + fileName.substring(0, fileName.lastIndexOf('/')); + +export class DtsSnapshotCreator { + constructor(private readonly logger: Logger) {} + + getClasses(css: string, fileName: string) { + try { + const fileType = getFileType(fileName); + let transformedCss = ''; + + if (fileType === FileTypes.less) { + less.render(css, { asyncImport: true } as any, (err, output) => { + transformedCss = output.css.toString(); + }); + } else if (fileType === FileTypes.scss) { + const filePath = getFilePath(fileName); + transformedCss = sass + .renderSync({ + data: css, + includePaths: [filePath], + }) + .css.toString(); + } else { + transformedCss = css; + } + + const processedCss = processor.process(transformedCss); + + return extractICSS(processedCss.root).icssExports; + } catch (e) { + this.logger.error(e); + return {}; + } + } + + createExports(classes: IICSSExports, options: Options) { + const isCamelCase = (className: string) => + !NOT_CAMELCASE_REGEXP.test(className); + const isReservedWord = (className: string) => !reserved.check(className); + + const processedClasses = Object.keys(classes) + .map(transformClasses(options.camelCase)) + .reduce(flattenClassNames, []); + const camelCasedKeys = processedClasses + .filter(isCamelCase) + .filter(isReservedWord) + .map(classNameToNamedExport); + + const defaultExport = `\ +declare const classes: { + ${processedClasses.map(classNameToProperty).join('\n ')} +}; +export default classes; +`; + + if (camelCasedKeys.length) { + return defaultExport + camelCasedKeys.join('\n') + '\n'; + } + return defaultExport; + } + + getDtsSnapshot( + ts: typeof ts_module, + fileName: string, + scriptSnapshot: ts.IScriptSnapshot, + options: Options, + ) { + const css = scriptSnapshot.getText(0, scriptSnapshot.getLength()); + const classes = this.getClasses(css, fileName); + const dts = this.createExports(classes, options); + return ts.ScriptSnapshot.fromString(dts); + } +} diff --git a/src/helpers/Logger.ts b/src/helpers/Logger.ts new file mode 100644 index 0000000..6f24c14 --- /dev/null +++ b/src/helpers/Logger.ts @@ -0,0 +1,19 @@ +import { pluginName } from './config'; + +export interface Logger { + log(msg: string): void; + error(e: Error): void; +} + +export class LanguageServiceLogger implements Logger { + constructor(private readonly info: ts.server.PluginCreateInfo) {} + + public log(msg: string) { + this.info.project.projectService.logger.info(`[${pluginName}] ${msg}`); + } + + public error(e: Error) { + this.log(`Failed ${e.toString()}`); + this.log(`Stack trace: ${e.stack}`); + } +} diff --git a/src/helpers/__tests__/cssSnapshots.test.ts b/src/helpers/__tests__/DtsSnapshotCreator.test.ts similarity index 71% rename from src/helpers/__tests__/cssSnapshots.test.ts rename to src/helpers/__tests__/DtsSnapshotCreator.test.ts index 1fbe44a..647e5a9 100644 --- a/src/helpers/__tests__/cssSnapshots.test.ts +++ b/src/helpers/__tests__/DtsSnapshotCreator.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'fs'; import { IICSSExports } from 'icss-utils'; import { join } from 'path'; -import { createExports, getClasses, getFileType } from '../cssSnapshots'; +import { DtsSnapshotCreator } from '../DtsSnapshotCreator'; const testFileNames = [ 'test.module.css', @@ -14,11 +14,16 @@ const testFileNames = [ describe('utils / cssSnapshots', () => { testFileNames.forEach((fileName) => { let classes: IICSSExports; + let dtsSnapshotCreator: DtsSnapshotCreator; const fullFileName = join(__dirname, 'fixtures', fileName); const testFile = readFileSync(fullFileName, 'utf8'); beforeAll(() => { - classes = getClasses(testFile, fullFileName); + dtsSnapshotCreator = new DtsSnapshotCreator({ + log: jest.fn(), + error: jest.fn(), + }); + classes = dtsSnapshotCreator.getClasses(testFile, fullFileName); }); describe(`with file '${fileName}'`, () => { @@ -30,7 +35,7 @@ describe('utils / cssSnapshots', () => { describe('createExports', () => { it('should create an exports file', () => { - const exports = createExports(classes, {}); + const exports = dtsSnapshotCreator.createExports(classes, {}); expect(exports).toMatchSnapshot(); }); }); diff --git a/src/helpers/__tests__/__snapshots__/cssSnapshots.test.ts.snap b/src/helpers/__tests__/__snapshots__/DtsSnapshotCreator.test.ts.snap similarity index 100% rename from src/helpers/__tests__/__snapshots__/cssSnapshots.test.ts.snap rename to src/helpers/__tests__/__snapshots__/DtsSnapshotCreator.test.ts.snap diff --git a/src/helpers/__tests__/createMatchers.test.ts b/src/helpers/__tests__/createMatchers.test.ts index 5eeb119..918f20a 100644 --- a/src/helpers/__tests__/createMatchers.test.ts +++ b/src/helpers/__tests__/createMatchers.test.ts @@ -1,10 +1,12 @@ import { createMatchers } from '../createMatchers'; import { Options } from '../../options'; +import { Logger } from '../Logger'; describe('utils / createMatchers', () => { + const logger: Logger = { log: jest.fn(), error: jest.fn() }; it('should match `customMatcher` regexp', () => { const options: Options = { customMatcher: '\\.css$' }; - const { isCSS, isRelativeCSS } = createMatchers(options); + const { isCSS, isRelativeCSS } = createMatchers(logger, options); expect(isCSS('./myfile.css')).toBe(true); expect(isCSS('./myfile.m.css')).toBe(true); @@ -16,7 +18,7 @@ describe('utils / createMatchers', () => { it('should handle bad `customMatcher` regexp', () => { const options: Options = { customMatcher: '$([a' }; - const { isCSS, isRelativeCSS } = createMatchers(options); + const { isCSS, isRelativeCSS } = createMatchers(logger, options); expect(isCSS('./myfile.module.css')).toBe(true); expect(isRelativeCSS('../folders/myfile.module.scss')).toBe(true); diff --git a/src/helpers/config.ts b/src/helpers/config.ts new file mode 100644 index 0000000..4dc12de --- /dev/null +++ b/src/helpers/config.ts @@ -0,0 +1 @@ +export const pluginName = 'typescript-plugin-css-modules'; diff --git a/src/helpers/createMatchers.ts b/src/helpers/createMatchers.ts index 1e0f0a6..a57e22b 100644 --- a/src/helpers/createMatchers.ts +++ b/src/helpers/createMatchers.ts @@ -1,7 +1,8 @@ import { createIsCSS, createIsRelativeCSS } from './cssExtensions'; import { Options } from '../options'; +import { Logger } from './Logger'; -export const createMatchers = (options: Options = {}) => { +export const createMatchers = (logger: Logger, options: Options = {}) => { // Allow custom matchers to be used, and handle bad matcher patterns. let isCSS = createIsCSS(); try { @@ -11,6 +12,7 @@ export const createMatchers = (options: Options = {}) => { isCSS = createIsCSS(customMatcherRegExp); } } catch (e) { + logger.error(e); // TODO: Provide error/warning to user. } diff --git a/src/helpers/cssSnapshots.ts b/src/helpers/cssSnapshots.ts deleted file mode 100644 index d2d77cf..0000000 --- a/src/helpers/cssSnapshots.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { extractICSS, IICSSExports } from 'icss-utils'; -import * as postcss from 'postcss'; -import * as postcssIcssSelectors from 'postcss-icss-selectors'; -import * as ts_module from 'typescript/lib/tsserverlibrary'; -import * as less from 'less'; -import * as sass from 'sass'; -import * as reserved from 'reserved-words'; -import { transformClasses } from './classTransforms'; -import { Options } from '../options'; - -const NOT_CAMELCASE_REGEXP = /[\-_]/; -const processor = postcss(postcssIcssSelectors({ mode: 'local' })); - -const classNameToProperty = (className: string) => `'${className}': string;`; -const classNameToNamedExport = (className: string) => - `export const ${className}: string;`; - -const flattenClassNames = ( - previousValue: string[] = [], - currentValue: string[], -) => previousValue.concat(currentValue); - -export const enum FileTypes { - css = 'css', - less = 'less', - scss = 'scss', -} - -export const getFileType = (fileName: string) => { - if (fileName.endsWith('.css')) return FileTypes.css; - if (fileName.endsWith('.less')) return FileTypes.less; - return FileTypes.scss; -}; - -const getFilePath = (fileName: string) => - fileName.substring(0, fileName.lastIndexOf('/')); - -export const getClasses = (css: string, fileName: string) => { - try { - const fileType = getFileType(fileName); - let transformedCss = ''; - - if (fileType === FileTypes.less) { - less.render(css, { asyncImport: true } as any, (err, output) => { - transformedCss = output.css.toString(); - }); - } else if (fileType === FileTypes.scss) { - const filePath = getFilePath(fileName); - transformedCss = sass - .renderSync({ - data: css, - includePaths: [filePath], - }) - .css.toString(); - } else { - transformedCss = css; - } - - const processedCss = processor.process(transformedCss); - - return extractICSS(processedCss.root).icssExports; - } catch (e) { - return {}; - } -}; - -export const createExports = (classes: IICSSExports, options: Options) => { - const isCamelCase = (className: string) => - !NOT_CAMELCASE_REGEXP.test(className); - const isReservedWord = (className: string) => !reserved.check(className); - - const processedClasses = Object.keys(classes) - .map(transformClasses(options.camelCase)) - .reduce(flattenClassNames, []); - const camelCasedKeys = processedClasses - .filter(isCamelCase) - .filter(isReservedWord) - .map(classNameToNamedExport); - - const defaultExport = `\ -declare const classes: { - ${processedClasses.map(classNameToProperty).join('\n ')} -}; -export default classes; -`; - - if (camelCasedKeys.length) { - return defaultExport + camelCasedKeys.join('\n') + '\n'; - } - return defaultExport; -}; - -export const getDtsSnapshot = ( - ts: typeof ts_module, - fileName: string, - scriptSnapshot: ts.IScriptSnapshot, - options: Options, -) => { - const css = scriptSnapshot.getText(0, scriptSnapshot.getLength()); - const classes = getClasses(css, fileName); - const dts = createExports(classes, options); - return ts.ScriptSnapshot.fromString(dts); -}; diff --git a/src/index.ts b/src/index.ts index 20352c7..b119e26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,17 +3,23 @@ import * as path from 'path'; import * as ts_module from 'typescript/lib/tsserverlibrary'; import { createMatchers } from './helpers/createMatchers'; import { isCSSFn } from './helpers/cssExtensions'; -import { getDtsSnapshot } from './helpers/cssSnapshots'; +import { DtsSnapshotCreator } from './helpers/DtsSnapshotCreator'; import { Options } from './options'; +import { LanguageServiceLogger } from './helpers/Logger'; function init({ typescript: ts }: { typescript: typeof ts_module }) { let _isCSS: isCSSFn; function create(info: ts.server.PluginCreateInfo) { + const logger = new LanguageServiceLogger(info); + const dtsSnapshotCreator = new DtsSnapshotCreator(logger); + // User options for plugin. const options: Options = info.config.options || {}; + logger.log(`options: ${JSON.stringify(options)}`); + // Create matchers using options object. - const { isCSS, isRelativeCSS } = createMatchers(options); + const { isCSS, isRelativeCSS } = createMatchers(logger, options); _isCSS = isCSS; // Creates new virtual source files for the CSS modules. @@ -24,7 +30,12 @@ function init({ typescript: ts }: { typescript: typeof ts_module }) { ...rest ): ts.SourceFile => { if (isCSS(fileName)) { - scriptSnapshot = getDtsSnapshot(ts, fileName, scriptSnapshot, options); + scriptSnapshot = dtsSnapshotCreator.getDtsSnapshot( + ts, + fileName, + scriptSnapshot, + options, + ); } const sourceFile = _createLanguageServiceSourceFile( fileName, @@ -45,7 +56,7 @@ function init({ typescript: ts }: { typescript: typeof ts_module }) { ...rest ): ts.SourceFile => { if (isCSS(sourceFile.fileName)) { - scriptSnapshot = getDtsSnapshot( + scriptSnapshot = dtsSnapshotCreator.getDtsSnapshot( ts, sourceFile.fileName, scriptSnapshot, @@ -132,6 +143,7 @@ function init({ typescript: ts }: { typescript: typeof ts_module }) { } } } catch (e) { + logger.error(e); return resolvedModules[index]; } return resolvedModules[index];