Created
January 26, 2016 08:56
-
-
Save vitalyrotari/36db5ab13aa3351b4417 to your computer and use it in GitHub Desktop.
[email protected] SCSS fixes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const path = Plugin.path; | |
const fs = Plugin.fs; | |
const sass = Npm.require('node-sass'); | |
const Future = Npm.require('fibers/future'); | |
const files = Plugin.files; | |
Plugin.registerCompiler({ | |
extensions: ['scss', 'sass'], | |
archMatching: 'web' | |
}, () => new SassCompiler()); | |
function hasUnderscore(file) { | |
return path.basename(file)[0] === '_'; | |
} | |
// CompileResult is {css, sourceMap}. | |
class SassCompiler extends MultiFileCachingCompiler { | |
constructor() { | |
super({ | |
compilerName: 'sass', | |
defaultCacheSize: 1024*1024*10 | |
}); | |
} | |
getCacheKey(inputFile) { | |
return inputFile.getSourceHash(); | |
} | |
compileResultSize(compileResult) { | |
return compileResult.css.length + | |
this.sourceMapSize(compileResult.sourceMap); | |
} | |
// The heuristic is that a file is an import (ie, is not itself processed as a | |
// root) if it matches _*.sass, _*.scss | |
// This can be overridden in either direction via an explicit | |
// `isImport` file option in api.addFiles. | |
isRoot(inputFile) { | |
const fileOptions = inputFile.getFileOptions(); | |
if (fileOptions.hasOwnProperty('isImport')) { | |
return !fileOptions.isImport; | |
} | |
const pathInPackage = inputFile.getPathInPackage(); | |
const fileBaseName = inputFile.getBasename(); | |
return !(/^_/.test(fileBaseName) || | |
/\.import\.(sass|scss)$/.test(pathInPackage) || | |
/\.(sass|scss)import$/.test(pathInPackage) || | |
/(?:^|\/)imports\//.test(pathInPackage)); | |
} | |
compileOneFile(inputFile, allFiles) { | |
const referencedImportPaths = []; | |
const totalImportPath = []; | |
const sourceMapPaths = ['.' + inputFile.getDisplayPath()]; | |
//Handle deprecation of fs.existsSYnc | |
//XXX: remove when meteor is fully on node 4+ | |
function fileExists(file) { | |
if (fs.accessSync) { | |
try { | |
fs.accessSync(file, fs.R_OK); | |
} catch (e) { | |
return false; | |
} | |
return true; | |
} else { | |
return fs.existsSync(file); | |
} | |
} | |
function addUnderscore(file) { | |
if (!hasUnderscore(file)) { | |
file = path.join(path.dirname(file), '_' + path.basename(file)); | |
} | |
return file; | |
} | |
const getRealImportPath = function (importPath) { | |
const rawImportPath = importPath; | |
var isAbsolute = false; | |
if (importPath[0] === '/') { | |
importPath = path.join(process.cwd(), importPath); | |
isAbsolute = true; | |
} | |
//SASS has a whole range of possible import files from one import statement, try each of them | |
const possibleFiles = []; | |
//If the referenced file has no extension, try possible extensions, starting with extension of the parent file. | |
let possibleExtensions = ['scss', 'sass', 'css']; | |
if (!importPath.match(/\.s?(a|c)ss$/)) { | |
possibleExtensions = [inputFile.getExtension()].concat(_.without(possibleExtensions, inputFile.getExtension())); | |
for (const extension of possibleExtensions) { | |
possibleFiles.push(importPath + '.' + extension); | |
} | |
} else { | |
possibleFiles.push(importPath); | |
} | |
//Try files prefixed with underscore | |
for (let possibleFile of possibleFiles) { | |
if (!hasUnderscore(possibleFile)) { | |
possibleFiles.push(addUnderscore(possibleFile)); | |
} | |
} | |
//Try if one of the possible files exists | |
for (let possibleFile of possibleFiles) { | |
if ((isAbsolute && fileExists(possibleFile)) || (!isAbsolute && allFiles.has(possibleFile))) { | |
return {absolute: isAbsolute, path: possibleFile}; | |
} | |
} | |
//Nothing found... | |
throw new Error(`File to import: ${rawImportPath} not found in file: ${totalImportPath[totalImportPath.length - 2]}`); | |
}; | |
//Handle import statements found by the sass compiler, used to handle cross-package imports | |
const importer = function (url, prev, done) { | |
if (!totalImportPath.length) { | |
totalImportPath.push(prev); | |
} | |
if (totalImportPath[totalImportPath.length] !== prev) { | |
//backtracked, splice of part we don't need anymore | |
// (XXX: this might give problems when multiple parts of the path have the same name) | |
totalImportPath.splice(totalImportPath.indexOf(prev) + 1, totalImportPath.length); | |
} | |
let importPath = url; | |
for (let i = totalImportPath.length - 1; i >= 0; i--) { | |
if (importPath[0] === '/' || importPath[0] === '{') { | |
break; | |
} | |
importPath = path.join(path.dirname(totalImportPath[i]), importPath); | |
} | |
totalImportPath.push(url); | |
let accPosition = importPath.indexOf('{'); | |
if (accPosition > -1) { | |
importPath = importPath.substr(accPosition, importPath.length); | |
} | |
try { | |
const parsed = getRealImportPath(importPath); | |
if (parsed.absolute) { | |
sourceMapPaths.push(parsed.path); | |
done({contents: fs.readFileSync(parsed.path, 'utf8')}); | |
} else { | |
referencedImportPaths.push(parsed.path); | |
sourceMapPaths.push(decodeFilePath(parsed.path)); | |
done({contents: allFiles.get(parsed.path).getContentsAsString()}); | |
} | |
} catch (e) { | |
return done(e); | |
} | |
}; | |
const f = new Future; | |
const options = { | |
sourceMap: true, | |
sourceMapContents: true, | |
sourceMapEmbed: false, | |
sourceComments: false, | |
sourceMapRoot: '.', | |
indentedSyntax : inputFile.getExtension() === 'sass', | |
outFile: '.'+inputFile.getBasename(), | |
importer: importer, | |
includePaths: [] | |
}; | |
options.file = this.getAbsoluteImportPath(inputFile); | |
options.data = inputFile.getContentsAsBuffer().toString('utf8'); | |
//If the file is empty, options.data is an empty string | |
// In that case options.file will be used by node-sass, | |
// which it can not read since it will contain a meteor package or app reference '{}' | |
// This is one workaround, another one would be to not set options.file, in which case the importer 'prev' will be 'stdin' | |
// However, this would result in problems if a file named std�n.scss would exist. | |
// Not the most elegant of solutions, but it works. | |
if (!options.data.trim()) { | |
options.data = "$fakevariable : blue;" | |
} | |
let output; | |
try { | |
sass.render(options, f.resolver()); | |
output = f.wait(); | |
} catch (e) { | |
inputFile.error({ | |
message: `[SCSS Compiler Error] ${e.message}\n`, | |
sourcePath: inputFile.getDisplayPath(), | |
line: e.line, | |
column: e.column | |
}); | |
return null; | |
} | |
if (output.map) { | |
const map = JSON.parse(output.map.toString('utf-8')); | |
map.sources = sourceMapPaths; | |
output.map = map; | |
} | |
const compileResult = {css: output.css.toString('utf-8'), sourceMap: output.map}; | |
return {compileResult, referencedImportPaths}; | |
} | |
addCompileResult(inputFile, compileResult) { | |
inputFile.addStylesheet({ | |
data: compileResult.css, | |
path: inputFile.getPathInPackage() + '.css', | |
sourceMap: compileResult.sourceMap | |
}); | |
} | |
} | |
function decodeFilePath(filePath) { | |
const match = filePath.match(/^{(.*)}\/(.*)$/); | |
if (! match) | |
throw new Error('Failed to decode Less path: ' + filePath); | |
if (match[1] === '') { | |
// app | |
return match[2]; | |
} | |
return 'packages/' + match[1] + '/' + match[2]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
how to use it?