/Carbon.Pipeline

Ultra-fast build stack for Neos CMS based on esbuild and PostCSS

Primary LanguageJavaScriptMIT LicenseMIT

Carbon.Pipeline – Build stack for Neos CMS

Download David GitHub stars GitHub watchers GitHub license GitHub issues GitHub forks Twitter Sponsor @Jonnitto on GitHub

Carbon.Pipeline is a delicious blend of esbuild and PostCSS to form a full-featured, ultra-fast modern Javascript and CSS bundler for Flow Framework and Neos CMS.

Getting started

First, thank you that you want to give this build stack a try! If you miss a ✨ feature or found a 🐛 bug, feel free to open an issue.

Install via composer

Run composer require carbon/pipeline --dev. Some files (if not already existing) will be copied to your root folder during the installation. After installing the package, run the command yarn install to install the required packages, defined in package.json. Feel free to modify and change dependencies before installing 👍

Manual install

If you want to make some significant adjustments to the build stack, you can also download the code as zip file and put it in the folder Build/Carbon.Pipeline. Go to Carbon.Pipeline/Installer/Distribution/Defaults and copy the files to your root folder (Don't forget the hidden files, starting with a dot). After this is done, run the command yarn install to install the required packages, defined in package.json. Feel free to modify and change dependencies before installing 👍

Add files to the build stack

The whole configuration, including which files to build, is configured in pipeline.yaml. The default values are set in defaults.yaml and merged with your configuration. Under the key packages, you can either add an array with package settings or, if you have just one entry, you can directly add the configuration:

packages:
  - package: Vendor.Bar
    files:
      - Main.pcss
      - Main.js

# This is the same as
packages:
  package: Vendor.Bar
  files:
    - Main.pcss
    - Main.js

If you have just one file, you can pass this directly without creating an array:

packages:
  package: Vendor.Bar
  files: Main.js

If you don't set files, all parsable files from the input folder get rendered. Files that start with an underscore (_) will be ignored.

packages:
  package: Vendor.Bar

To change the input and/or the output folder, you can do this with the folder option:

packages:
  package: Vendor.Bar
  folder:
    input: Assets
    output:
      inline: Private/Templates
      style: Public
      script: Public
      module: Public
      commonJS: Public

Further, you can write the files to another package:

packages:
  package: Vendor.Bar
  folder:
    output:
      package: Vendor.Theme

If you want to go crazy with multi-sites in Neos, you can also write the files to multiple packages:

packages:
  package: Vendor.Bar
  folder:
    output:
      package:
        - Vendor.Theme
        - Vendor.Bar

A package entry has the following options:

Key Type Description Example
package string The name of the package (required) Vendor.Foo
files string or array The names of the entry files. If none given, all parsable files in the input folder get rendered Main.js
folder.input string The folder under Resources/Private where to look for the entry files Assets
folder.output.package string or array If set, the files will be writen in a different package (one or multiple) Foo.Bar
folder.output.inline string The folder where inline files get rendered Private/Templates/
folder.output.style string The folder where inline styles rendered Public/Assets
folder.output.script string The folder where inline scripts rendered Public/Assets
folder.output.module string The folder where inline modules rendered Public/Assets
folder.output.commonJS string The folder where inline commonJS files get rendered Public/Assets
external string or array You can mark a file or a package as external to exclude it from your build. */Modules/*
inline boolean Flag to toggle if the files should be inlined. If set, sourcemaps are disabled true
sourcemap boolean Flag to toggle source map generation false
format string Set the format of the output file. Read more cjs

These are the default values for the folders:

folder:
  input: Fusion
  output:
    inline: Private/Templates/InlineAssets
    style: Public/Styles
    script: Public/Scripts
    module: Public/Modules
    commonJS: Public/CommonJS

and these for the build options:

external: null
inline: false
sourcemap: true
format: iife

The target folders can be adjusted under the key folder.output. If you want to change the defaults for all your packages, you can also set this globally in your pipeline.yaml:

folder:
  input: Assets
  output:
    inline: Private/Templates
    style: Public
    script: Public
    module: Public
    commonJS: Public

buildDefaults:
  sourcemap: false
  format: esm

Please look at the defaults.yaml file for all the options.

If you set an entry file with the javascript module suffix (.mjs, .mjsx, .mts or .mtsx) the format of this file will be enforced to esm. The same with commonJS: If you set an entry file with the javascript commonJS suffix (.cjs, .cjsx, .cts or .ctsx) the format of this file will be enforced to cjs. E.g., if you have the following array ["Main.js", "Module.mjs", "CommonJS.cjs"], and have no specific setting for the format, Main.js will have the format iife, Module.mjs will have the format esm and CommonJS.cjs will have the format cjs.

Yarn tasks

There are five predefined main tasks:

Command Description Command
yarn watch Start the file watcher concurrently -r yarn:watch:*
yarn dev Build the files once concurrently -r yarn:dev:*
yarn build Build the files once for production (with optimzed file size) concurrently -r yarn:build:*
yarn pipeline Run install, and build the files for production yarn install --silent --non-interactive;concurrently -r yarn:pipeline:*;yarn build
yarn showConfig Shows the merged configuration from pipeline.yaml and defaults.yaml node Build/Carbon.Pipeline/showConfig.mjs

The tasks are split up, so they can run in parallel mode. But you can also run them separately:

Command Description Command
yarn watch:js Start the file watcher for JavaScript files node Build/Carbon.Pipeline/esbuild.mjs --watch
yarn watch:css Start the file watcher for CSS files node Build/Carbon.Pipeline/postcss.mjs --watch
yarn dev:js Build the files once for JavaScript files node Build/Carbon.Pipeline/esbuild.mjs
yarn dev:css Build the files once for CSS files node Build/Carbon.Pipeline/postcss.mjs
yarn build:js Build the JavaScript files once for production node Build/Carbon.Pipeline/esbuild.mjs --production
yarn build:css Build the CSS files once for production node Build/Carbon.Pipeline/postcss.mjs --production

Extendibility

Of course, you can also add your own tasks in the scripts section of your package.json file. For example, if you have a Neos UI custom editor and want to start all your tasks in one place, you can add them like this:

"build:editor": "yarn --cwd DistributionPackages/Foo.Editor/Resources/Private/Editor/ build",
"watch:editor": "yarn --cwd DistributionPackages/Foo.Editor/Resources/Private/Editor/ watch",
"pipeline:editor": "yarn --cwd DistributionPackages/Foo.Editor/Resources/Private/Editor/ install --silent --non-interactive",

Because the tasks start with build:, respectively with watch: or pipeline:, the tasks will be included in the corresponding root command. In this example, yarn build, yarn watch or yarn pipeline.

Compression of files

In production mode (yarn build), the files also get compressed with gzip and brotli. You can edit the compression level under the key buildDefaults.compression. Per default, the highest compression level is set. To disable compression at all, you can set it to false:

buildDefaults:
  compression: false

Or, if you want to disable just one of them, you can set the entry to false:

buildDefaults:
  compression:
    gzip: false

Import files from DistributionPackages and other Packages

By default, two aliases are predefined: DistributionPackages and Packages. Like that you can import (CSS and JS) files from other packages like that:

import "DistributionPackages/Vendor.Foo/Resources/Private/Fusion/Main";
import "Packages/Plugins/Jonnitto.PhotoSwipe/Resources/Private/Assets/PhotoSwipe";
@import "DistributionPackages/Vendor.Foo/Resources/Private/Fusion/Main.pcss";
@import "Packages/Carbon/Carbon.Image/Resources/Private/Assets/Tailwind.pcss";

Thanks to a custom made resolve function, you can also use globbing in CSS imports: @import "Presentation/**/*.pcss";

CSS

Sass

If you want to use Sass (.scssor .sass files) you have to install sass and node-sass-tilde-importer:

yarn add --dev sass node-sass-tilde-importer

You have to ways to import files from node_modules (Example with bootstrap):

@import "node_modules/bootstrap/scss/bootstrap";
@import "~bootstrap/scss/bootstrap";

PostCSS

This template comes with a variety of PostCSS Plugins. Feel free to remove some or add your own favorites packages. The configuration is located in .postcssrc.js. The suffix of these files should be .pcss.

PostCSS Plugins

Following plugins are included:
Name Description
postcss-import Plugin to transform @import rules by inlining content. Thanks to a custom resolve function you can also use glob
Tailwind CSS A utility-first CSS framework for rapidly building custom user interfaces
postcss-nested Unwrap nested rules like how Sass does it
postcss-assets Plugin to manage assets
postcss-focus-visible PostCSS Focus Visible lets you use the :focus-visible pseudo-class in CSS, following the Selectors Level 4 specification.
postcss-clip-path-polyfill Add SVG hack for clip-path property to make it work in Firefox. Currently supports only polygon()
postcss-sort-media-queries Combine and sort CSS media queries
autoprefixer Parse CSS and add vendor prefixes to CSS rules using values from Can I Use
cssnano Modern CSS compression
postcss-reporter console.log() the messages (warnings, etc.) registered by other PostCSS plugins

Of course, you can add your own or remove not-needed Plugins as you want. This is just meant as a starting point.

Tailwind CSS

This setup comes with Tailwind CSS, a highly customizable, low-level CSS framework. An example configuration is provided in tailwind.config.js. The setup for purge the CSS files is also configured. Read more about controlling the file size here. Because the CSS bundling is done with the Javascript API from PostCSS, the Just-in-Time Mode from Tailwind CSS works perfectly.

By the way: Alpine.js is excellent in combination with Tailwind CSS.

Javascript

Flow Settings in Javascript

If you use tools like Flownative.Sentry, you perhaps want to pass some of the settings to your Javascript, without setting a data attribute somewhere in the markup. For that, you can enable esbuild.defineFlowSettings. If set to true, all settings are passed. It is recommended to set it to a path (e.g. Flownative.Sentry). This path is added as --path attribute to the flow configuration:show command. If you run yarn build, which has automatically the flag --production, the FLOW_CONTEXT is set to Production.

esbuild:
  defineFlowSettings: Flownative.Sentry

In Javascript, you can access the variables like this:

Sentry.init({
  dsn: FLOW.Flownative.Sentry.dsn,
  release: FLOW.Flownative.Sentry.release,
  environment: FLOW.Flownative.Sentry.environment,
  integrations: [new Integrations.BrowserTracing()],
});

Make sure your .eslintrc has the global FLOW enabled:

{
  "globals": {
    "FLOW": "readonly"
  }
}
Pass options to esbuild

You can pass options to the esbuild API with esbuild.options.

Example: To remove some functions from the production build you can use the esbuild.options.pure setting. If you have just one function, you can pass a string, otherwise, you have to set it to an array:

esbuild:
  options:
    pure:
      - console.log
      - console.pure
TypeScript

If you want to use TypeScript, add the following packages to package.json:

yarn add --dev typescript @typescript-eslint/eslint-plugin

Add your tsconfig.json file; this is just an example:

{
  "include": ["DistributionPackages/**/Private/*"],
  "exclude": [
    "node_modules/*",
    "DistributionPackages/**/Public/*",
    "DistributionPackages/**/Private/Templates/InlineAssets*",
    "Packages"
  ],
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "Packages/*": ["Packages/*"],
      "DistributionPackages/*": ["DistributionPackages/*"]
    }
  }
}

To enable the correct linting, edit .eslintrc:

{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "eslint:recommended",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  "env": {
    "es6": true,
    "node": true
  },
  "globals": {
    "FLOW": "readonly"
  }
}
React

Using JSX syntax usually requires you to manually import the JSX library you are using. For example, if you are using React, by default, you will need to import React into each JSX file like this:

import * as React from "react";
render(<div />);
Preact

If you're using JSX with a library other than React (such as Preact,), you'll likely need to configure the JSX factory and JSX fragment settings since they default to React.createElement and React.Fragment respectively. Add this to your tsconfig.json or jsconfig.json:

{
  "compilerOptions": {
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}
Svelte

If you want to use Svelte, add the following packages to package.json:

yarn add --dev svelte svelte-preprocess esbuild-svelte @tsconfig/svelte

Enable the plugin in your pipeline.yaml file:

esbuild:
  plugins:
    svelte:
      enable: true
      # Add here your options
      options:
        compileOptions:
          css: true

Your tsconfig.json may look like this:

{
  "extends": "@tsconfig/svelte/tsconfig.json",
  "include": ["DistributionPackages/**/Private/*"],
  "exclude": [
    "node_modules/*",
    "__sapper__/*",
    "DistributionPackages/**/Public/*",
    "DistributionPackages/**/Private/Templates/InlineAssets*",
    "Packages"
  ],
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "Packages/*": ["Packages/*"],
      "DistributionPackages/*": ["DistributionPackages/*"]
    }
  }
}
Vue.js

If you want to use Vue.js, add the following packages to package.json:

yarn add --dev vue vue-template-compiler esbuild-vue

Enable the plugin in your pipeline.yaml file:

esbuild:
  plugins:
    vue:
      enable: true
      # You can pass your needed options here
      # options:
Babel.js / IE 11 support

If you want to use Babel.js, add the following packages to package.json:

yarn add --dev @babel/core esbuild-plugin-babel

as well additonals babel plugins and/or presets:

yarn add --dev @babel/preset-env @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread

Further, you have to add a file called babel.config.json, for example:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ]
  ],
  "plugins": ["@babel/proposal-class-properties", "@babel/proposal-object-rest-spread"]
}

Finally, enable the plugin in your pipeline.yaml file:

esbuild:
  plugins:
    babel:
      enable: true
      # You can pass your needed options here
      # options:

As the ENV variable is set to development or production if you run the tasks, you can have different setups (For example remove console commands with babel-plugin-transform-remove-console on production):

{
  "env": {
    "development": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "modules": false
          }
        ]
      ],
      "plugins": ["@babel/proposal-class-properties", "@babel/proposal-object-rest-spread"]
    },
    "production": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "modules": false
          }
        ]
      ],
      "plugins": ["@babel/proposal-class-properties", "@babel/proposal-object-rest-spread", "transform-remove-console"]
    }
  }
}

If you a poor person and have to support Internet Explorer, you have to edit your .browserslistrc. If a browser starting with ie is found, the target es5 gets activated.

defaults
ie 11
not dead
Additional esbuild plugins

You can also add additional esbuild plugins, for example esbuild-envfile-plugin:

esbuild:
  additionalPlugins:
    esbuild-envfile-plugin:
      functionName: setup
      options: null

As the plugin return not the function directly (like others), you have to also to pass the name of the function. If a plugin returns directly the function, you don't have to set this. If you want to enable such a plugin without any options, you can just pass name-of-the-plugin: true

Live-Reloading

If you want to use live reloading, you can do this with Browsersync. You can choose to install it globally (recomended) or locally

To install it globally run yarn global add browser-sync, to install it locally run yarn add --dev browser-sync.

Then you have to create a inital config with browser-sync init (If you installed it locally yarn browser-sync init).
After that, you need to adjust the created bs-config.js file. You can adjust every parameter, but the two parameter you need to set is files and proxy:

module.exports = {
  files: ["DistributionPackages/**/Public/**/*.css", "DistributionPackages/**/Public/**/*.js"],
  proxy: "http://your.local.domain",
};

If you want to also reload the page if a fusion or a template file gets changed, you can do so:

module.exports = {
  files: [
    "DistributionPackages/**/Public/**/*.css",
    "DistributionPackages/**/Public/**/*.js",
    "DistributionPackages/**/Private/**/*.fusion",
    "DistributionPackages/**/Private/**/*.html",
  ],
  proxy: "http://your.local.domain",
};

Make sure you set the correct proxy with the corresponding protocol (https:// or http://), depending on your setup. To create a better overview of the parameter you can delete the not changed values from the file.

To start Browsersync you can run browser-sync start --config bs-config.js (If you installed it locally prepend it with yarn) or, if you want to start it together with yarn watch, you can add following line into the scripts section:

"watch:browsersync": "browser-sync start --config bs-config.js",