Skip to content

Commit

Permalink
Add TypeScript server logs for exceptions (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
dagda1 authored and mrmckeb committed Aug 26, 2019
1 parent cfdaca2 commit 143b7ea
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 113 deletions.
109 changes: 109 additions & 0 deletions 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);
}
}
19 changes: 19 additions & 0 deletions 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}`);
}
}
@@ -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',
Expand All @@ -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}'`, () => {
Expand All @@ -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();
});
});
Expand Down
6 changes: 4 additions & 2 deletions 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);
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/helpers/config.ts
@@ -0,0 +1 @@
export const pluginName = 'typescript-plugin-css-modules';
4 changes: 3 additions & 1 deletion 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 {
Expand All @@ -11,6 +12,7 @@ export const createMatchers = (options: Options = {}) => {
isCSS = createIsCSS(customMatcherRegExp);
}
} catch (e) {
logger.error(e);
// TODO: Provide error/warning to user.

This comment has been minimized.

Copy link
@dko-slapdash

dko-slapdash Sep 1, 2019

Is this TODO still relevant?

This comment has been minimized.

Copy link
@mrmckeb

mrmckeb Sep 4, 2019

Owner

I think no now, but I'll clean up.

}

Expand Down
103 changes: 0 additions & 103 deletions src/helpers/cssSnapshots.ts

This file was deleted.

20 changes: 16 additions & 4 deletions src/index.ts
Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -132,6 +143,7 @@ function init({ typescript: ts }: { typescript: typeof ts_module }) {
}
}
} catch (e) {
logger.error(e);
return resolvedModules[index];
}
return resolvedModules[index];
Expand Down

0 comments on commit 143b7ea

Please sign in to comment.