Skip to content

Commit 4c312c4

Browse files
authored
Support pure JS sass executable (#344)
1 parent a7d5c64 commit 4c312c4

File tree

12 files changed

+195
-80
lines changed

12 files changed

+195
-80
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

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: 25 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,78 +2,52 @@
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+
p.join(p.dirname(require.resolve('sass')), 'sass.js'),
41+
];
42+
} catch (e) {
43+
if (e.code !== 'MODULE_NOT_FOUND') {
6944
throw e;
7045
}
7146
}
7247

7348
throw new Error(
7449
"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.',
50+
`Please make sure the optional dependency ${compilerModule} or sass is ` +
51+
'installed in node_modules.',
7852
);
7953
})();

npm/all-unknown/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# `sass-embedded-all-unknown`
2+
3+
This is the pure js optional dependency for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded)
4+
5+
This package does not contain any dart binary. Instead, it installs the `sass` npm package as a transitive dependency on any CPU without native dart support.

npm/all-unknown/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "sass-embedded-all-unknown",
3+
"version": "1.89.2",
4+
"description": "The pure js optional dependency for sass-embedded",
5+
"repository": "sass/embedded-host-node",
6+
"author": "Google Inc.",
7+
"license": "MIT",
8+
"cpu": [
9+
"!arm",
10+
"!arm64",
11+
"!riscv64",
12+
"!x64"
13+
],
14+
"dependencies": {
15+
"sass": "1.89.2"
16+
}
17+
}

npm/unknown-all/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# `sass-embedded-unknown-all`
2+
3+
This is the pure js optional dependency for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded)
4+
5+
This package does not contain any dart binary. Instead, it installs the `sass` npm package as a transitive dependency on any OS without native dart support.

npm/unknown-all/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "sass-embedded-unknown-all",
3+
"version": "1.89.2",
4+
"description": "The pure js optional dependency for sass-embedded",
5+
"repository": "sass/embedded-host-node",
6+
"author": "Google Inc.",
7+
"license": "MIT",
8+
"os": [
9+
"!android",
10+
"!darwin",
11+
"!linux",
12+
"!win32"
13+
],
14+
"dependencies": {
15+
"sass": "1.89.2"
16+
}
17+
}

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"test": "jest"
3939
},
4040
"optionalDependencies": {
41+
"sass-embedded-all-unknown": "1.89.2",
4142
"sass-embedded-android-arm": "1.89.2",
4243
"sass-embedded-android-arm64": "1.89.2",
4344
"sass-embedded-android-riscv64": "1.89.2",
@@ -46,12 +47,13 @@
4647
"sass-embedded-darwin-x64": "1.89.2",
4748
"sass-embedded-linux-arm": "1.89.2",
4849
"sass-embedded-linux-arm64": "1.89.2",
49-
"sass-embedded-linux-riscv64": "1.89.2",
50-
"sass-embedded-linux-x64": "1.89.2",
5150
"sass-embedded-linux-musl-arm": "1.89.2",
5251
"sass-embedded-linux-musl-arm64": "1.89.2",
5352
"sass-embedded-linux-musl-riscv64": "1.89.2",
5453
"sass-embedded-linux-musl-x64": "1.89.2",
54+
"sass-embedded-linux-riscv64": "1.89.2",
55+
"sass-embedded-linux-x64": "1.89.2",
56+
"sass-embedded-unknown-all": "1.89.2",
5557
"sass-embedded-win32-arm64": "1.89.2",
5658
"sass-embedded-win32-x64": "1.89.2"
5759
},

tool/get-embedded-compiler.ts

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

5+
import {promises as fs} from 'fs';
56
import * as p from 'path';
67
import * as shell from 'shelljs';
78

9+
import {compilerModule} from '../lib/src/compiler-module';
810
import * as utils from './utils';
911

1012
/**
1113
* Downloads and builds the Embedded Dart Sass compiler.
1214
*
1315
* Can check out and build the source from a Git `ref` or build from the source
1416
* at `path`. By default, checks out the latest revision from GitHub.
17+
*
18+
* The embedded compiler will be built as dart snapshot by default, or pure node
19+
* js if the `js` option is `true`.
1520
*/
1621
export async function getEmbeddedCompiler(
17-
outPath: string,
18-
options?: {ref: string} | {path: string},
22+
options?:
23+
| {
24+
ref?: string;
25+
js?: boolean;
26+
}
27+
| {
28+
path: string;
29+
js?: boolean;
30+
},
1931
): Promise<void> {
2032
const repo = 'dart-sass';
2133

2234
let source: string;
23-
if (!options || 'ref' in options) {
35+
if (options !== undefined && 'path' in options) {
36+
source = options.path;
37+
} else {
2438
utils.fetchRepo({
2539
repo,
2640
outPath: 'build',
2741
ref: options?.ref ?? 'main',
2842
});
2943
source = p.join('build', repo);
30-
} else {
31-
source = options.path;
3244
}
3345

3446
// Make sure the compiler sees the same version of the language repo that the
@@ -41,21 +53,44 @@ export async function getEmbeddedCompiler(
4153
await utils.link(languageInHost, languageInCompiler);
4254
}
4355

44-
buildDartSassEmbedded(source);
45-
await utils.link(p.join(source, 'build'), p.join(outPath, repo));
56+
const js = options?.js ?? false;
57+
buildDartSassEmbedded(source, js);
58+
59+
const jsModulePath = p.resolve('node_modules/sass');
60+
const dartModulePath = p.resolve(p.join('node_modules', compilerModule));
61+
if (js) {
62+
await fs.rm(dartModulePath, {force: true, recursive: true});
63+
await utils.link(p.join(source, 'build/npm'), jsModulePath);
64+
} else {
65+
await fs.rm(jsModulePath, {force: true, recursive: true});
66+
await utils.link(p.join(source, 'build'), p.join(dartModulePath, repo));
67+
}
4668
}
4769

4870
// Builds the Embedded Dart Sass executable from the source at `repoPath`.
49-
function buildDartSassEmbedded(repoPath: string): void {
71+
function buildDartSassEmbedded(repoPath: string, js: boolean): void {
5072
console.log("Downloading Dart Sass's dependencies.");
5173
shell.exec('dart pub upgrade', {
5274
cwd: repoPath,
5375
silent: true,
5476
});
5577

56-
console.log('Building the Dart Sass executable.');
57-
shell.exec('dart run grinder protobuf pkg-standalone-dev', {
58-
cwd: repoPath,
59-
env: {...process.env, UPDATE_SASS_PROTOCOL: 'false'},
60-
});
78+
if (js) {
79+
shell.exec('npm install', {
80+
cwd: repoPath,
81+
silent: true,
82+
});
83+
84+
console.log('Building the Dart Sass npm package.');
85+
shell.exec('dart run grinder protobuf pkg-npm-dev', {
86+
cwd: repoPath,
87+
env: {...process.env, UPDATE_SASS_PROTOCOL: 'false'},
88+
});
89+
} else {
90+
console.log('Building the Dart Sass executable.');
91+
shell.exec('dart run grinder protobuf pkg-standalone-dev', {
92+
cwd: repoPath,
93+
env: {...process.env, UPDATE_SASS_PROTOCOL: 'false'},
94+
});
95+
}
6196
}

0 commit comments

Comments
 (0)