Skip to content

Commit

Permalink
Add a JavaScript API for source map generation (#302)
Browse files Browse the repository at this point in the history
Partially addresses #2
  • Loading branch information
nex3 committed Apr 21, 2018
1 parent 9ea0015 commit fd19bc8
Show file tree
Hide file tree
Showing 16 changed files with 504 additions and 104 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,12 @@

[`SingleMapping`]: https://www.dartdocs.org/documentation/source_maps/latest/source_maps.parser/SingleMapping-class.html

### Node API

* Added support for the `sourceMap`, `omitSourceMapUrl`, `outFile`,
`sourceMapContents`, `sourceMapEmbed`, and `sourceMapRoot` options to
`render()` and `renderSync()`.

## 1.2.1

* Always emit units in compressed mode for `0` dimensions other than lengths and
Expand Down
17 changes: 7 additions & 10 deletions README.md
Expand Up @@ -173,26 +173,23 @@ sass.render({

Both `render()` and `renderSync()` support the following options:

* [`file`](https://github.com/sass/node-sass#file)
* [`data`](https://github.com/sass/node-sass#data)
* [`file`](https://github.com/sass/node-sass#file)
* [`functions`](https://github.com/sass/node-sass#functions--v300---experimental)
* [`importer`](https://github.com/sass/node-sass#importer--v200---experimental)
* [`includePaths`](https://github.com/sass/node-sass#includepaths)
* [`indentedSyntax`](https://github.com/sass/node-sass#indentedsyntax)
* [`indentType`](https://github.com/sass/node-sass#indenttype)
* [`indentWidth`](https://github.com/sass/node-sass#indentwidth)
* [`indentedSyntax`](https://github.com/sass/node-sass#indentedsyntax)
* [`linefeed`](https://github.com/sass/node-sass#linefeed)
* [`importer`](https://github.com/sass/node-sass#importer--v200---experimental)
* [`functions`](https://github.com/sass/node-sass#functions--v300---experimental)
* Only the `"expanded"` and `"compressed"` values of
[`outputStyle`](https://github.com/sass/node-sass#outputstyle) are supported.

The following options are not yet supported, but are intended:

* [`omitSourceMapUrl`](https://github.com/sass/node-sass#omitsourcemapurl)
* [`outFile`](https://github.com/sass/node-sass#outfile)
* [`sourceMap`](https://github.com/sass/node-sass#sourcemap)
* [`sourceMapContents`](https://github.com/sass/node-sass#sourcemapcontents)
* [`sourceMapEmbed`](https://github.com/sass/node-sass#sourcemapembed)
* [`sourceMapRoot`](https://github.com/sass/node-sass#sourcemaproot)
* [`sourceMap`](https://github.com/sass/node-sass#sourcemap)
* Only the `"expanded"` and `"compressed"` values of
[`outputStyle`](https://github.com/sass/node-sass#outputstyle) are supported.

No support is intended for the following options:

Expand Down
12 changes: 8 additions & 4 deletions lib/sass.dart
Expand Up @@ -87,7 +87,8 @@ String compile(String path,
packageResolver: packageResolver,
functions: functions,
style: style,
sourceMap: sourceMap);
sourceMap: sourceMap != null);
if (sourceMap != null) sourceMap(result.sourceMap);
return result.css;
}

Expand Down Expand Up @@ -167,7 +168,8 @@ String compileString(String source,
style: style,
importer: importer,
url: url,
sourceMap: sourceMap);
sourceMap: sourceMap != null);
if (sourceMap != null) sourceMap(result.sourceMap);
return result.css;
}

Expand All @@ -192,7 +194,8 @@ Future<String> compileAsync(String path,
packageResolver: packageResolver,
functions: functions,
style: style,
sourceMap: sourceMap);
sourceMap: sourceMap != null);
if (sourceMap != null) sourceMap(result.sourceMap);
return result.css;
}

Expand Down Expand Up @@ -223,6 +226,7 @@ Future<String> compileStringAsync(String source,
style: style,
importer: importer,
url: url,
sourceMap: sourceMap);
sourceMap: sourceMap != null);
if (sourceMap != null) sourceMap(result.sourceMap);
return result.css;
}
2 changes: 1 addition & 1 deletion lib/src/ast/css/node.dart
Expand Up @@ -74,7 +74,7 @@ abstract class CssNode extends AstNode {
_parent = null;
}

String toString() => serialize(this, inspect: true);
String toString() => serialize(this, inspect: true).css;
}

// NOTE: New at-rule implementations should add themselves to [AtRootRule]'s
Expand Down
42 changes: 31 additions & 11 deletions lib/src/compile.dart
Expand Up @@ -5,6 +5,7 @@
import 'dart:async';

import 'package:source_maps/source_maps.dart';
import 'package:source_span/source_span.dart';

import 'ast/sass.dart';
import 'callable.dart';
Expand Down Expand Up @@ -32,7 +33,7 @@ CompileResult compile(String path,
bool useSpaces: true,
int indentWidth,
LineFeed lineFeed,
void sourceMap(SingleMapping map)}) =>
bool sourceMap: false}) =>
compileString(readFile(path),
indented: indented ?? p.extension(path) == '.sass',
logger: logger,
Expand Down Expand Up @@ -65,7 +66,7 @@ CompileResult compileString(String source,
int indentWidth,
LineFeed lineFeed,
url,
void sourceMap(SingleMapping map)}) {
bool sourceMap: false}) {
var sassTree = indented
? new Stylesheet.parseSass(source, url: url, logger: logger)
: new Stylesheet.parseScss(source, url: url, logger: logger);
Expand All @@ -77,14 +78,15 @@ CompileResult compileString(String source,
importer: importer,
functions: functions,
logger: logger);
var css = serialize(evaluateResult.stylesheet,

var serializeResult = serialize(evaluateResult.stylesheet,
style: style,
useSpaces: useSpaces,
indentWidth: indentWidth,
lineFeed: lineFeed,
sourceMap: sourceMap);

return new CompileResult(css, evaluateResult.includedFiles);
return new CompileResult(evaluateResult, serializeResult);
}

/// Like [compileAsync] in `lib/sass.dart`, but provides more options to support
Expand All @@ -101,7 +103,7 @@ Future<CompileResult> compileAsync(String path,
bool useSpaces: true,
int indentWidth,
LineFeed lineFeed,
void sourceMap(SingleMapping map)}) =>
bool sourceMap}) =>
compileStringAsync(readFile(path),
indented: indented ?? p.extension(path) == '.sass',
logger: logger,
Expand Down Expand Up @@ -134,7 +136,7 @@ Future<CompileResult> compileStringAsync(String source,
int indentWidth,
LineFeed lineFeed,
url,
void sourceMap(SingleMapping map)}) async {
bool sourceMap}) async {
var sassTree = indented
? new Stylesheet.parseSass(source, url: url, logger: logger)
: new Stylesheet.parseScss(source, url: url, logger: logger);
Expand All @@ -146,14 +148,15 @@ Future<CompileResult> compileStringAsync(String source,
importer: importer,
functions: functions,
logger: logger);
var css = serialize(evaluateResult.stylesheet,

var serializeResult = serialize(evaluateResult.stylesheet,
style: style,
useSpaces: useSpaces,
indentWidth: indentWidth,
lineFeed: lineFeed,
sourceMap: sourceMap);

return new CompileResult(css, evaluateResult.includedFiles);
return new CompileResult(evaluateResult, serializeResult);
}

/// Converts the user's [loadPaths] and [packageResolver] options into
Expand All @@ -173,15 +176,32 @@ List<Importer> _toImporters(
/// The result of compiling a Sass document to CSS, along with metadata about
/// the compilation process.
class CompileResult {
/// The result of evaluating the source file.
final EvaluateResult _evaluate;

/// The result of serializing the CSS AST to CSS text.
final SerializeResult _serialize;

/// The compiled CSS.
final String css;
String get css => _serialize.css;

/// The source map indicating how the source files map to [css].
///
/// This is `null` if source mapping was disabled for this compilation.
SingleMapping get sourceMap => _serialize.sourceMap;

/// A map from source file URLs to the corresponding [SourceFile]s.
///
/// This can be passed to [sourceMap]'s [Mapping.spanFor] method. It's `null`
/// if source mapping was disabled for this compilation.
Map<String, SourceFile> get sourceFiles => _serialize.sourceFiles;

/// The set that will eventually populate the JS API's
/// `result.stats.includedFiles` field.
///
/// For filesystem imports, this contains the import path. For all other
/// imports, it contains the URL passed to the `@import`.
final Set<String> includedFiles;
Set<String> get includedFiles => _evaluate.includedFiles;

CompileResult(this.css, this.includedFiles);
CompileResult(this._evaluate, this._serialize);
}
104 changes: 78 additions & 26 deletions lib/src/node.dart
Expand Up @@ -4,8 +4,10 @@

import 'dart:async';
import 'dart:js_util';
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:dart2_constant/convert.dart' as convert;
import 'package:js/js.dart';
import 'package:tuple/tuple.dart';

Expand Down Expand Up @@ -93,12 +95,13 @@ Future<RenderResult> _renderAsync(RenderOptions options) async {
result = await compileStringAsync(options.data,
nodeImporter: _parseImporter(options, start),
functions: _parseFunctions(options, asynch: true),
indented: options.indentedSyntax ?? false,
indented: isTruthy(options.indentedSyntax),
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
indentWidth: _parseIndentWidth(options.indentWidth),
lineFeed: _parseLineFeed(options.linefeed),
url: options.file == null ? 'stdin' : p.toUri(options.file).toString());
url: options.file == null ? 'stdin' : p.toUri(options.file).toString(),
sourceMap: _enableSourceMaps(options));
} else if (options.file != null) {
result = await compileAsync(options.file,
nodeImporter: _parseImporter(options, start),
Expand All @@ -107,18 +110,13 @@ Future<RenderResult> _renderAsync(RenderOptions options) async {
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
indentWidth: _parseIndentWidth(options.indentWidth),
lineFeed: _parseLineFeed(options.linefeed));
lineFeed: _parseLineFeed(options.linefeed),
sourceMap: _enableSourceMaps(options));
} else {
throw new ArgumentError("Either options.data or options.file must be set.");
}
var end = new DateTime.now();

return newRenderResult(result.css,
entry: options.file ?? 'data',
start: start.millisecondsSinceEpoch,
end: end.millisecondsSinceEpoch,
duration: end.difference(start).inMilliseconds,
includedFiles: result.includedFiles.toList());
return _newRenderResult(options, result, start);
}

/// Converts Sass to CSS.
Expand All @@ -135,14 +133,14 @@ RenderResult _renderSync(RenderOptions options) {
result = compileString(options.data,
nodeImporter: _parseImporter(options, start),
functions: DelegatingList.typed(_parseFunctions(options)),
indented: options.indentedSyntax ?? false,
indented: isTruthy(options.indentedSyntax),
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
indentWidth: _parseIndentWidth(options.indentWidth),
lineFeed: _parseLineFeed(options.linefeed),
url: options.file == null
? 'stdin'
: p.toUri(options.file).toString());
url:
options.file == null ? 'stdin' : p.toUri(options.file).toString(),
sourceMap: _enableSourceMaps(options));
} else if (options.file != null) {
result = compile(options.file,
nodeImporter: _parseImporter(options, start),
Expand All @@ -151,19 +149,14 @@ RenderResult _renderSync(RenderOptions options) {
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
indentWidth: _parseIndentWidth(options.indentWidth),
lineFeed: _parseLineFeed(options.linefeed));
lineFeed: _parseLineFeed(options.linefeed),
sourceMap: _enableSourceMaps(options));
} else {
throw new ArgumentError(
"Either options.data or options.file must be set.");
}
var end = new DateTime.now();

return newRenderResult(result.css,
entry: options.file ?? 'data',
start: start.millisecondsSinceEpoch,
end: end.millisecondsSinceEpoch,
duration: end.difference(start).inMilliseconds,
includedFiles: result.includedFiles.toList());

return _newRenderResult(options, result, start);
} on SassException catch (error) {
jsThrow(_wrapException(error));
} catch (error) {
Expand Down Expand Up @@ -280,9 +273,10 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
indentType: options.indentType == 'tab' ? 1 : 0,
indentWidth: _parseIndentWidth(options.indentWidth) ?? 2,
linefeed: _parseLineFeed(options.linefeed).text,
result: newRenderResult(null,
start: start.millisecondsSinceEpoch,
entry: options.file ?? 'data')));
result: new RenderResult(
stats: new RenderResultStats(
start: start.millisecondsSinceEpoch,
entry: options.file ?? 'data'))));
context.options.context = context;
}

Expand Down Expand Up @@ -333,6 +327,64 @@ LineFeed _parseLineFeed(String str) {
}
}

/// Creates a [RenderResult] that exposes [result] in the Node Sass API format.
RenderResult _newRenderResult(
RenderOptions options, CompileResult result, DateTime start) {
var end = new DateTime.now();

var css = result.css;
Uint8List sourceMapBytes;
if (_enableSourceMaps(options)) {
var sourceMapPath = options.sourceMap is String
? options.sourceMap as String
: options.outFile + '.map';
var sourceMapDir = p.dirname(sourceMapPath);

result.sourceMap.sourceRoot = options.sourceMapRoot;
result.sourceMap.targetUrl =
p.toUri(p.relative(options.outFile, from: sourceMapDir)).toString();

var sourcesContent = options.sourceMapContents ? <String>[] : null;
var sourceMapDirUrl = p.toUri(sourceMapDir).toString();
for (var i = 0; i < result.sourceMap.urls.length; i++) {
var source = result.sourceMap.urls[i];

if (sourcesContent != null) {
sourcesContent.add(result.sourceFiles[source].getText(0));
}

if (source == "stdin") continue;
result.sourceMap.urls[i] = pUrl.relative(source, from: sourceMapDirUrl);
}

var json = result.sourceMap.toJson();
if (sourcesContent != null) json['sourcesContent'] = sourcesContent;
sourceMapBytes = utf8Encode(convert.json.encode(json));

if (!isTruthy(options.omitSourceMapUrl)) {
var url = options.sourceMapEmbed
? new Uri.dataFromBytes(sourceMapBytes, mimeType: "application/json")
: p.toUri(
p.relative(sourceMapPath, from: p.dirname(options.outFile)));
css += "\n\n/*# sourceMappingURL=$url */";
}
}

return new RenderResult(
css: utf8Encode(css),
map: sourceMapBytes,
stats: new RenderResultStats(
entry: options.file ?? 'data',
start: start.millisecondsSinceEpoch,
end: end.millisecondsSinceEpoch,
duration: end.difference(start).inMilliseconds,
includedFiles: result.includedFiles.toList()));
}

/// Returns whether source maps are enabled by [options].
bool _enableSourceMaps(RenderOptions options) =>
isTruthy(options.sourceMap) && options.outFile != null;

/// Creates a [JSError] with the given fields added to it so it acts like a Node
/// Sass error.
JSError _newRenderError(String message,
Expand Down

0 comments on commit fd19bc8

Please sign in to comment.