Rails 7: Compiling assets with different folder structure - ruby-on-rails

I am having a problem with my rails app. Today I upgraded from Rails 6 to Rails 7. In Rails 7 webpacker is kinda removed, so I am now using ESBuild. In my project I have TypeScript and SASS files. To compile these assets I am running the following script:
import esbuild from "esbuild";
import { sassPlugin } from "esbuild-sass-plugin";
import postcss from 'postcss';
import autoprefixer from 'autoprefixer';
// Generate CSS/JS Builds
esbuild
.build({
entryPoints: ['app/assets/scss/app.scss', 'app/assets/ts/app.ts'],
outdir: "dist",
bundle: true,
metafile: true,
plugins: [
sassPlugin({
async transform(source) {
const { css } = await postcss([autoprefixer]).process(source);
return css;
},
}),
],
})
.then(() => console.log("⚡ Build complete! ⚡"))
.catch(() => process.exit(1));
In the app/assets/config/manifest.js I have the following content:
// = link_tree ../images
// = link_tree ../scss .css
// = link_tree ../ts .js
// = link_tree ../builds
But I am using a different folder structure that is probably causing the issues. This is my folder structure for .scss and .ts files:
app/
└── assets/
├── scss/
│ ├── math/
│ │ └── calculate.scss
│ └── app.scss
└── ts/
├── math/
│ └── calculate.ts
└── app.ts
As you can see, under assets I have created the folders scss and ts, which makes more sense to me than putting the typescript in the javascript folder first. The problem is that when I use the include tag:
<%= javascript_include_tag 'math/calculate', 'data-turbolinks-track': 'reload' %>
The asset cannot be found, I think it is caused by the fact that it is still looking in the assets/javascript folder.
I can see in the public/assets that all my ts files are now .js files and all the .scss files are .css files, so ESBuild does his job, but the problem is in the including part I think. I get this error:
The asset "math/calculate.js" is not present in the asset pipeline.
Can someone help me fix this, I hope I can keep my folder structure like it is now!?

First, I have to mention that rails comes with tools to build javascript and css, we'll be doing our own set up, but these are still useful as a reference:
https://github.com/rails/jsbundling-rails
https://github.com/rails/cssbundling-rails
In Rails 7 js, css, and any other local assets are served by sprockets. For sprockets to find your assets they have to be in Rails.application.config.assets.paths; any directory under app/assets/ will be automatically be part of asset paths:
>> Rails.application.config.assets.paths
=> ["/home/alex/code/SO/ts/app/assets/builds",
"/home/alex/code/SO/ts/app/assets/config",
"/home/alex/code/SO/ts/app/assets/scss",
"/home/alex/code/SO/ts/app/assets/ts",
...
Most of the time an app would serve application.js and application.css. Any assets that go into javascript_include_tag, stylesheet_link_tag or anything else that needs to be served in the browser, also have to be declared for precompilation. For js and css these are the entrypoints, where you #import other files to be part of a bundle:
# app/assets/config/manifest.js
//= link_tree ../builds
app/assets/builds directory is in asset paths and every file in that directory will be precompiled in production. Because sprockets works with js and css, there has to be an additional build step to compile ts => js and scss => css.
The asset source files can be anywhere and organized however you like, rails doesn't care about them, as long as compiled assets end up in app/assets/builds.
CSS:
// esbuild.css.js
import esbuild from "esbuild";
import { sassPlugin } from "esbuild-sass-plugin";
import postcss from "postcss";
import autoprefixer from "autoprefixer";
const watch = process.argv.includes("--watch");
esbuild
.build({
// declare you entrypoint, where, I hope, you're importing all other styles.
entryPoints: ["app/assets/scss/app.scss"],
// spit out plain css into `builds`, so that sprockets can serve them.
outdir: "app/assets/builds",
// you'll want this running with --watch flag
watch: watch,
publicPath: "assets",
bundle: true,
metafile: true,
plugins: [
sassPlugin({
async transform(source) {
const { css } = await postcss([autoprefixer]).process(source);
return css;
},
}),
],
})
.then(() => console.log("⚡ CSS build complete! ⚡"))
.catch(() => process.exit(1));
JS:
// esbuild.js
import esbuild from "esbuild";
import glob from "glob";
const watch = process.argv.includes("--watch");
esbuild
.build({
// you say you want every file compiled separately, best I could come up:
entryPoints: glob.sync("app/assets/ts/**/*.ts"),
// spit out plain js into `builds`
outdir: "app/assets/builds",
watch: watch,
publicPath: "assets",
bundle: true,
metafile: true,
})
.then(() => console.log("⚡ JS build complete! ⚡"))
.catch(() => process.exit(1));
Rails compiles js and css separately, and so can we. This also avoids nested build/ts/ and build/scss/ directories.
In case you don't have this:
# bin/dev
#!/usr/bin/env sh
if ! gem list foreman -i --silent; then
echo "Installing foreman..."
gem install foreman
fi
exec foreman start -f Procfile.dev "$#"
# Procfile.dev
web: bin/rails server
_js: yarn build --watch
css: yarn build:css --watch
Add build scripts:
// package.json
{
...
"scripts": {
"build": "node esbuild.js",
"build:css": "node esbuild.css.js"
}
}
I think this should be everything:
app/assets
├── builds # <= this is the only directory "servable" by sprockets
├── config
│ └── manifest.js # //= link_tree ../builds
├── scss
│ ├── app.scss # #import "./math/calculate.scss"
│ └── math
│ └── calculate.scss
└── ts
├── app.ts # no imports here? ¯\_(ツ)_/¯
└── math
└── calculate.ts # console.log("do the calc")
You can run bin/dev to start the server and start compiling scss and ts:
$ bin/dev
or run build scripts manually:
$ yarn build && yarn build:css
⚡ JS build complete! ⚡
⚡ CSS build complete! ⚡
And it compiles into builds directory:
app/assets/builds
├── app.css
├── app.js
└── math
└── calculate.js
These ^ are the assets you can use in your layout:
<%= stylesheet_link_tag "app", "data-turbo-track": "reload" %>
<%= javascript_include_tag "app", "math/calculate", "data-turbo-track": "reload", defer: true %>
Now, when you ask for math/calculate, sprockets will find it in Rails.application.config.assets.paths and it will check if it is declared for precompilation:
# NOTE: if it is, you get a digested url to that file
>> helper.asset_path("math/calculate.js")
=> "/assets/math/calculate-11273ac5ce5f76704d22644f4b03b94908a318451578f2d10a85847c0f7f2998.js"
# everything as expected here
>> puts URI.open(helper.asset_url("math/calculate.js", host: "http://localhost:5555")).read
(() => {
// app/assets/ts/math/calculate.ts
console.log("do the calc");
})();
Hook your custom build scripts into a few rails tasks, like assets:precompile so that everything gets built automatically when deploying:
https://github.com/rails/jsbundling-rails/blob/v1.1.1/lib/tasks/jsbundling/build.rake
# lib/tasks/build.rake
namespace :ts_scss do
desc "Build your TS & SCSS bundle"
task :build do
# put your build commands here VVVVVVVVVVVVVVVVVVVVVVVVVVVV
unless system "yarn install && yarn build && yarn build:css"
raise "Command build failed, ensure yarn is installed"
end
end
end
if Rake::Task.task_defined?("assets:precompile")
Rake::Task["assets:precompile"].enhance(["ts_scss:build"])
end
if Rake::Task.task_defined?("test:prepare")
Rake::Task["test:prepare"].enhance(["ts_scss:build"])
elsif Rake::Task.task_defined?("spec:prepare")
Rake::Task["spec:prepare"].enhance(["ts_scss:build"])
elsif Rake::Task.task_defined?("db:test:prepare")
Rake::Task["db:test:prepare"].enhance(["ts_scss:build"])
end
OMG! Do not precompile assets in development! I don't know where people got that idea. You'll be serving assets from public/assets and they will not update automatically. If you did, just undo it:
bin/rails assets:clobber
Long story short:
app/assets/ts/app.ts # compile to `js` with esbuild
V # output into builds
app/assets/builds # is in `Rails.application.config.assets.paths`
V # is in `manifest.js` (//=link_tree ../builds)
javascript_include_tag("app")
V
asset_path("app.js") # served by `sprockets`
V # or by something else in production (thats why we precompile)
Browser!

Related

Rails 6 with Webpacker & SCSS not working

I am having two separate issues (or maybe they are combined and I'm missing it). The app was picking up the bootstrap styles, but is no longer doing so.
Issue 1
When I make any updates to application.js no matter how small (an extra line break anywhere in the file) it would kill the imported bootstrap files.
Now I can't get the bootstrap styles to show period.
Issue 2
When I put the following into the head tag in application.html.erb:
<!-- before -->
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<!-- after -->
It renders no output to the browser:
<!-- before -->
<!-- after -->
I'm uncertain if this is a Webpacker issue or what is causing this. Please let me know if any other details are required.
I have a full repo here that you can clone / browse with instructions for bringing up the dev environment with Docker.
You can check it out here: Funtime Github repo
Webpacker is not configured to extract any css.
Set extract_css: true in webpacker.yml. Setting this to true will extract any css you import in js files under /packs to separate css files. In your case any css imported in application.js will be available in application.css. If you had a pack called test, the css will be extracted to test.css.
Move out application.scss from packs to /css (or stylesheets, whatever you want)
Update application.js like this:
import "./../css/application";
import Rails from "#rails/ujs";
....
Make sure you start webpacker dev server with bin/webpacker-dev-server.
Here's how it looks like:
With Shakapacker:
app/javascript:
├── packs:
│ # only webpack entry files here
│ └── online_giving.js
│ └── online_giving.scss
└── src:
│ └── my_component.js
└── stylesheets:
│ └── my_styles.css
└── images:
└── logo.svg
And in the app/views/layouts/mylayout.html.haml :
= javascript_pack_tag 'online_giving'
= stylesheet_pack_tag 'online_giving'
you don't need to import the online_giving.scss in the online_giving.js file, Shakapacker will find it.

How can I easily reference separate js / css files for debugging when using gulp or grunt?

I'm developing a legacy ASP.NET MVC 5 project which still uses ASP.NET Bundling and Minification. I'm interested in switching to Gulp or Grunt, because I need to save source maps for my js files.
It seems easy to generate a minified script bundle with Gulp or Grunt, but what I do not understand yet is the recommended setup for loading single js files when debugging and minified bundles in production. I guess it would be quite easy to generate a razor view for including the scripts as part of my Grunt / Gulp compilation process, but it feels like re-inventing the wheel.
For instance, in ASP.NET MVC i can write something like this:
#Scripts.Render("~/bundles/MyJSBundle")
and it will automatically load separate js files in development and a single script bundle in production. What is the easiest way achieve this with Gulp or Grunt?
Short answer:
Typically when using Grunt you generate two builds - one for "dev" (development) and another for "dist" (distribution/production). Whereby for the scenario you've described;
Both the "dev" and "dist" builds generate a single concatenated/minified file version (e.g. bundle.min.js) derived from multiple source .js files.
However, only the "dev" build generates an additional Source Map file(s), that holds information about your original .js files, for the purpose of debugging during the development lifecycle.
Grunt plugins, such as grunt-processhtml, provide a way to update any links to .js assets in the .html file. For example, let's say your source .html contains these two links;
<script src="js/a.js"/>
<script src="js/b.js"/>
They can be substituted during the "dist" and/or "dev" build step to the following single <script> element:
<script src="dir/bundle.min.js"/>
Example demo:
The following somewhat contrived example demonstrates how you may approach your requirement using Grunt.
Let's say our initial project directory is structured as follows:
project
├── Gruntfile.js
├── node_modules
│   └── ...
├── package.json
└── src
├── index.html
└── js
├── a.js
└── b.js
Note, in the src directory we have a single index.html file, and two .js files in the js directory.
In the contents of index.html shown below it contains two <script> elements, each one referencing a .js file.
project/src/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>demo</title>
</head>
<body>
<!--build:js js/bundle.min.js-->
<script src="js/a.js"></script>
<script src="js/b.js"></script>
<!--/build-->
</body>
</html>
Note, the custom HTML comments encasing both <script> elements. These custom HTML comments are utilized by grunt-processhtml. The part that reads; js/bundle.min.js in the comment essentially defines the new pathname to be used.
Let's consider the following Gruntfile.js configuration:
Gruntfile.js
module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-processhtml');
grunt.initConfig({
// 1. Concatenate .js files.
concat: {
dist: {
src: [
'src/js/a.js',
'src/js/b.js'
],
dest: './dist/js/bundle.min.js'
},
dev: {
options: {
sourceMap: true
},
src: [
'src/js/a.js',
'src/js/b.js'
],
dest: './dev/js/bundle.min.js'
}
},
// 2. Minify .js files.
uglify: {
dist: {
files: {
'./dist/js/bundle.min.js': './dist/js/bundle.min.js' // dest : src
}
},
dev: {
options: {
mangle: false,
sourceMap: true,
sourceMapIn: './dev/js/bundle.min.js.map'
},
files: {
'./dev/js/bundle.min.js': './dev/js/bundle.min.js' // dest : src
}
}
},
// 2. Process .html file.
processhtml: {
dist: {
files: {
'./dist/index.html': './src/index.html' // dest : src
}
},
dev: {
files: {
'./dev/index.html': './src/index.html' // dest : src
}
}
}
});
grunt.registerTask('default', ['dist', 'dev']);
grunt.registerTask('dist', [
'concat:dist',
'uglify:dist',
'processhtml:dist'
]);
grunt.registerTask('dev', [
'concat:dev',
'uglify:dev',
'processhtml:dev'
]);
};
Explanation of Gruntfile.js:
In addition to the previously mentioned grunt-processhtml plugin the following two are also utilized in this example:
grunt-contrib-concat - for concatenating the two .js files.
grunt-contrib-uglify - for minifying the .js file.
Note: There are other plugins available for these types of task. I have chosen these additional two plugins for the purpose of this demonstration.
Each of the three Tasks (concat, uglify, and processhtml) contain two separate Targets named dist and dev. The main differences in each Target are:
Different dest (destination) paths for the resultant generated .jsfile(s).
For the concat:dev and uglify:dev Targets its options object defines the configuration for the resultant Source Map file.
At the end of Gruntfile.js three different grunt.registerTask() have been defined. Each one defines a taskList that essentially defines which Task and Target to run in the order specified.
For example consider the following registered task named dist:
grunt.registerTask('dist', [
'concat:dist',
'uglify:dist',
'processhtml:dist'
]);
When running grunt dist via the command line Grunt essentially invokes this Task, which subsequently performs the following in this order:
Firstly, runs the dist Target defined in the concat Task.
Then runs the dist Target defined in the uglify Task.
Finally, runs the dist Target defined in the processhtml Task.
Running Gruntfile.js (above) and its output
Running the following command via the command line:
grunt dev
outputs the following additional assets to the project directory:
project
├── ...
├── dev
│   ├── index.html
│   └── js
│   ├── bundle.min.js
│   └── bundle.min.js.map
└── ...
As you can see it has:
Created a new dev folder in the root of the project directory.
The two <script> elements originally defined in project/src/index.html have been substituted in the newly generated project/dev/index.html with a single <script> tag as follows:
<script src="js/bundle.min.js"></script>
Both files; project/src/js/a.js and project/src/js/b.js, have been concatenated and minified in the resultant project/dev/js/bundle.min.js.
The following source map file has been generated; project/dev/js/bundle.min.js.map. This file essentially maps back to the original project/src/js/a.js and project/src/js/b.js files.
Running the following command via the command line:
grunt dist
outputs the following additional assets to the project directory:
project
├── ...
├── dist
│ ├── index.html
│ └── js
│ └── bundle.min.js
└── ...
As you can see this time it has;
Created a dist folder in the root of the project directory.
Again, the two <script> elements originally defined in project/src/index.html have been substituted in the newly generated project/dist/index.html with a single <script> tag (as per the aforementioned dev Task).
Again, both files; project/src/js/a.js and project/src/js/b.js, have been concatenated and minified in the resultant project/dist/js/bundle.min.js.
However, the main notable difference is that NO source map file has been created.
Running the following command via the command line:
grunt
will produce both the outputs defined in the previous steps 1 and 2.

How to build with Electron-Builder when index.html has relative path

I want to build a project written in electron.js with Electron-builder when index.html file in the parent directory, a simple app with structure like below:
project
├── css
│ ├── style.css
│ └── img.png
├── electron
│ ├── main.js
│ └── package.json
│── index.html
│── index.js
└── icon.png
use win.loadFile('./../index.html'); in the main.js file, everythings works fine when use electron . command in the electron folder, but after run electron-builder, when run the app with the executable file nothing works and the index.html file doesn't appear.
I was trying to use files config in the package.json file but no success:
"build": {
"appId": "x.y.z",
"productName": "projectName",
"files": ["**/*", "build/icon.*", "../**/*"]
},
I don't know what to do ?!
I've found this issue https://github.com/electron-userland/electron-builder/issues/2693 but can't undestand it and don't know what should I do?
I can't answer in the general case, but here's the solution when using vue-electron-builder and TypeScript. This will let you organize your files in any arbitrary hierarchy.
Create a file vue.config.js in your project root (this more or less corresponds to the overrides you'd need for an electron project with webpack):
module.exports = {
pluginOptions: {
electronBuilder: {
// my 'main' process is in src/main.ts:
mainProcessFile: 'src/main/main.ts',
// my 'main.ts' should get recompiled if any of these change:
mainProcessWatch: [
'src/main/extra_file_1.ts',
'src/main/extra_file_2.ts',
]
}
},
pages: {
// I only have one renderer process (otherwise I'd need another entry in 'pages')
"renderer": {
// 'main' for the renderer process
entry: "src/renderer/main.ts",
// this is the part you care about: you can put the template anywhere
template: "src/viewer/index.html",
// this makes sure your renderer page has all the ts-to-js chunks it needs
chunks: ["chunk-vendors", "chunk-common", "renderer"]
}
}
};
Note that if you want your index.html to <link> to a static css file, you should put the static file in the public folder of your project root. Then you can link to it like this:
<link rel="stylesheet" href="<%= BASE_URL %>my_css_file.css">

I'm trying to install toastr by using webpacker in Rails

I'm trying to install toastr by using webpacker in Rails
Now, I'm using toastr by gem 'toastr-rails'
But replace it by using yarn add toastr
How I do setting app/javascript/packs/application.js?
This is application.js in my application
app/japascript/packs/application.js
import "jquery"
global.$ = require("jquery")
// JS-----
// install by yarn
import 'modaal/dist/js/modaal'
import 'flatpickr/dist/flatpickr'
require("jquery-ui-bundle")
// CSS ------
// install by yarn
import 'flatpickr/dist/flatpickr.min.css';
import 'font-awesome/css/font-awesome.min.css';
import 'jquery/dist/jquery';
import 'stylesheets/application';
import 'javascripts/application';
require.context('../images', true, /\.(png|jpg|jpeg|svg)$/);
$("#login-button").click(function(event){
event.preventDefault();
$('form').fadeOut(500);
$('.wrapper').addClass('form-success');
});
console.log('Hello World from Webpacker')
// Support component names relative to this directory:
var componentRequireContext = require.context("components", true)
var ReactRailsUJS = require("react_ujs")
ReactRailsUJS.useContext(componentRequireContext)
config/initializers/asset.rb
Do I have to add something in the asset.rb?
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your
assets.
Rails.application.config.assets.version = '1.0'
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Add Yarn node_modules folder to the asset load path.
Rails.application.config.assets.paths <<
Rails.root.join('node_modules')
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css
)
to use toastr in rails app with webpacker:
install toastr with yarn: yarn add toastr
load lib in your application.js:
import toastr from 'toastr';
toastr.options = {
"closeButton": true
.... add options here ...
};
global.toastr = toastr;
create helper method (for example in your application_helper.rb file):
def custom_bootstrap_flash
flash_messages = []
flash.each do |type, message|
type = 'success' if type == 'notice'
type = 'error' if type == 'alert'
text = "toastr.#{type}('#{message}');"
flash_messages << text.html_safe if message
end
flash_messages = flash_messages.join('\n')
"<script>$(document).ready(function() { #{ flash_messages } });</script>".html_safe
end
use it on the bottom of your layout file (app/views/layouts/application.html.erb):
<%= custom_bootstrap_flash %>
This advice assumes you have a node_modules folder at the root of your rails application.
If you installed toastr with the command...
yarn add toastr
Then yarn will automatically generate a node_modules folder in your application root. This folder gets created the first time you add a dependency with yarn add.
Its possible that you already have this node_modules folder and you just cant see it. This is because by default rails adds this folder to .gitignore which makes it invisible in some file systems.
You can check to see if you have a node_modules folder by temporarily removing node_modules line inside your .gitignore file then refreshing your file tree.
You should be able to see the node_modules folder at this time. Add node_modules back to your .gitignore after you've confirmed that this folder exists in your project.
Once this is done then open your config/initializers/assets.rb file and add the following lines
Rails.application.config.assets.paths << Rails.root.join('node_modules')
Rails.application.config.assets.precompile += ['node_modules/toastr/build/toastr.min.js']
Rails.application.config.assets.precompile += ['node_modules/toastr/build/toastr.min.css']
Then add the following require statement to your application.js file
//= require toastr/build/toastr.min
And assuming your using .scss extensions on your css files then you should add the following to your application.scss file
#import 'toastr/build/toastr.min';
This works in my environment.
Note that toastr requires jquery be loaded before it is loaded.
As such you should make sure that you include the jquery script before your <%= javascript_include_tag 'application' %> tag inside your application.html.erb layout.

How to add multiple source_path in Rails Webpacker

We are using Webpacker for loading JavaScripts and CSS files into the webpage.
Currently, in webpacker.yml we have set the source_path to app/javascript. Which is working fine to load the JavaScript files form this directory.
But in our application, we have an engines directory, and all the JavaScript files are located inside different engines in engines directory, to load these JavaScript files we created a link in app/javascript/packs for each pack in engines directory.
Is there a better way to do this, without providing links OR by providing multiple source_path in the webpacker.yml file.
For reference:
This is the folder structure currently we have:
-root
|
|-app
|-javascript
|-packs
|-[link to engine1.js pack files]
|-[link to engine2.js pack files]
|-engines
|- engine1
|-app
|-javascript
|-packs
|-engine1.js
|- engine2
|-app
|-javascript
|-packs
|-engine1.js
And this is how the webpacker.yml configuration
default: &default
source_path: app/javascript
source_entry_path: packs
public_output_path: packs
cache_path: tmp/cache/webpacker
# Additional paths webpack should lookup modules
# ['app/assets', 'engine/foo/app/assets']
resolved_paths: ['app/assets']
I think you'll want to do something like this:
additional_paths: ['engines']
Source: https://github.com/rails/webpacker#resolved
If you are adding Webpacker to an existing app that has most of the assets inside app/assets or inside an engine, and you want to share that with webpack modules, you can use the additional_paths option available in config/webpacker.yml. This lets you add additional paths that webpack should lookup when resolving modules:
Perhaps a better way to do what you mention is to take advantage of the webpacker folder structure.
Instead of creating links to files inside the "app/javascript/packs" folder, maybe you could reference them through the "index.js" files in those folders.
According to the documentation, webpacker will look for the "index.js" file inside each imported folder.
So you could modify the file structure to something like this:
-root
|
|-app
|-javascript
|-packs
|-engine1.js
|-engine2.js
|-engines
|- engine1
|-index.js
|-app
|-javascript
|-packs
|-engine1.js
|- engine2
|-index.js
|-app
|-javascript
|-packs
|-engine1.js
Create the files and import the respective folder
// app/javascript/packs/engine1.js
import 'engines/engine1'
// app/javascript/packs/engine2.js
import 'engines/engine2'
This will import the "index.js" file from the mentioned folder, and within that file you can import everything else you need from the "engineX" folder.
Then you can reference them as follows
<%# app/views/layouts/application.html.erb %>
<%= javascript_pack_tag 'engine1' %>
<%= javascript_pack_tag 'engine2' %>
All this can be done, without the need to modify the default Webpacker configuration

Resources