From 1c2638b1c24c1260c8abc605e160a4a831f43163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Fri, 25 Oct 2024 08:36:21 -0700 Subject: [PATCH 1/5] Implement `sass --embedded` in pure JS mode --- .github/workflows/ci.yml | 12 +----- bin/sass.ts | 5 +++ lib/src/compiler-module.ts | 34 ++++++++++++++++ lib/src/compiler-path.ts | 76 ++++++++++++----------------------- package.json | 1 + tool/get-embedded-compiler.ts | 42 ++++++++++++++----- tool/init.ts | 10 +++-- 7 files changed, 106 insertions(+), 74 deletions(-) create mode 100644 lib/src/compiler-module.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd3614a3..c16c75c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,17 +117,7 @@ jobs: working-directory: sass-spec - name: Compile - run: | - npm run compile - if [[ "$RUNNER_OS" == "Windows" ]]; then - # Avoid copying the entire Dart Sass build directory on Windows, - # since it may contain symlinks that cp will choke on. - mkdir -p dist/lib/src/vendor/dart-sass/ - cp {`pwd`/,dist/}lib/src/vendor/dart-sass/sass.bat - cp {`pwd`/,dist/}lib/src/vendor/dart-sass/sass.snapshot - else - ln -s {`pwd`/,dist/}lib/src/vendor/dart-sass - fi + run: npm run compile - name: Run tests run: npm run js-api-spec -- --sassPackage .. --sassSassRepo ../language diff --git a/bin/sass.ts b/bin/sass.ts index 80c60125..f057b5e1 100755 --- a/bin/sass.ts +++ b/bin/sass.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import * as child_process from 'child_process'; +import * as path from 'path'; import {compilerCommand} from '../lib/src/compiler-path'; // TODO npm/cmd-shim#152 and yarnpkg/berry#6422 - If and when the package @@ -12,6 +13,10 @@ try { compilerCommand[0], [...compilerCommand.slice(1), ...process.argv.slice(2)], { + // Node blocks launching .bat and .cmd without a shell due to CVE-2024-27980 + shell: ['.bat', '.cmd'].includes( + path.extname(compilerCommand[0]).toLowerCase(), + ), stdio: 'inherit', windowsHide: true, }, diff --git a/lib/src/compiler-module.ts b/lib/src/compiler-module.ts new file mode 100644 index 00000000..113170c7 --- /dev/null +++ b/lib/src/compiler-module.ts @@ -0,0 +1,34 @@ +// Copyright 2025 Google LLC. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import * as p from 'path'; +import {getElfInterpreter} from './elf'; + +/** + * Detect if the given binary is linked with musl libc by checking if + * the interpreter basename starts with "ld-musl-" + */ +function isLinuxMusl(path: string): boolean { + try { + const interpreter = getElfInterpreter(path); + return p.basename(interpreter).startsWith('ld-musl-'); + } catch (error) { + console.warn( + `Warning: Failed to detect linux-musl, fallback to linux-gnu: ${error.message}`, + ); + return false; + } +} + +/** The module name for the embedded compiler executable. */ +export const compilerModule = (() => { + const platform = + process.platform === 'linux' && isLinuxMusl(process.execPath) + ? 'linux-musl' + : (process.platform as string); + + const arch = process.arch; + + return `sass-embedded-${platform}-${arch}`; +})(); diff --git a/lib/src/compiler-path.ts b/lib/src/compiler-path.ts index b9344cf5..169c0c08 100644 --- a/lib/src/compiler-path.ts +++ b/lib/src/compiler-path.ts @@ -2,78 +2,52 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import * as fs from 'fs'; import * as p from 'path'; -import {getElfInterpreter} from './elf'; -import {isErrnoException} from './utils'; - -/** - * Detect if the given binary is linked with musl libc by checking if - * the interpreter basename starts with "ld-musl-" - */ -function isLinuxMusl(path: string): boolean { - try { - const interpreter = getElfInterpreter(path); - return p.basename(interpreter).startsWith('ld-musl-'); - } catch (error) { - console.warn( - `Warning: Failed to detect linux-musl, fallback to linux-gnu: ${error.message}`, - ); - return false; - } -} +import {compilerModule} from './compiler-module'; /** The full command for the embedded compiler executable. */ export const compilerCommand = (() => { - const platform = - process.platform === 'linux' && isLinuxMusl(process.execPath) - ? 'linux-musl' - : (process.platform as string); - - const arch = process.arch; - - // find for development - for (const path of ['vendor', '../../../lib/src/vendor']) { - const executable = p.resolve( - __dirname, - path, - `dart-sass/sass${platform === 'win32' ? '.bat' : ''}`, - ); - - if (fs.existsSync(executable)) return [executable]; - } - try { return [ require.resolve( - `sass-embedded-${platform}-${arch}/dart-sass/src/dart` + - (platform === 'win32' ? '.exe' : ''), - ), - require.resolve( - `sass-embedded-${platform}-${arch}/dart-sass/src/sass.snapshot`, + `${compilerModule}/dart-sass/src/dart` + + (process.platform === 'win32' ? '.exe' : ''), ), + require.resolve(`${compilerModule}/dart-sass/src/sass.snapshot`), ]; - } catch (ignored) { - // ignored + } catch (e) { + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } } try { return [ require.resolve( - `sass-embedded-${platform}-${arch}/dart-sass/sass` + - (platform === 'win32' ? '.bat' : ''), + `${compilerModule}/dart-sass/sass` + + (process.platform === 'win32' ? '.bat' : ''), ), ]; - } catch (e: unknown) { - if (!(isErrnoException(e) && e.code === 'MODULE_NOT_FOUND')) { + } catch (e) { + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } + } + + try { + return [ + process.execPath, + p.join(p.dirname(require.resolve('sass')), 'sass.js'), + ]; + } catch (e) { + if (e.code !== 'MODULE_NOT_FOUND') { throw e; } } throw new Error( "Embedded Dart Sass couldn't find the embedded compiler executable. " + - 'Please make sure the optional dependency ' + - `sass-embedded-${platform}-${arch} is installed in ` + - 'node_modules.', + `Please make sure the optional dependency ${compilerModule} or sass is ` + + 'installed in node_modules.', ); })(); diff --git a/package.json b/package.json index 6a8f0f0b..9373389b 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "test": "jest" }, "optionalDependencies": { + "sass": "1.89.2", "sass-embedded-android-arm": "1.89.2", "sass-embedded-android-arm64": "1.89.2", "sass-embedded-android-riscv64": "1.89.2", diff --git a/tool/get-embedded-compiler.ts b/tool/get-embedded-compiler.ts index 828c22b8..85b44490 100644 --- a/tool/get-embedded-compiler.ts +++ b/tool/get-embedded-compiler.ts @@ -2,9 +2,11 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import {promises as fs} from 'fs'; import * as p from 'path'; import * as shell from 'shelljs'; +import {compilerModule} from '../lib/src/compiler-module'; import * as utils from './utils'; /** @@ -14,7 +16,7 @@ import * as utils from './utils'; * at `path`. By default, checks out the latest revision from GitHub. */ export async function getEmbeddedCompiler( - outPath: string, + js?: boolean, options?: {ref: string} | {path: string}, ): Promise { const repo = 'dart-sass'; @@ -41,21 +43,43 @@ export async function getEmbeddedCompiler( await utils.link(languageInHost, languageInCompiler); } - buildDartSassEmbedded(source); - await utils.link(p.join(source, 'build'), p.join(outPath, repo)); + buildDartSassEmbedded(source, js ?? false); + + const jsModulePath = p.resolve('node_modules/sass'); + const dartModulePath = p.resolve(p.join('node_modules', compilerModule)); + if (js) { + await fs.rm(dartModulePath, {force: true, recursive: true}); + await utils.link(p.join(source, 'build/npm'), jsModulePath); + } else { + await fs.rm(jsModulePath, {force: true, recursive: true}); + await utils.link(p.join(source, 'build'), p.join(dartModulePath, repo)); + } } // Builds the Embedded Dart Sass executable from the source at `repoPath`. -function buildDartSassEmbedded(repoPath: string): void { +function buildDartSassEmbedded(repoPath: string, js: boolean): void { console.log("Downloading Dart Sass's dependencies."); shell.exec('dart pub upgrade', { cwd: repoPath, silent: true, }); - console.log('Building the Dart Sass executable.'); - shell.exec('dart run grinder protobuf pkg-standalone-dev', { - cwd: repoPath, - env: {...process.env, UPDATE_SASS_PROTOCOL: 'false'}, - }); + if (js) { + shell.exec('npm install', { + cwd: repoPath, + silent: true, + }); + + console.log('Building the Dart Sass npm package.'); + shell.exec('dart run grinder protobuf pkg-npm-dev', { + cwd: repoPath, + env: {...process.env, UPDATE_SASS_PROTOCOL: 'false'}, + }); + } else { + console.log('Building the Dart Sass executable.'); + shell.exec('dart run grinder protobuf pkg-standalone-dev', { + cwd: repoPath, + env: {...process.env, UPDATE_SASS_PROTOCOL: 'false'}, + }); + } } diff --git a/tool/init.ts b/tool/init.ts index 230cd09d..80f7b71d 100644 --- a/tool/init.ts +++ b/tool/init.ts @@ -18,6 +18,10 @@ const argv = yargs(process.argv.slice(2)) type: 'string', description: 'Build the Embedded Dart Sass binary from this Git ref.', }) + .option('compiler-js', { + type: 'boolean', + description: 'Build the Embedded Dart Sass with dart2js.', + }) .option('skip-compiler', { type: 'boolean', description: "Don't Embedded Dart Sass at all.", @@ -55,15 +59,15 @@ void (async () => { if (!argv['skip-compiler']) { if (argv['compiler-ref']) { - await getEmbeddedCompiler(outPath, { + await getEmbeddedCompiler(argv['compiler-js'], { ref: argv['compiler-ref'], }); } else if (argv['compiler-path']) { - await getEmbeddedCompiler(outPath, { + await getEmbeddedCompiler(argv['compiler-js'], { path: argv['compiler-path'], }); } else { - await getEmbeddedCompiler(outPath); + await getEmbeddedCompiler(argv['compiler-js']); } } From 6dd70371bf2279a8c093ca9c1a3297b5fc826119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Wed, 16 Jul 2025 15:38:22 -0700 Subject: [PATCH 2/5] Make `js` a key of `options` --- tool/get-embedded-compiler.ts | 23 +++++++++++++++++------ tool/init.ts | 10 +++++++--- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/tool/get-embedded-compiler.ts b/tool/get-embedded-compiler.ts index 85b44490..aac304ca 100644 --- a/tool/get-embedded-compiler.ts +++ b/tool/get-embedded-compiler.ts @@ -14,23 +14,33 @@ import * as utils from './utils'; * * Can check out and build the source from a Git `ref` or build from the source * at `path`. By default, checks out the latest revision from GitHub. + * + * The embedded compiler will be built as dart snapshot by default, or pure node + * js if the `js` option is `true`. */ export async function getEmbeddedCompiler( - js?: boolean, - options?: {ref: string} | {path: string}, + options?: + | { + ref?: string; + js?: boolean; + } + | { + path: string; + js?: boolean; + }, ): Promise { const repo = 'dart-sass'; let source: string; - if (!options || 'ref' in options) { + if (options !== undefined && 'path' in options) { + source = options.path; + } else { utils.fetchRepo({ repo, outPath: 'build', ref: options?.ref ?? 'main', }); source = p.join('build', repo); - } else { - source = options.path; } // Make sure the compiler sees the same version of the language repo that the @@ -43,7 +53,8 @@ export async function getEmbeddedCompiler( await utils.link(languageInHost, languageInCompiler); } - buildDartSassEmbedded(source, js ?? false); + const js = options?.js ?? false; + buildDartSassEmbedded(source, js); const jsModulePath = p.resolve('node_modules/sass'); const dartModulePath = p.resolve(p.join('node_modules', compilerModule)); diff --git a/tool/init.ts b/tool/init.ts index 80f7b71d..ffed630a 100644 --- a/tool/init.ts +++ b/tool/init.ts @@ -59,15 +59,19 @@ void (async () => { if (!argv['skip-compiler']) { if (argv['compiler-ref']) { - await getEmbeddedCompiler(argv['compiler-js'], { + await getEmbeddedCompiler({ ref: argv['compiler-ref'], + js: argv['compiler-js'], }); } else if (argv['compiler-path']) { - await getEmbeddedCompiler(argv['compiler-js'], { + await getEmbeddedCompiler({ path: argv['compiler-path'], + js: argv['compiler-js'], }); } else { - await getEmbeddedCompiler(argv['compiler-js']); + await getEmbeddedCompiler({ + js: argv['compiler-js'], + }); } } From 49a65725ec95e7ebb4b01a9a7580553b68923c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Fri, 18 Jul 2025 18:23:37 -0700 Subject: [PATCH 3/5] Add "binary" packages for unknown platforms --- npm/all-unknown/README.md | 3 +++ npm/all-unknown/package.json | 17 +++++++++++++++++ npm/unknown-all/README.md | 3 +++ npm/unknown-all/package.json | 17 +++++++++++++++++ package.json | 7 ++++--- tool/prepare-optional-release.ts | 23 +++++++++++++++++++++++ 6 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 npm/all-unknown/README.md create mode 100644 npm/all-unknown/package.json create mode 100644 npm/unknown-all/README.md create mode 100644 npm/unknown-all/package.json diff --git a/npm/all-unknown/README.md b/npm/all-unknown/README.md new file mode 100644 index 00000000..e9b7cf6f --- /dev/null +++ b/npm/all-unknown/README.md @@ -0,0 +1,3 @@ +# `sass-embedded-all-unknown` + +This is the **all-unknown** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) diff --git a/npm/all-unknown/package.json b/npm/all-unknown/package.json new file mode 100644 index 00000000..234bd6f6 --- /dev/null +++ b/npm/all-unknown/package.json @@ -0,0 +1,17 @@ +{ + "name": "sass-embedded-all-unknown", + "version": "1.89.2", + "description": "The all-unknown binary for sass-embedded", + "repository": "sass/embedded-host-node", + "author": "Google Inc.", + "license": "MIT", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "dependencies": { + "sass": "1.89.2" + } +} diff --git a/npm/unknown-all/README.md b/npm/unknown-all/README.md new file mode 100644 index 00000000..14851af3 --- /dev/null +++ b/npm/unknown-all/README.md @@ -0,0 +1,3 @@ +# `sass-embedded-unknown-all` + +This is the **unknown-all** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) diff --git a/npm/unknown-all/package.json b/npm/unknown-all/package.json new file mode 100644 index 00000000..92190b6d --- /dev/null +++ b/npm/unknown-all/package.json @@ -0,0 +1,17 @@ +{ + "name": "sass-embedded-unknown-all", + "version": "1.89.2", + "description": "The unknown-all binary for sass-embedded", + "repository": "sass/embedded-host-node", + "author": "Google Inc.", + "license": "MIT", + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "dependencies": { + "sass": "1.89.2" + } +} diff --git a/package.json b/package.json index 9373389b..b0488590 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test": "jest" }, "optionalDependencies": { - "sass": "1.89.2", + "sass-embedded-all-unknown": "1.89.2", "sass-embedded-android-arm": "1.89.2", "sass-embedded-android-arm64": "1.89.2", "sass-embedded-android-riscv64": "1.89.2", @@ -47,12 +47,13 @@ "sass-embedded-darwin-x64": "1.89.2", "sass-embedded-linux-arm": "1.89.2", "sass-embedded-linux-arm64": "1.89.2", - "sass-embedded-linux-riscv64": "1.89.2", - "sass-embedded-linux-x64": "1.89.2", "sass-embedded-linux-musl-arm": "1.89.2", "sass-embedded-linux-musl-arm64": "1.89.2", "sass-embedded-linux-musl-riscv64": "1.89.2", "sass-embedded-linux-musl-x64": "1.89.2", + "sass-embedded-linux-riscv64": "1.89.2", + "sass-embedded-linux-x64": "1.89.2", + "sass-embedded-unknown-all": "1.89.2", "sass-embedded-win32-arm64": "1.89.2", "sass-embedded-win32-x64": "1.89.2" }, diff --git a/tool/prepare-optional-release.ts b/tool/prepare-optional-release.ts index d98529d6..bffdce45 100644 --- a/tool/prepare-optional-release.ts +++ b/tool/prepare-optional-release.ts @@ -119,6 +119,29 @@ void (async () => { ); } + const optPkg = JSON.parse( + ( + await fs.readFile(p.join('npm', argv.package, 'package.json')) + ).toString(), + ) as {['version']: string; ['dependencies']?: {['sass']?: string}}; + + if (optPkg.version !== pkg.version) { + throw Error( + "Optional package's version does not match main package's version", + ); + } + + const sassDependencyVersion = optPkg?.dependencies?.sass; + if (sassDependencyVersion !== undefined) { + if (sassDependencyVersion !== pkg.version) { + throw Error( + "Optional package's sass dependency version does not match main package's version", + ); + } + + return; + } + const index = argv.package.lastIndexOf('-'); const nodePlatform = argv.package.substring(0, index); const nodeArch = argv.package.substring(index + 1); From 7da381c86aef975471d25b87b10703fe95f20109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Fri, 18 Jul 2025 18:54:26 -0700 Subject: [PATCH 4/5] Update README.md --- npm/all-unknown/README.md | 4 +++- npm/all-unknown/package.json | 2 +- npm/unknown-all/README.md | 4 +++- npm/unknown-all/package.json | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/npm/all-unknown/README.md b/npm/all-unknown/README.md index e9b7cf6f..5223ace6 100644 --- a/npm/all-unknown/README.md +++ b/npm/all-unknown/README.md @@ -1,3 +1,5 @@ # `sass-embedded-all-unknown` -This is the **all-unknown** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) +This is the pure js optional dependency for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) + +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. diff --git a/npm/all-unknown/package.json b/npm/all-unknown/package.json index 234bd6f6..0ee85b62 100644 --- a/npm/all-unknown/package.json +++ b/npm/all-unknown/package.json @@ -1,7 +1,7 @@ { "name": "sass-embedded-all-unknown", "version": "1.89.2", - "description": "The all-unknown binary for sass-embedded", + "description": "The pure js optional dependency for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", diff --git a/npm/unknown-all/README.md b/npm/unknown-all/README.md index 14851af3..4f20cb4a 100644 --- a/npm/unknown-all/README.md +++ b/npm/unknown-all/README.md @@ -1,3 +1,5 @@ # `sass-embedded-unknown-all` -This is the **unknown-all** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) +This is the pure js optional dependency for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) + +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. diff --git a/npm/unknown-all/package.json b/npm/unknown-all/package.json index 92190b6d..47a2f003 100644 --- a/npm/unknown-all/package.json +++ b/npm/unknown-all/package.json @@ -1,7 +1,7 @@ { "name": "sass-embedded-unknown-all", "version": "1.89.2", - "description": "The unknown-all binary for sass-embedded", + "description": "The pure js optional dependency for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", From a75b008344019d6b7ec06b0e7696c82f9d73123f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Sat, 19 Jul 2025 01:29:20 -0700 Subject: [PATCH 5/5] Remove unnecessary null check --- tool/prepare-optional-release.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/prepare-optional-release.ts b/tool/prepare-optional-release.ts index bffdce45..9b7d695e 100644 --- a/tool/prepare-optional-release.ts +++ b/tool/prepare-optional-release.ts @@ -131,7 +131,7 @@ void (async () => { ); } - const sassDependencyVersion = optPkg?.dependencies?.sass; + const sassDependencyVersion = optPkg.dependencies?.sass; if (sassDependencyVersion !== undefined) { if (sassDependencyVersion !== pkg.version) { throw Error(