vite-plugin-sass-dts
Version:
A plugin that automatically creates a type file when using the css module type-safely.
480 lines (466 loc) • 17 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
default: () => Plugin
});
module.exports = __toCommonJS(index_exports);
// node_modules/.pnpm/tsup@8.4.0_postcss@8.5.3_typescript@5.8.2/node_modules/tsup/assets/cjs_shims.js
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href;
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
// src/index.ts
var import_prettier2 = __toESM(require("prettier"), 1);
var import_vite2 = require("vite");
// src/main.ts
var import_fs2 = __toESM(require("fs"), 1);
var import_postcss = require("postcss");
var import_postcss_js = require("postcss-js");
// src/util.ts
var import_node_path = __toESM(require("path"), 1);
var cssLangs = `\\.(css|pcss|sass|scss)($|\\?)`;
var cssLangReg = new RegExp(cssLangs);
var cssModuleReg = new RegExp(`\\.module${cssLangs}`);
var importCssRE = /@import ('[^']+\.css'|"[^"]+\.css"|[^'")]+\.css)/;
var sameDirRE = /^[./]/;
var isCSSModuleRequest = (request) => cssModuleReg.test(request);
var getRelativePath = (from, to) => {
let relativePath = import_node_path.default.relative(from || "", to || "") || "./";
if (import_node_path.default.sep !== "/") {
relativePath = relativePath.replaceAll(import_node_path.default.sep, "/");
}
relativePath = sameDirRE.test(relativePath) ? relativePath : `./${relativePath}`;
return !relativePath.endsWith("/") ? `${relativePath}/` : relativePath;
};
var toDashCase = (target) => target.replace(/[-_ /~ . ][A-z0-9]/g, (v) => {
return "-" + v.slice(1);
}).toLowerCase();
var toCamelCase = (target) => target.replace(/^[A-Z]/, (m) => m.toLowerCase()).replace(/[-_ ./~ ]+([A-z0-9])/g, (m, $1) => $1.toUpperCase());
var isSassException = (e) => typeof e === "object" && !!e && "file" in e;
var collectionToObj = (collection) => {
return collection.reduce((acc, item) => {
return { ...acc, ...item };
}, {});
};
// src/options.ts
var getParseCase = (config) => {
if (!config.css || !config.css.modules || !config.css.modules.localsConvention) {
return;
}
const { localsConvention } = config.css.modules;
if (localsConvention === "camelCase" || localsConvention === "camelCaseOnly") {
return toCamelCase;
} else if (localsConvention === "dashes" || localsConvention === "dashesOnly") {
return toDashCase;
}
return;
};
var getPreprocessorOptions = (config) => {
let additionalData, includePaths, importer;
if (!config.css || !config.css.preprocessorOptions || !config.css.preprocessorOptions.scss) {
return {
additionalData,
includePaths,
importer,
alias: config.resolve.alias
};
}
return config.css.preprocessorOptions.scss;
};
// src/css.ts
var import_node_module = require("module");
var import_vite = require("vite");
var import_path = __toESM(require("path"), 1);
var import_fs = __toESM(require("fs"), 1);
var import_node_url = require("url");
var SPLIT_STR = `/* vite-plugin-sass-dts */
`;
var loadedSassPreprocessor;
var _require = importMetaUrl ? (0, import_node_module.createRequire)(importMetaUrl) : require;
var parseCss = async (file, fileName, config) => {
const sass = loadSassPreprocessor(config);
const options = getPreprocessorOptions(config);
const resolveFn = config.createResolver({
extensions: [".scss", ".sass", ".pcss", ".css"],
mainFields: ["sass", "style"],
tryIndex: true,
tryPrefix: "_",
preferRelative: true
});
const internalImporter = (url, importer, done) => {
resolveFn(url, importer).then((resolved) => {
if (resolved) {
rebaseUrls(resolved, fileName, config.resolve.alias, "$").then(done).catch(done);
} else {
done && done(null);
}
});
};
const data = await getData(file.toString(), fileName, options.additionalData);
const finalImporter = [];
if (options.api !== "modern" && options.api !== "modern-compiler") {
if (options.importer) {
Array.isArray(options.importer) ? finalImporter.push(...options.importer) : finalImporter.push(options.importer);
}
finalImporter.push(internalImporter);
const result = await new Promise((resolve, reject) => {
sass.render(
{
...options,
data,
pkgImporter: new sass.NodePackageImporter(),
file: fileName,
includePaths: ["node_modules"],
importer: finalImporter,
indentedSyntax: fileName.endsWith(".sass")
},
(err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
}
);
});
const splitted = result.css.toString().split(SPLIT_STR);
return { localStyle: splitted[1] || "", globalStyle: splitted[0] };
} else {
if (options.importers) {
Array.isArray(options.importers) ? finalImporter.push(...options.importers) : finalImporter.push(options.importers);
}
const sassOptions = { ...options };
sassOptions.url = (0, import_node_url.pathToFileURL)(fileName);
sassOptions.pkgImporter = new sass.NodePackageImporter();
sassOptions.importers = finalImporter;
const result = await sass.compileStringAsync(data, sassOptions);
const splitted = result.css.toString().split(SPLIT_STR);
return { localStyle: splitted[1] || "", globalStyle: splitted[0] };
}
};
var getData = (data, filename, additionalData) => {
if (!additionalData) return `
${SPLIT_STR}${data}`;
if (typeof additionalData === "function") {
return additionalData(`
${SPLIT_STR}${data}`, filename);
}
return `${additionalData}
${SPLIT_STR}${data}`;
};
var loadSassPreprocessor = (config) => {
try {
if (loadedSassPreprocessor) {
return loadedSassPreprocessor;
}
const fallbackPaths = _require.resolve.paths?.("sass-embedded") || [];
const resolved = _require.resolve("sass-embedded", {
paths: [config.root, ...fallbackPaths]
});
return loadedSassPreprocessor = _require(resolved);
} catch (e) {
console.error(e);
throw new Error(
`Preprocessor dependency 'sass' not found. Did you install it?`
);
}
};
var rebaseUrls = async (file, rootFile, alias, variablePrefix) => {
file = import_path.default.resolve(file);
const fileDir = import_path.default.dirname(file);
const rootDir = import_path.default.dirname(rootFile);
if (fileDir === rootDir) {
return { file };
}
const content = import_fs.default.readFileSync(file, "utf-8");
const hasImportCss = importCssRE.test(content);
if (!hasImportCss) {
return { file };
}
let rebased;
const rebaseFn = (url) => {
if (url.startsWith("/")) return url;
if (url.startsWith(variablePrefix)) return url;
for (const { find } of alias) {
const matches = typeof find === "string" ? url.startsWith(find) : find.test(url);
if (matches) {
return url;
}
}
const absolute = import_path.default.resolve(fileDir, url);
const relative = import_path.default.relative(rootDir, absolute);
return (0, import_vite.normalizePath)(relative);
};
if (hasImportCss) {
rebased = await rewriteImportCss(content, rebaseFn);
}
return {
file,
contents: rebased
};
};
var rewriteImportCss = (css, replacer) => {
return asyncReplace(css, importCssRE, async (match) => {
const [matched, rawUrl] = match;
return await doImportCSSReplace(rawUrl, matched, replacer);
});
};
var asyncReplace = async (input, re, replacer) => {
let match;
let remaining = input;
let rewritten = "";
while (match = re.exec(remaining)) {
rewritten += remaining.slice(0, match.index);
rewritten += await replacer(match);
remaining = remaining.slice(match.index + match[0].length);
}
rewritten += remaining;
return rewritten;
};
var doImportCSSReplace = async (rawUrl, matched, replacer) => {
let wrap = "";
const first = rawUrl[0];
if (first === `"` || first === `'`) {
wrap = first;
rawUrl = rawUrl.slice(1, -1);
}
if (isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl.startsWith("#")) {
return matched;
}
return `@import ${wrap}${await replacer(rawUrl)}${wrap}`;
};
var externalRE = /^(https?:)?\/\//;
var isExternalUrl = (url) => externalRE.test(url);
var dataUrlRE = /^\s*data:/i;
var isDataUrl = (url) => dataUrlRE.test(url);
// src/extract.ts
var importRe = new RegExp(/^(@import|@apply)/);
var supportRe = new RegExp(/^(@support)/);
var keySeparatorRe = new RegExp(/(?=[\s.:[\]><+,()])/g);
var extractClassNameKeys = (obj, toParseCase, parentKey) => {
return Object.entries(obj).reduce(
(curr, [key, value]) => {
if (importRe.test(key)) return curr;
const splitKeys = key.split(keySeparatorRe);
if (!supportRe.test(key)) {
for (const splitKey of splitKeys) {
if (parentKey === ":export" || splitKey.startsWith(".")) {
if (toParseCase) {
curr.set(toParseCase(splitKey.replace(".", "").trim()), true);
} else {
curr.set(splitKey.replace(".", "").trim(), true);
}
}
}
}
if (typeof value === "object" && Object.keys(value).length > 0) {
const valueToExtract = Array.isArray(value) ? collectionToObj(value) : value;
const map = extractClassNameKeys(valueToExtract, toParseCase, key);
for (const key2 of map.keys()) {
if (toParseCase) {
curr.set(toParseCase(key2), true);
} else {
curr.set(key2, true);
}
}
}
return curr;
},
/* @__PURE__ */ new Map()
);
};
// src/write.ts
var import_node_fs = require("fs");
var import_node_path2 = require("path");
var import_prettier = __toESM(require("prettier"), 1);
var import_path2 = __toESM(require("path"), 1);
var import_promises = require("fs/promises");
var { format } = import_prettier.default;
var writeToFile = async (prettierOptions, fileName, classNameKeys, options) => {
const baseName = import_path2.default.basename(fileName);
const typeName = getReplacerResult(baseName, options?.typeName);
const exportName = getReplacerResult(baseName, options?.exportName) ?? "classNames";
let exportTypes = "";
let namedExports = "";
const exportStyle = options?.esmExport ? `export default ${exportName};` : `export = ${exportName};`;
for (const classNameKey of classNameKeys.keys()) {
exportTypes = `${exportTypes}
${formatExportType(classNameKey, typeName)}`;
namedExports = `${namedExports}
export const ${classNameKey}: '${typeName ?? classNameKey}';`;
}
let outputFileString = "";
if (options?.global?.outputFilePath) {
const relativePath = getRelativePath(
(0, import_node_path2.dirname)(fileName),
(0, import_node_path2.dirname)(options.global.outputFilePath)
);
const exportTypeFileName = formatExportTypeFileName(
options.global.outputFilePath
);
outputFileString = `import globalClassNames from '${relativePath}${exportTypeFileName}'
`;
outputFileString = `declare const ${exportName}: typeof globalClassNames & {${exportTypes}
};
${exportStyle}`;
if (options?.useNamedExport) {
outputFileString = `${outputFileString}
${namedExports}
`;
}
} else {
outputFileString = `declare const ${exportName}: {${exportTypes}
};
${exportStyle}`;
if (options?.useNamedExport) {
outputFileString = `${outputFileString}
${namedExports}`;
}
}
const prettierdOutputFileString = await format(
outputFileString,
prettierOptions
);
const writePath = formatWriteFilePath(fileName, options);
await ensureDirectoryExists(writePath);
(0, import_node_fs.writeFile)(writePath, `${prettierdOutputFileString}`, (err) => {
if (err) {
console.log(err);
throw err;
}
});
};
var getReplacerResult = (fileName, replacer) => {
if (replacer && replacer.replacement) {
if (typeof replacer.replacement === "function") {
return replacer.replacement(fileName);
} else {
return replacer.replacement;
}
}
return void 0;
};
var formatExportType = (key, type = `'${key}'`) => ` readonly '${key}': ${type};`;
var formatWriteFilePath = (file, options) => {
let path4 = file;
const src = options?.sourceDir;
const dist = options?.outputDir;
if (src && !(0, import_node_path2.isAbsolute)(src)) {
throw new Error("vite-plugin-sass-dts sourceDir must be an absolute path");
}
if (dist && !(0, import_node_path2.isAbsolute)(dist)) {
throw new Error("vite-plugin-sass-dts outputDir must be an absolute path");
}
if (src && dist) {
path4 = path4.replace(src, dist);
}
return formatWriteFileName(path4);
};
var formatWriteFileName = (file) => `${file}${file.endsWith("d.ts") ? "" : ".d.ts"}`;
var formatExportTypeFileName = (file) => (0, import_node_path2.basename)(file.replace(".ts", ""));
var ensureDirectoryExists = async (file) => {
await (0, import_promises.mkdir)((0, import_node_path2.dirname)(file), { recursive: true });
};
// src/main.ts
var main = (fileName, config, option) => {
try {
import_fs2.default.readFile(fileName, async (err, file) => {
if (err) {
console.error(err);
} else {
try {
const css = fileName.endsWith(".css") ? { localStyle: file.toString() } : await parseCss(file, fileName, config);
const toParseCase = getParseCase(config);
const classNameKeys = extractClassNameKeys(
(0, import_postcss_js.objectify)((0, import_postcss.parse)(css.localStyle)),
toParseCase
);
writeToFile(config.prettierOptions, fileName, classNameKeys, option);
if (!!css.globalStyle && option.global?.generate) {
const globalClassNameKeys = extractClassNameKeys(
(0, import_postcss_js.objectify)((0, import_postcss.parse)(css.globalStyle)),
toParseCase
);
writeToFile(
config.prettierOptions,
option.global.outputFilePath,
globalClassNameKeys,
{ esmExport: option.esmExport }
);
}
} catch (e) {
if (isSassException(e)) {
if (e.name !== fileName) {
console.error("e :>> ", e);
}
}
}
}
});
} catch (e) {
console.error("e :>> ", e);
}
};
// src/index.ts
var { resolveConfig } = import_prettier2.default;
function Plugin(option = {}) {
let cacheConfig;
let filter;
const enabledMode = option.enabledMode || ["development"];
return {
name: "vite-plugin-sass-dts",
async configResolved(config) {
filter = (0, import_vite2.createFilter)(void 0, option.excludePath);
const prettierOptions = await resolveConfig(option.prettierFilePath || config.root) || {};
cacheConfig = {
...config,
prettierOptions: { ...prettierOptions, filepath: "*.d.ts" }
};
},
handleHotUpdate(context) {
if (!isCSSModuleRequest(context.file) || !filter(context.file)) return;
main(context.file, cacheConfig, option);
return;
},
transform(code, id) {
const fileName = id.replace(/(?:\?|&)(used|direct|inline|vue).*/, "");
if (!enabledMode.includes(cacheConfig.env.MODE) || !isCSSModuleRequest(fileName) || !filter(id)) {
return void 0;
}
return new Promise(
(resolve) => resolve(main(fileName, cacheConfig, option))
);
},
watchChange(id) {
if (isCSSModuleRequest(id) && filter(id)) {
this.addWatchFile(id);
}
}
};
}
;