Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ env:
- GIT_AUTHOR_EMAIL=yosuke.kurami@gmail.com
language: node_js
node_js:
- 6
- 8
- 9
before_script:
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
],
"author": "quramy",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
},
"dependencies": {
"camelcase": "^5.3.1",
"chalk": "^2.1.0",
Expand Down
84 changes: 84 additions & 0 deletions src/DtsContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import isThere from "is-there";
import * as mkdirp from 'mkdirp';
import * as util from "util";

const writeFile = util.promisify(fs.writeFile);


interface DtsContentOptions {
dropExtension: boolean;
rootDir: string;
searchDir: string;
outDir: string;
rInputPath: string;
rawTokenList: string[];
resultList: string[];
EOL: string;
}

export class DtsContent {
private dropExtension: boolean;
private rootDir: string;
private searchDir: string;
private outDir: string;
private rInputPath: string;
private rawTokenList: string[];
private resultList: string[];
private EOL: string;

constructor(options: DtsContentOptions) {
this.dropExtension = options.dropExtension;
this.rootDir = options.rootDir;
this.searchDir = options.searchDir;
this.outDir = options.outDir;
this.rInputPath = options.rInputPath;
this.rawTokenList = options.rawTokenList;
this.resultList = options.resultList;
this.EOL = options.EOL;
}

public get contents(): string[] {
return this.resultList;
}

public get formatted(): string {
if(!this.resultList || !this.resultList.length) return '';
return [
'declare const styles: {',
...this.resultList.map(line => ' ' + line),
'};',
'export = styles;',
''
].join(os.EOL) + this.EOL;
}

public get tokens(): string[] {
return this.rawTokenList;
}

public get outputFilePath(): string {
const outputFileName = this.dropExtension ? removeExtension(this.rInputPath) : this.rInputPath;
return path.join(this.rootDir, this.outDir, outputFileName + '.d.ts');
}

public get inputFilePath(): string {
return path.join(this.rootDir, this.searchDir, this.rInputPath);
}

public async writeFile(): Promise<void> {
const outPathDir = path.dirname(this.outputFilePath);
if(!isThere(outPathDir)) {
mkdirp.sync(outPathDir);
}

await writeFile(this.outputFilePath, this.formatted, 'utf8');
}
}

function removeExtension(filePath: string): string {
const ext = path.extname(filePath);
return filePath.replace(new RegExp(ext + '$'), '');
}
107 changes: 107 additions & 0 deletions src/DtsCreator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as process from 'process';
import * as path from'path';
import * as os from 'os';
import camelcase from "camelcase"
import FileSystemLoader from './FileSystemLoader';
import {DtsContent} from "./DtsContent";


type CamelCaseOption = boolean | 'dashes' | undefined;

interface DtsCreatorOptions {
rootDir?: string;
searchDir?: string;
outDir?: string;
camelCase?: CamelCaseOption;
dropExtension?: boolean;
EOL?: string;
}

export class DtsCreator {
private rootDir: string;
private searchDir: string;
private outDir: string;
private loader: FileSystemLoader;
private inputDirectory: string;
private outputDirectory: string;
private camelCase: boolean | 'dashes' | undefined;
private dropExtension: boolean;
private EOL: string;

constructor(options?: DtsCreatorOptions) {
if(!options) options = {};
this.rootDir = options.rootDir || process.cwd();
this.searchDir = options.searchDir || '';
this.outDir = options.outDir || this.searchDir;
this.loader = new FileSystemLoader(this.rootDir);
this.inputDirectory = path.join(this.rootDir, this.searchDir);
this.outputDirectory = path.join(this.rootDir, this.outDir);
this.camelCase = options.camelCase;
this.dropExtension = !!options.dropExtension;
this.EOL = options.EOL || os.EOL;
}

public async create(filePath: string, initialContents?: string, clearCache: boolean = false): Promise<DtsContent> {
let rInputPath: string;
if(path.isAbsolute(filePath)) {
rInputPath = path.relative(this.inputDirectory, filePath);
}else{
rInputPath = path.relative(this.inputDirectory, path.join(process.cwd(), filePath));
}
if(clearCache) {
this.loader.tokensByFile = {};
}

const res = await this.loader.fetch(filePath, "/", undefined, initialContents);
if(res) {
const tokens = res;
const keys = Object.keys(tokens);

const convertKey = this.getConvertKeyMethod(this.camelCase);

const result = keys
.map(k => convertKey(k))
.map(k => 'readonly "' + k + '": string;')

const content = new DtsContent({
dropExtension: this.dropExtension,
rootDir: this.rootDir,
searchDir: this.searchDir,
outDir: this.outDir,
rInputPath,
rawTokenList: keys,
resultList: result,
EOL: this.EOL
});

return content;
}else{
throw res;
}
}

private getConvertKeyMethod(camelCaseOption: CamelCaseOption): (str: string) => string {
switch (camelCaseOption) {
case true:
return camelcase;
case 'dashes':
return this.dashesCamelCase;
default:
return (key) => key;
}
}

/**
* Replaces only the dashes and leaves the rest as-is.
*
* Mirrors the behaviour of the css-loader:
* https://github.com/webpack-contrib/css-loader/blob/1fee60147b9dba9480c9385e0f4e581928ab9af9/lib/compile-exports.js#L3-L7
*/
private dashesCamelCase(str: string): string {
return str.replace(/-+(\w)/g, function(match, firstLetter) {
return firstLetter.toUpperCase();
});
}


}
75 changes: 75 additions & 0 deletions src/FileSystemLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* this file is forked from https://raw.githubusercontent.com/css-modules/css-modules-loader-core/master/src/file-system-loader.js */

import Core from 'css-modules-loader-core'
import * as fs from 'fs'
import * as path from 'path'
import * as util from 'util'
import { Plugin } from "postcss";


type Dictionary<T> = {
[key: string]: T | undefined;
};

const readFile = util.promisify(fs.readFile);


export default class FileSystemLoader {
private root: string;
private sources: Dictionary<string>;
private importNr: number;
private core: Core;
public tokensByFile: Dictionary<Core.ExportTokens>;

constructor( root: string, plugins?: Array<Plugin<any>> ) {
this.root = root;
this.sources = {};
this.importNr = 0;
this.core = new Core(plugins);
this.tokensByFile = {};
}

public async fetch(_newPath: string, relativeTo: string, _trace?: string, initialContents?: string): Promise<Core.ExportTokens> {
const newPath = _newPath.replace(/^["']|["']$/g, "");
const trace = _trace || String.fromCharCode(this.importNr++);

const relativeDir = path.dirname(relativeTo);
const rootRelativePath = path.resolve(relativeDir, newPath);
let fileRelativePath = path.resolve(path.join(this.root, relativeDir), newPath);

// if the path is not relative or absolute, try to resolve it in node_modules
if (newPath[0] !== '.' && newPath[0] !== '/') {
try {
fileRelativePath = require.resolve(newPath);
}
catch (e) {}
}

let source: string;

if (!initialContents) {
const tokens = this.tokensByFile[fileRelativePath]
if (tokens) {
return tokens;
}

try {
source = await readFile(fileRelativePath, "utf-8");
}
catch (error) {
if (relativeTo && relativeTo !== '/') {
return {};
}

throw error;
}
} else {
source = initialContents;
}

const { injectableSource, exportTokens } = await this.core.load(source, rootRelativePath, trace, this.fetch.bind(this));
this.sources[trace] = injectableSource;
this.tokensByFile[fileRelativePath] = exportTokens;
return exportTokens;
}
}
31 changes: 18 additions & 13 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import * as chokidar from 'chokidar';
import glob from 'glob';
import * as yargs from 'yargs';
import chalk from 'chalk';
import {DtsCreator} from './dtsCreator';
import {DtsCreator} from './DtsCreator';
import {DtsContent} from "./DtsContent";

let yarg = yargs.usage('Create .css.d.ts from CSS modules *.css files.\nUsage: $0 [options] <input directory>')
const yarg = yargs.usage('Create .css.d.ts from CSS modules *.css files.\nUsage: $0 [options] <input directory>')
.example('$0 src/styles', '')
.example('$0 src -o dist', '')
.example('$0 -p styles/**/*.icss -w', '')
Expand All @@ -21,22 +22,26 @@ let yarg = yargs.usage('Create .css.d.ts from CSS modules *.css files.\nUsage: $
.alias('s', 'silent').describe('s', 'Silent output. Do not show "files written" messages').boolean('s')
.alias('h', 'help').help('h')
.version(() => require('../package.json').version);
let argv = yarg.argv;
const argv = yarg.argv;
let creator: DtsCreator;

function writeFile(f: string) {
creator.create(f, undefined, !!argv.w)
.then(content => content.writeFile())
.then(content => {
async function writeFile(f: string): Promise<void> {
try {
const content: DtsContent = await creator.create(f, undefined, !!argv.w);
await content.writeFile();

if (!argv.s) {
console.log('Wrote ' + chalk.green(content.outputFilePath));
}
})
.catch((reason: unknown) => console.error(chalk.red('[Error] ' + reason)));
}
catch (error) {
console.error(chalk.red('[Error] ' + error));
}
};

let main = () => {
let rootDir, searchDir;
function main() {
let rootDir: string;
let searchDir: string;
if(argv.h) {
yarg.showHelp();
return;
Expand All @@ -50,7 +55,7 @@ let main = () => {
yarg.showHelp();
return;
}
let filesPattern = path.join(searchDir, argv.p || '**/*.css');
const filesPattern = path.join(searchDir, argv.p || '**/*.css');
rootDir = process.cwd();
creator = new DtsCreator({
rootDir,
Expand All @@ -72,7 +77,7 @@ let main = () => {
} else {
console.log('Watch ' + filesPattern + '...');

var watcher = chokidar.watch([filesPattern.replace(/\\/g, "/")]);
const watcher = chokidar.watch([filesPattern.replace(/\\/g, "/")]);
watcher.on('add', writeFile);
watcher.on('change', writeFile);
}
Expand Down
Loading