@@ -6,6 +6,8 @@ import { TSDocConfiguration, TSDocParser, TextRange } from "@microsoft/tsdoc";
6
6
import * as tsdoc from "@microsoft/tsdoc" ;
7
7
import type { TSDocConfigFile } from "@microsoft/tsdoc-config" ;
8
8
import * as ts from "typescript" ;
9
+ import * as fs from "fs" ;
10
+ import * as path from "path" ;
9
11
10
12
// import { Debug } from "./Debug";
11
13
import { ConfigCache } from "./ConfigCache" ;
@@ -141,6 +143,68 @@ function walkCompilerAstAndFindComments(node: ts.Node, indent: string, notFoundC
141
143
return node . forEachChild ( ( child ) => walkCompilerAstAndFindComments ( child , indent + " " , notFoundComments , sourceText , getterSetterFound ) ) ;
142
144
}
143
145
146
+ type TsConfig = {
147
+ compilerOptions : {
148
+ baseUrl : string ;
149
+ paths : Record < string , string [ ] > ;
150
+ } ;
151
+ } ;
152
+
153
+ let tsConfig : TsConfig | null = null ;
154
+ function loadTsConfig ( projectRoot : string ) : TsConfig | null {
155
+ if ( tsConfig ) {
156
+ return tsConfig ;
157
+ }
158
+
159
+ try {
160
+ const tsconfigPath = path . join ( projectRoot , "tsconfig.json" ) ;
161
+ const tsconfigContent = fs . readFileSync ( tsconfigPath , "utf8" ) ;
162
+ // Remove comments and parse JSON
163
+ const cleanJson = tsconfigContent . replace ( / \/ \* [ \s \S ] * ?\* \/ | \/ \/ .* $ / gm, "" ) ;
164
+ tsConfig = JSON . parse ( cleanJson ) ;
165
+ } catch ( error ) {
166
+ // eslint-disable-next-line no-console
167
+ console . warn ( `BabylonJS custom eslint plugin failed to load tsconfig.json: ${ error . message } ` ) ;
168
+ }
169
+
170
+ return tsConfig ;
171
+ }
172
+
173
+ function shouldUsePathMapping ( importPath : string , filename : string , tsConfig : TsConfig ) {
174
+ if ( ! importPath . startsWith ( "../" ) || ! tsConfig ?. compilerOptions ?. paths ) {
175
+ return null ;
176
+ }
177
+
178
+ const { baseUrl = "." , paths } = tsConfig . compilerOptions ;
179
+ const projectRoot = path . dirname ( path . join ( path . dirname ( filename ) , ".." , ".." , ".." ) ) ;
180
+
181
+ // Resolve the relative import to an absolute path
182
+ const resolvedImportPath = path . resolve ( path . dirname ( filename ) , importPath ) ;
183
+
184
+ // Check if this resolved path matches any of the path mappings
185
+ for ( const [ pathKey , pathValues ] of Object . entries ( paths ) ) {
186
+ for ( const pathValue of pathValues ) {
187
+ // Convert tsconfig path to absolute path
188
+ const absolutePathPattern = path . resolve ( projectRoot , baseUrl , pathValue ) ;
189
+
190
+ // Check if the resolved import matches this path mapping
191
+ if ( resolvedImportPath . startsWith ( absolutePathPattern . replace ( "*" , "" ) ) ) {
192
+ // Calculate what the import should be
193
+ const relativePart = path . relative ( absolutePathPattern . replace ( "*" , "" ) , resolvedImportPath ) ;
194
+
195
+ const suggestedImport = pathKey
196
+ . replace ( "*" , relativePart )
197
+ . replace ( / \\ / g, "/" ) // Normalize to forward slashes
198
+ . replace ( / \. ( t s | t s x ) $ / , "" ) ; // Remove extension
199
+
200
+ return suggestedImport ;
201
+ }
202
+ }
203
+ }
204
+
205
+ return null ;
206
+ }
207
+
144
208
const plugin : IPlugin = {
145
209
rules : {
146
210
// NOTE: The actual ESLint rule name will be "tsdoc/syntax". It is calculated by deleting "eslint-plugin-"
@@ -388,6 +452,49 @@ const plugin: IPlugin = {
388
452
} ;
389
453
} ,
390
454
} ,
455
+ "no-cross-package-relative-imports" : {
456
+ meta : {
457
+ type : "problem" ,
458
+ docs : {
459
+ description : "Prevent relative imports that should use TypeScript path mappings" ,
460
+ } ,
461
+ fixable : "code" ,
462
+ messages : {
463
+ usePathMapping : 'Use path mapping "{{suggestion}}" instead of relative import "{{importPath}}".' ,
464
+ } ,
465
+ } ,
466
+ create ( context ) {
467
+ return {
468
+ Program ( ) {
469
+ // Load tsconfig once per file
470
+ const filename = context . filename ;
471
+ const projectRoot = filename . split ( "packages" ) [ 0 ] ;
472
+ tsConfig = loadTsConfig ( projectRoot ) ;
473
+ } ,
474
+
475
+ ImportDeclaration ( node ) {
476
+ const importPath = node . source . value as string ;
477
+ const filename = context . filename ;
478
+
479
+ const suggestion = shouldUsePathMapping ( importPath , filename , tsConfig ! ) ;
480
+
481
+ if ( suggestion ) {
482
+ context . report ( {
483
+ node,
484
+ messageId : "usePathMapping" ,
485
+ data : {
486
+ importPath,
487
+ suggestion,
488
+ } ,
489
+ fix ( fixer ) {
490
+ return fixer . replaceText ( node . source , `"${ suggestion } "` ) ;
491
+ } ,
492
+ } ) ;
493
+ }
494
+ } ,
495
+ } ;
496
+ } ,
497
+ } ,
391
498
} ,
392
499
} ;
393
500
0 commit comments