Add build.assetsPrefix
option for CDN support (#6714)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
26daba8d9f
commit
ff04307863
35 changed files with 481 additions and 62 deletions
8
.changeset/two-beans-dress.md
Normal file
8
.changeset/two-beans-dress.md
Normal 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.
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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' &&
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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)}")`
|
||||
)}
|
||||
}
|
||||
|
||||
|
|
110
packages/astro/test/astro-assets-prefix.test.js
Normal file
110
packages/astro/test/astro-assets-prefix.test.js
Normal 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);
|
||||
});
|
||||
});
|
15
packages/astro/test/fixtures/astro-assets-prefix/astro.config.mjs
vendored
Normal file
15
packages/astro/test/fixtures/astro-assets-prefix/astro.config.mjs
vendored
Normal 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
|
||||
}
|
||||
});
|
11
packages/astro/test/fixtures/astro-assets-prefix/package.json
vendored
Normal file
11
packages/astro/test/fixtures/astro-assets-prefix/package.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
packages/astro/test/fixtures/astro-assets-prefix/src/assets/penguin1.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/astro-assets-prefix/src/assets/penguin1.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
11
packages/astro/test/fixtures/astro-assets-prefix/src/components/Counter.jsx
vendored
Normal file
11
packages/astro/test/fixtures/astro-assets-prefix/src/components/Counter.jsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
6
packages/astro/test/fixtures/astro-assets-prefix/src/content/blog/my-post.md
vendored
Normal file
6
packages/astro/test/fixtures/astro-assets-prefix/src/content/blog/my-post.md
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: My Post
|
||||
cover: ../../assets/penguin1.jpg
|
||||
---
|
||||
|
||||
Hello world
|
12
packages/astro/test/fixtures/astro-assets-prefix/src/content/config.ts
vendored
Normal file
12
packages/astro/test/fixtures/astro-assets-prefix/src/content/config.ts
vendored
Normal 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,
|
||||
};
|
16
packages/astro/test/fixtures/astro-assets-prefix/src/pages/blog.astro
vendored
Normal file
16
packages/astro/test/fixtures/astro-assets-prefix/src/pages/blog.astro
vendored
Normal 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>
|
||||
))
|
||||
}
|
23
packages/astro/test/fixtures/astro-assets-prefix/src/pages/index.astro
vendored
Normal file
23
packages/astro/test/fixtures/astro-assets-prefix/src/pages/index.astro
vendored
Normal 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>
|
5
packages/astro/test/fixtures/astro-assets-prefix/src/pages/markdown.md
vendored
Normal file
5
packages/astro/test/fixtures/astro-assets-prefix/src/pages/markdown.md
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Assets Prefix
|
||||
|
||||
Relative image has assetsPrefix
|
||||
|
||||
![Relative image](../assets/penguin1.jpg)
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
22
packages/integrations/image/test/assets-prefix.test.js
Normal file
22
packages/integrations/image/test/assets-prefix.test.js
Normal 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);
|
||||
});
|
||||
});
|
10
packages/integrations/image/test/fixtures/assets-prefix/astro.config.mjs
vendored
Normal file
10
packages/integrations/image/test/fixtures/assets-prefix/astro.config.mjs
vendored
Normal 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',
|
||||
}
|
||||
});
|
9
packages/integrations/image/test/fixtures/assets-prefix/package.json
vendored
Normal file
9
packages/integrations/image/test/fixtures/assets-prefix/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/image-assets-prefix",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/image": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
BIN
packages/integrations/image/test/fixtures/assets-prefix/src/assets/social.png
vendored
Normal file
BIN
packages/integrations/image/test/fixtures/assets-prefix/src/assets/social.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
13
packages/integrations/image/test/fixtures/assets-prefix/src/pages/index.astro
vendored
Normal file
13
packages/integrations/image/test/fixtures/assets-prefix/src/pages/index.astro
vendored
Normal 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>
|
|
@ -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:*
|
||||
|
|
Loading…
Reference in a new issue