Extra features
Importing image assets
The following image types are supported in sku: bmp, gif, jpg, jpeg, png, svg, webp and avif.
Using an image in your application is as simple as importing it:
import heroImageUrl from './heroImage.png';
const HeroImage = () => <img src={heroImageUrl} alt="A hero image" />;All supported image types (except SVG) will be imported as strings you can pass to a src attribute. The imported string is typically a URL, however files smaller than 10,000 bytes will be inlined as a base64-encoded data: URL.
TIP
Browser support for webp and avif varies. To ensure compatibility across browsers, consider providing fallback image formats using the picture element.
import avifImageUrl from './image.avif';
import webpImageUrl from './image.webp';
import pngImageUrl from './image.png';
const ImageWithFallbacks = () => (
<picture>
<source srcset={avifImageUrl} type="image/avif" />
<source srcset={webpImageUrl} type="image/webp" />
<img src={pngImageUrl} alt="An image" />
</picture>
);If you want to use a currently unsupported format feel free to submit a PR or contact #sku-support.
SVGs
TIP
Importing SVGs without query parameters is handled differently in webpack and Vite. See bundler-specific behaviour for more information.
SVGs are handled differently to other image formats. Imported SVGs are raw strings representing optimized (via SVGO) markup, not URLs. These markup strings can then be passed to an HTML element in React:
import svgMarkup from './icon.svg';
const MySvgComponent = () => {
return <div dangerouslySetInnerHTML={{ __html: svgMarkup }} />;
};TIP
Importing optimized SVG markup from files is recommended over rendering SVG elements with React. SVG elements rendered by React are not optimized by sku.
Importing SVGs may not be possible in all use cases, such as when the SVG elements require user-configurable props. In those cases you can render SVG elements directly in React:
const SvgComponent = ({ tone }: { tone: 'critical' }) => {
const stroke = tone === 'critical' ? 'red' : 'black';
return (
<svg
width="50"
height="50"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="10"
y="10"
width="30"
height="30"
stroke={stroke}
fill="transparent"
stroke-width="5"
/>
</svg>
);
};Bundler-specific behaviour
Importing SVG files with no query parameters has different behaviour in webpack and Vite. Webpack imports the optimized contents of the SVG file, while Vite handles SVGs like any other image asset: inlining small assets as data URLs and returning asset URLs for larger assets.
Rather than changing the default SVG behaviour in webpack to address these inconsistencies, which could break existing apps, as of sku v15.13.0 webpack apps now support the same url, raw and inline query parameters that Vite provides for importing assets, but only when importing SVG files. This allows applications and libraries to opt-in to consistent behaviour for both bundlers. See the vite docs for more details on these query parameters.
To guarantee consistent behaviour across bundlers, it's recommended to include a query parameter when importing SVG files in both applications and libraries. See sku's Vite migration guide for more details.
Source maps
Source maps are enabled by default when running both sku start and sku build. If you want to disable source map generation for production builds, you can set sourceMapsProd to false.
Compile packages
Sometimes you might want to extract and share code between sku projects, but this code is likely to rely on the same tooling and language features that sku provides. sku supports loading packages as if they were part of your app via the compilePackages feature.
The best way to configure a package as a compilePackage is to set "skuCompilePackage": true in the package's package.json. This method only works for @seek scoped packages.
{
"name": "@seek/my-package",
"skuCompilePackage": true
}Alternatively, you can add any packages you like to the compilePackages option in the consuming app's sku config file.
export default {
compilePackages: ['awesome-shared-components'],
} satisfies SkuConfig;Any node_modules marked as a compilePackage will be compiled through webpack as if they are part of your app.
Polyfills
Since sku injects its own code into your bundle in development mode, it's important for polyfills that modify the global environment to be loaded before all other code. To address this, the polyfills option allows you to provide an array of modules to import before any other code is executed.
NOTE: Polyfills are only loaded in a browser context. This feature can't be used to modify the global environment in Node.
export default {
polyfills: [
'promise-polyfill',
'core-js/modules/es6.symbol',
'regenerator-runtime/runtime',
],
} satisfies SkuConfig;Caching
sku emits two different caches that can help speed up local and production builds.
Webpack filesystem cache
This cache stores generated webpack modules and chunks. It is only emitted during local development. Its purpose is to reduce the time it takes to start the local development server.
This cache is stored in
node_modules/.cache/webpackand can be safely deleted at any time.
babel-loader cache
This cache stores the result of module transpilation performed by babel-loader. It is emitted during both local development and production builds. Its purpose is to speed up transpilation of TypeScript/JavaScript code. This can benefit both local development (when the webpack cache is invalidated) and production builds. For applications with a large number of source files and/or dependencies, this cache can significantly reduce build times.
This cache is stored in
node_modules/.cache/babel-loaderand can be safely deleted at any time.
Utilizing the babel-loader cache in CI
Buildkite
To utilize the babel-loader cache in Buildkite, you can use the cache plugin to cache the node_modules/.cache/babel-loader directory.
The example below stores the cache in an S3 bucket. It is recommended to add a lifecycle configuration to your bucket in order to automatically delete old cache files.
# .buildkite/pipeline.yaml
steps:
- label: 'Build sku app'
command: 'pnpm exec sku build'
# Add these environment variables and plugin item to your pipeline steps that run `sku build`
env:
BUILDKITE_PLUGIN_S3_CACHE_BUCKET: my-buildkite-cache-bucket
BUILDKITE_PLUGIN_S3_CACHE_PREFIX: my-app-babel-loader-cache
plugins:
- cache#v1.1.0:
path: ./node_modules/.cache/babel-loader
restore: file
save:
- file
manifest: pnpm-lock.yaml
backend: s3
compression: tgzGitHub actions
To utilize the babel-loader cache in GitHub actions, you can use the cache action to cache the node_modules/.cache/babel-loader directory.
# .github/workflows/deploy-site.yaml
# Add this step before the step that runs `sku build`
- name: Cache babel-loader
id: cache-babel-loader
uses: actions/cache@v4
with:
path: 'node_modules/.cache/babel-loader'
key: babel-loader-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}Bundle analysis
sku comes with bundle analysis built in via webpack-bundle-analyzer. A report is generated in the /report directory when sku build is run.
Pre-commit hook
The
sku pre-commitcommand was removed in v16. It saw very little use and bundledlint-staged(and its many transitive dependencies) into everyskuinstall. Setting this up yourself is straightforward and gives you full control over which commands run before each commit.
To speed up the feedback loop on linting and formatting errors, you can run sku format and sku lint against staged files before they're committed. We recommend pairing nano-staged (a tiny, zero-dependency alternative to lint-staged) with husky.
- Install both tools as development dependencies:
pnpm install --dev nano-staged husky- Add the
nano-stagedconfig and husky'spreparescript to yourpackage.json. Adjust the nano-staged config to your project's needs - an example is provided below:
// package.json
{
"scripts": {
"prepare": "husky"
},
"nano-staged": {
"**/*.{js,jsx,ts,tsx,md}": ["sku format", "sku lint"]
}
}- Create the pre-commit hook that runs
nano-staged:
echo "pnpm nano-staged" > .husky/pre-commitFor more details, see the nano-staged and husky documentation.
Assertion removal
By default, sku will remove assertions in your production builds with babel-plugin-unassert. This allows you to perform more expensive checks during development without worrying about the perfomance impacts on users.
For example, given the following code:
import React from 'react';
import assert from 'assert';
export const Rating = ({ rating }: { rating: number }) => {
assert(rating >= 0 && rating <= 5, 'Rating must be between 0 and 5');
return <div>...</div>;
};In production, sku would transform the code above into code roughly equivalent to:
import React from 'react';
export const Rating = ({ rating }) => <div>...</div>;Supported Assertion Function Names
invariantassert
Supported Assertion Libraries
tiny-invariant(Recommended)assert(Node.js built-in or browser port)node:assert(Node.js built-in)
Any combination of function name and library name is supported. tiny-invariant is recommended over assert due to its simplicity and size.
Environment-specific code
sku configures webpack to replace all instances of process.env.NODE_ENV with its actual value. During the start and start-ssr commands, process.env.NODE_ENV is replaced with 'development'. During the build and build-ssr commands, process.env.NODE_ENV is replaced with 'production'.
Combined with dead code elimination during minification, this allows you to write environment-specific code that is removed in production.
For example, consider the following code:
import someDevOnlyFunction from './some-dev-only-function';
import someProdOnlyFunction from './some-prod-only-function';
if (process.env.NODE_ENV === 'development') {
someDevOnlyFunction();
}
if (process.env.NODE_ENV === 'production') {
someProdOnlyFunction();
}In development this code would be transformed into:
import someDevOnlyFunction from './some-dev-only-function';
import someProdOnlyFunction from './some-prod-only-function';
if ('development' === 'development') {
someDevOnlyFunction();
}
if ('development' === 'production') {
someProdOnlyFunction();
}Note that both if statements are still present in the code, but clearly only someDevOnlyFunction will be called.
However, in production, the code would be transformed into:
import someProdOnlyFunction from './some-prod-only-function';
someProdOnlyFunction();The first if block is removed entirely as its condition is always false. This allows the someDevOnlyFunction import to be removed as well. The second if block is removed, however the contents of the block are kept as its condition is always true.
DevServer Middleware
Supply a devServerMiddleware path in your sku config to access the internal dev Express server.
The file must export a function that will receive the express server:
export default (app) => {
app.get('/mock-api', (req, res) => {
// ...
});
};