node-sass-magic-importer
Version:
Custom node-sass importer for selector specific imports, node importing, module importing, globbing support and importing files only once
174 lines (146 loc) • 5.31 kB
text/typescript
import * as cssNodeExtract from 'css-node-extract';
import * as cssSelectorExtract from 'css-selector-extract';
import * as findupSync from 'findup-sync';
import * as fs from 'fs';
import * as hash from 'object-hash';
import * as path from 'path';
import * as postcssSyntax from 'postcss-scss';
import { defaultOptions } from './default-options';
import {
buildIncludePaths,
cleanImportUrl,
parseNodeFilters,
parseSelectorFilters,
resolveGlobUrl,
resolvePackageUrl,
resolveUrl,
} from './toolbox';
import { IMagicImporterOptions } from './interfaces/IImporterOptions';
import { ISelectorFilter } from './interfaces/ISelectorFilter';
const EMPTY_IMPORT = {
file: ``,
contents: ``,
};
const DIRECTORY_SEPARATOR = `/`;
function getStoreId(
resolvedUrl: string|null,
selectorFilters: ISelectorFilter[]|null = null,
nodeFilters: string[]|null = null,
) {
return hash({
resolvedUrl,
selectorFilters,
nodeFilters,
}, { unorderedArrays: true });
}
export = function magicImporter(userOptions?: IMagicImporterOptions) {
const options = Object.assign({}, defaultOptions, userOptions);
if (options.hasOwnProperty(`prefix`)) {
process.emitWarning(
'Using the `prefix` option is not supported anymore, use `packagePrefix` instead.',
);
}
if (options.hasOwnProperty(`inlcudePaths`)) {
process.emitWarning(
'Using the `inlcudePaths` option is not supported anymore, Use the node-sass `includePaths` option instead.',
);
}
const escapedPackagePrefix = options.packagePrefix.replace(/[-/\\^$*+?.()|[\]{}]/g, `\\$&`);
const matchPackageUrl = new RegExp(`^${escapedPackagePrefix}(?!/)`);
return function importer(url: string, prev: string) {
const nodeSassOptions = this.options;
// Create a context for the current importer run.
// An importer run is different from an importer instance,
// one importer instance can spawn infinite importer runs.
if (!this.nodeSassOnceImporterContext) {
this.nodeSassOnceImporterContext = { store: new Set() };
}
// Each importer run has it's own new store, otherwise
// files already imported in a previous importer run
// would be detected as multiple imports of the same file.
const store = this.nodeSassOnceImporterContext.store;
const includePaths = buildIncludePaths(
nodeSassOptions.includePaths,
prev,
);
let data = null;
let filterPrefix: string = ``;
let filteredContents: string|null = null;
let cleanedUrl = cleanImportUrl(url);
let resolvedUrl: string|null = null;
const isPackageUrl = cleanedUrl.match(matchPackageUrl);
if (isPackageUrl) {
cleanedUrl = cleanedUrl.replace(matchPackageUrl, ``);
const packageName = cleanedUrl.charAt(0) === `@`
? cleanedUrl.split(DIRECTORY_SEPARATOR).slice(0, 2).join(DIRECTORY_SEPARATOR)
: cleanedUrl.split(DIRECTORY_SEPARATOR)[0];
const normalizedPackageName = path.normalize(packageName);
const packageSearchPath = path.join('node_modules', normalizedPackageName, `package.json`);
const packagePath = path.dirname(findupSync(packageSearchPath, { cwd: options.cwd }));
const escapedNormalizedPackageName = normalizedPackageName.replace(`\\`, `\\\\`);
cleanedUrl = path.resolve(
packagePath.replace(new RegExp(`${escapedNormalizedPackageName}$`), ``),
path.normalize(cleanedUrl),
);
resolvedUrl = resolvePackageUrl(
cleanedUrl,
options.extensions,
options.cwd,
options.packageKeys,
);
if (resolvedUrl) {
data = { file: resolvedUrl.replace(/\.css$/, ``) };
}
} else {
resolvedUrl = resolveUrl(cleanedUrl, includePaths);
}
const nodeFilters = parseNodeFilters(url);
const selectorFilters = parseSelectorFilters(url);
const hasFilters = nodeFilters.length || selectorFilters.length;
const globFilePaths = resolveGlobUrl(cleanedUrl, includePaths);
const storeId = getStoreId(resolvedUrl, selectorFilters, nodeFilters);
if (hasFilters) {
filterPrefix = `${url.split(` from `)[0]} from `;
}
if (globFilePaths) {
const contents = globFilePaths
.filter((x) => !store.has(getStoreId(x, selectorFilters, nodeFilters)))
.map((x) => `@import '${filterPrefix}${x}';`)
.join(`\n`);
return { contents };
}
if (store.has(storeId)) {
return EMPTY_IMPORT;
}
if (resolvedUrl && hasFilters) {
filteredContents = fs.readFileSync(resolvedUrl, { encoding: `utf8` });
if (selectorFilters.length) {
filteredContents = cssSelectorExtract.processSync({
css: filteredContents,
filters: selectorFilters,
postcssSyntax,
preserveLines: true,
});
}
if (nodeFilters.length) {
filteredContents = cssNodeExtract.processSync({
css: filteredContents,
filters: nodeFilters,
customFilters: options.customFilters,
postcssSyntax,
preserveLines: true,
});
}
}
if (!options.disableImportOnce) {
store.add(storeId);
}
if (filteredContents) {
data = {
file: resolvedUrl,
contents: filteredContents,
};
}
return data;
};
};