UNPKG

6.94 kBPlain TextView Raw
1import { tsUtils } from '@neo-one/ts-utils';
2import * as appRootDir from 'app-root-dir';
3import * as fs from 'fs-extra';
4// tslint:disable-next-line match-default-export-name
5import glob from 'glob';
6import * as path from 'path';
7import ts from 'typescript';
8import { Context } from './Context';
9import { getGlobals, getLibAliases, getLibs } from './symbols';
10
11function createContext(program: ts.Program, typeChecker: ts.TypeChecker, languageService: ts.LanguageService): Context {
12 return new Context(
13 program,
14 typeChecker,
15 languageService,
16 getGlobals(program, typeChecker),
17 getLibs(program, typeChecker),
18 getLibAliases(program, languageService),
19 );
20}
21
22export function updateContext(context: Context, files: { readonly [fileName: string]: string | undefined }): Context {
23 const { program, typeChecker, languageService } = createProgram(
24 context.program.getCompilerOptions(),
25 Object.keys(files),
26 createModifyHostFiles(files),
27 );
28
29 return context.update(
30 program,
31 typeChecker,
32 languageService,
33 getGlobals(program, typeChecker),
34 getLibs(program, typeChecker),
35 getLibAliases(program, languageService),
36 );
37}
38
39const doGlob = async (value: string) =>
40 new Promise<ReadonlyArray<string>>((resolve, reject) =>
41 glob(value, (error, matches) => {
42 if (error) {
43 reject(error);
44 } else {
45 resolve(matches);
46 }
47 }),
48 );
49
50const makeContext = async (
51 rootNames: ReadonlyArray<string>,
52 modifyHost: (host: ts.LanguageServiceHost) => void = () => {
53 // do nothing
54 },
55): Promise<Context> => {
56 const tsConfigFilePath = path.resolve(
57 require.resolve('@neo-one/smart-contract-compiler'),
58 '..',
59 '..',
60 'tsconfig.default.json',
61 );
62
63 const res = ts.readConfigFile(tsConfigFilePath, (value) => fs.readFileSync(value, 'utf8'));
64 const parseConfigHost = {
65 fileExists: fs.existsSync,
66 readDirectory: ts.sys.readDirectory,
67 readFile: ts.sys.readFile,
68 useCaseSensitiveFileNames: true,
69 };
70 const parsed = ts.parseJsonConfigFileContent(res.config, parseConfigHost, path.dirname(tsConfigFilePath));
71
72 const { program, typeChecker, languageService } = createProgram(parsed.options, rootNames, modifyHost);
73
74 return createContext(program, typeChecker, languageService);
75};
76
77const createModifyHostFiles = (files: { readonly [fileName: string]: string | undefined }) => (
78 host: ts.LanguageServiceHost,
79) => {
80 const originalFileExists = host.fileExists === undefined ? ts.sys.fileExists : host.fileExists.bind(host);
81 // tslint:disable-next-line no-object-mutation no-any
82 host.fileExists = (file) => {
83 if (files[file] !== undefined) {
84 return true;
85 }
86
87 return originalFileExists(file);
88 };
89
90 const originalReadFile = host.readFile === undefined ? ts.sys.readFile : host.readFile.bind(host);
91 // tslint:disable-next-line no-object-mutation no-any
92 host.readFile = (file, ...args: any[]) => {
93 const foundFile = files[file];
94 if (foundFile !== undefined) {
95 return foundFile;
96 }
97
98 return originalReadFile(file, ...args);
99 };
100};
101
102const createProgram = (
103 options: ts.CompilerOptions,
104 rootNamesIn: ReadonlyArray<string>,
105 modifyHost: (host: ts.LanguageServiceHost) => void = () => {
106 // do nothing
107 },
108) => {
109 const smartContractDir = path.dirname(require.resolve('@neo-one/smart-contract'));
110 const smartContractModule = path.resolve(smartContractDir, 'index.ts');
111 const smartContractFiles = [
112 path.resolve(smartContractDir, 'index.d.ts'),
113 smartContractModule,
114 path.resolve(smartContractDir, 'lib.ts'),
115 ];
116
117 const rootNames = [
118 ...new Set(rootNamesIn.concat(smartContractFiles).concat(require.resolve('@types/node/index.d.ts'))),
119 ];
120
121 const mutableFiles: ts.MapLike<{ version: number } | undefined> = {};
122 // initialize the list of files
123 rootNames.forEach((fileName) => {
124 mutableFiles[fileName] = { version: 0 };
125 });
126 const servicesHost: ts.LanguageServiceHost = {
127 getScriptFileNames: () => [...rootNames],
128 getScriptVersion: (fileName) => {
129 const file = mutableFiles[fileName];
130
131 return file === undefined ? '' : file.version.toString();
132 },
133 getScriptSnapshot: (fileName) => {
134 // tslint:disable-next-line no-non-null-assertion
135 if (!servicesHost.fileExists!(fileName)) {
136 return undefined;
137 }
138
139 // tslint:disable-next-line no-non-null-assertion
140 return ts.ScriptSnapshot.fromString(servicesHost.readFile!(fileName)!);
141 },
142 getCurrentDirectory: () => process.cwd(),
143 getCompilationSettings: () => options,
144 getDefaultLibFileName: (opts) => ts.getDefaultLibFilePath(opts),
145 fileExists: ts.sys.fileExists,
146 readFile: ts.sys.readFile,
147 readDirectory: ts.sys.readDirectory,
148 resolveModuleNames,
149 };
150
151 const smartContractLibModule = path.resolve(path.dirname(require.resolve('@neo-one/smart-contract-lib')), 'index.ts');
152 function resolveModuleNames(moduleNames: string[], containingFile: string): ts.ResolvedModule[] {
153 const mutableResolvedModules: ts.ResolvedModule[] = [];
154 // tslint:disable-next-line no-loop-statement
155 for (const moduleName of moduleNames) {
156 if (moduleName === '@neo-one/smart-contract') {
157 mutableResolvedModules.push({ resolvedFileName: smartContractModule });
158 } else if (moduleName === '@neo-one/smart-contract-lib') {
159 mutableResolvedModules.push({ resolvedFileName: smartContractLibModule });
160 } else {
161 const result = ts.resolveModuleName(moduleName, containingFile, options, {
162 fileExists: ts.sys.fileExists,
163 readFile: ts.sys.readFile,
164 });
165 // tslint:disable-next-line no-non-null-assertion
166 mutableResolvedModules.push(result.resolvedModule!);
167 }
168 }
169
170 return mutableResolvedModules;
171 }
172
173 modifyHost(servicesHost);
174
175 const languageService = ts.createLanguageService(servicesHost, ts.createDocumentRegistry());
176 const program = languageService.getProgram();
177 if (program === undefined) {
178 throw new Error('Something went wrong');
179 }
180
181 return {
182 program,
183 typeChecker: program.getTypeChecker(),
184 languageService,
185 };
186};
187
188export const createContextForDir = async (dir: string): Promise<Context> => {
189 const files = await doGlob(path.join(dir, '**', '*.ts'));
190
191 return makeContext(files);
192};
193
194export const createContextForPath = async (filePath: string): Promise<Context> => makeContext([filePath]);
195
196export interface SnippetResult {
197 readonly context: Context;
198 readonly sourceFile: ts.SourceFile;
199}
200
201export const createContextForSnippet = async (code: string): Promise<SnippetResult> => {
202 const dir = appRootDir.get();
203 const fileName = path.resolve(dir, 'snippetCode.ts');
204
205 const context = await makeContext([fileName], createModifyHostFiles({ [fileName]: code }));
206 const sourceFile = tsUtils.file.getSourceFileOrThrow(context.program, fileName);
207
208 return {
209 context,
210 sourceFile,
211 };
212};