├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── lib ├── asyncRunner.js ├── asyncWorker.js ├── index.js └── types.js ├── package.json ├── patchtest.js ├── src ├── entrypoint.cpp ├── functions.cpp ├── functions.h ├── importers.cpp ├── importers.h └── workaround8806.js └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | libsass 3 | node_modules 4 | test 5 | *.tgz 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows 28 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Google LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | all: dist/binding.js dist/binding.wasm dist/version.js 8 | 9 | LIBSASS_VERSION = 3.5.5 10 | # It also works with 3.6.0 but sass-spec isn't compatible with 3.6.0 yet. 11 | 12 | CXXFLAGS = -Wall -O2 -std=c++17 -I libsass/include $(EXTRA_CXXFLAGS) 13 | 14 | EMCC_OPTIONS = \ 15 | -s ENVIRONMENT=node \ 16 | -s NODERAWFS=1 \ 17 | -s DISABLE_EXCEPTION_CATCHING=0 \ 18 | -s NODEJS_CATCH_EXIT=0 \ 19 | -s WASM_ASYNC_COMPILATION=0 \ 20 | -s ALLOW_MEMORY_GROWTH=1 \ 21 | --bind \ 22 | --js-library src/workaround8806.js 23 | # `--js-library workaround8806.js` must be AFTER `--bind` to override it! 24 | 25 | #EMCC_OPTIONS += -s DYNAMIC_EXECUTION=0 --profiling 26 | #EMCC_OPTIONS += --closure 1 27 | 28 | BINDING_SOURCES = dist/entrypoint.o dist/functions.o dist/importers.o libsass/lib/libsass.a 29 | 30 | dist/binding.js dist/binding.wasm: $(BINDING_SOURCES) src/workaround8806.js Makefile 31 | emcc -O2 -o $@ $(BINDING_SOURCES) $(EMCC_OPTIONS) 32 | 33 | dist/version.js: dist/binding.js dist/binding.wasm 34 | node -e 'console.log("exports.libsass = %j;", require("./dist/binding").sassVersion())' > $@ 35 | 36 | dist/%.o: src/%.cpp | dist libsass 37 | $(CXX) $(CXXFLAGS) -c -o $@ $< 38 | 39 | dist/entrypoint.o: src/functions.h src/importers.h 40 | dist/functions.o: src/functions.h 41 | dist/importers.o: src/importers.h 42 | 43 | libsass/lib/libsass.a: libsass 44 | $(MAKE) -C libsass lib/libsass.a 45 | 46 | libsass: 47 | git -c advice.detachedHead=false clone https://github.com/sass/libsass -b $(LIBSASS_VERSION) 48 | 49 | dist: 50 | mkdir dist 51 | 52 | clean: 53 | -rm -rf dist 54 | [ -d libsass ] && $(MAKE) -C libsass clean 55 | 56 | veryclean: 57 | -rm -rf dist libsass 58 | 59 | .PHONY: all clean veryclean 60 | .DELETE_ON_ERROR: 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-sass-wasm 2 | 3 | This is an experimental proof-of-concept rewrite of 4 | [node-sass](https://github.com/sass/node-sass) using WebAssembly. Just like 5 | node-sass, it allows you to compile .sass and .scss to CSS. It is fully 6 | compatible with the node-sass API, including support for importers and custom 7 | functions. Thanks to WebAssembly, it has zero dependencies, it doesn't need to 8 | be updated when a new version of node.js is released, and it might theoretically 9 | run on more platforms than node-sass. However, it requires **node.js >= 10 | 10.5.0**, see below. 11 | 12 | ## Usage 13 | 14 | The package is not published on npm yet. Please download it from GitHub for now 15 | if you want to try it. 16 | 17 | ```bash 18 | npm install git+https://github.com/TomiBelan/node-sass-wasm#built/master 19 | # or 20 | yarn add git+https://github.com/TomiBelan/node-sass-wasm#built/master 21 | ``` 22 | 23 | ```js 24 | const sass = require('node-sass-wasm'); 25 | sass.render(...); 26 | ``` 27 | 28 | To use it with webpack and sass-loader, add the `implementation` option: 29 | 30 | ```js 31 | { 32 | loader: 'sass-loader', 33 | options: { 34 | implementation: require('node-sass-wasm'), 35 | ... 36 | } 37 | } 38 | ``` 39 | 40 | Or you can install it as a package alias, allowing code that calls 41 | `require('node-sass')` to keep working: 42 | 43 | ```bash 44 | npm --version # must be at least 6.9.0 45 | npm install node-sass@git+https://github.com/TomiBelan/node-sass-wasm#built/master 46 | # or 47 | yarn add node-sass@git+https://github.com/TomiBelan/node-sass-wasm#built/master 48 | ``` 49 | 50 | ## Building 51 | 52 | - Install the 53 | [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html) 54 | - Activate the SDK (add it to \$PATH): `source ./emsdk_env.sh` 55 | - Clone the node-sass-wasm Git repository 56 | - Run `emmake make` 57 | - Run `npm pack .` 58 | 59 | ## Caveats 60 | 61 | - node-sass-wasm relies on the 62 | [worker_threads](https://nodejs.org/api/worker_threads.html) module, which is 63 | experimental and may break. 64 | - Because of the above, node-sass-wasm only works in **node.js >= 10.5.0**. 65 | Plus, in **node.js < 11.7.0** you must also enable the flag 66 | `--experimental-worker`, perhaps in an environment variable as 67 | `export NODE_OPTIONS=--experimental-worker`. 68 | - Loading the wasm module takes a while (around 0.9s on my system). It is loaded 69 | lazily on the first call to `render` or `renderSync`. 70 | - `render` calls run serially, waiting in a queue. One call cannot begin until 71 | the previous call finishes. It is important not to get stuck in an 72 | asynchronous importer or custom function that never calls `done`. 73 | - node-sass-wasm might be stricter than node-sass about validating the types of 74 | the options passed to `render` and `renderSync`. 75 | - node-sass-wasm doesn't provide a CLI binary. I think that belongs in a 76 | separate package anyway. 77 | 78 | ## Status 79 | 80 | The project is feature-complete and implements the full API of node-sass 4.12.0. 81 | But it is still just an experimental prototype and I can't guarantee I'll keep 82 | updating it over the long term -- nothing is preventing it, but it's too early 83 | to tell. Please keep that in mind if you decide to depend on it. 84 | 85 | ## Disclaimer 86 | 87 | My employer wanted me to write this disclaimer: 88 | 89 | This is not an official Google product. 90 | -------------------------------------------------------------------------------- /lib/asyncRunner.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | const { Worker, MessageChannel } = require('worker_threads'); 8 | const { TextEncoder } = require('util'); 9 | const path = require('path'); 10 | 11 | const BUFFER_SIZE = 8 * 1024; 12 | 13 | let worker = undefined; 14 | let sab = undefined; 15 | 16 | exports.run = function(payload, externalHelper) { 17 | if (!worker) { 18 | sab = new SharedArrayBuffer(4 + BUFFER_SIZE); 19 | worker = new Worker(path.join(__dirname, 'asyncWorker.js'), { 20 | workerData: { sab, BUFFER_SIZE }, 21 | }); 22 | worker.unref(); 23 | } 24 | 25 | const { port1, port2 } = new MessageChannel(); 26 | const lockArray = new Int32Array(sab, 0, 1); 27 | const responseArray = new Uint8Array(sab, 4, BUFFER_SIZE); 28 | let encodedHelperOutput = undefined; 29 | 30 | function sendChunk(offset) { 31 | responseArray.set( 32 | encodedHelperOutput.subarray(offset, offset + BUFFER_SIZE) 33 | ); 34 | Atomics.store(lockArray, 0, encodedHelperOutput.length); 35 | Atomics.notify(lockArray, 0); 36 | } 37 | 38 | return new Promise((resolve, reject) => { 39 | port1.on('message', message => { 40 | if (message.type == 'result') { 41 | port1.close(); 42 | resolve(message.result); 43 | } 44 | if (message.type == 'chunk') { 45 | sendChunk(message.offset); 46 | } 47 | if (message.type == 'callHelper') { 48 | externalHelper(message.helperInput).then(helperOutput => { 49 | encodedHelperOutput = new TextEncoder().encode(helperOutput); 50 | sendChunk(0); 51 | }); 52 | } 53 | }); 54 | 55 | worker.postMessage({ payload, port: port2 }, [port2]); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /lib/asyncWorker.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | const { workerData, parentPort } = require('worker_threads'); 8 | const { TextDecoder } = require('util'); 9 | const binding = require('../dist/binding'); 10 | 11 | const { sab, BUFFER_SIZE } = workerData; 12 | const lockArray = new Int32Array(sab, 0, 1); 13 | const responseArray = new Uint8Array(sab, 4, BUFFER_SIZE); 14 | 15 | function sendAndWait(port, message) { 16 | Atomics.store(lockArray, 0, 0); 17 | port.postMessage(message); 18 | while (Atomics.load(lockArray, 0) == 0) { 19 | Atomics.wait(lockArray, 0, 1); 20 | } 21 | } 22 | 23 | parentPort.on('message', function({ payload, port }) { 24 | function callHelper(helperInput) { 25 | sendAndWait(port, { type: 'callHelper', helperInput }); 26 | const outputLength = Atomics.load(lockArray, 0); 27 | const encodedHelperOutput = new Uint8Array(outputLength); 28 | for (let offset = 0; offset < outputLength; offset += BUFFER_SIZE) { 29 | if (offset > 0) { 30 | sendAndWait(port, { type: 'chunk', offset }); 31 | } 32 | const chunkSize = Math.min(BUFFER_SIZE, outputLength - offset); 33 | encodedHelperOutput.set(responseArray.subarray(0, chunkSize), offset); 34 | } 35 | return new TextDecoder().decode(encodedHelperOutput); 36 | } 37 | 38 | const result = binding.sassRender(payload, callHelper); 39 | port.postMessage({ type: 'result', result }); 40 | }); 41 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | const asyncRunner = require('./asyncRunner'); 8 | const libsassVersion = require('../dist/version').libsass; 9 | const packageVersion = require('../package.json').version; 10 | const path = require('path'); 11 | const types = require('./types'); 12 | 13 | function stringifyJson(obj) { 14 | if (obj === undefined) obj = null; 15 | return JSON.stringify(obj, (key, value) => { 16 | if (value instanceof Error) { 17 | return { __error: '' + (value.stack || value.message) }; 18 | } 19 | return value; 20 | }); 21 | } 22 | 23 | function preprocess(options) { 24 | const start = Date.now(); 25 | 26 | let { 27 | data, 28 | file, 29 | outFile, 30 | sourceMap, 31 | sourceMapRoot, 32 | linefeed, 33 | indentWidth, 34 | indentType, 35 | outputStyle, 36 | precision, 37 | indentedSyntax, 38 | sourceComments, 39 | omitSourceMapUrl, 40 | sourceMapEmbed, 41 | sourceMapContents, 42 | includePaths, 43 | } = options; 44 | 45 | if (typeof file == 'string') file = path.resolve(file); 46 | 47 | if (typeof outFile == 'string') outFile = path.resolve(outFile); 48 | 49 | if (sourceMap == true) { 50 | if (!outFile) { 51 | throw Error('options.sourceMap is true but options.outFile is not set'); 52 | } 53 | sourceMap = outFile + '.map'; 54 | } 55 | if (typeof sourceMap == 'string') sourceMap = path.resolve(sourceMap); 56 | 57 | const feeds = { cr: '\r', crlf: '\r\n', lf: '\n', lfcr: '\n\r' }; 58 | if (typeof linefeed == 'string') linefeed = feeds[linefeed] || '\n'; 59 | 60 | let indent = undefined; 61 | if (indentWidth || indentType) { 62 | const character = indentType == 'tab' ? '\t' : ' '; 63 | indent = character.repeat(indentWidth || 2); 64 | } 65 | 66 | includePaths = [ 67 | process.cwd(), 68 | ...(includePaths || []), 69 | ...(process.env.hasOwnProperty('SASS_PATH') 70 | ? process.env.SASS_PATH.split(path.delimiter) 71 | : []), 72 | ]; 73 | 74 | const importers = Array.isArray(options.importer) 75 | ? options.importer 76 | : options.importer 77 | ? [options.importer] 78 | : []; 79 | const importersLength = importers.length; 80 | 81 | const functions = []; 82 | const functionSignatures = []; 83 | Object.entries(options.functions || {}) 84 | .map(normalizeFunctionSignature) 85 | .forEach(([signature, callback]) => { 86 | functionSignatures.push(signature); 87 | functions.push(callback); 88 | }); 89 | 90 | const processedOptions = { 91 | data, 92 | file, 93 | outFile, 94 | sourceMap, 95 | sourceMapRoot, 96 | linefeed, 97 | indent, // instead of indentType + indentWidth 98 | outputStyle, 99 | precision, 100 | indentedSyntax, 101 | sourceComments, 102 | omitSourceMapUrl, 103 | sourceMapEmbed, 104 | sourceMapContents, 105 | includePaths, 106 | importersLength, // instead of importer 107 | functionSignatures, // instead of functions 108 | }; 109 | 110 | const thisObj = { options: processedOptions }; 111 | 112 | return { processedOptions, start, importers, functions, thisObj }; 113 | } 114 | 115 | function normalizeFunctionSignature([signature, callback]) { 116 | if ( 117 | signature == '*' || 118 | signature == '@warn' || 119 | signature == '@error' || 120 | signature == '@debug' || 121 | /^\w+\(.*\)$/.test(signature) 122 | ) { 123 | return [signature, callback]; 124 | } else if (/^\w+$/.test(signature)) { 125 | const newSignature = signature + '(...)'; 126 | function newCallback(firstArg, ...otherArgs) { 127 | if (!firstArg || firstArg._type != 'list') { 128 | throw Error('Expected a list from libsass'); 129 | } 130 | return callback.apply(this, [...firstArg._values, ...otherArgs]); 131 | } 132 | return [newSignature, newCallback]; 133 | } else { 134 | throw Error('Bad function signature: ' + signature); 135 | } 136 | } 137 | 138 | function externalHelperSync(context, helperInput) { 139 | try { 140 | if (helperInput.type == 'importer') { 141 | const { index, file, prev } = helperInput; 142 | const result = context.importers[index].call(context.thisObj, file, prev); 143 | return stringifyJson(result); 144 | } 145 | if (helperInput.type == 'function') { 146 | const index = helperInput.index; 147 | const args = types.revive(helperInput.args); 148 | if (args._type != 'list') throw Error('Expected a list from libsass'); 149 | const result = context.functions[index].apply( 150 | context.thisObj, 151 | args._values 152 | ); 153 | return stringifyJson(result); 154 | } 155 | throw Error('Bad helperInput.type'); 156 | } catch (e) { 157 | return stringifyJson(normalizeError(e)); 158 | } 159 | } 160 | 161 | async function externalHelperAsync(context, helperInput) { 162 | try { 163 | if (helperInput.type == 'importer') { 164 | const { index, file, prev } = helperInput; 165 | const result = await callWithSyncOrAsyncResult( 166 | context.importers[index], 167 | context.thisObj, 168 | [file, prev] 169 | ); 170 | return stringifyJson(result); 171 | } 172 | if (helperInput.type == 'function') { 173 | const index = helperInput.index; 174 | const args = types.revive(helperInput.args); 175 | if (args._type != 'list') throw Error('Expected a list from libsass'); 176 | const result = await callWithSyncOrAsyncResult( 177 | context.functions[index], 178 | context.thisObj, 179 | args._values 180 | ); 181 | return stringifyJson(result); 182 | } 183 | throw Error('Bad helperInput.type'); 184 | } catch (e) { 185 | return stringifyJson(normalizeError(e)); 186 | } 187 | } 188 | 189 | function callWithSyncOrAsyncResult(func, thisArg, args) { 190 | return new Promise((resolve, reject) => { 191 | function done(value) { 192 | if (value instanceof Error) reject(value); 193 | else resolve(value); 194 | } 195 | const syncResult = func.apply(thisArg, [...args, done]); 196 | if (syncResult !== undefined) done(syncResult); 197 | }); 198 | } 199 | 200 | function normalizeError(e) { 201 | return e instanceof Error 202 | ? e 203 | : typeof e == 'string' 204 | ? Error(e) 205 | : Error('An unexpected error occurred'); 206 | } 207 | 208 | function postprocess(result, { processedOptions, start }) { 209 | if (result.optionsError) throw Error(result.optionsError); 210 | if (result.error) throw Object.assign(new Error(), JSON.parse(result.error)); 211 | const end = Date.now(); 212 | return { 213 | css: Buffer.from(result.css, 'utf8'), 214 | map: result.map && Buffer.from(result.map, 'utf8'), 215 | stats: { 216 | start, 217 | end, 218 | duration: end - start, 219 | entry: processedOptions.file || 'data', 220 | includedFiles: result.includedFiles, 221 | }, 222 | }; 223 | } 224 | 225 | exports.renderSync = function(options) { 226 | const binding = require('../dist/binding'); 227 | const context = preprocess(options); 228 | const result = binding.sassRender( 229 | context.processedOptions, 230 | externalHelperSync.bind(null, context) 231 | ); 232 | return postprocess(result, context); 233 | }; 234 | 235 | exports.render = function(options, callback) { 236 | const context = preprocess(options); 237 | asyncRunner 238 | .run(context.processedOptions, externalHelperAsync.bind(null, context)) 239 | .then(result => postprocess(result, context)) 240 | .then( 241 | result => callback.call(context.thisObj, null, result), 242 | error => callback.call(context.thisObj, error) 243 | ); 244 | }; 245 | 246 | // Lie about the version because sass-loader wants node-sass ^4.0.0. 247 | const emulatedVersion = '4.12.0'; 248 | 249 | exports.info = 250 | `node-sass\t${emulatedVersion}\t(compatible)\t[compatible]\n` + 251 | `libsass \t${libsassVersion}\t(Sass Compiler)\t[C/C++]\n` + 252 | `node-sass-wasm\t${packageVersion}\t(Wrapper)\t[JavaScript]`; 253 | 254 | exports.wasm = packageVersion; 255 | 256 | exports.types = types; 257 | exports.TRUE = types.Boolean.TRUE; 258 | exports.FALSE = types.Boolean.FALSE; 259 | exports.NULL = types.Null.NULL; 260 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | function checkType(name, type, value) { 8 | if (typeof value != type) throw TypeError(name + ' must be a ' + type); 9 | return value; 10 | } 11 | 12 | function setItem(array, index, value) { 13 | if (index < 0 || index >= array.length) throw Error('index out of bounds'); 14 | if (value !== null && !value._type) { 15 | throw TypeError('value must be a sass type'); 16 | } 17 | array[index] = value; 18 | } 19 | 20 | function SassBoolean(value) { 21 | if (new.target) throw Error('Cannot instantiate SassBoolean'); 22 | return checkType('value', 'boolean', value) 23 | ? SassBoolean.TRUE 24 | : SassBoolean.FALSE; 25 | } 26 | SassBoolean.prototype.getValue = function() { 27 | return this._value; 28 | }; 29 | SassBoolean.TRUE = Object.assign(Object.create(SassBoolean.prototype), { 30 | _type: 'boolean', 31 | _value: true, 32 | }); 33 | SassBoolean.FALSE = Object.assign(Object.create(SassBoolean.prototype), { 34 | _type: 'boolean', 35 | _value: false, 36 | }); 37 | 38 | function SassNumber(value = 0, unit = '') { 39 | if (!new.target) return new SassNumber(value, unit); 40 | this._type = 'number'; 41 | this.setValue(value); 42 | this.setUnit(unit); 43 | } 44 | SassNumber.prototype.setValue = function(v) { 45 | this._value = checkType('value', 'number', v); 46 | }; 47 | SassNumber.prototype.getValue = function() { 48 | return this._value; 49 | }; 50 | SassNumber.prototype.setUnit = function(v) { 51 | this._unit = checkType('unit', 'string', v); 52 | }; 53 | SassNumber.prototype.getUnit = function() { 54 | return this._unit; 55 | }; 56 | 57 | function SassColor(r = undefined, g = undefined, b = undefined, a = undefined) { 58 | if (!new.target) return new SassColor(r, g, b, a); 59 | if ( 60 | r === undefined && 61 | g === undefined && 62 | b === undefined && 63 | a === undefined 64 | ) { 65 | r = g = b = 0; 66 | a = 1; 67 | } else if ( 68 | r !== undefined && 69 | g === undefined && 70 | b === undefined && 71 | a === undefined 72 | ) { 73 | const argb = checkType('argb', 'number', r); 74 | a = ((argb >> 0o30) & 0xff) / 0xff; 75 | r = (argb >> 0o20) & 0xff; 76 | g = (argb >> 0o10) & 0xff; 77 | b = (argb >> 0o00) & 0xff; 78 | } else if ( 79 | r !== undefined && 80 | g !== undefined && 81 | b !== undefined && 82 | a === undefined 83 | ) { 84 | a = 1; 85 | } else if ( 86 | r !== undefined && 87 | g !== undefined && 88 | b !== undefined && 89 | a !== undefined 90 | ) { 91 | } else { 92 | throw Error('Color should be constructed with 0, 1, 3 or 4 arguments'); 93 | } 94 | this._type = 'color'; 95 | this.setR(r); 96 | this.setG(g); 97 | this.setB(b); 98 | this.setA(a); 99 | } 100 | SassColor.prototype.setR = function(v) { 101 | this._r = checkType('r', 'number', v); 102 | }; 103 | SassColor.prototype.getR = function(v) { 104 | return this._r; 105 | }; 106 | SassColor.prototype.setG = function(v) { 107 | this._g = checkType('g', 'number', v); 108 | }; 109 | SassColor.prototype.getG = function(v) { 110 | return this._g; 111 | }; 112 | SassColor.prototype.setB = function(v) { 113 | this._b = checkType('b', 'number', v); 114 | }; 115 | SassColor.prototype.getB = function(v) { 116 | return this._b; 117 | }; 118 | SassColor.prototype.setA = function(v) { 119 | this._a = checkType('a', 'number', v); 120 | }; 121 | SassColor.prototype.getA = function(v) { 122 | return this._a; 123 | }; 124 | 125 | function SassString(value = '') { 126 | if (!new.target) return new SassString(value); 127 | this._type = 'string'; 128 | this.setValue(value); 129 | this.setQuoted(false); 130 | } 131 | SassString.prototype.setValue = function(v) { 132 | this._value = checkType('value', 'string', v); 133 | }; 134 | SassString.prototype.getValue = function() { 135 | return this._value; 136 | }; 137 | SassString.prototype.setQuoted = function(v) { 138 | this._quoted = checkType('quoted', 'boolean', v); 139 | }; 140 | SassString.prototype.getQuoted = function() { 141 | return this._quoted; 142 | }; 143 | 144 | function SassList(length = 0, commaSeparator = true) { 145 | if (!new.target) return new SassList(length, commaSeparator); 146 | checkType('length', 'number', length); 147 | this._type = 'list'; 148 | this._values = []; 149 | for (let i = 0; i < length; i++) this._values[i] = null; 150 | this.setSeparator(commaSeparator); 151 | this.setIsBracketed(false); 152 | } 153 | SassList.prototype.setValue = function(i, v) { 154 | setItem(this._values, i, v); 155 | }; 156 | SassList.prototype.getValue = function(i) { 157 | return this._values[i]; 158 | }; 159 | SassList.prototype.setSeparator = function(v) { 160 | this._separator = checkType('separator', 'boolean', v); 161 | }; 162 | SassList.prototype.getSeparator = function() { 163 | return this._separator; 164 | }; 165 | SassList.prototype.setIsBracketed = function(v) { 166 | this._isBracketed = checkType('isBracketed', 'boolean', v); 167 | }; 168 | SassList.prototype.getIsBracketed = function() { 169 | return this._isBracketed; 170 | }; 171 | SassList.prototype.getLength = function() { 172 | return this._values.length; 173 | }; 174 | 175 | function SassMap(length = 0) { 176 | if (!new.target) return new SassMap(length); 177 | checkType('length', 'number', length); 178 | this._type = 'map'; 179 | this._keys = []; 180 | this._values = []; 181 | for (let i = 0; i < length; i++) this._keys[i] = this._values[i] = null; 182 | } 183 | SassMap.prototype.setKey = function(i, v) { 184 | setItem(this._keys, i, v); 185 | }; 186 | SassMap.prototype.getKey = function(i) { 187 | return this._keys[i]; 188 | }; 189 | SassMap.prototype.setValue = function(i, v) { 190 | setItem(this._values, i, v); 191 | }; 192 | SassMap.prototype.getValue = function(i) { 193 | return this._values[i]; 194 | }; 195 | SassMap.prototype.getLength = function() { 196 | return this._values.length; 197 | }; 198 | 199 | function SassError(message = '') { 200 | if (!new.target) return new SassError(message); 201 | this._type = 'error'; 202 | this._message = checkType('message', 'string', message); 203 | } 204 | 205 | function SassWarning(message = '') { 206 | if (!new.target) return new SassWarning(message); 207 | this._type = 'warning'; 208 | this._message = checkType('message', 'string', message); 209 | } 210 | 211 | function SassNull() { 212 | if (new.target) throw Error('Cannot instantiate SassNull'); 213 | return SassNull.NULL; 214 | } 215 | SassNull.prototype.toJSON = function() { 216 | return null; 217 | }; 218 | SassNull.NULL = Object.create(SassNull.prototype); 219 | 220 | const typeMap = {}; 221 | exports.Boolean = typeMap.boolean = SassBoolean; 222 | exports.Number = typeMap.number = SassNumber; 223 | exports.Color = typeMap.color = SassColor; 224 | exports.String = typeMap.string = SassString; 225 | exports.List = typeMap.list = SassList; 226 | exports.Map = typeMap.map = SassMap; 227 | exports.Error = typeMap.error = SassError; 228 | exports.Warning = typeMap.warning = SassWarning; 229 | exports.Null = SassNull; 230 | 231 | function revive(value) { 232 | if (value === null) return SassNull.NULL; 233 | if (typeof value != 'object') throw TypeError('Sass value is not an object'); 234 | const type = value._type; 235 | if (!typeMap[type]) throw TypeError('Invalid _type'); 236 | 237 | if (type == 'boolean') { 238 | return value._value ? SassBoolean.TRUE : SassBoolean.FALSE; 239 | } 240 | 241 | const result = Object.assign(Object.create(typeMap[type].prototype), value); 242 | if (result._keys) result._keys = result._keys.map(revive); 243 | if (result._values) result._values = result._values.map(revive); 244 | return result; 245 | } 246 | 247 | exports.revive = revive; 248 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sass-wasm", 3 | "version": "0.1.0", 4 | "description": "WebAssembly rewrite of node-sass", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib/*.js", 8 | "dist/*.js", 9 | "dist/*.wasm" 10 | ], 11 | "license": "MIT", 12 | "devDependencies": { 13 | "mocha": "^6.1.4", 14 | "prettier": "^1.18.2", 15 | "read-yaml": "^1.1.0", 16 | "sass-spec": "^3.5.4-1" 17 | }, 18 | "prettier": { 19 | "singleQuote": true, 20 | "trailingComma": "es5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /patchtest.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | fs = require('fs'); 8 | 9 | if (!process.argv[2] || !process.argv[3]) throw Error('Usage: patchtest.js input.js output.js'); 10 | 11 | content = fs.readFileSync(process.argv[2], 'utf8'); 12 | old = content; 13 | 14 | content = content.replace("path = require('path'),", "path = require('path'),\n util = require('util'),"); 15 | content = content.replace("sass = require(sassPath),", "sass = require(sassPath),\n render = util.promisify(sass.render),"); 16 | 17 | content = content.replace(/describe/, 18 | `function catchError(promise) { 19 | return promise.then( 20 | function (success) { throw Error("Unexpected success: " + util.inspect(success)); }, 21 | function (error) { return error; }); 22 | } 23 | 24 | describe`) 25 | 26 | content = content.replace("function(err, data) {\n assert.equal(err, null);\n", "function(error, result) {"); 27 | content = content.replace("data.css", "result.css"); 28 | 29 | content = content.replace(" sass.render({ }, function(error) {", " sass.render({\n }, function(error) {"); 30 | 31 | content = content.replace(/\n\n+( +done\(\);)/g, "\n$1"); 32 | 33 | content = content.replace( 34 | /\n sass\.render\((options|\{\n(?:\n| .*\n)* \}), function\(\) \{\n( .*this\..*\n) done\(\);\n \}\);\n/g, 35 | function (match, g1, g2) { 36 | return ` 37 | var thisObject = await new Promise((resolve, reject) => sass.render(${g1}, function(error, result) { 38 | if (error) reject(error); 39 | else resolve(this); 40 | })); 41 | ` + g2.replace(/^ /gm, '').replace(/\bthis\b/g, 'thisObject'); 42 | }); 43 | 44 | content = content.replace( 45 | /\n sass\.render\((options|\{\n(?:\n| .*\n)* \}), function\(\) \{\n((?:\n| .*\n)*?)(?: done\(\);\n)? \}\);\n/g, 46 | function (match, g1, g2) { 47 | return '\n await render(' + g1 + ');\n' + g2.replace(/^ /gm, ''); 48 | }); 49 | 50 | content = content.replace( 51 | /\n sass\.render\((options|\{\n(?:\n| .*\n)* \}), function\(errr?or, result\) \{\n((?:\n| .*\n)*) done\(\);\n \}\);\n/g, 52 | function (match, g1, g2) { 53 | g2 = g2.replace(/^ assert\(\!error\);\n/gm, ''); 54 | if (/error/.test(g2)) throw Error(g2); 55 | return '\n var result = await render(' + g1 + ');\n' + g2.replace(/^ /gm, ''); 56 | }); 57 | 58 | content = content.replace( 59 | /\n sass\.render\((options|\{\n(?:\n| .*\n)* \}), function\(error\) \{\n((?:\n| .*\n)*) done\(\);\n \}\);\n/g, 60 | function (match, g1, g2) { 61 | return '\n var error = await catchError(render(' + g1 + '));\n' + g2.replace(/^ /gm, ''); 62 | }); 63 | 64 | content = content.replace( 65 | /\n sass\.render\((options|\{\n(?:\n| .*\n)* \}), function\(error\) \{\n((?:\n| .*\n)*) done\(\);\n \}\);\n/g, 66 | function (match, g1, g2) { 67 | return '\n var error = await catchError(render(' + g1 + '));\n' + g2.replace(/^ /gm, ''); 68 | }); 69 | 70 | content = content.replace( 71 | /\n sass\.render\((options|\{\n(?:\n| .*\n)* \}), function\(error, result\) \{\n(.*expectedRed.*\n) \}\);\n/g, 72 | function (match, g1, g2) { 73 | g2 = g2.replace(/^ assert\(\!error\);\n/gm, ''); 74 | if (/error/.test(g2)) throw Error(g2); 75 | return '\n var result = await render(' + g1 + ');\n' + g2.replace(/^ /gm, ''); 76 | }); 77 | 78 | content = content.replace( 79 | /(\n it\(.*, )function *\(done\) \{\n((?:\n| .*\n)*) done\(\);\n \}\);\n/g, 80 | function (match, g1, g2) { 81 | if (/await/.test(g2)) return g1 + 'async function() {\n' + g2 + ' });\n'; 82 | return g1 + 'function() {\n' + g2 + ' });\n'; 83 | }); 84 | 85 | content = content.replace( 86 | /(\n it\(.*, )function *\(done\) \{\n((?:\n| .*\n)*) \}\);\n/g, 87 | function (match, g1, g2) { 88 | if (/await/.test(g2)) return g1 + 'async function() {\n' + g2 + ' });\n'; 89 | return match; 90 | }); 91 | 92 | content = content.replace( 93 | /(\n it\(.*, )function *\(done\) \{\n((?:\n| .*\n)*) \}\);\n/g, 94 | function (match, g1, g2) { 95 | if (/await/.test(g2)) return g1 + 'async function() {\n' + g2 + ' });\n'; 96 | return match; 97 | }); 98 | 99 | content = content.replace("render({\n })", "render({})"); 100 | 101 | content = content.replace(/returned value of `contents` must be a string/g, "result.contents must be a string if it's set"); 102 | content = content.replace(/A SassValue object was expected/g, "Found a raw JavaScript value instead of an instance of a sass type"); 103 | content = content.replace(/Expected one boolean argument/g, "value must be a boolean"); 104 | content = content.replace(/Supplied value should be a string/g, "unit must be a string"); 105 | content = content.replace(/Supplied value should be a SassValue object/g, "value must be a sass type"); 106 | content = content.replace(/A SassValue is expected as the list item/g, "value must be a sass type"); 107 | content = content.replace(/A SassValue is expected as a map key/g, "value must be a sass type"); 108 | content = content.replace(/A SassValue is expected as a map value/g, "value must be a sass type"); 109 | content = content.replace(/Constructor arguments should be numbers exclusively/g, "r must be a number"); 110 | content = content.replace(/Constructor should be invoked with either 0, 1, 3 or 4 arguments/g, "Color should be constructed with 0, 1, 3 or 4 arguments"); 111 | content = content.replace(/Only argument should be an integer/g, "argb must be a number"); 112 | 113 | var count = 0; 114 | content = content.replace(/No input specified: provide a file name or a source string to process/g, 115 | () => (++count % 2) ? 'Data context created with empty source string' : 'At least one of options.data or options.file must be set'); 116 | 117 | fs.writeFileSync(process.argv[3], content, 'utf8'); 118 | -------------------------------------------------------------------------------- /src/entrypoint.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "functions.h" 16 | #include "importers.h" 17 | 18 | namespace node_sass { 19 | 20 | using ::emscripten::val; 21 | using ::std::make_unique; 22 | using ::std::nullopt; 23 | using ::std::optional; 24 | using ::std::string; 25 | using ::std::unique_ptr; 26 | using ::std::vector; 27 | 28 | class OptionsError : public std::invalid_argument { 29 | public: 30 | using invalid_argument::invalid_argument; 31 | }; 32 | 33 | optional getString(val options, const string& key) { 34 | val value = options[key]; 35 | if (value.isUndefined() || value.isNull()) { 36 | return nullopt; 37 | } else if (value.isString()) { 38 | return value.as(); 39 | } else { 40 | throw OptionsError("options." + key + " is not a string or null"); 41 | } 42 | } 43 | 44 | optional getInt(val options, const string& key) { 45 | val value = options[key]; 46 | if (value.isUndefined() || value.isNull()) { 47 | return nullopt; 48 | } else if (value.isNumber()) { 49 | return value.as(); 50 | } else { 51 | throw OptionsError("options." + key + " is not a number or null"); 52 | } 53 | } 54 | 55 | val sassRender(val options, val externalHelper) { 56 | try { 57 | optional data = getString(options, "data"); 58 | optional file = getString(options, "file"); 59 | optional outFile = getString(options, "outFile"); 60 | optional sourceMap = getString(options, "sourceMap"); 61 | optional sourceMapRoot = getString(options, "sourceMapRoot"); 62 | optional linefeed = getString(options, "linefeed"); 63 | optional indent = getString(options, "indent"); 64 | optional outputStyle = getString(options, "outputStyle"); 65 | optional precision = getInt(options, "precision"); 66 | int importersLength = getInt(options, "importersLength").value_or(0); 67 | bool indentedSyntax = options["indentedSyntax"].as(); 68 | bool sourceComments = options["sourceComments"].as(); 69 | bool omitSourceMapUrl = options["omitSourceMapUrl"].as(); 70 | bool sourceMapEmbed = options["sourceMapEmbed"].as(); 71 | bool sourceMapContents = options["sourceMapContents"].as(); 72 | 73 | vector includePaths; 74 | if (val arr = options["includePaths"]; !arr.isUndefined() && !arr.isNull()) { 75 | if (!arr.isArray()) throw OptionsError("options.includePaths is not an array or null"); 76 | int length = arr["length"].as(); 77 | for (int i = 0; i < length; i++) { 78 | val element = arr[i]; 79 | if (!element.isString()) throw OptionsError("options.includePaths[" + std::to_string(i) + "] is not a string"); 80 | includePaths.push_back(element.as()); 81 | } 82 | } 83 | 84 | vector> importers; 85 | for (int i = 0; i < importersLength; i++) { 86 | importers.push_back(make_unique(externalHelper, i)); 87 | } 88 | 89 | vector functionSignatures; 90 | vector> functions; 91 | if (val arr = options["functionSignatures"]; !arr.isUndefined() && !arr.isNull()) { 92 | if (!arr.isArray()) throw OptionsError("options.functionSignatures is not an array or null"); 93 | int length = arr["length"].as(); 94 | for (int i = 0; i < length; i++) { 95 | val element = arr[i]; 96 | if (!element.isString()) throw OptionsError("options.functionSignatures[" + std::to_string(i) + "] is not a string"); 97 | functionSignatures.push_back(element.as()); 98 | functions.push_back(make_unique(externalHelper, i)); 99 | } 100 | } 101 | 102 | auto extractOptions = [&](Sass_Context* ctx) { 103 | Sass_Options* sass_options = sass_context_get_options(ctx); 104 | 105 | if (outFile) sass_option_set_output_path(sass_options, outFile->c_str()); 106 | if (sourceMap) sass_option_set_source_map_file(sass_options, sourceMap->c_str()); 107 | if (sourceMapRoot) sass_option_set_source_map_root(sass_options, sourceMapRoot->c_str()); 108 | if (linefeed) sass_option_set_linefeed(sass_options, linefeed->c_str()); 109 | if (indent) sass_option_set_indent(sass_options, indent->c_str()); 110 | for (const string& path : includePaths) sass_option_push_include_path(sass_options, path.c_str()); 111 | if (outputStyle == "nested") sass_option_set_output_style(sass_options, SASS_STYLE_NESTED); 112 | if (outputStyle == "expanded") sass_option_set_output_style(sass_options, SASS_STYLE_EXPANDED); 113 | if (outputStyle == "compact") sass_option_set_output_style(sass_options, SASS_STYLE_COMPACT); 114 | if (outputStyle == "compressed") sass_option_set_output_style(sass_options, SASS_STYLE_COMPRESSED); 115 | sass_option_set_is_indented_syntax_src(sass_options, indentedSyntax); 116 | sass_option_set_source_comments(sass_options, sourceComments); 117 | sass_option_set_omit_source_map_url(sass_options, omitSourceMapUrl); 118 | sass_option_set_source_map_embed(sass_options, sourceMapEmbed); 119 | sass_option_set_source_map_contents(sass_options, sourceMapContents); 120 | sass_option_set_precision(sass_options, precision.value_or(5)); // libsass default is 10 but node-sass default is 5. 121 | 122 | if (importersLength) { 123 | Sass_Importer_List c_importers = sass_make_importer_list(importersLength); 124 | for (int i = 0; i < importersLength; i++) { 125 | sass_importer_set_list_entry(c_importers, i, sass_make_importer(runImporter, importersLength - i - 1, importers[i].get())); 126 | } 127 | sass_option_set_c_importers(sass_options, c_importers); 128 | } 129 | 130 | if (!functions.empty()) { 131 | int functionsLength = functionSignatures.size(); 132 | Sass_Function_List c_functions = sass_make_function_list(functionsLength); 133 | for (int i = 0; i < functionsLength; i++) { 134 | sass_function_set_list_entry(c_functions, i, sass_make_function(functionSignatures[i].c_str(), runFunction, functions[i].get())); 135 | } 136 | sass_option_set_c_functions(sass_options, c_functions); 137 | } 138 | }; 139 | 140 | auto createResult = [&](Sass_Context* ctx) { 141 | val result = val::object(); 142 | 143 | if (sass_context_get_error_status(ctx) != 0) { 144 | result.set("error", sass_context_get_error_json(ctx)); 145 | } else { 146 | result.set("css", sass_context_get_output_string(ctx)); 147 | 148 | const char* map = sass_context_get_source_map_string(ctx); 149 | if (map) result.set("map", map); 150 | 151 | vector included_files_vector; 152 | char** included_files = sass_context_get_included_files(ctx); 153 | for (int i = 0; included_files && included_files[i]; i++) { 154 | included_files_vector.push_back(included_files[i]); 155 | } 156 | result.set("includedFiles", val::array(included_files_vector)); 157 | } 158 | 159 | return result; 160 | }; 161 | 162 | if (data) { 163 | Sass_Data_Context* dctx = sass_make_data_context(data->data()); 164 | extractOptions(sass_data_context_get_context(dctx)); 165 | if (file) sass_option_set_input_path(sass_data_context_get_options(dctx), file->c_str()); 166 | 167 | sass_compile_data_context(dctx); 168 | val result = createResult(sass_data_context_get_context(dctx)); 169 | 170 | sass_delete_data_context(dctx); 171 | return result; 172 | } else if (file) { 173 | Sass_File_Context* fctx = sass_make_file_context(file->c_str()); 174 | extractOptions(sass_file_context_get_context(fctx)); 175 | 176 | sass_compile_file_context(fctx); 177 | val result = createResult(sass_file_context_get_context(fctx)); 178 | 179 | sass_delete_file_context(fctx); 180 | return result; 181 | } else { 182 | throw OptionsError("At least one of options.data or options.file must be set"); 183 | } 184 | } catch (OptionsError& e) { 185 | val result = val::object(); 186 | result.set("optionsError", e.what()); 187 | return result; 188 | } 189 | } 190 | 191 | string sassVersion() { 192 | return libsass_version(); 193 | } 194 | 195 | EMSCRIPTEN_BINDINGS(sass_bindings) { 196 | emscripten::function("sassRender", &sassRender); 197 | emscripten::function("sassVersion", &sassVersion); 198 | } 199 | 200 | } // namespace node_sass 201 | -------------------------------------------------------------------------------- /src/functions.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | #include "functions.h" 8 | 9 | #include 10 | 11 | namespace node_sass { 12 | 13 | using ::emscripten::val; 14 | using ::std::string; 15 | 16 | class ConversionError : public std::invalid_argument { 17 | public: 18 | using invalid_argument::invalid_argument; 19 | }; 20 | 21 | static val cppToJs(const Sass_Value* value) { 22 | switch (sass_value_get_tag(value)) { 23 | case SASS_BOOLEAN: { 24 | val result = val::object(); 25 | result.set("_type", "boolean"); 26 | result.set("_value", sass_boolean_get_value(value)); 27 | return result; 28 | } 29 | 30 | case SASS_NUMBER: { 31 | val result = val::object(); 32 | result.set("_type", "number"); 33 | result.set("_value", sass_number_get_value(value)); 34 | result.set("_unit", sass_number_get_unit(value)); 35 | return result; 36 | } 37 | 38 | case SASS_COLOR: { 39 | val result = val::object(); 40 | result.set("_type", "color"); 41 | result.set("_r", sass_color_get_r(value)); 42 | result.set("_g", sass_color_get_g(value)); 43 | result.set("_b", sass_color_get_b(value)); 44 | result.set("_a", sass_color_get_a(value)); 45 | return result; 46 | } 47 | 48 | case SASS_STRING: { 49 | val result = val::object(); 50 | result.set("_type", "string"); 51 | result.set("_value", sass_string_get_value(value)); 52 | result.set("_quoted", sass_string_is_quoted(value)); 53 | return result; 54 | } 55 | 56 | case SASS_LIST: { 57 | int length = sass_list_get_length(value); 58 | val values = val::array(); 59 | for (int i = 0; i < length; i++) { 60 | values.call("push", cppToJs(sass_list_get_value(value, i))); 61 | } 62 | val result = val::object(); 63 | result.set("_type", "list"); 64 | result.set("_values", values); 65 | result.set("_separator", sass_list_get_separator(value) == SASS_COMMA); 66 | result.set("_isBracketed", sass_list_get_is_bracketed(value)); 67 | return result; 68 | } 69 | 70 | case SASS_MAP: { 71 | int length = sass_map_get_length(value); 72 | val keys = val::array(), values = val::array(); 73 | for (int i = 0; i < length; i++) { 74 | keys.call("push", cppToJs(sass_map_get_key(value, i))); 75 | values.call("push", cppToJs(sass_map_get_value(value, i))); 76 | } 77 | val result = val::object(); 78 | result.set("_type", "map"); 79 | result.set("_keys", keys); 80 | result.set("_values", values); 81 | return result; 82 | } 83 | 84 | case SASS_NULL: { 85 | return val::null(); 86 | } 87 | 88 | case SASS_ERROR: { 89 | val result = val::object(); 90 | result.set("_type", "error"); 91 | result.set("_message", sass_error_get_message(value)); 92 | return result; 93 | } 94 | 95 | case SASS_WARNING: { 96 | val result = val::object(); 97 | result.set("_type", "warning"); 98 | result.set("_message", sass_warning_get_message(value)); 99 | return result; 100 | } 101 | 102 | default: { 103 | val result = val::object(); 104 | result.set("_type", "error"); 105 | result.set("_message", "Unsupported Sass_Value type"); 106 | return result; 107 | } 108 | } 109 | } 110 | 111 | static Sass_Value* jsToCpp(val value) { 112 | if (value.isNull() || value.isUndefined()) return sass_make_null(); 113 | 114 | val errorVal = value["__error"]; 115 | if (!errorVal.isUndefined() && !errorVal.isNull()) { 116 | if (!errorVal.isString()) throw ConversionError("Error message is not a string"); 117 | return sass_make_error(errorVal.as().c_str()); 118 | } 119 | 120 | val typeVal = value["_type"]; 121 | if (!typeVal.isString()) { 122 | if (value.isNumber()) throw ConversionError("Found a raw JavaScript number instead of types.Number"); 123 | if (value.isString()) throw ConversionError("Found a raw JavaScript string instead of types.String"); 124 | if (value.isArray()) throw ConversionError("Found a raw JavaScript array instead of types.List"); 125 | if (value.isTrue() || value.isFalse()) throw ConversionError("Found a raw JavaScript boolean instead of types.Boolean"); 126 | throw ConversionError("Found a raw JavaScript value instead of an instance of a sass type"); 127 | } 128 | 129 | string type = typeVal.as(); 130 | 131 | auto readString = [&](string what) { 132 | val field = value[what]; 133 | if (!field.isString()) throw ConversionError(type + "." + what + " is missing or not a string"); 134 | return field.as(); 135 | }; 136 | auto readBool = [&](string what) { 137 | val field = value[what]; 138 | if (!field.isTrue() && !field.isFalse()) throw ConversionError(type + "." + what + " is missing or not a bool"); 139 | return field.as(); 140 | }; 141 | auto readNumber = [&](string what) { 142 | val field = value[what]; 143 | if (!field.isNumber()) throw ConversionError(type + "." + what + " is missing or not a number"); 144 | return field.as(); 145 | }; 146 | auto readArray = [&](string what) { 147 | val field = value[what]; 148 | if (!field.isArray()) throw ConversionError(type + "." + what + " is missing or not an array"); 149 | return field; 150 | }; 151 | 152 | if (type == "boolean") { 153 | return sass_make_boolean(readBool("_value")); 154 | } else if (type == "number") { 155 | return sass_make_number(readNumber("_value"), readString("_unit").c_str()); 156 | } else if (type == "color") { 157 | return sass_make_color(readNumber("_r"), readNumber("_g"), readNumber("_b"), readNumber("_a")); 158 | } else if (type == "string") { 159 | return readBool("_quoted") ? sass_make_qstring(readString("_value").c_str()) : sass_make_string(readString("_value").c_str()); 160 | } else if (type == "list") { 161 | val values = readArray("_values"); 162 | int length = values["length"].as(); 163 | Sass_Value* result = sass_make_list(length, readBool("_separator") ? SASS_COMMA : SASS_SPACE, readBool("_isBracketed")); 164 | try { 165 | for (int i = 0; i < length; i++) { 166 | sass_list_set_value(result, i, jsToCpp(values[i])); 167 | } 168 | } catch (ConversionError&) { 169 | sass_delete_value(result); 170 | throw; 171 | } 172 | return result; 173 | } else if (type == "map") { 174 | val keys = readArray("_keys"); 175 | val values = readArray("_values"); 176 | int length = values["length"].as(); 177 | Sass_Value* result = sass_make_map(length); 178 | try { 179 | for (int i = 0; i < length; i++) { 180 | sass_map_set_key(result, i, jsToCpp(keys[i])); 181 | sass_map_set_value(result, i, jsToCpp(values[i])); 182 | } 183 | } catch (ConversionError&) { 184 | sass_delete_value(result); 185 | throw; 186 | } 187 | return result; 188 | } else if (type == "error") { 189 | return sass_make_error(readString("_message").c_str()); 190 | } else if (type == "warning") { 191 | return sass_make_warning(readString("_message").c_str()); 192 | } else { 193 | throw ConversionError("Unrecognized value of _type"); 194 | } 195 | } 196 | 197 | Sass_Value* runFunction(const Sass_Value* s_args, Sass_Function_Entry cb, Sass_Compiler* comp) { 198 | FunctionData* functionData = static_cast(sass_function_get_cookie(cb)); 199 | val externalHelper = functionData->externalHelper; 200 | int index = functionData->index; 201 | 202 | val helperInput = val::object(); 203 | helperInput.set("type", "function"); 204 | helperInput.set("index", index); 205 | helperInput.set("args", cppToJs(s_args)); 206 | 207 | val helperOutput = externalHelper(helperInput); 208 | 209 | val helperOutputDecoded = val::global("JSON").call("parse", helperOutput); 210 | Sass_Value* result; 211 | try { 212 | result = jsToCpp(helperOutputDecoded); 213 | } catch (ConversionError& e) { 214 | result = sass_make_error((string("Internal error deserializing sass value: ") + e.what()).c_str()); 215 | } 216 | return result; 217 | } 218 | 219 | } // namespace node_sass 220 | -------------------------------------------------------------------------------- /src/functions.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | 12 | namespace node_sass { 13 | 14 | struct FunctionData { 15 | FunctionData(emscripten::val externalHelper, int index) : externalHelper(externalHelper), index(index) {} 16 | emscripten::val externalHelper; 17 | int index; 18 | }; 19 | 20 | Sass_Value* runFunction(const Sass_Value* s_args, Sass_Function_Entry cb, Sass_Compiler* comp); 21 | 22 | } // namespace node_sass 23 | -------------------------------------------------------------------------------- /src/importers.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | #include "importers.h" 8 | 9 | #include 10 | 11 | #include 12 | 13 | namespace node_sass { 14 | 15 | using ::emscripten::val; 16 | using ::std::nullopt; 17 | using ::std::optional; 18 | using ::std::string; 19 | 20 | static Sass_Import_Entry makeError(const string& what) { 21 | Sass_Import_Entry import = sass_make_import_entry(nullptr, nullptr, nullptr); 22 | sass_import_set_error(import, what.c_str(), -1, -1); 23 | return import; 24 | } 25 | 26 | static Sass_Import_Entry makeImport(string debugName, val value) { 27 | if (value.isNull() || value.isUndefined() || value.typeOf().as() != "object") { 28 | return makeError("Importer error: " + debugName + " must be an object"); 29 | } 30 | 31 | #define READ_STRING_FIELD(out_var, field_name) \ 32 | optional out_var; \ 33 | { \ 34 | val field = value[field_name]; \ 35 | if (field.isString()) { \ 36 | out_var = field.as(); \ 37 | } else if (!field.isUndefined() && !field.isNull()) { \ 38 | return makeError("Importer error: " + debugName + "." + (field_name) + " must be a string if it's set"); \ 39 | } \ 40 | } 41 | 42 | READ_STRING_FIELD(error, "__error"); 43 | READ_STRING_FIELD(file, "file"); 44 | READ_STRING_FIELD(contents, "contents"); 45 | READ_STRING_FIELD(map, "map"); 46 | 47 | if (error) return makeError(*error); 48 | 49 | // Note well: sass_make_import_entry takes ownership of "source" and "srcmap" but makes a copy of "path". 50 | return sass_make_import_entry( 51 | file ? file->c_str() : nullptr, 52 | contents ? strdup(contents->c_str()) : nullptr, 53 | map ? strdup(map->c_str()) : nullptr); 54 | } 55 | 56 | Sass_Import_List runImporter(const char* cur_path, Sass_Importer_Entry cb, Sass_Compiler* comp) { 57 | ImporterData* importerData = static_cast(sass_importer_get_cookie(cb)); 58 | val externalHelper = importerData->externalHelper; 59 | int index = importerData->index; 60 | 61 | Sass_Import* previous = sass_compiler_get_last_import(comp); 62 | const char* prev_path = sass_import_get_abs_path(previous); 63 | 64 | val helperInput = val::object(); 65 | helperInput.set("type", "importer"); 66 | helperInput.set("index", index); 67 | helperInput.set("file", cur_path); 68 | helperInput.set("prev", prev_path); 69 | 70 | val helperOutput = externalHelper(helperInput); 71 | 72 | val helperOutputDecoded = val::global("JSON").call("parse", helperOutput); 73 | if (helperOutputDecoded.isArray()) { 74 | int length = helperOutputDecoded["length"].as(); 75 | Sass_Import_List imports = sass_make_import_list(length); 76 | for (int i = 0; i < length; i++) imports[i] = makeImport("imports[" + std::to_string(i) + "]", helperOutputDecoded[i]); 77 | return imports; 78 | } else if (!helperOutputDecoded.isNull() && !helperOutputDecoded.isUndefined() && !helperOutputDecoded.isFalse()) { 79 | Sass_Import_List imports = sass_make_import_list(1); 80 | imports[0] = makeImport("result", helperOutputDecoded); 81 | return imports; 82 | } else { 83 | return nullptr; 84 | } 85 | } 86 | 87 | } // namespace node_sass 88 | -------------------------------------------------------------------------------- /src/importers.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | 12 | namespace node_sass { 13 | 14 | struct ImporterData { 15 | ImporterData(emscripten::val externalHelper, int index) : externalHelper(externalHelper), index(index) {} 16 | emscripten::val externalHelper; 17 | int index; 18 | }; 19 | 20 | Sass_Import_List runImporter(const char* cur_path, Sass_Importer_Entry cb, Sass_Compiler* comp); 21 | 22 | } // namespace node_sass 23 | -------------------------------------------------------------------------------- /src/workaround8806.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file or at 5 | // https://opensource.org/licenses/MIT. 6 | 7 | // Workaround for https://github.com/emscripten-core/emscripten/issues/8806 8 | 9 | if (!LibraryManager.library['$getStringOrSymbol']) { 10 | throw Error("emval.js didn't run yet"); 11 | } 12 | 13 | mergeInto(LibraryManager.library, { 14 | $getStringOrSymbol__deps: ['$emval_symbols'], 15 | $getStringOrSymbol: function(address) { 16 | var symbol = emval_symbols[address]; 17 | if (symbol === undefined) { 18 | return UTF8ToString(address); 19 | } else { 20 | return symbol; 21 | } 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2019 Google LLC 3 | # 4 | # Use of this source code is governed by an MIT-style 5 | # license that can be found in the LICENSE file or at 6 | # https://opensource.org/licenses/MIT. 7 | 8 | set -e 9 | 10 | npm install 11 | 12 | if ! [ -d test ]; then 13 | dir=$(mktemp -d) 14 | git clone https://github.com/sass/node-sass "$dir/node-sass" 15 | cp -r "$dir/node-sass/test" . 16 | fi 17 | 18 | node patchtest.js test/api.js test/api.patched.js 19 | 20 | sed -r ' 21 | s/function\(error, result\) \{/& try {/ 22 | s/done\(\);/& } catch (err) { done(err); }/ 23 | ' test/spec.js > test/spec.patched.js 24 | 25 | node_modules/.bin/mocha ${RUN_TESTS:-test/api.patched.js test/spec.patched.js} 26 | --------------------------------------------------------------------------------