gulp-sass-dependent-partials
Version:
It proces scss partials files and add the files that import them to the pipe.
246 lines (207 loc) • 7.36 kB
JavaScript
// #####################################################################################################################
/**
* @author Jakub Zasański <jakub.zasanski.dev@gmail.com>
* @version 1.0.0
*/
// #####################################################################################################################
import fs from 'node:fs'
import path from 'node:path'
import glob from 'glob'
import tokenizer from 'scss-tokenizer'
import process from 'node:process'
// #####################################################################################################################
export default class Graph {
constructor(dir, options) {
this.options = this.processOptions(options)
this.index = {}
if (this.options.exclude !== null && !(this.options.exclude instanceof RegExp)) {
throw new Error('Options exclude in not a instance of RegExp.')
}
this.loadPaths = this.options.loadPaths.map((p) => path.resolve(p))
if (!dir) {
throw new Error('Base dir is required.')
}
this.dir = path.resolve(dir)
}
/**
*
* Set class options
*
* @param {Object} options
* @return {Object}
*
*/
processOptions(options) {
const defaults = {
loadPaths: [process.cwd()],
extensions: ['scss', 'sass'],
follow: false,
exclude: null,
}
return Object.assign(defaults, options)
}
/**
*
* Create graph from base dir
*
* @return {Object}
*
*/
createGraph() {
glob.sync(`${this.dir}/**/*.@(${this.options.extensions.join('|')})`, {
dot: true,
nodir: true,
follow: this.options.follow,
}).forEach((file) => {
this.addGraphEntry(path.resolve(file))
})
}
/**
*
* Refresh graph entry
*
* @param {string} filePath
* @return {void}
*
*/
refreshGraphEntry(filePath) {
this.addGraphEntry(filePath)
}
/**
*
* Add graph entry
*
* @param {string} filePath
* @param {null|string} parent
* @return {Object}
*
*/
addGraphEntry(filePath, parent = null) {
if (this.options.exclude !== null && this.options.exclude.test(filePath)) {
return
}
const entry = (this.index[filePath] = this.index[filePath] || {
imports: [],
importedBy: [],
modified: fs.statSync(filePath).mtime,
})
let resolvedParent
const isIndentedSyntax = path.extname(filePath) === '.sass'
const imports = this.parseImports(fs.readFileSync(filePath, 'utf-8'), isIndentedSyntax)
const cwd = path.dirname(filePath)
for (let i = 0; i < imports.length; i++) {
const loadPaths = [cwd, this.dir]
.concat(this.loadPaths)
.filter(Boolean)
.filter((item, index, array) => array.indexOf(item) === index)
const resolved = this.resolveSassPath(imports[i], loadPaths, this.options.extensions)
if (!resolved) {
continue
}
if (this.options.exclude !== null && this.options.exclude.test(resolved)) continue
if (!entry.imports.includes(resolved)) {
entry.imports.push(resolved)
this.addGraphEntry(fs.realpathSync(resolved), filePath)
}
}
if (parent) {
resolvedParent = parent
this.loadPaths.forEach((loadPath) => {
if (parent.includes(loadPath)) {
resolvedParent = parent.substr(parent.indexOf(loadPath))
}
})
if (!(this.options.exclude !== null && this.options.exclude.test(resolvedParent))) {
entry.importedBy.push(resolvedParent)
}
}
}
/**
*
* Resolve Sass path
*
* @param {string} sassPath
* @param {[]} loadPaths
* @param {string[]} extensions
* @return {string|boolean}
*
*/
resolveSassPath(sassPath, loadPaths, extensions) {
const re = new RegExp(`(.(${extensions.join('|')}))$`, 'i')
const sassPathName = sassPath.replace(re, '')
for (let i = 0; i < loadPaths.length; i++) {
for (let j = 0; j < extensions.length; j++) {
const scssPath = path.normalize(`${loadPaths[i]}/${sassPathName}.${extensions[j]}`)
if (fs.existsSync(scssPath) && fs.lstatSync(scssPath).isFile()) {
return scssPath
}
}
for (let j = 0; j < extensions.length; j++) {
const scssPath = path.normalize(`${loadPaths[i]}/${sassPathName}.${extensions[j]}`)
const partialPath = path.join(path.dirname(scssPath), `_${path.basename(scssPath)}`)
if (fs.existsSync(partialPath) && fs.lstatSync(partialPath).isFile()) {
return partialPath
}
}
}
return false
}
/**
*
* Resolve Sass path
*
* @param {string} content
* @param {boolean} isIndentedSyntax
* @return {[]}
*
*/
parseImports(content, isIndentedSyntax) {
const tokens = tokenizer.tokenize(content)
const results = []
let tmp = ''
let inImport = false
let inParen = false
let prevToken = tokens[0]
for (let i = 1; i < tokens.length; i++) {
const token = tokens[i]
if (inImport && !inParen && token[0] === 'string') {
results.push(token[1])
} else if (
(token[1] === 'import' || token[1] === 'use' || token[1] === 'forward') &&
prevToken[1] === '@'
) {
if (inImport && !isIndentedSyntax) {
throw new Error('Encountered invalid @import syntax.')
}
inImport = true
} else if (inImport && !inParen && (token[0] === 'ident' || token[0] === '/')) {
tmp += token[1]
} else if (inImport && !inParen && (token[0] === 'space' || token[0] === 'newline')) {
if (tmp !== '') {
results.push(tmp)
tmp = ''
if (isIndentedSyntax) {
inImport = false
}
}
} else if (inImport && token[0] === ';') {
inImport = false
if (tmp !== '') {
results.push(tmp)
tmp = ''
}
} else if (inImport && token[0] === '(') {
inParen = true
tmp = ''
} else if (inImport && token[0] === ')') {
inParen = false
}
prevToken = token
}
if (tmp !== '') {
results.push(tmp)
}
return results
}
}