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

re-export common packages #9124

Merged
merged 2 commits into from
Mar 29, 2021
Merged

Conversation

paul-marechal
Copy link
Member

What it does

The goal of this refactoring is to make downstream application builds more stable by removing implicit dependencies and exporting common packages from @theia/core/shared/<package>.

This method should not break dependents. See commit message for explanation.

Fixes #8403

Most changes are just about updating all locations where implicit dependencies are used. I think this is a good change for the framework as it is an opt-in feature, doesn't break dependents, and improves the build stability for application developers making use of it.

How to test

This is a refactoring: Everything should work like before.

Review checklist

Reminder for reviewers

@paul-marechal paul-marechal force-pushed the mp/no-extraneous-dependencies branch 4 times, most recently from e19ffe3 to 6944273 Compare February 25, 2021 17:08
@paul-marechal
Copy link
Member Author

Ok so the last step is to find a way to configure webpack to ignore non-browser stuff from @theia/application-package and it should work just like before. I might break the environment file apart in its own package as the simpliest solution.

@paul-marechal paul-marechal force-pushed the mp/no-extraneous-dependencies branch 2 times, most recently from 6086d20 to 8266abb Compare February 26, 2021 01:34
@paul-marechal paul-marechal marked this pull request as ready for review February 26, 2021 01:35
@paul-marechal
Copy link
Member Author

paul-marechal commented Feb 26, 2021

Fixed CI and the frontend build by re-exporting @theia/application-package/lib/environment as @theia/core/shared/application-package/lib/environment. I did not need to create a new package nor edit webpack configs.

@kittaakos
Copy link
Contributor

Related: #7303

@paul-marechal paul-marechal added core issues related to the core of the application dependencies pull requests that update a dependency file enhancement issues that are enhancements to current functionality - nice to haves labels Feb 26, 2021
@paul-marechal paul-marechal force-pushed the mp/no-extraneous-dependencies branch from 8266abb to 049121a Compare February 26, 2021 16:55
@paul-marechal
Copy link
Member Author

paul-marechal commented Feb 26, 2021

Added links to the respective npm page for each re-exported package in EXPORTS.md.

@paul-marechal paul-marechal force-pushed the mp/no-extraneous-dependencies branch from 049121a to b2f89fe Compare February 26, 2021 17:14
@paul-marechal
Copy link
Member Author

paul-marechal commented Feb 26, 2021

Updated packages/core/README.md to list the re-exported packages instead of being in its own file. It makes discovering the mechanism easier. The readme is now autogenerated based on the contents of README.in.md.

@paul-marechal paul-marechal force-pushed the mp/no-extraneous-dependencies branch 7 times, most recently from 3234565 to 90463e6 Compare March 2, 2021 19:15
@paul-marechal paul-marechal force-pushed the mp/no-extraneous-dependencies branch 2 times, most recently from 626ec02 to 1ffb1ac Compare March 8, 2021 20:30
Copy link
Member

@vince-fugnitto vince-fugnitto left a comment

Choose a reason for hiding this comment

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

I confirmed the following:

The no-extraneous-dependencies rule works well (fixes #7079 if you'd like to automatically close):

error '@theia/debug' should be listed in the project's dependencies. Run 'npm i -S @theia/debug' to add it import/no-extraneous-dependencies

The new eslint plugin properly picks up violations of not using the shared dependencies re-exported by @theia/core (attempting to use inversify the old way):

"inversify" is a @theia/core shared dependency, please use "@theia/core/shared/inversify" instead. eslint(@theia/shared-dependencies)

dev-packages/eslint-plugin/package.json Outdated Show resolved Hide resolved
@paul-marechal paul-marechal force-pushed the mp/no-extraneous-dependencies branch from 1ffb1ac to d020eb9 Compare March 9, 2021 14:40
@paul-marechal
Copy link
Member Author

@tsmaeder will this change cause issues with how you consume Theia?

@tsmaeder
Copy link
Contributor

tsmaeder commented Mar 9, 2021

@tsmaeder will this change cause issues with how you consume Theia?

As it's an optional change, I'm not sure it's the best use of our effort.

@paul-marechal
Copy link
Member Author

paul-marechal commented Mar 9, 2021

@tsmaeder does that mean that this change is fine to go as is, or are you against merging?

edit:

As it's an optional change, I'm not sure it's the best use of our effort.

This change goes in the way of improving the framework's stability for people consuming it through NPM, so I'd argue that it is not simply "optional". There's always the option to refuse changes in general, so that would make this change optional in that aspect? But this patch fixes a class of issues, it's not like I just want to make things different.

Regarding the effort it's a do once and forget about it. Search and replace, follow the eslint errors and done. People writing Theia extensions or Theia applications by pulling packages from NPM will not even be affected. Only forks would need to align.

@tsmaeder
Copy link
Contributor

tsmaeder commented Mar 9, 2021

One think I still don't understand is if types will be compatible when I import them directly instead of via reexports: for example, if I extend a class and override a method, will the parameter types be compatible or do I need to change my source? In brief: is the change potentially breaking?

@paul-marechal
Copy link
Member Author

paul-marechal commented Mar 9, 2021

This change is not breaking in that regard, it makes it even more reliable: You will always use the same types than those used by @theia/core. If you happened to use a different version than what @theia/core uses and types happen to be incompatible then you will need to update your code indeed. But this is what this PR fixes, by aligning dependents with what is used by the framework.

edit: This alignment is currently only enforced in our repo and potentially forks, downstream extensions are encouraged to do the same but it wouldn't be enforced there.

@tsmaeder
Copy link
Contributor

tsmaeder commented Mar 9, 2021

@marechal-p duck typing works for interfaces, but what if a return type is a class, for example? They may be structurally the same, even refer to the same version, but does it compile?

@paul-marechal
Copy link
Member Author

but what if a return type is a class, for example?

@tsmaeder Then it is important to understand how one would end up with two distinct references to what looks like the same class.

This usually happens when the same package is installed twice under different versions e.g. doing yarn why <package> returns multiple entries. This can be due to unlucky hoisting due to any dependency from the whole project that depends on the same package than @theia/core but with an incompatible range: Yarn will install both versions to satisfy everyone, and it might pull the rug under your feet and make your implicit dependency require a different version than which from @theia/core.

The same package installation mismatch scenario can happen if you don't rely on implicit dependencies but rather try to explicitly depend on the same package as @theia/core. In this case if you happen to lag behind while @theia/core updates its own dependency and you don't, you might end up with two installations for the same package again.

In both scenarios, you have two packages. If both export a class and they are structurally the same, since they come from different packages they will be two different references and type compatibility will only depend on whether or not those classes declare private fields: if they do declare private fields then TS will complain at build time when you use one in place of the other. It does not matter that the private fields are identical, the class definitions are separate so TS complains. See error and success.

Using re-exports prevents this completely since the types involved will always be the same and hoisting will never cause these scenarios to happen. Dependents will only need to update code if @theia/core updates one of its own shared dependencies to a new incompatible version compared to the one before. But this was already the case, implicitly.

Note that type incompatibility issue will never happen if your code requires the classes from the same package version, as it will most likely mean that the package is not duplicated due to mismatched ranges.

This is a complex issue due to hoisting. This change gets rid of our dependence on hoisting.

@paul-marechal
Copy link
Member Author

@tsmaeder I got burned when doing one of the earliest yarn upgrade PR: #6255

I had to fight with how types were imported/exported, and implicit dependencies didn't help. The gist to fixing the issue was to re-export lsp types from a central Theia file and use that as source of truth everywhere else.

@tsmaeder
Copy link
Contributor

tsmaeder commented Mar 9, 2021

But if I do this in @theia/pkg:

import  { Foo } from '@theia/shared/pkg'

export function foo(): Foo {
  return new Foo();
}

and I currently have code that does

import { Foo } from 'pkg'
import { foo } from '@theia/pkg'

var myFoo: Foo = foo();

I would expect the two Foo types to be not assignment compatible (if they have private vars). What I'm trying to understand is this: can I get compilation errors in existing code with this change. I think I can.

@paul-marechal
Copy link
Member Author

paul-marechal commented Mar 9, 2021

@tsmaeder the issue you are describing here can already happen without my changes, even if both locations just import pkg. It depends on the hoisting layout. As long as require pulls things from different packages you will have the issue you are mentioning. But in your example it may or may not break, depending if import { Foo } from 'pkg' resolves to the same package that @theia/core/shared/pkg points to or not. Same issue we currently have without this change.

My patch allows us to get rid of that issue by allowing everyone to always refer to the same package thanks to re-exporting things. Here is an example showing how re-exporting is preserving types: ts-class-re-export.zip. Note how the recopied class fails, but the re-exported type works.

edit: But you are right that it can still break if people keep using implicit dependencies, but only if there's hoisting issues. Just like is currently happening, this change doesn't make this case worse or better. It only improves things when @theia/core/shared/<package> is used.

@paul-marechal
Copy link
Member Author

@tsmaeder here's another way to put the benefits of this PR:

This change prevents the issue you are talking about in this repository, and enforces it. After merging, if dependents keep using implicit dependencies then the issue you are mentioning can still happen, but it will be caused by their own implementations and not by the core packages anymore. Extenders that wish to also prevent the issue can now use this @theia/core/shared/ mechanism. Without this change not much can be done except tippy-toeing with those dependencies like we do today.

@paul-marechal
Copy link
Member Author

I'll resolve conflicts before merging somewhere around Thursday/Friday of this week.

@kittaakos
Copy link
Contributor

@marechal-p, this PR won't fix this problem, right?

If we merge the PR, I can still create a Theia extension that has a couple of dependencies that depend on fs-extra. For example: fs-extra@8. It has a different version than we use in Theia: fs-extra@4. If there are more dependencies that depend on fs-extra@8 than Theia extensions that depend on fs-extra@4, fs-extra@8 will be hoisted, and re-exporting common dependencies has no effect. Can you please help with this? I might be overlooking something. Thank you!

(If my example needs more clarification, let me know.)

@paul-marechal
Copy link
Member Author

paul-marechal commented Mar 23, 2021

@kittaakos as long as the Theia extension requires fs-extra through @theia/core/shared/fs-extra then even when fs-extra is duplicated it will keep working, so it should fix the problem you linked.

See example: fs-extra-duplicated.zip

Package a depends on fs-extra@9 and package b on fs-extra@6. Yarn will always make sure that each package sees what it explicitly requested. And as you can see, when a re-exports it's own fs-extra for b, the correct fs-extra@9 module is passed around. This is the exact same mechanism I used in this PR.

This means that hoisting will keep happening, but it won't de-stabilize builds since packages will be able to precisely import the same shared packages from the places that explicitly depend on them.

@kittaakos
Copy link
Contributor

See example: fs-extra-duplicated.zip

👍 Thanks for the example.

so it should fix the problem you linked.

That would be great.

@paul-marechal paul-marechal force-pushed the mp/no-extraneous-dependencies branch from d020eb9 to 03ae712 Compare March 26, 2021 22:58
git diff explains better what changed than git status.

Signed-off-by: Paul <paul.marechal@ericsson.com>
@paul-marechal paul-marechal force-pushed the mp/no-extraneous-dependencies branch 2 times, most recently from e871e25 to 00dd8ab Compare March 29, 2021 15:06
Enable errors for import/no-extraneous-dependencies.

Relying on hoisting makes building Theia application somewhat unstable.
In general cases, everything is fine. But as soon as Yarn decides to
hoist things differently then it can become difficult to fix.

This commit fixes the issue by re-exporting dependencies from
@theia/core, this way instead of relying on implicit dependencies one
can directly import what @theia/core uses like:

    import * as dep from '@theia/core/shared/dep'

To make this work I added a 'shared' folder under 'packages/core' with
js scripts that re-export everything from their matching library.

To ease with the development there is a script named
'packages/core/scripts/generate-shared.js' that will read values
from 'packages/core/package.json#theiaReExports' in order to more easily
add new packages. Depending on how the package exports things we need to
re-export things the same way.

This same script generates the README.md to list all re-exported
packages. It uses README.in.md as a base.

@theia/electron now re-exports all members from electron. This
prevents having to use electron as an implicit dependency.

@theia/electron now also is a @theia/core optional peer dependency. This
directly represent the use case we have for this package where we will
only use electron features if this package is installed by application
makers.

If people don't know about this way to consume @theia/core's shared
dependencies then nothing will change and they might keep using implicit
dependencies. But the new recommended method is to use those re-exports
in order to make builds more stable in the face of hoisting and other
dependency tree optimizations.

Noteworthy: There was an issue with sharing @theia/application-package
since I only re-export the main 'index.js' it tried to include
Node-specific code in frontend applications. I fixed this by
re-exporting @theia/application-package/lib/environment separately.

eslint: add @theia/eslint-plugin package

Add a new package @theia/eslint-plugin to provide rules for downstream
projects to re-use.

The goal was simply to give better error messages if committers used
implicit dependencies again, by notifying when a shared package is
depended upon without going through @theia/core/shared/...

It turned out that implementing that rule revealed places where
import/no-extraneous-dependencies simply didn't catch. The reason might
be that the plugin wasn't designed to work on TS sources. This commit
fixes those newly found occurences of implicit dependencies.

Signed-off-by: Paul <paul.marechal@ericsson.com>
@paul-marechal paul-marechal force-pushed the mp/no-extraneous-dependencies branch from 00dd8ab to 0269515 Compare March 29, 2021 16:18
@paul-marechal paul-marechal merged commit 96930a2 into master Mar 29, 2021
@github-actions github-actions bot added this to the 1.13.0 milestone Mar 29, 2021
@paul-marechal
Copy link
Member Author

I'll keep an eye out for any breakage but I don't expect any. I'll also assist with PRs if people need help rebasing.

@paul-marechal paul-marechal deleted the mp/no-extraneous-dependencies branch August 19, 2021 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core issues related to the core of the application dependencies pull requests that update a dependency file enhancement issues that are enhancements to current functionality - nice to haves
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants