Skip to content
Draft
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 .plzconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ GoTool = //third_party/go:toolchain|go
ModFile = //:mod
RequireLicences = true
Stdlib = //third_party/go:std
CgoEnabled = true

[Plugin "cc"]
target = //plugins:cc
defaultoptcppflags = --std=c++11 -O2 -DNDEBUG -Wall -Wextra -Werror -Wno-unused-parameter
defaultdbgcppflags = --std=c++11 -g3 -DDEBUG -Wall -Wextra -Werror -Wno-unused-parameter

[Alias "puku"]
Cmd = run //cmd/puku --
Expand Down
6 changes: 5 additions & 1 deletion config/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ go_library(
"//sync/integration/syncmod:all",
"//work:all",
],
deps = ["//kinds"],
deps = [
"///third_party/go/github.com_muhammadmuzzammil1998_jsonc//:jsonc",
"//kinds",
"//logging",
],
)

go_test(
Expand Down
54 changes: 54 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import (
"path/filepath"
"strings"

"github.com/muhammadmuzzammil1998/jsonc"
"github.com/please-build/puku/kinds"
"github.com/please-build/puku/logging"
)

var log = logging.GetLogger()

// KindConfig represents the configuration for a custom kind. See kinds.Kind for more information on how kinds work.
type KindConfig struct {
// NonGoSources indicates that this rule operates on non-go sources and we shouldn't attempt to parse them to
Expand Down Expand Up @@ -209,3 +213,53 @@ func (c *Config) GetKind(kind string) *kinds.Kind {
}
return nil
}

// TSConfig represents a tsconfig.json file discovered in the repo.
type TSConfig struct {
Dir string
CompilerOptions struct {
Paths map[string][]string `json:"paths"`
} `json:"compilerOptions"`
}

var tsconfigs = map[string]*TSConfig{}

// ReadTSConfig finds the closest tsconfig by walking up the directory tree.
// Note: we don't try and resolve the full config inheritance, it just resolves
// to the first tsconfig.json file that we find.
func ReadTSConfig(dir string) (*TSConfig, error) {
origDir := dir
dir = filepath.Clean(dir)

for true {
if c, ok := tsconfigs[dir]; ok {
return c, nil
}

f, err := os.ReadFile(filepath.Join(dir, "tsconfig.json"))
if err != nil {
if os.IsNotExist(err) {
if dir == "." {
break
}

// try parent directory
dir = filepath.Dir(dir)
continue
}
return nil, err
}

c := new(TSConfig)
c.Dir = dir
// tsconfig files are jsonc: https://code.visualstudio.com/docs/languages/json#_json-with-comments
if err := jsonc.Unmarshal(f, c); err != nil {
return nil, fmt.Errorf("in %s: %w", dir, err)
}
tsconfigs[dir] = c
return c, nil
}

log.Debugf("Can't find tsconfig for dir: %s", origDir)
return nil, nil
}
7 changes: 5 additions & 2 deletions generate/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ go_library(
deps = [
"///third_party/go/github.com_please-build_buildtools//build",
"///third_party/go/github.com_please-build_buildtools//labels",
"///third_party/go/github.com_smacker_go-tree-sitter//:go-tree-sitter",
"///third_party/go/github.com_smacker_go-tree-sitter//typescript/tsx",
"///third_party/go/github.com_smacker_go-tree-sitter//typescript/typescript",
"//config",
"//edit",
"//eval",
Expand All @@ -26,10 +29,10 @@ go_library(
"//knownimports",
"//licences",
"//logging",
"//options",
"//please",
"//proxy",
"//trie",
"//options",
],
)

Expand All @@ -44,9 +47,9 @@ testify_test(
"//config",
"//edit",
"//kinds",
"//options",
"//please",
"//proxy",
"//trie",
"//options",
],
)
198 changes: 198 additions & 0 deletions generate/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/please-build/buildtools/build"
Expand Down Expand Up @@ -101,6 +102,127 @@ func (u *updater) reallyResolveImport(conf *config.Config, i string) (string, er
return "", fmt.Errorf("module not found")
}

// resolveTSImport resolves an import path to a build target. It will return an
// empty string if the import is for a third party package. Otherwise, it will
// return the build target for that dependency, or an error if it can't be resolved.
func (u *updater) resolveTSImport(conf *config.Config, tsConfig *config.TSConfig, f *SourceFile, importPath string, currentRule *edit.Rule) (string, error) {
var t string
var err error

var importPaths []string
type tsAlias struct {
alias string
targets []string
}
var matchedAlias *tsAlias

tsPaths := make(map[string][]string)
if tsConfig != nil {
tsPaths = tsConfig.CompilerOptions.Paths
}

for alias, targets := range tsPaths {
re := regexp.MustCompile(wildCardToRegexp(alias))
matched := re.FindString(importPath)
if err != nil {
return t, err
}

if matched != "" {
matchedAlias = &tsAlias{
alias: alias,
targets: targets,
}
}
}

// Ignore all imports that aren't relative
if !strings.HasPrefix(importPath, ".") && matchedAlias == nil {
log.Debugf("Skipping TS import (not relative): %s", importPath)
return t, nil
}

if matchedAlias != nil {
// TODO: at the moment we only support the first target and we only support
// aliases that are simple prefix replacements
target := matchedAlias.targets[0]
alias := matchedAlias.alias
origImportPath := importPath

aliasPrefix := alias[0:strings.Index(alias, "*")]
targetPrefix := target[0:strings.Index(target, "*")]

importPath = strings.Replace(importPath, aliasPrefix, targetPrefix, 1)
importPath = filepath.Join(tsConfig.Dir, importPath)
log.Debugf("alias matched %s %s; newImportPath: %s", alias, origImportPath, importPath)
}

// If importPath is a folder then append `index.{ts,tsx}`
if filepath.Ext(importPath) == "" {
// filepath.Join removes the './' at the beginning of the import so we can't
// use it
importPaths = append(importPaths, importPath+".ts")
importPaths = append(importPaths, importPath+".tsx")
importPaths = append(importPaths, importPath+string(filepath.Separator)+"index.ts")
importPaths = append(importPaths, importPath+string(filepath.Separator)+"index.tsx")
log.Debugf("adding file extensions and index paths: %s", importPaths)
} else {
importPaths = append(importPaths, importPath)
}

// Try every possible import path
for _, path := range importPaths {
fullPath := path
if strings.HasPrefix(path, ".") {
fullPath = filepath.Join(f.Dir(), path)
}
log.Debugf("fullPath: %s", fullPath)

if t, ok := u.resolvedImports[fullPath]; ok {
return t, nil
}

// TODO
if t := conf.GetKnownTarget(fullPath); t != "" {
return t, nil
}

// Check to see if the target exists in the current repo
t, err = u.localTSDep(fullPath, currentRule)
if err != nil {
return "", err
}

if t != "" {
u.resolvedImports[fullPath] = t
return t, nil
}
}

return "", nil
}

func wildCardToRegexp(pattern string) string {
components := strings.Split(pattern, "*")
if len(components) == 1 {
// if len is 1, there are no *'s, return exact match pattern
return "^" + pattern + "$"
}
var result strings.Builder
for i, literal := range components {

// Replace * with .*
if i > 0 {
result.WriteString("(.*)")
}

// Quote any regular expression meta characters in the
// literal text.
result.WriteString(regexp.QuoteMeta(literal))
}
return "^" + result.String() + "$"
}

// isInScope returns true when the given path is in scope of the current run i.e. if we are going to format the BUILD
// file there.
func (u *updater) isInScope(path string) bool {
Expand Down Expand Up @@ -170,6 +292,82 @@ func (u *updater) localDep(importPath string) (string, error) {
return "", nil
}

// localTSDep finds a dependency local to this repository, checking the BUILD
// file for a js_library target. Returns an empty string when no target is found.
func (u *updater) localTSDep(importPath string, currentRule *edit.Rule) (string, error) {
path := filepath.Dir(importPath)
// Check the directory exists. If it doesn't it's not a local import.
if _, err := os.Lstat(path); os.IsNotExist(err) {
log.Debugf("dir doesn't exist %s", path)
return "", nil
}
file, err := u.graph.LoadFile(path)
if err != nil {
return "", fmt.Errorf("failed to parse BUILD files in %v: %v", path, err)
}

conf, err := config.ReadConfig(path)
if err != nil {
return "", err
}

// TODO allow other rule names?
for _, rule := range file.Rules("js_library") {
kind := conf.GetKind(rule.Kind())
if kind == nil {
continue
}

// Skip rules that are the same as the current rule to prevent circular
// imports
if currentRule.Dir == path && rule.Name() == currentRule.Name() {
continue
}

if kind.Type == kinds.Lib {
ruleSrcs, err := u.eval.EvalGlobs(path, rule, kind.SrcsAttr)
if err != nil {
return "", err
}

// TODO if rule is the same as the current rule then skip

// Check if import file matches any of the srcs
for _, src := range ruleSrcs {
fileName := filepath.Base(importPath)
// Files don't have to have an extension. If they don't then they could
// map with .ts or .tsx files.
if src == fileName || src == fileName+".ts" || src == fileName+".tsx" {
log.Debugf("found rule for import %s: %s:%s", importPath, path, rule.Name())
return edit.BuildTarget(rule.Name(), path, ""), nil
}
}
}
}

// if !u.isInScope(importPath) {
// return "", fmt.Errorf("resolved %v to a local package, but no library target was found and it's not in scope to generate the target", importPath)
// }

// files, err := ImportDir(path)
// if err != nil {
// if os.IsNotExist(err) {
// return "", nil
// }
// return "", fmt.Errorf("failed to import %v: %v", path, err)
// }

// If there are any non-test sources, then we will generate a js_library here later on. Return that target name.
// for _, f := range files {
// if !f.IsTest() {
// return BuildTarget(filepath.Base(importPath), path, ""), nil
// }
// }

log.Debugf("failed to find rule for import: %s", importPath)
return "", nil
}

func depTarget(modules []string, importPath, thirdPartyFolder string) string {
module := moduleForPackage(modules, importPath)
if module == "" {
Expand Down
Loading