From e4c043acdfba61e100ebd1dabb6f5fff0eb1dd1d Mon Sep 17 00:00:00 2001 From: Luca Colonnello Date: Wed, 6 Jan 2021 14:51:00 +0000 Subject: [PATCH 1/2] implement named exports option This option creates named exports in the output as opposed to using a default export. This is intended to be used in combination with the `namedExports` options in webpack's `css-loader` and `style-loader` config, which enable proper tree shaking of the final CSS. --- src/cli.ts | 2 ++ src/dts-content.ts | 62 ++++++++++++++++++++++++++++++++++++++-- src/dts-creator.ts | 44 +++++----------------------- src/run.ts | 2 ++ test/dts-creator.spec.ts | 26 +++++++++++++++++ 5 files changed, 97 insertions(+), 39 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 7b9e338..10a4e8a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,6 +14,7 @@ const yarg = yargs.usage('Create .css.d.ts from CSS modules *.css files.\nUsage: .alias('o', 'outDir').describe('o', 'Output directory').string("o") .alias('w', 'watch').describe('w', 'Watch input directory\'s css files or pattern').boolean('w') .alias('c', 'camelCase').describe('c', 'Convert CSS class tokens to camelcase').boolean("c") + .alias('e', 'namedExports').describe('e', 'Use named exports as opposed to default exports to enable tree shaking.').boolean("e") .alias('d', 'dropExtension').describe('d', 'Drop the input files extension').boolean('d') .alias('s', 'silent').describe('s', 'Silent output. Do not show "files written" messages').boolean('s') .alias('h', 'help').help('h') @@ -45,6 +46,7 @@ async function main(): Promise { outDir: argv.o, watch: argv.w, camelCase: argv.c, + namedExports: argv.e, dropExtension: argv.d, silent: argv.s }); diff --git a/src/dts-content.ts b/src/dts-content.ts index 6b22d35..a9ad456 100644 --- a/src/dts-content.ts +++ b/src/dts-content.ts @@ -4,10 +4,13 @@ import * as path from "path"; import isThere from "is-there"; import * as mkdirp from 'mkdirp'; import * as util from "util"; +import camelcase from "camelcase"; const writeFile = util.promisify(fs.writeFile); const readFile = util.promisify(fs.readFile); +export type CamelCaseOption = boolean | 'dashes' | undefined; + interface DtsContentOptions { dropExtension: boolean; rootDir: string; @@ -15,7 +18,8 @@ interface DtsContentOptions { outDir: string; rInputPath: string; rawTokenList: string[]; - resultList: string[]; + namedExports: boolean; + camelCase: CamelCaseOption; EOL: string; } @@ -26,6 +30,8 @@ export class DtsContent { private outDir: string; private rInputPath: string; private rawTokenList: string[]; + private namedExports: boolean; + private camelCase: CamelCaseOption; private resultList: string[]; private EOL: string; @@ -36,8 +42,19 @@ export class DtsContent { this.outDir = options.outDir; this.rInputPath = options.rInputPath; this.rawTokenList = options.rawTokenList; - this.resultList = options.resultList; + this.namedExports = options.namedExports; + this.camelCase = options.camelCase; this.EOL = options.EOL; + + // when using named exports, camelCase must be enabled by default + // (see https://webpack.js.org/loaders/css-loader/#namedexport) + // we still accept external control for the 'dashes' option, + // so we only override in case is false or undefined + if (this.namedExports && !this.camelCase) { + this.camelCase = true; + } + + this.resultList = this.createResultList(); } public get contents(): string[] { @@ -46,6 +63,14 @@ export class DtsContent { public get formatted(): string { if(!this.resultList || !this.resultList.length) return ''; + + if (this.namedExports) { + return [ + ...this.resultList.map(line => 'export ' + line), + '' + ].join(os.EOL) + this.EOL; + } + return [ 'declare const styles: {', ...this.resultList.map(line => ' ' + line), @@ -92,6 +117,39 @@ export class DtsContent { await writeFile(this.outputFilePath, finalOutput, 'utf8'); } } + + private createResultList(): string[] { + const convertKey = this.getConvertKeyMethod(this.camelCase); + + const result = this.rawTokenList + .map(k => convertKey(k)) + .map(k => !this.namedExports ? 'readonly "' + k + '": string;' : 'const ' + k + ': string;') + + return result; + } + + 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(); + }); + } } function removeExtension(filePath: string): string { diff --git a/src/dts-creator.ts b/src/dts-creator.ts index 20ca229..7073a03 100644 --- a/src/dts-creator.ts +++ b/src/dts-creator.ts @@ -1,19 +1,17 @@ import * as process from 'process'; import * as path from'path'; import * as os from 'os'; -import camelcase from "camelcase" import FileSystemLoader from './file-system-loader'; -import {DtsContent} from "./dts-content"; +import {DtsContent, CamelCaseOption} from "./dts-content"; import {Plugin} from "postcss"; -type CamelCaseOption = boolean | 'dashes' | undefined; - interface DtsCreatorOptions { rootDir?: string; searchDir?: string; outDir?: string; camelCase?: CamelCaseOption; + namedExports?: boolean; dropExtension?: boolean; EOL?: string; loaderPlugins?: Plugin[]; @@ -26,7 +24,8 @@ export class DtsCreator { private loader: FileSystemLoader; private inputDirectory: string; private outputDirectory: string; - private camelCase: boolean | 'dashes' | undefined; + private camelCase: CamelCaseOption; + private namedExports: boolean; private dropExtension: boolean; private EOL: string; @@ -39,6 +38,7 @@ export class DtsCreator { this.inputDirectory = path.join(this.rootDir, this.searchDir); this.outputDirectory = path.join(this.rootDir, this.outDir); this.camelCase = options.camelCase; + this.namedExports = !!options.namedExports; this.dropExtension = !!options.dropExtension; this.EOL = options.EOL || os.EOL; } @@ -59,12 +59,6 @@ export class DtsCreator { 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, @@ -72,7 +66,8 @@ export class DtsCreator { outDir: this.outDir, rInputPath, rawTokenList: keys, - resultList: result, + namedExports: this.namedExports, + camelCase: this.camelCase, EOL: this.EOL }); @@ -81,29 +76,4 @@ export class DtsCreator { 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/run.ts b/src/run.ts index 2a32c93..83b8627 100644 --- a/src/run.ts +++ b/src/run.ts @@ -14,6 +14,7 @@ interface RunOptions { outDir?: string; watch?: boolean; camelCase?: boolean; + namedExports?: boolean; dropExtension?: boolean; silent?: boolean; } @@ -26,6 +27,7 @@ export async function run(searchDir: string, options: RunOptions = {}): Promise< searchDir, outDir: options.outDir, camelCase: options.camelCase, + namedExports: options.namedExports, dropExtension: options.dropExtension, }); diff --git a/test/dts-creator.spec.ts b/test/dts-creator.spec.ts index de62670..0a6214a 100644 --- a/test/dts-creator.spec.ts +++ b/test/dts-creator.spec.ts @@ -109,6 +109,32 @@ declare const styles: { }; export = styles; +` + ); + done(); + }); + }); + + it('returns named exports formatted .d.ts string', done => { + new DtsCreator({ namedExports: true }).create('test/testStyle.css').then(content => { + assert.equal( + content.formatted, + `\ +export const myClass: string; + +` + ); + done(); + }); + }); + + it('returns camelcase names when using named exports as formatted .d.ts string', done => { + new DtsCreator({ namedExports: true }).create('test/kebabedUpperCase.css').then(content => { + assert.equal( + content.formatted, + `\ +export const myClass: string; + ` ); done(); From 1c2d195d161c0b31b4219c2dd32b4f7724ca0b36 Mon Sep 17 00:00:00 2001 From: Luca Colonnello Date: Wed, 6 Jan 2021 14:56:01 +0000 Subject: [PATCH 2/2] document named exports --- README.md | 41 ++++++++++++++++++++++++++++++++++++++--- example/package.json | 4 ++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 72fc82c..d8e4805 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ So, you can import CSS modules' class or variable into your TypeScript sources: ```ts /* app.ts */ -import * as styles from './styles.css'; +import styles from './styles.css'; console.log(`
`); console.log(`
`); ``` @@ -102,11 +102,45 @@ webpack `css-loader`. This will keep upperCase class names intact, e.g.: becomes ```typescript -export const SomeComponent: string; +declare const styles: { + readonly "SomeComponent": string; +}; +export = styles; ``` See also [webpack css-loader's camelCase option](https://github.com/webpack/css-loader#camelcase). +#### named exports (enable tree shaking) +With `-e` or `--namedExports`, types are exported as named exports as opposed to default exports. +This enables support for the `namedExports` css-loader feature, required for webpack to tree shake the final CSS (learn more [here](https://webpack.js.org/loaders/css-loader/#namedexport)). + +Use this option in combination with https://webpack.js.org/loaders/css-loader/#namedexport and https://webpack.js.org/loaders/style-loader/#namedexport (if you use `style-loader`). + +When this option is enabled, the type definition changes to support named exports. + +*NOTE: this option enables camelcase by default.* + +```css +.SomeComponent { + height: 10px; +} +``` + +**Standard output:** + +```typescript +declare const styles: { + readonly "SomeComponent": string; +}; +export = styles; +``` + +**Named exports output:** + +```typescript +export const someComponent: string; +``` + ## API ```sh @@ -133,6 +167,7 @@ You can set the following options: * `option.searchDir`: Directory which includes target `*.css` files(default: `'./'`). * `option.outDir`: Output directory(default: `option.searchDir`). * `option.camelCase`: Camelize CSS class tokens. +* `option.namedExports`: Use named exports as opposed to default exports to enable tree shaking. Requires `import * as style from './file.module.css';` (default: `false`) * `option.EOL`: EOL (end of line) for the generated `d.ts` files. Possible values `'\n'` or `'\r\n'`(default: `os.EOL`). #### `create(filepath, contents) => Promise(dtsContent)` @@ -182,7 +217,7 @@ e.g. `['my-class is not valid TypeScript variable name.']`. Final output file path. ## Remarks -If your input CSS file has the followng class names, these invalid tokens are not written to output `.d.ts` file. +If your input CSS file has the following class names, these invalid tokens are not written to output `.d.ts` file. ```css /* TypeScript reserved word */ diff --git a/example/package.json b/example/package.json index ae09cb9..a37f912 100644 --- a/example/package.json +++ b/example/package.json @@ -4,8 +4,8 @@ "description": "", "main": "app.js", "scripts": { - "tcm": "node ../lib/cli.js -p style01.css", - "tcmw": "node ../lib/cli.js -w -p style01.css", + "tcm": "node ../lib/cli.js -e -p style01.css", + "tcmw": "node ../lib/cli.js -e -w -p style01.css", "compile": "npm run tcm && ./node_modules/.bin/tsc -p .", "bundle": "npm run compile && ./node_modules/.bin/browserify -o bundle.js -p [ css-modulesify -o bundle.css ] app.js", "start": "npm run bundle && node bundle.js"