import * as fs from 'fs' ; import { Path } from './Path' ; export type InputFileMessageIds = 'file-in-packets-folder' | 'invalid-packlet-name' | 'misplaced-packlets-folder' | 'missing-src-folder' | 'missing-tsconfig' | 'packlet-folder-case' ; export type ImportMessageIds = 'bypassed-entry-point' | 'circular-entry-point' | 'packlet-importing-project-file' ; export interface IAnalyzerError { messageId : InputFileMessageIds | ImportMessageIds ; data ? : Readonly < Record < string , unknown > > ; } export class PackletAnalyzer { private static _validPackletName : RegExp = /^[a-z0-9]+(-[a-z0-9]+)*$/ ; /**
     * The input file being linted.
     *
     * Example: "/path/to/my-project/src/file.ts"
     */ public readonly inputFilePath : string ; /**
     * An error that occurred while analyzing the inputFilePath.
     */ public readonly error : IAnalyzerError | undefined ; /**
     * Returned to indicate that the linter can ignore this file.  Possible reasons:
     * - It's outside the "src" folder
     * - The project doesn't define any packlets
     */ public readonly nothingToDo : boolean ; /**
     * If true, then the "src/packlets" folder exists.
     */ public readonly projectUsesPacklets : boolean ; /**
     * The absolute path of the "src/packlets" folder.
     */ public readonly packletsFolderPath : string | undefined ; /**
     * The packlet that the inputFilePath is under, if any.
     */ public readonly inputFilePackletName : string | undefined ; /**
     * Returns true if inputFilePath belongs to a packlet and is the entry point index.ts.
     */ public readonly isEntryPoint : boolean ; private constructor ( inputFilePath , tsconfigFilePath ) { this . inputFilePath = inputFilePath ; this . error = undefined ; this . nothingToDo = false ; this . projectUsesPacklets = false ; this . packletsFolderPath = undefined ; this . inputFilePackletName = undefined ; this . isEntryPoint = false ; let srcFolderPath ; if ( ! tsconfigFilePath ) { this . error = { messageId : 'missing-tsconfig' } ; return ; } srcFolderPath = Path . join ( Path . dirname ( tsconfigFilePath ) , 'src' ) ; if ( ! fs . existsSync ( srcFolderPath ) ) { this . error = { messageId : 'missing-src-folder' , data : { srcFolderPath } } ; return ; } if ( ! Path . isUnder ( inputFilePath , srcFolderPath ) ) { this . nothingToDo = true ; return ; } const inputFilePathRelativeToSrc = Path . relative ( srcFolderPath , inputFilePath ) ; const pathParts = inputFilePathRelativeToSrc . split ( /[\/\\]+/ ) ; let underPackletsFolder = false ; const expectedPackletsFolder = Path . join ( srcFolderPath , 'packlets' ) ; for ( let i = 0 ; i < pathParts . length ; ++ i ) { const pathPart = pathParts [ i ] ; if ( pathPart . toUpperCase ( ) === 'PACKLETS' ) { if ( pathPart !== 'packlets' ) { const packletsFolderPath = Path . join ( srcFolderPath , ... pathParts . slice ( 0 , i + 1 ) ) ; this . error = { messageId : 'packlet-folder-case' , data : { packletsFolderPath } } ; return ; } if ( i !== 0 ) { this . error = { messageId : 'misplaced-packlets-folder' , data : { expectedPackletsFolder } } ; return ; } underPackletsFolder = true ; } } if ( underPackletsFolder || fs . existsSync ( expectedPackletsFolder ) ) { this . projectUsesPacklets = true ; this . packletsFolderPath = expectedPackletsFolder ; } if ( underPackletsFolder ) { if ( pathParts . length === 2 ) { this . error = { messageId : 'file-in-packets-folder' } ; return ; } if ( pathParts . length >= 2 ) { const packletName = pathParts [ 1 ] ; this . inputFilePackletName = packletName ; if ( pathParts . length === 3 ) { const thirdPart = pathParts [ 2 ] ; const thirdPartWithoutExtension = Path . parse ( thirdPart ) . name ; if ( thirdPartWithoutExtension . toUpperCase ( ) === 'INDEX' ) { if ( ! PackletAnalyzer . _validPackletName . test ( packletName ) ) { this . error = { messageId : 'invalid-packlet-name' , data : { packletName } } ; return ; } this . isEntryPoint = true ; } } } } if ( this . error === undefined && ! this . projectUsesPacklets ) { this . nothingToDo = true ; } } public static analyzeInputFile ( inputFilePath , tsconfigFilePath ) { return new PackletAnalyzer ( inputFilePath , tsconfigFilePath ) ; } public analyzeImport ( modulePath ) { if ( ! this . packletsFolderPath ) { throw new Error ( 'Internal error: packletsFolderPath is not defined' ) ; } const inputFileFolder = Path . dirname ( this . inputFilePath ) ; const importedPath = Path . resolve ( inputFileFolder , modulePath ) ; if ( Path . isUnder ( importedPath , this . packletsFolderPath ) ) { const importedPathRelativeToPackletsFolder = Path . relative ( this . packletsFolderPath , importedPath ) ; const importedPathParts = importedPathRelativeToPackletsFolder . split ( /[\/\\]+/ ) ; if ( importedPathParts . length > 0 ) { const importedPackletName = importedPathParts [ 0 ] ; if ( this . inputFilePackletName && importedPackletName === this . inputFilePackletName ) { const lastPart = Path . parse ( importedPathParts [ importedPathParts . length - 1 ] ) . name ; let pathToCompare ; if ( lastPart . toUpperCase ( ) === 'INDEX' ) { pathToCompare = Path . dirname ( importedPath ) ; } else { pathToCompare = importedPath ; } const entryPointPath = Path . join ( this . packletsFolderPath , importedPackletName ) ; if ( Path . isEqual ( pathToCompare , entryPointPath ) ) { return { messageId : 'circular-entry-point' } ; } } else { const entryPointPath = Path . join ( this . packletsFolderPath , importedPackletName ) ; if ( ! Path . isEqual ( importedPath , entryPointPath ) ) { const entryPointModulePath = Path . convertToSlashes ( Path . relative ( inputFileFolder , entryPointPath ) ) ; return { messageId : 'bypassed-entry-point' , data : { entryPointModulePath } } ; } } } } else { if ( this . inputFilePackletName ) { return { messageId : 'packlet-importing-project-file' } ; } } return undefined ; } }