Skip to content

Commit b3ad64b

Browse files
authored
Merge pull request #392 from sass/merge-main
Merge main into feature.v2
2 parents af7dbd0 + 390880d commit b3ad64b

File tree

43 files changed

+415
-248
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+415
-248
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,7 @@ jobs:
117117
working-directory: sass-spec
118118

119119
- name: Compile
120-
run: |
121-
npm run compile
122-
if [[ "$RUNNER_OS" == "Windows" ]]; then
123-
# Avoid copying the entire Dart Sass build directory on Windows,
124-
# since it may contain symlinks that cp will choke on.
125-
mkdir -p dist/lib/src/vendor/dart-sass/
126-
cp {`pwd`/,dist/}lib/src/vendor/dart-sass/sass.bat
127-
cp {`pwd`/,dist/}lib/src/vendor/dart-sass/sass.snapshot
128-
else
129-
ln -s {`pwd`/,dist/}lib/src/vendor/dart-sass
130-
fi
120+
run: npm run compile
131121

132122
- name: Run tests
133123
run: npm run js-api-spec -- --sassPackage .. --sassSassRepo ../language

CHANGELOG.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,72 @@
1+
## 1.91.0
2+
3+
* **Potentially breaking change:** `meta.inspect()` (as well as other systems
4+
that use it such as `@debug` and certain error messages) now emits numbers
5+
with as high precision as is available instead of rounding to the nearest
6+
1e⁻¹⁰ as we do when serializing to CSS. This better fits the purpose of
7+
`meta.inspect()`, which is to provide full information about the structure of
8+
a Sass value.
9+
10+
* Passing a rest argument (`$arg...`) before a positional or named argument when
11+
calling a function or mixin is now deprecated. This was always outside the
12+
specified syntax, but it was historically treated the same as passing the rest
13+
argument at the end of the argument list whether or not that matched the
14+
visual order of the arguments.
15+
16+
## 1.90.0
17+
18+
* Allow a `@forward`ed module to be loaded with a configuration when that module
19+
has already been loaded with a different configuration *and* the module
20+
doesn't define any variables that would have been configured anyway.
21+
22+
## 1.89.2
23+
24+
### Embedded Host
25+
26+
* Fixed a compilation error caused by an outdated `buf` dependency.
27+
28+
## 1.89.1
29+
30+
* No user-visible changes.
31+
32+
## 1.89.0
33+
34+
* Allow the Node package importer to load files even when there are multiple
35+
potential resolutions, as long as those resolutions all point to the same
36+
file.
37+
38+
## 1.88.0
39+
40+
* Allow custom properties with empty values (such as `--var:;`).
41+
42+
* Fix a bug when calculating source spans for interpolations.
43+
44+
### Dart and JS APIs
45+
46+
* **Potentially breaking bug fix:** Throw an error when passing a function or
47+
mixin object from one compilation to another.
48+
49+
### Dart API
50+
51+
* Deprecate passing a relative URL to `compileString()` and related functions.
52+
53+
## 1.87.0
54+
55+
* **Potentially breaking bug fix:** When a plain CSS file with a top-level
56+
nesting selector `&` is loaded into a nested Sass context via
57+
`meta.load-css()` or `@import`, Sass now emits plain CSS nesting rather than
58+
incorrectly combining it with the parent selector using a descendant
59+
combinator.
60+
61+
## 1.86.3
62+
63+
* Fix a bug introduced in 1.86.1 where Sass fails to resolve paths starting with
64+
a `..` segment.
65+
66+
## 1.86.2
67+
68+
* No user-visible changes.
69+
170
## 1.86.1
271

372
* Improve the performance of `file:` URL case canonicalization on Windows and

bin/sass.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22

33
import * as child_process from 'child_process';
4+
import * as path from 'path';
45
import {compilerCommand} from '../lib/src/compiler-path';
56

67
// TODO npm/cmd-shim#152 and yarnpkg/berry#6422 - If and when the package
@@ -12,6 +13,10 @@ try {
1213
compilerCommand[0],
1314
[...compilerCommand.slice(1), ...process.argv.slice(2)],
1415
{
16+
// Node blocks launching .bat and .cmd without a shell due to CVE-2024-27980
17+
shell: ['.bat', '.cmd'].includes(
18+
path.extname(compilerCommand[0]).toLowerCase(),
19+
),
1520
stdio: 'inherit',
1621
windowsHide: true,
1722
},

lib/src/compiler-module.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2025 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+
import * as p from 'path';
6+
import {getElfInterpreter} from './elf';
7+
8+
/**
9+
* Detect if the given binary is linked with musl libc by checking if
10+
* the interpreter basename starts with "ld-musl-"
11+
*/
12+
function isLinuxMusl(path: string): boolean {
13+
try {
14+
const interpreter = getElfInterpreter(path);
15+
return p.basename(interpreter).startsWith('ld-musl-');
16+
} catch (error) {
17+
console.warn(
18+
`Warning: Failed to detect linux-musl, fallback to linux-gnu: ${error.message}`,
19+
);
20+
return false;
21+
}
22+
}
23+
24+
/** The module name for the embedded compiler executable. */
25+
export const compilerModule = (() => {
26+
const platform =
27+
process.platform === 'linux' && isLinuxMusl(process.execPath)
28+
? 'linux-musl'
29+
: (process.platform as string);
30+
31+
const arch = process.arch;
32+
33+
return `sass-embedded-${platform}-${arch}`;
34+
})();

lib/src/compiler-path.ts

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

5-
import * as fs from 'fs';
65
import * as p from 'path';
7-
import {getElfInterpreter} from './elf';
8-
import {isErrnoException} from './utils';
9-
10-
/**
11-
* Detect if the given binary is linked with musl libc by checking if
12-
* the interpreter basename starts with "ld-musl-"
13-
*/
14-
function isLinuxMusl(path: string): boolean {
15-
try {
16-
const interpreter = getElfInterpreter(path);
17-
return p.basename(interpreter).startsWith('ld-musl-');
18-
} catch (error) {
19-
console.warn(
20-
`Warning: Failed to detect linux-musl, fallback to linux-gnu: ${error.message}`,
21-
);
22-
return false;
23-
}
24-
}
6+
import {compilerModule} from './compiler-module';
257

268
/** The full command for the embedded compiler executable. */
279
export const compilerCommand = (() => {
28-
const platform =
29-
process.platform === 'linux' && isLinuxMusl(process.execPath)
30-
? 'linux-musl'
31-
: (process.platform as string);
32-
33-
const arch = process.arch;
34-
35-
// find for development
36-
for (const path of ['vendor', '../../../lib/src/vendor']) {
37-
const executable = p.resolve(
38-
__dirname,
39-
path,
40-
`dart-sass/sass${platform === 'win32' ? '.bat' : ''}`,
41-
);
42-
43-
if (fs.existsSync(executable)) return [executable];
44-
}
45-
4610
try {
4711
return [
4812
require.resolve(
49-
`sass-embedded-${platform}-${arch}/dart-sass/src/dart` +
50-
(platform === 'win32' ? '.exe' : ''),
51-
),
52-
require.resolve(
53-
`sass-embedded-${platform}-${arch}/dart-sass/src/sass.snapshot`,
13+
`${compilerModule}/dart-sass/src/dart` +
14+
(process.platform === 'win32' ? '.exe' : ''),
5415
),
16+
require.resolve(`${compilerModule}/dart-sass/src/sass.snapshot`),
5517
];
56-
} catch (ignored) {
57-
// ignored
18+
} catch (e) {
19+
if (e.code !== 'MODULE_NOT_FOUND') {
20+
throw e;
21+
}
5822
}
5923

6024
try {
6125
return [
6226
require.resolve(
63-
`sass-embedded-${platform}-${arch}/dart-sass/sass` +
64-
(platform === 'win32' ? '.bat' : ''),
27+
`${compilerModule}/dart-sass/sass` +
28+
(process.platform === 'win32' ? '.bat' : ''),
6529
),
6630
];
67-
} catch (e: unknown) {
68-
if (!(isErrnoException(e) && e.code === 'MODULE_NOT_FOUND')) {
31+
} catch (e) {
32+
if (e.code !== 'MODULE_NOT_FOUND') {
33+
throw e;
34+
}
35+
}
36+
37+
try {
38+
return [
39+
process.execPath,
40+
// This is a fallback which is required indirectly through
41+
// sass-embedded-all-unknown.
42+
// eslint-disable-next-line n/no-extraneous-require
43+
p.join(p.dirname(require.resolve('sass')), 'sass.js'),
44+
];
45+
} catch (e) {
46+
if (e.code !== 'MODULE_NOT_FOUND') {
6947
throw e;
7048
}
7149
}
7250

7351
throw new Error(
7452
"Embedded Dart Sass couldn't find the embedded compiler executable. " +
75-
'Please make sure the optional dependency ' +
76-
`sass-embedded-${platform}-${arch} is installed in ` +
77-
'node_modules.',
53+
`Please make sure the optional dependency ${compilerModule} or sass is ` +
54+
'installed in node_modules.',
7855
);
7956
})();

lib/src/function-registry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import {Value} from './value';
1818
* execute them.
1919
*/
2020
export class FunctionRegistry<sync extends 'sync' | 'async'> {
21+
/**
22+
* The globally unique identifier of the current compilation used for tracking
23+
* the ownership of CompilerFunction and CompilerMixin objects.
24+
*/
25+
public readonly compileContext = Symbol();
2126
private readonly functionsByName = new Map<string, CustomFunction<sync>>();
2227
private readonly functionsById = new Map<number, CustomFunction<sync>>();
2328
private readonly idsByFunction = new Map<CustomFunction<sync>, number>();

lib/src/protofier.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class Protofier {
4444
get accessedArgumentLists(): number[] {
4545
return this.argumentLists
4646
.filter(list => list.keywordsAccessed)
47-
.map(list => list.id);
47+
.map(list => list.id!);
4848
}
4949

5050
constructor(
@@ -85,15 +85,21 @@ export class Protofier {
8585
});
8686
result.value = {case: 'list', value: list};
8787
} else if (value instanceof SassArgumentList) {
88-
const list = create(proto.Value_ArgumentListSchema, {
89-
id: value.id,
90-
separator: this.protofySeparator(value.separator),
91-
contents: value.asList.map(element => this.protofy(element)).toArray(),
92-
});
93-
for (const [key, mapValue] of value.keywordsInternal) {
94-
list.keywords[key] = this.protofy(mapValue);
88+
if (value.compileContext === this.functions.compileContext) {
89+
const list = create(proto.Value_ArgumentListSchema, {id: value.id});
90+
result.value = {case: 'argumentList', value: list};
91+
} else {
92+
const list = create(proto.Value_ArgumentListSchema, {
93+
separator: this.protofySeparator(value.separator),
94+
contents: value.asList
95+
.map(element => this.protofy(element))
96+
.toArray(),
97+
});
98+
for (const [key, mapValue] of value.keywordsInternal) {
99+
list.keywords[key] = this.protofy(mapValue);
100+
}
101+
result.value = {case: 'argumentList', value: list};
95102
}
96-
result.value = {case: 'argumentList', value: list};
97103
} else if (value instanceof SassMap) {
98104
const map = create(proto.Value_MapSchema, {
99105
entries: value.contents.toArray().map(([key, value]) => ({
@@ -104,6 +110,11 @@ export class Protofier {
104110
result.value = {case: 'map', value: map};
105111
} else if (value instanceof SassFunction) {
106112
if (value.id !== undefined) {
113+
if (value.compileContext !== this.functions.compileContext) {
114+
throw utils.compilerError(
115+
`Value ${value} does not belong to this compilation`,
116+
);
117+
}
107118
const fn = create(proto.Value_CompilerFunctionSchema, value);
108119
result.value = {case: 'compilerFunction', value: fn};
109120
} else {
@@ -114,6 +125,11 @@ export class Protofier {
114125
result.value = {case: 'hostFunction', value: fn};
115126
}
116127
} else if (value instanceof SassMixin) {
128+
if (value.compileContext !== this.functions.compileContext) {
129+
throw utils.compilerError(
130+
`Value ${value} does not belong to this compilation`,
131+
);
132+
}
117133
const mixin = create(proto.Value_CompilerMixinSchema, value);
118134
result.value = {case: 'compilerMixin', value: mixin};
119135
} else if (value instanceof SassCalculation) {
@@ -349,6 +365,7 @@ export class Protofier {
349365
),
350366
separator,
351367
list.id,
368+
this.functions.compileContext,
352369
);
353370
this.argumentLists.push(result);
354371
return result;
@@ -369,15 +386,21 @@ export class Protofier {
369386
);
370387

371388
case 'compilerFunction':
372-
return new SassFunction(value.value.value.id);
389+
return new SassFunction(
390+
value.value.value.id,
391+
this.functions.compileContext,
392+
);
373393

374394
case 'hostFunction':
375395
throw utils.compilerError(
376396
'The compiler may not send Value.host_function.',
377397
);
378398

379399
case 'compilerMixin':
380-
return new SassMixin(value.value.value.id);
400+
return new SassMixin(
401+
value.value.value.id,
402+
this.functions.compileContext,
403+
);
381404

382405
case 'calculation':
383406
return this.deprotofyCalculation(value.value.value);

lib/src/request-tracker.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ describe('request tracker', () => {
3636
});
3737

3838
it('errors if the request ID is invalid', () => {
39-
expect(() => tracker.add(-1, 'compileResponse')).toThrowError(
39+
expect(() => tracker.add(-1, 'compileResponse')).toThrow(
4040
'Invalid request ID -1.',
4141
);
4242
});
4343

4444
it('errors if the request ID overlaps that of an existing in-flight request', () => {
4545
tracker.add(0, 'compileResponse');
46-
expect(() => tracker.add(0, 'compileResponse')).toThrowError(
46+
expect(() => tracker.add(0, 'compileResponse')).toThrow(
4747
'Request ID 0 is already in use by an in-flight request.',
4848
);
4949
});
@@ -74,14 +74,14 @@ describe('request tracker', () => {
7474
});
7575

7676
it('errors if the response ID does not match any existing request IDs', () => {
77-
expect(() => tracker.resolve(0, 'compileResponse')).toThrowError(
77+
expect(() => tracker.resolve(0, 'compileResponse')).toThrow(
7878
'Response ID 0 does not match any pending requests.',
7979
);
8080
});
8181

8282
it('errors if the response type does not match what the request is expecting', () => {
8383
tracker.add(0, 'importResponse');
84-
expect(() => tracker.resolve(0, 'fileImportResponse')).toThrowError(
84+
expect(() => tracker.resolve(0, 'fileImportResponse')).toThrow(
8585
"Response with ID 0 does not match pending request's type. Expected " +
8686
'importResponse but received fileImportResponse.',
8787
);

0 commit comments

Comments
 (0)