Skip to content

Commit c8d0643

Browse files
authored
Better handle filesystem importers when load paths aren't necessary (#2203)
See #2199 See sass/sass#3815
1 parent 9302b35 commit c8d0643

File tree

8 files changed

+133
-28
lines changed

8 files changed

+133
-28
lines changed

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,31 @@
33
* Add support for nesting in plain CSS files. This is not processed by Sass at
44
all; it's emitted exactly as-is in the CSS.
55

6+
* In certain circumstances, the current working directory was unintentionally
7+
being made available as a load path. This is now deprecated. Anyone relying on
8+
this should explicitly pass in `.` as a load path or `FilesystemImporter('.')`
9+
as the current importer.
10+
611
* Add linux-riscv64 and windows-arm64 releases.
712

13+
### Command-Line Interface
14+
15+
* Fix a bug where absolute `file:` URLs weren't loaded for files compiled via
16+
the command line unless an unrelated load path was also passed.
17+
18+
* Fix a bug where `--update` would always update files that were specified via
19+
absolute path unless an unrelated load path was also passed.
20+
21+
### Dart API
22+
23+
* Add `FilesystemImporter.noLoadPath`, which is a `FilesystemImporter` that can
24+
load absolute `file:` URLs and resolve URLs relative to the base file but
25+
doesn't load relative URLs from a load path.
26+
27+
* `FilesystemImporter.cwd` is now deprecated. Either use
28+
`FilesystemImporter.noLoadPath` if you weren't intending to rely on the load
29+
path, or `FilesystemImporter('.')` if you were.
30+
831
## 1.72.0
932

1033
* Support adjacent `/`s without whitespace in between when parsing plain CSS

bin/sass.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:sass/src/executable/options.dart';
1313
import 'package:sass/src/executable/repl.dart';
1414
import 'package:sass/src/executable/watch.dart';
1515
import 'package:sass/src/import_cache.dart';
16+
import 'package:sass/src/importer/filesystem.dart';
1617
import 'package:sass/src/io.dart';
1718
import 'package:sass/src/logger/deprecation_handling.dart';
1819
import 'package:sass/src/stylesheet_graph.dart';
@@ -45,7 +46,7 @@ Future<void> main(List<String> args) async {
4546
}
4647

4748
var graph = StylesheetGraph(ImportCache(
48-
importers: options.pkgImporters,
49+
importers: [...options.pkgImporters, FilesystemImporter.noLoadPath],
4950
loadPaths: options.loadPaths,
5051
// This logger is only used for handling fatal/future deprecations
5152
// during parsing, and is re-used across parses, so we don't want to

lib/src/deprecation.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ enum Deprecation {
6969
deprecatedIn: '1.62.3',
7070
description: 'Passing null as alpha in the ${isJS ? 'JS' : 'Dart'} API.'),
7171

72+
fsImporterCwd('fs-importer-cwd',
73+
deprecatedIn: '1.73.0',
74+
description:
75+
'Using the current working directory as an implicit load path.'),
76+
7277
@Deprecated('This deprecation name was never actually used.')
7378
calcInterp('calc-interp', deprecatedIn: null),
7479

lib/src/evaluation_context.dart

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:async';
77
import 'package:source_span/source_span.dart';
88

99
import 'deprecation.dart';
10+
import 'logger.dart';
1011

1112
/// An interface that exposes information about the current Sass evaluation.
1213
///
@@ -17,13 +18,16 @@ abstract interface class EvaluationContext {
1718
///
1819
/// Throws [StateError] if there isn't a Sass stylesheet currently being
1920
/// evaluated.
20-
static EvaluationContext get current {
21-
if (Zone.current[#_evaluationContext] case EvaluationContext context) {
22-
return context;
23-
} else {
24-
throw StateError("No Sass stylesheet is currently being evaluated.");
25-
}
26-
}
21+
static EvaluationContext get current =>
22+
currentOrNull ??
23+
(throw StateError("No Sass stylesheet is currently being evaluated."));
24+
25+
/// The current evaluation context, or `null` if none exists.
26+
static EvaluationContext? get currentOrNull =>
27+
switch (Zone.current[#_evaluationContext]) {
28+
EvaluationContext context => context,
29+
_ => null
30+
};
2731

2832
/// Returns the span for the currently executing callable.
2933
///
@@ -50,13 +54,20 @@ abstract interface class EvaluationContext {
5054
/// This may only be called within a custom function or importer callback.
5155
/// {@category Compile}
5256
void warn(String message, {bool deprecation = false}) =>
53-
EvaluationContext.current
54-
.warn(message, deprecation ? Deprecation.userAuthored : null);
57+
switch (EvaluationContext.currentOrNull) {
58+
var context? =>
59+
context.warn(message, deprecation ? Deprecation.userAuthored : null),
60+
_ when deprecation => (const Logger.stderr())
61+
.warnForDeprecation(Deprecation.userAuthored, message),
62+
_ => (const Logger.stderr()).warn(message)
63+
};
5564

5665
/// Prints a deprecation warning with [message] of type [deprecation].
57-
void warnForDeprecation(String message, Deprecation deprecation) {
58-
EvaluationContext.current.warn(message, deprecation);
59-
}
66+
void warnForDeprecation(String message, Deprecation deprecation) =>
67+
switch (EvaluationContext.currentOrNull) {
68+
var context? => context.warn(message, deprecation),
69+
_ => (const Logger.stderr()).warnForDeprecation(deprecation, message)
70+
};
6071

6172
/// Runs [callback] with [context] as [EvaluationContext.current].
6273
///

lib/src/executable/compile_stylesheet.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ Future<void> _compileStylesheetWithoutErrorHandling(ExecutableOptions options,
7373
try {
7474
if (source != null &&
7575
destination != null &&
76-
!graph.modifiedSince(
77-
p.toUri(source), modificationTime(destination), importer)) {
76+
!graph.modifiedSince(p.toUri(p.absolute(source)),
77+
modificationTime(destination), importer)) {
7878
return;
7979
}
8080
} on FileSystemException catch (_) {

lib/src/importer/filesystem.dart

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,86 @@
55
import 'package:meta/meta.dart';
66
import 'package:path/path.dart' as p;
77

8+
import '../deprecation.dart';
9+
import '../evaluation_context.dart';
810
import '../importer.dart';
911
import '../io.dart' as io;
1012
import '../syntax.dart';
1113
import '../util/nullable.dart';
1214
import 'utils.dart';
1315

14-
/// An importer that loads files from a load path on the filesystem.
16+
/// An importer that loads files from a load path on the filesystem, either
17+
/// relative to the path passed to [FilesystemImporter.new] or absolute `file:`
18+
/// URLs.
19+
///
20+
/// Use [FilesystemImporter.noLoadPath] to _only_ load absolute `file:` URLs and
21+
/// URLs relative to the current file.
1522
///
1623
/// {@category Importer}
1724
@sealed
1825
class FilesystemImporter extends Importer {
1926
/// The path relative to which this importer looks for files.
20-
final String _loadPath;
27+
///
28+
/// If this is `null`, this importer will _only_ load absolute `file:` URLs
29+
/// and URLs relative to the current file.
30+
final String? _loadPath;
31+
32+
/// Whether loading from files from this importer's [_loadPath] is deprecated.
33+
final bool _loadPathDeprecated;
2134

2235
/// Creates an importer that loads files relative to [loadPath].
23-
FilesystemImporter(String loadPath) : _loadPath = p.absolute(loadPath);
36+
FilesystemImporter(String loadPath)
37+
: _loadPath = p.absolute(loadPath),
38+
_loadPathDeprecated = false;
39+
40+
FilesystemImporter._deprecated(String loadPath)
41+
: _loadPath = p.absolute(loadPath),
42+
_loadPathDeprecated = true;
43+
44+
/// Creates an importer that _only_ loads absolute `file:` URLs and URLs
45+
/// relative to the current file.
46+
FilesystemImporter._noLoadPath()
47+
: _loadPath = null,
48+
_loadPathDeprecated = false;
2449

25-
/// Creates an importer relative to the current working directory.
26-
static final cwd = FilesystemImporter('.');
50+
/// A [FilesystemImporter] that loads files relative to the current working
51+
/// directory.
52+
///
53+
/// Historically, this was the best default for supporting `file:` URL loads
54+
/// when the load path didn't matter. However, adding the current working
55+
/// directory to the load path wasn't always desirable, so it's no longer
56+
/// recommended. Instead, either use [FilesystemImporter.noLoadPath] if the
57+
/// load path doesn't matter, or `FilesystemImporter('.')` to explicitly
58+
/// preserve the existing behavior.
59+
@Deprecated(
60+
"Use FilesystemImporter.noLoadPath or FilesystemImporter('.') instead.")
61+
static final cwd = FilesystemImporter._deprecated('.');
62+
63+
/// Creates an importer that _only_ loads absolute `file:` URLsand URLs
64+
/// relative to the current file.
65+
static final noLoadPath = FilesystemImporter._noLoadPath();
2766

2867
Uri? canonicalize(Uri url) {
29-
if (url.scheme != 'file' && url.scheme != '') return null;
30-
return resolveImportPath(p.join(_loadPath, p.fromUri(url)))
31-
.andThen((resolved) => p.toUri(io.canonicalize(resolved)));
68+
String? resolved;
69+
if (url.scheme == 'file') {
70+
resolved = resolveImportPath(p.fromUri(url));
71+
} else if (url.scheme != '') {
72+
return null;
73+
} else if (_loadPath case var loadPath?) {
74+
resolved = resolveImportPath(p.join(loadPath, p.fromUri(url)));
75+
76+
if (resolved != null && _loadPathDeprecated) {
77+
warnForDeprecation(
78+
"Using the current working directory as an implicit load path is "
79+
"deprecated. Either add it as an explicit load path or importer, or "
80+
"load this stylesheet from a different URL.",
81+
Deprecation.fsImporterCwd);
82+
}
83+
} else {
84+
return null;
85+
}
86+
87+
return resolved.andThen((resolved) => p.toUri(io.canonicalize(resolved)));
3288
}
3389

3490
ImporterResult? load(Uri url) {
@@ -53,5 +109,5 @@ class FilesystemImporter extends Importer {
53109
basename == p.url.withoutExtension(canonicalBasename);
54110
}
55111

56-
String toString() => _loadPath;
112+
String toString() => _loadPath ?? '<absolute file importer>';
57113
}

test/cli/shared/update.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5+
import 'package:path/path.dart' as p;
56
import 'package:test/test.dart';
67
import 'package:test_descriptor/test_descriptor.dart' as d;
78
import 'package:test_process/test_process.dart';
@@ -148,6 +149,18 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
148149
await d.file("out.css", "x {y: z}").validate();
149150
});
150151

152+
// Regression test for #2203
153+
test("whose sources weren't modified with an absolute path", () async {
154+
await d.file("test.scss", "a {b: c}").create();
155+
await d.file("out.css", "x {y: z}").create();
156+
157+
var sass = await update(["${p.absolute(d.path('test.scss'))}:out.css"]);
158+
expect(sass.stdout, emitsDone);
159+
await sass.shouldExit(0);
160+
161+
await d.file("out.css", "x {y: z}").validate();
162+
});
163+
151164
test("whose sibling was modified", () async {
152165
await d.file("test1.scss", "a {b: c}").create();
153166
await d.file("out1.css", "x {y: z}").create();

test/dart_api/logger_test.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,6 @@ void main() {
227227
mustBeCalled();
228228
}));
229229
});
230-
231-
test("throws an error outside a callback", () {
232-
expect(() => warn("heck"), throwsStateError);
233-
});
234230
});
235231
}
236232

0 commit comments

Comments
 (0)