Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.23.0
0.24.0
30 changes: 27 additions & 3 deletions internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ".") {
Expand All @@ -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 {
Expand Down
48 changes: 29 additions & 19 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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: "**/*"}}
Expand Down
Loading