import * as ts from "typescript" ; import * as path from "path" ; import * as fs from "fs" ; import { safeWalkSync , normalizeToAngleBracketName } from "./utils" ; import { Project } from '@lifeart/ember-language-server' ; import { withDebug } from './logger' ; const services = { } ; const components = new WeakMap ( ) ; export function componentsForService ( service , clean = false ) { if ( clean ) { components . set ( service , { } ) ; } return components . get ( service ) ; } type ProjectFile = { version : number ; } ; type TSMeta = { version : number ; snapshot : ts . IScriptSnapshot ; } ; type MatchResultType = 'helper' | 'service' | 'route' | 'controller' | 'modifier' | 'template' | 'component' | 'model' | 'transform' | 'adapter' | 'serializer' ; export interface MatchResult { type : MatchResultType ; name : string ; kind : string ; scope : string ; className ? : string ; } interface RegistryItem { [ key ] : string [ ] ; } export interface LSRegistry { 'transform' : RegistryItem ; 'helper' : RegistryItem ; 'component' : RegistryItem ; 'routePath' : RegistryItem ; 'model' : RegistryItem ; 'service' : RegistryItem ; 'modifier' : RegistryItem ; } export interface LanguageServer { getRegistry ( root ) : LSRegistry ; } interface ProjectMirror { project : { files : Map < string , ProjectFile > ; matchPathToType ( filePath ) : null | MatchResult ; } ; server : LanguageServer ; files : WeakMap < ProjectFile , TSMeta > ; } const STABLE_FILES = new Map ( ) ; const PROJECTS_MAP = new Map ( ) ; export function registerProject ( item , server ) { PROJECTS_MAP . set ( item . root , { project : item , server : server , files : new WeakMap ( ) } ) ; } const serverMock = { getRegistry ( _ ) { return { 'transform' : { } , 'helper' : { } , 'component' : { } , 'routePath' : { } , 'model' : { } , 'service' : { } , 'modifier' : { } } ; } } ; export function serverForProject ( root ) { const projectMirror = PROJECTS_MAP . get ( root ) as ProjectMirror ; if ( ! projectMirror ) { withDebug ( ( ) => { console . log ( 'server-mock used' ) ; } ) ; return serverMock as LanguageServer ; } return projectMirror . server ; } export function matchPathToType ( project , uri ) { let result = ( project . matchPathToType ( uri ) as unknown ) as MatchResult | null ; if ( result === null ) { return null ; } result . className = normalizeToAngleBracketName ( result . name ) + result . type . charAt ( 0 ) . toUpperCase ( ) + result . type . slice ( 1 ) ; result . className = result . className . split ( '-' ) . join ( '_' ) ; return result ; } export function typeForPath ( root , uri ) { withDebug ( ( ) => { console . log ( 'typeForPath' , root , uri ) ; } ) ; const projectMirror = PROJECTS_MAP . get ( root ) as ProjectMirror ; return matchPathToType ( ( projectMirror . project as unknown ) as Project , uri ) ; } function getProjectTypeScriptConfig ( root ) { let tsConfig = { } ; if ( fs . existsSync ( path . join ( root , "tsconfig.json" ) ) ) { try { tsConfig = JSON . parse ( fs . readFileSync ( path . join ( root , "tsconfig.json" ) , "utf8" ) ) ; if ( tsConfig && tsConfig . compilerOptions ) { tsConfig = tsConfig . compilerOptions ; } } catch ( e ) { } } return tsConfig ; } export default class TypescriptService implements ts . LanguageServiceHost { ts ! : ts . LanguageService ; registry ! : ts . DocumentRegistry ; tsConfig ! : { } ; projectRoot ! : string ; project ! : ProjectMirror ; initialFiles : any ; constructor ( projectRoot ) { this . projectRoot = projectRoot ; const uri = projectRoot ; const registry = ts . createDocumentRegistry ( false , uri ) ; this . registry = registry ; this . project = PROJECTS_MAP . get ( uri ) as ProjectMirror ; this . tsConfig = getProjectTypeScriptConfig ( projectRoot ) ; this . initialFiles = this . initialProjectFileStructure ( ) ; this . ts = ts . createLanguageService ( this , registry ) ; } private getProjectFilesFromFolder ( entry ) { const walkParams = { directories : true , globs : [ "**/*.{js,ts,d.ts}" ] } ; let projectEntry = path . join ( this . projectRoot , entry ) ; return safeWalkSync ( path . join ( this . projectRoot , entry ) , walkParams ) . map ( el => path . resolve ( path . join ( projectEntry , el ) ) ) ; } private initialProjectFileStructure ( ) { let commonTypes = path . join ( __dirname , "./../../../src/lib/common-types.d.ts" ) ; let projectTypes = this . getProjectFilesFromFolder ( "types" ) ; let projectAppFiles = this . getProjectFilesFromFolder ( "app" ) ; let projectAddonFiles = this . getProjectFilesFromFolder ( "addon" ) ; return Array . from ( new Set ( [ commonTypes , ... projectTypes , ... projectAppFiles , ... projectAddonFiles ] ) ) as string [ ] ; } getDefaultLibFileName ( opts ) { return path . resolve ( ts . getDefaultLibFilePath ( opts ) ) ; } getCompilationSettings ( ) { return Object . assign ( { } , this . tsConfig , { baseUrl : "." , allowJs : true , checkJs : true , allowSyntheticDefaultImports : true , skipLibCheck : true , experimentalDecorators : true , noImplicitAny : false , moduleResolution : ts . ModuleResolutionKind . NodeJs , strictPropertyInitialization : false , module : ts . ModuleKind . ES2015 } ) ; } getScriptFileNames ( ) { let els = [ ... Object . keys ( componentsForService ( services [ this . projectRoot ] ) ) ] ; return Array . from ( new Set ( [ ... this . initialFiles , ... els , ... this . project . project . files . keys ( ) ] ) ) ; } getScriptVersion ( fileName ) { const _fileName = path . resolve ( fileName ) ; const projectFiles = this . project . project . files ; if ( projectFiles . has ( _fileName ) ) { return projectFiles . get ( _fileName ) ! . version . toString ( ) ; } if ( STABLE_FILES . has ( _fileName ) ) { return STABLE_FILES . get ( _fileName ) ! . version . toString ( ) ; } if ( fs . existsSync ( _fileName ) ) { let stats = fs . statSync ( _fileName ) ; return stats . mtime . getTime ( ) . toString ( ) ; } return Date . now ( ) . toString ( ) ; } getScriptSnapshot ( rawFileName ) { const maybeVirtualFile = componentsForService ( services [ this . projectRoot ] ) [ path . resolve ( rawFileName ) ] ; if ( maybeVirtualFile ) { return ts . ScriptSnapshot . fromString ( maybeVirtualFile ) ; } else { let fileName = path . resolve ( path . normalize ( rawFileName ) ) ; const originalProjectFiles = this . project . project . files ; const tsProjectFiles = this . project . files ; if ( originalProjectFiles . has ( fileName ) ) { let mirror = originalProjectFiles . get ( fileName ) as ProjectFile ; if ( ! tsProjectFiles . has ( mirror ) ) { tsProjectFiles . set ( mirror , { version : - 1 , snapshot : ts . ScriptSnapshot . fromString ( "" ) } ) ; } if ( tsProjectFiles . has ( mirror ) ) { let tsMeta = tsProjectFiles . get ( mirror ) ; if ( ! tsMeta ) { tsMeta = { version : - 1 , snapshot : ts . ScriptSnapshot . fromString ( "" ) } ; tsProjectFiles . set ( mirror , tsMeta ) ; } if ( tsMeta . version !== mirror . version ) { if ( ! fs . existsSync ( fileName ) ) { console . log ( `typed-template: unable to get file ${ fileName }, fix watcher` ) ; tsProjectFiles . delete ( mirror ) ; originalProjectFiles . delete ( fileName ) ; return ts . ScriptSnapshot . fromString ( "" ) ; } tsMeta . snapshot = ts . ScriptSnapshot . fromString ( fs . readFileSync ( fileName ) . toString ( ) ) ; if ( STABLE_FILES . has ( fileName ) ) { const stableItem = STABLE_FILES . get ( fileName ) as TSMeta ; stableItem . snapshot = tsMeta . snapshot ; stableItem . version = tsMeta . version ; } } return tsMeta . snapshot ; } } else { if ( ! STABLE_FILES . has ( fileName ) && fs . existsSync ( fileName ) ) { let text = fs . readFileSync ( fileName ) . toString ( ) ; STABLE_FILES . set ( fileName , { version : 0 , snapshot : ts . ScriptSnapshot . fromString ( text ) } ) ; } if ( STABLE_FILES . has ( fileName ) ) { return ( STABLE_FILES . get ( fileName ) as TSMeta ) . snapshot ; } let name = path . basename ( fileName , path . extname ( fileName ) ) ; let libName = "lib." + name . toLowerCase ( ) + ".d.ts" ; let libFileNmae = path . join ( path . dirname ( fileName ) , libName ) ; if ( fs . existsSync ( libFileNmae ) ) { return ts . ScriptSnapshot . fromString ( fs . readFileSync ( libFileNmae ) . toString ( ) ) ; } return ts . ScriptSnapshot . fromString ( "" ) ; } } } getCurrentDirectory ( ) { return this . projectRoot ; } } export function serviceForRoot ( uri ) { if ( ! services [ uri ] ) { services [ uri ] = new TypescriptService ( uri ) . ts ; components . set ( services [ uri ] , { } ) ; } return services [ uri ] ; }