#!/usr/bin/env dotnet #:property Nullable=enable #:property PublishAOT=false #:package ArcaneLibs@1.0.1-preview.20260616-220331 using System.Text.Json.Serialization; using System.Text.RegularExpressions; using ArcaneLibs; using ArcaneLibs.Attributes; using ArcaneLibs.Extensions; var logJson = false; var importErrors = new Dictionary>(); void LogError(string fileName, ImportReference import) { importErrors.GetOrCreate(fileName).Add(import); Console.Write( $"\n{ConsoleUtils.ColoredString("!", 255, 49, 49)} {ConsoleUtils.ColoredString(fileName + ":" + import.Line + ":" + import.Position, 49 + 128, 49 + 128, 255)}: {(logJson ? import.ToJson(indent: false) : import.ToColorizedString())}"); } foreach (var f in ReadDirRecursive("./src")) { if (f.EndsWith(".test.ts")) continue; //ignore for now else await foreach (var import in GetImports(f)) { // These files are completely idempotent and safe to import if (import.ImportSource.EndsWith("util/util/ProcessLifeCycle") && import.ImportEntities.SequenceEqual(["ProcessLifecycle"])) continue; // Technically valid but what the heck if (import.ImportSource.StartsWith('#')) LogError(f, import); // file level else if (new FileInfo(f).Name == "start.ts") { if (import is { ImportSource: "module-alias" or "dotenv" or "./Server" // good imports or "node:cluster" or "node:os" or "node:fs" // do we want to keep these around? }) continue; LogError(f, import); } else if (f != "./src/bundle/Server.ts" && import is { ImportSource: "@spacebar/api" or "@spacebar/cdn" // or "@spacebar/gateway" // or "@spacebar/webrtc" }) LogError(f, import); else if (f == "./src/schemas/Validator.ts") { if (import is not { ImportSource: "ajv" or "ajv-formats" or "node:fs" or "node:path" }) LogError(f, import); } else if (f == "./src/database/Database.ts") { if (import is not { ImportSource: "picocolors" or "dotenv" or "typeorm" or "node:fs" or "node:path" }) LogError(f, import); } //directory level else if (f.StartsWith("./src/extensions")) { if (import.ImportSourceType == ImportSourceTypeValue.LocalPath) continue; LogError(f, import); } else if (f.StartsWith("./src/database")) { if (import.ImportSource == "typeorm" || import.ImportSourceType == ImportSourceTypeValue.LocalPath || import.ImportSource == "node:crypto" || (import.ImportSource == "schemas" && import.IsTypeImport) || (import.ImportSource == "../Database") // maybe? ) continue; LogError(f, import); } else if (f.StartsWith("./src/schemas")) { LogError(f, import); } else if (f.StartsWith("./src/api")) { if (import.ImportSource == "express" && ((string[])["Request", "Response", "Router"]).ContainsAll(import.ImportEntities)) continue; LogError(f, import); } } if (importErrors.ContainsKey(f)) Console.Write("\n\n"); } if (importErrors.Any()) Console.WriteLine($"\rFinished with {importErrors.Sum(x => x.Value.Count)} warnings...\e[K"); Console.WriteLine(); // await foreach (var import in GetImports("./src/apply-migrations.ts")) // { // Console.WriteLine(import.ToJson(indent: false)); // } IEnumerable ReadDirRecursive(string path) { foreach (var f in Directory.GetFiles(path).OrderBy(x => x.TrimEnd(".ts").ToString())) yield return f; foreach (var d in Directory.GetDirectories(path).Order()) foreach (var f in ReadDirRecursive(d)) yield return f; } async IAsyncEnumerable GetImports(string path) { var basicImportRegex = new Regex( @"^import (?type )?{?(?[a-zA-Z ,\n]+)}? from ""(?.*)"";", RegexOptions.Multiline ); var basicRequireRegex = new Regex( @"require\(""(?.*)""\)(\.(?\w+))?", RegexOptions.Multiline ); // Console.WriteLine(basicImportRegex); Console.Write($"\rReading imports for {path}: \e[K"); var fileContents = await File.ReadAllTextAsync(path); Console.Write($"{fileContents.Length} chars"); // if (basicImportRegex.IsMatch(fileContents)) Console.WriteLine("Match!"); // else Console.WriteLine(fileContents + "\n^ Did not match regex " + basicImportRegex); ImportSourceTypeValue ClassifyImportSourceType(string p) { if (p.StartsWith("node:")) return ImportSourceTypeValue.Node; if (p.StartsWith("@types/")) return ImportSourceTypeValue.Types; if (p.StartsWith("@spacebar/")) return ImportSourceTypeValue.SpacebarAlias; if (p.StartsWith("lambert-server")) return ImportSourceTypeValue.VendoredModule; if (p.StartsWith("../")) return ImportSourceTypeValue.Path; if (p.StartsWith("./")) return ImportSourceTypeValue.LocalPath; if (p.StartsWith('#')) return ImportSourceTypeValue.Hashtag; return ImportSourceTypeValue.Npm; } ImportReference GetRef(Match m, bool isRequire) => new() { IsRequire = isRequire, IsTypeImport = m.Groups.ContainsKey("typeSpecifier") && m.Groups["typeSpecifier"].Success, ImportEntities = isRequire ? [m.Groups["entities"].Value.ReplaceLineEndings("")] : m.Groups["entities"].Value.ReplaceLineEndings("").Replace(" ", "").Split(","), ImportSource = m.Groups["source"].Value, ImportSourceType = ClassifyImportSourceType(m.Groups["source"].Value), Line = fileContents[..(m.Index)].CountInstances("\n"), Position = m.Index - fileContents[..(m.Index)].LastIndexOf('\n') }; foreach (Match m in basicImportRegex.Matches(fileContents)) yield return GetRef(m, false); foreach (Match m in basicRequireRegex.Matches(fileContents)) yield return GetRef(m, true); } struct ImportReference { public int Line { get; set; } public int Position { get; set; } public bool IsTypeImport { get; set; } public bool IsRequire { get; set; } public string[] ImportEntities { get; set; } public string ImportSource { get; set; } public ImportSourceTypeValue ImportSourceType { get; set; } public override string ToString() { var sourceType = ImportSourceType; return $"{ImportSourceType.GetType().GetMember((sourceType.ToString())).First().GetFriendlyName()} {(IsTypeImport ? "type " : "")}{(IsRequire ? "require" : "import")} from {ImportSource}, obtaining {string.Join(", ", ImportEntities)}"; } public string ToColorizedString() { var sourceType = ImportSourceType.GetType().GetMember((ImportSourceType.ToString())).First(); var sourceTypeClr = sourceType.GetColorOrNull(); return $"{ConsoleUtils.ColoredString(sourceType.GetFriendlyName(), sourceTypeClr?.R ?? 255, sourceTypeClr?.G ?? 255, sourceTypeClr?.B ?? 255)} " + $"{(IsTypeImport ? ConsoleUtils.ColoredString("type ", 255, 127, 0) : "")}{(IsRequire ? ConsoleUtils.ColoredString("require", 255, 255, 0) : ConsoleUtils.ColoredString("import", 0, 255, 0))} " + $"from {ConsoleUtils.ColoredString(ImportSource, sourceTypeClr?.R ?? 255, sourceTypeClr?.G ?? 255, sourceTypeClr?.B ?? 255)}, " + $"obtaining {(string.Join(", ", ImportEntities.Select(x => ConsoleUtils.ColoredString(x, 150, 150, 150))))}"; } } enum ImportType { Unknown, Import, ImportType, Require } [JsonConverter(typeof(JsonStringEnumConverter))] enum ImportSourceTypeValue { Unknown, [FriendlyName(Name = "NPM")] [Color(203, 56, 55)] Npm, [FriendlyName(Name = "NodeJS")] [Color(60, 135, 58)] Node, [FriendlyName(Name = "Types")] [Color(49, 120, 198)] Types, [FriendlyName(Name = "Spacebar internals")] [Color(11, 133, 255)] SpacebarAlias, [FriendlyName(Name = "Vendored module")] [Color(127, 255, 0)] VendoredModule, [FriendlyName(Name = "File path")] [Color(0, 255, 0)] Path, [Color(255, 255, 0)] [FriendlyName(Name = "Local file path")] LocalPath, [FriendlyName(Name = "Hashtag import")] [Color(255, 82, 82)] Hashtag }