Project Configuration with Babel#

If you don't want to use TypeScript, you can still get the full power of ny@f. All features that the package requires are provided by ES2017 and above. The recommended tool to setup a package for ES2015 any modern browser supports is Babel.

This section describes the setup and usage with pure ECMAScript.

Setup the Environment#

If you use Visual Studio Code it's recommended to tell the editor the specific features you use, especially decorators. To do so, add a file jsconfig.json in the project root and add this content:

{
  "compilerOptions": {
    "baseUrl": "./src",
    "target": "es6",
    "module": "commonjs",
    "experimentalDecorators": true
  },
  "include": ["src/**/*"]
}

This assumes your sources are in the folder ./src. Adjust the settings according your needs.

Project Dependencies#

Next add the following dependencies to your project's package.json. This is the current Babel 7 setup.

"dependencies": {
    "@nyaf/forms": "^0.7.4",
    "@nyaf/lib": "^0.7.4",
    "@nyaf/store": "^0.7.4",
    "babel-loader": "^8.1.0"
  },
  "devDependencies": {
    "@babel/core": "^7.11.6",
    "@babel/plugin-proposal-decorators": "^7.10.5",
    "@babel/preset-env": "^7.11.5",
    "@babel/preset-react": "^7.10.4",
    "html-webpack-plugin": "^4.4.1",
    "webpack-cli": "^3.3.12",
    "webpack": "^4.44.1",
    "webpack-dev-server": "^3.11.0"
  }

This setup allows the compilation and packaging with WebPack, but the transformation invoked from WebPack is based on the Babel plug-ins.

Configuring Babel#

Next, configure Babel to support the features ny@f needs. This is primarily the JSX namespaces, that are different from React. It's similar to the procedure described for TypeScript. However, the settings look a bit different.

You can use either .babelrc or the settings in package.json. The following example shows the settings in package.json (on root level).

"babel": {
    "presets": [
      "@babel/preset-env",
      [
        "@babel/preset-react", {
          "runtime": "classic",
          "pragma": "JSX.createElement",
          "pragmaFrag": "null"
        }
      ]
    ],
    "plugins": [
      [
        "@babel/plugin-proposal-decorators",
        {
          "legacy": true
        }
      ]
    ]
  }

The core settings you'll need are preset-react and plugin-proposal-decorators. The first activates the compilation for the JSX namespace JSX.createElement. This is the exact and complete call to the ny@f JSX module. The second parameter pragmaFrag is the support for the <> fragment syntax. In React it's React.fragment. In ny@f it's just nothing, as the JSX module treats a missing element information as fragment. To enforce this, we provide null.

The decorator support is provided by a plugin. Babel takes care to compile this using a polyfill so it runs on the selected ECMAScript version.

Packer#

You can, of course, use any packer. Most examples use WebPack, but it will work with others, too. The following section covers WebPack and Rollup.

Configure WebPack#

The Babel transpiler can create a bundle, but putting it all together requires additional steps. The most powerful way (not always the easiest) is WebPack. The following webpack.config.js file is all you need to setup WebPack to create a bundle using Babel:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/main.js',
  module: {
    rules: [
      {
        test: /\.(jsx?)$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
    ],
  },
  resolve: {
    extensions: ['*', '.js', '.jsx'],
  },
  output: {
    path: __dirname + '/dist',
    publicPath: '/',
    filename: 'bundle.js',
  },
  devServer: {
    contentBase: './dist',
  },
  plugins: [
    new HtmlWebpackPlugin({
      inject: true,
      template: './src/index.html',
    }),
  ],
};

The entry point is the file main.js. All component files have the extension .jsx, so we need to resolve that extension, too. Apart from this the babel-loader invokes the Babel transpiler and these settings, described above, apply here. The bundle is copied to the distribution folder dist and the bundle is added to the HTML file using the appropriate plug-in.

Configure Rollup#

The following packagejson file shows a setup with Babel and Rollup. It has several plug-ins to handle the tasks required to make a small bundle. We experienced issues with the typical uglify plug-in and got much better results using Terser. Other compression and mangle tools may work, too.

{
  "name": "nyaf-todo",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@nyaf/lib": "^0.6.2"
  },
  "devDependencies": {
    "@babel/cli": "^7.11.6",
    "@babel/core": "^7.11.6",
    "@babel/plugin-proposal-class-properties": "^7.10.4",
    "@babel/plugin-proposal-decorators": "^7.10.5",
    "@babel/preset-env": "^7.11.5",
    "@babel/preset-react": "^7.10.4",
    "@rollup/plugin-babel": "^5.2.1",
    "@rollup/plugin-commonjs": "^15.1.0",
    "@rollup/plugin-image": "^2.0.5",
    "@rollup/plugin-node-resolve": "^9.0.0",
    "babel-plugin-external-helpers": "^6.22.0",
    "http-server": "^0.12.3",
    "rollup": "^2.28.1",
    "rollup-plugin-postcss": "^3.1.8",
    "terser": "^5.3.2"
  },
  "scripts": {
    "start": "http-server ./dist -p 8080 -o",
    "build": "rollup -c && npx terser --compress --mangle -- dist/index.js > dist/index.min.js",
    "test": "echo \"No tests\""
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "babel": {
    "presets": [
      [
        "@babel/preset-env",
         {
          "targets": {
            "esmodules": true
          }
         }
      ],
      [
        "@babel/preset-react",
        {
          "runtime": "classic",
          "pragma": "JSX.createElement",
          "pragmaFrag": "null"
        }
      ]
    ],
    "plugins": [
      [
        "@babel/plugin-proposal-decorators",
        {
          "legacy": true
        }
      ],
      "@babel/plugin-proposal-class-properties"
    ]
  }
}

The whole build procedure uses this command (here on command line for manual execution):

$ npx rollup -c
$ npx terser --compress --mangle -- dist/index.js > dist/index.min.js

Rollup itself comes with its own configuration. It's JavaScript and consists mainly of some plug-ins.

import babel from '@rollup/plugin-babel';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import image from '@rollup/plugin-image';

import postcss from 'rollup-plugin-postcss';

const extensions = ['.js', '.jsx'];

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/index.js',
    format: 'iife',
    name: 'bundle',
  },
  plugins: [
    postcss({
      plugins: []
    }),
    image(),
    resolve({
      extensions
    }),
    commonjs({
      include: 'node_modules/**'
    }),
    babel({
      exclude: 'node_modules/**',
    })
  ],
};

This file starts with the processing of inline CSS. In a component you can use something like this:

import JSX, { GlobalProvider, BaseComponent, CustomElement } from '@nyaf/lib';

import 'main.css';

@CustomElement('app-main')
export class MainComponent extends BaseComponent {
  // omitted for brevity
}

Here, the CSS is being importat directly into the code and you can use the class names in JSX elements (the code assumes that main.css has a rule like .ClassName { color: red }):

<div class={ClassName}>

A similar behavior happens to image resources with the plug-in @rollup/plugin-image. @rollup/plugin-node-resolve is required to resolve the jsx file extension. It's not really a requiement, the component files could be named .js, but the typical editors and IDEs process the JSX stuff better, then. @rollup/plugin-commonjs is used to handle the umd bundle and import/export syntax. Finally we call Babel to transform the ES2020 JavaScript into something any modern browser can read.

Note that in typical Rollup setups the compression is made by rollup-plugin-uglify. This makes trouble processing the umd-files of the @nyaf package. Hence we use Terser to do a final compression for prduction environments.

This setup leads to a quite small package. The nyaf-todo app has a final bundle of 26KB zipped, including the @nyaf/lib package, all the CSS, some components, and the loader provided by Rollup.

Writing Components#

The components look exactly like the ones using TypeScript, apart from missing types and generics. Let's assume you have this index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JS Demo</title>
  </head>
  <body>
    <app-main></app-main>
  </body>
</html>

This requires to load and upgrade one component. To do this, you need the start procedure in main.js:

import JSX, { GlobalProvider } from '@nyaf/lib';
import { MessageComponent } from './components/message.component';
import { MainComponent } from './components/main.component';

GlobalProvider.bootstrap({
  components: [MainComponent, MessageComponent],
});

The two demo components are shown below.

import JSX, { GlobalProvider, BaseComponent, CustomElement } from '@nyaf/lib';

@CustomElement('app-main')
export class MainComponent extends BaseComponent {
  render() {
    return (
      <>
        <h1>Hello NYAF</h1>
        <app-message></app-message>
      </>
    );
  }
}
import JSX, { GlobalProvider, BaseComponent, CustomElement } from '@nyaf/lib';

@CustomElement('app-message')
export class MessageComponent extends BaseComponent {
  render() {
    return <div>Hallo @nyaf</div>;
  }
}

As you can see you use JSX and decorators along with ES2018 import/export instructions.

Improvements#

Imagine a main file like this:

import JSX, { GlobalProvider } from '@nyaf/lib';

import * as cmp from '@components';

GlobalProvider.bootstrap({
    components: [
        cmp.MainComponent,
        cmp.MessageComponent
    ]
});

The import from @components makes it so much more convenient. To setup this local path resolution you need to create an index file (index.js) for your components in the root folder of the components:

export * from './main.component';
export * from './message.component';

Then, set an alias in webpack.config.js to resolve this file:

resolve: {
    extensions: ["*", ".js", ".jsx"],
    alias: {
      "@components": path.join(__dirname, "src/components"),
    },
  }

To let Visual Studio Code accept this, too, add this jsconfig.json (look for the key paths):

{
  "compilerOptions": {
    "baseUrl": "./src",
    "target": "es6",
    "module": "commonjs",
    "experimentalDecorators": true,
    "paths": {
      "@components/*": ["./src/components/*"]
    }
  },
  "include": ["src/**/*"]
}

Both, the alias' for WebPack as well as the paths key can handle multiple entries for more complex setups.

Handle the JSX Import#

Each component needs to import the JSX namespace in classic mode. This can be simplified by adding the import globally. The following snippet is usually provided in the main.js or index.js file used to configure the app and before calling GlobalProvider.bootstrap.

import JSX from "@nyaf/lib";

window.JSX = JSX;

From now on you can avoid any of the JSX imports in all the component files. Please note, that the new import of JSX that was introduced in React 17 and is supported in Babel 7.9 and newer will be available in ny@f beginning with version 0.7.0.

Bundle Size#

For the demo files shown in the code above the whole bundle is 43.7 KB (11.6 KB zipped). The HTML remains with 230 Bytes (squeezed).

With all the loader and polyfill stuff this is an extremely small footprint for a client app. Forms and Flux Store would add another 10 KBytes roughly.

The @nyaf CLI#

The @nyaf CLI currently creates TypeScript projects only. To use Babel and pure JS refer to the documentation in this section.