1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 | const Fs = require('fs');
|
6 | const Path = require('path');
|
7 |
|
8 | const Globby = require('globby');
|
9 | const Hoek = require('@hapi/hoek');
|
10 | const Ts = require('typescript');
|
11 |
|
12 | const Utils = require('./utils');
|
13 |
|
14 |
|
15 | const 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 |
|
26 |
|
27 | skip: [
|
28 | 1308
|
29 | ],
|
30 |
|
31 | report: [
|
32 | 2304,
|
33 | 2345,
|
34 | 2339,
|
35 | 2540,
|
36 | 2322,
|
37 | 2314,
|
38 | 2554,
|
39 | 2769
|
40 | ]
|
41 | };
|
42 |
|
43 |
|
44 | exports.expect = {
|
45 | error: Hoek.ignore,
|
46 | type: Hoek.ignore
|
47 | };
|
48 |
|
49 |
|
50 | exports.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 |
|
120 | internals.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 |
|
154 | internals.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 |
|
178 | internals.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 |
|
210 | internals.transpile = function (localModule, filename) {
|
211 |
|
212 | const source = Fs.readFileSync(filename, 'utf8');
|
213 | const valids = source.toString()
|
214 | .split('\n')
|
215 | .filter((line) => !/^expect\.error\(/.test(line));
|
216 |
|
217 | const executed = [];
|
218 | let skipped = false;
|
219 | for (const line of valids) {
|
220 | if (/^\s*\/\/\s*\$lab\:types\:off\$/.test(line)) {
|
221 | skipped = true;
|
222 | continue;
|
223 | }
|
224 |
|
225 | if (/^\s*\/\/\s*\$lab\:types\:on\$/.test(line)) {
|
226 | skipped = false;
|
227 | continue;
|
228 | }
|
229 |
|
230 | if (/\/\/\s*\$lab\:types\:skip\$\s*[\n\r]*$/.test(line)) {
|
231 | continue;
|
232 | }
|
233 |
|
234 | if (skipped) {
|
235 | continue;
|
236 | }
|
237 |
|
238 | executed.push(line);
|
239 | }
|
240 |
|
241 | const sanitized = executed.join('\n');
|
242 |
|
243 | const result = Ts.transpileModule(sanitized, { compilerOptions: internals.compiler });
|
244 | let transpiled = result.outputText;
|
245 |
|
246 | if (/await/.test(transpiled)) {
|
247 | transpiled = `module.exports = async function () { ${transpiled} };\n`;
|
248 | }
|
249 |
|
250 | return localModule._compile(transpiled, filename);
|
251 | };
|