diff --git a/README.md b/README.md index b2eff9e..7d28901 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,52 @@ e.g.: ``` ### `banner`-option -To add a "banner" prefix to each generated `*.d.ts` file, you can pass a string to this option as shown below. The prefix is quite literally prefixed into the generated file, so please ensure it conforms to the type definition syntax. +To add a "banner" prefix to each generated `.d.ts` file, you can pass a string to this option as shown below. The prefix is quite literally prefixed into the generated file, so please ensure it conforms to the type definition syntax. ```js { test: /\.css$/, loader: 'typings-for-css-modules?banner="// This file is automatically generated by typings-for-css-modules.\n// Please do not change this file!"' } ``` +### `pathPrefix`-option +By default, `.d.ts` files are generated in the same directory as the CSS module source. To place them in a different directory, pass the `pathPrefix` option as a string or function: +* String: + * The path prefix must i) not be absolute, and ii) the imported CSS module must lie within the [webpack context](https://webpack.github.io/docs/configuration.html#context). Otherwise the `.d.ts` will be generated in the same directory as the CSS module, as though the `pathPrefix` option was ignored. The reason here is that there isn't a straightforward compiler configuration that will allow TypeScript to find the type definition files in the above two cases. + * Paths satisfying the above constraint result in generated `.d.ts` files placed in a directory of the given name relative to the webpack context, wedged before the module path (see example, next, and compiler configuration below). + * For example, if your webpack context is `./src`, the `pathPrefix` option is `generated/styles`, and your CSS module is imported as `component/widget.css`, then the generated file will be `./src/generated/styles/component/widget.d.ts`. +* Function: + * You have full control of the generated `.d.ts` path and filename. + * The function interface is given by `(sourcePath: string, contextPath: string) => string` where `sourcePath` is the path to the source CSS module, and `contextPath` is the webpack context path. The function must return the full path (including filename) of the generated `.d.ts` file, or `undefined` to abort with error. The loader will create any intermediate folders as is required to write the generated file. + +When placing the `.d.ts` files in a directory separate to the CSS module, you also need to configure the TypeScript compiler to find these type definitions. You can do this by adding a `paths` section to your tsconfig.json `compilerOptions`. + +For example, say we have a project having all `.ts` files in the `src` tree, and where generated `.d.ts` files should be placed into the `src/generated` tree. Our basic `tssconfig.json` is then as follows: +```json +{ + "compilerOptions": { + "baseUrl": "./src", + "paths": { + "*": [ + "*", + "generated/*" + ] + } + } +} +``` +where it the [webpack context](https://webpack.github.io/docs/configuration.html#context) is also configured to be `path.resolve(__dirname, "src")`. + +Finally, when importing your CSS modules, use a path relative to your `baseUrl` path for import. For example, use +```ts +// [./src/component/widget.ts] +import * as styles from "component/widget.css"; // do this +``` +instead of +```ts +// [./src/component/widget.ts] +import * as styles from "./widget.css"; // don't do this +``` +otherwise TypeScript may not search the configured paths for the associated `.d.ts` file. + ## Usage Keep your `webpack.config` as is just instead of using `css-loader` use `typings-for-css-modules-loader` diff --git a/src/cssModuleToInterface.js b/src/cssModuleToInterface.js index 5c8ccb2..f43505c 100644 --- a/src/cssModuleToInterface.js +++ b/src/cssModuleToInterface.js @@ -28,10 +28,52 @@ export const filterNonWordClasses = (cssModuleKeys) => { return [filteredClassNames, nonWordClassNames,]; }; -export const filenameToTypingsFilename = (filename) => { - const dirName = path.dirname(filename); - const baseName = path.basename(filename); - return path.join(dirName, `${baseName}.d.ts`); +export const filenameToTypingsFilename = (filename, pathPrefix, contextPath) => { + // Default implementation: co-locate .d.ts with the resource + const pathPrefixCallbackDefault = function (filename) { + const dirName = path.dirname(filename); + const baseName = path.basename(filename); + return path.join(dirName, baseName + '.d.ts'); + }; + + let pathPrefixCallback = pathPrefixCallbackDefault; + + if (pathPrefix) { + switch (typeof pathPrefix) { + case 'string': { + if (pathPrefix.endsWith(path.sep)) { + pathPrefix = pathPrefix.slice(0, -1); + } + let dirName = path.dirname(filename); + const baseName = path.basename(filename); + // There are three cases here: + // 1. filename is within the webpack context: add the prefix + if (dirName.startsWith(contextPath)) { + dirName = dirName.slice(contextPath.length); + pathPrefixCallback = function () { + return path.join(contextPath, pathPrefix, dirName, baseName + '.d.ts'); + }; + } + // 2. pathPrefix is absolute + // 3. filename is outside the webpack context + else { + // Use the default implementation for co-locating the .d.ts + } + break; + } + + case 'function': + // Use the user-configured filename mapping function + pathPrefixCallback = pathPrefix; + break; + + default: + // Bad configuration + return undefined; + } + } + + return pathPrefixCallback(filename, contextPath); }; export const generateNamedExports = (cssModuleKeys) => { diff --git a/src/index.js b/src/index.js index 9893f4c..36074e1 100644 --- a/src/index.js +++ b/src/index.js @@ -38,13 +38,16 @@ module.exports = function(...input) { return callback(err); } const filename = this.resourcePath; - const cssModuleInterfaceFilename = filenameToTypingsFilename(filename); + const cssModuleInterfaceFilename = filenameToTypingsFilename(filename, query.pathPrefix, this.options.context); + if (!cssModuleInterfaceFilename) { + return callback('invalid typings filename; check your typings-for-css-modules loader configuration.'); + } const keyRegex = /"([^\\"]+)":/g; let match; const cssModuleKeys = []; - while (match = keyRegex.exec(content)) { + while ((match = keyRegex.exec(content))) { if (cssModuleKeys.indexOf(match[1]) < 0) { cssModuleKeys.push(match[1]); } diff --git a/src/persist.js b/src/persist.js index 5609a9b..a7de0be 100644 --- a/src/persist.js +++ b/src/persist.js @@ -1,7 +1,22 @@ import fs from 'graceful-fs'; import os from 'os'; +import path from 'path'; export const writeToFileIfChanged = (filename, content) => { + // Create the containing folder, as required + const dirName = path.dirname(filename); + if (!fs.existsSync(dirName)) { + const sep = path.sep; + const initDir = path.isAbsolute(dirName) ? sep : ''; + dirName.split(sep).reduce(function (parentDir, childDir) { + const curDir = path.resolve(parentDir, childDir); + if (!fs.existsSync(curDir)) { + fs.mkdirSync(curDir); + } + return curDir; + }, initDir); + } + if (fs.existsSync(filename)) { const currentInput = fs.readFileSync(filename, 'utf-8');