Skip to content

Commit 3080e76

Browse files
authored
Implement access tracking for containingUrl (#285)
1 parent fd32e8e commit 3080e76

File tree

6 files changed

+93
-21
lines changed

6 files changed

+93
-21
lines changed

lib/src/canonicalize-context.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2024 Google LLC. 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+
export class CanonicalizeContext {
6+
readonly fromImport: boolean;
7+
8+
private readonly _containingUrl: URL | null;
9+
10+
get containingUrl(): URL | null {
11+
this._containingUrlAccessed = true;
12+
return this._containingUrl;
13+
}
14+
15+
private _containingUrlAccessed = false;
16+
17+
/**
18+
* Whether the `containingUrl` getter has been accessed.
19+
*
20+
* This is marked as public so that the importer registry can access it, but
21+
* it's not part of the package's public API and should not be accessed by
22+
* user code. It may be renamed or removed without warning in the future.
23+
*/
24+
get containingUrlAccessed(): boolean {
25+
return this._containingUrlAccessed;
26+
}
27+
28+
constructor(containingUrl: URL | null, fromImport: boolean) {
29+
this._containingUrl = containingUrl;
30+
this.fromImport = fromImport;
31+
}
32+
}

lib/src/importer-registry.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as p from 'path';
77
import {URL} from 'url';
88
import {inspect} from 'util';
99

10+
import {CanonicalizeContext} from './canonicalize-context';
1011
import * as utils from './utils';
1112
import {FileImporter, Importer, Options} from './vendor/sass';
1213
import * as proto from './vendor/embedded_sass_pb';
@@ -115,21 +116,22 @@ export class ImporterRegistry<sync extends 'sync' | 'async'> {
115116
throw utils.compilerError('Unknown CanonicalizeRequest.importer_id');
116117
}
117118

119+
const canonicalizeContext = new CanonicalizeContext(
120+
request.containingUrl ? new URL(request.containingUrl) : null,
121+
request.fromImport
122+
);
123+
118124
return catchOr(
119125
() => {
120126
return thenOr(
121-
importer.canonicalize(request.url, {
122-
fromImport: request.fromImport,
123-
containingUrl: request.containingUrl
124-
? new URL(request.containingUrl)
125-
: null,
126-
}),
127+
importer.canonicalize(request.url, canonicalizeContext),
127128
url =>
128129
new proto.InboundMessage_CanonicalizeResponse({
129130
result:
130131
url === null
131132
? {case: undefined}
132133
: {case: 'url', value: url.toString()},
134+
containingUrlUnused: !canonicalizeContext.containingUrlAccessed,
133135
})
134136
);
135137
},
@@ -197,15 +199,15 @@ export class ImporterRegistry<sync extends 'sync' | 'async'> {
197199
throw utils.compilerError('Unknown FileImportRequest.importer_id');
198200
}
199201

202+
const canonicalizeContext = new CanonicalizeContext(
203+
request.containingUrl ? new URL(request.containingUrl) : null,
204+
request.fromImport
205+
);
206+
200207
return catchOr(
201208
() => {
202209
return thenOr(
203-
importer.findFileUrl(request.url, {
204-
fromImport: request.fromImport,
205-
containingUrl: request.containingUrl
206-
? new URL(request.containingUrl)
207-
: null,
208-
}),
210+
importer.findFileUrl(request.url, canonicalizeContext),
209211
url => {
210212
if (!url) return new proto.InboundMessage_FileImportResponse();
211213
if (url.protocol !== 'file:') {
@@ -216,6 +218,7 @@ export class ImporterRegistry<sync extends 'sync' | 'async'> {
216218
}
217219
return new proto.InboundMessage_FileImportResponse({
218220
result: {case: 'fileUrl', value: url.toString()},
221+
containingUrlUnused: !canonicalizeContext.containingUrlAccessed,
219222
});
220223
}
221224
);

lib/src/legacy/importer.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,34 @@ export class LegacyImporterWrapper<sync extends 'sync' | 'async'>
8686

8787
canonicalize(
8888
url: string,
89-
options: {fromImport: boolean}
89+
options: {fromImport: boolean; containingUrl: URL | null}
9090
): PromiseOr<URL | null, sync> {
9191
if (url.startsWith(endOfLoadProtocol)) return new URL(url);
9292

93+
// Emulate a base importer instead of using a real base importer,
94+
// because we want to mark containingUrl as used, which is impossible
95+
// in a real base importer.
96+
if (options.containingUrl !== null) {
97+
try {
98+
const absoluteUrl = new URL(url, options.containingUrl).toString();
99+
const resolved = this.canonicalize(absoluteUrl, {
100+
fromImport: options.fromImport,
101+
containingUrl: null,
102+
});
103+
if (resolved !== null) return resolved;
104+
} catch (error: unknown) {
105+
if (
106+
error instanceof TypeError &&
107+
isErrnoException(error) &&
108+
error.code === 'ERR_INVALID_URL'
109+
) {
110+
// ignore
111+
} else {
112+
throw error;
113+
}
114+
}
115+
}
116+
93117
if (
94118
url.startsWith(legacyImporterProtocolPrefix) ||
95119
url.startsWith(legacyImporterProtocol)

lib/src/legacy/index.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,20 @@ function convertStringOptions<sync extends 'sync' | 'async'>(
184184
): StringOptions<sync> & {legacy: true} {
185185
const modernOptions = convertOptions(options, sync);
186186

187-
// Find the first non-NodePackageImporter to pass as legacy `importer` option.
188-
// NodePackageImporter will be passed in `modernOptions.importers`.
189-
const importer = modernOptions.importers?.find(
190-
_importer => !(_importer instanceof NodePackageImporter)
191-
) as Importer<sync> | FileImporter<sync>;
187+
// Use a no-op base importer, because the LegacyImporterWrapper will emulate
188+
// the base importer by itself in order to mark containingUrl as accessed.
189+
const importer = modernOptions.importers?.some(
190+
importer => importer instanceof LegacyImporterWrapper
191+
)
192+
? {
193+
canonicalize() {
194+
return null;
195+
},
196+
load() {
197+
return null;
198+
},
199+
}
200+
: undefined;
192201

193202
return {
194203
...modernOptions,

lib/src/value/argument-list.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,21 @@ export class SassArgumentList extends SassList {
3131
*/
3232
readonly keywordsInternal: OrderedMap<string, Value>;
3333

34+
private _keywordsAccessed = false;
35+
3436
/**
3537
* Whether the `keywords` getter has been accessed.
3638
*
3739
* This is marked as public so that the protofier can access it, but it's not
3840
* part of the package's public API and should not be accessed by user code.
3941
* It may be renamed or removed without warning in the future.
4042
*/
41-
keywordsAccessed = false;
43+
get keywordsAccessed(): boolean {
44+
return this._keywordsAccessed;
45+
}
4246

4347
get keywords(): OrderedMap<string, Value> {
44-
this.keywordsAccessed = true;
48+
this._keywordsAccessed = true;
4549
return this.keywordsInternal;
4650
}
4751

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "sass-embedded",
33
"version": "1.75.0",
4-
"protocol-version": "2.6.0",
4+
"protocol-version": "2.7.0",
55
"compiler-version": "1.75.0",
66
"description": "Node.js library that communicates with Embedded Dart Sass using the Embedded Sass protocol",
77
"repository": "sass/embedded-host-node",

0 commit comments

Comments
 (0)