|
| 1 | +// Copyright 2018 Google Inc. Use of this source code is governed by an |
| 2 | +// MIT-style license that can be found in the LICENSE file or at |
| 3 | +// https://opensource.org/licenses/MIT. |
| 4 | + |
| 5 | +import 'dart:convert'; |
| 6 | +import 'dart:io'; |
| 7 | + |
| 8 | +import 'package:charcode/charcode.dart'; |
| 9 | +import 'package:grinder/grinder.dart'; |
| 10 | +import 'package:meta/meta.dart'; |
| 11 | +import 'package:node_preamble/preamble.dart' as preamble; |
| 12 | +import 'package:path/path.dart' as p; |
| 13 | +import 'package:source_span/source_span.dart'; |
| 14 | + |
| 15 | +import 'utils.dart'; |
| 16 | + |
| 17 | +@Task('Compile to JS in dev mode.') |
| 18 | +void js() => _js(release: false); |
| 19 | + |
| 20 | +@Task('Compile to JS in release mode.') |
| 21 | +void jsRelease() => _js(release: true); |
| 22 | + |
| 23 | +/// Compiles Sass to JS. |
| 24 | +/// |
| 25 | +/// If [release] is `true`, this compiles minified with |
| 26 | +/// --trust-type-annotations. Otherwise, it compiles unminified with pessimistic |
| 27 | +/// type checks. |
| 28 | +void _js({@required bool release}) { |
| 29 | + ensureBuild(); |
| 30 | + var destination = File('build/sass.dart.js'); |
| 31 | + |
| 32 | + Dart2js.compile(File('bin/sass.dart'), outFile: destination, extraArgs: [ |
| 33 | + '--categories=Server', |
| 34 | + '-Dnode=true', |
| 35 | + '-Dversion=$version', |
| 36 | + '-Ddart-version=$dartVersion', |
| 37 | + // We use O4 because: |
| 38 | + // |
| 39 | + // * We don't care about the string representation of types. |
| 40 | + // * We expect our test coverage to ensure that nothing throws subtypes of |
| 41 | + // Error. |
| 42 | + // * We thoroughly test edge cases in user input. |
| 43 | + // |
| 44 | + // We don't minify because download size isn't especially important |
| 45 | + // server-side and it's nice to get readable stack traces from bug reports. |
| 46 | + if (release) ...["-O4", "--no-minify", "--fast-startup"] |
| 47 | + ]); |
| 48 | + var text = destination |
| 49 | + .readAsStringSync() |
| 50 | + // Some dependencies dynamically invoke `require()`, which makes Webpack |
| 51 | + // complain. We replace those with direct references to the modules, which |
| 52 | + // we load explicitly after the preamble. |
| 53 | + .replaceAllMapped(RegExp(r'self\.require\("([a-zA-Z0-9_-]+)"\)'), |
| 54 | + (match) => "self.${match[1]}"); |
| 55 | + |
| 56 | + if (release) { |
| 57 | + // We don't ship the source map, so remove the source map comment. |
| 58 | + text = |
| 59 | + text.replaceFirst(RegExp(r"\n*//# sourceMappingURL=[^\n]+\n*$"), "\n"); |
| 60 | + } |
| 61 | + |
| 62 | + // Reassigning require() makes Webpack complain. |
| 63 | + var preambleText = |
| 64 | + preamble.getPreamble().replaceFirst("self.require = require;\n", ""); |
| 65 | + |
| 66 | + destination.writeAsStringSync(""" |
| 67 | +$preambleText |
| 68 | +self.fs = require("fs"); |
| 69 | +self.chokidar = require("chokidar"); |
| 70 | +self.readline = require("readline"); |
| 71 | +self.workerThreads = require("worker_threads"); |
| 72 | +$text"""); |
| 73 | +} |
| 74 | + |
| 75 | +@Task('Build a pure-JS dev-mode npm package.') |
| 76 | +@Depends(js) |
| 77 | +void npmPackage() => _npm(release: false); |
| 78 | + |
| 79 | +@Task('Build a pure-JS release-mode npm package.') |
| 80 | +@Depends(jsRelease) |
| 81 | +void npmReleasePackage() => _npm(release: true); |
| 82 | + |
| 83 | +/// Builds a pure-JS npm package. |
| 84 | +/// |
| 85 | +/// If [release] is `true`, this compiles minified with |
| 86 | +/// --trust-type-annotations. Otherwise, it compiles unminified with pessimistic |
| 87 | +/// type checks. |
| 88 | +void _npm({@required bool release}) { |
| 89 | + var json = { |
| 90 | + ...(jsonDecode(File('package/package.json').readAsStringSync()) |
| 91 | + as Map<String, Object>), |
| 92 | + "version": version |
| 93 | + }; |
| 94 | + |
| 95 | + _writeNpmPackage('build/npm', json); |
| 96 | + if (release) { |
| 97 | + _writeNpmPackage('build/npm-old', {...json, "name": "dart-sass"}); |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +/// Writes a Dart Sass NPM package to the directory at [destination]. |
| 102 | +/// |
| 103 | +/// The [json] will be used as the package's package.json. |
| 104 | +void _writeNpmPackage(String destination, Map<String, dynamic> json) { |
| 105 | + var dir = Directory(destination); |
| 106 | + if (dir.existsSync()) dir.deleteSync(recursive: true); |
| 107 | + dir.createSync(recursive: true); |
| 108 | + |
| 109 | + log("copying package/package.json to $destination"); |
| 110 | + File(p.join(destination, 'package.json')).writeAsStringSync(jsonEncode(json)); |
| 111 | + |
| 112 | + copy(File(p.join('package', 'sass.js')), dir); |
| 113 | + copy(File(p.join('build', 'sass.dart.js')), dir); |
| 114 | + |
| 115 | + log("copying package/README.npm.md to $destination"); |
| 116 | + File(p.join(destination, 'README.md')) |
| 117 | + .writeAsStringSync(_readAndResolveMarkdown('package/README.npm.md')); |
| 118 | +} |
| 119 | + |
| 120 | +final _readAndResolveRegExp = RegExp( |
| 121 | + r"^<!-- +#include +([^\s]+) +" |
| 122 | + '"([^"\n]+)"' |
| 123 | + r" +-->$", |
| 124 | + multiLine: true); |
| 125 | + |
| 126 | +/// Reads a Markdown file from [path] and resolves include directives. |
| 127 | +/// |
| 128 | +/// Include directives have the syntax `"<!-- #include" PATH HEADER "-->"`, |
| 129 | +/// which must appear on its own line. PATH is a relative file: URL to another |
| 130 | +/// Markdown file, and HEADER is the name of a header in that file whose |
| 131 | +/// contents should be included as-is. |
| 132 | +String _readAndResolveMarkdown(String path) => File(path) |
| 133 | + .readAsStringSync() |
| 134 | + .replaceAllMapped(_readAndResolveRegExp, (match) { |
| 135 | + String included; |
| 136 | + try { |
| 137 | + included = File(p.join(p.dirname(path), p.fromUri(match[1]))) |
| 138 | + .readAsStringSync(); |
| 139 | + } catch (error) { |
| 140 | + _matchError(match, error.toString(), url: p.toUri(path)); |
| 141 | + } |
| 142 | + |
| 143 | + Match headerMatch; |
| 144 | + try { |
| 145 | + headerMatch = "# ${match[2]}\n".allMatches(included).first; |
| 146 | + } on StateError { |
| 147 | + _matchError(match, "Could not find header.", url: p.toUri(path)); |
| 148 | + } |
| 149 | + |
| 150 | + var headerLevel = 0; |
| 151 | + var index = headerMatch.start; |
| 152 | + while (index >= 0 && included.codeUnitAt(index) == $hash) { |
| 153 | + headerLevel++; |
| 154 | + index--; |
| 155 | + } |
| 156 | + |
| 157 | + // The section goes until the next header of the same level, or the end |
| 158 | + // of the document. |
| 159 | + var sectionEnd = included.indexOf("#" * headerLevel, headerMatch.end); |
| 160 | + if (sectionEnd == -1) sectionEnd = included.length; |
| 161 | + |
| 162 | + return included.substring(headerMatch.end, sectionEnd).trim(); |
| 163 | + }); |
| 164 | + |
| 165 | +/// Throws a nice [SourceSpanException] associated with [match]. |
| 166 | +void _matchError(Match match, String message, {Object url}) { |
| 167 | + var file = SourceFile.fromString(match.input, url: url); |
| 168 | + throw SourceSpanException(message, file.span(match.start, match.end)); |
| 169 | +} |
0 commit comments