Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix 2 issues with experimentalWatchApi #1159

Merged
merged 7 commits into from Aug 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog

## v8.0.2

* [Fix 2 issues with experimentalWatchApi](https://github.com/TypeStrong/ts-loader/pull/1159) - thanks @appzuka

## v8.0.1

* [Fix webpack deprecations](https://github.com/TypeStrong/ts-loader/pull/1135) - thanks @g-plane
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "ts-loader",
"version": "8.0.1",
"version": "8.0.2",
"description": "TypeScript loader for webpack",
"main": "index.js",
"types": "dist",
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Expand Up @@ -383,6 +383,11 @@ function updateFileInCache(
}
}

// Added in case the files were already updated by the watch API
if (instance.modifiedFiles && instance.modifiedFiles.get(key)) {
fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Changed;
}

if (instance.watchHost !== undefined && fileWatcherEventKind !== undefined) {
instance.hasUnaccountedModifiedFiles =
instance.watchHost.invokeFileWatcher(filePath, fileWatcherEventKind) ||
Expand Down
7 changes: 4 additions & 3 deletions src/instances.ts
Expand Up @@ -689,15 +689,16 @@ export function getEmitFromWatchHost(instance: TSInstance, filePath?: string) {
if (!result) {
break;
}
if ((result.affected as typescript.SourceFile).fileName) {

// Only put the output file in the cache if the source came from webpack and
// was processed by the loaders
if (result.affected === sourceFile) {
instance.watchHost!.outputFiles.set(
instance.filePathKeyMapper(
(result.affected as typescript.SourceFile).fileName
),
outputFiles.slice()
);
}
if (result.affected === sourceFile) {
return outputFiles;
}
}
Expand Down
Expand Up @@ -94,7 +94,7 @@
/***/ (function(module, exports, __webpack_require__) {

"use strict";
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three);\n\n\n//# sourceURL=webpack:///./app.ts?");
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nvar helper_1 = __webpack_require__(/*! ./lib/helper */ \"./lib/helper.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three, helper_1.helper.four); // consume new number\n\n\n//# sourceURL=webpack:///./app.ts?");

/***/ }),

Expand Down
@@ -1,7 +1,8 @@
Asset Size Chunks Chunk Names
app.d.ts 11 bytes [emitted]
bundle.js 4.85 KiB main [emitted] main
Asset Size Chunks Chunk Names
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems incorrect from project references point of view. It should not write lib\tsconfig.tsbuildinfo file since there are errors in the build of project lib this will cause unknown issues as project cannot have tsbuild info file if there are errors. (not in 3.9 for sure)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for commenting @sheetalkamat! Would you advise reverting this?

In a side note, @appzuka has been observing that the watch API support (which I believe you originally added 🤗) does not improve performance of ts-loader. This is surprising given that it provides a massive performance boost to fork-ts-checker-webpack-plugin - however that does seem to be the case.

I'd always hoped that the watch API usage would become the default way to run ts-loader. The idea being that it stayed behind the experimentalWatchApi flag until it was fully functional and outperforming the servicehost approach. Then we'd drop the old approach in favour of the watch API. However, that day has not come sadly! Due to various bugs and the performance issue we're now thinking about potentially removing it.

What are your thoughts? I'm mindful you put a good amount of effort into implementation in the first place (which we're super grateful for BTW!) And also, if there's a simple thing ts-loader is doing slightly wrong here that would make the watch API perform I'd still love to make that happen. Again I can't forget that fork-ts-checker-webpack-plugin got turbo charged from using the watch API so there's a good chance we're doing something sub optimal.

What do you think?

cc @andrewbranch in case there's anything he'd like to add to this discussion too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the commit prior to this PR, in projectReferencesWatchRefWithTwoFilesAlreadyBuilt, patch2 is supposed to use the new number added to the helper in patch 1:

console.log(lib.one, lib.two, lib.three, helper.four); // consume new number

In the output bundle you can see the new number has been added:

eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nvar helper_1 = __webpack_require__(/*! ./lib/helper */ \"./lib/helper.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three, helper_1.helper.four); // consume new number\n\n\n//# sourceURL=webpack:///./app.ts?");

But in the watchAPI version it does not:

eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three);\n\n\n//# sourceURL=webpack:///./app.ts?");

And the same with the Composite_WatchApi

eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three);\n\n\n//# sourceURL=webpack:///./app.ts?");

Also note that tsconfig.tsbuildinfo is not written to the patch2 output:

https://github.com/TypeStrong/ts-loader/tree/a77470f9072bf24dc7165653c2ee86b243b65040/test/comparison-tests/projectReferencesWatchRefWithTwoFilesAlreadyBuilt_Composite_WatchApi/expectedOutput-3.9/patch2

After the PR#1159 the output of patch2 correctly includes the new content and the tsconfig.tsbuildinfo:

eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nvar helper_1 = __webpack_require__(/*! ./lib/helper */ \"./lib/helper.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three, helper_1.helper.four); // consume new number\n\n\n//# sourceURL=webpack:///./app.ts?");

Note that the new tsconfig.tsbuildinfo is in the top level directory not in the lib folder.

I was concerned that the PR changed the comparison test results but I believe the previous output was incorrect so the changes reflect the fixing of behaviour by the PR. The watchapi code is quite complex so there could be further issues which need to be fixed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@appzuka my bad.. i saw that tsbuildinfo as from lib project instead of root project. Change looks good.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnnyreilly Yes watchAPI should be faster and should see benefit.. I am investigating performance for internal team that uses ts-loader with solution builder work.. i can take a issue with watch API but i will need concrete repro steps so i can investigate. Watch API is kind of similar to solution builder API (how it handles file changes etc) so current perf work i am doing might help not sure. In the mean time if you can get repro where not using watch api is faster than watch api, i will be better equipped to investigate it. without that its just analysing code which i might not have enough time to do.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sheetalkamat - thanks so much!

@appzuka you've bumped on the problems with the performance of the watch API the most as you've been looking into this. I'm bearing in mind your comment here:

This PR fixes these issues. When I use this on a real project however experimentalWatchApi is very slow. Without experimentalWatchApi the build time for a larger project is around 37 seconds but with the watchApi it jumps to 168 seconds. This PR increases the build time to 199 seconds. Using transpileOnly and for-ts-checker the build time is reduced to 23 seconds.

Would you be up for providing some kind of demo repo which demonstrates performance being better with the non watch API vs the watch API?

No worries if you can't - I can try and get to it later (after my holidays)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To observe any performance change a large codebase will be needed. This will not just be a single file with many lines, but many files nested several levels, like a real, large project. I have created a repo which generates a codebase to test the various options. The repo is at:

https://github.com/appzuka/ts-loader-watchapitest.git

The Readme gives instructions to install and run and the results I get on my system.

Perhaps I have misunderstood how the new watchApi is supposed to work in ts-loader but I cannot see any reduction in build time at any stage. A simple build with expermentalWatchApi is over 2x the time using transpileOnly and fork-ts-checker-webpack-plugin and 30% slower than a normal build. Even incremental builds are much slower.

I love the --incremental feature of tsc, but it seems to me it is hard to integrate this into a webpack loader. Loaders should perform one simple task - they take the source text given to them by webpack and they return transformed text. ts-loader with transpileOnly does this very well.

Trying to implement the incremental behaviour means that the loader must consider the whole project every time webpack asks it to process every file. ts-loader handles this by caching the compiled files after the first ts file is built and recovering them from the cache for later files at the cost of greatly increasing the complexity of ts-loader.

It seems to me that incremental mode would be better implemented as a webpack plugin that compiles the entire project to a js-cache folder before webpack starts to compile the project. tsc would need to be called just once and Webpack would then build using the js files. This would still work in watch mode - tsc would update the files in the js-cache after a ts file was updated then webpack would recompile the project.

Apart from greatly simplifying the process, this would enable the performance gains of incremental mode to be used with webpack. If the js-cache persists between runs on the filesystem, subsequent builds will use the tsconfig.tsbuildinfo file to only recompile as necessary.

As a proof of concept I separated the tsc part of the build of a project from webpack. For my project I found that about 50% of the initial build time was taken by tsc and 50% by webpack. On the second build the tsc part dropped close to zero, as would be expected using incremental mode.

Perhaps fork-ts-checker-webpack-plugin works because it is a plugin - it runs just once for each build not once for every file.

There would be some disadvantages to this method. You could not use a webpack loader to transform the code before being processed by tsc. I assume fork-ts-checker-webpack-plugin suffers from this as well, but I doubt it is often a requirement in real code.

I could have a go at creating such a plugin. It would probably be separate from ts-loader.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have created a repo which generates a codebase to test the various options. The repo is at:

https://github.com/appzuka/ts-loader-watchapitest.git

Thanks @appzuka! @sheetalkamat you should find what you need here ☝️

Props on your plugin experiment; the results sound very interesting! Incidentally, fork-ts-checker-webpack-plugin is part of the TypeStrong family; if you fancied applying some of your experiments there @piotr-oles and myself may well be grateful 🤗

You could not use a webpack loader to transform the code before being processed by tsc. I assume fork-ts-checker-webpack-plugin suffers from this as well, but I doubt it is often a requirement in real code.

Funnily enough I think this was an issue and has been resolved.... I've been digging through recent PRs but I can't find the one in question. Pretty sure it's out there though!

app.d.ts 11 bytes [emitted]
bundle.js 4.97 KiB main [emitted] main
tsconfig.tsbuildinfo 1.72 KiB [emitted]
Entrypoint main = bundle.js
[./app.ts] 131 bytes {main} [built]
[./app.ts] 215 bytes {main} [built]
[./lib/helper.ts] 138 bytes {main}
[./lib/index.ts] 224 bytes {main}
@@ -0,0 +1,63 @@
{
"program": {
"fileInfos": {
"../../node_modules/typescript/lib/lib.d.ts": {
"version": "-10496480823",
"signature": "-10496480823",
"affectsGlobalScope": false
},
"../../node_modules/typescript/lib/lib.es5.d.ts": {
"version": "-218882352090",
"signature": "-218882352090",
"affectsGlobalScope": true
},
"../../node_modules/typescript/lib/lib.dom.d.ts": {
"version": "300634082611",
"signature": "300634082611",
"affectsGlobalScope": true
},
"../../node_modules/typescript/lib/lib.webworker.importscripts.d.ts": {
"version": "-24714112149",
"signature": "-24714112149",
"affectsGlobalScope": true
},
"../../node_modules/typescript/lib/lib.scripthost.d.ts": {
"version": "204309182321",
"signature": "204309182321",
"affectsGlobalScope": true
},
"./lib/index.d.ts": {
"version": "11215156582",
"signature": "11215156582",
"affectsGlobalScope": false
},
"./lib/helper.d.ts": {
"version": "7897218607",
"signature": "7897218607",
"affectsGlobalScope": false
},
"./app.ts": {
"version": "-12553192154",
"signature": "-3531856636",
"affectsGlobalScope": false
}
},
"options": {
"types": [],
"composite": true,
"newLine": 1,
"configFilePath": "./tsconfig.json",
"skipLibCheck": true,
"suppressOutputPathCheck": true
},
"referencedMap": {
"./app.ts": [
"./lib/helper.d.ts",
"./lib/index.d.ts"
]
},
"exportedModulesMap": {},
"semanticDiagnosticsPerFile": []
},
"version": "3.9.3"
}
Expand Up @@ -94,7 +94,7 @@
/***/ (function(module, exports, __webpack_require__) {

"use strict";
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three);\n\n\n//# sourceURL=webpack:///./app.ts?");
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nvar helper_1 = __webpack_require__(/*! ./lib/helper */ \"./lib/helper.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three, helper_1.helper.four); // consume new number\n\n\n//# sourceURL=webpack:///./app.ts?");

/***/ }),

Expand Down
Expand Up @@ -94,7 +94,7 @@
/***/ (function(module, exports, __webpack_require__) {

"use strict";
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three);\n\n\n//# sourceURL=webpack:///./app.ts?");
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nvar helper_1 = __webpack_require__(/*! ./lib/helper */ \"./lib/helper.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three, helper_1.helper.four); // consume new number\n\n\n//# sourceURL=webpack:///./app.ts?");

/***/ }),

Expand Down
@@ -1,7 +1,8 @@
Asset Size Chunks Chunk Names
app.d.ts 11 bytes [emitted]
bundle.js 4.83 KiB main [emitted] main
Asset Size Chunks Chunk Names
app.d.ts 11 bytes [emitted]
bundle.js 4.95 KiB main [emitted] main
tsconfig.tsbuildinfo 1.72 KiB [emitted]
Entrypoint main = bundle.js
[./app.ts] 131 bytes {main} [built]
[./app.ts] 215 bytes {main} [built]
[./lib/helper.ts] 138 bytes {main}
[./lib/index.ts] 211 bytes {main}
@@ -0,0 +1,63 @@
{
"program": {
"fileInfos": {
"../../node_modules/typescript/lib/lib.d.ts": {
"version": "-10496480823",
"signature": "-10496480823",
"affectsGlobalScope": false
},
"../../node_modules/typescript/lib/lib.es5.d.ts": {
"version": "-218882352090",
"signature": "-218882352090",
"affectsGlobalScope": true
},
"../../node_modules/typescript/lib/lib.dom.d.ts": {
"version": "300634082611",
"signature": "300634082611",
"affectsGlobalScope": true
},
"../../node_modules/typescript/lib/lib.webworker.importscripts.d.ts": {
"version": "-24714112149",
"signature": "-24714112149",
"affectsGlobalScope": true
},
"../../node_modules/typescript/lib/lib.scripthost.d.ts": {
"version": "204309182321",
"signature": "204309182321",
"affectsGlobalScope": true
},
"./lib/index.d.ts": {
"version": "12503634626",
"signature": "12503634626",
"affectsGlobalScope": false
},
"./lib/helper.d.ts": {
"version": "7897218607",
"signature": "7897218607",
"affectsGlobalScope": false
},
"./app.ts": {
"version": "-12553192154",
"signature": "-3531856636",
"affectsGlobalScope": false
}
},
"options": {
"types": [],
"composite": true,
"newLine": 1,
"configFilePath": "./tsconfig.json",
"skipLibCheck": true,
"suppressOutputPathCheck": true
},
"referencedMap": {
"./app.ts": [
"./lib/helper.d.ts",
"./lib/index.d.ts"
]
},
"exportedModulesMap": {},
"semanticDiagnosticsPerFile": []
},
"version": "3.9.3"
}
Expand Up @@ -94,7 +94,7 @@
/***/ (function(module, exports, __webpack_require__) {

"use strict";
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three);\n\n\n//# sourceURL=webpack:///./app.ts?");
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nvar helper_1 = __webpack_require__(/*! ./lib/helper */ \"./lib/helper.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three, helper_1.helper.four); // consume new number\n\n\n//# sourceURL=webpack:///./app.ts?");

/***/ }),

Expand Down
Expand Up @@ -94,7 +94,7 @@
/***/ (function(module, exports, __webpack_require__) {

"use strict";
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three);\n\n\n//# sourceURL=webpack:///./app.ts?");
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three, lib_1.lib.four); // consume new number\n\n\n//# sourceURL=webpack:///./app.ts?");

/***/ }),

Expand Down
@@ -1,6 +1,7 @@
Asset Size Chunks Chunk Names
app.d.ts 11 bytes [emitted]
bundle.js 4.33 KiB main [emitted] main
Asset Size Chunks Chunk Names
app.d.ts 11 bytes [emitted]
bundle.js 4.37 KiB main [emitted] main
tsconfig.tsbuildinfo 1.56 KiB [emitted]
Entrypoint main = bundle.js
[./app.ts] 131 bytes {main} [built]
[./app.ts] 169 bytes {main} [built]
[./lib/index.ts] 150 bytes {main}
@@ -0,0 +1,57 @@
{
"program": {
"fileInfos": {
"../../node_modules/typescript/lib/lib.d.ts": {
"version": "-10496480823",
"signature": "-10496480823",
"affectsGlobalScope": false
},
"../../node_modules/typescript/lib/lib.es5.d.ts": {
"version": "-218882352090",
"signature": "-218882352090",
"affectsGlobalScope": true
},
"../../node_modules/typescript/lib/lib.dom.d.ts": {
"version": "300634082611",
"signature": "300634082611",
"affectsGlobalScope": true
},
"../../node_modules/typescript/lib/lib.webworker.importscripts.d.ts": {
"version": "-24714112149",
"signature": "-24714112149",
"affectsGlobalScope": true
},
"../../node_modules/typescript/lib/lib.scripthost.d.ts": {
"version": "204309182321",
"signature": "204309182321",
"affectsGlobalScope": true
},
"./lib/index.d.ts": {
"version": "11215156582",
"signature": "11215156582",
"affectsGlobalScope": false
},
"./app.ts": {
"version": "-16299197056",
"signature": "-3531856636",
"affectsGlobalScope": false
}
},
"options": {
"types": [],
"composite": true,
"newLine": 1,
"configFilePath": "./tsconfig.json",
"skipLibCheck": true,
"suppressOutputPathCheck": true
},
"referencedMap": {
"./app.ts": [
"./lib/index.d.ts"
]
},
"exportedModulesMap": {},
"semanticDiagnosticsPerFile": []
},
"version": "3.9.3"
}
Expand Up @@ -94,7 +94,7 @@
/***/ (function(module, exports, __webpack_require__) {

"use strict";
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three);\n\n\n//# sourceURL=webpack:///./app.ts?");
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three, lib_1.lib.four); // consume new number\n\n\n//# sourceURL=webpack:///./app.ts?");

/***/ }),

Expand Down
Expand Up @@ -94,7 +94,7 @@
/***/ (function(module, exports, __webpack_require__) {

"use strict";
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three, lib_1.lib.four); // consume new number\n\n\n//# sourceURL=webpack:///./app.ts?");
eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */ \"./lib/index.ts\");\nconsole.log(lib_1.lib.one, lib_1.lib.two, lib_1.lib.three, lib_1.lib.four, lib_1.lib.ffive); // consume new number\n\n\n//# sourceURL=webpack:///./app.ts?");

/***/ }),

Expand All @@ -106,7 +106,7 @@ eval("\nexports.__esModule = true;\nvar lib_1 = __webpack_require__(/*! ./lib */
/***/ (function(module, exports, __webpack_require__) {

"use strict";
eval("\r\nexports.__esModule = true;\r\nexports.lib = void 0;\r\nexports.lib = {\r\n one: 1,\r\n two: 2,\r\n three: 3,\r\n four: 4,\r\n five: 5\r\n};\r\n\n\n//# sourceURL=webpack:///./lib/index.ts?");
eval("\nexports.__esModule = true;\nexports.lib = void 0;\nexports.lib = {\n one: 1,\n two: 2,\n three: 3,\n four: 4,\n five: 5\n};\n\n\n//# sourceURL=webpack:///./lib/index.ts?");

/***/ })

Expand Down
@@ -1,6 +1,12 @@
Asset Size Chunks Chunk Names
app.d.ts 11 bytes [emitted]
bundle.js 4.37 KiB main [emitted] main
Asset Size Chunks Chunk Names
app.d.ts 11 bytes [emitted]
bundle.js 4.38 KiB main [emitted] main
tsconfig.tsbuildinfo 1.56 KiB [emitted]
Entrypoint main = bundle.js
[./app.ts] 169 bytes {main} [built]
[./lib/index.ts] 145 bytes {main}
[./app.ts] 186 bytes {main} [built] [1 error]
[./lib/index.ts] 145 bytes {main}

ERROR in app.ts
./app.ts
[tsl] ERROR in app.ts(3,56)
 TS2551: Property 'ffive' does not exist on type '{ one: number; two: number; three: number; four: number; five: number; }'. Did you mean 'five'?