UNPKG

els-addon-typed-templates

Version:
321 lines (301 loc) 10.3 kB
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: any = {}; 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'; // {"type":"component","name":"my-component","kind":"template","scope":"application","className":"MyComponentComponent"} export interface MatchResult { type: MatchResultType; name: string; kind: string; scope: string; className?: string; } interface RegistryItem { [key: string]: string[] } export interface LSRegistry { 'transform': RegistryItem; 'helper': RegistryItem; 'component': RegistryItem; 'routePath': RegistryItem; 'model': RegistryItem; 'service': RegistryItem; 'modifier': RegistryItem; } export interface LanguageServer { getRegistry(root: string): LSRegistry } interface ProjectMirror { project: { files: Map<string, ProjectFile>; matchPathToType(filePath: string): null | MatchResult }; server: LanguageServer, files: WeakMap<ProjectFile, TSMeta>; } const STABLE_FILES: Map<string, TSMeta> = new Map(); const PROJECTS_MAP: Map<string, ProjectMirror> = new Map(); export function registerProject(item, server) { PROJECTS_MAP.set(item.root, { project: item, server: server, files: new WeakMap() }); } const serverMock: LanguageServer = { getRegistry(_: string): LSRegistry { return { 'transform': {}, 'helper': {}, 'component': {}, 'routePath': {}, 'model': {}, 'service': {}, 'modifier': {} } } } export function serverForProject(root: string) { 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: Project, uri: string) { let result: MatchResult | null = (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: string, uri: string) { 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: string) { let tsConfig: any = {}; 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: string) { this.projectRoot = projectRoot; const uri = projectRoot; const registry: ts.DocumentRegistry = 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: string) { 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: ts.CompilerOptions) { 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: string) { const _fileName = path.resolve(fileName); const projectFiles = this.project.project.files; if (projectFiles.has(_fileName)) { // return Date.now().toString(); return projectFiles.get(_fileName)!.version.toString(); } if (STABLE_FILES.has(_fileName)) { // return Date.now().toString(); 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) { // if file is virtual (constructed template) -> return fresh snapshot return ts.ScriptSnapshot.fromString(maybeVirtualFile); } else { let fileName = path.resolve(path.normalize(rawFileName)); const originalProjectFiles = this.project.project.files; const tsProjectFiles = this.project.files; // console.log(fileName); // console.log(project.project.files); // if project has changed files // console.log('project.project.files', project.project.files); // console.log('has file?' , project.project.files.has(fileName)); if (originalProjectFiles.has(fileName)) { // project changed file let mirror = originalProjectFiles.get(fileName) as ProjectFile; // ts mirrors if (!tsProjectFiles.has(mirror)) { tsProjectFiles.set(mirror, { version: -1, snapshot: ts.ScriptSnapshot.fromString("") }); } if (tsProjectFiles.has(mirror)) { let tsMeta = tsProjectFiles.get(mirror); // if no ts-mirror - we must create it; if (!tsMeta) { tsMeta = { version: -1, snapshot: ts.ScriptSnapshot.fromString("") }; tsProjectFiles.set(mirror, tsMeta); } if (tsMeta.version !== mirror.version) { if (!fs.existsSync(fileName)) { // @to-do - figure out why remove event don't touch it console.log(`typed-template: unable to get file ${fileName}, fix watcher`); tsProjectFiles.delete(mirror); originalProjectFiles.delete(fileName); return ts.ScriptSnapshot.fromString(""); } // if versions different - we need to update file 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 file is not marked as changed, we count it as stable if (!STABLE_FILES.has(fileName) && fs.existsSync(fileName)) { // if no stable record, but file exists - we must create it. 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; } // file not exists 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): ts.LanguageService { if (!services[uri]) { services[uri] = new TypescriptService(uri).ts; components.set(services[uri], {}); } return services[uri]; }