dart-sass/tool/grind/npm.dart
Natalie Weizenbaum a841882724 Don't compile to JS in minified mode
The JS size doesn't really matter on the server side, and this makes
profiling the release-mode binary substantially easier, as well as
improving the quality of stack traces provided by downstream users.
2019-07-15 21:59:54 +01:00

169 lines
5.6 KiB
Dart

// Copyright 2018 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:convert';
import 'dart:io';
import 'package:charcode/charcode.dart';
import 'package:grinder/grinder.dart';
import 'package:meta/meta.dart';
import 'package:node_preamble/preamble.dart' as preamble;
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';
import 'utils.dart';
@Task('Compile to JS in dev mode.')
js() => _js(release: false);
@Task('Compile to JS in release mode.')
jsRelease() => _js(release: true);
/// Compiles Sass to JS.
///
/// If [release] is `true`, this compiles minified with
/// --trust-type-annotations. Otherwise, it compiles unminified with pessimistic
/// type checks.
void _js({@required bool release}) {
ensureBuild();
var destination = File('build/sass.dart.js');
Dart2js.compile(File('bin/sass.dart'), outFile: destination, extraArgs: [
'--categories=Server',
'-Dnode=true',
'-Dversion=$version',
'-Ddart-version=$dartVersion',
// We use O4 because:
//
// * We don't care about the string representation of types.
// * We expect our test coverage to ensure that nothing throws subtypes of
// Error.
// * We thoroughly test edge cases in user input.
//
// We don't minify because download size isn't especially important
// server-side and it's nice to get readable stack traces from bug reports.
if (release) ...["-O4", "--no-minify", "--fast-startup"]
]);
var text = destination
.readAsStringSync()
// Some dependencies dynamically invoke `require()`, which makes Webpack
// complain. We replace those with direct references to the modules, which
// we load explicitly after the preamble.
.replaceAllMapped(RegExp(r'self\.require\("([a-zA-Z0-9_-]+)"\)'),
(match) => "self.${match[1]}");
if (release) {
// We don't ship the source map, so remove the source map comment.
text =
text.replaceFirst(RegExp(r"\n*//# sourceMappingURL=[^\n]+\n*$"), "\n");
}
// Reassigning require() makes Webpack complain.
var preambleText =
preamble.getPreamble().replaceFirst("self.require = require;\n", "");
destination.writeAsStringSync("""
$preambleText
self.fs = require("fs");
self.chokidar = require("chokidar");
self.readline = require("readline");
$text""");
}
@Task('Build a pure-JS dev-mode npm package.')
@Depends(js)
npmPackage() => _npm(release: false);
@Task('Build a pure-JS release-mode npm package.')
@Depends(jsRelease)
npmReleasePackage() => _npm(release: true);
/// Builds a pure-JS npm package.
///
/// If [release] is `true`, this compiles minified with
/// --trust-type-annotations. Otherwise, it compiles unminified with pessimistic
/// type checks.
void _npm({@required bool release}) {
var json = {
...(jsonDecode(File('package/package.json').readAsStringSync())
as Map<String, Object>),
"version": version
};
_writeNpmPackage('build/npm', json);
if (release) {
_writeNpmPackage('build/npm-old', {...json, "name": "dart-sass"});
}
}
/// Writes a Dart Sass NPM package to the directory at [destination].
///
/// The [json] will be used as the package's package.json.
void _writeNpmPackage(String destination, Map<String, dynamic> json) {
var dir = Directory(destination);
if (dir.existsSync()) dir.deleteSync(recursive: true);
dir.createSync(recursive: true);
log("copying package/package.json to $destination");
File(p.join(destination, 'package.json')).writeAsStringSync(jsonEncode(json));
copy(File(p.join('package', 'sass.js')), dir);
copy(File(p.join('build', 'sass.dart.js')), dir);
log("copying package/README.npm.md to $destination");
File(p.join(destination, 'README.md'))
.writeAsStringSync(_readAndResolveMarkdown('package/README.npm.md'));
}
final _readAndResolveRegExp = RegExp(
r"^<!-- +#include +([^\s]+) +"
'"([^"\n]+)"'
r" +-->$",
multiLine: true);
/// Reads a Markdown file from [path] and resolves include directives.
///
/// Include directives have the syntax `"<!-- #include" PATH HEADER "-->"`,
/// which must appear on its own line. PATH is a relative file: URL to another
/// Markdown file, and HEADER is the name of a header in that file whose
/// contents should be included as-is.
String _readAndResolveMarkdown(String path) => File(path)
.readAsStringSync()
.replaceAllMapped(_readAndResolveRegExp, (match) {
String included;
try {
included = File(p.join(p.dirname(path), p.fromUri(match[1])))
.readAsStringSync();
} catch (error) {
_matchError(match, error.toString(), url: p.toUri(path));
}
Match headerMatch;
try {
headerMatch = "# ${match[2]}\n".allMatches(included).first;
} on StateError {
_matchError(match, "Could not find header.", url: p.toUri(path));
}
var headerLevel = 0;
var index = headerMatch.start;
while (index >= 0 && included.codeUnitAt(index) == $hash) {
headerLevel++;
index--;
}
// The section goes until the next header of the same level, or the end
// of the document.
var sectionEnd = included.indexOf("#" * headerLevel, headerMatch.end);
if (sectionEnd == -1) sectionEnd = included.length;
return included.substring(headerMatch.end, sectionEnd).trim();
});
/// Throws a nice [SourceSpanException] associated with [match].
void _matchError(Match match, String message, {url}) {
var file = SourceFile.fromString(match.input, url: url);
throw SourceSpanException(message, file.span(match.start, match.end));
}