diff --git a/js-api-spec.ts b/js-api-spec.ts index 3eb36453e6..4a48ef75dc 100644 --- a/js-api-spec.ts +++ b/js-api-spec.ts @@ -82,6 +82,12 @@ if (!fs.existsSync(specIndex)) { // `node_modules` directory. fs.copySync(p.resolve(specPath), p.join(sassPackagePath, 'js-api')); +// Copy deprecations YAML so we can test against it. +fs.copySync( + p.resolve(p.join(argv.sassSassRepo, 'spec/deprecations.yaml')), + p.join(sassPackagePath, 'deprecations.yaml') +); + fs.writeFileSync( p.join(sassPackagePath, 'package.json'), JSON.stringify({ diff --git a/js-api-spec/deprecations.node.test.ts b/js-api-spec/deprecations.node.test.ts new file mode 100644 index 0000000000..b0e649f49a --- /dev/null +++ b/js-api-spec/deprecations.node.test.ts @@ -0,0 +1,68 @@ +// Copyright 2024 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 fs from 'fs'; +import yaml from 'js-yaml'; +import {deprecations, Deprecation, Version} from 'sass'; + +describe('deprecation type', () => { + const deprecationsMap = deprecations as unknown as { + [key: string]: Deprecation; + }; + const obsoleteDeprecations: {[key: string]: [string, string]} = {}; + const activeDeprecations: {[key: string]: string} = {}; + const futureDeprecations: Set = new Set(); + const data = yaml.load( + fs.readFileSync('js-api-spec/node_modules/sass/deprecations.yaml', 'utf8') + ) as { + [key: string]: { + 'dart-sass': + | {status: 'future'} + | {status: 'active'; deprecated: string} + | {status: 'obsolete'; deprecated: string; obsolete: string}; + }; + }; + for (const [id, deprecation] of Object.entries(data)) { + const dartSass = deprecation['dart-sass']; + if (dartSass.status === 'obsolete') { + obsoleteDeprecations[id] = [dartSass.deprecated, dartSass.obsolete]; + } else if (dartSass.status === 'active') { + activeDeprecations[id] = dartSass.deprecated; + } else if (dartSass.status === 'future') { + futureDeprecations.add(id); + } + } + + // These tests assume that the JS API being tested is backed by Dart Sass. + // If there's a JS API implementation in the future with a different compiler, + // then these tests shouldn't be run. + for (const [id, versions] of Object.entries(obsoleteDeprecations)) { + if (!versions) continue; + const [deprecatedIn, obsoleteIn] = versions; + it(`${id} deprecated in ${deprecatedIn} and obsolete in ${obsoleteIn}`, () => { + const deprecation = deprecationsMap[id]; + expect(deprecation?.id).toBe(id); + expect(deprecation?.status).toBe('obsolete'); + expect(deprecation?.deprecatedIn).toEqual(Version.parse(deprecatedIn)); + expect(deprecation?.obsoleteIn).toEqual(Version.parse(obsoleteIn)); + }); + } + + for (const [id, version] of Object.entries(activeDeprecations)) { + it(`${id} deprecated in ${version}`, () => { + const deprecation = deprecationsMap[id]; + expect(deprecation?.id).toBe(id); + expect(deprecation?.status).toBe('active'); + expect(deprecation?.deprecatedIn).toEqual(Version.parse(version)); + }); + } + + for (const id of futureDeprecations) { + it(`${id} is a future deprecation`, () => { + const deprecation = deprecationsMap[id]; + expect(deprecation?.id).toBe(id); + expect(deprecation?.status).toBe('future'); + }); + } +}); diff --git a/js-api-spec/deprecations.test.ts b/js-api-spec/deprecations.test.ts index fd343122a8..852c068f88 100644 --- a/js-api-spec/deprecations.test.ts +++ b/js-api-spec/deprecations.test.ts @@ -6,116 +6,12 @@ import { compileString, deprecations, Deprecation, - Deprecations, Importer, Version, } from 'sass'; import {captureStdio, URL} from './utils'; -/** - * Map from obsolete deprecation IDs to version pairs. - * - * The first version is the version this deprecation type was deprecated in, - * while the second version is the version it was made obsolete in. - */ -const obsoleteDeprecations: {[key in keyof Deprecations]?: [string, string]} = - {}; - -/** Map from active deprecation IDs to the version they were deprecated in. */ -const activeDeprecations: {[key in keyof Deprecations]?: string} = { - 'call-string': '0.0.0', - elseif: '1.3.2', - 'moz-document': '1.7.2', - 'relative-canonical': '1.14.2', - 'new-global': '1.17.2', - 'color-module-compat': '1.23.0', - 'slash-div': '1.33.0', - 'bogus-combinators': '1.54.0', - 'strict-unary': '1.55.0', - 'function-units': '1.56.0', - 'duplicate-var-flags': '1.62.0', - 'null-alpha': '1.62.3', - 'abs-percent': '1.65.0', - 'fs-importer-cwd': '1.73.0', - 'color-4-api': '1.76.0', - 'color-functions': '1.76.0', -}; - -/** - * List of future deprecation IDs. - * - * This is only structured as an object to allow us to use a mapped object type - * to ensure that all deprecation IDs listed here are included in the JS API - * spec. - */ -const futureDeprecations: {[key in keyof Deprecations]?: true} = {import: true}; - -/** - * This is a temporary synchronization check to ensure that any new deprecation - * types are added to all five of these locations: - * - lib/src/deprecation.dart in sass/dart-sass - * - js-api-doc/deprecations.d.ts in sass/sass - * - spec/js-api/deprecations.d.ts.md in sass/sass - * - lib/src/deprecations.ts in sass/embedded-host-node - * - js-api-spec/deprecations.test.ts in sass/sass-spec (this file) - * - * Work to replace these manual changes with generated code from a single - * source-of-truth is tracked in sass/sass#3827 - */ -it('there are no extra or missing deprecation types', () => { - const expectedDeprecations = [ - ...Object.keys(obsoleteDeprecations), - ...Object.keys(activeDeprecations), - ...Object.keys(futureDeprecations), - 'user-authored', - ]; - const actualDeprecations = Object.keys(deprecations); - const extraDeprecations = actualDeprecations.filter( - deprecation => !expectedDeprecations.includes(deprecation) - ); - expect(extraDeprecations).toBeEmptyArray(); - const missingDeprecations = expectedDeprecations.filter( - deprecation => !actualDeprecations.includes(deprecation) - ); - expect(missingDeprecations).toBeEmptyArray(); -}); - -describe('deprecation type', () => { - const deprecationsMap = deprecations as unknown as { - [key: string]: Deprecation; - }; - - for (const [id, versions] of Object.entries(obsoleteDeprecations)) { - if (!versions) continue; - const [deprecatedIn, obsoleteIn] = versions; - it(`${id} deprecated in ${deprecatedIn} and obsolete in ${obsoleteIn}`, () => { - const deprecation = deprecationsMap[id]; - expect(deprecation?.id).toBe(id); - expect(deprecation?.status).toBe('obsolete'); - expect(deprecation?.deprecatedIn).toEqual(Version.parse(deprecatedIn)); - expect(deprecation?.obsoleteIn).toEqual(Version.parse(obsoleteIn)); - }); - } - - for (const [id, version] of Object.entries(activeDeprecations)) { - it(`${id} deprecated in ${version}`, () => { - const deprecation = deprecationsMap[id]; - expect(deprecation?.id).toBe(id); - expect(deprecation?.status).toBe('active'); - expect(deprecation?.deprecatedIn).toEqual(Version.parse(version)); - }); - } - - for (const [id] of Object.entries(futureDeprecations)) { - it(`${id} is a future deprecation`, () => { - const deprecation = deprecationsMap[id]; - expect(deprecation?.id).toBe(id); - expect(deprecation?.status).toBe('future'); - }); - } -}); - describe('a warning', () => { it('is emitted with no flags', done => { compileString('a { $b: c !global; }', { diff --git a/js-api-spec/importer.node.test.ts b/js-api-spec/importer.node.test.ts index a240ccc27c..e03ad18fb9 100644 --- a/js-api-spec/importer.node.test.ts +++ b/js-api-spec/importer.node.test.ts @@ -12,6 +12,7 @@ import { } from 'sass'; import {sandbox} from './sandbox'; +import {spy} from './utils'; it('avoids importer when canonicalize() returns null', () => sandbox(dir => { @@ -366,6 +367,30 @@ describe('FileImporter', () => { ).toThrowSassException({line: 0}); }); }); + + // Regression test for sass/dart-sass#2208. + it('imports the same relative url from different base urls as different files', () => + sandbox(dir => { + const findFileUrl = spy((url, context) => { + return url === 'y' ? new URL('x.scss', context.containingUrl) : null; + }); + + dir.write({ + 'main.scss': '@import "sub1/test"; @import "sub1/sub2/test"', + 'sub1/test.scss': '@import "y"', + 'sub1/x.scss': 'x { from: sub1; }', + 'sub1/sub2/test.scss': '@import "y"', + 'sub1/sub2/x.scss': 'x { from: sub2; }', + }); + + expect( + compile(dir('main.scss'), { + importers: [{findFileUrl}], + }).css.toString() + ).toEqualIgnoringWhitespace('x { from: sub1; } x { from: sub2; }'); + + expect(findFileUrl).toHaveBeenCalledTimes(2); + })); }); it( diff --git a/js-api-spec/legacy/importer.node.test.ts b/js-api-spec/legacy/importer.node.test.ts index 081d36f536..43f567fc24 100644 --- a/js-api-spec/legacy/importer.node.test.ts +++ b/js-api-spec/legacy/importer.node.test.ts @@ -178,6 +178,36 @@ describe('with contents', () => { }), }).stats.includedFiles ).toContain(p.resolve('bar'))); + + // Regression test for sass/dart-sass#2208. + it('imports the same relative url from different base urls as different files', () => + sandbox(dir => { + const importer = spy((url: string, prev: string) => { + return url === 'x' + ? { + contents: `x {from: ${p.basename(p.dirname(prev))}}`, + file: p.resolve(p.dirname(prev), 'x.scss'), + } + : null; + }); + + dir.write({ + 'main.scss': '@import "sub1/test"; @import "sub1/sub2/test"', + 'sub1/test.scss': '@import "x"', + 'sub1/sub2/test.scss': '@import "x"', + }); + + expect( + sass + .renderSync({ + file: dir('main.scss'), + importer, + }) + .css.toString() + ).toEqualIgnoringWhitespace('x { from: sub1; } x { from: sub2; }'); + + expect(importer).toHaveBeenCalledTimes(2); + })); }); describe('with a file redirect', () => { diff --git a/spec/css/keyframes.hrx b/spec/css/keyframes.hrx index 1e7648e4dc..3169f8f140 100644 --- a/spec/css/keyframes.hrx +++ b/spec/css/keyframes.hrx @@ -248,3 +248,48 @@ $a: b; c: d; } } + +<===> +================================================================================ +<===> in_keyframe_block/unknown_at_rule/input.scss +@keyframes a { + to {@b} +} + +<===> in_keyframe_block/unknown_at_rule/output.css +@keyframes a { + to { + @b; + } +} + +<===> +================================================================================ +<===> in_keyframe_block/known_at_rule/input.scss +@keyframes a { + to {@media screen {b: c}} +} + +<===> in_keyframe_block/known_at_rule/output.css +@keyframes a { + to { + @media screen { + b: c; + } + } +} + +<===> +================================================================================ +<===> error/in_keyframe_block/style_rule/input.scss +@keyframes a { + to {to {c: d}} +} + +<===> error/in_keyframe_block/style_rule/error +Error: Style rules may not be used within keyframe blocks. + , +2 | to {to {c: d}} + | ^^^^^^^^^ + ' + input.scss 2:7 root stylesheet diff --git a/spec/directives/function.hrx b/spec/directives/function.hrx new file mode 100644 index 0000000000..7c8c993088 --- /dev/null +++ b/spec/directives/function.hrx @@ -0,0 +1,94 @@ +<===> escaped/input.scss +// Function names can be defined and referred to using escapes, which are +// normalized. +@function f\6Fo-bar() {@return 1} + +a {b: foo-b\61r()} + +<===> escaped/output.css +a { + b: 1; +} + +<===> +================================================================================ +<===> custom_ident_name/input.scss +@function --a() {@return 1} +b {c: --a()} + +<===> custom_ident_name/output.css +b { + c: 1; +} + +<===> custom_ident_name/warning +DEPRECATION WARNING on line 1, column 11 of input.scss: +Sass @function names beginning with -- are deprecated for forward-compatibility with plain CSS mixins. + +For details, see https://sass-lang.com/d/css-function-mixin + , +1 | @function --a() {@return 1} + | ^^^ + ' + +<===> +================================================================================ +<===> double_underscore_name/input.scss +@function __a() {@return 1} +b {c: __a()} + +<===> double_underscore_name/output.css +b { + c: 1; +} + +<===> +================================================================================ +<===> custom_ident_call/input.scss +@function __a() {@return 1} +b {c: --a()} + +<===> custom_ident_call/output.css +b { + c: 1; +} + +<===> custom_ident_call/warning +DEPRECATION WARNING: Sass @function names beginning with -- are deprecated for forward-compatibility with plain CSS functions. + +For details, see https://sass-lang.com/d/css-function-mixin + + , +2 | b {c: --a()} + | ^^^ + ' + input.scss 2:7 root stylesheet + +<===> +================================================================================ +<===> vendor_like_underscore/README.md +Function names like `-moz-calc()` aren't allowed, but they are with underscores. + +<===> +================================================================================ +<===> vendor_like_underscore/start/input.scss +@function _moz-calc() {@return 1} +b {c: _moz-calc()} + +<===> vendor_like_underscore/start/output.css +b { + c: 1; +} + +<===> +================================================================================ +<===> vendor_like_underscore/middle/input.scss +@function -moz_calc() {@return 1} +b {c: -moz_calc()} + +<===> vendor_like_underscore/middle/output.css +b { + c: 1; +} + + diff --git a/spec/directives/function/escaped.hrx b/spec/directives/function/escaped.hrx deleted file mode 100644 index a43f229f71..0000000000 --- a/spec/directives/function/escaped.hrx +++ /dev/null @@ -1,11 +0,0 @@ -<===> input.scss -// Function names can be defined and referred to using escapes, which are -// normalized. -@function f\6Fo-bar() {@return 1} - -a {b: foo-b\61r()} - -<===> output.css -a { - b: 1; -} diff --git a/spec/directives/mixin.hrx b/spec/directives/mixin.hrx new file mode 100644 index 0000000000..93e3568647 --- /dev/null +++ b/spec/directives/mixin.hrx @@ -0,0 +1,51 @@ +<===> custom_ident_name/input.scss +@mixin --a {b: c} +d {@include --a} + +<===> custom_ident_name/output.css +d { + b: c; +} + +<===> custom_ident_name/warning +DEPRECATION WARNING on line 1, column 8 of input.scss: +Sass @mixin names beginning with -- are deprecated for forward-compatibility with plain CSS mixins. + +For details, see https://sass-lang.com/d/css-function-mixin + , +1 | @mixin --a {b: c} + | ^^^ + ' + +<===> +================================================================================ +<===> double_underscore_name/input.scss +@mixin __a() {b: c} +d {@include __a} + +<===> double_underscore_name/output.css +d { + b: c; +} + +<===> +================================================================================ +<===> custom_ident_include/input.scss +@mixin __a() {b: c} +d {@include --a} + +<===> custom_ident_include/output.css +d { + b: c; +} + +<===> custom_ident_include/warning +DEPRECATION WARNING: Sass @mixin names beginning with -- are deprecated for forward-compatibility with plain CSS mixins. + +For details, see https://sass-lang.com/d/css-function-mixin + + , +2 | d {@include --a} + | ^^^ + ' + input.scss 2:13 root stylesheet diff --git a/spec/libsass/selectors/simple.hrx b/spec/libsass/selectors/simple.hrx index 706b807c7d..b87ebeb52e 100644 --- a/spec/libsass/selectors/simple.hrx +++ b/spec/libsass/selectors/simple.hrx @@ -14,9 +14,6 @@ div { @-webkit-keyframes { from { left: 0px; - 10% { - whatever: hoo; - } } to { left: 200px; @@ -57,9 +54,6 @@ div:nth(-3) { @-webkit-keyframes { from { left: 0px; - 10% { - whatever: hoo; - } } to { left: 200px;