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 all 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.
23 changes: 23 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,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
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
17 changes: 13 additions & 4 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 @@ -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
Expand Down Expand Up @@ -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' &&
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
2 changes: 1 addition & 1 deletion packages/astro/src/core/render/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
12 changes: 7 additions & 5 deletions packages/astro/src/core/render/dev/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,40 @@ 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;
}

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