We're trying to move all our EE-specific code out of files shared with GitLab CE and into EE-specific modules, which are prepended to their CE equivalents. These go into EE subdirectories, of which there are currently many:
The is no universal convention on where to place an EE-only file, and this leads to muddles.
If we instead use ee/ as an unconditional top-level prefix, we have a simple, clear idiom to follow, and the EE-specific code can be screened out with a single rule: ee/*. Our directory tree might then look like:
It's also possible that, by following this rule, we could eventually do away with the manual prepend ::EE::... lines we currently have to add to CE files. Instead, we could traverse the ee/{app,lib} directory tree, require each file and its CE equivalent, then run ce_class.prepend(ee_module) at startup, or cleverly hooked into the autoloading mechanism somehow.
Database migrations and schema left as an exercise for the reader.
Would love to see this. If we get this all moved we could conceivably have a CI task to ensure CE classes don't diverge? It would be tricky, still, but nice to have.
Oh, the most difficult part was left as an exercise. I love that. (well, I love that as a joke)
Jokes aside, we also need to think about this for the API, as we're adding more and more EE only API. I am unsure if we could do the same trick for Grape.
On the other hand, the most difficult part is probably the views.
Before we could have a single codebase, I think this is probably the way to go.
It's also possible that, by following this rule, we could eventually do away with the manual prepend ::EE::... lines we currently have to add to CE files. Instead, we could traverse the ee/{app,lib} directory tree, require each file and its CE equivalent, then run ce_class.prepend(ee_module) at startup, or cleverly hooked into the autoloading mechanism somehow.
@nick.thomas That seems a bit too implicit to me. I think it's nice to be able to see whether or not a class has EE specific functionality by looking for that prepend ::EE line.
If we instead use ee/ as an unconditional top-level prefix, we have a simple, clear idiom to follow, and the EE-specific code can be screened out with a single rule: ee/*. Our directory tree might then look like:
@nick.thomas How would that work with autoloading, which assumes the path to match the class/module name? I think that requires us to have the /ee/ part afterapp/{models,controllers,...}.
How would that work with autoloading, which assumes the path to match the class/module name?
@DouweM it should be possible. Autoloading tries paths in a configurable order, and will look in both app/ and lib/ for a constant by default, for instance. I've not tried it yet, though - there may be some problematic areas.
@nick I mean that for ee/app/controllers/application_controller.rb, autoloading will expect something like EE::App::Controllers::ApplicationController. Or if we add ee/app/controllers to the autoload paths, it'll just find ApplicationController, which will of course conflict. If we want to keep the prepend EE::Thing pattern, I think we need [autoload path]/ee/thing.rb, with the /ee/ close to the end, instead of top-level.
@DouweM@nick.thomas If we want to keep the prepend EE::Thing pattern, we could also put everything under a top ee/ folder as proposed by @nick.thomas, and prepend the EE constants with EE, that way there wouldn't be conflicts when autoloading kicks in:
ee/ is added to the autoladed paths
ee/app/controllers/ee_application_controller.rb would define EEApplicationController
In app/controllers/application_controller.rb, we'd put prepend EEApplicationController (no EE namespace since ee/ is autoloaded, thus its namespace is not taken in account)
@rymai What's the benefit of ee/app/*/ee_*.rb over app/*/ee/*.rb? I think of app/{models,controllers,...}/ee like app/{models,controllers,...}/concerns. It makes more sense to me than app/{ee,concerns}/{models,controllers,...} or {ee,concerns}/app/{models,controllers,...} to group for type first, and EE-only-ness second, as long as we do that everwhere (inside app, lib and spec)
In it, we currently have a separate assets/javascripts/<feature>/ee folder. That would load a separate bundle.
And we move the EE-specific SCSS to the bottom of a file.
How hard would it be to make an ee/assets-approach work?
@DouweM@rymai Alternatively we could have ee/app/**/ee/*.rb and add ee/ to autoload path. Of course, then we're back to possible implicit constant conflicts. I am unsure which we would want though.
@reprazent Thanks for bringing frontend consideration in, I totally forgot that part.
The experiments https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2483 seem to be working fine, and we need to tweak a bit due to some code moved. For example, EE helpers are no longer automatically included, so we'll need to include them explicitly, which I think it's a good thing (at least that's more consistent).
However, I am a bit worried that this could create a lot of conflicts which are hard to be resolved. This would impact any existing EE MRs.
@mikegreiling I would like to seek help for some webpack configurations :) We're trying to move all EE specific files to a separate directory, namely /ee/* therefore I would wish we could move app/assets/javascripts/vue_merge_request_widget/ee to ee/app/assets/javascripts/vue_merge_request_widget/ee.
Looking at the code, it seems it's configured in config/webpack.config.js for config.context. Looking at the document:
@godfat While it works in either of the approaches, I'm not sure if having top-level EE directory would be appropriate for all features, as if you look at just Protected Tags and Branches feature on current EE master, it has CE files too, so in total we have following currently;
protected_branches
protected_branches/ee
protected_tags
protected_tags/ee
And soon I'll refactor all 4 of these chunks to move out common parts into single bundle, so having top-level ee directory means I'll end up with lot of ../ in my ES6 imports to work around relative paths. And I'm sure we'll have more such cases in the future which is why I'm sceptical of having top-level directory for EE.
@kushalpandya Thank you for the explanation! I don't have enough knowledge to comment on the technical detail.
If we don't move JavaScript into ee/ directory, then the value of this move would be much lower. If we could still do this for JavaScript in the future, I would think this is still worth it. However if we decided that we would never do this for JavaScript, then I won't be sure as this would be quite inconsistent amongst the other parts of the codes.
@lbennett We did a lot in the backend, but I think that's not a good approach because sometimes we ended up putting the same thing in different directories. For example, sometimes we prefix ee in the root, but sometimes it's just before the component name. I think having ee in a separate directory would make this much more clear.
@kushalpandya I started trying to update some paths, and I realized that this:
Which I think it's much easier to read and understand. Now I think we don't need to have any ../ at all. We could just always begin with ~/ or ee/. What do you think?
I think ee file separation in scss could make it more difficult to maintain from a frontend perspective. Ideally, I think CE and EE scss files should share the same codebase
@ClemMakesApps We wouldn't want to download, parse and compute EE styles for CE would we? And wouldn't that be more confusing as there is no clear separation in the styling but there is a clear separation in the markup?
In an ideal world, 99% of all the common elements would be shared but for now, I think doing inline commenting designating EE would be the best way forward because it's hard to determine context just by looking at multiple css files
We can configure a resolve directive for ee similar to ~, but I really don't like this idea. The use of ~ is already discouraged and meant to be relegated to our specs only (to save from using import Foo from '../../app/assets/javascripts/foo'; when we're in the /specs/javascripts/ directory). Outside of this, it is preferred to use relative paths because webpack resolve magic is non-standard.
Still, I suppose if we're already committed to moving to /ee/app/assets/javascripts/, I suppose a resolve configuration is the best available option.
My preference would be to treat the javascripts as their own, separate codebase (which they are) and add the prefix at /app/assets/javascripts/ee/* or /app/assets/javascripts/some_feature/ee/* as we have been.
Since we're not using the rails asset pipeline for javascript anymore, we could even go a step further and move our frontend code to /app/javascripts (as I believe Rails 5 does) or just /javascript/. This would make prefixing with /ee far less painful.
Which I think it's much easier to read and understand. Now I think we don't need to have any ../ at all. We could just always begin with ~/ or ee/. What do you think?
We can discuss revising those guidelines if it becomes necessary though...
@mikegreiling I'm in favour of revising. It isn't strictly a webpack thing: can be done as a babel transform, or with a rollup plugin, or any number of other ways. Worst case we can easily find/replace the webpack imports with a codemod.
@kushalpandya if your only concern for top level was lots of dots this also addresses that.
While not being tied to the tool is good, missing benefits because we might switch to another tool seems like some kind of YAGNI. And using ~ in our codebase means you can easily copy/paste imports between files!
add the prefix at /app/assets/javascripts/ee/* or /app/assets/javascripts/some_feature/ee/*
That you provided 2 options here exactly illustrates a problem mentioned earlier - as soon as go below top level we introduce inconsistencies. What if just some part of a feature is ee only? /app/assets/javascripts/some_feature/component/ee/*?
The top level way has some aspects I don't like, but it seems to map better to current rails setup of app/spec.
I think ee subfolders is easier to work with than a top-level ee that mirrors the entire non-ee structure.
I actually feel the same, but for me the advantage with subdirectories is not that much, considering that most of the time I just hit cmd+t to find the file, and the real location of the files are not too important to me, except when I need to create a new file, then of course I'll need to figure out the actual location.
On the other hand, having ee in the top level, making diff against CE and EE very easy, which is super helpful to figure out what's the difference between CE and EE. I am unsure when we really need this, but for example, we may no longer need https://gitlab.com/rymai-gitlab/ce_ee_diffs_generator
I somehow feel this is feature development VS CE/EE management :P
I think ee file separation in scss could make it more difficult to maintain from a frontend perspective. Ideally, I think CE and EE scss files should share the same codebase
I think we could discuss about scss later, and let's focus on things we could clearly have separate files in this issue.
We can configure a resolve directive for ee similar to ~, but I really don't like this idea. The use of ~ is already discouraged and meant to be relegated to our specs only (to save from using import Foo from '../../app/assets/javascripts/foo'; when we're in the /specs/javascripts/ directory). Outside of this, it is preferred to use relative paths because webpack resolve magic is non-standard.
Yes, I figured that later and tried to either use ~/ or ee/ which seems to be working well. There's still a few test failures, but I feel they could be fixed.
Frankly, I don't really understand the preference of having ../../../ over ~/, and I feel this is just the wrong approach, because in order to understand what's ../../../ leading to, I need to both know the current path to the file I am reading, and also count what's the directory of last 3 parents? And eventually I realized that oh, it's just app/javascripts/assets... and sometimes it could be ../ and sometimes it could be ../../ which is also inconsistent to me.
That being said, I could totally understand having ../ though, that is 1 layer at most. If this is all because:
webpack resolve magic is non-standard
Then I would think that the other tools should also have similar approach. In Ruby we have $LOAD_PATH, and in C/C++ we also have -I (and Ruby has this too) and I believe almost all the languages would have similar ideas to setup load/module paths.
It isn't strictly a webpack thing: can be done as a babel transform, or with a rollup plugin, or any number of other ways. Worst case we can easily find/replace the webpack imports with a codemod.
add the prefix at /app/assets/javascripts/ee/* or /app/assets/javascripts/some_feature/ee/*
That you provided 2 options here exactly illustrates a problem mentioned earlier - as soon as go below top level we introduce inconsistencies. What if just some part of a feature is ee only? /app/assets/javascripts/some_feature/component/ee/*?
For something like that, you laid it out really well @godfat! Mad props to you!
Whatever we end up deciding on the best way to handle CE and EE code, could be very valuable for us to share with the community in a blog post. afaik, there aren't many good resources out there for managing open core projects
It isn't strictly a webpack thing: can be done as a babel transform, or with a rollup plugin, or any number of other ways. Worst case we can easily find/replace the webpack imports with a codemod.
Frankly, I don't really understand the preference of having ../../../ over ~/, and I feel this is just the wrong approach
Fair enough. I wouldn't be strongly against changing this policy.
I actually feel the same, but for me the advantage with subdirectories is not that much, considering that most of the time I just hit cmd+t to find the file, and the real location of the files are not too important to me, except when I need to create a new file, then of course I'll need to figure out the actual location.
I appreciate the utility of having most of the EE-specific code under one directory, and if you use cmd+t that does alleviate some of the issue, but in instances where you're not using that shortcut I still think it is incredibly awkward to need to search in another nested directory, 5 levels removed, in order to find a dependency related to a script or feature you're working on
@mikegreiling Just curious. Would it work with symlinks? For example, we could make feature/ee point to ee/feature. I guess this won't work well with some editors, and symlinks don't work on Windows, but I am still curious if this would help?
@mikegreiling Just curious. Would it work with symlinks? For example, we could make feature/ee point to ee/feature. I guess this won't work well with some editors, and symlinks don't work on Windows, but I am still curious if this would help?
It might work, but it might make things more confusing. We'd have multiple ways to reference the same file, and we'd need to have a policy in place to create a symlink whenever a new feature subdirectory is added.
The issue is about best place for EE-specific files, right? I think this is one of those reversible decisions that should not take too much time. Let's do that, I like it.
Since we're not using the rails asset pipeline for javascript anymore, we could even go a step further and move our frontend code to /app/javascripts (as I believe Rails 5 does) or just /javascript/. This would make prefixing with /ee far less painful.
@godfat Regarding the migrations, I see that we have the following for the post-deployment migrations (in config/initializers/0_post_deployment_migrations.rb):
unlessENV['SKIP_POST_DEPLOYMENT_MIGRATIONS']Rails.application.config.paths['db'].eachdo|db_path|path=Rails.root.join(db_path,'post_migrate').to_sRails.application.config.paths['db/migrate']<<path# Rails memoizes migrations at certain points where it won't read the above# path just yet. As such we must also update the following list of paths.ActiveRecord::Migrator.migrations_paths<<pathendend
Couldn't we do something similar for EE migrations? e.g.
Rails.application.config.paths['db'].eachdo|db_path|path=Rails.root.join('ee',db_path,'migrate').to_sRails.application.config.paths['db/migrate']<<path# Rails memoizes migrations at certain points where it won't read the above# path just yet. As such we must also update the following list of paths.ActiveRecord::Migrator.migrations_paths<<pathend
@rymai I wonder if we could go further and have an ee/app/models/project.rb, for example, where we open the Project class and add all the EE specific methods there. At the very least it would get rid of some more CE -> EE merge conflicts.
I wonder if we could go further and have an ee/app/models/project.rb, for example, where we open the Project class and add all the EE specific methods there. At the very least it would get rid of some more CE -> EE merge conflicts.
@eReGeBe Note that reopening the model class wouldn't cut it, since we need to prepend so we can override existing method implementations.
@DouweM Umm, in that case we could probably reopen and prepend. So in ee/app/models/project.rb:
classProjectmoduleEEdefwhateverendendprependEEend
However I am unsure how we should reopen classes. Does this mean we need to explicitly require all of them at some point? (e.g. like eager loading all the stuffs) It would probably never work well with Rails auto-loading.