// Generate Go workspace files for building a custom tsgolint binary. // Uses internal/runner.Run() from the fork — codegen is just a static // main.go template + go.work with shim replaces. // // Key: module names must be child paths of github.com/typescript-eslint/tsgolint // so Go allows importing internal/ packages across the module boundary. import fs from 'node:fs' import path from 'node:path' import type { RuleMetadata } from './discover.ts' // Shim modules that need replace directives in go.work. const SHIM_MODULES = [ 'ast', 'bundled', 'checker', 'compiler', 'core', 'lsp/lsproto', 'parser', 'project', 'scanner', 'tsoptions', 'tspath', 'vfs', 'vfs/cachedvfs', 'vfs/osvfs', ] as const const TSGOLINT_MODULE = 'github.com/typescript-eslint/tsgolint' function generateReplaceDirectives(tsgolintRelPath: string): string { return SHIM_MODULES.map((mod) => { return `\tgithub.com/microsoft/typescript-go/shim/${mod} => ${tsgolintRelPath}/shim/${mod}` }).join('\n') } /** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support. */ export function generateEditorGoFiles(lintcnDir: string): void { const goWork = `go 1.26 use ( \t. \t./.tsgolint \t./.tsgolint/typescript-go ) replace ( ${generateReplaceDirectives('./.tsgolint')} ) ` // Module name is a child path of tsgolint — this is required so Go allows // importing internal/ packages across the module boundary in a workspace. const goMod = `module ${TSGOLINT_MODULE}/lintcn-rules go 1.26 ` const gitignore = `.tsgolint/ go.work go.work.sum go.mod go.sum ` fs.writeFileSync(path.join(lintcnDir, 'go.work'), goWork) fs.writeFileSync(path.join(lintcnDir, 'go.mod'), goMod) const gitignorePath = path.join(lintcnDir, '.gitignore') if (!fs.existsSync(gitignorePath)) { fs.writeFileSync(gitignorePath, gitignore) } } /** Generate build workspace for compiling the custom binary. */ export function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }: { buildDir: string tsgolintDir: string lintcnDir: string rules: RuleMetadata[] }): void { fs.mkdirSync(path.join(buildDir, 'wrapper'), { recursive: true }) // symlink tsgolint source const tsgolintLink = path.join(buildDir, 'tsgolint') fs.rmSync(tsgolintLink, { recursive: true, force: true }) fs.symlinkSync(tsgolintDir, tsgolintLink) // symlink user rules const rulesLink = path.join(buildDir, 'rules') fs.rmSync(rulesLink, { recursive: true, force: true }) fs.symlinkSync(path.resolve(lintcnDir), rulesLink) // go.work const goWork = `go 1.26 use ( \t./tsgolint \t./tsgolint/typescript-go \t./wrapper \t./rules ) replace ( ${generateReplaceDirectives('./tsgolint')} ) ` fs.writeFileSync(path.join(buildDir, 'go.work'), goWork) // wrapper module — child path of tsgolint for internal/ access const wrapperGoMod = `module ${TSGOLINT_MODULE}/lintcn-wrapper go 1.26 ` fs.writeFileSync(path.join(buildDir, 'wrapper', 'go.mod'), wrapperGoMod) // wrapper/main.go — static template const mainGo = generateMainGo(rules) fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo) } /** Sanitize a package name into a valid Go identifier for use as an import alias. * Replaces hyphens/dots with underscores, prepends _ if starts with a digit. */ function toGoAlias(pkg: string): string { let alias = pkg.replace(/[^a-zA-Z0-9_]/g, '_') if (/^[0-9]/.test(alias)) { alias = '_' + alias } return alias } /** Generate main.go that imports user rules and calls internal/runner.Run(). * Each rule subfolder is its own Go package, imported by package name. */ function generateMainGo(rules: RuleMetadata[]): string { // Deduplicate imports by package name (in case a subfolder has multiple rules) const uniquePackages = [...new Set(rules.map((r) => { return r.packageName }))] const imports = uniquePackages.map((pkg) => { const alias = toGoAlias(pkg) return `\t${alias} "${TSGOLINT_MODULE}/lintcn-rules/${pkg}"` }).join('\n') const ruleEntries = rules.map((r) => { const alias = toGoAlias(r.packageName) return `\t\t${alias}.${r.varName},` }).join('\n') return `// Code generated by lintcn. DO NOT EDIT. package main import ( \t"os" \t"${TSGOLINT_MODULE}/internal/rule" \t"${TSGOLINT_MODULE}/internal/runner" ${imports} ) func main() { \trules := []rule.Rule{ ${ruleEntries} \t} \tos.Exit(runner.Run(rules, os.Args[1:])) } ` }