Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/early-geese-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@web/rollup-plugin-import-meta-assets': minor
---

Add option `preserveDynamicStructure` that emits dynamic assets and rewrites the URL pattern to resolve the original dynamic path relative to the first emitted asset.
It requires that the output preserves both filenames (no hashing) and directory structure from the dynamic expression onwards.
6 changes: 6 additions & 0 deletions .changeset/ten-scissors-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@web/rollup-plugin-html': minor
---

Support preserving the assets input structure in the output.
Support transforming of assets found in CSS.
46 changes: 46 additions & 0 deletions docs/docs/building/rollup-plugin-import-meta-assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,52 @@ export default {
};
```

### `preserveDynamicStructure`

Type: `Boolean`<br>
Default: `false`

When enabled, dynamic asset URLs (using template literals) are emitted to the Rollup pipeline and the URL pattern is rewritten to resolve relative to the first emitted asset.

**Requirements:** The output must preserve both filenames (no hashing) and the directory structure from the dynamic expression onwards.
If filenames are hashed or the directory structure changes, the runtime URL resolution will fail.

This is useful when your application or CDN already has versioned URLs, so you don't need filename hashing.
It also avoids generating a large switch statement in the output when you have many dynamic assets (e.g. an icon library).

```js
import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets';

const projectRoot = process.cwd();

export default {
input: 'src/index.js',
output: {
dir: 'output',
format: 'es',
// preserve original file paths, relative to the project root
assetFileNames: asset =>
path.relative(projectRoot, asset.originalFileNames[0]).split(path.sep).join('/'),
},
plugins: [
importMetaAssets({
preserveDynamicStructure: true,
}),
],
};
```

Given this source code:

```js
const icon = new URL(`./assets/icons/${category}/${name}.svg`, import.meta.url);
```

The plugin will:

1. Emit all matching assets (e.g. `./assets/icons/outline/arrow.svg`, `./assets/icons/solid/check.svg`, etc..)
2. Rewrite the URL to resolve relative to the first emitted asset

## Examples

Source directory:
Expand Down
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/rollup-plugin-html/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"html-minifier-terser": "^7.1.0",
"lightningcss": "^1.24.0",
"parse5": "^6.0.1",
"picomatch": "^2.2.2"
"picomatch": "^2.2.2",
"xxhash-wasm": "^1.1.0"
},
"devDependencies": {
"@prettier/sync": "^0.6.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface RollupPluginHTMLOptions {
extractAssets?: boolean | 'legacy-html' | 'legacy-html-and-css';
/** Whether to bundle extracted CSS assets. Bundling is done via Lightning CSS. Defaults to true. */
bundleCss?: boolean;
/** Whether to minify extracted CSS assets. Minificaiton is done via Lightning CSS. Defaults to false. */
/** Whether to minify extracted CSS assets. Minification is done via Lightning CSS. Defaults to false. */
minifyCss?: boolean;
/** Whether to ignore assets referenced in HTML and CSS with glob patterns. */
externalAssets?: string | string[];
Expand Down
89 changes: 89 additions & 0 deletions packages/rollup-plugin-html/src/output/css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import path from 'path';
import type { OutputBundle, PluginContext } from 'rollup';
import { toBrowserPath } from './utils.js';

/**
* Regular expression to match asset URL placeholders in CSS content.
* Captures the hashes encoded as HEX strings like "abc123" from placeholders like "__ROLLUP_ASSET_URL_abc123__".
*/
const ASSET_URL_PLACEHOLDER_REGEX = /__ROLLUP_ASSET_URL_([a-f0-9]+)__/g;

/**
* Creates a placeholder string for the given hash.
*
* @param hash - Hash encoded as a HEX string (e.g. "abc123")
* @returns Placeholder string like "__ROLLUP_ASSET_URL_abc123__"
*/
export function createAssetPlaceholder(hash: string): string {
return `__ROLLUP_ASSET_URL_${hash}__`;
}

/**
* Replaces all asset URL placeholders in CSS content with resolved paths.
*
* @param cssContent - The CSS content with placeholders
* @param resolver - Function that resolves a hash to the final path
* @returns CSS content with placeholders replaced
*/
export function replacePlaceholders(
cssContent: string,
resolver: (hash: string) => string | undefined,
): string {
return cssContent.replace(ASSET_URL_PLACEHOLDER_REGEX, (match, hash) => {
const resolvedPath = resolver(hash);
return resolvedPath ?? match;
});
}

/**
* Calculates the path from a CSS file to a referenced asset in the output.
* If publicPath is provided, returns an absolute path. Otherwise returns a relative path.
*
* @param cssFilePath - The CSS file's path in the bundle (e.g. 'styles/main.css')
* @param assetFilePath - The asset's path in the bundle (e.g. 'assets/image.png')
* @param publicPath - Optional public path prefix (e.g. '/static/')
* @returns Absolute path if publicPath provided, otherwise relative path from CSS to asset
*/
export function calculateRelativePath(
cssFilePath: string,
assetFilePath: string,
publicPath?: string,
): string {
if (publicPath) {
return toBrowserPath(path.join(publicPath, assetFilePath));
}

const cssDir = path.dirname(cssFilePath);
const relativePath = path.relative(cssDir, assetFilePath);
return toBrowserPath(relativePath);
}

/**
* Processes all CSS files in the bundle, replacing placeholders with resolved paths.
*
* @param {PluginContext} pluginContext - The Rollup plugin context
* @param {OutputBundle} bundle - The Rollup output bundle
* @param {Record<string, { ref: string }>} assetsInCssByHash - Map of asset hashes to their Rollup refs for assets found in CSS
* @param {string} [publicPath] - Optional public path prefix for absolute URLs (e.g. '/static/')
*/
export function processCssAssets(
pluginContext: PluginContext,
bundle: OutputBundle,
assetsInCssByHash: Record<string, { ref: string }>,
publicPath?: string,
): void {
for (const [filePath, asset] of Object.entries(bundle)) {
if (asset.type !== 'asset' || !filePath.endsWith('.css')) continue;

const cssContent =
typeof asset.source === 'string' ? asset.source : Buffer.from(asset.source).toString('utf-8');

const resolvedContent = replacePlaceholders(cssContent, (hash: string) => {
const ref = assetsInCssByHash[hash]!.ref;
const assetFilePath = pluginContext.getFileName(ref);
return calculateRelativePath(filePath, assetFilePath, publicPath);
});

asset.source = resolvedContent;
}
}
Loading
Loading