diff --git a/.travis.yml b/.travis.yml index 8321f275..79388e00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ env: - GIT_AUTHOR_EMAIL=yosuke.kurami@gmail.com language: node_js node_js: -- 6 - 8 - 9 before_script: diff --git a/package.json b/package.json index 18b25d77..dfc934dd 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,9 @@ ], "author": "quramy", "license": "MIT", + "engines": { + "node": ">=8.0.0" + }, "dependencies": { "camelcase": "^5.3.1", "chalk": "^2.1.0", diff --git a/src/DtsContent.ts b/src/DtsContent.ts new file mode 100644 index 00000000..ab3e0749 --- /dev/null +++ b/src/DtsContent.ts @@ -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 { + 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 + '$'), ''); +} diff --git a/src/DtsCreator.ts b/src/DtsCreator.ts new file mode 100644 index 00000000..642555d3 --- /dev/null +++ b/src/DtsCreator.ts @@ -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 { + 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(); + }); + } + + +} diff --git a/src/FileSystemLoader.ts b/src/FileSystemLoader.ts new file mode 100644 index 00000000..70f62e0f --- /dev/null +++ b/src/FileSystemLoader.ts @@ -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 = { + [key: string]: T | undefined; +}; + +const readFile = util.promisify(fs.readFile); + + +export default class FileSystemLoader { + private root: string; + private sources: Dictionary; + private importNr: number; + private core: Core; + public tokensByFile: Dictionary; + + constructor( root: string, plugins?: Array> ) { + 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 { + 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; + } +} diff --git a/src/cli.ts b/src/cli.ts index ebe75fea..f8eabc4d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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] ') +const yarg = yargs.usage('Create .css.d.ts from CSS modules *.css files.\nUsage: $0 [options] ') .example('$0 src/styles', '') .example('$0 src -o dist', '') .example('$0 -p styles/**/*.icss -w', '') @@ -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 { + 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; @@ -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, @@ -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); } diff --git a/src/dtsCreator.ts b/src/dtsCreator.ts deleted file mode 100644 index 9caa2c81..00000000 --- a/src/dtsCreator.ts +++ /dev/null @@ -1,194 +0,0 @@ -import * as process from 'process'; -import * as fs from 'fs'; -import * as path from'path'; - -import isThere from 'is-there'; -import * as mkdirp from 'mkdirp'; -import camelcase from "camelcase" - -import FileSystemLoader from './fileSystemLoader'; -import * as os from 'os'; - -function removeExtension(filePath: string): string { - const ext = path.extname(filePath); - return filePath.replace(new RegExp(ext + '$'), ''); -} - -interface DtsContentOptions { - dropExtension: boolean; - rootDir: string; - searchDir: string; - outDir: string; - rInputPath: string; - rawTokenList: string[]; - resultList: string[]; - EOL: string; -} - -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 writeFile(): Promise { - var outPathDir = path.dirname(this.outputFilePath); - if(!isThere(outPathDir)) { - mkdirp.sync(outPathDir); - } - return new Promise((resolve, reject) => { - fs.writeFile(this.outputFilePath, this.formatted, 'utf8', (err) => { - if(err) { - reject(err); - }else{ - resolve(this); - } - }); - }); - } -} - -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; - } - - create(filePath: string, initialContents?: string, clearCache: boolean = false): Promise { - return new Promise((resolve, reject) => { - 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 = {}; - } - this.loader.fetch(filePath, "/", undefined, initialContents).then((res) => { - if(res) { - var tokens = res; - var keys = Object.keys(tokens); - - var convertKey = this.getConvertKeyMethod(this.camelCase); - - var result = keys - .map(k => convertKey(k)) - .map(k => 'readonly "' + k + '": string;') - - var content = new DtsContent({ - dropExtension: this.dropExtension, - rootDir: this.rootDir, - searchDir: this.searchDir, - outDir: this.outDir, - rInputPath, - rawTokenList: keys, - resultList: result, - EOL: this.EOL - }); - - resolve(content); - }else{ - reject(res); - } - }).catch(err => reject(err)); - }); - } - - 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(); - }); - } - - -} diff --git a/src/fileSystemLoader.ts b/src/fileSystemLoader.ts deleted file mode 100644 index c411ea30..00000000 --- a/src/fileSystemLoader.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* 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 { Plugin } from "postcss"; - -// Sorts dependencies in the following way: -// AAA comes before AA and A -// AB comes after AA and before A -// All Bs come after all As -// This ensures that the files are always returned in the following order: -// - In the order they were required, except -// - After all their dependencies -const traceKeySorter = ( a: string, b: string ): number => { - if ( a.length < b.length ) { - return a < b.substring( 0, a.length ) ? -1 : 1 - } else if ( a.length > b.length ) { - return a.substring( 0, b.length ) <= b ? -1 : 1 - } else { - return a < b ? -1 : 1 - } -}; - -export type Dictionary = { - [key: string]: T | undefined; -}; - -export default class FileSystemLoader { - private root: string; - private sources: Dictionary; - private importNr: number; - private core: Core; - public tokensByFile: Dictionary; - - constructor( root: string, plugins?: Array> ) { - this.root = root - this.sources = {} - this.importNr = 0 - this.core = new Core(plugins) - this.tokensByFile = {}; - } - - public fetch( _newPath: string, relativeTo: string, _trace?: string, initialContents?: string ): Promise { - let newPath = _newPath.replace( /^["']|["']$/g, "" ), - trace = _trace || String.fromCharCode( this.importNr++ ) - return new Promise( ( resolve, reject ) => { - let relativeDir = path.dirname( relativeTo ), - rootRelativePath = path.resolve( relativeDir, newPath ), - 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) {} - } - - if(!initialContents) { - const tokens = this.tokensByFile[fileRelativePath] - if (tokens) { return resolve(tokens) } - - fs.readFile( fileRelativePath, "utf-8", ( err, source ) => { - if ( err && relativeTo && relativeTo !== '/') { - resolve({}); - }else if ( err && (!relativeTo || relativeTo === '/')) { - reject(err); - }else{ - this.core.load( source, rootRelativePath, trace, this.fetch.bind( this ) ) - .then( ( { injectableSource, exportTokens } ) => { - this.sources[trace] = injectableSource - this.tokensByFile[fileRelativePath] = exportTokens - resolve( exportTokens ) - }, reject ) - } - } ) - }else{ - this.core.load( initialContents, rootRelativePath, trace, this.fetch.bind(this) ) - .then( ( { injectableSource, exportTokens } ) => { - this.sources[trace] = injectableSource - this.tokensByFile[fileRelativePath] = exportTokens - resolve( exportTokens ) - }, reject ) - } - } ) - } - - private get finalSource(): string { - return Object.keys( this.sources ).sort( traceKeySorter ).map( s => this.sources[s] ) - .join( "" ) - } - - private clear(): FileSystemLoader { - this.tokensByFile = {}; - return this; - } -} diff --git a/src/index.ts b/src/index.ts index f76ffac9..729f3986 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -import { DtsCreator } from './dtsCreator'; +import { DtsCreator } from './DtsCreator'; export = DtsCreator; diff --git a/test/dtsCreator.spec.js b/test/dtsCreator.spec.js index 51b812c9..9f3757b4 100644 --- a/test/dtsCreator.spec.js +++ b/test/dtsCreator.spec.js @@ -2,7 +2,7 @@ var path = require('path'); var assert = require('assert'); -var DtsCreator = require('../lib/dtsCreator').DtsCreator; +var DtsCreator = require('../lib/DtsCreator').DtsCreator; var os = require('os'); describe('DtsCreator', () => { @@ -190,4 +190,4 @@ export = styles; }); }); }); -}); \ No newline at end of file +});