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
27 changes: 27 additions & 0 deletions language/js/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ const (
Directive_TestFiles = "js_test_files"
// The directive controlling whether asset collection is enabled for import types.
Directive_Assets = "js_assets"
// Directive_RuleKind overrides the generated rule kind for a specific target group.
// Syntax: # gazelle:js_rule_kind <group_name> [kind_name]
Directive_RuleKind = "js_rule_kind"

// TODO(deprecated): remove - replaced with js_files [group]
Directive_CustomTargetFiles = "js_custom_files"
Expand Down Expand Up @@ -111,6 +114,10 @@ type TargetGroup struct {

// If the targets are always testonly
testonly bool

// Per-group rule kind override. Empty string means use the default
// (ts_project or js_library based on source content).
ruleKind string
}

var DefaultSourceGlobs = []*TargetGroup{
Expand Down Expand Up @@ -235,6 +242,7 @@ func (g *TargetGroup) newChild() *TargetGroup {
defaultSources: sources,
testonly: g.testonly,
visibility: g.visibility,
ruleKind: g.ruleKind,
}
}

Expand Down Expand Up @@ -287,6 +295,25 @@ func (c *JsGazelleConfig) SetVisibility(groupName string, visLabels []string) er
return nil
}

func (c *JsGazelleConfig) SetRuleKind(groupName, kindName string) error {
target := c.GetSourceTarget(groupName)
if target == nil {
return fmt.Errorf("Target group %q not found in %q", groupName, c.rel)
}

if kindName != "" {
switch kindName {
case TsProjectKind, JsLibraryKind, JsTestKind:
// valid source target kinds
default:
return fmt.Errorf("unknown rule kind %q; must be one of: ts_project, js_library, js_test. Use map_kind to remap to a custom rule", kindName)
}
}

target.ruleKind = kindName
return nil
}

// SetGenerationEnabled sets whether the extension is enabled or not.
func (c *JsGazelleConfig) SetGenerationEnabled(enabled bool) {
c.generationEnabled = enabled
Expand Down
22 changes: 22 additions & 0 deletions language/js/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func (ts *typeScriptLang) KnownDirectives() []string {
Directive_LibraryFiles,
Directive_TestFiles,
Directive_Assets,
Directive_RuleKind,

// TODO(deprecated): remove
Directive_CustomTargetFiles,
Expand Down Expand Up @@ -257,6 +258,27 @@ func (ts *typeScriptLang) readDirectives(c *config.Config, rel string, f *rule.F
// Overwrite any inherited configuration for asset collection.
config.SetCollectAssetsFrom(assetKinds...)

case Directive_RuleKind:
parts := strings.Fields(value)
if len(parts) == 1 {
// Reset to default (auto-detect from source content)
err := config.SetRuleKind(parts[0], "")
if err != nil {
Comment thread
sallustfire marked this conversation as resolved.
common.MisconfiguredErrorf(c, "Invalid %s: %v", Directive_RuleKind, err)
return
}
} else if len(parts) == 2 {
err := config.SetRuleKind(parts[0], parts[1])
if err != nil {
common.MisconfiguredErrorf(c, "Invalid %s: %v", Directive_RuleKind, err)
return
}
} else {
common.MisconfiguredErrorf(c, "invalid value for directive %q: %s: expected 'group_name [kind_name]'",
Directive_RuleKind, d.Value)
return
}

// TODO: remove, deprecated
case Directive_CustomTargetFiles:
fmt.Fprintf(os.Stderr, "DEPRECATED: %s is deprecated, use %s\n", Directive_CustomTargetFiles, Directive_LibraryFiles)
Expand Down
17 changes: 13 additions & 4 deletions language/js/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,11 @@ func hasTranspiledSources(sourceFiles *treeset.Set[string]) bool {

func (ts *typeScriptLang) addProjectRule(cfg *JsGazelleConfig, tsconfigRel string, tsconfig *typescript.TsConfig, args language.GenerateArgs, group *TargetGroup, targetName string, sourceFiles, genFiles, dataFiles []string, result *language.GenerateResult) (*rule.Rule, error) {
// Check for name-collisions with the rule being generated.
colError := ruleUtils.CheckCollisionErrors(targetName, TsProjectKind, sourceRuleKinds, args)
expectedKind := TsProjectKind
if group.ruleKind != "" {
expectedKind = group.ruleKind
}
colError := ruleUtils.CheckCollisionErrors(targetName, expectedKind, sourceRuleKinds, args)
if colError != nil {
Comment on lines 485 to 492
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

expectedKind used for collision checks is set to ts_project unless a js_rule_kind override is present, but the actual generated kind may still become js_library when !hasTranspiledSources(info.sources). This can produce misleading collision error messages (reporting ts_project when the rule would be js_library). Consider computing expectedKind from the same logic as defaultKind (and then overriding with group.ruleKind), so collisions report the kind that will actually be generated.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is pre-existing behavior. The original code also hardcoded TsProjectKind here. The expectedKind is only used in the error message, not the collision logic itself. Fixing it to account for js_library (which requires inspecting source file extensions) is a separate concern and scope creep. Happy to entertain it in a follow-up if desired.

return nil, fmt.Errorf("%v "+
"Use the '# aspect:%s' directive to change the naming convention.\n\n"+
Expand Down Expand Up @@ -569,9 +573,14 @@ func (ts *typeScriptLang) addProjectRule(cfg *JsGazelleConfig, tsconfigRel strin
// A rule of the same name might already exist
existing := ruleUtils.GetFileRuleByName(args, targetName)

ruleKind := TsProjectKind
defaultKind := TsProjectKind
if !hasTranspiledSources(info.sources) {
ruleKind = JsLibraryKind
defaultKind = JsLibraryKind
}

ruleKind := defaultKind
if group.ruleKind != "" {
ruleKind = group.ruleKind
}
sourceRule := rule.NewRule(ruleKind, targetName)

Expand Down Expand Up @@ -604,7 +613,7 @@ func (ts *typeScriptLang) addProjectRule(cfg *JsGazelleConfig, tsconfigRel strin
sourceRule.DelAttr("assets")
}

if group.testonly {
if group.testonly && ruleKind != JsTestKind {
sourceRule.SetAttr("testonly", true)
}

Expand Down
18 changes: 16 additions & 2 deletions language/js/kinds.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
TsProjectKind = "ts_project"
TsProtoLibraryKind = "ts_proto_library"
JsLibraryKind = "js_library"
JsTestKind = "js_test"
JsBinaryKind = "js_binary"
JsRunBinaryKind = "js_run_binary"
TsConfigKind = "ts_config"
Expand All @@ -23,7 +24,7 @@ const (
NpmRepositoryName = "npm"
)

var sourceRuleKinds = treeset.NewWith(strings.Compare, TsProjectKind, JsLibraryKind, TsProtoLibraryKind)
var sourceRuleKinds = treeset.NewWith(strings.Compare, TsProjectKind, JsLibraryKind, JsTestKind, TsProtoLibraryKind)

// Kinds returns a map that maps rule names (kinds) and information on how to
// match and merge attributes that may be found in rules of those kinds.
Expand Down Expand Up @@ -78,6 +79,19 @@ var tsKinds = map[string]rule.KindInfo{
"deps": true,
},
},
JsTestKind: {
MatchAny: false,
NonEmptyAttrs: map[string]bool{
"srcs": true,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Isn't this incorrect?

js_test is just js_binary which has data and entry_point but no srcs or deps...?

},
SubstituteAttrs: map[string]bool{},
MergeableAttrs: map[string]bool{
"srcs": true,
},
ResolveAttrs: map[string]bool{
"deps": true,
},
},
JsBinaryKind: {
MatchAny: false,
NonEmptyAttrs: map[string]bool{
Expand Down Expand Up @@ -183,7 +197,7 @@ func (h *typeScriptLang) ApparentLoads(moduleToApparentName func(string) string)
{
Name: "@" + jsModName + "//js:defs.bzl",
Symbols: []string{
JsLibraryKind, JsBinaryKind, JsRunBinaryKind,
JsLibraryKind, JsTestKind, JsBinaryKind, JsRunBinaryKind,
},
},

Expand Down
6 changes: 2 additions & 4 deletions language/js/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ func (ts *typeScriptLang) Imports(c *config.Config, r *rule.Rule, f *rule.File)
return ts.protoLibraryImports(r, f)
case TsConfigKind:
return ts.tsconfigImports(r, f)
case TsProjectKind:
fallthrough
case JsLibraryKind:
case TsProjectKind, JsLibraryKind, JsTestKind:
return ts.sourceFileImports(c, r, f)
}
return nil
Expand Down Expand Up @@ -296,7 +294,7 @@ func (ts *typeScriptLang) Resolve(

// TsProject imports are resolved as deps
switch r.Kind() {
case TsProjectKind, JsLibraryKind, TsConfigKind, TsProtoLibraryKind:
case TsProjectKind, JsLibraryKind, JsTestKind, TsConfigKind, TsProtoLibraryKind:
deps := common.NewLabelSet(from)

// Support this target representing a project or a package
Expand Down
3 changes: 3 additions & 0 deletions language/js/tests/rule_kind_custom_group/BUILD.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# gazelle:js_test_files e2e *.e2e.ts
# gazelle:js_rule_kind e2e js_test
# gazelle:map_kind js_test jest_test //:defs.bzl
24 changes: 24 additions & 0 deletions language/js/tests/rule_kind_custom_group/BUILD.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("//:defs.bzl", "jest_test")

# gazelle:js_test_files e2e *.e2e.ts
# gazelle:js_rule_kind e2e js_test
# gazelle:map_kind js_test jest_test //:defs.bzl

ts_project(
name = "rule_kind_custom_group",
srcs = ["main.ts"],
)

ts_project(
name = "rule_kind_custom_group_tests",
testonly = True,
srcs = ["main.spec.ts"],
deps = [":rule_kind_custom_group"],
)

jest_test(
name = "e2e",
srcs = ["main.e2e.ts"],
deps = [":rule_kind_custom_group"],
)
2 changes: 2 additions & 0 deletions language/js/tests/rule_kind_custom_group/WORKSPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This is a Bazel workspace for the Gazelle test data.
workspace(name = "rule_kind_custom_group")
3 changes: 3 additions & 0 deletions language/js/tests/rule_kind_custom_group/main.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { A } from './main';

console.log('e2e', A);
3 changes: 3 additions & 0 deletions language/js/tests/rule_kind_custom_group/main.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { A } from './main';

console.log(A);
1 change: 1 addition & 0 deletions language/js/tests/rule_kind_custom_group/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const A = 'hello';
1 change: 1 addition & 0 deletions language/js/tests/rule_kind_override/BUILD.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# gazelle:js_rule_kind {dirname}_tests js_test
15 changes: 15 additions & 0 deletions language/js/tests/rule_kind_override/BUILD.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
load("@aspect_rules_js//js:defs.bzl", "js_test")
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")

# gazelle:js_rule_kind {dirname}_tests js_test

ts_project(
name = "rule_kind_override",
srcs = ["main.ts"],
)

js_test(
name = "rule_kind_override_tests",
srcs = ["main.spec.ts"],
deps = [":rule_kind_override"],
)
2 changes: 2 additions & 0 deletions language/js/tests/rule_kind_override/WORKSPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This is a Bazel workspace for the Gazelle test data.
workspace(name = "rule_kind_override")
3 changes: 3 additions & 0 deletions language/js/tests/rule_kind_override/main.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { A } from './main';

console.log(A);
1 change: 1 addition & 0 deletions language/js/tests/rule_kind_override/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const A = 'hello';
Empty file.
13 changes: 13 additions & 0 deletions language/js/tests/rule_kind_override/sub/BUILD.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
load("@aspect_rules_js//js:defs.bzl", "js_test")
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")

ts_project(
name = "sub",
srcs = ["lib.ts"],
)

js_test(
name = "sub_tests",
srcs = ["lib.spec.ts"],
deps = [":sub"],
)
3 changes: 3 additions & 0 deletions language/js/tests/rule_kind_override/sub/lib.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { B } from './lib';

console.log(B);
1 change: 1 addition & 0 deletions language/js/tests/rule_kind_override/sub/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const B = 'world';