From fbdc10f90f7baa5c49f2f53e3e4ce8f453814c01 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:43:41 +0100 Subject: [PATCH] fix(assets): Fixes assets generation when using custom paths and configs (#10567) * fix(assets): Fixes assets generation when using custom paths and configs * fix: tests * fix: make sure remote files don't end up in nested folders by accident * test: add a whole bunch of tests * chore: changeset --- .changeset/few-avocados-notice.md | 5 + packages/astro/e2e/dev-toolbar-audits.test.js | 2 +- packages/astro/src/assets/build/generate.ts | 11 +- packages/astro/src/assets/internal.ts | 12 +- packages/astro/src/assets/types.ts | 4 +- .../astro/src/assets/utils/transformToPath.ts | 13 +- .../astro/src/assets/vite-plugin-assets.ts | 31 ++- ...core-image-unconventional-settings.test.js | 187 ++++++++++++++++++ .../package.json | 8 + .../src/assets/light_walrus.avif | Bin 0 -> 19439 bytes .../src/pages/index.astro | 7 + .../tsconfig.json | 9 + packages/astro/test/image-deletion.test.js | 2 +- packages/internal-helpers/src/path.ts | 7 + pnpm-lock.yaml | 6 + 15 files changed, 272 insertions(+), 32 deletions(-) create mode 100644 .changeset/few-avocados-notice.md create mode 100644 packages/astro/test/core-image-unconventional-settings.test.js create mode 100644 packages/astro/test/fixtures/core-image-unconventional-settings/package.json create mode 100644 packages/astro/test/fixtures/core-image-unconventional-settings/src/assets/light_walrus.avif create mode 100644 packages/astro/test/fixtures/core-image-unconventional-settings/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/core-image-unconventional-settings/tsconfig.json diff --git a/.changeset/few-avocados-notice.md b/.changeset/few-avocados-notice.md new file mode 100644 index 0000000000..72c8ba2499 --- /dev/null +++ b/.changeset/few-avocados-notice.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Fixes `astro:assets` not working when using complex config with `vite.build.rollupOptions.output.assetFileNames` diff --git a/packages/astro/e2e/dev-toolbar-audits.test.js b/packages/astro/e2e/dev-toolbar-audits.test.js index 5066376e45..9a628a1c06 100644 --- a/packages/astro/e2e/dev-toolbar-audits.test.js +++ b/packages/astro/e2e/dev-toolbar-audits.test.js @@ -44,7 +44,7 @@ test.describe('Dev Toolbar - Audits', () => { await appButton.click(); }); - test('can handle mutations zzz', async ({ page, astro }) => { + test('can handle mutations', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/audits-mutations')); const toolbar = page.locator('astro-dev-toolbar'); diff --git a/packages/astro/src/assets/build/generate.ts b/packages/astro/src/assets/build/generate.ts index 496dc1e6e4..f209345387 100644 --- a/packages/astro/src/assets/build/generate.ts +++ b/packages/astro/src/assets/build/generate.ts @@ -1,5 +1,5 @@ import fs, { readFileSync } from 'node:fs'; -import { basename, join } from 'node:path/posix'; +import { basename } from 'node:path/posix'; import { dim, green } from 'kleur/colors'; import type PQueue from 'p-queue'; import type { AstroConfig } from '../../@types/astro.js'; @@ -9,7 +9,7 @@ import { getTimeStat } from '../../core/build/util.js'; import { AstroError } from '../../core/errors/errors.js'; import { AstroErrorData } from '../../core/errors/index.js'; import type { Logger } from '../../core/logger/core.js'; -import { isRemotePath, prependForwardSlash } from '../../core/path.js'; +import { isRemotePath, removeLeadingForwardSlash } from '../../core/path.js'; import { isServerLikeOutput } from '../../prerender/utils.js'; import type { MapValue } from '../../type-utils.js'; import { getConfiguredImageService } from '../internal.js'; @@ -89,10 +89,7 @@ export async function prepareAssetsGenerationEnv( } function getFullImagePath(originalFilePath: string, env: AssetEnv): URL { - return new URL( - '.' + prependForwardSlash(join(env.assetsFolder, basename(originalFilePath))), - env.serverRoot - ); + return new URL(removeLeadingForwardSlash(originalFilePath), env.serverRoot); } export async function generateImagesForPath( @@ -115,7 +112,7 @@ export async function generateImagesForPath( // For instance, the same image could be referenced in both a server-rendered page and build-time-rendered page if ( !env.isSSR && - !isRemotePath(originalFilePath) && + transformsAndPath.originalSrcPath && !globalThis.astroAsset.referencedImages?.has(transformsAndPath.originalSrcPath) ) { try { diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts index 5cd3cc7bf5..6a3becde3d 100644 --- a/packages/astro/src/assets/internal.ts +++ b/packages/astro/src/assets/internal.ts @@ -74,9 +74,9 @@ export async function getImage( } } - const originalPath = isESMImportedImage(resolvedOptions.src) + const originalFilePath = isESMImportedImage(resolvedOptions.src) ? resolvedOptions.src.fsPath - : resolvedOptions.src; + : undefined; // Only set for ESM imports, where we do have a file path // Clone the `src` object if it's an ESM import so that we don't refer to any properties of the original object // Causing our generate step to think the image is used outside of the image optimization pipeline @@ -112,10 +112,14 @@ export async function getImage( !(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src) ) { const propsToHash = service.propertiesToHash ?? DEFAULT_HASH_PROPS; - imageURL = globalThis.astroAsset.addStaticImage(validatedOptions, propsToHash, originalPath); + imageURL = globalThis.astroAsset.addStaticImage( + validatedOptions, + propsToHash, + originalFilePath + ); srcSets = srcSetTransforms.map((srcSet) => ({ transform: srcSet.transform, - url: globalThis.astroAsset.addStaticImage!(srcSet.transform, propsToHash, originalPath), + url: globalThis.astroAsset.addStaticImage!(srcSet.transform, propsToHash, originalFilePath), descriptor: srcSet.descriptor, attributes: srcSet.attributes, })); diff --git a/packages/astro/src/assets/types.ts b/packages/astro/src/assets/types.ts index b77180e209..29cf1ef9ee 100644 --- a/packages/astro/src/assets/types.ts +++ b/packages/astro/src/assets/types.ts @@ -11,7 +11,7 @@ export type ImageOutputFormat = (typeof VALID_OUTPUT_FORMATS)[number] | (string export type AssetsGlobalStaticImagesList = Map< string, { - originalSrcPath: string; + originalSrcPath: string | undefined; transforms: Map; } >; @@ -21,7 +21,7 @@ declare global { var astroAsset: { imageService?: ImageService; addStaticImage?: - | ((options: ImageTransform, hashProperties: string[], fsPath: string) => string) + | ((options: ImageTransform, hashProperties: string[], fsPath: string | undefined) => string) | undefined; staticImages?: AssetsGlobalStaticImagesList; referencedImages?: Set; diff --git a/packages/astro/src/assets/utils/transformToPath.ts b/packages/astro/src/assets/utils/transformToPath.ts index 0cac1a6715..9a91098cc8 100644 --- a/packages/astro/src/assets/utils/transformToPath.ts +++ b/packages/astro/src/assets/utils/transformToPath.ts @@ -1,19 +1,18 @@ -import { basename, extname } from 'node:path'; +import { basename, dirname, extname } from 'node:path'; import { deterministicString } from 'deterministic-object-hash'; import { removeQueryString } from '../../core/path.js'; import { shorthash } from '../../runtime/server/shorthash.js'; import type { ImageTransform } from '../types.js'; import { isESMImportedImage } from './imageKind.js'; -export function propsToFilename(transform: ImageTransform, hash: string) { - let filename = removeQueryString( - isESMImportedImage(transform.src) ? transform.src.src : transform.src - ); +export function propsToFilename(filePath: string, transform: ImageTransform, hash: string) { + let filename = decodeURIComponent(removeQueryString(filePath)); const ext = extname(filename); - filename = decodeURIComponent(basename(filename, ext)); + filename = basename(filename, ext); + const prefixDirname = isESMImportedImage(transform.src) ? dirname(filePath) : ''; let outputExt = transform.format ? `.${transform.format}` : ext; - return `/${filename}_${hash}${outputExt}`; + return decodeURIComponent(`${prefixDirname}/${filename}_${hash}${outputExt}`); } export function hashTransform( diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index 563874a561..fe92b2538e 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -9,6 +9,7 @@ import { appendForwardSlash, joinPaths, prependForwardSlash, + removeBase, removeQueryString, } from '../core/path.js'; import { isServerLikeOutput } from '../prerender/utils.js'; @@ -91,7 +92,7 @@ export default function assets({ return; } - globalThis.astroAsset.addStaticImage = (options, hashProperties, originalPath) => { + globalThis.astroAsset.addStaticImage = (options, hashProperties, originalFSPath) => { if (!globalThis.astroAsset.staticImages) { globalThis.astroAsset.staticImages = new Map< string, @@ -102,13 +103,18 @@ export default function assets({ >(); } - // Rollup will copy the file to the output directory, this refer to this final path, not to the original path + // Rollup will copy the file to the output directory, as such this is the path in the output directory, including the asset prefix / base const ESMImportedImageSrc = isESMImportedImage(options.src) ? options.src.src : options.src; const fileExtension = extname(ESMImportedImageSrc); - const pf = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix); - const finalOriginalImagePath = ESMImportedImageSrc.replace(pf, ''); + const assetPrefix = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix); + + // This is the path to the original image, from the dist root, without the base or the asset prefix (e.g. /_astro/image.hash.png) + const finalOriginalPath = removeBase( + removeBase(ESMImportedImageSrc, settings.config.base), + assetPrefix + ); const hash = hashTransform( options, @@ -117,21 +123,26 @@ export default function assets({ ); let finalFilePath: string; - let transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalImagePath); + let transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath); let transformForHash = transformsForPath?.transforms.get(hash); + + // If the same image has already been transformed with the same options, we'll reuse the final path if (transformsForPath && transformForHash) { finalFilePath = transformForHash.finalPath; } else { finalFilePath = prependForwardSlash( - joinPaths(settings.config.build.assets, propsToFilename(options, hash)) + joinPaths( + isESMImportedImage(options.src) ? '' : settings.config.build.assets, + prependForwardSlash(propsToFilename(finalOriginalPath, options, hash)) + ) ); if (!transformsForPath) { - globalThis.astroAsset.staticImages.set(finalOriginalImagePath, { - originalSrcPath: originalPath, + globalThis.astroAsset.staticImages.set(finalOriginalPath, { + originalSrcPath: originalFSPath, transforms: new Map(), }); - transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalImagePath)!; + transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath)!; } transformsForPath.transforms.set(hash, { @@ -143,7 +154,7 @@ export default function assets({ // The paths here are used for URLs, so we need to make sure they have the proper format for an URL // (leading slash, prefixed with the base / assets prefix, encoded, etc) if (settings.config.build.assetsPrefix) { - return encodeURI(joinPaths(pf, finalFilePath)); + return encodeURI(joinPaths(assetPrefix, finalFilePath)); } else { return encodeURI(prependForwardSlash(joinPaths(settings.config.base, finalFilePath))); } diff --git a/packages/astro/test/core-image-unconventional-settings.test.js b/packages/astro/test/core-image-unconventional-settings.test.js new file mode 100644 index 0000000000..9ecf6aab6b --- /dev/null +++ b/packages/astro/test/core-image-unconventional-settings.test.js @@ -0,0 +1,187 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; + +import { testImageService } from './test-image-service.js'; +import { loadFixture } from './test-utils.js'; + +/** + ** @typedef {import('../src/@types/astro').AstroInlineConfig & { root?: string | URL }} AstroInlineConfig + */ + +/** @type {AstroInlineConfig} */ +const defaultSettings = { + root: './fixtures/core-image-unconventional-settings/', + image: { + service: testImageService(), + }, +}; + +describe('astro:assets - Support unconventional build settings properly', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + it('supports assetsPrefix', async () => { + fixture = await loadFixture({ + ...defaultSettings, + build: { + assetsPrefix: 'https://cdn.example.com/', + }, + }); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + const src = $('#walrus-img').attr('src'); + assert.equal(src.startsWith('https://cdn.example.com/'), true); + + const data = await fixture.readFile(src.replace('https://cdn.example.com/', ''), null); + assert.equal(data instanceof Buffer, true); + }); + + it('supports base', async () => { + fixture = await loadFixture({ + ...defaultSettings, + build: { + base: '/subdir/', + }, + }); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + const src = $('#walrus-img').attr('src'); + const data = await fixture.readFile(src.replace('/subdir/', ''), null); + assert.equal(data instanceof Buffer, true); + }); + + // This test is a bit of a stretch, but it's a good sanity check, `assetsPrefix` should take precedence over `base` in this context + it('supports assetsPrefix + base', async () => { + fixture = await loadFixture({ + ...defaultSettings, + build: { + assetsPrefix: 'https://cdn.example.com/', + base: '/subdir/', + }, + }); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + const src = $('#walrus-img').attr('src'); + assert.equal(src.startsWith('https://cdn.example.com/'), true); + + const data = await fixture.readFile(src.replace('https://cdn.example.com/', ''), null); + assert.equal(data instanceof Buffer, true); + }); + + it('supports custom build.assets', async () => { + fixture = await loadFixture({ + ...defaultSettings, + build: { + assets: 'assets', + }, + }); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + + const unoptimizedSrc = $('#walrus-img-unoptimized').attr('src'); + assert.equal(unoptimizedSrc.startsWith('/assets/'), true); + + const src = $('#walrus-img').attr('src'); + const data = await fixture.readFile(src, null); + + assert.equal(data instanceof Buffer, true); + }); + + it('supports custom vite.build.rollupOptions.output.assetFileNames', async () => { + fixture = await loadFixture({ + ...defaultSettings, + vite: { + build: { + rollupOptions: { + output: { + assetFileNames: 'images/hello_[name].[ext]', + }, + }, + }, + }, + }); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + const unoptimizedSrc = $('#walrus-img-unoptimized').attr('src'); + assert.equal(unoptimizedSrc, '/images/hello_light_walrus.avif'); + + const src = $('#walrus-img').attr('src'); + const data = await fixture.readFile(src, null); + + assert.equal(data instanceof Buffer, true); + }); + + it('supports complex vite.build.rollupOptions.output.assetFileNames', async () => { + fixture = await loadFixture({ + ...defaultSettings, + vite: { + build: { + rollupOptions: { + output: { + assetFileNames: 'assets/[hash]/[name][extname]', + }, + }, + }, + }, + }); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + const unoptimizedSrc = $('#walrus-img-unoptimized').attr('src'); + const originalData = await fixture.readFile(unoptimizedSrc, null); + assert.equal(originalData instanceof Buffer, true); + + const src = $('#walrus-img').attr('src'); + const data = await fixture.readFile(src, null); + + assert.equal(data instanceof Buffer, true); + }); + + it('supports custom vite.build.rollupOptions.output.assetFileNames with assetsPrefix', async () => { + fixture = await loadFixture({ + ...defaultSettings, + vite: { + build: { + rollupOptions: { + output: { + assetFileNames: 'images/hello_[name].[ext]', + }, + }, + }, + }, + build: { + assetsPrefix: 'https://cdn.example.com/', + }, + }); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + const unoptimizedSrc = $('#walrus-img-unoptimized').attr('src'); + assert.equal(unoptimizedSrc, 'https://cdn.example.com/images/hello_light_walrus.avif'); + + const unoptimizedData = await fixture.readFile( + unoptimizedSrc.replace('https://cdn.example.com/', ''), + null + ); + assert.equal(unoptimizedData instanceof Buffer, true); + + const src = $('#walrus-img').attr('src'); + assert.equal(src.startsWith('https://cdn.example.com/'), true); + + const data = await fixture.readFile(src.replace('https://cdn.example.com/', ''), null); + assert.equal(data instanceof Buffer, true); + }); +}); diff --git a/packages/astro/test/fixtures/core-image-unconventional-settings/package.json b/packages/astro/test/fixtures/core-image-unconventional-settings/package.json new file mode 100644 index 0000000000..c3a0834e8a --- /dev/null +++ b/packages/astro/test/fixtures/core-image-unconventional-settings/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/core-image-unconventional-settings", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/core-image-unconventional-settings/src/assets/light_walrus.avif b/packages/astro/test/fixtures/core-image-unconventional-settings/src/assets/light_walrus.avif new file mode 100644 index 0000000000000000000000000000000000000000..89e1c3a143111f0a609c83f05c5fecd0ddce77b2 GIT binary patch literal 19439 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 es8D~R1oB4={TAM+9brJNiAM +A walrus diff --git a/packages/astro/test/fixtures/core-image-unconventional-settings/tsconfig.json b/packages/astro/test/fixtures/core-image-unconventional-settings/tsconfig.json new file mode 100644 index 0000000000..b5bf6a715e --- /dev/null +++ b/packages/astro/test/fixtures/core-image-unconventional-settings/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "astro/tsconfigs/base", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "~/assets/*": ["src/assets/*"] + }, + } +} diff --git a/packages/astro/test/image-deletion.test.js b/packages/astro/test/image-deletion.test.js index cb4b464bf6..9af94a7543 100644 --- a/packages/astro/test/image-deletion.test.js +++ b/packages/astro/test/image-deletion.test.js @@ -3,7 +3,7 @@ import { before, describe, it } from 'node:test'; import { testImageService } from './test-image-service.js'; import { loadFixture } from './test-utils.js'; -describe('astro:assets - delete images that are unused zzz', () => { +describe('astro:assets - delete images that are unused', () => { /** @type {import('./test-utils.js').Fixture} */ let fixture; diff --git a/packages/internal-helpers/src/path.ts b/packages/internal-helpers/src/path.ts index 50daf97bce..6b1c981257 100644 --- a/packages/internal-helpers/src/path.ts +++ b/packages/internal-helpers/src/path.ts @@ -97,3 +97,10 @@ export function fileExtension(path: string) { const ext = path.split('.').pop(); return ext !== path ? `.${ext}` : ''; } + +export function removeBase(path: string, base: string) { + if (path.startsWith(base)) { + return path.slice(removeTrailingForwardSlash(base).length); + } + return path; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e939aac94e..5e3f64bcd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2553,6 +2553,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/core-image-unconventional-settings: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/css-assets: dependencies: '@test/astro-font-awesome-package':