els-addon-typed-templates
Version:
Ember Language Server Typed Templates
321 lines (301 loc) • 10.3 kB
text/typescript
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];
}