Updates from January, 2021 Toggle Comment Threads | Keyboard Shortcuts

  • kmitov 9:32 pm on January 11, 2021 Permalink |
    Tags: acts_as_paranoid, cancancan, globalize-rails, , ,   

    The problems with acts_as_paranoid and generally with Soft Delete of records in Rails 

    I am about to refactor and completely remove acts_as_paranoid from one of the models in our platform. There are certain issues that were piling up in the last few years and it is now already difficult to support it. As I am about to tell this to colleagues tomorrow I thought to first structure the summary in an article and directly send the article to the whole team.

    If you are thinking about using acts_as_paranoid and generally soft delete your records then this article could give you a good understanding of what to expect.

    Disclaimer: I am not trying to roast acts_as_paranoid here. I think it is a great gem that I’ve used successfully for years and it has helped me save people’s work where they accidentally delete something. It could be exactly what you need. For us it got too entangled and dependent on workarounds with other gems and I am planning to remove it.

    acts_as_paranoid VS Globalize – problem 1

    We have a model called Material. Material has a title. The title is managed by globalize as we are delivering the content in a number of languages.

    class Material < ApplicationRecord
      acts_as_paranoid
      translates :title
    end

    The problem is that Globalize knows nothing about acts_as_paranoid. You can delete a Material, and it should delete the translations, but when you try to recover the Material then there is an error because of how the translations are implemented and the order in which the translations and the Material are recovered. Which record should be recovered first? Details are at https://github.com/ActsAsParanoid/acts_as_paranoid/issues/166#issuecomment-650974373, but as a quote here

    Ok, then I think I see what happens: ActsAsParanoid tries to recover your translations but their validation fails because they are recovered before the main object. When you just call recover this means the records are just not saved and the code proceeds to save the main record. However, when you call recover!, the validation failure leads to the exception you see.

    In this case, it seems, the main record needs to be recovered first. I wonder if that should be made the default.

    I have work around this and I used it like this for an year

    module TranslatableSoftdeletable
      extend ActiveSupport::Concern
    
      included do
    
        translation_class.class_eval do
          acts_as_paranoid
        end
    
        before_recover :restore_translations
      
      end
    
      def restore_translations
        translations.with_deleted.each do |translation|
          translation.deleted_at = nil
          translation.save(validate: false)
        end
        self.translations.reload
      end
    
    end

    acts_as_paranoid VS Globalize – problem 2

    Let’s say that you delete a Material. What is the title of this deleted material?

    material = Material.find(1)
    material.destroy
    material.title == ?

    Remember that the Material has all of its translations for the title in a table that just got soft deleted. So the correct answer is “nil”. The title of the delete material is nil.

    material = Material.find(1)
    material.destroy
    material.title == nil # is true

    You can workaround this with

    material.translations.with_deleted.where(locale: I18n.locale).first.title

    But this is quite ugly.

    acts_as_paranoid VS cancancan

    The material could have authors. An author is the connection between the User and the Material. We are using cancancan for the controllers.

    In the controller for a specific author we show only the models that are for this author. We have the following rule:

    can [:access, :read, :update, :destroy], clazz, authors: { user_id: user.id }
    

    Here the problem is that you can only return non deleted records. If you would like to implement a Trash/Recycle Bin/Bin functionality for the Materials that you can not reuse the rule. The reason is the cancancan can not have can? and cannot? with an sql statement and the “authors: {user_id: user.id}” is an sql statement.

    What we could do is to use scopes

    # in the ability
    can [:access, :read, :update, :destroy], Material, Material.with_authors(user.id)
    
    # In the Material
    scope :with_authors, -> (user_ids) {
        joins(:authors).where(authors: {user_id: user_ids})
    }
    

    We move the logic for authorization from the ability to the model. I can live with that. It is not that outrageous. But it does not work, because the association with authors will return empty authors for the Material as the materials are also soft deleted.

    acts_as_paranoid vs cancancan – case 2

    When declaring load_and_authorize in the controller it will not fetch records that are already deleted. So you can not use cancancan to load in the controller a record that is deleted and you must load it yourself.

    class MaterialsTrashController < ApplicationController
    
      before_action only: [:show, :edit, :update, :destroy] do
        @content_picture = Material.only_deleted.find(params[:id])
      end
    
    
      load_and_authorize_resource :content_picture, class: "Material", parent: false
    
    end

    This was ugly, but as we had one trash controller it was kind of acceptable. But with a second one it got more difficult and deviated from the other controllers logic.

    acts_as_paranoid vs ContentPictures

    Every Material has many ContentPictures on our platform. There is a ContentPictureRef model. One of the pictures could be the thumbnail. Once you delete a material what is the thumbnail that we can show for this material?

    As the content_picture_refs relation as also soft deleted we should change the logic for returning the thumbnail. We change it from

    def thumbnail
        thumbnail_ref = self.content_picture_refs.where(content_picture_type:  :thumbnail).first
    end

    to

    def thumbnail
        thumbnail_ref = self.content_picture_refs.with_deleted.where(content_picture_type:  :thumbnail).first
    end

    I can live with this. Even relation that we have for the deleted record we must call with “with_deleted”. ContentPictures, Authors and other relations all should be changed for us to be able to see the deleted materials in a table that represents the Trash.

    acts_as_paranoid vs Active Record Callbacks

    There is another issue right at https://github.com/ActsAsParanoid/acts_as_paranoid/issues/168

    It took me hours to debug it a few months back. The order of before_destroy should not really matter.

    class Episode < ApplicationRecord
       acts_as_paranoid
       before_destroy do 
          puts self.authors.count
       end
       has_many :authors, dependent: :destroy
    
    class Episode < ApplicationRecord
       before_destroy do 
          puts self.authors.count
       end
       acts_as_paranoid 
       has_many :authors, dependent: :destroy

    acts_as_paranoid vs belongs_to

    By using acts_as_paranoid belongs_to should be:

    class Author < ApplicationRecord
       belongs_to :material, with_deleted: true
    end

    I could also live with this. At a certain stage it was easier to just add with_deleted: true to the belongs_to relations.

    acts_as_paranoid vs Shrine

    So the Material has a Shrine attachment stored on Amazon S3. What should happen when you delete the Material. Should you delete the file from S3? Of course not. If you delete the file you will not be able to recover the file.

    The solution was to modify the Shrine Uploader

    class Attacher < Shrine::Attacher
    
      def activerecord_after_destroy 
        # Just dont call super and keep the file
        # Sine objects at BuildIn3D & FLLCasts are softdeleted we want to 
        # handle the deletion logic in another way.
        true
      end
    
    end
    

    I was living with this for months and did not had many issues.

    What Pain and Problem are we addressing?

    The current problem that I can not find a workaround for is the MaterialsTrashController that is using CanCanCan. All the solutions would require for this controller to be different than the rest of the controllers and my biggest concern is that this will later result in issues. I would like to have a single place were we check if a User has access to the Material and whether they could read or update it or recover it. If we split the logic of the MaterialsController and the MaterialsTrashController we would end up with a hidden and difficult to maintain duplication.

    But was is the real problem that we want to solve?

    On our platform we have authors for the instructions and we work close with them. I imaging one particular author that I will call TM (Taekwondon Master). So TM uploads a material and from time to time he incidentally could delete and material. That’s it. When he deletes a Material it should not be deleted, but rather put in a trash. Then when he deletes it from the trash he must confirm with a blood sample. That’s he. I just want to stop TM from losing any Material by accident.

    The solution is pretty simple.

    In the MaterialsController just show all the materials that do not have a :deleted_at column set.

    In the MaterialsTrashController just show only the Materials with :delete_at controller.

    I can solve the whole problem with one simple filter that would take me like 1 minute to implement. We don’t need any of the problems above. They simply will not exist.

    That’s it. I will start with the Material and I will move through the other models as we are implementing the additional Trash controllers for the other models.

     
  • kmitov 12:27 pm on December 10, 2020 Permalink |
    Tags: encoding   

    There are two ways to get the Cyrillic character ‘й’ in your string 

    [Everyday code]

    Today we had an issue with a Cyrillic character where some of the python scripts were not working. Turns out that the it is a know and special case. In summary, be careful when using Cyrillic character ‘й’ and pay special attention on how this files are processed.:

    In addition, you are probably hitting normalization issues. There are two ways to get the Cyrillic character 'й' in your string, one of them is a single code point, the other is two code points:>>> a = 'й'
    >>> b = 'й'
    >>> len(a), unicodedata.name(a)
    (1, 'CYRILLIC SMALL LETTER SHORT I')
    >>> len(b), unicodedata.name(b[0]), unicodedata.name(b[1])
    (2, 'CYRILLIC SMALL LETTER I', 'COMBINING BREVE')
     
  • kmitov 6:18 am on November 30, 2020 Permalink |
    Tags: , , ,   

    Testing an index page in a web application where we depend on the order. 

    [Everyday Code]

    Today the specs for an index page failed and I had to improve it. I decided to share a little trick for when we depend on the order of the objects.

    The specs is for the /subscriptions page where we show an index of all the subscriptions. We order the subscriptions by created_at in DESC. There are 20 subscriptions on the page. Then you must got to the next page. In the spec we open the page and check that there is a link to our subscription.

    visit "/admin/user/subscriptions"
    
    expect(page).to have_link subscription.random_id.to_s, href: "/admin/user/subscriptions/#{subscription.to_param}/edit"

    The problem was that there are other specs which for some reason create subscriptions into the future. This means that at a certain point in time when more than 20 subscriptions are created into the future in the DB, then our spec will fail. It will fail because the newly created subscription is on the second page.

    All the subscriptions on this page are into the future as today is 2020-11-30. So our newly created subscription is not here.

    What are our options?

    Move to the correct page in the spec

    This is an option. It will require us to have a loop in the spec that would more to the next page in the index and search for each subscription. This is too much logic for a spec.

    Delete all the future subscriptions before starting the spec

    Could be done. But is more logic for the spec. It actually needs to delete subscriptions and this is not the job of this specs.

    Create a subscription that is with the most recent created_at

    It is simple.

      let(:subscription) {FactoryBot.create(:subscription, created_at: subscription_created_at_time)}
    
      def subscription_created_at_time
        (Subscription.order(:created_at).last.try(:created_at) || Time.now)+1.second
      end
    
    

    It is the simpler change for this spec. Just create a subscription that’s last. In this way we know it will appear on the first page even when other specs have created subscriptions into the future.

     
  • kmitov 9:56 pm on November 28, 2020 Permalink |
    Tags: , ,   

    How to do headless specs with the BABYLON JS NullEngine 

    [Everyday code]

    In buildin3d.com we are using BABYLON JS. To develop a headless specs for BABYLON JS that could run in a Node.js environment or without the need of an actual canvas we can use BABYLON.NullEngine. A spec could then look like

     const engine = new BABYLON.NullEngine();
     this.scene = new BABYLON.Scene(engine);

    Here is what I found out.

    Headless specs

    We run a lot of specs for our BABYLON JS logic. All of this specs are against preview.babylonjs.com. The preview version of babylon give us access to the latest most recent changes of babylon that are mode available to the public. These are still not release changes, but are the work of progress of the framework. They are quite stable so I guess at least the internal suite of BABYLON has passed. Preview is much like a nightly build. In BABYLON JS case it is also quite stable.

    2 days ago much of our specs failed. I reported at https://forum.babylonjs.com/t/failure-error-typeerror-cannot-read-property-trackubosinframe-of-undefined/16087. There were a lot of errors for :

    Cannot read property 'trackUbosInFrame' of undefined.

    Turns out that many of our specs were using BABYLON.Engine to construct the scene like

    const engine = new BABYLON.Engine();
    this.scene = new BABYLON.Scene(engine);

    The BABYLON.Engine class is only intended for the cases where we have a canvas. What we should have been doing for headless specs without a canvas is to use BABYLON.NullEngine. Hope you find it helpful.

    Write more specs.

     
  • kmitov 4:25 pm on October 23, 2020 Permalink |
    Tags: , mandarin, utf, wget   

    "wget 1.20 is here!" (never in my life I though I'd say that) 

    (this particle is part of the Everyday Code series)

    There are tools that just work. Low level tolls doing much of the heavy lifting and one thing you know about them is that they work. You never write

    
    $ mv --version source target
    

    For that matter you also never write

    $ cp -- version 1.3 source target
    $ ssh --version 1.2.1 use@machine
    $ curl --version 773.2  url

    Same applies for wget. There are tools that always work. Because they do one thing and they do it well. Not that there are no version. There are. But today was the first time in my career that we had to upgrade a wget version. We moved from version 1.17 to 1.20

    Why the change

    What could have changed that made it important to update from 1.17 to 1.20?

    It was the introduction of support of some mandarin symbols. Mandarin. A language. Users were uploading files names with such symbols and we had to support them.

    The internet. What a beautiful place.

     
  • kmitov 4:22 am on October 21, 2020 Permalink |
    Tags: ,   

    Advanced compilation with Google Closure Compiler 

    Tonight I am doing a live stream lecture about Google Closure Compiler and how we use it to compile the JavaScript code of the Instructions Steps Framework. The framework is the core of buildin3d.com where we deliver 3D assembly instructions.

    The repo for the lecture is located at https://github.com/thebravoman/google-closure-compiler-presentation/.

     
  • kmitov 5:38 am on October 19, 2020 Permalink |
    Tags: management,   

    Don’t fix the issue in the software. Improve the process. 

    Yesterday one of the features on our platform did not work. I was in a meeting, demonstrating it over a shared screen and talking with a potential client. I went to the page showing the IS Editor in our buildin3d.com platform and the editor for editing the assembly instructions did not start. A little rush of embarrassment and a few milliseconds later I knew what I had to do. Thanks to my seniority and extended experience in the world of web development I moved my fingers lighting fast on the keyboard and I refreshed the page. The editor started. The demonstration continue.

    I could remember that I stumbled upon this issue a few days earlier and I saw that the IS Editor was not loading when you first visit the page. The meeting continue, I said something like “Sometimes when we are sharing the screen my bandwidth is small so we have to wait”. I suppose the client did not exactly understood what has just happen, but what I know is that the next time they try it on their side it will not work and they will be disappointed.

    Right after the meeting there was a problem I was facing. Should I now open the repo and start debugging or should I wait a day or two for our team to look at this.

    One of the most difficult things running a Software company as a good software developer is the patience to wait for the team of developers to resolve an issue.

    I was close to mad. How difficult could it be? After you commit something just go to the platform and see that it works. We have a lot of automation, a lot of testing and spec that have helped us a lot. We have a clean and I would say quite fast process for releasing a new version of any module to the platform. It takes anywhere from 2 minutes to about 20 minutes depending on what you are releasing. So after you release something just go and see and test and try it and make sure it works. How difficult could it be?

    I was mad. Like naturally and really mad. Not that this demonstration was almost ruined by this issue. I was mad that we’ve spend about 3-4 months working on this editor and it currently does not start. It is not true that the editor itself is not working. It is just not starting. Once it starts it works flawlessly, but a mis-configuration in the way it is started prevents it from even starting.

    It’s like getting to your Ferrary and it does not start because of law battery on your key or something. There is nothing wrong with the Ferrary itself, but your key is not working.

    In this state of anger I opened up the repo. I tracked down the moment it was introduced. And here is the dilemma:

    1. Should I now start debugging it, and resolving it?
    2. Should I just revert the last 11 days of commits and return the platform to a previous state completely removing the great improvements we’ve introduced in this last 11 days?
    3. Should I leave it for the next few days for the team to look at?

    The worst part is that I can fix the issue myself. But that is not my job. My team counts on me to spend more of my time with potential & existing clients, talking and discussing with them. Looking for ways they could integrate us. But in the same time I had an issue where a major feature is not working and will not work for the next few days and in one sleepless night I could resolve it.

    I don’t have this problem with the other departments. When there is an issue with some of the 3D product animations and models or there is an issue with some of the engineering designs I do not feel the urge to go and resolve this issue. I have the patience to rely on the team for this. Basically because I lack the knowledge and the tools to resolve such issues.

    Years ago when we were starting with 3D animations and models I had great interest, but I openly refused to install any software about 3D animations and models on my machine. I knew myself and I knew my team. In school an in university and was trying some 3D models and animations and it felt great. I learned a lot and I had some great time working on such projects. So I knew that if I install some of the software on my machine there will be issue that will come to me, but that was not my role in my organization.

    Same for engineering. I have the complete patience to wait for days for an engineering design task to complete. I never start the SOLIDWORKS myself and go on and “fix the things”. I could. I just don’t want as it will distract me from other important things and I know I can count on the engineers to do it.

    But with software it is always a little difficult. Not that I can not delegate. I can. There are large parts of the code we are running that I have never touched, or changed or anything. So I though – why was this particular issue different? What was my problem? Why was it bothering me? Why was this different from any other issue in software development that is reported, debugged and resolved. Where did the anger come from?

    I was angry because the process I’ve setup has allowed for this issue to occur.

    The IS Editor was working a few days ago. Now it was not working. This was not an issue of my software development skills, this was a challenge for my “organizing a software development process that produces a working software and deploys it to production a few times a day in a team with a large code base and a new R&D challenge that we were working on”.

    This I have found in my experience to be the most difficult problem for good software developers that mediocre and bad software developers do not face. When you know how to fix it, how to implement it and you take on the task then your time and energy is spend on resolving the issue. It might be better for the team as a whole if you spend your energy and resources on a different tasks – like how to avoid a regression in a multi-teams multi-frameworks environment.

    Know what is important and where your efforts would be most valuable. I’ve stepped up and did a lot of software development int he team. I’ve single-handedly implemented a number of frameworks. Not just the architecture, but actual implementation. I once deleted two human years of development and re-implemented the whole module almost from scratch. There is even a saying in the team “Kiril will roll up his sleeves and will implement this”.

    But no.

    There will always be issues in software development and we should think if our task is to resolve this issues, or to make sure this issues never occur in the first place. The later is objectively the more important and difficult task.

     
  • kmitov 5:23 pm on May 21, 2020 Permalink |
    Tags: assets, dependency, , , , , rubygems,   

    With assets in a rails engine it could be Gemfile vs gemspec dependency that is messing it up 

    You have a rails engine. It has an asset (like a scss file). You include this engine in another engine. How do you test that the assets is correctly resolved?

    There is a moment when you simply can not understand where and how the asset is resolved/not resolved. The solution actually is in what are the Gemfile and .gemspec for a rails engine. Both of them could describe dependencies, but they contain different things.

    The whole premise of the situation seems strange until you get to building a rails engine that would contain the “theme” of your app. We go in this situation with two of our platforms – FLLCasts.com and 3DAssemblyInstructions.com. We wanted to have different layouts in different gems that are providing different look an feel- separate the main layout from dashboard and from main. We also wanted to have this layouts in a different gems so that we could release them with different versions and test them as separate gems in separate builds.

    Let’s get into this step by step tutorial and by the end I am sure you would know much about how assets are resolved, packed and testes when they are within a rails. Again I am writing this tutorial to help spread the knowledge internally, but the problem is so common that it might be of interest to the community as a whole.

    Case 1 – simple app with an asset

    Just to get the understanding correctly and to have a base we would create a simple rails app with an asset and start it. I am using rails 6 and ruby 2.6.5

    $ rails new simple_assets_app
    $ cd simple_asset_app
    # Create a new asset.scss file with some content
    $ echo "asset_style { background-color: white}" > app/assets/stylesheets/asset.scss
    $ rails s
    => Booting Puma
    => Rails 6.0.3.1 application starting in development 
    => Run `rails server --help` for more startup options
    Puma starting in single mode...
    * Version 4.3.5 (ruby 2.6.5-p114), codename: Mysterious Traveller
    * Min threads: 5, max threads: 5
    * Environment: development
    * Listening on tcp://127.0.0.1:3000
    * Listening on tcp://[::1]:3000
    Use Ctrl-C to stop
    
    

    In another terminal request the asset

    # Request the asset and dislay its content. Everything works. Perfect
    $ curl localhost:3000/assets/asset.scss
    asset_style { background-color: white}

    Case 2 – engine with an asset

    Now let’s build a rails plugin that would hold this asset

    $ rails plugin new simple_asset_engine --full
    $ cd simple_asset_engine/
    $ echo "asset_style { background-color: white}" > app/assets/stylesheets/asset.scss
    # You have to fix the TODOs in the gemspec. After you are ready do 
    $ rails s -p 3001
    => Booting WEBrick
    => Rails 6.0.3.1 application starting in development http://localhost:3001
    => Run `rails server --help` for more startup options
    [2020-05-21 19:48:51] INFO  WEBrick 1.4.2
    [2020-05-21 19:48:51] INFO  ruby 2.6.5 (2019-10-01) [x86_64-linux]
    [2020-05-21 19:48:51] INFO  WEBrick::HTTPServer#start: pid=13394 port=3001
    

    Now that the port is different.

    Request the asset

    # Nothing strange here. Asset is delivered as expected.
    $ curl localhost:3001/assets/asset.scss
    asset_style { background-color: white}
    

    Conclusion – engine, no engine the asset is delivered.

    Case 3 – require simple_asset_engine in another engine

    This could be a requirement. It has happen to us. It is a valid case. It is rather strange to have one engine require another, but we are all grown ups here…

    $ rails plugin new host_engine --full
    # Fix todos in .gemspec
    
    # Add a dependency to simple_asset_engine. Asset should be found, rigth
    $ echo 'gem "simple_asset_engine", path: "~/axles/tmp/simple_asset_engine"' >> Gemfile
    
    # install gems - mainly the simple_asset_engine
    $ bundle install
    
    # Start server. Note the different port
    $ rails s -p 4010
    
    

    Request asset

    $ curl localhost:4010/assets/asset.scss
    asset_style { background-color: white}

    Asset is found.

    That’s the power of rails. It just works. You can have an asset in an app, asset in an engine, asset in an engine, required in another engine and the asset is always found. Except…

    This might sound as a conclusion, because everything works. Until you get to production, which is actually the only time it matters if anything works, but that’s another story.

    Our current host_engine depends on a gem with a local path in the local filesystem.

    host_engine: $ cat Gemfile
    source 'https://rubygems.org'
    git_source(:github) { |repo| "https://github.com/#{repo}.git" }
    
    # Declare your gem's dependencies in host_engine.gemspec.
    # Bundler will treat runtime dependencies like base dependencies, and
    # development dependencies will be added by default to the :development group.
    gemspec
    
    # Declare any dependencies that are still in development here instead of in
    # your gemspec. These might include edge Rails or gems from your path or
    # Git. Remember to move these dependencies to your gemspec before releasing
    # your gem to rubygems.org.
    
    # To use a debugger
    # gem 'byebug', group: [:development, :test]
    gem "simple_asset_engine", path: "~/axles/tmp/simple_asset_engine"

    This is something that we can not ship to production. That’s why we pack “simple_asset_engine” as a gem and place this gem in a gemrepo. This is not part of the tutorial so I will simulate it.

    We should also modify the host_engine.gemspec to depend on simple_asset_engine. We add a dependency.

    # in host_engine.gemspec we add
    
    spec.add_dependency "simple_asset_engine"
    

    This means – ‘add a dependency to “simple_asset_engine”‘. Our gem will host_engine will depend on ‘simple_asset_engine’, but and here is a but, but only for ‘production’ environment. So this means – “production”. To simulate the fact that we don’t have this gem in a repo we would change the dependency in the Gemfile to be only for production.

    # This means the same as spec.add_dependency 'simple_asset_engine' It add the gem as a dependency to production. We are doing it like this to simulate that we have 'spec.add_dependency' and the gem is coming from a gems repo
    
    gem "simple_asset_engine", group: [:production], path: "~/axles/tmp/simple_asset_engine"

    Now as you do request the asset a strange error occurs:

    $ curl localhost:4010/assets/asset.scss
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8" />
      <title>Action Controller: Exception caught</title>
        ....
       <div id="Framework-Trace-0" style="display: none;">
          <code style="font-size: 11px;">
              <a class="trace-frames trace-frames-0" data-exception-object-id="47236752082100" data-frame-id="0" href="#">
                actionpack (6.0.3.1) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
              </a>
              <br>
              <a class="trace-frames trace-frames-0" data-exception-object-id="47236752082100" data-frame-id="1" href="#">
                actionpack (6.0.3.1) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
              </a>
              <br>
              <a class="trace-frames trace-frames-0" data-exception-object-id="47236752082100" data-frame-id="2" href="#">
                railties (6.0.3.1) lib/rails/rack/logger.rb:37:in `call_app'
              </a>
    ...
    </div>
    </body>
    </html>
    

    In the log you see

    ActionController::RoutingError (No route matches [GET] "/assets/asset.scss"):
      
    actionpack (6.0.3.1) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
    railties (6.0.3.1) lib/rails/rack/logger.rb:37:in `call_app'
    railties (6.0.3.1) lib/rails/rack/logger.rb:26:in `block in call'
    activesupport (6.0.3.1) lib/active_support/tagged_logging.rb:80:in `block in tagged'
    activesupport (6.0.3.1) lib/active_support/tagged_logging.rb:28:in `tagged'
    activesupport (6.0.3.1) lib/active_support/tagged_logging.rb:80:in `tagged'
    railties (6.0.3.1) lib/rails/rack/logger.rb:26:in `call'
    sprockets-rails (3.2.1) lib/sprockets/rails/quiet_assets.rb:11:in `block in call'
    activesupport (6.0.3.1) lib/active_support/logger_silence.rb:36:in `silence'
    activesupport (6.0.3.1) lib/active_support/logger.rb:64:in `block (3 levels) in broadcast'
    activesupport (6.0.3.1) lib/active_support/logger_silence.rb:36:in `silence'
    activesupport (6.0.3.1) lib/active_support/logger.rb:62:in `block (2 levels) in broadcast'
    sprockets-rails (3.2.1) lib/sprockets/rails/quiet_assets.rb:11:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/request_id.rb:27:in `call'
    rack (2.2.2) lib/rack/method_override.rb:24:in `call'
    rack (2.2.2) lib/rack/runtime.rb:22:in `call'
    activesupport (6.0.3.1) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/executor.rb:14:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/static.rb:126:in `call'
    rack (2.2.2) lib/rack/sendfile.rb:110:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/host_authorization.rb:82:in `call'
    railties (6.0.3.1) lib/rails/engine.rb:527:in `call'
    rack (2.2.2) lib/rack/handler/webrick.rb:95:in `service'
    

    It is basically telling us that an error occurred. An error also should occur. It is logical, it is the right thing to do, but it can take hours to debug and understand this.

    The error occurs because we are starting the rails server in ‘development’ environment and the dependency to ‘simple_asset_engine’ is only for production.

    Case 3.1 – change to production and development

    # Change host_engine/Gemfile to have
    
    gem "simple_asset_engine", group: [:production, :development], path: "~/axles/tmp/simple_asset_engine"
    

    Restart server on port 4010.

    host_engine: $ rails s -p 4010
    => Booting WEBrick
    => Rails 6.0.3.1 application starting in development http://localhost:4010
    => Run `rails server --help` for more startup options
    [2020-05-21 20:14:55] INFO  WEBrick 1.4.2
    [2020-05-21 20:14:55] INFO  ruby 2.6.5 (2019-10-01) [x86_64-linux]
    [2020-05-21 20:14:55] INFO  WEBrick::HTTPServer#start: pid=14167 port=4010

    Request the asset

    $ curl localhost:4010/assets/asset.scss
    asset_style { background-color: white}

    Now it is time for conclusion

    spec.add_dependency in a gemspec gives you a dependency for production. There is a ‘spec.add_development_dependency’ that exists, but there are great discussion about it here https://github.com/rubygems/rubygems/issues/1104 Read more there. Really. Read more.

    But as we are trying to test assets separately in an engine it is important to understand what Gemfile and gemspec could be used for. If an asset is not found from a dependency of a gemspec, probably the whole dependency is required for a different environment. In the same time if the dependency contains only assets, it might be difficult to get what is happening at first sight and is it your fault, or that the F..k is sprockets doing or event what the bigger f..k is webpacker doing. But for this case it is just plain old ruby dependencies that are messing us up.

     
  • kmitov 8:45 am on April 22, 2020 Permalink |
    Tags: css, , less, npm, , , , , yarn   

    Rails 6 + webpacker + yarn + Fancytree + LESS 

    We had to migrate a gem from using fancytree-rails as a ruby gem to a new rails 6 gem using webpacker and jquery.fancytree coming from npm. On top of that jquery.fancytree is using LESS (CSS) and you have to do a few configurations.

    App is available at https://github.com/thebravoman/rails6_webpacker_fancytree_less

    Here is how to do it it a few simple commands

    Table of contents

    Create a new rails project

    We want to have Fancrytree in this project.

       $ rails new project_with_less_and_fancytree
       $ cd project_with_less_and_fancytree
       $ rails g scaffold books
       $ rails db:migrate
     

    Add fancytree yarn package

    $ yarn add jquery.fancytree
       yarn add v1.22.4
       [1/4] Resolving packages...
       [2/4] Fetching packages...
       info fsevents@1.2.12: The platform "linux" is incompatible with this module.
       info "fsevents@1.2.12" is an optional dependency and failed compatibility check. Excluding it from installation.
       [3/4] Linking dependencies...
       warning " > webpack-dev-server@3.10.3" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
       warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
       warning " > jquery.fancytree@2.35.0" has unmet peer dependency "jquery@>=1.9".
       [4/4] Building fresh packages...
       success Saved lockfile.
       success Saved 1 new dependency.
       info Direct dependencies
       └─ jquery.fancytree@2.35.0
       info All dependencies
       └─ jquery.fancytree@2.35.0
       Done in 3.29s.

    I like yarn.

    Add less and less-loader

    Later to include fancytree we would have to do things like

    import 'jquery.fancytree/dist/skin-lion/ui.fancytree.less'

    This means fancytree uses LESS. So we need to process this .less files. Oh, css, oh you evil you.

    yarn add less

    $ yarn add less
       yarn add v1.22.4
       [1/4] Resolving packages...
       warning less > request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
       [2/4] Fetching packages...
       info fsevents@1.2.12: The platform "linux" is incompatible with this module.
       info "fsevents@1.2.12" is an optional dependency and failed compatibility check. Excluding it from installation.
       [3/4] Linking dependencies...
       warning " > jquery.fancytree@2.35.0" has unmet peer dependency "jquery@>=1.9".
       warning " > less-loader@5.0.0" has unmet peer dependency "webpack@^2.0.0 || ^3.0.0 || ^4.0.0".
       warning " > webpack-dev-server@3.10.3" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
       warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
       [4/4] Building fresh packages...
       success Saved lockfile.
       success Saved 4 new dependencies.
       info Direct dependencies
       └─ less@3.11.1
       info All dependencies
       ├─ asap@2.0.6
       ├─ image-size@0.5.5
       ├─ less@3.11.1
       └─ promise@7.3.1
       Done in 5.80s.

    yarn add less-loader

    You need less and less-loader

    $ yarn add less-loader
       yarn add v1.22.4
       [1/4] Resolving packages...
       [2/4] Fetching packages...
       info fsevents@1.2.12: The platform "linux" is incompatible with this module.
       info "fsevents@1.2.12" is an optional dependency and failed compatibility check. Excluding it from installation.
       [3/4] Linking dependencies...
       warning " > jquery.fancytree@2.35.0" has unmet peer dependency "jquery@>=1.9".
       warning " > webpack-dev-server@3.10.3" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
       warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
       warning " > less-loader@5.0.0" has unmet peer dependency "less@^2.3.1 || ^3.0.0".
       warning " > less-loader@5.0.0" has unmet peer dependency "webpack@^2.0.0 || ^3.0.0 || ^4.0.0".
       [4/4] Building fresh packages...
       success Saved lockfile.
       success Saved 2 new dependencies.
       info Direct dependencies
       └─ less-loader@5.0.0
       info All dependencies
       ├─ clone@2.1.2
       └─ less-loader@5.0.0
       Done in 3.26s.
     

    Add less-loader to webpack environment

    They must be registered. Probably in another file, but here in enrovonments.js is fine this tutorial.

    // config/webpack/environments.js
    
    const { environment } = require('@rails/webpacker')
    
    // THIS IS THE NEW CODE
    const less_loader= {
      test: /\.less$/,
      use: ['css-loader', 'less-loader']
    };
    environment.loaders.append('less', less_loader)
    // END: THIS IS THE NEW CODE
    
    module.exports = environment

    Use fancytree

    Check out the documentation at https://github.com/mar10/fancytree/wiki#use-a-module-loader

    But basically you must require fancytree and use it.

    // NOTE: This seems to be working
    // app/javascripts/packs/application.js
    
    //... some other code.
    
    // THIS IS THE NEW CODE ADDED AT THE BOTTOM OF application.js
    // Import LESS or CSS:
    import 'jquery.fancytree/dist/skin-lion/ui.fancytree.less'
    
    const $ = require('jquery');
    
    const fancytree = require('jquery.fancytree');
    require('jquery.fancytree/dist/modules/jquery.fancytree.edit');
    require('jquery.fancytree/dist/modules/jquery.fancytree.filter');
    
    console.log(fancytree.version);
    
    $(function(){
      $('#tree').fancytree({
        extensions: ['edit', 'filter'],
        source: [
          {title: "Node 1", key: "1"},
          {title: "Folder 2", key: "2", folder: true, children: [
            {title: "Node 2.1", key: "3"},
            {title: "Node 2.2", key: "4"}
          ]}
        ],
      });
      const tree = fancytree.getTree('#tree');
      // Note: Loading and initialization may be asynchronous, so the nodes may not be accessible yet.
    })
    // END: THIS IS THE NEW CODE ADDED AT THE BOTTOM OF application.js

    NOTE – import is kind of not working

    There is another configuration at https://github.com/mar10/fancytree/wiki#use-a-module-loader but I could not make it work

    // NOTE: This is not working
    import 'jquery.fancytree/dist/skin-lion/ui.fancytree.less';  // CSS or LESS
    import {createTree} from 'jquery.fancytree';
    import 'jquery.fancytree/dist/modules/jquery.fancytree.edit';
    import 'jquery.fancytree/dist/modules/jquery.fancytree.filter';
    
    const tree = createTree('#tree', {
      extensions: ['edit', 'filter'],
      source: [
          {title: "Node 1", key: "1"},
          {title: "Folder 2", key: "2", folder: true, children: [
            {title: "Node 2.1", key: "3"},
            {title: "Node 2.2", key: "4"}
          ]}
        ],
    });
    // Note: Loading and initialization may be asynchronous, so the nodes may not be accessible yet.

    Start the application

    $ rails s
    => Booting Puma
    => Rails 6.0.2.2 application starting in development 
    => Run `rails server --help` for more startup options
    Puma starting in single mode...
    * Version 4.3.3 (ruby 2.6.5-p114), codename: Mysterious Traveller
    * Min threads: 5, max threads: 5
    * Environment: development
    * Listening on tcp://127.0.0.1:3000
    * Listening on tcp://[::1]:3000
    Use Ctrl-C to stop
    Started GET "/" for ::1 at 2020-04-22 09:20:06 +0300
       (0.4ms)  SELECT sqlite_version(*)
       (0.2ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
    Processing by Rails::WelcomeController#index as HTML
      Rendering /home/user/.rvm/gems/ruby-2.6.5/gems/railties-6.0.2.2/lib/rails/templates/rails/welcome/index.html.erb
      Rendered /home/user/.rvm/gems/ruby-2.6.5/gems/railties-6.0.2.2/lib/rails/templates/rails/welcome/index.html.erb (Duration: 17.4ms | Allocations: 471)
    Completed 200 OK in 45ms (Views: 25.4ms | ActiveRecord: 0.0ms | Allocations: 2931)
    
    
    Started GET "/books" for ::1 at 2020-04-22 09:20:09 +0300
    Processing by BooksController#index as HTML
      Rendering books/index.html.erb within layouts/application
      Book Load (0.2ms)  SELECT "books".* FROM "books"
      ↳ app/views/books/index.html.erb:13
      Rendered books/index.html.erb within layouts/application (Duration: 29.0ms | Allocations: 1230)
    [Webpacker] Compiling...
    [Webpacker] Compiled all packs in /home/user/axles/tmp/project_with_less_and_fancytree/public/packs
    [Webpacker] Hash: 32e57f147dbdcbbf0c82
    Version: webpack 4.43.0
    Time: 2269ms
    Built at: 04/22/2020 9:20:13 AM
                                         Asset       Size       Chunks                         Chunk Names
        js/application-bbe9c4a129ab949e0636.js    124 KiB  application  [emitted] [immutable]  application
    js/application-bbe9c4a129ab949e0636.js.map    139 KiB  application  [emitted] [dev]        application
                                 manifest.json  364 bytes               [emitted]              
    Entrypoint application = js/application-bbe9c4a129ab949e0636.js js/application-bbe9c4a129ab949e0636.js.map
    [./app/javascript/channels sync recursive _channel\.js$] ./app/javascript/channels sync _channel\.js$ 160 bytes {application} [built]
    [./app/javascript/channels/index.js] 211 bytes {application} [built]
    [./app/javascript/packs/application.js] 749 bytes {application} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 552 bytes {application} [built]
        + 3 hidden modules
    
    Completed 200 OK in 3998ms (Views: 3994.1ms | ActiveRecord: 0.7ms | Allocations: 23364)
    

    Open /books

    Visit http://localhost:3000/books. You should see no books

    Started GET "/books" for ::1 at 2020-04-22 09:23:56 +0300
    Processing by BooksController#index as HTML
      Rendering books/index.html.erb within layouts/application
      Book Load (0.2ms)  SELECT "books".* FROM "books"
      ↳ app/views/books/index.html.erb:13
      Rendered books/index.html.erb within layouts/application (Duration: 1.7ms | Allocations: 633)
    [Webpacker] Compiling...
    [Webpacker] Compilation failed:
    Hash: 60e4cd172f04061a66be
    Version: webpack 4.43.0
    Time: 4365ms
    Built at: 04/22/2020 9:24:02 AM
                                         Asset       Size       Chunks                         Chunk Names
        js/application-6ffd14b1620a1ad7ff96.js    717 KiB  application  [emitted] [immutable]  application
    js/application-6ffd14b1620a1ad7ff96.js.map    841 KiB  application  [emitted] [dev]        application
                                 manifest.json  364 bytes               [emitted]              
    Entrypoint application = js/application-6ffd14b1620a1ad7ff96.js js/application-6ffd14b1620a1ad7ff96.js.map
    [./app/javascript/channels sync recursive _channel\.js$] ./app/javascript/channels sync _channel\.js$ 160 bytes {application} [built]
    [./app/javascript/channels/index.js] 211 bytes {application} [built]
    [./app/javascript/packs/application.js] 1.52 KiB {application} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 552 bytes {application} [built]
        + 9 hidden modules
    

    Change books.html.erb

    Add a div element with id=tree

    Books

    <%# app/vies/books/index.html.erb %>
    <p id="notice"><%= notice %></p>
    
    <!-- THIS HERE IS WHAT WE ARE ADDING -->
    
    <div id="tree"></div>
    
    <!-- END: THIS HERE IS WHAT WE ARE ADDING -->
    
    <h1>Books</h1>
    
    <table>
      <thead>
        <tr>
          <th colspan="3"></th>
        </tr>
      </thead>
    
      <tbody>
        <% @books.each do |book| %>
          <tr>
            <td><%= link_to 'Show', book %></td>
            <td><%= link_to 'Edit', edit_book_path(book) %></td>
            <td><%= link_to 'Destroy', book, method: :delete, data: { confirm: 'Are you sure?' } %></td>
          </tr>
        <% end %>
      </tbody>
    </table>

    Final picture

    Showing how books index works with fancytree

    Errors that might occur

    No less-loader

    If no less loader is available the following could occur.

    Started GET "/books" for ::1 at 2020-04-22 08:52:40 +0300
       (0.1ms)  SELECT sqlite_version(*)
    Processing by BooksController#index as HTML
      Rendering books/index.html.erb within layouts/application
      Book Load (0.2ms)  SELECT "books".* FROM "books"
      ↳ app/views/books/index.html.erb:13
      Rendered books/index.html.erb within layouts/application (Duration: 2.1ms | Allocations: 762)
    [Webpacker] Compiling...
    [Webpacker] Compilation failed:
    Hash: 6210a48eff6aa0097a4c
    Version: webpack 4.43.0
    Time: 1464ms
    Built at: 04/22/2020 8:52:43 AM
                                         Asset       Size       Chunks                         Chunk Names
        js/application-8dcd2b9e8cc222d43650.js    718 KiB  application  [emitted] [immutable]  application
    js/application-8dcd2b9e8cc222d43650.js.map    841 KiB  application  [emitted] [dev]        application
                                 manifest.json  364 bytes               [emitted]              
    Entrypoint application = js/application-8dcd2b9e8cc222d43650.js js/application-8dcd2b9e8cc222d43650.js.map
    [./app/javascript/channels sync recursive _channel\.js$] ./app/javascript/channels sync _channel\.js$ 160 bytes {application} [built]
    [./app/javascript/channels/index.js] 211 bytes {application} [built]
    [./app/javascript/packs/application.js] 1.07 KiB {application} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 552 bytes {application} [built]
        + 9 hidden modules
    
    ERROR in ./node_modules/jquery.fancytree/dist/skin-lion/ui.fancytree.less 28:0
    Module parse failed: Unexpected token (28:0)
    File was processed with these loaders:
     * ./node_modules/less-loader/dist/cjs.js
    You may need an additional loader to handle the result of these loaders.
    |  * Helpers
    |  *----------------------------------------------------------------------------*/
    > .fancytree-helper-hidden {
    |   display: none;
    | }
     @ ./app/javascript/packs/application.js 19:0-59
    

    no less available

    If less was not installed this would happen

    Started GET "/books" for ::1 at 2020-04-22 09:26:54 +0300
    Processing by BooksController#index as HTML
      Rendering books/index.html.erb within layouts/application
      Book Load (0.1ms)  SELECT "books".* FROM "books"
      ↳ app/views/books/index.html.erb:13
      Rendered books/index.html.erb within layouts/application (Duration: 2.1ms | Allocations: 617)
    [Webpacker] Compiling...
    [Webpacker] Compilation failed:
    Hash: 1adef07918f113c9c28e
    Version: webpack 4.43.0
    Time: 1380ms
    Built at: 04/22/2020 9:26:56 AM
                                         Asset       Size       Chunks                         Chunk Names
        js/application-b032c274e5b1d8d383da.js    721 KiB  application  [emitted] [immutable]  application
    js/application-b032c274e5b1d8d383da.js.map    841 KiB  application  [emitted] [dev]        application
                                 manifest.json  364 bytes               [emitted]              
    Entrypoint application = js/application-b032c274e5b1d8d383da.js js/application-b032c274e5b1d8d383da.js.map
    [./app/javascript/channels sync recursive _channel\.js$] ./app/javascript/channels sync _channel\.js$ 160 bytes {application} [built]
    [./app/javascript/channels/index.js] 211 bytes {application} [built]
    [./app/javascript/packs/application.js] 1.52 KiB {application} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 552 bytes {application} [built]
        + 9 hidden modules
    
    ERROR in ./node_modules/jquery.fancytree/dist/skin-lion/ui.fancytree.less
    Module build failed (from ./node_modules/less-loader/dist/cjs.js):
    Error: Cannot find module 'less'
    Require stack:
    - /home/kireto/axles/tmp/pesho2/node_modules/less-loader/dist/index.js
    - /home/kireto/axles/tmp/pesho2/node_modules/less-loader/dist/cjs.js
    - /home/kireto/axles/tmp/pesho2/node_modules/loader-runner/lib/loadLoader.js
    - /home/kireto/axles/tmp/pesho2/node_modules/loader-runner/lib/LoaderRunner.js
    - /home/kireto/axles/tmp/pesho2/node_modules/webpack/lib/NormalModule.js
    - /home/kireto/axles/tmp/pesho2/node_modules/webpack/lib/NormalModuleFactory.js
    - /home/kireto/axles/tmp/pesho2/node_modules/webpack/lib/Compiler.js
    - /home/kireto/axles/tmp/pesho2/node_modules/webpack/lib/webpack.js
    - /home/kireto/axles/tmp/pesho2/node_modules/webpack-cli/bin/utils/validate-options.js
    - /home/kireto/axles/tmp/pesho2/node_modules/webpack-cli/bin/utils/convert-argv.js
    - /home/kireto/axles/tmp/pesho2/node_modules/webpack-cli/bin/cli.js
    - /home/kireto/axles/tmp/pesho2/node_modules/webpack/bin/webpack.js
        at Function.Module._resolveFilename (internal/modules/cjs/loader.js:982:15)
        at Function.Module._load (internal/modules/cjs/loader.js:864:27)
        at Module.require (internal/modules/cjs/loader.js:1044:19)
        at require (/home/kireto/axles/tmp/pesho2/node_modules/v8-compile-cache/v8-compile-cache.js:161:20)
        at Object.<anonymous> (/home/kireto/axles/tmp/pesho2/node_modules/less-loader/dist/index.js:8:36)
        at Module._compile (/home/kireto/axles/tmp/pesho2/node_modules/v8-compile-cache/v8-compile-cache.js:192:30)
        at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
        at Module.load (internal/modules/cjs/loader.js:1002:32)
        at Function.Module._load (internal/modules/cjs/loader.js:901:14)
        at Module.require (internal/modules/cjs/loader.js:1044:19)
        at require (/home/kireto/axles/tmp/pesho2/node_modules/v8-compile-cache/v8-compile-cache.js:161:20)
        at Object.<anonymous> (/home/kireto/axles/tmp/pesho2/node_modules/less-loader/dist/cjs.js:3:18)
        at Module._compile (/home/kireto/axles/tmp/pesho2/node_modules/v8-compile-cache/v8-compile-cache.js:192:30)
        at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
        at Module.load (internal/modules/cjs/loader.js:1002:32)
        at Function.Module._load (internal/modules/cjs/loader.js:901:14)
        at Module.require (internal/modules/cjs/loader.js:1044:19)
        at require (/home/kireto/axles/tmp/pesho2/node_modules/v8-compile-cache/v8-compile-cache.js:161:20)
        at loadLoader (/home/kireto/axles/tmp/pesho2/node_modules/loader-runner/lib/loadLoader.js:18:17)
        at iteratePitchingLoaders (/home/kireto/axles/tmp/pesho2/node_modules/loader-runner/lib/LoaderRunner.js:169:2)
        at iteratePitchingLoaders (/home/kireto/axles/tmp/pesho2/node_modules/loader-runner/lib/LoaderRunner.js:165:10)
        at /home/kireto/axles/tmp/pesho2/node_modules/loader-runner/lib/LoaderRunner.js:176:18
        at loadLoader (/home/kireto/axles/tmp/pesho2/node_modules/loader-runner/lib/loadLoader.js:47:3)
        at iteratePitchingLoaders (/home/kireto/axles/tmp/pesho2/node_modules/loader-runner/lib/LoaderRunner.js:169:2)
        at runLoaders (/home/kireto/axles/tmp/pesho2/node_modules/loader-runner/lib/LoaderRunner.js:365:2)
        at NormalModule.doBuild (/home/kireto/axles/tmp/pesho2/node_modules/webpack/lib/NormalModule.js:295:3)
        at NormalModule.build (/home/kireto/axles/tmp/pesho2/node_modules/webpack/lib/NormalModule.js:446:15)
        at Compilation.buildModule (/home/kireto/axles/tmp/pesho2/node_modules/webpack/lib/Compilation.js:739:10)
        at /home/kireto/axles/tmp/pesho2/node_modules/webpack/lib/Compilation.js:981:14
        at /home/kireto/axles/tmp/pesho2/node_modules/webpack/lib/NormalModuleFactory.js:409:6
     @ ./app/javascript/packs/application.js 20:0-59
    
    Completed 200 OK in 2801ms (Views: 2800.1ms | ActiveRecord: 0.1ms | Allocations: 5363)
    
     
  • kmitov 9:56 am on April 4, 2020 Permalink |
    Tags: bundler, , geminabox, rake,   

    bundle exec vs non bundle exec. 

    This article is part of the series [Everyday code].

    We use bundler to pack parts of the Instruction Steps Framework, especially the parts that should be easy to port to the rails world. We learned something about Bundler so I decide to share it with everybody.

    TL; DR;

    Question is – which of these two should you use:

    # Call bundle exec before rake
    $ bundle exec rake 
    
    # Call just rake
    $ rake

    ‘bundle exec rake’ will look at what is written in your .gemspec/Gemfile while rake will use whatever is in your env.

    Gem.. but in a box
    gem inabox with bundler

    Bundle exec

    For example we use geminabox, a great tool to keep an internal repo of plugins. In this way rails projects could include the Instruction Steps framework directly as a gem. This makes it very easy for rails projects to use the Instruction Steps.

    To put a gem in the repo one must execute:

    $ gem inabox

    You could make this call in three different ways. The difference is subtle, but important.

    Most of the time the env in which your shell is working will be almost the same as the env in which the gem is bundled. Except with the cases when it is not.

    From the shell

    # This will use the env of the shell. Whatever you have in the shell.
    $ gem inabox

    From a rake file

    If you have this rake file

    require 'rails/all'
    
    task :inabox do 
      system("gem inabox")
    end

    then you could call rake in the following ways:

    rake inabox

    # This will call rake in the env defined by the shell in which you are executing
    $ rake inabox

    bundle exec rake inabox

    # This will call rake in the env of the gem
    $ bundle exec rake inabox

    When using the second call bundle will look at the ‘.gemspec’/’Gemfile’ and what is in the gemspec. If non of the gems in the .gemspec adds the ‘inabox’ command to the env then the command is not found and an error occurs like:

    ERROR:  While executing gem ... (Gem::CommandLineError)
        Unknown command inabox

    If ‘gem inabox’ is called directly from the shell it works, but to call gem inabox from a rake job you must have ‘geminabox’ as development dependency of the gem. When calling ‘gem inabox’ from a shell we are not using the development env of the gem, we are using the env of the shell. But once we call ‘bundle exec rake inabox’ and it calls ‘gem inabox’, this second call is in the environment of the gem. So we should have a development_dependency to the ‘geminabox’ gem:

     spec.add_development_dependency 'geminabox'

    Nice. Simple, nice, logical. One just has to know it.

     
c
compose new post
j
next post/next comment
k
previous post/previous comment
r
reply
e
edit
o
show/hide comments
t
go to top
l
go to login
h
show/hide help
shift + esc
cancel