Add build.assetsPrefix option for CDN support (#6714)

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Bjorn Lu 2023-04-05 21:31:17 +08:00 committed by GitHub
parent 26daba8d9f
commit ff04307863
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 481 additions and 62 deletions

View file

@ -0,0 +1,8 @@
---
'astro': minor
'@astrojs/image': patch
---
Add `build.assetsPrefix` option for CDN support. If set, all Astro-generated asset links will be prefixed with it. For example, setting it to `https://cdn.example.com` would generate `https://cdn.example.com/_astro/penguin.123456.png` links.
Also adds `import.meta.env.ASSETS_PREFIX` environment variable that can be used to manually create asset links not handled by Astro.

View file

@ -624,6 +624,29 @@ export interface AstroUserConfig {
* ```
*/
assets?: string;
/**
* @docs
* @name build.assetsPrefix
* @type {string}
* @default `undefined`
* @version 2.2.0
* @description
* Specifies the prefix for Astro-generated asset links. This can be used if assets are served from a different domain than the current site.
*
* For example, if this is set to `https://cdn.example.com`, assets will be fetched from `https://cdn.example.com/_astro/...` (regardless of the `base` option).
* You would need to upload the files in `./dist/_astro/` to `https://cdn.example.com/_astro/` to serve the assets.
* The process varies depending on how the third-party domain is hosted.
* To rename the `_astro` path, specify a new directory in `build.assets`.
*
* ```js
* {
* build: {
* assetsPrefix: 'https://cdn.example.com'
* }
* }
* ```
*/
assetsPrefix?: string;
/**
* @docs
* @name build.serverEntry

View file

@ -8,7 +8,11 @@ import type * as vite from 'vite';
import { normalizePath } from 'vite';
import type { AstroPluginOptions, ImageTransform } from '../@types/astro';
import { error } from '../core/logger/core.js';
import { joinPaths, prependForwardSlash } from '../core/path.js';
import {
appendForwardSlash,
joinPaths,
prependForwardSlash,
} from '../core/path.js';
import { VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js';
import { isESMImportedImage } from './internal.js';
import { isLocalService } from './services/service.js';
@ -174,7 +178,11 @@ export default function assets({
globalThis.astroAsset.staticImages.set(hash, { path: filePath, options: options });
}
return prependForwardSlash(joinPaths(settings.config.base, filePath));
if (settings.config.build.assetsPrefix) {
return joinPaths(settings.config.build.assetsPrefix, filePath);
} else {
return prependForwardSlash(joinPaths(settings.config.base, filePath));
}
};
},
async buildEnd() {
@ -202,7 +210,10 @@ export default function assets({
const [full, hash, postfix = ''] = match;
const file = this.getFileName(hash);
const outputFilepath = normalizePath(resolvedConfig.base + file + postfix);
const prefix = settings.config.build.assetsPrefix
? appendForwardSlash(settings.config.build.assetsPrefix)
: resolvedConfig.base;
const outputFilepath = prefix + normalizePath(file + postfix);
s.overwrite(match.index, match.index + full.length, outputFilepath);
}

View file

@ -8,7 +8,7 @@ import type { AstroBuildPlugin } from '../core/build/plugin.js';
import type { StaticBuildOptions } from '../core/build/types';
import type { ModuleLoader } from '../core/module-loader/loader.js';
import { createViteLoader } from '../core/module-loader/vite.js';
import { prependForwardSlash } from '../core/path.js';
import { joinPaths, prependForwardSlash } from '../core/path.js';
import { getStylesForURL } from '../core/render/dev/css.js';
import { getScriptsForURL } from '../core/render/dev/scripts.js';
import {
@ -71,7 +71,11 @@ export function astroContentAssetPropagationPlugin({
'development'
);
const hoistedScripts = await getScriptsForURL(pathToFileURL(basePath), devModuleLoader);
const hoistedScripts = await getScriptsForURL(
pathToFileURL(basePath),
settings.config.root,
devModuleLoader
);
return {
code: code
@ -106,8 +110,13 @@ export function astroConfigBuildPlugin(
},
'build:post': ({ ssrOutputs, clientOutputs, mutate }) => {
const outputs = ssrOutputs.flatMap((o) => o.output);
const prependBase = (src: string) =>
prependForwardSlash(npath.posix.join(options.settings.config.base, src));
const prependBase = (src: string) => {
if (options.settings.config.build.assetsPrefix) {
return joinPaths(options.settings.config.build.assetsPrefix, src);
} else {
return prependForwardSlash(joinPaths(options.settings.config.base, src));
}
};
for (const chunk of outputs) {
if (
chunk.type === 'chunk' &&

View file

@ -32,7 +32,11 @@ import { AstroError } from '../errors/index.js';
import { debug, info } from '../logger/core.js';
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
import { callGetStaticPaths } from '../render/route-cache.js';
import { createLinkStylesheetElementSet, createModuleScriptsSet } from '../render/ssr-element.js';
import {
createAssetLink,
createLinkStylesheetElementSet,
createModuleScriptsSet,
} from '../render/ssr-element.js';
import { createRequest } from '../request.js';
import { matchRoute } from '../routing/match.js';
import { getOutputFilename } from '../util.js';
@ -351,10 +355,15 @@ async function generatePath(
debug('build', `Generating: ${pathname}`);
const links = createLinkStylesheetElementSet(linkIds, settings.config.base);
const links = createLinkStylesheetElementSet(
linkIds,
settings.config.base,
settings.config.build.assetsPrefix
);
const scripts = createModuleScriptsSet(
hoistedScripts ? [hoistedScripts] : [],
settings.config.base
settings.config.base,
settings.config.build.assetsPrefix
);
if (settings.scripts.some((script) => script.stage === 'page')) {
@ -362,7 +371,11 @@ async function generatePath(
if (typeof hashedFilePath !== 'string') {
throw new Error(`Cannot find the built path for ${PAGE_SCRIPT_ID}`);
}
const src = prependForwardSlash(npath.posix.join(settings.config.base, hashedFilePath));
const src = createAssetLink(
hashedFilePath,
settings.config.base,
settings.config.build.assetsPrefix
);
scripts.add({
props: { type: 'module', src },
children: '',
@ -403,7 +416,11 @@ async function generatePath(
}
throw new Error(`Cannot find the built path for ${specifier}`);
}
return prependForwardSlash(npath.posix.join(settings.config.base, hashedFilePath));
return createAssetLink(
hashedFilePath,
settings.config.base,
settings.config.build.assetsPrefix
);
},
routeCache,
site: settings.config.site

View file

@ -9,7 +9,7 @@ import { fileURLToPath } from 'url';
import { runHookBuildSsr } from '../../../integrations/index.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
import { pagesVirtualModuleId } from '../../app/index.js';
import { removeLeadingForwardSlash, removeTrailingForwardSlash } from '../../path.js';
import { joinPaths, prependForwardSlash } from '../../path.js';
import { serializeRouteData } from '../../routing/index.js';
import { addRollupInput } from '../add-rollup-input.js';
import { getOutFile, getOutFolder } from '../common.js';
@ -134,8 +134,13 @@ function buildManifest(
staticFiles.push(entryModules[PAGE_SCRIPT_ID]);
}
const bareBase = removeTrailingForwardSlash(removeLeadingForwardSlash(settings.config.base));
const joinBase = (pth: string) => (bareBase ? bareBase + '/' + pth : pth);
const prefixAssetPath = (pth: string) => {
if (settings.config.build.assetsPrefix) {
return joinPaths(settings.config.build.assetsPrefix, pth);
} else {
return prependForwardSlash(joinPaths(settings.config.base, pth));
}
};
for (const pageData of eachPrerenderedPageData(internals)) {
if (!pageData.route.pathname) continue;
@ -165,7 +170,7 @@ function buildManifest(
const scripts: SerializedRouteInfo['scripts'] = [];
if (pageData.hoistedScript) {
const hoistedValue = pageData.hoistedScript.value;
const value = hoistedValue.endsWith('.js') ? joinBase(hoistedValue) : hoistedValue;
const value = hoistedValue.endsWith('.js') ? prefixAssetPath(hoistedValue) : hoistedValue;
scripts.unshift(
Object.assign({}, pageData.hoistedScript, {
value,
@ -177,11 +182,11 @@ function buildManifest(
scripts.push({
type: 'external',
value: joinBase(src),
value: prefixAssetPath(src),
});
}
const links = sortedCSS(pageData).map((pth) => joinBase(pth));
const links = sortedCSS(pageData).map((pth) => prefixAssetPath(pth));
routes.push({
file: '',
@ -212,7 +217,7 @@ function buildManifest(
componentMetadata: Array.from(internals.componentMetadata),
renderers: [],
entryModules,
assets: staticFiles.map((s) => settings.config.base + s),
assets: staticFiles.map(prefixAssetPath),
};
return ssrManifest;

View file

@ -97,6 +97,7 @@ export const AstroConfigSchema = z.object({
.default(ASTRO_CONFIG_DEFAULTS.build.server)
.transform((val) => new URL(val)),
assets: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.assets),
assetsPrefix: z.string().optional(),
serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
})
.optional()
@ -222,6 +223,7 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
.default(ASTRO_CONFIG_DEFAULTS.build.server)
.transform((val) => new URL(val, fileProtocolRoot)),
assets: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.assets),
assetsPrefix: z.string().optional(),
serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
})
.optional()

View file

@ -26,6 +26,7 @@ import astroScannerPlugin from '../vite-plugin-scanner/index.js';
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
import { joinPaths } from './path.js';
interface CreateViteOptions {
settings: AstroSettings;
@ -174,6 +175,20 @@ export async function createVite(
},
};
// If the user provides a custom assets prefix, make sure assets handled by Vite
// are prefixed with it too. This uses one of it's experimental features, but it
// has been stable for a long time now.
const assetsPrefix = settings.config.build.assetsPrefix;
if (assetsPrefix) {
commonConfig.experimental = {
renderBuiltUrl(filename, { type }) {
if (type === 'asset') {
return joinPaths(assetsPrefix, filename);
}
},
};
}
// Merge configs: we merge vite configuration objects together in the following order,
// where future values will override previous values.
// 1. common vite config

View file

@ -86,11 +86,6 @@ export async function createContainer(params: CreateContainerParams = {}): Promi
optimizeDeps: {
include: rendererClientEntries,
},
define: {
'import.meta.env.BASE_URL': settings.config.base
? JSON.stringify(settings.config.base)
: 'undefined',
},
},
{ settings, logging, mode: 'dev', command: 'dev', fs }
);

View file

@ -76,7 +76,7 @@ interface GetScriptsAndStylesParams {
async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) {
// Add hoisted script tags
const scripts = await getScriptsForURL(filePath, env.loader);
const scripts = await getScriptsForURL(filePath, env.settings.config.root, env.loader);
// Inject HMR scripts
if (isPage(filePath, env.settings) && env.mode === 'development') {

View file

@ -2,30 +2,31 @@ import type { SSRElement } from '../../../@types/astro';
import type { PluginMetadata as AstroPluginMetadata } from '../../../vite-plugin-astro/types';
import type { ModuleInfo, ModuleLoader } from '../../module-loader/index';
import { viteID } from '../../util.js';
import { rootRelativePath, viteID } from '../../util.js';
import { createModuleScriptElementWithSrc } from '../ssr-element.js';
import { crawlGraph } from './vite.js';
export async function getScriptsForURL(
filePath: URL,
root: URL,
loader: ModuleLoader
): Promise<Set<SSRElement>> {
const elements = new Set<SSRElement>();
const rootID = viteID(filePath);
const modInfo = loader.getModuleInfo(rootID);
addHoistedScripts(elements, modInfo);
addHoistedScripts(elements, modInfo, root);
for await (const moduleNode of crawlGraph(loader, rootID, true)) {
const id = moduleNode.id;
if (id) {
const info = loader.getModuleInfo(id);
addHoistedScripts(elements, info);
addHoistedScripts(elements, info, root);
}
}
return elements;
}
function addHoistedScripts(set: Set<SSRElement>, info: ModuleInfo | null) {
function addHoistedScripts(set: Set<SSRElement>, info: ModuleInfo | null, root: URL) {
if (!info?.meta?.astro) {
return;
}
@ -33,7 +34,8 @@ function addHoistedScripts(set: Set<SSRElement>, info: ModuleInfo | null) {
let id = info.id;
const astro = info?.meta?.astro as AstroPluginMetadata['astro'];
for (let i = 0; i < astro.scripts.length; i++) {
const scriptId = `${id}?astro&type=script&index=${i}&lang.ts`;
let scriptId = `${id}?astro&type=script&index=${i}&lang.ts`;
scriptId = rootRelativePath(root, scriptId);
const element = createModuleScriptElementWithSrc(scriptId);
set.add(element);
}

View file

@ -1,37 +1,48 @@
import slashify from 'slash';
import type { SSRElement } from '../../@types/astro';
import { appendForwardSlash, removeLeadingForwardSlash } from '../../core/path.js';
import { joinPaths, prependForwardSlash } from '../../core/path.js';
function getRootPath(base?: string): string {
return appendForwardSlash(new URL(base || '/', 'http://localhost/').pathname);
export function createAssetLink(href: string, base?: string, assetsPrefix?: string): string {
if (assetsPrefix) {
return joinPaths(assetsPrefix, slashify(href));
} else if (base) {
return prependForwardSlash(joinPaths(base, slashify(href)));
} else {
return href;
}
}
function joinToRoot(href: string, base?: string): string {
const rootPath = getRootPath(base);
const normalizedHref = slashify(href);
return appendForwardSlash(rootPath) + removeLeadingForwardSlash(normalizedHref);
}
export function createLinkStylesheetElement(href: string, base?: string): SSRElement {
export function createLinkStylesheetElement(
href: string,
base?: string,
assetsPrefix?: string
): SSRElement {
return {
props: {
rel: 'stylesheet',
href: joinToRoot(href, base),
href: createAssetLink(href, base, assetsPrefix),
},
children: '',
};
}
export function createLinkStylesheetElementSet(hrefs: string[], base?: string) {
return new Set<SSRElement>(hrefs.map((href) => createLinkStylesheetElement(href, base)));
export function createLinkStylesheetElementSet(
hrefs: string[],
base?: string,
assetsPrefix?: string
) {
return new Set<SSRElement>(
hrefs.map((href) => createLinkStylesheetElement(href, base, assetsPrefix))
);
}
export function createModuleScriptElement(
script: { type: 'inline' | 'external'; value: string },
base?: string
base?: string,
assetsPrefix?: string
): SSRElement {
if (script.type === 'external') {
return createModuleScriptElementWithSrc(script.value, base);
return createModuleScriptElementWithSrc(script.value, base, assetsPrefix);
} else {
return {
props: {
@ -42,11 +53,15 @@ export function createModuleScriptElement(
}
}
export function createModuleScriptElementWithSrc(src: string, site?: string): SSRElement {
export function createModuleScriptElementWithSrc(
src: string,
base?: string,
assetsPrefix?: string
): SSRElement {
return {
props: {
type: 'module',
src: joinToRoot(src, site),
src: createAssetLink(src, base, assetsPrefix),
},
children: '',
};
@ -54,14 +69,20 @@ export function createModuleScriptElementWithSrc(src: string, site?: string): SS
export function createModuleScriptElementWithSrcSet(
srces: string[],
site?: string
site?: string,
assetsPrefix?: string
): Set<SSRElement> {
return new Set<SSRElement>(srces.map((src) => createModuleScriptElementWithSrc(src, site)));
return new Set<SSRElement>(
srces.map((src) => createModuleScriptElementWithSrc(src, site, assetsPrefix))
);
}
export function createModuleScriptsSet(
scripts: { type: 'inline' | 'external'; value: string }[],
base?: string
base?: string,
assetsPrefix?: string
): Set<SSRElement> {
return new Set<SSRElement>(scripts.map((script) => createModuleScriptElement(script, base)));
return new Set<SSRElement>(
scripts.map((script) => createModuleScriptElement(script, base, assetsPrefix))
);
}

View file

@ -151,14 +151,14 @@ export function relativeToSrcDir(config: AstroConfig, idOrUrl: URL | string) {
return id.slice(slash(fileURLToPath(config.srcDir)).length);
}
export function rootRelativePath(config: AstroConfig, idOrUrl: URL | string) {
export function rootRelativePath(root: URL, idOrUrl: URL | string) {
let id: string;
if (typeof idOrUrl !== 'string') {
id = unwrapId(viteID(idOrUrl));
} else {
id = idOrUrl;
}
return prependForwardSlash(id.slice(normalizePath(fileURLToPath(config.root)).length));
return prependForwardSlash(id.slice(normalizePath(fileURLToPath(root)).length));
}
export function emoji(char: string, fallback: string) {

View file

@ -40,6 +40,9 @@ function getPrivateEnv(
privateEnv.SITE = astroConfig.site ? JSON.stringify(astroConfig.site) : 'undefined';
privateEnv.SSR = JSON.stringify(true);
privateEnv.BASE_URL = astroConfig.base ? JSON.stringify(astroConfig.base) : 'undefined';
privateEnv.ASSETS_PREFIX = astroConfig.build.assetsPrefix
? JSON.stringify(astroConfig.build.assetsPrefix)
: 'undefined';
return privateEnv;
}
@ -60,6 +63,18 @@ export default function envVitePlugin({ settings }: EnvPluginOptions): vite.Plug
return {
name: 'astro:vite-plugin-env',
enforce: 'pre',
config() {
return {
define: {
'import.meta.env.BASE_URL': astroConfig.base
? JSON.stringify(astroConfig.base)
: 'undefined',
'import.meta.env.ASSETS_PREFIX': astroConfig.build.assetsPrefix
? JSON.stringify(astroConfig.build.assetsPrefix)
: 'undefined',
},
};
},
configResolved(resolvedConfig) {
viteConfig = resolvedConfig;
},

View file

@ -128,7 +128,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
(entry) =>
`'${entry.raw}': await getImageSafely((await import("${entry.raw}")).default, "${
entry.raw
}", "${rootRelativePath(settings.config, entry.resolved)}")`
}", "${rootRelativePath(settings.config.root, entry.resolved)}")`
)}
}

View file

@ -0,0 +1,110 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import testAdapter from './test-adapter.js';
import { loadFixture } from './test-utils.js';
const assetsPrefix = 'http://localhost:4321';
const assetsPrefixRegex = /^http:\/\/localhost:4321\/_astro\/.*/;
// Asset prefix for CDN support
describe('Assets Prefix - Static', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-assets-prefix/',
});
await fixture.build();
});
it('all stylesheets should start with assetPrefix', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const stylesheets = $('link[rel="stylesheet"]');
stylesheets.each((i, el) => {
expect(el.attribs.href).to.match(assetsPrefixRegex);
});
});
it('image src start with assetsPrefix', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const imgAsset = $('#image-asset');
expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
});
it('react component astro-island should import from assetsPrefix', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const island = $('astro-island');
expect(island.attr('component-url')).to.match(assetsPrefixRegex);
expect(island.attr('renderer-url')).to.match(assetsPrefixRegex);
});
it('import.meta.env.ASSETS_PREFIX works', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const env = $('#assets-prefix-env');
expect(env.text()).to.equal(assetsPrefix);
});
it('markdown image src start with assetsPrefix', async () => {
const html = await fixture.readFile('/markdown/index.html');
const $ = cheerio.load(html);
const imgAsset = $('img');
expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
});
it('content collections image src start with assetsPrefix', async () => {
const html = await fixture.readFile('/blog/index.html');
const $ = cheerio.load(html);
const imgAsset = $('img');
expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
});
});
describe('Assets Prefix - Server', () => {
let app;
before(async () => {
const fixture = await loadFixture({
root: './fixtures/astro-assets-prefix/',
output: 'server',
adapter: testAdapter(),
});
await fixture.build();
app = await fixture.loadTestAdapterApp();
});
it('all stylesheets should start with assetPrefix', async () => {
const request = new Request('http://example.com/custom-base/');
const response = await app.render(request);
expect(response.status).to.equal(200);
const html = await response.text();
const $ = cheerio.load(html);
const stylesheets = $('link[rel="stylesheet"]');
stylesheets.each((i, el) => {
expect(el.attribs.href).to.match(assetsPrefixRegex);
});
});
it('image src start with assetsPrefix', async () => {
const request = new Request('http://example.com/custom-base/');
const response = await app.render(request);
expect(response.status).to.equal(200);
const html = await response.text();
const $ = cheerio.load(html);
const imgAsset = $('#image-asset');
expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
});
it('markdown image src start with assetsPrefix', async () => {
const request = new Request('http://example.com/custom-base/markdown/');
const response = await app.render(request);
expect(response.status).to.equal(200);
const html = await response.text();
const $ = cheerio.load(html);
const imgAsset = $('img');
expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
});
});

View file

@ -0,0 +1,15 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react'
// https://astro.build/config
export default defineConfig({
// test custom base to make sure things work
base: '/custom-base',
integrations: [react()],
build: {
assetsPrefix: 'http://localhost:4321'
},
experimental: {
assets: true
}
});

View file

@ -0,0 +1,11 @@
{
"name": "@test/astro-assets-prefix",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/react": "workspace:*",
"astro": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,11 @@
import React, { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<div>Count: {count}</div>
<button type="button" onClick={() => setCount(count+1)}>Increment</button>
</div>
);
}

View file

@ -0,0 +1,6 @@
---
title: My Post
cover: ../../assets/penguin1.jpg
---
Hello world

View file

@ -0,0 +1,12 @@
import { defineCollection, z, image } from "astro:content";
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
cover: image(),
}),
});
export const collections = {
blog: blogCollection,
};

View file

@ -0,0 +1,16 @@
---
import { Image } from "astro:assets";
import { getCollection } from "astro:content";
const allBlogPosts = await getCollection("blog");
---
{
allBlogPosts.map((post) => (
<div>
<Image src={post.data.cover} alt="cover" width="100" height="100" />
<h2>
<a href={"/blog/" + post.slug}>{post.data.title}</a>
</h2>
</div>
))
}

View file

@ -0,0 +1,23 @@
---
import { Image } from 'astro:assets'
import p1Image from '../assets/penguin1.jpg';
import Counter from '../components/Counter.jsx';
---
<html lang="en">
<head>
<title>Assets Prefix</title>
</head>
<body>
<h1>I am red</h1>
<img id="image-asset" src={p1Image.src} width={p1Image.width} height={p1Image.height} alt="penguin" />
<Image src={p1Image} alt="penguin" />
<Counter client:load />
<p id="assets-prefix-env">{import.meta.env.ASSETS_PREFIX}</p>
<style>
h1 {
color: red;
}
</style>
</body>
</html>

View file

@ -0,0 +1,5 @@
# Assets Prefix
Relative image has assetsPrefix
![Relative image](../assets/penguin1.jpg)

View file

@ -27,7 +27,7 @@ describe('Projects with a space in the folder name', () => {
const html = await fixture.fetch('/').then((r) => r.text());
const $ = cheerio.load(html);
expect($('script[src*="space in folder name"]')).to.have.a.lengthOf(1);
expect($('script[src*="/src/pages/index.astro"]')).to.have.a.lengthOf(1);
});
});
});

View file

@ -8,7 +8,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { SSRImageService, TransformOptions } from '../loaders/index.js';
import { debug, info, warn, type LoggerLevel } from '../utils/logger.js';
import { isRemoteImage } from '../utils/paths.js';
import { isRemoteImage, prependForwardSlash } from '../utils/paths.js';
import { ImageCache } from './cache.js';
async function loadLocalImage(src: string | URL) {
@ -135,10 +135,15 @@ export async function ssgBuild({
// tracks the cache duration for the original source image
let expires = 0;
// Vite will prefix a hashed image with the base path, we need to strip this
// off to find the actual file relative to /dist
if (config.base && src.startsWith(config.base)) {
src = src.substring(config.base.length - +config.base.endsWith('/'));
// Strip leading assetsPrefix or base added by addStaticImage
if (config.build.assetsPrefix) {
if (src.startsWith(config.build.assetsPrefix)) {
src = prependForwardSlash(src.slice(config.build.assetsPrefix.length));
}
} else if (config.base) {
if (src.startsWith(config.base)) {
src = prependForwardSlash(src.slice(config.base.length));
}
}
if (isRemoteImage(src)) {

View file

@ -130,7 +130,11 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
// Doing this here makes sure that base is ignored when building
// staticImages to /dist, but the rendered HTML will include the
// base prefix for `src`.
return prependForwardSlash(joinPaths(_config.base, _buildConfig.assets, filename));
if (_config.build.assetsPrefix) {
return joinPaths(_config.build.assetsPrefix, _buildConfig.assets, filename);
} else {
return prependForwardSlash(joinPaths(_config.base, _buildConfig.assets, filename));
}
}
// Helpers for building static images should only be available for SSG

View file

@ -9,6 +9,7 @@ import type { Plugin, ResolvedConfig } from 'vite';
import type { IntegrationOptions } from './index.js';
import type { InputFormat } from './loaders/index.js';
import { metadata } from './utils/metadata.js';
import { appendForwardSlash } from './utils/paths.js';
export interface ImageMetadata {
src: string;
@ -118,7 +119,10 @@ export function createPlugin(config: AstroConfig, options: Required<IntegrationO
const [full, hash, postfix = ''] = match;
const file = this.getFileName(hash);
const outputFilepath = resolvedConfig.base + file + postfix;
const prefix = config.build.assetsPrefix
? appendForwardSlash(config.build.assetsPrefix)
: config.base;
const outputFilepath = prefix + file + postfix;
s.overwrite(match.index, match.index + full.length, outputFilepath);
}

View file

@ -0,0 +1,22 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const assetsPrefixRegex = /^http:\/\/localhost:4321\/_astro\/.*/;
describe('Assets Prefix', function () {
/** @type {import('../../../astro/test/test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({ root: './fixtures/assets-prefix/' });
await fixture.build();
});
it('images src has assets prefix', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const img = $('#social-jpg');
expect(img.attr('src')).to.match(assetsPrefixRegex);
});
});

View file

@ -0,0 +1,10 @@
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
// https://astro.build/config
export default defineConfig({
integrations: [image()],
build: {
assetsPrefix: 'http://localhost:4321',
}
});

View file

@ -0,0 +1,9 @@
{
"name": "@test/image-assets-prefix",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/image": "workspace:*",
"astro": "workspace:*"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -0,0 +1,13 @@
---
import socialJpg from '../assets/social.png';
import { Image } from '@astrojs/image/components';
---
<html>
<head>
<!-- Head Stuff -->
</head>
<body>
<Image id="social-jpg" src={socialJpg} width={506} height={253} alt="social-jpg" />
</body>
</html>

View file

@ -1301,6 +1301,18 @@ importers:
dependencies:
astro: link:../../..
packages/astro/test/fixtures/astro-assets-prefix:
specifiers:
'@astrojs/react': workspace:*
astro: workspace:*
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
'@astrojs/react': link:../../../../integrations/react
astro: link:../../..
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
packages/astro/test/fixtures/astro-attrs:
specifiers:
'@astrojs/react': workspace:*
@ -2958,6 +2970,14 @@ importers:
sharp: 0.31.3
vite: 4.1.2
packages/integrations/image/test/fixtures/assets-prefix:
specifiers:
'@astrojs/image': workspace:*
astro: workspace:*
dependencies:
'@astrojs/image': link:../../..
astro: link:../../../../../astro
packages/integrations/image/test/fixtures/background-color-image:
specifiers:
'@astrojs/image': workspace:*