Skip to content

Commit

Permalink
feat: Support for Dart sass (#51)
Browse files Browse the repository at this point in the history
* feat(importer): passing a `this` context to sass-extract's own `importer()` call

* feat(importer): allow importers to return an array of resolved files

node-sass allows custom importers to resolve a given URL to multiple files paths, by returning an
Array containing multiple objects: `[{file: '...'}, {file: '...'}, ...]`. This change implements
this feature also for sass-extract's extracting phase.

* refactor(importer): importer functions should always call a callback instead of returning a Promise.

BREAKING CHANGE: Compatibility with node-sass: importers so far had to either call a callback (when they're called in the rendering phase, by `node-sass`) or return a Promise (when they're called in the extracting phase, by `makeImporter()`). From now, importers should always call the callback function provided in the third parameter of the importer function.

fix #36

* fix(importer): should work also with single importer

This has been removed by accident from the original code.

* --temp: adding lib files to git (until it will be available in npm)

* .gitignore cosm.

* chore: NPM script to install peer dependencies for development

* chore: Fixing tests

- `foundation-sites` changed its folder structure, and also some internal Sass values, which breaks the tests. We rather stick to the exact version from `foundation-sites`.
- `node-sass` throws Error for the nested `(1, 2, 3)` key. The problem should be with node-sass itself, as it works OK with `sass`.

* feat: adding `getSassImplementation()` to resolve the correct Sass implementation + Dart Sass added to `peerDependencies`

* chore(deps): updating all dependencies to their latest

* test: add test for the upcoming `extractOptions.implementation` option

We are iterating over each test with both the Node Sass and the Dart Sass implementations

* feat: Removing inner `node-sass` imports, using the `extractOptions.implementation` option instead everywhere

* fix(dart sass): Function signature in the `functions` options property must have attribute block if the function will accept parameters

While Node Sass allowed the signature without the `($params)` block after the function name, Dart Sass is more strict on this, and throws Error if we try to call the function with parameters.

* fix (dart-sass): constructor-based comparison causes error sometimes in Dart Sass

- We had issues eg. in case of `SassBooleans`.
- Dart Sass did not report the constructor name correctly before `1.22.5`. It instead reported a minified name, which we cannot rely on. We either need to increase the peerDependency version to `^1.22.5`, or use our `getConstructorName()`.

* fix: Upgrading to `getFileId` to the new `Buffer()` API

* we only promisify the render function and we only do it if renderAsync does not already exist, it is not necessarily the case for dart sass

* dart sass seems to require a semicolon (or curly brackets) after mixin inclusions

* dart sass does not call our importer for each file so instead, we patch fs.readFile and fs.readFileSync

* dart sass seems to honor the order of @import directives unlike node sass

* fix: fix extraction on windows

dart sass extraction was not working on windows because paths of patched files included backslashes

* chore: New build

* refactor: Revert adding lib folder to git

* refactor: Remove lib folder

* chore: Minor code style changes

---------

Co-authored-by: Pierre-Dominique CHARRON <[email protected]>
Co-authored-by: Athorcis <[email protected]>
  • Loading branch information
3 people authored Oct 25, 2023
1 parent 618fd93 commit 90e4225
Show file tree
Hide file tree
Showing 36 changed files with 521 additions and 263 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules
lib/
lib/
.idea
package-lock.json
27 changes: 15 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
"description": "Extract structured variables from sass files. Fast and accurate.",
"main": "lib/index.js",
"scripts": {
"peers": "npx npm-install-peers",
"compile": "babel -d lib/ src/",
"prepublish": "npm run compile",
"test": "mocha --require babel-core/register --timeout 10000",
"test": "mocha --require babel-core/register --timeout 10000 --file \"./test/helpers/implementation_iterator.js\"",
"test:fast": "FAST_TEST=true mocha --require babel-core/register",
"changelog": "conventional-changelog -i CHANGELOG.md -s -r 0",
"watch": "mocha --watch --reporter min --require babel-core/register --timeout 10000",
Expand All @@ -28,23 +29,25 @@
"node": ">=4"
},
"peerDependencies": {
"node-sass": ">=3.8.0"
"node-sass": ">=3.8.0",
"sass": ">=1"
},
"dependencies": {
"bluebird": "^3.4.7",
"gonzales-pe": "^4.2.2",
"bluebird": "^3.7.2",
"fs-monkey": "^1.0.4",
"gonzales-pe": "^4.2.4",
"parse-color": "^1.0.0",
"query-ast": "^1.0.1"
"query-ast": "^1.0.2"
},
"devDependencies": {
"babel-cli": "^6.22.2",
"babel-core": "^6.22.1",
"babel-preset-es2015": "^6.22.0",
"chai": "^4.1.2",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-preset-es2015": "^6.24.1",
"chai": "^4.2.0",
"chai-subset": "^1.6.0",
"cz-conventional-changelog": "^2.1.0",
"foundation-sites": "^6.4.3",
"mocha": "^4.0.1"
"cz-conventional-changelog": "^3.1.0",
"foundation-sites": "6.4.3",
"mocha": "^7.0.1"
},
"config": {
"commitizen": {
Expand Down
35 changes: 28 additions & 7 deletions src/extract.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import Promise from 'bluebird';
import sass from 'node-sass';
import { normalizePath, makeAbsolute } from './util';
import { normalizePath, makeAbsolute, getSassImplementation, promisifySass, isDartSass } from './util';
import { loadCompiledFiles, loadCompiledFilesSync } from './load';
import { processFiles, parseFiles } from './process';
import { makeImporter, makeSyncImporter } from './importer';
import { Pluggable } from './pluggable';

Promise.promisifyAll(sass);
import { patchReadFile, unpatchReadFile } from './fs-patcher';

/**
* Get rendered stats required for extraction
Expand Down Expand Up @@ -88,18 +85,30 @@ function compileExtractionResult(orderedFiles, extractions) {
*/
export function extract(rendered, { compileOptions = {}, extractOptions = {} } = {}) {
const pluggable = new Pluggable(extractOptions.plugins).init();
const sass = promisifySass(getSassImplementation(extractOptions));

const { entryFilename, includedFiles, includedPaths } = getRenderedStats(rendered, compileOptions);

return loadCompiledFiles(includedFiles, entryFilename, compileOptions.data)
.then(({ compiledFiles, orderedFiles }) => {
const parsedDeclarations = parseFiles(compiledFiles);
const extractions = processFiles(orderedFiles, compiledFiles, parsedDeclarations, pluggable);
const extractions = processFiles(orderedFiles, compiledFiles, parsedDeclarations, pluggable, sass);
const importer = makeImporter(extractions, includedFiles, includedPaths, compileOptions.importer);
const extractionCompileOptions = makeExtractionCompileOptions(compileOptions, entryFilename, extractions, importer);

const isDart = isDartSass(sass);

if (isDart) {
extractionCompileOptions.importer = compileOptions.importer;
patchReadFile(extractions, entryFilename);
}

return sass.renderAsync(extractionCompileOptions)
.then(() => {
if (isDart) {
unpatchReadFile();
}

return pluggable.run(Pluggable.POST_EXTRACT, compileExtractionResult(orderedFiles, extractions));
});
});
Expand All @@ -111,16 +120,28 @@ export function extract(rendered, { compileOptions = {}, extractOptions = {} } =
*/
export function extractSync(rendered, { compileOptions = {}, extractOptions = {} } = {}) {
const pluggable = new Pluggable(extractOptions.plugins).init();
const sass = getSassImplementation(extractOptions);

const { entryFilename, includedFiles, includedPaths } = getRenderedStats(rendered, compileOptions);

const { compiledFiles, orderedFiles } = loadCompiledFilesSync(includedFiles, entryFilename, compileOptions.data);
const parsedDeclarations = parseFiles(compiledFiles);
const extractions = processFiles(orderedFiles, compiledFiles, parsedDeclarations, pluggable);
const extractions = processFiles(orderedFiles, compiledFiles, parsedDeclarations, pluggable, sass);
const importer = makeSyncImporter(extractions, includedFiles, includedPaths, compileOptions.importer);
const extractionCompileOptions = makeExtractionCompileOptions(compileOptions, entryFilename, extractions, importer);

const isDart = isDartSass(sass);

if (isDart) {
extractionCompileOptions.importer = compileOptions.importer;
patchReadFile(extractions, entryFilename);
}

sass.renderSync(extractionCompileOptions);

if (isDart) {
unpatchReadFile();
}

return pluggable.run(Pluggable.POST_EXTRACT, compileExtractionResult(orderedFiles, extractions));
}
53 changes: 53 additions & 0 deletions src/fs-patcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { patchFs } from 'fs-monkey';
import fs from 'fs';

const originalReadFileSync = fs.readFileSync;
const originalReadFile = fs.readFile;

function getInjectedData(path, extractions, entryFilename) {
path = fs.realpathSync(path);

if (process.platform === 'win32') {
path = path.replace(/\\/g, '/');
}

if (path in extractions && path !== entryFilename) {
return extractions[path].injectedData;
}
}

export function patchReadFile(extractions, entryFilename) {

patchFs({
readFileSync(path, options) {
const injectedData = getInjectedData(path, extractions, entryFilename);

if (injectedData) {
return injectedData;
}

return originalReadFileSync(path, options);
},

readFile(path, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}

const injectedData = getInjectedData(path, extractions, entryFilename);

if (injectedData) {
callback(null, injectedData);
}
else {
originalReadFile(path, options, callback);
}
}
});
}

export function unpatchReadFile() {
fs.readFileSync = originalReadFileSync;
fs.readFile = originalReadFile;
}
55 changes: 27 additions & 28 deletions src/importer.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,18 @@ function getImportAbsolutePath(url, prev, includedFilesMap, includedPaths = [])
* Get the resulting source and path for a given @import request
*/
function getImportResult(extractions, url, prev, includedFilesMap, includedPaths) {
const absolutePath = getImportAbsolutePath(url, prev, includedFilesMap, includedPaths);
const contents = extractions[absolutePath].injectedData;
if (!Array.isArray(url)) {
url = [url]
}

const returnObj = url.map(url_item => {
const absolutePath = getImportAbsolutePath(url_item, prev, includedFilesMap, includedPaths);
const contents = extractions[absolutePath].injectedData;

return { file: absolutePath, contents };
});

return { file: absolutePath, contents };
return returnObj;
}

function getIncludedFilesMap(includedFiles) {
Expand All @@ -92,34 +100,25 @@ export function makeImporter(extractions, includedFiles, includedPaths, customIm

return function(url, prev, done) {
try {
let promise = Promise.resolve();
const promises = [];
if (customImporter) {
promise = new Promise(resolve => {
if (Array.isArray(customImporter)) {
const promises = [];
customImporter.forEach(importer => {
const thisPromise = new Promise(res => {
const modifiedUrl = importer(url, prev, res);
if (modifiedUrl !== undefined) {
res(modifiedUrl);
}
});
promises.push(thisPromise);
})
Promise.all(promises).then(results => {
resolve(results.find(item => item !== null));
});
} else {
const modifiedUrl = customImporter(url, prev, resolve);
if (modifiedUrl !== undefined) {
resolve(modifiedUrl);
}
}
if (!Array.isArray(customImporter)) {
customImporter = [customImporter]
}

customImporter.forEach(importer => {
promises.push(new Promise(res => {
importer.apply({}, [url, prev, res]);
}));
});
}
promise.then(modifiedUrl => {
if (modifiedUrl && modifiedUrl.file) {
url = modifiedUrl.file;
Promise.all(promises).then(results => results.find(item => item !== null)).then(modifiedUrl => {
if (modifiedUrl) {
if (!Array.isArray(modifiedUrl)) {
modifiedUrl = [modifiedUrl];
}

url = modifiedUrl.map(url_item => url_item.file);
}
const result = getImportResult(extractions, url, prev, includedFilesMap, includedPaths);
done(result);
Expand Down
21 changes: 11 additions & 10 deletions src/inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ const FN_SUFFIX_VALUE = 'VALUE';
/**
* Create injection function and source for a file, category, declaration and result handler
*/
function createInjection(fileId, categoryPrefix, declaration, idx, declarationResultHandler) {
function createInjection(fileId, categoryPrefix, declaration, idx, declarationResultHandler, sass) {
const fnName = `${FN_PREFIX}_${fileId}_${categoryPrefix}_${declaration.declarationClean}_${idx}`;
const fnSignature = `${fnName}(${declaration.declaration})`;

const injectedFunction = function(sassValue) {
const value = createStructuredValue(sassValue);
const value = createStructuredValue(sassValue, sass);
declarationResultHandler(declaration, value, sassValue);
return sassValue;
};
Expand All @@ -22,7 +23,7 @@ function createInjection(fileId, categoryPrefix, declaration, idx, declarationRe
$${fnName}: ${fnName}(${declaration.declaration});
}\n`

return { fnName, injectedFunction, injectedCode };
return { fnSignature, injectedFunction, injectedCode };
}

/**
Expand All @@ -31,30 +32,30 @@ function createInjection(fileId, categoryPrefix, declaration, idx, declarationRe
* Declaration result handlers will be called with the extracted value of each declaration
* Provided file id will be used to ensure unique function names per file
*/
export function injectExtractionFunctions(fileId, declarations, dependentDeclarations, { globalDeclarationResultHandler }) {
export function injectExtractionFunctions(fileId, declarations, dependentDeclarations, { globalDeclarationResultHandler }, sass) {
let injectedData = ``;
const injectedFunctions = {};

// Create injections for implicit global variables
declarations.implicitGlobals.forEach((declaration, idx) => {
const { fnName, injectedFunction, injectedCode } = createInjection(fileId, FN_PREFIX_IMPLICIT_GLOBAL, declaration, idx, globalDeclarationResultHandler);
injectedFunctions[fnName] = injectedFunction;
const { fnSignature, injectedFunction, injectedCode } = createInjection(fileId, FN_PREFIX_IMPLICIT_GLOBAL, declaration, idx, globalDeclarationResultHandler, sass);
injectedFunctions[fnSignature] = injectedFunction;
injectedData += injectedCode;
});

// Create injections for explicit global variables
declarations.explicitGlobals.forEach((declaration, idx) => {
const { fnName, injectedFunction, injectedCode } = createInjection(fileId, FN_PREFIX_EXPLICIT_GLOBAL, declaration, idx, globalDeclarationResultHandler);
injectedFunctions[fnName] = injectedFunction;
const { fnSignature, injectedFunction, injectedCode } = createInjection(fileId, FN_PREFIX_EXPLICIT_GLOBAL, declaration, idx, globalDeclarationResultHandler, sass);
injectedFunctions[fnSignature] = injectedFunction;
injectedData += injectedCode;
});

dependentDeclarations.forEach(({ declaration, decFileId }, idx) => {
// Do not add dependent injection if the declaration is in the current file
// It will already be added by explicits
if(decFileId === fileId) { return; }
const { fnName, injectedFunction, injectedCode } = createInjection(fileId, FN_PREFIX_DEPENDENT_GLOBAL, declaration, idx, globalDeclarationResultHandler);
injectedFunctions[fnName] = injectedFunction;
const { fnSignature, injectedFunction, injectedCode } = createInjection(fileId, FN_PREFIX_DEPENDENT_GLOBAL, declaration, idx, globalDeclarationResultHandler, sass);
injectedFunctions[fnSignature] = injectedFunction;
injectedData += injectedCode;
});

Expand Down
6 changes: 3 additions & 3 deletions src/plugins/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { serialize } from '../serialize';
*/
export function run() {
return {
postValue: ({ value, sassValue }) => {
return { value: { value: serialize(sassValue) }, sassValue };
postValue: ({ value, sassValue, sass }) => {
return { value: { value: serialize(sassValue, false, sass) }, sassValue };
}
}
}
}
12 changes: 6 additions & 6 deletions src/process.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Pluggable } from './pluggable';
* Get a string id for a filename
*/
function getFileId(filename) {
return new Buffer(filename).toString('base64').replace(/=/g, '');
return new Buffer.from(filename).toString('base64').replace(/=/g, '');
}

function parseFile(filename, data) {
Expand All @@ -29,7 +29,7 @@ function getDependentDeclarations(filename, declarations) {
/**
* Process a single sass files to get declarations, injected source and functions
*/
function processFile(idx, count, filename, data, parsedDeclarations, pluggable) {
function processFile(idx, count, filename, data, parsedDeclarations, pluggable, sass) {
const declarations = parsedDeclarations.files[filename];
// Inject dependent declaration extraction to last file
const dependentDeclarations = idx === count - 1 ? parsedDeclarations.dependentDeclarations : [];
Expand All @@ -39,12 +39,12 @@ function processFile(idx, count, filename, data, parsedDeclarations, pluggable)
if(!variables.global[declaration.declaration]) {
variables.global[declaration.declaration] = [];
}
const variableValue = pluggable.run(Pluggable.POST_VALUE, { value, sassValue }).value;
const variableValue = pluggable.run(Pluggable.POST_VALUE, { value, sassValue, sass }).value;
variables.global[declaration.declaration].push({ declaration, value: variableValue });
}

const fileId = getFileId(filename);
const injection = injectExtractionFunctions(fileId, declarations, dependentDeclarations, { globalDeclarationResultHandler });
const injection = injectExtractionFunctions(fileId, declarations, dependentDeclarations, { globalDeclarationResultHandler }, sass);
const injectedData = `${data}\n\n${injection.injectedData}`;
const injectedFunctions = injection.injectedFunctions;

Expand Down Expand Up @@ -76,11 +76,11 @@ export function parseFiles(files) {
* Process a set of sass files to get declarations, injected source and functions
* Files are provided in a map of filename -> key entries
*/
export function processFiles(orderedFiles, files, parsedDeclarations, pluggable) {
export function processFiles(orderedFiles, files, parsedDeclarations, pluggable, sass) {
const extractions = {};

orderedFiles.forEach((filename, idx) => {
extractions[filename] = processFile(idx, orderedFiles.length, filename, files[filename], parsedDeclarations, pluggable);
extractions[filename] = processFile(idx, orderedFiles.length, filename, files[filename], parsedDeclarations, pluggable, sass);
});

return extractions;
Expand Down
Loading

0 comments on commit 90e4225

Please sign in to comment.