Skip to content

Instantly share code, notes, and snippets.

@vitalyrotari
Created January 26, 2016 08:56
Show Gist options
  • Save vitalyrotari/36db5ab13aa3351b4417 to your computer and use it in GitHub Desktop.
Save vitalyrotari/36db5ab13aa3351b4417 to your computer and use it in GitHub Desktop.
METEOR@1.3-modules-beta.4 SCSS fixes
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];
}
@infacq
Copy link

infacq commented Mar 23, 2016

how to use it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment