1 | import { tsUtils } from '@neo-one/ts-utils';
|
2 | import * as appRootDir from 'app-root-dir';
|
3 | import * as fs from 'fs-extra';
|
4 |
|
5 | import glob from 'glob';
|
6 | import * as path from 'path';
|
7 | import ts from 'typescript';
|
8 | import { Context } from './Context';
|
9 | import { getGlobals, getLibAliases, getLibs } from './symbols';
|
10 |
|
11 | function 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 |
|
22 | export 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 |
|
39 | const 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 |
|
50 | const makeContext = async (
|
51 | rootNames: ReadonlyArray<string>,
|
52 | modifyHost: (host: ts.LanguageServiceHost) => void = () => {
|
53 |
|
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 |
|
77 | const 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 |
|
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 |
|
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 |
|
102 | const createProgram = (
|
103 | options: ts.CompilerOptions,
|
104 | rootNamesIn: ReadonlyArray<string>,
|
105 | modifyHost: (host: ts.LanguageServiceHost) => void = () => {
|
106 |
|
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 |
|
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 |
|
135 | if (!servicesHost.fileExists!(fileName)) {
|
136 | return undefined;
|
137 | }
|
138 |
|
139 |
|
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 |
|
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 |
|
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 |
|
188 | export const createContextForDir = async (dir: string): Promise<Context> => {
|
189 | const files = await doGlob(path.join(dir, '**', '*.ts'));
|
190 |
|
191 | return makeContext(files);
|
192 | };
|
193 |
|
194 | export const createContextForPath = async (filePath: string): Promise<Context> => makeContext([filePath]);
|
195 |
|
196 | export interface SnippetResult {
|
197 | readonly context: Context;
|
198 | readonly sourceFile: ts.SourceFile;
|
199 | }
|
200 |
|
201 | export 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 | };
|