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.21.0] - 2026-05-02

### Added
- Top-level `type` field in `.goodchangesrc.json` (`"library"` or `"app"`). When set, overrides the automatic library-vs-app inference from `package.json`. Invalid values cause a fatal error at startup.

## [0.20.0] - 2026-05-01

### Changed
Expand Down Expand Up @@ -287,6 +292,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Multi-stage Docker build
- Automated vendor upgrade workflow

[0.21.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.20.0...v0.21.0
[0.20.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.19.4...v0.20.0
[0.19.4]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.19.3...v0.19.4
[0.19.3]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.19.2...v0.19.3
Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,17 @@ JSON array of target objects:

## Library vs app detection

A package is classified as a **library** if its `package.json` contains any of:
Detection can be overridden by setting the top-level `type` field in `.goodchangesrc.json` to `"library"` or `"app"` (see [Configuration](#configuration)). When unset, classification is inferred.

A package is inferred as a **library** if its `package.json` contains any of:

- `types` (TypeScript type declarations)
- `exports` (modern package exports field)
- `module` (ES module entry)

Libraries get full AST-level analysis: entrypoint resolution, symbol diffing, and taint propagation through their internal import graph.

Everything else is an **app** (bundled). Apps are not analyzed for granular exports -- if any file in an app changes, the app is considered fully tainted.
Everything else is inferred as an **app** (bundled). Apps are not analyzed for granular exports -- if any file in an app changes, the app is considered fully tainted.

## Configuration

Expand Down Expand Up @@ -152,11 +154,12 @@ Each `changeDirs` entry is an object with:

**Top-level fields:**

| Field | Type | Description |
|--------------|---------------|---------------------------------------------------------------------------------------------------------|
| `targets` | `TargetDef[]` | Array of target definitions (see below) |
| `ignores` | `string[]` | Glob patterns for files to exclude from change detection |
| `changeDirs` | `ChangeDir[]` | Global changeDirs. When triggered, taints all library exports and triggers all targets in this package. |
| Field | Type | Description |
|--------------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `type` | `"library" \| "app"` | Optional. Forces this package's classification, skipping the inference described in [Library vs app detection](#library-vs-app-detection). Invalid values cause a fatal error. |
| `targets` | `TargetDef[]` | Array of target definitions (see below) |
| `ignores` | `string[]` | Glob patterns for files to exclude from change detection |
| `changeDirs` | `ChangeDir[]` | Global changeDirs. When triggered, taints all library exports and triggers all targets in this package. |

**TargetDef fields (each entry in `targets`):**

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.20.0
0.21.0
7 changes: 6 additions & 1 deletion internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ type AffectedExport struct {
}

// IsLibrary determines if a package is a library (transpiled) vs a bundled app.
func IsLibrary(pkg rush.PackageJSON) bool {
// When the project config sets an explicit `type`, that value wins; otherwise
// the result is inferred from package.json fields.
func IsLibrary(pc *rush.ProjectConfig, pkg rush.PackageJSON) bool {
if pc != nil && pc.Type != nil {
return *pc.Type == "library"
}
if pkg.Types != "" {
return true
}
Expand Down
1 change: 1 addition & 0 deletions internal/rush/rush.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ func (td TargetDef) OutputName(packageName string) string {
}

type ProjectConfig struct {
Type *string `json:"type,omitempty"` // "library" or "app". When set, overrides automatic inference.
Targets []TargetDef `json:"targets,omitempty"`
Ignores []string `json:"ignores,omitempty"`
ChangeDirs []ChangeDir `json:"changeDirs,omitempty"` // global changeDirs: triggers all exports (library) or all targets (app)
Expand Down
14 changes: 12 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ func main() {
projectMap := rush.BuildProjectMap(rushConfig)
configMap := rush.LoadAllProjectConfigs(rushConfig)

for projectFolder, cfg := range configMap {
if cfg == nil || cfg.Type == nil {
continue
}
if *cfg.Type != "library" && *cfg.Type != "app" {
fmt.Fprintf(os.Stderr, "Invalid type %q in %s/.goodchangesrc.json: must be \"library\" or \"app\"\n", *cfg.Type, projectFolder)
os.Exit(1)
}
}

// Parse TARGETS filter early to skip expensive detection for non-matching targets
var targetPatterns []string
if targetsEnv := os.Getenv("TARGETS"); targetsEnv != "" {
Expand Down Expand Up @@ -208,7 +218,7 @@ func main() {
if info == nil {
continue
}
if analyzer.IsLibrary(info.Package) {
if analyzer.IsLibrary(configMap[rp.ProjectFolder], info.Package) {
if allUpstreamTaint[rp.PackageName] == nil {
allUpstreamTaint[rp.PackageName] = make(map[string]bool)
}
Expand All @@ -233,7 +243,7 @@ func main() {
continue
}
pkg := info.Package
lib := analyzer.IsLibrary(pkg)
lib := analyzer.IsLibrary(configMap[info.ProjectFolder], pkg)
directlyChanged := changedProjects[pkgName] != nil
changedDeps := depChangedDeps[info.ProjectFolder]
isDepAffected := len(changedDeps) > 0
Expand Down
Loading