Recent Updates Toggle Comment Threads | Keyboard Shortcuts

  • 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 1:03 pm on April 22, 2020 Permalink |
    Tags: , jquery, , , , ,   

    Rails 6 + webpacker + jquery + sprockets + jquery plugin (fancetree) 

    So you might be in the process of migrating to webpacker. This means your sprockets should continue working. This is difficult. Sprockets wants jquery available in the view and you don’t have jquery available in the views. You have it in the webpacker packs.

    These things won’t work

      <script> 
        $("element_id")
      </script>

    jQuery is only available to the PACKS it is not available to the VIEWS.

    But there is a solution – expose-loader

    Here is how to setup jquery to be available to the views in sprockets app that you are migrating to rails 6. I am starting from the previous article were we set up the project from 0. – https://kmitov.com/posts/rails-6-webpacker-yarn-fancytree-less/

    $ yarn add expose-loader

    Add configuration for exposing of jquery

    // config/webpack/environments.js
    const { environment } = require('@rails/webpacker')
     
    const less_loader= {
     test: /\.less$/,
     use: ['css-loader', 'less-loader']
    };
    environment.loaders.append('less', less_loader)
    
    +
    +const webpack = require('webpack')
    +// this makes jquery available in all pack and you don't
    +// have to import or require it each time 
    +environment.plugins.prepend(
    +  'Provide',
    +  new webpack.ProvidePlugin({
    +    $: 'jquery',
    +    jQuery: 'jquery'
    +  })
    +)
    +
    +// this exposes jquery to be available in the views
    +// <script>
    +//   console.log($('#tree'))
    +// </script>
    +environment.loaders.append('expose', {
    +  test: require.resolve('jquery'),
    +  use: [{
    +    loader: 'expose-loader',
    +    options: '$'
    +  }, {
    +    loader: 'expose-loader',
    +    options: 'jQuery',
    +  }]
    +})

    Also expose fancytree

    // config/webpack/environments.js
    ...
    +// this exposes fancytree to be available in the views
    +// <script>
    +//   console.log($('#tree').fancytree())
    +// </script>
    +environment.loaders.append('fancytree', {
    +  test: require.resolve('jquery.fancytree'),
    +  use: [{
    +    loader: 'expose-loader',
    +    options: 'fancytree'
    +  }]
    +})
    

    And you are done.

    How in your views you could do:

    <script>
      console.log($('#tree'))
      $(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.
      })
    </script>
    

    Fancytree as a jquery plugin is working in rails 6 views and is available also to sprockets compiled files.

     
  • 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.

     
  • kmitov 10:45 am on April 3, 2020 Permalink |
    Tags: bash, , linux   

    99 versions are not good enough – [Everyday Code] 

    This article is part of the series [Everyday Code]

    You’ve done nothing until you release more than 99 versions of your product. 99 versions are just not good enough.

    TL;DR;

    Today we released version 103 of is-core – the core of the Instruction Steps Framework. We noticed a bug. Generally the build would produce two files:

    is-core-sdk-6.0.0.pre.103.js - that is the current version 
    is-core-sdk-latest.js  - this is pointing to the content of the latest version. 

    Problem was that while the current version was 103, the latest version in is-core-sdk-latest.js was pointing to version is-core-sdk-6.0.0.pre.99.js.

    As a conclusion – You have done nothing until you’ve released at least 100 versions of your software (and probably at least it works through a millennium shift with a leap year, but that’s another story)

    Details

    It’s pretty simple actually. This is what we were doing to get the latest file generated:

    # Creates is-core-sdk-latest.js link to the latest compiled 
     cd ../../release
     rm is-core-sdk-latest.js -f
    -latest=`find is-core-sdk-* -type f | tail -1`
    +latest=`ls -1v is-core-sdk* | tail -1`
     echo "Latest sdk is: $latest"
    

    Notice the find is-core-sdk-* -type f | tail -1 If the files are like

    # Find all the files but they are listed in non natural order of the integer for the version.
    # This code is: BAD
    $ find is-core-sdk-* -type f 
    is-core-sdk-6.0.0.pre.102.js
    is-core-sdk-6.0.0.pre.103.js
    is-core-sdk-6.0.0.pre.97.js
    is-core-sdk-6.0.0.pre.98.js
    
    # If we get just the tail it will give us version 99 which is clearly not right
    $ find is-core-sdk-* -type f | tail -1
    is-core-sdk-6.0.0.pre.99.js
    

    I have done this mistake at least a few times in my career.

    Solution is an option in ls:

    # This code is GOOD
    # This will list all the files
    $ ls -1v is-core-sdk*
    is-core-sdk-6.0.0.pre.97.js
    is-core-sdk-6.0.0.pre.98.js
    is-core-sdk-6.0.0.pre.102.js
    is-core-sdk-6.0.0.pre.103.js
    
    # This will get just the last
    $ ls -1v is-core-sdk* | tail -1
    is-core-sdk-6.0.0.pre.103.js
    

    Moral of the story

    For months I thougth we have a rock solid infrastructure. There was almost no failed build. Delivery to production is in 2 minutes for a pretty complex framework with a lot of projects and modules. And then it “broke” after months of stable work just as we were to release version 100.

    Show me your 100-th version of your product. Then we can talk.

     
  • kmitov 4:40 am on April 2, 2020 Permalink |
    Tags: , javascript, promise   

    Should you care about the settlement of Promise(s) or use Promise.finally() – [Everyday code] 

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

    – This logic should not be in Promise#finally()?

    – Why? We just care that the Promise is settled.

    – No. We care why it is settled.

    TL; DR;

    You might be tempted to put some specs logic in Promise#finally(), but here is why you should not do it.

    It’s like try/catch/finally

    In Instruction Steps Framework we try to load the list of parts in the instructions. There could be no list of parts in the instructions. How should we test this?

    Consider the examples:

    it("shows message 'No parts list provided' when there is no parts list", function() {
          // get the promise that the part list will be loaded, 
          // but we know that it will not be loaded, 
          // because this is how we setup the test. 
          this.promise = ... 
    
          // Using then()
          this.promise.then(() => {
                  expect($("#partsList").text()).toContain("No parts list provided");
                  done();
                });
    
          // Using catch()
          this.promise.catch(() => {
                  expect($("#partsList").text()).toContain("No parts list provided");
                  done();
                });
    
          // Using finally()
          this.promise.finally(() => {
                  expect($("#partsList").text()).toContain("No parts list provided");
                  done();
                });
        })
        

    Would you use then(), catch() or finally() in the spec?

    Using then()

    The purpose of the promise is to load a file. The file is not there. So it is not successfully settled. As this is not successfully settled then() should not be called.

    Using catch()

    The promise is promising us that it will load a file and show something on the screen. It fails. It settles, but fails. It would be best to put the spec in catch().

    Using finally()

    The promise fails as the test is setup like this. We’ve setup the test to have the wrong url. But this is only in this test. What if other clients are waiting on the same promise in a production code. Should they use finally()?

    From the finally documentation – https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally

    The finally() method can be useful if you want to do some processing or cleanup once the promise is settled, regardless of its outcome.

    The key here is “regardless of its outcome”. We get a Promise that is promising us to load a file. It fails. We care about the outcome. We care to have a successfully loaded list of parts, and if there is an ‘exceptional case’ we should catch() it and process it. We know exactly why the promise settles. We care.

     
  • kmitov 4:43 pm on April 1, 2020 Permalink |
    Tags: documentation,   

    This code is: ‘BAD’. This code is: ‘GOOD’ 

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

    TL; DR;

    When providing examples in developers documentation always add the bad example, but make it stupid clear that “this is a bad example and it should not be done like this, while this is a good example and it should be done like this!”

    Too long; but I will actually read it.

    We are copy/paste developers. We are fighting challenging problems on tight schedules with complex frameworks running on enormous stacks and we rarely use tools designed with ergonomics in mind. When we stumble on a problem we go for an internet search, copy the first result that looks like a solution and continue. (this is also my main mode of work with nginx. I love it, but it is always so much work to get this assets through a proxy, and this server was even designed for this.). Or AWS API…

    Yet, we somehow manage to deliver software platforms and most of them are in a running state. Brilliant.

    One thing that we are missing in the documentation is of course the understanding of the “why”. Why things are implemented like this? What were the considerations? Why it should not be implemented in different ways? You get this in a book, but you don’t have it in the documentation.

    Most of the time of course we don’t have the time to enter into the details, but I would make the argument that the more we understand the underlying problem, the more fun it is and the more satisfaction that we can get from work. Because most of the time we just can get not satisfaction. (no reference intended, but here it is – https://youtu.be/EU1t1PqASuQ?t=47)

    But this brings us to the problem. When developing a tutorial for how to use your framework should you include the bad examples along with the good?

    Should you include the bad examples in the documentation?

    This was an issue in the early stage of our Instruction Steps Framework developed at Axlessoft.com. As a proper framework it has documentation. As it is a framework it puts some boundaries and rules on what you should do. But we’ve tried to go one step further and describe why we are doing it like this.

    I’ve found it rewarding to include information about why specific decisions were taken not only what are these decisions.

    Example 1 – we’ve decided not to have a central object model on which all extension are based. A central object model would simplify a lot of things and is a lot easier for many developers, but it is less flexible.

    Example 2 – we’ve decided that certain event methods could return Promises, but could also not. Now why would you do this to yourself is a really good question?

    In these two and other examples I have taken the time to include proper documentation and tutorials on why we are implementing a feature like this. But I failed at one place. I was including both the bad and the good examples in the documentation.

    Looks at the example below for iterations from the previous article. This is an example for iterating over trees.

    anArray = node.getDescendants();
    // here you have all the elements of the tree in anArray
    
    // loop over the array
    anArray.forEach((a)=> {
       if(a.name.includes("1")) {
          print a.getDescendants().length
       }
    })

    Here is an example without getDescendants()

    anArray = []
    rootNode.traverse((node)=> {
       anArray.push(node)
    })

    See what I did here. If you came to this article looking for ways to iterate over trees it is completely unclear what is good and what is bad. Writing it right here in the text as “the first example is bad”, “the second example is good” won’t help. Who is even reading this text? So you arrive at the article, get the first iteration that is “bad iteration”, copy it and paste it in your code.

    The point is – it is valuable to include the bad examples as they give people the understanding why the good examples are good. But by including the bad example a large percentage of your readers will just copy paste the bad example and continue.

    My solution

    Mark your examples clearly in the code as “GOOD” or “BAD”. Not in the text. We are developers, we don’t read text.

    // BAD EXAMPLE - because it uses a lot of memory
    anArray = node.getDescendants();
    // here you have all the elements of the tree in anArray
    
    // loop over the array
    anArray.forEach((a)=> {
       if(a.name.includes("1")) {
          print a.getDescendants().length
       }
    })

    Again some text that no developer is ever reading, as it is in between the examples.

    // GOOD EXAMPLE
    anArray = []
    rootNode.traverse((node)=> {
       anArray.push(node)
    })

    See how clear this is. I dare any developer to now copy the bad example in their code. Hope this approach could help you in time of need.

     
  • kmitov 6:17 pm on March 31, 2020 Permalink |
    Tags: algorithms, , , tree   

    Everyday Trees in Computer Science – [Everyday Code] 

    The goal of this article was to equip developers with everything they need to know about trees to use Instruction Steps Framework (IS) and Instruction Converter (IC) that we are currently working on in Axlessoft. While developing the article for internal use I realized that these are things that every developer should have under their sleeve to successfully use tree structures in their day to day tasks.

    image of a tree that will make the article more interesting. Not a real programming tree

    Some of us might be tempted to revise the heavy Knuth books when it comes to solving the next tree problem. But generally we don’t need to do it. You know the meme:

    • First year Computer Science – “algorithms, data structures, etc …”
    • Second year – “functional programming, object oriented, etc…”
    • Last year – “cryptography, network programming, design of languages, etc
    • First day at work – “move this button to the left”

    So let’s start with a few things you need to know day to day.

    You will see “parent->children” more often than “node->left,right”, eg. binary trees.

    If you have a structure of parent and children this is a tree structure. You have three scenarios:

    1. The parent knows about the children and the children know about the parent. There is a parent.children and child.parent
    2. The parent knows about the children, but the children do not know about the parent. There is parent.children, but there is no child.parent
    3. The parent does not know about the children, but the children know about the parent. There is no parent.children, but there is child.parent

    In many books, algorithms and generally articles you will have people talking about Binary Trees. Now, it might be because of my line of work, but:

    1. I can not remember the last time that I had to properly work with a binary tree – something more than “just find this node”.
    2. For every time I have to work with “binary trees” there are at least 1024 times I have to work with “parent->children” structures.

    Yes, it is the same. But it is also not the same.

    Take away:

    All the three cases represent a tree. It is best to have case 1, but this is not always possible. You will deal with “parent->children” more often than with “left node”, “right node”.

    If there is a library for it, use a library.

    This should be a no-brainer, but a lot of times we like to code a few iterations here and there and develop things for basic iteration over a tree. Just don’t. If there is something tree related that you can not find a library to do for you in about 5 minutes internet search, then you are doing something worth of a Phd or you are searching with the wrong queries (or you are trying to do something stupid!).

    There are of course exceptions. Because when we, as great developers, implement Pre and Post order iteration we are doing it better than anyone else. So this exception is acceptable. Of course.

    Iteration -Pre, Post, In and visiting a node

    Visiting a node means that you are processing it. For example printing to the screen.

    There are a few basic ways. Just search for them on the internet. There are a lot of really nice resources of how you iterate over tress. The basic ways are:

    1. LRN – Left Right Node – first you visit the left child, then you visit the right child, then you visit the node. But as we say this is the theory. In a real life scenario with “parent->children” you visit children[0]..children[n] and then the parent
    2. LNR – Left Node Right – How do you do this for “parent->children” I don’t know.
    3. NLR – Node Left Right – first the parent then children[0]..children[n]
    4. NRL – Node Right Left – first the parent then children[n]..children[0]
    5. RLN – Right Left Node – first children[n]..children[0] then the parent.

    Avoid recursion

    Yes, you have trees. But avoid recursion. Use “traversers”. Almost any reasonably useful library has a “traverser” support. The idea is simple – somebody else is doing the recursion for you and this somebody else is the library.

    In pseudo code:

    rootNode = ... 
    rootNode.traverse((node)=> {
       print node
    })

    The idea is simple. You have a rootNode. It has a method ‘traverse’ that will traverse the whole tree, iterate, recurse, do a lot of magic, but at the end it will traverse the whole tree and will call the “print node” for every node in the tree.

    There is no recursion or iteration in your code. There is just traverse. Sometimes it is called “visitor” ( Visitors pattern, Gang of four, things like this). I kind of preferred “visitor” in early stage of my software development career, but now I more often call them “traversers”. Traversers are visitors, but not all visitors are traversers from my point of view. Would be happy to talk about this over a beer. Anyone..?

    Take away:

    If you are using a library that does not have traverse you should seriously consider using another library.

    Avoid ‘getDescendants()’ and getDeepChildren()

    Yes. These are some libraries that try to help. Imagine that you nave:

    node 1
      node 1.1
        node 1.1.1
      node 1.2
        node 1.2.1
        node 1.2.2
      node 1.3

    You need to get all the children of node 1 and all of their children and you need this in an array. Why? Because reasons. This is the feature request. There are libraries like BABYLON.js that try to help. They offer a “getDescendants()’ method. You can call:

    anArray = node.getDescendants();
    // here you have all the elements of the tree in anArray
    
    // loop over the array
    anArray.forEach((a)=> {
       print a
    })

    This method saves a lot. You do not need a “traverser”. You don’t need to know anything about order. You need all the elements of the whole tree so get an array. But there are drawbacks.

    1. It could be a large tree. So you are creating a copy, but this time in an array and memory and user experience are expensive.
    2. Even if it is not large you are still creating an array. Do you really need an array or you could just traverse the whole tree and do your job without previously creating an array?
    3. If you have to call getDescendants() more then once, you should refactor your code.

    Consider the following:

    anArray = node.getDescendants();
    // here you have all the elements of the tree in anArray
    
    // loop over the array
    anArray.forEach((a)=> {
       if(a.name.includes("1")) {
          print a.getDescendants().length
       }
    })

    In the example above we would like to print the number of descendants for each node that includes the string “1” in its name. How many times is the tree traversed? How many arrays are created? The answer is “a lot”. You traverse once for the rootNode and then for every other node again and you create new arrays.

    Take away:

    If there is a helper method it is probably giving you, the developer, something, but it is at the expense of the user experience – more memory, slower execution. Consider the implications of using helper methods. They might help you, but are they helping the user using your software.

     
  • kmitov 6:30 pm on March 28, 2020 Permalink |
    Tags: , git   

    Time to be praised as “The Guru” in your office – or how to move a folder from one Git repo to another Git repo and preserve the history 

    Ok. This is the 5th or 6th time that I am doing this and one of my colleagues asked me.

    Could you please write down how you do it so that we could do it without you next time.

    So here it is. 2 minutes read and you can move files from one git repo to another repo and move the commit history for some of the files. Commands below, but first:

    Word of caution (please):

    Such an enormous power under your fingers would make you and object of great attention in your company. Colleagues will sing songs about you. You would be praised as “The Guru”. Small talks before meetings will start with “Have you heard of that dude that can move files to a new git repo and keep all the history”. (believe me, it is like walking on water). Also I can not promise, but I am pretty sure you can get laid with such knowledge. I once got laid for knowing JUnit and Eclipse so… who knows.

    The disadvantage is that once people learn about you and your knowledge, you will be their go to person for questions about git. Git is quite complex and many people are lazy when it comes to reading documentation, so naturally many people would start asking you questions. Mostly stupid questions, of course. Can you handle the load?

    Task

    Moving folder integration/processors from old_repo repo to new_repo

    Commands

    # Enter new repo
    $ cd new_repo/
    
    # Make sure you are up to date
    $ git pull
    Already up to date.
    
    # Check remotes. Just to see what you've got
    $ git remote -v
    origin  git@host:new_repo (fetch)
    origin  git@host:new_repo (push)
    
    # You are in the new repo. Add the remote to the old_repo
    $ git remote add old_repo git@host:old_repo
    
    # Make sure the remote for old_repo is added
    $ git remote -v
    old_repo  git@host:old_repo (fetch)
    old_repo  git@host:old_repo (push)
    origin  git@host:new_repo (fetch)
    origin  git@host:new_repo (push)
    
    # Fetch from the branch of old_repo as it is now an origin
    $ git fetch old_repo
    ...
    From host:old_repo
     * [new branch]      dev                     -> old_repo/dev
    
    # Checkout the branch from old_repo
    $ git checkout --track old_repo/dev
    
    # Remove all the paths that you don't need. Keep the paths that you do need. Bunch of magic. Better read the documentation about it.  
    $ git filter-branch --force --index-filter   "git rm --cached -r --ignore-unmatch PATH_1 PATH_2 EVERYPATH_THAT_DOES_NOT_INCLUDE_INTEGRATION/PROCESSORS"   --prune-empty --tag-name-filter cat -- --all
    
    # Return to your master branch
    $ git checkout master
    
    # Merge the already filtered branch to your master.
    $ git merge dev  --allow-unrelated-histories
    
    # Think not twice, but three times. After this there is not turning back. It's the Fame or the Shame!!!
    $ git push -f

     
  • kmitov 6:26 am on March 23, 2020 Permalink |
    Tags: , prettier   

    Simple warning goes a long way 

    TL;DR;

    Just warn people with a simple message when you are deprecating a behavior in your tool and you are introducing a breaking change. It’s not that difficult

    Story

    Yesterday I kind of wake up to a nasty surprise in our local Continuous Integration.

    Continuous Integration on Jenkins failing miserably

    The problem

    Prettier (https://github.com/prettier/prettier/) have released a breaking change from version 1.19.1 to 2.0.1. This breaks most of our projects.

    The bigger problem

    Prettier is one of the nicest tools we’ve used. It allows us to keep the code formatted. It is also integrated in our CI and if a file is not properly formatted when committed the build fails.

    Several months ago it took us 17.5 hours to integrate Prettier to all developers and all projects and since then we had no problems.

    Then the update happened.

    I have nothing against breaking changes in an API or a project. I welcome them especially in non-critical tools as formatting the code. People learn. People need to learn and building and maintaining an API takes practice, consideration and a lot of experience. I have personally broken some API(s) that I’ve developed in the past. But what I think about breaking changes is that you should properly communicate this with you clients. We are using prettier in a very simple way. Here is the command:

    npx prettier app/**/*.js test/dummy/spec/javascripts/**/*_spec.js vendor/assets/javascripts/gcc/externs/*.externs.js --write --config prettier_conf.json"  

    That’s it. Turns out that as of version 2.0.1 prettier have broken this behavior and now if the project has no files for any of the globs it will return an error.

    For version 2.0.1

    $ mkdir pretti
    $ cd pretti/
    /pretti$ touch some.js
    /pretti$ npx prettier --version
    2.0.1
    /pretti$ ls
    some.js
    /pretti$ npx prettier app/**/*.js *.js
    [error] No files matching the pattern were found: "app/**/*.js".
    /pretti$ echo $?
    127

    For version 1.19.1

    $ mkdir pretti
    $ cd pretti/
    /pretti$ touch some.js
    /pretti$ npx prettier --version
    1.19.1
    /pretti$ ls
    some.js
    /pretti$ npx prettier app/**/*.js *.js
    /pretti$ echo $?
    0

    See what they did there. Previously if a pattern was not matched prettier returned 0 and now it returns 127, which for a Linux is just error.

    Conclusion and solution

    “Professionals have standards”

    When designing tools have Interoperability in mind. Do breaking changes, but release a version that warns people for the deprecation and for the breaking change they are about to experience. Like a simple print to console in version 1.99 (the one before the breaking change) that says “hey, this is deprecated and will be removed in 2.0. Please read here ‘link’ so that your clients don’t break, you don’t open issues on our github and you don’t write blog posts. Stay safe.”


     
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