package main import ( "go/parser" "go/token" "os" "path/filepath" "regexp" "strconv" "strings" ) const ( blockCoreImportPrefix = "git.dev.alexdunmow.com/block/core" pluginSDKImportPrefix = "git.dev.alexdunmow.com/block/pluginsdk" blockNinjaImportPrefix = "git.dev.alexdunmow.com/block/cms" orchestratorImportPrefix = "git.dev.alexdunmow.com/block/orchestrator" firstPartyImportPrefix = "git.dev.alexdunmow.com/block/" ) func hasImportPrefix(importPath, prefix string) bool { return importPath == prefix || strings.HasPrefix(importPath, prefix+"/") } // isForbiddenPluginImport reports whether a standalone plugin may not import // the package: ALL BlockNinja first-party Go code (cms, orchestrator, core) is // off-limits — block/pluginsdk is the one plugin-facing module. (The former // core/captcha carve-out ended when captcha moved to the host-stamped // X-Bn-Verified-Captcha trusted header, pluginsdk v0.2.2.) func isForbiddenPluginImport(importPath string) bool { return hasImportPrefix(importPath, blockNinjaImportPrefix) || hasImportPrefix(importPath, orchestratorImportPrefix) || hasImportPrefix(importPath, blockCoreImportPrefix) } type pluginImportViolation struct { file string line int importPath string } var templImportPattern = regexp.MustCompile(`"([^"]+)"`) func shouldCheckStandalonePluginImports(root string) bool { return isPluginModuleRoot(root) && !isBundledPluginRoot(root) } func isBundledPluginRoot(root string) bool { bundledPluginsDir := filepath.Join(blockNinjaRepoRoot(), "backend", "internal", "plugins") absRoot, err := filepath.Abs(root) if err != nil { return false } absBundledPluginsDir, err := filepath.Abs(bundledPluginsDir) if err != nil { return false } rel, err := filepath.Rel(absBundledPluginsDir, absRoot) if err != nil { return false } if rel == "." { return false } return !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != ".." } func checkStandalonePluginImports(root string) []pluginImportViolation { var violations []pluginImportViolation _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } if info.IsDir() { switch info.Name() { case ".git", ".worktrees", "vendor", "node_modules": return filepath.SkipDir } return nil } relPath, _ := filepath.Rel(root, path) if strings.HasSuffix(path, ".templ") { data, readErr := os.ReadFile(path) if readErr != nil { return nil } lines := strings.Split(string(data), "\n") for i, line := range lines { if !strings.Contains(line, firstPartyImportPrefix) { continue } matches := templImportPattern.FindAllStringSubmatch(line, -1) for _, match := range matches { importPath := match[1] if !isForbiddenPluginImport(importPath) { continue } violations = append(violations, pluginImportViolation{ file: relPath, line: i + 1, importPath: importPath, }) } } return nil } if !strings.HasSuffix(path, ".go") { return nil } fset := token.NewFileSet() file, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) if err != nil { return nil } for _, spec := range file.Imports { importPath, err := strconv.Unquote(spec.Path.Value) if err != nil { continue } if !isForbiddenPluginImport(importPath) { continue } pos := fset.Position(spec.Pos()) violations = append(violations, pluginImportViolation{ file: relPath, line: pos.Line, importPath: importPath, }) } return nil }) return violations }