diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index 41451d61..add971d9 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -407,7 +407,7 @@ class ExecutableOptions { Uri sourceMapUrl(Uri url, String destination) { if (url.scheme.isNotEmpty && url.scheme != 'file') return url; - var path = p.canonicalize(p.fromUri(url)); + var path = p.fromUri(url); return p.toUri(_options['source-map-urls'] == 'relative' ? p.relative(path, from: p.dirname(destination)) : p.absolute(path)); diff --git a/lib/src/importer/filesystem.dart b/lib/src/importer/filesystem.dart index c0e17a09..e4936679 100644 --- a/lib/src/importer/filesystem.dart +++ b/lib/src/importer/filesystem.dart @@ -27,7 +27,12 @@ class FilesystemImporter extends Importer { ImporterResult load(Uri url) { var path = p.fromUri(url); return ImporterResult(io.readFile(path), - sourceMapUrl: url, syntax: Syntax.forPath(path)); + sourceMapUrl: + // [io.realCasePath] will short-circuit on case-sensitive + // filesystems anyway, but we still avoid calling it here so we + // don't have to re-parse the URL. + io.couldBeCaseInsensitive ? p.toUri(io.realCasePath(path)) : url, + syntax: Syntax.forPath(path)); } DateTime modificationTime(Uri url) => io.modificationTime(p.fromUri(url)); diff --git a/lib/src/io.dart b/lib/src/io.dart index 8524834b..aba160a5 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart @@ -2,6 +2,41 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:path/path.dart' as p; + +import 'io/interface.dart' + if (dart.library.io) 'io/vm.dart' + if (dart.library.js) 'io/node.dart'; +import 'utils.dart'; + export 'io/interface.dart' if (dart.library.io) 'io/vm.dart' if (dart.library.js) 'io/node.dart'; + +/// Returns whether the current operating system might be case-insensitive. +/// +/// We can't know for sure because different Mac OS systems are configured +/// differently. +bool get couldBeCaseInsensitive => isWindows || isMacOS; + +/// Returns `path` with the case updated to match the path's case on disk. +/// +/// This only updates `path`'s basename. It always returns `path` as-is on +/// operating systems other than Windows or Mac OS, since they almost never uses +/// case-insensitive filesystems. +String realCasePath(String path) { + // TODO(nweiz): Use an SDK function for this when dart-lang/sdk#35370 and/or + // nodejs/node#24942 are fixed. + + if (!couldBeCaseInsensitive) return path; + + var basename = p.basename(path); + var matches = listDir(p.dirname(path)) + .where((realPath) => equalsIgnoreCase(p.basename(realPath), basename)) + .toList(); + + // If the file doesn't exist, or if there are multiple options (meaning the + // filesystem isn't actually case-insensitive), return `path` as-is. + if (matches.length != 1) return path; + return matches.first; +} diff --git a/lib/src/io/interface.dart b/lib/src/io/interface.dart index 498c3252..16653d65 100644 --- a/lib/src/io/interface.dart +++ b/lib/src/io/interface.dart @@ -33,6 +33,9 @@ Stderr get stderr => null; /// Whether the current process is running on Windows. bool get isWindows => false; +/// Whether the current process is running on Mac OS. +bool get isMacOS => false; + /// Returns whether or not stdout is connected to an interactive terminal. bool get hasTerminal => false; diff --git a/lib/src/io/node.dart b/lib/src/io/node.dart index b7c6485b..f7cec7bb 100644 --- a/lib/src/io/node.dart +++ b/lib/src/io/node.dart @@ -232,6 +232,8 @@ bool get hasTerminal => _hasTerminal ?? false; bool get isWindows => _process.platform == 'win32'; +bool get isMacOS => _process.platform == 'darwin'; + bool get isNode => true; // Node seems to support ANSI escapes on all terminals. diff --git a/lib/src/io/vm.dart b/lib/src/io/vm.dart index e2b8c41e..849f7dc9 100644 --- a/lib/src/io/vm.dart +++ b/lib/src/io/vm.dart @@ -19,6 +19,8 @@ io.Stdout get stderr => io.stderr; bool get isWindows => io.Platform.isWindows; +bool get isMacOS => io.Platform.isMacOS; + bool get hasTerminal => io.stdout.hasTerminal; bool get isNode => false; diff --git a/test/cli/shared/source_maps.dart b/test/cli/shared/source_maps.dart index 94915160..fb3f032b 100644 --- a/test/cli/shared/source_maps.dart +++ b/test/cli/shared/source_maps.dart @@ -89,6 +89,34 @@ void sharedTests(Future runSass(Iterable arguments)) { }); }); + group("doesn't normalize file case", () { + setUp(() => d.file("TeSt.scss", "a {b: c}").create()); + + test("when loaded with the same case", () async { + await (await runSass(["TeSt.scss", "out.css"])).shouldExit(0); + expect(_readJson("out.css.map"), containsPair("sources", ["TeSt.scss"])); + }); + + test("when imported with the same case", () async { + await d.file("importer.scss", "@import 'TeSt.scss'").create(); + await (await runSass(["importer.scss", "out.css"])).shouldExit(0); + expect(_readJson("out.css.map"), containsPair("sources", ["TeSt.scss"])); + }); + + // The following tests rely on Windows' case-insensitive filesystem. + + test("when loaded with a different case", () async { + await (await runSass(["test.scss", "out.css"])).shouldExit(0); + expect(_readJson("out.css.map"), containsPair("sources", ["TeSt.scss"])); + }, testOn: "windows"); + + test("when imported with a different case", () async { + await d.file("importer.scss", "@import 'test.scss'").create(); + await (await runSass(["importer.scss", "out.css"])).shouldExit(0); + expect(_readJson("out.css.map"), containsPair("sources", ["TeSt.scss"])); + }, testOn: "windows"); + }); + test("includes a source map comment", () async { await d.file("test.scss", "a {b: c}").create(); await (await runSass(["test.scss", "out.css"])).shouldExit(0);