From 62f03e2b04386e26af8b0193d096d53cc45164fd Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Wed, 29 Apr 2026 11:31:22 +0200 Subject: [PATCH] feat: support preserving assets input structure --- .../rollup-plugin-import-meta-assets.md | 46 + .../src/RollupPluginHTMLOptions.ts | 2 +- packages/rollup-plugin-html/src/output/css.ts | 95 ++ .../src/output/emitAssets.ts | 66 +- .../src/output/getEntrypointBundles.ts | 4 +- .../src/rollupPluginHTML.ts | 7 + .../test/rollup-plugin-html.test.ts | 233 +++- .../test/src/input/InputData.test.ts | 2 +- .../src/input/extract/extractAssets.test.ts | 9 +- .../src/input/extract/extractModules.test.ts | 2 +- .../test/src/output/css.test.ts | 91 ++ .../test/src/output/getOutputHTML.test.ts | 2 +- .../test/src/output/injectBundles.test.ts | 2 +- .../output/injectedUpdatedAssetPaths.test.ts | 2 +- packages/rollup-plugin-html/test/utils.ts | 106 -- .../src/rollup-plugin-import-meta-assets.js | 213 ++- .../test/fixtures/bad-url-entrypoint.js | 3 - .../directories-and-simple-entrypoint.js | 20 - .../test/fixtures/dynamic-assets/one.svg | 5 - .../test/fixtures/dynamic-assets/three.svg | 5 - .../test/fixtures/dynamic-assets/two.svg | 5 - .../test/fixtures/dynamic-vars.js | 8 - .../test/fixtures/five | 5 - .../test/fixtures/four.svg | 5 - .../test/fixtures/image.jpg | Bin 36287 -> 0 bytes .../test/fixtures/multi-level-entrypoint.js | 11 - .../test/fixtures/one.svg | 5 - .../test/fixtures/one/one-deep.svg | 5 - .../test/fixtures/one/one.js | 2 - .../two/different-asset-levels-entrypoint.js | 18 - .../fixtures/one/two/three/four/four-deep.svg | 5 - .../test/fixtures/one/two/three/four/four.js | 2 - .../three/four/multi-level-entrypoint-deep.js | 11 - .../fixtures/one/two/three/three-deep.svg | 5 - .../test/fixtures/one/two/three/three.js | 2 - .../test/fixtures/one/two/two-deep.svg | 5 - .../test/fixtures/one/two/two.js | 2 - .../test/fixtures/simple-entrypoint.js | 13 - .../test/fixtures/three.svg | 5 - .../test/fixtures/transform-entrypoint.js | 13 - .../test/fixtures/two.svg | 5 - .../test/integration.test.js | 1210 +++++++++++++++-- .../different-asset-levels-bundle.js | 18 - .../test/snapshots/directories-ignored.js | 20 - .../test/snapshots/dynamic-vars.js | 21 - .../test/snapshots/five | 5 - .../test/snapshots/four-bundle.js | 4 - .../test/snapshots/four.min.svg | 6 - .../test/snapshots/four.svg | 5 - .../test/snapshots/image.jpg | Bin 36287 -> 0 bytes .../test/snapshots/multi-level-bundle.js | 18 - .../test/snapshots/one-bundle.js | 4 - .../test/snapshots/one.min.svg | 6 - .../test/snapshots/one.svg | 5 - .../test/snapshots/simple-bundle.js | 13 - .../test/snapshots/three-bundle.js | 4 - .../test/snapshots/three.min.svg | 6 - .../test/snapshots/three.svg | 5 - .../snapshots/transform-bundle-ignored.js | 13 - .../test/snapshots/transform-bundle.js | 13 - .../test/snapshots/two-bundle.js | 4 - .../test/snapshots/two.min.svg | 6 - .../test/snapshots/two.svg | 5 - test-utils/rollup-test-utils.d.ts | 19 + test-utils/rollup-test-utils.js | 118 ++ 65 files changed, 1868 insertions(+), 707 deletions(-) create mode 100644 packages/rollup-plugin-html/src/output/css.ts create mode 100644 packages/rollup-plugin-html/test/src/output/css.test.ts delete mode 100644 packages/rollup-plugin-html/test/utils.ts delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/bad-url-entrypoint.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/directories-and-simple-entrypoint.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/one.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/three.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/two.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-vars.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/five delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/four.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/image.jpg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/multi-level-entrypoint.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one/one-deep.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one/one.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/different-asset-levels-entrypoint.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/four-deep.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/four.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/multi-level-entrypoint-deep.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/three-deep.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/three.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/two-deep.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/two.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/simple-entrypoint.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/three.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/transform-entrypoint.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/fixtures/two.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/different-asset-levels-bundle.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/directories-ignored.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/dynamic-vars.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/five delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/four-bundle.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/four.min.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/four.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/image.jpg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/multi-level-bundle.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/one-bundle.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/one.min.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/one.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/simple-bundle.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/three-bundle.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/three.min.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/three.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/transform-bundle-ignored.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/transform-bundle.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/two-bundle.js delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/two.min.svg delete mode 100644 packages/rollup-plugin-import-meta-assets/test/snapshots/two.svg create mode 100644 test-utils/rollup-test-utils.d.ts create mode 100644 test-utils/rollup-test-utils.js diff --git a/docs/docs/building/rollup-plugin-import-meta-assets.md b/docs/docs/building/rollup-plugin-import-meta-assets.md index 3272a7be37..80a12d81f2 100644 --- a/docs/docs/building/rollup-plugin-import-meta-assets.md +++ b/docs/docs/building/rollup-plugin-import-meta-assets.md @@ -121,6 +121,52 @@ export default { }; ``` +### `preserveDynamicStructure` + +Type: `Boolean`
+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: diff --git a/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts b/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts index 884f669fb5..48e6fe977f 100644 --- a/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts +++ b/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts @@ -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[]; diff --git a/packages/rollup-plugin-html/src/output/css.ts b/packages/rollup-plugin-html/src/output/css.ts new file mode 100644 index 0000000000..80a65f74ec --- /dev/null +++ b/packages/rollup-plugin-html/src/output/css.ts @@ -0,0 +1,95 @@ +import path from 'path'; +import { OutputBundle, PluginContext } from 'rollup'; +import { toBrowserPath } from './utils.js'; + +/** + * Regular expression to match asset URL placeholders in CSS content. + * Captures the reference ID like "abc123" from placeholders like "__ROLLUP_ASSET_URL_abc123__". + * Note: Rollup reference IDs can contain alphanumeric characters, underscores, and $ (base-64-like encoding). + */ +const ASSET_URL_PLACEHOLDER_REGEX = /__ROLLUP_ASSET_URL_([a-zA-Z0-9_$]+)__/g; + +/** + * Creates a placeholder string for the given reference ID. + * @param refId - The Rollup file reference ID + * @returns Placeholder string like "__ROLLUP_ASSET_URL_abc123__" + */ +export function createAssetPlaceholder(refId: string): string { + return `__ROLLUP_ASSET_URL_${refId}__`; +} + +/** + * Replaces all asset URL placeholders in CSS content with resolved paths. + * Anything after the placeholder (#id, ?queryString) is preserved naturally. + * + * @param cssContent - The CSS content with placeholders + * @param resolver - Function that resolves a reference ID to the final path + * @returns CSS content with placeholders replaced + */ +export function replacePlaceholders( + cssContent: string, + resolver: (refId: string) => string | undefined, +): string { + return cssContent.replace(ASSET_URL_PLACEHOLDER_REGEX, (match, refId) => { + const resolvedPath = resolver(refId); + return resolvedPath ?? match; + }); +} + +/** + * Calculates the path from a CSS file to a referenced asset. + * 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 is provided, return an absolute path + if (publicPath) { + return toBrowserPath(`${publicPath}${assetFilePath}`); + } + + // Otherwise, calculate relative path + const cssDir = path.dirname(cssFilePath); + const relativePath = path.relative(cssDir, assetFilePath); + + // Convert to browser-style forward slashes + 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 {string} [publicPath] - Optional public path prefix for absolute URLs (e.g. '/static/') + */ +export function processCssAssets( + pluginContext: PluginContext, + bundle: OutputBundle, + publicPath?: string, +): void { + for (const [fileName, asset] of Object.entries(bundle)) { + if (asset.type !== 'asset' || !fileName.endsWith('.css')) continue; + + const content = + typeof asset.source === 'string' ? asset.source : Buffer.from(asset.source).toString('utf-8'); + + const resolvedContent = replacePlaceholders(content, (refId: string) => { + try { + const assetFileName = pluginContext.getFileName(refId); + return calculateRelativePath(fileName, assetFileName, publicPath); + } catch { + pluginContext.error(`Could not resolve CSS asset reference '${refId}' in ${fileName}`); + } + }); + + asset.source = resolvedContent; + } +} diff --git a/packages/rollup-plugin-html/src/output/emitAssets.ts b/packages/rollup-plugin-html/src/output/emitAssets.ts index 377db1f25a..bc6c499fa7 100644 --- a/packages/rollup-plugin-html/src/output/emitAssets.ts +++ b/packages/rollup-plugin-html/src/output/emitAssets.ts @@ -4,9 +4,9 @@ import { bundleAsync, transform } from 'lightningcss'; import fs from 'fs'; import { InputAsset, InputData } from '../input/InputData'; -import { toBrowserPath } from './utils.js'; import { createAssetPicomatchMatcher } from '../assets/utils.js'; import { RollupPluginHTMLOptions, TransformAssetFunction } from '../RollupPluginHTMLOptions'; +import { createAssetPlaceholder } from './css.js'; export interface EmittedAssets { static: Map; @@ -88,7 +88,7 @@ export async function emitAssets( let ref: string; let basename = path.basename(asset.filePath); const isExternal = createAssetPicomatchMatcher(options.externalAssets); - const emittedExternalAssets = new Map(); + const emittedAssets = new Map(); if (asset.hashed) { if (basename.endsWith('.css') && extractAssets) { const { code } = await (bundleCss ? bundleAsync : transform)({ @@ -106,49 +106,34 @@ export async function emitAssets( const assetLocation = path.resolve(path.dirname(asset.filePath), filePath); const assetContent = fs.readFileSync(assetLocation); - // Avoid duplicates - if (!emittedExternalAssets.has(assetLocation)) { + let emittedAsset = emittedAssets.get(assetLocation); + + if (!emittedAsset) { + // Avoid duplicates const basename = path.basename(filePath); const fileRef = this.emitFile({ type: 'asset', name: extractAssetsLegacyCss ? `assets/${basename}` : basename, + originalFileName: assetLocation, source: assetContent, }); const emittedAssetFilepath = this.getFileName(fileRef); - const emittedAssetBasename = path.basename(emittedAssetFilepath); - emittedExternalAssets.set(assetLocation, emittedAssetFilepath); - // Update the URL in the original CSS file to point to the emitted asset file - if (extractAssetsLegacyCss) { - url.url = `assets/${emittedAssetBasename}`; - } else { - if (options.publicPath) { - url.url = toBrowserPath( - path.join(options.publicPath, emittedAssetFilepath), - ); - } else { - url.url = emittedAssetBasename; - } - } - if (idRef) { - url.url = `${url.url}#${idRef}`; - } + emittedAsset = { + filePath: emittedAssetFilepath, + refId: fileRef, + }; + emittedAssets.set(assetLocation, emittedAsset); + } + + if (extractAssetsLegacyCss) { + const emittedAssetBasename = path.basename(emittedAsset.filePath); + url.url = `assets/${emittedAssetBasename}`; } else { - const emittedAssetFilepath = emittedExternalAssets.get(assetLocation); - const emittedAssetBasename = path.basename(emittedAssetFilepath); - if (extractAssetsLegacyCss) { - url.url = `assets/${emittedAssetBasename}`; - } else { - if (options.publicPath) { - url.url = toBrowserPath( - path.join(options.publicPath, emittedAssetFilepath), - ); - } else { - url.url = emittedAssetBasename; - } - } - if (idRef) { - url.url = `${url.url}#${idRef}`; - } + url.url = createAssetPlaceholder(emittedAsset.refId); + } + + if (idRef) { + url.url = `${url.url}#${idRef}`; } } return url; @@ -161,7 +146,12 @@ export async function emitAssets( } } - ref = this.emitFile({ type: 'asset', name: basename, source }); + ref = this.emitFile({ + type: 'asset', + name: basename, + originalFileName: asset.filePath, + source, + }); } else { // ensure the output filename is unique let i = 1; diff --git a/packages/rollup-plugin-html/src/output/getEntrypointBundles.ts b/packages/rollup-plugin-html/src/output/getEntrypointBundles.ts index 12c1bbb5ca..871f009de3 100644 --- a/packages/rollup-plugin-html/src/output/getEntrypointBundles.ts +++ b/packages/rollup-plugin-html/src/output/getEntrypointBundles.ts @@ -74,9 +74,9 @@ export function getEntrypointBundles(params: GetEntrypointBundlesParams) { outputDir, fileOutputDir: options.dir ?? '', htmlFileName, - fileName: chunkOrAsset.fileName, + fileName: chunk.fileName, }); - entrypoints.push({ importPath, chunk: chunkOrAsset, attributes: found.attributes }); + entrypoints.push({ importPath, chunk, attributes: found.attributes }); } } } diff --git a/packages/rollup-plugin-html/src/rollupPluginHTML.ts b/packages/rollup-plugin-html/src/rollupPluginHTML.ts index 388c329675..135468b614 100644 --- a/packages/rollup-plugin-html/src/rollupPluginHTML.ts +++ b/packages/rollup-plugin-html/src/rollupPluginHTML.ts @@ -14,6 +14,7 @@ import { } from './RollupPluginHTMLOptions.js'; import { createError, NOOP_IMPORT } from './utils.js'; import { emitAssets } from './output/emitAssets.js'; +import { processCssAssets } from './output/css.js'; export interface RollupPluginHtml extends Plugin { api: { @@ -140,6 +141,9 @@ export function rollupPluginHTML(pluginOptions: RollupPluginHTMLOptions = {}): R generatedBundles.push({ name: 'default', options, bundle }); const emittedAssets = await emitAssets.call(this, inputs, pluginOptions); + + processCssAssets(this, bundle, pluginOptions.publicPath); + const outputs = await createHTMLOutput({ outputDir: path.resolve(options.dir), inputs, @@ -199,6 +203,9 @@ export function rollupPluginHTML(pluginOptions: RollupPluginHTMLOptions = {}): R } const emittedAssets = await emitAssets.call(this, inputs, pluginOptions); + + processCssAssets(this, bundle, pluginOptions.publicPath); + const outputs = await createHTMLOutput({ outputDir: path.resolve(options.dir), inputs, diff --git a/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts b/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts index 564cb96c28..feae3bf8b0 100644 --- a/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts +++ b/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts @@ -2,7 +2,15 @@ import { rollup, OutputChunk, OutputOptions, Plugin } from 'rollup'; import { expect } from 'chai'; import path from 'path'; import { rollupPluginHTML } from '../src/index.js'; -import { html, css, js, svg, generateTestBundle, createApp, cleanApp } from './utils.js'; +import { + html, + css, + js, + svg, + generateTestBundle, + createApp, + cleanApp, +} from '../../../test-utils/rollup-test-utils.js'; const outputConfig: OutputOptions = { format: 'es', @@ -2115,20 +2123,20 @@ describe('rollup-plugin-html', () => { expect(assets).to.have.keys([ 'assets/font-normal-Cht9ZB76.woff2', 'assets/font-bold-eQjSonqH.woff2', - 'assets/styles-Dhs3ufep.css', + 'assets/styles-CJ7-ESJg.css', 'index.html', ]); expect(assets['index.html']).to.equal(html` - + `); - expect(assets['assets/styles-Dhs3ufep.css']).to.equal(css` + expect(assets['assets/styles-CJ7-ESJg.css']).to.equal(css` @font-face { font-family: Font; src: url('font-normal-Cht9ZB76.woff2') format('woff2'); @@ -2280,20 +2288,20 @@ describe('rollup-plugin-html', () => { expect(assets).to.have.keys([ 'assets/font-normal-Cht9ZB76.woff2', 'assets/font-bold-eQjSonqH.woff2', - 'assets/styles-Dhs3ufep.css', + 'assets/styles-CJ7-ESJg.css', 'index.html', ]); expect(assets['index.html']).to.equal(html` - + `); - expect(assets['assets/styles-Dhs3ufep.css']).to.equal(css` + expect(assets['assets/styles-CJ7-ESJg.css']).to.equal(css` @font-face { font-family: Font; src: url('font-normal-Cht9ZB76.woff2') format('woff2'); @@ -2445,22 +2453,22 @@ describe('rollup-plugin-html', () => { expect(assets).to.have.keys([ 'assets/font-normal-Cht9ZB76.woff2', - 'assets/styles-a-jFIfrzm8.css', - 'assets/styles-b-B-8m1N7T.css', + 'assets/styles-a-Dhot3uqa.css', + 'assets/styles-b-BRueNdBl.css', 'index.html', ]); expect(assets['index.html']).to.equal(html` - - + + `); - expect(assets['assets/styles-a-jFIfrzm8.css']).to.equal(css` + expect(assets['assets/styles-a-Dhot3uqa.css']).to.equal(css` @font-face { font-family: Font; src: url('font-normal-Cht9ZB76.woff2') format('woff2'); @@ -2470,7 +2478,7 @@ describe('rollup-plugin-html', () => { } `); - expect(assets['assets/styles-b-B-8m1N7T.css']).to.equal(css` + expect(assets['assets/styles-b-BRueNdBl.css']).to.equal(css` @font-face { font-family: Font2; src: url('font-normal-Cht9ZB76.woff2') format('woff2'); @@ -2557,20 +2565,20 @@ describe('rollup-plugin-html', () => { 'assets/star-CXig10q7.png', 'assets/star-CwhgM_z4.svg', 'assets/star-CKbh5mKn.webp', - 'assets/styles-mywkihBc.css', + 'assets/styles-Ee2nhCU_.css', 'index.html', ]); expect(assets['index.html']).to.equal(html` - + `); - expect(assets['assets/styles-mywkihBc.css']).to.equal(css` + expect(assets['assets/styles-Ee2nhCU_.css']).to.equal(css` #a { background-image: url('star-D_LO5feX.avif'); } @@ -2806,7 +2814,7 @@ describe('rollup-plugin-html', () => { expect(assets).to.have.keys([ 'assets/image-a-XOCPHCrV.png', 'assets/image-b-BgQHKcRn.png', - 'assets/styles-Bv-4gk2N.css', + 'assets/styles-CxU15QYj.css', 'assets/webmanifest-BkrOR1WG.json', 'index.html', ]); @@ -2819,7 +2827,7 @@ describe('rollup-plugin-html', () => { - + @@ -2832,7 +2840,7 @@ describe('rollup-plugin-html', () => { `); - expect(assets['assets/styles-Bv-4gk2N.css']).to.equal(css` + expect(assets['assets/styles-CxU15QYj.css']).to.equal(css` #a1 { background-image: url('image-a-XOCPHCrV.png'); } @@ -3036,17 +3044,17 @@ describe('rollup-plugin-html', () => { expect(assets).to.have.keys([ 'static/font.immutable.C5MNjX-h.woff2', - 'static/global.immutable.DB0fKkjs.css', + 'static/global.immutable.tvdj3JiE.css', 'static/image.immutable.7xJLr_7N.png', - 'static/styles.immutable.D4tZXVv0.css', + 'static/styles.immutable.DlXCehN4.css', 'index.html', ]); expect(assets['index.html']).to.equal(html` - - + + { `); - expect(assets['static/global.immutable.DB0fKkjs.css']).to.equal(css` + expect(assets['static/global.immutable.tvdj3JiE.css']).to.equal(css` @font-face { font-family: Font; src: url('font.immutable.C5MNjX-h.woff2') format('woff2'); @@ -3070,7 +3078,7 @@ describe('rollup-plugin-html', () => { } `); - expect(assets['static/styles.immutable.D4tZXVv0.css']).to.equal(css` + expect(assets['static/styles.immutable.DlXCehN4.css']).to.equal(css` #a { background-image: url('image.immutable.7xJLr_7N.png'); } @@ -3149,17 +3157,17 @@ describe('rollup-plugin-html', () => { expect(assets).to.have.keys([ 'fonts/font.immutable.C5MNjX-h.woff2', - 'styles/global.immutable.B3Q0ucg4.css', + 'styles/global.immutable.tvdj3JiE.css', 'images/image.immutable.7xJLr_7N.png', - 'styles/styles.immutable.C3Z0Fs2-.css', + 'styles/styles.immutable.DlXCehN4.css', 'index.html', ]); expect(assets['index.html']).to.equal(html` - - + + { `); - expect(assets['styles/global.immutable.B3Q0ucg4.css']).to.equal(css` + expect(assets['styles/global.immutable.tvdj3JiE.css']).to.equal(css` @font-face { font-family: Font; src: url('/static/fonts/font.immutable.C5MNjX-h.woff2') format('woff2'); @@ -3183,7 +3191,7 @@ describe('rollup-plugin-html', () => { } `); - expect(assets['styles/styles.immutable.C3Z0Fs2-.css']).to.equal(css` + expect(assets['styles/styles.immutable.DlXCehN4.css']).to.equal(css` #a { background-image: url('/static/images/image.immutable.7xJLr_7N.png'); } @@ -3306,4 +3314,167 @@ describe('rollup-plugin-html', () => { } `); }); + + describe('preserved output paths', () => { + it('resolves relative paths', async () => { + const rootDir = createApp({ + 'assets/icons/arrow.svg': '', + 'fonts/font.woff2': 'font-data', + 'styles/main.css': css` + @font-face { + font-family: Font; + src: url('../fonts/font.woff2') format('woff2'); + } + + .arrow { + background: url('../assets/icons/arrow.svg'); + } + `, + 'styles/components/button.css': css` + .button { + background: url('../../assets/icons/arrow.svg'); + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: html` + + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: (asset: { originalFileNames: string[] }) => + path.relative(rootDir, asset.originalFileNames[0]).split(path.sep).join('/'), + }); + + expect(assets['styles/main.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('../fonts/font.woff2') format('woff2'); + } + + .arrow { + background: url('../assets/icons/arrow.svg'); + } + `); + + expect(assets['styles/components/button.css']).to.equal(css` + .button { + background: url('../../assets/icons/arrow.svg'); + } + `); + }); + + it('handles CSS files with SVG fragments (#icon)', async () => { + const rootDir = createApp({ + 'assets/sprite.svg': '', + 'styles.css': css` + .icon { + background: url('assets/sprite.svg#icon'); + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: (asset: { originalFileNames: string[] }) => + path.relative(rootDir, asset.originalFileNames[0]).split(path.sep).join('/'), + }); + + expect(assets['styles.css']).to.equal(css` + .icon { + background: url('assets/sprite.svg#icon'); + } + `); + }); + + it('handles multiple CSS files referencing the same asset', async () => { + const rootDir = createApp({ + 'shared/image.png': 'image-data', + 'styles/a.css': css` + .a { + background: url('../shared/image.png'); + } + `, + 'styles/nested/b.css': css` + .b { + background: url('../../shared/image.png'); + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: html` + + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: (asset: { originalFileNames: string[] }) => + path.relative(rootDir, asset.originalFileNames[0]).split(path.sep).join('/'), + }); + + expect(assets['styles/a.css']).to.equal(css` + .a { + background: url('../shared/image.png'); + } + `); + + expect(assets['styles/nested/b.css']).to.equal(css` + .b { + background: url('../../shared/image.png'); + } + `); + }); + }); }); diff --git a/packages/rollup-plugin-html/test/src/input/InputData.test.ts b/packages/rollup-plugin-html/test/src/input/InputData.test.ts index f1fab22f99..9810a5dfa6 100644 --- a/packages/rollup-plugin-html/test/src/input/InputData.test.ts +++ b/packages/rollup-plugin-html/test/src/input/InputData.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import path from 'path'; -import { cleanApp, createApp, html, js } from '../../utils.js'; +import { cleanApp, createApp, html, js } from '../../../../../test-utils/rollup-test-utils.js'; import { getInputData } from '../../../src/input/getInputData.js'; import { InputData } from '../../../src/input/InputData.js'; diff --git a/packages/rollup-plugin-html/test/src/input/extract/extractAssets.test.ts b/packages/rollup-plugin-html/test/src/input/extract/extractAssets.test.ts index 24a8dbd2bb..e4abcf640c 100644 --- a/packages/rollup-plugin-html/test/src/input/extract/extractAssets.test.ts +++ b/packages/rollup-plugin-html/test/src/input/extract/extractAssets.test.ts @@ -2,7 +2,14 @@ import { expect } from 'chai'; import { parse } from 'parse5'; import path from 'path'; import { extractAssets } from '../../../../src/input/extract/extractAssets.js'; -import { html, css, js, svg, createApp, cleanApp } from '../../../utils.js'; +import { + html, + css, + js, + svg, + createApp, + cleanApp, +} from '../../../../../../test-utils/rollup-test-utils.js'; describe('extractAssets', () => { afterEach(() => { diff --git a/packages/rollup-plugin-html/test/src/input/extract/extractModules.test.ts b/packages/rollup-plugin-html/test/src/input/extract/extractModules.test.ts index f560f339ee..1ce06c93ee 100644 --- a/packages/rollup-plugin-html/test/src/input/extract/extractModules.test.ts +++ b/packages/rollup-plugin-html/test/src/input/extract/extractModules.test.ts @@ -1,7 +1,7 @@ import path from 'path'; import { parse, serialize } from 'parse5'; import { expect } from 'chai'; -import { html, js } from '../../../utils.js'; +import { html, js } from '../../../../../../test-utils/rollup-test-utils.js'; import { extractModules } from '../../../../src/input/extract/extractModules.js'; import { ScriptModuleTag } from '../../../../src/RollupPluginHTMLOptions.js'; diff --git a/packages/rollup-plugin-html/test/src/output/css.test.ts b/packages/rollup-plugin-html/test/src/output/css.test.ts new file mode 100644 index 0000000000..a8e2d2f349 --- /dev/null +++ b/packages/rollup-plugin-html/test/src/output/css.test.ts @@ -0,0 +1,91 @@ +import { expect } from 'chai'; +import { + createAssetPlaceholder, + replacePlaceholders, + calculateRelativePath, +} from '../../../src/output/css.js'; + +describe('cssAssetPlaceholders', () => { + describe('createAssetPlaceholder', () => { + it('creates a placeholder with the given reference ID', () => { + expect(createAssetPlaceholder('abc123')).to.equal('__ROLLUP_ASSET_URL_abc123__'); + }); + }); + + describe('replacePlaceholders', () => { + it('replaces placeholders with resolved paths', () => { + const css = `.foo { background: url('__ROLLUP_ASSET_URL_abc123__'); }`; + const resolver = (refId: string) => (refId === 'abc123' ? '../assets/image.png' : undefined); + const result = replacePlaceholders(css, resolver); + expect(result).to.equal(`.foo { background: url('../assets/image.png'); }`); + }); + + it('preserves fragments after placeholder', () => { + const css = `.foo { background: url('__ROLLUP_ASSET_URL_abc123__#icon'); }`; + const resolver = () => '../assets/sprite.svg'; + const result = replacePlaceholders(css, resolver); + expect(result).to.equal(`.foo { background: url('../assets/sprite.svg#icon'); }`); + }); + + it('preserves query strings after placeholder', () => { + const css = `.foo { src: url('__ROLLUP_ASSET_URL_abc123__?v=1.0'); }`; + const resolver = () => '../fonts/font.woff2'; + const result = replacePlaceholders(css, resolver); + expect(result).to.equal(`.foo { src: url('../fonts/font.woff2?v=1.0'); }`); + }); + + it('keeps placeholder when resolver returns undefined', () => { + const css = `.foo { background: url('__ROLLUP_ASSET_URL_unknown__'); }`; + const resolver = () => undefined; + const result = replacePlaceholders(css, resolver); + expect(result).to.equal(css); + }); + + it('replaces multiple placeholders', () => { + const css = ` + .a { background: url('__ROLLUP_ASSET_URL_img1__'); } + .b { background: url('__ROLLUP_ASSET_URL_img2__'); } + `; + const resolver = (refId: string) => { + if (refId === 'img1') return 'assets/image1.png'; + if (refId === 'img2') return 'assets/image2.png'; + return undefined; + }; + const result = replacePlaceholders(css, resolver); + expect(result).to.include("url('assets/image1.png')"); + expect(result).to.include("url('assets/image2.png')"); + }); + }); + + describe('calculateRelativePath', () => { + it('calculates relative path for same directory', () => { + expect(calculateRelativePath('styles/main.css', 'styles/image.png')).to.equal('image.png'); + }); + + it('calculates relative path for parent directory', () => { + expect(calculateRelativePath('styles/main.css', 'image.png')).to.equal('../image.png'); + }); + + it('calculates relative path for sibling directory', () => { + expect(calculateRelativePath('styles/main.css', 'assets/image.png')).to.equal( + '../assets/image.png', + ); + }); + + it('calculates relative path for deeply nested CSS', () => { + expect( + calculateRelativePath('styles/components/button.css', 'assets/icons/arrow.svg'), + ).to.equal('../../assets/icons/arrow.svg'); + }); + + it('calculates relative path when CSS is at root', () => { + expect(calculateRelativePath('main.css', 'assets/image.png')).to.equal('assets/image.png'); + }); + + it('calculates relative path when both are deeply nested', () => { + expect(calculateRelativePath('a/b/c/style.css', 'x/y/z/image.png')).to.equal( + '../../../x/y/z/image.png', + ); + }); + }); +}); diff --git a/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts b/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts index 024635e434..114ed9f1e1 100644 --- a/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts +++ b/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import path from 'path'; import { getOutputHTML, GetOutputHTMLParams } from '../../../src/output/getOutputHTML.js'; import { EntrypointBundle } from '../../../src/RollupPluginHTMLOptions.js'; -import { html } from '../../utils.js'; +import { html } from '../../../../../test-utils/rollup-test-utils.js'; describe('getOutputHTML()', () => { const defaultEntrypointBundles: Record = { diff --git a/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts b/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts index fb52cda0cf..6625159778 100644 --- a/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts +++ b/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts @@ -1,7 +1,7 @@ import { getTextContent } from '@web/parse5-utils'; import { expect } from 'chai'; import { parse, serialize } from 'parse5'; -import { html } from '../../utils.js'; +import { html } from '../../../../../test-utils/rollup-test-utils.js'; import { injectBundles, createLoadScript } from '../../../src/output/injectBundles.js'; diff --git a/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts b/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts index 9e720b1577..af07a155f9 100644 --- a/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts +++ b/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import path from 'path'; import { parse, serialize } from 'parse5'; -import { html } from '../../utils.js'; +import { html } from '../../../../../test-utils/rollup-test-utils.js'; import { InputData } from '../../../src/input/InputData.js'; import { injectedUpdatedAssetPaths } from '../../../src/output/injectedUpdatedAssetPaths.js'; diff --git a/packages/rollup-plugin-html/test/utils.ts b/packages/rollup-plugin-html/test/utils.ts deleted file mode 100644 index 14c060c5da..0000000000 --- a/packages/rollup-plugin-html/test/utils.ts +++ /dev/null @@ -1,106 +0,0 @@ -import synchronizedPrettier from '@prettier/sync'; -import fs from 'fs'; -import path from 'path'; -import * as prettier from 'prettier'; -import { OutputOptions, RollupBuild } from 'rollup'; - -function collapseWhitespaceAll(str: string) { - return ( - str && - str.replace(/[ \n\r\t\f\xA0]+/g, spaces => { - return spaces === '\t' ? '\t' : spaces.replace(/(^|\xA0+)[^\xA0]+/g, '$1 '); - }) - ); -} - -function format(str: string, parser: prettier.BuiltInParserName) { - return synchronizedPrettier.format(str, { parser, semi: true, singleQuote: true }); -} - -function merge(strings: TemplateStringsArray, ...values: string[]): string { - return strings.reduce((acc, str, i) => acc + str + (values[i] || ''), ''); -} - -const extnameToFormatter: Record string> = { - '.html': (str: string) => format(collapseWhitespaceAll(str), 'html'), - '.css': (str: string) => format(str, 'css'), - '.js': (str: string) => format(str, 'typescript'), - '.json': (str: string) => format(str, 'json'), - '.svg': (str: string) => format(collapseWhitespaceAll(str), 'html'), -}; - -function getFormatterFromFilename(name: string): undefined | ((str: string) => string) { - return extnameToFormatter[path.extname(name)]; -} - -export const html = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.html'](merge(strings, ...values)); - -export const css = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.css'](merge(strings, ...values)); - -export const js = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.js'](merge(strings, ...values)); - -export const svg = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.svg'](merge(strings, ...values)); - -export async function generateTestBundle(build: RollupBuild, outputConfig: OutputOptions) { - const { output } = await build.generate(outputConfig); - const chunks: Record = {}; - const assets: Record = {}; - const assetsUnformatted: Record = {}; - - for (const file of output) { - const filename = file.fileName; - const formatter = getFormatterFromFilename(filename); - if (file.type === 'chunk') { - chunks[filename] = formatter ? formatter(file.code) : file.code; - } else if (file.type === 'asset') { - let code = file.source; - let codeUnformatted = file.source; - if (typeof code !== 'string' && filename.endsWith('.css')) { - code = Buffer.from(code).toString('utf8'); - codeUnformatted = Buffer.from(codeUnformatted).toString('utf8'); - } - if (typeof code === 'string' && formatter) { - code = formatter(code); - } - assets[filename] = code; - assetsUnformatted[filename] = codeUnformatted; - } - } - - return { output, chunks, assets, assetsUnformatted }; -} - -export function createApp(structure: Record) { - const timestamp = Date.now(); - const rootDir = path.join(__dirname, `./.tmp/app-${timestamp}`); - if (!fs.existsSync(rootDir)) { - fs.mkdirSync(rootDir, { recursive: true }); - } - Object.keys(structure).forEach(filePath => { - const fullPath = path.join(rootDir, filePath); - const dir = path.dirname(fullPath); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - if (!fs.existsSync(fullPath)) { - const content = structure[filePath]; - const contentForWrite = - typeof content === 'object' && !(content instanceof Buffer) - ? JSON.stringify(content) - : content; - fs.writeFileSync(fullPath, contentForWrite); - } - }); - return rootDir; -} - -export function cleanApp() { - const tmpDir = path.join(__dirname, './.tmp'); - if (fs.existsSync(tmpDir)) { - fs.rmSync(tmpDir, { recursive: true }); - } -} diff --git a/packages/rollup-plugin-import-meta-assets/src/rollup-plugin-import-meta-assets.js b/packages/rollup-plugin-import-meta-assets/src/rollup-plugin-import-meta-assets.js index bd24c07eac..cef9ea126c 100644 --- a/packages/rollup-plugin-import-meta-assets/src/rollup-plugin-import-meta-assets.js +++ b/packages/rollup-plugin-import-meta-assets/src/rollup-plugin-import-meta-assets.js @@ -66,13 +66,50 @@ function getImportMetaUrlType(node) { * @param {string|string[]} [options.exclude] A picomatch pattern, or array of patterns, which specifies the files in the build the plugin should _ignore_. By default no files are ignored. * @param {boolean} [options.warnOnError] By default, the plugin quits the build process when it encounters an error. If you set this option to true, it will throw a warning instead and leave the code untouched. * @param {function} [options.transform] A function to transform assets. + * @param {boolean} [options.preserveDynamicStructure] When enabled, emits dynamic assets and rewrites the URL pattern to resolve the original dynamic path relative to the first emitted asset. Requires that the output preserves both filenames (no hashing) and directory structure from the dynamic expression onwards. * @return {import('rollup').Plugin} A Rollup Plugin */ -function importMetaAssets({ include, exclude, warnOnError, transform } = {}) { +function importMetaAssets({ + include, + exclude, + warnOnError, + transform, + preserveDynamicStructure, +} = {}) { const filter = createFilter(include, exclude); - return { + /** + * Read and optionally transform an asset file. + * Returns null if the path should be skipped. + * + * @param {object} context - Rollup plugin context (for warn/error) + * @param {string} absoluteAssetPath - Absolute path to the asset + * @param {number} errorPosition - Position in source for error reporting + * @returns {Promise} The asset contents, or null to skip + */ + async function readAndTransformAsset(context, absoluteAssetPath, errorPosition) { + try { + const assetContents = await fs.promises.readFile(absoluteAssetPath); + if (transform != null) { + return transform(assetContents, absoluteAssetPath); + } + return assetContents; + } catch (error) { + // Do not process directories, just skip + if (error.code !== 'EISDIR') { + if (warnOnError) { + context.warn(error, errorPosition); + } else { + context.error(error, errorPosition); + } + } + return null; + } + } + + return /* @type {import('rollup').Plugin} */ { name: 'rollup-plugin-import-meta-assets', + async transform(code, id) { if (!filter(id)) { return null; @@ -114,12 +151,105 @@ function importMetaAssets({ include, exclude, warnOnError, transform } = {}) { .sort() .map(r => (r.startsWith('./') || r.startsWith('../') ? r : `./${r}`)); - // create magic string if it wasn't created already - ms1 = ms1 || new MagicString(newCode); - // unpack variable dynamic url into a function with url statements per file, rollup - // will turn these into chunks automatically - ms1.prepend( - `function __variableDynamicURLRuntime${dynamicURLIndex}__(path) { + if (preserveDynamicStructure) { + // Rewrite the template literal to use relative path from first asset + // Algorithm: + // 1. Get static prefix from template + // 2. Strip static prefix from first matched file to get "variable part" + // 3. Count directory levels in variable part (excluding filename) + // 4. Prepend that many "../" to navigate from first asset to siblings + // + // Example: `./dynamic-assets/icons-${iconCategory}/${iconName}.svg` + // - Static prefix: `./dynamic-assets/icons-` + // - First matched file: `./dynamic-assets/icons-solid/alarm.svg` (defines relative path from the output chunk perspective automatically by rollup) + // - Variable part: `icons-solid/alarm.svg` + // - Directory levels: 1 (icons-solid/) + // - Output: `../icons-${iconCategory}/${iconName}.svg` (defines relative path from the first asset) + + const absoluteScriptDir = path.dirname(id); + let firstRef = null; + + for (const p of paths) { + const absoluteAssetPath = path.resolve(absoluteScriptDir, p); + const assetContents = await readAndTransformAsset( + this, + absoluteAssetPath, + node.arguments[0].start, + ); + if (assetContents === null) { + continue; + } + const ref = this.emitFile({ + type: 'asset', + name: path.basename(absoluteAssetPath), + originalFileName: absoluteAssetPath, + source: assetContents, + }); + if (firstRef === null) { + firstRef = ref; + } + } + + if (firstRef !== null && paths.length > 0) { + ms1 = ms1 || new MagicString(newCode); + + const templateLiteral = node.arguments[0]; + const quasis = templateLiteral.quasis; + const expressions = templateLiteral.expressions; + + // Get static prefix (everything before first expression) + const staticPrefix = quasis[0].value.cooked; + + // Split static prefix into directory and filename prefix at last slash + // e.g. "./dynamic-assets/icons-" -> dir: "./dynamic-assets/", filename prefix: "icons-" + const lastSlashInPrefix = staticPrefix.lastIndexOf('/'); + const staticDirPrefix = + lastSlashInPrefix >= 0 ? staticPrefix.substring(0, lastSlashInPrefix + 1) : ''; + const staticFilenamePrefix = + lastSlashInPrefix >= 0 + ? staticPrefix.substring(lastSlashInPrefix + 1) + : staticPrefix; + + // Get first matched file and strip the static directory prefix + const firstFile = paths[0]; + const variablePart = firstFile.startsWith(staticDirPrefix) + ? firstFile.slice(staticDirPrefix.length) + : firstFile; + + // Count directory levels in variable part (excluding filename) + const variableDir = variablePart.substring(0, variablePart.lastIndexOf('/') + 1); + const dirLevels = variableDir ? variableDir.split('/').filter(Boolean).length : 0; + + // Build prefix: "../" repeated dirLevels times, or "./" if no levels + // Then append any filename prefix from the original static portion + const relativePrefix = + (dirLevels > 0 ? '../'.repeat(dirLevels) : './') + staticFilenamePrefix; + + // Build the new template literal + let newTemplateParts = [relativePrefix]; + for (let i = 0; i < expressions.length; i++) { + const exprCode = newCode.substring(expressions[i].start, expressions[i].end); + newTemplateParts.push('${' + exprCode + '}'); + newTemplateParts.push(quasis[i + 1].value.raw); + } + const newTemplate = '`' + newTemplateParts.join('') + '`'; + + ms1.overwrite(templateLiteral.start, templateLiteral.end, newTemplate); + + ms1.overwrite( + node.arguments[1].start, + node.arguments[1].end, + `import.meta.ROLLUP_FILE_URL_${firstRef}`, + ); + } + } else { + // default behavior + // create magic string if it wasn't created already + ms1 = ms1 || new MagicString(newCode); + // unpack variable dynamic url into a function with url statements per file, rollup + // will turn these into chunks automatically + ms1.prepend( + `function __variableDynamicURLRuntime${dynamicURLIndex}__(path) { switch (path) { ${paths.map(p => ` case '${p}': return new URL('${p}', import.meta.url);`).join('\n')} ${` default: return new Promise(function(resolve, reject) { @@ -128,14 +258,15 @@ ${` default: return new Promise(function(resolve, reject) { ); })\n`} } }\n\n`, - ); - // call the runtime function instead of doing a dynamic url, the url specifier will - // be evaluated at runtime and the correct url will be returned by the injected function - ms1.overwrite( - node.start, - node.start + 7, - `__variableDynamicURLRuntime${dynamicURLIndex}__`, - ); + ); + // call the runtime function instead of doing a dynamic url, the url specifier will + // be evaluated at runtime and the correct url will be returned by the injected function + ms1.overwrite( + node.start, + node.start + 7, + `__variableDynamicURLRuntime${dynamicURLIndex}__`, + ); + } } catch (error) { if (error instanceof VariableURLError && warnOnError) { this.warn(error); @@ -167,37 +298,27 @@ ${` default: return new Promise(function(resolve, reject) { const absoluteAssetPath = path.resolve(absoluteScriptDir, relativeAssetPath); const assetName = path.basename(absoluteAssetPath); - try { - const assetContents = await fs.promises.readFile(absoluteAssetPath); - const transformedAssetContents = - transform != null - ? await transform(assetContents, absoluteAssetPath) - : assetContents; - if (transformedAssetContents === null) { - return; - } - const ref = this.emitFile({ - type: 'asset', - name: assetName, - originalFileName: absoluteAssetPath, - source: transformedAssetContents, - }); - ms2 = ms2 || new MagicString(newCode); - ms2.overwrite( - node.arguments[0].start, - node.arguments[1].end, - `import.meta.ROLLUP_FILE_URL_${ref}`, - ); - } catch (error) { - // Do not process directories, just skip - if (error.code !== 'EISDIR') { - if (warnOnError) { - this.warn(error, node.arguments[0].start); - } else { - this.error(error, node.arguments[0].start); - } - } + const assetContents = await readAndTransformAsset( + this, + absoluteAssetPath, + node.arguments[0].start, + ); + if (assetContents === null) { + return; } + + const ref = this.emitFile({ + type: 'asset', + name: assetName, + originalFileName: absoluteAssetPath, + source: assetContents, + }); + ms2 = ms2 || new MagicString(newCode); + ms2.overwrite( + node.arguments[0].start, + node.arguments[1].end, + `import.meta.ROLLUP_FILE_URL_${ref}`, + ); } }, }); diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/bad-url-entrypoint.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/bad-url-entrypoint.js deleted file mode 100644 index 065d65d6d1..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/bad-url-entrypoint.js +++ /dev/null @@ -1,3 +0,0 @@ -const badImage1 = new URL('/absolute-path.svg', import.meta.url).href; -const badImage2 = new URL('../../missing-relative-path.svg', import.meta.url).href; -console.log(badImage1, badImage2); diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/directories-and-simple-entrypoint.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/directories-and-simple-entrypoint.js deleted file mode 100644 index e7a18e1ea6..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/directories-and-simple-entrypoint.js +++ /dev/null @@ -1,20 +0,0 @@ -const justUrlObject = new URL('./one.svg', import.meta.url); -const href = new URL('./two.svg', import.meta.url).href; -const pathname = new URL('./three.svg', import.meta.url).pathname; -const searchParams = new URL('./four.svg', import.meta.url).searchParams; - -const directories = [ - new URL('./', import.meta.url), - new URL('./one', import.meta.url), - new URL('./one/', import.meta.url), - new URL('./one/two', import.meta.url), - new URL('./one/two/', import.meta.url), -]; - -console.log({ - justUrlObject, - href, - pathname, - searchParams, - directories, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/one.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/one.svg deleted file mode 100644 index dfe13a152f..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/one.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/three.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/three.svg deleted file mode 100644 index 9ebc1e4895..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/three.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/two.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/two.svg deleted file mode 100644 index 071582b8ff..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-assets/two.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-vars.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-vars.js deleted file mode 100644 index 4ccfdced0e..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/dynamic-vars.js +++ /dev/null @@ -1,8 +0,0 @@ -const names = ['one', 'two']; -// value of one could also be "two" or "three", bundler does not analyze the value itself -// Therefore, we expect both one.svg, two.svg and three.svg to be bundled, and this to turn into a switch statement -// with 3 cases (for all 3 assets in the dynamic-assets folder) -const dynamicImgs = names.map(n => new URL(`./dynamic-assets/${n}.svg`,import.meta.url)); -const backticksImg = new URL(`./dynamic-assets/three.svg`, import.meta.url); - -console.log(dynamicImgs, backticksImg); diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/five b/packages/rollup-plugin-import-meta-assets/test/fixtures/five deleted file mode 100644 index 6434d8dc54..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/five +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/four.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/four.svg deleted file mode 100644 index 86eb06cfd2..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/four.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/image.jpg b/packages/rollup-plugin-import-meta-assets/test/fixtures/image.jpg deleted file mode 100644 index b0f2052e045a4826ed450e1ed2849e6e40e925d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36287 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 JbAN9~|Jli_$}Ru^ diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/multi-level-entrypoint.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/multi-level-entrypoint.js deleted file mode 100644 index bf15f8c132..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/multi-level-entrypoint.js +++ /dev/null @@ -1,11 +0,0 @@ -import { imageOne, nameOne } from './one/one.js'; -import { imageTwo, nameTwo } from './one/two/two.js'; -import { imageThree, nameThree } from './one/two/three/three.js'; -import { imageFour, nameFour } from './one/two/three/four/four.js'; - -console.log({ - [nameOne]: imageOne, - [nameTwo]: imageTwo, - [nameThree]: imageThree, - [nameFour]: imageFour, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/one.svg deleted file mode 100644 index dfe13a152f..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/one-deep.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/one/one-deep.svg deleted file mode 100644 index dfe13a152f..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/one-deep.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/one.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/one/one.js deleted file mode 100644 index 4688ba51fa..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/one.js +++ /dev/null @@ -1,2 +0,0 @@ -export const nameOne = 'one-name'; -export const imageOne = new URL('../one.svg', import.meta.url).href; diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/different-asset-levels-entrypoint.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/different-asset-levels-entrypoint.js deleted file mode 100644 index 81dacfe5dc..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/different-asset-levels-entrypoint.js +++ /dev/null @@ -1,18 +0,0 @@ -const nameOne = 'one-name'; -const imageOne = new URL('../one-deep.svg', import.meta.url).href; - -const nameTwo = 'two-name'; -const imageTwo = new URL('./two-deep.svg', import.meta.url).href; - -const nameThree = 'three-name'; -const imageThree = new URL('./three/three-deep.svg', import.meta.url).href; - -const nameFour = 'four-name'; -const imageFour = new URL('./three/four/four-deep.svg', import.meta.url).href; - -console.log({ - [nameOne]: imageOne, - [nameTwo]: imageTwo, - [nameThree]: imageThree, - [nameFour]: imageFour, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/four-deep.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/four-deep.svg deleted file mode 100644 index 86eb06cfd2..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/four-deep.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/four.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/four.js deleted file mode 100644 index 56c5feb192..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/four.js +++ /dev/null @@ -1,2 +0,0 @@ -export const nameFour = 'four-name'; -export const imageFour = new URL('../../../../four.svg', import.meta.url).href; diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/multi-level-entrypoint-deep.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/multi-level-entrypoint-deep.js deleted file mode 100644 index a5cdec821c..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/four/multi-level-entrypoint-deep.js +++ /dev/null @@ -1,11 +0,0 @@ -import { imageOne, nameOne } from '../../../one.js'; -import { imageTwo, nameTwo } from '../../two.js'; -import { imageThree, nameThree } from '../three.js'; -import { imageFour, nameFour } from './four.js'; - -console.log({ - [nameOne]: imageOne, - [nameTwo]: imageTwo, - [nameThree]: imageThree, - [nameFour]: imageFour, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/three-deep.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/three-deep.svg deleted file mode 100644 index 9ebc1e4895..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/three-deep.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/three.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/three.js deleted file mode 100644 index fa3ec8b3c4..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/three/three.js +++ /dev/null @@ -1,2 +0,0 @@ -export const nameThree = 'three-name'; -export const imageThree = new URL('../../../three.svg', import.meta.url).href; diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/two-deep.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/two-deep.svg deleted file mode 100644 index 071582b8ff..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/two-deep.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/two.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/two.js deleted file mode 100644 index 11f10a4b41..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/one/two/two.js +++ /dev/null @@ -1,2 +0,0 @@ -export const nameTwo = 'two-name'; -export const imageTwo = new URL('../../two.svg', import.meta.url).href; diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/simple-entrypoint.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/simple-entrypoint.js deleted file mode 100644 index 55dba4b26b..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/simple-entrypoint.js +++ /dev/null @@ -1,13 +0,0 @@ -const justUrlObject = new URL('./one.svg', import.meta.url); -const href = new URL('./two.svg', import.meta.url).href; -const pathname = new URL('./three.svg', import.meta.url).pathname; -const searchParams = new URL('./four.svg', import.meta.url).searchParams; -const noExtension = new URL('./five', import.meta.url); - -console.log({ - justUrlObject, - href, - pathname, - searchParams, - noExtension, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/three.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/three.svg deleted file mode 100644 index 9ebc1e4895..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/three.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/transform-entrypoint.js b/packages/rollup-plugin-import-meta-assets/test/fixtures/transform-entrypoint.js deleted file mode 100644 index 313e3f8f9d..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/transform-entrypoint.js +++ /dev/null @@ -1,13 +0,0 @@ -const justUrlObject = new URL('./one.svg', import.meta.url); -const href = new URL('./two.svg', import.meta.url).href; -const pathname = new URL('./three.svg', import.meta.url).pathname; -const searchParams = new URL('./four.svg', import.meta.url).searchParams; -const someJpg = new URL('./image.jpg', import.meta.url); - -console.log({ - justUrlObject, - href, - pathname, - searchParams, - someJpg, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/fixtures/two.svg b/packages/rollup-plugin-import-meta-assets/test/fixtures/two.svg deleted file mode 100644 index 071582b8ff..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/fixtures/two.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/integration.test.js b/packages/rollup-plugin-import-meta-assets/test/integration.test.js index 88eb78b67e..f41023a1e2 100644 --- a/packages/rollup-plugin-import-meta-assets/test/integration.test.js +++ b/packages/rollup-plugin-import-meta-assets/test/integration.test.js @@ -1,34 +1,22 @@ -const fs = require('fs'); const path = require('path'); -const rollup = require('rollup'); +const { rollup } = require('rollup'); const { expect } = require('chai'); const hanbi = require('hanbi'); const { importMetaAssets } = require('../src/rollup-plugin-import-meta-assets.js'); +const { + js, + svg, + generateTestBundle, + createApp, + cleanApp, +} = require('../../../test-utils/rollup-test-utils.js'); const outputConfig = { format: 'es', dir: 'dist', }; -function expectChunk(output, shapshotUrl, chunkName, referencedFiles) { - const bundleJsSource = fs.readFileSync(path.join(__dirname, shapshotUrl)); - const bundleJs = output.find(({ fileName }) => fileName === chunkName); - expect(bundleJs.type).to.equal('chunk'); - expect(bundleJs.code).to.deep.equal(bundleJsSource.toString()); - expect(bundleJs.referencedFiles).to.deep.equal(referencedFiles); -} - -function expectAsset(output, snapshotUrl, assetName, distName) { - const snapshotSource = fs.readFileSync(path.join(__dirname, snapshotUrl)); - const asset = output.find(({ name }) => name === assetName); - expect(typeof asset).to.equal('object'); - expect(asset.type).to.equal('asset'); - expect(asset.source.toString()).to.equal(snapshotSource.toString()); - expect(asset.fileName).to.equal(distName); - return distName; -} - describe('rollup-plugin-import-meta-assets', () => { let consoleStub; @@ -38,30 +26,119 @@ describe('rollup-plugin-import-meta-assets', () => { afterEach(() => { hanbi.restore(); + cleanApp(); }); it("simple bundle with different new URL('', import.meta.url)", async () => { + const rootDir = createApp({ + 'app.js': js` + const justUrlObject = new URL('./one.svg', import.meta.url); + const href = new URL('./two.svg', import.meta.url).href; + const pathname = new URL('./three.svg', import.meta.url).pathname; + const searchParams = new URL('./four.svg', import.meta.url).searchParams; + const noExtension = new URL('./five', import.meta.url); + + console.log({ + justUrlObject, + href, + pathname, + searchParams, + noExtension, + }); + `, + 'one.svg': svg``, + 'two.svg': svg``, + 'three.svg': svg``, + 'four.svg': svg``, + five: 'five', + }); + const config = { - input: { 'simple-bundle': require.resolve('./fixtures/simple-entrypoint.js') }, + input: { app: path.join(rootDir, 'app.js') }, plugins: [importMetaAssets()], }; - const bundle = await rollup.rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(5); + + expect(chunks['app.js']).to.equal(js` + const justUrlObject = new URL( + new URL('assets/one-BCCvKrTe.svg', import.meta.url).href + ); + const href = new URL(new URL('assets/two-C4stzVZW.svg', import.meta.url).href) + .href; + const pathname = new URL( + new URL('assets/three-DPeYetg3.svg', import.meta.url).href + ).pathname; + const searchParams = new URL( + new URL('assets/four-2QgOKKkO.svg', import.meta.url).href + ).searchParams; + const noExtension = new URL( + new URL('assets/five-DeBsXz7d', import.meta.url).href + ); + + console.log({ + justUrlObject, + href, + pathname, + searchParams, + noExtension, + }); + `); - expect(output.length).to.equal(6); - expectChunk(output, 'snapshots/simple-bundle.js', 'simple-bundle.js', [ - expectAsset(output, 'snapshots/one.svg', 'one.svg', 'assets/one-Bkie7h0E.svg'), - expectAsset(output, 'snapshots/two.svg', 'two.svg', 'assets/two-D7JyS-th.svg'), - expectAsset(output, 'snapshots/three.svg', 'three.svg', 'assets/three-IN2CmsMK.svg'), - expectAsset(output, 'snapshots/four.svg', 'four.svg', 'assets/four-CUlW6cvD.svg'), - expectAsset(output, 'snapshots/five', 'five', 'assets/five-Bnvj_R70'), + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal([ + 'assets/one-BCCvKrTe.svg', + 'assets/two-C4stzVZW.svg', + 'assets/three-DPeYetg3.svg', + 'assets/four-2QgOKKkO.svg', + 'assets/five-DeBsXz7d', ]); + + expect(assets['assets/one-BCCvKrTe.svg']).to.equal( + svg``, + ); + expect(assets['assets/two-C4stzVZW.svg']).to.equal( + svg``, + ); + expect(assets['assets/three-DPeYetg3.svg']).to.equal( + svg``, + ); + expect(assets['assets/four-2QgOKKkO.svg']).to.equal( + svg``, + ); + expect(assets['assets/five-DeBsXz7d']).to.equal('five'); }); it('simple bundle with transform assets', async () => { + const rootDir = createApp({ + 'app.js': js` + const justUrlObject = new URL('./one.svg', import.meta.url); + const href = new URL('./two.svg', import.meta.url).href; + const pathname = new URL('./three.svg', import.meta.url).pathname; + const searchParams = new URL('./four.svg', import.meta.url).searchParams; + const someJpg = new URL('./image.jpg', import.meta.url); + + console.log({ + justUrlObject, + href, + pathname, + searchParams, + someJpg, + }); + `, + 'one.svg': svg``, + 'two.svg': svg``, + 'three.svg': svg``, + 'four.svg': svg``, + 'image.jpg': 'image.jpg', + }); + const config = { - input: { 'transform-bundle': require.resolve('./fixtures/transform-entrypoint.js') }, + input: { app: path.join(rootDir, 'app.js') }, plugins: [ importMetaAssets({ transform: async (assetBuffer, assetPath) => { @@ -75,22 +152,87 @@ describe('rollup-plugin-import-meta-assets', () => { ], }; - const bundle = await rollup.rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(5); + + expect(chunks['app.js']).to.equal(js` + const justUrlObject = new URL( + new URL('assets/one-QPKGlwhS.svg', import.meta.url).href + ); + const href = new URL(new URL('assets/two-T4ecKj7d.svg', import.meta.url).href) + .href; + const pathname = new URL( + new URL('assets/three-LuNZrcLX.svg', import.meta.url).href + ).pathname; + const searchParams = new URL( + new URL('assets/four-Cf59sBI1.svg', import.meta.url).href + ).searchParams; + const someJpg = new URL( + new URL('assets/image-B360jR14.jpg', import.meta.url).href + ); - expect(output.length).to.equal(6); - expectChunk(output, 'snapshots/transform-bundle.js', 'transform-bundle.js', [ - expectAsset(output, 'snapshots/one.min.svg', 'one.svg', 'assets/one--RhQWA3U.svg'), - expectAsset(output, 'snapshots/two.min.svg', 'two.svg', 'assets/two-CZdxIUwi.svg'), - expectAsset(output, 'snapshots/three.min.svg', 'three.svg', 'assets/three-tFhyRH_R.svg'), - expectAsset(output, 'snapshots/four.min.svg', 'four.svg', 'assets/four-Cs1OId-q.svg'), - expectAsset(output, 'snapshots/image.jpg', 'image.jpg', 'assets/image-C92N8yPj.jpg'), + console.log({ + justUrlObject, + href, + pathname, + searchParams, + someJpg, + }); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal([ + 'assets/one-QPKGlwhS.svg', + 'assets/two-T4ecKj7d.svg', + 'assets/three-LuNZrcLX.svg', + 'assets/four-Cf59sBI1.svg', + 'assets/image-B360jR14.jpg', ]); + + expect(assets['assets/one-QPKGlwhS.svg']).to.equal( + svg`${svg``}`, + ); + expect(assets['assets/two-T4ecKj7d.svg']).to.equal( + svg`${svg``}`, + ); + expect(assets['assets/three-LuNZrcLX.svg']).to.equal( + svg`${svg``}`, + ); + expect(assets['assets/four-Cf59sBI1.svg']).to.equal( + svg`${svg``}`, + ); + expect(assets['assets/image-B360jR14.jpg']).to.equal('image.jpg'); }); it('simple bundle with ignored assets', async () => { + const rootDir = createApp({ + 'app.js': js` + const justUrlObject = new URL('./one.svg', import.meta.url); + const href = new URL('./two.svg', import.meta.url).href; + const pathname = new URL('./three.svg', import.meta.url).pathname; + const searchParams = new URL('./four.svg', import.meta.url).searchParams; + const someJpg = new URL('./image.jpg', import.meta.url); + + console.log({ + justUrlObject, + href, + pathname, + searchParams, + someJpg, + }); + `, + 'one.svg': svg``, + 'two.svg': svg``, + 'three.svg': svg``, + 'four.svg': svg``, + 'image.jpg': 'image.jpg', + }); + const config = { - input: { 'transform-bundle-ignored': require.resolve('./fixtures/transform-entrypoint.js') }, + input: { app: path.join(rootDir, 'app.js') }, plugins: [ importMetaAssets({ transform: async (assetBuffer, assetPath) => { @@ -104,97 +246,479 @@ describe('rollup-plugin-import-meta-assets', () => { ], }; - const bundle = await rollup.rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); - expect(output.length).to.equal(5); - expectChunk(output, 'snapshots/transform-bundle-ignored.js', 'transform-bundle-ignored.js', [ - expectAsset(output, 'snapshots/one.min.svg', 'one.svg', 'assets/one--RhQWA3U.svg'), - expectAsset(output, 'snapshots/two.min.svg', 'two.svg', 'assets/two-CZdxIUwi.svg'), - expectAsset(output, 'snapshots/three.min.svg', 'three.svg', 'assets/three-tFhyRH_R.svg'), - expectAsset(output, 'snapshots/four.min.svg', 'four.svg', 'assets/four-Cs1OId-q.svg'), + // image.jpg is NOT transformed, so it keeps original URL + expect(chunks['app.js']).to.equal(js` + const justUrlObject = new URL( + new URL('assets/one-QPKGlwhS.svg', import.meta.url).href + ); + const href = new URL(new URL('assets/two-T4ecKj7d.svg', import.meta.url).href) + .href; + const pathname = new URL( + new URL('assets/three-LuNZrcLX.svg', import.meta.url).href + ).pathname; + const searchParams = new URL( + new URL('assets/four-Cf59sBI1.svg', import.meta.url).href + ).searchParams; + const someJpg = new URL('./image.jpg', import.meta.url); + + console.log({ + justUrlObject, + href, + pathname, + searchParams, + someJpg, + }); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal([ + 'assets/one-QPKGlwhS.svg', + 'assets/two-T4ecKj7d.svg', + 'assets/three-LuNZrcLX.svg', + 'assets/four-Cf59sBI1.svg', ]); + + expect(assets['assets/one-QPKGlwhS.svg']).to.equal( + svg`${svg``}`, + ); + expect(assets['assets/two-T4ecKj7d.svg']).to.equal( + svg`${svg``}`, + ); + expect(assets['assets/three-LuNZrcLX.svg']).to.equal( + svg`${svg``}`, + ); + expect(assets['assets/four-Cf59sBI1.svg']).to.equal( + svg`${svg``}`, + ); }); it('multiple level bundle (downward modules)', async () => { + const rootDir = createApp({ + 'app.js': js` + import { imageOne, nameOne } from './one/one.js'; + import { imageTwo, nameTwo } from './one/two/two.js'; + import { imageThree, nameThree } from './one/two/three/three.js'; + import { imageFour, nameFour } from './one/two/three/four/four.js'; + + console.log({ + [nameOne]: imageOne, + [nameTwo]: imageTwo, + [nameThree]: imageThree, + [nameFour]: imageFour, + }); + `, + 'one/one.js': js` + export const nameOne = 'one-name'; + export const imageOne = new URL('../one.svg', import.meta.url).href; + `, + 'one/two/two.js': js` + export const nameTwo = 'two-name'; + export const imageTwo = new URL('../../two.svg', import.meta.url).href; + `, + 'one/two/three/three.js': js` + export const nameThree = 'three-name'; + export const imageThree = new URL('../../../three.svg', import.meta.url).href; + `, + 'one/two/three/four/four.js': js` + export const nameFour = 'four-name'; + export const imageFour = new URL('../../../../four.svg', import.meta.url).href; + `, + 'one.svg': svg``, + 'two.svg': svg``, + 'three.svg': svg``, + 'four.svg': svg``, + }); + const config = { - input: { 'multi-level-bundle': require.resolve('./fixtures/multi-level-entrypoint.js') }, + input: { app: path.join(rootDir, 'app.js') }, plugins: [importMetaAssets()], }; - const bundle = await rollup.rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(chunks['app.js']).to.equal(js` + const nameOne = 'one-name'; + const imageOne = new URL( + new URL('assets/one-BCCvKrTe.svg', import.meta.url).href + ).href; + + const nameTwo = 'two-name'; + const imageTwo = new URL( + new URL('assets/two-C4stzVZW.svg', import.meta.url).href + ).href; - expect(output.length).to.equal(5); - expectChunk(output, 'snapshots/multi-level-bundle.js', 'multi-level-bundle.js', [ - expectAsset(output, 'snapshots/one.svg', 'one.svg', 'assets/one-Bkie7h0E.svg'), - expectAsset(output, 'snapshots/two.svg', 'two.svg', 'assets/two-D7JyS-th.svg'), - expectAsset(output, 'snapshots/three.svg', 'three.svg', 'assets/three-IN2CmsMK.svg'), - expectAsset(output, 'snapshots/four.svg', 'four.svg', 'assets/four-CUlW6cvD.svg'), + const nameThree = 'three-name'; + const imageThree = new URL( + new URL('assets/three-DPeYetg3.svg', import.meta.url).href + ).href; + + const nameFour = 'four-name'; + const imageFour = new URL( + new URL('assets/four-2QgOKKkO.svg', import.meta.url).href + ).href; + + console.log({ + [nameOne]: imageOne, + [nameTwo]: imageTwo, + [nameThree]: imageThree, + [nameFour]: imageFour, + }); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal([ + 'assets/one-BCCvKrTe.svg', + 'assets/two-C4stzVZW.svg', + 'assets/three-DPeYetg3.svg', + 'assets/four-2QgOKKkO.svg', ]); + + expect(assets['assets/one-BCCvKrTe.svg']).to.equal( + svg``, + ); + expect(assets['assets/two-C4stzVZW.svg']).to.equal( + svg``, + ); + expect(assets['assets/three-DPeYetg3.svg']).to.equal( + svg``, + ); + expect(assets['assets/four-2QgOKKkO.svg']).to.equal( + svg``, + ); }); it('multiple level bundle (upward modules)', async () => { + const rootDir = createApp({ + 'one/two/three/four/app.js': js` + import { imageOne, nameOne } from '../../../one.js'; + import { imageTwo, nameTwo } from '../../two.js'; + import { imageThree, nameThree } from '../three.js'; + import { imageFour, nameFour } from './four.js'; + + console.log({ + [nameOne]: imageOne, + [nameTwo]: imageTwo, + [nameThree]: imageThree, + [nameFour]: imageFour, + }); + `, + 'one/one.js': js` + export const nameOne = 'one-name'; + export const imageOne = new URL('../one.svg', import.meta.url).href; + `, + 'one/two/two.js': js` + export const nameTwo = 'two-name'; + export const imageTwo = new URL('../../two.svg', import.meta.url).href; + `, + 'one/two/three/three.js': js` + export const nameThree = 'three-name'; + export const imageThree = new URL('../../../three.svg', import.meta.url).href; + `, + 'one/two/three/four/four.js': js` + export const nameFour = 'four-name'; + export const imageFour = new URL('../../../../four.svg', import.meta.url).href; + `, + 'one.svg': svg``, + 'two.svg': svg``, + 'three.svg': svg``, + 'four.svg': svg``, + }); + const config = { input: { - 'multi-level-bundle': require.resolve( - './fixtures/one/two/three/four/multi-level-entrypoint-deep.js', - ), + app: path.join(rootDir, 'one/two/three/four/app.js'), }, plugins: [importMetaAssets()], }; - const bundle = await rollup.rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); - expect(output.length).to.equal(5); - expectChunk(output, 'snapshots/multi-level-bundle.js', 'multi-level-bundle.js', [ - expectAsset(output, 'snapshots/one.svg', 'one.svg', 'assets/one-Bkie7h0E.svg'), - expectAsset(output, 'snapshots/two.svg', 'two.svg', 'assets/two-D7JyS-th.svg'), - expectAsset(output, 'snapshots/three.svg', 'three.svg', 'assets/three-IN2CmsMK.svg'), - expectAsset(output, 'snapshots/four.svg', 'four.svg', 'assets/four-CUlW6cvD.svg'), + expect(chunks['app.js']).to.equal(js` + const nameOne = 'one-name'; + const imageOne = new URL( + new URL('assets/one-BCCvKrTe.svg', import.meta.url).href + ).href; + + const nameTwo = 'two-name'; + const imageTwo = new URL( + new URL('assets/two-C4stzVZW.svg', import.meta.url).href + ).href; + + const nameThree = 'three-name'; + const imageThree = new URL( + new URL('assets/three-DPeYetg3.svg', import.meta.url).href + ).href; + + const nameFour = 'four-name'; + const imageFour = new URL( + new URL('assets/four-2QgOKKkO.svg', import.meta.url).href + ).href; + + console.log({ + [nameOne]: imageOne, + [nameTwo]: imageTwo, + [nameThree]: imageThree, + [nameFour]: imageFour, + }); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal([ + 'assets/one-BCCvKrTe.svg', + 'assets/two-C4stzVZW.svg', + 'assets/three-DPeYetg3.svg', + 'assets/four-2QgOKKkO.svg', ]); + + expect(assets['assets/one-BCCvKrTe.svg']).to.equal( + svg``, + ); + expect(assets['assets/two-C4stzVZW.svg']).to.equal( + svg``, + ); + expect(assets['assets/three-DPeYetg3.svg']).to.equal( + svg``, + ); + expect(assets['assets/four-2QgOKKkO.svg']).to.equal( + svg``, + ); }); it('different asset levels', async () => { + const rootDir = createApp({ + 'one/two/app.js': js` + const nameOne = 'one-name'; + const imageOne = new URL('../one-deep.svg', import.meta.url).href; + + const nameTwo = 'two-name'; + const imageTwo = new URL('./two-deep.svg', import.meta.url).href; + + const nameThree = 'three-name'; + const imageThree = new URL('./three/three-deep.svg', import.meta.url).href; + + const nameFour = 'four-name'; + const imageFour = new URL('./three/four/four-deep.svg', import.meta.url).href; + + console.log({ + [nameOne]: imageOne, + [nameTwo]: imageTwo, + [nameThree]: imageThree, + [nameFour]: imageFour, + }); + `, + 'one/one-deep.svg': svg``, + 'one/two/two-deep.svg': svg``, + 'one/two/three/three-deep.svg': svg``, + 'one/two/three/four/four-deep.svg': svg``, + }); + const config = { input: { - 'different-asset-levels-bundle': require.resolve( - './fixtures/one/two/different-asset-levels-entrypoint.js', - ), + app: path.join(rootDir, 'one/two/app.js'), }, plugins: [importMetaAssets()], }; - const bundle = await rollup.rollup(config); - const { output } = await bundle.generate(outputConfig); - - expect(output.length).to.equal(5); - expectChunk( - output, - 'snapshots/different-asset-levels-bundle.js', - 'different-asset-levels-bundle.js', - [ - expectAsset(output, 'snapshots/one.svg', 'one-deep.svg', 'assets/one-deep-Bkie7h0E.svg'), - expectAsset(output, 'snapshots/two.svg', 'two-deep.svg', 'assets/two-deep-D7JyS-th.svg'), - expectAsset( - output, - 'snapshots/three.svg', - 'three-deep.svg', - 'assets/three-deep-IN2CmsMK.svg', - ), - expectAsset(output, 'snapshots/four.svg', 'four-deep.svg', 'assets/four-deep-CUlW6cvD.svg'), - ], + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(chunks['app.js']).to.equal(js` + const nameOne = 'one-name'; + const imageOne = new URL( + new URL('assets/one-deep-BCCvKrTe.svg', import.meta.url).href + ).href; + + const nameTwo = 'two-name'; + const imageTwo = new URL( + new URL('assets/two-deep-C4stzVZW.svg', import.meta.url).href + ).href; + + const nameThree = 'three-name'; + const imageThree = new URL( + new URL('assets/three-deep-DPeYetg3.svg', import.meta.url).href + ).href; + + const nameFour = 'four-name'; + const imageFour = new URL( + new URL('assets/four-deep-2QgOKKkO.svg', import.meta.url).href + ).href; + + console.log({ + [nameOne]: imageOne, + [nameTwo]: imageTwo, + [nameThree]: imageThree, + [nameFour]: imageFour, + }); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal([ + 'assets/one-deep-BCCvKrTe.svg', + 'assets/two-deep-C4stzVZW.svg', + 'assets/three-deep-DPeYetg3.svg', + 'assets/four-deep-2QgOKKkO.svg', + ]); + + expect(assets['assets/one-deep-BCCvKrTe.svg']).to.equal( + svg``, + ); + expect(assets['assets/two-deep-C4stzVZW.svg']).to.equal( + svg``, + ); + expect(assets['assets/three-deep-DPeYetg3.svg']).to.equal( + svg``, + ); + expect(assets['assets/four-deep-2QgOKKkO.svg']).to.equal( + svg``, + ); + }); + + it('different asset levels with static paths and preserved output structure', async () => { + const rootDir = createApp({ + 'one/two/app.js': js` + const nameOne = 'one-name'; + const imageOne = new URL('../one-deep.svg', import.meta.url).href; + + const nameTwo = 'two-name'; + const imageTwo = new URL('./two-deep.svg', import.meta.url).href; + + const nameThree = 'three-name'; + const imageThree = new URL('./three/three-deep.svg', import.meta.url).href; + + const nameFour = 'four-name'; + const imageFour = new URL('./three/four/four-deep.svg', import.meta.url).href; + + console.log({ + [nameOne]: imageOne, + [nameTwo]: imageTwo, + [nameThree]: imageThree, + [nameFour]: imageFour, + }); + `, + 'one/one-deep.svg': svg``, + 'one/two/two-deep.svg': svg``, + 'one/two/three/three-deep.svg': svg``, + 'one/two/three/four/four-deep.svg': svg``, + }); + + const config = { + input: { + app: path.join(rootDir, 'one/two/app.js'), + }, + plugins: [importMetaAssets()], + }; + + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: asset => + path.relative(rootDir, asset.originalFileNames[0]).split(path.sep).join('/'), + }); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(chunks['app.js']).to.equal(js` + const nameOne = 'one-name'; + const imageOne = new URL( + new URL('one/one-deep.svg', import.meta.url).href + ).href; + + const nameTwo = 'two-name'; + const imageTwo = new URL( + new URL('one/two/two-deep.svg', import.meta.url).href + ).href; + + const nameThree = 'three-name'; + const imageThree = new URL( + new URL('one/two/three/three-deep.svg', import.meta.url).href + ).href; + + const nameFour = 'four-name'; + const imageFour = new URL( + new URL( + 'one/two/three/four/four-deep.svg', + import.meta.url + ).href + ).href; + + console.log({ + [nameOne]: imageOne, + [nameTwo]: imageTwo, + [nameThree]: imageThree, + [nameFour]: imageFour, + }); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal([ + 'one/one-deep.svg', + 'one/two/two-deep.svg', + 'one/two/three/three-deep.svg', + 'one/two/three/four/four-deep.svg', + ]); + + expect(assets['one/one-deep.svg']).to.equal( + svg``, + ); + expect(assets['one/two/two-deep.svg']).to.equal( + svg``, + ); + expect(assets['one/two/three/three-deep.svg']).to.equal( + svg``, + ); + expect(assets['one/two/three/four/four-deep.svg']).to.equal( + svg``, ); }); it('include/exclude options', async () => { + const rootDir = createApp({ + 'one/one.js': js` + export const nameOne = 'one-name'; + export const imageOne = new URL('../one.svg', import.meta.url).href; + `, + 'one/two/two.js': js` + export const nameTwo = 'two-name'; + export const imageTwo = new URL('../../two.svg', import.meta.url).href; + `, + 'one/two/three/three.js': js` + export const nameThree = 'three-name'; + export const imageThree = new URL('../../../three.svg', import.meta.url).href; + `, + 'one/two/three/four/four.js': js` + export const nameFour = 'four-name'; + export const imageFour = new URL('../../../../four.svg', import.meta.url).href; + `, + 'one.svg': svg``, + 'two.svg': svg``, + 'three.svg': svg``, + 'four.svg': svg``, + }); + const config = { input: { - 'one-bundle': require.resolve('./fixtures/one/one.js'), - 'two-bundle': require.resolve('./fixtures/one/two/two.js'), - 'three-bundle': require.resolve('./fixtures/one/two/three/three.js'), - 'four-bundle': require.resolve('./fixtures/one/two/three/four/four.js'), + one: path.join(rootDir, 'one/one.js'), + two: path.join(rootDir, 'one/two/two.js'), + three: path.join(rootDir, 'one/two/three/three.js'), + four: path.join(rootDir, 'one/two/three/four/four.js'), }, plugins: [ importMetaAssets({ @@ -207,32 +731,81 @@ describe('rollup-plugin-import-meta-assets', () => { ], }; - const bundle = await rollup.rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, outputConfig); - // 4 ES modules + 2 assets - expect(output.length).to.equal(6); - expectChunk(output, 'snapshots/one-bundle.js', 'one-bundle.js', []); - expectChunk(output, 'snapshots/two-bundle.js', 'two-bundle.js', []); - expectChunk(output, 'snapshots/three-bundle.js', 'three-bundle.js', [ - expectAsset(output, 'snapshots/three.svg', 'three.svg', 'assets/three-IN2CmsMK.svg'), - ]); - expectChunk(output, 'snapshots/four-bundle.js', 'four-bundle.js', [ - expectAsset(output, 'snapshots/four.svg', 'four.svg', 'assets/four-CUlW6cvD.svg'), - ]); + expect(Object.keys(chunks)).to.have.lengthOf(4); + expect(Object.keys(assets)).to.have.lengthOf(2); + + // one and two keep original URLs (excluded) + expect(chunks['one.js']).to.equal(js` + const nameOne = 'one-name'; + const imageOne = new URL('../one.svg', import.meta.url).href; + + export { imageOne, nameOne }; + `); + expect(chunks['two.js']).to.equal(js` + const nameTwo = 'two-name'; + const imageTwo = new URL('../../two.svg', import.meta.url).href; + + export { imageTwo, nameTwo }; + `); + + // three and four have transformed URLs (included) + expect(chunks['three.js']).to.equal(js` + const nameThree = 'three-name'; + const imageThree = new URL( + new URL('assets/three-DPeYetg3.svg', import.meta.url).href + ).href; + + export { imageThree, nameThree }; + `); + expect(chunks['four.js']).to.equal(js` + const nameFour = 'four-name'; + const imageFour = new URL( + new URL('assets/four-2QgOKKkO.svg', import.meta.url).href + ).href; + + export { imageFour, nameFour }; + `); + + expect(assets['assets/three-DPeYetg3.svg']).to.equal( + svg``, + ); + expect(assets['assets/four-2QgOKKkO.svg']).to.equal( + svg``, + ); + + const oneChunk = output.find(({ fileName }) => fileName === 'one.js'); + const twoChunk = output.find(({ fileName }) => fileName === 'two.js'); + const threeChunk = output.find(({ fileName }) => fileName === 'three.js'); + const fourChunk = output.find(({ fileName }) => fileName === 'four.js'); + + expect(oneChunk.referencedFiles).to.deep.equal([]); + expect(twoChunk.referencedFiles).to.deep.equal([]); + expect(threeChunk.referencedFiles).to.deep.equal(['assets/three-DPeYetg3.svg']); + expect(fourChunk.referencedFiles).to.deep.equal(['assets/four-2QgOKKkO.svg']); }); it('bad URL example', async () => { + const rootDir = createApp({ + 'app.js': js` + const badImage1 = new URL('/absolute-path.svg', import.meta.url).href; + const badImage2 = new URL('../../missing-relative-path.svg', import.meta.url).href; + console.log(badImage1, badImage2); + `, + }); + const config = { - input: { 'bad-url-bundle': require.resolve('./fixtures/bad-url-entrypoint.js') }, + input: { app: path.join(rootDir, 'app.js') }, plugins: [importMetaAssets()], }; let error; try { - const bundle = await rollup.rollup(config); - await bundle.generate(outputConfig); + const build = await rollup(config); + await build.generate(outputConfig); } catch (e) { error = e; } @@ -241,13 +814,21 @@ describe('rollup-plugin-import-meta-assets', () => { }); it('bad URL example with warnOnError: true', async () => { + const rootDir = createApp({ + 'app.js': js` + const badImage1 = new URL('/absolute-path.svg', import.meta.url).href; + const badImage2 = new URL('../../missing-relative-path.svg', import.meta.url).href; + console.log(badImage1, badImage2); + `, + }); + const config = { - input: { 'bad-url-bundle': require.resolve('./fixtures/bad-url-entrypoint.js') }, + input: { app: path.join(rootDir, 'app.js') }, plugins: [importMetaAssets({ warnOnError: true })], }; - const bundle = await rollup.rollup(config); - await bundle.generate(outputConfig); + const build = await rollup(config); + await build.generate(outputConfig); expect(consoleStub.callCount).to.equal(2); expect(consoleStub.getCall(0).args[0]).to.match( @@ -258,40 +839,407 @@ describe('rollup-plugin-import-meta-assets', () => { ); }); - it('allows backtics and dynamic vars in path', async () => { + it('allows backtics in path', async () => { + const rootDir = createApp({ + 'app.js': + 'const backticksImg = new URL(`./one/one-deep.svg`, import.meta.url);\n\nconsole.log(backticksImg);\n', + 'one/one-deep.svg': svg``, + }); + const config = { - input: { 'dynamic-vars': require.resolve('./fixtures/dynamic-vars.js') }, + input: { app: path.join(rootDir, 'app.js') }, plugins: [importMetaAssets({ warnOnError: true })], }; - const bundle = await rollup.rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, outputConfig); - expect(output.length).to.equal(4); - expectChunk(output, 'snapshots/dynamic-vars.js', 'dynamic-vars.js', [ - expectAsset(output, 'snapshots/three.svg', 'three.svg', 'assets/three-IN2CmsMK.svg'), - expectAsset(output, 'snapshots/two.svg', 'two.svg', 'assets/two-D7JyS-th.svg'), - expectAsset(output, 'snapshots/one.svg', 'one.svg', 'assets/one-Bkie7h0E.svg'), + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['app.js']).to.equal(js` + const backticksImg = new URL( + new URL('assets/one-deep-BCCvKrTe.svg', import.meta.url).href + ); + + console.log(backticksImg); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal(['assets/one-deep-BCCvKrTe.svg']); + + expect(assets['assets/one-deep-BCCvKrTe.svg']).to.equal( + svg``, + ); + }); + + it('allows dynamic vars with a concatenated file name', async () => { + const rootDir = createApp({ + // actual names don't matter, we glob and include everything always + 'app.js': js` + const images = ['one', 'two']; + + const paths = images.map((name) => + new URL(\`./assets/images/image-\${name}.svg\`, import.meta.url), + ); + + console.log(paths); + `, + 'assets/images/image-one.svg': svg``, + 'assets/images/image-two.svg': svg``, + 'assets/images/image-three.svg': svg``, + }); + + const config = { + input: { app: path.join(rootDir, 'app.js') }, + plugins: [importMetaAssets({ warnOnError: true })], + }; + + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(chunks['app.js']).to.equal(js` + function __variableDynamicURLRuntime0__(path) { + switch (path) { + case './assets/images/image-one.svg': + return new URL( + new URL('assets/image-one-BCCvKrTe.svg', import.meta.url).href + ); + case './assets/images/image-three.svg': + return new URL( + new URL('assets/image-three-DPeYetg3.svg', import.meta.url).href + ); + case './assets/images/image-two.svg': + return new URL( + new URL('assets/image-two-C4stzVZW.svg', import.meta.url).href + ); + default: + return new Promise(function (resolve, reject) { + (typeof queueMicrotask === 'function' + ? queueMicrotask + : setTimeout)(reject.bind(null, new Error('Unknown variable dynamic new URL statement: ' + path))); + }); + } + } + + const images = ['one', 'two']; + + const paths = images.map((name) => __variableDynamicURLRuntime0__(\`./assets/images/image-\${name}.svg\`)); + + console.log(paths); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal([ + 'assets/image-two-C4stzVZW.svg', + 'assets/image-three-DPeYetg3.svg', + 'assets/image-one-BCCvKrTe.svg', ]); + + expect(assets['assets/image-one-BCCvKrTe.svg']).to.equal( + svg``, + ); + expect(assets['assets/image-two-C4stzVZW.svg']).to.equal( + svg``, + ); + expect(assets['assets/image-three-DPeYetg3.svg']).to.equal( + svg``, + ); }); it('ignores patterns that reference a directory', async () => { + const rootDir = createApp({ + 'app.js': js` + const justUrlObject = new URL('./one.svg', import.meta.url); + const href = new URL('./two.svg', import.meta.url).href; + const pathname = new URL('./three.svg', import.meta.url).pathname; + const searchParams = new URL('./four.svg', import.meta.url).searchParams; + + const directories = [ + new URL('./', import.meta.url), + new URL('./one', import.meta.url), + new URL('./one/', import.meta.url), + new URL('./one/two', import.meta.url), + new URL('./one/two/', import.meta.url), + ]; + + console.log({ + justUrlObject, + href, + pathname, + searchParams, + directories, + }); + `, + 'one.svg': svg``, + 'two.svg': svg``, + 'three.svg': svg``, + 'four.svg': svg``, + 'one/': '', // create directory + 'one/two/': '', // create directory + }); + const config = { input: { - 'directories-ignored': require.resolve('./fixtures/directories-and-simple-entrypoint.js'), + app: path.join(rootDir, 'app.js'), }, plugins: [importMetaAssets()], }; - const bundle = await rollup.rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(chunks['app.js']).to.equal(js` + const justUrlObject = new URL( + new URL('assets/one-BCCvKrTe.svg', import.meta.url).href + ); + const href = new URL(new URL('assets/two-C4stzVZW.svg', import.meta.url).href) + .href; + const pathname = new URL( + new URL('assets/three-DPeYetg3.svg', import.meta.url).href + ).pathname; + const searchParams = new URL( + new URL('assets/four-2QgOKKkO.svg', import.meta.url).href + ).searchParams; - expect(output.length).to.equal(5); - expectChunk(output, 'snapshots/directories-ignored.js', 'directories-ignored.js', [ - expectAsset(output, 'snapshots/one.svg', 'one.svg', 'assets/one-Bkie7h0E.svg'), - expectAsset(output, 'snapshots/two.svg', 'two.svg', 'assets/two-D7JyS-th.svg'), - expectAsset(output, 'snapshots/three.svg', 'three.svg', 'assets/three-IN2CmsMK.svg'), - expectAsset(output, 'snapshots/four.svg', 'four.svg', 'assets/four-CUlW6cvD.svg'), + const directories = [ + new URL('./', import.meta.url), + new URL('./one', import.meta.url), + new URL('./one/', import.meta.url), + new URL('./one/two', import.meta.url), + new URL('./one/two/', import.meta.url), + ]; + + console.log({ + justUrlObject, + href, + pathname, + searchParams, + directories, + }); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal([ + 'assets/one-BCCvKrTe.svg', + 'assets/two-C4stzVZW.svg', + 'assets/three-DPeYetg3.svg', + 'assets/four-2QgOKKkO.svg', ]); + + expect(assets['assets/one-BCCvKrTe.svg']).to.equal( + svg``, + ); + expect(assets['assets/two-C4stzVZW.svg']).to.equal( + svg``, + ); + expect(assets['assets/three-DPeYetg3.svg']).to.equal( + svg``, + ); + expect(assets['assets/four-2QgOKKkO.svg']).to.equal( + svg``, + ); + }); + + describe('preserveDynamicStructure', () => { + it('allows dynamic vars with a concatenated file name with a preserved output path', async () => { + const rootDir = createApp({ + // actual names don't matter, we glob and include everything always + 'app.js': js` + const images = ['one', 'two']; + + const paths = images.map((name) => + new URL(\`./assets/images/image-\${name}.svg\`, import.meta.url), + ); + + console.log(paths); + `, + 'assets/images/image-one.svg': svg``, + 'assets/images/image-two.svg': svg``, + 'assets/images/image-three.svg': svg``, + }); + + const config = { + input: { app: path.join(rootDir, 'app.js') }, + plugins: [importMetaAssets({ preserveDynamicStructure: true })], + }; + + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: asset => + path.relative(rootDir, asset.originalFileNames[0]).split(path.sep).join('/'), + }); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(chunks['app.js']).to.equal(js` + const images = ['one', 'two']; + + const paths = images.map((name) => + new URL(\`./image-\${name}.svg\`, new URL('assets/images/image-one.svg', import.meta.url).href), + ); + + console.log(paths); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal(['assets/images/image-one.svg']); + + expect(assets['assets/images/image-one.svg']).to.equal( + svg``, + ); + expect(assets['assets/images/image-two.svg']).to.equal( + svg``, + ); + expect(assets['assets/images/image-three.svg']).to.equal( + svg``, + ); + }); + + it('allows dynamic vars in deep paths with a preserved output path', async () => { + const rootDir = createApp({ + // actual names don't matter, we glob and include everything always + 'app.js': js` + const images = { + 'category-name': ['image-name'], + }; + + const paths = Object.entries(images).flatMap(([category, names]) => + names.map((name) => + new URL(\`./assets/images/\${category}/\${name}.svg\`, import.meta.url), + ), + ); + + console.log(paths); + `, + 'assets/images/category-one/image-one.svg': svg``, + 'assets/images/category-one/image-two.svg': svg``, + 'assets/images/category-two/image-three.svg': svg``, + 'assets/images/category-two/image-four.svg': svg``, + }); + + const config = { + input: { app: path.join(rootDir, 'app.js') }, + plugins: [importMetaAssets({ preserveDynamicStructure: true })], + }; + + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: asset => + path.relative(rootDir, asset.originalFileNames[0]).split(path.sep).join('/'), + }); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(chunks['app.js']).to.equal(js` + const images = { + 'category-name': ['image-name'], + }; + + const paths = Object.entries(images).flatMap(([category, names]) => + names.map((name) => + new URL(\`../\${category}/\${name}.svg\`, new URL('assets/images/category-one/image-one.svg', import.meta.url).href), + ), + ); + + console.log(paths); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal(['assets/images/category-one/image-one.svg']); + + expect(assets['assets/images/category-one/image-one.svg']).to.equal( + svg``, + ); + expect(assets['assets/images/category-one/image-two.svg']).to.equal( + svg``, + ); + expect(assets['assets/images/category-two/image-three.svg']).to.equal( + svg``, + ); + expect(assets['assets/images/category-two/image-four.svg']).to.equal( + svg``, + ); + }); + + it('allows dynamic vars in deep paths with a static path in the middle with a preserved output path', async () => { + const rootDir = createApp({ + // actual names don't matter, we glob and include everything always + 'app.js': js` + const images = { + 'category-name': ['image-name'], + }; + + const paths = Object.entries(images).flatMap(([category, names]) => + names.map((name) => + new URL(\`./assets/images/\${category}/static/\${name}.svg\`, import.meta.url), + ), + ); + + console.log(paths); + `, + 'assets/images/category-one/static/image-one.svg': svg``, + 'assets/images/category-one/static/image-two.svg': svg``, + 'assets/images/category-two/static/image-three.svg': svg``, + 'assets/images/category-two/static/image-four.svg': svg``, + }); + + const config = { + input: { app: path.join(rootDir, 'app.js') }, + plugins: [importMetaAssets({ preserveDynamicStructure: true })], + }; + + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: asset => + path.relative(rootDir, asset.originalFileNames[0]).split(path.sep).join('/'), + }); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(chunks['app.js']).to.equal(js` + const images = { + 'category-name': ['image-name'], + }; + + const paths = Object.entries(images).flatMap(([category, names]) => + names.map((name) => + new URL(\`../../\${category}/static/\${name}.svg\`, new URL('assets/images/category-one/static/image-one.svg', import.meta.url).href), + ), + ); + + console.log(paths); + `); + + const appChunk = output.find(({ fileName }) => fileName === 'app.js'); + expect(appChunk.referencedFiles).to.deep.equal([ + 'assets/images/category-one/static/image-one.svg', + ]); + + expect(assets['assets/images/category-one/static/image-one.svg']).to.equal( + svg``, + ); + expect(assets['assets/images/category-one/static/image-two.svg']).to.equal( + svg``, + ); + expect(assets['assets/images/category-two/static/image-three.svg']).to.equal( + svg``, + ); + expect(assets['assets/images/category-two/static/image-four.svg']).to.equal( + svg``, + ); + }); }); }); diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/different-asset-levels-bundle.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/different-asset-levels-bundle.js deleted file mode 100644 index 7933fcf0e1..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/different-asset-levels-bundle.js +++ /dev/null @@ -1,18 +0,0 @@ -const nameOne = 'one-name'; -const imageOne = new URL(new URL('assets/one-deep-Bkie7h0E.svg', import.meta.url).href).href; - -const nameTwo = 'two-name'; -const imageTwo = new URL(new URL('assets/two-deep-D7JyS-th.svg', import.meta.url).href).href; - -const nameThree = 'three-name'; -const imageThree = new URL(new URL('assets/three-deep-IN2CmsMK.svg', import.meta.url).href).href; - -const nameFour = 'four-name'; -const imageFour = new URL(new URL('assets/four-deep-CUlW6cvD.svg', import.meta.url).href).href; - -console.log({ - [nameOne]: imageOne, - [nameTwo]: imageTwo, - [nameThree]: imageThree, - [nameFour]: imageFour, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/directories-ignored.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/directories-ignored.js deleted file mode 100644 index 035e1ce550..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/directories-ignored.js +++ /dev/null @@ -1,20 +0,0 @@ -const justUrlObject = new URL(new URL('assets/one-Bkie7h0E.svg', import.meta.url).href); -const href = new URL(new URL('assets/two-D7JyS-th.svg', import.meta.url).href).href; -const pathname = new URL(new URL('assets/three-IN2CmsMK.svg', import.meta.url).href).pathname; -const searchParams = new URL(new URL('assets/four-CUlW6cvD.svg', import.meta.url).href).searchParams; - -const directories = [ - new URL('./', import.meta.url), - new URL('./one', import.meta.url), - new URL('./one/', import.meta.url), - new URL('./one/two', import.meta.url), - new URL('./one/two/', import.meta.url), -]; - -console.log({ - justUrlObject, - href, - pathname, - searchParams, - directories, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/dynamic-vars.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/dynamic-vars.js deleted file mode 100644 index 982ad452ff..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/dynamic-vars.js +++ /dev/null @@ -1,21 +0,0 @@ -function __variableDynamicURLRuntime0__(path) { - switch (path) { - case './dynamic-assets/one.svg': return new URL(new URL('assets/one-Bkie7h0E.svg', import.meta.url).href); - case './dynamic-assets/three.svg': return new URL(new URL('assets/three-IN2CmsMK.svg', import.meta.url).href); - case './dynamic-assets/two.svg': return new URL(new URL('assets/two-D7JyS-th.svg', import.meta.url).href); - default: return new Promise(function(resolve, reject) { - (typeof queueMicrotask === 'function' ? queueMicrotask : setTimeout)( - reject.bind(null, new Error("Unknown variable dynamic new URL statement: " + path)) - ); - }) - } - } - -const names = ['one', 'two']; -// value of one could also be "two" or "three", bundler does not analyze the value itself -// Therefore, we expect both one.svg, two.svg and three.svg to be bundled, and this to turn into a switch statement -// with 3 cases (for all 3 assets in the dynamic-assets folder) -const dynamicImgs = names.map(n => __variableDynamicURLRuntime0__(`./dynamic-assets/${n}.svg`)); -const backticksImg = new URL(new URL('assets/three-IN2CmsMK.svg', import.meta.url).href); - -console.log(dynamicImgs, backticksImg); diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/five b/packages/rollup-plugin-import-meta-assets/test/snapshots/five deleted file mode 100644 index 6434d8dc54..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/five +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/four-bundle.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/four-bundle.js deleted file mode 100644 index 6dd1345d77..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/four-bundle.js +++ /dev/null @@ -1,4 +0,0 @@ -const nameFour = 'four-name'; -const imageFour = new URL(new URL('assets/four-CUlW6cvD.svg', import.meta.url).href).href; - -export { imageFour, nameFour }; diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/four.min.svg b/packages/rollup-plugin-import-meta-assets/test/snapshots/four.min.svg deleted file mode 100644 index 2a566f1737..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/four.min.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/four.svg b/packages/rollup-plugin-import-meta-assets/test/snapshots/four.svg deleted file mode 100644 index 86eb06cfd2..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/four.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/image.jpg b/packages/rollup-plugin-import-meta-assets/test/snapshots/image.jpg deleted file mode 100644 index b0f2052e045a4826ed450e1ed2849e6e40e925d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36287 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 JbAN9~|Jli_$}Ru^ diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/multi-level-bundle.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/multi-level-bundle.js deleted file mode 100644 index 7ea7d4c17b..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/multi-level-bundle.js +++ /dev/null @@ -1,18 +0,0 @@ -const nameOne = 'one-name'; -const imageOne = new URL(new URL('assets/one-Bkie7h0E.svg', import.meta.url).href).href; - -const nameTwo = 'two-name'; -const imageTwo = new URL(new URL('assets/two-D7JyS-th.svg', import.meta.url).href).href; - -const nameThree = 'three-name'; -const imageThree = new URL(new URL('assets/three-IN2CmsMK.svg', import.meta.url).href).href; - -const nameFour = 'four-name'; -const imageFour = new URL(new URL('assets/four-CUlW6cvD.svg', import.meta.url).href).href; - -console.log({ - [nameOne]: imageOne, - [nameTwo]: imageTwo, - [nameThree]: imageThree, - [nameFour]: imageFour, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/one-bundle.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/one-bundle.js deleted file mode 100644 index ad74d524a2..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/one-bundle.js +++ /dev/null @@ -1,4 +0,0 @@ -const nameOne = 'one-name'; -const imageOne = new URL('../one.svg', import.meta.url).href; - -export { imageOne, nameOne }; diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/one.min.svg b/packages/rollup-plugin-import-meta-assets/test/snapshots/one.min.svg deleted file mode 100644 index 01f16c2852..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/one.min.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/one.svg b/packages/rollup-plugin-import-meta-assets/test/snapshots/one.svg deleted file mode 100644 index dfe13a152f..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/one.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/simple-bundle.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/simple-bundle.js deleted file mode 100644 index 0c2e6a0a73..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/simple-bundle.js +++ /dev/null @@ -1,13 +0,0 @@ -const justUrlObject = new URL(new URL('assets/one-Bkie7h0E.svg', import.meta.url).href); -const href = new URL(new URL('assets/two-D7JyS-th.svg', import.meta.url).href).href; -const pathname = new URL(new URL('assets/three-IN2CmsMK.svg', import.meta.url).href).pathname; -const searchParams = new URL(new URL('assets/four-CUlW6cvD.svg', import.meta.url).href).searchParams; -const noExtension = new URL(new URL('assets/five-Bnvj_R70', import.meta.url).href); - -console.log({ - justUrlObject, - href, - pathname, - searchParams, - noExtension, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/three-bundle.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/three-bundle.js deleted file mode 100644 index a033b7db26..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/three-bundle.js +++ /dev/null @@ -1,4 +0,0 @@ -const nameThree = 'three-name'; -const imageThree = new URL(new URL('assets/three-IN2CmsMK.svg', import.meta.url).href).href; - -export { imageThree, nameThree }; diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/three.min.svg b/packages/rollup-plugin-import-meta-assets/test/snapshots/three.min.svg deleted file mode 100644 index 342277191b..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/three.min.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/three.svg b/packages/rollup-plugin-import-meta-assets/test/snapshots/three.svg deleted file mode 100644 index 9ebc1e4895..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/three.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/transform-bundle-ignored.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/transform-bundle-ignored.js deleted file mode 100644 index cc18a7ddd7..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/transform-bundle-ignored.js +++ /dev/null @@ -1,13 +0,0 @@ -const justUrlObject = new URL(new URL('assets/one--RhQWA3U.svg', import.meta.url).href); -const href = new URL(new URL('assets/two-CZdxIUwi.svg', import.meta.url).href).href; -const pathname = new URL(new URL('assets/three-tFhyRH_R.svg', import.meta.url).href).pathname; -const searchParams = new URL(new URL('assets/four-Cs1OId-q.svg', import.meta.url).href).searchParams; -const someJpg = new URL('./image.jpg', import.meta.url); - -console.log({ - justUrlObject, - href, - pathname, - searchParams, - someJpg, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/transform-bundle.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/transform-bundle.js deleted file mode 100644 index 71c9bf205f..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/transform-bundle.js +++ /dev/null @@ -1,13 +0,0 @@ -const justUrlObject = new URL(new URL('assets/one--RhQWA3U.svg', import.meta.url).href); -const href = new URL(new URL('assets/two-CZdxIUwi.svg', import.meta.url).href).href; -const pathname = new URL(new URL('assets/three-tFhyRH_R.svg', import.meta.url).href).pathname; -const searchParams = new URL(new URL('assets/four-Cs1OId-q.svg', import.meta.url).href).searchParams; -const someJpg = new URL(new URL('assets/image-C92N8yPj.jpg', import.meta.url).href); - -console.log({ - justUrlObject, - href, - pathname, - searchParams, - someJpg, -}); diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/two-bundle.js b/packages/rollup-plugin-import-meta-assets/test/snapshots/two-bundle.js deleted file mode 100644 index 0b472bb93a..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/two-bundle.js +++ /dev/null @@ -1,4 +0,0 @@ -const nameTwo = 'two-name'; -const imageTwo = new URL('../../two.svg', import.meta.url).href; - -export { imageTwo, nameTwo }; diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/two.min.svg b/packages/rollup-plugin-import-meta-assets/test/snapshots/two.min.svg deleted file mode 100644 index f37fafe51a..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/two.min.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/packages/rollup-plugin-import-meta-assets/test/snapshots/two.svg b/packages/rollup-plugin-import-meta-assets/test/snapshots/two.svg deleted file mode 100644 index 071582b8ff..0000000000 --- a/packages/rollup-plugin-import-meta-assets/test/snapshots/two.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/test-utils/rollup-test-utils.d.ts b/test-utils/rollup-test-utils.d.ts new file mode 100644 index 0000000000..0685122184 --- /dev/null +++ b/test-utils/rollup-test-utils.d.ts @@ -0,0 +1,19 @@ +import type { OutputOptions, RollupBuild, OutputChunk, OutputAsset } from 'rollup'; + +export function html(strings: TemplateStringsArray, ...values: string[]): string; +export function css(strings: TemplateStringsArray, ...values: string[]): string; +export function js(strings: TemplateStringsArray, ...values: string[]): string; +export function svg(strings: TemplateStringsArray, ...values: string[]): string; + +export function generateTestBundle( + build: RollupBuild, + outputConfig: OutputOptions, +): Promise<{ + output: (OutputChunk | OutputAsset)[]; + chunks: Record; + assets: Record; + assetsUnformatted: Record; +}>; + +export function createApp(structure: Record): string; +export function cleanApp(): void; diff --git a/test-utils/rollup-test-utils.js b/test-utils/rollup-test-utils.js new file mode 100644 index 0000000000..c0fc874db8 --- /dev/null +++ b/test-utils/rollup-test-utils.js @@ -0,0 +1,118 @@ +const synchronizedPrettier = require('@prettier/sync'); +const fs = require('fs'); +const path = require('path'); + +function collapseWhitespaceAll(str) { + return ( + str && + str.replace(/[ \n\r\t\f\xA0]+/g, spaces => { + return spaces === '\t' ? '\t' : spaces.replace(/(^|\xA0+)[^\xA0]+/g, '$1 '); + }) + ); +} + +function format(str, parser) { + return synchronizedPrettier.format(str, { parser, semi: true, singleQuote: true }); +} + +function merge(strings, ...values) { + return strings.reduce((acc, str, i) => acc + str + (values[i] || ''), ''); +} + +const extnameToFormatter = { + '.html': str => format(collapseWhitespaceAll(str), 'html'), + '.css': str => format(str, 'css'), + '.js': str => format(str, 'typescript'), + '.json': str => format(str, 'json'), + '.svg': str => format(collapseWhitespaceAll(str), 'html'), +}; + +function getFormatterFromFilename(name) { + return extnameToFormatter[path.extname(name)]; +} + +const html = (strings, ...values) => extnameToFormatter['.html'](merge(strings, ...values)); + +const css = (strings, ...values) => extnameToFormatter['.css'](merge(strings, ...values)); + +const js = (strings, ...values) => extnameToFormatter['.js'](merge(strings, ...values)); + +const svg = (strings, ...values) => extnameToFormatter['.svg'](merge(strings, ...values)); + +async function generateTestBundle(build, outputConfig) { + const { output } = await build.generate(outputConfig); + const chunks = {}; + const assets = {}; + const assetsUnformatted = {}; + + for (const file of output) { + const filename = file.fileName; + const formatter = getFormatterFromFilename(filename); + if (file.type === 'chunk') { + chunks[filename] = formatter ? formatter(file.code) : file.code; + } else if (file.type === 'asset') { + let code = file.source; + let codeUnformatted = file.source; + if (Buffer.isBuffer(code)) { + code = code.toString('utf8'); + codeUnformatted = codeUnformatted.toString('utf8'); + } + if (typeof code === 'string' && formatter) { + code = formatter(code); + } + assets[filename] = code; + assetsUnformatted[filename] = codeUnformatted; + } + } + + return { output, chunks, assets, assetsUnformatted }; +} + +let tmpDirs = []; + +function createApp(structure) { + const timestamp = Date.now(); + const randomId = Math.random().toString(36).substring(2, 8); + const rootDir = path.join(process.cwd(), `.tmp/app-${timestamp}-${randomId}`); + if (!fs.existsSync(rootDir)) { + fs.mkdirSync(rootDir, { recursive: true }); + } + tmpDirs.push(rootDir); + + Object.keys(structure).forEach(filePath => { + const fullPath = path.join(rootDir, filePath); + const isDir = fullPath.endsWith(path.sep); + const dir = isDir ? fullPath.slice(0, -1) : path.dirname(fullPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + if (!isDir && !fs.existsSync(fullPath)) { + const content = structure[filePath]; + const contentForWrite = + typeof content === 'object' && !(content instanceof Buffer) + ? JSON.stringify(content) + : content; + fs.writeFileSync(fullPath, contentForWrite); + } + }); + return rootDir; +} + +function cleanApp() { + for (const dir of tmpDirs) { + if (fs.existsSync(dir)) { + fs.rmSync(dir, { recursive: true }); + } + } + tmpDirs = []; +} + +module.exports = { + html, + css, + js, + svg, + generateTestBundle, + createApp, + cleanApp, +};