diff --git a/CHANGELOG.md b/CHANGELOG.md index 997e349..4f9b018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.24.0] - 2026-06-08 + +### Added +- CSS/SCSS taint now bridges into JS imports for the "JS-bundled CSS" pattern (most `libs/gdc-*`). Previously, a changed SCSS file propagated through cross-library `@use` chains (e.g. `gdc-dashboards-runtime/src/styles/app.scss` `@use`s `@gooddata/sdk-ui-dashboard`) but the taint lived in a separate `__css__:` namespace that was only matched against *style* imports. A consumer that pulls the styles in purely via a JavaScript import — `import { Root } from "gdc-dashboards-runtime"`, where `Root.tsx` does `import "./styles/app.scss"` — never matched, so prod-affecting CSS changes failed to trigger app targets like `gdc-dashboards`. Now, while analysing each library (with `INCLUDE_CSS=1`), a local style file is treated as tainted if it `@use`s the styles of a CSS-tainted upstream package; any TS file that side-effect-imports that style file inherits taint on its exported symbols, which then rides the normal TS import graph into JS consumers. The `__css__` closure is computed before library analysis (and threaded through the per-package upstream-taint filter) so it is available during seeding. This implements Stage 3 of `properly-support-tree-shaken-scss-or-scss-modules.md`; `package.json` `sideEffects` gating is still to come. + ## [0.23.0] - 2026-06-07 ### Removed @@ -317,6 +322,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Multi-stage Docker build - Automated vendor upgrade workflow +[0.24.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.23.0...v0.24.0 [0.23.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.22.0...v0.23.0 [0.22.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.21.3...v0.22.0 [0.21.3]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.21.2...v0.21.3 diff --git a/VERSION b/VERSION index 2ba6141..286d5b0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.23.0 \ No newline at end of file +0.24.0 \ No newline at end of file diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 63b54fb..12f7b15 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -427,10 +427,34 @@ func AnalyzeLibraryPackage(projectFolder string, entrypoints []Entrypoint, merge } } - // Seed taint from changed CSS/SCSS files within this package. + // Determine the full set of tainted style files in this package. A style file is + // tainted if it was directly changed, or if it @use's the styles of a CSS-tainted + // upstream package (Pattern A — JS-bundled CSS). A TS file that side-effect-imports + // a tainted style file then inherits taint on its exported symbols, so the change + // rides through the normal TS import graph into JS consumers of this package. + taintedStyleFiles := make(map[string]bool) + for f := range changedStyleFiles { + taintedStyleFiles[f] = true + } + if IncludeCSS && len(upstreamTaint) > 0 { + for _, styleFile := range globStyleFiles(projectFolder) { + if taintedStyleFiles[styleFile] { + continue + } + for _, useSpec := range parseScssUses(filepath.Join(projectFolder, styleFile)) { + if matchesCSSTaint(useSpec, upstreamTaint) { + taintedStyleFiles[styleFile] = true + log.Debugf(" %s: style file tainted via @use of %s", styleFile, useSpec) + break + } + } + } + } + + // Seed taint from tainted CSS/SCSS files within this package. // For CSS module imports (*.module.scss/css) with named bindings, only taint symbols // that use the imported binding. For all other style imports, taint all symbols. - if len(changedStyleFiles) > 0 { + if len(taintedStyleFiles) > 0 { for stem, analysis := range fileAnalyses { for _, imp := range analysis.Imports { if !strings.HasPrefix(imp.Source, ".") { @@ -442,7 +466,7 @@ func AnalyzeLibraryPackage(projectFolder string, entrypoints []Entrypoint, merge fileDir := filepath.Dir(stem + ".ts") resolved := filepath.Join(fileDir, imp.Source) resolved = filepath.Clean(resolved) - if !changedStyleFiles[resolved] { + if !taintedStyleFiles[resolved] { continue } if tainted[stem] == nil { diff --git a/main.go b/main.go index dea0962..82c5dec 100644 --- a/main.go +++ b/main.go @@ -226,6 +226,28 @@ func main() { } } + // CSS/SCSS taint propagation: when --include-css is set, any changed CSS/SCSS + // file in a library taints all style imports from that library in downstream packages. + // This runs BEFORE library analysis so the __css__ closure is available while seeding + // taint: a library whose TS entrypoint side-effect-imports a style file that @use's a + // CSS-tainted package inherits taint on its JS exports, which then propagates through + // the normal bottom-up TS import graph into JS consumers (Pattern A — JS-bundled CSS). + if flagIncludeCSS { + cssTaintedPkgs := analyzer.FindCSSTaintedPackages(changedFiles, rushConfig, projectMap) + for pkgName := range cssTaintedPkgs { + key := analyzer.CSSTaintPrefix + pkgName + if allUpstreamTaint[key] == nil { + allUpstreamTaint[key] = make(map[string]bool) + } + allUpstreamTaint[key]["*"] = true + if flagDebug { + fmt.Fprintf(os.Stderr, "[DEBUG] CSS taint: %s\n", pkgName) + } + } + // Propagate CSS taint through SCSS @use chains across libraries + analyzer.PropagateCSSTaint(rushConfig, projectMap, allUpstreamTaint) + } + type pkgResult struct { pkgName string affected []analyzer.AffectedExport @@ -314,7 +336,13 @@ func main() { pkgUpstreamTaint := make(map[string]map[string]bool) for _, dep := range info.DependsOn { for specifier, names := range allUpstreamTaint { - if strings.HasPrefix(specifier, dep) { + matches := strings.HasPrefix(specifier, dep) + if !matches && strings.HasPrefix(specifier, analyzer.CSSTaintPrefix) { + // CSS taint keys are namespaced ("__css__:pkg"); match on the package name + // so a dep's CSS taint is visible while analysing this package's style @use chains. + matches = strings.HasPrefix(strings.TrimPrefix(specifier, analyzer.CSSTaintPrefix), dep) + } + if matches { if pkgUpstreamTaint[specifier] == nil { pkgUpstreamTaint[specifier] = make(map[string]bool) } @@ -366,24 +394,6 @@ func main() { } } - // CSS/SCSS taint propagation: when --include-css is set, any changed CSS/SCSS - // file in a library taints all style imports from that library in downstream packages. - if flagIncludeCSS { - cssTaintedPkgs := analyzer.FindCSSTaintedPackages(changedFiles, rushConfig, projectMap) - for pkgName := range cssTaintedPkgs { - key := analyzer.CSSTaintPrefix + pkgName - if allUpstreamTaint[key] == nil { - allUpstreamTaint[key] = make(map[string]bool) - } - allUpstreamTaint[key]["*"] = true - if flagDebug { - fmt.Fprintf(os.Stderr, "[DEBUG] CSS taint: %s\n", pkgName) - } - } - // Propagate CSS taint through SCSS @use chains across libraries - analyzer.PropagateCSSTaint(rushConfig, projectMap, allUpstreamTaint) - } - // Detect affected targets from .goodchangesrc.json configs. changedE2E := make(map[string]*TargetResult) defaultChangeDirs := []rush.ChangeDir{{Glob: "**/*"}}