Skip to content

Commit

Permalink
Merge pull request #100 from LucaColonnello/named-exports
Browse files Browse the repository at this point in the history
Add named exports output format
  • Loading branch information
Quramy authored Jan 7, 2021
2 parents 01723c7 + 1c2d195 commit f7efbf2
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 44 deletions.
41 changes: 38 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<div class="${styles.myClass}"></div>`);
console.log(`<div style="color: ${styles.primary}"></div>`);
```
Expand Down Expand Up @@ -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
Expand All @@ -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)`
Expand Down Expand Up @@ -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 */
Expand Down
4 changes: 2 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -45,6 +46,7 @@ async function main(): Promise<void> {
outDir: argv.o,
watch: argv.w,
camelCase: argv.c,
namedExports: argv.e,
dropExtension: argv.d,
silent: argv.s
});
Expand Down
62 changes: 60 additions & 2 deletions src/dts-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ 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;
searchDir: string;
outDir: string;
rInputPath: string;
rawTokenList: string[];
resultList: string[];
namedExports: boolean;
camelCase: CamelCaseOption;
EOL: string;
}

Expand All @@ -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;

Expand All @@ -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[] {
Expand All @@ -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),
Expand Down Expand Up @@ -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 {
Expand Down
44 changes: 7 additions & 37 deletions src/dts-creator.ts
Original file line number Diff line number Diff line change
@@ -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<any>[];
Expand All @@ -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;

Expand All @@ -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;
}
Expand All @@ -59,20 +59,15 @@ 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,
searchDir: this.searchDir,
outDir: this.outDir,
rInputPath,
rawTokenList: keys,
resultList: result,
namedExports: this.namedExports,
camelCase: this.camelCase,
EOL: this.EOL
});

Expand All @@ -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();
});
}


}
2 changes: 2 additions & 0 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface RunOptions {
outDir?: string;
watch?: boolean;
camelCase?: boolean;
namedExports?: boolean;
dropExtension?: boolean;
silent?: boolean;
}
Expand All @@ -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,
});

Expand Down
26 changes: 26 additions & 0 deletions test/dts-creator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit f7efbf2

Please sign in to comment.