From 1afed84abe179d2ffac70d53524b48b21a40f916 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Thu, 30 Oct 2025 14:08:38 +0100 Subject: [PATCH 01/21] WIP1 --- .gitignore | 3 + package-lock.json | 57 +- packages/rollup-plugin-html/package.json | 9 +- .../test-experiments/experiments.test.ts | 324 +++ .../rollup-plugin-html/test-new/new.test.ts | 2402 +++++++++++++++++ 5 files changed, 2791 insertions(+), 4 deletions(-) create mode 100644 packages/rollup-plugin-html/test-experiments/experiments.test.ts create mode 100644 packages/rollup-plugin-html/test-new/new.test.ts diff --git a/.gitignore b/.gitignore index 1d8925996..03a8d4933 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ local.log docs/_merged_data/ docs/_merged_assets/ docs/_merged_includes/ + +## temp +.tmp \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 27e8208a1..43bfd297f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4146,6 +4146,22 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@prettier/sync": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@prettier/sync/-/sync-0.6.1.tgz", + "integrity": "sha512-yF9G8vK/LYUTF3Cijd7VC9La3b20F20/J/fgoR4H0B8JGOWnZVZX6+I6+vODPosjmMcpdlUV+gUqJQZp3kLOcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "make-synchronized": "^0.8.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier-synchronized?sponsor=1" + }, + "peerDependencies": { + "prettier": "*" + } + }, "node_modules/@promptbook/utils": { "version": "0.69.5", "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", @@ -7145,6 +7161,17 @@ "@types/node": "*" } }, + "node_modules/@types/prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-mFMBfMOz8QxhYVbuINtswBp9VL2b4Y0QqYHwqLz3YbgtfAcat2Dl6Y1o4e22S/OVE6Ebl9m7wWiMT2lSbAs1wA==", + "deprecated": "This is a stub types definition. prettier provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier": "*" + } + }, "node_modules/@types/prismjs": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.0.tgz", @@ -21143,6 +21170,16 @@ "dev": true, "license": "ISC" }, + "node_modules/make-synchronized": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/make-synchronized/-/make-synchronized-0.8.0.tgz", + "integrity": "sha512-DZu4lwc0ffoFz581BSQa/BJl+1ZqIkoRQ+VejMlH0VrP4E86StAODnZujZ4sepumQj8rcP7wUnUBGM8Gu+zKUA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/fisker/make-synchronized?sponsor=1" + } + }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -36451,11 +36488,14 @@ "html-minifier-terser": "^7.1.0", "lightningcss": "^1.24.0", "parse5": "^6.0.1", - "picomatch": "^2.2.2" + "picomatch": "^2.2.2", + "prettier": "^3.6.2" }, "devDependencies": { + "@prettier/sync": "^0.6.1", "@types/html-minifier-terser": "^7.0.0", "@types/picomatch": "^2.2.1", + "@types/prettier": "^3.0.0", "rollup": "^4.4.0" }, "engines": { @@ -36505,6 +36545,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/rollup-plugin-html/node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "packages/rollup-plugin-import-meta-assets": { "name": "@web/rollup-plugin-import-meta-assets", "version": "2.3.0", diff --git a/packages/rollup-plugin-html/package.json b/packages/rollup-plugin-html/package.json index 0c5c80172..2f6514120 100644 --- a/packages/rollup-plugin-html/package.json +++ b/packages/rollup-plugin-html/package.json @@ -28,8 +28,8 @@ "demo:mpa": "rm -rf demo/dist && rollup -c demo/mpa/rollup.config.js --watch & npm run serve-demo", "demo:spa": "rm -rf demo/dist && rollup -c demo/spa/rollup.config.js --watch & npm run serve-demo", "serve-demo": "node ../dev-server/dist/bin.js --watch --root-dir demo/dist --app-index index.html --compatibility none --open", - "test:node": "mocha test/**/*.test.ts --require ts-node/register --reporter dot", - "test:watch": "mocha test/**/*.test.ts --require ts-node/register --watch --watch-files src,test" + "test:node": "mocha test-new/**/*.test.ts --require ts-node/register --reporter dot", + "test:watch": "mocha test-new/**/*.test.ts --require ts-node/register --watch --watch-files src,test" }, "files": [ "*.js", @@ -49,11 +49,14 @@ "html-minifier-terser": "^7.1.0", "lightningcss": "^1.24.0", "parse5": "^6.0.1", - "picomatch": "^2.2.2" + "picomatch": "^2.2.2", + "prettier": "^3.6.2" }, "devDependencies": { + "@prettier/sync": "^0.6.1", "@types/html-minifier-terser": "^7.0.0", "@types/picomatch": "^2.2.1", + "@types/prettier": "^3.0.0", "rollup": "^4.4.0" } } diff --git a/packages/rollup-plugin-html/test-experiments/experiments.test.ts b/packages/rollup-plugin-html/test-experiments/experiments.test.ts new file mode 100644 index 000000000..8bbbec3c8 --- /dev/null +++ b/packages/rollup-plugin-html/test-experiments/experiments.test.ts @@ -0,0 +1,324 @@ +import synchronizedPrettier from '@prettier/sync'; +import * as prettier from 'prettier'; +import { rollup, OutputChunk, OutputOptions, Plugin, RollupBuild } from 'rollup'; +import { expect } from 'chai'; +import path from 'path'; +import fs from 'fs'; +import { rollupPluginHTML } from '../src/index.js'; + +// TODO: test output "fileName" too, like the real output name, not always it's properly checked besides checking the index.html source + +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)]; +} + +const html = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.html'](merge(strings, ...values)); + +const css = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.css'](merge(strings, ...values)); + +const js = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.js'](merge(strings, ...values)); + +const svg = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.svg'](merge(strings, ...values)); + +const outputConfig: OutputOptions = { + format: 'es', + dir: 'dist', +}; + +async function generateTestBundle(build: RollupBuild, outputConfig: OutputOptions) { + const { output } = await build.generate(outputConfig); + const chunks: Record = {}; + const assets: 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; + if (typeof code !== 'string' && filename.endsWith('.css')) { + code = Buffer.from(code).toString('utf8'); + } + if (typeof code === 'string' && formatter) { + code = formatter(code); + } + assets[filename] = code; + } + } + + return { output, chunks, assets }; +} + +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; +} + +function cleanApp() { + const tmpDir = path.join(__dirname, './.tmp'); + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true }); + } +} + +describe('rollup-plugin-html', () => { + afterEach(() => { + cleanApp(); + }); + + it('hashes all assets using assetFileNames', async () => { + const rootDir = createApp({ + 'node_modules/ing-web/fonts/font.woff2': 'font.woff', + 'node_modules/ing-web/global.css': css` + @font-face { + font-family: Font; + src: url('fonts/font.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `, + 'assets/images/image.png': 'image.png', + 'assets/styles.css': css` + #a { + background-image: url('images/image.png'); + } + `, + 'src/main.js': js` + const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; + `, + 'index.html': html` + + + + + + + + + + + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: './index.html', + }), + ], + }; + + const build = await rollup(config); + const { output, assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: 'static/[name].immutable.[hash][extname]', + }); + + expect(assets).to.have.keys([ + 'static/font.immutable.C5MNjX-h.woff2', + 'static/global.immutable.DB0fKkjs.css', + 'static/image.immutable.7xJLr_7N.png', + 'static/styles.immutable.D4tZXVv0.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + `); + + expect(assets['static/global.immutable.DB0fKkjs.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('font.immutable.C5MNjX-h.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['static/styles.immutable.D4tZXVv0.css']).to.equal(css` + #a { + background-image: url('image.immutable.7xJLr_7N.png'); + } + `); + }); + + it('correctly resolves paths by using publicPath when assetFileNames puts assets in different dirs', async () => { + const rootDir = createApp({ + 'node_modules/ing-web/fonts/font.woff2': 'font.woff', + 'node_modules/ing-web/global.css': css` + @font-face { + font-family: Font; + src: url('fonts/font.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `, + 'assets/images/image.png': 'image.png', + 'assets/styles.css': css` + #a { + background-image: url('images/image.png'); + } + `, + 'src/main.js': js` + const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; + `, + 'index.html': html` + + + + + + + + + + + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: './index.html', + publicPath: '/static/', + }), + ], + }; + + const build = await rollup(config); + const { output, assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: assetInfo => { + const name = assetInfo.names[0] || ''; + if (name.endsWith('.woff2')) { + return 'fonts/[name].immutable.[hash][extname]'; + } else if (name.endsWith('.css')) { + return 'styles/[name].immutable.[hash][extname]'; + } else if (name.endsWith('.png')) { + return 'images/[name].immutable.[hash][extname]'; + } + return '[name].immutable.[hash][extname]'; + }, + }); + + expect(assets).to.have.keys([ + 'fonts/font.immutable.C5MNjX-h.woff2', + 'styles/global.immutable.B3Q0ucg4.css', + 'images/image.immutable.7xJLr_7N.png', + 'styles/styles.immutable.C3Z0Fs2-.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + `); + + expect(assets['styles/global.immutable.B3Q0ucg4.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('/static/fonts/font.immutable.C5MNjX-h.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['styles/styles.immutable.C3Z0Fs2-.css']).to.equal(css` + #a { + background-image: url('/static/images/image.immutable.7xJLr_7N.png'); + } + `); + }); +}); diff --git a/packages/rollup-plugin-html/test-new/new.test.ts b/packages/rollup-plugin-html/test-new/new.test.ts new file mode 100644 index 000000000..e98a8438f --- /dev/null +++ b/packages/rollup-plugin-html/test-new/new.test.ts @@ -0,0 +1,2402 @@ +import synchronizedPrettier from '@prettier/sync'; +import * as prettier from 'prettier'; +import { rollup, OutputChunk, OutputOptions, Plugin, RollupBuild } from 'rollup'; +import { expect } from 'chai'; +import path from 'path'; +import fs from 'fs'; +import { rollupPluginHTML } from '../src/index.js'; + +// TODO: test output "fileName" too, like the real output name, not always it's properly checked besides checking the index.html source + +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)]; +} + +const html = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.html'](merge(strings, ...values)); + +const css = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.css'](merge(strings, ...values)); + +const js = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.js'](merge(strings, ...values)); + +const svg = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.svg'](merge(strings, ...values)); + +const outputConfig: OutputOptions = { + format: 'es', + dir: 'dist', +}; + +async function generateTestBundle(build: RollupBuild, outputConfig: OutputOptions) { + const { output } = await build.generate(outputConfig); + const chunks: Record = {}; + const assets: 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; + if (typeof code !== 'string' && filename.endsWith('.css')) { + code = Buffer.from(code).toString('utf8'); + } + if (typeof code === 'string' && formatter) { + code = formatter(code); + } + assets[filename] = code; + } + } + + return { output, chunks, assets }; +} + +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; +} + +function cleanApp() { + const tmpDir = path.join(__dirname, './.tmp'); + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true }); + } +} + +describe('rollup-plugin-html', () => { + afterEach(() => { + cleanApp(); + }); + + it('can build with an input path as input', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + `, + 'entrypoint-a.js': js` + import './modules/module-a.js'; + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + import './modules/module-b.js'; + console.log('entrypoint-b.js'); + `, + 'modules/module-a.js': js` + import './shared-module.js'; + console.log('module-a.js'); + `, + 'modules/module-b.js': js` + import './shared-module.js'; + console.log('module-b.js'); + `, + 'modules/shared-module.js': js` + console.log('shared-module.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: './index.html', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(3); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` + + + + + + + + `); + }); + + it('can build with html file as rollup input', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + `, + 'entrypoint-a.js': js` + import './modules/module-a.js'; + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + import './modules/module-b.js'; + console.log('entrypoint-b.js'); + `, + 'modules/module-a.js': js` + import './shared-module.js'; + console.log('module-a.js'); + `, + 'modules/module-b.js': js` + import './shared-module.js'; + console.log('module-b.js'); + `, + 'modules/shared-module.js': js` + console.log('shared-module.js'); + `, + }); + + const config = { + input: './index.html', + plugins: [rollupPluginHTML({ rootDir })], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(3); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` + + + + + + + + `); + }); + + it('will retain attributes on script tags', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + `, + 'entrypoint-a.js': js` + import './modules/module-a.js'; + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + import './modules/module-b.js'; + console.log('entrypoint-b.js'); + `, + 'modules/module-a.js': js` + import './shared-module.js'; + console.log('module-a.js'); + `, + 'modules/module-b.js': js` + import './shared-module.js'; + console.log('module-b.js'); + `, + 'modules/shared-module.js': js` + console.log('shared-module.js'); + `, + }); + + const config = { + input: './index.html', + plugins: [rollupPluginHTML({ rootDir })], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(3); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` + + + + + + + + `); + }); + + it('can build with pure html file as rollup input', async () => { + const rootDir = createApp({ + 'index.html': html` + + + +

hello world

+ + + `, + }); + + const config = { + input: './index.html', + plugins: [rollupPluginHTML({ rootDir })], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + expect(assets['index.html']).to.equal(html` + + + +

hello world

+ + + `); + }); + + it('can build with multiple pure html inputs', async () => { + const rootDir = createApp({ + 'index1.html': html` + + + +

hello world

+ + + `, + 'index2.html': html` + + + +

hey there

+ + + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: ['./index1.html', './index2.html'], + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + expect(assets['index1.html']).to.equal(html` + + + +

hello world

+ + + `); + expect(assets['index2.html']).to.equal(html` + + + +

hey there

+ + + `); + }); + + it('can build with html string as input', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + name: 'index.html', + html: ``, + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + }); + + it('resolves paths relative to virtual html filename', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + name: 'nested/index.html', + html: ``, + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['nested/index.html']).to.equal(html` + + + + + + + `); + }); + + it('can build with inline modules', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + name: 'index.html', + html: ``, + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + const hash = '16165cb387fc14ed1fe1749d05f19f7b'; + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + + expect(chunks[`inline-module-${hash}.js`]).to.include(js`console.log('app.js');`); + }); + + it('resolves inline module imports relative to the HTML file', async () => { + const rootDir = createApp({ + 'nested/index.html': html` + + + + + + + `, + 'nested/app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: './nested/index.html', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + const hash = 'b774aefb8bf002b291fd54d27694a34d'; + expect(chunks[`inline-module-${hash}.js`]).to.include(js`console.log('app.js');`); + }); + + it('can build transforming final output', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: `

Hello world

`, + }, + transformHtml(html) { + return html.replace('Hello world', 'Goodbye world'); + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['index.html']).to.equal(html` + + + +

Goodbye world

+ + + + `); + }); + + it('can build with a public path', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: ``, + }, + publicPath: '/static/', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + }); + + it('can build with a public path with a file in a directory', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + name: 'nested/index.html', + html: ``, + }, + publicPath: '/static/', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['nested/index.html']).to.equal(html` + + + + + + + `); + }); + + it('can build with multiple build outputs', async () => { + const rootDir = createApp({ + 'app.js': js` + import './modules/module.js'; + console.log('app.js'); + `, + 'modules/module.js': js` + console.log('module.js'); + `, + }); + + const plugin = rollupPluginHTML({ + rootDir, + input: { + html: ``, + }, + publicPath: '/static/', + }); + + const config = { + input: path.join(rootDir, 'app.js'), + plugins: [plugin], + }; + + const build = await rollup(config); + + const bundleA = generateTestBundle(build, { + format: 'system', + dir: 'dist', + plugins: [plugin.api.addOutput('legacy')], + }); + + const bundleB = generateTestBundle(build, { + format: 'es', + dir: 'dist', + plugins: [plugin.api.addOutput('modern')], + }); + + const { chunks: chunksA, assets: assetsA } = await bundleA; + const { chunks: chunksB, assets: assetsB } = await bundleB; + + expect(Object.keys(chunksA)).to.have.lengthOf(1); + expect(Object.keys(assetsA)).to.have.lengthOf(0); + expect(Object.keys(chunksB)).to.have.lengthOf(1); + expect(Object.keys(assetsB)).to.have.lengthOf(1); + + expect(chunksA['app.js']).to.include(js`console.log('app.js');`); + expect(chunksA['app.js']).to.include(js`console.log('module.js');`); + expect(chunksB['app.js']).to.include(js`console.log('app.js');`); + expect(chunksB['app.js']).to.include(js`console.log('module.js');`); + + expect(assetsA['index.html']).to.not.exist; + expect(assetsB['index.html']).to.equal(html` + + + + + + + + `); + }); + + it('can build with index.html as input and an extra html file as output', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: ``, + }, + }), + rollupPluginHTML({ + rootDir, + input: { + name: 'foo.html', + html: `

foo.html

`, + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(2); + expect(Object.keys(assets)).to.have.lengthOf(2); + + expect(chunks['app.js']).to.exist; + expect(assets['index.html']).to.equal(html` + + + + + + + `); + expect(assets['foo.html']).to.equal(html` + + + +

foo.html

+ + + `); + }); + + it('can build with multiple html inputs', async () => { + const rootDir = createApp({ + 'entrypoint-a.js': js` + import './modules/module-a.js'; + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + import './modules/module-b.js'; + console.log('entrypoint-b.js'); + `, + 'entrypoint-c.js': js` + import './modules/module-c.js'; + console.log('entrypoint-c.js'); + `, + 'modules/module-a.js': js` + import './shared-module.js'; + console.log('module-a.js'); + `, + 'modules/module-b.js': js` + import './shared-module.js'; + console.log('module-b.js'); + `, + 'modules/module-c.js': js` + import './shared-module.js'; + console.log('module-c.js'); + `, + 'modules/shared-module.js': js` + console.log('shared-module.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: [ + { + name: 'page-a.html', + html: `

Page A

`, + }, + { + name: 'page-b.html', + html: `

Page B

`, + }, + { + name: 'page-c.html', + html: `

Page C

`, + }, + ], + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(4); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(chunks['entrypoint-a.js']).to.exist; + expect(chunks['entrypoint-b.js']).to.exist; + expect(chunks['entrypoint-c.js']).to.exist; + + expect(assets['page-a.html']).to.equal(html` + + + +

Page A

+ + + + `); + expect(assets['page-b.html']).to.equal(html` + + + +

Page B

+ + + + `); + expect(assets['page-c.html']).to.equal(html` + + + +

Page C

+ + + + `); + }); + + it('can use a glob to build multiple pages', async () => { + const rootDir = createApp({ + 'pages/page-a.html': html` + + +

page-a.html

+ + + + + `, + 'pages/page-b.html': html` + + +

page-b.html

+ + + + + `, + 'pages/page-c.html': html` + + +

page-c.html

+ + + + + `, + 'pages/page-a.js': js` + export default 'page a'; + `, + 'pages/page-b.js': js` + export default 'page b'; + `, + 'pages/page-c.js': js` + export default 'page c'; + `, + 'pages/shared.js': js` + export default 'shared'; + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: 'pages/**/*.html', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(4); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(chunks['page-a.js']).to.exist; + expect(chunks['page-b.js']).to.exist; + expect(chunks['page-c.js']).to.exist; + + expect(assets['page-a.html']).to.equal(html` + + + +

page-a.html

+ + + + + `); + expect(assets['page-b.html']).to.equal(html` + + + +

page-b.html

+ + + + + `); + // TODO: investigate why shared.js is after page-c.js here but before in the others + expect(assets['page-c.html']).to.equal(html` + + + +

page-c.html

+ + + + + `); + }); + + it('can exclude globs', async () => { + const rootDir = createApp({ + 'exclude/index.html': html``, + 'exclude/assets/partial.html': html`I'm a partial!`, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: 'exclude/**/*.html', + exclude: '**/partial.html', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + }); + + it('creates unique inline script names', async () => { + const rootDir = createApp({}); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: [ + { + name: 'nestedA/indexA.html', + html: `

Page A

`, + }, + { + name: 'nestedB/indexB.html', + html: `

Page B

`, + }, + { + name: 'indexC.html', + html: `

Page C

`, + }, + ], + }), + ], + }; + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(3); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(chunks['inline-module-d463148d1d5869e52917a3b270db9e72.js']).to.exist; + expect(chunks['inline-module-b81da853430abdf130bcc7c4d0ade6d9.js']).to.exist; + expect(chunks['inline-module-170bb2146da66c440259138c7e0fea7e.js']).to.exist; + + expect(assets['nestedA/indexA.html']).to.equal(html` + + + +

Page A

+ + + + `); + expect(assets['nestedB/indexB.html']).to.equal(html` + + + +

Page B

+ + + + `); + expect(assets['indexC.html']).to.equal(html` + + + +

Page C

+ + + + `); + }); + + it('deduplicates common modules', async () => { + const rootDir = createApp({}); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: [ + { + name: 'a.html', + html: `

Page A

`, + }, + { + name: 'b.html', + html: `

Page B

`, + }, + { + name: 'c.html', + html: `

Page C

`, + }, + ], + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(chunks['inline-module-44281cf3dede62434e0dd368df08902f.js']).to.exist; + + expect(assets['a.html']).to.equal(html` + + + +

Page A

+ + + + `); + expect(assets['b.html']).to.equal(html` + + + +

Page B

+ + + + `); + expect(assets['c.html']).to.equal(html` + + + +

Page C

+ + + + `); + }); + + it('outputs the hashed entrypoint name', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: ``, + }, + }), + ], + }; + + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, { + ...outputConfig, + entryFileNames: '[name]-[hash].js', + }); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + const appChunk = output.find(f => + // @ts-ignore + f.facadeModuleId.endsWith('app.js'), + ) as OutputChunk; + + // ensure it's actually hashed + expect(appChunk.fileName).to.not.equal('app.js'); + + // get hashed name dynamically + expect(assets['index.html']).to.equal(html` + + + + + + + `); + }); + + it('outputs import path relative to the final output html', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + name: 'nested/index.html', + html: '', + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['nested/index.html']).to.equal(html` + + + + + + + `); + }); + + it('can change HTML root directory', async () => { + const rootDir = createApp({ + 'different-root/src/app.js': js` + console.log('app.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir: path.join(rootDir, 'different-root'), + input: { + name: 'src/nested/index.html', + html: '', + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['src/nested/index.html']).to.equal(html` + + + + + + + `); + }); + + it('can get the input with getInputs()', async () => { + // default filename + const pluginA = rollupPluginHTML({ input: { html: 'Hello world' } }); + + // filename inferred from input filename + const rootDirB = createApp({ + 'my-page.html': html``, + 'app.js': js`console.log('app.js');`, + }); + const pluginB = rollupPluginHTML({ + input: path.join(rootDirB, 'my-page.html'), + }); + + // filename explicitly set + const rootDirC = createApp({ + 'index.html': html``, + 'app.js': js`console.log('app.js');`, + }); + const pluginC = rollupPluginHTML({ + input: { + name: 'nested/my-other-page.html', + path: path.join(rootDirC, 'index.html'), + }, + }); + + await rollup({ plugins: [pluginA] }); + await rollup({ plugins: [pluginB] }); + await rollup({ plugins: [pluginC] }); + + expect(pluginA.api.getInputs()[0].name).to.equal('index.html'); + expect(pluginB.api.getInputs()[0].name).to.equal('my-page.html'); + expect(pluginC.api.getInputs()[0].name).to.equal('nested/my-other-page.html'); + }); + + it('supports other plugins injecting a transform function', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + `, + 'entrypoint-a.js': js` + import './modules/module-a.js'; + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + import './modules/module-b.js'; + console.log('entrypoint-b.js'); + `, + 'modules/module-a.js': js` + import './shared-module.js'; + console.log('module-a.js'); + `, + 'modules/module-b.js': js` + import './shared-module.js'; + console.log('module-b.js'); + `, + 'modules/shared-module.js': js` + console.log('shared-module.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: './index.html', + }), + { + name: 'other-plugin', + buildStart(options) { + if (!options.plugins) throw new Error('no plugins'); + const plugin = options.plugins.find(pl => { + if (pl.name === '@web/rollup-plugin-html') { + return pl!.api.getInputs()[0].name === 'index.html'; + } + return false; + }); + plugin!.api.addHtmlTransformer((html: string) => + html.replace('', ''), + ); + }, + } as Plugin, + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(3); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` + + + + + + + + + `); + }); + + it('includes referenced assets in the bundle', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + 'image-b.png': 'image-b.png', + 'image-c.png': 'image-c.png', + 'image-a.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + :root { + color: blue; + } + `, + 'foo/x.css': css` + :root { + color: x; + } + `, + 'foo/bar/y.css': css` + :root { + color: y; + } + `, + 'webmanifest.json': { message: 'hello world' }, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: html` + + + + + + + + + + + + +
+ +
+ + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(assets).to.have.keys([ + 'assets/image-a.png', + 'assets/image-b.png', + 'assets/image-c-C4yLPiIL.png', + 'assets/image-a.svg', + 'assets/image-b-C4stzVZW.svg', + 'assets/styles-CF2Iy5n1.css', + 'assets/x-DDGg8O6h.css', + 'assets/y-DJTrnPH3.css', + 'assets/webmanifest.json', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + +
+ +
+ + + `); + }); + + it('deduplicates static assets with similar names', async () => { + const rootDir = createApp({ + 'foo.svg': svg``, + 'x/foo.svg': svg``, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + expect(assets['index.html']).to.equal(html` + + + + + + + + `); + }); + + // TODO: this will probably go away + it('static and hashed asset nodes can reference the same files', async () => { + const rootDir = createApp({ + 'foo.svg': svg``, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + `); + }); + + it('deduplicates common assets', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: html` + + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + `); + }); + + it('deduplicates common assets across HTML files', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: [ + { + name: 'page-a.html', + html: html` + + + + + + `, + }, + { + name: 'page-b.html', + html: html` + + + + + + `, + }, + { + name: 'page-c.html', + html: html` + + + + + + + `, + }, + ], + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + expect(assets['page-a.html']).to.equal(html` + + + + + + + `); + + expect(assets['page-b.html']).to.equal(html` + + + + + + + `); + + expect(assets['page-c.html']).to.equal(html` + + + + + + + + `); + }); + + it('can turn off extracting assets', async () => { + const rootDir = createApp({ + 'image-c.png': 'image-c.png', + 'image-b.svg': svg``, + 'styles.css': css` + :root { + color: blue; + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + extractAssets: false, + rootDir, + input: { + html: html` + + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + `); + }); + + it('can inject a CSP meta tag for inline scripts', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + + + `, + 'entrypoint-a.js': js` + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + console.log('entrypoint-b.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + strictCSPInlineScripts: true, + rootDir, + input: './index.html', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(2); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + `); + }); + + it('can add to an existing CSP meta tag for inline scripts', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + + + + + `, + 'entrypoint-a.js': js` + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + console.log('entrypoint-b.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + strictCSPInlineScripts: true, + rootDir, + input: './index.html', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(2); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + `); + }); + + it('can add to an existing CSP meta tag for inline scripts even if script-src is already there', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + + + + + `, + 'entrypoint-a.js': js` + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + console.log('entrypoint-b.js'); + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + strictCSPInlineScripts: true, + rootDir, + input: './index.html', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(2); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + `); + }); + + it('can inject a service worker registration script if injectServiceWorker and serviceWorkerPath are provided', async () => { + const rootDir = createApp({ + 'index.html': html` + + +

inject a service worker into /index.html

+ + + `, + 'sub-pure-html/index.html': html` + + +

inject a service worker into /sub-page/index.html

+ + + `, + 'sub-with-js/index.html': html` + + +

inject a service worker into /sub-page/index.html

+ + + + `, + 'sub-with-js/sub-js.js': js`console.log('sub-with-js');`, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: '**/*.html', + flattenOutput: false, + injectServiceWorker: true, + serviceWorkerPath: path.join( + path.resolve(outputConfig.dir as string), + 'service-worker.js', + ), + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + function extractServiceWorkerPath(code: string) { + const registerOpen = code.indexOf(".register('"); + const registerClose = code.indexOf("')", registerOpen + 11); + return code.substring(registerOpen + 11, registerClose); + } + + expect(extractServiceWorkerPath(assets['index.html'] as string)).to.equal('service-worker.js'); + expect(extractServiceWorkerPath(assets['sub-with-js/index.html'] as string)).to.equal( + '../service-worker.js', + ); + expect(extractServiceWorkerPath(assets['sub-pure-html/index.html'] as string)).to.equal( + '../service-worker.js', + ); + }); + + it('does support a absolutePathPrefix to allow for sub folder deployments', async () => { + const rootDir = createApp({ + 'x/foo.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + :root { + color: blue; + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + absolutePathPrefix: '/my-prefix/', + rootDir, + input: { + html: html` + + + + + + + + + + `, + name: 'x/index.html', + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + expect(assets['x/index.html']).to.equal(html` + + + + + + + + + + `); + }); + + it('handles fonts linked from css files', async () => { + const rootDir = createApp({ + 'fonts/font-bold.woff2': 'font-bold', + 'fonts/font-normal.woff2': 'font-normal', + 'styles.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + bundleAssetsFromCss: true, + rootDir, + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + // TODO: this looks to be adding extra "/assets/" folder layer + // TODO: actually totally unclear why the names are not hashed here, but the folder has changed, like half-baked + expect(assets).to.have.keys([ + 'assets/assets/font-normal-Cht9ZB76.woff2', + 'assets/assets/font-bold-eQjSonqH.woff2', + 'assets/styles-BUBaODov.css', + 'index.html', + ]); + + expect(assets['assets/styles-BUBaODov.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('assets/font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('assets/font-bold-eQjSonqH.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + }); + + it('handles fonts linked from css files in node_modules', async () => { + const rootDir = createApp({ + 'node_modules/foo/fonts/font-bold.woff2': 'font-bold', + 'node_modules/foo/fonts/font-normal.woff2': 'font-normal', + 'node_modules/foo/styles.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + bundleAssetsFromCss: true, + rootDir, + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + // TODO: this looks to be adding extra "/assets/" folder layer + // TODO: actually totally unclear why the names are not hashed here, but the folder has changed, like half-baked + expect(assets).to.have.keys([ + 'assets/assets/font-normal-Cht9ZB76.woff2', + 'assets/assets/font-bold-eQjSonqH.woff2', + 'assets/styles-BUBaODov.css', + 'index.html', + ]); + + expect(assets['assets/styles-BUBaODov.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('assets/font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('assets/font-bold-eQjSonqH.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + }); + + it('handles duplicate fonts correctly', async () => { + const rootDir = createApp({ + 'fonts/font-bold.woff2': 'font-bold', + 'fonts/font-normal.woff2': 'font-normal', + 'styles-a.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `, + 'styles-b.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + bundleAssetsFromCss: true, + rootDir, + input: { + html: html` + + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { output } = await generateTestBundle(build, outputConfig); + + const fonts = output.filter(o => o.name?.endsWith('font-normal.woff2')); + expect(fonts.length).to.equal(1); + }); + + it('handles images referenced from css', async () => { + const rootDir = createApp({ + 'images/star.avif': 'star.avif', + 'images/star.gif': 'star.gif', + 'images/star.jpeg': 'star.jpeg', + 'images/star.jpg': 'star.jpg', + 'images/star.png': 'star.png', + 'images/star.svg': 'star.svg', + 'images/star.webp': 'star.webp', + 'styles.css': css` + #a { + background-image: url('images/star.avif'); + } + + #b { + background-image: url('images/star.gif'); + } + + #c { + background-image: url('images/star.jpeg'); + } + + #d { + background-image: url('images/star.jpg'); + } + + #e { + background-image: url('images/star.png'); + } + + #f { + background-image: url('images/star.svg'); + } + + #g { + background-image: url('images/star.svg#foo'); + } + + #h { + background-image: url('images/star.webp'); + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + bundleAssetsFromCss: true, + rootDir, + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + // TODO: this looks to be adding extra "/assets/" folder layer + // TODO: actually totally unclear why the names are not hashed here, but the folder has changed, like half-baked + expect(assets).to.have.keys([ + 'assets/assets/star-D_LO5feX.avif', + 'assets/assets/star-BKg9qmmf.gif', + 'assets/assets/star-BZWqL7hS.jpeg', + 'assets/assets/star-Df0JryvN.jpg', + 'assets/assets/star-CXig10q7.png', + 'assets/assets/star-CwhgM_z4.svg', + 'assets/assets/star-CKbh5mKn.webp', + 'assets/styles-Cuqf3qRf.css', + 'index.html', + ]); + + expect(assets['assets/styles-Cuqf3qRf.css']).to.equal(css` + #a { + background-image: url('assets/star-D_LO5feX.avif'); + } + + #b { + background-image: url('assets/star-BKg9qmmf.gif'); + } + + #c { + background-image: url('assets/star-BZWqL7hS.jpeg'); + } + + #d { + background-image: url('assets/star-Df0JryvN.jpg'); + } + + #e { + background-image: url('assets/star-CXig10q7.png'); + } + + #f { + background-image: url('assets/star-CwhgM_z4.svg'); + } + + #g { + background-image: url('assets/star-CwhgM_z4.svg#foo'); + } + + #h { + background-image: url('assets/star-CKbh5mKn.webp'); + } + `); + }); + + it('allows to exclude external assets usign a glob pattern', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + 'image-b.png': 'image-b.png', + 'image-a.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + #a1 { + background-image: url('image-a.png'); + } + + #a2 { + background-image: url('image-a.svg'); + } + + #d1 { + background-image: url('./image-b.png'); + } + + #d2 { + background-image: url('./image-b.svg'); + } + `, + 'foo/x.css': css` + :root { + color: x; + } + `, + 'foo/bar/y.css': css` + :root { + color: y; + } + `, + 'webmanifest.json': { message: 'hello world' }, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + bundleAssetsFromCss: true, + externalAssets: ['**/foo/**/*', '*.svg'], + rootDir, + input: { + html: html` + + + + + + + + + + + + + +
+ +
+ + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + // TODO: this looks to be adding extra "/assets/" folder layer + // TODO: actually totally unclear why the names are not hashed here, but the folder has changed, like half-baked + expect(assets).to.have.keys([ + 'assets/assets/image-a-XOCPHCrV.png', + 'assets/assets/image-b-BgQHKcRn.png', + 'assets/image-a.png', + 'assets/image-b.png', + 'assets/styles-DFIb0lB5.css', + 'assets/webmanifest.json', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + + +
+ +
+ + + `); + + expect(assets['assets/styles-DFIb0lB5.css']).to.equal(css` + #a1 { + background-image: url('assets/image-a-XOCPHCrV.png'); + } + + #a2 { + background-image: url('image-a.svg'); + } + + #d1 { + background-image: url('assets/image-b-BgQHKcRn.png'); + } + + #d2 { + background-image: url('./image-b.svg'); + } + `); + }); +}); From 2b750dc919d3bdc634ae144ff5d4d42b1842aae8 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Thu, 30 Oct 2025 14:09:08 +0100 Subject: [PATCH 02/21] WIP2 --- docs/docs/building/rollup-plugin-html.md | 13 +- packages/rollup-plugin-html/package.json | 2 + .../src/RollupPluginHTMLOptions.ts | 6 +- .../rollup-plugin-html/src/assets/utils.ts | 27 +- .../rollup-plugin-html/src/input/InputData.ts | 2 +- .../src/input/extract/extractAssets.ts | 6 +- .../input/extract/extractModulesAndAssets.ts | 2 +- .../src/input/getInputData.ts | 13 +- .../src/output/emitAssets.ts | 76 +++-- .../src/output/injectedUpdatedAssetPaths.ts | 2 +- .../src/rollupPluginHTML.ts | 4 +- .../rollup-plugin-html/test-new/new.test.ts | 313 ++++++++++++++++-- packages/storybook-builder/src/index.ts | 1 - 13 files changed, 392 insertions(+), 75 deletions(-) diff --git a/docs/docs/building/rollup-plugin-html.md b/docs/docs/building/rollup-plugin-html.md index f3b805245..55ef75037 100644 --- a/docs/docs/building/rollup-plugin-html.md +++ b/docs/docs/building/rollup-plugin-html.md @@ -127,7 +127,9 @@ export default { #### Including assets referenced from css -If your css files reference other assets via `url`, like for example: +TODO: update + +Your css files reference other assets via `url`, like for example: ```css body { @@ -141,15 +143,6 @@ body { } ``` -You can enable the `bundleAssetsFromCss` option: - -```js -rollupPluginHTML({ - bundleAssetsFromCss: true, - // ...etc -}); -``` - And those assets will get output to the `assets/` dir, and the source css file will get updated with the output locations of those assets, e.g.: ```css diff --git a/packages/rollup-plugin-html/package.json b/packages/rollup-plugin-html/package.json index 2f6514120..5b92b8c5e 100644 --- a/packages/rollup-plugin-html/package.json +++ b/packages/rollup-plugin-html/package.json @@ -28,6 +28,8 @@ "demo:mpa": "rm -rf demo/dist && rollup -c demo/mpa/rollup.config.js --watch & npm run serve-demo", "demo:spa": "rm -rf demo/dist && rollup -c demo/spa/rollup.config.js --watch & npm run serve-demo", "serve-demo": "node ../dev-server/dist/bin.js --watch --root-dir demo/dist --app-index index.html --compatibility none --open", + "test:experiments:node": "mocha test-experiments/**/*.test.ts --require ts-node/register --reporter dot", + "test:experiments:watch": "mocha test-experiments/**/*.test.ts --require ts-node/register --watch --watch-files src,test", "test:node": "mocha test-new/**/*.test.ts --require ts-node/register --reporter dot", "test:watch": "mocha test-new/**/*.test.ts --require ts-node/register --watch --watch-files src,test" }, diff --git a/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts b/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts index c65198af3..26a34fb6a 100644 --- a/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts +++ b/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts @@ -27,8 +27,8 @@ export interface RollupPluginHTMLOptions { transformAsset?: TransformAssetFunction | TransformAssetFunction[]; /** Transform HTML file before output. */ transformHtml?: TransformHtmlFunction | TransformHtmlFunction[]; - /** Whether to extract and bundle assets referenced in HTML. Defaults to true. */ - extractAssets?: boolean; + /** Whether to extract and bundle assets referenced in HTML and CSS. Defaults to true. */ + extractAssets?: boolean | 'legacy-html' | 'legacy-html-and-css'; /** Whether to ignore assets referenced in HTML and CSS with glob patterns. */ externalAssets?: string | string[]; /** Define a full absolute url to your site (e.g. https://domain.com) */ @@ -43,8 +43,6 @@ export interface RollupPluginHTMLOptions { absolutePathPrefix?: string; /** When set to true, will insert meta tags for CSP and add script-src values for inline scripts by sha256-hashing the contents */ strictCSPInlineScripts?: boolean; - /** Bundle assets reference from CSS via `url` */ - bundleAssetsFromCss?: boolean; } export interface GeneratedBundle { diff --git a/packages/rollup-plugin-html/src/assets/utils.ts b/packages/rollup-plugin-html/src/assets/utils.ts index 5af29a52f..8159575a5 100644 --- a/packages/rollup-plugin-html/src/assets/utils.ts +++ b/packages/rollup-plugin-html/src/assets/utils.ts @@ -5,8 +5,21 @@ import { findElements, getTagName, getAttribute } from '@web/parse5-utils'; import { createError } from '../utils.js'; import { serialize } from 'v8'; -const hashedLinkRels = ['stylesheet']; -const linkRels = [...hashedLinkRels, 'icon', 'manifest', 'apple-touch-icon', 'mask-icon']; +const assetLinkRels: Record boolean)> = { + icon: true, + 'apple-touch-icon': true, + 'mask-icon': true, + stylesheet: true, + manifest: true, + // TODO: write a separate tests for these + preload: (node: Element) => { + return ['font', 'image', 'style'].includes(getAttribute(node, 'as') || ''); + }, + prefetch: true, + modulepreload: true, +}; +const legacyHashedLinkRels = ['stylesheet']; +const assetMetaProperties = ['og:image']; function getSrcSetUrls(srcset: string) { if (!srcset) { @@ -42,12 +55,14 @@ function isAsset(node: Element) { } break; case 'link': - if (linkRels.includes(getAttribute(node, 'rel') ?? '')) { + // eslint-disable-next-line no-case-declarations + const linkCheck = assetLinkRels[getAttribute(node, 'rel') ?? ''] || false; + if (typeof linkCheck === 'function' ? linkCheck(node) : linkCheck) { path = getAttribute(node, 'href') ?? ''; } break; case 'meta': - if (getAttribute(node, 'property') === 'og:image' && getAttribute(node, 'content')) { + if (assetMetaProperties.includes(getAttribute(node, 'property') ?? '')) { path = getAttribute(node, 'content') ?? ''; } break; @@ -70,7 +85,7 @@ function isAsset(node: Element) { } } -export function isHashedAsset(node: Element) { +export function isHashedAsset(node: Element, legacy = false) { switch (getTagName(node)) { case 'img': return true; @@ -79,7 +94,7 @@ export function isHashedAsset(node: Element) { case 'script': return true; case 'link': - return hashedLinkRels.includes(getAttribute(node, 'rel')!); + return (legacy ? legacyHashedLinkRels : assetLinkRels).includes(getAttribute(node, 'rel')!); case 'meta': return true; default: diff --git a/packages/rollup-plugin-html/src/input/InputData.ts b/packages/rollup-plugin-html/src/input/InputData.ts index d08a217ef..2d5838856 100644 --- a/packages/rollup-plugin-html/src/input/InputData.ts +++ b/packages/rollup-plugin-html/src/input/InputData.ts @@ -2,7 +2,7 @@ import { ScriptModuleTag } from '../RollupPluginHTMLOptions'; export interface InputAsset { filePath: string; - hashed: boolean; + legacyHashed: boolean; content: Buffer; } diff --git a/packages/rollup-plugin-html/src/input/extract/extractAssets.ts b/packages/rollup-plugin-html/src/input/extract/extractAssets.ts index b2ae8b8fc..7c6a0c4f9 100644 --- a/packages/rollup-plugin-html/src/input/extract/extractAssets.ts +++ b/packages/rollup-plugin-html/src/input/extract/extractAssets.ts @@ -36,7 +36,9 @@ export function extractAssets(params: ExtractAssetsParams): InputAsset[] { params.absolutePathPrefix, ); const hashed = isHashedAsset(node); - const alreadyHandled = allAssets.find(a => a.filePath === filePath && a.hashed === hashed); + const alreadyHandled = allAssets.find( + a => a.filePath === filePath && a.legacyHashed === hashed, + ); if (!alreadyHandled) { try { fs.accessSync(filePath); @@ -49,7 +51,7 @@ export function extractAssets(params: ExtractAssetsParams): InputAsset[] { } const content = fs.readFileSync(filePath); - allAssets.push({ filePath, hashed, content }); + allAssets.push({ filePath, legacyHashed: hashed, content }); } } } diff --git a/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts b/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts index b06d59ded..d2dc9805c 100644 --- a/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts +++ b/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts @@ -7,7 +7,7 @@ export interface ExtractParams { html: string; htmlFilePath: string; rootDir: string; - extractAssets: boolean; + extractAssets: boolean | 'legacy-html' | 'legacy-html-and-css'; externalAssets?: string | string[]; absolutePathPrefix?: string; } diff --git a/packages/rollup-plugin-html/src/input/getInputData.ts b/packages/rollup-plugin-html/src/input/getInputData.ts index 102cb062e..9d217c5f5 100644 --- a/packages/rollup-plugin-html/src/input/getInputData.ts +++ b/packages/rollup-plugin-html/src/input/getInputData.ts @@ -30,14 +30,21 @@ export interface CreateInputDataParams { html: string; rootDir: string; filePath?: string; - extractAssets: boolean; + extractAssets: boolean | 'legacy-html' | 'legacy-html-and-css'; externalAssets?: string | string[]; absolutePathPrefix?: string; } function createInputData(params: CreateInputDataParams): InputData { - const { name, html, rootDir, filePath, extractAssets, externalAssets, absolutePathPrefix } = - params; + const { + name, + html, + rootDir, + filePath, + extractAssets = true, + externalAssets, + absolutePathPrefix, + } = params; const htmlFilePath = filePath ? filePath : path.resolve(rootDir, name); const result = extractModulesAndAssets({ html, diff --git a/packages/rollup-plugin-html/src/output/emitAssets.ts b/packages/rollup-plugin-html/src/output/emitAssets.ts index 19a790ce4..010b477ef 100644 --- a/packages/rollup-plugin-html/src/output/emitAssets.ts +++ b/packages/rollup-plugin-html/src/output/emitAssets.ts @@ -4,12 +4,13 @@ import { 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'; export interface EmittedAssets { static: Map; - hashed: Map; + legacyHashed: Map; } const allowedFileExtensions = [ @@ -39,6 +40,11 @@ export async function emitAssets( inputs: InputData[], options: RollupPluginHTMLOptions, ) { + console.log('emitAssets this', this); + console.log('emitAssets inputs', inputs); + console.log('emitAssets options', options); + const extractAssets = options.extractAssets ?? true; + const extractAssetsLegacyCss = options.extractAssets === 'legacy-html-and-css'; const emittedStaticAssets = new Map(); const emittedHashedAssets = new Map(); const emittedStaticAssetNames = new Set(); @@ -52,12 +58,12 @@ export async function emitAssets( } } const staticAssets: InputAsset[] = []; - const hashedAssets: InputAsset[] = []; + const legacyHashedAssets: InputAsset[] = []; for (const input of inputs) { for (const asset of input.assets) { - if (asset.hashed) { - hashedAssets.push(asset); + if (asset.legacyHashed) { + legacyHashedAssets.push(asset); } else { staticAssets.push(asset); } @@ -65,10 +71,10 @@ export async function emitAssets( } // ensure static assets are last because of https://github.com/rollup/rollup/issues/3853 - const allAssets = [...hashedAssets, ...staticAssets]; + const allAssets = [...legacyHashedAssets, ...staticAssets]; for (const asset of allAssets) { - const map = asset.hashed ? emittedHashedAssets : emittedStaticAssets; + const map = asset.legacyHashed ? emittedHashedAssets : emittedStaticAssets; if (!map.has(asset.filePath)) { let source: Buffer = asset.content; @@ -84,8 +90,9 @@ export async function emitAssets( let basename = path.basename(asset.filePath); const isExternal = createAssetPicomatchMatcher(options.externalAssets); const emittedExternalAssets = new Map(); - if (asset.hashed) { - if (basename.endsWith('.css') && options.bundleAssetsFromCss) { + if (asset.legacyHashed) { + console.log('asset', asset); + if (basename.endsWith('.css') && extractAssets) { let updatedCssSource = false; const { code } = await transform({ filename: basename, @@ -104,22 +111,48 @@ export async function emitAssets( // Avoid duplicates if (!emittedExternalAssets.has(assetLocation)) { - const fontFileRef = this.emitFile({ + const basename = path.basename(filePath); + const fileRef = this.emitFile({ type: 'asset', - name: path.join('assets', path.basename(filePath)), + name: extractAssetsLegacyCss ? path.join('assets', basename) : basename, source: assetContent, }); - const emittedAssetFilePath = path.basename(this.getFileName(fontFileRef)); - emittedExternalAssets.set(assetLocation, emittedAssetFilePath); + const emittedAssetFilepath = this.getFileName(fileRef); + const emittedAssetBasename = path.basename(emittedAssetFilepath); + console.log('1', emittedAssetBasename); + emittedExternalAssets.set(assetLocation, emittedAssetFilepath); // Update the URL in the original CSS file to point to the emitted asset file - url.url = `assets/${ - idRef ? `${emittedAssetFilePath}#${idRef}` : 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}`; + } } else { - const emittedAssetFilePath = emittedExternalAssets.get(assetLocation); - url.url = `assets/${ - idRef ? `${emittedAssetFilePath}#${idRef}` : emittedAssetFilePath - }`; + 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}`; + } } } updatedCssSource = true; @@ -132,6 +165,7 @@ export async function emitAssets( } } + console.log('2', basename); ref = this.emitFile({ type: 'asset', name: basename, source }); } else { // ensure the output filename is unique @@ -142,7 +176,9 @@ export async function emitAssets( i += 1; } emittedStaticAssetNames.add(basename); + // TODO: not sure what to do with this one yet const fileName = `assets/${basename}`; + console.log('3', basename, fileName); ref = this.emitFile({ type: 'asset', name: basename, fileName, source }); } @@ -150,5 +186,5 @@ export async function emitAssets( } } - return { static: emittedStaticAssets, hashed: emittedHashedAssets }; + return { static: emittedStaticAssets, legacyHashed: emittedHashedAssets }; } diff --git a/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts b/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts index e5ba484ae..9ef5c2dca 100644 --- a/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts +++ b/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts @@ -59,7 +59,7 @@ export function injectedUpdatedAssetPaths(args: InjectUpdatedAssetPathsArgs) { const htmlFilePath = input.filePath ? input.filePath : path.join(rootDir, input.name); const htmlDir = path.dirname(htmlFilePath); const filePath = resolveAssetFilePath(sourcePath, htmlDir, rootDir, absolutePathPrefix); - const assetPaths = isHashedAsset(node) ? emittedAssets.hashed : emittedAssets.static; + const assetPaths = isHashedAsset(node) ? emittedAssets.legacyHashed : emittedAssets.static; const relativeOutputPath = assetPaths.get(filePath); if (!relativeOutputPath) { diff --git a/packages/rollup-plugin-html/src/rollupPluginHTML.ts b/packages/rollup-plugin-html/src/rollupPluginHTML.ts index ffe1a0880..97963ab14 100644 --- a/packages/rollup-plugin-html/src/rollupPluginHTML.ts +++ b/packages/rollup-plugin-html/src/rollupPluginHTML.ts @@ -60,6 +60,9 @@ export function rollupPluginHTML(pluginOptions: RollupPluginHTMLOptions = {}): R moduleImports.push(NOOP_IMPORT); } + if (pluginOptions.extractAssets) { + } + if (pluginOptions.serviceWorkerPath) { serviceWorkerPath = pluginOptions.serviceWorkerPath; } @@ -72,7 +75,6 @@ export function rollupPluginHTML(pluginOptions: RollupPluginHTMLOptions = {}): R if (pluginOptions.strictCSPInlineScripts) { strictCSPInlineScripts = pluginOptions.strictCSPInlineScripts; } - pluginOptions.bundleAssetsFromCss = !!pluginOptions.bundleAssetsFromCss; if (pluginOptions.input == null) { // we are reading rollup input, so replace whatever was there diff --git a/packages/rollup-plugin-html/test-new/new.test.ts b/packages/rollup-plugin-html/test-new/new.test.ts index e98a8438f..96de57d57 100644 --- a/packages/rollup-plugin-html/test-new/new.test.ts +++ b/packages/rollup-plugin-html/test-new/new.test.ts @@ -8,6 +8,9 @@ import { rollupPluginHTML } from '../src/index.js'; // TODO: test output "fileName" too, like the real output name, not always it's properly checked besides checking the index.html source +// TODO: write tests for 'legacy-html' (this is when for CSS they are not extracted) and 'legacy-html-and-css' separately +// TODO: write a test for 'shortcut icon' + function collapseWhitespaceAll(str: string) { return ( str && @@ -1427,7 +1430,7 @@ describe('rollup-plugin-html', () => { `); }); - // TODO: this will probably go away + // TODO: this will probably go away (or rewrite this test to a test of a filter for which files to hash and which not) it('static and hashed asset nodes can reference the same files', async () => { const rootDir = createApp({ 'foo.svg': svg``, @@ -1947,7 +1950,7 @@ describe('rollup-plugin-html', () => { `); }); - it('handles fonts linked from css files', async () => { + it('[new] handles fonts linked from css files', async () => { const rootDir = createApp({ 'fonts/font-bold.woff2': 'font-bold', 'fonts/font-normal.woff2': 'font-normal', @@ -1973,7 +1976,6 @@ describe('rollup-plugin-html', () => { const config = { plugins: [ rollupPluginHTML({ - bundleAssetsFromCss: true, rootDir, input: { html: html` @@ -1992,8 +1994,86 @@ describe('rollup-plugin-html', () => { const build = await rollup(config); const { assets } = await generateTestBundle(build, outputConfig); - // TODO: this looks to be adding extra "/assets/" folder layer - // TODO: actually totally unclear why the names are not hashed here, but the folder has changed, like half-baked + expect(assets).to.have.keys([ + 'assets/font-normal-Cht9ZB76.woff2', + 'assets/font-bold-eQjSonqH.woff2', + 'assets/styles-Dhs3ufep.css', + 'index.html', + ]); + + expect(assets['assets/styles-Dhs3ufep.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('font-bold-eQjSonqH.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + }); + + it('[legacy] handles fonts linked from css files', async () => { + const rootDir = createApp({ + 'fonts/font-bold.woff2': 'font-bold', + 'fonts/font-normal.woff2': 'font-normal', + 'styles.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + extractAssets: 'legacy-html-and-css', + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + expect(assets).to.have.keys([ 'assets/assets/font-normal-Cht9ZB76.woff2', 'assets/assets/font-bold-eQjSonqH.woff2', @@ -2029,7 +2109,7 @@ describe('rollup-plugin-html', () => { `); }); - it('handles fonts linked from css files in node_modules', async () => { + it('[new] handles fonts linked from css files in node_modules', async () => { const rootDir = createApp({ 'node_modules/foo/fonts/font-bold.woff2': 'font-bold', 'node_modules/foo/fonts/font-normal.woff2': 'font-normal', @@ -2055,7 +2135,6 @@ describe('rollup-plugin-html', () => { const config = { plugins: [ rollupPluginHTML({ - bundleAssetsFromCss: true, rootDir, input: { html: html` @@ -2074,8 +2153,86 @@ describe('rollup-plugin-html', () => { const build = await rollup(config); const { assets } = await generateTestBundle(build, outputConfig); - // TODO: this looks to be adding extra "/assets/" folder layer - // TODO: actually totally unclear why the names are not hashed here, but the folder has changed, like half-baked + expect(assets).to.have.keys([ + 'assets/font-normal-Cht9ZB76.woff2', + 'assets/font-bold-eQjSonqH.woff2', + 'assets/styles-Dhs3ufep.css', + 'index.html', + ]); + + expect(assets['assets/styles-Dhs3ufep.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('font-bold-eQjSonqH.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + }); + + it('[legacy] handles fonts linked from css files in node_modules', async () => { + const rootDir = createApp({ + 'node_modules/foo/fonts/font-bold.woff2': 'font-bold', + 'node_modules/foo/fonts/font-normal.woff2': 'font-normal', + 'node_modules/foo/styles.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + extractAssets: 'legacy-html-and-css', + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + expect(assets).to.have.keys([ 'assets/assets/font-normal-Cht9ZB76.woff2', 'assets/assets/font-bold-eQjSonqH.woff2', @@ -2138,7 +2295,6 @@ describe('rollup-plugin-html', () => { const config = { plugins: [ rollupPluginHTML({ - bundleAssetsFromCss: true, rootDir, input: { html: html` @@ -2162,7 +2318,119 @@ describe('rollup-plugin-html', () => { expect(fonts.length).to.equal(1); }); - it('handles images referenced from css', async () => { + it('[new] handles images referenced from css', async () => { + const rootDir = createApp({ + 'images/star.avif': 'star.avif', + 'images/star.gif': 'star.gif', + 'images/star.jpeg': 'star.jpeg', + 'images/star.jpg': 'star.jpg', + 'images/star.png': 'star.png', + 'images/star.svg': 'star.svg', + 'images/star.webp': 'star.webp', + 'styles.css': css` + #a { + background-image: url('images/star.avif'); + } + + #b { + background-image: url('images/star.gif'); + } + + #c { + background-image: url('images/star.jpeg'); + } + + #d { + background-image: url('images/star.jpg'); + } + + #e { + background-image: url('images/star.png'); + } + + #f { + background-image: url('images/star.svg'); + } + + #g { + background-image: url('images/star.svg#foo'); + } + + #h { + background-image: url('images/star.webp'); + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + expect(assets).to.have.keys([ + 'assets/star-D_LO5feX.avif', + 'assets/star-BKg9qmmf.gif', + 'assets/star-BZWqL7hS.jpeg', + 'assets/star-Df0JryvN.jpg', + 'assets/star-CXig10q7.png', + 'assets/star-CwhgM_z4.svg', + 'assets/star-CKbh5mKn.webp', + 'assets/styles-mywkihBc.css', + 'index.html', + ]); + + expect(assets['assets/styles-mywkihBc.css']).to.equal(css` + #a { + background-image: url('star-D_LO5feX.avif'); + } + + #b { + background-image: url('star-BKg9qmmf.gif'); + } + + #c { + background-image: url('star-BZWqL7hS.jpeg'); + } + + #d { + background-image: url('star-Df0JryvN.jpg'); + } + + #e { + background-image: url('star-CXig10q7.png'); + } + + #f { + background-image: url('star-CwhgM_z4.svg'); + } + + #g { + background-image: url('star-CwhgM_z4.svg#foo'); + } + + #h { + background-image: url('star-CKbh5mKn.webp'); + } + `); + }); + + it('[legacy] handles images referenced from css', async () => { const rootDir = createApp({ 'images/star.avif': 'star.avif', 'images/star.gif': 'star.gif', @@ -2209,8 +2477,8 @@ describe('rollup-plugin-html', () => { const config = { plugins: [ rollupPluginHTML({ - bundleAssetsFromCss: true, rootDir, + extractAssets: 'legacy-html-and-css', input: { html: html` @@ -2228,8 +2496,6 @@ describe('rollup-plugin-html', () => { const build = await rollup(config); const { assets } = await generateTestBundle(build, outputConfig); - // TODO: this looks to be adding extra "/assets/" folder layer - // TODO: actually totally unclear why the names are not hashed here, but the folder has changed, like half-baked expect(assets).to.have.keys([ 'assets/assets/star-D_LO5feX.avif', 'assets/assets/star-BKg9qmmf.gif', @@ -2316,7 +2582,6 @@ describe('rollup-plugin-html', () => { const config = { plugins: [ rollupPluginHTML({ - bundleAssetsFromCss: true, externalAssets: ['**/foo/**/*', '*.svg'], rootDir, input: { @@ -2348,14 +2613,12 @@ describe('rollup-plugin-html', () => { const build = await rollup(config); const { assets } = await generateTestBundle(build, outputConfig); - // TODO: this looks to be adding extra "/assets/" folder layer - // TODO: actually totally unclear why the names are not hashed here, but the folder has changed, like half-baked expect(assets).to.have.keys([ - 'assets/assets/image-a-XOCPHCrV.png', - 'assets/assets/image-b-BgQHKcRn.png', + 'assets/image-a-XOCPHCrV.png', + 'assets/image-b-BgQHKcRn.png', 'assets/image-a.png', 'assets/image-b.png', - 'assets/styles-DFIb0lB5.css', + 'assets/styles-Bv-4gk2N.css', 'assets/webmanifest.json', 'index.html', ]); @@ -2368,12 +2631,12 @@ describe('rollup-plugin-html', () => { - + - +
@@ -2381,9 +2644,9 @@ describe('rollup-plugin-html', () => { `); - expect(assets['assets/styles-DFIb0lB5.css']).to.equal(css` + expect(assets['assets/styles-Bv-4gk2N.css']).to.equal(css` #a1 { - background-image: url('assets/image-a-XOCPHCrV.png'); + background-image: url('image-a-XOCPHCrV.png'); } #a2 { @@ -2391,7 +2654,7 @@ describe('rollup-plugin-html', () => { } #d1 { - background-image: url('assets/image-b-BgQHKcRn.png'); + background-image: url('image-b-BgQHKcRn.png'); } #d2 { diff --git a/packages/storybook-builder/src/index.ts b/packages/storybook-builder/src/index.ts index 99f4b1199..410e66d6d 100644 --- a/packages/storybook-builder/src/index.ts +++ b/packages/storybook-builder/src/index.ts @@ -164,7 +164,6 @@ export const build: WdsBuilder['build'] = async ({ startTime, options }) => { rollupPluginHTML({ input: { html: await generateIframeHtml(options), name: 'iframe.html' }, extractAssets: true, - bundleAssetsFromCss: true, externalAssets: 'sb-common-assets/**', }), rollupPluginNodeResolve(), From 93d94c121ec2b68f53173669b6d9d173ffa3ad4e Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Tue, 16 Dec 2025 12:27:54 +0100 Subject: [PATCH 03/21] WIP3 --- .../rollup-plugin-html/src/assets/utils.ts | 20 +- .../src/input/extract/extractAssets.ts | 3 +- .../input/extract/extractModulesAndAssets.ts | 1 + .../src/output/emitAssets.ts | 7 - .../src/output/getOutputHTML.ts | 1 + .../src/output/injectedUpdatedAssetPaths.ts | 6 +- .../src/rollupPluginHTML.ts | 3 - .../test-experiments/experiments.test.ts | 9 +- .../rollup-plugin-html/test-new/new.test.ts | 273 +++++++++++++++++- 9 files changed, 289 insertions(+), 34 deletions(-) diff --git a/packages/rollup-plugin-html/src/assets/utils.ts b/packages/rollup-plugin-html/src/assets/utils.ts index 8159575a5..2441440d5 100644 --- a/packages/rollup-plugin-html/src/assets/utils.ts +++ b/packages/rollup-plugin-html/src/assets/utils.ts @@ -12,6 +12,8 @@ const assetLinkRels: Record boolean)> = { stylesheet: true, manifest: true, // TODO: write a separate tests for these + // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload + // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/prefetch preload: (node: Element) => { return ['font', 'image', 'style'].includes(getAttribute(node, 'as') || ''); }, @@ -54,13 +56,13 @@ function isAsset(node: Element) { path = extractFirstUrlOfSrcSet(node) ?? ''; } break; - case 'link': - // eslint-disable-next-line no-case-declarations + case 'link': { const linkCheck = assetLinkRels[getAttribute(node, 'rel') ?? ''] || false; if (typeof linkCheck === 'function' ? linkCheck(node) : linkCheck) { path = getAttribute(node, 'href') ?? ''; } break; + } case 'meta': if (assetMetaProperties.includes(getAttribute(node, 'property') ?? '')) { path = getAttribute(node, 'content') ?? ''; @@ -85,7 +87,10 @@ function isAsset(node: Element) { } } -export function isHashedAsset(node: Element, legacy = false) { +export function isHashedAsset( + node: Element, + extractAssets: boolean | 'legacy-html' | 'legacy-html-and-css', +) { switch (getTagName(node)) { case 'img': return true; @@ -93,8 +98,13 @@ export function isHashedAsset(node: Element, legacy = false) { return true; case 'script': return true; - case 'link': - return (legacy ? legacyHashedLinkRels : assetLinkRels).includes(getAttribute(node, 'rel')!); + case 'link': { + if (extractAssets === 'legacy-html' || extractAssets === 'legacy-html-and-css') { + return legacyHashedLinkRels.includes(getAttribute(node, 'rel') ?? ''); + } else { + return true; + } + } case 'meta': return true; default: diff --git a/packages/rollup-plugin-html/src/input/extract/extractAssets.ts b/packages/rollup-plugin-html/src/input/extract/extractAssets.ts index 7c6a0c4f9..a026c8c31 100644 --- a/packages/rollup-plugin-html/src/input/extract/extractAssets.ts +++ b/packages/rollup-plugin-html/src/input/extract/extractAssets.ts @@ -15,6 +15,7 @@ export interface ExtractAssetsParams { htmlFilePath: string; htmlDir: string; rootDir: string; + extractAssets: boolean | 'legacy-html' | 'legacy-html-and-css'; externalAssets?: string | string[]; absolutePathPrefix?: string; } @@ -35,7 +36,7 @@ export function extractAssets(params: ExtractAssetsParams): InputAsset[] { params.rootDir, params.absolutePathPrefix, ); - const hashed = isHashedAsset(node); + const hashed = isHashedAsset(node, params.extractAssets); const alreadyHandled = allAssets.find( a => a.filePath === filePath && a.legacyHashed === hashed, ); diff --git a/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts b/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts index d2dc9805c..0deea609f 100644 --- a/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts +++ b/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts @@ -30,6 +30,7 @@ export function extractModulesAndAssets(params: ExtractParams) { htmlDir, htmlFilePath, rootDir, + extractAssets: params.extractAssets, externalAssets, absolutePathPrefix, }) diff --git a/packages/rollup-plugin-html/src/output/emitAssets.ts b/packages/rollup-plugin-html/src/output/emitAssets.ts index 010b477ef..831bc387e 100644 --- a/packages/rollup-plugin-html/src/output/emitAssets.ts +++ b/packages/rollup-plugin-html/src/output/emitAssets.ts @@ -40,9 +40,6 @@ export async function emitAssets( inputs: InputData[], options: RollupPluginHTMLOptions, ) { - console.log('emitAssets this', this); - console.log('emitAssets inputs', inputs); - console.log('emitAssets options', options); const extractAssets = options.extractAssets ?? true; const extractAssetsLegacyCss = options.extractAssets === 'legacy-html-and-css'; const emittedStaticAssets = new Map(); @@ -91,7 +88,6 @@ export async function emitAssets( const isExternal = createAssetPicomatchMatcher(options.externalAssets); const emittedExternalAssets = new Map(); if (asset.legacyHashed) { - console.log('asset', asset); if (basename.endsWith('.css') && extractAssets) { let updatedCssSource = false; const { code } = await transform({ @@ -119,7 +115,6 @@ export async function emitAssets( }); const emittedAssetFilepath = this.getFileName(fileRef); const emittedAssetBasename = path.basename(emittedAssetFilepath); - console.log('1', emittedAssetBasename); emittedExternalAssets.set(assetLocation, emittedAssetFilepath); // Update the URL in the original CSS file to point to the emitted asset file if (extractAssetsLegacyCss) { @@ -165,7 +160,6 @@ export async function emitAssets( } } - console.log('2', basename); ref = this.emitFile({ type: 'asset', name: basename, source }); } else { // ensure the output filename is unique @@ -178,7 +172,6 @@ export async function emitAssets( emittedStaticAssetNames.add(basename); // TODO: not sure what to do with this one yet const fileName = `assets/${basename}`; - console.log('3', basename, fileName); ref = this.emitFile({ type: 'asset', name: basename, fileName, source }); } diff --git a/packages/rollup-plugin-html/src/output/getOutputHTML.ts b/packages/rollup-plugin-html/src/output/getOutputHTML.ts index 7f2725ae6..9b39ea7c4 100644 --- a/packages/rollup-plugin-html/src/output/getOutputHTML.ts +++ b/packages/rollup-plugin-html/src/output/getOutputHTML.ts @@ -53,6 +53,7 @@ export async function getOutputHTML(params: GetOutputHTMLParams) { outputDir, rootDir, emittedAssets, + extractAssets: pluginOptions.extractAssets, externalAssets: pluginOptions.externalAssets, absolutePathPrefix, publicPath: pluginOptions.publicPath, diff --git a/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts b/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts index 9ef5c2dca..0990cd89e 100644 --- a/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts +++ b/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts @@ -21,6 +21,7 @@ export interface InjectUpdatedAssetPathsArgs { outputDir: string; rootDir: string; emittedAssets: EmittedAssets; + extractAssets?: boolean | 'legacy-html' | 'legacy-html-and-css'; externalAssets?: string | string[]; publicPath?: string; absolutePathPrefix?: string; @@ -44,6 +45,7 @@ export function injectedUpdatedAssetPaths(args: InjectUpdatedAssetPathsArgs) { outputDir, rootDir, emittedAssets, + extractAssets = true, externalAssets, publicPath = './', absolutePathPrefix, @@ -59,7 +61,9 @@ export function injectedUpdatedAssetPaths(args: InjectUpdatedAssetPathsArgs) { const htmlFilePath = input.filePath ? input.filePath : path.join(rootDir, input.name); const htmlDir = path.dirname(htmlFilePath); const filePath = resolveAssetFilePath(sourcePath, htmlDir, rootDir, absolutePathPrefix); - const assetPaths = isHashedAsset(node) ? emittedAssets.legacyHashed : emittedAssets.static; + const assetPaths = isHashedAsset(node, extractAssets) + ? emittedAssets.legacyHashed + : emittedAssets.static; const relativeOutputPath = assetPaths.get(filePath); if (!relativeOutputPath) { diff --git a/packages/rollup-plugin-html/src/rollupPluginHTML.ts b/packages/rollup-plugin-html/src/rollupPluginHTML.ts index 97963ab14..388c32967 100644 --- a/packages/rollup-plugin-html/src/rollupPluginHTML.ts +++ b/packages/rollup-plugin-html/src/rollupPluginHTML.ts @@ -60,9 +60,6 @@ export function rollupPluginHTML(pluginOptions: RollupPluginHTMLOptions = {}): R moduleImports.push(NOOP_IMPORT); } - if (pluginOptions.extractAssets) { - } - if (pluginOptions.serviceWorkerPath) { serviceWorkerPath = pluginOptions.serviceWorkerPath; } diff --git a/packages/rollup-plugin-html/test-experiments/experiments.test.ts b/packages/rollup-plugin-html/test-experiments/experiments.test.ts index 8bbbec3c8..76d0aa9e6 100644 --- a/packages/rollup-plugin-html/test-experiments/experiments.test.ts +++ b/packages/rollup-plugin-html/test-experiments/experiments.test.ts @@ -1,6 +1,6 @@ import synchronizedPrettier from '@prettier/sync'; import * as prettier from 'prettier'; -import { rollup, OutputChunk, OutputOptions, Plugin, RollupBuild } from 'rollup'; +import { rollup, OutputOptions, RollupBuild } from 'rollup'; import { expect } from 'chai'; import path from 'path'; import fs from 'fs'; @@ -46,9 +46,6 @@ const css = (strings: TemplateStringsArray, ...values: string[]) => const js = (strings: TemplateStringsArray, ...values: string[]) => extnameToFormatter['.js'](merge(strings, ...values)); -const svg = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.svg'](merge(strings, ...values)); - const outputConfig: OutputOptions = { format: 'es', dir: 'dist', @@ -165,7 +162,7 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { output, assets } = await generateTestBundle(build, { + const { assets } = await generateTestBundle(build, { ...outputConfig, assetFileNames: 'static/[name].immutable.[hash][extname]', }); @@ -264,7 +261,7 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { output, assets } = await generateTestBundle(build, { + const { assets } = await generateTestBundle(build, { ...outputConfig, assetFileNames: assetInfo => { const name = assetInfo.names[0] || ''; diff --git a/packages/rollup-plugin-html/test-new/new.test.ts b/packages/rollup-plugin-html/test-new/new.test.ts index 96de57d57..97177e44f 100644 --- a/packages/rollup-plugin-html/test-new/new.test.ts +++ b/packages/rollup-plugin-html/test-new/new.test.ts @@ -1300,7 +1300,7 @@ describe('rollup-plugin-html', () => { `); }); - it('includes referenced assets in the bundle', async () => { + it('[new] includes referenced assets in the bundle', async () => { const rootDir = createApp({ 'image-a.png': 'image-a.png', 'image-b.png': 'image-b.png', @@ -1357,6 +1357,99 @@ describe('rollup-plugin-html', () => { const build = await rollup(config); const { chunks, assets } = await generateTestBundle(build, outputConfig); + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(assets).to.have.keys([ + 'assets/image-a-XOCPHCrV.png', + 'assets/image-b-BgQHKcRn.png', + 'assets/image-c-C4yLPiIL.png', + 'assets/image-a-BCCvKrTe.svg', + 'assets/image-b-C4stzVZW.svg', + 'assets/styles-CF2Iy5n1.css', + 'assets/x-DDGg8O6h.css', + 'assets/y-DJTrnPH3.css', + 'assets/webmanifest-BkrOR1WG.json', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + +
+ +
+ + + `); + }); + + it('[legacy] includes referenced assets in the bundle', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + 'image-b.png': 'image-b.png', + 'image-c.png': 'image-c.png', + 'image-a.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + :root { + color: blue; + } + `, + 'foo/x.css': css` + :root { + color: x; + } + `, + 'foo/bar/y.css': css` + :root { + color: y; + } + `, + 'webmanifest.json': { message: 'hello world' }, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + extractAssets: 'legacy-html-and-css', + input: { + html: html` + + + + + + + + + + + + +
+ +
+ + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + expect(Object.keys(chunks)).to.have.lengthOf(1); expect(assets).to.have.keys([ 'assets/image-a.png', @@ -1392,7 +1485,7 @@ describe('rollup-plugin-html', () => { `); }); - it('deduplicates static assets with similar names', async () => { + it('[new] does not deduplicate static assets with similar names', async () => { const rootDir = createApp({ 'foo.svg': svg``, 'x/foo.svg': svg``, @@ -1419,6 +1512,45 @@ describe('rollup-plugin-html', () => { const build = await rollup(config); const { assets } = await generateTestBundle(build, outputConfig); + expect(assets['index.html']).to.equal(html` + + + + + + + + `); + }); + + it('[legacy] deduplicates static assets with similar names', async () => { + const rootDir = createApp({ + 'foo.svg': svg``, + 'x/foo.svg': svg``, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + extractAssets: 'legacy-html-and-css', + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + expect(assets['index.html']).to.equal(html` @@ -1430,8 +1562,7 @@ describe('rollup-plugin-html', () => { `); }); - // TODO: this will probably go away (or rewrite this test to a test of a filter for which files to hash and which not) - it('static and hashed asset nodes can reference the same files', async () => { + it('[legacy] static and hashed asset nodes can reference the same files', async () => { const rootDir = createApp({ 'foo.svg': svg``, }); @@ -1440,6 +1571,7 @@ describe('rollup-plugin-html', () => { plugins: [ rollupPluginHTML({ rootDir, + extractAssets: 'legacy-html-and-css', input: { html: html` @@ -2543,7 +2675,7 @@ describe('rollup-plugin-html', () => { `); }); - it('allows to exclude external assets usign a glob pattern', async () => { + it('[new] allows to exclude external assets usign a glob pattern', async () => { const rootDir = createApp({ 'image-a.png': 'image-a.png', 'image-b.png': 'image-b.png', @@ -2616,19 +2748,17 @@ describe('rollup-plugin-html', () => { expect(assets).to.have.keys([ 'assets/image-a-XOCPHCrV.png', 'assets/image-b-BgQHKcRn.png', - 'assets/image-a.png', - 'assets/image-b.png', 'assets/styles-Bv-4gk2N.css', - 'assets/webmanifest.json', + 'assets/webmanifest-BkrOR1WG.json', 'index.html', ]); expect(assets['index.html']).to.equal(html` - - - + + + @@ -2662,4 +2792,125 @@ describe('rollup-plugin-html', () => { } `); }); + + it('[legacy] allows to exclude external assets usign a glob pattern', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + 'image-b.png': 'image-b.png', + 'image-a.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + #a1 { + background-image: url('image-a.png'); + } + + #a2 { + background-image: url('image-a.svg'); + } + + #d1 { + background-image: url('./image-b.png'); + } + + #d2 { + background-image: url('./image-b.svg'); + } + `, + 'foo/x.css': css` + :root { + color: x; + } + `, + 'foo/bar/y.css': css` + :root { + color: y; + } + `, + 'webmanifest.json': { message: 'hello world' }, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + externalAssets: ['**/foo/**/*', '*.svg'], + rootDir, + extractAssets: 'legacy-html-and-css', + input: { + html: html` + + + + + + + + + + + + + +
+ +
+ + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { assets } = await generateTestBundle(build, outputConfig); + + expect(assets).to.have.keys([ + 'assets/assets/image-a-XOCPHCrV.png', + 'assets/assets/image-b-BgQHKcRn.png', + 'assets/image-a.png', + 'assets/image-b.png', + 'assets/styles-DFIb0lB5.css', + 'assets/webmanifest.json', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + + +
+ +
+ + + `); + + expect(assets['assets/styles-DFIb0lB5.css']).to.equal(css` + #a1 { + background-image: url('assets/image-a-XOCPHCrV.png'); + } + + #a2 { + background-image: url('image-a.svg'); + } + + #d1 { + background-image: url('assets/image-b-BgQHKcRn.png'); + } + + #d2 { + background-image: url('./image-b.svg'); + } + `); + }); }); From de764cb4a55d808a2154035933f44f3a63998f41 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Mon, 22 Dec 2025 16:30:14 +0400 Subject: [PATCH 04/21] WIP4 --- .../rollup-plugin-html/test-new/new.test.ts | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/rollup-plugin-html/test-new/new.test.ts b/packages/rollup-plugin-html/test-new/new.test.ts index 97177e44f..756a2f5da 100644 --- a/packages/rollup-plugin-html/test-new/new.test.ts +++ b/packages/rollup-plugin-html/test-new/new.test.ts @@ -9,7 +9,6 @@ import { rollupPluginHTML } from '../src/index.js'; // TODO: test output "fileName" too, like the real output name, not always it's properly checked besides checking the index.html source // TODO: write tests for 'legacy-html' (this is when for CSS they are not extracted) and 'legacy-html-and-css' separately -// TODO: write a test for 'shortcut icon' function collapseWhitespaceAll(str: string) { return ( @@ -2402,7 +2401,6 @@ describe('rollup-plugin-html', () => { it('handles duplicate fonts correctly', async () => { const rootDir = createApp({ - 'fonts/font-bold.woff2': 'font-bold', 'fonts/font-normal.woff2': 'font-normal', 'styles-a.css': css` @font-face { @@ -2415,9 +2413,9 @@ describe('rollup-plugin-html', () => { `, 'styles-b.css': css` @font-face { - font-family: Font; - src: url('fonts/font-bold.woff2') format('woff2'); - font-weight: bold; + font-family: Font2; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; font-style: normal; font-display: swap; } @@ -2444,10 +2442,44 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { output } = await generateTestBundle(build, outputConfig); + const { assets } = await generateTestBundle(build, outputConfig); + + expect(assets).to.have.keys([ + 'assets/font-normal-Cht9ZB76.woff2', + 'assets/styles-a-jFIfrzm8.css', + 'assets/styles-b-B-8m1N7T.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + `); + + expect(assets['assets/styles-a-jFIfrzm8.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `); - const fonts = output.filter(o => o.name?.endsWith('font-normal.woff2')); - expect(fonts.length).to.equal(1); + expect(assets['assets/styles-b-B-8m1N7T.css']).to.equal(css` + @font-face { + font-family: Font2; + src: url('font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `); }); it('[new] handles images referenced from css', async () => { From 6d218f50517141460e532a0fac1e4750ce75fdaf Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Mon, 22 Dec 2025 18:01:45 +0400 Subject: [PATCH 05/21] WIP5 --- .../test-experiments/experiments.test.ts | 10 +- .../rollup-plugin-html/test-new/new.test.ts | 242 +++++++++++++----- 2 files changed, 193 insertions(+), 59 deletions(-) diff --git a/packages/rollup-plugin-html/test-experiments/experiments.test.ts b/packages/rollup-plugin-html/test-experiments/experiments.test.ts index 76d0aa9e6..a53e70233 100644 --- a/packages/rollup-plugin-html/test-experiments/experiments.test.ts +++ b/packages/rollup-plugin-html/test-experiments/experiments.test.ts @@ -162,11 +162,14 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, { + const { chunks, assets } = await generateTestBundle(build, { ...outputConfig, assetFileNames: 'static/[name].immutable.[hash][extname]', }); + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(5); + expect(assets).to.have.keys([ 'static/font.immutable.C5MNjX-h.woff2', 'static/global.immutable.DB0fKkjs.css', @@ -261,7 +264,7 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, { + const { chunks, assets } = await generateTestBundle(build, { ...outputConfig, assetFileNames: assetInfo => { const name = assetInfo.names[0] || ''; @@ -276,6 +279,9 @@ describe('rollup-plugin-html', () => { }, }); + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(5); + expect(assets).to.have.keys([ 'fonts/font.immutable.C5MNjX-h.woff2', 'styles/global.immutable.B3Q0ucg4.css', diff --git a/packages/rollup-plugin-html/test-new/new.test.ts b/packages/rollup-plugin-html/test-new/new.test.ts index 756a2f5da..0e72515fb 100644 --- a/packages/rollup-plugin-html/test-new/new.test.ts +++ b/packages/rollup-plugin-html/test-new/new.test.ts @@ -166,6 +166,7 @@ describe('rollup-plugin-html', () => { expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` @@ -222,6 +223,7 @@ describe('rollup-plugin-html', () => { expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` @@ -278,6 +280,7 @@ describe('rollup-plugin-html', () => { expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` @@ -307,7 +310,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); expect(assets['index.html']).to.equal(html` @@ -349,7 +355,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(2); expect(assets['index1.html']).to.equal(html` @@ -359,6 +368,7 @@ describe('rollup-plugin-html', () => { `); + expect(assets['index2.html']).to.equal(html` @@ -466,6 +476,8 @@ describe('rollup-plugin-html', () => { const hash = '16165cb387fc14ed1fe1749d05f19f7b'; + expect(chunks[`inline-module-${hash}.js`]).to.include(js`console.log('app.js');`); + expect(assets['index.html']).to.equal(html` @@ -474,8 +486,6 @@ describe('rollup-plugin-html', () => { `); - - expect(chunks[`inline-module-${hash}.js`]).to.include(js`console.log('app.js');`); }); it('resolves inline module imports relative to the HTML file', async () => { @@ -720,6 +730,7 @@ describe('rollup-plugin-html', () => { expect(Object.keys(assets)).to.have.lengthOf(2); expect(chunks['app.js']).to.exist; + expect(assets['index.html']).to.equal(html` @@ -728,6 +739,7 @@ describe('rollup-plugin-html', () => { `); + expect(assets['foo.html']).to.equal(html` @@ -810,6 +822,7 @@ describe('rollup-plugin-html', () => { `); + expect(assets['page-b.html']).to.equal(html` @@ -819,6 +832,7 @@ describe('rollup-plugin-html', () => { `); + expect(assets['page-c.html']).to.equal(html` @@ -902,6 +916,7 @@ describe('rollup-plugin-html', () => { `); + expect(assets['page-b.html']).to.equal(html` @@ -912,6 +927,7 @@ describe('rollup-plugin-html', () => { `); + // TODO: investigate why shared.js is after page-c.js here but before in the others expect(assets['page-c.html']).to.equal(html` @@ -946,6 +962,8 @@ describe('rollup-plugin-html', () => { expect(Object.keys(chunks)).to.have.lengthOf(1); expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets).to.have.keys(['index.html']); }); it('creates unique inline script names', async () => { @@ -991,6 +1009,7 @@ describe('rollup-plugin-html', () => { `); + expect(assets['nestedB/indexB.html']).to.equal(html` @@ -1000,6 +1019,7 @@ describe('rollup-plugin-html', () => { `); + expect(assets['indexC.html']).to.equal(html` @@ -1053,6 +1073,7 @@ describe('rollup-plugin-html', () => { `); + expect(assets['b.html']).to.equal(html` @@ -1062,6 +1083,7 @@ describe('rollup-plugin-html', () => { `); + expect(assets['c.html']).to.equal(html` @@ -1287,6 +1309,7 @@ describe('rollup-plugin-html', () => { expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` @@ -1357,6 +1380,8 @@ describe('rollup-plugin-html', () => { const { chunks, assets } = await generateTestBundle(build, outputConfig); expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(10); + expect(assets).to.have.keys([ 'assets/image-a-XOCPHCrV.png', 'assets/image-b-BgQHKcRn.png', @@ -1450,6 +1475,8 @@ describe('rollup-plugin-html', () => { const { chunks, assets } = await generateTestBundle(build, outputConfig); expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(10); + expect(assets).to.have.keys([ 'assets/image-a.png', 'assets/image-b.png', @@ -1509,7 +1536,16 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(assets).to.have.keys([ + 'assets/foo-BCCvKrTe.svg', + 'assets/foo-C4stzVZW.svg', + 'index.html', + ]); expect(assets['index.html']).to.equal(html` @@ -1548,7 +1584,12 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(assets).to.have.keys(['assets/foo.svg', 'assets/foo1.svg', 'index.html']); expect(assets['index.html']).to.equal(html` @@ -1586,7 +1627,12 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(assets).to.have.keys(['assets/foo.svg', 'assets/foo-BCCvKrTe.svg', 'index.html']); expect(assets['index.html']).to.equal(html` @@ -1625,7 +1671,12 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(2); + + expect(assets).to.have.keys(['assets/image-a-XOCPHCrV.png', 'index.html']); expect(assets['index.html']).to.equal(html` @@ -1686,7 +1737,17 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(assets).to.have.keys([ + 'assets/image-a-XOCPHCrV.png', + 'page-a.html', + 'page-b.html', + 'page-c.html', + ]); expect(assets['page-a.html']).to.equal(html` @@ -1770,7 +1831,7 @@ describe('rollup-plugin-html', () => { const rootDir = createApp({ 'index.html': html` - + @@ -1809,6 +1870,7 @@ describe('rollup-plugin-html', () => { expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` @@ -1879,6 +1941,7 @@ describe('rollup-plugin-html', () => { expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` @@ -1949,6 +2012,7 @@ describe('rollup-plugin-html', () => { expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + expect(assets['index.html']).to.equal(html` @@ -2014,7 +2078,16 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(assets).to.have.keys([ + 'index.html', + 'sub-with-js/index.html', + 'sub-pure-html/index.html', + ]); function extractServiceWorkerPath(code: string) { const registerOpen = code.indexOf(".register('"); @@ -2066,7 +2139,17 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(assets).to.have.keys([ + 'assets/styles-CF2Iy5n1.css', + 'assets/foo-CxmWeBHm.svg', + 'assets/image-b-C4stzVZW.svg', + 'x/index.html', + ]); expect(assets['x/index.html']).to.equal(html` @@ -2123,7 +2206,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); expect(assets).to.have.keys([ 'assets/font-normal-Cht9ZB76.woff2', @@ -2132,6 +2218,15 @@ describe('rollup-plugin-html', () => { 'index.html', ]); + expect(assets['index.html']).to.equal(html` + + + + + + + `); + expect(assets['assets/styles-Dhs3ufep.css']).to.equal(css` @font-face { font-family: Font; @@ -2149,15 +2244,6 @@ describe('rollup-plugin-html', () => { font-display: swap; } `); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); }); it('[legacy] handles fonts linked from css files', async () => { @@ -2203,7 +2289,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); expect(assets).to.have.keys([ 'assets/assets/font-normal-Cht9ZB76.woff2', @@ -2212,6 +2301,15 @@ describe('rollup-plugin-html', () => { 'index.html', ]); + expect(assets['index.html']).to.equal(html` + + + + + + + `); + expect(assets['assets/styles-BUBaODov.css']).to.equal(css` @font-face { font-family: Font; @@ -2229,15 +2327,6 @@ describe('rollup-plugin-html', () => { font-display: swap; } `); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); }); it('[new] handles fonts linked from css files in node_modules', async () => { @@ -2282,7 +2371,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); expect(assets).to.have.keys([ 'assets/font-normal-Cht9ZB76.woff2', @@ -2291,6 +2383,15 @@ describe('rollup-plugin-html', () => { 'index.html', ]); + expect(assets['index.html']).to.equal(html` + + + + + + + `); + expect(assets['assets/styles-Dhs3ufep.css']).to.equal(css` @font-face { font-family: Font; @@ -2308,15 +2409,6 @@ describe('rollup-plugin-html', () => { font-display: swap; } `); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); }); it('[legacy] handles fonts linked from css files in node_modules', async () => { @@ -2362,7 +2454,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); expect(assets).to.have.keys([ 'assets/assets/font-normal-Cht9ZB76.woff2', @@ -2371,6 +2466,15 @@ describe('rollup-plugin-html', () => { 'index.html', ]); + expect(assets['index.html']).to.equal(html` + + + + + + + `); + expect(assets['assets/styles-BUBaODov.css']).to.equal(css` @font-face { font-family: Font; @@ -2388,15 +2492,6 @@ describe('rollup-plugin-html', () => { font-display: swap; } `); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); }); it('handles duplicate fonts correctly', async () => { @@ -2442,7 +2537,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); expect(assets).to.have.keys([ 'assets/font-normal-Cht9ZB76.woff2', @@ -2545,7 +2643,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(9); expect(assets).to.have.keys([ 'assets/star-D_LO5feX.avif', @@ -2559,6 +2660,15 @@ describe('rollup-plugin-html', () => { 'index.html', ]); + expect(assets['index.html']).to.equal(html` + + + + + + + `); + expect(assets['assets/styles-mywkihBc.css']).to.equal(css` #a { background-image: url('star-D_LO5feX.avif'); @@ -2658,7 +2768,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(9); expect(assets).to.have.keys([ 'assets/assets/star-D_LO5feX.avif', @@ -2672,6 +2785,15 @@ describe('rollup-plugin-html', () => { 'index.html', ]); + expect(assets['index.html']).to.equal(html` + + + + + + + `); + expect(assets['assets/styles-Cuqf3qRf.css']).to.equal(css` #a { background-image: url('assets/star-D_LO5feX.avif'); @@ -2775,7 +2897,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(5); expect(assets).to.have.keys([ 'assets/image-a-XOCPHCrV.png', @@ -2894,7 +3019,10 @@ describe('rollup-plugin-html', () => { }; const build = await rollup(config); - const { assets } = await generateTestBundle(build, outputConfig); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(7); expect(assets).to.have.keys([ 'assets/assets/image-a-XOCPHCrV.png', From 28b43402e3783a76f236d20b95407cbcb70dbd38 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Mon, 22 Dec 2025 18:04:12 +0400 Subject: [PATCH 06/21] WIP6 --- packages/rollup-plugin-html/package.json | 2 - .../test-experiments/experiments.test.ts | 327 ------------------ .../rollup-plugin-html/test-new/new.test.ts | 213 ++++++++++++ 3 files changed, 213 insertions(+), 329 deletions(-) delete mode 100644 packages/rollup-plugin-html/test-experiments/experiments.test.ts diff --git a/packages/rollup-plugin-html/package.json b/packages/rollup-plugin-html/package.json index 5b92b8c5e..2f6514120 100644 --- a/packages/rollup-plugin-html/package.json +++ b/packages/rollup-plugin-html/package.json @@ -28,8 +28,6 @@ "demo:mpa": "rm -rf demo/dist && rollup -c demo/mpa/rollup.config.js --watch & npm run serve-demo", "demo:spa": "rm -rf demo/dist && rollup -c demo/spa/rollup.config.js --watch & npm run serve-demo", "serve-demo": "node ../dev-server/dist/bin.js --watch --root-dir demo/dist --app-index index.html --compatibility none --open", - "test:experiments:node": "mocha test-experiments/**/*.test.ts --require ts-node/register --reporter dot", - "test:experiments:watch": "mocha test-experiments/**/*.test.ts --require ts-node/register --watch --watch-files src,test", "test:node": "mocha test-new/**/*.test.ts --require ts-node/register --reporter dot", "test:watch": "mocha test-new/**/*.test.ts --require ts-node/register --watch --watch-files src,test" }, diff --git a/packages/rollup-plugin-html/test-experiments/experiments.test.ts b/packages/rollup-plugin-html/test-experiments/experiments.test.ts deleted file mode 100644 index a53e70233..000000000 --- a/packages/rollup-plugin-html/test-experiments/experiments.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -import synchronizedPrettier from '@prettier/sync'; -import * as prettier from 'prettier'; -import { rollup, OutputOptions, RollupBuild } from 'rollup'; -import { expect } from 'chai'; -import path from 'path'; -import fs from 'fs'; -import { rollupPluginHTML } from '../src/index.js'; - -// TODO: test output "fileName" too, like the real output name, not always it's properly checked besides checking the index.html source - -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)]; -} - -const html = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.html'](merge(strings, ...values)); - -const css = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.css'](merge(strings, ...values)); - -const js = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.js'](merge(strings, ...values)); - -const outputConfig: OutputOptions = { - format: 'es', - dir: 'dist', -}; - -async function generateTestBundle(build: RollupBuild, outputConfig: OutputOptions) { - const { output } = await build.generate(outputConfig); - const chunks: Record = {}; - const assets: 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; - if (typeof code !== 'string' && filename.endsWith('.css')) { - code = Buffer.from(code).toString('utf8'); - } - if (typeof code === 'string' && formatter) { - code = formatter(code); - } - assets[filename] = code; - } - } - - return { output, chunks, assets }; -} - -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; -} - -function cleanApp() { - const tmpDir = path.join(__dirname, './.tmp'); - if (fs.existsSync(tmpDir)) { - fs.rmSync(tmpDir, { recursive: true }); - } -} - -describe('rollup-plugin-html', () => { - afterEach(() => { - cleanApp(); - }); - - it('hashes all assets using assetFileNames', async () => { - const rootDir = createApp({ - 'node_modules/ing-web/fonts/font.woff2': 'font.woff', - 'node_modules/ing-web/global.css': css` - @font-face { - font-family: Font; - src: url('fonts/font.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `, - 'assets/images/image.png': 'image.png', - 'assets/styles.css': css` - #a { - background-image: url('images/image.png'); - } - `, - 'src/main.js': js` - const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; - `, - 'index.html': html` - - - - - - - - - - - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: './index.html', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, { - ...outputConfig, - assetFileNames: 'static/[name].immutable.[hash][extname]', - }); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(5); - - expect(assets).to.have.keys([ - 'static/font.immutable.C5MNjX-h.woff2', - 'static/global.immutable.DB0fKkjs.css', - 'static/image.immutable.7xJLr_7N.png', - 'static/styles.immutable.D4tZXVv0.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - `); - - expect(assets['static/global.immutable.DB0fKkjs.css']).to.equal(css` - @font-face { - font-family: Font; - src: url('font.immutable.C5MNjX-h.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `); - - expect(assets['static/styles.immutable.D4tZXVv0.css']).to.equal(css` - #a { - background-image: url('image.immutable.7xJLr_7N.png'); - } - `); - }); - - it('correctly resolves paths by using publicPath when assetFileNames puts assets in different dirs', async () => { - const rootDir = createApp({ - 'node_modules/ing-web/fonts/font.woff2': 'font.woff', - 'node_modules/ing-web/global.css': css` - @font-face { - font-family: Font; - src: url('fonts/font.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `, - 'assets/images/image.png': 'image.png', - 'assets/styles.css': css` - #a { - background-image: url('images/image.png'); - } - `, - 'src/main.js': js` - const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; - `, - 'index.html': html` - - - - - - - - - - - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: './index.html', - publicPath: '/static/', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, { - ...outputConfig, - assetFileNames: assetInfo => { - const name = assetInfo.names[0] || ''; - if (name.endsWith('.woff2')) { - return 'fonts/[name].immutable.[hash][extname]'; - } else if (name.endsWith('.css')) { - return 'styles/[name].immutable.[hash][extname]'; - } else if (name.endsWith('.png')) { - return 'images/[name].immutable.[hash][extname]'; - } - return '[name].immutable.[hash][extname]'; - }, - }); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(5); - - expect(assets).to.have.keys([ - 'fonts/font.immutable.C5MNjX-h.woff2', - 'styles/global.immutable.B3Q0ucg4.css', - 'images/image.immutable.7xJLr_7N.png', - 'styles/styles.immutable.C3Z0Fs2-.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - `); - - expect(assets['styles/global.immutable.B3Q0ucg4.css']).to.equal(css` - @font-face { - font-family: Font; - src: url('/static/fonts/font.immutable.C5MNjX-h.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `); - - expect(assets['styles/styles.immutable.C3Z0Fs2-.css']).to.equal(css` - #a { - background-image: url('/static/images/image.immutable.7xJLr_7N.png'); - } - `); - }); -}); diff --git a/packages/rollup-plugin-html/test-new/new.test.ts b/packages/rollup-plugin-html/test-new/new.test.ts index 0e72515fb..12e5fe629 100644 --- a/packages/rollup-plugin-html/test-new/new.test.ts +++ b/packages/rollup-plugin-html/test-new/new.test.ts @@ -3073,4 +3073,217 @@ describe('rollup-plugin-html', () => { } `); }); + + it('rewrites paths according to assetFileNames', async () => { + const rootDir = createApp({ + 'node_modules/ing-web/fonts/font.woff2': 'font.woff', + 'node_modules/ing-web/global.css': css` + @font-face { + font-family: Font; + src: url('fonts/font.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `, + 'assets/images/image.png': 'image.png', + 'assets/styles.css': css` + #a { + background-image: url('images/image.png'); + } + `, + 'src/main.js': js` + const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; + `, + 'index.html': html` + + + + + + + + + + + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: './index.html', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: 'static/[name].immutable.[hash][extname]', + }); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(5); + + expect(assets).to.have.keys([ + 'static/font.immutable.C5MNjX-h.woff2', + 'static/global.immutable.DB0fKkjs.css', + 'static/image.immutable.7xJLr_7N.png', + 'static/styles.immutable.D4tZXVv0.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + `); + + expect(assets['static/global.immutable.DB0fKkjs.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('font.immutable.C5MNjX-h.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['static/styles.immutable.D4tZXVv0.css']).to.equal(css` + #a { + background-image: url('image.immutable.7xJLr_7N.png'); + } + `); + }); + + it('resolves paths by using publicPath when assetFileNames puts assets in different dirs', async () => { + const rootDir = createApp({ + 'node_modules/ing-web/fonts/font.woff2': 'font.woff', + 'node_modules/ing-web/global.css': css` + @font-face { + font-family: Font; + src: url('fonts/font.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `, + 'assets/images/image.png': 'image.png', + 'assets/styles.css': css` + #a { + background-image: url('images/image.png'); + } + `, + 'src/main.js': js` + const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; + `, + 'index.html': html` + + + + + + + + + + + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: './index.html', + publicPath: '/static/', + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: assetInfo => { + const name = assetInfo.names[0] || ''; + if (name.endsWith('.woff2')) { + return 'fonts/[name].immutable.[hash][extname]'; + } else if (name.endsWith('.css')) { + return 'styles/[name].immutable.[hash][extname]'; + } else if (name.endsWith('.png')) { + return 'images/[name].immutable.[hash][extname]'; + } + return '[name].immutable.[hash][extname]'; + }, + }); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(5); + + expect(assets).to.have.keys([ + 'fonts/font.immutable.C5MNjX-h.woff2', + 'styles/global.immutable.B3Q0ucg4.css', + 'images/image.immutable.7xJLr_7N.png', + 'styles/styles.immutable.C3Z0Fs2-.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + `); + + expect(assets['styles/global.immutable.B3Q0ucg4.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('/static/fonts/font.immutable.C5MNjX-h.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['styles/styles.immutable.C3Z0Fs2-.css']).to.equal(css` + #a { + background-image: url('/static/images/image.immutable.7xJLr_7N.png'); + } + `); + }); }); From eb971e23cace5f145757d45c83ccd9e4fcaeda15 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Mon, 22 Dec 2025 18:39:57 +0400 Subject: [PATCH 07/21] WIP7 --- .../rollup-plugin-html/test-new/new.test.ts | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/rollup-plugin-html/test-new/new.test.ts b/packages/rollup-plugin-html/test-new/new.test.ts index 12e5fe629..5f90c4925 100644 --- a/packages/rollup-plugin-html/test-new/new.test.ts +++ b/packages/rollup-plugin-html/test-new/new.test.ts @@ -6,8 +6,6 @@ import path from 'path'; import fs from 'fs'; import { rollupPluginHTML } from '../src/index.js'; -// TODO: test output "fileName" too, like the real output name, not always it's properly checked besides checking the index.html source - // TODO: write tests for 'legacy-html' (this is when for CSS they are not extracted) and 'legacy-html-and-css' separately function collapseWhitespaceAll(str: string) { @@ -3095,30 +3093,31 @@ describe('rollup-plugin-html', () => { 'src/main.js': js` const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; `, - 'index.html': html` - - - - - - - - - - - `, }); const config = { plugins: [ rollupPluginHTML({ rootDir, - input: './index.html', + input: { + html: html` + + + + + + + + + + + `, + }, }), ], }; @@ -3196,31 +3195,32 @@ describe('rollup-plugin-html', () => { 'src/main.js': js` const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; `, - 'index.html': html` - - - - - - - - - - - `, }); const config = { plugins: [ rollupPluginHTML({ rootDir, - input: './index.html', publicPath: '/static/', + input: { + html: html` + + + + + + + + + + + `, + }, }), ], }; From 01acebb7796885e93aa238df522809f844370690 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Mon, 22 Dec 2025 18:40:07 +0400 Subject: [PATCH 08/21] WIP8 --- packages/rollup-plugin-html/test-new/new.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/rollup-plugin-html/test-new/new.test.ts b/packages/rollup-plugin-html/test-new/new.test.ts index 5f90c4925..a95338bca 100644 --- a/packages/rollup-plugin-html/test-new/new.test.ts +++ b/packages/rollup-plugin-html/test-new/new.test.ts @@ -6,8 +6,6 @@ import path from 'path'; import fs from 'fs'; import { rollupPluginHTML } from '../src/index.js'; -// TODO: write tests for 'legacy-html' (this is when for CSS they are not extracted) and 'legacy-html-and-css' separately - function collapseWhitespaceAll(str: string) { return ( str && @@ -1443,7 +1441,7 @@ describe('rollup-plugin-html', () => { plugins: [ rollupPluginHTML({ rootDir, - extractAssets: 'legacy-html-and-css', + extractAssets: 'legacy-html', input: { html: html` @@ -1566,7 +1564,7 @@ describe('rollup-plugin-html', () => { plugins: [ rollupPluginHTML({ rootDir, - extractAssets: 'legacy-html-and-css', + extractAssets: 'legacy-html', input: { html: html` @@ -1609,7 +1607,7 @@ describe('rollup-plugin-html', () => { plugins: [ rollupPluginHTML({ rootDir, - extractAssets: 'legacy-html-and-css', + extractAssets: 'legacy-html', input: { html: html` From 5863242052b53622724d6f8cc8e02847d1273464 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Mon, 22 Dec 2025 18:42:22 +0400 Subject: [PATCH 09/21] WIP9 --- packages/rollup-plugin-html/test-new/new.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/rollup-plugin-html/test-new/new.test.ts b/packages/rollup-plugin-html/test-new/new.test.ts index a95338bca..bec535513 100644 --- a/packages/rollup-plugin-html/test-new/new.test.ts +++ b/packages/rollup-plugin-html/test-new/new.test.ts @@ -1318,7 +1318,7 @@ describe('rollup-plugin-html', () => { `); }); - it('[new] includes referenced assets in the bundle', async () => { + it('includes referenced assets in the bundle', async () => { const rootDir = createApp({ 'image-a.png': 'image-a.png', 'image-b.png': 'image-b.png', @@ -1507,7 +1507,7 @@ describe('rollup-plugin-html', () => { `); }); - it('[new] does not deduplicate static assets with similar names', async () => { + it('does not deduplicate static assets with similar names', async () => { const rootDir = createApp({ 'foo.svg': svg``, 'x/foo.svg': svg``, @@ -2160,7 +2160,7 @@ describe('rollup-plugin-html', () => { `); }); - it('[new] handles fonts linked from css files', async () => { + it('handles fonts linked from css files', async () => { const rootDir = createApp({ 'fonts/font-bold.woff2': 'font-bold', 'fonts/font-normal.woff2': 'font-normal', @@ -2325,7 +2325,7 @@ describe('rollup-plugin-html', () => { `); }); - it('[new] handles fonts linked from css files in node_modules', async () => { + it('handles fonts linked from css files in node_modules', async () => { const rootDir = createApp({ 'node_modules/foo/fonts/font-bold.woff2': 'font-bold', 'node_modules/foo/fonts/font-normal.woff2': 'font-normal', @@ -2576,7 +2576,7 @@ describe('rollup-plugin-html', () => { `); }); - it('[new] handles images referenced from css', async () => { + it('handles images referenced from css', async () => { const rootDir = createApp({ 'images/star.avif': 'star.avif', 'images/star.gif': 'star.gif', @@ -2825,7 +2825,7 @@ describe('rollup-plugin-html', () => { `); }); - it('[new] allows to exclude external assets usign a glob pattern', async () => { + it('allows to exclude external assets usign a glob pattern', async () => { const rootDir = createApp({ 'image-a.png': 'image-a.png', 'image-b.png': 'image-b.png', From f050a1eb67e9f0f1558f4bfcb10b27f92a7d43f1 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Mon, 22 Dec 2025 18:43:19 +0400 Subject: [PATCH 10/21] WIP10 --- packages/rollup-plugin-html/package.json | 4 +- .../rollup-plugin-html/test-new/new.test.ts | 3287 ---------------- .../test/fixtures/assets/favicon.ico | Bin 15086 -> 0 bytes .../test/fixtures/assets/foo.svg | 1 - .../test/fixtures/assets/foo/bar/y.css | 3 - .../test/fixtures/assets/foo/x.css | 3 - .../test/fixtures/assets/image-a.png | Bin 1648 -> 0 bytes .../test/fixtures/assets/image-a.svg | 1 - .../test/fixtures/assets/image-b.png | Bin 1648 -> 0 bytes .../test/fixtures/assets/image-b.svg | 1 - .../test/fixtures/assets/image-c.png | Bin 1648 -> 0 bytes .../test/fixtures/assets/image-d.png | 1 - .../test/fixtures/assets/image-d.svg | 1 - .../test/fixtures/assets/image-social.png | Bin 1648 -> 0 bytes .../fixtures/assets/images/eb26e6ca-30.avif | Bin 391 -> 0 bytes .../fixtures/assets/images/eb26e6ca-30.jpeg | Bin 513 -> 0 bytes .../fixtures/assets/images/eb26e6ca-60.avif | Bin 568 -> 0 bytes .../fixtures/assets/images/eb26e6ca-60.jpeg | Bin 928 -> 0 bytes .../test/fixtures/assets/index.html | 17 - .../test/fixtures/assets/no-module.js | 1 - .../assets/styles-with-referenced-assets.css | 15 - .../test/fixtures/assets/styles.css | 3 - .../assets/videos/typer-hydration.mp4 | Bin 50425 -> 0 bytes .../test/fixtures/assets/webmanifest.json | 1 - .../test/fixtures/assets/x/foo.svg | 1 - .../test/fixtures/basic/app.js | 1 - .../test/fixtures/basic/index.html | 6 - .../test/fixtures/basic/not-index.html | 5 - .../test/fixtures/basic/pages/page-a.html | 7 - .../test/fixtures/basic/pages/page-a.js | 0 .../test/fixtures/basic/pages/page-b.html | 7 - .../test/fixtures/basic/pages/page-b.js | 0 .../test/fixtures/basic/pages/page-c.html | 7 - .../test/fixtures/basic/pages/page-c.js | 0 .../test/fixtures/basic/pages/shared.js | 0 .../test/fixtures/basic/src/foo.js | 1 - .../test/fixtures/basic/src/index.html | 6 - .../fixtures/inject-service-worker/index.html | 5 - .../sub-pure-html/index.html | 5 - .../sub-with-js/index.html | 6 - .../sub-with-js/sub-js.js | 1 - .../fonts/font-normal.woff2 | 0 .../styles-a.css | 7 - .../styles-b.css | 7 - .../images/star.avif | 1 - .../images/star.gif | 1 - .../images/star.jpeg | 1 - .../images/star.jpg | 1 - .../images/star.png | 1 - .../images/star.svg | 1 - .../images/star.webp | 1 - .../styles.css | 24 - .../node_modules/foo/fonts/font-bold.woff2 | 0 .../node_modules/foo/fonts/font-normal.woff2 | 0 .../foo/node_modules-styles-with-fonts.css | 15 - .../fonts/font-bold.woff2 | 0 .../fonts/font-normal.woff2 | 0 .../styles-with-fonts.css | 15 - .../rollup-plugin-html/csp-page-a.html | 15 - .../rollup-plugin-html/csp-page-b.html | 19 - .../rollup-plugin-html/csp-page-c.html | 19 - .../rollup-plugin-html/entrypoint-a.js | 3 - .../rollup-plugin-html/entrypoint-b.js | 3 - .../rollup-plugin-html/entrypoint-c.js | 3 - .../exclude/assets/partial.html | 1 - .../rollup-plugin-html/exclude/index.html | 1 - .../fixtures/rollup-plugin-html/foo/foo.html | 8 - .../fixtures/rollup-plugin-html/foo/foo.js | 1 - .../fixtures/rollup-plugin-html/index.html | 3 - .../fixtures/rollup-plugin-html/module.js | 0 .../rollup-plugin-html/modules/module-a.js | 3 - .../rollup-plugin-html/modules/module-b.js | 3 - .../rollup-plugin-html/modules/module-c.js | 3 - .../modules/shared-module.js | 1 - .../fixtures/rollup-plugin-html/my-page.html | 1 - .../rollup-plugin-html/pages/page-a.html | 7 - .../rollup-plugin-html/pages/page-a.js | 1 - .../rollup-plugin-html/pages/page-b.html | 7 - .../rollup-plugin-html/pages/page-b.js | 1 - .../rollup-plugin-html/pages/page-c.html | 7 - .../rollup-plugin-html/pages/page-c.js | 1 - .../rollup-plugin-html/pages/shared.js | 1 - .../rollup-plugin-html/pure-index.html | 1 - .../rollup-plugin-html/pure-index2.html | 1 - .../rollup-plugin-html/retain-attributes.html | 3 - .../fixtures/rollup-plugin-html/styles.css | 3 - .../test/rollup-plugin-html.test.ts | 3410 +++++++++++++---- .../test/src/input/InputData.test.ts | 359 -- .../src/input/extract/extractAssets.test.ts | 314 -- .../src/input/extract/extractModules.test.ts | 194 - .../src/output/getEntrypointBundles.test.ts | 360 -- .../test/src/output/getOutputHTML.test.ts | 210 - .../test/src/output/injectBundles.test.ts | 129 - .../output/injectedUpdatedAssetPaths.test.ts | 352 -- 94 files changed, 2693 insertions(+), 6227 deletions(-) delete mode 100644 packages/rollup-plugin-html/test-new/new.test.ts delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/favicon.ico delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/foo.svg delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/foo/bar/y.css delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/foo/x.css delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/image-a.png delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/image-a.svg delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/image-b.png delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/image-b.svg delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/image-c.png delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/image-d.png delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/image-d.svg delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/image-social.png delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/images/eb26e6ca-30.avif delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/images/eb26e6ca-30.jpeg delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/images/eb26e6ca-60.avif delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/images/eb26e6ca-60.jpeg delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/index.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/no-module.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/styles-with-referenced-assets.css delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/styles.css delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/videos/typer-hydration.mp4 delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/webmanifest.json delete mode 100644 packages/rollup-plugin-html/test/fixtures/assets/x/foo.svg delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/app.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/index.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/not-index.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/pages/page-a.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/pages/page-a.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/pages/page-b.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/pages/page-b.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/pages/page-c.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/pages/page-c.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/pages/shared.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/src/foo.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/basic/src/index.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/inject-service-worker/index.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-pure-html/index.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-with-js/index.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-with-js/sub-js.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/fonts/font-normal.woff2 delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-a.css delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-b.css delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.avif delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.gif delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpeg delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpg delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.png delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.svg delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.webp delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/styles.css delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/fonts/font-bold.woff2 delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/fonts/font-normal.woff2 delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/node_modules-styles-with-fonts.css delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/fonts/font-bold.woff2 delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/fonts/font-normal.woff2 delete mode 100644 packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/styles-with-fonts.css delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-a.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-b.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-c.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-a.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-b.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-c.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/exclude/assets/partial.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/exclude/index.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/foo/foo.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/foo/foo.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/index.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/module.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-a.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-b.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-c.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/shared-module.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/my-page.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-a.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-a.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-b.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-b.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-c.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-c.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/shared.js delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pure-index.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pure-index2.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/retain-attributes.html delete mode 100644 packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/styles.css delete mode 100644 packages/rollup-plugin-html/test/src/input/InputData.test.ts delete mode 100644 packages/rollup-plugin-html/test/src/input/extract/extractAssets.test.ts delete mode 100644 packages/rollup-plugin-html/test/src/input/extract/extractModules.test.ts delete mode 100644 packages/rollup-plugin-html/test/src/output/getEntrypointBundles.test.ts delete mode 100644 packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts delete mode 100644 packages/rollup-plugin-html/test/src/output/injectBundles.test.ts delete mode 100644 packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts diff --git a/packages/rollup-plugin-html/package.json b/packages/rollup-plugin-html/package.json index 2f6514120..fb14e5ac7 100644 --- a/packages/rollup-plugin-html/package.json +++ b/packages/rollup-plugin-html/package.json @@ -28,8 +28,8 @@ "demo:mpa": "rm -rf demo/dist && rollup -c demo/mpa/rollup.config.js --watch & npm run serve-demo", "demo:spa": "rm -rf demo/dist && rollup -c demo/spa/rollup.config.js --watch & npm run serve-demo", "serve-demo": "node ../dev-server/dist/bin.js --watch --root-dir demo/dist --app-index index.html --compatibility none --open", - "test:node": "mocha test-new/**/*.test.ts --require ts-node/register --reporter dot", - "test:watch": "mocha test-new/**/*.test.ts --require ts-node/register --watch --watch-files src,test" + "test:node": "mocha test/**/*.test.ts --require ts-node/register --reporter dot", + "test:watch": "mocha test/**/*.test.ts --require ts-node/register --watch --watch-files src,test" }, "files": [ "*.js", diff --git a/packages/rollup-plugin-html/test-new/new.test.ts b/packages/rollup-plugin-html/test-new/new.test.ts deleted file mode 100644 index bec535513..000000000 --- a/packages/rollup-plugin-html/test-new/new.test.ts +++ /dev/null @@ -1,3287 +0,0 @@ -import synchronizedPrettier from '@prettier/sync'; -import * as prettier from 'prettier'; -import { rollup, OutputChunk, OutputOptions, Plugin, RollupBuild } from 'rollup'; -import { expect } from 'chai'; -import path from 'path'; -import fs from 'fs'; -import { rollupPluginHTML } from '../src/index.js'; - -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)]; -} - -const html = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.html'](merge(strings, ...values)); - -const css = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.css'](merge(strings, ...values)); - -const js = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.js'](merge(strings, ...values)); - -const svg = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.svg'](merge(strings, ...values)); - -const outputConfig: OutputOptions = { - format: 'es', - dir: 'dist', -}; - -async function generateTestBundle(build: RollupBuild, outputConfig: OutputOptions) { - const { output } = await build.generate(outputConfig); - const chunks: Record = {}; - const assets: 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; - if (typeof code !== 'string' && filename.endsWith('.css')) { - code = Buffer.from(code).toString('utf8'); - } - if (typeof code === 'string' && formatter) { - code = formatter(code); - } - assets[filename] = code; - } - } - - return { output, chunks, assets }; -} - -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; -} - -function cleanApp() { - const tmpDir = path.join(__dirname, './.tmp'); - if (fs.existsSync(tmpDir)) { - fs.rmSync(tmpDir, { recursive: true }); - } -} - -describe('rollup-plugin-html', () => { - afterEach(() => { - cleanApp(); - }); - - it('can build with an input path as input', async () => { - const rootDir = createApp({ - 'index.html': html` - - - - - - - - `, - 'entrypoint-a.js': js` - import './modules/module-a.js'; - console.log('entrypoint-a.js'); - `, - 'entrypoint-b.js': js` - import './modules/module-b.js'; - console.log('entrypoint-b.js'); - `, - 'modules/module-a.js': js` - import './shared-module.js'; - console.log('module-a.js'); - `, - 'modules/module-b.js': js` - import './shared-module.js'; - console.log('module-b.js'); - `, - 'modules/shared-module.js': js` - console.log('shared-module.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: './index.html', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(3); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); - expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); - - expect(assets['index.html']).to.equal(html` - - - - - - - - `); - }); - - it('can build with html file as rollup input', async () => { - const rootDir = createApp({ - 'index.html': html` - - - - - - - - `, - 'entrypoint-a.js': js` - import './modules/module-a.js'; - console.log('entrypoint-a.js'); - `, - 'entrypoint-b.js': js` - import './modules/module-b.js'; - console.log('entrypoint-b.js'); - `, - 'modules/module-a.js': js` - import './shared-module.js'; - console.log('module-a.js'); - `, - 'modules/module-b.js': js` - import './shared-module.js'; - console.log('module-b.js'); - `, - 'modules/shared-module.js': js` - console.log('shared-module.js'); - `, - }); - - const config = { - input: './index.html', - plugins: [rollupPluginHTML({ rootDir })], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(3); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); - expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); - - expect(assets['index.html']).to.equal(html` - - - - - - - - `); - }); - - it('will retain attributes on script tags', async () => { - const rootDir = createApp({ - 'index.html': html` - - - - - - - - `, - 'entrypoint-a.js': js` - import './modules/module-a.js'; - console.log('entrypoint-a.js'); - `, - 'entrypoint-b.js': js` - import './modules/module-b.js'; - console.log('entrypoint-b.js'); - `, - 'modules/module-a.js': js` - import './shared-module.js'; - console.log('module-a.js'); - `, - 'modules/module-b.js': js` - import './shared-module.js'; - console.log('module-b.js'); - `, - 'modules/shared-module.js': js` - console.log('shared-module.js'); - `, - }); - - const config = { - input: './index.html', - plugins: [rollupPluginHTML({ rootDir })], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(3); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); - expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); - - expect(assets['index.html']).to.equal(html` - - - - - - - - `); - }); - - it('can build with pure html file as rollup input', async () => { - const rootDir = createApp({ - 'index.html': html` - - - -

hello world

- - - `, - }); - - const config = { - input: './index.html', - plugins: [rollupPluginHTML({ rootDir })], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(assets['index.html']).to.equal(html` - - - -

hello world

- - - `); - }); - - it('can build with multiple pure html inputs', async () => { - const rootDir = createApp({ - 'index1.html': html` - - - -

hello world

- - - `, - 'index2.html': html` - - - -

hey there

- - - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: ['./index1.html', './index2.html'], - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(2); - - expect(assets['index1.html']).to.equal(html` - - - -

hello world

- - - `); - - expect(assets['index2.html']).to.equal(html` - - - -

hey there

- - - `); - }); - - it('can build with html string as input', async () => { - const rootDir = createApp({ - 'app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - name: 'index.html', - html: ``, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); - }); - - it('resolves paths relative to virtual html filename', async () => { - const rootDir = createApp({ - 'app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - name: 'nested/index.html', - html: ``, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(assets['nested/index.html']).to.equal(html` - - - - - - - `); - }); - - it('can build with inline modules', async () => { - const rootDir = createApp({ - 'app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - name: 'index.html', - html: ``, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - const hash = '16165cb387fc14ed1fe1749d05f19f7b'; - - expect(chunks[`inline-module-${hash}.js`]).to.include(js`console.log('app.js');`); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); - }); - - it('resolves inline module imports relative to the HTML file', async () => { - const rootDir = createApp({ - 'nested/index.html': html` - - - - - - - `, - 'nested/app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: './nested/index.html', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - const hash = 'b774aefb8bf002b291fd54d27694a34d'; - expect(chunks[`inline-module-${hash}.js`]).to.include(js`console.log('app.js');`); - }); - - it('can build transforming final output', async () => { - const rootDir = createApp({ - 'app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: `

Hello world

`, - }, - transformHtml(html) { - return html.replace('Hello world', 'Goodbye world'); - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(assets['index.html']).to.equal(html` - - - -

Goodbye world

- - - - `); - }); - - it('can build with a public path', async () => { - const rootDir = createApp({ - 'app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: ``, - }, - publicPath: '/static/', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); - }); - - it('can build with a public path with a file in a directory', async () => { - const rootDir = createApp({ - 'app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - name: 'nested/index.html', - html: ``, - }, - publicPath: '/static/', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(assets['nested/index.html']).to.equal(html` - - - - - - - `); - }); - - it('can build with multiple build outputs', async () => { - const rootDir = createApp({ - 'app.js': js` - import './modules/module.js'; - console.log('app.js'); - `, - 'modules/module.js': js` - console.log('module.js'); - `, - }); - - const plugin = rollupPluginHTML({ - rootDir, - input: { - html: ``, - }, - publicPath: '/static/', - }); - - const config = { - input: path.join(rootDir, 'app.js'), - plugins: [plugin], - }; - - const build = await rollup(config); - - const bundleA = generateTestBundle(build, { - format: 'system', - dir: 'dist', - plugins: [plugin.api.addOutput('legacy')], - }); - - const bundleB = generateTestBundle(build, { - format: 'es', - dir: 'dist', - plugins: [plugin.api.addOutput('modern')], - }); - - const { chunks: chunksA, assets: assetsA } = await bundleA; - const { chunks: chunksB, assets: assetsB } = await bundleB; - - expect(Object.keys(chunksA)).to.have.lengthOf(1); - expect(Object.keys(assetsA)).to.have.lengthOf(0); - expect(Object.keys(chunksB)).to.have.lengthOf(1); - expect(Object.keys(assetsB)).to.have.lengthOf(1); - - expect(chunksA['app.js']).to.include(js`console.log('app.js');`); - expect(chunksA['app.js']).to.include(js`console.log('module.js');`); - expect(chunksB['app.js']).to.include(js`console.log('app.js');`); - expect(chunksB['app.js']).to.include(js`console.log('module.js');`); - - expect(assetsA['index.html']).to.not.exist; - expect(assetsB['index.html']).to.equal(html` - - - - - - - - `); - }); - - it('can build with index.html as input and an extra html file as output', async () => { - const rootDir = createApp({ - 'app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: ``, - }, - }), - rollupPluginHTML({ - rootDir, - input: { - name: 'foo.html', - html: `

foo.html

`, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(2); - expect(Object.keys(assets)).to.have.lengthOf(2); - - expect(chunks['app.js']).to.exist; - - expect(assets['index.html']).to.equal(html` - - - - - - - `); - - expect(assets['foo.html']).to.equal(html` - - - -

foo.html

- - - `); - }); - - it('can build with multiple html inputs', async () => { - const rootDir = createApp({ - 'entrypoint-a.js': js` - import './modules/module-a.js'; - console.log('entrypoint-a.js'); - `, - 'entrypoint-b.js': js` - import './modules/module-b.js'; - console.log('entrypoint-b.js'); - `, - 'entrypoint-c.js': js` - import './modules/module-c.js'; - console.log('entrypoint-c.js'); - `, - 'modules/module-a.js': js` - import './shared-module.js'; - console.log('module-a.js'); - `, - 'modules/module-b.js': js` - import './shared-module.js'; - console.log('module-b.js'); - `, - 'modules/module-c.js': js` - import './shared-module.js'; - console.log('module-c.js'); - `, - 'modules/shared-module.js': js` - console.log('shared-module.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: [ - { - name: 'page-a.html', - html: `

Page A

`, - }, - { - name: 'page-b.html', - html: `

Page B

`, - }, - { - name: 'page-c.html', - html: `

Page C

`, - }, - ], - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(4); - expect(Object.keys(assets)).to.have.lengthOf(3); - - expect(chunks['entrypoint-a.js']).to.exist; - expect(chunks['entrypoint-b.js']).to.exist; - expect(chunks['entrypoint-c.js']).to.exist; - - expect(assets['page-a.html']).to.equal(html` - - - -

Page A

- - - - `); - - expect(assets['page-b.html']).to.equal(html` - - - -

Page B

- - - - `); - - expect(assets['page-c.html']).to.equal(html` - - - -

Page C

- - - - `); - }); - - it('can use a glob to build multiple pages', async () => { - const rootDir = createApp({ - 'pages/page-a.html': html` - - -

page-a.html

- - - - - `, - 'pages/page-b.html': html` - - -

page-b.html

- - - - - `, - 'pages/page-c.html': html` - - -

page-c.html

- - - - - `, - 'pages/page-a.js': js` - export default 'page a'; - `, - 'pages/page-b.js': js` - export default 'page b'; - `, - 'pages/page-c.js': js` - export default 'page c'; - `, - 'pages/shared.js': js` - export default 'shared'; - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: 'pages/**/*.html', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(4); - expect(Object.keys(assets)).to.have.lengthOf(3); - - expect(chunks['page-a.js']).to.exist; - expect(chunks['page-b.js']).to.exist; - expect(chunks['page-c.js']).to.exist; - - expect(assets['page-a.html']).to.equal(html` - - - -

page-a.html

- - - - - `); - - expect(assets['page-b.html']).to.equal(html` - - - -

page-b.html

- - - - - `); - - // TODO: investigate why shared.js is after page-c.js here but before in the others - expect(assets['page-c.html']).to.equal(html` - - - -

page-c.html

- - - - - `); - }); - - it('can exclude globs', async () => { - const rootDir = createApp({ - 'exclude/index.html': html``, - 'exclude/assets/partial.html': html`I'm a partial!`, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: 'exclude/**/*.html', - exclude: '**/partial.html', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(assets).to.have.keys(['index.html']); - }); - - it('creates unique inline script names', async () => { - const rootDir = createApp({}); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: [ - { - name: 'nestedA/indexA.html', - html: `

Page A

`, - }, - { - name: 'nestedB/indexB.html', - html: `

Page B

`, - }, - { - name: 'indexC.html', - html: `

Page C

`, - }, - ], - }), - ], - }; - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(3); - expect(Object.keys(assets)).to.have.lengthOf(3); - - expect(chunks['inline-module-d463148d1d5869e52917a3b270db9e72.js']).to.exist; - expect(chunks['inline-module-b81da853430abdf130bcc7c4d0ade6d9.js']).to.exist; - expect(chunks['inline-module-170bb2146da66c440259138c7e0fea7e.js']).to.exist; - - expect(assets['nestedA/indexA.html']).to.equal(html` - - - -

Page A

- - - - `); - - expect(assets['nestedB/indexB.html']).to.equal(html` - - - -

Page B

- - - - `); - - expect(assets['indexC.html']).to.equal(html` - - - -

Page C

- - - - `); - }); - - it('deduplicates common modules', async () => { - const rootDir = createApp({}); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: [ - { - name: 'a.html', - html: `

Page A

`, - }, - { - name: 'b.html', - html: `

Page B

`, - }, - { - name: 'c.html', - html: `

Page C

`, - }, - ], - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(3); - - expect(chunks['inline-module-44281cf3dede62434e0dd368df08902f.js']).to.exist; - - expect(assets['a.html']).to.equal(html` - - - -

Page A

- - - - `); - - expect(assets['b.html']).to.equal(html` - - - -

Page B

- - - - `); - - expect(assets['c.html']).to.equal(html` - - - -

Page C

- - - - `); - }); - - it('outputs the hashed entrypoint name', async () => { - const rootDir = createApp({ - 'app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: ``, - }, - }), - ], - }; - - const build = await rollup(config); - const { output, chunks, assets } = await generateTestBundle(build, { - ...outputConfig, - entryFileNames: '[name]-[hash].js', - }); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - const appChunk = output.find(f => - // @ts-ignore - f.facadeModuleId.endsWith('app.js'), - ) as OutputChunk; - - // ensure it's actually hashed - expect(appChunk.fileName).to.not.equal('app.js'); - - // get hashed name dynamically - expect(assets['index.html']).to.equal(html` - - - - - - - `); - }); - - it('outputs import path relative to the final output html', async () => { - const rootDir = createApp({ - 'app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - name: 'nested/index.html', - html: '', - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(assets['nested/index.html']).to.equal(html` - - - - - - - `); - }); - - it('can change HTML root directory', async () => { - const rootDir = createApp({ - 'different-root/src/app.js': js` - console.log('app.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir: path.join(rootDir, 'different-root'), - input: { - name: 'src/nested/index.html', - html: '', - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(assets['src/nested/index.html']).to.equal(html` - - - - - - - `); - }); - - it('can get the input with getInputs()', async () => { - // default filename - const pluginA = rollupPluginHTML({ input: { html: 'Hello world' } }); - - // filename inferred from input filename - const rootDirB = createApp({ - 'my-page.html': html``, - 'app.js': js`console.log('app.js');`, - }); - const pluginB = rollupPluginHTML({ - input: path.join(rootDirB, 'my-page.html'), - }); - - // filename explicitly set - const rootDirC = createApp({ - 'index.html': html``, - 'app.js': js`console.log('app.js');`, - }); - const pluginC = rollupPluginHTML({ - input: { - name: 'nested/my-other-page.html', - path: path.join(rootDirC, 'index.html'), - }, - }); - - await rollup({ plugins: [pluginA] }); - await rollup({ plugins: [pluginB] }); - await rollup({ plugins: [pluginC] }); - - expect(pluginA.api.getInputs()[0].name).to.equal('index.html'); - expect(pluginB.api.getInputs()[0].name).to.equal('my-page.html'); - expect(pluginC.api.getInputs()[0].name).to.equal('nested/my-other-page.html'); - }); - - it('supports other plugins injecting a transform function', async () => { - const rootDir = createApp({ - 'index.html': html` - - - - - - - - `, - 'entrypoint-a.js': js` - import './modules/module-a.js'; - console.log('entrypoint-a.js'); - `, - 'entrypoint-b.js': js` - import './modules/module-b.js'; - console.log('entrypoint-b.js'); - `, - 'modules/module-a.js': js` - import './shared-module.js'; - console.log('module-a.js'); - `, - 'modules/module-b.js': js` - import './shared-module.js'; - console.log('module-b.js'); - `, - 'modules/shared-module.js': js` - console.log('shared-module.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: './index.html', - }), - { - name: 'other-plugin', - buildStart(options) { - if (!options.plugins) throw new Error('no plugins'); - const plugin = options.plugins.find(pl => { - if (pl.name === '@web/rollup-plugin-html') { - return pl!.api.getInputs()[0].name === 'index.html'; - } - return false; - }); - plugin!.api.addHtmlTransformer((html: string) => - html.replace('', ''), - ); - }, - } as Plugin, - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(3); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); - expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - `); - }); - - it('includes referenced assets in the bundle', async () => { - const rootDir = createApp({ - 'image-a.png': 'image-a.png', - 'image-b.png': 'image-b.png', - 'image-c.png': 'image-c.png', - 'image-a.svg': svg``, - 'image-b.svg': svg``, - 'styles.css': css` - :root { - color: blue; - } - `, - 'foo/x.css': css` - :root { - color: x; - } - `, - 'foo/bar/y.css': css` - :root { - color: y; - } - `, - 'webmanifest.json': { message: 'hello world' }, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: html` - - - - - - - - - - - - -
- -
- - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(10); - - expect(assets).to.have.keys([ - 'assets/image-a-XOCPHCrV.png', - 'assets/image-b-BgQHKcRn.png', - 'assets/image-c-C4yLPiIL.png', - 'assets/image-a-BCCvKrTe.svg', - 'assets/image-b-C4stzVZW.svg', - 'assets/styles-CF2Iy5n1.css', - 'assets/x-DDGg8O6h.css', - 'assets/y-DJTrnPH3.css', - 'assets/webmanifest-BkrOR1WG.json', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - - -
- -
- - - `); - }); - - it('[legacy] includes referenced assets in the bundle', async () => { - const rootDir = createApp({ - 'image-a.png': 'image-a.png', - 'image-b.png': 'image-b.png', - 'image-c.png': 'image-c.png', - 'image-a.svg': svg``, - 'image-b.svg': svg``, - 'styles.css': css` - :root { - color: blue; - } - `, - 'foo/x.css': css` - :root { - color: x; - } - `, - 'foo/bar/y.css': css` - :root { - color: y; - } - `, - 'webmanifest.json': { message: 'hello world' }, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - extractAssets: 'legacy-html', - input: { - html: html` - - - - - - - - - - - - -
- -
- - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(10); - - expect(assets).to.have.keys([ - 'assets/image-a.png', - 'assets/image-b.png', - 'assets/image-c-C4yLPiIL.png', - 'assets/image-a.svg', - 'assets/image-b-C4stzVZW.svg', - 'assets/styles-CF2Iy5n1.css', - 'assets/x-DDGg8O6h.css', - 'assets/y-DJTrnPH3.css', - 'assets/webmanifest.json', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - - -
- -
- - - `); - }); - - it('does not deduplicate static assets with similar names', async () => { - const rootDir = createApp({ - 'foo.svg': svg``, - 'x/foo.svg': svg``, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: html` - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(3); - - expect(assets).to.have.keys([ - 'assets/foo-BCCvKrTe.svg', - 'assets/foo-C4stzVZW.svg', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - - `); - }); - - it('[legacy] deduplicates static assets with similar names', async () => { - const rootDir = createApp({ - 'foo.svg': svg``, - 'x/foo.svg': svg``, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - extractAssets: 'legacy-html', - input: { - html: html` - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(3); - - expect(assets).to.have.keys(['assets/foo.svg', 'assets/foo1.svg', 'index.html']); - - expect(assets['index.html']).to.equal(html` - - - - - - - - `); - }); - - it('[legacy] static and hashed asset nodes can reference the same files', async () => { - const rootDir = createApp({ - 'foo.svg': svg``, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - extractAssets: 'legacy-html', - input: { - html: html` - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(3); - - expect(assets).to.have.keys(['assets/foo.svg', 'assets/foo-BCCvKrTe.svg', 'index.html']); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - `); - }); - - it('deduplicates common assets', async () => { - const rootDir = createApp({ - 'image-a.png': 'image-a.png', - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: html` - - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(2); - - expect(assets).to.have.keys(['assets/image-a-XOCPHCrV.png', 'index.html']); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - `); - }); - - it('deduplicates common assets across HTML files', async () => { - const rootDir = createApp({ - 'image-a.png': 'image-a.png', - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: [ - { - name: 'page-a.html', - html: html` - - - - - - `, - }, - { - name: 'page-b.html', - html: html` - - - - - - `, - }, - { - name: 'page-c.html', - html: html` - - - - - - - `, - }, - ], - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(4); - - expect(assets).to.have.keys([ - 'assets/image-a-XOCPHCrV.png', - 'page-a.html', - 'page-b.html', - 'page-c.html', - ]); - - expect(assets['page-a.html']).to.equal(html` - - - - - - - `); - - expect(assets['page-b.html']).to.equal(html` - - - - - - - `); - - expect(assets['page-c.html']).to.equal(html` - - - - - - - - `); - }); - - it('can turn off extracting assets', async () => { - const rootDir = createApp({ - 'image-c.png': 'image-c.png', - 'image-b.svg': svg``, - 'styles.css': css` - :root { - color: blue; - } - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - extractAssets: false, - rootDir, - input: { - html: html` - - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - `); - }); - - it('can inject a CSP meta tag for inline scripts', async () => { - const rootDir = createApp({ - 'index.html': html` - - - - - - - - - - `, - 'entrypoint-a.js': js` - console.log('entrypoint-a.js'); - `, - 'entrypoint-b.js': js` - console.log('entrypoint-b.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - strictCSPInlineScripts: true, - rootDir, - input: './index.html', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(2); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); - expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - - `); - }); - - it('can add to an existing CSP meta tag for inline scripts', async () => { - const rootDir = createApp({ - 'index.html': html` - - - - - - - - - - - - `, - 'entrypoint-a.js': js` - console.log('entrypoint-a.js'); - `, - 'entrypoint-b.js': js` - console.log('entrypoint-b.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - strictCSPInlineScripts: true, - rootDir, - input: './index.html', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(2); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); - expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - - `); - }); - - it('can add to an existing CSP meta tag for inline scripts even if script-src is already there', async () => { - const rootDir = createApp({ - 'index.html': html` - - - - - - - - - - - - `, - 'entrypoint-a.js': js` - console.log('entrypoint-a.js'); - `, - 'entrypoint-b.js': js` - console.log('entrypoint-b.js'); - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - strictCSPInlineScripts: true, - rootDir, - input: './index.html', - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(2); - expect(Object.keys(assets)).to.have.lengthOf(1); - - expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); - expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - - `); - }); - - it('can inject a service worker registration script if injectServiceWorker and serviceWorkerPath are provided', async () => { - const rootDir = createApp({ - 'index.html': html` - - -

inject a service worker into /index.html

- - - `, - 'sub-pure-html/index.html': html` - - -

inject a service worker into /sub-page/index.html

- - - `, - 'sub-with-js/index.html': html` - - -

inject a service worker into /sub-page/index.html

- - - - `, - 'sub-with-js/sub-js.js': js`console.log('sub-with-js');`, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: '**/*.html', - flattenOutput: false, - injectServiceWorker: true, - serviceWorkerPath: path.join( - path.resolve(outputConfig.dir as string), - 'service-worker.js', - ), - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(3); - - expect(assets).to.have.keys([ - 'index.html', - 'sub-with-js/index.html', - 'sub-pure-html/index.html', - ]); - - function extractServiceWorkerPath(code: string) { - const registerOpen = code.indexOf(".register('"); - const registerClose = code.indexOf("')", registerOpen + 11); - return code.substring(registerOpen + 11, registerClose); - } - - expect(extractServiceWorkerPath(assets['index.html'] as string)).to.equal('service-worker.js'); - expect(extractServiceWorkerPath(assets['sub-with-js/index.html'] as string)).to.equal( - '../service-worker.js', - ); - expect(extractServiceWorkerPath(assets['sub-pure-html/index.html'] as string)).to.equal( - '../service-worker.js', - ); - }); - - it('does support a absolutePathPrefix to allow for sub folder deployments', async () => { - const rootDir = createApp({ - 'x/foo.svg': svg``, - 'image-b.svg': svg``, - 'styles.css': css` - :root { - color: blue; - } - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - absolutePathPrefix: '/my-prefix/', - rootDir, - input: { - html: html` - - - - - - - - - - `, - name: 'x/index.html', - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(4); - - expect(assets).to.have.keys([ - 'assets/styles-CF2Iy5n1.css', - 'assets/foo-CxmWeBHm.svg', - 'assets/image-b-C4stzVZW.svg', - 'x/index.html', - ]); - - expect(assets['x/index.html']).to.equal(html` - - - - - - - - - - `); - }); - - it('handles fonts linked from css files', async () => { - const rootDir = createApp({ - 'fonts/font-bold.woff2': 'font-bold', - 'fonts/font-normal.woff2': 'font-normal', - 'styles.css': css` - @font-face { - font-family: Font; - src: url('fonts/font-normal.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - - @font-face { - font-family: Font; - src: url('fonts/font-bold.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; - } - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: html` - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(4); - - expect(assets).to.have.keys([ - 'assets/font-normal-Cht9ZB76.woff2', - 'assets/font-bold-eQjSonqH.woff2', - 'assets/styles-Dhs3ufep.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); - - expect(assets['assets/styles-Dhs3ufep.css']).to.equal(css` - @font-face { - font-family: Font; - src: url('font-normal-Cht9ZB76.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - - @font-face { - font-family: Font; - src: url('font-bold-eQjSonqH.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; - } - `); - }); - - it('[legacy] handles fonts linked from css files', async () => { - const rootDir = createApp({ - 'fonts/font-bold.woff2': 'font-bold', - 'fonts/font-normal.woff2': 'font-normal', - 'styles.css': css` - @font-face { - font-family: Font; - src: url('fonts/font-normal.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - - @font-face { - font-family: Font; - src: url('fonts/font-bold.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; - } - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - extractAssets: 'legacy-html-and-css', - input: { - html: html` - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(4); - - expect(assets).to.have.keys([ - 'assets/assets/font-normal-Cht9ZB76.woff2', - 'assets/assets/font-bold-eQjSonqH.woff2', - 'assets/styles-BUBaODov.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); - - expect(assets['assets/styles-BUBaODov.css']).to.equal(css` - @font-face { - font-family: Font; - src: url('assets/font-normal-Cht9ZB76.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - - @font-face { - font-family: Font; - src: url('assets/font-bold-eQjSonqH.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; - } - `); - }); - - it('handles fonts linked from css files in node_modules', async () => { - const rootDir = createApp({ - 'node_modules/foo/fonts/font-bold.woff2': 'font-bold', - 'node_modules/foo/fonts/font-normal.woff2': 'font-normal', - 'node_modules/foo/styles.css': css` - @font-face { - font-family: Font; - src: url('fonts/font-normal.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - - @font-face { - font-family: Font; - src: url('fonts/font-bold.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; - } - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: html` - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(4); - - expect(assets).to.have.keys([ - 'assets/font-normal-Cht9ZB76.woff2', - 'assets/font-bold-eQjSonqH.woff2', - 'assets/styles-Dhs3ufep.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); - - expect(assets['assets/styles-Dhs3ufep.css']).to.equal(css` - @font-face { - font-family: Font; - src: url('font-normal-Cht9ZB76.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - - @font-face { - font-family: Font; - src: url('font-bold-eQjSonqH.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; - } - `); - }); - - it('[legacy] handles fonts linked from css files in node_modules', async () => { - const rootDir = createApp({ - 'node_modules/foo/fonts/font-bold.woff2': 'font-bold', - 'node_modules/foo/fonts/font-normal.woff2': 'font-normal', - 'node_modules/foo/styles.css': css` - @font-face { - font-family: Font; - src: url('fonts/font-normal.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - - @font-face { - font-family: Font; - src: url('fonts/font-bold.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; - } - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - extractAssets: 'legacy-html-and-css', - input: { - html: html` - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(4); - - expect(assets).to.have.keys([ - 'assets/assets/font-normal-Cht9ZB76.woff2', - 'assets/assets/font-bold-eQjSonqH.woff2', - 'assets/styles-BUBaODov.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); - - expect(assets['assets/styles-BUBaODov.css']).to.equal(css` - @font-face { - font-family: Font; - src: url('assets/font-normal-Cht9ZB76.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - - @font-face { - font-family: Font; - src: url('assets/font-bold-eQjSonqH.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; - } - `); - }); - - it('handles duplicate fonts correctly', async () => { - const rootDir = createApp({ - 'fonts/font-normal.woff2': 'font-normal', - 'styles-a.css': css` - @font-face { - font-family: Font; - src: url('fonts/font-normal.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `, - 'styles-b.css': css` - @font-face { - font-family: Font2; - src: url('fonts/font-normal.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: html` - - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(4); - - expect(assets).to.have.keys([ - 'assets/font-normal-Cht9ZB76.woff2', - 'assets/styles-a-jFIfrzm8.css', - 'assets/styles-b-B-8m1N7T.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - - `); - - expect(assets['assets/styles-a-jFIfrzm8.css']).to.equal(css` - @font-face { - font-family: Font; - src: url('font-normal-Cht9ZB76.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `); - - expect(assets['assets/styles-b-B-8m1N7T.css']).to.equal(css` - @font-face { - font-family: Font2; - src: url('font-normal-Cht9ZB76.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `); - }); - - it('handles images referenced from css', async () => { - const rootDir = createApp({ - 'images/star.avif': 'star.avif', - 'images/star.gif': 'star.gif', - 'images/star.jpeg': 'star.jpeg', - 'images/star.jpg': 'star.jpg', - 'images/star.png': 'star.png', - 'images/star.svg': 'star.svg', - 'images/star.webp': 'star.webp', - 'styles.css': css` - #a { - background-image: url('images/star.avif'); - } - - #b { - background-image: url('images/star.gif'); - } - - #c { - background-image: url('images/star.jpeg'); - } - - #d { - background-image: url('images/star.jpg'); - } - - #e { - background-image: url('images/star.png'); - } - - #f { - background-image: url('images/star.svg'); - } - - #g { - background-image: url('images/star.svg#foo'); - } - - #h { - background-image: url('images/star.webp'); - } - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: html` - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(9); - - expect(assets).to.have.keys([ - 'assets/star-D_LO5feX.avif', - 'assets/star-BKg9qmmf.gif', - 'assets/star-BZWqL7hS.jpeg', - 'assets/star-Df0JryvN.jpg', - 'assets/star-CXig10q7.png', - 'assets/star-CwhgM_z4.svg', - 'assets/star-CKbh5mKn.webp', - 'assets/styles-mywkihBc.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); - - expect(assets['assets/styles-mywkihBc.css']).to.equal(css` - #a { - background-image: url('star-D_LO5feX.avif'); - } - - #b { - background-image: url('star-BKg9qmmf.gif'); - } - - #c { - background-image: url('star-BZWqL7hS.jpeg'); - } - - #d { - background-image: url('star-Df0JryvN.jpg'); - } - - #e { - background-image: url('star-CXig10q7.png'); - } - - #f { - background-image: url('star-CwhgM_z4.svg'); - } - - #g { - background-image: url('star-CwhgM_z4.svg#foo'); - } - - #h { - background-image: url('star-CKbh5mKn.webp'); - } - `); - }); - - it('[legacy] handles images referenced from css', async () => { - const rootDir = createApp({ - 'images/star.avif': 'star.avif', - 'images/star.gif': 'star.gif', - 'images/star.jpeg': 'star.jpeg', - 'images/star.jpg': 'star.jpg', - 'images/star.png': 'star.png', - 'images/star.svg': 'star.svg', - 'images/star.webp': 'star.webp', - 'styles.css': css` - #a { - background-image: url('images/star.avif'); - } - - #b { - background-image: url('images/star.gif'); - } - - #c { - background-image: url('images/star.jpeg'); - } - - #d { - background-image: url('images/star.jpg'); - } - - #e { - background-image: url('images/star.png'); - } - - #f { - background-image: url('images/star.svg'); - } - - #g { - background-image: url('images/star.svg#foo'); - } - - #h { - background-image: url('images/star.webp'); - } - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - extractAssets: 'legacy-html-and-css', - input: { - html: html` - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(9); - - expect(assets).to.have.keys([ - 'assets/assets/star-D_LO5feX.avif', - 'assets/assets/star-BKg9qmmf.gif', - 'assets/assets/star-BZWqL7hS.jpeg', - 'assets/assets/star-Df0JryvN.jpg', - 'assets/assets/star-CXig10q7.png', - 'assets/assets/star-CwhgM_z4.svg', - 'assets/assets/star-CKbh5mKn.webp', - 'assets/styles-Cuqf3qRf.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - `); - - expect(assets['assets/styles-Cuqf3qRf.css']).to.equal(css` - #a { - background-image: url('assets/star-D_LO5feX.avif'); - } - - #b { - background-image: url('assets/star-BKg9qmmf.gif'); - } - - #c { - background-image: url('assets/star-BZWqL7hS.jpeg'); - } - - #d { - background-image: url('assets/star-Df0JryvN.jpg'); - } - - #e { - background-image: url('assets/star-CXig10q7.png'); - } - - #f { - background-image: url('assets/star-CwhgM_z4.svg'); - } - - #g { - background-image: url('assets/star-CwhgM_z4.svg#foo'); - } - - #h { - background-image: url('assets/star-CKbh5mKn.webp'); - } - `); - }); - - it('allows to exclude external assets usign a glob pattern', async () => { - const rootDir = createApp({ - 'image-a.png': 'image-a.png', - 'image-b.png': 'image-b.png', - 'image-a.svg': svg``, - 'image-b.svg': svg``, - 'styles.css': css` - #a1 { - background-image: url('image-a.png'); - } - - #a2 { - background-image: url('image-a.svg'); - } - - #d1 { - background-image: url('./image-b.png'); - } - - #d2 { - background-image: url('./image-b.svg'); - } - `, - 'foo/x.css': css` - :root { - color: x; - } - `, - 'foo/bar/y.css': css` - :root { - color: y; - } - `, - 'webmanifest.json': { message: 'hello world' }, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - externalAssets: ['**/foo/**/*', '*.svg'], - rootDir, - input: { - html: html` - - - - - - - - - - - - - -
- -
- - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(5); - - expect(assets).to.have.keys([ - 'assets/image-a-XOCPHCrV.png', - 'assets/image-b-BgQHKcRn.png', - 'assets/styles-Bv-4gk2N.css', - 'assets/webmanifest-BkrOR1WG.json', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - - - -
- -
- - - `); - - expect(assets['assets/styles-Bv-4gk2N.css']).to.equal(css` - #a1 { - background-image: url('image-a-XOCPHCrV.png'); - } - - #a2 { - background-image: url('image-a.svg'); - } - - #d1 { - background-image: url('image-b-BgQHKcRn.png'); - } - - #d2 { - background-image: url('./image-b.svg'); - } - `); - }); - - it('[legacy] allows to exclude external assets usign a glob pattern', async () => { - const rootDir = createApp({ - 'image-a.png': 'image-a.png', - 'image-b.png': 'image-b.png', - 'image-a.svg': svg``, - 'image-b.svg': svg``, - 'styles.css': css` - #a1 { - background-image: url('image-a.png'); - } - - #a2 { - background-image: url('image-a.svg'); - } - - #d1 { - background-image: url('./image-b.png'); - } - - #d2 { - background-image: url('./image-b.svg'); - } - `, - 'foo/x.css': css` - :root { - color: x; - } - `, - 'foo/bar/y.css': css` - :root { - color: y; - } - `, - 'webmanifest.json': { message: 'hello world' }, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - externalAssets: ['**/foo/**/*', '*.svg'], - rootDir, - extractAssets: 'legacy-html-and-css', - input: { - html: html` - - - - - - - - - - - - - -
- -
- - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, outputConfig); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(7); - - expect(assets).to.have.keys([ - 'assets/assets/image-a-XOCPHCrV.png', - 'assets/assets/image-b-BgQHKcRn.png', - 'assets/image-a.png', - 'assets/image-b.png', - 'assets/styles-DFIb0lB5.css', - 'assets/webmanifest.json', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - - - -
- -
- - - `); - - expect(assets['assets/styles-DFIb0lB5.css']).to.equal(css` - #a1 { - background-image: url('assets/image-a-XOCPHCrV.png'); - } - - #a2 { - background-image: url('image-a.svg'); - } - - #d1 { - background-image: url('assets/image-b-BgQHKcRn.png'); - } - - #d2 { - background-image: url('./image-b.svg'); - } - `); - }); - - it('rewrites paths according to assetFileNames', async () => { - const rootDir = createApp({ - 'node_modules/ing-web/fonts/font.woff2': 'font.woff', - 'node_modules/ing-web/global.css': css` - @font-face { - font-family: Font; - src: url('fonts/font.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `, - 'assets/images/image.png': 'image.png', - 'assets/styles.css': css` - #a { - background-image: url('images/image.png'); - } - `, - 'src/main.js': js` - const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - input: { - html: html` - - - - - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, { - ...outputConfig, - assetFileNames: 'static/[name].immutable.[hash][extname]', - }); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(5); - - expect(assets).to.have.keys([ - 'static/font.immutable.C5MNjX-h.woff2', - 'static/global.immutable.DB0fKkjs.css', - 'static/image.immutable.7xJLr_7N.png', - 'static/styles.immutable.D4tZXVv0.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - `); - - expect(assets['static/global.immutable.DB0fKkjs.css']).to.equal(css` - @font-face { - font-family: Font; - src: url('font.immutable.C5MNjX-h.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `); - - expect(assets['static/styles.immutable.D4tZXVv0.css']).to.equal(css` - #a { - background-image: url('image.immutable.7xJLr_7N.png'); - } - `); - }); - - it('resolves paths by using publicPath when assetFileNames puts assets in different dirs', async () => { - const rootDir = createApp({ - 'node_modules/ing-web/fonts/font.woff2': 'font.woff', - 'node_modules/ing-web/global.css': css` - @font-face { - font-family: Font; - src: url('fonts/font.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `, - 'assets/images/image.png': 'image.png', - 'assets/styles.css': css` - #a { - background-image: url('images/image.png'); - } - `, - 'src/main.js': js` - const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; - `, - }); - - const config = { - plugins: [ - rollupPluginHTML({ - rootDir, - publicPath: '/static/', - input: { - html: html` - - - - - - - - - - - `, - }, - }), - ], - }; - - const build = await rollup(config); - const { chunks, assets } = await generateTestBundle(build, { - ...outputConfig, - assetFileNames: assetInfo => { - const name = assetInfo.names[0] || ''; - if (name.endsWith('.woff2')) { - return 'fonts/[name].immutable.[hash][extname]'; - } else if (name.endsWith('.css')) { - return 'styles/[name].immutable.[hash][extname]'; - } else if (name.endsWith('.png')) { - return 'images/[name].immutable.[hash][extname]'; - } - return '[name].immutable.[hash][extname]'; - }, - }); - - expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(5); - - expect(assets).to.have.keys([ - 'fonts/font.immutable.C5MNjX-h.woff2', - 'styles/global.immutable.B3Q0ucg4.css', - 'images/image.immutable.7xJLr_7N.png', - 'styles/styles.immutable.C3Z0Fs2-.css', - 'index.html', - ]); - - expect(assets['index.html']).to.equal(html` - - - - - - - - - - - `); - - expect(assets['styles/global.immutable.B3Q0ucg4.css']).to.equal(css` - @font-face { - font-family: Font; - src: url('/static/fonts/font.immutable.C5MNjX-h.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; - } - `); - - expect(assets['styles/styles.immutable.C3Z0Fs2-.css']).to.equal(css` - #a { - background-image: url('/static/images/image.immutable.7xJLr_7N.png'); - } - `); - }); -}); diff --git a/packages/rollup-plugin-html/test/fixtures/assets/favicon.ico b/packages/rollup-plugin-html/test/fixtures/assets/favicon.ico deleted file mode 100644 index e7b0a98b6f29f0a7cd31e79e62418ab89126bdbc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 tbKGDUeb-x diff --git a/packages/rollup-plugin-html/test/fixtures/assets/foo/bar/y.css b/packages/rollup-plugin-html/test/fixtures/assets/foo/bar/y.css deleted file mode 100644 index 85419bc9e..000000000 --- a/packages/rollup-plugin-html/test/fixtures/assets/foo/bar/y.css +++ /dev/null @@ -1,3 +0,0 @@ -:root { - color: y; -} diff --git a/packages/rollup-plugin-html/test/fixtures/assets/foo/x.css b/packages/rollup-plugin-html/test/fixtures/assets/foo/x.css deleted file mode 100644 index feb977525..000000000 --- a/packages/rollup-plugin-html/test/fixtures/assets/foo/x.css +++ /dev/null @@ -1,3 +0,0 @@ -:root { - color: x; -} diff --git a/packages/rollup-plugin-html/test/fixtures/assets/image-a.png b/packages/rollup-plugin-html/test/fixtures/assets/image-a.png deleted file mode 100644 index 088d83d6d4bd20cb59aa4757bee6588a9c048389..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1648 zcmZ`(dpy%?7=LStNRmpYt5O}O>o~elk(n47t;MJq!(7s4=327KWi&#RTf^K-av3RX zxrEHU6$ue)5xVS@&-t8lI#1`n^GCnW^LyXl`~JS)=lMR*?|FYQwnTGP<+aKPAyt9} zo&@vqAGZ<)MxEViCt+GdH?cNBsQkLhUoHx;)}mREtPu*`ju0mTp+8}ZBSR#P;k4~>vKavK2jxX0^I6ilTkTVY!{7LQb1^j|w|nLgI{f|n_poAV zvUuDpv6)lS@4^)^(R&d{Bjbc#W=OmUCp?UJ76Dbhdri15&7P*QrdKz>0F& z-@MwfYmY{gnkLgE(^=9_xi94fL*I&C&lir&r*(da7rnV!_A*L1bX`1t>*-WR*9>6t zUdjQM|LSY*fE>8o2g4T%`hs%0Z??(e>c$cpCX!oZX&uwxFtcYCkb?d%>0L7c{7%lj zem1|;GrkE}k(IBw)x*5HF$j^=^fskcmi_!ww)7Jq(>p%|WOXsq+o{o_c=336$&1i} zKHy%tKM-9t3|ydObkBhERLL|{!zbwpl`A^VDueyY_xkkRFFc1b{DHd^Lp7i-HfbGMou?;_aH%!U%KRwTY-UVzLkF& z7!b`F_pDfDKw+e{dnUKKC%4dVwfW!f@J((7Ne7&B_0Q&jb3h((FN6p1Pl>3dMAkV* zi_Y>I&Ly|f;~U*##XgD6&^CZSNX^^tBrtT%$N82J0?LUOs>qigfmF!UDY6a{fdA|d>?CIZW)24B)-7a${erk*(0}ug(90^ZKgP(xM9h{ zT~59!TzI{XfHxtpdA4Brg1LWwz&C8%q_ugA_SS9N7s3*==vSTJcI@1>d(U31uAahT zr1-o3zWoOd8W4nc|n29W%EeSXv#oCfXp|6C~x6GK!;11|?(4GoXH9vxeaG}pWt zpO}36PWFCk`oqjGYa!otvvVImef}c<_f>EHpwMO zi^*bmc%5_CVuvu@wY->Y8bZOD diff --git a/packages/rollup-plugin-html/test/fixtures/assets/image-b.png b/packages/rollup-plugin-html/test/fixtures/assets/image-b.png deleted file mode 100644 index 088d83d6d4bd20cb59aa4757bee6588a9c048389..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1648 zcmZ`(dpy%?7=LStNRmpYt5O}O>o~elk(n47t;MJq!(7s4=327KWi&#RTf^K-av3RX zxrEHU6$ue)5xVS@&-t8lI#1`n^GCnW^LyXl`~JS)=lMR*?|FYQwnTGP<+aKPAyt9} zo&@vqAGZ<)MxEViCt+GdH?cNBsQkLhUoHx;)}mREtPu*`ju0mTp+8}ZBSR#P;k4~>vKavK2jxX0^I6ilTkTVY!{7LQb1^j|w|nLgI{f|n_poAV zvUuDpv6)lS@4^)^(R&d{Bjbc#W=OmUCp?UJ76Dbhdri15&7P*QrdKz>0F& z-@MwfYmY{gnkLgE(^=9_xi94fL*I&C&lir&r*(da7rnV!_A*L1bX`1t>*-WR*9>6t zUdjQM|LSY*fE>8o2g4T%`hs%0Z??(e>c$cpCX!oZX&uwxFtcYCkb?d%>0L7c{7%lj zem1|;GrkE}k(IBw)x*5HF$j^=^fskcmi_!ww)7Jq(>p%|WOXsq+o{o_c=336$&1i} zKHy%tKM-9t3|ydObkBhERLL|{!zbwpl`A^VDueyY_xkkRFFc1b{DHd^Lp7i-HfbGMou?;_aH%!U%KRwTY-UVzLkF& z7!b`F_pDfDKw+e{dnUKKC%4dVwfW!f@J((7Ne7&B_0Q&jb3h((FN6p1Pl>3dMAkV* zi_Y>I&Ly|f;~U*##XgD6&^CZSNX^^tBrtT%$N82J0?LUOs>qigfmF!UDY6a{fdA|d>?CIZW)24B)-7a${erk*(0}ug(90^ZKgP(xM9h{ zT~59!TzI{XfHxtpdA4Brg1LWwz&C8%q_ugA_SS9N7s3*==vSTJcI@1>d(U31uAahT zr1-o3zWoOd8W4nc|n29W%EeSXv#oCfXp|6C~x6GK!;11|?(4GoXH9vxeaG}pWt zpO}36PWFCk`oqjGYa!otvvVImef}c<_f>EHpwMO zi^*bmc%5_CVuvu@wY->Y8bZOD diff --git a/packages/rollup-plugin-html/test/fixtures/assets/image-c.png b/packages/rollup-plugin-html/test/fixtures/assets/image-c.png deleted file mode 100644 index 088d83d6d4bd20cb59aa4757bee6588a9c048389..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1648 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 - - - - - - - - - - - -
- -
- - diff --git a/packages/rollup-plugin-html/test/fixtures/assets/no-module.js b/packages/rollup-plugin-html/test/fixtures/assets/no-module.js deleted file mode 100644 index b089a583f..000000000 --- a/packages/rollup-plugin-html/test/fixtures/assets/no-module.js +++ /dev/null @@ -1 +0,0 @@ -/* no module script file */ diff --git a/packages/rollup-plugin-html/test/fixtures/assets/styles-with-referenced-assets.css b/packages/rollup-plugin-html/test/fixtures/assets/styles-with-referenced-assets.css deleted file mode 100644 index acb8baa56..000000000 --- a/packages/rollup-plugin-html/test/fixtures/assets/styles-with-referenced-assets.css +++ /dev/null @@ -1,15 +0,0 @@ -#a1 { - background-image: url('image-a.png'); -} - -#a2 { - background-image: url('image-a.svg'); -} - -#d1 { - background-image: url('./image-d.png'); -} - -#d2 { - background-image: url('./image-d.svg'); -} diff --git a/packages/rollup-plugin-html/test/fixtures/assets/styles.css b/packages/rollup-plugin-html/test/fixtures/assets/styles.css deleted file mode 100644 index c11eb03fe..000000000 --- a/packages/rollup-plugin-html/test/fixtures/assets/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -:root { - color: blue; -} diff --git a/packages/rollup-plugin-html/test/fixtures/assets/videos/typer-hydration.mp4 b/packages/rollup-plugin-html/test/fixtures/assets/videos/typer-hydration.mp4 deleted file mode 100644 index 53176ab71ce9e82ea0f93cc49221a9c32f8044e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50425 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

eG)#8_j^M9 zQpfoGg@qj*iS75vtX14zY%8$t{=^Uh3-ai30n~z7E&0pev7)B|JP!R)pg;rX!~b_d z0@MAd>qi6@K#TWe3VZD984!s1El`>Z$wB*P5nN!=Gp!mFv{l0g+Q;e7!kblir>=iy zy(pU22>u3hm|VPL>7ePZXwD5X#s;ZHb|=4BJIFGg7uc}j9`ftp(4MA`9yHN*{&o$1 zUktjX$j@WF{=OO{h3VVWA0Z^4=@G+*R-=4&MS`Glv9Z1_`zA4vMlVncsQl#s zEYVdC{N#rdL4S<5*iS;ids^G-ra6>T$V(6r8_?AL@%aC{_a2o(ro4BImT!*a`6bt~!0e)$`Pq**&KJ-c555V_$ zI04++A#vyf3SibpMs_U4KOD z$yxm$XaG%Ruueiu4iHcJzmcn`hnRS=3$7hr$o~V7kRV z{tJ`wXSjCT8PBNv#Z_n4*ev$o;lE*)wlMF3z~}&=fO+WGUou;e;p{<`OK#U!n-)Y~ zxgZ-45n7--!UTh$|C(9wp6KomV%;VOH3tE`0ZxhkgeJ4~?P)KRB~WGw$tL+XmNYY% zafLikeDBr{{n88;;$g%17Kxi{ZBotd>6<=xqrILhmwN4=tB+B1gqEz`qMHN@`Bi4A zs2j7Jpv=>YFzPu(+=f@&0B6;|)hn2ZoWK9sKe6}e@O%cz6Z{8wJlf6f#PuaWfWUhN zK!F9$(|H2dZ!suDfggmE_*AO*7hZMcvvrv+mqbfe1KbW$r zO#fos@-;lu;&3q7^}`pR`Vng=zebOqC=hfhFbqB}7M)8Ti0B8h00nBIG{b))0a~S17vcvudVA$3h=&EWSB>(uhTw@Q| zeKi|b2VTAYXk|}+$rETvA#%K1#VCAb3{nv<#qB{;X>1_}QSuw;)%oWU@j}1U`{*{h zf*X%3`Nw1cJ^QBH24ZjxdF}i??>Qd>*z@gA{CZ90sMCZHIjs(Jw$mMt+pkyFBQbs- zU*W>KUGt&eKXX%Z+%5TTqoF@02OvWP$t(H?QEX=O52{h&M(uf(e~?fDN-1s3lG8VqHId9c{9hqU4|5@kt2 z(nrHdEYc(A$4wSl^!h|2J74J~xQ6;kvgPAK-9zQ=>>?d;(c>`Lgt@V-&R5E`#^lBn zP>WB5>nTa%(h4JXcx1 zLj8crJ%1GMUh>9(-H)dMM&}drUe6S&*mM@9WT|tuJq0axP{R-lal3)Zr%h#s@xIcE z3eLP0OWRk`!c^>+apr=j5TDhb_=n?Y3eYPW@*Qt$f}dT`f4GQ3WIPp{VUu_+zzIUS z0!Snc$vb?ccjh8)Ks~~S*IQaTzq)xQa{}uef3JHXBz0d0ibct&+19CR(*)6$t(STP z&Qrhl3!MydJAYplmmnbFFhPld;4Xk~5rgyLfXS)BN~h(cWl`Y`$wP|wA-Ec)p#~&I zpq?F^j{rdDx*##Uzzl;h^l8+cVfpw>X3 z(>^2z^}iD}QE=}e7CfE!5VRgI-1Oub6lX);-RnX*OSqlzLi6g+zs3r?|l8c&v^jWCRsfSo+SvHi)|7&InV zZsMw9R9%axYdM?Gbf&^){_lv&Z0@MmM81vKId0c*WgBk9oM)6ydBY8`T#T8Yb%Z#} z-s@M?SJl$1z9p(FrudMNyT0D{Vbu68x666ue8x^n=`>%+<;4CY<%e+F260idsY|$r zcDT5BQr%m+2)k{Nejk7+6(pbW&rE!jza`uHM8h=;bGYkDSAPPvA}a<*3eckZk!=FW z4P+mQ{5pNnQ4M!FoOlyk(lg9y(mfE!isAq?cm~dwe_HfBgH{I)=By?((t#kV9iXZZ zlB4uQOB{K_Yl`-oSH5z68ml4cVM-|NNz>oqcvnsZ$x4E0Hh#PX1;g4&1Q#-7=8=-f zX=^N`5k`iL+*bJbPR*)2O$n!2t1(6)%J!Btz9;1)GMb-!5gcLKbA=LPVdBR$MCKrY zZ5KdY;t569L$a%(#*$s`-vL(5N39GrZPLaK%noRcH5SY&Ont%m!eJf*xesHhmZ!ZA zq}dBVi3iSi_=A8@go%NuNlExeD*+P|XNp8OZ^ zA^AyB#KR7mBAc)EwkV4lh6Q22X#*@$M+q*lDb(S+7SRGa7fobQJH?6TdM$s z_Q+Jg|1p0d^!zrS>yNIcz&)-rJK5$LXAt49E$S8$5X zG8=U4BlDx2823DP$OCb5WD5RdvZ{aoa!$l(Yur?gm1RiR8W|=PO&8i%E&ut-Z#s!* z`aW`KUk$(H?F|u7_5Ox>-JhnryR(G`xB2pG0=Xa|-!**I`c@TlA63dh%5aH{cm~=7 zX5EL|mtWbiM&GZ0O5P21QQx4=JAoFIGU5pF(BEO$g(R;Djy4%))S^xNa(cFk9=M7g zPiTlLA2t+Q3`S2?v|%0yp`A)5MsPYor21`}bcN6GO@@17p|>y73U@zT?Y2=2OaxFy z4wAq4c>Rv~(JQXl-0T899w_10bI5I(m*-4B38ylIT^N5SvG-8hl7+JtuUZ#e{Y`*I zBGq~vfyI0rBF@Q_k#pu?^mx7FrT{(%*qbi^@@GpLhqN>o)h5V_k)eC8Nd$th9f4XQ zaQ-DQ4b)v_b9FqZXW7*e4&tIRqVL;J!G<@=3*&B@${2Yz@8gTU9zW!3Y{Dt|5I_j| z4tBl;f}qQQ{#}rqoBwbZEhKPKng(h+PuDv5*vlVdY?{)b8LjY6sC%SD7)x}vdQSI< zD$1|QN{Frj)B1{fL1O9+rG^#;HOJMl$z`=)r|Z#-vdrkD_>ou4So}*%K(>k=*VL?9NVRzunlIwDLEWx zcxbVPmC(^S8+17$2#)4oEz^pTV*^lxs)s}bl_lJ4%U^*Ak^lq%F$N#t8k*!)FDe9L zn|xiwIb9w=e?$|&sgVC^iE__)r%Hjqj6i!&NVdp-5e9GVKTt&XH_7_#OT!!^e0k#< z?>6WkSX@Mpka5&uRpT?QM01nnCc!JJ$tosaJqz0QGSn z0^*FxfAK-|!H)xBq=o#_#MF$&)F5iNMGVay&VSY|Q1N_&{y8s`WZZ}ydKB(Mk)t~- zKF+s?oYDNZ80bg(B-FO6xV!l`+@EdS6`%->|1SVqu}Ot z)`{U?rkqIaRb+)QJI1E}P3qe%ab|AQBB>ZLUepBc3G(=)$tQ<}uA_hp&HGC&En_C| z2)55#Dt=Oiad|A|fEv>jDeQW-zNTrU-T6|?tb z?&Ki;NNjWQxG!bgB00(A?_Q>8Q6Lu7UEfQ2j-{8ySR^WCW9{H@SO_}77|HK}e;2;x zpo|dMUq;b=J%~shwQDphRw`YL|HVqw##1$;JNg|c3j3sU{d&)$G>$Gwvzh(FCbB-K z0@GS`jP4l0D;(v?qpl+m7%<@ghfrP*unz0a>(S5B&!7d6RNZ?)urxpoJ~;0!uoVAX za_2cxq~MY82Pi&)pmKmZK}dG#e^(=T{TEiGku2pW*|sGrQ{LMS10>fX)F}JfI|(O?u?A%zYPAAAi4Cqb@$s7E;L995Q$>%^tqD(6`>TVPKbO(Xz1A!L6;CoNo zBc}Xtd!lO|Bw9nU{iI$5pB)#9I!m{@I|r9Ss)bb?)9Z*)nYnuUe06)&By z^Zh8=0YC=*#{-rCk}v;=4r!jk<(5##n@-N0#Fj-6h{EUzcH95O=ut6;2hO(#Frp4A zD8kN^YruTv5eNd!06&l|B**z5s{M}RpJN=^reyM_bJx8qu2*z-O76Br*pxD|do%9< z^6MG_vuuPFIR0k+g4885MU!vwi|4zVdm<*bs~(=DDdlJp3t_$GbN%P4m=g#?n(0GH zZQU`Ow9-@(S+xkDp+})^43eMt2zzOXfGo)-E;k%^mt!=RpGFI`iuqoI36+HM@a1}J z|4(iO2r|>ooKo}re$t4Q^q>?|t=bEtzIe}x3$2~k<pW_BD&>8|Aq@XkGmEd|W;QaT%WEBQ_ z?4C<>*1!yuRFieph{5pu5=Hz9U17+xs}frH07n&{KsfkuO1R|+(c^xW$UtAI4ZT>W$6h0T{XX9vYwB-O5P3m>!Ljl)Nlam!CsJuR z&X`ba!%Ug9h%7;o2#2#Fb44|TU;ju||2;r2b-*<7XS)hTXH314li?E|!|xV;yPua> zGYH|~k7+d5qwKgC!?PhwLE|gj@L^$LTnwI$k>UN(dRR&$KtS3p*g9S0E@o*T3MJ(X zU%W`E{$$xUgXAp!BZXht#f5P(UKH|&QYyz-Yf>=x)glC%U5%=;X1vFO=4)g0duw~S zpgz^XDok4pvf`^S@alg4MeZ2n7jWq}%ELl1A^|dxOnw-V8)-Fie6|8M*D%}`UrEgZ z5TJ+TT>s+}ab+vF7z+nzSaG4TW+Ev=ao7wQFI=vYi;~cBVu7}Re&pGsKN}#xql$g3 zp(x2<3))Pw`IF)cVC6jj+52MP*SMc1aIl+=fnI=w#P^3p+yKXv1uRc}Kq0^|g@L8; zAIH-thWWTl0iD0@U&E5+Bf|`O0fJe7`i%E~2LORq1D^_g|9{^G(sCO8!&d&d$LTWm zAzfs1!_W{^bGH}yh=y)Ei6NUlM-nYRe>dmlih6ljFNhQ;>L>F)hCVgL>I-aC`LTa2 z&-XpNW*4a5fR{hWEXgt8M}HQ<1m@bC_X6YMLh|weR`RGs) zbc?D?7kf(h2+ZZ)6B0!8TF@sx^=m+0F7Q{N3y{3?;J-Uq3s^qL#!wDtiD6tM7-m() zHl(PaKq>W zrXw?)(M92!*)}tikYk>g_z{TM2^a~uV8i}_K`4gq2Msiy3p)Sf8T1yYN`T}b{j)V5 z=a=q(w6yq}%rCS~6{p`gI+5BnK$K~ORYy;p{Um$+%M2*Ewfn2{_A~Bt^KtuRY_cMR zZ*?P3KWmz4e@f|f=pILx>8Kc}bp?XEdn%Xt&ti^*3N_gV!9E`T4UingfA1Aq0N=I$ z2hRMfMPNUEXlpdwcO*XF5<@3pa!9e7QNM`-r>`WA zx#p6SVRnL84#8W2+ChYRsBGRP1$J@|brCW6%t}BJ&uvQwM`Dw?jYifO!cwoDhyk9=?k#U8bzu zVnUB!&=!f)kV3yrK(J0-y9rHeeHQLL%BK%gQ~wA#FC@S2pB;x~enxxf=+BVy!`}8H z0$SPas(YuJLh z&FccfJ8nnV`Xe;;@3aF^AN^hgeSQ6c0u;pS{*07mVXKSrW)zmg2Gc*>N=g3J>quvM zZ~Mw$PECg0wZfS^kL?l}&eT!h>!v`J69Gqgbr~Nbp`T_2=xPK|^IBBh8dsWn7;&4BK){Ep6&`a4NhR2Wm+bbKtWAUN{3Br$JcAhi)EK6ux&OA7crK93>^2PBX0 zzjVX*{Sf4fqNIE~&(C<^6aa(GD`)2?DI$Kpii3hz02AlQF#7G0sRBG-^DoV01$xQ6 zI==$C;XhF^fCrxTU#ckY)!t5dG8iU;X|S(u!Gb_P21vHv6Ga|nI)EEf2=ie;Gtl*p z(1mZXV2=A97gSQv4>ByQ0F8lUjXr-qB^13iX@16i@)0&@D-bgSR1EMf&T{QOViSkZ9r$gZw zSAOQ2CF=sRn8mn4+8Sm&8*1MbV#c=BOFq9NZT-Sl|3=8d-7xbU4+I#92%Puk5g0HS z>0J@sjf%~@w6Tr=_@QNq-2m+J@(^koZfmRc4j>SC*Ih?cq+yhPh;W3vRqnfF0?kQjrni>- z@KwM27Umvo*cAki^qv?RTW}NnP>kQe5Cn`)0M6@u#0Yek_z{JUp^qc>BZU(|t$U6! zP-zFr?*DH91TYr@4#sJD#fGv-3np_Fz0`UJTsupQtu&v&n6p7$xc4E!MTn<9$5m!l zId0=beL;#ISg%aC#^Fi6>Eo)|eS5dDU@cb=EHLxsas4qua$x^W15}NNur|RKtqq?= zWJ2U+rtQ!~ywB>C*<(sLfV2TJfaK8qFACRj;xD=0$$YbqOtvYx*%n03VH$;wBW?N+ zS+v6E8u?>|Be_o5n%29^MI!W#0rt}YwvZI#3=lZIo;;>#< zzz6`el7D9GCrqpDU@we5G=Eosj5E7hFHlJb$&vmyw9p`|D+1j!b0=Da&F&89d*vu< zmG374t3XpgKVbdureu0hufCJszk?EMaPa#G(~kh-{l0Xi6_pcu4}`2f%*%>7k5BR= z$ecgFLOxgw@;)wk#aY@7JVAEveg3(>hi3g)>y{zP`hjtW1fw)u1#x^)tkqN)kqXNt z(QnV$^8mfes3{#&D~oQD($a7K@RV^-apF(R8aE^-`rm*k?7Yc02&7N0&sF+V0mMjc znFKi*ZBy=CG`Y)*Xc;kKhGOkq&H@;4LUJnq-4PX@o#6nlXfGw}B?3}cV@piaVN-sS zN`ZoWVo3)g4GX$7%%eb>1j(8F(+&2YRmL!5jFEl~v1c%%3s}g$h>ib*2d{+p98jIX z5DReJ?F=(Bm4&b;Mv(5jQ@2SQ3lV8UxzQN%mZrXvplBA^37L3ZKU64#&-ntRZHw@w z({ou#dt@Zc6Kk>Z`&V3g&`4PlKjw7Hwh^=4j1g^@`Io^tjZplHB=Um|tH>R*Y%u^R zdT{=?KWk80b1Khb+%G*aL>DnVjMS_S{9*aXS16k-z#z%32}?djV4*-`pedO$f)*@8Iy8@gWY%f^G`+5BOd0|enNY>wi6SuSF^Q< z2(GgHlz1a+4(s&hmp9&g6}48-0Xy2O4#AwxIR^@)8#p>9+Vt9uC|Muf$tE9*c*IQ17F6)afPmW%+n?opnACSP)oq;sdX@+jheEO+ADQ{jWlvt% zgWdCcD|4b)^=lNGlHr-cH$;Im6&Aw%W!+iGy4`$*G}3Zu(^c;eXOLv7LZ?$ zW}P7fy-Pxgk?M)w;4hYi%Hg$2JzKYOz*ewfQTd=$Y$HUxf`YUh8jJ1R0kJ8^dL6Z? zNoLsl@qDp&`!rp;Y2VzWx+CS{X^7xgg&M%}a7N(umBVD-M?UkbmnF zkPSmWNK-d)m&WSm`asQ#Oh_=?8m#6arf#H-iMBPn}IYPYasy#zMoxYwDMObo906!ki20yyUA zJ>zO2L0u@A2|I4@n-`&(B=X)8#JRPodMQrY&!;bYbWM(5QxR*ziDcor9KWgXj6rZB zLx7!7c_TSDgwtvq7eNg6JIIIA2!)eBjIQiHbSPq-AQfP15|_@pekR zGi2Hz@WN%)Z$!j97}-j=)+I<<#S~gf8@jyDnNYW(Ysg@CR7na@q1YD@Vqu2EY7;73 zzxj~lEEIrxLL49$^ZKpLvXgZ`m73%#K`F1GA6ay)dZtOgkWPSy9pwPq@s#I})Glw# zxy^=2c)08r#YUyigIem0V%H~_{mEwt#HNPUkD=Y zImkM_U;C1ZG>sb#H^?ZstP}W%E!%3$zbPcB)mm%#tv(zO9Xys>=VXL`X?k8clUz{7 zr!c@0gBy^w8|8P^{ULjae|_X;>OA_Jer8zcfasKWY?$Iy+Sk~PD+TwTsiaKptFR>I z)0%QSzA6D|Ae!;KD$FVvk9y){i)Z-S58ErXoi(%ZM4e|e{3 zIsV!id7cwLS>qVm`&`Rug5mH?CT&gw$9b_zHD7)}GP^ zoHwDR0Ude+&JW0yr}h`bK5sApF;i*qov`=0q?+k?))9_jifBwU`ZaY&S0rgJM=PlW zgO!?{v9eCsvXzJ#p#E=xlKO%lr9WpsIGc>twu}z5{i}aXi9P8mp;?s0vg=e(t?_go z5uKors(cRjbBJcO9~KTKw<_~SA45Npjt$ta|2f%!F`uYkeP^y~CjL*IbJ0uw7tZWZ zoilV=S4N+s52D9mB3#GcGM3zgK~z&_2LT|zvbXF z$zP~P&oCD5mx59QGFAKJNWG?n`X0jg8DIF|bnkiMWW-Xzz=c>oKn9LB=rG3#WUZNN z=^h%{$Pn$!bX3`+J2t7~d2@pCf7U}vEc~YDvX*4%Mbqe*nHCkHh-f#^ut-}}4J!Cc zF@K|NvX49L&wlvc9Cvq7vG4J67QH0#9zNjXRJ1~$oZqqKTgRV~xU$>E8iy24IPQ}$ zYy3w9@U_Wj!DU9YJ&~ivv(uyoH6U;^LV-k2xl5+yH-@??k#ppzAEg7AdD%V)V? z#cMmuGRyzW7N9$DKAx=K<+~eX1+yno^9!{hl>Is`sS<@1cm7@8DIn_agF$0g@TrAy zy$`MnHmRz#Sb~8tleC4TfjGwA?1BHotlxqJr8*SIU}`{y0@b8J5SfQRS9H-a5b14Q zyuL9PKPx-tWZI?1XECd_n(GfZz~yAMWJbt^19tHh?}^|D6s4=0T!tKaa< zOuVCdp;_-XVtnCMupIhIarVGTeucQzOKQwbndKV+5JVE*@q4x5HU;xA5rtN$U5PT6tV#^@ia4 z$9YHfx8GP2tbxHFDd(Iw+JYE!k-?uy&-IK|7zuV>7%C$c;0P9R#~g1zn$z!FSd59O0Z_@L}O#cQk!sp zQv5JPqcc{IGz}#!-a*PsnBKZdIyzUSZL7|2`+6}3RlH1HO3Sy?d8 zyJo;1%I$3v+LWdaN`+hC^$!$87qd$SQaULrxce?5BVTRHIN=ztb$PoHDPoX}YPG*M zs6c%n|9zUf9AP|_Qs(&4NK%ySbN!{$PCz&_7m|j|xijAO{n?^MaNOA1%Ry*GIZIi5 z0yZjI&fWB+(FU8nHOHFG(febgPQ?1hJ!{Gd*9eT*_h~ABU?R+k>U_ozU%d#j5(5e*#k0751m* zw~(caD;aGTmYZ_7y7qDUP>4aOvH6e>)!{-?=a28T|J;y$&?~6`Nwt7ZA<2bRD{rMk z3_N=`^?|}Wna*>qF?#P_Dt4kH z+}s6)i01P0URwIL_fuNF;VIIgrQ-z{~pbh2Ac(V$|uSY*k$Il*W7}ed|k^?5_RTBdN|GpGg%u;6rR^ ztLR2fCJGorW=BLu_f)O!=>^{Sno>3kL;7yX0}Qt-b_O1ZS?NEV)@gtBUJm!1!Z6 zzyAE~agPIk4NF_&70M#S6P!yZ>iBhgQhW7dha#YF<9e$z-xV*UCWu~DpE+!lJX zOy}m+j>>*mhukS@-D%%0p)h&Lt&o}c7&O*|IRi15MpTKPHEed{E7U3v9K?JGp+vh< zbNfirpnlvQc8_~0=SU?eW4Q5F`-P_sc3s<~E|P@i?#k+^NEF}9et04yIfJ72(Mz&p z+X~8Wg&|$CFH+ywG|YXpx&M%KDk_B*6fv8|Dpf#f^nPCUoLl!UOu{l5Z?>An%y4Al z*_77|W6F=LXgXepVx;Iv1hvWVln6XUt#%Pk;-AfZpdTUJBplwfQT!OGr#(p9$?lhq z;KuZD={CfK%h)L>rQ>dS3$iT^;b*}`X#;ep_`M5lO?3?kXk##$b1zD@s4Ap8q2dan zj=nW8F@L^j#yvvvm)>oMOkeCTM9#fd4a__&D*QS5qrYhMYxoB9en^vFOMcSJKA)A? zHW^In<#fam0Vt>ZEbi-8kx95*0GL+{e!9ydw5FgL%J9fLfY8(od}vyUE>(dQKl-$ zvFN^OY7-hdpwa0M6&peSY4X(yZy5y^4r<`w2K|yo?ws{B17!u#OwmSX1MV4PK$AQZ zURcH*x9n~x$(g=LgqFqd@)uUC?6#N27$AD!DY0qqUjuV-!JF(FQ=b}2g?CWny6|hu zUviUewK71e>L4!}WCH6P_RWbc`U=_0j1rUs)eo%?8`a^gIOu2cQ*yGvW#-&k0zTGOEiplQ#-#HK`sHi^M zWHD&;W=Pg`{Zyp-WNr*o$fst)mZce2m5=$gJ^2{|oQ;I=E@?$pLWY0sN}~$N&3}6-=R+875PV- zd|!f)GE&|u71^;(-0|SEr&tUbxAD<1*xHR%1oxKhVNqf$4C?|fln7KPm{ovqg+1}?2`4B8zRQ%hFR%&gM;4Pr0z zM5NxB2y}p>; zK{zWQK<^o#SCJ-41*^NybFSXKVxZUSho%?Ep2u{9 z5A~3$_0#>C4p-G!uMVCx*UEgj(x249I?$n0ziaquN$m{T3@_YUw3B$(cDUc6I_)>9yRM~k@=qBfG8*;7|1_^m+ zo%_A$#opq|F5k%)KZeX$*FL}QO*$7!2lpp=b|86FZ{0NhLKEWf&b>}q7FsEpq*&^j z@KEIPR;M%O9St^!dC6c%d%`UuA^%h9`=?Ktfzltz^_BGwT45jX_Ui#W?BeGne~w@B}_`1_)d;SIT&t?u}8l`iXS6c zP1Bkp3BG=y*xv0V?9D#0BJ^CeE$-k?%$fHC9Z$Uo<_Hs^?%ubj^Np>@{Dk{8{)15< zX|InjT4naYYNS)~8s=F?n{$pv}`%Rc_-k7|rExnqJL)6Sovf=g`&&U{csPvO`gtWXRc< zy!dU*v6SbNbvITRje51MTb1AVqanslYU5-=IuB>4AcWQ*?f{69Ktzk#M!ikPTEYat#mvwyn}>!)Mw{y@i(NxU@QCGZP9rz?|?d#=L_zg{UtksG%0* zDHn^%Z$qq?vr<-Wa;}Rlc_8bn@`99sg{2L3FM=dt;WT#Yd zf+BkEvMhYYr`Fm5?ijWBARS>x(rc<0`AF{9heeJFduyb>E*no-=9aeRXB>8;dfmo>S+&cZ+mvfA6P%uaK- ztxrjF%-f^CBuhm?2M-UM%y(y8es=8&>WMj5BOAuR^TTj=SgOi&aQWKzh1})EH9Km< zB-gBRHC1Iv@{O*xdfl$`dSpSemn?Z&&CH9H4NfeiKy6ONyU;zEPOee9-^kTJA$0e=Viu>jMz-JibNp-uc53U9N0$w6in2e)KL#aDk=JGr9m3t zsWP^L1{E-ZU^AZTtO%8UnBUno^3jk@vj?qiQ)Ji;zowB?Ol^AYR*5v#HNxf{#gNMo zM=)Yf#&2A|h&*iGgSaa-a@OZgqk_-P4D2Y_#KA_Rm|UoX)K&^R3{)o1BqbuKG+y|* z?z>#!HC1h|@=zDomv*R*nq|F{l_?`Zvf&hw*Kf!XU*ZYatr^T};pz7lYi-&vo|%Nj z!XhIlg1={13id8H2!Ey0yocYAooLQs9*2}FpDn%q)wQfoeF0va0!L6Rg^Nm1AaDi5 z^nhV+B!bRK7g^}}THWkXgmThO99h+ zv%60rd_v$_iQuI5VmkNRXwiwO8RvoD-4V}n3D(ryvm5#~L>qP|IuJ7+MWzN~l>bHx za`uDM*AVXJlG}X-*D^&ZD#B~-l0^|>h1bwFck#~Y5}c+N`Q)Eqt(`y8S}9P!8rHzw z?3ec^Z{XB?mDZ>9#?7z2&?$gV&zj3+JO@R2n^D7EkEmG z;ZT1%pKT42KSzYo83vYd1rm3#2oZPs~A zG^{1Lwet#1&3reB9|h8V5UxIiB~*(uDRzfOoozqwSH0r6yH;NXelIHrkpSC?no7 zXvn3s=y8s+1;u!AN&Kv&MODW!r{#%S#&0RM6yWxl~eoc@b<@b6^K+=x#%Xx@^L!UJqWKEGfm(|GU%iZh2nzzH>WRTcI zJ#%b1a_AzeiS$MfizUF$)19lPpOHnNUiM+3WY`;#=5<7`yts3>&>T~s#>wLSIjZx5 z)UJg+tE!*-y-DxDi%?0BlDKPtnXvBy6wmvLmL!qwhTZ18jGD|^nys>O!8Sg+t%onH zm*2uRou!MjdosJ1zPE)=$(GZas04Wpzx%FfvsdYh62=3E0J!EVGxn#@p$w*+NS??g}>9C9M;%J5~$qV$q2y~;IPKJ>xz z1G(-U)6`U2FCs=~Gd?p!qI((U<10hB)(_Z5$>q|P@1WfW5w$RVt?89($EbT8HI@`m zZUs(j-#vdzrLDQvs}C1Gf9igNTNk21fnHi@%~ZESryaCY?Vo*XM_aOo%08tHAN&oL z0^vGnC$;(eUaqnK3rwA3R0_o$#x)nd-i&N4LFc0js%tJjt}gJRnP@07sn+dHO4+L) zW0m{b7TE%g1MWzol`(j?8L3=c6zef&M#oEv(!?9@HoqA;7Ymy&%|{H~Q4)q*>s!Zn zzZdc<&$#rC`V!Rdt?QQ?adn8N2{mJ}l)~-ObH+sMx0RIgkX!Vf>a_sh#qJ)fehsqA zvnZBsn)LPQvdtF%f(g#jgLg|!bMJDUIzXsaX`HgN=&b9iPMf3GN2ewj^ zV9B&O4-6NWPG3x?wcJ3sqrQ%1AnAws^`zGke59&G`)2NFPk~*%2%;s3Mrl9>(^Msr z<;YY8FN%iK#LIr-6ged%nvgf(Xa$KhL}>Ti#4PL{_+O08GEBc(M`_!69)y$YAVc&F z`g6WfATzBWA77?QY*#Y+oZm~GK0a~P*<(d&5%a;Jh8MD*4SetsOe}gr2+9ca7%wGl zXjIeM2l3g5N+^|GZl`r#nzr9iGMip^UMVJ|LyIg}WmGWTs(G7acB<E|mVXj6TycHMe6-z?8{AN^{%gmCAh<8U%Pd^GMbh1t}xo?3Iy z^L4vpFXd_A!DT-?998euc{mW9H)pno{91B`=!?zS2vlTz*6fyM@)gJT8~$8b8p%xl z4~0JJCS-kbiU|p0myIJ^-sngbZNlE%uXjm0m!%SX!YH^vnS*t6hxT6yEZUsTF-UNF zJ}%2Pe)lf{7dT?%8$$*LRnBJD&bU7kByb+)tuP?jNY`~zRGsHHO!kC!Or!>-9^$hN zw6xpgo!>EP=7`h^?rQT639w5p!EoXeuPPPPZX!761`<(!QO9{#1dr5V`%JdL^t+qt zHp1s=*^H)&W4*d=O1B^>g7Wrl)9POs5yWBaw_OE+AZYH%gN}m1My>WK>_KeAkKv&@ z&(_2#T<~Ic_uSzJkoZ0mPMof{wko__F_`o!(zv<9Q?#Lt69~a8glC}>VxFF+y83Yd zBhb{*On{ER1yv946vJGJ!YVY(JYraLazApJ6g>z6t;cMAAz7X07*Rb*D==gfZcy`a zfHLFami{WFj8X4=ykXy#9@uDStE}7O-G&(->$`>c>q;_?Ihw7btV zA?2ZW?|giU>Apt7v6e#ojlyf<{D(7X+nCByop?U1!uW*G11j-Svk);%?Lc13*;}Ll zGRDodVQwlfLoF1OXnTrLTofMa9Jbd5xX+iGf4CXtALokoEc7P);I=@Z&+i>3%y(yX8-DrH)uvO68-&8Ljf2h z6(HFZk6G=tK%k?oWY^FwF+!qXkF!(^>=qc$^1H>F_Q4K!gey^R6Z9*VbGi=eC@zJ0 zy(R9Vz7L_W&S0_*EXK-ibHBo0Zj-^VD8L8Zfeb7{6e$d$e_VM;2xd%}mW8ZhoYd|^ z$>E)O+xFY+Y+FjpnzUnt@en^xmY{^oc9v4%536*A=)?Zfi&Ji{>i=jfk|9mDSXPIo z!_+-C=0;uPo18s_F;5)%y-?$yx12Q~*{1*Y3P5fq zJ}jC{1eu3lD7}q=TkU{{sl3icBWt%ER*`G_wAm;dJUy1 zOv{bh5KlhiE8^WW{_Ad-4FD%!NOtJIzfA1Wwt|IAvudOp$L`FlH}FQ@v3O8x!1!g5?6UtE-zFVM$hnyay;DRt zR9=%}&lI6<&RE znTV@uC*x0*oT957w~y_=*-N4>>Q^1jl+CP{rTjtaj}Z+IWSRjSSo4MePY6vA`1Ac) z_g!Y<&{V_xUFZkEjiU>w2K?F&{x}U_P0@Rrq}IV61BLdG?9WfG4u5mAl8`e_hKixI z5mng{{t)4}x(nF1-a1%Z#m=t&fV2n}sSq1nYN1j%UazRqtAhGTD)ePt@W=8^cFi#H zHSx<~ua7+~)Gr$@7)1y@r%phDs9}GKDeJbKhYB+1?l8CpcMvW^MiRKQCK<|oZc+X@piG2Zo`CD z-cBftz+@>&6Xm}bt8~~$$c4p*$c?5nrIz`PGT=_5un)V@YstDL-w!RID$Io;M!Fo)_kvf-D~Z=*ILiI z@4cNiKx0(r3$=U6D;n=i0Cq zoe2jcD@acV;*ARA2l+-$sS)fU`v0V10}nzDp8PysaW{=#t1SInGIOpB)13!;0s>Nx zrj@I_O+QMV*QccC+ff=Nkkj$ptX4YSFngKGc;u-4$TL|I_j2BgHrDZhChT)M^me{2 z$!j`;9veSWSM-T;4c4_&dcw^ycf;kOTRN#Kkt2d84EC5zBCkzf7Skp@4AD2$GoSx> zSfBkw2!&=DeC}s7;_#hqAM7qvd#GK!S8pkul$u@;R+lj9RS~s@-Cn)`|4H1VCH6MY zGlr>6X*-GweMY$?#ALUWD(4S1FDd$sTkMExw&lHCWHR1+_Lh;nf>xX<6{Cu7c6QIF ztDVPrE>Is)YB7IXpYvdnG@O^PAOk<_O37aW~m%sc&!I(!^AE{^Ppd^|TDUCuQqs zzFc2m$FOqLQE-Ha8FdtHj$=_oZGZj5l@^S47bJk;5LVPvxW$i)f-2;~+u-vnVGwUX zJW32BW#vhsQs)xpuKBH`kJF+*IYhVE!RO>VUarK0GTZd#_>b+Tk@#%P+1#A8X*M-F zsNFXd4qVRik@c*H?yx57I{-q%yFVBpSA7 z8_dtmecYrn_SJnp!angKzR_h;eb4a^_%|$bQc1WGzR#~@np#XF9Xc9GcSxz$#CFz4 z4)K~jil9p>9u0i{Ch)Kd)Ai5gQl!I@X#;#ACV1^X_VHXw(eIa_RjEvKT#8Ww#Tej5 z66qo%wFumqN7>2`Ric^7_X^!GnvpoNxYhVW&YE_ms}sloNa zAt9$|vA|a47-NefjyXrhii0Y;krpk227eZ}8g~l$XjHa1gfJIQ_#)1dK>C73e!u8)i5CkV+G44pRpe0%Y< zmw&;Pnuq~{g&BC$67+E~otGZ!HK%JxgP=L9inv&fN>-%0OJFVs93#aU{-TJhm|?-i zPsZVtnjjz9T=MR--1_%?e!MTHbo5Nk{H`A8Sbo6sa^SpJV7V;^L&uxWE&bY_iZ&HPL&v+$&ehFy z7Fo$Iv>dLq=&3rgG2mgCQ2z5HMmK4@e7OR;|VX*uB zC7e7(j6%Y5-`y7VQ5gRq2%C9Tiy8N4kvs1QO|XI*CW>xKx)mj6Y+qE;DYaEYUjz+i zTpfk2deGz&wzdQ^g55=Ansr5kd1Veyu)9dNqEZYNOdS+3+6|ByK2t<^1EbkJ;oCO|lRb8IhIj_sUZq@BEG$I|1*PjWGBPyQ~_ zTTpKOiViTUOn39u{6~c66qs2fzGW!}8bDVvIh@D?@eRAe1BtJCX2p*!So?*thJMT# zGmzd>vVQ3@j>5Q&VK`H0?UZLjW}8h&w|4s0h8)F+cGrOBcTEFdrww-)8i)4Sjjel{ zvToP4UAZkaT<^wa1zlc@NMFJCZ~CHb5E_4YyEe(?%>_+8miNPN3%r^vx}S4I4S&X6 z`p(E5*EIrmCBo0AEEC1eD|>cDW>6kUzD0kZ6t`%YMBT3G9=Z03j{SLa<%aF8zH%|y zTgRKk@94({@9S5pV_f(wFvUZgTg`K0#`X@+6n*M#dzH?XyzqtBwHf-C%#RkE*sIsr zvd+bx(YjwTo2Qs4GQl5t4U5=G7pPo89j3ve@@CLmo#dRQhW|6$(mO+Gj8<=lMWaX7 zd<(B~2>22zU$wDzG9Q zWnbh7xpRKgpWB`$Cgy2X6}%xCHC7uoAsPNrB34X$T*F}p-Lk-dwzQm!vG zMAN+{wOKYK_R#x_4DPqK+r04B3g?plQgQzeuW{ETey&*i9H!E@aW~YK51hhA&`4xe z&*>x_)`c;POo#honlL>?gB0BtFkhS{%nN01R#cBf2kj+K+GGRI z#Zy0-V8Jj2MGMi8(`EvPwRh%4-SaKJ%Gl_Y`K3S9#&w^XOpuQo)w{g2TR5boMAv$} z?w698Kj8B%l|@oL>hS5WQ5V+j&@-)z*=6yBKc+HdSL~^c%6_iOGu>L3Y6EaBRe?4! zSj`Q~ip&>7x(}#hQ;w}+NDuMrJ$EkQ1$9niMtgYp#M@z6LFGdsb-QSLS)FVJKH9c* zY47zp`r<)5iC-;qs=L+a&|9DTRA&y-_hme^VP`5CGOFumJihTGY^l(@uCQ@tNvH<@SRtTg!SrUb7O-;y*4V?b$Q-bUNBHcJR|wz`|3ZMBb%iEcz<_ z`}JoN1Fur3-{tu_u+-gkF`ezYY?hpm?#w<0$DxtUrSAkN#TL4_uhMQ_)N20x`ig4{ z%Y1QaE9;KWvF|1hohlzUWR-D9_VsWe#hFKuLl*OtE&EAY8oJ;H9u~$MN$Tz!o*kW$NqeO*8|xF{JklK!K=@=2tTK^p z8JHsl7rFD%e0Am};o@%AU&q?u*#I31hF9eXJxoCrN>rVOcRXQ=hpTLj7zkKkn zqc9^*o;ZFoJcG`8`-D?}J;|4ZiSqz4x>egSKR#-S)2;7MR@#+m*7pcd)&9Zw%a2d) z^(%H);9=@S@@`upCYuM9z>AqMIl*p|(I%#M9>RDBh_T3^2Zl9jHf6EzeMd0&hXccY zKbZ0W{mBSC<<6;NGz%v!mLIrvUhMji7E7dYS(9$`JPhOa$_w^a*%k=zLCyJnK#vWvlm}%8<_CV*zYPN zF%ayPVi0Vmz?3xf+`;Khf2@4rNPG&$I*PBN%FF+P}6} z5I=-xitj$A`26mKa{GI^j?VO;Fwb6m9zDgR$`%@zsO>#lyRU4Mco6HiEw69Qj_In$ zNu?e=S+~yx3^|-)U}Vd!HA#+HQk}`^^^A!%iHo&nc=?T`96tRKFSN$b?3KCMem=3r z{p9V!apbK2yjmxoZ=RM*!m$i>ghC&*Y;uSM&Rx*Up0Ax^PG35 zGbK}<)Y#3&bCKs;zJOZ30g2)5gY#-TZVW4CkI>yS&J@J+$uKA8FP@$~(zYS!1Oua3 zk%Q_cg+Dnj^(|`{wvt$;)FxJ{OFq)<|LUJSer9u{raZa)uIIHMaL?4f+|j4EFT2%( zZ`R>hvKUIvq)FNSK?b{3IJjoP%(AcDL^tp{$;{WvefOi4Tb$#Vx;`rjuKgr_s3sY^ zAm`Y@&X;hNLCX(Bd^r93XvH&_Y4&my zY)UlGGLkjs*;&^^{w74&I{6JMEImxJ&xaFZ1{=uV{PN3cPIyWASl^S)*6_PcBD za>>6kTKfki!j~t1GDSo2?d5ywbYp9x(HVx-i5!-2q~5R>;~j(|-zRYwJmd{ADzs{a zD8eKT`yNGN^vewH=lRd}>7k1wP3&aMPA$3dI)N<`86$@58R_G>Ya~93h#N(xot_}~ z@sy#Y`=Zz9UVUZ0&}1_y`M!S2^b8SgP0QOnxd#oX^%nOn$>}Nye_a2hRqJ#%gZ;ZE z61hsD4DJQbhvq^h!Dc6~&t+yOTC4Tme`0b)+M_hOn|h$SxPjeW)a7ndYOVcfjg4rj zNihB5`MblrMs3%=zM3rQ*BmcwNK@W*zA3bAjlT%bUm-Ak#8#W&wgwq9pKf=Y;>w~N}g;j$P{=!Xm?Qb zW^vi9NeC`c;LNpa{2d+S62{(_uCmf+*2FE7eR1@hz7)H@IOL>Yu3>VddXy29l5DwRY=?&KywUO(T?>vc#52aDebT-ys zNIGIH+{eHyBEn~SZ#KU=#--|V-aEHPEh)Y+)|GFoHl`U!ni##$*`TZU#EQ|>?s(D2 zObx3O6|L*;@X?!aME(PF;zmx#dCaE~h8j$72quNll$Ucb30_}jwGm|lo^7dyj z&FXTE`n}B0ToUG>dal(K0q!v7d?R1C`)reA!iJHiLUuJrtSlUIz_%y}s{`D*r+K8@ zQhq((Zdi1Z%ORB^G~Xhw5{UDykbVU-f;DDXpR6tx2pk39JMazXl|ojzz9@g5hhKft zPVbu}MtU=V+Gu<1z z=^pMl#TM)LQr_EqBzh$YM(VY{PxvU8->6|&hG7l8?ex^ng-bWC>a$h6E0_+C%%Fd+ z=aKm0^^sUEx7aa@#k`BoU;Z#Em6S1|kP?f1#L3ml#rb(_7*q^KS%{Mqv~1evT__~F zk+7VC$pUe9vO50+d&9*2CyIg0JQ@6!TqtZ!wDkf@KO!p{TjLrFj!y2X#qHFO!Xl3~ zdd0Oz+D?jWFYIcpyF9~re`BlT2^l;2yK-CyuAdj!eZpQg;4Do>uWF!-QWf*w3fpxh zi9>v^o-Qlz+1@G%eK%v;o-hy*f1nAs44rULJv(Q05fRF#yFY0%a8J4V zuzMO=e``rSm5ZEjT;zI=@`X@qZIwoy_k5?6y>&9G_HDpl(pGOk2$f_Iix=3{+r%%dR6#RLS*7#1#GRb?P(xG5k1XxMOXZ#*^a< z4^L5wH(!rS2@&LP<34flYp1CN)gAMx4Hwgo(gZtb$1P4i8+D!t9xhafGCCM6$d_C} z_;%!61=8N94_F16^I;pJmvn)as>ke&7-Eg46 zLgp>It7_j{I+WQy|=XD$tBD@!w3c#>2TR^R+?2>@)_3wl^7F$ zIfA={dUWZSc-f{>?U4pFR4^ z_}Ih_o5=dI-#zJ88dvtMvDk!F`Kcsfo=%542A;3vs9l;tcgaN8zkM-rxR9*@Ye!Eu zc*}1hG>qfz*hNi(txkBN2m)t&g}RLMopnOhhB^L#Pt7JlW+C-VF`@*ima*5(6EQ1G z0T`{gGEEMV8+%6@P!!>VQDFRK52{pyr|a~s(Jd`lVM|kdMSEB-MyUX_;5QT}s95)G z^&J_`tsSaIYQQq0kTr4c0ajmKfCYx8*|4R|11^}bHI!gd&-7f`eQ95*cmvWKhn=ZJ zR1xR?qt<~&EK(D(X&7k&BtYA){L(gvD*ajNZ0xS??tYNl#nsQj4%{CuQrLJ_uEKCI zfBqr%=Kv)9l!#$!Kj!^gKWx%&(Dd=LafT!dA7?_H@a+74tx$t{f4zRG=ijQAM06Jq zu8dGn(%#O;8+?NHF5W&Xq|gR2&oW%ZvdX97yWuPRvzEHrIod!v-qr3`IlvDVq~)0L z9Rd|82Rj$96?&MTqn-V#AGsRtUiSEX_CN`50!hfm6sxJLqZ|4ih{?~DK={XI@Y@m7 zHrjdFqde$B=`_AxE_kA!mDStlkPG-#e+#}&r-mg!vSoSjHWNW#$AgMVD)@hr_a zY^}q`*Uh{QUl$e-DMnj&7cVzkmjI{_{lP`=VdL$A;kvv5vJhrES13Oe!o9#S)K%C1>% z|LpntujBNex<809ubCmplN{(8;Tn3&oSehAYMlSPwwQZ4uZ2jDIIA z18Hz<@_QbX|JU;WongLT_%VSn%L>1G;93I=0LB1w0FoCkz#4$kod75ezXD_?)ds){ zfISxs(F4Qa7!TkSKoo#ZJ)v+SARnL#-~^!fU*(D7ev~h&`-d>@0)!F2s4w`T>*T?e z2B16;&m02oJpiJEfclRMz_bEX4uw%4to8%qZPky`=K*p6R1a!fD}WoI13+xC^M(y1SK(ayUh!(0F`8fdS=$HZk#Uc9E0NC5a{sf?X6vP+WLqX#y z27vO+0$`tn&?kGqtp~UQ>;Q<4HsB-x$z>3*AArU+N^1d#0ZIX=o>kh9SK>`Uo~htR z^`Q2lzD3(ps0@lje4w_h$_}*w$zwI_4Stjdg^_IZ0H_a;AL4Mg!Cg&9<#GXtFO-hx zdIC^>Y;GfXvXx!leXj~`)H1=K-<+L=(va(L(Ka1Ry#x zfTIA^CJ%uBieCcULo0p}a8bJz0CIpd0BTz@0P%q8Mr}aj9r2Iw9Drz|{z5XSfUNc4 z>H~rR2LNG!O8~?Nk_Q?~h)=}ZSpd2ZQ5vEn4M22|3{YLcD{d{gh(A;(N<;m?0tf)0 zGOPU5fFH>Lg%O=}0Fn=?7xLpw!9{6|#4v#myb z(S3yIBma-(QM(XdE5O0n$5lZXwFR{g^$p?$l>v~LfxAi{<=F;U9hXoap?^^sR6nAv z13>X8jCiyIpz&n^K>cU~FahiaAUddw0RYvvAAssYeiXm@>_B`Wo)Fzt*`Pd3-afWy z+@lKzkOUm^wevxB9B{QKxZuTje%zaf{d_MQ4-fQg`hJNy5`PR3DE4tj1Uc<&2!#<* TY_E;qVHtUGIazTjNy+~QkK3|- diff --git a/packages/rollup-plugin-html/test/fixtures/assets/webmanifest.json b/packages/rollup-plugin-html/test/fixtures/assets/webmanifest.json deleted file mode 100644 index 3c935e111..000000000 --- a/packages/rollup-plugin-html/test/fixtures/assets/webmanifest.json +++ /dev/null @@ -1 +0,0 @@ -{ "message": "hello world" } diff --git a/packages/rollup-plugin-html/test/fixtures/assets/x/foo.svg b/packages/rollup-plugin-html/test/fixtures/assets/x/foo.svg deleted file mode 100644 index ae50e4b7a..000000000 --- a/packages/rollup-plugin-html/test/fixtures/assets/x/foo.svg +++ /dev/null @@ -1 +0,0 @@ -y diff --git a/packages/rollup-plugin-html/test/fixtures/basic/app.js b/packages/rollup-plugin-html/test/fixtures/basic/app.js deleted file mode 100644 index 6be02374d..000000000 --- a/packages/rollup-plugin-html/test/fixtures/basic/app.js +++ /dev/null @@ -1 +0,0 @@ -console.log('hello world'); diff --git a/packages/rollup-plugin-html/test/fixtures/basic/index.html b/packages/rollup-plugin-html/test/fixtures/basic/index.html deleted file mode 100644 index e3b2a7bfb..000000000 --- a/packages/rollup-plugin-html/test/fixtures/basic/index.html +++ /dev/null @@ -1,6 +0,0 @@ - - -

Hello world

- - - diff --git a/packages/rollup-plugin-html/test/fixtures/basic/not-index.html b/packages/rollup-plugin-html/test/fixtures/basic/not-index.html deleted file mode 100644 index 0d1ef727f..000000000 --- a/packages/rollup-plugin-html/test/fixtures/basic/not-index.html +++ /dev/null @@ -1,5 +0,0 @@ - - -

not-index.html

- - diff --git a/packages/rollup-plugin-html/test/fixtures/basic/pages/page-a.html b/packages/rollup-plugin-html/test/fixtures/basic/pages/page-a.html deleted file mode 100644 index b73a61323..000000000 --- a/packages/rollup-plugin-html/test/fixtures/basic/pages/page-a.html +++ /dev/null @@ -1,7 +0,0 @@ - - -

page-a.html

- - - - diff --git a/packages/rollup-plugin-html/test/fixtures/basic/pages/page-a.js b/packages/rollup-plugin-html/test/fixtures/basic/pages/page-a.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rollup-plugin-html/test/fixtures/basic/pages/page-b.html b/packages/rollup-plugin-html/test/fixtures/basic/pages/page-b.html deleted file mode 100644 index c61c79147..000000000 --- a/packages/rollup-plugin-html/test/fixtures/basic/pages/page-b.html +++ /dev/null @@ -1,7 +0,0 @@ - - -

page-b.html

- - - - diff --git a/packages/rollup-plugin-html/test/fixtures/basic/pages/page-b.js b/packages/rollup-plugin-html/test/fixtures/basic/pages/page-b.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rollup-plugin-html/test/fixtures/basic/pages/page-c.html b/packages/rollup-plugin-html/test/fixtures/basic/pages/page-c.html deleted file mode 100644 index 05e306ede..000000000 --- a/packages/rollup-plugin-html/test/fixtures/basic/pages/page-c.html +++ /dev/null @@ -1,7 +0,0 @@ - - -

page-c.html

- - - - diff --git a/packages/rollup-plugin-html/test/fixtures/basic/pages/page-c.js b/packages/rollup-plugin-html/test/fixtures/basic/pages/page-c.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rollup-plugin-html/test/fixtures/basic/pages/shared.js b/packages/rollup-plugin-html/test/fixtures/basic/pages/shared.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rollup-plugin-html/test/fixtures/basic/src/foo.js b/packages/rollup-plugin-html/test/fixtures/basic/src/foo.js deleted file mode 100644 index 6be02374d..000000000 --- a/packages/rollup-plugin-html/test/fixtures/basic/src/foo.js +++ /dev/null @@ -1 +0,0 @@ -console.log('hello world'); diff --git a/packages/rollup-plugin-html/test/fixtures/basic/src/index.html b/packages/rollup-plugin-html/test/fixtures/basic/src/index.html deleted file mode 100644 index 02ab03884..000000000 --- a/packages/rollup-plugin-html/test/fixtures/basic/src/index.html +++ /dev/null @@ -1,6 +0,0 @@ - - -

Foo

- - - diff --git a/packages/rollup-plugin-html/test/fixtures/inject-service-worker/index.html b/packages/rollup-plugin-html/test/fixtures/inject-service-worker/index.html deleted file mode 100644 index cc61d8f8f..000000000 --- a/packages/rollup-plugin-html/test/fixtures/inject-service-worker/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - -

inject a service worker into /index.html

- - diff --git a/packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-pure-html/index.html b/packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-pure-html/index.html deleted file mode 100644 index 2719f7b18..000000000 --- a/packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-pure-html/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - -

inject a service worker into /sub-page/index.html

- - diff --git a/packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-with-js/index.html b/packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-with-js/index.html deleted file mode 100644 index ea1b64867..000000000 --- a/packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-with-js/index.html +++ /dev/null @@ -1,6 +0,0 @@ - - -

inject a service worker into /sub-page/index.html

- - - diff --git a/packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-with-js/sub-js.js b/packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-with-js/sub-js.js deleted file mode 100644 index 50e2d5d4b..000000000 --- a/packages/rollup-plugin-html/test/fixtures/inject-service-worker/sub-with-js/sub-js.js +++ /dev/null @@ -1 +0,0 @@ -console.log('sub-with-js'); diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/fonts/font-normal.woff2 b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/fonts/font-normal.woff2 deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-a.css b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-a.css deleted file mode 100644 index 391732403..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-a.css +++ /dev/null @@ -1,7 +0,0 @@ -@font-face { - font-family: 'Font'; - src: url('fonts/font-normal.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; -} \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-b.css b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-b.css deleted file mode 100644 index 2c13aec86..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-b.css +++ /dev/null @@ -1,7 +0,0 @@ -@font-face { - font-family: 'Font2'; - src: url('fonts/font-normal.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; -} \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.avif b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.avif deleted file mode 100644 index 2e65efe2a..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.avif +++ /dev/null @@ -1 +0,0 @@ -a \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.gif b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.gif deleted file mode 100644 index 63d8dbd40..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.gif +++ /dev/null @@ -1 +0,0 @@ -b \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpeg b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpeg deleted file mode 100644 index 3410062ba..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpeg +++ /dev/null @@ -1 +0,0 @@ -c \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpg b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpg deleted file mode 100644 index c59d9b634..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpg +++ /dev/null @@ -1 +0,0 @@ -d \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.png b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.png deleted file mode 100644 index 9cbe6ea56..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.png +++ /dev/null @@ -1 +0,0 @@ -e \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.svg b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.svg deleted file mode 100644 index 4d1ae35ba..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.svg +++ /dev/null @@ -1 +0,0 @@ -f \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.webp b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.webp deleted file mode 100644 index 7937c68fb..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.webp +++ /dev/null @@ -1 +0,0 @@ -g \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/styles.css b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/styles.css deleted file mode 100644 index bb0fa19d5..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/styles.css +++ /dev/null @@ -1,24 +0,0 @@ -#a { - background-image: url("images/star.svg"); -} -#b { - background-image: url("images/star.svg#foo"); -} -#c { - background-image: url("images/star.png"); -} -#d { - background-image: url("images/star.jpg"); -} -#e { - background-image: url("images/star.jpeg"); -} -#f { - background-image: url("images/star.webp"); -} -#g { - background-image: url("images/star.gif"); -} -#h { - background-image: url("images/star.avif"); -} \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/fonts/font-bold.woff2 b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/fonts/font-bold.woff2 deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/fonts/font-normal.woff2 b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/fonts/font-normal.woff2 deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/node_modules-styles-with-fonts.css b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/node_modules-styles-with-fonts.css deleted file mode 100644 index a12383545..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/node_modules-styles-with-fonts.css +++ /dev/null @@ -1,15 +0,0 @@ -@font-face { - font-family: 'Font'; - src: url('fonts/font-normal.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'Font'; - src: url('fonts/font-bold.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; -} diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/fonts/font-bold.woff2 b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/fonts/font-bold.woff2 deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/fonts/font-normal.woff2 b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/fonts/font-normal.woff2 deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/styles-with-fonts.css b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/styles-with-fonts.css deleted file mode 100644 index a12383545..000000000 --- a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/styles-with-fonts.css +++ /dev/null @@ -1,15 +0,0 @@ -@font-face { - font-family: 'Font'; - src: url('fonts/font-normal.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'Font'; - src: url('fonts/font-bold.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; -} diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-a.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-a.html deleted file mode 100644 index cb228937b..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-a.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - -

hello world

- - - - - - diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-b.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-b.html deleted file mode 100644 index 16aa25114..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-b.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - -

hello world

- - - - - - diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-c.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-c.html deleted file mode 100644 index 59ae9f80d..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/csp-page-c.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - -

hello world

- - - - - - diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-a.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-a.js deleted file mode 100644 index 1e396263d..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-a.js +++ /dev/null @@ -1,3 +0,0 @@ -import './modules/module-a.js'; - -console.log('entrypoint-a.js'); diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-b.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-b.js deleted file mode 100644 index 43071b698..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-b.js +++ /dev/null @@ -1,3 +0,0 @@ -import './modules/module-b.js'; - -console.log('entrypoint-b.js'); diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-c.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-c.js deleted file mode 100644 index bfca03130..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/entrypoint-c.js +++ /dev/null @@ -1,3 +0,0 @@ -import './modules/module-c.js'; - -console.log('entrypoint-c.js'); diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/exclude/assets/partial.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/exclude/assets/partial.html deleted file mode 100644 index ab7da9e61..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/exclude/assets/partial.html +++ /dev/null @@ -1 +0,0 @@ -I'm a partial! diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/exclude/index.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/exclude/index.html deleted file mode 100644 index 97ed84e72..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/exclude/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/foo/foo.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/foo/foo.html deleted file mode 100644 index 5f2b937fa..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/foo/foo.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/foo/foo.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/foo/foo.js deleted file mode 100644 index 81afa3157..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/foo/foo.js +++ /dev/null @@ -1 +0,0 @@ -console.log('foo'); diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/index.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/index.html deleted file mode 100644 index 2dffb27c2..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/index.html +++ /dev/null @@ -1,3 +0,0 @@ -

hello world

- - \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/module.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/module.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-a.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-a.js deleted file mode 100644 index 5c2200d03..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-a.js +++ /dev/null @@ -1,3 +0,0 @@ -import './shared-module.js'; - -console.log('module-a.js'); diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-b.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-b.js deleted file mode 100644 index 48ebae220..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-b.js +++ /dev/null @@ -1,3 +0,0 @@ -import './shared-module.js'; - -console.log('module-b.js'); diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-c.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-c.js deleted file mode 100644 index 48ebae220..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/module-c.js +++ /dev/null @@ -1,3 +0,0 @@ -import './shared-module.js'; - -console.log('module-b.js'); diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/shared-module.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/shared-module.js deleted file mode 100644 index 0842867ca..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/modules/shared-module.js +++ /dev/null @@ -1 +0,0 @@ -console.log('shared-module.js'); diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/my-page.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/my-page.html deleted file mode 100644 index bbbc2338d..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/my-page.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-a.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-a.html deleted file mode 100644 index b73a61323..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-a.html +++ /dev/null @@ -1,7 +0,0 @@ - - -

page-a.html

- - - - diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-a.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-a.js deleted file mode 100644 index e1fa0e0ce..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-a.js +++ /dev/null @@ -1 +0,0 @@ -export default 'page a'; diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-b.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-b.html deleted file mode 100644 index c61c79147..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-b.html +++ /dev/null @@ -1,7 +0,0 @@ - - -

page-b.html

- - - - diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-b.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-b.js deleted file mode 100644 index c469b20a3..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-b.js +++ /dev/null @@ -1 +0,0 @@ -export default 'page b'; diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-c.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-c.html deleted file mode 100644 index 05e306ede..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-c.html +++ /dev/null @@ -1,7 +0,0 @@ - - -

page-c.html

- - - - diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-c.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-c.js deleted file mode 100644 index a236bb8bd..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/page-c.js +++ /dev/null @@ -1 +0,0 @@ -export default 'page c'; diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/shared.js b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/shared.js deleted file mode 100644 index 7c18e65fc..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pages/shared.js +++ /dev/null @@ -1 +0,0 @@ -export default 'shared'; diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pure-index.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pure-index.html deleted file mode 100644 index b44880118..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pure-index.html +++ /dev/null @@ -1 +0,0 @@ -

hello world

diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pure-index2.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pure-index2.html deleted file mode 100644 index 45b615663..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/pure-index2.html +++ /dev/null @@ -1 +0,0 @@ -

hey there

diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/retain-attributes.html b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/retain-attributes.html deleted file mode 100644 index 7d4b2712d..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/retain-attributes.html +++ /dev/null @@ -1,3 +0,0 @@ -

hello world

- - diff --git a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/styles.css b/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/styles.css deleted file mode 100644 index c11eb03fe..000000000 --- a/packages/rollup-plugin-html/test/fixtures/rollup-plugin-html/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -:root { - color: blue; -} 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 7ccd949b0..bec535513 100644 --- a/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts +++ b/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts @@ -1,224 +1,538 @@ -import { rollup, OutputChunk, OutputAsset, OutputOptions, Plugin } from 'rollup'; +import synchronizedPrettier from '@prettier/sync'; +import * as prettier from 'prettier'; +import { rollup, OutputChunk, OutputOptions, Plugin, RollupBuild } from 'rollup'; import { expect } from 'chai'; import path from 'path'; +import fs from 'fs'; import { rollupPluginHTML } from '../src/index.js'; -type Output = (OutputChunk | OutputAsset)[]; +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 getChunk(output: Output, name: string) { - return output.find(o => o.fileName === name && o.type === 'chunk') as OutputChunk; +function merge(strings: TemplateStringsArray, ...values: string[]): string { + return strings.reduce((acc, str, i) => acc + str + (values[i] || ''), ''); } -function getAsset(output: Output, name: string) { - return output.find(o => o.name === name && o.type === 'asset') as OutputAsset & { - source: string; - }; +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)]; } +const html = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.html'](merge(strings, ...values)); + +const css = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.css'](merge(strings, ...values)); + +const js = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.js'](merge(strings, ...values)); + +const svg = (strings: TemplateStringsArray, ...values: string[]) => + extnameToFormatter['.svg'](merge(strings, ...values)); + const outputConfig: OutputOptions = { format: 'es', dir: 'dist', }; -function stripNewlines(str: string) { - return str.replace(/(\r\n|\n|\r)/gm, ''); +async function generateTestBundle(build: RollupBuild, outputConfig: OutputOptions) { + const { output } = await build.generate(outputConfig); + const chunks: Record = {}; + const assets: 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; + if (typeof code !== 'string' && filename.endsWith('.css')) { + code = Buffer.from(code).toString('utf8'); + } + if (typeof code === 'string' && formatter) { + code = formatter(code); + } + assets[filename] = code; + } + } + + return { output, chunks, assets }; +} + +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; } -const rootDir = path.join(__dirname, 'fixtures', 'rollup-plugin-html'); +function cleanApp() { + const tmpDir = path.join(__dirname, './.tmp'); + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true }); + } +} describe('rollup-plugin-html', () => { + afterEach(() => { + cleanApp(); + }); + it('can build with an input path as input', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + `, + 'entrypoint-a.js': js` + import './modules/module-a.js'; + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + import './modules/module-b.js'; + console.log('entrypoint-b.js'); + `, + 'modules/module-a.js': js` + import './shared-module.js'; + console.log('module-a.js'); + `, + 'modules/module-b.js': js` + import './shared-module.js'; + console.log('module-b.js'); + `, + 'modules/shared-module.js': js` + console.log('shared-module.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ - input: require.resolve('./fixtures/rollup-plugin-html/index.html'), rootDir, + input: './index.html', }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(4); - const { code: entryA } = getChunk(output, 'entrypoint-a.js'); - const { code: entryB } = getChunk(output, 'entrypoint-b.js'); - expect(entryA).to.include("console.log('entrypoint-a.js');"); - expect(entryB).to.include("console.log('entrypoint-b.js');"); - expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal( - '

hello world

' + - '' + - '' + - '', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(3); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + + expect(assets['index.html']).to.equal(html` + + + + + + + + `); }); it('can build with html file as rollup input', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + `, + 'entrypoint-a.js': js` + import './modules/module-a.js'; + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + import './modules/module-b.js'; + console.log('entrypoint-b.js'); + `, + 'modules/module-a.js': js` + import './shared-module.js'; + console.log('module-a.js'); + `, + 'modules/module-b.js': js` + import './shared-module.js'; + console.log('module-b.js'); + `, + 'modules/shared-module.js': js` + console.log('shared-module.js'); + `, + }); + const config = { - input: require.resolve('./fixtures/rollup-plugin-html/index.html'), + input: './index.html', plugins: [rollupPluginHTML({ rootDir })], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(4); - const { code: entryA } = getChunk(output, 'entrypoint-a.js'); - const { code: entryB } = getChunk(output, 'entrypoint-b.js'); - expect(entryA).to.include("console.log('entrypoint-a.js');"); - expect(entryB).to.include("console.log('entrypoint-b.js');"); - expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal( - '

hello world

' + - '' + - '' + - '', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(3); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + + expect(assets['index.html']).to.equal(html` + + + + + + + + `); }); it('will retain attributes on script tags', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + `, + 'entrypoint-a.js': js` + import './modules/module-a.js'; + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + import './modules/module-b.js'; + console.log('entrypoint-b.js'); + `, + 'modules/module-a.js': js` + import './shared-module.js'; + console.log('module-a.js'); + `, + 'modules/module-b.js': js` + import './shared-module.js'; + console.log('module-b.js'); + `, + 'modules/shared-module.js': js` + console.log('shared-module.js'); + `, + }); + const config = { - input: require.resolve('./fixtures/rollup-plugin-html/retain-attributes.html'), + input: './index.html', plugins: [rollupPluginHTML({ rootDir })], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(4); - const { code: entryA } = getChunk(output, 'entrypoint-a.js'); - const { code: entryB } = getChunk(output, 'entrypoint-b.js'); - expect(entryA).to.include("console.log('entrypoint-a.js');"); - expect(entryB).to.include("console.log('entrypoint-b.js');"); - expect(stripNewlines(getAsset(output, 'retain-attributes.html').source)).to.equal( - '

hello world

' + - '' + - '' + - '', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(3); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + + expect(assets['index.html']).to.equal(html` + + + + + + + + `); }); it('can build with pure html file as rollup input', async () => { + const rootDir = createApp({ + 'index.html': html` + + + +

hello world

+ + + `, + }); + const config = { - input: require.resolve('./fixtures/rollup-plugin-html/pure-index.html'), + input: './index.html', plugins: [rollupPluginHTML({ rootDir })], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(stripNewlines(getAsset(output, 'pure-index.html').source)).to.equal( - '

hello world

', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['index.html']).to.equal(html` + + + +

hello world

+ + + `); }); it('can build with multiple pure html inputs', async () => { + const rootDir = createApp({ + 'index1.html': html` + + + +

hello world

+ + + `, + 'index2.html': html` + + + +

hey there

+ + + `, + }); + const config = { plugins: [ rollupPluginHTML({ - input: [ - require.resolve('./fixtures/rollup-plugin-html/pure-index.html'), - require.resolve('./fixtures/rollup-plugin-html/pure-index2.html'), - ], rootDir, + input: ['./index1.html', './index2.html'], }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(stripNewlines(getAsset(output, 'pure-index.html').source)).to.equal( - '

hello world

', - ); - expect(stripNewlines(getAsset(output, 'pure-index2.html').source)).to.equal( - '

hey there

', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(2); + + expect(assets['index1.html']).to.equal(html` + + + +

hello world

+ + + `); + + expect(assets['index2.html']).to.equal(html` + + + +

hey there

+ + + `); }); it('can build with html string as input', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ + rootDir, input: { name: 'index.html', - html: '

Hello world

', + html: ``, }, - rootDir, }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(2); - expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal( - '

Hello world

' + - '', - ); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); }); it('resolves paths relative to virtual html filename', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ + rootDir, input: { - name: 'pages/index.html', - html: '

Hello world

', + name: 'nested/index.html', + html: ``, }, - rootDir, }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(2); - expect(stripNewlines(getAsset(output, 'pages/index.html').source)).to.equal( - '

Hello world

' + - '', - ); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['nested/index.html']).to.equal(html` + + + + + + + `); }); it('can build with inline modules', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ rootDir, input: { name: 'index.html', - html: '

Hello world

', + html: ``, }, }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(2); - const hash = '5ec680a4efbb48ae254268ab1defe610'; - const { code: appCode } = getChunk(output, `inline-module-${hash}.js`); - expect(appCode).to.include("console.log('entrypoint-a.js');"); - expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal( - '

Hello world

' + - `` + - '', - ); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + const hash = '16165cb387fc14ed1fe1749d05f19f7b'; + + expect(chunks[`inline-module-${hash}.js`]).to.include(js`console.log('app.js');`); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); }); it('resolves inline module imports relative to the HTML file', async () => { + const rootDir = createApp({ + 'nested/index.html': html` + + + + + + + `, + 'nested/app.js': js` + console.log('app.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ - input: require.resolve('./fixtures/rollup-plugin-html/foo/foo.html'), rootDir, + input: './nested/index.html', }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(2); - const { code: appCode } = getChunk(output, 'inline-module-1b13383486c70d87f4e2585ff87b147c.js'); - expect(appCode).to.include("console.log('foo');"); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + const hash = 'b774aefb8bf002b291fd54d27694a34d'; + expect(chunks[`inline-module-${hash}.js`]).to.include(js`console.log('app.js');`); }); it('can build transforming final output', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + const config = { - input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'), plugins: [ rollupPluginHTML({ rootDir, input: { - html: '

Hello world

', + html: `

Hello world

`, }, transformHtml(html) { return html.replace('Hello world', 'Goodbye world'); @@ -226,133 +540,243 @@ describe('rollup-plugin-html', () => { }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(2); - expect(getAsset(output, 'index.html').source).to.equal( - '

Goodbye world

' + - '', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['index.html']).to.equal(html` + + + +

Goodbye world

+ + + + `); }); it('can build with a public path', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + const config = { - input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'), plugins: [ rollupPluginHTML({ rootDir, input: { - html: '

Hello world

', + html: ``, }, publicPath: '/static/', }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(2); - expect(getAsset(output, 'index.html').source).to.equal( - '

Hello world

' + - '', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); }); it('can build with a public path with a file in a directory', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + const config = { - input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'), plugins: [ rollupPluginHTML({ rootDir, input: { - name: 'pages/index.html', - html: '

Hello world

', + name: 'nested/index.html', + html: ``, }, publicPath: '/static/', }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(2); - expect(getAsset(output, 'pages/index.html').source).to.equal( - '

Hello world

' + - '', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['nested/index.html']).to.equal(html` + + + + + + + `); }); it('can build with multiple build outputs', async () => { + const rootDir = createApp({ + 'app.js': js` + import './modules/module.js'; + console.log('app.js'); + `, + 'modules/module.js': js` + console.log('module.js'); + `, + }); + const plugin = rollupPluginHTML({ rootDir, input: { - html: '

Hello world

', + html: ``, }, publicPath: '/static/', }); + const config = { - input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'), + input: path.join(rootDir, 'app.js'), plugins: [plugin], }; + const build = await rollup(config); - const bundleA = build.generate({ + + const bundleA = generateTestBundle(build, { format: 'system', dir: 'dist', plugins: [plugin.api.addOutput('legacy')], }); - const bundleB = build.generate({ + + const bundleB = generateTestBundle(build, { format: 'es', dir: 'dist', plugins: [plugin.api.addOutput('modern')], }); - const { output: outputA } = await bundleA; - const { output: outputB } = await bundleB; - expect(outputA.length).to.equal(1); - expect(outputB.length).to.equal(2); - const { code: entrypointA1 } = getChunk(outputA, 'entrypoint-a.js'); - const { code: entrypointA2 } = getChunk(outputB, 'entrypoint-a.js'); - expect(entrypointA1).to.include("console.log('entrypoint-a.js');"); - expect(entrypointA1).to.include("console.log('module-a.js');"); - expect(entrypointA2).to.include("console.log('entrypoint-a.js');"); - expect(entrypointA2).to.include("console.log('module-a.js');"); - expect(getAsset(outputA, 'index.html')).to.not.exist; - expect(getAsset(outputB, 'index.html').source).to.equal( - '

Hello world

' + - '' + - '', - ); + + const { chunks: chunksA, assets: assetsA } = await bundleA; + const { chunks: chunksB, assets: assetsB } = await bundleB; + + expect(Object.keys(chunksA)).to.have.lengthOf(1); + expect(Object.keys(assetsA)).to.have.lengthOf(0); + expect(Object.keys(chunksB)).to.have.lengthOf(1); + expect(Object.keys(assetsB)).to.have.lengthOf(1); + + expect(chunksA['app.js']).to.include(js`console.log('app.js');`); + expect(chunksA['app.js']).to.include(js`console.log('module.js');`); + expect(chunksB['app.js']).to.include(js`console.log('app.js');`); + expect(chunksB['app.js']).to.include(js`console.log('module.js');`); + + expect(assetsA['index.html']).to.not.exist; + expect(assetsB['index.html']).to.equal(html` + + + + + + + + `); }); it('can build with index.html as input and an extra html file as output', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ rootDir, input: { - html: '

Hello world

', + html: ``, }, }), rollupPluginHTML({ rootDir, input: { name: 'foo.html', - html: '

foo.html

', + html: `

foo.html

`, }, }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(4); - expect(getChunk(output, 'entrypoint-a.js')).to.exist; - expect(getAsset(output, 'index.html').source).to.equal( - '

Hello world

' + - '', - ); - expect(getAsset(output, 'foo.html').source).to.equal( - '

foo.html

', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(2); + expect(Object.keys(assets)).to.have.lengthOf(2); + + expect(chunks['app.js']).to.exist; + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + + expect(assets['foo.html']).to.equal(html` + + + +

foo.html

+ + + `); }); it('can build with multiple html inputs', async () => { + const rootDir = createApp({ + 'entrypoint-a.js': js` + import './modules/module-a.js'; + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + import './modules/module-b.js'; + console.log('entrypoint-b.js'); + `, + 'entrypoint-c.js': js` + import './modules/module-c.js'; + console.log('entrypoint-c.js'); + `, + 'modules/module-a.js': js` + import './shared-module.js'; + console.log('module-a.js'); + `, + 'modules/module-b.js': js` + import './shared-module.js'; + console.log('module-b.js'); + `, + 'modules/module-c.js': js` + import './shared-module.js'; + console.log('module-c.js'); + `, + 'modules/shared-module.js': js` + console.log('shared-module.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ @@ -374,24 +798,91 @@ describe('rollup-plugin-html', () => { }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(7); - expect(getChunk(output, 'entrypoint-a.js')).to.exist; - expect(getChunk(output, 'entrypoint-b.js')).to.exist; - expect(getChunk(output, 'entrypoint-c.js')).to.exist; - expect(getAsset(output, 'page-a.html').source).to.equal( - '

Page A

', - ); - expect(getAsset(output, 'page-b.html').source).to.equal( - '

Page B

', - ); - expect(getAsset(output, 'page-c.html').source).to.equal( - '

Page C

', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(4); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(chunks['entrypoint-a.js']).to.exist; + expect(chunks['entrypoint-b.js']).to.exist; + expect(chunks['entrypoint-c.js']).to.exist; + + expect(assets['page-a.html']).to.equal(html` + + + +

Page A

+ + + + `); + + expect(assets['page-b.html']).to.equal(html` + + + +

Page B

+ + + + `); + + expect(assets['page-c.html']).to.equal(html` + + + +

Page C

+ + + + `); }); it('can use a glob to build multiple pages', async () => { + const rootDir = createApp({ + 'pages/page-a.html': html` + + +

page-a.html

+ + + + + `, + 'pages/page-b.html': html` + + +

page-b.html

+ + + + + `, + 'pages/page-c.html': html` + + +

page-c.html

+ + + + + `, + 'pages/page-a.js': js` + export default 'page a'; + `, + 'pages/page-b.js': js` + export default 'page b'; + `, + 'pages/page-c.js': js` + export default 'page c'; + `, + 'pages/shared.js': js` + export default 'shared'; + `, + }); + const config = { plugins: [ rollupPluginHTML({ @@ -401,82 +892,144 @@ describe('rollup-plugin-html', () => { ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - const pageA = getAsset(output, 'page-a.html').source; - const pageB = getAsset(output, 'page-b.html').source; - const pageC = getAsset(output, 'page-c.html').source; - expect(output.length).to.equal(7); - expect(getChunk(output, 'page-a.js')).to.exist; - expect(getChunk(output, 'page-b.js')).to.exist; - expect(getChunk(output, 'page-c.js')).to.exist; - expect(pageA).to.include('

page-a.html

'); - expect(pageA).to.include(''); - expect(pageA).to.include(''); - expect(pageB).to.include('

page-b.html

'); - expect(pageB).to.include(''); - expect(pageB).to.include(''); - expect(pageC).to.include('

page-c.html

'); - expect(pageC).to.include(''); - expect(pageC).to.include(''); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(4); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(chunks['page-a.js']).to.exist; + expect(chunks['page-b.js']).to.exist; + expect(chunks['page-c.js']).to.exist; + + expect(assets['page-a.html']).to.equal(html` + + + +

page-a.html

+ + + + + `); + + expect(assets['page-b.html']).to.equal(html` + + + +

page-b.html

+ + + + + `); + + // TODO: investigate why shared.js is after page-c.js here but before in the others + expect(assets['page-c.html']).to.equal(html` + + + +

page-c.html

+ + + + + `); }); it('can exclude globs', async () => { + const rootDir = createApp({ + 'exclude/index.html': html``, + 'exclude/assets/partial.html': html`I'm a partial!`, + }); + const config = { plugins: [ rollupPluginHTML({ + rootDir, input: 'exclude/**/*.html', exclude: '**/partial.html', - rootDir, }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(2); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets).to.have.keys(['index.html']); }); it('creates unique inline script names', async () => { + const rootDir = createApp({}); + const config = { plugins: [ rollupPluginHTML({ rootDir, input: [ { - name: 'foo/index.html', - html: '

Page A

', + name: 'nestedA/indexA.html', + html: `

Page A

`, }, { - name: 'bar/index.html', - html: '

Page B

', + name: 'nestedB/indexB.html', + html: `

Page B

`, }, { - name: 'x.html', - html: '

Page C

', + name: 'indexC.html', + html: `

Page C

`, }, ], }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(6); - expect(getChunk(output, 'inline-module-b8667c926d8a16ee8b4499492c1726ed.js')).to.exist; - expect(getChunk(output, 'inline-module-c91911481b66e7483731d4de5df616a6.js')).to.exist; - expect(getChunk(output, 'inline-module-fbf0242ebea027b7392472c19328791d.js')).to.exist; - expect(getAsset(output, 'foo/index.html').source).to.equal( - '

Page A

', - ); - expect(getAsset(output, 'bar/index.html').source).to.equal( - '

Page B

', - ); - expect(getAsset(output, 'x.html').source).to.equal( - '

Page C

', - ); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(3); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(chunks['inline-module-d463148d1d5869e52917a3b270db9e72.js']).to.exist; + expect(chunks['inline-module-b81da853430abdf130bcc7c4d0ade6d9.js']).to.exist; + expect(chunks['inline-module-170bb2146da66c440259138c7e0fea7e.js']).to.exist; + + expect(assets['nestedA/indexA.html']).to.equal(html` + + + +

Page A

+ + + + `); + + expect(assets['nestedB/indexB.html']).to.equal(html` + + + +

Page B

+ + + + `); + + expect(assets['indexC.html']).to.equal(html` + + + +

Page C

+ + + + `); }); it('deduplicates common modules', async () => { + const rootDir = createApp({}); + const config = { plugins: [ rollupPluginHTML({ @@ -484,136 +1037,247 @@ describe('rollup-plugin-html', () => { input: [ { name: 'a.html', - html: '

Page A

', + html: `

Page A

`, }, { name: 'b.html', - html: '

Page B

', + html: `

Page B

`, }, { name: 'c.html', - html: '

Page C

', + html: `

Page C

`, }, ], }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(4); - expect(getChunk(output, 'inline-module-b8667c926d8a16ee8b4499492c1726ed.js')).to.exist; - expect(getAsset(output, 'a.html').source).to.equal( - '

Page A

', - ); - expect(getAsset(output, 'b.html').source).to.equal( - '

Page B

', - ); - expect(getAsset(output, 'c.html').source).to.equal( - '

Page C

', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(chunks['inline-module-44281cf3dede62434e0dd368df08902f.js']).to.exist; + + expect(assets['a.html']).to.equal(html` + + + +

Page A

+ + + + `); + + expect(assets['b.html']).to.equal(html` + + + +

Page B

+ + + + `); + + expect(assets['c.html']).to.equal(html` + + + +

Page C

+ + + + `); }); it('outputs the hashed entrypoint name', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ rootDir, input: { - html: - '

Hello world

' + ``, + html: ``, }, }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate({ + + const build = await rollup(config); + const { output, chunks, assets } = await generateTestBundle(build, { ...outputConfig, entryFileNames: '[name]-[hash].js', }); - expect(output.length).to.equal(2); - const entrypoint = output.find(f => + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + const appChunk = output.find(f => // @ts-ignore - f.facadeModuleId.endsWith('entrypoint-a.js'), + f.facadeModuleId.endsWith('app.js'), ) as OutputChunk; + // ensure it's actually hashed - expect(entrypoint.fileName).to.not.equal('entrypoint-a.js'); + expect(appChunk.fileName).to.not.equal('app.js'); + // get hashed name dynamically - expect(getAsset(output, 'index.html').source).to.equal( - `

Hello world

`, - ); + expect(assets['index.html']).to.equal(html` + + + + + + + `); }); it('outputs import path relative to the final output html', async () => { + const rootDir = createApp({ + 'app.js': js` + console.log('app.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ rootDir, input: { - name: 'pages/index.html', - html: '

Hello world

', + name: 'nested/index.html', + html: '', }, }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(2); - expect(getAsset(output, 'pages/index.html').source).to.equal( - '

Hello world

', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['nested/index.html']).to.equal(html` + + + + + + + `); }); it('can change HTML root directory', async () => { + const rootDir = createApp({ + 'different-root/src/app.js': js` + console.log('app.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ - rootDir: path.join(__dirname, 'fixtures'), + rootDir: path.join(rootDir, 'different-root'), input: { - name: 'rollup-plugin-html/pages/index.html', - html: '

Hello world

', + name: 'src/nested/index.html', + html: '', }, }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(2); - expect(getAsset(output, 'rollup-plugin-html/pages/index.html').source).to.equal( - '

Hello world

', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['src/nested/index.html']).to.equal(html` + + + + + + + `); }); it('can get the input with getInputs()', async () => { // default filename const pluginA = rollupPluginHTML({ input: { html: 'Hello world' } }); + // filename inferred from input filename + const rootDirB = createApp({ + 'my-page.html': html``, + 'app.js': js`console.log('app.js');`, + }); const pluginB = rollupPluginHTML({ - input: require.resolve('./fixtures/rollup-plugin-html/my-page.html'), + input: path.join(rootDirB, 'my-page.html'), }); + // filename explicitly set + const rootDirC = createApp({ + 'index.html': html``, + 'app.js': js`console.log('app.js');`, + }); const pluginC = rollupPluginHTML({ input: { - name: 'pages/my-other-page.html', - path: require.resolve('./fixtures/rollup-plugin-html/index.html'), + name: 'nested/my-other-page.html', + path: path.join(rootDirC, 'index.html'), }, }); - await rollup({ - input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'), - plugins: [pluginA], - }); + + await rollup({ plugins: [pluginA] }); await rollup({ plugins: [pluginB] }); await rollup({ plugins: [pluginC] }); + expect(pluginA.api.getInputs()[0].name).to.equal('index.html'); expect(pluginB.api.getInputs()[0].name).to.equal('my-page.html'); - expect(pluginC.api.getInputs()[0].name).to.equal('pages/my-other-page.html'); + expect(pluginC.api.getInputs()[0].name).to.equal('nested/my-other-page.html'); }); it('supports other plugins injecting a transform function', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + `, + 'entrypoint-a.js': js` + import './modules/module-a.js'; + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + import './modules/module-b.js'; + console.log('entrypoint-b.js'); + `, + 'modules/module-a.js': js` + import './shared-module.js'; + console.log('module-a.js'); + `, + 'modules/module-b.js': js` + import './shared-module.js'; + console.log('module-b.js'); + `, + 'modules/shared-module.js': js` + console.log('shared-module.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ rootDir, - input: require.resolve('./fixtures/rollup-plugin-html/index.html'), + input: './index.html', }), { name: 'other-plugin', @@ -632,684 +1296,1992 @@ describe('rollup-plugin-html', () => { } as Plugin, ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(4); - const { code: entryA } = getChunk(output, 'entrypoint-a.js'); - const { code: entryB } = getChunk(output, 'entrypoint-b.js'); - expect(entryA).to.include("console.log('entrypoint-a.js');"); - expect(entryB).to.include("console.log('entrypoint-b.js');"); - expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal( - '

hello world

' + - '' + - '' + - '', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(3); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + `); }); it('includes referenced assets in the bundle', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + 'image-b.png': 'image-b.png', + 'image-c.png': 'image-c.png', + 'image-a.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + :root { + color: blue; + } + `, + 'foo/x.css': css` + :root { + color: x; + } + `, + 'foo/bar/y.css': css` + :root { + color: y; + } + `, + 'webmanifest.json': { message: 'hello world' }, + }); + const config = { plugins: [ rollupPluginHTML({ + rootDir, input: { - html: ` - - - - - - - - - - - -
- -
- -`, + html: html` + + + + + + + + + + + + +
+ +
+ + + `, }, - rootDir: path.join(__dirname, 'fixtures', 'assets'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(11); - const expectedAssets = [ - 'image-c.png', - 'webmanifest.json', - 'image-a.svg', - 'styles.css', - 'x.css', - 'y.css', - 'image-b.svg', - ]; - - for (const name of expectedAssets) { - const asset = getAsset(output, name); - expect(asset).to.exist; - expect(asset.source).to.exist; - } - - const outputHtml = getAsset(output, 'index.html').source; - expect(outputHtml).to.include( - '', - ); - expect(outputHtml).to.include( - '', - ); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include( - '', - ); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include(''); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(10); + + expect(assets).to.have.keys([ + 'assets/image-a-XOCPHCrV.png', + 'assets/image-b-BgQHKcRn.png', + 'assets/image-c-C4yLPiIL.png', + 'assets/image-a-BCCvKrTe.svg', + 'assets/image-b-C4stzVZW.svg', + 'assets/styles-CF2Iy5n1.css', + 'assets/x-DDGg8O6h.css', + 'assets/y-DJTrnPH3.css', + 'assets/webmanifest-BkrOR1WG.json', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + +
+ +
+ + + `); }); - it('deduplicates static assets with similar names', async () => { + it('[legacy] includes referenced assets in the bundle', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + 'image-b.png': 'image-b.png', + 'image-c.png': 'image-c.png', + 'image-a.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + :root { + color: blue; + } + `, + 'foo/x.css': css` + :root { + color: x; + } + `, + 'foo/bar/y.css': css` + :root { + color: y; + } + `, + 'webmanifest.json': { message: 'hello world' }, + }); + const config = { plugins: [ rollupPluginHTML({ + rootDir, + extractAssets: 'legacy-html', input: { - html: ` - - - - -`, + html: html` + + + + + + + + + + + + +
+ +
+ + + `, }, - rootDir: path.join(__dirname, 'fixtures', 'assets'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(10); + + expect(assets).to.have.keys([ + 'assets/image-a.png', + 'assets/image-b.png', + 'assets/image-c-C4yLPiIL.png', + 'assets/image-a.svg', + 'assets/image-b-C4stzVZW.svg', + 'assets/styles-CF2Iy5n1.css', + 'assets/x-DDGg8O6h.css', + 'assets/y-DJTrnPH3.css', + 'assets/webmanifest.json', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + +
+ +
+ + + `); + }); + + it('does not deduplicate static assets with similar names', async () => { + const rootDir = createApp({ + 'foo.svg': svg``, + 'x/foo.svg': svg``, + }); - expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal( - '' + - '' + - '' + - '', - ); + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(assets).to.have.keys([ + 'assets/foo-BCCvKrTe.svg', + 'assets/foo-C4stzVZW.svg', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + `); }); - it('static and hashed asset nodes can reference the same files', async () => { + it('[legacy] deduplicates static assets with similar names', async () => { + const rootDir = createApp({ + 'foo.svg': svg``, + 'x/foo.svg': svg``, + }); + const config = { plugins: [ rollupPluginHTML({ + rootDir, + extractAssets: 'legacy-html', input: { - html: ` - - - - -`, + html: html` + + + + + + + `, }, - rootDir: path.join(__dirname, 'fixtures', 'assets'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(assets).to.have.keys(['assets/foo.svg', 'assets/foo1.svg', 'index.html']); + + expect(assets['index.html']).to.equal(html` + + + + + + + + `); + }); - expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal( - '' + - '', - ); + it('[legacy] static and hashed asset nodes can reference the same files', async () => { + const rootDir = createApp({ + 'foo.svg': svg``, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + extractAssets: 'legacy-html', + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(assets).to.have.keys(['assets/foo.svg', 'assets/foo-BCCvKrTe.svg', 'index.html']); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + `); }); it('deduplicates common assets', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + }); + const config = { plugins: [ rollupPluginHTML({ + rootDir, input: { - html: ` - - - - - -`, + html: html` + + + + + + + + `, }, - rootDir: path.join(__dirname, 'fixtures', 'assets'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - - expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal( - '' + - '' + - '' + - '' + - '', - ); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(2); + + expect(assets).to.have.keys(['assets/image-a-XOCPHCrV.png', 'index.html']); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + `); }); it('deduplicates common assets across HTML files', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + }); + const config = { plugins: [ rollupPluginHTML({ + rootDir, input: [ { name: 'page-a.html', - html: ` - - - - `, + html: html` + + + + + + `, }, { name: 'page-b.html', - html: ` - - - - `, + html: html` + + + + + + `, }, { name: 'page-c.html', - html: ` - - - - - `, + html: html` + + + + + + + `, }, ], - rootDir: path.join(__dirname, 'fixtures', 'assets'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - - expect(stripNewlines(getAsset(output, 'page-a.html').source)).to.equal( - '' + - ' ' + - ' ', - ); - - expect(stripNewlines(getAsset(output, 'page-b.html').source)).to.equal( - '' + - ' ' + - ' ', - ); - - expect(stripNewlines(getAsset(output, 'page-c.html').source)).to.equal( - '' + - ' ' + - ' ' + - ' ', - ); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(assets).to.have.keys([ + 'assets/image-a-XOCPHCrV.png', + 'page-a.html', + 'page-b.html', + 'page-c.html', + ]); + + expect(assets['page-a.html']).to.equal(html` + + + + + + + `); + + expect(assets['page-b.html']).to.equal(html` + + + + + + + `); + + expect(assets['page-c.html']).to.equal(html` + + + + + + + + `); }); it('can turn off extracting assets', async () => { + const rootDir = createApp({ + 'image-c.png': 'image-c.png', + 'image-b.svg': svg``, + 'styles.css': css` + :root { + color: blue; + } + `, + }); + const config = { plugins: [ rollupPluginHTML({ extractAssets: false, + rootDir, input: { - html: ` - - - - - -`, + html: html` + + + + + + + + `, }, - rootDir: path.join(__dirname, 'fixtures', 'assets'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - - expect(output.length).to.equal(2); - expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal( - '', - ); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + `); }); it('can inject a CSP meta tag for inline scripts', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + + + `, + 'entrypoint-a.js': js` + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + console.log('entrypoint-b.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ - input: require.resolve('./fixtures/rollup-plugin-html/csp-page-a.html'), - rootDir, strictCSPInlineScripts: true, + rootDir, + input: './index.html', }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(4); - const { code: entryA } = getChunk(output, 'entrypoint-a.js'); - const { code: entryB } = getChunk(output, 'entrypoint-b.js'); - expect(entryA).to.include("console.log('entrypoint-a.js');"); - expect(entryB).to.include("console.log('entrypoint-b.js');"); - expect(stripNewlines(getAsset(output, 'csp-page-a.html').source)).to.equal( - '' + - "

hello world

' + - "" + - "" + - '' + - '' + - '', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(2); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + `); }); it('can add to an existing CSP meta tag for inline scripts', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + + + + + `, + 'entrypoint-a.js': js` + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + console.log('entrypoint-b.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ - input: require.resolve('./fixtures/rollup-plugin-html/csp-page-b.html'), - rootDir, strictCSPInlineScripts: true, + rootDir, + input: './index.html', }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(4); - const { code: entryA } = getChunk(output, 'entrypoint-a.js'); - const { code: entryB } = getChunk(output, 'entrypoint-b.js'); - expect(entryA).to.include("console.log('entrypoint-a.js');"); - expect(entryB).to.include("console.log('entrypoint-b.js');"); - expect(stripNewlines(getAsset(output, 'csp-page-b.html').source)).to.equal( - '' + - "

hello world

' + - "" + - "" + - '' + - '' + - '', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(2); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + `); }); it('can add to an existing CSP meta tag for inline scripts even if script-src is already there', async () => { + const rootDir = createApp({ + 'index.html': html` + + + + + + + + + + + + `, + 'entrypoint-a.js': js` + console.log('entrypoint-a.js'); + `, + 'entrypoint-b.js': js` + console.log('entrypoint-b.js'); + `, + }); + const config = { plugins: [ rollupPluginHTML({ - input: require.resolve('./fixtures/rollup-plugin-html/csp-page-c.html'), - rootDir, strictCSPInlineScripts: true, + rootDir, + input: './index.html', }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - expect(output.length).to.equal(4); - const { code: entryA } = getChunk(output, 'entrypoint-a.js'); - const { code: entryB } = getChunk(output, 'entrypoint-b.js'); - expect(entryA).to.include("console.log('entrypoint-a.js');"); - expect(entryB).to.include("console.log('entrypoint-b.js');"); - expect(stripNewlines(getAsset(output, 'csp-page-c.html').source)).to.equal( - '' + - "

hello world

' + - "" + - "" + - '' + - '' + - '', - ); + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(2); + expect(Object.keys(assets)).to.have.lengthOf(1); + + expect(chunks['entrypoint-a.js']).to.include(js`console.log('entrypoint-a.js');`); + expect(chunks['entrypoint-b.js']).to.include(js`console.log('entrypoint-b.js');`); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + `); }); it('can inject a service worker registration script if injectServiceWorker and serviceWorkerPath are provided', async () => { - const serviceWorkerPath = path.join( - // @ts-ignore - path.resolve(outputConfig.dir), - 'service-worker.js', - ); + const rootDir = createApp({ + 'index.html': html` + + +

inject a service worker into /index.html

+ + + `, + 'sub-pure-html/index.html': html` + + +

inject a service worker into /sub-page/index.html

+ + + `, + 'sub-with-js/index.html': html` + + +

inject a service worker into /sub-page/index.html

+ + + + `, + 'sub-with-js/sub-js.js': js`console.log('sub-with-js');`, + }); const config = { plugins: [ rollupPluginHTML({ + rootDir, input: '**/*.html', - rootDir: path.join(__dirname, 'fixtures', 'inject-service-worker'), flattenOutput: false, injectServiceWorker: true, - serviceWorkerPath, + serviceWorkerPath: path.join( + path.resolve(outputConfig.dir as string), + 'service-worker.js', + ), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - function extractServiceWorkerPath(src: string) { - const registerOpen = src.indexOf(".register('"); - const registerClose = src.indexOf("')", registerOpen + 11); - return src.substring(registerOpen + 11, registerClose); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(3); + + expect(assets).to.have.keys([ + 'index.html', + 'sub-with-js/index.html', + 'sub-pure-html/index.html', + ]); + + function extractServiceWorkerPath(code: string) { + const registerOpen = code.indexOf(".register('"); + const registerClose = code.indexOf("')", registerOpen + 11); + return code.substring(registerOpen + 11, registerClose); } - expect(extractServiceWorkerPath(getAsset(output, 'index.html').source)).to.equal( - 'service-worker.js', + expect(extractServiceWorkerPath(assets['index.html'] as string)).to.equal('service-worker.js'); + expect(extractServiceWorkerPath(assets['sub-with-js/index.html'] as string)).to.equal( + '../service-worker.js', + ); + expect(extractServiceWorkerPath(assets['sub-pure-html/index.html'] as string)).to.equal( + '../service-worker.js', ); - expect( - extractServiceWorkerPath(getAsset(output, path.join('sub-with-js', 'index.html')).source), - ).to.equal(`../service-worker.js`); - expect( - extractServiceWorkerPath(getAsset(output, path.join('sub-pure-html', 'index.html')).source), - ).to.equal(`../service-worker.js`); }); it('does support a absolutePathPrefix to allow for sub folder deployments', async () => { + const rootDir = createApp({ + 'x/foo.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + :root { + color: blue; + } + `, + }); + const config = { plugins: [ rollupPluginHTML({ + absolutePathPrefix: '/my-prefix/', + rootDir, input: { - html: ` - - - - - -`, + html: html` + + + + + + + + + + `, name: 'x/index.html', }, - rootDir: path.join(__dirname, 'fixtures', 'assets'), - absolutePathPrefix: '/my-prefix/', }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - - expect(stripNewlines(getAsset(output, 'x/index.html').source)).to.equal( - [ - '', - '', - '', - '', - '', - ].join(''), - ); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(assets).to.have.keys([ + 'assets/styles-CF2Iy5n1.css', + 'assets/foo-CxmWeBHm.svg', + 'assets/image-b-C4stzVZW.svg', + 'x/index.html', + ]); + + expect(assets['x/index.html']).to.equal(html` + + + + + + + + + + `); }); it('handles fonts linked from css files', async () => { + const rootDir = createApp({ + 'fonts/font-bold.woff2': 'font-bold', + 'fonts/font-normal.woff2': 'font-normal', + 'styles.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `, + }); + const config = { plugins: [ rollupPluginHTML({ - bundleAssetsFromCss: true, + rootDir, input: { - html: ` + html: html` - + - - + + + `, + }, + }), + ], + }; + + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(assets).to.have.keys([ + 'assets/font-normal-Cht9ZB76.woff2', + 'assets/font-bold-eQjSonqH.woff2', + 'assets/styles-Dhs3ufep.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + + expect(assets['assets/styles-Dhs3ufep.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('font-bold-eQjSonqH.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `); + }); + + it('[legacy] handles fonts linked from css files', async () => { + const rootDir = createApp({ + 'fonts/font-bold.woff2': 'font-bold', + 'fonts/font-normal.woff2': 'font-normal', + 'styles.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `, + }); + + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + extractAssets: 'legacy-html-and-css', + input: { + html: html` + + + + + `, }, - rootDir: path.join(__dirname, 'fixtures', 'resolves-assets-in-styles'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - - const fontNormal = output.find(o => o.name?.endsWith('font-normal.woff2')); - const fontBold = output.find(o => o.name?.endsWith('font-normal.woff2')); - const style = output.find(o => o.name?.endsWith('styles-with-fonts.css')); - // It has emitted the font - expect(fontBold).to.exist; - expect(fontNormal).to.exist; - // e.g. "font-normal-f0mNRiTD.woff2" - // eslint-disable-next-line no-useless-escape - const regex = /assets[\/\\]font-normal-\w+\.woff2/; - // It outputs the font to the assets folder - expect(regex.test(fontNormal!.fileName)).to.equal(true); - - // The source of the style includes the font - const source = (style as OutputAsset)?.source.toString(); - expect(source.includes(fontNormal!.fileName)); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(assets).to.have.keys([ + 'assets/assets/font-normal-Cht9ZB76.woff2', + 'assets/assets/font-bold-eQjSonqH.woff2', + 'assets/styles-BUBaODov.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + + expect(assets['assets/styles-BUBaODov.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('assets/font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('assets/font-bold-eQjSonqH.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `); }); it('handles fonts linked from css files in node_modules', async () => { + const rootDir = createApp({ + 'node_modules/foo/fonts/font-bold.woff2': 'font-bold', + 'node_modules/foo/fonts/font-normal.woff2': 'font-normal', + 'node_modules/foo/styles.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `, + }); + const config = { plugins: [ rollupPluginHTML({ - bundleAssetsFromCss: true, + rootDir, input: { - html: ` + html: html` - + - - + `, }, - rootDir: path.join(__dirname, 'fixtures', 'resolves-assets-in-styles-node-modules'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(assets).to.have.keys([ + 'assets/font-normal-Cht9ZB76.woff2', + 'assets/font-bold-eQjSonqH.woff2', + 'assets/styles-Dhs3ufep.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + + expect(assets['assets/styles-Dhs3ufep.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('font-bold-eQjSonqH.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `); + }); - const font = output.find(o => o.name?.endsWith('font-normal.woff2')); - const style = output.find(o => o.name?.endsWith('node_modules-styles-with-fonts.css')); + it('[legacy] handles fonts linked from css files in node_modules', async () => { + const rootDir = createApp({ + 'node_modules/foo/fonts/font-bold.woff2': 'font-bold', + 'node_modules/foo/fonts/font-normal.woff2': 'font-normal', + 'node_modules/foo/styles.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `, + }); - // It has emitted the font - expect(font).to.exist; - // e.g. "font-normal-f0mNRiTD.woff2" - // eslint-disable-next-line no-useless-escape - const regex = /assets[\/\\]font-normal-\w+\.woff2/; - // It outputs the font to the assets folder - expect(regex.test(font!.fileName)).to.equal(true); + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + extractAssets: 'legacy-html-and-css', + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; - // The source of the style includes the font - const source = (style as OutputAsset)?.source.toString(); - expect(source.includes(font!.fileName)); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(assets).to.have.keys([ + 'assets/assets/font-normal-Cht9ZB76.woff2', + 'assets/assets/font-bold-eQjSonqH.woff2', + 'assets/styles-BUBaODov.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + + expect(assets['assets/styles-BUBaODov.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('assets/font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + + @font-face { + font-family: Font; + src: url('assets/font-bold-eQjSonqH.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; + } + `); }); it('handles duplicate fonts correctly', async () => { + const rootDir = createApp({ + 'fonts/font-normal.woff2': 'font-normal', + 'styles-a.css': css` + @font-face { + font-family: Font; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `, + 'styles-b.css': css` + @font-face { + font-family: Font2; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `, + }); + const config = { plugins: [ rollupPluginHTML({ - bundleAssetsFromCss: true, + rootDir, input: { - html: ` + html: html` - - + `, }, - rootDir: path.join(__dirname, 'fixtures', 'resolves-assets-in-styles-duplicates'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - - const fonts = output.filter(o => o.name?.endsWith('font-normal.woff2')); - expect(fonts.length).to.equal(1); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(4); + + expect(assets).to.have.keys([ + 'assets/font-normal-Cht9ZB76.woff2', + 'assets/styles-a-jFIfrzm8.css', + 'assets/styles-b-B-8m1N7T.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + `); + + expect(assets['assets/styles-a-jFIfrzm8.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['assets/styles-b-B-8m1N7T.css']).to.equal(css` + @font-face { + font-family: Font2; + src: url('font-normal-Cht9ZB76.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `); }); it('handles images referenced from css', async () => { + const rootDir = createApp({ + 'images/star.avif': 'star.avif', + 'images/star.gif': 'star.gif', + 'images/star.jpeg': 'star.jpeg', + 'images/star.jpg': 'star.jpg', + 'images/star.png': 'star.png', + 'images/star.svg': 'star.svg', + 'images/star.webp': 'star.webp', + 'styles.css': css` + #a { + background-image: url('images/star.avif'); + } + + #b { + background-image: url('images/star.gif'); + } + + #c { + background-image: url('images/star.jpeg'); + } + + #d { + background-image: url('images/star.jpg'); + } + + #e { + background-image: url('images/star.png'); + } + + #f { + background-image: url('images/star.svg'); + } + + #g { + background-image: url('images/star.svg#foo'); + } + + #h { + background-image: url('images/star.webp'); + } + `, + }); + const config = { plugins: [ rollupPluginHTML({ - bundleAssetsFromCss: true, + rootDir, input: { - html: ` + html: html` - - + `, }, - rootDir: path.join(__dirname, 'fixtures', 'resolves-assets-in-styles-images'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); - - expect(output.find(o => o.name?.endsWith('star.avif'))).to.exist; - expect(output.find(o => o.name?.endsWith('star.gif'))).to.exist; - expect(output.find(o => o.name?.endsWith('star.jpeg'))).to.exist; - expect(output.find(o => o.name?.endsWith('star.jpg'))).to.exist; - expect(output.find(o => o.name?.endsWith('star.png'))).to.exist; - expect(output.find(o => o.name?.endsWith('star.svg'))).to.exist; - expect(output.find(o => o.name?.endsWith('star.webp'))).to.exist; - - const rewrittenCss = (output.find(o => o.name === 'styles.css') as OutputAsset).source - .toString() - .trim(); - expect(rewrittenCss).to.equal( - `#a { - background-image: url("assets/star-CauvOfkF.svg"); -} - -#b { - background-image: url("assets/star-CauvOfkF.svg#foo"); -} + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(9); + + expect(assets).to.have.keys([ + 'assets/star-D_LO5feX.avif', + 'assets/star-BKg9qmmf.gif', + 'assets/star-BZWqL7hS.jpeg', + 'assets/star-Df0JryvN.jpg', + 'assets/star-CXig10q7.png', + 'assets/star-CwhgM_z4.svg', + 'assets/star-CKbh5mKn.webp', + 'assets/styles-mywkihBc.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + + expect(assets['assets/styles-mywkihBc.css']).to.equal(css` + #a { + background-image: url('star-D_LO5feX.avif'); + } + + #b { + background-image: url('star-BKg9qmmf.gif'); + } + + #c { + background-image: url('star-BZWqL7hS.jpeg'); + } + + #d { + background-image: url('star-Df0JryvN.jpg'); + } + + #e { + background-image: url('star-CXig10q7.png'); + } + + #f { + background-image: url('star-CwhgM_z4.svg'); + } + + #g { + background-image: url('star-CwhgM_z4.svg#foo'); + } + + #h { + background-image: url('star-CKbh5mKn.webp'); + } + `); + }); -#c { - background-image: url("assets/star-B4Suw7Xi.png"); -} + it('[legacy] handles images referenced from css', async () => { + const rootDir = createApp({ + 'images/star.avif': 'star.avif', + 'images/star.gif': 'star.gif', + 'images/star.jpeg': 'star.jpeg', + 'images/star.jpg': 'star.jpg', + 'images/star.png': 'star.png', + 'images/star.svg': 'star.svg', + 'images/star.webp': 'star.webp', + 'styles.css': css` + #a { + background-image: url('images/star.avif'); + } + + #b { + background-image: url('images/star.gif'); + } + + #c { + background-image: url('images/star.jpeg'); + } + + #d { + background-image: url('images/star.jpg'); + } + + #e { + background-image: url('images/star.png'); + } + + #f { + background-image: url('images/star.svg'); + } + + #g { + background-image: url('images/star.svg#foo'); + } + + #h { + background-image: url('images/star.webp'); + } + `, + }); -#d { - background-image: url("assets/star-DKp8fJdA.jpg"); -} + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + extractAssets: 'legacy-html-and-css', + input: { + html: html` + + + + + + + `, + }, + }), + ], + }; -#e { - background-image: url("assets/star-b-LlGmiF.jpeg"); -} + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(9); + + expect(assets).to.have.keys([ + 'assets/assets/star-D_LO5feX.avif', + 'assets/assets/star-BKg9qmmf.gif', + 'assets/assets/star-BZWqL7hS.jpeg', + 'assets/assets/star-Df0JryvN.jpg', + 'assets/assets/star-CXig10q7.png', + 'assets/assets/star-CwhgM_z4.svg', + 'assets/assets/star-CKbh5mKn.webp', + 'assets/styles-Cuqf3qRf.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + `); + + expect(assets['assets/styles-Cuqf3qRf.css']).to.equal(css` + #a { + background-image: url('assets/star-D_LO5feX.avif'); + } + + #b { + background-image: url('assets/star-BKg9qmmf.gif'); + } + + #c { + background-image: url('assets/star-BZWqL7hS.jpeg'); + } + + #d { + background-image: url('assets/star-Df0JryvN.jpg'); + } + + #e { + background-image: url('assets/star-CXig10q7.png'); + } + + #f { + background-image: url('assets/star-CwhgM_z4.svg'); + } + + #g { + background-image: url('assets/star-CwhgM_z4.svg#foo'); + } + + #h { + background-image: url('assets/star-CKbh5mKn.webp'); + } + `); + }); -#f { - background-image: url("assets/star-CXtvny3e.webp"); -} + it('allows to exclude external assets usign a glob pattern', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + 'image-b.png': 'image-b.png', + 'image-a.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + #a1 { + background-image: url('image-a.png'); + } + + #a2 { + background-image: url('image-a.svg'); + } + + #d1 { + background-image: url('./image-b.png'); + } + + #d2 { + background-image: url('./image-b.svg'); + } + `, + 'foo/x.css': css` + :root { + color: x; + } + `, + 'foo/bar/y.css': css` + :root { + color: y; + } + `, + 'webmanifest.json': { message: 'hello world' }, + }); -#g { - background-image: url("assets/star-_hNhEHAt.gif"); -} + const config = { + plugins: [ + rollupPluginHTML({ + externalAssets: ['**/foo/**/*', '*.svg'], + rootDir, + input: { + html: html` + + + + + + + + + + + + + +
+ +
+ + + `, + }, + }), + ], + }; -#h { - background-image: url("assets/star-fTpYetjL.avif"); -}`.trim(), - ); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(5); + + expect(assets).to.have.keys([ + 'assets/image-a-XOCPHCrV.png', + 'assets/image-b-BgQHKcRn.png', + 'assets/styles-Bv-4gk2N.css', + 'assets/webmanifest-BkrOR1WG.json', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + + +
+ +
+ + + `); + + expect(assets['assets/styles-Bv-4gk2N.css']).to.equal(css` + #a1 { + background-image: url('image-a-XOCPHCrV.png'); + } + + #a2 { + background-image: url('image-a.svg'); + } + + #d1 { + background-image: url('image-b-BgQHKcRn.png'); + } + + #d2 { + background-image: url('./image-b.svg'); + } + `); }); - it('allows to exclude external assets usign a glob pattern', async () => { + it('[legacy] allows to exclude external assets usign a glob pattern', async () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + 'image-b.png': 'image-b.png', + 'image-a.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + #a1 { + background-image: url('image-a.png'); + } + + #a2 { + background-image: url('image-a.svg'); + } + + #d1 { + background-image: url('./image-b.png'); + } + + #d2 { + background-image: url('./image-b.svg'); + } + `, + 'foo/x.css': css` + :root { + color: x; + } + `, + 'foo/bar/y.css': css` + :root { + color: y; + } + `, + 'webmanifest.json': { message: 'hello world' }, + }); + const config = { plugins: [ rollupPluginHTML({ + externalAssets: ['**/foo/**/*', '*.svg'], + rootDir, + extractAssets: 'legacy-html-and-css', input: { - html: ` - - - - - - - - - - - - -
- -
- -`, + html: html` + + + + + + + + + + + + + +
+ +
+ + + `, }, - bundleAssetsFromCss: true, - externalAssets: ['**/foo/**/*', '*.svg'], - rootDir: path.join(__dirname, 'fixtures', 'assets'), }), ], }; - const bundle = await rollup(config); - const { output } = await bundle.generate(outputConfig); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, outputConfig); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(7); + + expect(assets).to.have.keys([ + 'assets/assets/image-a-XOCPHCrV.png', + 'assets/assets/image-b-BgQHKcRn.png', + 'assets/image-a.png', + 'assets/image-b.png', + 'assets/styles-DFIb0lB5.css', + 'assets/webmanifest.json', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + + + +
+ +
+ + + `); + + expect(assets['assets/styles-DFIb0lB5.css']).to.equal(css` + #a1 { + background-image: url('assets/image-a-XOCPHCrV.png'); + } + + #a2 { + background-image: url('image-a.svg'); + } + + #d1 { + background-image: url('assets/image-b-BgQHKcRn.png'); + } + + #d2 { + background-image: url('./image-b.svg'); + } + `); + }); - expect(output.length).to.equal(8); + it('rewrites paths according to assetFileNames', async () => { + const rootDir = createApp({ + 'node_modules/ing-web/fonts/font.woff2': 'font.woff', + 'node_modules/ing-web/global.css': css` + @font-face { + font-family: Font; + src: url('fonts/font.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `, + 'assets/images/image.png': 'image.png', + 'assets/styles.css': css` + #a { + background-image: url('images/image.png'); + } + `, + 'src/main.js': js` + const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; + `, + }); - const expectedAssets = [ - 'image-a.png', - 'image-d.png', - 'styles-with-referenced-assets.css', - 'image-a.png', - 'image-d.png', - 'webmanifest.json', - ]; + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + input: { + html: html` + + + + + + + + + + + `, + }, + }), + ], + }; - for (const name of expectedAssets) { - const asset = getAsset(output, name); - expect(asset).to.exist; - expect(asset.source).to.exist; - } + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: 'static/[name].immutable.[hash][extname]', + }); - const outputHtml = getAsset(output, 'index.html').source; - expect(outputHtml).to.include( - '', - ); - expect(outputHtml).to.include( - '', - ); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include( - '', - ); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include(''); - expect(outputHtml).to.include(''); - - const rewrittenCss = getAsset(output, 'styles-with-referenced-assets.css') - .source.toString() - .trim(); - expect(rewrittenCss).to.equal( - `#a1 { - background-image: url("assets/image-a-yvktvaNB.png"); -} + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(5); + + expect(assets).to.have.keys([ + 'static/font.immutable.C5MNjX-h.woff2', + 'static/global.immutable.DB0fKkjs.css', + 'static/image.immutable.7xJLr_7N.png', + 'static/styles.immutable.D4tZXVv0.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + `); + + expect(assets['static/global.immutable.DB0fKkjs.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('font.immutable.C5MNjX-h.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['static/styles.immutable.D4tZXVv0.css']).to.equal(css` + #a { + background-image: url('image.immutable.7xJLr_7N.png'); + } + `); + }); -#a2 { - background-image: url("image-a.svg"); -} + it('resolves paths by using publicPath when assetFileNames puts assets in different dirs', async () => { + const rootDir = createApp({ + 'node_modules/ing-web/fonts/font.woff2': 'font.woff', + 'node_modules/ing-web/global.css': css` + @font-face { + font-family: Font; + src: url('fonts/font.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `, + 'assets/images/image.png': 'image.png', + 'assets/styles.css': css` + #a { + background-image: url('images/image.png'); + } + `, + 'src/main.js': js` + const imageUrl = new URL('../assets/images/image.png', import.meta.url).href; + `, + }); -#d1 { - background-image: url("assets/image-d-DLz8BAwO.png"); -} + const config = { + plugins: [ + rollupPluginHTML({ + rootDir, + publicPath: '/static/', + input: { + html: html` + + + + + + + + + + + `, + }, + }), + ], + }; -#d2 { - background-image: url("./image-d.svg"); -}`.trim(), - ); + const build = await rollup(config); + const { chunks, assets } = await generateTestBundle(build, { + ...outputConfig, + assetFileNames: assetInfo => { + const name = assetInfo.names[0] || ''; + if (name.endsWith('.woff2')) { + return 'fonts/[name].immutable.[hash][extname]'; + } else if (name.endsWith('.css')) { + return 'styles/[name].immutable.[hash][extname]'; + } else if (name.endsWith('.png')) { + return 'images/[name].immutable.[hash][extname]'; + } + return '[name].immutable.[hash][extname]'; + }, + }); + + expect(Object.keys(chunks)).to.have.lengthOf(1); + expect(Object.keys(assets)).to.have.lengthOf(5); + + expect(assets).to.have.keys([ + 'fonts/font.immutable.C5MNjX-h.woff2', + 'styles/global.immutable.B3Q0ucg4.css', + 'images/image.immutable.7xJLr_7N.png', + 'styles/styles.immutable.C3Z0Fs2-.css', + 'index.html', + ]); + + expect(assets['index.html']).to.equal(html` + + + + + + + + + + + `); + + expect(assets['styles/global.immutable.B3Q0ucg4.css']).to.equal(css` + @font-face { + font-family: Font; + src: url('/static/fonts/font.immutable.C5MNjX-h.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + } + `); + + expect(assets['styles/styles.immutable.C3Z0Fs2-.css']).to.equal(css` + #a { + background-image: url('/static/images/image.immutable.7xJLr_7N.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 deleted file mode 100644 index aaf229d0b..000000000 --- a/packages/rollup-plugin-html/test/src/input/InputData.test.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { expect } from 'chai'; -import path from 'path'; - -import { getInputData } from '../../../src/input/getInputData.js'; -import { InputData } from '../../../src/input/InputData.js'; - -const rootDir = path.join(__dirname, '..', '..', 'fixtures', 'basic'); - -function cleanupHtml(str: string) { - return str.replace(/(\r\n|\n|\r| )/gm, ''); -} - -function cleanupResult(result: InputData[]) { - return result.map(item => ({ - ...item, - inlineModules: Array.from(item.inlineModules.entries()), - html: cleanupHtml(item.html), - })); -} - -describe('getInputData()', () => { - it('supports setting input as string', () => { - const result = getInputData({ input: 'index.html', rootDir }); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'index.html'), - html: '

Helloworld

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: 'index.html', - }, - ]); - }); - - it('supports setting input as object', () => { - const result = getInputData({ input: { path: 'index.html' }, rootDir }); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'index.html'), - html: '

Helloworld

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: 'index.html', - }, - ]); - }); - - it('supports changing file name', () => { - const result = getInputData({ input: { path: 'index.html', name: 'foo.html' }, rootDir }); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'index.html'), - html: '

Helloworld

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: 'foo.html', - }, - ]); - }); - - it('supports setting multiple inputs', () => { - const result = getInputData({ - input: [{ path: 'index.html' }, { path: 'not-index.html' }], - rootDir, - }); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'index.html'), - html: '

Helloworld

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: 'index.html', - }, - { - filePath: path.join(rootDir, 'not-index.html'), - html: '

not-index.html

', - inlineModules: [], - moduleImports: [], - assets: [], - name: 'not-index.html', - }, - ]); - }); - - it('resolves modules relative to HTML file', () => { - const result = getInputData({ input: 'src/index.html', rootDir }); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'src/index.html'), - html: '

Foo

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'src', 'foo.js'), attributes: [] }], - assets: [], - name: 'index.html', - }, - ]); - }); - - it('supports setting input as rollup input string', () => { - const result = getInputData({ rootDir }, 'index.html'); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'index.html'), - html: '

Helloworld

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: 'index.html', - }, - ]); - }); - - it('supports setting input as rollup input array', () => { - const result = getInputData({ rootDir }, ['index.html']); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'index.html'), - html: '

Helloworld

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: 'index.html', - }, - ]); - }); - - it('supports setting input as rollup input array', () => { - const result = getInputData({ rootDir }, ['index.html', 'not-index.html']); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'index.html'), - html: '

Helloworld

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: 'index.html', - }, - { - filePath: path.join(rootDir, 'not-index.html'), - html: '

not-index.html

', - inlineModules: [], - moduleImports: [], - assets: [], - name: 'not-index.html', - }, - ]); - }); - - it('supports setting input as rollup input object', () => { - const result = getInputData( - { rootDir }, - { 'a.html': 'index.html', 'b.html': 'not-index.html' }, - ); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'index.html'), - html: '

Helloworld

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: 'a.html', - }, - { - filePath: path.join(rootDir, 'not-index.html'), - html: '

not-index.html

', - inlineModules: [], - moduleImports: [], - assets: [], - name: 'b.html', - }, - ]); - }); - - it('plugin input takes presedence over rollup input', () => { - const result = getInputData({ input: 'index.html', rootDir }, 'not-index.html'); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'index.html'), - html: '

Helloworld

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: 'index.html', - }, - ]); - }); - - it('can set html string as input', () => { - const html = ` - - -

HTML as string

- - - - `; - const result = getInputData({ input: { html }, rootDir }); - expect(cleanupResult(result)).to.eql([ - { - filePath: undefined, - html: '

HTMLasstring

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: 'index.html', - }, - ]); - }); - - it('can set multiple html strings as input', () => { - const html1 = ` - - -

HTML1

- - - - `; - const html2 = ` - - -

HTML2

- - - `; - const result = getInputData({ - input: [ - { html: html1, name: '1.html' }, - { html: html2, name: '2.html' }, - ], - rootDir, - }); - expect(cleanupResult(result)).to.eql([ - { - filePath: undefined, - html: '

HTML1

', - inlineModules: [], - moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], - assets: [], - name: '1.html', - }, - { - filePath: undefined, - html: '

HTML2

', - inlineModules: [], - moduleImports: [], - assets: [], - name: '2.html', - }, - ]); - }); - - it('supports setting input to a glob', () => { - const result = getInputData({ input: 'pages/**/*.html', rootDir }); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'pages', 'page-c.html'), - html: '

page-c.html

', - inlineModules: [], - moduleImports: [ - { importPath: path.join(rootDir, 'pages', 'page-c.js'), attributes: [] }, - { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, - ], - assets: [], - name: 'page-c.html', - }, - { - filePath: path.join(rootDir, 'pages', 'page-b.html'), - html: '

page-b.html

', - inlineModules: [], - moduleImports: [ - { importPath: path.join(rootDir, 'pages', 'page-b.js'), attributes: [] }, - { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, - ], - assets: [], - name: 'page-b.html', - }, - { - filePath: path.join(rootDir, 'pages', 'page-a.html'), - html: '

page-a.html

', - inlineModules: [], - moduleImports: [ - { importPath: path.join(rootDir, 'pages', 'page-a.js'), attributes: [] }, - { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, - ], - assets: [], - name: 'page-a.html', - }, - ]); - }); - - it('supports not flattening output directories', () => { - const result = getInputData({ input: 'pages/**/*.html', flattenOutput: false, rootDir }); - expect(cleanupResult(result)).to.eql([ - { - filePath: path.join(rootDir, 'pages', 'page-c.html'), - html: '

page-c.html

', - inlineModules: [], - moduleImports: [ - { importPath: path.join(rootDir, 'pages', 'page-c.js'), attributes: [] }, - { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, - ], - assets: [], - name: `pages${path.sep}page-c.html`, - }, - { - filePath: path.join(rootDir, 'pages', 'page-b.html'), - html: '

page-b.html

', - inlineModules: [], - moduleImports: [ - { importPath: path.join(rootDir, 'pages', 'page-b.js'), attributes: [] }, - { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, - ], - assets: [], - name: `pages${path.sep}page-b.html`, - }, - { - filePath: path.join(rootDir, 'pages', 'page-a.html'), - html: '

page-a.html

', - inlineModules: [], - moduleImports: [ - { importPath: path.join(rootDir, 'pages', 'page-a.js'), attributes: [] }, - { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, - ], - assets: [], - name: `pages${path.sep}page-a.html`, - }, - ]); - }); - - it('supports pure HTML files', () => { - const html = ` - - -

pure HTML

- - - `; - const result = getInputData({ input: { html }, rootDir }); - expect(cleanupResult(result)).to.eql([ - { - filePath: undefined, - html: '

pureHTML

', - inlineModules: [], - moduleImports: [], - assets: [], - name: 'index.html', - }, - ]); - }); - - it('throws when no files or html is given', () => { - expect(() => getInputData({ rootDir })).to.throw(); - }); -}); 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 deleted file mode 100644 index f2d4cdcd9..000000000 --- a/packages/rollup-plugin-html/test/src/input/extract/extractAssets.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { expect } from 'chai'; -import { parse } from 'parse5'; -import path from 'path'; -import { extractAssets } from '../../../../src/input/extract/extractAssets.js'; - -const rootDir = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'assets'); - -describe('extractAssets', () => { - it('extracts assets from a document', () => { - const document = parse(` - - - - - - - - - - - -
- -
- - - `); - const assets = extractAssets({ - document, - htmlFilePath: path.join(rootDir, 'index.html'), - htmlDir: rootDir, - rootDir, - }); - - const assetsWithoutcontent = assets.map(a => ({ ...a, content: undefined })); - expect(assetsWithoutcontent).to.eql([ - { - content: undefined, - filePath: path.join(rootDir, 'image-a.png'), - hashed: false, - }, - { - content: undefined, - filePath: path.join(rootDir, 'image-b.png'), - hashed: false, - }, - { - content: undefined, - filePath: path.join(rootDir, 'webmanifest.json'), - hashed: false, - }, - { - content: undefined, - filePath: path.join(rootDir, 'image-a.svg'), - hashed: false, - }, - { - content: undefined, - filePath: path.join(rootDir, 'styles.css'), - hashed: true, - }, - { - content: undefined, - filePath: path.join(rootDir, 'image-social.png'), - hashed: true, - }, - { - content: undefined, - filePath: path.join(rootDir, 'image-c.png'), - hashed: true, - }, - { - content: undefined, - filePath: path.join(rootDir, 'image-b.svg'), - hashed: true, - }, - ]); - }); - - it('reads file sources', () => { - const document = parse(` - - - - - - - - -
- -
- - - `); - const assets = extractAssets({ - document, - htmlFilePath: path.join(rootDir, 'index.html'), - htmlDir: rootDir, - rootDir, - }); - - const transformedAssets = assets.map(asset => ({ - ...asset, - content: asset.content.toString('utf-8').replace(/\s/g, ''), - })); - expect(transformedAssets).to.eql([ - { - content: '{"message":"helloworld"}', - filePath: path.join(rootDir, 'webmanifest.json'), - hashed: false, - }, - { - content: - '', - filePath: path.join(rootDir, 'image-a.svg'), - hashed: false, - }, - { - content: ':root{color:blue;}', - filePath: path.join(rootDir, 'styles.css'), - hashed: true, - }, - { - content: - '', - filePath: path.join(rootDir, 'image-b.svg'), - hashed: true, - }, - ]); - }); - - it('handles paths into directories', () => { - const document = parse(` - - - - - - - `); - const assets = extractAssets({ - document, - htmlFilePath: path.join(rootDir, 'index.html'), - htmlDir: rootDir, - rootDir, - }); - - expect(assets.length).to.equal(2); - expect(assets[0].filePath).to.equal(path.join(rootDir, 'foo', 'x.css')); - expect(assets[1].filePath).to.equal(path.join(rootDir, 'foo', 'bar', 'y.css')); - expect(assets[0].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:x;}'); - expect(assets[1].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:y;}'); - }); - - it('resolves relative to HTML file location', () => { - const document = parse(` - - - - - - - `); - const assets = extractAssets({ - document, - htmlFilePath: path.join(rootDir, 'foo', 'index.html'), - htmlDir: path.join(rootDir, 'foo'), - rootDir, - }); - - expect(assets.length).to.equal(2); - expect(assets[0].filePath).to.equal(path.join(rootDir, 'foo', 'x.css')); - expect(assets[1].filePath).to.equal(path.join(rootDir, 'styles.css')); - expect(assets[0].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:x;}'); - expect(assets[1].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:blue;}'); - }); - - it('resolves absolute paths relative to root dir', () => { - const document = parse(` - - - - - - - `); - const assets = extractAssets({ - document, - htmlFilePath: path.join(rootDir, 'foo', 'index.html'), - htmlDir: path.join(rootDir, 'foo'), - rootDir, - }); - - expect(assets.length).to.equal(2); - expect(assets[0].filePath).to.equal(path.join(rootDir, 'foo', 'x.css')); - expect(assets[1].filePath).to.equal(path.join(rootDir, 'styles.css')); - expect(assets[0].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:x;}'); - expect(assets[1].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:blue;}'); - }); - - it('can reference the same asset with a hashed and non-hashed node', () => { - const document = parse(` - - - - - - - `); - const assets = extractAssets({ - document, - htmlFilePath: path.join(rootDir, 'index.html'), - htmlDir: rootDir, - rootDir, - }); - - expect(assets.length).to.equal(2); - const assetsWithoutcontent = assets.map(a => ({ ...a, content: undefined })); - expect(assetsWithoutcontent).to.eql([ - { - content: undefined, - filePath: path.join(rootDir, 'image-a.png'), - hashed: true, - }, - { - content: undefined, - filePath: path.join(rootDir, 'image-a.png'), - hashed: false, - }, - ]); - }); - - it('does not count remote URLs as assets', () => { - const document = parse(` - - - - - - `); - const assets = extractAssets({ - document, - htmlFilePath: path.join(rootDir, 'foo', 'index.html'), - htmlDir: path.join(rootDir, 'foo'), - rootDir, - }); - - expect(assets.length).to.equal(0); - }); - - it('does treat non module script tags as assets', () => { - const document = parse(` - - - - - - `); - const assets = extractAssets({ - document, - htmlFilePath: path.join(rootDir, 'index.html'), - htmlDir: path.join(rootDir), - rootDir, - }); - - expect(assets.length).to.equal(1); - expect(assets[0].filePath).to.equal(path.join(rootDir, 'no-module.js')); - expect(assets[0].content.toString('utf-8')).to.equal('/* no module script file */\n'); - }); - - it('handles a picture tag using source tags with srcset', () => { - const document = parse(` - - - - - - My Image Alternative Text - - - - `); - const assets = extractAssets({ - document, - htmlFilePath: path.join(rootDir, 'index.html'), - htmlDir: path.join(rootDir), - rootDir, - }); - - // the src is not the same as the small jpeg image - expect(assets.length).to.equal(4); - expect(assets[0].filePath).to.equal(path.join(rootDir, 'images', 'eb26e6ca-30.avif')); - expect(assets[1].filePath).to.equal(path.join(rootDir, 'images', 'eb26e6ca-60.avif')); - expect(assets[2].filePath).to.equal(path.join(rootDir, 'images', 'eb26e6ca-30.jpeg')); - expect(assets[3].filePath).to.equal(path.join(rootDir, 'images', 'eb26e6ca-60.jpeg')); - }); -}); 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 deleted file mode 100644 index 11fd545b5..000000000 --- a/packages/rollup-plugin-html/test/src/input/extract/extractModules.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import path from 'path'; -import { parse, serialize } from 'parse5'; -import { expect } from 'chai'; - -import { extractModules } from '../../../../src/input/extract/extractModules.js'; - -const { sep } = path; - -describe('extractModules()', () => { - it('extracts all modules from a html document', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); - - const { moduleImports, inlineModules } = extractModules({ - document, - htmlDir: '/', - rootDir: '/', - }); - const htmlWithoutModules = serialize(document); - - expect(inlineModules.length).to.equal(0); - expect(moduleImports).to.eql([ - { importPath: `${sep}foo.js`, attributes: [] }, - { importPath: `${sep}bar.js`, attributes: [] }, - ]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); - }); - - it('does not touch non module scripts', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); - - const { moduleImports, inlineModules } = extractModules({ - document, - htmlDir: '/', - rootDir: '/', - }); - const htmlWithoutModules = serialize(document); - - expect(inlineModules.length).to.equal(0); - expect(moduleImports).to.eql([]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); - }); - - it('resolves imports relative to the root dir', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); - - const { moduleImports, inlineModules } = extractModules({ - document, - htmlDir: '/', - rootDir: '/base/', - }); - const htmlWithoutModules = serialize(document); - - expect(inlineModules.length).to.equal(0); - expect(moduleImports).to.eql([ - { importPath: `${sep}foo.js`, attributes: [] }, - { importPath: `${sep}base${sep}bar.js`, attributes: [] }, - ]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); - }); - - it('resolves relative imports relative to the relative import base', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); - - const { moduleImports, inlineModules } = extractModules({ - document, - htmlDir: '/base-1/base-2/', - rootDir: '/base-1/', - }); - const htmlWithoutModules = serialize(document); - - expect(inlineModules.length).to.equal(0); - expect(moduleImports).to.eql([ - { importPath: `${sep}base-1${sep}base-2${sep}foo.js`, attributes: [] }, - { importPath: `${sep}base-1${sep}bar.js`, attributes: [] }, - ]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); - }); - - it('extracts all inline modules from a html document', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); - - const { moduleImports, inlineModules } = extractModules({ - document, - htmlDir: '/', - rootDir: '/', - }); - const htmlWithoutModules = serialize(document); - - expect(inlineModules).to.eql([ - { - importPath: '/inline-module-cce79ce714e2c3b250afef32e61fb003.js', - code: '/* my module 1 */', - attributes: [], - }, - { - importPath: '/inline-module-d9a0918508784903d131c7c4eb98e424.js', - code: '/* my module 2 */', - attributes: [], - }, - ]); - expect(moduleImports).to.eql([]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); - }); - - it('prefixes inline module with index.html directory', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); - - const { moduleImports, inlineModules } = extractModules({ - document, - htmlDir: '/foo/bar/', - rootDir: '/', - }); - const htmlWithoutModules = serialize(document); - - expect(inlineModules).to.eql([ - { - importPath: '/foo/bar/inline-module-cce79ce714e2c3b250afef32e61fb003.js', - code: '/* my module 1 */', - attributes: [], - }, - { - importPath: '/foo/bar/inline-module-d9a0918508784903d131c7c4eb98e424.js', - code: '/* my module 2 */', - attributes: [], - }, - ]); - expect(moduleImports).to.eql([]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); - }); - - it('ignores absolute paths', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); - - const { moduleImports, inlineModules } = extractModules({ - document, - htmlDir: '/', - rootDir: '/', - }); - const htmlWithoutModules = serialize(document); - - expect(inlineModules.length).to.equal(0); - expect(moduleImports).to.eql([{ importPath: `${sep}bar.js`, attributes: [] }]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); - }); -}); diff --git a/packages/rollup-plugin-html/test/src/output/getEntrypointBundles.test.ts b/packages/rollup-plugin-html/test/src/output/getEntrypointBundles.test.ts deleted file mode 100644 index e7cf8d647..000000000 --- a/packages/rollup-plugin-html/test/src/output/getEntrypointBundles.test.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { expect } from 'chai'; -import { - getEntrypointBundles, - createImportPath, -} from '../../../src/output/getEntrypointBundles.js'; -import { GeneratedBundle, ScriptModuleTag } from '../../../src/RollupPluginHTMLOptions.js'; - -describe('createImportPath()', () => { - it('creates a relative import path', () => { - expect( - createImportPath({ - outputDir: 'dist', - fileOutputDir: 'dist', - htmlFileName: 'index.html', - fileName: 'foo.js', - }), - ).to.equal('./foo.js'); - }); - - it('handles files output in a different directory', () => { - expect( - createImportPath({ - outputDir: 'dist', - fileOutputDir: 'dist/legacy', - htmlFileName: 'index.html', - fileName: 'foo.js', - }), - ).to.equal('./legacy/foo.js'); - }); - - it('handles directory in filename', () => { - expect( - createImportPath({ - outputDir: 'dist', - fileOutputDir: 'dist', - htmlFileName: 'index.html', - fileName: 'legacy/foo.js', - }), - ).to.equal('./legacy/foo.js'); - }); - - it('allows configuring a public path', () => { - expect( - createImportPath({ - publicPath: 'static', - outputDir: 'dist', - fileOutputDir: 'dist', - htmlFileName: 'index.html', - fileName: 'foo.js', - }), - ).to.equal('./static/foo.js'); - }); - - it('allows configuring an absolute public path', () => { - expect( - createImportPath({ - publicPath: '/static', - outputDir: 'dist', - fileOutputDir: 'dist', - htmlFileName: 'index.html', - fileName: 'foo.js', - }), - ).to.equal('/static/foo.js'); - }); - - it('allows configuring an absolute public path with just a /', () => { - expect( - createImportPath({ - publicPath: '/', - outputDir: 'dist', - fileOutputDir: 'dist', - htmlFileName: 'index.html', - fileName: 'foo.js', - }), - ).to.equal('/foo.js'); - }); - - it('allows configuring an absolute public path with a trailing /', () => { - expect( - createImportPath({ - publicPath: '/static/public/', - outputDir: 'dist', - fileOutputDir: 'dist', - htmlFileName: 'index.html', - fileName: 'foo.js', - }), - ).to.equal('/static/public/foo.js'); - }); - - it('respects a different output dir when configuring a public path', () => { - expect( - createImportPath({ - publicPath: '/static', - outputDir: 'dist', - fileOutputDir: 'dist/legacy', - htmlFileName: 'index.html', - fileName: 'foo.js', - }), - ).to.equal('/static/legacy/foo.js'); - }); - - it('when html is output in a directory, creates a relative path from the html file to the js file', () => { - expect( - createImportPath({ - outputDir: 'dist', - fileOutputDir: 'dist', - htmlFileName: 'pages/index.html', - fileName: 'foo.js', - }), - ).to.equal('../foo.js'); - }); - - it('when html is output in a directory and absolute path is set, creates a direct path from the root to the js file', () => { - expect( - createImportPath({ - publicPath: '/static/', - outputDir: 'dist', - fileOutputDir: 'dist', - htmlFileName: 'pages/index.html', - fileName: 'foo.js', - }), - ).to.equal('/static/foo.js'); - }); -}); - -describe('getEntrypointBundles()', () => { - const defaultBundles: GeneratedBundle[] = [ - { - name: 'default', - options: { format: 'es', dir: 'dist' }, - bundle: { - // @ts-ignore - 'app.js': { - isEntry: true, - fileName: 'app.js', - facadeModuleId: '/root/app.js', - type: 'chunk', - }, - }, - }, - ]; - - const inputModuleIds: ScriptModuleTag[] = [ - { importPath: '/root/app.js' }, - { importPath: '/root/foo.js' }, - ]; - - const defaultOptions = { - pluginOptions: {}, - inputModuleIds, - outputDir: 'dist', - htmlFileName: 'index.html', - generatedBundles: defaultBundles, - }; - - it('generates entrypoints for a simple project', async () => { - const output = await getEntrypointBundles(defaultOptions); - expect(Object.keys(output).length).to.equal(1); - expect(output.default.options).to.equal(defaultBundles[0].options); - expect(output.default.bundle).to.equal(defaultBundles[0].bundle); - expect(output.default.entrypoints.length).to.equal(1); - expect(output.default.entrypoints[0].chunk).to.equal(defaultBundles[0].bundle['app.js']); - expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['./app.js']); - }); - - it('does not output non-entrypoints', async () => { - const generatedBundles: GeneratedBundle[] = [ - { - name: 'default', - options: { format: 'es', dir: 'dist' }, - bundle: { - // @ts-ignore - 'app.js': { - isEntry: true, - fileName: 'app.js', - facadeModuleId: '/root/app.js', - type: 'chunk', - }, - // @ts-ignore - 'not-app.js': { - isEntry: false, - fileName: 'not-app.js', - facadeModuleId: '/root/app.js', - type: 'chunk', - }, - }, - }, - ]; - const output = await getEntrypointBundles({ - ...defaultOptions, - generatedBundles, - }); - expect(Object.keys(output).length).to.equal(1); - expect(output.default.entrypoints.length).to.equal(1); - expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['./app.js']); - }); - - it('does not output non-chunks', async () => { - const generatedBundles: GeneratedBundle[] = [ - { - name: 'default', - options: { format: 'es', dir: 'dist' }, - bundle: { - // @ts-ignore - 'app.js': { - isEntry: true, - fileName: 'app.js', - facadeModuleId: '/root/app.js', - type: 'chunk', - }, - // @ts-ignore - 'not-app.js': { - // @ts-ignore - isEntry: true, - fileName: 'not-app.js', - facadeModuleId: '/root/app.js', - type: 'asset', - }, - }, - }, - ]; - const output = await getEntrypointBundles({ - ...defaultOptions, - generatedBundles, - }); - expect(Object.keys(output).length).to.equal(1); - expect(output.default.entrypoints.length).to.equal(1); - expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['./app.js']); - }); - - it('matches on facadeModuleId', async () => { - const generatedBundles: GeneratedBundle[] = [ - { - name: 'default', - options: { format: 'es', dir: 'dist' }, - bundle: { - // @ts-ignore - 'app.js': { - isEntry: true, - fileName: 'app.js', - facadeModuleId: '/root/app.js', - type: 'chunk', - }, - // @ts-ignore - 'not-app.js': { - isEntry: true, - fileName: 'not-app.js', - facadeModuleId: '/root/not-app.js', - type: 'chunk', - }, - }, - }, - ]; - const output = await getEntrypointBundles({ - ...defaultOptions, - generatedBundles, - }); - expect(Object.keys(output).length).to.equal(1); - expect(output.default.entrypoints.length).to.equal(1); - expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['./app.js']); - }); - - it('returns all entrypoints when no input module ids are given', async () => { - const generatedBundles: GeneratedBundle[] = [ - { - name: 'default', - options: { format: 'es', dir: 'dist' }, - bundle: { - // @ts-ignore - 'app.js': { - isEntry: true, - fileName: 'app.js', - facadeModuleId: '/root/app.js', - type: 'chunk', - }, - // @ts-ignore - 'not-app.js': { - isEntry: true, - fileName: 'not-app.js', - facadeModuleId: '/root/not-app.js', - type: 'chunk', - }, - }, - }, - ]; - - const inputModuleIds: ScriptModuleTag[] = [ - { importPath: '/root/app.js' }, - { importPath: '/root/not-app.js' }, - ]; - - const output = await getEntrypointBundles({ - ...defaultOptions, - inputModuleIds, - generatedBundles, - }); - expect(Object.keys(output).length).to.equal(1); - expect(output.default.entrypoints.length).to.equal(2); - expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['./app.js', './not-app.js']); - }); - - it('generates entrypoint for multiple bundles', async () => { - const generatedBundles: GeneratedBundle[] = [ - { - name: 'modern', - options: { format: 'es', dir: 'dist' }, - bundle: { - // @ts-ignore - 'app.js': { - isEntry: true, - fileName: 'app.js', - facadeModuleId: '/root/app.js', - type: 'chunk', - }, - }, - }, - { - name: 'legacy', - options: { format: 'es', dir: 'dist/legacy' }, - bundle: { - // @ts-ignore - 'app.js': { - isEntry: true, - fileName: 'app.js', - facadeModuleId: '/root/app.js', - type: 'chunk', - }, - }, - }, - ]; - - const output = await getEntrypointBundles({ - ...defaultOptions, - generatedBundles, - }); - - expect(Object.keys(output).length).to.equal(2); - expect(output.modern.options).to.equal(generatedBundles[0].options); - expect(output.legacy.options).to.equal(generatedBundles[1].options); - expect(output.modern.bundle).to.equal(generatedBundles[0].bundle); - expect(output.legacy.bundle).to.equal(generatedBundles[1].bundle); - expect(output.modern.entrypoints.length).to.equal(1); - expect(output.modern.entrypoints[0].chunk).to.equal(generatedBundles[0].bundle['app.js']); - expect(output.modern.entrypoints.map(e => e.importPath)).to.eql(['./app.js']); - expect(output.legacy.entrypoints.length).to.equal(1); - expect(output.legacy.entrypoints[0].chunk).to.equal(generatedBundles[1].bundle['app.js']); - expect(output.legacy.entrypoints.map(e => e.importPath)).to.eql(['./legacy/app.js']); - }); - - it('allows configuring a public path', async () => { - const output = await getEntrypointBundles({ - ...defaultOptions, - pluginOptions: { publicPath: '/static' }, - }); - - expect(Object.keys(output).length).to.equal(1); - expect(output.default.entrypoints.length).to.equal(1); - expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['/static/app.js']); - }); -}); diff --git a/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts b/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts deleted file mode 100644 index 11cf7a7ed..000000000 --- a/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { expect } from 'chai'; -import path from 'path'; -import { getOutputHTML, GetOutputHTMLParams } from '../../../src/output/getOutputHTML.js'; -import { EntrypointBundle } from '../../../src/RollupPluginHTMLOptions.js'; - -describe('getOutputHTML()', () => { - const defaultEntrypointBundles: Record = { - default: { - name: 'default', - options: { format: 'es' }, - // @ts-ignore - entrypoints: [{ importPath: '/app.js' }, { importPath: '/module.js' }], - }, - }; - - const defaultOptions: GetOutputHTMLParams = { - pluginOptions: {}, - outputDir: '/', - emittedAssets: { static: new Map(), hashed: new Map() }, - entrypointBundles: defaultEntrypointBundles, - input: { - html: '

Input HTML

', - name: 'index.html', - moduleImports: [], - assets: [], - inlineModules: [], - }, - defaultInjectDisabled: false, - injectServiceWorker: false, - serviceWorkerPath: '', - strictCSPInlineScripts: false, - }; - - it('injects output into the input HTML', async () => { - const output = await getOutputHTML(defaultOptions); - expect(output).to.equal( - '

Input HTML

' + - '' + - '' + - '', - ); - }); - - it('generates a HTML file for multiple rollup bundles', async () => { - const entrypointBundles: Record = { - modern: { - name: 'modern', - options: { format: 'es' }, - // @ts-ignore - entrypoints: [{ importPath: '/app.js' }, { importPath: '/module.js' }], - }, - legacy: { - name: 'legacy', - options: { format: 'system' }, - // @ts-ignore - entrypoints: [{ importPath: '/legacy/app.js' }, { importPath: '/legacy/module.js' }], - }, - }; - - const output = await getOutputHTML({ ...defaultOptions, entrypointBundles }); - expect(output).to.equal( - '

Input HTML

' + - '' + - '' + - '' + - '' + - '', - ); - }); - - it('can transform html output', async () => { - const output = await getOutputHTML({ - ...defaultOptions, - pluginOptions: { - ...defaultOptions.pluginOptions, - transformHtml: html => html.replace('Input HTML', 'Transformed Input HTML'), - }, - }); - - expect(output).to.equal( - '

Transformed Input HTML

' + - '' + - '' + - '', - ); - }); - - it('allows setting multiple html transform functions', async () => { - const output = await getOutputHTML({ - ...defaultOptions, - pluginOptions: { - ...defaultOptions.pluginOptions, - transformHtml: [ - html => html.replace('Input HTML', 'Transformed Input HTML'), - html => html.replace(/h1/g, 'h2'), - ], - }, - }); - - expect(output).to.equal( - '

Transformed Input HTML

' + - '' + - '' + - '', - ); - }); - - it('can combine external and regular transform functions', async () => { - const output = await getOutputHTML({ - ...defaultOptions, - pluginOptions: { - ...defaultOptions.pluginOptions, - transformHtml: html => html.replace('Input HTML', 'Transformed Input HTML'), - }, - externalTransformHtmlFns: [html => html.replace(/h1/g, 'h2')], - }); - - expect(output).to.equal( - '

Transformed Input HTML

' + - '' + - '' + - '', - ); - }); - - it('can disable default injection', async () => { - const output = await getOutputHTML({ - ...defaultOptions, - defaultInjectDisabled: true, - }); - - expect(output).to.equal('

Input HTML

'); - }); - - it('can converts absolute urls to full absolute urls', async () => { - const rootDir = path.resolve(__dirname, '..', '..', 'fixtures', 'assets'); - const hashed = new Map(); - hashed.set(path.join(rootDir, 'image-social.png'), 'image-social-xxx.png'); - - const output = await getOutputHTML({ - ...defaultOptions, - defaultInjectDisabled: true, - pluginOptions: { - absoluteBaseUrl: 'http://test.com', - rootDir, - }, - emittedAssets: { static: new Map(), hashed }, - input: { - ...defaultOptions.input, - html: [ - '', - '', - '', - '', - '', - '', - '', - ].join('\n'), - filePath: path.join(rootDir, 'index.html'), - }, - }); - - expect(output).to.equal( - [ - '', - '', - '', - '', - '', - '', - '', - ].join('\n'), - ); - }); - - it('can minify HTML', async () => { - const htmlInput = ` - - - - - - - - - - `; - const output = await getOutputHTML({ - ...defaultOptions, - pluginOptions: { - ...defaultOptions.pluginOptions, - minify: true, - }, - input: { - ...defaultOptions.input, - html: htmlInput, - }, - }); - - expect(output).to.equal( - '', - ); - }); -}); diff --git a/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts b/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts deleted file mode 100644 index 5068d8dc3..000000000 --- a/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { getTextContent } from '@web/parse5-utils'; -import { expect } from 'chai'; -import { parse, serialize } from 'parse5'; - -import { injectBundles, createLoadScript } from '../../../src/output/injectBundles.js'; - -describe('createLoadScript()', () => { - it('creates a script for es modules', () => { - // parse5 types are broken - const scriptAst = createLoadScript('./app.js', 'es') as any; - - expect(scriptAst.tagName).to.equal('script'); - expect(scriptAst.attrs).to.eql([ - { name: 'type', value: 'module' }, - { name: 'src', value: './app.js' }, - ]); - }); - - it('creates a script for systemjs', () => { - // parse5 types are broken - const scriptAst = createLoadScript('./app.js', 'system') as any; - - expect(scriptAst.tagName).to.equal('script'); - expect(getTextContent(scriptAst)).to.equal('System.import("./app.js");'); - }); - - it('creates a script for other modules types', () => { - const scriptAst = createLoadScript('./app.js', 'iife') as any; - - expect(scriptAst.tagName).to.equal('script'); - expect(scriptAst.attrs).to.eql([ - { name: 'src', value: './app.js' }, - { name: 'defer', value: '' }, - ]); - }); -}); - -describe('injectBundles()', () => { - it('can inject a single bundle', () => { - const document = parse( - [ - // - '', - '', - '', - '

Hello world

', - '', - '', - ].join(''), - ); - - injectBundles(document, [ - { - options: { format: 'es' }, - entrypoints: [ - { - importPath: 'app.js', - // @ts-ignore - chunk: {}, - }, - ], - }, - ]); - const expected = [ - // - '', - '', - '', - '

Hello world

', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); - }); - - it('can inject multiple bundles', () => { - const document = parse( - [ - // - '', - '', - '', - '

Hello world

', - '', - '', - ].join(''), - ); - - injectBundles(document, [ - // @ts-ignore - { - options: { format: 'es' }, - entrypoints: [ - { - importPath: './app.js', - // @ts-ignore - chunk: null, - }, - ], - }, - // @ts-ignore - { - options: { format: 'iife' }, - entrypoints: [ - { - importPath: '/scripts/script.js', - // @ts-ignore - chunk: null, - }, - ], - }, - ]); - const expected = [ - // - '', - '', - '', - '

Hello world

', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); - }); -}); diff --git a/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts b/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts deleted file mode 100644 index 85ae406fe..000000000 --- a/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { expect } from 'chai'; -import path from 'path'; -import { parse, serialize } from 'parse5'; -import { InputData } from '../../../src/input/InputData.js'; - -import { injectedUpdatedAssetPaths } from '../../../src/output/injectedUpdatedAssetPaths.js'; - -describe('injectedUpdatedAssetPaths()', () => { - it('injects updated asset paths', () => { - const document = parse( - [ - '', - '', - '', - '', - '', - '', - '', - '', - ].join(''), - ); - - const input: InputData = { - html: '', - name: 'index.html', - moduleImports: [], - inlineModules: [], - assets: [], - filePath: '/root/index.html', - }; - const hashed = new Map(); - hashed.set(path.join(path.sep, 'root', 'styles.css'), 'styles-xxx.css'); - hashed.set(path.join(path.sep, 'root', 'foo', 'image-a.png'), 'image-a-xxx.png'); - hashed.set(path.join(path.sep, 'root', 'image-b.png'), 'image-b-xxx.png'); - hashed.set(path.join(path.sep, 'root', 'no-module.js'), 'no-module-xxx.js'); - - injectedUpdatedAssetPaths({ - document, - input, - outputDir: '/root/dist/', - rootDir: '/root/', - emittedAssets: { static: new Map(), hashed }, - }); - - const expected = [ - '', - '', - '', - '', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); - }); - - it('handles a picture tag using source tags with srcset', () => { - const document = parse( - [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '', - ].join(''), - ); - - const input: InputData = { - html: '', - name: 'index.html', - moduleImports: [], - inlineModules: [], - assets: [], - filePath: '/root/index.html', - }; - const hashed = new Map(); - hashed.set(path.join(path.sep, 'root', 'images', 'eb26e6ca-30.avif'), 'eb26e6ca-30-xxx.avif'); - hashed.set(path.join(path.sep, 'root', 'images', 'eb26e6ca-60.avif'), 'eb26e6ca-60-xxx.avif'); - hashed.set(path.join(path.sep, 'root', 'images', 'eb26e6ca-30.jpeg'), 'eb26e6ca-30-xxx.jpeg'); - hashed.set(path.join(path.sep, 'root', 'images', 'eb26e6ca-60.jpeg'), 'eb26e6ca-60-xxx.jpeg'); - - injectedUpdatedAssetPaths({ - document, - input, - outputDir: '/root/dist/', - rootDir: '/root/', - emittedAssets: { static: new Map(), hashed }, - }); - - const expected = [ - '', - '', - ' ', - ' ', - ' My Image Alternative Text', - ' ', - ].join('\n'); - expect(serialize(document).replace(/ {4}/g, '\n')).to.eql(expected); - }); - - it('handles video tag using source tags with src', () => { - const document = parse( - [ - '', - ' ', - ' ', - ' ', - '', - ].join(''), - ); - - const input: InputData = { - html: '', - name: 'index.html', - moduleImports: [], - inlineModules: [], - assets: [], - filePath: '/root/index.html', - }; - const hashed = new Map(); - hashed.set( - path.join(path.sep, 'root', 'videos', 'typer-hydration.mp4'), - 'typer-hydration-xxx.mp4', - ); - - injectedUpdatedAssetPaths({ - document, - input, - outputDir: '/root/dist/', - rootDir: '/root/', - emittedAssets: { static: new Map(), hashed }, - }); - - const expected = [ - '', - ' ', - ].join('\n'); - expect(serialize(document).replace(/ {4}/g, '\n')).to.eql(expected); - }); - - it('handles virtual files', () => { - const document = parse( - [ - '', - '', - '', - '', - '', - '', - '', - ].join(''), - ); - - const input: InputData = { - html: '', - name: 'index.html', - moduleImports: [], - inlineModules: [], - assets: [], - }; - const hashed = new Map(); - hashed.set(path.join(path.sep, 'root', 'styles.css'), 'styles-xxx.css'); - hashed.set(path.join(path.sep, 'root', 'foo', 'image-a.png'), 'image-a-xxx.png'); - hashed.set(path.join(path.sep, 'root', 'image-b.png'), 'image-b-xxx.png'); - - injectedUpdatedAssetPaths({ - document, - input, - outputDir: '/root/dist/', - rootDir: '/root/', - emittedAssets: { static: new Map(), hashed }, - }); - const expected = [ - '', - '', - '', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); - }); - - it('handles HTML files in a sub directory', () => { - const document = parse( - [ - '', - '', - '', - '', - '', - '', - '', - ].join(''), - ); - - const input: InputData = { - html: '', - name: 'foo/index.html', - moduleImports: [], - inlineModules: [], - assets: [], - filePath: '/root/foo/index.html', - }; - const hashed = new Map(); - hashed.set(path.join(path.sep, 'root', 'styles.css'), 'styles-xxx.css'); - hashed.set(path.join(path.sep, 'root', 'foo', 'image-a.png'), 'image-a-xxx.png'); - hashed.set(path.join(path.sep, 'root', 'image-b.png'), 'image-b-xxx.png'); - - injectedUpdatedAssetPaths({ - document, - input, - outputDir: '/root/dist/', - rootDir: '/root/', - emittedAssets: { static: new Map(), hashed }, - }); - - const expected = [ - '', - '', - '', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); - }); - - it('handles virtual HTML files in a sub directory', () => { - const document = parse( - [ - '', - '', - '', - '', - '', - '', - '', - ].join(''), - ); - - const input: InputData = { - html: '', - name: 'foo/index.html', - moduleImports: [], - inlineModules: [], - assets: [], - }; - const hashed = new Map(); - hashed.set(path.join(path.sep, 'root', 'styles.css'), 'styles-xxx.css'); - hashed.set(path.join(path.sep, 'root', 'foo', 'image-a.png'), 'image-a-xxx.png'); - hashed.set(path.join(path.sep, 'root', 'image-b.png'), 'image-b-xxx.png'); - - injectedUpdatedAssetPaths({ - document, - input, - outputDir: '/root/dist/', - rootDir: '/root/', - emittedAssets: { static: new Map(), hashed }, - }); - - const expected = [ - '', - '', - '', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); - }); - - it('prefixes a publicpath', () => { - const document = parse( - [ - '', - '', - '', - '', - '', - '', - '', - ].join(''), - ); - - const input: InputData = { - html: '', - name: 'index.html', - moduleImports: [], - inlineModules: [], - assets: [], - filePath: '/root/index.html', - }; - const hashed = new Map(); - hashed.set(path.join(path.sep, 'root', 'styles.css'), 'styles-xxx.css'); - hashed.set(path.join(path.sep, 'root', 'foo', 'image-a.png'), 'image-a-xxx.png'); - hashed.set(path.join(path.sep, 'root', 'image-b.png'), 'image-b-xxx.png'); - - injectedUpdatedAssetPaths({ - document, - input, - outputDir: '/root/dist/', - rootDir: '/root/', - emittedAssets: { static: new Map(), hashed }, - publicPath: './public/', - }); - - const expected = [ - '', - '', - '', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); - }); -}); From 18e85e7e488d71ffc4ce9972aec22707ddaf3573 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Mon, 22 Dec 2025 18:57:56 +0400 Subject: [PATCH 11/21] WIP11 --- .../rollup-plugin-html/test/rollup-plugin-html.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 bec535513..3ec4b8c78 100644 --- a/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts +++ b/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts @@ -569,10 +569,10 @@ describe('rollup-plugin-html', () => { plugins: [ rollupPluginHTML({ rootDir, + publicPath: '/static/', input: { html: ``, }, - publicPath: '/static/', }), ], }; @@ -604,11 +604,11 @@ describe('rollup-plugin-html', () => { plugins: [ rollupPluginHTML({ rootDir, + publicPath: '/static/', input: { name: 'nested/index.html', html: ``, }, - publicPath: '/static/', }), ], }; @@ -642,10 +642,10 @@ describe('rollup-plugin-html', () => { const plugin = rollupPluginHTML({ rootDir, + publicPath: '/static/', input: { html: ``, }, - publicPath: '/static/', }); const config = { @@ -2117,6 +2117,7 @@ describe('rollup-plugin-html', () => { absolutePathPrefix: '/my-prefix/', rootDir, input: { + name: 'x/index.html', html: html` @@ -2128,7 +2129,6 @@ describe('rollup-plugin-html', () => { `, - name: 'x/index.html', }, }), ], From 5cf22b1b33b7eef03595461194c4aef65a5a58bb Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Tue, 23 Dec 2025 12:50:15 +0400 Subject: [PATCH 12/21] WIP12 --- .../rollup-plugin-html/src/assets/utils.ts | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/rollup-plugin-html/src/assets/utils.ts b/packages/rollup-plugin-html/src/assets/utils.ts index 2441440d5..eb0591e8b 100644 --- a/packages/rollup-plugin-html/src/assets/utils.ts +++ b/packages/rollup-plugin-html/src/assets/utils.ts @@ -5,21 +5,16 @@ import { findElements, getTagName, getAttribute } from '@web/parse5-utils'; import { createError } from '../utils.js'; import { serialize } from 'v8'; -const assetLinkRels: Record boolean)> = { - icon: true, - 'apple-touch-icon': true, - 'mask-icon': true, - stylesheet: true, - manifest: true, - // TODO: write a separate tests for these - // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload - // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/prefetch - preload: (node: Element) => { - return ['font', 'image', 'style'].includes(getAttribute(node, 'as') || ''); - }, - prefetch: true, - modulepreload: true, -}; +const assetLinkRels = [ + 'icon', + 'apple-touch-icon', + 'mask-icon', + 'stylesheet', + 'manifest', + 'preload', + 'prefetch', + 'modulepreload', +]; const legacyHashedLinkRels = ['stylesheet']; const assetMetaProperties = ['og:image']; @@ -57,8 +52,7 @@ function isAsset(node: Element) { } break; case 'link': { - const linkCheck = assetLinkRels[getAttribute(node, 'rel') ?? ''] || false; - if (typeof linkCheck === 'function' ? linkCheck(node) : linkCheck) { + if (assetLinkRels.includes(getAttribute(node, 'rel') ?? '')) { path = getAttribute(node, 'href') ?? ''; } break; From 7b1d53db076ef32c5de81a0248797ec55bd38904 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Tue, 23 Dec 2025 12:50:31 +0400 Subject: [PATCH 13/21] WIP13 --- docs/docs/building/rollup-plugin-html.md | 88 +++++++++++++++++------- packages/rollup-plugin-html/MIGRATION.md | 18 +++++ 2 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 packages/rollup-plugin-html/MIGRATION.md diff --git a/docs/docs/building/rollup-plugin-html.md b/docs/docs/building/rollup-plugin-html.md index 55ef75037..718dcbd34 100644 --- a/docs/docs/building/rollup-plugin-html.md +++ b/docs/docs/building/rollup-plugin-html.md @@ -105,31 +105,24 @@ export default { ### Bundling assets -The HTML plugin will bundle assets referenced from `img` and `link` and social media tag elements in your HTML. The assets are emitted as rollup assets, and the paths are updated to the rollup output paths. +The HTML plugin will bundle assets referenced in `img` and `link` and social media tag elements in your HTML: -By default rollup will hash the asset filenames, enabling long term caching. You can customize the filename pattern using the [assetFileNames option](https://rollupjs.org/guide/en/#outputassetfilenames) in your rollup config. - -To turn off bundling assets completely, set the `extractAssets` option to false: - -```js -import { rollupPluginHTML as html } from '@web/rollup-plugin-html'; - -export default { - input: 'index.html', - output: { dir: 'dist' }, - plugins: [ - html({ - extractAssets: false, - }), - ], -}; +```html + + + + + + + + + + + + ``` -#### Including assets referenced from css - -TODO: update - -Your css files reference other assets via `url`, like for example: +And the assets referenced in CSS via `url`: ```css body { @@ -143,7 +136,22 @@ body { } ``` -And those assets will get output to the `assets/` dir, and the source css file will get updated with the output locations of those assets, e.g.: +The assets are emitted as rollup assets, and the paths are updated to the rollup output paths: + +```html + + + + + + + + + + + + +``` ```css body { @@ -157,6 +165,38 @@ body { } ``` +You can configure the output paths via [assetFileNames option](https://rollupjs.org/guide/en/#outputassetfilenames) (by default `assets/[name]-[hash][extname]` at the time of writing). +The hash in the asset filenames enables long term caching. + +#### Disable assets bundling + +To turn off bundling assets completely, set the `extractAssets` option to false: + +```js +import { rollupPluginHTML as html } from '@web/rollup-plugin-html'; + +export default { + input: 'index.html', + output: { dir: 'dist' }, + plugins: [ + html({ + extractAssets: false, + }), + ], +}; +``` + +#### Enable legacy behavior + +For smooth migration we added legacy modes: + +- `extractAssets: 'legacy-html'` is the same as 2.x.x behavior when `bundleAssetsFromCss: false` +- `extractAssets: 'legacy-html-and-css'` is the same as 2.x.x behavior when `bundleAssetsFromCss: true` + +The 2.x.x behavior was limited to `` only and the assets referenced in the CSS files were hardcoded to be put into the nested `assets/` dir. + +We recommend to use legacy modes only during the migration of large multi-project codebases to 3.x.x in order to temporarily keep the old behavior until all projects can reliably use the new behavior, while at the same time upgrading tools centrally to the new version of `@web/rollup-plugin-html`. + ### Handling absolute paths If your HTML file contains any absolute paths they will be resolved against the current working directory. You can set a different root directory in the config. Input paths will be resolved relative to this root directory as well. @@ -354,7 +394,7 @@ export interface RollupPluginHTMLOptions { /** Transform HTML file before output. */ transformHtml?: TransformHtmlFunction | TransformHtmlFunction[]; /** Whether to extract and bundle assets referenced in HTML. Defaults to true. */ - extractAssets?: boolean; + extractAssets?: boolean | 'legacy-html' | 'legacy-html-and-css'; /** Whether to ignore assets referenced in HTML and CSS with glob patterns. */ externalAssets?: string | string[]; /** Define a full absolute url to your site (e.g. https://domain.com) */ diff --git a/packages/rollup-plugin-html/MIGRATION.md b/packages/rollup-plugin-html/MIGRATION.md new file mode 100644 index 000000000..ec4f00484 --- /dev/null +++ b/packages/rollup-plugin-html/MIGRATION.md @@ -0,0 +1,18 @@ +# Migration + +## From version 2.x.x to 3.x.x + +Check all output assets since now we handle all link `rel` types, specifically: + +- icon +- apple-touch-icon +- mask-icon +- stylesheet +- manifest +- preload +- prefetch +- modulepreload + +If any of them reference external assets or assets that don't need to be bundled, you can exclude such assets using the `externalAssets` configuration option. + +For old behavior which is only recommeneded during the migration you can check legacy modes `extractAssets: 'legacy-html'` and `extractAssets: 'legacy-html-and-css'` in the documentation. From c335e8e6df00776c68e0465ee3ee1f94f3045c1c Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Tue, 23 Dec 2025 12:58:22 +0400 Subject: [PATCH 14/21] WIP14 --- docs/docs/building/rollup-plugin-html.md | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/docs/building/rollup-plugin-html.md b/docs/docs/building/rollup-plugin-html.md index 718dcbd34..e202486c0 100644 --- a/docs/docs/building/rollup-plugin-html.md +++ b/docs/docs/building/rollup-plugin-html.md @@ -110,14 +110,15 @@ The HTML plugin will bundle assets referenced in `img` and `link` and social med ```html - - + + + - + - + ``` @@ -126,13 +127,11 @@ And the assets referenced in CSS via `url`: ```css body { - background-image: url('images/star.gif'); + background-image: url('images/image.png'); } -/* or */ @font-face { - src: url('fonts/font-bold.woff2') format('woff2'); - /* ...etc */ + src: url('fonts/font.woff2') format('woff2'); } ``` @@ -141,30 +140,31 @@ The assets are emitted as rollup assets, and the paths are updated to the rollup ```html - - + + + - + - + ``` ```css body { - background-image: url('assets/star-P4TYRBwL.gif'); + background-image: url('assets/image-C4yLPiIL.png'); } -/* or */ @font-face { - src: url('assets/font-bold-f0mNRiTD.woff2') format('woff2'); - /* ...etc */ + src: url('assets/font-f0mNRiTD.woff2') format('woff2'); } ``` +The images are deduped when same ones are referenced in different tags and files. + You can configure the output paths via [assetFileNames option](https://rollupjs.org/guide/en/#outputassetfilenames) (by default `assets/[name]-[hash][extname]` at the time of writing). The hash in the asset filenames enables long term caching. From 17ef93fd5f1439c16127afb1d01410c9daa34dce Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Tue, 23 Dec 2025 13:42:51 +0400 Subject: [PATCH 15/21] WIP15 --- package-lock.json | 5 +++-- packages/rollup-plugin-html/MIGRATION.md | 2 ++ packages/rollup-plugin-html/package.json | 4 ++-- packages/rollup-plugin-html/src/output/emitAssets.ts | 1 - 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 43bfd297f..d25393b8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36488,14 +36488,14 @@ "html-minifier-terser": "^7.1.0", "lightningcss": "^1.24.0", "parse5": "^6.0.1", - "picomatch": "^2.2.2", - "prettier": "^3.6.2" + "picomatch": "^2.2.2" }, "devDependencies": { "@prettier/sync": "^0.6.1", "@types/html-minifier-terser": "^7.0.0", "@types/picomatch": "^2.2.1", "@types/prettier": "^3.0.0", + "prettier": "^3.6.2", "rollup": "^4.4.0" }, "engines": { @@ -36549,6 +36549,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" diff --git a/packages/rollup-plugin-html/MIGRATION.md b/packages/rollup-plugin-html/MIGRATION.md index ec4f00484..099125caa 100644 --- a/packages/rollup-plugin-html/MIGRATION.md +++ b/packages/rollup-plugin-html/MIGRATION.md @@ -2,6 +2,8 @@ ## From version 2.x.x to 3.x.x +Remove `bundleAssetsFromCss` configuration option, now we bundle assets referenced in CSS by default. + Check all output assets since now we handle all link `rel` types, specifically: - icon diff --git a/packages/rollup-plugin-html/package.json b/packages/rollup-plugin-html/package.json index fb14e5ac7..15da1f9b6 100644 --- a/packages/rollup-plugin-html/package.json +++ b/packages/rollup-plugin-html/package.json @@ -49,14 +49,14 @@ "html-minifier-terser": "^7.1.0", "lightningcss": "^1.24.0", "parse5": "^6.0.1", - "picomatch": "^2.2.2", - "prettier": "^3.6.2" + "picomatch": "^2.2.2" }, "devDependencies": { "@prettier/sync": "^0.6.1", "@types/html-minifier-terser": "^7.0.0", "@types/picomatch": "^2.2.1", "@types/prettier": "^3.0.0", + "prettier": "^3.6.2", "rollup": "^4.4.0" } } diff --git a/packages/rollup-plugin-html/src/output/emitAssets.ts b/packages/rollup-plugin-html/src/output/emitAssets.ts index 831bc387e..8287199da 100644 --- a/packages/rollup-plugin-html/src/output/emitAssets.ts +++ b/packages/rollup-plugin-html/src/output/emitAssets.ts @@ -170,7 +170,6 @@ export async function emitAssets( i += 1; } emittedStaticAssetNames.add(basename); - // TODO: not sure what to do with this one yet const fileName = `assets/${basename}`; ref = this.emitFile({ type: 'asset', name: basename, fileName, source }); } From 9781c19918357fce87929956cfb78fdcf67565a5 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Tue, 23 Dec 2025 13:48:45 +0400 Subject: [PATCH 16/21] WIP16 --- .changeset/empty-carrots-sneeze.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/empty-carrots-sneeze.md diff --git a/.changeset/empty-carrots-sneeze.md b/.changeset/empty-carrots-sneeze.md new file mode 100644 index 000000000..bf77febf6 --- /dev/null +++ b/.changeset/empty-carrots-sneeze.md @@ -0,0 +1,11 @@ +--- +'@web/rollup-plugin-html': major +--- + +1. Enabled CSS assets extraction by default (as a result, removed configuration option bundleAssetsFromCss). +2. Made extraction of assets from all link rel types +3. Fixed "assetFileNames" behavior +4. Refactored all tests, added tests for more corner cases +5. Added legacy modes for old 2.x.x behavior. + +See MIGRATION.md for migration notes. From 8545aa43c1e3a5e1baffc7ab229131c3d694a3a8 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Wed, 24 Dec 2025 12:46:32 +0400 Subject: [PATCH 17/21] WIP17 --- .../rollup-plugin-html/src/input/InputData.ts | 2 +- .../src/input/extract/extractAssets.ts | 6 +- .../src/output/emitAssets.ts | 16 +- .../src/output/injectedUpdatedAssetPaths.ts | 2 +- .../test/rollup-plugin-html.test.ts | 113 +--- .../test/src/input/InputData.test.ts | 626 ++++++++++++++++++ .../src/input/extract/extractAssets.test.ts | 400 +++++++++++ .../src/input/extract/extractModules.test.ts | 194 ++++++ .../src/output/getEntrypointBundles.test.ts | 360 ++++++++++ .../test/src/output/getOutputHTML.test.ts | 210 ++++++ .../test/src/output/injectBundles.test.ts | 129 ++++ .../output/injectedUpdatedAssetPaths.test.ts | 352 ++++++++++ packages/rollup-plugin-html/test/utils.ts | 102 +++ 13 files changed, 2394 insertions(+), 118 deletions(-) create mode 100644 packages/rollup-plugin-html/test/src/input/InputData.test.ts create mode 100644 packages/rollup-plugin-html/test/src/input/extract/extractAssets.test.ts create mode 100644 packages/rollup-plugin-html/test/src/input/extract/extractModules.test.ts create mode 100644 packages/rollup-plugin-html/test/src/output/getEntrypointBundles.test.ts create mode 100644 packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts create mode 100644 packages/rollup-plugin-html/test/src/output/injectBundles.test.ts create mode 100644 packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts create mode 100644 packages/rollup-plugin-html/test/utils.ts diff --git a/packages/rollup-plugin-html/src/input/InputData.ts b/packages/rollup-plugin-html/src/input/InputData.ts index 2d5838856..d08a217ef 100644 --- a/packages/rollup-plugin-html/src/input/InputData.ts +++ b/packages/rollup-plugin-html/src/input/InputData.ts @@ -2,7 +2,7 @@ import { ScriptModuleTag } from '../RollupPluginHTMLOptions'; export interface InputAsset { filePath: string; - legacyHashed: boolean; + hashed: boolean; content: Buffer; } diff --git a/packages/rollup-plugin-html/src/input/extract/extractAssets.ts b/packages/rollup-plugin-html/src/input/extract/extractAssets.ts index a026c8c31..4a71ae84c 100644 --- a/packages/rollup-plugin-html/src/input/extract/extractAssets.ts +++ b/packages/rollup-plugin-html/src/input/extract/extractAssets.ts @@ -37,9 +37,7 @@ export function extractAssets(params: ExtractAssetsParams): InputAsset[] { params.absolutePathPrefix, ); const hashed = isHashedAsset(node, params.extractAssets); - const alreadyHandled = allAssets.find( - a => a.filePath === filePath && a.legacyHashed === hashed, - ); + const alreadyHandled = allAssets.find(a => a.filePath === filePath && a.hashed === hashed); if (!alreadyHandled) { try { fs.accessSync(filePath); @@ -52,7 +50,7 @@ export function extractAssets(params: ExtractAssetsParams): InputAsset[] { } const content = fs.readFileSync(filePath); - allAssets.push({ filePath, legacyHashed: hashed, content }); + allAssets.push({ filePath, hashed: hashed, content }); } } } diff --git a/packages/rollup-plugin-html/src/output/emitAssets.ts b/packages/rollup-plugin-html/src/output/emitAssets.ts index 8287199da..0a1b0b4ba 100644 --- a/packages/rollup-plugin-html/src/output/emitAssets.ts +++ b/packages/rollup-plugin-html/src/output/emitAssets.ts @@ -10,7 +10,7 @@ import { RollupPluginHTMLOptions, TransformAssetFunction } from '../RollupPlugin export interface EmittedAssets { static: Map; - legacyHashed: Map; + hashed: Map; } const allowedFileExtensions = [ @@ -55,12 +55,12 @@ export async function emitAssets( } } const staticAssets: InputAsset[] = []; - const legacyHashedAssets: InputAsset[] = []; + const hashedAssets: InputAsset[] = []; for (const input of inputs) { for (const asset of input.assets) { - if (asset.legacyHashed) { - legacyHashedAssets.push(asset); + if (asset.hashed) { + hashedAssets.push(asset); } else { staticAssets.push(asset); } @@ -68,10 +68,10 @@ export async function emitAssets( } // ensure static assets are last because of https://github.com/rollup/rollup/issues/3853 - const allAssets = [...legacyHashedAssets, ...staticAssets]; + const allAssets = [...hashedAssets, ...staticAssets]; for (const asset of allAssets) { - const map = asset.legacyHashed ? emittedHashedAssets : emittedStaticAssets; + const map = asset.hashed ? emittedHashedAssets : emittedStaticAssets; if (!map.has(asset.filePath)) { let source: Buffer = asset.content; @@ -87,7 +87,7 @@ export async function emitAssets( let basename = path.basename(asset.filePath); const isExternal = createAssetPicomatchMatcher(options.externalAssets); const emittedExternalAssets = new Map(); - if (asset.legacyHashed) { + if (asset.hashed) { if (basename.endsWith('.css') && extractAssets) { let updatedCssSource = false; const { code } = await transform({ @@ -178,5 +178,5 @@ export async function emitAssets( } } - return { static: emittedStaticAssets, legacyHashed: emittedHashedAssets }; + return { static: emittedStaticAssets, hashed: emittedHashedAssets }; } diff --git a/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts b/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts index 0990cd89e..a7cdedc9a 100644 --- a/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts +++ b/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts @@ -62,7 +62,7 @@ export function injectedUpdatedAssetPaths(args: InjectUpdatedAssetPathsArgs) { const htmlDir = path.dirname(htmlFilePath); const filePath = resolveAssetFilePath(sourcePath, htmlDir, rootDir, absolutePathPrefix); const assetPaths = isHashedAsset(node, extractAssets) - ? emittedAssets.legacyHashed + ? emittedAssets.hashed : emittedAssets.static; const relativeOutputPath = assetPaths.get(filePath); 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 3ec4b8c78..799e39099 100644 --- a/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts +++ b/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts @@ -1,113 +1,14 @@ -import synchronizedPrettier from '@prettier/sync'; -import * as prettier from 'prettier'; -import { rollup, OutputChunk, OutputOptions, Plugin, RollupBuild } from 'rollup'; +import { rollup, OutputChunk, OutputOptions, Plugin } from 'rollup'; import { expect } from 'chai'; import path from 'path'; -import fs from 'fs'; import { rollupPluginHTML } from '../src/index.js'; - -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)]; -} - -const html = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.html'](merge(strings, ...values)); - -const css = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.css'](merge(strings, ...values)); - -const js = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.js'](merge(strings, ...values)); - -const svg = (strings: TemplateStringsArray, ...values: string[]) => - extnameToFormatter['.svg'](merge(strings, ...values)); +import { html, css, js, svg, generateTestBundle, createApp, cleanApp } from './utils.js'; const outputConfig: OutputOptions = { format: 'es', dir: 'dist', }; -async function generateTestBundle(build: RollupBuild, outputConfig: OutputOptions) { - const { output } = await build.generate(outputConfig); - const chunks: Record = {}; - const assets: 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; - if (typeof code !== 'string' && filename.endsWith('.css')) { - code = Buffer.from(code).toString('utf8'); - } - if (typeof code === 'string' && formatter) { - code = formatter(code); - } - assets[filename] = code; - } - } - - return { output, chunks, assets }; -} - -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; -} - -function cleanApp() { - const tmpDir = path.join(__dirname, './.tmp'); - if (fs.existsSync(tmpDir)) { - fs.rmSync(tmpDir, { recursive: true }); - } -} - describe('rollup-plugin-html', () => { afterEach(() => { cleanApp(); @@ -1325,6 +1226,7 @@ describe('rollup-plugin-html', () => { 'image-c.png': 'image-c.png', 'image-a.svg': svg``, 'image-b.svg': svg``, + 'image-c.svg': svg``, 'styles.css': css` :root { color: blue; @@ -1358,6 +1260,7 @@ describe('rollup-plugin-html', () => { + @@ -1376,7 +1279,7 @@ describe('rollup-plugin-html', () => { const { chunks, assets } = await generateTestBundle(build, outputConfig); expect(Object.keys(chunks)).to.have.lengthOf(1); - expect(Object.keys(assets)).to.have.lengthOf(10); + expect(Object.keys(assets)).to.have.lengthOf(11); expect(assets).to.have.keys([ 'assets/image-a-XOCPHCrV.png', @@ -1384,6 +1287,7 @@ describe('rollup-plugin-html', () => { 'assets/image-c-C4yLPiIL.png', 'assets/image-a-BCCvKrTe.svg', 'assets/image-b-C4stzVZW.svg', + 'assets/image-c-DPeYetg3.svg', 'assets/styles-CF2Iy5n1.css', 'assets/x-DDGg8O6h.css', 'assets/y-DJTrnPH3.css', @@ -1401,6 +1305,7 @@ describe('rollup-plugin-html', () => { + @@ -1655,7 +1560,7 @@ describe('rollup-plugin-html', () => { html: html` - + @@ -1678,7 +1583,7 @@ describe('rollup-plugin-html', () => { - + diff --git a/packages/rollup-plugin-html/test/src/input/InputData.test.ts b/packages/rollup-plugin-html/test/src/input/InputData.test.ts new file mode 100644 index 000000000..f1fab22f9 --- /dev/null +++ b/packages/rollup-plugin-html/test/src/input/InputData.test.ts @@ -0,0 +1,626 @@ +import { expect } from 'chai'; +import path from 'path'; +import { cleanApp, createApp, html, js } from '../../utils.js'; + +import { getInputData } from '../../../src/input/getInputData.js'; +import { InputData } from '../../../src/input/InputData.js'; + +function cleanupHtml(str: string) { + return str.replace(/(\r\n|\n|\r| )/gm, ''); +} + +function cleanupResult(result: InputData[]) { + return result.map(item => ({ + ...item, + inlineModules: Array.from(item.inlineModules.entries()), + html: cleanupHtml(item.html), + })); +} + +describe('getInputData()', () => { + afterEach(() => { + cleanApp(); + }); + + it('supports setting input as string', () => { + const rootDir = createApp({ + 'index.html': html` + + +

Hello world

+ + + + `, + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ input: 'index.html', rootDir }); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'index.html'), + html: '

Helloworld

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: 'index.html', + }, + ]); + }); + + it('supports setting input as object', () => { + const rootDir = createApp({ + 'index.html': html` + + +

Hello world

+ + + + `, + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ input: { path: 'index.html' }, rootDir }); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'index.html'), + html: '

Helloworld

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: 'index.html', + }, + ]); + }); + + it('supports changing file name', () => { + const rootDir = createApp({ + 'index.html': html` + + +

Hello world

+ + + + `, + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ input: { path: 'index.html', name: 'foo.html' }, rootDir }); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'index.html'), + html: '

Helloworld

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: 'foo.html', + }, + ]); + }); + + it('supports setting multiple inputs', () => { + const rootDir = createApp({ + 'index.html': html` + + +

Hello world

+ + + + `, + 'not-index.html': html` + + +

not-index.html

+ + + `, + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ + input: [{ path: 'index.html' }, { path: 'not-index.html' }], + rootDir, + }); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'index.html'), + html: '

Helloworld

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: 'index.html', + }, + { + filePath: path.join(rootDir, 'not-index.html'), + html: '

not-index.html

', + inlineModules: [], + moduleImports: [], + assets: [], + name: 'not-index.html', + }, + ]); + }); + + it('resolves modules relative to HTML file', () => { + const rootDir = createApp({ + 'src/index.html': html` + + +

Hello world

+ + + + `, + 'src/app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ input: 'src/index.html', rootDir }); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'src/index.html'), + html: '

Helloworld

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'src', 'app.js'), attributes: [] }], + assets: [], + name: 'index.html', + }, + ]); + }); + + it('supports setting input as rollup input string', () => { + const rootDir = createApp({ + 'index.html': html` + + +

Hello world

+ + + + `, + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ rootDir }, 'index.html'); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'index.html'), + html: '

Helloworld

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: 'index.html', + }, + ]); + }); + + it('supports setting input as rollup input array', () => { + const rootDir = createApp({ + 'index.html': html` + + +

Hello world

+ + + + `, + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ rootDir }, ['index.html']); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'index.html'), + html: '

Helloworld

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: 'index.html', + }, + ]); + }); + + it('supports setting input as rollup input array', () => { + const rootDir = createApp({ + 'index.html': html` + + +

Hello world

+ + + + `, + 'not-index.html': html` + + +

not-index.html

+ + + `, + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ rootDir }, ['index.html', 'not-index.html']); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'index.html'), + html: '

Helloworld

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: 'index.html', + }, + { + filePath: path.join(rootDir, 'not-index.html'), + html: '

not-index.html

', + inlineModules: [], + moduleImports: [], + assets: [], + name: 'not-index.html', + }, + ]); + }); + + it('supports setting input as rollup input object', () => { + const rootDir = createApp({ + 'index.html': html` + + +

Hello world

+ + + + `, + 'not-index.html': html` + + +

not-index.html

+ + + `, + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData( + { rootDir }, + { 'a.html': 'index.html', 'b.html': 'not-index.html' }, + ); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'index.html'), + html: '

Helloworld

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: 'a.html', + }, + { + filePath: path.join(rootDir, 'not-index.html'), + html: '

not-index.html

', + inlineModules: [], + moduleImports: [], + assets: [], + name: 'b.html', + }, + ]); + }); + + it('plugin input takes presedence over rollup input', () => { + const rootDir = createApp({ + 'index.html': html` + + +

Hello world

+ + + + `, + 'not-index.html': html` + + +

not-index.html

+ + + `, + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ input: 'index.html', rootDir }, 'not-index.html'); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'index.html'), + html: '

Helloworld

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: 'index.html', + }, + ]); + }); + + it('can set html string as input', () => { + const rootDir = createApp({ + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ + input: { + html: html` + + +

HTML as string

+ + + + `, + }, + rootDir, + }); + expect(cleanupResult(result)).to.eql([ + { + filePath: undefined, + html: '

HTMLasstring

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: 'index.html', + }, + ]); + }); + + it('can set multiple html strings as input', () => { + const rootDir = createApp({ + 'app.js': js` + console.log('hello world'); + `, + }); + const result = getInputData({ + rootDir, + input: [ + { + name: '1.html', + html: html` + + +

HTML1

+ + + + `, + }, + { + name: '2.html', + html: html` + + +

HTML2

+ + + `, + }, + ], + }); + expect(cleanupResult(result)).to.eql([ + { + filePath: undefined, + html: '

HTML1

', + inlineModules: [], + moduleImports: [{ importPath: path.join(rootDir, 'app.js'), attributes: [] }], + assets: [], + name: '1.html', + }, + { + filePath: undefined, + html: '

HTML2

', + inlineModules: [], + moduleImports: [], + assets: [], + name: '2.html', + }, + ]); + }); + + it('supports setting input to a glob', () => { + const rootDir = createApp({ + 'pages/page-a.html': html` + + +

page-a.html

+ + + + + `, + 'pages/page-b.html': html` + + +

page-b.html

+ + + + + `, + 'pages/page-c.html': html` + + +

page-c.html

+ + + + + `, + 'pages/page-a.js': js` + export default 'page a'; + `, + 'pages/page-b.js': js` + export default 'page b'; + `, + 'pages/page-c.js': js` + export default 'page c'; + `, + 'pages/shared.js': js` + export default 'shared'; + `, + }); + const result = getInputData({ input: 'pages/**/*.html', rootDir }); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'pages', 'page-c.html'), + html: '

page-c.html

', + inlineModules: [], + moduleImports: [ + { importPath: path.join(rootDir, 'pages', 'page-c.js'), attributes: [] }, + { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, + ], + assets: [], + name: 'page-c.html', + }, + { + filePath: path.join(rootDir, 'pages', 'page-b.html'), + html: '

page-b.html

', + inlineModules: [], + moduleImports: [ + { importPath: path.join(rootDir, 'pages', 'page-b.js'), attributes: [] }, + { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, + ], + assets: [], + name: 'page-b.html', + }, + { + filePath: path.join(rootDir, 'pages', 'page-a.html'), + html: '

page-a.html

', + inlineModules: [], + moduleImports: [ + { importPath: path.join(rootDir, 'pages', 'page-a.js'), attributes: [] }, + { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, + ], + assets: [], + name: 'page-a.html', + }, + ]); + }); + + it('supports not flattening output directories', () => { + const rootDir = createApp({ + 'pages/page-a.html': html` + + +

page-a.html

+ + + + + `, + 'pages/page-b.html': html` + + +

page-b.html

+ + + + + `, + 'pages/page-c.html': html` + + +

page-c.html

+ + + + + `, + 'pages/page-a.js': js` + export default 'page a'; + `, + 'pages/page-b.js': js` + export default 'page b'; + `, + 'pages/page-c.js': js` + export default 'page c'; + `, + 'pages/shared.js': js` + export default 'shared'; + `, + }); + const result = getInputData({ input: 'pages/**/*.html', flattenOutput: false, rootDir }); + expect(cleanupResult(result)).to.eql([ + { + filePath: path.join(rootDir, 'pages', 'page-c.html'), + html: '

page-c.html

', + inlineModules: [], + moduleImports: [ + { importPath: path.join(rootDir, 'pages', 'page-c.js'), attributes: [] }, + { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, + ], + assets: [], + name: `pages${path.sep}page-c.html`, + }, + { + filePath: path.join(rootDir, 'pages', 'page-b.html'), + html: '

page-b.html

', + inlineModules: [], + moduleImports: [ + { importPath: path.join(rootDir, 'pages', 'page-b.js'), attributes: [] }, + { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, + ], + assets: [], + name: `pages${path.sep}page-b.html`, + }, + { + filePath: path.join(rootDir, 'pages', 'page-a.html'), + html: '

page-a.html

', + inlineModules: [], + moduleImports: [ + { importPath: path.join(rootDir, 'pages', 'page-a.js'), attributes: [] }, + { importPath: path.join(rootDir, 'pages', 'shared.js'), attributes: [] }, + ], + assets: [], + name: `pages${path.sep}page-a.html`, + }, + ]); + }); + + it('supports pure HTML files', () => { + const rootDir = createApp({}); + const result = getInputData({ + rootDir, + input: { + html: html` + + +

pure HTML

+ + + `, + }, + }); + expect(cleanupResult(result)).to.eql([ + { + filePath: undefined, + html: '

pureHTML

', + inlineModules: [], + moduleImports: [], + assets: [], + name: 'index.html', + }, + ]); + }); + + it('throws when no files or html is given', () => { + const rootDir = createApp({}); + expect(() => getInputData({ rootDir })).to.throw(); + }); +}); 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 new file mode 100644 index 000000000..24a8dbd2b --- /dev/null +++ b/packages/rollup-plugin-html/test/src/input/extract/extractAssets.test.ts @@ -0,0 +1,400 @@ +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'; + +describe('extractAssets', () => { + afterEach(() => { + cleanApp(); + }); + + it('extracts assets from a document', () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + 'image-b.png': 'image-b.png', + 'image-c.png': 'image-c.png', + 'image-a.svg': svg``, + 'image-b.svg': svg``, + 'image-c.svg': svg``, + 'styles.css': css` + :root { + color: blue; + } + `, + 'webmanifest.json': { message: 'hello world' }, + }); + + const document = parse(html` + + + + + + + + + + + +
+ +
+ + + `); + const assets = extractAssets({ + document, + htmlFilePath: path.join(rootDir, 'index.html'), + htmlDir: rootDir, + rootDir, + extractAssets: true, + }); + + const assetsWithoutcontent = assets.map(a => ({ ...a, content: undefined })); + expect(assetsWithoutcontent).to.eql([ + { + content: undefined, + filePath: path.join(rootDir, 'image-a.png'), + hashed: true, + }, + { + content: undefined, + filePath: path.join(rootDir, 'image-b.png'), + hashed: true, + }, + { + content: undefined, + filePath: path.join(rootDir, 'webmanifest.json'), + hashed: true, + }, + { + content: undefined, + filePath: path.join(rootDir, 'image-a.svg'), + hashed: true, + }, + { + content: undefined, + filePath: path.join(rootDir, 'styles.css'), + hashed: true, + }, + { + content: undefined, + filePath: path.join(rootDir, 'image-c.svg'), + hashed: true, + }, + { + content: undefined, + filePath: path.join(rootDir, 'image-c.png'), + hashed: true, + }, + { + content: undefined, + filePath: path.join(rootDir, 'image-b.svg'), + hashed: true, + }, + ]); + }); + + it('reads file sources', () => { + const rootDir = createApp({ + 'image-a.svg': svg``, + 'image-b.svg': svg``, + 'styles.css': css` + :root { + color: blue; + } + `, + 'webmanifest.json': { message: 'hello world' }, + }); + + const document = parse(html` + + + + + + + +
+ +
+ + + `); + const assets = extractAssets({ + document, + htmlFilePath: path.join(rootDir, 'index.html'), + htmlDir: rootDir, + rootDir, + extractAssets: true, + }); + + const transformedAssets = assets.map(asset => ({ + ...asset, + content: asset.content.toString('utf-8').replace(/\s/g, ''), + })); + expect(transformedAssets).to.eql([ + { + content: '{"message":"helloworld"}', + filePath: path.join(rootDir, 'webmanifest.json'), + hashed: true, + }, + { + content: '', + filePath: path.join(rootDir, 'image-a.svg'), + hashed: true, + }, + { + content: ':root{color:blue;}', + filePath: path.join(rootDir, 'styles.css'), + hashed: true, + }, + { + content: '', + filePath: path.join(rootDir, 'image-b.svg'), + hashed: true, + }, + ]); + }); + + it('handles paths into directories', () => { + const rootDir = createApp({ + 'foo/x.css': css` + :root { + color: x; + } + `, + 'foo/bar/y.css': css` + :root { + color: y; + } + `, + }); + + const document = parse(html` + + + + + + + `); + const assets = extractAssets({ + document, + htmlFilePath: path.join(rootDir, 'index.html'), + htmlDir: rootDir, + rootDir, + extractAssets: true, + }); + + expect(assets.length).to.equal(2); + expect(assets[0].filePath).to.equal(path.join(rootDir, 'foo', 'x.css')); + expect(assets[1].filePath).to.equal(path.join(rootDir, 'foo', 'bar', 'y.css')); + expect(assets[0].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:x;}'); + expect(assets[1].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:y;}'); + }); + + it('resolves relative to HTML file location', () => { + const rootDir = createApp({ + 'foo/x.css': css` + :root { + color: x; + } + `, + 'styles.css': css` + :root { + color: blue; + } + `, + }); + + const document = parse(html` + + + + + + + `); + const assets = extractAssets({ + document, + htmlFilePath: path.join(rootDir, 'foo', 'index.html'), + htmlDir: path.join(rootDir, 'foo'), + rootDir, + extractAssets: true, + }); + + expect(assets.length).to.equal(2); + expect(assets[0].filePath).to.equal(path.join(rootDir, 'foo', 'x.css')); + expect(assets[1].filePath).to.equal(path.join(rootDir, 'styles.css')); + expect(assets[0].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:x;}'); + expect(assets[1].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:blue;}'); + }); + + it('resolves absolute paths relative to root dir', () => { + const rootDir = createApp({ + 'foo/x.css': css` + :root { + color: x; + } + `, + 'styles.css': css` + :root { + color: blue; + } + `, + }); + + const document = parse(html` + + + + + + + `); + const assets = extractAssets({ + document, + htmlFilePath: path.join(rootDir, 'foo', 'index.html'), + htmlDir: path.join(rootDir, 'foo'), + rootDir, + extractAssets: true, + }); + + expect(assets.length).to.equal(2); + expect(assets[0].filePath).to.equal(path.join(rootDir, 'foo', 'x.css')); + expect(assets[1].filePath).to.equal(path.join(rootDir, 'styles.css')); + expect(assets[0].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:x;}'); + expect(assets[1].content.toString('utf-8').replace(/\s/g, '')).to.equal(':root{color:blue;}'); + }); + + it('can deduplicate assets with same names', () => { + const rootDir = createApp({ + 'image-a.png': 'image-a.png', + }); + + const document = parse(html` + + + + + + + `); + const assets = extractAssets({ + document, + htmlFilePath: path.join(rootDir, 'index.html'), + htmlDir: rootDir, + rootDir, + extractAssets: true, + }); + + expect(assets.length).to.equal(1); + const assetsWithoutcontent = assets.map(a => ({ ...a, content: undefined })); + expect(assetsWithoutcontent).to.eql([ + { + content: undefined, + filePath: path.join(rootDir, 'image-a.png'), + hashed: true, + }, + ]); + }); + + it('does not count remote URLs as assets', () => { + const rootDir = createApp({}); + + const document = parse(html` + + + + + + `); + const assets = extractAssets({ + document, + htmlFilePath: path.join(rootDir, 'foo', 'index.html'), + htmlDir: path.join(rootDir, 'foo'), + rootDir, + extractAssets: true, + }); + + expect(assets.length).to.equal(0); + }); + + it('does treat non module script tags as assets', () => { + const rootDir = createApp({ + 'no-module.js': js`/* no module script file */`, + }); + + const document = parse(html` + + + + + + `); + const assets = extractAssets({ + document, + htmlFilePath: path.join(rootDir, 'index.html'), + htmlDir: path.join(rootDir), + rootDir, + extractAssets: true, + }); + + expect(assets.length).to.equal(1); + expect(assets[0].filePath).to.equal(path.join(rootDir, 'no-module.js')); + expect(assets[0].content.toString('utf-8')).to.equal('/* no module script file */\n'); + }); + + it('handles a picture tag using source tags with srcset', () => { + const rootDir = createApp({ + 'images/eb26e6ca-30.avif': 'images/eb26e6ca-30.avif', + 'images/eb26e6ca-60.avif': 'images/eb26e6ca-60.avif', + 'images/eb26e6ca-30.jpeg': 'images/eb26e6ca-30.jpeg', + 'images/eb26e6ca-60.jpeg': 'images/eb26e6ca-60.jpeg', + }); + + const document = parse(html` + + + + + + My Image Alternative Text + + + + `); + const assets = extractAssets({ + document, + htmlFilePath: path.join(rootDir, 'index.html'), + htmlDir: path.join(rootDir), + rootDir, + extractAssets: true, + }); + + // the src is not the same as the small jpeg image + expect(assets.length).to.equal(4); + expect(assets[0].filePath).to.equal(path.join(rootDir, 'images', 'eb26e6ca-30.avif')); + expect(assets[1].filePath).to.equal(path.join(rootDir, 'images', 'eb26e6ca-60.avif')); + expect(assets[2].filePath).to.equal(path.join(rootDir, 'images', 'eb26e6ca-30.jpeg')); + expect(assets[3].filePath).to.equal(path.join(rootDir, 'images', 'eb26e6ca-60.jpeg')); + }); +}); 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 new file mode 100644 index 000000000..11fd545b5 --- /dev/null +++ b/packages/rollup-plugin-html/test/src/input/extract/extractModules.test.ts @@ -0,0 +1,194 @@ +import path from 'path'; +import { parse, serialize } from 'parse5'; +import { expect } from 'chai'; + +import { extractModules } from '../../../../src/input/extract/extractModules.js'; + +const { sep } = path; + +describe('extractModules()', () => { + it('extracts all modules from a html document', () => { + const document = parse( + '
before
' + + '' + + '' + + '
after
', + ); + + const { moduleImports, inlineModules } = extractModules({ + document, + htmlDir: '/', + rootDir: '/', + }); + const htmlWithoutModules = serialize(document); + + expect(inlineModules.length).to.equal(0); + expect(moduleImports).to.eql([ + { importPath: `${sep}foo.js`, attributes: [] }, + { importPath: `${sep}bar.js`, attributes: [] }, + ]); + expect(htmlWithoutModules).to.eql( + '
before
after
', + ); + }); + + it('does not touch non module scripts', () => { + const document = parse( + '
before
' + + '' + + '' + + '
after
', + ); + + const { moduleImports, inlineModules } = extractModules({ + document, + htmlDir: '/', + rootDir: '/', + }); + const htmlWithoutModules = serialize(document); + + expect(inlineModules.length).to.equal(0); + expect(moduleImports).to.eql([]); + expect(htmlWithoutModules).to.eql( + '
before
after
', + ); + }); + + it('resolves imports relative to the root dir', () => { + const document = parse( + '
before
' + + '' + + '' + + '
after
', + ); + + const { moduleImports, inlineModules } = extractModules({ + document, + htmlDir: '/', + rootDir: '/base/', + }); + const htmlWithoutModules = serialize(document); + + expect(inlineModules.length).to.equal(0); + expect(moduleImports).to.eql([ + { importPath: `${sep}foo.js`, attributes: [] }, + { importPath: `${sep}base${sep}bar.js`, attributes: [] }, + ]); + expect(htmlWithoutModules).to.eql( + '
before
after
', + ); + }); + + it('resolves relative imports relative to the relative import base', () => { + const document = parse( + '
before
' + + '' + + '' + + '
after
', + ); + + const { moduleImports, inlineModules } = extractModules({ + document, + htmlDir: '/base-1/base-2/', + rootDir: '/base-1/', + }); + const htmlWithoutModules = serialize(document); + + expect(inlineModules.length).to.equal(0); + expect(moduleImports).to.eql([ + { importPath: `${sep}base-1${sep}base-2${sep}foo.js`, attributes: [] }, + { importPath: `${sep}base-1${sep}bar.js`, attributes: [] }, + ]); + expect(htmlWithoutModules).to.eql( + '
before
after
', + ); + }); + + it('extracts all inline modules from a html document', () => { + const document = parse( + '
before
' + + '' + + '' + + '
after
', + ); + + const { moduleImports, inlineModules } = extractModules({ + document, + htmlDir: '/', + rootDir: '/', + }); + const htmlWithoutModules = serialize(document); + + expect(inlineModules).to.eql([ + { + importPath: '/inline-module-cce79ce714e2c3b250afef32e61fb003.js', + code: '/* my module 1 */', + attributes: [], + }, + { + importPath: '/inline-module-d9a0918508784903d131c7c4eb98e424.js', + code: '/* my module 2 */', + attributes: [], + }, + ]); + expect(moduleImports).to.eql([]); + expect(htmlWithoutModules).to.eql( + '
before
after
', + ); + }); + + it('prefixes inline module with index.html directory', () => { + const document = parse( + '
before
' + + '' + + '' + + '
after
', + ); + + const { moduleImports, inlineModules } = extractModules({ + document, + htmlDir: '/foo/bar/', + rootDir: '/', + }); + const htmlWithoutModules = serialize(document); + + expect(inlineModules).to.eql([ + { + importPath: '/foo/bar/inline-module-cce79ce714e2c3b250afef32e61fb003.js', + code: '/* my module 1 */', + attributes: [], + }, + { + importPath: '/foo/bar/inline-module-d9a0918508784903d131c7c4eb98e424.js', + code: '/* my module 2 */', + attributes: [], + }, + ]); + expect(moduleImports).to.eql([]); + expect(htmlWithoutModules).to.eql( + '
before
after
', + ); + }); + + it('ignores absolute paths', () => { + const document = parse( + '
before
' + + '' + + '' + + '
after
', + ); + + const { moduleImports, inlineModules } = extractModules({ + document, + htmlDir: '/', + rootDir: '/', + }); + const htmlWithoutModules = serialize(document); + + expect(inlineModules.length).to.equal(0); + expect(moduleImports).to.eql([{ importPath: `${sep}bar.js`, attributes: [] }]); + expect(htmlWithoutModules).to.eql( + '
before
after
', + ); + }); +}); diff --git a/packages/rollup-plugin-html/test/src/output/getEntrypointBundles.test.ts b/packages/rollup-plugin-html/test/src/output/getEntrypointBundles.test.ts new file mode 100644 index 000000000..e7cf8d647 --- /dev/null +++ b/packages/rollup-plugin-html/test/src/output/getEntrypointBundles.test.ts @@ -0,0 +1,360 @@ +import { expect } from 'chai'; +import { + getEntrypointBundles, + createImportPath, +} from '../../../src/output/getEntrypointBundles.js'; +import { GeneratedBundle, ScriptModuleTag } from '../../../src/RollupPluginHTMLOptions.js'; + +describe('createImportPath()', () => { + it('creates a relative import path', () => { + expect( + createImportPath({ + outputDir: 'dist', + fileOutputDir: 'dist', + htmlFileName: 'index.html', + fileName: 'foo.js', + }), + ).to.equal('./foo.js'); + }); + + it('handles files output in a different directory', () => { + expect( + createImportPath({ + outputDir: 'dist', + fileOutputDir: 'dist/legacy', + htmlFileName: 'index.html', + fileName: 'foo.js', + }), + ).to.equal('./legacy/foo.js'); + }); + + it('handles directory in filename', () => { + expect( + createImportPath({ + outputDir: 'dist', + fileOutputDir: 'dist', + htmlFileName: 'index.html', + fileName: 'legacy/foo.js', + }), + ).to.equal('./legacy/foo.js'); + }); + + it('allows configuring a public path', () => { + expect( + createImportPath({ + publicPath: 'static', + outputDir: 'dist', + fileOutputDir: 'dist', + htmlFileName: 'index.html', + fileName: 'foo.js', + }), + ).to.equal('./static/foo.js'); + }); + + it('allows configuring an absolute public path', () => { + expect( + createImportPath({ + publicPath: '/static', + outputDir: 'dist', + fileOutputDir: 'dist', + htmlFileName: 'index.html', + fileName: 'foo.js', + }), + ).to.equal('/static/foo.js'); + }); + + it('allows configuring an absolute public path with just a /', () => { + expect( + createImportPath({ + publicPath: '/', + outputDir: 'dist', + fileOutputDir: 'dist', + htmlFileName: 'index.html', + fileName: 'foo.js', + }), + ).to.equal('/foo.js'); + }); + + it('allows configuring an absolute public path with a trailing /', () => { + expect( + createImportPath({ + publicPath: '/static/public/', + outputDir: 'dist', + fileOutputDir: 'dist', + htmlFileName: 'index.html', + fileName: 'foo.js', + }), + ).to.equal('/static/public/foo.js'); + }); + + it('respects a different output dir when configuring a public path', () => { + expect( + createImportPath({ + publicPath: '/static', + outputDir: 'dist', + fileOutputDir: 'dist/legacy', + htmlFileName: 'index.html', + fileName: 'foo.js', + }), + ).to.equal('/static/legacy/foo.js'); + }); + + it('when html is output in a directory, creates a relative path from the html file to the js file', () => { + expect( + createImportPath({ + outputDir: 'dist', + fileOutputDir: 'dist', + htmlFileName: 'pages/index.html', + fileName: 'foo.js', + }), + ).to.equal('../foo.js'); + }); + + it('when html is output in a directory and absolute path is set, creates a direct path from the root to the js file', () => { + expect( + createImportPath({ + publicPath: '/static/', + outputDir: 'dist', + fileOutputDir: 'dist', + htmlFileName: 'pages/index.html', + fileName: 'foo.js', + }), + ).to.equal('/static/foo.js'); + }); +}); + +describe('getEntrypointBundles()', () => { + const defaultBundles: GeneratedBundle[] = [ + { + name: 'default', + options: { format: 'es', dir: 'dist' }, + bundle: { + // @ts-ignore + 'app.js': { + isEntry: true, + fileName: 'app.js', + facadeModuleId: '/root/app.js', + type: 'chunk', + }, + }, + }, + ]; + + const inputModuleIds: ScriptModuleTag[] = [ + { importPath: '/root/app.js' }, + { importPath: '/root/foo.js' }, + ]; + + const defaultOptions = { + pluginOptions: {}, + inputModuleIds, + outputDir: 'dist', + htmlFileName: 'index.html', + generatedBundles: defaultBundles, + }; + + it('generates entrypoints for a simple project', async () => { + const output = await getEntrypointBundles(defaultOptions); + expect(Object.keys(output).length).to.equal(1); + expect(output.default.options).to.equal(defaultBundles[0].options); + expect(output.default.bundle).to.equal(defaultBundles[0].bundle); + expect(output.default.entrypoints.length).to.equal(1); + expect(output.default.entrypoints[0].chunk).to.equal(defaultBundles[0].bundle['app.js']); + expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['./app.js']); + }); + + it('does not output non-entrypoints', async () => { + const generatedBundles: GeneratedBundle[] = [ + { + name: 'default', + options: { format: 'es', dir: 'dist' }, + bundle: { + // @ts-ignore + 'app.js': { + isEntry: true, + fileName: 'app.js', + facadeModuleId: '/root/app.js', + type: 'chunk', + }, + // @ts-ignore + 'not-app.js': { + isEntry: false, + fileName: 'not-app.js', + facadeModuleId: '/root/app.js', + type: 'chunk', + }, + }, + }, + ]; + const output = await getEntrypointBundles({ + ...defaultOptions, + generatedBundles, + }); + expect(Object.keys(output).length).to.equal(1); + expect(output.default.entrypoints.length).to.equal(1); + expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['./app.js']); + }); + + it('does not output non-chunks', async () => { + const generatedBundles: GeneratedBundle[] = [ + { + name: 'default', + options: { format: 'es', dir: 'dist' }, + bundle: { + // @ts-ignore + 'app.js': { + isEntry: true, + fileName: 'app.js', + facadeModuleId: '/root/app.js', + type: 'chunk', + }, + // @ts-ignore + 'not-app.js': { + // @ts-ignore + isEntry: true, + fileName: 'not-app.js', + facadeModuleId: '/root/app.js', + type: 'asset', + }, + }, + }, + ]; + const output = await getEntrypointBundles({ + ...defaultOptions, + generatedBundles, + }); + expect(Object.keys(output).length).to.equal(1); + expect(output.default.entrypoints.length).to.equal(1); + expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['./app.js']); + }); + + it('matches on facadeModuleId', async () => { + const generatedBundles: GeneratedBundle[] = [ + { + name: 'default', + options: { format: 'es', dir: 'dist' }, + bundle: { + // @ts-ignore + 'app.js': { + isEntry: true, + fileName: 'app.js', + facadeModuleId: '/root/app.js', + type: 'chunk', + }, + // @ts-ignore + 'not-app.js': { + isEntry: true, + fileName: 'not-app.js', + facadeModuleId: '/root/not-app.js', + type: 'chunk', + }, + }, + }, + ]; + const output = await getEntrypointBundles({ + ...defaultOptions, + generatedBundles, + }); + expect(Object.keys(output).length).to.equal(1); + expect(output.default.entrypoints.length).to.equal(1); + expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['./app.js']); + }); + + it('returns all entrypoints when no input module ids are given', async () => { + const generatedBundles: GeneratedBundle[] = [ + { + name: 'default', + options: { format: 'es', dir: 'dist' }, + bundle: { + // @ts-ignore + 'app.js': { + isEntry: true, + fileName: 'app.js', + facadeModuleId: '/root/app.js', + type: 'chunk', + }, + // @ts-ignore + 'not-app.js': { + isEntry: true, + fileName: 'not-app.js', + facadeModuleId: '/root/not-app.js', + type: 'chunk', + }, + }, + }, + ]; + + const inputModuleIds: ScriptModuleTag[] = [ + { importPath: '/root/app.js' }, + { importPath: '/root/not-app.js' }, + ]; + + const output = await getEntrypointBundles({ + ...defaultOptions, + inputModuleIds, + generatedBundles, + }); + expect(Object.keys(output).length).to.equal(1); + expect(output.default.entrypoints.length).to.equal(2); + expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['./app.js', './not-app.js']); + }); + + it('generates entrypoint for multiple bundles', async () => { + const generatedBundles: GeneratedBundle[] = [ + { + name: 'modern', + options: { format: 'es', dir: 'dist' }, + bundle: { + // @ts-ignore + 'app.js': { + isEntry: true, + fileName: 'app.js', + facadeModuleId: '/root/app.js', + type: 'chunk', + }, + }, + }, + { + name: 'legacy', + options: { format: 'es', dir: 'dist/legacy' }, + bundle: { + // @ts-ignore + 'app.js': { + isEntry: true, + fileName: 'app.js', + facadeModuleId: '/root/app.js', + type: 'chunk', + }, + }, + }, + ]; + + const output = await getEntrypointBundles({ + ...defaultOptions, + generatedBundles, + }); + + expect(Object.keys(output).length).to.equal(2); + expect(output.modern.options).to.equal(generatedBundles[0].options); + expect(output.legacy.options).to.equal(generatedBundles[1].options); + expect(output.modern.bundle).to.equal(generatedBundles[0].bundle); + expect(output.legacy.bundle).to.equal(generatedBundles[1].bundle); + expect(output.modern.entrypoints.length).to.equal(1); + expect(output.modern.entrypoints[0].chunk).to.equal(generatedBundles[0].bundle['app.js']); + expect(output.modern.entrypoints.map(e => e.importPath)).to.eql(['./app.js']); + expect(output.legacy.entrypoints.length).to.equal(1); + expect(output.legacy.entrypoints[0].chunk).to.equal(generatedBundles[1].bundle['app.js']); + expect(output.legacy.entrypoints.map(e => e.importPath)).to.eql(['./legacy/app.js']); + }); + + it('allows configuring a public path', async () => { + const output = await getEntrypointBundles({ + ...defaultOptions, + pluginOptions: { publicPath: '/static' }, + }); + + expect(Object.keys(output).length).to.equal(1); + expect(output.default.entrypoints.length).to.equal(1); + expect(output.default.entrypoints.map(e => e.importPath)).to.eql(['/static/app.js']); + }); +}); diff --git a/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts b/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts new file mode 100644 index 000000000..11cf7a7ed --- /dev/null +++ b/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts @@ -0,0 +1,210 @@ +import { expect } from 'chai'; +import path from 'path'; +import { getOutputHTML, GetOutputHTMLParams } from '../../../src/output/getOutputHTML.js'; +import { EntrypointBundle } from '../../../src/RollupPluginHTMLOptions.js'; + +describe('getOutputHTML()', () => { + const defaultEntrypointBundles: Record = { + default: { + name: 'default', + options: { format: 'es' }, + // @ts-ignore + entrypoints: [{ importPath: '/app.js' }, { importPath: '/module.js' }], + }, + }; + + const defaultOptions: GetOutputHTMLParams = { + pluginOptions: {}, + outputDir: '/', + emittedAssets: { static: new Map(), hashed: new Map() }, + entrypointBundles: defaultEntrypointBundles, + input: { + html: '

Input HTML

', + name: 'index.html', + moduleImports: [], + assets: [], + inlineModules: [], + }, + defaultInjectDisabled: false, + injectServiceWorker: false, + serviceWorkerPath: '', + strictCSPInlineScripts: false, + }; + + it('injects output into the input HTML', async () => { + const output = await getOutputHTML(defaultOptions); + expect(output).to.equal( + '

Input HTML

' + + '' + + '' + + '', + ); + }); + + it('generates a HTML file for multiple rollup bundles', async () => { + const entrypointBundles: Record = { + modern: { + name: 'modern', + options: { format: 'es' }, + // @ts-ignore + entrypoints: [{ importPath: '/app.js' }, { importPath: '/module.js' }], + }, + legacy: { + name: 'legacy', + options: { format: 'system' }, + // @ts-ignore + entrypoints: [{ importPath: '/legacy/app.js' }, { importPath: '/legacy/module.js' }], + }, + }; + + const output = await getOutputHTML({ ...defaultOptions, entrypointBundles }); + expect(output).to.equal( + '

Input HTML

' + + '' + + '' + + '' + + '' + + '', + ); + }); + + it('can transform html output', async () => { + const output = await getOutputHTML({ + ...defaultOptions, + pluginOptions: { + ...defaultOptions.pluginOptions, + transformHtml: html => html.replace('Input HTML', 'Transformed Input HTML'), + }, + }); + + expect(output).to.equal( + '

Transformed Input HTML

' + + '' + + '' + + '', + ); + }); + + it('allows setting multiple html transform functions', async () => { + const output = await getOutputHTML({ + ...defaultOptions, + pluginOptions: { + ...defaultOptions.pluginOptions, + transformHtml: [ + html => html.replace('Input HTML', 'Transformed Input HTML'), + html => html.replace(/h1/g, 'h2'), + ], + }, + }); + + expect(output).to.equal( + '

Transformed Input HTML

' + + '' + + '' + + '', + ); + }); + + it('can combine external and regular transform functions', async () => { + const output = await getOutputHTML({ + ...defaultOptions, + pluginOptions: { + ...defaultOptions.pluginOptions, + transformHtml: html => html.replace('Input HTML', 'Transformed Input HTML'), + }, + externalTransformHtmlFns: [html => html.replace(/h1/g, 'h2')], + }); + + expect(output).to.equal( + '

Transformed Input HTML

' + + '' + + '' + + '', + ); + }); + + it('can disable default injection', async () => { + const output = await getOutputHTML({ + ...defaultOptions, + defaultInjectDisabled: true, + }); + + expect(output).to.equal('

Input HTML

'); + }); + + it('can converts absolute urls to full absolute urls', async () => { + const rootDir = path.resolve(__dirname, '..', '..', 'fixtures', 'assets'); + const hashed = new Map(); + hashed.set(path.join(rootDir, 'image-social.png'), 'image-social-xxx.png'); + + const output = await getOutputHTML({ + ...defaultOptions, + defaultInjectDisabled: true, + pluginOptions: { + absoluteBaseUrl: 'http://test.com', + rootDir, + }, + emittedAssets: { static: new Map(), hashed }, + input: { + ...defaultOptions.input, + html: [ + '', + '', + '', + '', + '', + '', + '', + ].join('\n'), + filePath: path.join(rootDir, 'index.html'), + }, + }); + + expect(output).to.equal( + [ + '', + '', + '', + '', + '', + '', + '', + ].join('\n'), + ); + }); + + it('can minify HTML', async () => { + const htmlInput = ` + + + + + + + + + + `; + const output = await getOutputHTML({ + ...defaultOptions, + pluginOptions: { + ...defaultOptions.pluginOptions, + minify: true, + }, + input: { + ...defaultOptions.input, + html: htmlInput, + }, + }); + + expect(output).to.equal( + '', + ); + }); +}); diff --git a/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts b/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts new file mode 100644 index 000000000..5068d8dc3 --- /dev/null +++ b/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts @@ -0,0 +1,129 @@ +import { getTextContent } from '@web/parse5-utils'; +import { expect } from 'chai'; +import { parse, serialize } from 'parse5'; + +import { injectBundles, createLoadScript } from '../../../src/output/injectBundles.js'; + +describe('createLoadScript()', () => { + it('creates a script for es modules', () => { + // parse5 types are broken + const scriptAst = createLoadScript('./app.js', 'es') as any; + + expect(scriptAst.tagName).to.equal('script'); + expect(scriptAst.attrs).to.eql([ + { name: 'type', value: 'module' }, + { name: 'src', value: './app.js' }, + ]); + }); + + it('creates a script for systemjs', () => { + // parse5 types are broken + const scriptAst = createLoadScript('./app.js', 'system') as any; + + expect(scriptAst.tagName).to.equal('script'); + expect(getTextContent(scriptAst)).to.equal('System.import("./app.js");'); + }); + + it('creates a script for other modules types', () => { + const scriptAst = createLoadScript('./app.js', 'iife') as any; + + expect(scriptAst.tagName).to.equal('script'); + expect(scriptAst.attrs).to.eql([ + { name: 'src', value: './app.js' }, + { name: 'defer', value: '' }, + ]); + }); +}); + +describe('injectBundles()', () => { + it('can inject a single bundle', () => { + const document = parse( + [ + // + '', + '', + '', + '

Hello world

', + '', + '', + ].join(''), + ); + + injectBundles(document, [ + { + options: { format: 'es' }, + entrypoints: [ + { + importPath: 'app.js', + // @ts-ignore + chunk: {}, + }, + ], + }, + ]); + const expected = [ + // + '', + '', + '', + '

Hello world

', + '', + '', + '', + ].join(''); + + expect(serialize(document)).to.eql(expected); + }); + + it('can inject multiple bundles', () => { + const document = parse( + [ + // + '', + '', + '', + '

Hello world

', + '', + '', + ].join(''), + ); + + injectBundles(document, [ + // @ts-ignore + { + options: { format: 'es' }, + entrypoints: [ + { + importPath: './app.js', + // @ts-ignore + chunk: null, + }, + ], + }, + // @ts-ignore + { + options: { format: 'iife' }, + entrypoints: [ + { + importPath: '/scripts/script.js', + // @ts-ignore + chunk: null, + }, + ], + }, + ]); + const expected = [ + // + '', + '', + '', + '

Hello world

', + '', + '', + '', + '', + ].join(''); + + expect(serialize(document)).to.eql(expected); + }); +}); diff --git a/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts b/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts new file mode 100644 index 000000000..85ae406fe --- /dev/null +++ b/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts @@ -0,0 +1,352 @@ +import { expect } from 'chai'; +import path from 'path'; +import { parse, serialize } from 'parse5'; +import { InputData } from '../../../src/input/InputData.js'; + +import { injectedUpdatedAssetPaths } from '../../../src/output/injectedUpdatedAssetPaths.js'; + +describe('injectedUpdatedAssetPaths()', () => { + it('injects updated asset paths', () => { + const document = parse( + [ + '', + '', + '', + '', + '', + '', + '', + '', + ].join(''), + ); + + const input: InputData = { + html: '', + name: 'index.html', + moduleImports: [], + inlineModules: [], + assets: [], + filePath: '/root/index.html', + }; + const hashed = new Map(); + hashed.set(path.join(path.sep, 'root', 'styles.css'), 'styles-xxx.css'); + hashed.set(path.join(path.sep, 'root', 'foo', 'image-a.png'), 'image-a-xxx.png'); + hashed.set(path.join(path.sep, 'root', 'image-b.png'), 'image-b-xxx.png'); + hashed.set(path.join(path.sep, 'root', 'no-module.js'), 'no-module-xxx.js'); + + injectedUpdatedAssetPaths({ + document, + input, + outputDir: '/root/dist/', + rootDir: '/root/', + emittedAssets: { static: new Map(), hashed }, + }); + + const expected = [ + '', + '', + '', + '', + '', + '', + '', + '', + ].join(''); + + expect(serialize(document)).to.eql(expected); + }); + + it('handles a picture tag using source tags with srcset', () => { + const document = parse( + [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + ].join(''), + ); + + const input: InputData = { + html: '', + name: 'index.html', + moduleImports: [], + inlineModules: [], + assets: [], + filePath: '/root/index.html', + }; + const hashed = new Map(); + hashed.set(path.join(path.sep, 'root', 'images', 'eb26e6ca-30.avif'), 'eb26e6ca-30-xxx.avif'); + hashed.set(path.join(path.sep, 'root', 'images', 'eb26e6ca-60.avif'), 'eb26e6ca-60-xxx.avif'); + hashed.set(path.join(path.sep, 'root', 'images', 'eb26e6ca-30.jpeg'), 'eb26e6ca-30-xxx.jpeg'); + hashed.set(path.join(path.sep, 'root', 'images', 'eb26e6ca-60.jpeg'), 'eb26e6ca-60-xxx.jpeg'); + + injectedUpdatedAssetPaths({ + document, + input, + outputDir: '/root/dist/', + rootDir: '/root/', + emittedAssets: { static: new Map(), hashed }, + }); + + const expected = [ + '', + '', + ' ', + ' ', + ' My Image Alternative Text', + ' ', + ].join('\n'); + expect(serialize(document).replace(/ {4}/g, '\n')).to.eql(expected); + }); + + it('handles video tag using source tags with src', () => { + const document = parse( + [ + '', + ' ', + ' ', + ' ', + '', + ].join(''), + ); + + const input: InputData = { + html: '', + name: 'index.html', + moduleImports: [], + inlineModules: [], + assets: [], + filePath: '/root/index.html', + }; + const hashed = new Map(); + hashed.set( + path.join(path.sep, 'root', 'videos', 'typer-hydration.mp4'), + 'typer-hydration-xxx.mp4', + ); + + injectedUpdatedAssetPaths({ + document, + input, + outputDir: '/root/dist/', + rootDir: '/root/', + emittedAssets: { static: new Map(), hashed }, + }); + + const expected = [ + '', + ' ', + ].join('\n'); + expect(serialize(document).replace(/ {4}/g, '\n')).to.eql(expected); + }); + + it('handles virtual files', () => { + const document = parse( + [ + '', + '', + '', + '', + '', + '', + '', + ].join(''), + ); + + const input: InputData = { + html: '', + name: 'index.html', + moduleImports: [], + inlineModules: [], + assets: [], + }; + const hashed = new Map(); + hashed.set(path.join(path.sep, 'root', 'styles.css'), 'styles-xxx.css'); + hashed.set(path.join(path.sep, 'root', 'foo', 'image-a.png'), 'image-a-xxx.png'); + hashed.set(path.join(path.sep, 'root', 'image-b.png'), 'image-b-xxx.png'); + + injectedUpdatedAssetPaths({ + document, + input, + outputDir: '/root/dist/', + rootDir: '/root/', + emittedAssets: { static: new Map(), hashed }, + }); + const expected = [ + '', + '', + '', + '', + '', + '', + '', + ].join(''); + + expect(serialize(document)).to.eql(expected); + }); + + it('handles HTML files in a sub directory', () => { + const document = parse( + [ + '', + '', + '', + '', + '', + '', + '', + ].join(''), + ); + + const input: InputData = { + html: '', + name: 'foo/index.html', + moduleImports: [], + inlineModules: [], + assets: [], + filePath: '/root/foo/index.html', + }; + const hashed = new Map(); + hashed.set(path.join(path.sep, 'root', 'styles.css'), 'styles-xxx.css'); + hashed.set(path.join(path.sep, 'root', 'foo', 'image-a.png'), 'image-a-xxx.png'); + hashed.set(path.join(path.sep, 'root', 'image-b.png'), 'image-b-xxx.png'); + + injectedUpdatedAssetPaths({ + document, + input, + outputDir: '/root/dist/', + rootDir: '/root/', + emittedAssets: { static: new Map(), hashed }, + }); + + const expected = [ + '', + '', + '', + '', + '', + '', + '', + ].join(''); + + expect(serialize(document)).to.eql(expected); + }); + + it('handles virtual HTML files in a sub directory', () => { + const document = parse( + [ + '', + '', + '', + '', + '', + '', + '', + ].join(''), + ); + + const input: InputData = { + html: '', + name: 'foo/index.html', + moduleImports: [], + inlineModules: [], + assets: [], + }; + const hashed = new Map(); + hashed.set(path.join(path.sep, 'root', 'styles.css'), 'styles-xxx.css'); + hashed.set(path.join(path.sep, 'root', 'foo', 'image-a.png'), 'image-a-xxx.png'); + hashed.set(path.join(path.sep, 'root', 'image-b.png'), 'image-b-xxx.png'); + + injectedUpdatedAssetPaths({ + document, + input, + outputDir: '/root/dist/', + rootDir: '/root/', + emittedAssets: { static: new Map(), hashed }, + }); + + const expected = [ + '', + '', + '', + '', + '', + '', + '', + ].join(''); + + expect(serialize(document)).to.eql(expected); + }); + + it('prefixes a publicpath', () => { + const document = parse( + [ + '', + '', + '', + '', + '', + '', + '', + ].join(''), + ); + + const input: InputData = { + html: '', + name: 'index.html', + moduleImports: [], + inlineModules: [], + assets: [], + filePath: '/root/index.html', + }; + const hashed = new Map(); + hashed.set(path.join(path.sep, 'root', 'styles.css'), 'styles-xxx.css'); + hashed.set(path.join(path.sep, 'root', 'foo', 'image-a.png'), 'image-a-xxx.png'); + hashed.set(path.join(path.sep, 'root', 'image-b.png'), 'image-b-xxx.png'); + + injectedUpdatedAssetPaths({ + document, + input, + outputDir: '/root/dist/', + rootDir: '/root/', + emittedAssets: { static: new Map(), hashed }, + publicPath: './public/', + }); + + const expected = [ + '', + '', + '', + '', + '', + '', + '', + ].join(''); + + expect(serialize(document)).to.eql(expected); + }); +}); diff --git a/packages/rollup-plugin-html/test/utils.ts b/packages/rollup-plugin-html/test/utils.ts new file mode 100644 index 000000000..e34ab607b --- /dev/null +++ b/packages/rollup-plugin-html/test/utils.ts @@ -0,0 +1,102 @@ +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 = {}; + + 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; + if (typeof code !== 'string' && filename.endsWith('.css')) { + code = Buffer.from(code).toString('utf8'); + } + if (typeof code === 'string' && formatter) { + code = formatter(code); + } + assets[filename] = code; + } + } + + return { output, chunks, assets }; +} + +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 }); + } +} From a07c36efd243c252d0dcd38e0477a425ece48bf8 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Wed, 24 Dec 2025 16:04:12 +0400 Subject: [PATCH 18/21] WIP18 --- packages/rollup-plugin-html/src/output/emitAssets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rollup-plugin-html/src/output/emitAssets.ts b/packages/rollup-plugin-html/src/output/emitAssets.ts index 0a1b0b4ba..91c51df57 100644 --- a/packages/rollup-plugin-html/src/output/emitAssets.ts +++ b/packages/rollup-plugin-html/src/output/emitAssets.ts @@ -110,7 +110,7 @@ export async function emitAssets( const basename = path.basename(filePath); const fileRef = this.emitFile({ type: 'asset', - name: extractAssetsLegacyCss ? path.join('assets', basename) : basename, + name: extractAssetsLegacyCss ? `assets/${basename}` : basename, source: assetContent, }); const emittedAssetFilepath = this.getFileName(fileRef); From 72ea214ad7e70d31559e4adfe0cd172dc34adb06 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Wed, 24 Dec 2025 16:40:40 +0400 Subject: [PATCH 19/21] WIP19 --- .../rollup-plugin-html/src/output/createHTMLOutput.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/rollup-plugin-html/src/output/createHTMLOutput.ts b/packages/rollup-plugin-html/src/output/createHTMLOutput.ts index cb2224267..aa68f4da6 100644 --- a/packages/rollup-plugin-html/src/output/createHTMLOutput.ts +++ b/packages/rollup-plugin-html/src/output/createHTMLOutput.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import { getEntrypointBundles } from './getEntrypointBundles.js'; import { getOutputHTML } from './getOutputHTML.js'; import { createError } from '../utils.js'; @@ -65,7 +66,14 @@ export async function createHTMLAsset(params: CreateHTMLAssetParams): Promise Date: Wed, 24 Dec 2025 16:51:26 +0400 Subject: [PATCH 20/21] WIP20 --- .../test/src/rollupPluginPolyfillsLoader.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rollup-plugin-polyfills-loader/test/src/rollupPluginPolyfillsLoader.test.ts b/packages/rollup-plugin-polyfills-loader/test/src/rollupPluginPolyfillsLoader.test.ts index 3460565fd..65e86d6b3 100644 --- a/packages/rollup-plugin-polyfills-loader/test/src/rollupPluginPolyfillsLoader.test.ts +++ b/packages/rollup-plugin-polyfills-loader/test/src/rollupPluginPolyfillsLoader.test.ts @@ -167,7 +167,7 @@ describe('rollup-plugin-polyfills-loader', function describe() { await testSnapshot({ name: 'non-flattened', - fileName: path.normalize(`non-flat/index.html`), + fileName: 'non-flat/index.html', inputOptions, outputOptions: defaultOutputOptions, }); From b00f8fb50c0781c2b039d0ba7271730f89ad3006 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Wed, 24 Dec 2025 17:10:43 +0400 Subject: [PATCH 21/21] WIP21 --- .../src/input/extract/extractModules.test.ts | 208 ++++++---- .../test/src/output/getOutputHTML.test.ts | 144 ++++--- .../test/src/output/injectBundles.test.ts | 85 ++-- .../output/injectedUpdatedAssetPaths.test.ts | 370 ++++++++++-------- 4 files changed, 467 insertions(+), 340 deletions(-) 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 11fd545b5..f560f339e 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,19 +1,28 @@ import path from 'path'; import { parse, serialize } from 'parse5'; import { expect } from 'chai'; +import { html, js } from '../../../utils.js'; import { extractModules } from '../../../../src/input/extract/extractModules.js'; +import { ScriptModuleTag } from '../../../../src/RollupPluginHTMLOptions.js'; const { sep } = path; +function cleanupInlineModules(modules: ScriptModuleTag[]) { + return modules.map(module => ({ + ...module, + code: module.code ? js`${module.code}` : undefined, + })); +} + describe('extractModules()', () => { it('extracts all modules from a html document', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); + const document = parse(html` +
before
+ + +
after
+ `); const { moduleImports, inlineModules } = extractModules({ document, @@ -27,18 +36,24 @@ describe('extractModules()', () => { { importPath: `${sep}foo.js`, attributes: [] }, { importPath: `${sep}bar.js`, attributes: [] }, ]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); + expect(html`${htmlWithoutModules}`).to.eql(html` + + + +
before
+
after
+ + + `); }); it('does not touch non module scripts', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); + const document = parse(html` +
before
+ + +
after
+ `); const { moduleImports, inlineModules } = extractModules({ document, @@ -49,18 +64,26 @@ describe('extractModules()', () => { expect(inlineModules.length).to.equal(0); expect(moduleImports).to.eql([]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); + expect(html`${htmlWithoutModules}`).to.eql(html` + + + +
before
+ + +
after
+ + + `); }); it('resolves imports relative to the root dir', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); + const document = parse(html` +
before
+ + +
after
+ `); const { moduleImports, inlineModules } = extractModules({ document, @@ -74,18 +97,24 @@ describe('extractModules()', () => { { importPath: `${sep}foo.js`, attributes: [] }, { importPath: `${sep}base${sep}bar.js`, attributes: [] }, ]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); + expect(html`${htmlWithoutModules}`).to.eql(html` + + + +
before
+
after
+ + + `); }); it('resolves relative imports relative to the relative import base', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); + const document = parse(html` +
before
+ + +
after
+ `); const { moduleImports, inlineModules } = extractModules({ document, @@ -99,18 +128,28 @@ describe('extractModules()', () => { { importPath: `${sep}base-1${sep}base-2${sep}foo.js`, attributes: [] }, { importPath: `${sep}base-1${sep}bar.js`, attributes: [] }, ]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); + expect(html`${htmlWithoutModules}`).to.eql(html` + + + +
before
+
after
+ + + `); }); it('extracts all inline modules from a html document', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); + const document = parse(html` +
before
+ + +
after
+ `); const { moduleImports, inlineModules } = extractModules({ document, @@ -119,31 +158,41 @@ describe('extractModules()', () => { }); const htmlWithoutModules = serialize(document); - expect(inlineModules).to.eql([ + expect(cleanupInlineModules(inlineModules)).to.eql([ { - importPath: '/inline-module-cce79ce714e2c3b250afef32e61fb003.js', - code: '/* my module 1 */', + importPath: '/inline-module-80efb22c2d1ce27c40eae10611f7680f.js', + code: js`/* my module 1 */`, attributes: [], }, { - importPath: '/inline-module-d9a0918508784903d131c7c4eb98e424.js', - code: '/* my module 2 */', + importPath: '/inline-module-b8a73bff59b998da13ce8a6801934f77.js', + code: js`/* my module 2 */`, attributes: [], }, ]); expect(moduleImports).to.eql([]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); + expect(html`${htmlWithoutModules}`).to.eql(html` + + + +
before
+
after
+ + + `); }); it('prefixes inline module with index.html directory', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); + const document = parse(html` +
before
+ + +
after
+ `); const { moduleImports, inlineModules } = extractModules({ document, @@ -152,31 +201,37 @@ describe('extractModules()', () => { }); const htmlWithoutModules = serialize(document); - expect(inlineModules).to.eql([ + expect(cleanupInlineModules(inlineModules)).to.eql([ { - importPath: '/foo/bar/inline-module-cce79ce714e2c3b250afef32e61fb003.js', - code: '/* my module 1 */', + importPath: '/foo/bar/inline-module-80efb22c2d1ce27c40eae10611f7680f.js', + code: js`/* my module 1 */`, attributes: [], }, { - importPath: '/foo/bar/inline-module-d9a0918508784903d131c7c4eb98e424.js', - code: '/* my module 2 */', + importPath: '/foo/bar/inline-module-b8a73bff59b998da13ce8a6801934f77.js', + code: js`/* my module 2 */`, attributes: [], }, ]); expect(moduleImports).to.eql([]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); + expect(html`${htmlWithoutModules}`).to.eql(html` + + + +
before
+
after
+ + + `); }); it('ignores absolute paths', () => { - const document = parse( - '
before
' + - '' + - '' + - '
after
', - ); + const document = parse(html` +
before
+ + +
after
+ `); const { moduleImports, inlineModules } = extractModules({ document, @@ -187,8 +242,15 @@ describe('extractModules()', () => { expect(inlineModules.length).to.equal(0); expect(moduleImports).to.eql([{ importPath: `${sep}bar.js`, attributes: [] }]); - expect(htmlWithoutModules).to.eql( - '
before
after
', - ); + expect(html`${htmlWithoutModules}`).to.eql(html` + + + +
before
+ +
after
+ + + `); }); }); 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 11cf7a7ed..024635e43 100644 --- a/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts +++ b/packages/rollup-plugin-html/test/src/output/getOutputHTML.test.ts @@ -2,6 +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'; describe('getOutputHTML()', () => { const defaultEntrypointBundles: Record = { @@ -19,7 +20,7 @@ describe('getOutputHTML()', () => { emittedAssets: { static: new Map(), hashed: new Map() }, entrypointBundles: defaultEntrypointBundles, input: { - html: '

Input HTML

', + html: html`

Input HTML

`, name: 'index.html', moduleImports: [], assets: [], @@ -33,12 +34,16 @@ describe('getOutputHTML()', () => { it('injects output into the input HTML', async () => { const output = await getOutputHTML(defaultOptions); - expect(output).to.equal( - '

Input HTML

' + - '' + - '' + - '', - ); + expect(html`${output}`).to.equal(html` + + + +

Input HTML

+ + + + + `); }); it('generates a HTML file for multiple rollup bundles', async () => { @@ -58,14 +63,22 @@ describe('getOutputHTML()', () => { }; const output = await getOutputHTML({ ...defaultOptions, entrypointBundles }); - expect(output).to.equal( - '

Input HTML

' + - '' + - '' + - '' + - '' + - '', - ); + expect(html`${output}`).to.equal(html` + + + +

Input HTML

+ + + + + + + `); }); it('can transform html output', async () => { @@ -77,12 +90,16 @@ describe('getOutputHTML()', () => { }, }); - expect(output).to.equal( - '

Transformed Input HTML

' + - '' + - '' + - '', - ); + expect(html`${output}`).to.equal(html` + + + +

Transformed Input HTML

+ + + + + `); }); it('allows setting multiple html transform functions', async () => { @@ -97,12 +114,16 @@ describe('getOutputHTML()', () => { }, }); - expect(output).to.equal( - '

Transformed Input HTML

' + - '' + - '' + - '', - ); + expect(html`${output}`).to.equal(html` + + + +

Transformed Input HTML

+ + + + + `); }); it('can combine external and regular transform functions', async () => { @@ -115,12 +136,16 @@ describe('getOutputHTML()', () => { externalTransformHtmlFns: [html => html.replace(/h1/g, 'h2')], }); - expect(output).to.equal( - '

Transformed Input HTML

' + - '' + - '' + - '', - ); + expect(html`${output}`).to.equal(html` + + + +

Transformed Input HTML

+ + + + + `); }); it('can disable default injection', async () => { @@ -129,7 +154,14 @@ describe('getOutputHTML()', () => { defaultInjectDisabled: true, }); - expect(output).to.equal('

Input HTML

'); + expect(html`${output}`).to.equal(html` + + + +

Input HTML

+ + + `); }); it('can converts absolute urls to full absolute urls', async () => { @@ -147,30 +179,34 @@ describe('getOutputHTML()', () => { emittedAssets: { static: new Map(), hashed }, input: { ...defaultOptions.input, - html: [ - '', - '', - '', - '', - '', - '', - '', - ].join('\n'), + html: html` + + + + + + + + + + + `, filePath: path.join(rootDir, 'index.html'), }, }); - expect(output).to.equal( - [ - '', - '', - '', - '', - '', - '', - '', - ].join('\n'), - ); + expect(html`${output}`).to.equal(html` + + + + + + + + + + + `); }); it('can minify HTML', async () => { 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 5068d8dc3..fb52cda0c 100644 --- a/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts +++ b/packages/rollup-plugin-html/test/src/output/injectBundles.test.ts @@ -1,6 +1,7 @@ import { getTextContent } from '@web/parse5-utils'; import { expect } from 'chai'; import { parse, serialize } from 'parse5'; +import { html } from '../../utils.js'; import { injectBundles, createLoadScript } from '../../../src/output/injectBundles.js'; @@ -37,17 +38,14 @@ describe('createLoadScript()', () => { describe('injectBundles()', () => { it('can inject a single bundle', () => { - const document = parse( - [ - // - '', - '', - '', - '

Hello world

', - '', - '', - ].join(''), - ); + const document = parse(html` + + + +

Hello world

+ + + `); injectBundles(document, [ { @@ -61,32 +59,29 @@ describe('injectBundles()', () => { ], }, ]); - const expected = [ - // - '', - '', - '', - '

Hello world

', - '', - '', - '', - ].join(''); - expect(serialize(document)).to.eql(expected); + const htmlWithBundles = serialize(document); + + expect(html`${htmlWithBundles}`).to.eql(html` + + + +

Hello world

+ + + + `); }); it('can inject multiple bundles', () => { - const document = parse( - [ - // - '', - '', - '', - '

Hello world

', - '', - '', - ].join(''), - ); + const document = parse(html` + + + +

Hello world

+ + + `); injectBundles(document, [ // @ts-ignore @@ -112,18 +107,18 @@ describe('injectBundles()', () => { ], }, ]); - const expected = [ - // - '', - '', - '', - '

Hello world

', - '', - '', - '', - '', - ].join(''); - expect(serialize(document)).to.eql(expected); + const htmlWithBundles = serialize(document); + + expect(html`${htmlWithBundles}`).to.eql(html` + + + +

Hello world

+ + + + + `); }); }); 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 85ae406fe..9e720b157 100644 --- a/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts +++ b/packages/rollup-plugin-html/test/src/output/injectedUpdatedAssetPaths.test.ts @@ -1,24 +1,25 @@ import { expect } from 'chai'; import path from 'path'; import { parse, serialize } from 'parse5'; +import { html } from '../../utils.js'; import { InputData } from '../../../src/input/InputData.js'; import { injectedUpdatedAssetPaths } from '../../../src/output/injectedUpdatedAssetPaths.js'; describe('injectedUpdatedAssetPaths()', () => { it('injects updated asset paths', () => { - const document = parse( - [ - '', - '', - '', - '', - '', - '', - '', - '', - ].join(''), - ); + const document = parse(html` + + + + + + + + + + + `); const input: InputData = { html: '', @@ -42,50 +43,50 @@ describe('injectedUpdatedAssetPaths()', () => { emittedAssets: { static: new Map(), hashed }, }); - const expected = [ - '', - '', - '', - '', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); + const result = serialize(document); + + expect(html`${result}`).to.eql(html` + + + + + + + + + + + `); }); it('handles a picture tag using source tags with srcset', () => { - const document = parse( - [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '', - ].join(''), - ); + const document = parse(html` + + + + + + My Image Alternative Text + + + + `); const input: InputData = { html: '', @@ -109,29 +110,48 @@ describe('injectedUpdatedAssetPaths()', () => { emittedAssets: { static: new Map(), hashed }, }); - const expected = [ - '', - '', - ' ', - ' ', - ' My Image Alternative Text', - ' ', - ].join('\n'); - expect(serialize(document).replace(/ {4}/g, '\n')).to.eql(expected); + const result = serialize(document); + + expect(html`${result}`).to.eql(html` + + + + + + + My Image Alternative Text + + + + `); }); it('handles video tag using source tags with src', () => { - const document = parse( - [ - '', - ' ', - ' ', - ' ', - '', - ].join(''), - ); + const document = parse(html` + + + + + + `); const input: InputData = { html: '', @@ -155,27 +175,32 @@ describe('injectedUpdatedAssetPaths()', () => { emittedAssets: { static: new Map(), hashed }, }); - const expected = [ - '', - ' ', - ].join('\n'); - expect(serialize(document).replace(/ {4}/g, '\n')).to.eql(expected); + const result = serialize(document); + + expect(html`${result}`).to.eql(html` + + + + + + + `); }); it('handles virtual files', () => { - const document = parse( - [ - '', - '', - '', - '', - '', - '', - '', - ].join(''), - ); + const document = parse(html` + + + + + + + + + + `); const input: InputData = { html: '', @@ -196,31 +221,34 @@ describe('injectedUpdatedAssetPaths()', () => { rootDir: '/root/', emittedAssets: { static: new Map(), hashed }, }); - const expected = [ - '', - '', - '', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); + + const result = serialize(document); + + expect(html`${result}`).to.eql(html` + + + + + + + + + + `); }); it('handles HTML files in a sub directory', () => { - const document = parse( - [ - '', - '', - '', - '', - '', - '', - '', - ].join(''), - ); + const document = parse(html` + + + + + + + + + + `); const input: InputData = { html: '', @@ -243,31 +271,33 @@ describe('injectedUpdatedAssetPaths()', () => { emittedAssets: { static: new Map(), hashed }, }); - const expected = [ - '', - '', - '', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); + const result = serialize(document); + + expect(html`${result}`).to.eql(html` + + + + + + + + + + `); }); it('handles virtual HTML files in a sub directory', () => { - const document = parse( - [ - '', - '', - '', - '', - '', - '', - '', - ].join(''), - ); + const document = parse(html` + + + + + + + + + + `); const input: InputData = { html: '', @@ -289,31 +319,33 @@ describe('injectedUpdatedAssetPaths()', () => { emittedAssets: { static: new Map(), hashed }, }); - const expected = [ - '', - '', - '', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); + const result = serialize(document); + + expect(html`${result}`).to.eql(html` + + + + + + + + + + `); }); it('prefixes a publicpath', () => { - const document = parse( - [ - '', - '', - '', - '', - '', - '', - '', - ].join(''), - ); + const document = parse(html` + + + + + + + + + + `); const input: InputData = { html: '', @@ -337,16 +369,18 @@ describe('injectedUpdatedAssetPaths()', () => { publicPath: './public/', }); - const expected = [ - '', - '', - '', - '', - '', - '', - '', - ].join(''); - - expect(serialize(document)).to.eql(expected); + const result = serialize(document); + + expect(html`${result}`).to.eql(html` + + + + + + + + + + `); }); });