The bundler for packages for Node.js and browser with support of various tools.
This bundler is wrapper around esbuild with various plugins.
The forge
doesn't allow to configure details of bundling, and expects
predefined project structure.
Use the package manager pnpm to install @tabula/forge
.
pnpm add @tabula/forge --save-dev
The forge
has a few moments which should be highlighted:
- looking for sources in the
<packageRoot>/src
directory; - produces output to the
<packageRoot>/lib
directory; - produces typings to the
<packageRoot>/typings
directory; - uses ESM format for produced module;
- doesn't bundle dependencies;
- generates source maps which include sources content.
Create an initial file for the selected target.
forge init -t,--target browser|node
-t,--target
- defines target for which a config will be generated.
Build a package.
forge [build] <-t,--target browser|node> [-e,--entry <in>[:<out>]] [-p,--production] [-c,--check] [-t,--typings] [-s,--storybook] [-b,--post-build <command>[:<cwd>]] [-w,--watch]
-t
,--target
- defines which platform is target for build. Must bebrowser
ornode
. This option is required, if not defined in the configuration file.-e
,--entry
- (default: index) defines an entry point. Can be used multiple times to define multiple entry points.-p
,--production
- (default: true for build and false for watch) enables bundling for production environment. Build for production mode doesn't enable minification for debug purposes in the target user application.-c
,--check
- (default: true) enables types checking through TypeScript compiler running. It stops build if any type error has been found.-t
,--typings
- (default: true) enables typings generation. It generates typings only if type checking is enabled.-s
,--storybook
- (default: false for build and true for watch) enables emitting additional meta for Storybook. It usesreact-docgen
under the hood. This option is useful only for thebrowser
target.-h,--css-class-prefix
- (default: true) option defines usage of prefix for CSS classes which generates by the CSS modules and vanilla-extract in production mode. It can be string in special format or boolean.-b
,--post-build
- defines post build hook. A hook is an external command, which executed in a shell. Can be used multiple times to define multiple post build hooks.-w
,--watch
- (default: false) enables watch mode.
You can use configuration file. We're looking for:
- a
forge
property in thepackage.json
; - a JSON or YAML
.forgerc
file; - an
.forgerc
file with.json
,.yaml
,.yml
,.js
,.mjs
or.cjs
- any of the above two inside a .config subdirectory;
- a
forge.config.js
,forge.config.mjs
, orforge.config.cjs
file.
{
"$schema": "https://github.com/ReTable/forge/blob/main/schemas/forgerc.json",
"target": "node",
"entry": "index",
"check": true,
"typings": true,
"cssClassPrefix": true,
"postBuild": "touch lib/meta.js",
"build": {
"production": true
},
"watch": {
"production": false,
"storybook": true
}
}
You can use one of following entry formats:
-
"<input>"
-
"<input>:<output>"
-
{ "in": "<input>" }
-
{ "in": "<input>", "out": "<output>" }
You can define one or more entries:
{
"entry": // <entry>
}
or
{
entries: [
// <entry>
// <entry>
// ...
],
}
You can use one of following hook formats:
-
"<command>"
-
"<command>:<cwd>"
-
{ "command": "<command>" }
-
{ "command": "<command>", "cwd": "<cwd>" }
We resolve option in following order:
- an option provided through CLI;
- command specific option from the config (
build
orwatch
properties); - option from the config file;
- default value.
You can look at the JSON Schema for configuration file.
Not all options are available through static files. For example, svgrComponentName
is available only in JS/TS files.
By default, the forge
looking for <packageRoot>/src/index.tsx
or <packageRoot/src/index.ts
file, and bundles it
to the <packageRoot>/lib/index.js
.
You can provide entry in two possible variants:
- only input:
<input>
; - input and output:
<input>:<output>
.
All input files will be searched in the <packageRoot>/src
directory.
If you provide input as file name, then it will be searched exactly.
For example the following command:
$ forge build node --entry nodes/entry.ts
The forge
will use <packageRoot>/src/nodes/entry.ts
as an entry file.
But you can provide module name instead of file.
Look at the next command:
$ forge build node --entry nodes/entry
In that case, the forge
will looking for an entry file in the following order:
<packageRoot>/src/nodes/entry.tsx
;<packageRoot>/src/nodes/entry.ts
;<packageRoot>/src/nodes/entry/index.tsx
;<packageRoot>/src/nodes/entry/index.ts
.
By default, we use relative path of entry module as output path. For example:
- when input is
nodes/entry
, then bundle will be<packageRoot>/lib/nodes/entry.js
(for example, an entry file may be<packageRoot>/nodes/entry.ts
or<packageRoot>/nodes/entry/index.ts
); - when input is
nodes/index.ts
, then bundle will be<packageRoot>/lib/nodes/index.js
.
But you can define your own path for bundle. For example:
forge build node --entry nodes/entry:bundles/nodes
This command creates a bundle <packageRoot>/lib/bundles/nodes.js
.
NOTE: Don't use .js
extension for output module, because we add it by default before transfer parameters to the
esbuild
. But even if you add the extension, we will fix it and your bundle will haven't doubled .js
extension
anyway.
We use code splitting feature of the esbuild
. If you have multiple entries which share the same code, bundler will
create ESM modules with shared code, and will use it in the bundles.
Be carefully when use code splitting and CSS. Constants which extracted from CSS modules or vanilla-extract
styles
will be shared. But, extracted CSS from shared modules will be duplicated for each bundle.
All hashed class names will be the same between all modules which use them.
You can run scripts after each build.
For example:
forge -b "touch lib/meta.js" -b "touch meta.d.ts":"typings"
The two commands will be executed after build. The first one will use <packageRoot>
as working directory. And the
second one will use <packageRoot>/typings
as working directory.
The only one moment which you should know about bundling for Node.js that we use version 18 as target.
Bundling for browser has a much more implementation details.
We support bundling of images and fonts. But we don't inline it, and not change names or assets structure like a Vite or Webpack.
We only solve a task to bundle package for using in projects which will be bundled for serving later.
The CSS supported out of the box.
If your package uses CSS then a line import "./index.css";
automatically
will be added to the end of a lib/index.js
file.
Also, all CSS are processed by the Autoprefixer.
We support CSS Modules with predefined settings:
- use
camelCaseOnly
locals convention; - different scoped names are generated for development and production modes;
- package name and file path are used in scoped name for debug purposes in development mode.
Style files which use CSS Modules must have *.module.[ext]
filename.
The forge
supports usage of the PostCSS and
Sass.
You should use *.pcss
extension for PostCSS and *.scss
for the Sass.
We support imports in format of ~<pkg>
. It's similar to the Webpack, but
has own restrictions.
The forge
doesn't support paths inside the package. It does search
the package.json
of the given package, and try to read sass
field inside
of it.
Example:
{
"name": "@tabula/ui-theme",
"sass": "./sass/index.scss"
}
will be resolved to the <node_modules>/@tabula/ui-theme/sass/index.scss
.
We support the vanilla-extract.
This is zero-runtime CSS-in-JS solution with TypeScript support.
IMPORTANT: All imports of CSS files in *.css.ts
files is ignored.
We support the SVGR to allow to use SVG images not only as loadable assets, but also as a React components.
import iconUrl, { ReactComponent as IconUrl } from './icon.svg';
<>
<IconUrl className="react-icon" />
<img className="img-icon" src={iconUrl} />
</>;
An SVG file already exports React component as ReactComponent
.
By default, SVGR uses Svg<CamelCaseFileName>
name for components. You can override this behaviour through
svgrComponentName
option, which should be function of format (svgrName: string) => string
.
Example:
export default {
// ...
svgrComponentName(name) {
return `Ui${name.slice(3)}Icon`;
},
// ...
};
If you have a file column.svg
then component name is SvgColumn
by default. But with config from about the name
will be UiColumnIcon
.
If you use memoization it looks like:
import { memo } from 'react';
const UiColumnIcon = (props) => {
// ...
};
const Memo = memo(UiColumnIcon);
export { Memo as ReactComponent };
This option doesn't affect named exports.
By default, SVGR doesn't append displayName
for exported components. You can add this behaviour through svgrDisplayName
option, which should be function of format (componentName: string) => string | { displayName: string; isDebugOnly?: boolean }
.
When function is returns string, then isDebugOnly
equals to false
.
The componentName
is name of component itself (before memoization if enabled). If you provide svgrComponentName
option,
then result of applying this function is componentName
.
The isDebugOnly
enables wrapping the assignment in Vite compatible condition.
// `isDebugOnly` = false
Component.displayName = 'scope(ComponentDisplayName)';
// `isDebugOnly` = true
if (import.meta.env.DEV) {
Component.displayName = `scope(ComponentDisplayName)`;
}
If memoization is enabled, then the displayName
will be assigned to the memoized component:
const Component = (props) => {
// ...
};
const Memo = memo(Component);
Memo.displayName = `scope(ComponentDisplayName)`;
We use automatic runtime only for React.
For more details, see here.
This feature is supported by the esbuild already.
We generate additional documentation for Storybook:
AwesomeComponent.__docgenInfo = { ...componentDocumentation };
The forge
supports TypeScript. It runs tsc
before each build automatically.
You should provide tsconfig.forge.json
in your project which will be used by
the forge
.
You can use @tabula/typescript-config
for Node.js:
{
"extends": "@tabula/typescript-config/tsconfig.node.json",
"include": ["src/**/*"]
}
or browser:
{
"extends": "@tabula/typescript-config/tsconfig.browser.json",
"include": ["src/**/*"]
}
The configuration for browser also includes typings for CSS and CSS Modules, static files and SVG files with SVGR support.
That configs are recommended for usage with the forge
.
The forge
supports option cssClassPrefix
which used in production for browser builds. It enables adding prefix to
the classes which generated by the CSS Modules and vanilla-extract
.
The option can be boolean or string.
If string option is used, then it will be used as simple template with following placeholders:
[full-name]
- full package name (with scope if it presented);[scope]
- package scope if presented or an empty string;[name]
- package name without scope.
The prefix has format [full-name]__
by default or when option is true
.
When package name is awesome-ui
, then:
- when the option is
[full-name]__
, then the prefix isawesome_ui__
; - when the option is
[scope]__
, then the prefix is__
; - when the option is
[scope]__[name]__
, then the prefix is__awesome_ui__
.
When package name is @awesome-ui/theme
, then:
- when the option is
[full-name]__
, then the prefix isawesome_ui_theme_
; - when the option is
[scope]__
, then the prefix isawesome_ui__
; - when the option is
[scope]__[name]__
, then the prefix isawesome_ui__theme__
.
This project is ISC licensed.