UNPKG

6.26 kBJavaScriptView Raw
1'use strict';
2
3// Adapted from: https://github.com/SamVerschueren/tsd, copyright (c) 2018-2019 Sam Verschueren, MIT licensed
4
5const Fs = require('fs');
6const Path = require('path');
7
8const Globby = require('globby');
9const Hoek = require('@hapi/hoek');
10const Ts = require('typescript');
11
12const Utils = require('./utils');
13
14
15const internals = {
16 compiler: {
17 strict: true,
18 jsx: Ts.JsxEmit.React,
19 lib: ['lib.es2018.d.ts'],
20 module: Ts.ModuleKind.CommonJS,
21 target: Ts.ScriptTarget.ES2018,
22 moduleResolution: Ts.ModuleResolutionKind.NodeJs
23 },
24
25 // Codes from https://github.com/microsoft/TypeScript/blob/master/src/compiler/diagnosticMessages.json
26
27 skip: [
28 1308 // Await is only allowed in async function
29 ],
30
31 report: [
32 2304, // Cannot find name
33 2345, // Argument type is not assignable to parameter type
34 2339, // Property does not exist on type
35 2540, // Cannot assign to readonly property
36 2322, // Type is not assignable to other type
37 2314, // Generic type requires type arguments
38 2554, // Expected arguments but got other
39 2769 // No overload matches this call
40 ]
41};
42
43
44exports.expect = {
45 error: Hoek.ignore,
46 type: Hoek.ignore
47};
48
49
50exports.validate = async (options = {}) => {
51
52 const cwd = process.cwd();
53 const pkg = require(Path.join(cwd, 'package.json'));
54
55 if (!pkg.types) {
56 return [{ filename: 'package.json', message: 'File missing "types" property' }];
57 }
58
59 if (!Fs.existsSync(Path.join(cwd, pkg.types))) {
60 return [{ filename: pkg.types, message: 'Cannot find types file' }];
61 }
62
63 if (pkg.files) {
64 const packaged = await Globby(pkg.files, { cwd });
65 if (!packaged.includes(pkg.types)) {
66 return [{ filename: 'package.json', message: 'Types file is not covered by "files" property' }];
67 }
68 }
69
70 const testFiles = await Globby(options['types-test'] || 'test/**/*.ts', { cwd, absolute: true });
71 if (!testFiles.length) {
72 return [{ filename: pkg.types, message: 'Cannot find tests for types file' }];
73 }
74
75 const executions = await internals.execute(testFiles);
76 if (executions) {
77 return executions;
78 }
79
80 const program = Ts.createProgram(testFiles, internals.compiler);
81
82 const diagnostics = [
83 ...program.getSemanticDiagnostics(),
84 ...program.getSyntacticDiagnostics()
85 ];
86
87 const errors = internals.extractErrors(program);
88
89 const result = [];
90 for (const diagnostic of diagnostics) {
91 if (internals.ignore(diagnostic, errors)) {
92 continue;
93 }
94
95 const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
96
97 result.push({
98 filename: diagnostic.file.fileName,
99 message: Ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
100 line: position.line + 1,
101 column: position.character
102 });
103 }
104
105 for (const [, diagnostic] of errors) {
106 result.push({
107 ...diagnostic,
108 message: 'Expected an error'
109 });
110 }
111
112 for (const error of result) {
113 error.filename = error.filename.slice(process.cwd().length + 1);
114 }
115
116 return result;
117};
118
119
120internals.extractErrors = function (program) {
121
122 const errors = new Map();
123
124 const extract = (node) => {
125
126 if (node.kind === Ts.SyntaxKind.ExpressionStatement &&
127 node.getText().startsWith('expect.error')) {
128
129 const location = {
130 filename: node.getSourceFile().fileName,
131 start: node.getStart(),
132 end: node.getEnd()
133 };
134
135 const pos = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
136 errors.set(location, {
137 filename: location.filename,
138 line: pos.line + 1,
139 column: pos.character
140 });
141 }
142
143 Ts.forEachChild(node, extract);
144 };
145
146 for (const sourceFile of program.getSourceFiles()) {
147 extract(sourceFile);
148 }
149
150 return errors;
151};
152
153
154internals.ignore = function (diagnostic, expectedErrors) {
155
156 if (internals.skip.includes(diagnostic.code)) {
157 return true;
158 }
159
160 if (!internals.report.includes(diagnostic.code)) {
161 return false;
162 }
163
164 for (const [location] of expectedErrors) {
165 if (diagnostic.file.fileName === location.filename &&
166 diagnostic.start > location.start &&
167 diagnostic.start < location.end) {
168
169 expectedErrors.delete(location);
170 return true;
171 }
172 }
173
174 return false;
175};
176
177
178internals.execute = async function (filenames) {
179
180 const orig = require.extensions['.ts'];
181 require.extensions['.ts'] = internals.transpile;
182
183 const errors = [];
184
185 for (const filename of filenames) {
186 try {
187 const test = require(filename);
188
189 if (typeof test === 'function') {
190 await test();
191 }
192 }
193 catch (err) {
194 const report = {
195 filename,
196 message: err.message,
197 ...Utils.position(err)
198 };
199
200 errors.push(report);
201 }
202 }
203
204 require.extensions['.ts'] = orig;
205
206 return errors.length ? errors : null;
207};
208
209
210internals.transpile = function (localModule, filename) {
211
212 const source = Fs.readFileSync(filename, 'utf8');
213 const valid = source.toString()
214 .split('\n')
215 .filter((line) => !/^expect\.error\(/.test(line))
216 .join('\n');
217
218 const result = Ts.transpileModule(valid, { compilerOptions: internals.compiler });
219 let transpiled = result.outputText;
220
221 if (/await/.test(transpiled)) {
222 transpiled = `module.exports = async function () { ${transpiled} };\n`;
223 }
224
225 return localModule._compile(transpiled, filename);
226};