terriajs-typings-for-css-modules-loader
Version:
Webpack loader that generates TypeScript typings for CSS modules from css-loader on the fly
163 lines (142 loc) • 4.21 kB
JavaScript
// @ts-check
const {
filenameToPascalCase,
filenameToTypingsFilename,
getCssModuleKeys,
generateGenericExportInterface,
} = require("./utils");
const persist = require("./persist");
const verify = require("./verify");
const { getOptions } = require("loader-utils");
const validateOptions = require("schema-utils");
const schema = {
type: "object",
properties: {
eol: {
description:
"Newline character to be used in generated d.ts files. Uses OS default. This option is overridden by the formatter option.",
type: "string",
},
banner: {
description: "To add a 'banner' prefix to each generated `*.d.ts` file",
type: "string",
},
formatter: {
description:
"Possible options: none and prettier (requires prettier package installed). Defaults to prettier if `prettier` module can be resolved",
enum: ["prettier", "none"],
},
disableLocalsExport: {
description: "Disable the use of locals export. Defaults to `false`",
type: "boolean",
},
verifyOnly: {
description:
"Validate generated `*.d.ts` files and fail if an update is needed (useful in CI). Defaults to `false`",
type: "boolean",
},
prettierConfigFile: {
description:
"Path to prettier config file",
type: "string",
}
},
additionalProperties: false,
};
/** @type {any} */
const configuration = {
name: "typings-for-css-modules-loader",
baseDataPath: "options",
};
/** @type {((this: import('webpack').loader.LoaderContext, ...args: any[]) => void) & {pitch?: import('webpack').loader.Loader['pitch']}} */
module.exports = function (content, ...args) {
const options = getOptions(this) || {};
validateOptions(schema, options, configuration);
if (this.cacheable) {
this.cacheable();
}
const cssModuleKeys = getCssModuleKeys(content);
/** @type {any} */
const callback = this.async();
const successfulCallback = () => {
callback(null, content, ...args);
};
if (cssModuleKeys.length === 0) {
// no css module output found
successfulCallback();
return;
}
const filename = this.resourcePath;
const cssModuleInterfaceFilename = filenameToTypingsFilename(filename);
const cssModuleDefinition = generateGenericExportInterface(
cssModuleKeys,
filenameToPascalCase(filename),
options.disableLocalsExport
);
applyFormattingAndOptions(cssModuleDefinition, options)
.then((output) => {
if (options.verifyOnly === true) {
return verify(cssModuleInterfaceFilename, output);
} else {
persist(cssModuleInterfaceFilename, output);
}
})
.catch((err) => {
this.emitError(err);
})
.then(successfulCallback);
};
/**
* @param {string} cssModuleDefinition
* @param {any} options
*/
async function applyFormattingAndOptions(cssModuleDefinition, options) {
if (options.banner) {
// Prefix banner to CSS module
cssModuleDefinition = options.banner + "\n" + cssModuleDefinition;
}
if (
options.formatter === "prettier" ||
(!options.formatter && canUsePrettier())
) {
cssModuleDefinition = await applyPrettier(cssModuleDefinition, options);
} else {
// at very least let's ensure we're using OS eol if it's not provided
cssModuleDefinition = cssModuleDefinition.replace(
/\r?\n/g,
options.eol || require("os").EOL
);
}
return cssModuleDefinition;
}
/**
* @param {string} input
* @param {any} options
* @returns {Promise<string>}
*/
async function applyPrettier(input, options) {
const prettier = require("prettier");
const configPath = options.prettierConfigFile ? options.prettierConfigFile : "./";
const config = await prettier.resolveConfig(configPath, {
editorconfig: true,
});
return prettier.format(
input,
Object.assign({}, config, { parser: "typescript" })
);
}
let isPrettierInstalled;
/**
* @returns {boolean}
*/
function canUsePrettier() {
if (typeof isPrettierInstalled !== "boolean") {
try {
require.resolve("prettier");
isPrettierInstalled = true;
} catch (_) {
isPrettierInstalled = false;
}
}
return isPrettierInstalled;
}