Skip to content

Commit

Permalink
Add Sass support for esbuild, update frontend dependency versions (br…
Browse files Browse the repository at this point in the history
…idgetownrb#542)

* Add Sass support for esbuild, update frontend dep versions

* Add ability to configure file filter for PostCSS plugin

* Update package.json.erb

* Update esbuild.defaults.js.erb

* Fix PostCSS esbuild watch bug & remove easy-import

* Touch up Sass docs
  • Loading branch information
jaredcwhite authored May 21, 2022
1 parent 62ffbbb commit 1a4482b
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,33 @@
const path = require("path")
const fsLib = require("fs")
const fs = fsLib.promises
const { pathToFileURL, fileURLToPath } = require("url")
const glob = require("glob")
const postcss = require("postcss")
const postCssImport = require("postcss-import")
const readCache = require("read-cache")

// Detect if an NPM package is available
const moduleAvailable = name => {
try {
require.resolve(name)
return true
} catch (e) { }
return false
}

// Generate a Source Map URL (used by the Sass plugin)
const generateSourceMappingURL = sourceMap => {
const data = Buffer.from(JSON.stringify(sourceMap), "utf-8").toString("base64")
return `/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${data} */`
}

// Import Sass if available
let sass
if (moduleAvailable("sass")) {
sass = require("sass")
}

// Glob plugin derived from:
// https://github.com/thomaschaaf/esbuild-plugin-import-glob
// https://github.com/xiaohui-zhangxh/jsbundling-rails/commit/b15025dcc20f664b2b0eb238915991afdbc7cb58
Expand Down Expand Up @@ -61,24 +83,23 @@ const importGlobPlugin = () => ({
},
})

const postCssPlugin = (options) => ({
// Plugin for PostCSS
const postCssPlugin = (options, configuration) => ({
name: "postcss",
async setup(build) {
// Process .css files with PostCSS
build.onLoad({ filter: /\.(css)$/ }, async (args) => {
build.onLoad({ filter: (configuration.filter || /\.css$/) }, async (args) => {
const additionalFilePaths = []
const css = await fs.readFile(args.path, "utf8")

// Configure import plugin so PostCSS can properly resolve `@import`ed CSS files
const importPlugin = postCssImport({
filter: itemPath => {
// We'll want to track any imports later when in watch mode
additionalFilePaths.push(path.resolve(path.dirname(args.path), itemPath))
return true
},
filter: itemPath => !itemPath.startsWith("/"), // ensure it doesn't try to import source-relative paths
load: async filename => {
let contents = await readCache(filename, "utf-8")
const filedir = path.dirname(filename)
// We'll want to track any imports later when in watch mode:
additionalFilePaths.push(filename)

// We need to transform `url(...)` in imported CSS so the filepaths are properly
// relative to the entrypoint. Seems icky to have to hack this! C'est la vie...
Expand Down Expand Up @@ -106,6 +127,65 @@ const postCssPlugin = (options) => ({
},
})

// Plugin for Sass
const sassPlugin = (options) => ({
name: "sass",
async setup(build) {
// Process .scss and .sass files with Sass
build.onLoad({ filter: /\.(sass|scss)$/ }, async (args) => {
if (!sass) {
console.error("error: Sass is not installed. Try running `yarn add sass` and then building again.")
return
}

const modulesFolder = pathToFileURL("node_modules/")

const localOptions = {
importers: [{
// An importer that redirects relative URLs starting with "~" to
// `node_modules`.
findFileUrl(url) {
if (!url.startsWith('~')) return null
return new URL(url.substring(1), modulesFolder)
}
}],
sourceMap: true,
...options
}
const result = sass.compile(args.path, localOptions)

const watchPaths = result.loadedUrls
.filter((x) => x.protocol === "file:" && !x.pathname.startsWith(modulesFolder.pathname))
.map((x) => x.pathname)

let cssOutput = result.css.toString()

if (result.sourceMap) {
const basedir = process.cwd()
const sourceMap = result.sourceMap

const promises = sourceMap.sources.map(async source => {
const sourceFile = await fs.readFile(fileURLToPath(source), "utf8")
return sourceFile
})
sourceMap.sourcesContent = await Promise.all(promises)

sourceMap.sources = sourceMap.sources.map(source => {
return path.relative(basedir, fileURLToPath(source))
})

cssOutput += '\n' + generateSourceMappingURL(sourceMap)
}

return {
contents: cssOutput,
loader: "css",
watchFiles: [args.path, ...watchPaths],
}
})
},
})

// Set up defaults and generate frontend bundling manifest file
const bridgetownPreset = (outputFolder) => ({
name: "bridgetownPreset",
Expand Down Expand Up @@ -151,9 +231,9 @@ const bridgetownPreset = (outputFolder) => ({
// We have an entrypoint!
manifest[stripPrefix(value.entryPoint)] = outputPath
entrypoints.push([outputPath, fileSize(key)])
} else if (key.match(/index(\.js)?\.[^-.]*\.css/) && inputs.find(item => item.endsWith("index.css"))) {
} else if (key.match(/index(\.js)?\.[^-.]*\.css/) && inputs.find(item => item.match(/\.(s?css|sass)$/))) {
// Special treatment for index.css
manifest[stripPrefix(inputs.find(item => item.endsWith("index.css")))] = outputPath
manifest[stripPrefix(inputs.find(item => item.match(/\.(s?css|sass)$/)))] = outputPath
entrypoints.push([outputPath, fileSize(key)])
} else if (inputs.length > 0) {
// Naive implementation, we'll just grab the first input and hope it's accurate
Expand Down Expand Up @@ -182,9 +262,12 @@ const postCssConfig = postcssrc.sync()
module.exports = (outputFolder, esbuildOptions) => {
esbuildOptions.plugins = esbuildOptions.plugins || []
// Add the PostCSS & glob plugins to the top of the plugin stack
esbuildOptions.plugins.unshift(postCssPlugin(postCssConfig))
esbuildOptions.plugins.unshift(postCssPlugin(postCssConfig, esbuildOptions.postCssPluginConfig || {}))
if (esbuildOptions.postCssPluginConfig) delete esbuildOptions.postCssPluginConfig
esbuildOptions.plugins.unshift(importGlobPlugin())
// Add the Bridgetown preset to the bottom of the plugin stack
// Add the Sass plugin
esbuildOptions.plugins.push(sassPlugin(esbuildOptions.sassOptions || {}))
// Add the Bridgetown preset
esbuildOptions.plugins.push(bridgetownPreset(outputFolder))

// esbuild, take it away!
Expand All @@ -200,7 +283,7 @@ module.exports = (outputFolder, esbuildOptions) => {
".ttf": "file",
".eot": "file",
},
resolveExtensions: [".tsx",".ts",".jsx",".js",".css",".json",".js.rb"],
resolveExtensions: [".tsx", ".ts", ".jsx", ".js", ".css", ".scss", ".sass", ".json", ".js.rb"],
nodePaths: ["frontend/javascript", "frontend/styles"],
watch: process.argv.includes("--watch"),
minify: process.argv.includes("--minify"),
Expand Down
18 changes: 9 additions & 9 deletions bridgetown-core/lib/bridgetown-core/commands/new.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def self.banner
desc: "Skip 'yarn install'"
class_option :"use-sass",
type: :boolean,
desc: "(Webpack only) Create a Sass configuration instead of using PostCSS"
desc: "Set up a Sass configuration for your stylesheet"

DOCSURL = "https://bridgetownrb.com/docs"

Expand All @@ -62,11 +62,6 @@ class << self
def new_site
raise ArgumentError, "You must specify a path." if args.empty?

if frontend_bundling_option != "webpack" && options["use-sass"]
raise ArgumentError,
"To install Sass, you must choose Webpack (-e webpack) as your frontend bundler"
end

new_site_path = File.expand_path(args.join(" "), Dir.pwd)
@site_name = new_site_path.split(File::SEPARATOR).last

Expand Down Expand Up @@ -98,7 +93,11 @@ def frontend_bundling_option
end

def postcss_option
!(frontend_bundling_option == "webpack" && options["use-sass"])
!options["use-sass"]
end

def disable_postcss?
options["use-sass"] && options["frontend-bundling"] == "webpack"
end

def create_site(new_site_path)
Expand Down Expand Up @@ -127,11 +126,11 @@ def create_site(new_site_path)
setup_liquid_templates
end

postcss_option ? configure_postcss : configure_sass

if frontend_bundling_option == "esbuild"
configure_postcss
invoke(Esbuild, ["setup"], {})
else
postcss_option ? configure_postcss : configure_sass
invoke(Webpack, ["setup"], {})
end
end
Expand Down Expand Up @@ -166,6 +165,7 @@ def setup_liquid_templates
end

def configure_sass
template("postcss.config.js.erb", "postcss.config.js") unless disable_postcss?
copy_file("frontend/styles/index.css", "frontend/styles/index.scss")
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

return if Bridgetown.environment.test?

required_packages = %w(esbuild esbuild-loader webpack@5.39.1 webpack-cli@4.7.2 webpack-manifest-plugin@3.1.1)
required_packages = %w(esbuild esbuild-loader [email protected] webpack@5.72.0 webpack-cli@4.9.2 webpack-manifest-plugin@5.0.0)
redundant_packages = %w(@babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-transform-runtime @babel/preset-env babel-loader)

say "Installing required packages"
run "yarn add -D #{required_packages.join(" ")}"
run "yarn add -D --tilde #{required_packages.join(" ")}"

packages_to_remove = package_json["devDependencies"].slice(*redundant_packages).keys
unless packages_to_remove.empty?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ const cssRules = {
{
loader: "css-loader",
options: {
url: url => !url.startsWith('/'),
url: {
filter: url => !url.startsWith('/')
},
importLoaders: 1
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
confirm = ask "This configuration will ovewrite your existing #{"postcss.config.js".bold.white}. Would you like to continue? [Yn]"
return unless confirm.casecmp?("Y")

plugins = %w(postcss-easy-import postcss-mixins postcss-color-function cssnano)
plugins = %w(postcss-mixins postcss-color-function cssnano)

say "Adding the following PostCSS plugins: #{plugins.join(' | ')}", :green
run "yarn add -D #{plugins.join(' ')}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
module.exports = {
plugins: {
'postcss-easy-import': {},
'postcss-mixins': {},
'postcss-color-function': {},
'postcss-flexbugs-fixes': {},
Expand All @@ -14,7 +13,7 @@ module.exports = {
'custom-media-queries': true
},
},
'cssnano' : {
'cssnano': {
preset: 'default'
}
}
Expand Down
23 changes: 13 additions & 10 deletions bridgetown-core/lib/bridgetown-core/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@ def reindent_for_markdown(input)
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity

# Return an asset path based on a frontend manifest file
#
# @param site [Bridgetown::Site] The current site object
# @param asset_type [String] js or css, or filename in manifest
# @return [String, nil]
def parse_frontend_manifest_file(site, asset_type)
case frontend_bundler_type(site.root_dir)
when :webpack
Expand All @@ -362,9 +367,6 @@ def parse_frontend_manifest_file(site, asset_type)
# file isnt found
# @return [nil] Returns nil if the asset isnt found
# @return [String] Returns the path to the asset if no issues parsing
#
# @raise [WebpackAssetError] if unable to find css or js in the manifest
# file
def parse_webpack_manifest_file(site, asset_type)
return log_frontend_asset_error(site, "Webpack manifest") if site.frontend_manifest.nil?

Expand All @@ -389,16 +391,17 @@ def parse_webpack_manifest_file(site, asset_type)
# file isnt found
# @return [nil] Returns nil if the asset isnt found
# @return [String] Returns the path to the asset if no issues parsing
#
# @raise [WebpackAssetError] if unable to find css or js in the manifest
# file
def parse_esbuild_manifest_file(site, asset_type) # rubocop:disable Metrics/PerceivedComplexity
return log_frontend_asset_error(site, "esbuild manifest") if site.frontend_manifest.nil?

asset_path = if %w(js css).include?(asset_type)
folder = asset_type == "js" ? "javascript" : "styles"
site.frontend_manifest["#{folder}/index.#{asset_type}"] ||
site.frontend_manifest["#{folder}/index.#{asset_type}.rb"]
asset_path = case asset_type
when "css"
site.frontend_manifest["styles/index.css"] ||
site.frontend_manifest["styles/index.scss"] ||
site.frontend_manifest["styles/index.sass"]
when "js"
site.frontend_manifest["javascript/index.js"] ||
site.frontend_manifest["javascript/index.js.rb"]
else
site.frontend_manifest.find do |item, _|
item.sub(%r{^../(frontend/|src/)?}, "") == asset_type
Expand Down
35 changes: 18 additions & 17 deletions bridgetown-core/lib/site_template/package.json.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,35 @@
},
"devDependencies": {
<%- if frontend_bundling_option == "webpack" -%>
"css-loader": "^4.3.0",
"css-loader": "^6.7.1",
<%- end -%>
"esbuild": "^0.13.15",
"esbuild": "^0.14.39",
<%- if frontend_bundling_option == "webpack" -%>
"esbuild-loader": "^2.16.0",
"esbuild-loader": "^2.18.0",
"file-loader": "^6.2.0",
"mini-css-extract-plugin": "^1.3.1",
"mini-css-extract-plugin": "^2.6.0",
<%- else -%>
"glob": "^7.2.0",
"glob": "^8.0.1",
<%- end -%>
<%- if postcss_option -%>
"postcss": "^8.4.0",
<%- unless disable_postcss? -%>
"postcss": "^8.4.12",
"postcss-flexbugs-fixes": "^5.0.2",
<%- if frontend_bundling_option == "esbuild" -%>
"postcss-import": "^14.0.2",
"postcss-load-config": "^3.1.0",
"postcss-import": "^14.1.0",
"postcss-load-config": "^3.1.4",
<%- else -%>
"postcss-loader": "^4.3.0",
"postcss-loader": "^6.2.1",
<%- end -%>
"postcss-preset-env": "^7.0.1"<%= "," if frontend_bundling_option == "webpack" %>
<%- else -%>
"sass": "^1.32.8",
"sass-loader": "^8.0.2",
"postcss-preset-env": "^7.4.3"<%= "," if frontend_bundling_option == "webpack" || !postcss_option %>
<%- end -%>
<%- unless postcss_option -%>
"sass": "^1.50.1",
"sass-loader": "^12.6.0"<%= "," if frontend_bundling_option == "webpack" %>
<%- end -%>
<%- if frontend_bundling_option == "webpack" -%>
"webpack": "^5.39.1",
"webpack-cli": "^4.7.2",
"webpack-manifest-plugin": "^3.1.1",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-manifest-plugin": "^5.0.0",
"webpack-merge": "^5.8.0"
<%- end -%>
}
Expand Down
1 change: 0 additions & 1 deletion bridgetown-website/src/_docs/bundled-configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ bin/bridgetown configure purgecss

⛓️ Installs and configures a set of [PostCSS](https://postcss.org) plugins recommended by the Bridgetown community:

- [`postcss-easy-import`](https://github.com/trysound/postcss-easy-import)
- [`postcss-mixins`](https://github.com/postcss/postcss-mixins)
- [`postcss-color-function`](https://github.com/postcss/postcss-color-function)
- [`cssnano`](https://cssnano.co)
Expand Down
2 changes: 1 addition & 1 deletion bridgetown-website/src/_docs/command-line-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Available commands are:
* Use the `--configure=` or `-c` option to [apply one or more bundled configurations](/docs/bundled-configurations) to the new site.
* Use the `-t` option to choose ERB or Serbea templates instead of Liquid (aka `-t erb`).
* Use the `-e` option to choose Webpack instead of esbuild for your frontend bundler (aka `-e webpack`).
* When using Webpack, you can also choose to use Sass over PostCSS by adding the `--use-sass` option.
* Use the `--use-sass` option to configure your project to support Sass.
* `bin/bridgetown start` or `s` - Boots the Rack-based server (using Puma) at `localhost:4000`. In development, you'll get live reload functionality as long as `{% live_reload_dev_js %}` or `<%= live_reload_dev_js %>` is in your HTML head.
* `bin/bridgetown deploy` - Ensures that all frontend assets get built alongside the published Bridgetown output. This is the command you'll want to use for [deployment](/docs/deployment).
* `bin/bridgetown build` or `b` - Performs a single build of your site to the `output` folder. Add the `-w` flag to also regenerate the site whenever a source file changes.
Expand Down
Loading

0 comments on commit 1a4482b

Please sign in to comment.