Skip to content

Commit 3bb0a60

Browse files
committed
Initial commit
0 parents  commit 3bb0a60

15 files changed

+4405
-0
lines changed

.editorconfig

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# editorconfig.org
2+
root = true
3+
4+
[*]
5+
charset = utf-8
6+
end_of_line = lf
7+
indent_size = 2
8+
indent_style = space
9+
insert_final_newline = true
10+
max_line_length = 80
11+
trim_trailing_whitespace = true

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/.vscode
2+
/lib
3+
/node_modules
4+
npm-debug.log
5+
yarn-error.log

package.json

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "typescript-css-modules",
3+
"version": "1.0.0",
4+
"main": "lib/index.js",
5+
"repository": "git@github.com:mrmckeb/typescript-css-modules.git",
6+
"author": "Brody McKee <mrmckeb@hotmail.com>",
7+
"license": "MIT",
8+
"description": "CSS modules support for TypeScript",
9+
"keywords": [
10+
"css",
11+
"modules",
12+
"plugin",
13+
"postcss",
14+
"sass",
15+
"typescript"
16+
],
17+
"scripts": {
18+
"build": "tsc",
19+
"test": "jest"
20+
},
21+
"husky": {
22+
"hooks": {
23+
"pre-commit": "pretty-quick --staged"
24+
}
25+
},
26+
"jest": {
27+
"preset": "ts-jest",
28+
"testEnvironment": "node"
29+
},
30+
"prettier": {
31+
"arrowParens": "always",
32+
"singleQuote": true,
33+
"trailingComma": "all"
34+
},
35+
"dependencies": {
36+
"icss-utils": "^4.0.0",
37+
"postcss": "^7.0.5",
38+
"postcss-icss-selectors": "^2.0.3"
39+
},
40+
"devDependencies": {
41+
"@types/jest": "^23.3.9",
42+
"@types/node": "^10.12.6",
43+
"husky": "^1.1.3",
44+
"jest": "^23.6.0",
45+
"prettier": "^1.15.2",
46+
"pretty-quick": "^1.8.0",
47+
"ts-jest": "^23.10.4",
48+
"tslint": "^5.11.0",
49+
"typescript": "^3.1.6"
50+
},
51+
"peerDependencies": {
52+
"typescript": "^3.0.0"
53+
}
54+
}

src/@types/icss-utils.d.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
declare module 'icss-utils' {
2+
import { Root } from 'postcss';
3+
interface IICSSExports {
4+
[exportName: string]: string;
5+
}
6+
export const extractICSS: (
7+
css: Root,
8+
removeRules?: boolean,
9+
) => {
10+
icssExports: IICSSExports;
11+
};
12+
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
declare module 'postcss-icss-selectors' {
2+
import { Plugin } from 'postcss';
3+
const plugin: Plugin<{ mode: 'local' | 'global' }>;
4+
export = plugin;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { isCSS, isRelativeCSS } from '../cssExtensions';
2+
3+
describe('utils / cssExtensions', () => {
4+
describe('isCSS', () => {
5+
it('should match CSS module extensions', () => {
6+
expect(isCSS('./myfile.module.scss')).toBe(true);
7+
expect(isCSS('./myfile.module.sass')).toBe(true);
8+
expect(isCSS('./myfile.module.css')).toBe(true);
9+
});
10+
11+
it('should not match non-CSS module extensions', () => {
12+
expect(isCSS('./myfile.module.s')).toBe(false);
13+
expect(isCSS('./myfile.scss')).toBe(false);
14+
expect(isCSS('./myfile.sass')).toBe(false);
15+
expect(isCSS('./myfile.css')).toBe(false);
16+
});
17+
});
18+
19+
describe('isRelativeCSS', () => {
20+
it('should match relative CSS modules', () => {
21+
expect(isRelativeCSS('./myfile.module.css')).toBe(true);
22+
expect(isRelativeCSS('../folder/myfile.module.css')).toBe(true);
23+
});
24+
25+
it('should not match non-relative CSS modules', () => {
26+
expect(isRelativeCSS('myfile.module.css')).toBe(false);
27+
});
28+
});
29+
});
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { readFileSync } from 'fs';
2+
import { IICSSExports } from 'icss-utils';
3+
import { join } from 'path';
4+
import { createExports, getClasses } from '../cssSnapshots';
5+
6+
describe('utils / cssSnapshots', () => {
7+
let classesA: IICSSExports;
8+
let classesB: IICSSExports;
9+
10+
beforeAll(() => {
11+
const testFileA = readFileSync(
12+
join(__dirname, 'fixtures/testA.module.css'),
13+
'utf8',
14+
);
15+
const testFileB = readFileSync(
16+
join(__dirname, 'fixtures/testB.module.scss'),
17+
'utf8',
18+
);
19+
classesA = getClasses(testFileA);
20+
classesB = getClasses(testFileB);
21+
});
22+
23+
describe('getClasses', () => {
24+
it('should return an object matching expected CSS classes', () => {
25+
expect(classesA).toEqual({
26+
ClassB: 'file__ClassB---2bPVi',
27+
childA: 'file__childA---1hjQD',
28+
childB: 'file__childB---pq4Ks',
29+
'class-c': 'file__class-c---DZ1TD',
30+
classA: 'file__classA---2xcnJ',
31+
nestedChild: 'file__nestedChild---2d15b',
32+
parent: 'file__parent---1ATMj',
33+
});
34+
expect(classesB).toEqual({
35+
'local-class': 'file__local-class---3KegX',
36+
'local-class-2': 'file__local-class-2---2h6qz',
37+
'local-class-inside-global': 'file__local-class-inside-global---2xH_Y',
38+
'local-class-inside-local': 'file__local-class-inside-local---QdL6b',
39+
});
40+
});
41+
});
42+
43+
describe('createExports', () => {
44+
it('should create an exports file', () => {
45+
const exportsA = createExports(classesA);
46+
const exportsB = createExports(classesB);
47+
// tslint:disable max-line-length
48+
expect(exportsA).toMatchInlineSnapshot(
49+
`"export const classA: string;export const ClassB: string;export const class-c: string;export const parent: string;export const childA: string;export const childB: string;export const nestedChild: string;"`,
50+
);
51+
expect(exportsB).toMatchInlineSnapshot(
52+
`"export const local-class-inside-global: string;export const local-class: string;export const local-class-2: string;export const local-class-inside-local: string;"`,
53+
);
54+
// tslint:enable max-line-length
55+
});
56+
});
57+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.classA {
2+
}
3+
4+
.ClassB {
5+
}
6+
7+
.class-c {
8+
}
9+
10+
.parent {
11+
.childA {
12+
}
13+
.childB {
14+
.nestedChild {
15+
}
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
:global .global-class {
2+
}
3+
4+
:global(.global-class-2) {
5+
.local-class-inside-global {
6+
}
7+
}
8+
9+
:local .local-class {
10+
}
11+
12+
:local(.local-class-2) {
13+
.local-class-inside-local {
14+
}
15+
}

src/helpers/cssExtensions.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const isRelative = (fileName: string) => /^\.\.?($|[\\/])/.test(fileName);
2+
3+
export const isCSS = (fileName: string) =>
4+
/\.module\.(sa|sc|c)ss$/.test(fileName);
5+
6+
export const isRelativeCSS = (fileName: string) =>
7+
isCSS(fileName) && isRelative(fileName);

src/helpers/cssSnapshots.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { extractICSS, IICSSExports } from 'icss-utils';
2+
import * as postcss from 'postcss';
3+
import * as postcssIcssSelectors from 'postcss-icss-selectors';
4+
import * as ts_module from 'typescript/lib/tsserverlibrary';
5+
6+
const processor = postcss(postcssIcssSelectors({ mode: 'local' }));
7+
8+
export const getClasses = (css: string) =>
9+
extractICSS(processor.process(css).root).icssExports;
10+
export const createExports = (classes: IICSSExports) =>
11+
Object.keys(classes)
12+
.map((exportName) => `export const ${exportName}: string;`)
13+
.join('');
14+
15+
export const getDtsSnapshot = (
16+
ts: typeof ts_module,
17+
scriptSnapshot: ts.IScriptSnapshot,
18+
) => {
19+
const css = scriptSnapshot.getText(0, scriptSnapshot.getLength());
20+
const classes = getClasses(css);
21+
const dts = createExports(classes);
22+
return ts.ScriptSnapshot.fromString(dts);
23+
};

src/index.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import * as path from 'path';
2+
import * as ts_module from 'typescript/lib/tsserverlibrary';
3+
import { types } from 'util';
4+
import { isCSS as _isCSS, isRelativeCSS } from './helpers/cssExtensions';
5+
import { getDtsSnapshot } from './helpers/cssSnapshots';
6+
7+
interface IOptions {
8+
customMatcher?: string;
9+
}
10+
11+
function init({ typescript: ts }: { typescript: typeof ts_module }) {
12+
let isCSS = _isCSS;
13+
function create(info: ts.server.PluginCreateInfo) {
14+
// Allow custom matchers to be used, handling bad matcher patterns;
15+
try {
16+
const { customMatcher }: IOptions = info.config.options || {};
17+
if (customMatcher) {
18+
isCSS = (fileName) => new RegExp(customMatcher).test(fileName);
19+
}
20+
} catch (e) {
21+
// There doesn't appear to be a way to throw warnings yet.
22+
}
23+
24+
// Creates new virtual source files for the CSS modules.
25+
const _createLanguageServiceSourceFile = ts.createLanguageServiceSourceFile;
26+
ts.createLanguageServiceSourceFile = (
27+
fileName,
28+
scriptSnapshot,
29+
...rest
30+
): ts.SourceFile => {
31+
if (isCSS(fileName)) {
32+
scriptSnapshot = getDtsSnapshot(ts, scriptSnapshot);
33+
}
34+
const sourceFile = _createLanguageServiceSourceFile(
35+
fileName,
36+
scriptSnapshot,
37+
...rest,
38+
);
39+
if (isCSS(fileName)) {
40+
sourceFile.isDeclarationFile = true;
41+
}
42+
return sourceFile;
43+
};
44+
45+
// Updates virtual source files as files update.
46+
const _updateLanguageServiceSourceFile = ts.updateLanguageServiceSourceFile;
47+
ts.updateLanguageServiceSourceFile = (
48+
sourceFile,
49+
scriptSnapshot,
50+
...rest
51+
): ts.SourceFile => {
52+
if (isCSS(sourceFile.fileName)) {
53+
scriptSnapshot = getDtsSnapshot(ts, scriptSnapshot);
54+
}
55+
sourceFile = _updateLanguageServiceSourceFile(
56+
sourceFile,
57+
scriptSnapshot,
58+
...rest,
59+
);
60+
if (isCSS(sourceFile.fileName)) {
61+
sourceFile.isDeclarationFile = true;
62+
}
63+
return sourceFile;
64+
};
65+
66+
if (info.languageServiceHost.resolveModuleNames) {
67+
const _resolveModuleNames = info.languageServiceHost.resolveModuleNames.bind(
68+
info.languageServiceHost,
69+
);
70+
71+
info.languageServiceHost.resolveModuleNames = (
72+
moduleNames,
73+
containingFile,
74+
reusedNames,
75+
) => {
76+
const resolvedCSS: ts.ResolvedModuleFull[] = [];
77+
78+
return _resolveModuleNames(
79+
moduleNames.filter((moduleName) => {
80+
if (isRelativeCSS(moduleName)) {
81+
resolvedCSS.push({
82+
extension: ts_module.Extension.Dts,
83+
resolvedFileName: path.resolve(
84+
path.dirname(containingFile),
85+
moduleName,
86+
),
87+
});
88+
return false;
89+
}
90+
return true;
91+
}),
92+
containingFile,
93+
reusedNames,
94+
).concat(resolvedCSS);
95+
};
96+
}
97+
98+
return info.languageService;
99+
}
100+
101+
function getExternalFiles(project: ts_module.server.ConfiguredProject) {
102+
return project.getFileNames().filter(isCSS);
103+
}
104+
105+
return { create, getExternalFiles };
106+
}
107+
108+
export = init;

tsconfig.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"declaration": true,
4+
"lib": ["esnext"],
5+
"module": "commonjs",
6+
"moduleResolution": "node",
7+
"noEmitOnError": true,
8+
"noImplicitAny": true,
9+
"noImplicitReturns": true,
10+
"noImplicitThis": true,
11+
"outDir": "lib",
12+
"rootDir": "src",
13+
"skipLibCheck": false,
14+
"strict": true,
15+
"strictNullChecks": true,
16+
"target": "es5"
17+
}
18+
}

tslint.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"extends": "tslint:recommended",
3+
"rules": {
4+
"object-literal-key-quotes": [true, "as-needed"],
5+
"quotemark": [true, "single"],
6+
"trailing-comma": [
7+
true,
8+
{
9+
"multiline": "always",
10+
"singleline": "never",
11+
"esSpecCompliant": true
12+
}
13+
],
14+
"variable-name": [true, "allow-leading-underscore"]
15+
}
16+
}

0 commit comments

Comments
 (0)