Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add build.assetsPrefix option for CDN support #6714

Merged
merged 24 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/two-beans-dress.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,27 @@ 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.
*
* If this is set to `https://cdn.example.com`, assets will be fetched from `https://cdn.example.com/_astro/...` (regardless of the `base` option).
* To change the `_astro` path, you can use the `build.assets` option to rename it.
bluwy marked this conversation as resolved.
Show resolved Hide resolved
*
* ```js
* {
* build: {
* assetsPrefix: 'https://cdn.example.com'
* }
* }
* ```
*/
assetsPrefix?: string;
/**
* @docs
* @name build.serverEntry
Expand Down
17 changes: 14 additions & 3 deletions packages/astro/src/assets/vite-plugin-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -177,7 +181,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() {
Expand Down Expand Up @@ -205,7 +213,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);
}
Expand Down
11 changes: 8 additions & 3 deletions packages/astro/src/content/vite-plugin-content-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -106,8 +106,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' &&
Expand Down
27 changes: 22 additions & 5 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -351,18 +355,27 @@ 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')) {
const hashedFilePath = internals.entrySpecifierToBundleMap.get(PAGE_SCRIPT_ID);
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: '',
Expand Down Expand Up @@ -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
Expand Down
19 changes: 12 additions & 7 deletions packages/astro/src/core/build/plugins/plugin-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
}
};
matthewp marked this conversation as resolved.
Show resolved Hide resolved

for (const pageData of eachPrerenderedPageData(internals)) {
if (!pageData.route.pathname) continue;
Expand Down Expand Up @@ -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,
Expand All @@ -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: '',
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
15 changes: 15 additions & 0 deletions packages/astro/src/core/create-vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
5 changes: 0 additions & 5 deletions packages/astro/src/core/dev/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
);
Expand Down
63 changes: 42 additions & 21 deletions packages/astro/src/core/render/ssr-element.ts
Original file line number Diff line number Diff line change
@@ -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);
}

function joinToRoot(href: string, base?: string): string {
const rootPath = getRootPath(base);
const normalizedHref = slashify(href);
return appendForwardSlash(rootPath) + removeLeadingForwardSlash(normalizedHref);
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;
}
}

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: {
Expand All @@ -42,26 +53,36 @@ 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: '',
};
}

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))
);
}
Loading