zshy automatically handles non-JavaScript assets and copies them to the output directory during builds. This feature enables including custom files like LICENSE, README.md, Protocol Buffer definitions, Ethereum ABIs, and other artifacts in your TypeScript packages without additional configuration.
Key Concept: zshy detects imported assets and copies them to the output directory, but does not make them available as JavaScript modules. The assets remain as separate files that you access at runtime.
- Automatic Detection: Any imported file that isn't JavaScript/TypeScript (
.js,.mjs,.cjs,.ts,.mts,.cts,.tsx) is treated as an asset - Import Tracking: Assets are detected during TypeScript compilation when imported in source files
- Structure Preservation: Assets maintain their relative directory structure in the output
- Dual Build Support: Assets are copied for both ESM and CJS builds
zshy supports any non-JS/TS file type, including:
- LICENSE files (plain text)
- README.md files (Markdown documentation)
- JSON files (configuration, Ethereum ABIs, schemas)
- Protocol Buffer definitions (
.protofiles) - CSS stylesheets (
.cssfiles) - Binary assets (images, fonts, etc.)
- Configuration files (YAML, TOML, XML, etc.)
- Any other custom file formats
// ❌ This DOES NOT WORK with zshy - it's not a bundler!
import licenseText from './assets/LICENSE'; // ERROR: Can't import as string
import readmeContent from './assets/README.md'; // ERROR: Can't import as string
import appConfig from './assets/config.json'; // ERROR: Can't import as object
// ❌ This fails because zshy doesn't transform assets into JS modules
export const config = {
license: licenseText, // undefined - import failed
readme: readmeContent // undefined - import failed
};// ✅ Import to trigger copying (no content available at build time)
import './assets/styles.css'; // Copies to dist/assets/styles.css
import './assets/LICENSE'; // Copies to dist/assets/LICENSE
import './assets/README.md'; // Copies to dist/assets/README.md
import './contracts/schema.proto'; // Copies to dist/contracts/schema.proto
// ✅ Access copied files at runtime (browser)
export async function loadDocs() {
const licenseResponse = await fetch('/assets/LICENSE');
const licenseText = await licenseResponse.text();
const readmeResponse = await fetch('/assets/README.md');
const readmeContent = await readmeResponse.text();
return { licenseText, readmeContent };
}
// ✅ Or access via file system (Node.js)
import { readFileSync } from 'fs';
import { join } from 'path';
export function loadDocsSync() {
const licenseText = readFileSync(join(__dirname, 'assets/LICENSE'), 'utf-8');
const readmeContent = readFileSync(join(__dirname, 'assets/README.md'), 'utf-8');
return { licenseText, readmeContent };
}If you need the content available as JavaScript variables (like the failed example), you need to embed it manually:
// ✅ Embed content as string literals (bypasses zshy's asset handling)
export const LICENSE = `MIT License
Copyright (c) 2024...`;
export const README = `# My Package
This is my package...`;src/
├── index.ts # Main entry point
├── assets/
│ ├── LICENSE # Plain text license
│ ├── README.md # Documentation
│ ├── config.json # Configuration
│ └── styles.css # Stylesheets
└── contracts/
├── MyContract.abi.json # Ethereum ABI
└── schema.proto # Protocol buffers
dist/
├── index.js # Compiled JavaScript (ESM)
├── index.cjs # Compiled JavaScript (CJS)
├── index.d.ts # Type definitions (ESM)
├── index.d.cts # Type definitions (CJS)
├── assets/ # Copied assets (structure preserved)
│ ├── LICENSE
│ ├── README.md
│ ├── config.json
│ └── styles.css
└── contracts/
├── MyContract.abi.json
└── schema.proto
When running zshy, you'll see asset copying in the build output:
» Found 5 asset import(s), copying to output directory...
» Copied asset: ./src/assets/LICENSE → ./dist/assets/LICENSE
» Copied asset: ./src/assets/README.md → ./dist/assets/README.md
» Copied asset: ./src/assets/config.json → ./dist/assets/config.json
» Copied asset: ./src/contracts/MyContract.abi.json → ./dist/contracts/MyContract.abi.json
» Copied asset: ./src/contracts/schema.proto → ./dist/contracts/schema.proto- ✅ Zero Configuration: Works automatically without setup
- ✅ Import-Based: Only copies assets that are actually imported
- ✅ Structure Preservation: Maintains relative directory paths
- ✅ Dual Module Support: Works with both ESM and CJS builds
- ✅ Universal File Support: Handles any non-JS/TS file type
- ✅ Build Integration: Integrated into the standard zshy build process
// ❌ These imports don't work - zshy is not webpack!
import tokenAbi from './contracts/Token.abi.json';
import userSchema from './schemas/user.proto';
import license from './LICENSE';
import defaultConfig from './config/default.json';// ✅ Import to copy files
import './contracts/Token.abi.json';
import './contracts/NFT.abi.json';
// ✅ Load at runtime
export async function loadTokenAbi() {
const response = await fetch('/contracts/Token.abi.json');
return response.json();
}// ✅ Import to copy files
import './schemas/user.proto';
import './schemas/config.proto';
// ✅ Access in Node.js at runtime
import { readFileSync } from 'fs';
export function loadUserSchema() {
return readFileSync('./schemas/user.proto', 'utf-8');
}// ✅ Import to copy files to package
import './LICENSE';
import './CHANGELOG.md';
import './README.md';
// Files are included in npm package and can be served/accessed// ✅ CSS files get copied (common pattern)
import './styles/components.css';
// ✅ Config files get copied
import './config/default.json';
// ✅ Load config at runtime
export async function loadConfig() {
const response = await fetch('/config/default.json');
return response.json();
}- Detection Logic: Uses file extension checking to identify non-JS/TS files
- Copy Operation: Files are physically copied from source to output directory
- NO Transformation: Assets remain as-is - no bundling or module transformation
- Import Purpose:
importstatements only trigger copying, they don't make content available - Copy Timing: Assets are copied during both CJS and ESM compilation phases
- Path Resolution: Relative imports are resolved relative to the importing file
- Deduplication: Same asset imported multiple times is only copied once
- Error Handling: Missing assets are logged but don't fail the build
- ❌ Does not transform assets into JavaScript modules
- ❌ Does not make asset content available via imports
- ❌ Does not bundle or inline assets
- ❌ Does not provide build-time access to asset content
- ❌ Does not work like webpack, Vite, or other bundlers
Think of zshy's asset handling like a smart cp command:
- It tracks which files you import
- It copies those files to the output directory
- The files remain separate from your JavaScript code
- You access them at runtime using normal file operations
- Organize Assets: Use consistent directory structure (
assets/,contracts/, etc.) - Import for Copying: Use
import './file.ext'to trigger copying - Runtime Access: Load assets with
fetch(),readFileSync(), or serve as static files - Consider File Sizes: Large binary assets increase package size
- Version Control: Include all assets in your repository
- Documentation: Document that assets need runtime loading
- Don't expect module imports:
import content from './file.txt'won't work - Don't add type declarations: Creating
declare module "*.md"won't fix the fundamental issue - Don't treat like webpack: zshy is not a bundler and doesn't transform assets
- Don't expect build-time content: Asset content is not available during compilation
- Don't mix patterns: Either use zshy's copying OR embed content as strings, not both
To verify asset handling is working:
- Import assets in your TypeScript files:
import './assets/file.ext' - Run
zshy --verboseto see asset copying logs - Check the
dist/directory for copied assets - Confirm relative paths are preserved
- Test runtime access - verify you can load files with
fetch()orfs.readFile()
Problem: TypeScript can't resolve the import
Solution: This is expected! The import is only for copying. Access the file at runtime instead.
Problem: Expecting import content from './file.md' to work
Solution: zshy doesn't transform assets. Use runtime loading or embed content as strings.
Problem: Files are copied but runtime access fails
Solution: Make sure your server serves the copied files, or use correct file paths for Node.js.
zshy's asset handling is perfect for including files in your package (documentation, configs, schemas), but not for importing content as modules. It's a smart file copier, not a bundler. Use it to include assets that you'll access at runtime, not at build time.