package bundler // esbuild silently resolves a `.js` import to a sibling `.ts`/`.tsx`/`.jsx`, // which lets misnamed specifiers slip through. This check catches them up // front so every import path names the file that actually exists on disk. import ( "fmt" "os" "path/filepath" "regexp" "strings" ) type importViolation struct { file string line int spec string // the written specifier, e.g. "./Icons.js" actual string // the corrected specifier, e.g. "./Icons.ts" } // reImportSpec matches a quoted relative module specifier ending in ".js". var reImportSpec = regexp.MustCompile(`(["'])((?:\.\.?/)[^"']*?\.js)["']`) // tsExtensions are the non-.js source extensions a .js specifier may // actually resolve to, in esbuild's resolution order. var tsExtensions = []string{".ts", ".tsx", ".jsx"} // checkImportExtensions scans every JS/TS source under frontend/src for // relative import specifiers written with a ".js" extension whose literal // target does not exist but a sibling ".ts"/".tsx"/".jsx" does. esbuild // resolves these transparently, so they bundle fine — but the import path // lies about the file it points to. Specifiers that resolve to a real ".js" // file, and dangling specifiers with no sibling at all, are left for esbuild. func checkImportExtensions() []importViolation { var violations []importViolation srcRoot := filepath.Join(frontendDir, "src") filepath.WalkDir(srcRoot, func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() { return err } switch filepath.Ext(path) { case ".js", ".ts", ".tsx", ".jsx": default: return nil } raw, err := os.ReadFile(path) if err != nil { return err } dir := filepath.Dir(path) for i, line := range strings.Split(string(raw), "\n") { for _, m := range reImportSpec.FindAllStringSubmatch(line, -1) { spec := m[2] if _, statErr := os.Stat(filepath.Join(dir, spec)); statErr == nil { continue // resolves to a real .js file — fine } base := strings.TrimSuffix(spec, ".js") for _, ext := range tsExtensions { if _, statErr := os.Stat(filepath.Join(dir, base+ext)); statErr == nil { violations = append(violations, importViolation{ file: path, line: i + 1, spec: spec, actual: base + ext, }) break } } } } return nil }) return violations } func reportImportViolations(violations []importViolation) { fmt.Fprintf(os.Stderr, "\nImport extension check failed: %d import(s) use a .js extension for a non-.js file.\n", len(violations)) for _, v := range violations { fmt.Fprintf(os.Stderr, " %s:%d: %q should be %q\n", v.file, v.line, v.spec, v.actual) } fmt.Fprintln(os.Stderr, "Rename each specifier to match the file's real extension.") }