├── .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 |
--------------------------------------------------------------------------------