Skip to content
Open
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ If you have custom implementations of the rules generated by this extension, you
# gazelle:map_kind ts_project ts_project @my_local_repo
```

`map_kind` works for every kind this extension generates, including the test rule. For
example, to emit `vitest_test` rules (that run Vitest) instead of `jest_test` while keeping
full automatic dependency management:

```starlark
# gazelle:map_kind jest_test vitest_test //build:vitest_test.bzl
```

Generated test targets are then named `vitest_test`, loaded from your `.bzl` file, and still
get their `srcs`, `deps`, `data`, and `config` populated from the test file's imports.

## Directives

Gazelle can be configured with _directives_, which are written as top-level
Expand Down
58 changes: 35 additions & 23 deletions gazelle/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,24 @@ func (lang *JS) Loads() []rule.LoadInfo {
}
}

func getKind(c *config.Config, kindName string) string {
// Extract kind_name from KindMap
if kind, ok := c.KindMap[kindName]; ok {
return kind.KindName

// unmapKind reverses "# gazelle:map_kind" renames to recover the builtin kind
// of a rule read from disk, following chains (identity if unmapped).
func unmapKind(c *config.Config, kindName string) string {
seen := map[string]bool{kindName: true}
for {
from := ""
for _, mapped := range c.KindMap {
if mapped.KindName == kindName {
from = mapped.FromKind
break
}
}
if from == "" || seen[from] {
return kindName
}
seen[from] = true
kindName = from
}
return kindName
}

// GenerateRules extracts build metadata from source files in a directory.
Expand Down Expand Up @@ -228,7 +239,7 @@ func (lang *JS) GenerateRules(args language.GenerateArgs) language.GenerateResul
generatedImports = append(generatedImports, generatedTSImports...)

existingRules := lang.readExistingRules(args, true)
lang.pruneManagedRules(existingRules, generatedRules)
lang.pruneManagedRules(args.Config, existingRules, generatedRules)

return language.GenerateResult{
Gen: generatedRules,
Expand Down Expand Up @@ -362,7 +373,7 @@ func readFileAndParse(filePath string, rel string) (*imports, int) {
func (lang *JS) genPkgRule(args language.GenerateArgs, jsConfig *JsConfig) *rule.Rule {
for _, baseName := range args.RegularFiles {
if baseName == "package.json" {
r := rule.NewRule(getKind(args.Config, "js_library"), "package_json")
r := rule.NewRule("js_library", "package_json")
r.SetAttr("srcs", []string{baseName})
if len(jsConfig.Visibility.Labels) > 0 {
r.SetAttr("visibility", jsConfig.Visibility.Labels)
Expand All @@ -386,7 +397,7 @@ func (lang *JS) genJestTest(args language.GenerateArgs, jsConfig *JsConfig, jest

ruleName := strings.TrimSuffix(baseName, extension) + ".test"
r := rule.NewRule(
getKind(args.Config, "jest_test"),
"jest_test",
ruleName,
)
r.SetAttr("srcs", []string{baseName})
Expand Down Expand Up @@ -421,7 +432,7 @@ func (lang *JS) genJestTest(args language.GenerateArgs, jsConfig *JsConfig, jest
pkgName := PkgName(args.Rel)
ruleName := fmt.Sprintf("%s_test", pkgName)
r := rule.NewRule(
getKind(args.Config, "jest_test"),
"jest_test",
ruleName,
)

Expand Down Expand Up @@ -492,7 +503,8 @@ func (lang *JS) genRules(args language.GenerateArgs, jsConfig *JsConfig, isBarre
// add as a folder
for _, existingRule := range lang.readExistingRules(args, false) {
// Look for existing rules with the same name, but different kind
if existingRule.Name() == name && existingRule.Kind() != getKind(args.Config, "ts_project") && existingRule.Kind() != getKind(args.Config, "js_library") {
existingKind := unmapKind(args.Config, existingRule.Kind())
if existingRule.Name() == name && existingKind != "ts_project" && existingKind != "js_library" {
if kind == "ts_project" {
name = name + "_ts"
} else {
Expand All @@ -504,7 +516,7 @@ func (lang *JS) genRules(args language.GenerateArgs, jsConfig *JsConfig, isBarre
folderImports, folderRule := lang.makeFolderRule(moduleRuleArgs{
pkgName: name,
cwd: args.Rel,
ruleType: getKind(args.Config, kind),
ruleType: kind,
srcs: sources,
imports: imports,
}, jsConfig)
Expand All @@ -522,7 +534,7 @@ func (lang *JS) genRules(args language.GenerateArgs, jsConfig *JsConfig, isBarre
moduleImports, moduleRules := lang.makeModuleRules(moduleRuleArgs{
pkgName: name,
cwd: args.Rel,
ruleType: getKind(args.Config, kind),
ruleType: kind,
srcs: sources,
imports: imports,
}, jsConfig)
Expand All @@ -542,7 +554,7 @@ func (lang *JS) genRules(args language.GenerateArgs, jsConfig *JsConfig, isBarre
} else {
// add as singletons
singletonRules := lang.makeRules(ruleArgs{
ruleType: getKind(args.Config, kind),
ruleType: kind,
srcs: sources,
trimExt: true,
}, jsConfig)
Expand Down Expand Up @@ -766,7 +778,7 @@ func (lang *JS) genWebAssets(args language.GenerateArgs, webAssetsSet map[string
if jsConfig.CollectWebAssets {
// aggregate rule
name := "assets"
r := rule.NewRule(getKind(args.Config, "web_assets"), name)
r := rule.NewRule("web_assets", name)
r.SetAttr("srcs", webAssets)
if len(jsConfig.Visibility.Labels) > 0 {
r.SetAttr("visibility", jsConfig.Visibility.Labels)
Expand All @@ -782,7 +794,7 @@ func (lang *JS) genWebAssets(args language.GenerateArgs, webAssetsSet map[string
} else {
// add as singletons
rules := lang.makeRules(ruleArgs{
ruleType: getKind(args.Config, "web_assets"),
ruleType: "web_assets",
srcs: webAssets,
trimExt: false, //shadow the original file name
}, jsConfig)
Expand Down Expand Up @@ -812,7 +824,7 @@ func (lang *JS) genAllAssets(args language.GenerateArgs, isJSRoot bool, jsConfig
JSRootDeps = append(JSRootDeps, fqName)
}
name := "all_assets"
r := rule.NewRule(getKind(args.Config, "web_assets"), name)
r := rule.NewRule("web_assets", name)
r.SetAttr("srcs", JSRootDeps)

generatedRules = append(generatedRules, r)
Expand All @@ -829,7 +841,7 @@ func (lang *JS) genCollectedTargetsRule(args language.GenerateArgs, jsConfig *Js
if jsConfig.CollectTargets != "" {

// Add an empty `js_library` rule. This will be given `deps` later in resolve.go
r := rule.NewRule(getKind(args.Config, "js_library"), jsConfig.CollectTargets)
r := rule.NewRule("js_library", jsConfig.CollectTargets)

if len(jsConfig.Visibility.Labels) > 0 {
r.SetAttr("visibility", jsConfig.Visibility.Labels)
Expand All @@ -849,7 +861,7 @@ func (lang *JS) readExistingRules(args language.GenerateArgs, managedOnly bool)
// For each existing rule
for _, r := range BUILD.Rules {
if managedOnly {
if _, ok := managedRulesSet[r.Kind()]; !ok {
if _, ok := managedRulesSet[unmapKind(args.Config, r.Kind())]; !ok {
// skip unmanaged rules
continue
}
Expand All @@ -860,7 +872,7 @@ func (lang *JS) readExistingRules(args language.GenerateArgs, managedOnly bool)
return existingRules
}

func (lang *JS) pruneManagedRules(existingRules map[string]*rule.Rule, generatedRules []*rule.Rule) {
func (lang *JS) pruneManagedRules(c *config.Config, existingRules map[string]*rule.Rule, generatedRules []*rule.Rule) {
// Generate a list of rules that may be deleted and mark them for deletion
// This is generated from existing rules that are managed by gazelle
// that didn't get generated this run
Expand All @@ -869,19 +881,19 @@ func (lang *JS) pruneManagedRules(existingRules map[string]*rule.Rule, generated
deleteRulesSet := make(map[string]*rule.Rule)
for _, existingRule := range existingRules {
// use kind/name to enable deletion of old rules when a new rule would use the same name
key := fmt.Sprintf("%s/%s", existingRule.Kind(), existingRule.Name())
key := fmt.Sprintf("%s/%s", unmapKind(c, existingRule.Kind()), existingRule.Name())
deleteRulesSet[key] = existingRule
}

// Prune generated rules
for _, generatedRule := range generatedRules {
key := fmt.Sprintf("%s/%s", generatedRule.Kind(), generatedRule.Name())
key := fmt.Sprintf("%s/%s", unmapKind(c, generatedRule.Kind()), generatedRule.Name())
delete(deleteRulesSet, key)
}

for _, r := range deleteRulesSet {
// Is this rule managed by Gazelle?
if _, ok := managedRulesSet[r.Kind()]; ok {
if _, ok := managedRulesSet[unmapKind(c, r.Kind())]; ok {
// It is managed, and wasn't generated, so delete it
r.Delete()
}
Expand Down
35 changes: 35 additions & 0 deletions gazelle/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package js

import (
"testing"

"github.com/bazelbuild/bazel-gazelle/config"
)

func TestPattern(t *testing.T) {
Expand All @@ -26,3 +28,36 @@ func TestPattern(t *testing.T) {
t.FailNow()
}
}

func TestUnmapKind(t *testing.T) {
std := map[string]config.MappedKind{
"jest_test": {FromKind: "jest_test", KindName: "vitest_test"},
"ts_project": {FromKind: "ts_project", KindName: "mid"},
"mid": {FromKind: "mid", KindName: "outer"},
"web_assets": {FromKind: "web_assets", KindName: "web_assets"},
}
cycle := map[string]config.MappedKind{
"x": {FromKind: "x", KindName: "y"},
"y": {FromKind: "y", KindName: "x"},
}
for _, tc := range []struct {
desc string
kindMap map[string]config.MappedKind
in, want string
}{
{"single step reverse", std, "vitest_test", "jest_test"},
{"chained reverse to builtin", std, "outer", "ts_project"},
{"partial chain", std, "mid", "ts_project"},
{"self map is identity", std, "web_assets", "web_assets"},
{"unmapped builtin unchanged", std, "jest_test", "jest_test"},
{"completely unmapped", std, "js_library", "js_library"},
{"cycle terminates", cycle, "x", "y"},
} {
t.Run(tc.desc, func(t *testing.T) {
c := &config.Config{KindMap: tc.kindMap}
if got := unmapKind(c, tc.in); got != tc.want {
t.Errorf("unmapKind(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
6 changes: 3 additions & 3 deletions gazelle/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,15 @@ func (lang *JS) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.
}

// modules can be resolved via the directory containing them
if (isBarrel || jsConfig.CollectAll) && r.Kind() != getKind(c, "jest_test") {
if (isBarrel || jsConfig.CollectAll) && r.Kind() != "jest_test" {
importSpecs = append(importSpecs, resolve.ImportSpec{
Lang: lang.Name(),
Imp: f.Pkg,
Comment on lines 114 to 118

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gazelle sets the mapped kind on the rule in the file, but for resolution it wraps the language resolver in an inverseMapKindResolver that resets the rule's kind back to the builtin before calling Imports/Resolve. So in these methods r.Kind() is jest_test, not vitest_test, which is why the literal matches (and why the existing r.Kind() == "ts_project" checks work the same way).

})
}

// Any subfolders could be used to depend on this rule
folderImports := jsConfig.CollectAll && (r.Kind() == getKind(c, "ts_project") || r.Kind() == getKind(c, "js_library"))
folderImports := jsConfig.CollectAll && (r.Kind() == "ts_project" || r.Kind() == "js_library")
if folderImports {
base := filepath.Dir(f.Path)
subDirectories := make(map[string]bool)
Expand Down Expand Up @@ -256,7 +256,7 @@ func (lang *JS) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.Remote
}

// Add in additional jest dependencies
if r.Kind() == getKind(c, "jest_test") {
if r.Kind() == "jest_test" {
// All deps are also data for jest_test rules.
for name := range depSet {
dataSet[name] = true
Comment on lines 258 to 262

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment on the above for resolve.go

Expand Down
1 change: 1 addition & 0 deletions tests/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ go_binary(
"jest_mock",
"jsx_conversion",
"lookup_types",
"map_kind_test",
"module_self_import",
"react_example",
"simple_barrel",
Expand Down
5 changes: 5 additions & 0 deletions tests/map_kind_test/BUILD.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# gazelle:js_root
# gazelle:js_package_file package.json :node_modules
# gazelle:js_jest_config :jest.config

# gazelle:map_kind jest_test vitest_test //build:vitest_test.bzl
24 changes: 24 additions & 0 deletions tests/map_kind_test/BUILD.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
load("@aspect_rules_js//js:defs.bzl", "js_library")
load("//build:vitest_test.bzl", "vitest_test")

# gazelle:js_root
# gazelle:js_package_file package.json :node_modules
# gazelle:js_jest_config :jest.config

# gazelle:map_kind jest_test vitest_test //build:vitest_test.bzl

js_library(
name = "package_json",
srcs = ["package.json"],
)

vitest_test(
name = "foo.test",
srcs = ["foo.test.ts"],
config = "//:jest.config",
data = [
"//:node_modules/lodash",
"//:package_json",
],
deps = ["//:node_modules/lodash"],
)
Empty file added tests/map_kind_test/WORKSPACE
Empty file.
5 changes: 5 additions & 0 deletions tests/map_kind_test/foo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { isEqual } from 'lodash'

it('works', () => {
isEqual(1, 1)
})
8 changes: 8 additions & 0 deletions tests/map_kind_test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "map_kind_test",
"description": "A test case",
"version": "0.0.0",
"dependencies": {
"lodash": "^4.17.21"
}
}
6 changes: 6 additions & 0 deletions tests/map_kind_test/rerun/BUILD.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
load("//build:vitest_test.bzl", "vitest_test")

vitest_test(
name = "bar.test",
srcs = ["bar.test.ts"],
)
12 changes: 12 additions & 0 deletions tests/map_kind_test/rerun/BUILD.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
load("//build:vitest_test.bzl", "vitest_test")

vitest_test(
name = "bar.test",
srcs = ["bar.test.ts"],
config = "//:jest.config",
data = [
"//:node_modules/lodash",
"//:package_json",
],
deps = ["//:node_modules/lodash"],
)
5 changes: 5 additions & 0 deletions tests/map_kind_test/rerun/bar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { isEqual } from 'lodash'

it('still works after re-run', () => {
isEqual(2, 2)
})
Loading