Skip to content

Commit

Permalink
Add multiple cdn v2 (#10189)
Browse files Browse the repository at this point in the history
* feat: add multiple cdn

* add multiple cdn test

* assetsPrefix not empty string

* add changeset

* simplify code

* change docs

* replace getFileExtension with node path.extname

* Adapt node extname

* multiple image test

* wip space

* update docs

* update docs, assetsPrefix type

* update docs

* update docs

* chore: update types and rename to `fallback`

* enhance changelog

* change docs

* update change defaultAeestsPrefix to fallback key test

* move utility to a new to avoid importing `node:path` inside vite plugins

* Update packages/astro/src/@types/astro.ts

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* chore: address Bjorn's comments

* kill the variable

* kill the variable /2

* Fix CI fail

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* uniform code sample

* add `.` string for fit getAssetsPrefix

* Fix extension function

---------

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
Co-authored-by: bluwy <bjornlu.dev@gmail.com>
Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>
  • Loading branch information
5 people committed Mar 8, 2024
1 parent d933666 commit 1ea0a25
Show file tree
Hide file tree
Showing 13 changed files with 289 additions and 37 deletions.
26 changes: 26 additions & 0 deletions .changeset/fluffy-readers-add.md
@@ -0,0 +1,26 @@
---
"@astrojs/internal-helpers": minor
"astro": minor
---

Adds the option to pass an object to `build.assetsPrefix`. This allows for the use of multiple CDN prefixes based on the target file type.

When passing an object to `build.assetsPrefix`, you must also specify a `fallback` domain to be used for all other file types not specified.

Specify a file extension as the key (e.g. 'js', 'png') and the URL serving your assets of that file type as the value:

```js
// astro.config.mjs
import { defineConfig } from "astro/config"

export default defineConfig({
build: {
assetsPrefix: {
'js': "https://js.cdn.example.com",
'mjs': "https://js.cdn.example.com", // if you have .mjs files, you must add a new entry like this
'png': "https://images.cdn.example.com",
'fallback': "https://generic.cdn.example.com"
}
}
})
```
33 changes: 26 additions & 7 deletions packages/astro/src/@types/astro.ts
Expand Up @@ -13,7 +13,7 @@ import type * as babel from '@babel/core';
import type * as rollup from 'rollup';
import type * as vite from 'vite';
import type { RemotePattern } from '../assets/utils/remotePattern.js';
import type { SerializedSSRManifest } from '../core/app/types.js';
import type { SerializedSSRManifest, AssetsPrefix } from '../core/app/types.js';
import type { PageBuildData } from '../core/build/types.js';
import type { AstroConfigType } from '../core/config/index.js';
import type { AstroTimer } from '../core/config/timer.js';
Expand Down Expand Up @@ -67,7 +67,7 @@ export type {
UnresolvedImageTransform,
} from '../assets/types.js';
export type { RemotePattern } from '../assets/utils/remotePattern.js';
export type { SSRManifest } from '../core/app/types.js';
export type { SSRManifest, AssetsPrefix } from '../core/app/types.js';
export type {
AstroCookieGetOptions,
AstroCookieSetOptions,
Expand Down Expand Up @@ -881,15 +881,15 @@ export interface AstroUserConfig {
/**
* @docs
* @name build.assetsPrefix
* @type {string}
* @type {string | Record<string, 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.
* This requires uploading the assets in your local `./dist/_astro` folder to a corresponding `/_astro/` folder on the remote domain.
*
* To fetch all assets uploaded to the same domain (e.g. `https://cdn.example.com/_astro/...`), set `assetsPrefix` to the root domain as a string (regardless of your `base` configuration):
* To rename the `_astro` path, specify a new directory in `build.assets`.
*
* ```js
Expand All @@ -899,8 +899,27 @@ export interface AstroUserConfig {
* }
* }
* ```
*
* **Added in 4.5.0**
*
* You can also pass an object to `assetsPrefix` to specify a different domain for each file type.
* In this case, a `fallback` property is required and will be used by default for any other files.
*
* ```js
* {
* build: {
* assetsPrefix: {
* 'js': 'https://js.cdn.example.com',
* 'mjs': 'https://js.cdn.example.com',
* 'css': 'https://css.cdn.example.com',
* 'fallback': 'https://cdn.example.com'
* }
* }
* }
* ```
*
*/
assetsPrefix?: string;
assetsPrefix?: AssetsPrefix;
/**
* @docs
* @name build.serverEntry
Expand Down
12 changes: 12 additions & 0 deletions packages/astro/src/assets/utils/getAssetsPrefix.ts
@@ -0,0 +1,12 @@
import type { AssetsPrefix } from '../../core/app/types.js';

export function getAssetsPrefix(fileExtension: string, assetsPrefix?: AssetsPrefix): string {
if (!assetsPrefix) return '';
if (typeof assetsPrefix === 'string') return assetsPrefix;
// we assume the file extension has a leading '.' and we remove it
const dotLessFileExtension = fileExtension.slice(1);
if (assetsPrefix[dotLessFileExtension]) {
return assetsPrefix[dotLessFileExtension];
}
return assetsPrefix.fallback;
}
19 changes: 12 additions & 7 deletions packages/astro/src/assets/vite-plugin-assets.ts
@@ -1,6 +1,7 @@
import MagicString from 'magic-string';
import type * as vite from 'vite';
import { normalizePath } from 'vite';
import { extname } from 'node:path';
import type { AstroPluginOptions, ImageTransform } from '../@types/astro.js';
import { extendManualChunks } from '../core/build/plugins/util.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
Expand All @@ -16,6 +17,7 @@ import { emitESMImage } from './utils/emitAsset.js';
import { isESMImportedImage } from './utils/imageKind.js';
import { getProxyCode } from './utils/proxy.js';
import { hashTransform, propsToFilename } from './utils/transformToPath.js';
import { getAssetsPrefix } from './utils/getAssetsPrefix.js';

const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;

Expand Down Expand Up @@ -95,9 +97,12 @@ export default function assets({
}

// Rollup will copy the file to the output directory, this refer to this final path, not to the original path
const finalOriginalImagePath = (
isESMImportedImage(options.src) ? options.src.src : options.src
).replace(settings.config.build.assetsPrefix || '', '');
const ESMImportedImageSrc = isESMImportedImage(options.src)
? options.src.src
: options.src;
const fileExtension = extname(ESMImportedImageSrc);
const pf = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix);
const finalOriginalImagePath = ESMImportedImageSrc.replace(pf, '');

const hash = hashTransform(
options,
Expand Down Expand Up @@ -132,7 +137,7 @@ export default function assets({
// The paths here are used for URLs, so we need to make sure they have the proper format for an URL
// (leading slash, prefixed with the base / assets prefix, encoded, etc)
if (settings.config.build.assetsPrefix) {
return encodeURI(joinPaths(settings.config.build.assetsPrefix, finalFilePath));
return encodeURI(joinPaths(pf, finalFilePath));
} else {
return encodeURI(prependForwardSlash(joinPaths(settings.config.base, finalFilePath)));
}
Expand All @@ -149,9 +154,9 @@ export default function assets({
const [full, hash, postfix = ''] = match;

const file = this.getFileName(hash);
const prefix = settings.config.build.assetsPrefix
? appendForwardSlash(settings.config.build.assetsPrefix)
: resolvedConfig.base;
const fileExtension = extname(file);
const pf = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix);
const prefix = pf ? appendForwardSlash(pf) : resolvedConfig.base;
const outputFilepath = prefix + normalizePath(file + postfix);

s.overwrite(match.index, match.index + full.length, outputFilepath);
Expand Down
8 changes: 6 additions & 2 deletions packages/astro/src/content/vite-plugin-content-assets.ts
Expand Up @@ -19,6 +19,7 @@ import {
STYLES_PLACEHOLDER,
} from './consts.js';
import { hasContentFlag } from './utils.js';
import { getAssetsPrefix } from '../assets/utils/getAssetsPrefix.js';

export function astroContentAssetPropagationPlugin({
mode,
Expand Down Expand Up @@ -148,8 +149,11 @@ export function astroConfigBuildPlugin(
'build:post': ({ ssrOutputs, clientOutputs, mutate }) => {
const outputs = ssrOutputs.flatMap((o) => o.output);
const prependBase = (src: string) => {
if (options.settings.config.build.assetsPrefix) {
return joinPaths(options.settings.config.build.assetsPrefix, src);
const { assetsPrefix } = options.settings.config.build;
if (assetsPrefix) {
const fileExtension = extname(src);
const pf = getAssetsPrefix(fileExtension, assetsPrefix);
return joinPaths(pf, src);
} else {
return prependForwardSlash(joinPaths(options.settings.config.base, src));
}
Expand Down
9 changes: 8 additions & 1 deletion packages/astro/src/core/app/types.ts
Expand Up @@ -35,6 +35,13 @@ export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {

export type ImportComponentInstance = () => Promise<SinglePageBuiltModule>;

export type AssetsPrefix =
| string
| ({
fallback: string;
} & Record<string, string>)
| undefined;

export type SSRManifest = {
adapterName: string;
routes: RouteInfo[];
Expand All @@ -43,7 +50,7 @@ export type SSRManifest = {
trailingSlash: 'always' | 'never' | 'ignore';
buildFormat: 'file' | 'directory' | 'preserve';
compressHTML: boolean;
assetsPrefix?: string;
assetsPrefix?: AssetsPrefix;
renderers: SSRLoadedRenderer[];
/**
* Map of directive name (e.g. `load`) to the directive script code
Expand Down
6 changes: 4 additions & 2 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Expand Up @@ -11,13 +11,14 @@ import type {
SerializedRouteInfo,
SerializedSSRManifest,
} from '../../app/types.js';
import { joinPaths, prependForwardSlash } from '../../path.js';
import { joinPaths, prependForwardSlash, fileExtension } from '../../path.js';
import { serializeRouteData } from '../../routing/index.js';
import { addRollupInput } from '../add-rollup-input.js';
import { getOutFile, getOutFolder } from '../common.js';
import { type BuildInternals, cssOrder, mergeInlineCss } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
import type { StaticBuildOptions } from '../types.js';
import { getAssetsPrefix } from '../../../assets/utils/getAssetsPrefix.js';

const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
const replaceExp = new RegExp(`['"]${manifestReplace}['"]`, 'g');
Expand Down Expand Up @@ -163,7 +164,8 @@ function buildManifest(

const prefixAssetPath = (pth: string) => {
if (settings.config.build.assetsPrefix) {
return joinPaths(settings.config.build.assetsPrefix, pth);
const pf = getAssetsPrefix(fileExtension(pth), settings.config.build.assetsPrefix);
return joinPaths(pf, pth);
} else {
return prependForwardSlash(joinPaths(settings.config.base, pth));
}
Expand Down
38 changes: 35 additions & 3 deletions packages/astro/src/core/config/schema.ts
Expand Up @@ -11,7 +11,7 @@ import type { AstroUserConfig, ViteUserConfig } from '../../@types/astro.js';
import type { OutgoingHttpHeaders } from 'node:http';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { z } from 'zod';
import { type TypeOf, z } from 'zod';
import { appendForwardSlash, prependForwardSlash, removeTrailingForwardSlash } from '../path.js';

// These imports are required to appease TypeScript!
Expand Down Expand Up @@ -134,7 +134,23 @@ 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(),
assetsPrefix: z
.string()
.optional()
.or(z.object({ fallback: z.string() }).and(z.record(z.string())).optional())
.refine(
(value) => {
if (value && typeof value !== 'string') {
if (!value.fallback) {
return false;
}
}
return true;
},
{
message: 'The `fallback` is mandatory when defining the option as an object.',
}
),
serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
redirects: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.redirects),
inlineStylesheets: z
Expand Down Expand Up @@ -524,7 +540,23 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: string) {
.default(ASTRO_CONFIG_DEFAULTS.build.server)
.transform((val) => resolveDirAsUrl(val, fileProtocolRoot)),
assets: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.assets),
assetsPrefix: z.string().optional(),
assetsPrefix: z
.string()
.optional()
.or(z.object({ fallback: z.string() }).and(z.record(z.string())).optional())
.refine(
(value) => {
if (value && typeof value !== 'string') {
if (!value.fallback) {
return false;
}
}
return true;
},
{
message: 'The `fallback` is mandatory when defining the option as an object.',
}
),
serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
redirects: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.redirects),
inlineStylesheets: z
Expand Down
10 changes: 6 additions & 4 deletions packages/astro/src/core/create-vite.ts
Expand Up @@ -35,6 +35,8 @@ import type { Logger } from './logger/core.js';
import { createViteLogger } from './logger/vite.js';
import { vitePluginMiddleware } from './middleware/vite-plugin.js';
import { joinPaths } from './path.js';
import { isObject } from './util.js';
import { getAssetsPrefix } from '../assets/utils/getAssetsPrefix.js';

interface CreateViteOptions {
settings: AstroSettings;
Expand Down Expand Up @@ -214,9 +216,9 @@ export async function createVite(
const assetsPrefix = settings.config.build.assetsPrefix;
if (assetsPrefix) {
commonConfig.experimental = {
renderBuiltUrl(filename, { type }) {
renderBuiltUrl(filename, { type, hostType }) {
if (type === 'asset') {
return joinPaths(assetsPrefix, filename);
return joinPaths(getAssetsPrefix(`.${hostType}`, assetsPrefix), filename);
}
},
};
Expand Down Expand Up @@ -318,6 +320,6 @@ function isCommonNotAstro(dep: string): boolean {
);
}

function stringifyForDefine(value: string | undefined): string {
return typeof value === 'string' ? JSON.stringify(value) : 'undefined';
function stringifyForDefine(value: string | undefined | object): string {
return typeof value === 'string' || isObject(value) ? JSON.stringify(value) : 'undefined';
}
22 changes: 12 additions & 10 deletions packages/astro/src/core/render/ssr-element.ts
@@ -1,10 +1,12 @@
import type { SSRElement } from '../../@types/astro.js';
import { joinPaths, prependForwardSlash, slash } from '../../core/path.js';
import type { SSRElement, AssetsPrefix } from '../../@types/astro.js';
import { fileExtension, joinPaths, prependForwardSlash, slash } from '../../core/path.js';
import type { StylesheetAsset } from '../app/types.js';
import { getAssetsPrefix } from '../../assets/utils/getAssetsPrefix.js';

export function createAssetLink(href: string, base?: string, assetsPrefix?: string): string {
export function createAssetLink(href: string, base?: string, assetsPrefix?: AssetsPrefix): string {
if (assetsPrefix) {
return joinPaths(assetsPrefix, slash(href));
const pf = getAssetsPrefix(fileExtension(href), assetsPrefix);
return joinPaths(pf, slash(href));
} else if (base) {
return prependForwardSlash(joinPaths(base, slash(href)));
} else {
Expand All @@ -15,7 +17,7 @@ export function createAssetLink(href: string, base?: string, assetsPrefix?: stri
export function createStylesheetElement(
stylesheet: StylesheetAsset,
base?: string,
assetsPrefix?: string
assetsPrefix?: AssetsPrefix
): SSRElement {
if (stylesheet.type === 'inline') {
return {
Expand All @@ -36,15 +38,15 @@ export function createStylesheetElement(
export function createStylesheetElementSet(
stylesheets: StylesheetAsset[],
base?: string,
assetsPrefix?: string
assetsPrefix?: AssetsPrefix
): Set<SSRElement> {
return new Set(stylesheets.map((s) => createStylesheetElement(s, base, assetsPrefix)));
}

export function createModuleScriptElement(
script: { type: 'inline' | 'external'; value: string },
base?: string,
assetsPrefix?: string
assetsPrefix?: AssetsPrefix
): SSRElement {
if (script.type === 'external') {
return createModuleScriptElementWithSrc(script.value, base, assetsPrefix);
Expand All @@ -61,7 +63,7 @@ export function createModuleScriptElement(
export function createModuleScriptElementWithSrc(
src: string,
base?: string,
assetsPrefix?: string
assetsPrefix?: AssetsPrefix
): SSRElement {
return {
props: {
Expand All @@ -75,7 +77,7 @@ export function createModuleScriptElementWithSrc(
export function createModuleScriptElementWithSrcSet(
srces: string[],
site?: string,
assetsPrefix?: string
assetsPrefix?: AssetsPrefix
): Set<SSRElement> {
return new Set<SSRElement>(
srces.map((src) => createModuleScriptElementWithSrc(src, site, assetsPrefix))
Expand All @@ -85,7 +87,7 @@ export function createModuleScriptElementWithSrcSet(
export function createModuleScriptsSet(
scripts: { type: 'inline' | 'external'; value: string }[],
base?: string,
assetsPrefix?: string
assetsPrefix?: AssetsPrefix
): Set<SSRElement> {
return new Set<SSRElement>(
scripts.map((script) => createModuleScriptElement(script, base, assetsPrefix))
Expand Down

0 comments on commit 1ea0a25

Please sign in to comment.