English
Features
At the very basic level, developing using Wite is not that much different from using a static file server. However, Wite provides many enhancements over native ESM imports to support various features that are typically seen in bundler-based setups.
NPM Dependency Resolving and Pre-Bundling
Native ES imports do not support bare module imports like the following:
import { someMethod } from 'my-dep'
The above will throw an error in the browser. Wite will detect such bare module imports in all served source files and perform the following:
Pre-bundle them to improve page loading speed and convert CommonJS / UMD modules to ESM. The pre-bundling step is performed with esbuild and makes Wite's cold start time significantly faster than any JavaScript-based bundler.
Rewrite the imports to valid URLs like
/node_modules/.wite/deps/my-dep.js?v=f3sf2ebd
so that the browser can import them properly.
Dependencies are Strongly Cached
Wite caches dependency requests via HTTP headers, so if you wish to locally edit/debug a dependency, follow the steps here.
Hot Module Replacement
Wite provides an HMR API over native ESM. Frameworks with HMR capabilities can leverage the API to provide instant, precise updates without reloading the page or blowing away application state. Wite provides first-party HMR integrations for Kdu Single File Components and React Fast Refresh.
Note you don't need to manually set these up - when you create an app via create-wite
, the selected templates would have these pre-configured for you already.
TypeScript
Wite supports importing .ts
files out of the box.
Wite only performs transpilation on .ts
files and does NOT perform type checking. It assumes type checking is taken care of by your IDE and build process (you can run tsc --noEmit
in the build script or install kdu-tsc
and run kdu-tsc --noEmit
to also type check your *.kdu
files).
Wite uses esbuild to transpile TypeScript into JavaScript which is about 20~30x faster than vanilla tsc
, and HMR updates can reflect in the browser in under 50ms.
Use the Type-Only Imports and Export syntax to avoid potential problems like type-only imports being incorrectly bundled, for example:
import type { T } from 'only/types'
export type { T }
TypeScript Compiler Options
Some configuration fields under compilerOptions
in tsconfig.json
require special attention.
isolatedModules
Should be set to true
.
It is because esbuild
only performs transpilation without type information, it doesn't support certain features like const enum and implicit type-only imports.
You must set "isolatedModules": true
in your tsconfig.json
under compilerOptions
, so that TS will warn you against the features that do not work with isolated transpilation.
However, some libraries don't work well with "isolatedModules": true
. You can use "skipLibCheck": true
to temporarily suppress the errors until it is fixed upstream.
useDefineForClassFields
Starting from Wite 2.5.0, the default value will be true
if the TypeScript target is ESNext
. It is consistent with the behavior of tsc
4.3.2 and later. It is also the standard ECMAScript runtime behavior.
But it may be counter-intuitive for those coming from other programming languages or older versions of TypeScript. You can read more about the transition in the TypeScript 3.7 release notes.
If you are using a library that heavily relies on class fields, please be careful about the library's intended usage of it.
Most libraries expect "useDefineForClassFields": true
, such as MobX, etc.
But a few libraries haven't transitioned to this new default yet, including lit-element
. Please explicitly set useDefineForClassFields
to false
in these cases.
Other Compiler Options Affecting the Build Result
If migrating your codebase to "isolatedModules": true
is an unsurmountable effort, you may be able to get around it with a third-party plugin such as rollup-plugin-friendly-type-imports. However, this approach is not officially supported by Wite.
Client Types
Wite's default types are for its Node.js API. To shim the environment of client side code in a Wite application, add a d.ts
declaration file:
/// <reference types="wite/client" />
Also, you can add wite/client
to compilerOptions.types
of your tsconfig
:
{
"compilerOptions": {
"types": ["wite/client"]
}
}
This will provide the following type shims:
- Asset imports (e.g. importing an
.svg
file) - Types for the Wite-injected env variables on
import.
meta.env - Types for the HMR API on
import.
meta.hot
Kdu
Wite provides first-class Kdu support:
- Kdu 3 SFC support via
@witejs/plugin-kdu
- Kdu 3 JSX support via
@witejs/plugin-kdu-jsx
- Kdu 2.7 support via
witejs/wite-plugin-kdu2
JSX
.jsx
and .tsx
files are also supported out of the box. JSX transpilation is also handled via esbuild.
Kdu users should use the official @witejs/plugin-kdu-jsx plugin, which provides Kdu 3 specific features including HMR, global component resolving, directives and slots.
If not using JSX with React or Kdu, custom jsxFactory
and jsxFragment
can be configured using the esbuild
option. For example for Preact:
// wite.config.js
import { defineConfig } from 'wite'
export default defineConfig({
esbuild: {
jsxFactory: 'h',
jsxFragment: 'Fragment'
}
})
More details in esbuild docs.
You can inject the JSX helpers using jsxInject
(which is a Wite-only option) to avoid manual imports:
// wite.config.js
import { defineConfig } from 'wite'
export default defineConfig({
esbuild: {
jsxInject: `import React from 'react'`
}
})
CSS
Importing .css
files will inject its content to the page via a <style>
tag with HMR support. You can also retrieve the processed CSS as a string as the module's default export.
@import
Inlining and Rebasing
Wite is pre-configured to support CSS @import
inlining via postcss-import
. Wite aliases are also respected for CSS @import
. In addition, all CSS url()
references, even if the imported files are in different directories, are always automatically rebased to ensure correctness.
@import
aliases and URL rebasing are also supported for Sass and Less files (see CSS Pre-processors).
PostCSS
If the project contains valid PostCSS config (any format supported by postcss-load-config, e.g. postcss.config.js
), it will be automatically applied to all imported CSS.
CSS Modules
Any CSS file ending with .module.css
is considered a CSS modules file. Importing such a file will return the corresponding module object:
/* example.module.css */
.red {
color: red;
}
import classes from './example.module.css'
document.getElementById('foo').className = classes.red
CSS modules behavior can be configured via the css.modules
option.
If css.modules.localsConvention
is set to enable camelCase locals (e.g. localsConvention: 'camelCaseOnly'
), you can also use named imports:
// .apply-color -> applyColor
import { applyColor } from './example.module.css'
document.getElementById('foo').className = applyColor
CSS Pre-processors
Because Wite targets modern browsers only, it is recommended to use native CSS variables with PostCSS plugins that implement CSSWG drafts (e.g. postcss-nesting) and author plain, future-standards-compliant CSS.
That said, Wite does provide built-in support for .scss
, .sass
, .less
, .styl
and .stylus
files. There is no need to install Wite-specific plugins for them, but the corresponding pre-processor itself must be installed:
# .scss and .sass
npm add -D sass
# .less
npm add -D less
# .styl and .stylus
npm add -D stylus
If using Kdu single file components, this also automatically enables <style lang="sass">
et al.
Wite improves @import
resolving for Sass and Less so that Wite aliases are also respected. In addition, relative url()
references inside imported Sass/Less files that are in different directories from the root file are also automatically rebased to ensure correctness.
@import
alias and url rebasing are not supported for Stylus due to its API constraints.
You can also use CSS modules combined with pre-processors by prepending .module
to the file extension, for example style.module.scss
.
Disabling CSS injection into the page
The automatic injection of CSS contents can be turned off via the ?inline
query parameter. In this case, the processed CSS string is returned as the module's default export as usual, but the styles aren't injected to the page.
import styles from './foo.css' // will be injected into the page
import otherStyles from './bar.css?inline' // will not be injected into the page
Static Assets
Importing a static asset will return the resolved public URL when it is served:
import imgUrl from './img.png'
document.getElementById('hero-img').src = imgUrl
Special queries can modify how assets are loaded:
// Explicitly load assets as URL
import assetAsURL from './asset.js?url'
// Load assets as strings
import assetAsString from './shader.glsl?raw'
// Load Web Workers
import Worker from './worker.js?worker'
// Web Workers inlined as base64 strings at build time
import InlineWorker from './worker.js?worker&inline'
More details in Static Asset Handling.
JSON
JSON files can be directly imported - named imports are also supported:
// import the entire object
import json from './example.json'
// import a root field as named exports - helps with tree-shaking!
import { field } from './example.json'
Glob Import
Wite supports importing multiple modules from the file system via the special import.
function:
const modules = import.meta.glob('./dir/*.js')
The above will be transformed into the following:
// code produced by wite
const modules = {
'./dir/foo.js': () => import('./dir/foo.js'),
'./dir/bar.js': () => import('./dir/bar.js')
}
You can then iterate over the keys of the modules
object to access the corresponding modules:
for (const path in modules) {
modules[path]().then((mod) => {
console.log(path, mod)
})
}
Matched files are by default lazy-loaded via dynamic import and will be split into separate chunks during build. If you'd rather import all the modules directly (e.g. relying on side-effects in these modules to be applied first), you can pass { eager: true }
as the second argument:
const modules = import.meta.glob('./dir/*.js', { eager: true })
The above will be transformed into the following:
// code produced by wite
import * as __glob__0_0 from './dir/foo.js'
import * as __glob__0_1 from './dir/bar.js'
const modules = {
'./dir/foo.js': __glob__0_0,
'./dir/bar.js': __glob__0_1
}
Glob Import As
import.
also supports importing files as strings (similar to Importing Asset as String) with the Import Reflection syntax:
const modules = import.meta.glob('./dir/*.js', { as: 'raw' })
The above will be transformed into the following:
// code produced by wite
const modules = {
'./dir/foo.js': 'export default "foo"\n',
'./dir/bar.js': 'export default "bar"\n'
}
{ as: 'url' }
is also supported for loading assets as URLs.
Multiple Patterns
The first argument can be an array of globs, for example
const modules = import.meta.glob(['./dir/*.js', './another/*.js'])
Negative Patterns
Negative glob patterns are also supported (prefixed with !
). To ignore some files from the result, you can add exclude glob patterns to the first argument:
const modules = import.meta.glob(['./dir/*.js', '!**/bar.js'])
// code produced by wite
const modules = {
'./dir/foo.js': () => import('./dir/foo.js')
}
Named Imports
It's possible to only import parts of the modules with the import
options.
const modules = import.meta.glob('./dir/*.js', { import: 'setup' })
// code produced by wite
const modules = {
'./dir/foo.js': () => import('./dir/foo.js').then((m) => m.setup),
'./dir/bar.js': () => import('./dir/bar.js').then((m) => m.setup)
}
When combined with eager
it's even possible to have tree-shaking enabled for those modules.
const modules = import.meta.glob('./dir/*.js', { import: 'setup', eager: true })
// code produced by wite:
import { setup as __glob__0_0 } from './dir/foo.js'
import { setup as __glob__0_1 } from './dir/bar.js'
const modules = {
'./dir/foo.js': __glob__0_0,
'./dir/bar.js': __glob__0_1
}
Set import
to default
to import the default export.
const modules = import.meta.glob('./dir/*.js', {
import: 'default',
eager: true
})
// code produced by wite:
import __glob__0_0 from './dir/foo.js'
import __glob__0_1 from './dir/bar.js'
const modules = {
'./dir/foo.js': __glob__0_0,
'./dir/bar.js': __glob__0_1
}
Custom Queries
You can also use the query
option to provide custom queries to imports for other plugins to consume.
const modules = import.meta.glob('./dir/*.js', {
query: { foo: 'bar', bar: true }
})
// code produced by wite:
const modules = {
'./dir/foo.js': () =>
import('./dir/foo.js?foo=bar&bar=true').then((m) => m.setup),
'./dir/bar.js': () =>
import('./dir/bar.js?foo=bar&bar=true').then((m) => m.setup)
}
Glob Import Caveats
Note that:
- This is a Wite-only feature and is not a web or ES standard.
- The glob patterns are treated like import specifiers: they must be either relative (start with
./
) or absolute (start with/
, resolved relative to project root) or an alias path (seeresolve.alias
option). - The glob matching is done via
fast-glob
- check out its documentation for supported glob patterns. - You should also be aware that all the arguments in the
import.
must be passed as literals. You can NOT use variables or expressions in them.meta.glob
Dynamic Import
Similar to glob import, Wite also supports dynamic import with variables.
const module = await import(`./dir/${file}.js`)
Note that variables only represent file names one level deep. If file
is 'foo/bar'
, the import would fail. For more advanced usage, you can use the glob import feature.
WebAssembly
Pre-compiled .wasm
files can be imported with ?init
- the default export will be an initialization function that returns a Promise of the wasm instance:
import init from './example.wasm?init'
init().then((instance) => {
instance.exports.test()
})
The init function can also take the imports
object which is passed along to WebAssembly.instantiate
as its second argument:
init({
imports: {
someFunc: () => {
/* ... */
}
}
}).then(() => {
/* ... */
})
In the production build, .wasm
files smaller than assetInlineLimit
will be inlined as base64 strings. Otherwise, they will be copied to the dist directory as an asset and fetched on-demand.
Web Workers
Import with Constructors
A web worker script can be imported using new Worker()
and new SharedWorker()
. Compared to the worker suffixes, this syntax leans closer to the standards and is the recommended way to create workers.
const worker = new Worker(new URL('./worker.js', import.meta.url))
The worker constructor also accepts options, which can be used to create "module" workers:
const worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
})
Import with Query Suffixes
A web worker script can be directly imported by appending ?worker
or ?sharedworker
to the import request. The default export will be a custom worker constructor:
import MyWorker from './worker?worker'
const worker = new MyWorker()
The worker script can also use import
statements instead of importScripts()
- note during dev this relies on browser native support and currently only works in Chrome, but for the production build it is compiled away.
By default, the worker script will be emitted as a separate chunk in the production build. If you wish to inline the worker as base64 strings, add the inline
query:
import MyWorker from './worker?worker&inline'
If you wish to retrieve the worker as a URL, add the url
query:
import MyWorker from './worker?worker&url'
See Worker Options for details on configuring the bundling of all workers.
Build Optimizations
Features listed below are automatically applied as part of the build process and there is no need for explicit configuration unless you want to disable them.
CSS Code Splitting
Wite automatically extracts the CSS used by modules in an async chunk and generates a separate file for it. The CSS file is automatically loaded via a <link>
tag when the associated async chunk is loaded, and the async chunk is guaranteed to only be evaluated after the CSS is loaded to avoid FOUC.
If you'd rather have all the CSS extracted into a single file, you can disable CSS code splitting by setting build.cssCodeSplit
to false
.
Preload Directives Generation
Wite automatically generates <link rel="modulepreload">
directives for entry chunks and their direct imports in the built HTML.
Async Chunk Loading Optimization
In real world applications, Rollup often generates "common" chunks - code that is shared between two or more other chunks. Combined with dynamic imports, it is quite common to have the following scenario:
In the non-optimized scenarios, when async chunk A
is imported, the browser will have to request and parse A
before it can figure out that it also needs the common chunk C
. This results in an extra network roundtrip:
Entry ---> A ---> C
Wite automatically rewrites code-split dynamic import calls with a preload step so that when A
is requested, C
is fetched in parallel:
Entry ---> (A + C)
It is possible for C
to have further imports, which will result in even more roundtrips in the un-optimized scenario. Wite's optimization will trace all the direct imports to completely eliminate the roundtrips regardless of import depth.