UNPKG

27.4 kBJavaScriptView Raw
1"use strict";
2const path = require("path");
3const process = require("process");
4const childProcess = require("child_process");
5const worker_rpc_1 = require("worker-rpc");
6const semver = require("semver");
7const chalk_1 = require("chalk");
8const micromatch = require("micromatch");
9const os = require("os");
10const CancellationToken_1 = require("./CancellationToken");
11const NormalizedMessage_1 = require("./NormalizedMessage");
12const defaultFormatter_1 = require("./formatter/defaultFormatter");
13const codeframeFormatter_1 = require("./formatter/codeframeFormatter");
14const FsHelper_1 = require("./FsHelper");
15const hooks_1 = require("./hooks");
16const RpcTypes_1 = require("./RpcTypes");
17const checkerPluginName = 'fork-ts-checker-webpack-plugin';
18/**
19 * ForkTsCheckerWebpackPlugin
20 * Runs typescript type checker and linter (tslint) on separate process.
21 * This speed-ups build a lot.
22 *
23 * Options description in README.md
24 */
25class ForkTsCheckerWebpackPlugin {
26 constructor(options) {
27 this.startAt = 0;
28 this.nodeArgs = [];
29 this.computeContextPath = (filePath) => path.isAbsolute(filePath)
30 ? filePath
31 : path.resolve(this.compiler.options.context, filePath);
32 options = options || {};
33 this.options = Object.assign({}, options);
34 this.tsconfig = options.tsconfig || './tsconfig.json';
35 this.compilerOptions =
36 typeof options.compilerOptions === 'object'
37 ? options.compilerOptions
38 : {};
39 this.tslint = options.tslint
40 ? options.tslint === true
41 ? true
42 : options.tslint
43 : undefined;
44 this.tslintAutoFix = options.tslintAutoFix || false;
45 this.watch =
46 typeof options.watch === 'string' ? [options.watch] : options.watch || [];
47 this.ignoreDiagnostics = options.ignoreDiagnostics || [];
48 this.ignoreLints = options.ignoreLints || [];
49 this.ignoreLintWarnings = options.ignoreLintWarnings === true;
50 this.reportFiles = options.reportFiles || [];
51 this.logger = options.logger || console;
52 this.silent = options.silent === true; // default false
53 this.async = options.async !== false; // default true
54 this.checkSyntacticErrors = options.checkSyntacticErrors === true; // default false
55 this.resolveModuleNameModule = options.resolveModuleNameModule;
56 this.resolveTypeReferenceDirectiveModule =
57 options.resolveTypeReferenceDirectiveModule;
58 this.workersNumber = options.workers || ForkTsCheckerWebpackPlugin.ONE_CPU;
59 this.memoryLimit =
60 options.memoryLimit || ForkTsCheckerWebpackPlugin.DEFAULT_MEMORY_LIMIT;
61 this.useColors = options.colors !== false; // default true
62 this.colors = new chalk_1.default.constructor({ enabled: this.useColors });
63 this.formatter =
64 options.formatter && typeof options.formatter === 'function'
65 ? options.formatter
66 : ForkTsCheckerWebpackPlugin.createFormatter(options.formatter || 'default', options.formatterOptions || {});
67 this.tsconfigPath = undefined;
68 this.tslintPath = undefined;
69 this.watchPaths = [];
70 this.compiler = undefined;
71 this.started = undefined;
72 this.elapsed = undefined;
73 this.cancellationToken = undefined;
74 this.isWatching = false;
75 this.checkDone = false;
76 this.compilationDone = false;
77 this.diagnostics = [];
78 this.lints = [];
79 this.emitCallback = this.createNoopEmitCallback();
80 this.doneCallback = this.createDoneCallback();
81 this.typescriptPath = options.typescript || require.resolve('typescript');
82 try {
83 this.typescript = require(this.typescriptPath);
84 this.typescriptVersion = this.typescript.version;
85 }
86 catch (_ignored) {
87 throw new Error('When you use this plugin you must install `typescript`.');
88 }
89 try {
90 this.tslintVersion = this.tslint
91 ? // tslint:disable-next-line:no-implicit-dependencies
92 require('tslint').Linter.VERSION
93 : undefined;
94 }
95 catch (_ignored) {
96 throw new Error('When you use `tslint` option, make sure to install `tslint`.');
97 }
98 this.validateVersions();
99 this.vue = options.vue === true; // default false
100 this.useTypescriptIncrementalApi =
101 options.useTypescriptIncrementalApi === undefined
102 ? semver.gte(this.typescriptVersion, '3.0.0') && !this.vue
103 : options.useTypescriptIncrementalApi;
104 this.measureTime = options.measureCompilationTime === true;
105 if (this.measureTime) {
106 // Node 8+ only
107 this.performance = require('perf_hooks').performance;
108 }
109 }
110 static getCompilerHooks(compiler) {
111 return hooks_1.getForkTsCheckerWebpackPluginHooks(compiler);
112 }
113 validateVersions() {
114 if (semver.lt(this.typescriptVersion, '2.1.0')) {
115 throw new Error(`Cannot use current typescript version of ${this.typescriptVersion}, the minimum required version is 2.1.0`);
116 }
117 else if (this.tslintVersion && semver.lt(this.tslintVersion, '4.0.0')) {
118 throw new Error(`Cannot use current tslint version of ${this.tslintVersion}, the minimum required version is 4.0.0`);
119 }
120 }
121 static createFormatter(type, options) {
122 switch (type) {
123 case 'default':
124 return defaultFormatter_1.createDefaultFormatter();
125 case 'codeframe':
126 return codeframeFormatter_1.createCodeframeFormatter(options);
127 default:
128 throw new Error('Unknown "' + type + '" formatter. Available are: default, codeframe.');
129 }
130 }
131 apply(compiler) {
132 this.compiler = compiler;
133 this.tsconfigPath = this.computeContextPath(this.tsconfig);
134 this.tslintPath =
135 typeof this.tslint === 'string'
136 ? this.computeContextPath(this.tslint)
137 : undefined;
138 this.watchPaths = this.watch.map(this.computeContextPath);
139 // validate config
140 const tsconfigOk = FsHelper_1.FsHelper.existsSync(this.tsconfigPath);
141 const tslintOk = !this.tslintPath || FsHelper_1.FsHelper.existsSync(this.tslintPath);
142 if (this.useTypescriptIncrementalApi && this.workersNumber !== 1) {
143 throw new Error('Using typescript incremental compilation API ' +
144 'is currently only allowed with a single worker.');
145 }
146 // validate logger
147 if (this.logger) {
148 if (!this.logger.error || !this.logger.warn || !this.logger.info) {
149 throw new Error("Invalid logger object - doesn't provide `error`, `warn` or `info` method.");
150 }
151 }
152 if (tsconfigOk && tslintOk) {
153 this.pluginStart();
154 this.pluginStop();
155 this.pluginCompile();
156 this.pluginEmit();
157 this.pluginDone();
158 }
159 else {
160 if (!tsconfigOk) {
161 throw new Error('Cannot find "' +
162 this.tsconfigPath +
163 '" file. Please check webpack and ForkTsCheckerWebpackPlugin configuration. \n' +
164 'Possible errors: \n' +
165 ' - wrong `context` directory in webpack configuration' +
166 ' (if `tsconfig` is not set or is a relative path in fork plugin configuration)\n' +
167 ' - wrong `tsconfig` path in fork plugin configuration' +
168 ' (should be a relative or absolute path)');
169 }
170 if (!tslintOk) {
171 throw new Error('Cannot find "' +
172 this.tslintPath +
173 '" file. Please check webpack and ForkTsCheckerWebpackPlugin configuration. \n' +
174 'Possible errors: \n' +
175 ' - wrong `context` directory in webpack configuration' +
176 ' (if `tslint` is not set or is a relative path in fork plugin configuration)\n' +
177 ' - wrong `tslint` path in fork plugin configuration' +
178 ' (should be a relative or absolute path)\n' +
179 ' - `tslint` path is not set to false in fork plugin configuration' +
180 ' (if you want to disable tslint support)');
181 }
182 }
183 }
184 pluginStart() {
185 const run = (_compiler, callback) => {
186 this.isWatching = false;
187 callback();
188 };
189 const watchRun = (_compiler, callback) => {
190 this.isWatching = true;
191 callback();
192 };
193 if ('hooks' in this.compiler) {
194 // webpack 4+
195 this.compiler.hooks.run.tapAsync(checkerPluginName, run);
196 this.compiler.hooks.watchRun.tapAsync(checkerPluginName, watchRun);
197 }
198 else {
199 // webpack 2 / 3
200 this.compiler.plugin('run', run);
201 this.compiler.plugin('watch-run', watchRun);
202 }
203 }
204 pluginStop() {
205 const watchClose = () => {
206 this.killService();
207 };
208 const done = (_stats) => {
209 if (!this.isWatching) {
210 this.killService();
211 }
212 };
213 if ('hooks' in this.compiler) {
214 // webpack 4+
215 this.compiler.hooks.watchClose.tap(checkerPluginName, watchClose);
216 this.compiler.hooks.done.tap(checkerPluginName, done);
217 }
218 else {
219 // webpack 2 / 3
220 this.compiler.plugin('watch-close', watchClose);
221 this.compiler.plugin('done', done);
222 }
223 process.on('exit', () => {
224 this.killService();
225 });
226 }
227 pluginCompile() {
228 if ('hooks' in this.compiler) {
229 // webpack 4+
230 const forkTsCheckerHooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(this.compiler);
231 this.compiler.hooks.compile.tap(checkerPluginName, () => {
232 this.compilationDone = false;
233 forkTsCheckerHooks.serviceBeforeStart.callAsync(() => {
234 if (this.cancellationToken) {
235 // request cancellation if there is not finished job
236 this.cancellationToken.requestCancellation();
237 forkTsCheckerHooks.cancel.call(this.cancellationToken);
238 }
239 this.checkDone = false;
240 this.started = process.hrtime();
241 // create new token for current job
242 this.cancellationToken = new CancellationToken_1.CancellationToken(this.typescript);
243 if (!this.service || !this.service.connected) {
244 this.spawnService();
245 }
246 try {
247 if (this.measureTime) {
248 this.startAt = this.performance.now();
249 }
250 this.serviceRpc.rpc(RpcTypes_1.RUN, this.cancellationToken.toJSON()).then(result => {
251 if (result) {
252 this.handleServiceMessage(result);
253 }
254 });
255 }
256 catch (error) {
257 if (!this.silent && this.logger) {
258 this.logger.error(this.colors.red('Cannot start checker service: ' +
259 (error ? error.toString() : 'Unknown error')));
260 }
261 forkTsCheckerHooks.serviceStartError.call(error);
262 }
263 });
264 });
265 }
266 else {
267 // webpack 2 / 3
268 this.compiler.plugin('compile', () => {
269 this.compilationDone = false;
270 this.compiler.applyPluginsAsync(hooks_1.legacyHookMap.serviceBeforeStart, () => {
271 if (this.cancellationToken) {
272 // request cancellation if there is not finished job
273 this.cancellationToken.requestCancellation();
274 this.compiler.applyPlugins(hooks_1.legacyHookMap.cancel, this.cancellationToken);
275 }
276 this.checkDone = false;
277 this.started = process.hrtime();
278 // create new token for current job
279 this.cancellationToken = new CancellationToken_1.CancellationToken(this.typescript, undefined, undefined);
280 if (!this.service || !this.service.connected) {
281 this.spawnService();
282 }
283 try {
284 this.serviceRpc.rpc(RpcTypes_1.RUN, this.cancellationToken.toJSON()).then(result => {
285 if (result) {
286 this.handleServiceMessage(result);
287 }
288 });
289 }
290 catch (error) {
291 if (!this.silent && this.logger) {
292 this.logger.error(this.colors.red('Cannot start checker service: ' +
293 (error ? error.toString() : 'Unknown error')));
294 }
295 this.compiler.applyPlugins(hooks_1.legacyHookMap.serviceStartError, error);
296 }
297 });
298 });
299 }
300 }
301 pluginEmit() {
302 const emit = (compilation, callback) => {
303 if (this.isWatching && this.async) {
304 callback();
305 return;
306 }
307 this.emitCallback = this.createEmitCallback(compilation, callback);
308 if (this.checkDone) {
309 this.emitCallback();
310 }
311 this.compilationDone = true;
312 };
313 if ('hooks' in this.compiler) {
314 // webpack 4+
315 this.compiler.hooks.emit.tapAsync(checkerPluginName, emit);
316 }
317 else {
318 // webpack 2 / 3
319 this.compiler.plugin('emit', emit);
320 }
321 }
322 pluginDone() {
323 if ('hooks' in this.compiler) {
324 // webpack 4+
325 const forkTsCheckerHooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(this.compiler);
326 this.compiler.hooks.done.tap(checkerPluginName, (_stats) => {
327 if (!this.isWatching || !this.async) {
328 return;
329 }
330 if (this.checkDone) {
331 this.doneCallback();
332 }
333 else {
334 if (this.compiler) {
335 forkTsCheckerHooks.waiting.call(this.tslint !== false);
336 }
337 if (!this.silent && this.logger) {
338 this.logger.info(this.tslint
339 ? 'Type checking and linting in progress...'
340 : 'Type checking in progress...');
341 }
342 }
343 this.compilationDone = true;
344 });
345 }
346 else {
347 // webpack 2 / 3
348 this.compiler.plugin('done', () => {
349 if (!this.isWatching || !this.async) {
350 return;
351 }
352 if (this.checkDone) {
353 this.doneCallback();
354 }
355 else {
356 if (this.compiler) {
357 this.compiler.applyPlugins(hooks_1.legacyHookMap.waiting, this.tslint !== false);
358 }
359 if (!this.silent && this.logger) {
360 this.logger.info(this.tslint
361 ? 'Type checking and linting in progress...'
362 : 'Type checking in progress...');
363 }
364 }
365 this.compilationDone = true;
366 });
367 }
368 }
369 spawnService() {
370 const env = Object.assign({}, process.env, { TYPESCRIPT_PATH: this.typescriptPath, TSCONFIG: this.tsconfigPath, COMPILER_OPTIONS: JSON.stringify(this.compilerOptions), TSLINT: this.tslintPath || (this.tslint ? 'true' : ''), CONTEXT: this.compiler.options.context, TSLINTAUTOFIX: String(this.tslintAutoFix), WATCH: this.isWatching ? this.watchPaths.join('|') : '', WORK_DIVISION: String(Math.max(1, this.workersNumber)), MEMORY_LIMIT: String(this.memoryLimit), CHECK_SYNTACTIC_ERRORS: String(this.checkSyntacticErrors), USE_INCREMENTAL_API: String(this.useTypescriptIncrementalApi === true), VUE: String(this.vue) });
371 if (typeof this.resolveModuleNameModule !== 'undefined') {
372 env.RESOLVE_MODULE_NAME = this.resolveModuleNameModule;
373 }
374 else {
375 delete env.RESOLVE_MODULE_NAME;
376 }
377 if (typeof this.resolveTypeReferenceDirectiveModule !== 'undefined') {
378 env.RESOLVE_TYPE_REFERENCE_DIRECTIVE = this.resolveTypeReferenceDirectiveModule;
379 }
380 else {
381 delete env.RESOLVE_TYPE_REFERENCE_DIRECTIVE;
382 }
383 this.service = childProcess.fork(path.resolve(__dirname, this.workersNumber > 1 ? './cluster.js' : './service.js'), [], {
384 env,
385 execArgv: (this.workersNumber > 1
386 ? []
387 : ['--max-old-space-size=' + this.memoryLimit]).concat(this.nodeArgs),
388 stdio: ['inherit', 'inherit', 'inherit', 'ipc']
389 });
390 this.serviceRpc = new worker_rpc_1.RpcProvider(message => this.service.send(message));
391 this.service.on('message', message => this.serviceRpc.dispatch(message));
392 if ('hooks' in this.compiler) {
393 // webpack 4+
394 const forkTsCheckerHooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(this.compiler);
395 forkTsCheckerHooks.serviceStart.call(this.tsconfigPath, this.tslintPath, this.watchPaths, this.workersNumber, this.memoryLimit);
396 }
397 else {
398 // webpack 2 / 3
399 this.compiler.applyPlugins(hooks_1.legacyHookMap.serviceStart, this.tsconfigPath, this.tslintPath, this.watchPaths, this.workersNumber, this.memoryLimit);
400 }
401 if (!this.silent && this.logger) {
402 this.logger.info('Starting type checking' +
403 (this.tslint ? ' and linting' : '') +
404 ' service...');
405 this.logger.info('Using ' +
406 this.colors.bold(this.workersNumber === 1
407 ? '1 worker'
408 : this.workersNumber + ' workers') +
409 ' with ' +
410 this.colors.bold(this.memoryLimit + 'MB') +
411 ' memory limit');
412 if (this.watchPaths.length && this.isWatching) {
413 this.logger.info('Watching:' +
414 (this.watchPaths.length > 1 ? '\n' : ' ') +
415 this.watchPaths.map(wpath => this.colors.grey(wpath)).join('\n'));
416 }
417 }
418 this.service.on('exit', (code, signal) => this.handleServiceExit(code, signal));
419 }
420 killService() {
421 if (!this.service) {
422 return;
423 }
424 try {
425 if (this.cancellationToken) {
426 this.cancellationToken.cleanupCancellation();
427 }
428 this.service.kill();
429 this.service = undefined;
430 this.serviceRpc = undefined;
431 }
432 catch (e) {
433 if (this.logger && !this.silent) {
434 this.logger.error(e);
435 }
436 }
437 }
438 handleServiceMessage(message) {
439 if (this.measureTime) {
440 const delta = this.performance.now() - this.startAt;
441 const deltaRounded = Math.round(delta * 100) / 100;
442 this.logger.info(`Compilation took: ${deltaRounded} ms.`);
443 }
444 if (this.cancellationToken) {
445 this.cancellationToken.cleanupCancellation();
446 // job is done - nothing to cancel
447 this.cancellationToken = undefined;
448 }
449 this.checkDone = true;
450 this.elapsed = process.hrtime(this.started);
451 this.diagnostics = message.diagnostics.map(NormalizedMessage_1.NormalizedMessage.createFromJSON);
452 this.lints = message.lints.map(NormalizedMessage_1.NormalizedMessage.createFromJSON);
453 if (this.ignoreDiagnostics.length) {
454 this.diagnostics = this.diagnostics.filter(diagnostic => !this.ignoreDiagnostics.includes(parseInt(diagnostic.code, 10)));
455 }
456 if (this.ignoreLints.length) {
457 this.lints = this.lints.filter(lint => !this.ignoreLints.includes(lint.code));
458 }
459 if (this.reportFiles.length) {
460 const reportFilesPredicate = (diagnostic) => {
461 if (diagnostic.file) {
462 const relativeFileName = path.relative(this.compiler.options.context, diagnostic.file);
463 const matchResult = micromatch([relativeFileName], this.reportFiles);
464 if (matchResult.length === 0) {
465 return false;
466 }
467 }
468 return true;
469 };
470 this.diagnostics = this.diagnostics.filter(reportFilesPredicate);
471 this.lints = this.lints.filter(reportFilesPredicate);
472 }
473 if ('hooks' in this.compiler) {
474 // webpack 4+
475 const forkTsCheckerHooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(this.compiler);
476 forkTsCheckerHooks.receive.call(this.diagnostics, this.lints);
477 }
478 else {
479 // webpack 2 / 3
480 this.compiler.applyPlugins(hooks_1.legacyHookMap.receive, this.diagnostics, this.lints);
481 }
482 if (this.compilationDone) {
483 this.isWatching && this.async ? this.doneCallback() : this.emitCallback();
484 }
485 }
486 handleServiceExit(_code, signal) {
487 if (signal !== 'SIGABRT') {
488 return;
489 }
490 // probably out of memory :/
491 if (this.compiler) {
492 if ('hooks' in this.compiler) {
493 // webpack 4+
494 const forkTsCheckerHooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(this.compiler);
495 forkTsCheckerHooks.serviceOutOfMemory.call();
496 }
497 else {
498 // webpack 2 / 3
499 this.compiler.applyPlugins(hooks_1.legacyHookMap.serviceOutOfMemory);
500 }
501 }
502 if (!this.silent && this.logger) {
503 this.logger.error(this.colors.red('Type checking and linting aborted - probably out of memory. ' +
504 'Check `memoryLimit` option in ForkTsCheckerWebpackPlugin configuration.'));
505 }
506 }
507 createEmitCallback(compilation, callback) {
508 return function emitCallback() {
509 if (!this.elapsed) {
510 throw new Error('Execution order error');
511 }
512 const elapsed = Math.round(this.elapsed[0] * 1e9 + this.elapsed[1]);
513 if ('hooks' in this.compiler) {
514 // webpack 4+
515 const forkTsCheckerHooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(this.compiler);
516 forkTsCheckerHooks.emit.call(this.diagnostics, this.lints, elapsed);
517 }
518 else {
519 // webpack 2 / 3
520 this.compiler.applyPlugins(hooks_1.legacyHookMap.emit, this.diagnostics, this.lints, elapsed);
521 }
522 this.diagnostics.concat(this.lints).forEach(message => {
523 // webpack message format
524 const formatted = {
525 rawMessage: message.severity.toUpperCase() +
526 ' ' +
527 message.getFormattedCode() +
528 ': ' +
529 message.content,
530 message: this.formatter(message, this.useColors),
531 location: {
532 line: message.line,
533 character: message.character
534 },
535 file: message.file
536 };
537 if (message.isWarningSeverity()) {
538 if (!this.ignoreLintWarnings) {
539 compilation.warnings.push(formatted);
540 }
541 }
542 else {
543 compilation.errors.push(formatted);
544 }
545 });
546 callback();
547 };
548 }
549 createNoopEmitCallback() {
550 // tslint:disable-next-line:no-empty
551 return function noopEmitCallback() { };
552 }
553 printLoggerMessage(message, formattedMessage) {
554 if (message.isWarningSeverity()) {
555 if (this.ignoreLintWarnings) {
556 return;
557 }
558 this.logger.warn(formattedMessage);
559 }
560 else {
561 this.logger.error(formattedMessage);
562 }
563 }
564 createDoneCallback() {
565 return function doneCallback() {
566 if (!this.elapsed) {
567 throw new Error('Execution order error');
568 }
569 const elapsed = Math.round(this.elapsed[0] * 1e9 + this.elapsed[1]);
570 if (this.compiler) {
571 if ('hooks' in this.compiler) {
572 // webpack 4+
573 const forkTsCheckerHooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(this.compiler);
574 forkTsCheckerHooks.done.call(this.diagnostics, this.lints, elapsed);
575 }
576 else {
577 // webpack 2 / 3
578 this.compiler.applyPlugins(hooks_1.legacyHookMap.done, this.diagnostics, this.lints, elapsed);
579 }
580 }
581 if (!this.silent && this.logger) {
582 if (this.diagnostics.length || this.lints.length) {
583 (this.lints || []).concat(this.diagnostics).forEach(message => {
584 const formattedMessage = this.formatter(message, this.useColors);
585 this.printLoggerMessage(message, formattedMessage);
586 });
587 }
588 if (!this.diagnostics.length) {
589 this.logger.info(this.colors.green('No type errors found'));
590 }
591 if (this.tslint && !this.lints.length) {
592 this.logger.info(this.colors.green('No lint errors found'));
593 }
594 this.logger.info('Version: typescript ' +
595 this.colors.bold(this.typescriptVersion) +
596 (this.tslint
597 ? ', tslint ' + this.colors.bold(this.tslintVersion)
598 : ''));
599 this.logger.info('Time: ' +
600 this.colors.bold(Math.round(elapsed / 1e6).toString()) +
601 'ms');
602 }
603 };
604 }
605}
606ForkTsCheckerWebpackPlugin.DEFAULT_MEMORY_LIMIT = 2048;
607ForkTsCheckerWebpackPlugin.ONE_CPU = 1;
608ForkTsCheckerWebpackPlugin.ALL_CPUS = os.cpus && os.cpus() ? os.cpus().length : 1;
609ForkTsCheckerWebpackPlugin.ONE_CPU_FREE = Math.max(1, ForkTsCheckerWebpackPlugin.ALL_CPUS - 1);
610ForkTsCheckerWebpackPlugin.TWO_CPUS_FREE = Math.max(1, ForkTsCheckerWebpackPlugin.ALL_CPUS - 2);
611module.exports = ForkTsCheckerWebpackPlugin;
612//# sourceMappingURL=index.js.map
\No newline at end of file