From 4f35641102ac50eee7c83e7e85e4e5b66e124c74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 01:54:30 +0000 Subject: [PATCH 1/8] Bump colorjs.io from 0.5.2 to 0.6.1 Bumps [colorjs.io](https://github.com/color-js/color.js) from 0.5.2 to 0.6.1. - [Release notes](https://github.com/color-js/color.js/releases) - [Commits](https://github.com/color-js/color.js/compare/v0.5.2...v0.6.1) --- updated-dependencies: - dependency-name: colorjs.io dependency-version: 0.6.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af5b570e..7974076c 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@bufbuild/protobuf": "^2.5.0", - "colorjs.io": "^0.5.0", + "colorjs.io": "^0.6.1", "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", From ea932284ad64076703ea14fc0018aa7c830affb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Mon, 26 Jan 2026 12:43:53 -0800 Subject: [PATCH 2/8] Use gamma 2.40 for display-referred rec2020 --- lib/src/value/color.ts | 201 ++++++++++++++++++++++------------------- package.json | 2 +- tsconfig.json | 2 + 3 files changed, 112 insertions(+), 93 deletions(-) diff --git a/lib/src/value/color.ts b/lib/src/value/color.ts index 60b2928a..1ec03a30 100644 --- a/lib/src/value/color.ts +++ b/lib/src/value/color.ts @@ -118,29 +118,34 @@ function getColorSpace(options: ChannelOptions): KnownColorSpace { } /** - * Convert from the ColorJS representation of a missing component (`NaN`) to - * `null`. + * Convert from the ColorJS representation of a missing component (`null`) to + * `NaN`. */ -function NaNtoNull(val: number): number | null { - return Number.isNaN(val) ? null : val; +function nullToNaN(val: number | null): number { + return val ?? NaN; } /** - * Convert from the ColorJS representation of a missing component (`NaN`) to + * Convert from the ColorJS representation of a missing component (`null`) to * `0`. */ -function NaNtoZero(val: number): number { - return Number.isNaN(val) ? 0 : val; +function nullToZero(val: number | null): number { + return val ?? 0; } /** Convert from sRGB (0-1) to RGB (0-255) units. */ -function coordToRgb(val: number): number { - return val * 255; +function coordToRgb(val: number | null): number | null { + return val === null ? val : val * 255; +} + +/** Convert from RGB (0-255) to sRGB (0-1) units. */ +function rgbToCoord(val: number | null): number | null { + return val === null ? val : val / 255; } /** Normalize `hue` values to be within the range `[0, 360)`. */ -function normalizeHue(val: number): number { - return positiveMod(val, 360); +function normalizeHue(val: number | null): number | null { + return val === null ? val : positiveMod(val, 360); } /** @@ -268,24 +273,25 @@ function isPolarColorSpace(space: KnownColorSpace): space is PolarColorSpace { } /** - * Convert from ColorJS coordinates (which use `NaN` for missing components, and - * a range of `0-1` for `rgb` channel values) to Sass Color coordinates (which - * use `null` for missing components, and a range of `0-255` for `rgb` channel - * values). + * Convert from ColorJS coordinates (which use `null` for missing components, + * and a range of `0-1` for `rgb` channel values) to Sass Color coordinates + * (which use `null` for missing components, and a range of `0-255` for `rgb` + * channel values). */ function decodeCoordsFromColorJs( - coords: [number, number, number], // ColorJS coordinates + coords: [number | null, number | null, number | null], // ColorJS coordinates isRgb = false, // Whether this color is in the `rgb` color space ): [number | null, number | null, number | null] { let newCoords = coords; // If this color is in the `rgb` space, convert channel values to `0-255` - if (isRgb) newCoords = newCoords.map(coordToRgb) as [number, number, number]; - // Convert `NaN` values to `null` - return newCoords.map(NaNtoNull) as [ - number | null, - number | null, - number | null, - ]; + if (isRgb) { + newCoords = newCoords.map(coordToRgb) as [ + number | null, + number | null, + number | null, + ]; + } + return newCoords; } /** Returns `true` if `val` is a `number` or `null`. */ @@ -454,10 +460,10 @@ export class SassColor extends Value { const space = options.space ?? getColorSpace(options); this.setChannelIds(space); if (space === 'rgb') this.isRgb = true; - let alpha: number; + let alpha: number | null; if (options.alpha === null) { if (!options.space) emitNullAlphaDeprecation(); - alpha = NaN; + alpha = null; } else if (options.alpha === undefined) { alpha = 1; } else { @@ -467,14 +473,18 @@ export class SassColor extends Value { switch (space) { case 'rgb': case 'srgb': { - const red = options.red ?? NaN; - const green = options.green ?? NaN; - const blue = options.blue ?? NaN; + const red = options.red ?? null; + const green = options.green ?? null; + const blue = options.blue ?? null; if (this.isRgb) { this.color = new Color({ spaceId: encodeSpaceForColorJs(space), // convert from 0-255 to 0-1 - coords: [red / 255, green / 255, blue / 255], + coords: [red, green, blue].map(rgbToCoord) as [ + number | null, + number | null, + number | null, + ], alpha, }); } else { @@ -496,21 +506,23 @@ export class SassColor extends Value { this.color = new Color({ spaceId: encodeSpaceForColorJs(space), coords: [ - options.red ?? NaN, - options.green ?? NaN, - options.blue ?? NaN, + options.red ?? null, + options.green ?? null, + options.blue ?? null, ], alpha, }); break; case 'hsl': { - let hue = normalizeHue(options.hue ?? NaN); - let saturation = options.saturation ?? NaN; - const lightness = options.lightness ?? NaN; - if (!Number.isNaN(saturation) && fuzzyLessThan(saturation, 0)) { + let hue = normalizeHue(options.hue ?? null); + let saturation = options.saturation ?? null; + const lightness = options.lightness ?? null; + if (saturation !== null && fuzzyLessThan(saturation, 0)) { saturation = Math.abs(saturation); - hue = (hue + 180) % 360; + if (hue !== null) { + hue = (hue + 180) % 360; + } } this.color = new Color({ @@ -522,9 +534,9 @@ export class SassColor extends Value { } case 'hwb': { - const hue = normalizeHue(options.hue ?? NaN); - const whiteness = options.whiteness ?? NaN; - const blackness = options.blackness ?? NaN; + const hue = normalizeHue(options.hue ?? null); + const whiteness = options.whiteness ?? null; + const blackness = options.blackness ?? null; this.color = new Color({ spaceId: encodeSpaceForColorJs(space), coords: [hue, whiteness, blackness], @@ -535,9 +547,9 @@ export class SassColor extends Value { case 'lab': case 'oklab': { - const lightness = options.lightness ?? NaN; - const a = options.a ?? NaN; - const b = options.b ?? NaN; + const lightness = options.lightness ?? null; + const a = options.a ?? null; + const b = options.b ?? null; this.color = new Color({ spaceId: encodeSpaceForColorJs(space), coords: [lightness, a, b], @@ -548,12 +560,14 @@ export class SassColor extends Value { case 'lch': case 'oklch': { - const lightness = options.lightness ?? NaN; - let chroma = options.chroma ?? NaN; - let hue = normalizeHue(options.hue ?? NaN); - if (!Number.isNaN(chroma) && fuzzyLessThan(chroma, 0)) { + const lightness = options.lightness ?? null; + let chroma = options.chroma ?? null; + let hue = normalizeHue(options.hue ?? null); + if (chroma !== null && fuzzyLessThan(chroma, 0)) { chroma = Math.abs(chroma); - hue = (hue + 180) % 360; + if (hue !== null) { + hue = (hue + 180) % 360; + } } this.color = new Color({ @@ -569,23 +583,16 @@ export class SassColor extends Value { case 'xyz-d50': this.color = new Color({ spaceId: encodeSpaceForColorJs(space), - coords: [options.x ?? NaN, options.y ?? NaN, options.z ?? NaN], + coords: [options.x ?? null, options.y ?? null, options.z ?? null], alpha, }); break; } - - // @TODO Waiting on new release of ColorJS that includes allowing `alpha` - // to be `NaN` on initial construction. - // Fixed in: https://github.com/LeaVerou/color.js/commit/08b39c180565ae61408ad737d91bd71a1f79d3df - if (Number.isNaN(alpha)) { - this.color.alpha = NaN; - } } /** This color's alpha channel, between `0` and `1`. */ get alpha(): number { - return NaNtoZero(this.color.alpha); + return nullToZero(this.color.alpha); } /** The name of this color's color space. */ @@ -610,9 +617,13 @@ export class SassColor extends Value { get channelsOrNull(): List { let coords = this.color.coords; if (this.space === 'rgb') { - coords = coords.map(coordToRgb) as [number, number, number]; + coords = coords.map(coordToRgb) as [ + number | null, + number | null, + number | null, + ]; } - return List(coords.map(NaNtoNull)); + return List(coords); } /** @@ -624,9 +635,13 @@ export class SassColor extends Value { get channels(): List { let coords = this.color.coords; if (this.space === 'rgb') { - coords = coords.map(coordToRgb) as [number, number, number]; + coords = coords.map(coordToRgb) as [ + number | null, + number | null, + number | null, + ]; } - return List(coords.map(NaNtoZero)); + return List(coords.map(nullToZero)); } /** @@ -636,7 +651,7 @@ export class SassColor extends Value { */ get red(): number { emitColor4ApiGetterDeprecation('red'); - const val = NaNtoZero(coordToRgb(this.color.srgb.red)); + const val = nullToZero(coordToRgb(this.color.srgb.red)); return fuzzyRound(val); } @@ -647,7 +662,7 @@ export class SassColor extends Value { */ get green(): number { emitColor4ApiGetterDeprecation('green'); - const val = NaNtoZero(coordToRgb(this.color.srgb.green)); + const val = nullToZero(coordToRgb(this.color.srgb.green)); return fuzzyRound(val); } @@ -658,7 +673,7 @@ export class SassColor extends Value { */ get blue(): number { emitColor4ApiGetterDeprecation('blue'); - const val = NaNtoZero(coordToRgb(this.color.srgb.blue)); + const val = nullToZero(coordToRgb(this.color.srgb.blue)); return fuzzyRound(val); } @@ -669,7 +684,7 @@ export class SassColor extends Value { */ get hue(): number { emitColor4ApiGetterDeprecation('hue'); - return NaNtoZero(this.color.hsl.hue); + return nullToZero(this.color.hsl.hue); } /** @@ -679,7 +694,7 @@ export class SassColor extends Value { */ get saturation(): number { emitColor4ApiGetterDeprecation('saturation'); - return NaNtoZero(this.color.hsl.saturation); + return nullToZero(this.color.hsl.saturation); } /** @@ -689,7 +704,7 @@ export class SassColor extends Value { */ get lightness(): number { emitColor4ApiGetterDeprecation('lightness'); - return NaNtoZero(this.color.hsl.lightness); + return nullToZero(this.color.hsl.lightness); } /** @@ -699,7 +714,7 @@ export class SassColor extends Value { */ get whiteness(): number { emitColor4ApiGetterDeprecation('whiteness'); - return NaNtoZero(this.color.hwb.whiteness); + return nullToZero(this.color.hwb.whiteness); } /** @@ -709,7 +724,7 @@ export class SassColor extends Value { */ get blackness(): number { emitColor4ApiGetterDeprecation('blackness'); - return NaNtoZero(this.color.hwb.blackness); + return nullToZero(this.color.hwb.blackness); } assertColor(): SassColor { @@ -771,7 +786,7 @@ export class SassColor extends Value { channel(channel: ChannelNameXyz, options: {space: ColorSpaceXyz}): number; channel(channel: ChannelName, options?: {space: KnownColorSpace}): number { if (channel === 'alpha') return this.alpha; - let val: number; + let val: number | null; const space = options?.space ?? this.space; validateChannelInSpace(channel, space); if (options?.space) { @@ -786,7 +801,7 @@ export class SassColor extends Value { }); } if (space === 'rgb') val = coordToRgb(val); - return NaNtoZero(val); + return nullToZero(val); } /** @@ -796,13 +811,13 @@ export class SassColor extends Value { * [missing channel]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#missing_color_components */ isChannelMissing(channel: ChannelName): boolean { - if (channel === 'alpha') return Number.isNaN(this.color.alpha); + if (channel === 'alpha') return this.color.alpha === null; validateChannelInSpace(channel, this.space); - return Number.isNaN( + return ( this.color.get({ space: this.color.spaceId, coordId: encodeChannelForColorJs(channel), - }), + }) === null ); } @@ -909,7 +924,7 @@ export class SassColor extends Value { [this.channel0Id]: coords[0], [this.channel1Id]: coords[1], [this.channel2Id]: coords[2], - alpha: NaNtoNull(this.color.alpha), + alpha: this.color.alpha, }); } @@ -1143,23 +1158,25 @@ export class SassColor extends Value { coords = this.color .to('srgb') .coords.map(coordToRgb) + .map(nullToNaN) .map(fuzzyRound) as [number, number, number]; otherCoords = other.color .to('srgb') .coords.map(coordToRgb) + .map(nullToNaN) .map(fuzzyRound) as [number, number, number]; } return ( - fuzzyEquals(coords[0], otherCoords[0]) && - fuzzyEquals(coords[1], otherCoords[1]) && - fuzzyEquals(coords[2], otherCoords[2]) + fuzzyEquals(nullToNaN(coords[0]), nullToNaN(otherCoords[0])) && + fuzzyEquals(nullToNaN(coords[1]), nullToNaN(otherCoords[1])) && + fuzzyEquals(nullToNaN(coords[2]), nullToNaN(otherCoords[2])) ); } return ( this.space === other.space && - fuzzyEquals(coords[0], otherCoords[0]) && - fuzzyEquals(coords[1], otherCoords[1]) && - fuzzyEquals(coords[2], otherCoords[2]) && + fuzzyEquals(nullToNaN(coords[0]), nullToNaN(otherCoords[0])) && + fuzzyEquals(nullToNaN(coords[1]), nullToNaN(otherCoords[1])) && + fuzzyEquals(nullToNaN(coords[2]), nullToNaN(otherCoords[2])) && fuzzyEquals(this.alpha, other.alpha) ); } @@ -1167,23 +1184,23 @@ export class SassColor extends Value { hashCode(): number { let coords = this.color.coords; if (this.isLegacy) { - coords = this.color.to('srgb').coords.map(coordToRgb).map(fuzzyRound) as [ - number, - number, - number, - ]; + coords = this.color + .to('srgb') + .coords.map(coordToRgb) + .map(nullToNaN) + .map(fuzzyRound) as [number, number, number]; return ( - fuzzyHashCode(coords[0]) ^ - fuzzyHashCode(coords[1]) ^ - fuzzyHashCode(coords[2]) ^ + fuzzyHashCode(nullToNaN(coords[0])) ^ + fuzzyHashCode(nullToNaN(coords[1])) ^ + fuzzyHashCode(nullToNaN(coords[2])) ^ fuzzyHashCode(this.alpha) ); } return ( hash(this.space) ^ - fuzzyHashCode(coords[0]) ^ - fuzzyHashCode(coords[1]) ^ - fuzzyHashCode(coords[2]) ^ + fuzzyHashCode(nullToNaN(coords[0])) ^ + fuzzyHashCode(nullToNaN(coords[1])) ^ + fuzzyHashCode(nullToNaN(coords[2])) ^ fuzzyHashCode(this.alpha) ); } diff --git a/package.json b/package.json index 7974076c..d919ed3d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dist/**/*" ], "engines": { - "node": ">=16.0.0" + "node": ">=20.19.0" }, "bin": { "sass": "dist/bin/sass.js" diff --git a/tsconfig.json b/tsconfig.json index 91520088..babac750 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", "allowJs": true, "outDir": "dist", "resolveJsonModule": true, From 759cab5d3b49bead2969f4d0d55bd484f5f1a485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Tue, 27 Jan 2026 17:12:52 -0800 Subject: [PATCH 3/8] Update comments --- lib/src/value/color.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/value/color.ts b/lib/src/value/color.ts index 1ec03a30..690dbdbf 100644 --- a/lib/src/value/color.ts +++ b/lib/src/value/color.ts @@ -273,10 +273,9 @@ function isPolarColorSpace(space: KnownColorSpace): space is PolarColorSpace { } /** - * Convert from ColorJS coordinates (which use `null` for missing components, - * and a range of `0-1` for `rgb` channel values) to Sass Color coordinates - * (which use `null` for missing components, and a range of `0-255` for `rgb` - * channel values). + * Convert from ColorJS coordinates (which use a range of `0-1` for `rgb` + * channel values) to Sass Color coordinates (which use a range of `0-255` for + * `rgb` channel values). */ function decodeCoordsFromColorJs( coords: [number | null, number | null, number | null], // ColorJS coordinates From e2d640c2558005df38dbd1690765cdd0b7459ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Wed, 28 Jan 2026 11:59:55 -0800 Subject: [PATCH 4/8] Update tsconfig --- tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index babac750..337311fc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { - "module": "nodenext", - "moduleResolution": "nodenext", + "module": "node20", + "moduleResolution": "node16", "allowJs": true, "outDir": "dist", "resolveJsonModule": true, From 744cd7f8f799319eec0d58d45c35de6df19db1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Wed, 28 Jan 2026 12:16:02 -0800 Subject: [PATCH 5/8] Remove nullToNaN --- lib/src/value/color.ts | 57 ++++++++++++++++-------------------------- lib/src/value/utils.ts | 16 +++++++----- 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/lib/src/value/color.ts b/lib/src/value/color.ts index 690dbdbf..6277c392 100644 --- a/lib/src/value/color.ts +++ b/lib/src/value/color.ts @@ -117,14 +117,6 @@ function getColorSpace(options: ChannelOptions): KnownColorSpace { throw valueError('No color space found'); } -/** - * Convert from the ColorJS representation of a missing component (`null`) to - * `NaN`. - */ -function nullToNaN(val: number | null): number { - return val ?? NaN; -} - /** * Convert from the ColorJS representation of a missing component (`null`) to * `0`. @@ -650,8 +642,7 @@ export class SassColor extends Value { */ get red(): number { emitColor4ApiGetterDeprecation('red'); - const val = nullToZero(coordToRgb(this.color.srgb.red)); - return fuzzyRound(val); + return fuzzyRound(coordToRgb(this.color.srgb.red)) ?? 0; } /** @@ -661,8 +652,7 @@ export class SassColor extends Value { */ get green(): number { emitColor4ApiGetterDeprecation('green'); - const val = nullToZero(coordToRgb(this.color.srgb.green)); - return fuzzyRound(val); + return fuzzyRound(coordToRgb(this.color.srgb.green)) ?? 0; } /** @@ -672,8 +662,7 @@ export class SassColor extends Value { */ get blue(): number { emitColor4ApiGetterDeprecation('blue'); - const val = nullToZero(coordToRgb(this.color.srgb.blue)); - return fuzzyRound(val); + return fuzzyRound(coordToRgb(this.color.srgb.blue)) ?? 0; } /** @@ -1157,25 +1146,23 @@ export class SassColor extends Value { coords = this.color .to('srgb') .coords.map(coordToRgb) - .map(nullToNaN) - .map(fuzzyRound) as [number, number, number]; + .map(fuzzyRound) as [number | null, number | null, number | null]; otherCoords = other.color .to('srgb') .coords.map(coordToRgb) - .map(nullToNaN) - .map(fuzzyRound) as [number, number, number]; + .map(fuzzyRound) as [number | null, number | null, number | null]; } return ( - fuzzyEquals(nullToNaN(coords[0]), nullToNaN(otherCoords[0])) && - fuzzyEquals(nullToNaN(coords[1]), nullToNaN(otherCoords[1])) && - fuzzyEquals(nullToNaN(coords[2]), nullToNaN(otherCoords[2])) + fuzzyEquals(coords[0], otherCoords[0]) && + fuzzyEquals(coords[1], otherCoords[1]) && + fuzzyEquals(coords[2], otherCoords[2]) ); } return ( this.space === other.space && - fuzzyEquals(nullToNaN(coords[0]), nullToNaN(otherCoords[0])) && - fuzzyEquals(nullToNaN(coords[1]), nullToNaN(otherCoords[1])) && - fuzzyEquals(nullToNaN(coords[2]), nullToNaN(otherCoords[2])) && + fuzzyEquals(coords[0], otherCoords[0]) && + fuzzyEquals(coords[1], otherCoords[1]) && + fuzzyEquals(coords[2], otherCoords[2]) && fuzzyEquals(this.alpha, other.alpha) ); } @@ -1183,23 +1170,23 @@ export class SassColor extends Value { hashCode(): number { let coords = this.color.coords; if (this.isLegacy) { - coords = this.color - .to('srgb') - .coords.map(coordToRgb) - .map(nullToNaN) - .map(fuzzyRound) as [number, number, number]; + coords = this.color.to('srgb').coords.map(coordToRgb).map(fuzzyRound) as [ + number, + number, + number, + ]; return ( - fuzzyHashCode(nullToNaN(coords[0])) ^ - fuzzyHashCode(nullToNaN(coords[1])) ^ - fuzzyHashCode(nullToNaN(coords[2])) ^ + fuzzyHashCode(coords[0]) ^ + fuzzyHashCode(coords[1]) ^ + fuzzyHashCode(coords[2]) ^ fuzzyHashCode(this.alpha) ); } return ( hash(this.space) ^ - fuzzyHashCode(nullToNaN(coords[0])) ^ - fuzzyHashCode(nullToNaN(coords[1])) ^ - fuzzyHashCode(nullToNaN(coords[2])) ^ + fuzzyHashCode(coords[0]) ^ + fuzzyHashCode(coords[1]) ^ + fuzzyHashCode(coords[2]) ^ fuzzyHashCode(this.alpha) ); } diff --git a/lib/src/value/utils.ts b/lib/src/value/utils.ts index 7844c810..8f054675 100644 --- a/lib/src/value/utils.ts +++ b/lib/src/value/utils.ts @@ -16,7 +16,10 @@ export const precision = 10; const epsilon = 10 ** (-precision - 1); /** Whether `num1` and `num2` are equal within `epsilon`. */ -export function fuzzyEquals(num1: number, num2: number): boolean { +export function fuzzyEquals(num1: number | null, num2: number | null): boolean { + if (num1 === null || num2 === null) { + return false; + } return Math.abs(num1 - num2) < epsilon; } @@ -25,10 +28,8 @@ export function fuzzyEquals(num1: number, num2: number): boolean { * * Two numbers that `fuzzyEquals` each other must have the same hash code. */ -export function fuzzyHashCode(num: number): number { - return !isFinite(num) || isNaN(num) - ? hash(num) - : hash(Math.round(num / epsilon)); +export function fuzzyHashCode(num: number | null): number { + return num === null ? hash(num) : hash(Math.round(num / epsilon)); } /** Whether `num1` < `num2`, within `epsilon`. */ @@ -72,7 +73,10 @@ export function fuzzyAsInt(num: number): number | null { * * If `num` `fuzzyEquals` `x.5`, rounds away from zero. */ -export function fuzzyRound(num: number): number { +export function fuzzyRound(num: number | null): number | null { + if (num === null) { + return num; + } if (num > 0) { return fuzzyLessThan(num % 1, 0.5) ? Math.floor(num) : Math.ceil(num); } else { From 75bec9e206272c462bdfec94019342180d6ccc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Wed, 28 Jan 2026 12:19:09 -0800 Subject: [PATCH 6/8] Remove nullToZero --- lib/src/value/color.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/src/value/color.ts b/lib/src/value/color.ts index 6277c392..994fed6b 100644 --- a/lib/src/value/color.ts +++ b/lib/src/value/color.ts @@ -117,14 +117,6 @@ function getColorSpace(options: ChannelOptions): KnownColorSpace { throw valueError('No color space found'); } -/** - * Convert from the ColorJS representation of a missing component (`null`) to - * `0`. - */ -function nullToZero(val: number | null): number { - return val ?? 0; -} - /** Convert from sRGB (0-1) to RGB (0-255) units. */ function coordToRgb(val: number | null): number | null { return val === null ? val : val * 255; @@ -583,7 +575,7 @@ export class SassColor extends Value { /** This color's alpha channel, between `0` and `1`. */ get alpha(): number { - return nullToZero(this.color.alpha); + return this.color.alpha ?? 0; } /** The name of this color's color space. */ @@ -632,7 +624,7 @@ export class SassColor extends Value { number | null, ]; } - return List(coords.map(nullToZero)); + return List(coords.map(val => val ?? 0)); } /** @@ -672,7 +664,7 @@ export class SassColor extends Value { */ get hue(): number { emitColor4ApiGetterDeprecation('hue'); - return nullToZero(this.color.hsl.hue); + return this.color.hsl.hue ?? 0; } /** @@ -682,7 +674,7 @@ export class SassColor extends Value { */ get saturation(): number { emitColor4ApiGetterDeprecation('saturation'); - return nullToZero(this.color.hsl.saturation); + return this.color.hsl.saturation ?? 0; } /** @@ -692,7 +684,7 @@ export class SassColor extends Value { */ get lightness(): number { emitColor4ApiGetterDeprecation('lightness'); - return nullToZero(this.color.hsl.lightness); + return this.color.hsl.lightness ?? 0; } /** @@ -702,7 +694,7 @@ export class SassColor extends Value { */ get whiteness(): number { emitColor4ApiGetterDeprecation('whiteness'); - return nullToZero(this.color.hwb.whiteness); + return this.color.hwb.whiteness ?? 0; } /** @@ -712,7 +704,7 @@ export class SassColor extends Value { */ get blackness(): number { emitColor4ApiGetterDeprecation('blackness'); - return nullToZero(this.color.hwb.blackness); + return this.color.hwb.blackness ?? 0; } assertColor(): SassColor { @@ -789,7 +781,7 @@ export class SassColor extends Value { }); } if (space === 'rgb') val = coordToRgb(val); - return nullToZero(val); + return val ?? 0; } /** From 4d634e623e08b37ab4601e8ff7f7ee1ae79e4258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Wed, 28 Jan 2026 13:11:23 -0800 Subject: [PATCH 7/8] Disable node 20 jest test --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f3bb83f..877ce4b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,12 @@ jobs: strategy: matrix: os: [ubuntu, macos, windows] - node-version: ['lts/*', 'lts/-1', 'lts/-2'] + # Temporarily disable test on lts/-2 (currently node 20) + # https://github.com/jestjs/jest/issues/13350 + # https://github.com/kulshekhar/ts-jest/issues/4198 + # + # node-version: ['lts/*', 'lts/-1', 'lts/-2'] + node-version: ['lts/*', 'lts/-1'] fail-fast: false steps: From f5e37c8e6630779fb77e8bf8f5ebace4bb2e2eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Thu, 29 Jan 2026 17:54:30 -0800 Subject: [PATCH 8/8] Add a workaround to fix color.js type issue --- .github/workflows/ci.yml | 7 +------ package.json | 2 +- tool/fix-colorjs-types.ts | 16 ++++++++++++++++ tool/init.ts | 3 +++ tsconfig.json | 2 -- 5 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 tool/fix-colorjs-types.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 877ce4b0..4f3bb83f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,12 +42,7 @@ jobs: strategy: matrix: os: [ubuntu, macos, windows] - # Temporarily disable test on lts/-2 (currently node 20) - # https://github.com/jestjs/jest/issues/13350 - # https://github.com/kulshekhar/ts-jest/issues/4198 - # - # node-version: ['lts/*', 'lts/-1', 'lts/-2'] - node-version: ['lts/*', 'lts/-1'] + node-version: ['lts/*', 'lts/-1', 'lts/-2'] fail-fast: false steps: diff --git a/package.json b/package.json index d919ed3d..7974076c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dist/**/*" ], "engines": { - "node": ">=20.19.0" + "node": ">=16.0.0" }, "bin": { "sass": "dist/bin/sass.js" diff --git a/tool/fix-colorjs-types.ts b/tool/fix-colorjs-types.ts new file mode 100644 index 00000000..d5ee4ca1 --- /dev/null +++ b/tool/fix-colorjs-types.ts @@ -0,0 +1,16 @@ +// Workaround https://github.com/color-js/color.js/issues/707 + +import * as fs from 'fs'; + +export function fixColorJsTypes(): void { + const file = 'node_modules/colorjs.io/package.json'; + const pkg = JSON.parse(fs.readFileSync(file, {encoding: 'utf8'})); + if ( + pkg.types === undefined && + fs.existsSync('node_modules/colorjs.io/types') + ) { + console.log(`Patching '${file}'.`); + pkg.types = './types'; + fs.writeFileSync(file, JSON.stringify(pkg, null, 2)); + } +} diff --git a/tool/init.ts b/tool/init.ts index ffed630a..c15caf0e 100644 --- a/tool/init.ts +++ b/tool/init.ts @@ -4,6 +4,7 @@ import yargs from 'yargs'; +import {fixColorJsTypes} from './fix-colorjs-types'; import {getDeprecations} from './get-deprecations'; import {getEmbeddedCompiler} from './get-embedded-compiler'; import {getLanguageRepo} from './get-language-repo'; @@ -43,6 +44,8 @@ const argv = yargs(process.argv.slice(2)) void (async () => { try { + fixColorJsTypes(); + const outPath = 'lib/src/vendor'; if (argv['language-ref']) { diff --git a/tsconfig.json b/tsconfig.json index 337311fc..91520088 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,6 @@ { "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { - "module": "node20", - "moduleResolution": "node16", "allowJs": true, "outDir": "dist", "resolveJsonModule": true,