Assets
The studio-cli apps build command uses esbuild under the hood. This command combines an app’s frontend and backend files into single files (frontend-bundle.js and backend-bundle.js), removing unused code and reducing file size in the process. While bundling is useful, reading/importing/resolving assets within a bundle can sometimes be challenging because the file structure does not match what you see in development.
Supported File Types
Section titled “Supported File Types”Fortunately, esbuild can automatically bundle most imported assets during the build process. So a config.json import would be replaced by the JSON value itself. Supported file types for this inline replacement include:
- JSON (
.json): The import is replaced by a JSON object. - Text (
.txt): The import is replaced by the text contents as a string. - CSS (
.cssor.module.css): CSS is placed in a file alongside the bundle. - SCSS/SASS (
.scssor.sass): SCSS and SASS are compiled to CSS and placed in a file alongside the bundle. - Binary (
.data): The import is decoded from Base64 at runtime, and placed as a Uint8Array. - Native assets (
.node): Copies the .node file todistand replaces the import path. Copied.nodefiles will be renamed to include a hash ([name]-[hash].node) to avoid file clashes.
Examples
Section titled “Examples”Say we have a JSON file file.json:
{ "contents": "jsonFileContents" }If we import it like so in src/backend/file.ts:
const jsonFile = require("./any/path/to/file.json");Then esbuild will modify the require statement during the bundling process to backend-bundle.js like this:
const jsonFile = { contents: "jsonFileContents" };Localization Files
Section titled “Localization Files”Localization files are automatically found and copied from the current directory and all @itwin and @bentley dependencies by searching for files matching the pattern **/locales/*/*.json. For all other locations, or if you are experiencing problems using the default settings, use the --copyLocalesFrom flag. If using the --copyLocalesFrom flag, the default locations will no longer be searched. See studio-cli apps build --help for more details.
Examples
Section titled “Examples”- Copying locales from
process.cwd(),@itwin,@bentleypackages:
studio-cli apps build --frontendEntry frontend/index.tsx- Copying locales from custom
someThirdPartyModule/*/publicandfrontend/assetsonly:
studio-cli apps build --frontendEntry frontend/index.tsx --copyLocalesFrom node_modules/someThirdPartyModule/*/public --copyLocalesFrom frontend/assetsOther File Types
Section titled “Other File Types”For other files types, or if not importing (e.g., if you are reading a file with fs.readFileSync), use the --copy flag. The --copy flag copies the asset or directory next to the bundle, allowing you to resolve the path using StudioApp.resolveAssetPath() on the frontend and StudioHost.resolveAssetPath() on the backend. The asset will be copied exactly as you provide it, and the argument supports globs. See the tests for more examples.
Examples
Section titled “Examples”- Copying a frontend asset:
studio-cli apps build --frontendEntry frontend/index.tsx --copy frontend/assetsconst assetPath = StudioApp.resolveAssetPath("frontend/assets/asset.xml");- Copying all
.xmlbackend assets:
studio-cli apps build --backendEntry backend/index.ts --copy backend/**/*.xmlconst assetPath = StudioHost.resolveAssetPath("backend/asset/asset.xml");- Copying a backend asset executable and resolving using
require.resolve:
studio-cli apps build --watch --backendEntry backend/index.ts --copy node_modules/@org/package/bin/asset.execonst assetPath = require.resolve("@org/package/bin/asset.exe");Non-”bundle-able” Assets and Dependencies
Section titled “Non-”bundle-able” Assets and Dependencies”In cases where a dependency frequently accesses its package directory on the file system, bundling may be problematic as the file structure inside a bundle differs from the original. To address this, marking the package as external is recommended. This instructs the bundler not to include the package in the bundle, allowing it to retain its original file system structure at runtime. Use this flag cautiously, you are responsible for copying external packages entire dependency trees, which might have implications for the application’s structure and behavior.
Example
Section titled “Example”For example, we want to mark the sharp image processing library as external, which internally uses random files from the file system determined by runtime environment variables:
"scripts": { // ... "build": "studio-cli apps build --backendEntry backend/index.ts --backendExternal \"sharp\"", "build:externals": "node bundleExternals.js",}The bundleExternals.js script will copy sharp’s dependency tree to dist/node_modules, ensuring that the required assets and functionalities are retained in the bundled application.
const fs = require("fs/promises");
const copySharp = async () => { await fs.cp("./node_modules/sharp", "./dist/node_modules/sharp", { recursive: true, }); // sharp's dependencies await fs.cp("./node_modules/color", "./dist/node_modules/color", { recursive: true, }); await fs.cp("./node_modules/semver", "./dist/node_modules/semver", { recursive: true, }); await fs.cp("./node_modules/detect-libc", "./dist/node_modules/detect-libc", { recursive: true, }); // sharp -> color's dependencies await fs.cp("./node_modules/color-convert", "./dist/node_modules/color-convert", { recursive: true, }); await fs.cp("./node_modules/color-string", "./dist/node_modules/color-string", { recursive: true, }); // sharp -> color -> color-string's dependencies await fs.cp("./node_modules/color-name", "./dist/node_modules/color-name", { recursive: true, }); await fs.cp("./node_modules/simple-swizzle", "./dist/node_modules/simple-swizzle", { recursive: true, }); // sharp -> color -> color-string -> simple-swizzle's dependencies await fs.cp("./node_modules/is-arrayish", "./dist/node_modules/is-arrayish", { recursive: true, });};
copySharp();